Repository: react-component/dropdown Branch: master Commit: 99ae51aee181 Files: 43 Total size: 54.8 KB Directory structure: gitextract_t_l0mx02/ ├── .dumirc.ts ├── .editorconfig ├── .eslintrc.js ├── .fatherrc.ts ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ └── codeql.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .prettierrc ├── .travis.yml ├── HISTORY.md ├── LICENSE ├── README.md ├── assets/ │ └── index.less ├── docs/ │ ├── demo/ │ │ ├── arrow.md │ │ ├── context-menu.md │ │ ├── dropdown-menu-width.md │ │ ├── multiple.md │ │ ├── overlay-callback.md │ │ └── simple.md │ ├── examples/ │ │ ├── arrow.jsx │ │ ├── context-menu.jsx │ │ ├── dropdown-menu-width.jsx │ │ ├── multiple.jsx │ │ ├── overlay-callback.jsx │ │ └── simple.jsx │ └── index.md ├── index.js ├── now.json ├── package.json ├── script/ │ └── update-content.js ├── src/ │ ├── Dropdown.tsx │ ├── Overlay.tsx │ ├── hooks/ │ │ └── useAccessibility.ts │ ├── index.tsx │ └── placements.ts ├── tests/ │ ├── __mocks__/ │ │ └── @rc-component/ │ │ └── trigger.tsx │ ├── __snapshots__/ │ │ └── basic.test.tsx.snap │ ├── basic.test.tsx │ ├── point.test.tsx │ └── utils.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dumirc.ts ================================================ // more config: https://d.umijs.org/config import { defineConfig } from 'dumi'; export default defineConfig({ favicons: ['https://avatars0.githubusercontent.com/u/9441414?s=200&v=4'], themeConfig: { name: 'rc-dropdown', logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', }, outputPath: '.docs', exportStatic: {}, styles: [ ` section.dumi-default-header-left { width: 240px; } `, ], }); ================================================ FILE: .editorconfig ================================================ # top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*.{js,css}] end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 ================================================ FILE: .eslintrc.js ================================================ const base = require('@umijs/fabric/dist/eslint'); module.exports = { ...base, rules: { ...base.rules, 'no-template-curly-in-string': 0, 'prefer-promise-reject-errors': 0, 'react/no-array-index-key': 0, 'react/sort-comp': 0, '@typescript-eslint/no-explicit-any': 0, 'jsx-a11y/label-has-associated-control': 0, 'jsx-a11y/label-has-for': 0, 'no-shadow': 0 }, }; ================================================ FILE: .fatherrc.ts ================================================ import { defineConfig } from "father"; export default defineConfig({ plugins: ["@rc-component/father-plugin"], }); ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: ['push', 'pull_request'] jobs: CI: uses: react-component/rc-test/.github/workflows/test.yml@main secrets: inherit ================================================ FILE: .github/workflows/codeql.yml ================================================ name: "CodeQL" on: push: branches: [ "master" ] pull_request: branches: [ "master" ] schedule: - cron: "38 3 * * 6" jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ javascript ] steps: - name: Checkout uses: actions/checkout@v3 - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} queries: +security-and-quality - name: Autobuild uses: github/codeql-action/autobuild@v2 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 with: category: "/language:${{ matrix.language }}" ================================================ FILE: .gitignore ================================================ *.iml *.log .idea/ .ipr .iws *~ ~* *.diff *.patch *.bak .DS_Store Thumbs.db .project .*proj .svn/ *.swp *.swo *.pyc *.pyo .build node_modules .cache dist assets/**/*.css build lib es coverage yarn.lock package-lock.json pnpm-lock.yaml .vscode # dumi .dumi/tmp .dumi/tmp-test .dumi/tmp-production .docs ================================================ FILE: .husky/pre-commit ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx lint-staged ================================================ FILE: .prettierrc ================================================ { "endOfLine": "lf", "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": "all", "proseWrap": "never" } ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - 10 script: - | if [ "$TEST_TYPE" = test ]; then npm run coverage && \ bash <(curl -s https://codecov.io/bash) else npm run $TEST_TYPE fi env: matrix: - TEST_TYPE=lint - TEST_TYPE=test ================================================ FILE: HISTORY.md ================================================ # History ---- ## 2.4.0 / 2018-12-28 - `overlay` support function render ## 2.3.0 / 2018-12-21 - add `openClassName` ## 2.2.0 / 2018-06-06 - add `alignPoint` to support mosue point align ## 1.5.0 / 2016-07-27 - Add `onOverlayClick`. - ## 1.4.5 / 2016-03-02 - if exists getPopupContainer it will be passed to Trigger component ## 1.4.0 / 2015-10-26 - update for react 0.14 ## 1.2.0 / 2015-06-07 - remove closeOnSelect, use visible prop to control ## 0.8.0 / 2015-06-07 Already available ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015-present Alipay.com, https://www.alipay.com/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # @rc-component/dropdown react dropdown component. [![NPM version][npm-image]][npm-url] [![npm download][download-image]][download-url] [![build status][github-actions-image]][github-actions-url] [![Codecov][codecov-image]][codecov-url] [![bundle size][bundlephobia-image]][bundlephobia-url] [![dumi][dumi-image]][dumi-url] [npm-image]: https://img.shields.io/npm/v/@rc-component/dropdown.svg?style=flat-square [npm-url]: https://npmjs.org/package/@rc-component/dropdown [travis-image]: https://img.shields.io/travis/react-component/dropdown/master?style=flat-square [travis-url]: https://travis-ci.com/react-component/dropdown [github-actions-image]: https://github.com/react-component/dropdown/actions/workflows/ci.yml/badge.svg [github-actions-url]: https://github.com/react-component/dropdown/actions/workflows/ci.yml [codecov-image]: https://img.shields.io/codecov/c/github/react-component/dropdown/master.svg?style=flat-square [codecov-url]: https://app.codecov.io/gh/react-component/dropdown [david-url]: https://david-dm.org/react-component/dropdown [david-image]: https://david-dm.org/react-component/dropdown/status.svg?style=flat-square [david-dev-url]: https://david-dm.org/react-component/dropdown?type=dev [david-dev-image]: https://david-dm.org/react-component/dropdown/dev-status.svg?style=flat-square [download-image]: https://img.shields.io/npm/dm/@rc-component/dropdown.svg?style=flat-square [download-url]: https://npmjs.org/package/@rc-component/dropdown [bundlephobia-url]: https://bundlephobia.com/package/@rc-component/dropdown [bundlephobia-image]: https://badgen.net/bundlephobia/minzip/@rc-component/dropdown [dumi-url]: https://github.com/umijs/dumi [dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square ## Screenshot ![](https://t.alipayobjects.com/images/rmsweb/T1bWpgXgBaXXXXXXXX.png) ## Example online example: http://react-component.github.io/dropdown/examples/ ## install [![@rc-component/dropdown](https://nodei.co/npm/@rc-component/dropdown.png)](https://npmjs.org/package/@rc-component/dropdown) ## Usage ```js var Dropdown = require('@rc-component/dropdown'); // use dropdown ``` ## API ### props                                        
name type default description
overlayClassName String additional css class of root dom node
openClassName String`${prefixCls}-open`className of trigger when dropdown is opened
prefixCls String rc-dropdown prefix class name
transitionName String dropdown menu's animation css class name
animation String part of dropdown menu's animation css class name
placement String bottomLeft Position of menu item. There are: top, topCenter, topRight, bottomLeft, bottom, bottomRight
onVisibleChange Function call when visible is changed
visible boolean whether tooltip is visible
defaultVisible boolean whether tooltip is visible initially
overlay rc-menu rc-menu element
onOverlayClick function(e) call when overlay is clicked
minOverlayWidthMatchTrigger booleantrue (false when set alignPoint)whether overlay's width must not be less than trigger's
getPopupContainer Function(menuDOMNode): HTMLElement () => document.body Where to render the DOM node of dropdown
Note: Additional props are passed into the underlying [rc-trigger](https://github.com/react-component/trigger) component. This can be useful for example, to display the dropdown in a separate [portal](https://reactjs.org/docs/portals.html)-driven window via the `getDocument()` rc-trigger prop. ## Development ```bash npm install npm start ``` ## Test Case ```bash npm test npm run chrome-test ``` ## Coverage ```bash npm run coverage ``` open coverage/ dir ## License @rc-component/dropdown is released under the MIT license. ================================================ FILE: assets/index.less ================================================ @dropdownPrefixCls: rc-dropdown; @dropdown-arrow-width: 8px; @dropdown-distance: @dropdown-arrow-width + 4; @dropdown-arrow-color: #373737; @dropdown-overlay-shadow: 0 1px 5px #ccc; @font-face { font-family: 'anticon'; src: url('//at.alicdn.com/t/font_1434092639_4910953.eot'); /* IE9*/ src: url('//at.alicdn.com/t/font_1434092639_4910953.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('//at.alicdn.com/t/font_1434092639_4910953.woff') format('woff'), /* chrome、firefox */ url('//at.alicdn.com/t/font_1434092639_4910953.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/ url('//at.alicdn.com/t/font_1434092639_4910953.svg#iconfont') format('svg'); /* iOS 4.1- */ } .@{dropdownPrefixCls} { position: absolute; left: -9999px; top: -9999px; z-index: 1070; display: block; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 12px; font-weight: normal; line-height: 1.5; &-hidden { display: none; } .rc-menu { outline: none; position: relative; list-style-type: none; padding: 0; margin: 2px 0 2px; text-align: left; background-color: #fff; border-radius: 3px; box-shadow: @dropdown-overlay-shadow; background-clip: padding-box; border: 1px solid #ccc; > li { margin: 0; padding: 0; } &:before { content: ""; position: absolute; top: -4px; left: 0; width: 100%; height: 4px; background: rgb(255, 255, 255); background: rgba(255, 255, 255, 0.01); } & > &-item { position: relative; display: block; padding: 7px 10px; clear: both; font-size: 12px; font-weight: normal; color: #666666; white-space: nowrap; &:hover, &-active, &-selected { background-color: #ebfaff; } &-selected { position: relative; &:after { content: '\e613'; font-family: 'anticon'; font-weight: bold; position: absolute; top: 6px; right: 16px; color: #3CB8F0; } } &-disabled { color: #ccc; cursor: not-allowed; pointer-events: none; &:hover { color: #ccc; background-color: #fff; cursor: not-allowed; } } &:last-child { border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; } &:first-child { border-top-left-radius: 3px; border-top-right-radius: 3px; } &-divider { height: 1px; margin: 1px 0; overflow: hidden; background-color: #e5e5e5; line-height: 0; } } } .effect() { animation-duration: 0.3s; animation-fill-mode: both; transform-origin: 0 0; display: block !important; } &-slide-up-enter,&-slide-up-appear { .effect(); opacity: 0; animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); animation-play-state: paused; } &-slide-up-leave { .effect(); opacity: 1; animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34); animation-play-state: paused; } &-slide-up-enter&-slide-up-enter-active&-placement-bottomLeft, &-slide-up-appear&-slide-up-appear-active&-placement-bottomLeft, &-slide-up-enter&-slide-up-enter-active&-placement-bottomCenter, &-slide-up-appear&-slide-up-appear-active&-placement-bottomCenter, &-slide-up-enter&-slide-up-enter-active&-placement-bottomRight, &-slide-up-appear&-slide-up-appear-active&-placement-bottomRight { animation-name: rcDropdownSlideUpIn; animation-play-state: running; } &-slide-up-enter&-slide-up-enter-active&-placement-topLeft, &-slide-up-appear&-slide-up-appear-active&-placement-topLeft, &-slide-up-enter&-slide-up-enter-active&-placement-topCenter, &-slide-up-appear&-slide-up-appear-active&-placement-topCenter, &-slide-up-enter&-slide-up-enter-active&-placement-topRight, &-slide-up-appear&-slide-up-appear-active&-placement-topRight { animation-name: rcDropdownSlideDownIn; animation-play-state: running; } &-slide-up-leave&-slide-up-leave-active&-placement-bottomLeft, &-slide-up-leave&-slide-up-leave-active&-placement-bottomCenter, &-slide-up-leave&-slide-up-leave-active&-placement-bottomRight { animation-name: rcDropdownSlideUpOut; animation-play-state: running; } &-slide-up-leave&-slide-up-leave-active&-placement-topLeft, &-slide-up-leave&-slide-up-leave-active&-placement-topCenter, &-slide-up-leave&-slide-up-leave-active&-placement-topRight { animation-name: rcDropdownSlideDownOut; animation-play-state: running; } @keyframes rcDropdownSlideUpIn { 0% { opacity: 0; transform-origin: 0% 0%; transform: scaleY(0); } 100% { opacity: 1; transform-origin: 0% 0%; transform: scaleY(1); } } @keyframes rcDropdownSlideUpOut { 0% { opacity: 1; transform-origin: 0% 0%; transform: scaleY(1); } 100% { opacity: 0; transform-origin: 0% 0%; transform: scaleY(0); } } @keyframes rcDropdownSlideDownIn { 0% { opacity: 0; transform-origin: 0% 100%; transform: scaleY(0); } 100% { opacity: 1; transform-origin: 0% 100%; transform: scaleY(1); } } @keyframes rcDropdownSlideDownOut { 0% { opacity: 1; transform-origin: 0% 100%; transform: scaleY(1); } 100% { opacity: 0; transform-origin: 0% 100%; transform: scaleY(0); } } } // arrows .@{dropdownPrefixCls}-arrow { position: absolute; border-width: @dropdown-arrow-width / 2; border-color: transparent; box-shadow: @dropdown-overlay-shadow; border-style: solid; transform: rotate(45deg); } .@{dropdownPrefixCls} { // adjust padding &-show-arrow&-placement-top, &-show-arrow&-placement-topLeft, &-show-arrow&-placement-topRight { padding-bottom: 6px; } &-show-arrow&-placement-bottom, &-show-arrow&-placement-bottomLeft, &-show-arrow&-placement-bottomRight { padding-top: 6px; } // top-* &-placement-top &-arrow, &-placement-topLeft &-arrow, &-placement-topRight &-arrow { bottom: @dropdown-distance - @dropdown-arrow-width; border-top-color: white; } &-placement-top &-arrow { left: 50%; } &-placement-topLeft &-arrow { left: 15%; } &-placement-topRight &-arrow { right: 15%; } // bottom-* &-placement-bottom &-arrow, &-placement-bottomLeft &-arrow, &-placement-bottomRight &-arrow { top: @dropdown-distance - @dropdown-arrow-width; border-bottom-color: white; } &-placement-bottom &-arrow { left: 50%; } &-placement-bottomLeft &-arrow { left: 15%; } &-placement-bottomRight &-arrow { right: 15%; } } ================================================ FILE: docs/demo/arrow.md ================================================ --- title: arrow nav: title: Demo path: /demo --- ================================================ FILE: docs/demo/context-menu.md ================================================ --- title: context-menu nav: title: Demo path: /demo --- ================================================ FILE: docs/demo/dropdown-menu-width.md ================================================ --- title: dropdown-menu-width nav: title: Demo path: /demo --- ================================================ FILE: docs/demo/multiple.md ================================================ --- title: multiple nav: title: Demo path: /demo --- ================================================ FILE: docs/demo/overlay-callback.md ================================================ --- title: overlay-callback nav: title: Demo path: /demo --- ================================================ FILE: docs/demo/simple.md ================================================ --- title: simple nav: title: Demo path: /demo --- ================================================ FILE: docs/examples/arrow.jsx ================================================ import Dropdown from '@rc-component/dropdown'; import Menu, { Divider, Item as MenuItem } from '@rc-component/menu'; import React from 'react'; import '../../assets/index.less'; function onSelect({ key }) { console.log(`${key} selected`); } function onVisibleChange(visible) { console.log(visible); } const menu = ( disabled one two ); export default function Arrow() { return (
); } ================================================ FILE: docs/examples/context-menu.jsx ================================================ import Dropdown from '@rc-component/dropdown'; import Menu, { Item as MenuItem } from '@rc-component/menu'; import React from 'react'; import '../../assets/index.less'; function ContextMenu() { const menu = ( one two ); return (
Right click me!
); } export default ContextMenu; ================================================ FILE: docs/examples/dropdown-menu-width.jsx ================================================ import Dropdown from '@rc-component/dropdown'; import Menu, { Item as MenuItem } from '@rc-component/menu'; import React, { PureComponent } from 'react'; import '../../assets/index.less'; class Example extends PureComponent { state = { longList: false }; short = () => { this.setState({ longList: false }); }; long = () => { this.setState({ longList: true }); }; render() { const menuItems = [ 1st item, 2nd item, ]; if (this.state.longList) { menuItems.push(3rd LONG SUPER LONG item); } const menu = {menuItems}; return (
); } } export default Example; ================================================ FILE: docs/examples/multiple.jsx ================================================ import Dropdown from '@rc-component/dropdown'; import Menu, { Divider, Item as MenuItem } from '@rc-component/menu'; import React, { Component } from 'react'; import '../../assets/index.less'; class Test extends Component { state = { visible: false, }; onVisibleChange = (visible) => { console.log('visible', visible); this.setState({ visible, }); }; selected = []; saveSelected = ({ selectedKeys }) => { this.selected = selectedKeys; }; confirm = () => { console.log(this.selected); this.setState({ visible: false, }); }; render() { const menu = ( one two ); return ( ); } } export default Test; ================================================ FILE: docs/examples/overlay-callback.jsx ================================================ import Dropdown from '@rc-component/dropdown'; import Menu, { Divider, Item as MenuItem } from '@rc-component/menu'; import React from 'react'; import '../../assets/index.less'; function onSelect({ key }) { console.log(`${key} selected`); } function onVisibleChange(visible) { console.log(visible); } const menuCallback = () => ( disabled one two ); export default function OverlayCallback() { return (
); } ================================================ FILE: docs/examples/simple.jsx ================================================ /* eslint-disable no-console,react/button-has-type */ import Dropdown from '@rc-component/dropdown'; import Menu, { Divider, Item as MenuItem } from '@rc-component/menu'; import React from 'react'; import '../../assets/index.less'; function onSelect({ key }) { console.log(`${key} selected`); } function onVisibleChange(visible) { console.log(visible); } const menu = ( disabled one two ); export default function Simple() { return (
); } ================================================ FILE: docs/index.md ================================================ --- hero: title: rc-dropdown description: React Dropdown Component --- ================================================ FILE: index.js ================================================ 'use strict'; module.exports = require('./src'); ================================================ FILE: now.json ================================================ { "version": 2, "name": "rc-dropdown", "builds": [ { "src": "package.json", "use": "@now/static-build", "config": { "distDir": ".docs" } } ] } ================================================ FILE: package.json ================================================ { "name": "@rc-component/dropdown", "version": "1.0.2", "description": "dropdown ui component for react", "keywords": [ "react", "react-dropdown" ], "homepage": "http://github.com/react-component/dropdown", "bugs": { "url": "http://github.com/react-component/dropdown/issues" }, "repository": { "type": "git", "url": "git@github.com:react-component/dropdown.git" }, "license": "MIT", "maintainers": [ "yiminghe@gmail.com", "hualei5280@gmail.com" ], "main": "lib/index", "module": "./es/index", "files": [ "lib", "es", "assets/*.css" ], "scripts": { "build": "dumi build", "compile": "father build && lessc assets/index.less assets/index.css", "coverage": "rc-test --coverage", "lint": "eslint src/ docs/examples/ --ext .tsx,.ts,.jsx,.js", "now-build": "npm run build", "prepare": "husky install && dumi setup", "prepublishOnly": "npm run compile && rc-np", "start": "dumi dev", "test": "rc-test" }, "lint-staged": { "**/*.{js,jsx,tsx,ts,md,json}": [ "prettier --write", "git add" ] }, "dependencies": { "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "devDependencies": { "@rc-component/father-plugin": "^2.0.2", "@rc-component/menu": "^1.0.0", "@rc-component/np": "^1.0.3", "@rc-component/resize-observer": "^1.0.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@types/jest": "^29.0.0", "@types/node": "^24.5.2", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/warning": "^3.0.0", "@umijs/fabric": "^3.0.0", "dumi": "^2.0.0", "eslint": "^7.18.0", "father": "^4.0.0", "glob": "^10.0.0", "husky": "^8.0.3", "jest-environment-jsdom": "^29.5.0", "less": "^4.1.1", "lint-staged": "^13.2.1", "prettier": "^2.8.7", "rc-test": "^7.0.14", "react": "^18.0.0", "react-dom": "^18.0.0", "typescript": "^5.0.0" }, "peerDependencies": { "react": ">=16.11.0", "react-dom": ">=16.11.0" } } ================================================ FILE: script/update-content.js ================================================ /* 用于 dumi 改造使用, 可用于将 examples 的文件批量修改为 demo 引入形式, 其他项目根据具体情况使用。 */ const fs = require('fs'); const glob = require('glob'); const paths = glob.sync('./docs/examples/*.jsx'); paths.forEach((path) => { const name = path.split('/').pop().split('.')[0]; fs.writeFile( `./docs/demo/${name}.md`, `--- title: ${name} nav: title: Demo path: /demo --- `, 'utf8', function (error) { if (error) { console.log(error); return false; } console.log(`${name} 更新成功~`); }, ); }); ================================================ FILE: src/Dropdown.tsx ================================================ import type { TriggerProps, TriggerRef } from '@rc-component/trigger'; import Trigger from '@rc-component/trigger'; import type { ActionType, AlignType, AnimationType, BuildInPlacements, } from '@rc-component/trigger/lib/interface'; import { composeRef, getNodeRef, supportRef } from '@rc-component/util/lib/ref'; import { clsx } from 'clsx'; import React from 'react'; import useAccessibility from './hooks/useAccessibility'; import Overlay from './Overlay'; import Placements from './placements'; export interface DropdownProps extends Pick< TriggerProps, | 'getPopupContainer' | 'children' | 'mouseEnterDelay' | 'mouseLeaveDelay' | 'onPopupAlign' | 'builtinPlacements' | 'autoDestroy' > { minOverlayWidthMatchTrigger?: boolean; arrow?: boolean; onVisibleChange?: (visible: boolean) => void; onOverlayClick?: (e: Event) => void; prefixCls?: string; transitionName?: string; overlayClassName?: string; openClassName?: string; animation?: AnimationType; align?: AlignType; overlayStyle?: React.CSSProperties; placement?: keyof typeof Placements; placements?: BuildInPlacements; overlay?: (() => React.ReactElement) | React.ReactElement; trigger?: ActionType | ActionType[]; alignPoint?: boolean; showAction?: ActionType[]; hideAction?: ActionType[]; visible?: boolean; autoFocus?: boolean; } const Dropdown = React.forwardRef((props, ref) => { const { arrow = false, prefixCls = 'rc-dropdown', transitionName, animation, align, placement = 'bottomLeft', placements = Placements, getPopupContainer, showAction, hideAction, overlayClassName, overlayStyle, visible, trigger = ['hover'], autoFocus, overlay, children, onVisibleChange, ...otherProps } = props; const [triggerVisible, setTriggerVisible] = React.useState(); const mergedVisible = 'visible' in props ? visible : triggerVisible; const mergedMotionName = animation ? `${prefixCls}-${animation}` : transitionName; const triggerRef = React.useRef(null); const overlayRef = React.useRef(null); const childRef = React.useRef(null); React.useImperativeHandle(ref, () => triggerRef.current); const handleVisibleChange = (newVisible: boolean) => { setTriggerVisible(newVisible); onVisibleChange?.(newVisible); }; useAccessibility({ visible: mergedVisible, triggerRef: childRef, onVisibleChange: handleVisibleChange, autoFocus, overlayRef, }); const onClick = (e) => { const { onOverlayClick } = props; setTriggerVisible(false); if (onOverlayClick) { onOverlayClick(e); } }; const getMenuElement = () => ( ); const getMenuElementOrLambda = () => { if (typeof overlay === 'function') { return getMenuElement; } return getMenuElement(); }; const getMinOverlayWidthMatchTrigger = () => { const { minOverlayWidthMatchTrigger, alignPoint } = props; if ('minOverlayWidthMatchTrigger' in props) { return minOverlayWidthMatchTrigger; } return !alignPoint; }; const getOpenClassName = () => { const { openClassName } = props; if (openClassName !== undefined) { return openClassName; } return `${prefixCls}-open`; }; const childrenNode = React.cloneElement(children as React.ReactElement, { className: clsx( (children as React.ReactElement).props?.className, mergedVisible && getOpenClassName(), ), ref: supportRef(children) ? composeRef(childRef, getNodeRef(children as React.ReactElement)) : undefined, }); let triggerHideAction = hideAction; if (!triggerHideAction && trigger.indexOf('contextMenu') !== -1) { triggerHideAction = ['click']; } return ( {childrenNode} ); }); export default Dropdown; ================================================ FILE: src/Overlay.tsx ================================================ import { composeRef, getNodeRef, supportRef } from '@rc-component/util/lib/ref'; import React, { forwardRef, useMemo } from 'react'; import type { DropdownProps } from './Dropdown'; export type OverlayProps = Pick< DropdownProps, 'overlay' | 'arrow' | 'prefixCls' >; const Overlay = forwardRef((props, ref) => { const { overlay, arrow, prefixCls } = props; const overlayNode = useMemo(() => { let overlayElement: React.ReactElement; if (typeof overlay === 'function') { overlayElement = overlay(); } else { overlayElement = overlay; } return overlayElement; }, [overlay]); const composedRef = composeRef(ref, getNodeRef(overlayNode)); return ( <> {arrow &&
} {React.cloneElement(overlayNode, { ref: supportRef(overlayNode) ? composedRef : undefined, })} ); }); export default Overlay; ================================================ FILE: src/hooks/useAccessibility.ts ================================================ import KeyCode from '@rc-component/util/lib/KeyCode'; import raf from '@rc-component/util/lib/raf'; import * as React from 'react'; const { ESC, TAB } = KeyCode; interface UseAccessibilityProps { visible: boolean; triggerRef: React.RefObject; onVisibleChange?: (visible: boolean) => void; autoFocus?: boolean; overlayRef?: React.RefObject; } export default function useAccessibility({ visible, triggerRef, onVisibleChange, autoFocus, overlayRef, }: UseAccessibilityProps) { const focusMenuRef = React.useRef(false); const handleCloseMenuAndReturnFocus = () => { if (visible) { triggerRef.current?.focus?.(); onVisibleChange?.(false); } }; const focusMenu = () => { if (overlayRef.current?.focus) { overlayRef.current.focus(); focusMenuRef.current = true; return true; } return false; }; const handleKeyDown = (event) => { switch (event.keyCode) { case ESC: handleCloseMenuAndReturnFocus(); break; case TAB: { let focusResult: boolean = false; if (!focusMenuRef.current) { focusResult = focusMenu(); } if (focusResult) { event.preventDefault(); } else { handleCloseMenuAndReturnFocus(); } break; } } }; React.useEffect(() => { if (visible) { window.addEventListener('keydown', handleKeyDown); if (autoFocus) { // FIXME: hack with raf raf(focusMenu, 3); } return () => { window.removeEventListener('keydown', handleKeyDown); focusMenuRef.current = false; }; } return () => { focusMenuRef.current = false; }; }, [visible]); // eslint-disable-line react-hooks/exhaustive-deps } ================================================ FILE: src/index.tsx ================================================ export type { TriggerProps } from '@rc-component/trigger'; export type { DropdownProps } from './Dropdown'; export type { OverlayProps } from './Overlay'; import Dropdown from './Dropdown'; export default Dropdown; ================================================ FILE: src/placements.ts ================================================ const autoAdjustOverflow = { adjustX: 1, adjustY: 1, }; const targetOffset = [0, 0]; const placements = { topLeft: { points: ['bl', 'tl'], overflow: autoAdjustOverflow, offset: [0, -4], targetOffset, }, top: { points: ['bc', 'tc'], overflow: autoAdjustOverflow, offset: [0, -4], targetOffset, }, topRight: { points: ['br', 'tr'], overflow: autoAdjustOverflow, offset: [0, -4], targetOffset, }, bottomLeft: { points: ['tl', 'bl'], overflow: autoAdjustOverflow, offset: [0, 4], targetOffset, }, bottom: { points: ['tc', 'bc'], overflow: autoAdjustOverflow, offset: [0, 4], targetOffset, }, bottomRight: { points: ['tr', 'br'], overflow: autoAdjustOverflow, offset: [0, 4], targetOffset, }, }; export default placements; ================================================ FILE: tests/__mocks__/@rc-component/trigger.tsx ================================================ import Trigger from '@rc-component/trigger/lib/mock'; export default Trigger; ================================================ FILE: tests/__snapshots__/basic.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`dropdown simply works 1`] = `
`; ================================================ FILE: tests/basic.test.tsx ================================================ /* eslint-disable react/button-has-type,react/no-find-dom-node,react/no-render-return-value,object-shorthand,func-names,max-len */ import type { MenuRef } from '@rc-component/menu'; import Menu, { Divider, Item as MenuItem } from '@rc-component/menu'; import { _rs } from '@rc-component/resize-observer'; import { spyElementPrototypes } from '@rc-component/util/lib/test/domHook'; import { act, fireEvent } from '@testing-library/react'; import type { HTMLAttributes } from 'react'; import * as React from 'react'; import { createRef, forwardRef, useImperativeHandle } from 'react'; import Dropdown from '../src'; import { render, sleep } from './utils'; async function waitForTime() { for (let i = 0; i < 10; i += 1) { await act(async () => { jest.runAllTimers(); }); } } async function triggerResize(target: Element) { act(() => { _rs([{ target } as ResizeObserverEntry]); }); await waitForTime(); } spyElementPrototypes(HTMLElement, { offsetParent: { get: () => document.body, }, offsetLeft: { get: function () { return parseFloat(window.getComputedStyle(this).marginLeft) || 0; }, }, offsetTop: { get: function () { return parseFloat(window.getComputedStyle(this).marginTop) || 0; }, }, offsetHeight: { get: function () { return parseFloat(window.getComputedStyle(this).height) || 0; }, }, offsetWidth: { get: function () { return parseFloat(window.getComputedStyle(this).width) || 0; }, }, getBoundingClientRect: () => ({ width: 100, height: 100, }), }); describe('dropdown', () => { beforeEach(() => { jest.clearAllTimers(); }); it('default visible', () => { const { container } = render( Test
} visible> , ); expect(container instanceof HTMLDivElement).toBeTruthy(); expect( container .querySelector('.my-button') ?.classList.contains('rc-dropdown-open'), ).toBeTruthy(); }); it('supports controlled visible prop', () => { const onVisibleChange = jest.fn(); const { container } = render( Test
} visible trigger={['click']} onVisibleChange={onVisibleChange} > , ); expect(container instanceof HTMLDivElement).toBeTruthy(); expect( container .querySelector('.my-button') ?.classList.contains('rc-dropdown-open'), ).toBeTruthy(); fireEvent.click(container.querySelector('.my-button')); expect(onVisibleChange).toHaveBeenCalledWith(false); }); it('simply works', async () => { let clicked; function onClick({ key }) { clicked = key; } const onOverlayClick = jest.fn(); const menu = ( one two ); const { container, baseElement } = render( , ); expect(container.querySelector('.my-button')).toBeTruthy(); // should not display until be triggered expect(baseElement.querySelector('.rc-dropdown')).toBeFalsy(); fireEvent.click(container.querySelector('.my-button')); expect(clicked).toBeUndefined(); expect( baseElement .querySelector('.rc-dropdown') .classList.contains('rc-dropdown-hidden'), ).toBeFalsy(); expect(container).toMatchSnapshot(); fireEvent.click(baseElement.querySelector('.my-menuitem')); expect(clicked).toBe('1'); expect(onOverlayClick).toHaveBeenCalled(); expect( baseElement .querySelector('.rc-dropdown') .classList.contains('rc-dropdown-hidden'), ).toBeTruthy(); }); it('re-align works', async () => { jest.useFakeTimers(); const onPopupAlign = jest.fn(); const buttonStyle = { width: 600, height: 20, marginLeft: 100 }; const menu = ( one ); const { container } = render( , ); expect(onPopupAlign).not.toHaveBeenCalled(); fireEvent.click(container.querySelector('.my-btn')); await waitForTime(); expect(onPopupAlign).toHaveBeenCalled(); jest.useRealTimers(); }); it('Test default minOverlayWidthMatchTrigger', async () => { jest.useFakeTimers(); const overlayWidth = 50; const overlay =
Test
; const { container, baseElement } = render( , ); await triggerResize(container.querySelector('button')); expect(baseElement.querySelector('.rc-dropdown')).toHaveStyle({ minWidth: '100px', }); jest.useRealTimers(); }); it('user pass minOverlayWidthMatchTrigger', async () => { jest.useFakeTimers(); const overlayWidth = 50; const overlay =
Test
; const { container, baseElement } = render( , ); await triggerResize(container.querySelector('button')); expect(baseElement.querySelector('.rc-dropdown')).not.toHaveStyle({ minWidth: '100px', }); jest.useRealTimers(); }); it('should support default openClassName', () => { const overlay =
Test
; const { container } = render( , ); fireEvent.click(container.querySelector('.my-button')); expect( container .querySelector('.my-button') .classList.contains('rc-dropdown-open'), ).toBeTruthy(); fireEvent.click(container.querySelector('.my-button')); expect( container .querySelector('.my-button') .classList.contains('rc-dropdown-open'), ).toBeFalsy(); }); it('should support custom openClassName', async () => { const overlay =
Test
; const { container } = render( , ); fireEvent.click(container.querySelector('.my-button')); expect( container.querySelector('.my-button').classList.contains('opened'), ).toBeTruthy(); fireEvent.click(container.querySelector('.my-button')); expect( container.querySelector('.my-button').classList.contains('opened'), ).toBeFalsy(); }); it('overlay callback', async () => { const overlay =
Test
; const { container, baseElement } = render( overlay}> , ); fireEvent.click(container.querySelector('.my-button')); expect( baseElement .querySelector('.rc-dropdown') .classList.contains('rc-dropdown-hidden'), ).toBeFalsy(); }); it('should support arrow', async () => { const overlay =
Test
; const { container, baseElement } = render( , ); fireEvent.click(container.querySelector('.my-button')); await sleep(500); expect( baseElement .querySelector('.rc-dropdown') .classList.contains('rc-dropdown-show-arrow'), ).toBeTruthy(); expect( baseElement .querySelector('.rc-dropdown') .firstElementChild.classList.contains('rc-dropdown-arrow'), ).toBeTruthy(); }); it('Keyboard navigation works', async () => { jest.useFakeTimers(); const overlay = ( one two ); const { container, baseElement } = render( , ); const trigger = container.querySelector('.my-button'); // Open menu; fireEvent.click(trigger); await waitForTime(); expect( baseElement .querySelector('.rc-dropdown') .classList.contains('rc-dropdown-hidden'), ).toBeFalsy(); // Close menu with Esc fireEvent.keyDown(window, { key: 'Esc', keyCode: 27 }); await waitForTime(); expect(document.activeElement.className).toContain('my-button'); // Open menu fireEvent.click(trigger); await waitForTime(); expect( baseElement .querySelector('.rc-dropdown') .classList.contains('rc-dropdown-hidden'), ).toBeFalsy(); // Focus menu with Tab window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 9 })); // Tab expect(document.activeElement.className).toContain('menu'); // Close menu with Tab window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 9 })); // Tab await waitForTime(); expect(document.activeElement.className).toContain('my-button'); jest.useRealTimers(); }); it('Tab should close menu if overlay cannot be focused', async () => { jest.useFakeTimers(); const Overlay = () =>
test
; const { container, baseElement } = render( }> , ); const trigger = container.querySelector('.my-button'); // Open menu; fireEvent.click(trigger); await waitForTime(); expect( baseElement .querySelector('.rc-dropdown') .classList.contains('rc-dropdown-hidden'), ).toBeFalsy(); // Close menu with Esc fireEvent.keyDown(window, { key: 'Esc', keyCode: 27 }); await waitForTime(); expect(document.activeElement.className).toContain('my-button'); // Open menu fireEvent.click(trigger); await waitForTime(); expect( baseElement .querySelector('.rc-dropdown') .classList.contains('rc-dropdown-hidden'), ).toBeFalsy(); // Close menu with Tab window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 9 })); // Tab await waitForTime(); expect(document.activeElement.className).toContain('my-button'); jest.useRealTimers(); }); it('keyboard should work if menu is wrapped', async () => { const overlay = (
one two
); const { container, baseElement } = render( , ); const trigger = container.querySelector('.my-button'); // Open menu fireEvent.click(trigger); await sleep(200); expect( baseElement .querySelector('.rc-dropdown') .classList.contains('rc-dropdown-hidden'), ).toBeFalsy(); // Close menu with Esc window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 27 })); // Esc await sleep(200); expect(document.activeElement.className).toContain('my-button'); // Open menu fireEvent.click(trigger); await sleep(200); expect( baseElement .querySelector('.rc-dropdown') .classList.contains('rc-dropdown-hidden'), ).toBeFalsy(); // Focus menu with Tab window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 9 })); // Tab // Close menu with Tab window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 9 })); // Tab await sleep(200); expect(document.activeElement.className).toContain('my-button'); }); it('support Menu expandIcon', async () => { const props = { overlay: ( }> foo foo ), visible: true, getPopupContainer: (node) => node, }; const { container } = render( , ); await sleep(500); expect(container.querySelector('#customExpandIcon')).toBeTruthy(); }); it('should support customized menuRef', async () => { const menuRef = createRef(); const props = { overlay: ( foo ), visible: true, }; render( , ); await sleep(500); expect(menuRef.current).toBeTruthy(); }); it('should support trigger when child provide nativeElement', async () => { jest.useFakeTimers(); const Button = forwardRef>( (props, ref) => { const btnRef = createRef(); useImperativeHandle(ref, () => ({ foo: () => {}, nativeElement: btnRef.current, })); return ( ); }, ); const { container, baseElement } = render( node} overlay={ foo } > , ); const trigger = container.querySelector('.my-button'); // Open menu fireEvent.click(trigger); await waitForTime(); expect( container .querySelector('.rc-dropdown') .classList.contains('rc-dropdown-hidden'), ).toBeFalsy(); expect(document.activeElement.className).toContain('menu'); // Close menu with Tab window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 9 })); // Tab await waitForTime(); expect(document.activeElement.className).toContain('my-button'); jest.useRealTimers(); }); it('children cannot be given ref should not throw', () => { const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const Component = () =>
test
; render( test
}> , ); expect(errorSpy).not.toHaveBeenCalledWith( expect.stringContaining( 'Warning: Function components cannot be given refs', ), expect.anything(), expect.anything(), ); }); }); ================================================ FILE: tests/point.test.tsx ================================================ /* eslint-disable react/button-has-type,react/no-render-return-value */ import { act, fireEvent } from '@testing-library/react'; import * as React from 'react'; import Dropdown from '../src'; import { render } from './utils'; // Fix prettier rm this console.log(!!React); async function waitForTime() { for (let i = 0; i < 10; i += 1) { await act(async () => { jest.runAllTimers(); }); } } describe('point', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.clearAllTimers(); jest.useRealTimers(); }); it('click show', async () => { const overlay = (
Test
); const onPopupAlign = jest.fn(); const { container } = render( , ); fireEvent.contextMenu(container.querySelector('.my-button')); await waitForTime(); expect(container.querySelector('.rc-dropdown')).toBeTruthy(); }); }); ================================================ FILE: tests/utils.js ================================================ import { StrictMode } from 'react'; import { render, act } from '@testing-library/react'; const globalTimeout = global.setTimeout; export async function sleep(timeout = 0) { await act(async () => { await new Promise((resolve) => { globalTimeout(resolve, timeout); }); }); } function customRender(ui, options) { return render(ui, { wrapper: StrictMode, ...options }); } export { customRender as render }; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "allowSyntheticDefaultImports": true, "baseUrl": "./", "declaration": true, "module": "esnext", "target": "esnext", "moduleResolution": "node", "jsx": "react", "skipLibCheck": true, "paths": { "@@/*": [".dumi/tmp/*"] } }, "include": ["./src", "./tests", "./typings/"], "typings": "./typings/index.d.ts", "exclude": [ "node_modules", "build", "scripts", "acceptance-tests", "webpack", "jest", "src/setupTests.ts", "tslint:latest", "tslint-config-prettier" ] }