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

## Example
online example: http://react-component.github.io/dropdown/examples/
## install
[](https://npmjs.org/package/@rc-component/dropdown)
## Usage
```js
var Dropdown = require('@rc-component/dropdown');
// use dropdown
```
## API
### props
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="width: 100px;">name</th>
<th style="width: 50px;">type</th>
<th style="width: 50px;">default</th>
<th>description</th>
</tr>
</thead>
<tbody>
<tr>
<td>overlayClassName</td>
<td>String</td>
<td></td>
<td>additional css class of root dom node</td>
</tr>
<tr>
<td>openClassName</td>
<td>String</td>
<td>`${prefixCls}-open`</td>
<td>className of trigger when dropdown is opened</td>
</tr>
<tr>
<td>prefixCls</td>
<td>String</td>
<td>rc-dropdown</td>
<td>prefix class name</td>
</tr>
<tr>
<td>transitionName</td>
<td>String</td>
<td></td>
<td>dropdown menu's animation css class name</td>
</tr>
<tr>
<td>animation</td>
<td>String</td>
<td></td>
<td>part of dropdown menu's animation css class name</td>
</tr>
<tr>
<td>placement</td>
<td>String</td>
<td>bottomLeft</td>
<td>Position of menu item. There are: top, topCenter, topRight, bottomLeft, bottom, bottomRight </td>
</tr>
<tr>
<td>onVisibleChange</td>
<td>Function</td>
<td></td>
<td>call when visible is changed</td>
</tr>
<tr>
<td>visible</td>
<td>boolean</td>
<td></td>
<td>whether tooltip is visible</td>
</tr>
<tr>
<td>defaultVisible</td>
<td>boolean</td>
<td></td>
<td>whether tooltip is visible initially</td>
</tr>
<tr>
<td>overlay</td>
<td>rc-menu</td>
<td></td>
<td><a href="https://github.com/react-component/menu">rc-menu</a> element</td>
</tr>
<tr>
<td>onOverlayClick</td>
<td>function(e)</td>
<td></td>
<td>call when overlay is clicked</td>
</tr>
<tr>
<td>minOverlayWidthMatchTrigger</td>
<td>boolean</td>
<td>true (false when set alignPoint)</td>
<td>whether overlay's width must not be less than trigger's </td>
</tr>
<tr>
<td>getPopupContainer</td>
<td>Function(menuDOMNode): HTMLElement</td>
<th>() => document.body</th>
<td>Where to render the DOM node of dropdown</td>
</tr>
</tbody>
</table>
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
---
<code src="../examples/arrow.jsx"></code>
================================================
FILE: docs/demo/context-menu.md
================================================
---
title: context-menu
nav:
title: Demo
path: /demo
---
<code src="../examples/context-menu.jsx"></code>
================================================
FILE: docs/demo/dropdown-menu-width.md
================================================
---
title: dropdown-menu-width
nav:
title: Demo
path: /demo
---
<code src="../examples/dropdown-menu-width.jsx"></code>
================================================
FILE: docs/demo/multiple.md
================================================
---
title: multiple
nav:
title: Demo
path: /demo
---
<code src="../examples/multiple.jsx"></code>
================================================
FILE: docs/demo/overlay-callback.md
================================================
---
title: overlay-callback
nav:
title: Demo
path: /demo
---
<code src="../examples/overlay-callback.jsx"></code>
================================================
FILE: docs/demo/simple.md
================================================
---
title: simple
nav:
title: Demo
path: /demo
---
<code src="../examples/simple.jsx"></code>
================================================
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 = (
<Menu onSelect={onSelect}>
<MenuItem disabled>disabled</MenuItem>
<MenuItem key="1">one</MenuItem>
<Divider />
<MenuItem key="2">two</MenuItem>
</Menu>
);
export default function Arrow() {
return (
<div style={{ margin: 20 }}>
<div style={{ height: 100 }} />
<div>
<Dropdown
arrow
trigger={['click']}
overlay={menu}
animation="slide-up"
onVisibleChange={onVisibleChange}
>
<button style={{ width: 100 }}>open</button>
</Dropdown>
</div>
<div>
<Dropdown
placement="topLeft"
arrow
trigger={['click']}
overlay={menu}
animation="slide-up"
onVisibleChange={onVisibleChange}
>
<button style={{ width: 100 }}>open</button>
</Dropdown>
</div>
</div>
);
}
================================================
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 = (
<Menu style={{ width: 140 }}>
<MenuItem key="1">one</MenuItem>
<MenuItem key="2">two</MenuItem>
</Menu>
);
return (
<Dropdown
trigger={['contextMenu']}
overlay={menu}
animation="slide-up"
alignPoint
>
<div
role="button"
style={{
border: '1px solid #000',
padding: '100px 0',
textAlign: 'center',
}}
>
Right click me!
</div>
</Dropdown>
);
}
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 = [
<MenuItem key="1">1st item</MenuItem>,
<MenuItem key="2">2nd item</MenuItem>,
];
if (this.state.longList) {
menuItems.push(<MenuItem key="3">3rd LONG SUPER LONG item</MenuItem>);
}
const menu = <Menu>{menuItems}</Menu>;
return (
<div>
<Dropdown overlay={menu}>
<button>Actions</button>
</Dropdown>
<button onClick={this.long}>Long List</button>
<button onClick={this.short}>Short List</button>
</div>
);
}
}
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 = (
<Menu
style={{ width: 140 }}
multiple
onSelect={this.saveSelected}
onDeselect={this.saveSelected}
>
<MenuItem key="1">one</MenuItem>
<MenuItem key="2">two</MenuItem>
<Divider />
<MenuItem disabled>
<button
style={{
cursor: 'pointer',
color: '#000',
pointerEvents: 'visible',
}}
onClick={this.confirm}
>
确定
</button>
</MenuItem>
</Menu>
);
return (
<Dropdown
trigger={['click']}
onVisibleChange={this.onVisibleChange}
visible={this.state.visible}
closeOnSelect={false}
overlay={menu}
animation="slide-up"
>
<button>open</button>
</Dropdown>
);
}
}
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 = () => (
<Menu onSelect={onSelect}>
<MenuItem disabled>disabled</MenuItem>
<MenuItem key="1">one</MenuItem>
<Divider />
<MenuItem key="2">two</MenuItem>
</Menu>
);
export default function OverlayCallback() {
return (
<div style={{ margin: 20 }}>
<div style={{ height: 100 }} />
<div>
<Dropdown
trigger={['click']}
overlay={menuCallback}
animation="slide-up"
onVisibleChange={onVisibleChange}
>
<button style={{ width: 100 }}>open</button>
</Dropdown>
</div>
</div>
);
}
================================================
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 = (
<Menu onSelect={onSelect}>
<MenuItem disabled>disabled</MenuItem>
<MenuItem key="1">one</MenuItem>
<Divider />
<MenuItem key="2">two</MenuItem>
</Menu>
);
export default function Simple() {
return (
<div style={{ margin: 20 }}>
<div style={{ height: 100 }} />
<div>
<Dropdown
autoFocus
trigger={['click']}
overlay={menu}
animation="slide-up"
onVisibleChange={onVisibleChange}
>
<button style={{ width: 100 }}>open</button>
</Dropdown>
</div>
</div>
);
}
================================================
FILE: docs/index.md
================================================
---
hero:
title: rc-dropdown
description: React Dropdown Component
---
<embed src="../README.md"></embed>
================================================
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
---
<code src="../examples/${name}.jsx"></code>
`,
'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<TriggerRef, DropdownProps>((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<boolean>();
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 = () => (
<Overlay
ref={overlayRef}
overlay={overlay}
prefixCls={prefixCls}
arrow={arrow}
/>
);
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 (
<Trigger
builtinPlacements={placements}
{...otherProps}
prefixCls={prefixCls}
ref={triggerRef}
popupClassName={clsx(overlayClassName, {
[`${prefixCls}-show-arrow`]: arrow,
})}
popupStyle={overlayStyle}
action={trigger}
showAction={showAction}
hideAction={triggerHideAction}
popupPlacement={placement}
popupAlign={align}
popupMotion={{ motionName: mergedMotionName }}
popupVisible={mergedVisible}
stretch={getMinOverlayWidthMatchTrigger() ? 'minWidth' : ''}
popup={getMenuElementOrLambda()}
onOpenChange={handleVisibleChange}
onPopupClick={onClick}
getPopupContainer={getPopupContainer}
>
{childrenNode}
</Trigger>
);
});
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<HTMLElement, OverlayProps>((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 && <div className={`${prefixCls}-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<any>;
onVisibleChange?: (visible: boolean) => void;
autoFocus?: boolean;
overlayRef?: React.RefObject<any>;
}
export default function useAccessibility({
visible,
triggerRef,
onVisibleChange,
autoFocus,
overlayRef,
}: UseAccessibilityProps) {
const focusMenuRef = React.useRef<boolean>(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`] = `
<div>
<button
class="my-button rc-dropdown-open"
>
open
</button>
<div
class="rc-dropdown rc-dropdown-placement-bottomLeft"
style="--arrow-x: 0px; --arrow-y: 0px; left: -1000vw; top: -1000vh; box-sizing: border-box; min-width: 100px;"
>
<ul
class="rc-menu rc-menu-root rc-menu-vertical"
data-menu-list="true"
role="menu"
style="width: 140px;"
tabindex="0"
>
<li
class="rc-menu-item"
data-menu-id="rc-menu-uuid-1"
role="menuitem"
tabindex="-1"
>
<span
class="my-menuitem"
>
one
</span>
</li>
<li
class="rc-menu-item-divider"
role="separator"
/>
<li
class="rc-menu-item"
data-menu-id="rc-menu-uuid-2"
role="menuitem"
tabindex="-1"
>
two
</li>
</ul>
<div
aria-hidden="true"
style="display: none;"
/>
</div>
</div>
`;
================================================
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(
<Dropdown overlay={<div className="check-for-visible">Test</div>} visible>
<button className="my-button">open</button>
</Dropdown>,
);
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(
<Dropdown
overlay={<div className="check-for-visible">Test</div>}
visible
trigger={['click']}
onVisibleChange={onVisibleChange}
>
<button className="my-button">open</button>
</Dropdown>,
);
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 = (
<Menu style={{ width: 140 }} onClick={onClick}>
<MenuItem key="1">
<span className="my-menuitem">one</span>
</MenuItem>
<Divider />
<MenuItem key="2">two</MenuItem>
</Menu>
);
const { container, baseElement } = render(
<Dropdown
trigger={['click']}
overlay={menu}
onOverlayClick={onOverlayClick}
>
<button className="my-button">open</button>
</Dropdown>,
);
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 = (
<Menu>
<MenuItem key="1">one</MenuItem>
</Menu>
);
const { container } = render(
<Dropdown
trigger={['click']}
placement="bottomRight"
overlay={menu}
onPopupAlign={onPopupAlign}
>
<button className="my-btn" style={buttonStyle}>
open
</button>
</Dropdown>,
);
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 = <div style={{ width: overlayWidth }}>Test</div>;
const { container, baseElement } = render(
<Dropdown trigger={['click']} overlay={overlay} visible>
<button style={{ width: 100 }} className="my-button">
open
</button>
</Dropdown>,
);
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 = <div style={{ width: overlayWidth }}>Test</div>;
const { container, baseElement } = render(
<Dropdown
trigger={['click']}
overlay={overlay}
minOverlayWidthMatchTrigger={false}
visible
>
<button style={{ width: 100 }} className="my-button">
open
</button>
</Dropdown>,
);
await triggerResize(container.querySelector('button'));
expect(baseElement.querySelector('.rc-dropdown')).not.toHaveStyle({
minWidth: '100px',
});
jest.useRealTimers();
});
it('should support default openClassName', () => {
const overlay = <div style={{ width: 50 }}>Test</div>;
const { container } = render(
<Dropdown
trigger={['click']}
overlay={overlay}
minOverlayWidthMatchTrigger={false}
>
<button style={{ width: 100 }} className="my-button">
open
</button>
</Dropdown>,
);
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 = <div style={{ width: 50 }}>Test</div>;
const { container } = render(
<Dropdown
trigger={['click']}
overlay={overlay}
minOverlayWidthMatchTrigger={false}
openClassName="opened"
>
<button style={{ width: 100 }} className="my-button">
open
</button>
</Dropdown>,
);
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 = <div style={{ width: 50 }}>Test</div>;
const { container, baseElement } = render(
<Dropdown trigger={['click']} overlay={() => overlay}>
<button className="my-button">open</button>
</Dropdown>,
);
fireEvent.click(container.querySelector('.my-button'));
expect(
baseElement
.querySelector('.rc-dropdown')
.classList.contains('rc-dropdown-hidden'),
).toBeFalsy();
});
it('should support arrow', async () => {
const overlay = <div style={{ width: 50 }}>Test</div>;
const { container, baseElement } = render(
<Dropdown arrow overlay={overlay} trigger={['click']}>
<button style={{ width: 100 }} className="my-button">
open
</button>
</Dropdown>,
);
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 = (
<Menu>
<MenuItem key="1">
<span className="my-menuitem">one</span>
</MenuItem>
<MenuItem key="2">two</MenuItem>
</Menu>
);
const { container, baseElement } = render(
<Dropdown trigger={['click']} overlay={overlay}>
<button className="my-button">open</button>
</Dropdown>,
);
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 = () => <div>test</div>;
const { container, baseElement } = render(
<Dropdown trigger={['click']} overlay={<Overlay />}>
<button className="my-button">open</button>
</Dropdown>,
);
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 = (
<div>
<Menu>
<MenuItem key="1">
<span className="my-menuitem">one</span>
</MenuItem>
<MenuItem key="2">two</MenuItem>
</Menu>
</div>
);
const { container, baseElement } = render(
<Dropdown trigger={['click']} overlay={overlay}>
<button className="my-button">open</button>
</Dropdown>,
);
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: (
<Menu expandIcon={<span id="customExpandIcon" />}>
<Menu.Item key="1">foo</Menu.Item>
<Menu.SubMenu title="SubMenu">
<Menu.Item key="1">foo</Menu.Item>
</Menu.SubMenu>
</Menu>
),
visible: true,
getPopupContainer: (node) => node,
};
const { container } = render(
<Dropdown {...props}>
<button type="button">button</button>
</Dropdown>,
);
await sleep(500);
expect(container.querySelector('#customExpandIcon')).toBeTruthy();
});
it('should support customized menuRef', async () => {
const menuRef = createRef<MenuRef>();
const props = {
overlay: (
<Menu ref={menuRef}>
<Menu.Item key="1">foo</Menu.Item>
</Menu>
),
visible: true,
};
render(
<Dropdown {...props}>
<button type="button">button</button>
</Dropdown>,
);
await sleep(500);
expect(menuRef.current).toBeTruthy();
});
it('should support trigger when child provide nativeElement', async () => {
jest.useFakeTimers();
const Button = forwardRef<any, HTMLAttributes<HTMLButtonElement>>(
(props, ref) => {
const btnRef = createRef<HTMLButtonElement>();
useImperativeHandle(ref, () => ({
foo: () => {},
nativeElement: btnRef.current,
}));
return (
<button
ref={btnRef}
onClick={(e) => {
props?.onClick?.(e);
}}
>
trigger
</button>
);
},
);
const { container, baseElement } = render(
<Dropdown
trigger={['click']}
getPopupContainer={(node) => node}
overlay={
<Menu>
<Menu.Item key="1">foo</Menu.Item>
</Menu>
}
>
<Button />
</Dropdown>,
);
fireEvent.click(container.querySelector('button'));
fireEvent.click(baseElement.querySelectorAll('li')[0]);
jest.runAllTimers();
jest.useRealTimers();
});
it('should support autoFocus', async () => {
jest.useFakeTimers();
const overlay = (
<Menu>
<MenuItem key="1">
<span className="my-menuitem">one</span>
</MenuItem>
<MenuItem key="2">two</MenuItem>
</Menu>
);
const { container } = render(
<Dropdown autoFocus trigger={['click']} overlay={overlay}>
<button className="my-button">open</button>
</Dropdown>,
);
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 = () => <div>test</div>;
render(
<Dropdown overlay={<div>test</div>}>
<Component />
</Dropdown>,
);
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 = (
<div
className="check-for-visible"
style={{
width: 10,
}}
>
Test
</div>
);
const onPopupAlign = jest.fn();
const { container } = render(
<Dropdown
onPopupAlign={onPopupAlign}
trigger={['contextMenu']}
overlay={overlay}
alignPoint
align={{
points: ['tl'],
overflow: {},
}}
>
<button className="my-button">open</button>
</Dropdown>,
);
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"
]
}
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
SYMBOL INDEX (24 symbols across 12 files)
FILE: docs/examples/arrow.jsx
function onSelect (line 6) | function onSelect({ key }) {
function onVisibleChange (line 10) | function onVisibleChange(visible) {
function Arrow (line 23) | function Arrow() {
FILE: docs/examples/context-menu.jsx
function ContextMenu (line 6) | function ContextMenu() {
FILE: docs/examples/dropdown-menu-width.jsx
class Example (line 6) | class Example extends PureComponent {
method render (line 17) | render() {
FILE: docs/examples/multiple.jsx
class Test (line 6) | class Test extends Component {
method render (line 31) | render() {
FILE: docs/examples/overlay-callback.jsx
function onSelect (line 6) | function onSelect({ key }) {
function onVisibleChange (line 10) | function onVisibleChange(visible) {
function OverlayCallback (line 23) | function OverlayCallback() {
FILE: docs/examples/simple.jsx
function onSelect (line 7) | function onSelect({ key }) {
function onVisibleChange (line 11) | function onVisibleChange(visible) {
function Simple (line 24) | function Simple() {
FILE: src/Dropdown.tsx
type DropdownProps (line 16) | interface DropdownProps
FILE: src/Overlay.tsx
type OverlayProps (line 5) | type OverlayProps = Pick<
FILE: src/hooks/useAccessibility.ts
type UseAccessibilityProps (line 7) | interface UseAccessibilityProps {
function useAccessibility (line 15) | function useAccessibility({
FILE: tests/basic.test.tsx
function waitForTime (line 13) | async function waitForTime() {
function triggerResize (line 21) | async function triggerResize(target: Element) {
function onClick (line 105) | function onClick({ key }) {
FILE: tests/point.test.tsx
function waitForTime (line 10) | async function waitForTime() {
FILE: tests/utils.js
function sleep (line 6) | async function sleep(timeout = 0) {
function customRender (line 14) | function customRender(ui, options) {
Condensed preview — 43 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (61K chars).
[
{
"path": ".dumirc.ts",
"chars": 455,
"preview": "// more config: https://d.umijs.org/config\nimport { defineConfig } from 'dumi';\n\nexport default defineConfig({\n favicon"
},
{
"path": ".editorconfig",
"chars": 192,
"preview": "# top-most EditorConfig file\nroot = true\n\n# Unix-style newlines with a newline ending every file\n[*.{js,css}]\nend_of_lin"
},
{
"path": ".eslintrc.js",
"chars": 403,
"preview": "const base = require('@umijs/fabric/dist/eslint');\n\nmodule.exports = {\n ...base,\n rules: {\n ...base.rules,\n 'no-"
},
{
"path": ".fatherrc.ts",
"chars": 118,
"preview": "import { defineConfig } from \"father\";\n\nexport default defineConfig({\n plugins: [\"@rc-component/father-plugin\"],\n});\n"
},
{
"path": ".github/dependabot.yml",
"chars": 522,
"preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
},
{
"path": ".github/workflows/ci.yml",
"chars": 139,
"preview": "name: CI\n\non: ['push', 'pull_request']\n\njobs:\n CI:\n uses: react-component/rc-test/.github/workflows/test.yml@main\n "
},
{
"path": ".github/workflows/codeql.yml",
"chars": 841,
"preview": "name: \"CodeQL\"\n\non:\n push:\n branches: [ \"master\" ]\n pull_request:\n branches: [ \"master\" ]\n schedule:\n - cron"
},
{
"path": ".gitignore",
"chars": 303,
"preview": "*.iml\n*.log\n.idea/\n.ipr\n.iws\n*~\n~*\n*.diff\n*.patch\n*.bak\n.DS_Store\nThumbs.db\n.project\n.*proj\n.svn/\n*.swp\n*.swo\n*.pyc\n*.py"
},
{
"path": ".husky/pre-commit",
"chars": 69,
"preview": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpx lint-staged\n"
},
{
"path": ".prettierrc",
"chars": 130,
"preview": "{\n \"endOfLine\": \"lf\",\n \"semi\": true,\n \"singleQuote\": true,\n \"tabWidth\": 2,\n \"trailingComma\": \"all\",\n \"proseWrap\": "
},
{
"path": ".travis.yml",
"chars": 259,
"preview": "language: node_js\n\nnode_js:\n - 10\n\nscript:\n - |\n if [ \"$TEST_TYPE\" = test ]; then\n npm run coverage && \\\n "
},
{
"path": "HISTORY.md",
"chars": 503,
"preview": "# History\n----\n\n## 2.4.0 / 2018-12-28\n\n- `overlay` support function render\n\n## 2.3.0 / 2018-12-21\n\n- add `openClassName`"
},
{
"path": "LICENSE",
"chars": 1115,
"preview": "The MIT License (MIT)\nCopyright (c) 2015-present Alipay.com, https://www.alipay.com/\n\nPermission is hereby granted, free"
},
{
"path": "README.md",
"chars": 5335,
"preview": "# @rc-component/dropdown\n\nreact dropdown component.\n\n[![NPM version][npm-image]][npm-url] [![npm download][download-imag"
},
{
"path": "assets/index.less",
"chars": 6897,
"preview": "@dropdownPrefixCls: rc-dropdown;\n\n@dropdown-arrow-width: 8px;\n@dropdown-distance: @dropdown-arrow-width + 4;\n@dropdown-a"
},
{
"path": "docs/demo/arrow.md",
"chars": 97,
"preview": "---\ntitle: arrow\nnav:\n title: Demo\n path: /demo\n---\n\n<code src=\"../examples/arrow.jsx\"></code>\n"
},
{
"path": "docs/demo/context-menu.md",
"chars": 111,
"preview": "---\ntitle: context-menu\nnav:\n title: Demo\n path: /demo\n---\n\n<code src=\"../examples/context-menu.jsx\"></code>\n"
},
{
"path": "docs/demo/dropdown-menu-width.md",
"chars": 125,
"preview": "---\ntitle: dropdown-menu-width\nnav:\n title: Demo\n path: /demo\n---\n\n<code src=\"../examples/dropdown-menu-width.jsx\"></c"
},
{
"path": "docs/demo/multiple.md",
"chars": 103,
"preview": "---\ntitle: multiple\nnav:\n title: Demo\n path: /demo\n---\n\n<code src=\"../examples/multiple.jsx\"></code>\n"
},
{
"path": "docs/demo/overlay-callback.md",
"chars": 119,
"preview": "---\ntitle: overlay-callback\nnav:\n title: Demo\n path: /demo\n---\n\n<code src=\"../examples/overlay-callback.jsx\"></code>\n"
},
{
"path": "docs/demo/simple.md",
"chars": 99,
"preview": "---\ntitle: simple\nnav:\n title: Demo\n path: /demo\n---\n\n<code src=\"../examples/simple.jsx\"></code>\n"
},
{
"path": "docs/examples/arrow.jsx",
"chars": 1211,
"preview": "import Dropdown from '@rc-component/dropdown';\nimport Menu, { Divider, Item as MenuItem } from '@rc-component/menu';\nimp"
},
{
"path": "docs/examples/context-menu.jsx",
"chars": 725,
"preview": "import Dropdown from '@rc-component/dropdown';\nimport Menu, { Item as MenuItem } from '@rc-component/menu';\nimport React"
},
{
"path": "docs/examples/dropdown-menu-width.jsx",
"chars": 954,
"preview": "import Dropdown from '@rc-component/dropdown';\nimport Menu, { Item as MenuItem } from '@rc-component/menu';\nimport React"
},
{
"path": "docs/examples/multiple.jsx",
"chars": 1491,
"preview": "import Dropdown from '@rc-component/dropdown';\nimport Menu, { Divider, Item as MenuItem } from '@rc-component/menu';\nimp"
},
{
"path": "docs/examples/overlay-callback.jsx",
"chars": 923,
"preview": "import Dropdown from '@rc-component/dropdown';\nimport Menu, { Divider, Item as MenuItem } from '@rc-component/menu';\nimp"
},
{
"path": "docs/examples/simple.jsx",
"chars": 966,
"preview": "/* eslint-disable no-console,react/button-has-type */\nimport Dropdown from '@rc-component/dropdown';\nimport Menu, { Divi"
},
{
"path": "docs/index.md",
"chars": 111,
"preview": "---\nhero:\n title: rc-dropdown\n description: React Dropdown Component\n---\n\n<embed src=\"../README.md\"></embed>\n"
},
{
"path": "index.js",
"chars": 50,
"preview": "'use strict';\n\nmodule.exports = require('./src');\n"
},
{
"path": "now.json",
"chars": 177,
"preview": "{\n \"version\": 2,\n \"name\": \"rc-dropdown\",\n \"builds\": [\n {\n \"src\": \"package.json\",\n \"use\": \"@now/static-bu"
},
{
"path": "package.json",
"chars": 2134,
"preview": "{\n \"name\": \"@rc-component/dropdown\",\n \"version\": \"1.0.2\",\n \"description\": \"dropdown ui component for react\",\n \"keywo"
},
{
"path": "script/update-content.js",
"chars": 580,
"preview": "/*\n 用于 dumi 改造使用,\n 可用于将 examples 的文件批量修改为 demo 引入形式,\n 其他项目根据具体情况使用。\n*/\n\nconst fs = require('fs');\nconst glob = requir"
},
{
"path": "src/Dropdown.tsx",
"chars": 4727,
"preview": "import type { TriggerProps, TriggerRef } from '@rc-component/trigger';\nimport Trigger from '@rc-component/trigger';\nimpo"
},
{
"path": "src/Overlay.tsx",
"chars": 944,
"preview": "import { composeRef, getNodeRef, supportRef } from '@rc-component/util/lib/ref';\nimport React, { forwardRef, useMemo } f"
},
{
"path": "src/hooks/useAccessibility.ts",
"chars": 1801,
"preview": "import KeyCode from '@rc-component/util/lib/KeyCode';\nimport raf from '@rc-component/util/lib/raf';\nimport * as React fr"
},
{
"path": "src/index.tsx",
"chars": 217,
"preview": "export type { TriggerProps } from '@rc-component/trigger';\nexport type { DropdownProps } from './Dropdown';\nexport type "
},
{
"path": "src/placements.ts",
"chars": 845,
"preview": "const autoAdjustOverflow = {\n adjustX: 1,\n adjustY: 1,\n};\n\nconst targetOffset = [0, 0];\n\nconst placements = {\n topLef"
},
{
"path": "tests/__mocks__/@rc-component/trigger.tsx",
"chars": 79,
"preview": "import Trigger from '@rc-component/trigger/lib/mock';\n\nexport default Trigger;\n"
},
{
"path": "tests/__snapshots__/basic.test.tsx.snap",
"chars": 1067,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`dropdown simply works 1`] = `\n<div>\n <button\n class=\"my-button "
},
{
"path": "tests/basic.test.tsx",
"chars": 16902,
"preview": "/* eslint-disable react/button-has-type,react/no-find-dom-node,react/no-render-return-value,object-shorthand,func-names,"
},
{
"path": "tests/point.test.tsx",
"chars": 1301,
"preview": "/* eslint-disable react/button-has-type,react/no-render-return-value */\nimport { act, fireEvent } from '@testing-library"
},
{
"path": "tests/utils.js",
"chars": 428,
"preview": "import { StrictMode } from 'react';\nimport { render, act } from '@testing-library/react';\n\nconst globalTimeout = global."
},
{
"path": "tsconfig.json",
"chars": 576,
"preview": "{\n \"compilerOptions\": {\n \"allowSyntheticDefaultImports\": true,\n \"baseUrl\": \"./\",\n \"declaration\": true,\n \"mo"
}
]
About this extraction
This page contains the full source code of the react-component/dropdown GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 43 files (54.8 KB), approximately 15.7k tokens, and a symbol index with 24 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.