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
| 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 |
boolean |
true (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 = (
);
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 = (
);
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 = [
,
,
];
if (this.state.longList) {
menuItems.push();
}
const menu = ;
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 = (
);
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 = () => (
);
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 = (
);
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 = (
);
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 = (
);
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 = (
);
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 = (
);
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: (
),
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={
}
>
,
);
fireEvent.click(container.querySelector('button'));
fireEvent.click(baseElement.querySelectorAll('li')[0]);
jest.runAllTimers();
jest.useRealTimers();
});
it('should support autoFocus', async () => {
jest.useFakeTimers();
const overlay = (
);
const { container } = render(
,
);
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"
]
}