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
[](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(
,
,
);
const tools = (
<>
reRender({})}>
reRender
setAccordion((prev) => !prev)}>
{accordion ? 'Mode: accordion' : 'Mode: collapse'}
setActiveKey(['2'])}>
active header 2
>
);
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(
,
,
Extra Node}
>
Panel with extra
,
);
const handleCollapsibleChange = (e: React.ChangeEvent) => {
const values = [undefined, 'header', 'icon', 'disabled'];
setCollapsible(values[e.target.value]);
};
const tools = (
<>
reRender({})}>
reRender
setAccordion((prev) => !prev)}>
{accordion ? 'Mode: accordion' : 'Mode: collapse'}
collapsible:
default
header
icon
disabled
setActiveKey(['2'])}>
active header 2
>
);
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 (
);
});
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"
}