Repository: react-component/collapse Branch: master Commit: 41fb47344cd7 Files: 39 Total size: 74.8 KB Directory structure: gitextract_rjd6bn1q/ ├── .dumirc.ts ├── .editorconfig ├── .eslintrc.js ├── .fatherrc.ts ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── react-component-ci.yml │ └── site-deploy.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .prettierrc ├── HISTORY.md ├── LICENSE.md ├── README.md ├── assets/ │ ├── index.less │ └── motion.less ├── bunfig.toml ├── docs/ │ ├── demo/ │ │ ├── basic.md │ │ ├── custom-icon.md │ │ ├── fragment.md │ │ └── simple.md │ ├── examples/ │ │ ├── _util/ │ │ │ └── motionUtil.ts │ │ ├── basic.tsx │ │ ├── custom-icon.tsx │ │ ├── fragment.tsx │ │ └── simple.tsx │ └── index.md ├── jest.config.js ├── package.json ├── src/ │ ├── Collapse.tsx │ ├── Panel.tsx │ ├── PanelContent.tsx │ ├── hooks/ │ │ └── useItems.tsx │ ├── index.tsx │ └── interface.ts ├── tests/ │ ├── __snapshots__/ │ │ └── index.spec.tsx.snap │ ├── index.spec.tsx │ └── setupAfterEnv.ts ├── tsconfig.json └── vercel.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dumirc.ts ================================================ // more config: https://d.umijs.org/config import { defineConfig } from 'dumi'; import path from 'path'; const basePath = process.env.GITHUB_ACTIONS ? '/collapse/' : '/'; const publicPath = process.env.GITHUB_ACTIONS ? '/collapse/' : '/'; export default defineConfig({ alias: { 'rc-collapse$': path.resolve('src'), 'rc-collapse/es': path.resolve('src'), }, mfsu: false, favicons: ['https://avatars0.githubusercontent.com/u/9441414?s=200&v=4'], themeConfig: { name: 'Collapse', logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', }, base: basePath, publicPath, }); ================================================ 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/role-supports-aria-props': 0, 'jsx-a11y/label-has-associated-control': 0, 'jsx-a11y/label-has-for': 0, 'jsx-a11y/no-noninteractive-tabindex': 0, 'import/no-extraneous-dependencies': 0, '@typescript-eslint/consistent-type-exports': 2, }, }; ================================================ FILE: .fatherrc.ts ================================================ import { defineConfig } from 'father'; export default defineConfig({ plugins: ['@rc-component/father-plugin'], }); ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: npm directory: "/" schedule: interval: daily time: "21:00" open-pull-requests-limit: 10 ignore: - dependency-name: "@types/react-dom" versions: - 17.0.0 - 17.0.1 - 17.0.2 - dependency-name: "@types/react" versions: - 17.0.0 - 17.0.1 - 17.0.2 - 17.0.3 - dependency-name: np versions: - 7.2.0 - 7.3.0 - 7.4.0 - dependency-name: less versions: - 4.1.0 ================================================ FILE: .github/workflows/react-component-ci.yml ================================================ name: ✅ test on: [push, pull_request] jobs: test: uses: react-component/rc-test/.github/workflows/test.yml@main secrets: inherit ================================================ FILE: .github/workflows/site-deploy.yml ================================================ name: Deploy website on: push: tags: - '*' workflow_dispatch: permissions: contents: write jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v3 - name: setup node uses: actions/setup-node@v1 with: node-version: 14 - name: create package-lock.json run: npm i --package-lock-only --ignore-scripts - name: Install dependencies run: npm ci - name: build Docs run: npm run build - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./dist force_orphan: true user_name: 'github-actions[bot]' user_email: 'github-actions[bot]@users.noreply.github.com' ================================================ 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 coverage es yarn.lock package-lock.json pnpm-lock.yaml .storybook .doc # dumi .dumi/tmp .dumi/tmp-production .dumi/tmp-test .env.local src/.umi bun.lockb ================================================ FILE: .husky/pre-commit ================================================ lint-staged ================================================ FILE: .prettierrc ================================================ { "singleQuote": true, "trailingComma": "all", "proseWrap": "never", "printWidth": 100 } ================================================ FILE: HISTORY.md ================================================ # History --- ## 2.0.0 `2020-05-08` - Remove `react-lifecycles-compat` and `prop-types`. - Upgrade `rc-animate` to `3.x`. - Use `@ant-design/css-animation`. ## 1.11.0 - Add `extra`. ## 1.10.0 2018-08-13 - Add `expandIcon`. ## 1.9.1 2018-05-10 - Fix invalid aria-expanded prop in preact ## 1.9.0 2018-04-02 - Add keyboard support [#84](https://github.com/react-component/collapse/pull/84) ## 1.8.0 2018-01-30 - Add prop forceRender to Panel [#82](https://github.com/react-component/collapse/pull/82) ## 1.7.6 2017-06-06 - Add prop id for Panel [#69](https://github.com/react-component/collapse/issues/69) ## 1.7.4 2017-05-16 - Add prop disabled [!71](https://github.com/react-component/collapse/pull/71) - Add es module export [!70](https://github.com/react-component/collapse/pull/70) ## 1.7.2 2017-04-25 - Allow user to add custom header classe [!66](https://github.com/react-component/collapse/pull/66) ## 1.7.1 2017-04-19 - Add prop destroyInactivePanel [!61](https://github.com/react-component/collapse/pull/61) ## 1.7.0 - Change createClass to React.Component [!58](https://github.com/react-component/collapse/pull/58) ## 1.6.12 - Fix `style` support for Panel ## 1.6.11 - Add 'showArrow' prop to Panel to toggle arrow visibility [!48](https://github.com/react-component/collapse/pull/48) ## 1.6.10 - Child item support null [!45](https://github.com/react-component/collapse/pull/45) ## 1.6.6 - add className props to Panel ## 1.6.5 - fix missing rc-collapse-item-active classname on active panel header ## 1.6.0 - lazy render/controllable ## 1.5.0 - use css animation instead of velocity.js ## 1.4.0 - only support react 0.14+ ## 1.2.0 2015-07-10 - 'chore' Change name to Collapse - 'feat' Support Collapse and Accordion ## 1.1.0 2015-07-09 - `test` Add test - `refactor` add Panel Api ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2014-present yiminghe 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-collapse rc-collapse ui component for react [![NPM version][npm-image]][npm-url] [![build status][github-actions-image]][github-actions-url] [![Test coverage][codecov-image]][codecov-url] [![npm download][download-image]][download-url] [npm-image]: http://img.shields.io/npm/v/rc-collapse.svg?style=flat-square [npm-url]: http://npmjs.org/package/rc-collapse [github-actions-image]: https://github.com/react-component/collapse/workflows/CI/badge.svg [github-actions-url]: https://github.com/react-component/collapse/actions [codecov-image]: https://img.shields.io/codecov/c/github/react-component/collapse/master.svg?style=flat-square [codecov-url]: https://app.codecov.io/gh/react-component/collapse [download-image]: https://img.shields.io/npm/dm/rc-collapse.svg?style=flat-square [download-url]: https://npmjs.org/package/rc-collapse ## Live Demo https://collapse-react-component.vercel.app ## Install [![rc-collapse](https://nodei.co/npm/rc-collapse.png)](https://npmjs.org/package/rc-collapse) ## Usage ```js var Collapse = require('rc-collapse'); var Panel = Collapse.Panel; var React = require('react'); var ReactDOM = require('react-dom'); require('rc-collapse/assets/index.css'); var App = ( this is panel content this is panel content2 or other ); ReactDOM.render(App, container); ``` ## Features - support ie8,ie8+,chrome,firefox,safari ## API ### Collapse props
name type default description
activeKey String|Array The first panel key current active Panel key
className String or object custom className to apply
defaultActiveKey String|Array null default active key
destroyOnHidden Boolean false If destroy the panel which not active, default false.
accordion Boolean false accordion mode, default is null, is collapse mode
onChange Function(key) noop called when collapse Panel is changed
expandIcon (props: PanelProps) => ReactNode specific the custom expand icon.
collapsible 'header' | 'icon' | 'disabled' - specify whether the panel of children is collapsible or the area of collapsible.
items interface.ts#ItemType - collapse items content
If `accordion` is null or false, every panel can open. Opening another panel will not close any of the other panels. `activeKey` should be an string, if passing an array (the first item in the array will be used). If `accordion` is true, only one panel can be open. Opening another panel will cause the previously opened panel to close. `activeKey` should be an string, if passing an array (the first item in the array will be used). ### Collapse.Panel props > **deprecated** use `items` instead, will be removed in `v4.0.0`
name type default description
header String or node header content of Panel
headerClass String ' ' custom className to apply to header
showArrow boolean true show arrow beside header
className String or object custom className to apply
classNames { header?: string, body?: string } Semantic structure className
style object custom style
styles { header?: React.CSSProperties, body?: React.CSSProperties } Semantic structure styles
openMotion object set the animation of open behavior, [more](https://github.com/react-component/motion). Different with v2, closed pane use a `rc-collapse-content-hidden` class to set `display: none` for hidden.
forceRender boolean false forced render of content in panel, not lazy render after clicking on header
extra String | ReactNode Content to render in the right of the panel header
collapsible 'header' | 'icon' | 'disabled' - specify whether the panel be collapsible or the area of collapsible.
> `disabled` is removed since 3.0.0, please use `collapsible=disabled` replace it. #### key If `key` is not provided, the panel's index will be used instead. #### KeyBoard Event By default, Collapse will listen `onKeyDown`(<3.7.0 `onKeyPress`) event with `enter` key to toggle panel's active state when `collapsible` is not `disabled`. If you want to disable this behavior, you can prevent the event from bubbling like this: ```tsx | pure const App = () => { const items: CollapseProps['items'] = [ { label: e.stopPropagation()} />, children: 'content', }, { label: (
e.stopPropagation()}>
), children: 'content', }, { label: 'title 2', children: 'content 2', collapsible: 'disabled', }, { label: 'title 3', children: 'content 3', onItemClick: console.log, }, ]; return ; }; ``` ## Development ```bash npm install npm start ``` ## Test Case ```bash npm test ``` ## Coverage ```bash npm test -- --coverage ``` ## License rc-collapse is released under the MIT license. ================================================ FILE: assets/index.less ================================================ @prefixCls: rc-collapse; @text-color: #666; @borderStyle: 1px solid #d9d9d9; @import './motion.less'; #arrow { .common() { width: 0; height: 0; font-size: 0; line-height: 0; } .right(@w, @h, @color) { border-top: @w solid transparent; border-bottom: @w solid transparent; border-left: @h solid @color; } .bottom(@w, @h, @color) { border-left: @w solid transparent; border-right: @w solid transparent; border-top: @h solid @color; } } .@{prefixCls} { background-color: #f7f7f7; border-radius: 3px; border: @borderStyle; // &-anim-active { // transition: height 0.2s ease-out; // } & > &-item { border-top: @borderStyle; &:first-child { border-top: none; } > .@{prefixCls}-header { display: flex; align-items: center; line-height: 22px; padding: 10px 16px; color: #666; cursor: pointer; .arrow { display: inline-block; content: '\20'; #arrow > .common(); #arrow > .right(3px, 4px, #666); vertical-align: middle; margin-right: 8px; } .@{prefixCls}-extra { margin: 0 16px 0 auto; } } .@{prefixCls}-collapsible-header { cursor: default; .@{prefixCls}-title { cursor: pointer; } .@{prefixCls}-expand-icon { cursor: pointer; } } .@{prefixCls}-collapsible-icon { cursor: default; .@{prefixCls}-expand-icon { cursor: pointer; } } } & > &-item-disabled > .@{prefixCls}-header { cursor: not-allowed; color: #999; background-color: #f3f3f3; } &-panel { overflow: hidden; color: @text-color; padding: 0 16px; background-color: #fff; & > &-box { margin-top: 16px; margin-bottom: 16px; } // &-inactive { // display: none; // } } &-item:last-child { > .@{prefixCls}-panel { border-radius: 0 0 3px 3px; } } & > &-item-active { > .@{prefixCls}-header { .arrow { position: relative; top: 2px; #arrow > .bottom(3px, 4px, #666); margin-right: 6px; } } } } ================================================ FILE: assets/motion.less ================================================ @prefixCls: rc-collapse; .@{prefixCls} { &-motion { transition: height 0.3s, opacity 0.3s; } &-panel-hidden { display: none; } } ================================================ FILE: bunfig.toml ================================================ [install] peer = false ================================================ FILE: docs/demo/basic.md ================================================ --- title: Basic nav: title: Demo path: /demo --- ================================================ FILE: docs/demo/custom-icon.md ================================================ --- title: custom-icon nav: title: Demo path: /demo --- ================================================ FILE: docs/demo/fragment.md ================================================ --- title: fragment nav: title: Demo path: /demo --- ================================================ FILE: docs/demo/simple.md ================================================ --- title: simple nav: title: Demo path: /demo --- ================================================ FILE: docs/examples/_util/motionUtil.ts ================================================ import type { CSSMotionProps, MotionEndEventHandler, MotionEventHandler, } from '@rc-component/motion'; const getCollapsedHeight: MotionEventHandler = () => ({ height: 0, opacity: 0 }); const getRealHeight: MotionEventHandler = (node) => ({ height: node.scrollHeight, opacity: 1 }); const getCurrentHeight: MotionEventHandler = (node) => ({ height: node.offsetHeight }); const skipOpacityTransition: MotionEndEventHandler = (_, event) => (event as TransitionEvent).propertyName === 'height'; const collapseMotion: CSSMotionProps = { motionName: 'rc-collapse-motion', onEnterStart: getCollapsedHeight, onEnterActive: getRealHeight, onLeaveStart: getCurrentHeight, onLeaveActive: getCollapsedHeight, onEnterEnd: skipOpacityTransition, onLeaveEnd: skipOpacityTransition, motionDeadline: 500, leavedClassName: 'rc-collapse-panel-hidden', }; export default collapseMotion; ================================================ FILE: docs/examples/basic.tsx ================================================ import type { CollapseProps } from 'rc-collapse'; import Collapse from 'rc-collapse'; import * as React from 'react'; import '../../assets/index.less'; const App = () => { const items: CollapseProps['items'] = [ { label: e.stopPropagation()} />, children: 'content', }, { label: 'title 2', children: 'content 2', collapsible: 'disabled', }, { label: 'title 3', children: 'content 3', onItemClick: console.log, }, ]; return ; }; export default App; ================================================ FILE: docs/examples/custom-icon.tsx ================================================ import Collapse, { Panel } from 'rc-collapse'; import * as React from 'react'; import '../../assets/index.less'; import motion from './_util/motionUtil'; const initLength = 3; const text = ` A dog is a type of domesticated animal. Known for its loyalty and faithfulness, it can be found as a welcome guest in many households across the world. `; function random() { return parseInt((Math.random() * 10).toString(), 10) + 1; } const arrowPath = 'M869 487.8L491.2 159.9c-2.9-2.5-6.6-3.9-10.5-3.9h-88' + '.5c-7.4 0-10.8 9.2-5.2 14l350.2 304H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.' + '6 8 8 8h585.1L386.9 854c-5.6 4.9-2.2 14 5.2 14h91.5c1.9 0 3.8-0.7 5.' + '2-2L869 536.2c14.7-12.8 14.7-35.6 0-48.4z'; function expandIcon({ isActive }) { return ( ); } const App: React.FC = () => { const [, reRender] = React.useState({}); const [accordion, setAccordion] = React.useState(false); const [activeKey, setActiveKey] = React.useState(['4']); const time = random(); const panelItems = Array.from({ length: initLength }, (_, i) => { const key = i + 1; return (

{text.repeat(time)}

); }).concat(

{text}

,
, ); const tools = ( <>





); return ( <> {tools} {panelItems} ); }; export default App; ================================================ FILE: docs/examples/fragment.tsx ================================================ import Collapse, { Panel } from 'rc-collapse'; import * as React from 'react'; import { Fragment } from 'react'; import '../../assets/index.less'; const App = () => ( content content content content content content ); export default App; ================================================ FILE: docs/examples/simple.tsx ================================================ import type { CollapseProps } from 'rc-collapse'; import Collapse, { Panel } from 'rc-collapse'; import * as React from 'react'; import '../../assets/index.less'; import motion from './_util/motionUtil'; const initLength = 3; const text = ` A dog is a type of domesticated animal. Known for its loyalty and faithfulness, it can be found as a welcome guest in many households across the world. `; function random() { return parseInt((Math.random() * 10).toString(), 10) + 1; } const arrowPath = 'M869 487.8L491.2 159.9c-2.9-2.5-6.6-3.9-10.5-3.9h-88' + '.5c-7.4 0-10.8 9.2-5.2 14l350.2 304H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.' + '6 8 8 8h585.1L386.9 854c-5.6 4.9-2.2 14 5.2 14h91.5c1.9 0 3.8-0.7 5.' + '2-2L869 536.2c14.7-12.8 14.7-35.6 0-48.4z'; function expandIcon({ isActive }) { return ( ); } const App: React.FC = () => { const [, reRender] = React.useState({}); const [accordion, setAccordion] = React.useState(false); const [activeKey, setActiveKey] = React.useState(['4']); const [collapsible, setCollapsible] = React.useState(); const time = random(); const panelItems = Array.from({ length: initLength }, (_, i) => { const key = i + 1; return (

{text.repeat(time)}

); }).concat(

{text}

,
, Extra Node} >

Panel with extra

, ); const handleCollapsibleChange = (e: React.ChangeEvent) => { const values = [undefined, 'header', 'icon', 'disabled']; setCollapsible(values[e.target.value]); }; const tools = ( <>



collapsible:



); return ( <> {tools} {panelItems} ); }; export default App; ================================================ FILE: docs/index.md ================================================ --- hero: title: rc-collapse description: rc-collapse ui component for react --- ================================================ FILE: jest.config.js ================================================ module.exports = { setupFilesAfterEnv: ['/tests/setupAfterEnv.ts'], }; ================================================ FILE: package.json ================================================ { "name": "@rc-component/collapse", "version": "1.2.0", "description": "rc-collapse ui component for react", "keywords": [ "react", "react-component", "react-rc-collapse", "rc-collapse", "collapse", "accordion" ], "homepage": "http://github.com/react-component/collapse", "bugs": { "url": "http://github.com/react-component/collapse/issues" }, "repository": { "type": "git", "url": "git@github.com:react-component/collapse.git" }, "license": "MIT", "main": "./lib/index", "module": "./es/index", "typings": "es/index.d.ts", "files": [ "lib", "es", "assets/*.css" ], "scripts": { "compile": "father build && lessc assets/index.less assets/index.css", "coverage": "rc-test --coverage", "docs:build": "dumi build", "docs:deploy": "npm run docs:build && gh-pages -d dist", "lint": "eslint src/ --ext .ts,.tsx,.jsx,.js,.md", "prepare": "husky", "now-build": "npm run docs:build", "prepublishOnly": "npm run compile && rc-np", "prettier": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", "postpublish": "npm run docs:deploy", "start": "dumi dev", "test": "rc-test" }, "lint-staged": { "**/*.{ts,tsx,js,jsx,json,md}": "npm run prettier" }, "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "devDependencies": { "@rc-component/father-plugin": "^2.0.1", "@rc-component/np": "^1.0.4", "@testing-library/jest-dom": "^6.1.4", "@testing-library/react": "^16.3.0", "@types/jest": "^29.4.0", "@types/node": "^24.2.0", "@types/react": "^19.1.4", "@types/react-dom": "^19.1.5", "@umijs/fabric": "^4.0.0", "dumi": "^2.1.1", "eslint": "^8.55.0", "eslint-plugin-jest": "^29.0.1", "eslint-plugin-unicorn": "^49.0.0", "father": "^4.1.3", "gh-pages": "^6.2.0", "husky": "^9.0.0", "jest": "^30.0.3", "less": "^4.2.0", "lint-staged": "^16.0.0", "prettier": "^3.0.3", "rc-test": "^7.0.14", "react": "^19.1.0", "react-dom": "^19.1.0", "typescript": "^5.0.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } } ================================================ FILE: src/Collapse.tsx ================================================ import { clsx } from 'clsx'; import { useControlledState, useEvent } from '@rc-component/util'; import warning from '@rc-component/util/lib/warning'; import React from 'react'; import useItems from './hooks/useItems'; import type { CollapseProps } from './interface'; import CollapsePanel from './Panel'; import pickAttrs from '@rc-component/util/lib/pickAttrs'; function getActiveKeysArray(activeKey: React.Key | React.Key[]): React.Key[] { let currentActiveKey = activeKey; if (!Array.isArray(currentActiveKey)) { const activeKeyType = typeof currentActiveKey; currentActiveKey = activeKeyType === 'number' || activeKeyType === 'string' ? [currentActiveKey] : []; } return currentActiveKey.map((key) => String(key)); } const Collapse = React.forwardRef((props, ref) => { const { prefixCls = 'rc-collapse', destroyOnHidden = false, style, accordion, className, children, collapsible, openMotion, expandIcon, activeKey: rawActiveKey, defaultActiveKey, onChange, items, classNames: customizeClassNames, styles, } = props; const collapseClassName = clsx(prefixCls, className); const [internalActiveKey, setActiveKey] = useControlledState( defaultActiveKey, rawActiveKey, ); const activeKey = getActiveKeysArray(internalActiveKey); const triggerActiveKey = useEvent((next) => { const nextKeys = getActiveKeysArray(next); setActiveKey(nextKeys); onChange?.(nextKeys); }); const onItemClick = (key: React.Key) => { if (accordion) { triggerActiveKey(activeKey[0] === key ? [] : [key]); } else { triggerActiveKey( activeKey.includes(key) ? activeKey.filter((item) => item !== key) : [...activeKey, key], ); } }; // ======================== Children ======================== warning( !children, '[rc-collapse] `children` will be removed in next major version. Please use `items` instead.', ); const mergedChildren = useItems(items, children, { prefixCls, accordion, openMotion, expandIcon, collapsible, destroyOnHidden, onItemClick, activeKey, classNames: customizeClassNames, styles, }); // ======================== Render ======================== return (
{mergedChildren}
); }); export default Object.assign(Collapse, { /** * @deprecated use `items` instead, will be removed in `v4.0.0` */ Panel: CollapsePanel, }); ================================================ FILE: src/Panel.tsx ================================================ import { clsx } from 'clsx'; import CSSMotion from '@rc-component/motion'; import KeyCode from '@rc-component/util/lib/KeyCode'; import React from 'react'; import type { CollapsePanelProps } from './interface'; import PanelContent from './PanelContent'; const CollapsePanel = React.forwardRef((props, ref) => { const { showArrow = true, headerClass, isActive, onItemClick, forceRender, className, classNames: customizeClassNames = {}, styles = {}, prefixCls, collapsible, accordion, panelKey, extra, header, expandIcon, openMotion, destroyOnHidden, children, ...resetProps } = props; const disabled = collapsible === 'disabled'; const ifExtraExist = extra !== null && extra !== undefined && typeof extra !== 'boolean'; const collapsibleProps = { onClick: () => { onItemClick?.(panelKey); }, onKeyDown: (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.keyCode === KeyCode.ENTER || e.which === KeyCode.ENTER) { onItemClick?.(panelKey); } }, role: accordion ? 'tab' : 'button', ['aria-expanded']: isActive, ['aria-disabled']: disabled, tabIndex: disabled ? -1 : 0, }; // ======================== Icon ======================== const iconNodeInner = typeof expandIcon === 'function' ? expandIcon(props) : ; const iconNode = iconNodeInner && (
{iconNodeInner}
); const collapsePanelClassNames = clsx( `${prefixCls}-item`, { [`${prefixCls}-item-active`]: isActive, [`${prefixCls}-item-disabled`]: disabled, }, className, ); const headerClassName = clsx( headerClass, `${prefixCls}-header`, { [`${prefixCls}-collapsible-${collapsible}`]: !!collapsible, }, customizeClassNames?.header, ); // ======================== HeaderProps ======================== const headerProps: React.HTMLAttributes = { className: headerClassName, style: styles?.header, ...(['header', 'icon'].includes(collapsible) ? {} : collapsibleProps), }; // ======================== Render ======================== return (
{showArrow && iconNode} {header} {ifExtraExist &&
{extra}
}
{({ className: motionClassName, style: motionStyle }, motionRef) => { return ( {children} ); }}
); }); export default CollapsePanel; ================================================ FILE: src/PanelContent.tsx ================================================ import { clsx } from 'clsx'; import React from 'react'; import type { CollapsePanelProps } from './interface'; const PanelContent = React.forwardRef< HTMLDivElement, React.PropsWithChildren> >((props, ref) => { const { prefixCls, forceRender, className, style, children, isActive, role, classNames: customizeClassNames, styles, } = props; const [rendered, setRendered] = React.useState(isActive || forceRender); React.useEffect(() => { if (forceRender || isActive) { setRendered(true); } }, [forceRender, isActive]); if (!rendered) { return null; } return (
{children}
); }); if (process.env.NODE_ENV !== 'production') { PanelContent.displayName = 'PanelContent'; } export default PanelContent; ================================================ FILE: src/hooks/useItems.tsx ================================================ import toArray from '@rc-component/util/lib/Children/toArray'; import React from 'react'; import type { CollapsePanelProps, CollapseProps, ItemType } from '../interface'; import CollapsePanel from '../Panel'; import clsx from 'clsx'; type Props = Pick< CollapsePanelProps, 'prefixCls' | 'onItemClick' | 'openMotion' | 'expandIcon' | 'classNames' | 'styles' > & Pick & { activeKey: React.Key[]; }; function mergeSemantic(src: T, tgt: T, mergeFn: (a: any, b: any) => any) { if (!src || !tgt) { return src || tgt; } const keys = Array.from(new Set([...Object.keys(src), ...Object.keys(tgt)])); const result = {}; keys.forEach((key) => { result[key] = mergeFn(src[key], tgt[key]); }); return result; } function mergeSemanticClassNames(src: T, tgt: T) { return mergeSemantic(src, tgt, (a: string, b: string) => clsx(a, b)); } function mergeSemanticStyles(src: T, tgt: T) { return mergeSemantic(src, tgt, (a: React.CSSProperties, b: React.CSSProperties) => ({ ...a, ...b, })); } const convertItemsToNodes = (items: ItemType[], props: Props) => { const { prefixCls, accordion, collapsible, destroyOnHidden, onItemClick, activeKey, openMotion, expandIcon, classNames: collapseClassNames, styles: collapseStyles, } = props; return items.map((item, index) => { const { children, label, key: rawKey, collapsible: rawCollapsible, onItemClick: rawOnItemClick, destroyOnHidden: rawDestroyOnHidden, classNames, styles, ...restProps } = item; // You may be puzzled why you want to convert them all into strings, me too. // Maybe: https://github.com/react-component/collapse/blob/aac303a8b6ff30e35060b4f8fecde6f4556fcbe2/src/Collapse.tsx#L15 const key = String(rawKey ?? index); const mergeCollapsible = rawCollapsible ?? collapsible; const mergedDestroyOnHidden = rawDestroyOnHidden ?? destroyOnHidden; const handleItemClick = (value: React.Key) => { if (mergeCollapsible === 'disabled') { return; } onItemClick(value); rawOnItemClick?.(value); }; let isActive = false; if (accordion) { isActive = activeKey[0] === key; } else { isActive = activeKey.indexOf(key) > -1; } return ( {children} ); }); }; /** * @deprecated The next major version will be removed */ const getNewChild = ( child: React.ReactElement, index: number, props: Props, ) => { if (!child) { return null; } const { prefixCls, accordion, collapsible, destroyOnHidden, onItemClick, activeKey, openMotion, expandIcon, classNames: collapseClassNames, styles, } = props; const key = child.key || String(index); const { header, headerClass, destroyOnHidden: childDestroyOnHidden, collapsible: childCollapsible, onItemClick: childOnItemClick, } = child.props; let isActive = false; if (accordion) { isActive = activeKey[0] === key; } else { isActive = activeKey.indexOf(key) > -1; } const mergeCollapsible = childCollapsible ?? collapsible; const handleItemClick = (value: React.Key) => { if (mergeCollapsible === 'disabled') { return; } onItemClick(value); childOnItemClick?.(value); }; const childProps = { key, panelKey: key, header, headerClass, classNames: collapseClassNames, styles, isActive, prefixCls, destroyOnHidden: childDestroyOnHidden ?? destroyOnHidden, openMotion, accordion, children: child.props.children, onItemClick: handleItemClick, expandIcon, collapsible: mergeCollapsible, }; // https://github.com/ant-design/ant-design/issues/20479 if (typeof child.type === 'string') { return child; } Object.keys(childProps).forEach((propName) => { if (typeof childProps[propName] === 'undefined') { delete childProps[propName]; } }); return React.cloneElement(child, childProps); }; function useItems( items?: ItemType[], rawChildren?: React.ReactNode, props?: Props, ): React.ReactElement[] { if (Array.isArray(items)) { return convertItemsToNodes(items, props); } return toArray(rawChildren).map((child, index) => getNewChild(child as React.ReactElement, index, props), ); } export default useItems; ================================================ FILE: src/index.tsx ================================================ import Collapse from './Collapse'; export type { CollapsePanelProps, CollapseProps } from './interface'; export default Collapse; /** * @deprecated use `items` instead, will be removed in `v4.0.0` */ export const { Panel } = Collapse; ================================================ FILE: src/interface.ts ================================================ import type { CSSMotionProps } from '@rc-component/motion'; import type * as React from 'react'; export type CollapsibleType = 'header' | 'icon' | 'disabled'; export interface ItemType extends Omit< CollapsePanelProps, | 'header' // alias of label | 'prefixCls' | 'panelKey' // alias of key | 'isActive' | 'accordion' | 'openMotion' | 'expandIcon' > { key?: CollapsePanelProps['panelKey']; label?: CollapsePanelProps['header']; ref?: React.RefObject; } export interface CollapseProps { prefixCls?: string; activeKey?: React.Key | React.Key[]; defaultActiveKey?: React.Key | React.Key[]; openMotion?: CSSMotionProps; onChange?: (key: React.Key[]) => void; accordion?: boolean; className?: string; style?: object; destroyOnHidden?: boolean; expandIcon?: (props: object) => React.ReactNode; collapsible?: CollapsibleType; children?: React.ReactNode; /** * Collapse items content * @since 3.6.0 */ items?: ItemType[]; classNames?: Partial>; styles?: Partial>; } export type SemanticName = 'header' | 'title' | 'body' | 'icon'; export interface CollapsePanelProps extends React.DOMAttributes { id?: string; header?: React.ReactNode; prefixCls?: string; headerClass?: string; showArrow?: boolean; className?: string; classNames?: Partial>; style?: object; styles?: Partial>; isActive?: boolean; openMotion?: CSSMotionProps; destroyOnHidden?: boolean; accordion?: boolean; forceRender?: boolean; extra?: React.ReactNode; onItemClick?: (panelKey: React.Key) => void; expandIcon?: (props: object) => React.ReactNode; panelKey?: React.Key; role?: string; collapsible?: CollapsibleType; children?: React.ReactNode; } ================================================ FILE: tests/__snapshots__/index.spec.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`collapse props items should work with nested 1`] = `
`; ================================================ FILE: tests/index.spec.tsx ================================================ import type { RenderResult } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react'; import KeyCode from '@rc-component/util/lib/KeyCode'; import React, { Fragment } from 'react'; import Collapse, { Panel } from '../src/index'; import type { CollapseProps, ItemType } from '../src/interface'; describe('collapse', () => { let changeHook: jest.Mock | null; beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); changeHook = null; }); function onChange(...args: any[]) { if (changeHook) { // eslint-disable-next-line @typescript-eslint/no-invalid-this changeHook.apply(this, args); } } function runNormalTest(element: any) { let collapse: RenderResult; beforeEach(() => { collapse = render(element); }); afterEach(() => { collapse.unmount(); }); it('add className', () => { const expectedClassName = 'rc-collapse-item important'; expect(collapse.container.querySelectorAll('.rc-collapse-item')?.[2]).toHaveClass( expectedClassName, ); }); it('create works', () => { expect(collapse.container.querySelectorAll('.rc-collapse')).toHaveLength(1); }); it('header works', () => { expect(collapse.container.querySelectorAll('.rc-collapse-header')).toHaveLength(3); }); it('panel works', () => { expect(collapse.container.querySelectorAll('.rc-collapse-item')).toHaveLength(3); expect(collapse.container.querySelectorAll('.rc-collapse-panel')).toHaveLength(0); }); it('should render custom arrow icon correctly', () => { expect(collapse.container.querySelector('.rc-collapse-header')?.textContent).toContain( 'test>', ); }); it('default active works', () => { expect(collapse.container.querySelectorAll('.rc-collapse-item-active').length).toBeFalsy(); }); it('extra works', () => { const extraNodes = collapse.container.querySelectorAll('.rc-collapse-extra'); expect(extraNodes).toHaveLength(1); expect(extraNodes?.[0]?.innerHTML).toBe('ExtraSpan'); }); it('onChange works', () => { changeHook = jest.fn(); const header = collapse.container.querySelectorAll('.rc-collapse-header')?.[1]; fireEvent.click(header); expect(changeHook.mock.calls[0][0]).toEqual(['2']); }); it('click should toggle panel state', () => { const header = collapse.container.querySelectorAll('.rc-collapse-header')?.[1]; fireEvent.click(header); jest.runAllTimers(); expect(collapse.container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(1); fireEvent.click(header); jest.runAllTimers(); expect(collapse.container.querySelector('.rc-collapse-panel-inactive')?.innerHTML).toBe( '
second
', ); expect(collapse.container.querySelectorAll('.rc-collapse-panel-active').length).toBeFalsy(); }); it('click should not toggle disabled panel state', () => { const header = collapse.container.querySelector('.rc-collapse-header'); expect(header).toBeTruthy(); fireEvent.click(header!); jest.runAllTimers(); expect(collapse.container.querySelectorAll('.rc-collapse-panel-active').length).toBeFalsy(); }); it('should not have role', () => { const item = collapse.container.querySelector('.rc-collapse'); expect(item).toBeTruthy(); expect(item!.getAttribute('role')).toBe(null); }); it('should set button role on panel title', () => { const item = collapse.container.querySelector('.rc-collapse-header'); expect(item).toBeTruthy(); expect(item!.getAttribute('role')).toBe('button'); }); } describe('collapse', () => { const expandIcon = () => test{'>'}; const element = ( first ExtraSpan}> second third ); runNormalTest(element); it('controlled', () => { const onChangeSpy = jest.fn(); const ControlledCollapse = () => { const [activeKey, updateActiveKey] = React.useState(['2']); const handleChange: CollapseProps['onChange'] = (key) => { updateActiveKey(key); onChangeSpy(key); }; return ( first second third ); }; const { container } = render(); expect(container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(1); const header = container.querySelector('.rc-collapse-header'); expect(header).toBeTruthy(); fireEvent.click(header!); jest.runAllTimers(); expect(container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(2); expect(onChangeSpy).toBeCalledWith(['2', '1']); }); }); describe('it should support number key', () => { const expandIcon = () => test{'>'}; const element = ( first ExtraSpan}> second third ); runNormalTest(element); }); describe('prop: headerClass', () => { it('applies the passed headerClass to the header', () => { const element = ( first ); const { container } = render(element); const header = container.querySelector('.rc-collapse-header'); expect(header?.classList.contains('custom-class')).toBeTruthy(); }); }); it('should support extra whit number 0', () => { const { container } = render( zero , ); const extraNodes = container.querySelectorAll('.rc-collapse-extra'); expect(extraNodes).toHaveLength(1); expect(extraNodes[0].innerHTML).toBe('0'); }); it('should support activeKey number 0', () => { const { container } = render( zero first second , ); // activeKey number 0, should open one item expect(container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(1); }); it('click should toggle panel state', () => { const { container } = render( first second third , ); const header = container.querySelectorAll('.rc-collapse-header')?.[1]; fireEvent.click(header); expect(container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(1); fireEvent.click(header); expect(container.querySelectorAll('.rc-collapse-panel-inactive').length).toBeFalsy(); }); function runAccordionTest(element: React.ReactElement) { let collapse: RenderResult; beforeEach(() => { collapse = render(element); }); afterEach(() => { collapse.unmount(); }); it('accordion content, should default open zero item', () => { expect(collapse.container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(0); }); it('accordion item, should default open zero item', () => { expect(collapse.container.querySelectorAll('.rc-collapse-item-active')).toHaveLength(0); }); it('should toggle show on panel', () => { let header = collapse.container.querySelectorAll('.rc-collapse-header')?.[1]; fireEvent.click(header); jest.runAllTimers(); expect(collapse.container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(1); expect(collapse.container.querySelectorAll('.rc-collapse-item-active')).toHaveLength(1); header = collapse.container.querySelectorAll('.rc-collapse-header')?.[1]; fireEvent.click(header); jest.runAllTimers(); expect(collapse.container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(0); expect(collapse.container.querySelectorAll('.rc-collapse-item-active')).toHaveLength(0); }); it('should only show on panel', () => { let header = collapse.container.querySelector('.rc-collapse-header'); expect(header).toBeTruthy(); fireEvent.click(header!); jest.runAllTimers(); expect(collapse.container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(1); expect(collapse.container.querySelectorAll('.rc-collapse-item-active')).toHaveLength(1); header = collapse.container.querySelectorAll('.rc-collapse-header')?.[2]; fireEvent.click(header); jest.runAllTimers(); expect(collapse.container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(1); expect(collapse.container.querySelectorAll('.rc-collapse-item-active')).toHaveLength(1); }); it('should add tab role on panel title', () => { const item = collapse.container.querySelector('.rc-collapse-header'); expect(item).toBeTruthy(); expect(item!.getAttribute('role')).toBe('tab'); }); it('should add tablist role on accordion', () => { const item = collapse.container.querySelector('.rc-collapse'); expect(item).toBeTruthy(); expect(item!.getAttribute('role')).toBe('tablist'); }); it('should add tablist role on PanelContent', () => { const header = collapse.container.querySelector('.rc-collapse-header'); expect(header).toBeTruthy(); fireEvent.click(header!); const item = collapse.container.querySelector('.rc-collapse-panel'); expect(item).toBeTruthy(); expect(item!.getAttribute('role')).toBe('tabpanel'); }); } describe('prop: accordion', () => { runAccordionTest( first second third , ); }); describe('forceRender', () => { it('when forceRender is not supplied it should lazy render the panel content', () => { const { container } = render( first second , ); expect(container.querySelectorAll('.rc-collapse-panel')).toHaveLength(0); }); it('when forceRender is FALSE it should lazy render the panel content', () => { const { container } = render( first second , ); expect(container.querySelectorAll('.rc-collapse-panel')).toHaveLength(0); }); it('when forceRender is TRUE then it should render all the panel content to the DOM', () => { const { container } = render( first second , ); jest.runAllTimers(); expect(container.querySelectorAll('.rc-collapse-panel')).toHaveLength(1); expect(container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(0); const inactiveDom = container.querySelector('div.rc-collapse-panel-inactive'); expect(inactiveDom).not.toBeFalsy(); expect(getComputedStyle(inactiveDom!)).toHaveProperty('display', 'none'); }); }); it('should toggle panel when press enter', () => { const myKeyEvent = { key: 'Enter', keyCode: KeyCode.ENTER, which: KeyCode.ENTER, // https://github.com/testing-library/react-testing-library/issues/269#issuecomment-455854112 charCode: KeyCode.ENTER, }; const { container } = render( first second third , ); fireEvent.keyDown(container.querySelectorAll('.rc-collapse-header')?.[2], myKeyEvent); jest.runAllTimers(); expect(container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(0); fireEvent.keyDown(container.querySelector('.rc-collapse-header')!, myKeyEvent); jest.runAllTimers(); expect(container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(1); expect(container.querySelector('.rc-collapse-panel')).toHaveClass('rc-collapse-panel-active'); fireEvent.keyDown(container.querySelector('.rc-collapse-header')!, myKeyEvent); jest.runAllTimers(); expect(container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(0); expect(container.querySelector('.rc-collapse-panel')!.className).not.toContain( 'rc-collapse-panel-active', ); }); describe('wrapped in Fragment', () => { const expandIcon = () => test{'>'}; const element = ( first ExtraSpan}> second third ); runNormalTest(element); }); it('should support return null icon', () => { const { container } = render( null}> first , ); expect(container.querySelector('.rc-collapse-header')?.childNodes).toHaveLength(1); }); it('should support custom child', () => { const { container } = render( first custom-child , ); expect(container.querySelector('.custom-child')?.innerHTML).toBe('custom-child'); }); // https://github.com/ant-design/ant-design/issues/36327 // https://github.com/ant-design/ant-design/issues/6179 // https://github.com/react-component/collapse/issues/73#issuecomment-323626120 it('should support custom component', () => { const PanelElement = (props) => (

test

); const { container } = render( second , ); expect(container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(1); expect(container.querySelector('.rc-collapse-panel')).toHaveClass('rc-collapse-panel-active'); expect(container.querySelector('.rc-collapse-header')?.textContent).toBe('collapse 1'); expect(container.querySelector('.rc-collapse-header')?.querySelectorAll('.arrow')).toHaveLength( 1, ); fireEvent.click(container.querySelector('.rc-collapse-header')!); expect(container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(0); expect(container.querySelector('.rc-collapse-panel')).toHaveClass('rc-collapse-panel-inactive'); }); describe('prop: collapsible', () => { it('default', () => { const { container } = render( first , ); expect(container.querySelector('.rc-collapse-title')).toBeTruthy(); fireEvent.click(container.querySelector('.rc-collapse-header')!); expect(container.querySelectorAll('.rc-collapse-item-active')).toHaveLength(1); }); it('should work when value is header', () => { const { container } = render( first , ); expect(container.querySelector('.rc-collapse-title')).toBeTruthy(); fireEvent.click(container.querySelector('.rc-collapse-header')!); expect(container.querySelectorAll('.rc-collapse-item-active')).toHaveLength(0); fireEvent.click(container.querySelector('.rc-collapse-title')!); expect(container.querySelectorAll('.rc-collapse-item-active')).toHaveLength(1); }); it('should work when value is icon', () => { const { container } = render( first , ); expect(container.querySelector('.rc-collapse-expand-icon')).toBeTruthy(); fireEvent.click(container.querySelector('.rc-collapse-header')!); expect(container.querySelectorAll('.rc-collapse-item-active')).toHaveLength(0); fireEvent.click(container.querySelector('.rc-collapse-expand-icon')!); expect(container.querySelectorAll('.rc-collapse-item-active')).toHaveLength(1); }); it('should disabled when value is disabled', () => { const { container } = render( first , ); expect(container.querySelector('.rc-collapse-title')).toBeTruthy(); expect(container.querySelectorAll('.rc-collapse-item-disabled')).toHaveLength(1); fireEvent.click(container.querySelector('.rc-collapse-header')!); expect(container.querySelectorAll('.rc-collapse-item-active')).toHaveLength(0); }); it('the value of panel should be read first', () => { const { container } = render( first , ); expect(container.querySelector('.rc-collapse-title')).toBeTruthy(); expect(container.querySelectorAll('.rc-collapse-item-disabled')).toHaveLength(1); fireEvent.click(container.querySelector('.rc-collapse-header')!); expect(container.querySelectorAll('.rc-collapse-item-active')).toHaveLength(0); }); it('icon trigger when collapsible equal header', () => { const { container } = render( first , ); fireEvent.click(container.querySelector('.rc-collapse-header .arrow')!); expect(container.querySelectorAll('.rc-collapse-item-active')).toHaveLength(1); }); it('header not trigger when collapsible equal icon', () => { const { container } = render( first , ); fireEvent.click(container.querySelector('.rc-collapse-title')!); expect(container.querySelectorAll('.rc-collapse-item-active')).toHaveLength(0); }); }); it('!showArrow', () => { const { container } = render( first , ); expect(container.querySelectorAll('.rc-collapse-expand-icon')).toHaveLength(0); }); it('Panel container dom can set event handler', () => { const clickHandler = jest.fn(); const { container } = render(
Click this
, ); fireEvent.click(container.querySelector('.target')!); expect(clickHandler).toHaveBeenCalled(); }); it('falsy Panel', () => { const { container } = render( {null}

Panel 1 content

{0}

Panel 2 content

{undefined} {false} {true}
, ); expect(container.querySelectorAll('.rc-collapse-item')).toHaveLength(2); }); it('ref should work', () => { const ref = React.createRef(); const panelRef = React.createRef(); const { container } = render( first , ); expect(ref.current).toBe(container.firstChild); expect(panelRef.current).toBe(container.querySelector('.rc-collapse-item')); }); // https://github.com/react-component/collapse/issues/235 it('onItemClick should work', () => { const onItemClick = jest.fn(); const { container } = render( first , ); fireEvent.click(container.querySelector('.rc-collapse-header')!); expect(onItemClick).toHaveBeenCalled(); }); it('onItemClick should not work when collapsible is disabled', () => { const onItemClick = jest.fn(); const { container } = render( first , ); fireEvent.click(container.querySelector('.rc-collapse-header')!); expect(onItemClick).not.toHaveBeenCalled(); }); it('panel style should work', () => { const { container } = render( first , ); expect(container.querySelector('.rc-collapse-item')).toHaveStyle({ color: 'red' }); }); describe('props items', () => { const items: ItemType[] = [ { key: '1', label: 'collapse 1', children: 'first', collapsible: 'disabled', }, { key: '2', label: 'collapse 2', children: 'second', extra: ExtraSpan, }, { key: '3', label: 'collapse 3', className: 'important', children: 'third', }, ]; runNormalTest( test{'>'}} items={items} />, ); runAccordionTest( , ); it('should work with onItemClick', () => { const onItemClick = jest.fn(); const { container } = render( , ); fireEvent.click(container.querySelector('.rc-collapse-header')!); expect(onItemClick).toHaveBeenCalled(); expect(onItemClick).lastCalledWith('0'); }); it('should work with collapsible', () => { const onItemClick = jest.fn(); const onChangeFn = jest.fn(); const { container } = render( , ); fireEvent.click(container.querySelector('.rc-collapse-header')!); expect(onItemClick).not.toHaveBeenCalled(); fireEvent.click( container.querySelector('.rc-collapse-item:nth-child(2) .rc-collapse-expand-icon')!, ); expect(onItemClick).toHaveBeenCalled(); expect(onChangeFn).toBeCalledTimes(1); expect(onChangeFn).lastCalledWith(['1']); }); it('should work with nested', () => { const { container } = render( , }, ]} />, ); expect(container.firstChild).toMatchSnapshot(); }); it('should not support expandIcon', () => { const { container } = render( p} items={[ { label: 'title', expandIcon: () => c, } as any, ]} />, ); expect(container.querySelectorAll('.custom-icon')).toHaveLength(1); expect(container.querySelector('.custom-icon')?.innerHTML).toBe('p'); }); it('should support data- and aria- attributes', () => { const { container } = render( , ); expect(container.querySelector('.rc-collapse')?.getAttribute('data-testid')).toBe('1234'); expect(container.querySelector('.rc-collapse')?.getAttribute('aria-label')).toBe('test'); }); it('should support styles and classNames', () => { const customStyles = { header: { color: 'red' }, body: { color: 'blue' }, title: { color: 'green' }, icon: { color: 'yellow' }, }; const customClassnames = { header: 'custom-header', body: 'custom-body', title: 'custom-title', icon: 'custom-icon', }; const { container } = render( , ); const headerElement = container.querySelector('.rc-collapse-header') as HTMLElement; const bodyElement = container.querySelector('.rc-collapse-body') as HTMLElement; const titleElement = container.querySelector('.rc-collapse-title') as HTMLElement; const iconElement = container.querySelector('.rc-collapse-expand-icon') as HTMLElement; // check classNames expect(headerElement.classList).toContain('custom-header'); expect(bodyElement.classList).toContain('custom-body'); expect(titleElement.classList).toContain('custom-title'); expect(iconElement.classList).toContain('custom-icon'); // check styles expect(headerElement.style.color).toBe('red'); expect(bodyElement.style.color).toBe('blue'); expect(titleElement.style.color).toBe('green'); expect(iconElement.style.color).toBe('yellow'); }); it('should support styles and classNames in panel', () => { const customStyles = { header: { color: 'red' }, body: { color: 'blue' }, title: { color: 'green' }, icon: { color: 'yellow' }, }; const customClassnames = { header: 'custom-header', body: 'custom-body', }; const { container } = render( , ); const headerElement = container.querySelector('.rc-collapse-header') as HTMLElement; const bodyElement = container.querySelector('.rc-collapse-body') as HTMLElement; const titleElement = container.querySelector('.rc-collapse-title') as HTMLElement; const iconElement = container.querySelector('.rc-collapse-expand-icon') as HTMLElement; // check classNames expect(headerElement.classList).toContain('custom-header'); expect(headerElement.classList).toContain('custom-header-panel'); expect(bodyElement.classList).toContain('custom-body'); expect(bodyElement.classList).toContain('custom-body-panel'); // check styles expect(headerElement).toHaveStyle({ color: 'blue', fontSize: '20px' }); expect(bodyElement).toHaveStyle({ color: 'blue', fontSize: '20px' }); expect(titleElement).toHaveStyle({ color: 'red' }); expect(iconElement).toHaveStyle({ color: 'blue' }); }); }); }); ================================================ FILE: tests/setupAfterEnv.ts ================================================ import '@testing-library/jest-dom'; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "esnext", "moduleResolution": "node", "baseUrl": "./", "jsx": "react", "declaration": true, "skipLibCheck": true, "esModuleInterop": true, "paths": { "@/*": ["src/*"], "@@/*": ["src/.dumi/*"], "rc-collapse": ["src/index.tsx"] } }, "include": [ ".dumirc.ts", "./src/**/*.ts", "./src/**/*.tsx", "./tests/**/*.ts", "./tests/**/*.tsx", "./docs/**/*.tsx" ] } ================================================ FILE: vercel.json ================================================ { "framework": "umijs" }