├── .dumirc.ts ├── .editorconfig ├── .eslintrc.js ├── .fatherrc.ts ├── .github ├── dependabot.yml └── workflows │ ├── codeql.yml │ └── main.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── assets └── index.less ├── docs ├── demo │ ├── basic.md │ ├── blink.md │ ├── fill-width.md │ └── raw-render.md └── index.md ├── examples ├── basic.tsx ├── blink.tsx ├── common.less ├── fill-width.tsx └── raw-render.tsx ├── index.js ├── jest.config.js ├── now.json ├── package.json ├── script └── update-content.js ├── src ├── Item.tsx ├── Overflow.tsx ├── RawItem.tsx ├── context.ts ├── hooks │ ├── channelUpdate.ts │ └── useEffectState.tsx └── index.tsx ├── tests ├── __snapshots__ │ ├── index.spec.tsx.snap │ ├── invalidate.spec.tsx.snap │ ├── raw.spec.tsx.snap │ └── ssr.spec.tsx.snap ├── github.spec.tsx ├── index.spec.tsx ├── invalidate.spec.tsx ├── raw.spec.tsx ├── responsive.spec.tsx ├── setup.js ├── ssr.spec.tsx └── wrapper.ts └── tsconfig.json /.dumirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi'; 2 | const isGitPagesSite = process.env.GITHUB_ACTIONS; 3 | 4 | export default defineConfig({ 5 | favicons: ['https://avatars0.githubusercontent.com/u/9441414?s=200&v=4'], 6 | themeConfig: { 7 | name: 'rc-overflow', 8 | logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', 9 | }, 10 | exportStatic: {}, 11 | outputPath: 'docs-dist', 12 | base: isGitPagesSite ? `/rc-overflow/` : `/`, 13 | publicPath: isGitPagesSite ? `/rc-overflow/` : `/`, 14 | styles: [ 15 | ` 16 | .markdown table { 17 | width: auto !important; 18 | } 19 | `, 20 | ], 21 | }); 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*.{js,css,md}] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const base = require('@umijs/fabric/dist/eslint'); 2 | 3 | module.exports = { 4 | ...base, 5 | rules: { 6 | ...base.rules, 7 | 'no-template-curly-in-string': 0, 8 | 'prefer-promise-reject-errors': 0, 9 | 'react/no-array-index-key': 0, 10 | 'react/sort-comp': 0, 11 | '@typescript-eslint/no-explicit-any': 0, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | plugins: ['@rc-component/father-plugin'], 5 | }); 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "53 19 * * 0" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | setup: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@master 15 | 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: '18' 19 | 20 | - name: cache package-lock.json 21 | uses: actions/cache@v2 22 | with: 23 | path: package-temp-dir 24 | key: lock-${{ github.sha }} 25 | 26 | - name: create package-lock.json 27 | run: npm i --package-lock-only --ignore-scripts 28 | 29 | - name: hack for singe file 30 | run: | 31 | if [ ! -d "package-temp-dir" ]; then 32 | mkdir package-temp-dir 33 | fi 34 | cp package-lock.json package-temp-dir 35 | 36 | - name: cache node_modules 37 | id: node_modules_cache_id 38 | uses: actions/cache@v2 39 | with: 40 | path: node_modules 41 | key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} 42 | 43 | - name: install 44 | if: steps.node_modules_cache_id.outputs.cache-hit != 'true' 45 | run: npm ci 46 | 47 | lint: 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: checkout 51 | uses: actions/checkout@master 52 | 53 | - name: restore cache from package-lock.json 54 | uses: actions/cache@v2 55 | with: 56 | path: package-temp-dir 57 | key: lock-${{ github.sha }} 58 | 59 | - name: restore cache from node_modules 60 | uses: actions/cache@v2 61 | with: 62 | path: node_modules 63 | key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} 64 | 65 | - name: lint 66 | run: npm run lint 67 | 68 | needs: setup 69 | 70 | compile: 71 | runs-on: ubuntu-latest 72 | steps: 73 | - name: checkout 74 | uses: actions/checkout@master 75 | 76 | - name: restore cache from package-lock.json 77 | uses: actions/cache@v2 78 | with: 79 | path: package-temp-dir 80 | key: lock-${{ github.sha }} 81 | 82 | - name: restore cache from node_modules 83 | uses: actions/cache@v2 84 | with: 85 | path: node_modules 86 | key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} 87 | 88 | - name: compile 89 | run: npm run compile 90 | 91 | needs: setup 92 | 93 | coverage: 94 | runs-on: ubuntu-latest 95 | steps: 96 | - name: checkout 97 | uses: actions/checkout@master 98 | 99 | - name: restore cache from package-lock.json 100 | uses: actions/cache@v2 101 | with: 102 | path: package-temp-dir 103 | key: lock-${{ github.sha }} 104 | 105 | - name: restore cache from node_modules 106 | uses: actions/cache@v2 107 | with: 108 | path: node_modules 109 | key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} 110 | 111 | - name: coverage 112 | run: npm test -- --coverage && bash <(curl -s https://codecov.io/bash) 113 | 114 | needs: setup 115 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.log 3 | .idea/ 4 | .ipr 5 | .iws 6 | *~ 7 | ~* 8 | *.diff 9 | *.patch 10 | *.bak 11 | .DS_Store 12 | Thumbs.db 13 | .project 14 | .*proj 15 | .svn/ 16 | *.swp 17 | *.swo 18 | *.pyc 19 | *.pyo 20 | .build 21 | node_modules 22 | .cache 23 | assets/**/*.css 24 | build 25 | lib 26 | es 27 | yarn.lock 28 | package-lock.json 29 | coverage/ 30 | .doc 31 | dist/ 32 | 33 | # dumi 34 | .dumi/tmp 35 | .dumi/tmp-test 36 | .dumi/tmp-production 37 | .env.local 38 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.cfg 3 | nohup.out 4 | *.iml 5 | .idea/ 6 | .ipr 7 | .iws 8 | *~ 9 | ~* 10 | *.diff 11 | *.log 12 | *.patch 13 | *.bak 14 | .DS_Store 15 | Thumbs.db 16 | .project 17 | .*proj 18 | .svn/ 19 | *.swp 20 | out/ 21 | .build 22 | node_modules 23 | .cache 24 | examples 25 | tests 26 | src 27 | /index.js 28 | .* 29 | assets/**/*.less -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .storybook 2 | node_modules 3 | lib 4 | es 5 | .cache 6 | package.json 7 | package-lock.json 8 | public 9 | .site 10 | _site 11 | .umi 12 | .doc 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-present afc163 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rc-overflow 🐾 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![npm download][download-image]][download-url] 5 | [![build status][github-actions-image]][github-actions-url] 6 | [![Codecov][codecov-image]][codecov-url] 7 | [![bundle size][bundlephobia-image]][bundlephobia-url] 8 | [![dumi][dumi-image]][dumi-url] 9 | 10 | [npm-image]: http://img.shields.io/npm/v/rc-overflow.svg?style=flat-square 11 | [npm-url]: http://npmjs.org/package/rc-overflow 12 | [github-actions-image]: https://github.com/react-component/overflow/workflows/CI/badge.svg 13 | [github-actions-url]: https://github.com/react-component/overflow/actions 14 | [codecov-image]: https://img.shields.io/codecov/c/github/react-component/overflow/master.svg?style=flat-square 15 | [codecov-url]: https://codecov.io/gh/react-component/overflow/branch/master 16 | [david-url]: https://david-dm.org/react-component/overflow 17 | [david-image]: https://david-dm.org/react-component/overflow/status.svg?style=flat-square 18 | [david-dev-url]: https://david-dm.org/react-component/overflow?type=dev 19 | [david-dev-image]: https://david-dm.org/react-component/overflow/dev-status.svg?style=flat-square 20 | [download-image]: https://img.shields.io/npm/dm/rc-overflow.svg?style=flat-square 21 | [download-url]: https://npmjs.org/package/rc-overflow 22 | [bundlephobia-url]: https://bundlephobia.com/result?p=rc-overflow 23 | [bundlephobia-image]: https://badgen.net/bundlephobia/minzip/rc-overflow 24 | [dumi-url]: https://github.com/umijs/dumi 25 | [dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square 26 | 27 | Auto collapse box when overflow 28 | 29 | ## Live Demo 30 | 31 | https://overflow-react-component.vercel.app/ 32 | 33 | ## Install 34 | 35 | [![rc-overflow](https://nodei.co/npm/rc-overflow.png)](https://npmjs.org/package/rc-overflow) 36 | 37 | ## Usage 38 | 39 | ```ts 40 | // TODO 41 | ``` 42 | 43 | ## API 44 | 45 | | Property | Type | Default | Description | 46 | | -------- | ---- | ------- | ----------- | 47 | 48 | ## Development 49 | 50 | ``` 51 | npm install 52 | npm start 53 | ``` 54 | 55 | ## License 56 | 57 | rc-overflow is released under the MIT license. 58 | -------------------------------------------------------------------------------- /assets/index.less: -------------------------------------------------------------------------------- 1 | @overflow-prefix-cls: rc-overflow; 2 | 3 | .@{overflow-prefix-cls} { 4 | display: flex; 5 | flex-wrap: wrap; 6 | max-width: 100%; 7 | position: relative; 8 | 9 | &-item { 10 | background: rgba(0, 255, 0, 0.2); 11 | box-shadow: 0 0 1px black; 12 | flex: none; 13 | max-width: 100%; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docs/demo/basic.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: basic 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/blink.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blink 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/fill-width.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: fill-width 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/raw-render.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: raw-render 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hero: 3 | title: rc-overflow 4 | description: React Overflow Component 5 | --- 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/basic.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Overflow from '../src'; 3 | import '../assets/index.less'; 4 | import './common.less'; 5 | 6 | interface ItemType { 7 | value: string | number; 8 | label: string; 9 | } 10 | 11 | function createData(count: number): ItemType[] { 12 | const data: ItemType[] = new Array(count).fill(undefined).map((_, index) => ({ 13 | value: index, 14 | label: `Label ${index}`, 15 | })); 16 | 17 | return data; 18 | } 19 | 20 | function renderItem(item: ItemType) { 21 | return ( 22 |
29 | {item.label} 30 |
31 | ); 32 | } 33 | 34 | function renderRest(items: ItemType[]) { 35 | return ( 36 |
43 | +{items.length}... 44 |
45 | ); 46 | } 47 | 48 | const Demo = () => { 49 | const [responsive, setResponsive] = React.useState(true); 50 | const [data, setData] = React.useState(createData(1)); 51 | 52 | return ( 53 |
54 | 62 | 78 | 79 | 86 | 87 |
96 | 97 | data={data} 98 | renderItem={renderItem} 99 | renderRest={renderRest} 100 | maxCount={responsive ? 'responsive' : 6} 101 | // suffix={1} 102 | /> 103 |
104 |
105 | ); 106 | }; 107 | 108 | export default Demo; 109 | -------------------------------------------------------------------------------- /examples/blink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Overflow from '../src'; 3 | import '../assets/index.less'; 4 | import './common.less'; 5 | 6 | const overflowSharedStyle: React.CSSProperties = { 7 | background: 'rgba(0, 255, 0, 0.1)', 8 | }; 9 | 10 | interface ItemType { 11 | value: string | number; 12 | label: string; 13 | } 14 | 15 | function createData(count: number): ItemType[] { 16 | const data: ItemType[] = new Array(count).fill(undefined).map((_, index) => ({ 17 | value: index, 18 | label: `Label ${index}`, 19 | })); 20 | 21 | return data; 22 | } 23 | 24 | const sharedStyle: React.CSSProperties = { 25 | padding: '4px 8px', 26 | width: 90, 27 | overflow: 'hidden', 28 | background: 'rgba(255, 0, 0, 0.2)', 29 | }; 30 | 31 | function renderItem(item: ItemType) { 32 | return
{item.label}
; 33 | } 34 | 35 | function renderRest(items: ItemType[]) { 36 | if (items.length === 3) { 37 | return items.length; 38 | } 39 | 40 | return
+{items.length}...
; 41 | } 42 | 43 | const data = createData(5); 44 | const data2 = createData(2); 45 | 46 | const Demo = () => { 47 | return ( 48 |
49 |

50 | Test for a edge case that rest can not decide the final display count 51 |

52 |
59 | 60 | data={data} 61 | style={{ width: 300, ...overflowSharedStyle }} 62 | renderItem={renderItem} 63 | renderRest={renderRest} 64 | maxCount="responsive" 65 | /> 66 | 67 | 68 | data={data2} 69 | style={{ width: 180, ...overflowSharedStyle }} 70 | renderItem={renderItem} 71 | renderRest={renderRest} 72 | maxCount="responsive" 73 | /> 74 |
75 |
76 | ); 77 | }; 78 | 79 | export default Demo; 80 | -------------------------------------------------------------------------------- /examples/common.less: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | -------------------------------------------------------------------------------- /examples/fill-width.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useLayoutEffect from "rc-util/lib/hooks/useLayoutEffect"; 3 | import Overflow from '../src'; 4 | import '../assets/index.less'; 5 | import './common.less'; 6 | 7 | interface ItemType { 8 | value: string | number; 9 | label: string; 10 | } 11 | 12 | function createData(count: number): ItemType[] { 13 | const data: ItemType[] = new Array(count).fill(undefined).map((_, index) => ({ 14 | value: index, 15 | label: `Label ${index}`, 16 | })); 17 | 18 | return data; 19 | } 20 | 21 | function renderItem(item: ItemType) { 22 | return ( 23 |
30 | {item.label} 31 |
32 | ); 33 | } 34 | 35 | function renderRest(items: ItemType[]) { 36 | return ( 37 |
44 | +{items.length}... 45 |
46 | ); 47 | } 48 | 49 | const inputStyle: React.CSSProperties = { 50 | border: 'none', 51 | fontSize: 12, 52 | margin: 0, 53 | outline: 'none', 54 | lineHeight: '20px', 55 | fontFamily: '-apple-system', 56 | padding: '0 4px', 57 | }; 58 | 59 | const Demo = () => { 60 | const [responsive, setResponsive] = React.useState(true); 61 | const [inputValue, setInputValue] = React.useState(''); 62 | const [inputWidth, setInputWidth] = React.useState(0); 63 | const [data, setData] = React.useState(createData(3)); 64 | const inputRef = React.useRef(); 65 | const measureRef = React.useRef(); 66 | 67 | useLayoutEffect(() => { 68 | setInputWidth(measureRef.current.offsetWidth); 69 | }, [inputValue]); 70 | 71 | React.useEffect(() => { 72 | inputRef.current.focus(); 73 | }, []); 74 | 75 | return ( 76 |
77 | 85 | 101 | 102 |
110 | 111 | data={data} 112 | renderItem={renderItem} 113 | renderRest={renderRest} 114 | maxCount={responsive ? 'responsive' : 6} 115 | suffix={ 116 |
117 | { 127 | setInputValue(e.target.value); 128 | }} 129 | ref={inputRef} 130 | /> 131 |
141 | {inputValue} 142 |
143 |
144 | } 145 | /> 146 |
147 |
148 | ); 149 | }; 150 | 151 | export default Demo; 152 | -------------------------------------------------------------------------------- /examples/raw-render.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Overflow from '../src'; 3 | import '../assets/index.less'; 4 | import './common.less'; 5 | 6 | interface ItemType { 7 | value: string | number; 8 | label: string; 9 | } 10 | 11 | function createData(count: number): ItemType[] { 12 | const data: ItemType[] = new Array(count).fill(undefined).map((_, index) => ({ 13 | value: index, 14 | label: `Label ${index}`, 15 | })); 16 | 17 | return data; 18 | } 19 | 20 | function renderRawItem(item: ItemType) { 21 | return ( 22 | 23 |
30 | {item.label} 31 |
32 |
33 | ); 34 | } 35 | 36 | function renderRest(items: ItemType[]) { 37 | return ( 38 |
45 | +{items.length}... 46 |
47 | ); 48 | } 49 | 50 | const Demo = () => { 51 | const [responsive, setResponsive] = React.useState(true); 52 | const [data, setData] = React.useState(createData(1)); 53 | 54 | return ( 55 |
56 | 64 | 80 | 81 |
89 | 90 | data={data} 91 | renderRawItem={renderRawItem} 92 | renderRest={renderRest} 93 | maxCount={responsive ? 'responsive' : 6} 94 | /> 95 |
96 |
97 | ); 98 | }; 99 | 100 | export default Demo; 101 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/'); 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFiles: ['./tests/setup.js'], 3 | snapshotSerializers: [require.resolve('enzyme-to-json/serializer')], 4 | }; 5 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "rc-overflow", 4 | "builds": [ 5 | { 6 | "src": "package.json", 7 | "use": "@vercel/static-build", 8 | "config": { "distDir": "docs-dist" } 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rc-overflow", 3 | "version": "1.4.1", 4 | "description": "Auto collapse box when overflow", 5 | "keywords": [ 6 | "react", 7 | "react-component", 8 | "react-overflow", 9 | "overflow", 10 | "antd", 11 | "ant-design" 12 | ], 13 | "main": "./lib/index", 14 | "module": "./es/index", 15 | "files": [ 16 | "assets/*.css", 17 | "assets/*.less", 18 | "es", 19 | "lib", 20 | "dist" 21 | ], 22 | "homepage": "https://react-component.github.io/overflow", 23 | "repository": { 24 | "type": "git", 25 | "url": "git@github.com:react-component/overflow.git" 26 | }, 27 | "bugs": { 28 | "url": "http://github.com/react-component/overflow/issues" 29 | }, 30 | "license": "MIT", 31 | "scripts": { 32 | "start": "dumi dev", 33 | "docs:build": "dumi build", 34 | "docs:deploy": "gh-pages -d docs-dist", 35 | "compile": "father build", 36 | "prepare": "dumi setup", 37 | "deploy": "npm run docs:build && npm run docs:deploy", 38 | "prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"", 39 | "test": "rc-test", 40 | "test:coverage": "rc-test --coverage", 41 | "prepublishOnly": "npm run compile && np --no-cleanup --yolo --no-publish", 42 | "lint": "eslint src/ --ext .tsx,.ts", 43 | "lint:tsc": "tsc -p tsconfig.json --noEmit", 44 | "now-build": "npm run docs:build" 45 | }, 46 | "dependencies": { 47 | "@babel/runtime": "^7.11.1", 48 | "classnames": "^2.2.1", 49 | "rc-resize-observer": "^1.0.0", 50 | "rc-util": "^5.37.0" 51 | }, 52 | "devDependencies": { 53 | "@rc-component/father-plugin": "^1.0.0", 54 | "@testing-library/jest-dom": "^5.16.4", 55 | "@testing-library/react": "^12.0.0", 56 | "@types/classnames": "^2.2.9", 57 | "@types/enzyme": "^3.10.8", 58 | "@types/jest": "^26.0.23", 59 | "@types/react": "^16.14.2", 60 | "@types/react-dom": "^16.9.10", 61 | "@umijs/fabric": "^3.0.0", 62 | "glob": "^10.0.0", 63 | "cross-env": "^7.0.2", 64 | "dumi": "^2.0.0", 65 | "enzyme": "^3.0.0", 66 | "enzyme-adapter-react-16": "^1.0.1", 67 | "enzyme-to-json": "^3.4.0", 68 | "eslint": "^7.0.0", 69 | "father": "^4.0.0", 70 | "less": "^3.10.3", 71 | "np": "^7.0.0", 72 | "prettier": "^2.0.5", 73 | "rc-test": "^7.0", 74 | "react": "^16.0.0", 75 | "react-dom": "^16.0.0", 76 | "regenerator-runtime": "^0.13.7", 77 | "typescript": "^5.0.0" 78 | }, 79 | "peerDependencies": { 80 | "react": ">=16.9.0", 81 | "react-dom": ">=16.9.0" 82 | }, 83 | "overrides": { 84 | "cheerio": "1.0.0-rc.12" 85 | }, 86 | "cnpm": { 87 | "mode": "npm" 88 | }, 89 | "tnpm": { 90 | "mode": "npm" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /script/update-content.js: -------------------------------------------------------------------------------- 1 | /* 2 | 用于 dumi 改造使用, 3 | 可用于将 examples 的文件批量修改为 demo 引入形式, 4 | 其他项目根据具体情况使用。 5 | */ 6 | 7 | const fs = require('fs'); 8 | const glob = require('glob'); 9 | 10 | const paths = glob.sync('./examples/*.tsx'); 11 | 12 | paths.forEach(path => { 13 | const name = path.split('/').pop().split('.')[0]; 14 | fs.writeFile( 15 | `./docs/demo/${name}.md`, 16 | `--- 17 | title: ${name} 18 | nav: 19 | title: Demo 20 | path: /demo 21 | --- 22 | 23 | 24 | `, 25 | 'utf8', 26 | function(error) { 27 | if(error){ 28 | console.log(error); 29 | return false; 30 | } 31 | console.log(`${name} 更新成功~`); 32 | } 33 | ) 34 | }); 35 | -------------------------------------------------------------------------------- /src/Item.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import classNames from 'classnames'; 3 | import ResizeObserver from 'rc-resize-observer'; 4 | import type { ComponentType } from './RawItem'; 5 | 6 | // Use shared variable to save bundle size 7 | const UNDEFINED = undefined; 8 | 9 | export interface ItemProps extends React.HTMLAttributes { 10 | prefixCls: string; 11 | item?: ItemType; 12 | className?: string; 13 | style?: React.CSSProperties; 14 | renderItem?: (item: ItemType, info: { index: number }) => React.ReactNode; 15 | responsive?: boolean; 16 | // https://github.com/ant-design/ant-design/issues/35475 17 | /** 18 | * @private To make node structure stable. We need keep wrap with ResizeObserver. 19 | * But disable it when it's no need to real measure. 20 | */ 21 | responsiveDisabled?: boolean; 22 | itemKey?: React.Key; 23 | registerSize: (key: React.Key, width: number | null) => void; 24 | children?: React.ReactNode; 25 | display: boolean; 26 | order: number; 27 | component?: ComponentType; 28 | invalidate?: boolean; 29 | } 30 | 31 | function InternalItem( 32 | props: ItemProps, 33 | ref: React.Ref, 34 | ) { 35 | const { 36 | prefixCls, 37 | invalidate, 38 | item, 39 | renderItem, 40 | responsive, 41 | responsiveDisabled, 42 | registerSize, 43 | itemKey, 44 | className, 45 | style, 46 | children, 47 | display, 48 | order, 49 | component: Component = 'div', 50 | ...restProps 51 | } = props; 52 | 53 | const mergedHidden = responsive && !display; 54 | 55 | // ================================ Effect ================================ 56 | function internalRegisterSize(width: number | null) { 57 | registerSize(itemKey!, width); 58 | } 59 | 60 | React.useEffect( 61 | () => () => { 62 | internalRegisterSize(null); 63 | }, 64 | [], 65 | ); 66 | 67 | // ================================ Render ================================ 68 | const childNode = 69 | renderItem && item !== UNDEFINED ? renderItem(item, { index: order }) : children; 70 | 71 | let overflowStyle: React.CSSProperties | undefined; 72 | if (!invalidate) { 73 | overflowStyle = { 74 | opacity: mergedHidden ? 0 : 1, 75 | height: mergedHidden ? 0 : UNDEFINED, 76 | overflowY: mergedHidden ? 'hidden' : UNDEFINED, 77 | order: responsive ? order : UNDEFINED, 78 | pointerEvents: mergedHidden ? 'none' : UNDEFINED, 79 | position: mergedHidden ? 'absolute' : UNDEFINED, 80 | }; 81 | } 82 | 83 | const overflowProps: React.HTMLAttributes = {}; 84 | if (mergedHidden) { 85 | overflowProps['aria-hidden'] = true; 86 | } 87 | 88 | let itemNode = ( 89 | 99 | {childNode} 100 | 101 | ); 102 | 103 | if (responsive) { 104 | itemNode = ( 105 | { 107 | internalRegisterSize(offsetWidth); 108 | }} 109 | disabled={responsiveDisabled} 110 | > 111 | {itemNode} 112 | 113 | ); 114 | } 115 | 116 | return itemNode; 117 | } 118 | 119 | const Item = React.forwardRef(InternalItem); 120 | Item.displayName = 'Item'; 121 | 122 | export default Item; 123 | -------------------------------------------------------------------------------- /src/Overflow.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useState, useMemo, useCallback } from 'react'; 3 | import classNames from 'classnames'; 4 | import ResizeObserver from 'rc-resize-observer'; 5 | import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect'; 6 | import Item from './Item'; 7 | import useEffectState, { useBatcher } from './hooks/useEffectState'; 8 | import type { ComponentType } from './RawItem'; 9 | import RawItem from './RawItem'; 10 | import { OverflowContext } from './context'; 11 | 12 | const RESPONSIVE = 'responsive' as const; 13 | const INVALIDATE = 'invalidate' as const; 14 | 15 | export { OverflowContext } from './context'; 16 | 17 | export type { ComponentType } from './RawItem'; 18 | 19 | export interface OverflowProps extends React.HTMLAttributes { 20 | prefixCls?: string; 21 | className?: string; 22 | style?: React.CSSProperties; 23 | data?: ItemType[]; 24 | itemKey?: React.Key | ((item: ItemType) => React.Key); 25 | /** Used for `responsive`. It will limit render node to avoid perf issue */ 26 | itemWidth?: number; 27 | renderItem?: (item: ItemType, info: { index: number }) => React.ReactNode; 28 | /** @private Do not use in your production. Render raw node that need wrap Item by developer self */ 29 | renderRawItem?: (item: ItemType, index: number) => React.ReactElement; 30 | maxCount?: number | typeof RESPONSIVE | typeof INVALIDATE; 31 | renderRest?: 32 | | React.ReactNode 33 | | ((omittedItems: ItemType[]) => React.ReactNode); 34 | /** @private Do not use in your production. Render raw node that need wrap Item by developer self */ 35 | renderRawRest?: (omittedItems: ItemType[]) => React.ReactElement; 36 | suffix?: React.ReactNode; 37 | component?: ComponentType; 38 | itemComponent?: ComponentType; 39 | 40 | /** @private This API may be refactor since not well design */ 41 | onVisibleChange?: (visibleCount: number) => void; 42 | 43 | /** When set to `full`, ssr will render full items by default and remove at client side */ 44 | ssr?: 'full'; 45 | } 46 | 47 | function defaultRenderRest(omittedItems: ItemType[]) { 48 | return `+ ${omittedItems.length} ...`; 49 | } 50 | 51 | function Overflow( 52 | props: OverflowProps, 53 | ref: React.Ref, 54 | ) { 55 | const { 56 | prefixCls = 'rc-overflow', 57 | data = [], 58 | renderItem, 59 | renderRawItem, 60 | itemKey, 61 | itemWidth = 10, 62 | ssr, 63 | style, 64 | className, 65 | maxCount, 66 | renderRest, 67 | renderRawRest, 68 | suffix, 69 | component: Component = 'div', 70 | itemComponent, 71 | onVisibleChange, 72 | ...restProps 73 | } = props; 74 | 75 | const fullySSR = ssr === 'full'; 76 | 77 | const notifyEffectUpdate = useBatcher(); 78 | 79 | const [containerWidth, setContainerWidth] = useEffectState( 80 | notifyEffectUpdate, 81 | null, 82 | ); 83 | const mergedContainerWidth = containerWidth || 0; 84 | 85 | const [itemWidths, setItemWidths] = useEffectState( 86 | notifyEffectUpdate, 87 | new Map(), 88 | ); 89 | 90 | const [prevRestWidth, setPrevRestWidth] = useEffectState( 91 | notifyEffectUpdate, 92 | 0, 93 | ); 94 | const [restWidth, setRestWidth] = useEffectState( 95 | notifyEffectUpdate, 96 | 0, 97 | ); 98 | 99 | const [suffixWidth, setSuffixWidth] = useEffectState( 100 | notifyEffectUpdate, 101 | 0, 102 | ); 103 | const [suffixFixedStart, setSuffixFixedStart] = useState(null); 104 | 105 | const [displayCount, setDisplayCount] = useState(null); 106 | const mergedDisplayCount = React.useMemo(() => { 107 | if (displayCount === null && fullySSR) { 108 | return Number.MAX_SAFE_INTEGER; 109 | } 110 | 111 | return displayCount || 0; 112 | }, [displayCount, containerWidth]); 113 | 114 | const [restReady, setRestReady] = useState(false); 115 | 116 | const itemPrefixCls = `${prefixCls}-item`; 117 | 118 | // Always use the max width to avoid blink 119 | const mergedRestWidth = Math.max(prevRestWidth, restWidth); 120 | 121 | // ================================= Data ================================= 122 | const isResponsive = maxCount === RESPONSIVE; 123 | const shouldResponsive = data.length && isResponsive; 124 | const invalidate = maxCount === INVALIDATE; 125 | 126 | /** 127 | * When is `responsive`, we will always render rest node to get the real width of it for calculation 128 | */ 129 | const showRest = 130 | shouldResponsive || 131 | (typeof maxCount === 'number' && data.length > maxCount); 132 | 133 | const mergedData = useMemo(() => { 134 | let items = data; 135 | 136 | if (shouldResponsive) { 137 | if (containerWidth === null && fullySSR) { 138 | items = data; 139 | } else { 140 | items = data.slice( 141 | 0, 142 | Math.min(data.length, mergedContainerWidth / itemWidth), 143 | ); 144 | } 145 | } else if (typeof maxCount === 'number') { 146 | items = data.slice(0, maxCount); 147 | } 148 | 149 | return items; 150 | }, [data, itemWidth, containerWidth, maxCount, shouldResponsive]); 151 | 152 | const omittedItems = useMemo(() => { 153 | if (shouldResponsive) { 154 | return data.slice(mergedDisplayCount + 1); 155 | } 156 | return data.slice(mergedData.length); 157 | }, [data, mergedData, shouldResponsive, mergedDisplayCount]); 158 | 159 | // ================================= Item ================================= 160 | const getKey = useCallback( 161 | (item: ItemType, index: number) => { 162 | if (typeof itemKey === 'function') { 163 | return itemKey(item); 164 | } 165 | return (itemKey && (item as any)?.[itemKey]) ?? index; 166 | }, 167 | [itemKey], 168 | ); 169 | 170 | const mergedRenderItem = useCallback( 171 | renderItem || ((item: ItemType) => item), 172 | [renderItem], 173 | ); 174 | 175 | function updateDisplayCount( 176 | count: number, 177 | suffixFixedStartVal: number, 178 | notReady?: boolean, 179 | ) { 180 | // React 18 will sync render even when the value is same in some case. 181 | // We take `mergedData` as deps which may cause dead loop if it's dynamic generate. 182 | // ref: https://github.com/ant-design/ant-design/issues/36559 183 | if ( 184 | displayCount === count && 185 | (suffixFixedStartVal === undefined || 186 | suffixFixedStartVal === suffixFixedStart) 187 | ) { 188 | return; 189 | } 190 | 191 | setDisplayCount(count); 192 | if (!notReady) { 193 | setRestReady(count < data.length - 1); 194 | 195 | onVisibleChange?.(count); 196 | } 197 | 198 | if (suffixFixedStartVal !== undefined) { 199 | setSuffixFixedStart(suffixFixedStartVal); 200 | } 201 | } 202 | 203 | // ================================= Size ================================= 204 | function onOverflowResize(_: object, element: HTMLElement) { 205 | setContainerWidth(element.clientWidth); 206 | } 207 | 208 | function registerSize(key: React.Key, width: number | null) { 209 | setItemWidths(origin => { 210 | const clone = new Map(origin); 211 | 212 | if (width === null) { 213 | clone.delete(key); 214 | } else { 215 | clone.set(key, width); 216 | } 217 | return clone; 218 | }); 219 | } 220 | 221 | function registerOverflowSize(_: React.Key, width: number | null) { 222 | setRestWidth(width!); 223 | setPrevRestWidth(restWidth); 224 | } 225 | 226 | function registerSuffixSize(_: React.Key, width: number | null) { 227 | setSuffixWidth(width!); 228 | } 229 | 230 | // ================================ Effect ================================ 231 | function getItemWidth(index: number) { 232 | return itemWidths.get(getKey(mergedData[index], index)); 233 | } 234 | 235 | useLayoutEffect(() => { 236 | if ( 237 | mergedContainerWidth && 238 | typeof mergedRestWidth === 'number' && 239 | mergedData 240 | ) { 241 | let totalWidth = suffixWidth; 242 | 243 | const len = mergedData.length; 244 | const lastIndex = len - 1; 245 | 246 | // When data count change to 0, reset this since not loop will reach 247 | if (!len) { 248 | updateDisplayCount(0, null); 249 | return; 250 | } 251 | 252 | for (let i = 0; i < len; i += 1) { 253 | let currentItemWidth = getItemWidth(i); 254 | 255 | // Fully will always render 256 | if (fullySSR) { 257 | currentItemWidth = currentItemWidth || 0; 258 | } 259 | 260 | // Break since data not ready 261 | if (currentItemWidth === undefined) { 262 | updateDisplayCount(i - 1, undefined, true); 263 | break; 264 | } 265 | 266 | // Find best match 267 | totalWidth += currentItemWidth; 268 | 269 | if ( 270 | // Only one means `totalWidth` is the final width 271 | (lastIndex === 0 && totalWidth <= mergedContainerWidth) || 272 | // Last two width will be the final width 273 | (i === lastIndex - 1 && 274 | totalWidth + getItemWidth(lastIndex)! <= mergedContainerWidth) 275 | ) { 276 | // Additional check if match the end 277 | updateDisplayCount(lastIndex, null); 278 | break; 279 | } else if (totalWidth + mergedRestWidth > mergedContainerWidth) { 280 | // Can not hold all the content to show rest 281 | updateDisplayCount( 282 | i - 1, 283 | totalWidth - currentItemWidth - suffixWidth + restWidth, 284 | ); 285 | break; 286 | } 287 | } 288 | 289 | if (suffix && getItemWidth(0) + suffixWidth > mergedContainerWidth) { 290 | setSuffixFixedStart(null); 291 | } 292 | } 293 | }, [ 294 | mergedContainerWidth, 295 | itemWidths, 296 | restWidth, 297 | suffixWidth, 298 | getKey, 299 | mergedData, 300 | ]); 301 | 302 | // ================================ Render ================================ 303 | const displayRest = restReady && !!omittedItems.length; 304 | 305 | let suffixStyle: React.CSSProperties = {}; 306 | if (suffixFixedStart !== null && shouldResponsive) { 307 | suffixStyle = { 308 | position: 'absolute', 309 | left: suffixFixedStart, 310 | top: 0, 311 | }; 312 | } 313 | 314 | const itemSharedProps = { 315 | prefixCls: itemPrefixCls, 316 | responsive: shouldResponsive, 317 | component: itemComponent, 318 | invalidate, 319 | }; 320 | 321 | // >>>>> Choice render fun by `renderRawItem` 322 | const internalRenderItemNode = renderRawItem 323 | ? (item: ItemType, index: number) => { 324 | const key = getKey(item, index); 325 | 326 | return ( 327 | 338 | {renderRawItem(item, index)} 339 | 340 | ); 341 | } 342 | : (item: ItemType, index: number) => { 343 | const key = getKey(item, index); 344 | 345 | return ( 346 | 356 | ); 357 | }; 358 | 359 | // >>>>> Rest node 360 | const restContextProps = { 361 | order: displayRest ? mergedDisplayCount : Number.MAX_SAFE_INTEGER, 362 | className: `${itemPrefixCls}-rest`, 363 | registerSize: registerOverflowSize, 364 | display: displayRest, 365 | }; 366 | 367 | const mergedRenderRest = renderRest || defaultRenderRest; 368 | 369 | const restNode = renderRawRest ? ( 370 | 376 | {renderRawRest(omittedItems)} 377 | 378 | ) : ( 379 | 384 | {typeof mergedRenderRest === 'function' 385 | ? mergedRenderRest(omittedItems) 386 | : mergedRenderRest} 387 | 388 | ); 389 | 390 | const overflowNode = ( 391 | 397 | {mergedData.map(internalRenderItemNode)} 398 | 399 | {/* Rest Count Item */} 400 | {showRest ? restNode : null} 401 | 402 | {/* Suffix Node */} 403 | {suffix && ( 404 | 414 | {suffix} 415 | 416 | )} 417 | 418 | ); 419 | 420 | return isResponsive ? ( 421 | 422 | {overflowNode} 423 | 424 | ) : overflowNode; 425 | } 426 | 427 | const ForwardOverflow = React.forwardRef(Overflow); 428 | 429 | type ForwardOverflowType = ( 430 | props: React.PropsWithChildren> & { 431 | ref?: React.Ref; 432 | }, 433 | ) => React.ReactElement; 434 | 435 | type FilledOverflowType = ForwardOverflowType & { 436 | Item: typeof RawItem; 437 | RESPONSIVE: typeof RESPONSIVE; 438 | /** Will work as normal `component`. Skip patch props like `prefixCls`. */ 439 | INVALIDATE: typeof INVALIDATE; 440 | }; 441 | 442 | ForwardOverflow.displayName = 'Overflow'; 443 | 444 | (ForwardOverflow as unknown as FilledOverflowType).Item = RawItem; 445 | (ForwardOverflow as unknown as FilledOverflowType).RESPONSIVE = RESPONSIVE; 446 | (ForwardOverflow as unknown as FilledOverflowType).INVALIDATE = INVALIDATE; 447 | 448 | // Convert to generic type 449 | export default ForwardOverflow as unknown as FilledOverflowType; 450 | -------------------------------------------------------------------------------- /src/RawItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import classNames from 'classnames'; 3 | import Item from './Item'; 4 | import { OverflowContext } from './context'; 5 | 6 | export type ComponentType = 7 | | React.ComponentType 8 | | React.ForwardRefExoticComponent 9 | | React.FC 10 | | keyof React.ReactHTML; 11 | 12 | export interface RawItemProps extends React.HTMLAttributes { 13 | component?: ComponentType; 14 | children?: React.ReactNode; 15 | } 16 | 17 | const InternalRawItem = (props: RawItemProps, ref: React.Ref) => { 18 | const context = React.useContext(OverflowContext); 19 | 20 | // Render directly when context not provided 21 | if (!context) { 22 | const { component: Component = 'div', ...restProps } = props; 23 | return ; 24 | } 25 | 26 | const { className: contextClassName, ...restContext } = context; 27 | const { className, ...restProps } = props; 28 | 29 | // Do not pass context to sub item to avoid multiple measure 30 | return ( 31 | 32 | 38 | 39 | ); 40 | }; 41 | 42 | const RawItem = React.forwardRef(InternalRawItem); 43 | RawItem.displayName = 'RawItem'; 44 | 45 | export default RawItem; 46 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const OverflowContext = React.createContext<{ 4 | prefixCls: string; 5 | responsive: boolean; 6 | order: number; 7 | registerSize: (key: React.Key, width: number | null) => void; 8 | display: boolean; 9 | 10 | invalidate: boolean; 11 | 12 | // Item Usage 13 | item?: any; 14 | itemKey?: React.Key; 15 | 16 | // Rest Usage 17 | className?: string; 18 | }>(null); 19 | -------------------------------------------------------------------------------- /src/hooks/channelUpdate.ts: -------------------------------------------------------------------------------- 1 | import raf from 'rc-util/lib/raf'; 2 | 3 | export default function channelUpdate(callback: VoidFunction) { 4 | if (typeof MessageChannel === 'undefined') { 5 | raf(callback); 6 | } else { 7 | const channel = new MessageChannel(); 8 | channel.port1.onmessage = () => callback(); 9 | channel.port2.postMessage(undefined); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks/useEffectState.tsx: -------------------------------------------------------------------------------- 1 | import useEvent from 'rc-util/lib/hooks/useEvent'; 2 | import * as React from 'react'; 3 | import { unstable_batchedUpdates } from 'react-dom'; 4 | import channelUpdate from './channelUpdate'; 5 | 6 | type Updater = T | ((origin: T) => T); 7 | 8 | type UpdateCallbackFunc = VoidFunction; 9 | 10 | type NotifyEffectUpdate = (callback: UpdateCallbackFunc) => void; 11 | 12 | /** 13 | * Batcher for record any `useEffectState` need update. 14 | */ 15 | export function useBatcher() { 16 | // Updater Trigger 17 | const updateFuncRef = React.useRef(null); 18 | 19 | // Notify update 20 | const notifyEffectUpdate: NotifyEffectUpdate = callback => { 21 | if (!updateFuncRef.current) { 22 | updateFuncRef.current = []; 23 | 24 | channelUpdate(() => { 25 | unstable_batchedUpdates(() => { 26 | updateFuncRef.current.forEach(fn => { 27 | fn(); 28 | }); 29 | updateFuncRef.current = null; 30 | }); 31 | }); 32 | } 33 | 34 | updateFuncRef.current.push(callback); 35 | }; 36 | 37 | return notifyEffectUpdate; 38 | } 39 | 40 | /** 41 | * Trigger state update by `useLayoutEffect` to save perf. 42 | */ 43 | export default function useEffectState( 44 | notifyEffectUpdate: NotifyEffectUpdate, 45 | defaultValue?: T, 46 | ): [T, (value: Updater) => void] { 47 | // Value 48 | const [stateValue, setStateValue] = React.useState(defaultValue); 49 | 50 | // Set State 51 | const setEffectVal = useEvent((nextValue: Updater) => { 52 | notifyEffectUpdate(() => { 53 | setStateValue(nextValue); 54 | }); 55 | }); 56 | 57 | return [stateValue, setEffectVal]; 58 | } 59 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import Overflow from './Overflow'; 2 | import type { OverflowProps } from './Overflow'; 3 | 4 | export type { OverflowProps }; 5 | 6 | export default Overflow; 7 | -------------------------------------------------------------------------------- /tests/__snapshots__/index.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Overflow.Basic customize component 1`] = ` 4 |
    7 |
  • 11 | Label 0 12 |
  • 13 |
14 | `; 15 | -------------------------------------------------------------------------------- /tests/__snapshots__/invalidate.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Overflow.Invalidate render item 1`] = ` 4 |
    7 |
  • 10 | Label 0 11 |
  • 12 |
  • 15 | Label 1 16 |
  • 17 |
18 | `; 19 | 20 | exports[`Overflow.Invalidate render raw 1`] = ` 21 |
    24 |
  • 27 | Label 0 28 |
  • 29 |
  • 32 | Label 1 33 |
  • 34 |
35 | `; 36 | -------------------------------------------------------------------------------- /tests/__snapshots__/raw.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Overflow.Raw HOC usage 1`] = ` 4 |
7 |
11 | Bamboo 12 |
13 |
14 | `; 15 | 16 | exports[`Overflow.Raw render node directly 1`] = ` 17 |
    20 |
  • 24 | Label 0 25 |
  • 26 |
27 | `; 28 | 29 | exports[`Overflow.Raw safe with item directly 1`] = ` 30 |
31 | Bamboo 32 |
33 | `; 34 | -------------------------------------------------------------------------------- /tests/__snapshots__/ssr.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Overflow.SSR basic 1`] = ` 4 |
7 |
11 | Label 0 12 |
13 |
17 | Label 1 18 |
19 | 26 |
27 | `; 28 | -------------------------------------------------------------------------------- /tests/github.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, act } from '@testing-library/react'; 3 | import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; 4 | import Overflow from '../src'; 5 | 6 | import { _rs as onResize } from 'rc-resize-observer/lib/utils/observerUtil'; 7 | 8 | interface ItemType { 9 | label: React.ReactNode; 10 | key: React.Key; 11 | } 12 | 13 | function renderItem(item: ItemType) { 14 | return item.label; 15 | } 16 | 17 | function renderRest(items: ItemType[]) { 18 | return `+${items.length}...`; 19 | } 20 | 21 | describe('Overflow.github', () => { 22 | function getData(count: number) { 23 | return new Array(count).fill(undefined).map((_, index) => ({ 24 | label: `Label ${index}`, 25 | key: `k-${index}`, 26 | })); 27 | } 28 | 29 | beforeEach(() => { 30 | jest.useFakeTimers(); 31 | }); 32 | 33 | afterEach(() => { 34 | jest.useRealTimers(); 35 | }); 36 | 37 | const widths = { 38 | 'rc-overflow': 100, 39 | 'rc-overflow-item': 90, 40 | }; 41 | 42 | const propDef = { 43 | get() { 44 | let targetWidth = 0; 45 | Object.keys(widths).forEach(key => { 46 | if (this.className.includes(key)) { 47 | targetWidth = widths[key]; 48 | } 49 | }); 50 | 51 | return targetWidth; 52 | }, 53 | }; 54 | 55 | beforeAll(() => { 56 | spyElementPrototypes(HTMLDivElement, { 57 | clientWidth: propDef, 58 | offsetWidth: propDef, 59 | }); 60 | }); 61 | 62 | async function triggerResize(target: HTMLElement) { 63 | await act(async () => { 64 | onResize([{ target } as any]); 65 | for (let i = 0; i < 10; i += 1) { 66 | await Promise.resolve(); 67 | } 68 | }); 69 | } 70 | 71 | it('only one', async () => { 72 | const { container } = render( 73 | 74 | data={getData(2)} 75 | itemKey="key" 76 | renderItem={renderItem} 77 | renderRest={renderRest} 78 | maxCount="responsive" 79 | />, 80 | ); 81 | 82 | // width & rest resize 83 | await triggerResize(container.querySelector('.rc-overflow')); 84 | await triggerResize(container.querySelector('.rc-overflow-item-rest')); 85 | 86 | act(() => { 87 | jest.runAllTimers(); 88 | }); 89 | 90 | const items = Array.from( 91 | container.querySelectorAll( 92 | '.rc-overflow-item:not(.rc-overflow-item-rest)', 93 | ), 94 | ); 95 | 96 | for (let i = 0; i < items.length; i += 1) { 97 | await triggerResize(items[i]); 98 | } 99 | act(() => { 100 | jest.runAllTimers(); 101 | }); 102 | 103 | expect(container.querySelector('.rc-overflow-item-rest')).toHaveStyle({ 104 | opacity: 1, 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /tests/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Overflow from '../src'; 3 | import { mount } from './wrapper'; 4 | 5 | interface ItemType { 6 | label: React.ReactNode; 7 | key: React.Key; 8 | } 9 | 10 | function renderItem(item: ItemType) { 11 | return item.label; 12 | } 13 | 14 | describe('Overflow.Basic', () => { 15 | function getData(count: number) { 16 | return new Array(count).fill(undefined).map((_, index) => ({ 17 | label: `Label ${index}`, 18 | key: `k-${index}`, 19 | })); 20 | } 21 | 22 | it('no data', () => { 23 | const wrapper = mount( />); 24 | expect(wrapper.findItems()).toHaveLength(0); 25 | }); 26 | 27 | it('no maxCount', () => { 28 | const wrapper = mount( 29 | data={getData(6)} renderItem={renderItem} />, 30 | ); 31 | expect(wrapper.find('ResizeObserver')).toHaveLength(0); 32 | expect(wrapper.findItems()).toHaveLength(6); 33 | expect(wrapper.findRest()).toHaveLength(0); 34 | }); 35 | 36 | it('number maxCount', () => { 37 | const wrapper = mount( 38 | 39 | data={getData(6)} 40 | renderItem={renderItem} 41 | maxCount={4} 42 | />, 43 | ); 44 | expect(wrapper.find('ResizeObserver')).toHaveLength(0); 45 | expect(wrapper.findItems()).toHaveLength(4); 46 | expect(wrapper.findRest()).toHaveLength(1); 47 | }); 48 | 49 | it('without renderItem', () => { 50 | const wrapper = mount(Bamboo Is Light]} />); 51 | expect(wrapper.find('Item').text()).toEqual('Bamboo Is Light'); 52 | }); 53 | 54 | it('renderItem params have "order"', () => { 55 | const testData = getData(3); 56 | const wrapper = mount( 57 | { 60 | return `${item.label}-${info.index}-test`; 61 | }} 62 | />, 63 | ); 64 | const renderedItems = wrapper.find('.rc-overflow-item'); 65 | expect(renderedItems).toHaveLength(testData.length); 66 | renderedItems.forEach((node, index) => { 67 | expect(node.text()).toBe(`${testData[index].label}-${index}-test`); 68 | }); 69 | }); 70 | describe('renderRest', () => { 71 | it('function', () => { 72 | const wrapper = mount( 73 | `Bamboo: ${omittedItems.length}`} 77 | maxCount={3} 78 | />, 79 | ); 80 | 81 | expect(wrapper.findRest().text()).toEqual('Bamboo: 3'); 82 | }); 83 | 84 | it('node', () => { 85 | const wrapper = mount( 86 | Light Is Bamboo} 90 | maxCount={3} 91 | />, 92 | ); 93 | 94 | expect(wrapper.findRest().text()).toEqual('Light Is Bamboo'); 95 | }); 96 | }); 97 | 98 | describe('itemKey', () => { 99 | it('string', () => { 100 | const wrapper = mount( 101 | , 102 | ); 103 | 104 | expect(wrapper.find('Item').key()).toEqual('k-0'); 105 | }); 106 | it('function', () => { 107 | const wrapper = mount( 108 | `bamboo-${item.key}`} 112 | />, 113 | ); 114 | 115 | expect(wrapper.find('Item').key()).toEqual('bamboo-k-0'); 116 | }); 117 | }); 118 | 119 | it('customize component', () => { 120 | const wrapper = mount( 121 | `bamboo-${item.key}`} 125 | component="ul" 126 | itemComponent="li" 127 | />, 128 | ); 129 | 130 | expect(wrapper.render()).toMatchSnapshot(); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /tests/invalidate.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Overflow from '../src'; 3 | import { mount } from './wrapper'; 4 | 5 | interface ItemType { 6 | label: React.ReactNode; 7 | key: React.Key; 8 | } 9 | 10 | describe('Overflow.Invalidate', () => { 11 | function getData(count: number) { 12 | return new Array(count).fill(undefined).map((_, index) => ({ 13 | label: `Label ${index}`, 14 | key: `k-${index}`, 15 | })); 16 | } 17 | 18 | it('render item', () => { 19 | const wrapper = mount( 20 | 21 | data={getData(2)} 22 | renderItem={item => { 23 | return item.label; 24 | }} 25 | itemKey={item => `bamboo-${item.key}`} 26 | itemComponent="li" 27 | component="ul" 28 | maxCount={Overflow.INVALIDATE} 29 | />, 30 | ); 31 | 32 | expect(wrapper.render()).toMatchSnapshot(); 33 | }); 34 | 35 | it('render raw', () => { 36 | const wrapper = mount( 37 | 38 | data={getData(2)} 39 | renderRawItem={item => { 40 | return {item.label}; 41 | }} 42 | itemKey={item => `bamboo-${item.key}`} 43 | component="ul" 44 | maxCount={Overflow.INVALIDATE} 45 | />, 46 | ); 47 | 48 | expect(wrapper.render()).toMatchSnapshot(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/raw.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Overflow from '../src'; 3 | import Item from '../src/Item'; 4 | import { mount } from './wrapper'; 5 | 6 | interface ItemType { 7 | label: React.ReactNode; 8 | key: React.Key; 9 | } 10 | 11 | describe('Overflow.Raw', () => { 12 | function getData(count: number) { 13 | return new Array(count).fill(undefined).map((_, index) => ({ 14 | label: `Label ${index}`, 15 | key: `k-${index}`, 16 | })); 17 | } 18 | 19 | it('render node directly', () => { 20 | const elements = new Set(); 21 | 22 | const wrapper = mount( 23 | 24 | data={getData(1)} 25 | renderRawItem={item => { 26 | return ( 27 | { 30 | elements.add(ele); 31 | }} 32 | > 33 | {item.label} 34 | 35 | ); 36 | }} 37 | itemKey={item => `bamboo-${item.key}`} 38 | component="ul" 39 | />, 40 | ); 41 | 42 | const elementList = [...elements]; 43 | expect(elementList).toHaveLength(1); 44 | expect(elementList[0] instanceof HTMLLIElement).toBeTruthy(); 45 | 46 | expect(wrapper.render()).toMatchSnapshot(); 47 | }); 48 | 49 | it('safe with item directly', () => { 50 | const wrapper = mount(Bamboo); 51 | 52 | expect(wrapper.render()).toMatchSnapshot(); 53 | 54 | expect(wrapper.exists(Item)).toBeFalsy(); 55 | }); 56 | 57 | it('HOC usage', () => { 58 | interface SharedProps { 59 | visible?: boolean; 60 | children?: React.ReactNode; 61 | } 62 | 63 | const ComponentWrapper = (props: SharedProps) => ( 64 | 65 | ); 66 | 67 | const UserHOC = ({ visible, ...props }: SharedProps) => 68 | visible ? : null; 69 | 70 | const wrapper = mount( 71 | Light, 74 | 75 | Bamboo 76 | , 77 | ]} 78 | renderRawItem={node => node} 79 | itemKey={node => node.key} 80 | />, 81 | ); 82 | 83 | expect(wrapper.render()).toMatchSnapshot(); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /tests/responsive.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { act } from 'react-dom/test-utils'; 3 | import Overflow from '../src'; 4 | import { mount } from './wrapper'; 5 | 6 | interface ItemType { 7 | label: React.ReactNode; 8 | key: React.Key; 9 | } 10 | 11 | function renderItem(item: ItemType) { 12 | return item.label; 13 | } 14 | 15 | describe('Overflow.Responsive', () => { 16 | function getData(count: number) { 17 | return new Array(count).fill(undefined).map((_, index) => ({ 18 | label: `Label ${index}`, 19 | key: `k-${index}`, 20 | })); 21 | } 22 | 23 | beforeEach(() => { 24 | jest.useFakeTimers(); 25 | }); 26 | 27 | afterEach(() => { 28 | jest.useRealTimers(); 29 | }); 30 | 31 | it('basic', () => { 32 | const wrapper = mount( 33 | 34 | data={getData(6)} 35 | renderItem={renderItem} 36 | maxCount="responsive" 37 | />, 38 | ); 39 | 40 | wrapper.initSize(100, 20); // [0][1][2][3][4][+2](5)(6) 41 | expect(wrapper.findItems()).toHaveLength(6); 42 | [true, true, true, true, false, false].forEach((display, i) => { 43 | expect(wrapper.findItems().at(i).props().display).toBe(display); 44 | }); 45 | expect(wrapper.findRest()).toHaveLength(1); 46 | expect(wrapper.findRest().text()).toEqual('+ 2 ...'); 47 | expect( 48 | wrapper.findItems().find('div').last().prop('aria-hidden'), 49 | ).toBeTruthy(); 50 | }); 51 | 52 | it('only one', () => { 53 | const wrapper = mount( 54 | 55 | data={getData(1)} 56 | itemKey="key" 57 | renderItem={renderItem} 58 | maxCount="responsive" 59 | />, 60 | ); 61 | wrapper.initSize(100, 20); 62 | 63 | expect(wrapper.findItems()).toHaveLength(1); 64 | expect(wrapper.findRest().props().display).toBeFalsy(); 65 | }); 66 | 67 | it('just fit', () => { 68 | const wrapper = mount( 69 | 70 | data={getData(1)} 71 | itemKey="key" 72 | renderItem={renderItem} 73 | maxCount="responsive" 74 | />, 75 | ); 76 | wrapper.initSize(20, 20); 77 | 78 | expect(wrapper.findItems()).toHaveLength(1); 79 | expect(wrapper.findRest().props().display).toBeFalsy(); 80 | }); 81 | 82 | it('remove to clean up', () => { 83 | const data = getData(6); 84 | 85 | const wrapper = mount( 86 | 87 | data={data} 88 | itemKey="key" 89 | renderItem={renderItem} 90 | maxCount="responsive" 91 | />, 92 | ); 93 | wrapper.initSize(100, 20); 94 | 95 | // Remove one (Just fit the container width) 96 | const newData = [...data]; 97 | newData.splice(1, 1); 98 | wrapper.setProps({ data: newData }); 99 | wrapper.update(); 100 | 101 | expect(wrapper.findItems()).toHaveLength(5); 102 | expect(wrapper.findRest().props().display).toBeFalsy(); 103 | 104 | // Remove one (More place for container) 105 | const restData = [...newData]; 106 | restData.splice(1, 2); 107 | restData.push({ 108 | label: 'Additional', 109 | key: 'additional', 110 | }); 111 | wrapper.setProps({ data: restData }); 112 | wrapper.update(); 113 | 114 | expect(wrapper.findItems()).toHaveLength(4); 115 | expect(wrapper.findRest().props().display).toBeFalsy(); 116 | }); 117 | 118 | it('none to overflow', () => { 119 | const data = getData(5); 120 | 121 | const wrapper = mount( 122 | 123 | data={data} 124 | itemKey="key" 125 | renderItem={renderItem} 126 | maxCount="responsive" 127 | />, 128 | ); 129 | 130 | wrapper.initSize(100, 20); 131 | expect(wrapper.findItems()).toHaveLength(5); 132 | expect(wrapper.findRest().props().display).toBeFalsy(); 133 | 134 | // Add one 135 | const newData: ItemType[] = [ 136 | { 137 | label: 'Additional', 138 | key: 'additional', 139 | }, 140 | ...data, 141 | ]; 142 | wrapper.setProps({ data: newData }); 143 | wrapper.update(); 144 | 145 | // Currently resize observer not trigger, rest node is not ready 146 | expect(wrapper.findItems()).toHaveLength(6); 147 | expect(wrapper.findRest().props().display).toBeFalsy(); 148 | 149 | // Trigger resize, node ready 150 | wrapper.triggerItemResize(0, 20); 151 | expect(wrapper.findItems()).toHaveLength(6); 152 | expect(wrapper.findRest().props().display).toBeTruthy(); 153 | }); 154 | 155 | it('unmount no error', () => { 156 | const wrapper = mount( 157 | 158 | data={getData(1)} 159 | itemKey="key" 160 | renderItem={renderItem} 161 | maxCount="responsive" 162 | />, 163 | ); 164 | 165 | wrapper.initSize(100, 20); 166 | 167 | wrapper.unmount(); 168 | 169 | act(() => { 170 | jest.runAllTimers(); 171 | }); 172 | }); 173 | 174 | describe('suffix', () => { 175 | it('ping the position', () => { 176 | const wrapper = mount( 177 | 178 | data={getData(10)} 179 | itemKey="key" 180 | renderItem={renderItem} 181 | maxCount="responsive" 182 | suffix="Bamboo" 183 | />, 184 | ); 185 | 186 | wrapper.initSize(100, 20); 187 | 188 | expect(wrapper.findSuffix().props().style).toEqual( 189 | expect.objectContaining({ position: 'absolute', top: 0, left: 80 }), 190 | ); 191 | }); 192 | 193 | it('too long to pin', () => { 194 | const wrapper = mount( 195 | 196 | data={getData(1)} 197 | itemKey="key" 198 | renderItem={renderItem} 199 | maxCount="responsive" 200 | suffix="Bamboo" 201 | />, 202 | ); 203 | 204 | wrapper.initSize(100, 20); 205 | wrapper.triggerItemResize(0, 90); 206 | 207 | expect(wrapper.findSuffix().props().style.position).toBeFalsy(); 208 | }); 209 | 210 | it('long to short should keep correct position', () => { 211 | const wrapper = mount( 212 | 213 | data={getData(3)} 214 | itemKey="key" 215 | renderItem={renderItem} 216 | maxCount="responsive" 217 | suffix="Bamboo" 218 | />, 219 | ); 220 | 221 | wrapper.initSize(20, 20); 222 | wrapper.setProps({ data: [] }); 223 | 224 | expect(wrapper.findRest()).toHaveLength(0); 225 | expect(wrapper.findSuffix().props().style.position).toBeFalsy(); 226 | }); 227 | }); 228 | 229 | it('render rest directly', () => { 230 | const wrapper = mount( 231 | 232 | data={getData(10)} 233 | itemKey="key" 234 | renderItem={renderItem} 235 | maxCount="responsive" 236 | renderRawRest={omitItems => { 237 | return ( 238 | 239 | {omitItems.length} 240 | 241 | ); 242 | }} 243 | />, 244 | ); 245 | 246 | wrapper.initSize(100, 20); 247 | 248 | expect(wrapper.find('span.custom-rest').text()).toEqual('6'); 249 | }); 250 | }); 251 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | const Enzyme = require('enzyme'); 2 | const Adapter = require('enzyme-adapter-react-16'); 3 | const { act } = require('react-dom/test-utils'); 4 | require('regenerator-runtime/runtime'); 5 | 6 | window.requestAnimationFrame = (func) => { 7 | window.setTimeout(func, 16); 8 | }; 9 | 10 | Enzyme.configure({ adapter: new Adapter() }); 11 | 12 | Object.assign(Enzyme.ReactWrapper.prototype, { 13 | triggerResize(clientWidth) { 14 | const target = this.find('ResizeObserver').first() 15 | target.invoke('onResize')({}, { clientWidth }) 16 | act(() => { 17 | jest.runAllTimers(); 18 | }) 19 | this.update() 20 | }, 21 | triggerItemResize(index, offsetWidth) { 22 | const target = this.find('Item').at(index).find('ResizeObserver') 23 | target.invoke('onResize')({ offsetWidth }); 24 | act(() => { 25 | jest.runAllTimers(); 26 | }) 27 | this.update() 28 | }, 29 | initSize(width, itemWidth) { 30 | this.triggerResize(width); 31 | this.find('Item').forEach((_, index) => { 32 | this.triggerItemResize(index, itemWidth); 33 | }); 34 | }, 35 | findItems() { 36 | return this.find('Item').filterWhere( 37 | (item) => 38 | item.props().className !== 'rc-overflow-item-rest' && 39 | item.props().className !== 'rc-overflow-item-suffix', 40 | ); 41 | }, 42 | findRest() { 43 | return this.find('Item.rc-overflow-item-rest'); 44 | }, 45 | findSuffix() { 46 | return this.find('Item.rc-overflow-item-suffix'); 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /tests/ssr.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'enzyme'; 3 | import Overflow from '../src'; 4 | 5 | interface ItemType { 6 | label: React.ReactNode; 7 | key: React.Key; 8 | } 9 | 10 | function renderItem(item: ItemType) { 11 | return item.label; 12 | } 13 | 14 | describe('Overflow.SSR', () => { 15 | function getData(count: number) { 16 | return new Array(count).fill(undefined).map((_, index) => ({ 17 | label: `Label ${index}`, 18 | key: `k-${index}`, 19 | })); 20 | } 21 | 22 | beforeEach(() => { 23 | jest.useFakeTimers(); 24 | }); 25 | 26 | afterEach(() => { 27 | jest.useRealTimers(); 28 | }); 29 | 30 | it('basic', () => { 31 | const wrapper = render( 32 | 33 | data={getData(2)} 34 | renderItem={renderItem} 35 | maxCount="responsive" 36 | ssr="full" 37 | />, 38 | ); 39 | 40 | expect(wrapper).toMatchSnapshot(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/wrapper.ts: -------------------------------------------------------------------------------- 1 | import type { ReactWrapper } from 'enzyme'; 2 | import { mount as enzymeMount } from 'enzyme'; 3 | 4 | export type MountParam = Parameters; 5 | 6 | export interface WrapperType extends ReactWrapper { 7 | triggerResize: (offsetWidth: number) => WrapperType; 8 | triggerItemResize: (index: number, offsetWidth: number) => WrapperType; 9 | initSize: (width: number, itemWidth: number) => WrapperType; 10 | findItems: () => WrapperType; 11 | findRest: () => WrapperType; 12 | findSuffix: () => WrapperType; 13 | } 14 | 15 | export function mount(...args: MountParam) { 16 | return enzymeMount(...args) as WrapperType; 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "baseUrl": "./", 6 | "jsx": "react", 7 | "declaration": true, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "paths": { 11 | "@/*": ["src/*"], 12 | "@@/*": [".dumi/tmp/*"], 13 | "rc-overflow": ["src/index.tsx"] 14 | } 15 | } 16 | } --------------------------------------------------------------------------------