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 = (
<Collapse accordion={true}>
<Panel header="hello" headerClass="my-header-class">
this is panel content
</Panel>
<Panel header="title2">this is panel content2 or other</Panel>
</Collapse>
);
ReactDOM.render(App, container);
```
## Features
- support ie8,ie8+,chrome,firefox,safari
## API
### Collapse props
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="width: 100px;">name</th>
<th style="width: 50px;">type</th>
<th>default</th>
<th>description</th>
</tr>
</thead>
<tbody>
<tr>
<td>activeKey</td>
<td>String|Array<String></td>
<th>The first panel key</th>
<td>current active Panel key</td>
</tr>
<tr>
<td>className</td>
<td>String or object</td>
<th></th>
<td>custom className to apply</td>
</tr>
<tr>
<td>defaultActiveKey</td>
<td>String|Array<String></td>
<th>null</th>
<td>default active key</td>
</tr>
<tr>
<td>destroyOnHidden</td>
<td>Boolean</td>
<th>false</th>
<td>If destroy the panel which not active, default false. </td>
</tr>
<tr>
<td>accordion</td>
<td>Boolean</td>
<th>false</th>
<td>accordion mode, default is null, is collapse mode</td>
</tr>
<tr>
<td>onChange</td>
<td>Function(key)</td>
<th>noop</th>
<td>called when collapse Panel is changed</td>
</tr>
<tr>
<td>expandIcon</td>
<td>(props: PanelProps) => ReactNode</td>
<th></th>
<td>specific the custom expand icon.</td>
</tr>
<tr>
<td>collapsible</td>
<td>'header' | 'icon' | 'disabled'</td>
<th>-</th>
<td>specify whether the panel of children is collapsible or the area of collapsible.</td>
</tr>
<tr>
<td>items</td>
<td>
<a href="./src/interface.ts#ItemType">interface.ts#ItemType</a>
</td>
<th>-</th>
<td>collapse items content</td>
</tr>
</tbody>
</table>
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`
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="width: 100px;">name</th>
<th style="width: 200px;">type</th>
<th>default</th>
<th>description</th>
</tr>
</thead>
<tbody>
<tr>
<td>header</td>
<td>String or node</td>
<th></th>
<td>header content of Panel</td>
</tr>
<tr>
<td>headerClass</td>
<td>String</td>
<th>' '</th>
<td>custom className to apply to header</td>
</tr>
<tr>
<td>showArrow</td>
<td>boolean</td>
<th>true</th>
<td>show arrow beside header</td>
</tr>
<tr>
<td>className</td>
<td>String or object</td>
<th></th>
<td>custom className to apply</td>
</tr>
<tr>
<td>classNames</td>
<td>{ header?: string, body?: string }</td>
<th></th>
<td>Semantic structure className</td>
</tr>
<tr>
<td>style</td>
<td>object</td>
<th></th>
<td>custom style</td>
</tr>
<tr>
<td>styles</td>
<td>{ header?: React.CSSProperties, body?: React.CSSProperties }</td>
<th></th>
<td>Semantic structure styles</td>
</tr>
<tr>
<td>openMotion</td>
<td>object</td>
<th></th>
<td>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.</td>
</tr>
<tr>
<td>forceRender</td>
<td>boolean</td>
<th>false</th>
<td>forced render of content in panel, not lazy render after clicking on header</td>
</tr>
<tr>
<td>extra</td>
<td>String | ReactNode</td>
<th></th>
<td>Content to render in the right of the panel header</td>
</tr>
<tr>
<td>collapsible</td>
<td>'header' | 'icon' | 'disabled'</td>
<th>-</th>
<td>specify whether the panel be collapsible or the area of collapsible.</td>
</tr>
</tbody>
</table>
> `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: <input onKeyDown={(e) => e.stopPropagation()} />,
children: 'content',
},
{
label: (
<div onKeyDown={(e) => e.stopPropagation()}>
<CustomComponent />
</div>
),
children: 'content',
},
{
label: 'title 2',
children: 'content 2',
collapsible: 'disabled',
},
{
label: 'title 3',
children: 'content 3',
onItemClick: console.log,
},
];
return <Collapse items={items} />;
};
```
## 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
---
<code src="../examples/basic.tsx"></code>
================================================
FILE: docs/demo/custom-icon.md
================================================
---
title: custom-icon
nav:
title: Demo
path: /demo
---
<code src="../examples/custom-icon.tsx"></code>
================================================
FILE: docs/demo/fragment.md
================================================
---
title: fragment
nav:
title: Demo
path: /demo
---
<code src="../examples/fragment.tsx"></code>
================================================
FILE: docs/demo/simple.md
================================================
---
title: simple
nav:
title: Demo
path: /demo
---
<code src="../examples/simple.tsx"></code>
================================================
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: <input onKeyDown={(e) => e.stopPropagation()} />,
children: 'content',
},
{
label: 'title 2',
children: 'content 2',
collapsible: 'disabled',
},
{
label: 'title 3',
children: 'content 3',
onItemClick: console.log,
},
];
return <Collapse items={items} />;
};
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 (
<i style={{ marginRight: '.5rem' }}>
<svg
viewBox="0 0 1024 1024"
width="1em"
height="1em"
fill="currentColor"
style={{
verticalAlign: '-.125em',
transition: 'transform .2s',
transform: `rotate(${isActive ? 90 : 0}deg)`,
}}
>
<path d={arrowPath} />
</svg>
</i>
);
}
const App: React.FC = () => {
const [, reRender] = React.useState({});
const [accordion, setAccordion] = React.useState(false);
const [activeKey, setActiveKey] = React.useState<React.Key | React.Key[]>(['4']);
const time = random();
const panelItems = Array.from<object, React.ReactNode>({ length: initLength }, (_, i) => {
const key = i + 1;
return (
<Panel header={`This is panel header ${key}`} key={key}>
<p>{text.repeat(time)}</p>
</Panel>
);
}).concat(
<Panel header={`This is panel header ${initLength + 1}`} key={initLength + 1}>
<Collapse defaultActiveKey="1" expandIcon={expandIcon}>
<Panel header="This is panel nest panel" key="1" id="header-test">
<p>{text}</p>
</Panel>
</Collapse>
</Panel>,
<Panel header={`This is panel header ${initLength + 2}`} key={initLength + 2}>
<Collapse defaultActiveKey="1">
<Panel header="This is panel nest panel" key="1" id="another-test">
<form>
<label htmlFor="test">Name: </label>
<input type="text" id="test" />
</form>
</Panel>
</Collapse>
</Panel>,
);
const tools = (
<>
<button type="button" onClick={() => reRender({})}>
reRender
</button>
<br />
<br />
<button type="button" onClick={() => setAccordion((prev) => !prev)}>
{accordion ? 'Mode: accordion' : 'Mode: collapse'}
</button>
<br />
<br />
<button type="button" onClick={() => setActiveKey(['2'])}>
active header 2
</button>
<br />
<br />
</>
);
return (
<>
{tools}
<Collapse
accordion={accordion}
onChange={setActiveKey}
activeKey={activeKey}
expandIcon={expandIcon}
openMotion={motion}
>
{panelItems}
</Collapse>
</>
);
};
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 = () => (
<Collapse>
<Panel header="title">content</Panel>
<Panel header="title">content</Panel>
<Fragment>
<Panel header="title">content</Panel>
<Panel header="title">content</Panel>
</Fragment>
<Fragment>
<Fragment>
<Panel header="title">content</Panel>
<Panel header="title">content</Panel>
</Fragment>
</Fragment>
</Collapse>
);
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 (
<i style={{ marginRight: '.5rem' }}>
<svg
viewBox="0 0 1024 1024"
width="1em"
height="1em"
fill="currentColor"
style={{
verticalAlign: '-.125em',
transition: 'transform .2s',
transform: `rotate(${isActive ? 90 : 0}deg)`,
}}
>
<path d={arrowPath} />
</svg>
</i>
);
}
const App: React.FC = () => {
const [, reRender] = React.useState({});
const [accordion, setAccordion] = React.useState(false);
const [activeKey, setActiveKey] = React.useState<React.Key | React.Key[]>(['4']);
const [collapsible, setCollapsible] = React.useState<CollapseProps['collapsible']>();
const time = random();
const panelItems = Array.from<object, React.ReactNode>({ length: initLength }, (_, i) => {
const key = i + 1;
return (
<Panel header={`This is panel header ${key}`} key={key}>
<p>{text.repeat(time)}</p>
</Panel>
);
}).concat(
<Panel header={`This is panel header ${initLength + 1}`} key={initLength + 1}>
<Collapse defaultActiveKey="1" expandIcon={expandIcon}>
<Panel header="This is panel nest panel" key="1" id="header-test">
<p>{text}</p>
</Panel>
</Collapse>
</Panel>,
<Panel header={`This is panel header ${initLength + 2}`} key={initLength + 2}>
<Collapse defaultActiveKey="1">
<Panel header="This is panel nest panel" key="1" id="another-test">
<form>
<label htmlFor="test">Name: </label>
<input type="text" id="test" />
</form>
</Panel>
</Collapse>
</Panel>,
<Panel
header={`This is panel header ${initLength + 3}`}
key={initLength + 3}
extra={<span>Extra Node</span>}
>
<p>Panel with extra</p>
</Panel>,
);
const handleCollapsibleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const values = [undefined, 'header', 'icon', 'disabled'];
setCollapsible(values[e.target.value]);
};
const tools = (
<>
<button type="button" onClick={() => reRender({})}>
reRender
</button>
<br />
<br />
<button type="button" onClick={() => setAccordion((prev) => !prev)}>
{accordion ? 'Mode: accordion' : 'Mode: collapse'}
</button>
<br />
<br />
<div>
collapsible:
<select onChange={handleCollapsibleChange}>
<option value={0}>default</option>
<option value={1}>header</option>
<option value={2}>icon</option>
<option value={3}>disabled</option>
</select>
</div>
<br />
<button type="button" onClick={() => setActiveKey(['2'])}>
active header 2
</button>
<br />
<br />
</>
);
return (
<>
{tools}
<Collapse
accordion={accordion}
onChange={setActiveKey}
activeKey={activeKey}
expandIcon={expandIcon}
openMotion={motion}
collapsible={collapsible}
>
{panelItems}
</Collapse>
</>
);
};
export default App;
================================================
FILE: docs/index.md
================================================
---
hero:
title: rc-collapse
description: rc-collapse ui component for react
---
<embed src="../README.md"></embed>
================================================
FILE: jest.config.js
================================================
module.exports = {
setupFilesAfterEnv: ['<rootDir>/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<HTMLDivElement, CollapseProps>((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<React.Key[] | React.Key>(
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 (
<div
ref={ref}
className={collapseClassName}
style={style}
role={accordion ? 'tablist' : undefined}
{...pickAttrs(props, { aria: true, data: true })}
>
{mergedChildren}
</div>
);
});
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<HTMLDivElement, CollapsePanelProps>((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) : <i className="arrow" />;
const iconNode = iconNodeInner && (
<div
className={clsx(`${prefixCls}-expand-icon`, customizeClassNames?.icon)}
style={styles?.icon}
{...(['header', 'icon'].includes(collapsible) ? collapsibleProps : {})}
>
{iconNodeInner}
</div>
);
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<HTMLDivElement> = {
className: headerClassName,
style: styles?.header,
...(['header', 'icon'].includes(collapsible) ? {} : collapsibleProps),
};
// ======================== Render ========================
return (
<div {...resetProps} ref={ref} className={collapsePanelClassNames}>
<div {...headerProps}>
{showArrow && iconNode}
<span
className={clsx(`${prefixCls}-title`, customizeClassNames?.title)}
style={styles?.title}
{...(collapsible === 'header' ? collapsibleProps : {})}
>
{header}
</span>
{ifExtraExist && <div className={`${prefixCls}-extra`}>{extra}</div>}
</div>
<CSSMotion
visible={isActive}
leavedClassName={`${prefixCls}-panel-hidden`}
{...openMotion}
forceRender={forceRender}
removeOnLeave={destroyOnHidden}
>
{({ className: motionClassName, style: motionStyle }, motionRef) => {
return (
<PanelContent
ref={motionRef}
prefixCls={prefixCls}
className={motionClassName}
classNames={customizeClassNames}
style={motionStyle}
styles={styles}
isActive={isActive}
forceRender={forceRender}
role={accordion ? 'tabpanel' : undefined}
>
{children}
</PanelContent>
);
}}
</CSSMotion>
</div>
);
});
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<Readonly<CollapsePanelProps>>
>((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 (
<div
ref={ref}
className={clsx(
`${prefixCls}-panel`,
{
[`${prefixCls}-panel-active`]: isActive,
[`${prefixCls}-panel-inactive`]: !isActive,
},
className,
)}
style={style}
role={role}
>
<div className={clsx(`${prefixCls}-body`, customizeClassNames?.body)} style={styles?.body}>
{children}
</div>
</div>
);
});
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<CollapseProps, 'accordion' | 'collapsible' | 'destroyOnHidden'> & {
activeKey: React.Key[];
};
function mergeSemantic<T>(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<T>(src: T, tgt: T) {
return mergeSemantic(src, tgt, (a: string, b: string) => clsx(a, b));
}
function mergeSemanticStyles<T>(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 (
<CollapsePanel
{...restProps}
classNames={mergeSemanticClassNames(collapseClassNames, classNames)}
styles={mergeSemanticStyles(collapseStyles, styles)}
prefixCls={prefixCls}
key={key}
panelKey={key}
isActive={isActive}
accordion={accordion}
openMotion={openMotion}
expandIcon={expandIcon}
header={label}
collapsible={mergeCollapsible}
onItemClick={handleItemClick}
destroyOnHidden={mergedDestroyOnHidden}
>
{children}
</CollapsePanel>
);
});
};
/**
* @deprecated The next major version will be removed
*/
const getNewChild = (
child: React.ReactElement<CollapsePanelProps>,
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<CollapsePanelProps>(child, childProps);
};
function useItems(
items?: ItemType[],
rawChildren?: React.ReactNode,
props?: Props,
): React.ReactElement<CollapsePanelProps>[] {
if (Array.isArray(items)) {
return convertItemsToNodes(items, props);
}
return toArray(rawChildren).map((child, index) =>
getNewChild(child as React.ReactElement<CollapsePanelProps>, 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<HTMLDivElement>;
}
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<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
}
export type SemanticName = 'header' | 'title' | 'body' | 'icon';
export interface CollapsePanelProps extends React.DOMAttributes<HTMLDivElement> {
id?: string;
header?: React.ReactNode;
prefixCls?: string;
headerClass?: string;
showArrow?: boolean;
className?: string;
classNames?: Partial<Record<SemanticName, string>>;
style?: object;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
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`] = `
<div
class="rc-collapse"
>
<div
class="rc-collapse-item rc-collapse-item-disabled"
>
<div
aria-disabled="true"
aria-expanded="false"
class="rc-collapse-header rc-collapse-collapsible-disabled"
role="button"
tabindex="-1"
>
<div
class="rc-collapse-expand-icon"
>
<i
class="arrow"
/>
</div>
<span
class="rc-collapse-title"
>
collapse 1
</span>
</div>
</div>
<div
class="rc-collapse-item"
>
<div
aria-disabled="false"
aria-expanded="false"
class="rc-collapse-header"
role="button"
tabindex="0"
>
<div
class="rc-collapse-expand-icon"
>
<i
class="arrow"
/>
</div>
<span
class="rc-collapse-title"
>
collapse 2
</span>
<div
class="rc-collapse-extra"
>
<span>
ExtraSpan
</span>
</div>
</div>
</div>
<div
class="rc-collapse-item important"
>
<div
aria-disabled="false"
aria-expanded="false"
class="rc-collapse-header"
role="button"
tabindex="0"
>
<div
class="rc-collapse-expand-icon"
>
<i
class="arrow"
/>
</div>
<span
class="rc-collapse-title"
>
collapse 3
</span>
</div>
</div>
<div
class="rc-collapse-item"
>
<div
aria-disabled="false"
aria-expanded="false"
class="rc-collapse-header"
role="button"
tabindex="0"
>
<div
class="rc-collapse-expand-icon"
>
<i
class="arrow"
/>
</div>
<span
class="rc-collapse-title"
>
title 3
</span>
</div>
</div>
</div>
`;
================================================
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<any, any> | 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('<span>ExtraSpan</span>');
});
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(
'<div class="rc-collapse-body">second</div>',
);
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 = () => <span>test{'>'}</span>;
const element = (
<Collapse onChange={onChange} expandIcon={expandIcon}>
<Panel header="collapse 1" key="1" collapsible="disabled">
first
</Panel>
<Panel header="collapse 2" key="2" extra={<span>ExtraSpan</span>}>
second
</Panel>
<Panel header="collapse 3" key="3" className="important">
third
</Panel>
</Collapse>
);
runNormalTest(element);
it('controlled', () => {
const onChangeSpy = jest.fn();
const ControlledCollapse = () => {
const [activeKey, updateActiveKey] = React.useState<React.Key[] | React.Key>(['2']);
const handleChange: CollapseProps['onChange'] = (key) => {
updateActiveKey(key);
onChangeSpy(key);
};
return (
<Collapse onChange={handleChange} activeKey={activeKey}>
<Panel header="collapse 1" key="1">
first
</Panel>
<Panel header="collapse 2" key="2">
second
</Panel>
<Panel header="collapse 3" key="3">
third
</Panel>
</Collapse>
);
};
const { container } = render(<ControlledCollapse />);
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 = () => <span>test{'>'}</span>;
const element = (
<Collapse onChange={onChange} expandIcon={expandIcon}>
<Panel header="collapse 1" key={1} collapsible="disabled">
first
</Panel>
<Panel header="collapse 2" key={2} extra={<span>ExtraSpan</span>}>
second
</Panel>
<Panel header="collapse 3" key={3} className="important">
third
</Panel>
</Collapse>
);
runNormalTest(element);
});
describe('prop: headerClass', () => {
it('applies the passed headerClass to the header', () => {
const element = (
<Collapse onChange={onChange}>
<Panel header="collapse 1" key="1" headerClass="custom-class">
first
</Panel>
</Collapse>
);
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(
<Collapse onChange={onChange} activeKey={0}>
<Panel header="collapse 0" key={0} extra={0}>
zero
</Panel>
</Collapse>,
);
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(
<Collapse onChange={onChange} activeKey={0}>
<Panel header="collapse 0" key={0}>
zero
</Panel>
<Panel header="collapse 1" key={1}>
first
</Panel>
<Panel header="collapse 2" key={2}>
second
</Panel>
</Collapse>,
);
// 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(
<Collapse onChange={onChange} destroyOnHidden>
<Panel header="collapse 1" key="1">
first
</Panel>
<Panel header="collapse 2" key="2">
second
</Panel>
<Panel header="collapse 3" key="3" className="important">
third
</Panel>
</Collapse>,
);
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(
<Collapse onChange={onChange} accordion>
<Panel header="collapse 1" key="1">
first
</Panel>
<Panel header="collapse 2" key="2">
second
</Panel>
<Panel header="collapse 3" key="3">
third
</Panel>
</Collapse>,
);
});
describe('forceRender', () => {
it('when forceRender is not supplied it should lazy render the panel content', () => {
const { container } = render(
<Collapse>
<Panel header="collapse 1" key="1" collapsible="disabled">
first
</Panel>
<Panel header="collapse 2" key="2">
second
</Panel>
</Collapse>,
);
expect(container.querySelectorAll('.rc-collapse-panel')).toHaveLength(0);
});
it('when forceRender is FALSE it should lazy render the panel content', () => {
const { container } = render(
<Collapse>
<Panel header="collapse 1" key="1" forceRender={false} collapsible="disabled">
first
</Panel>
<Panel header="collapse 2" key="2">
second
</Panel>
</Collapse>,
);
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(
<Collapse>
<Panel header="collapse 1" key="1" forceRender collapsible="disabled">
first
</Panel>
<Panel header="collapse 2" key="2">
second
</Panel>
</Collapse>,
);
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(
<Collapse>
<Panel header="collapse 1" key="1">
first
</Panel>
<Panel header="collapse 2" key="2">
second
</Panel>
<Panel header="collapse 3" key="3" collapsible="disabled">
third
</Panel>
</Collapse>,
);
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 = () => <span>test{'>'}</span>;
const element = (
<Collapse onChange={onChange} expandIcon={expandIcon}>
<Fragment>
<Panel header="collapse 1" key="1" collapsible="disabled">
first
</Panel>
<Panel header="collapse 2" key="2" extra={<span>ExtraSpan</span>}>
second
</Panel>
<Fragment>
<Panel header="collapse 3" key="3" className="important">
third
</Panel>
</Fragment>
</Fragment>
</Collapse>
);
runNormalTest(element);
});
it('should support return null icon', () => {
const { container } = render(
<Collapse expandIcon={() => null}>
<Panel header="title" key="1">
first
</Panel>
</Collapse>,
);
expect(container.querySelector('.rc-collapse-header')?.childNodes).toHaveLength(1);
});
it('should support custom child', () => {
const { container } = render(
<Collapse>
<Panel header="collapse 1" key="1">
first
</Panel>
<a className="custom-child">custom-child</a>
</Collapse>,
);
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) => (
<Panel header="collapse 1" {...props}>
<p>test</p>
</Panel>
);
const { container } = render(
<Collapse defaultActiveKey="1">
<PanelElement key="1" />
<Panel header="collapse 2" key="2">
second
</Panel>
</Collapse>,
);
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(
<Collapse>
<Panel header="collapse 1" key="1">
first
</Panel>
</Collapse>,
);
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(
<Collapse collapsible="header">
<Panel header="collapse 1" key="1">
first
</Panel>
</Collapse>,
);
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(
<Collapse collapsible="icon">
<Panel header="collapse 1" key="1">
first
</Panel>
</Collapse>,
);
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(
<Collapse collapsible="disabled">
<Panel header="collapse 1" key="1">
first
</Panel>
</Collapse>,
);
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(
<Collapse collapsible="header">
<Panel collapsible="disabled" header="collapse 1" key="1">
first
</Panel>
</Collapse>,
);
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(
<Collapse collapsible="header">
<Panel header="collapse 1" key="1">
first
</Panel>
</Collapse>,
);
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(
<Collapse collapsible="icon">
<Panel header="collapse 1" key="1">
first
</Panel>
</Collapse>,
);
fireEvent.click(container.querySelector('.rc-collapse-title')!);
expect(container.querySelectorAll('.rc-collapse-item-active')).toHaveLength(0);
});
});
it('!showArrow', () => {
const { container } = render(
<Collapse>
<Panel header="collapse 1" key="1" showArrow={false}>
first
</Panel>
</Collapse>,
);
expect(container.querySelectorAll('.rc-collapse-expand-icon')).toHaveLength(0);
});
it('Panel container dom can set event handler', () => {
const clickHandler = jest.fn();
const { container } = render(
<Collapse defaultActiveKey="1">
<Panel header="collapse 1" key="1" onClick={clickHandler}>
<div className="target">Click this</div>
</Panel>
</Collapse>,
);
fireEvent.click(container.querySelector('.target')!);
expect(clickHandler).toHaveBeenCalled();
});
it('falsy Panel', () => {
const { container } = render(
<Collapse>
{null}
<Panel header="collapse 1" key="1">
<p>Panel 1 content</p>
</Panel>
{0}
<Panel header="collapse 2" key="2">
<p>Panel 2 content</p>
</Panel>
{undefined}
{false}
{true}
</Collapse>,
);
expect(container.querySelectorAll('.rc-collapse-item')).toHaveLength(2);
});
it('ref should work', () => {
const ref = React.createRef<any>();
const panelRef = React.createRef<any>();
const { container } = render(
<Collapse ref={ref}>
<Panel header="collapse 1" key="1" ref={panelRef}>
first
</Panel>
</Collapse>,
);
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(
<Collapse>
<Panel header="collapse 1" key="1" onItemClick={onItemClick}>
first
</Panel>
</Collapse>,
);
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(
<Collapse collapsible="disabled">
<Panel header="collapse 1" key="1" onItemClick={onItemClick}>
first
</Panel>
</Collapse>,
);
fireEvent.click(container.querySelector('.rc-collapse-header')!);
expect(onItemClick).not.toHaveBeenCalled();
});
it('panel style should work', () => {
const { container } = render(
<Collapse>
<Panel header="collapse 1" key="1" style={{ color: 'red' }}>
first
</Panel>
</Collapse>,
);
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: <span>ExtraSpan</span>,
},
{
key: '3',
label: 'collapse 3',
className: 'important',
children: 'third',
},
];
runNormalTest(
<Collapse onChange={onChange} expandIcon={() => <span>test{'>'}</span>} items={items} />,
);
runAccordionTest(
<Collapse
onChange={onChange}
accordion
items={[
{
key: '1',
label: 'collapse 1',
children: 'first',
},
{
key: '2',
label: 'collapse 2',
children: 'second',
},
{
key: '3',
label: 'collapse 3',
children: 'third',
},
]}
/>,
);
it('should work with onItemClick', () => {
const onItemClick = jest.fn();
const { container } = render(
<Collapse
items={[
{
label: 'title 3',
onItemClick,
},
]}
/>,
);
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(
<Collapse
onChange={onChangeFn}
items={[
...items.slice(0, 1),
{
label: 'title 3',
onItemClick,
collapsible: 'icon',
},
]}
/>,
);
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(
<Collapse
items={[
...items,
{
label: 'title 3',
children: <Collapse items={items} />,
},
]}
/>,
);
expect(container.firstChild).toMatchSnapshot();
});
it('should not support expandIcon', () => {
const { container } = render(
<Collapse
expandIcon={() => <i className="custom-icon">p</i>}
items={[
{
label: 'title',
expandIcon: () => <i className="custom-icon">c</i>,
} 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(
<Collapse
data-testid="1234"
aria-label="test"
items={[
{
label: 'title',
} as any,
]}
/>,
);
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(
<Collapse
activeKey={['1']}
styles={customStyles}
classNames={customClassnames}
items={[
{
key: '1',
label: 'title',
},
]}
/>,
);
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(
<Collapse
activeKey={['1']}
styles={customStyles}
classNames={customClassnames}
items={[
{
key: '1',
styles: {
header: {
color: 'blue',
fontSize: 20,
},
body: {
fontSize: 20,
},
title: {
color: 'red',
},
icon: {
color: 'blue',
},
},
classNames: {
header: 'custom-header-panel',
body: 'custom-body-panel',
},
label: 'title',
},
]}
/>,
);
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"
}
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
SYMBOL INDEX (18 symbols across 6 files)
FILE: docs/examples/custom-icon.tsx
function random (line 14) | function random() {
function expandIcon (line 24) | function expandIcon({ isActive }) {
FILE: docs/examples/simple.tsx
function random (line 15) | function random() {
function expandIcon (line 25) | function expandIcon({ isActive }) {
FILE: src/Collapse.tsx
function getActiveKeysArray (line 10) | function getActiveKeysArray(activeKey: React.Key | React.Key[]): React.K...
FILE: src/hooks/useItems.tsx
type Props (line 7) | type Props = Pick<
function mergeSemantic (line 15) | function mergeSemantic<T>(src: T, tgt: T, mergeFn: (a: any, b: any) => a...
function mergeSemanticClassNames (line 28) | function mergeSemanticClassNames<T>(src: T, tgt: T) {
function mergeSemanticStyles (line 32) | function mergeSemanticStyles<T>(src: T, tgt: T) {
function useItems (line 194) | function useItems(
FILE: src/interface.ts
type CollapsibleType (line 4) | type CollapsibleType = 'header' | 'icon' | 'disabled';
type ItemType (line 6) | interface ItemType
type CollapseProps (line 22) | interface CollapseProps {
type SemanticName (line 44) | type SemanticName = 'header' | 'title' | 'body' | 'icon';
type CollapsePanelProps (line 46) | interface CollapsePanelProps extends React.DOMAttributes<HTMLDivElement> {
FILE: tests/index.spec.tsx
function onChange (line 20) | function onChange(...args: any[]) {
function runNormalTest (line 27) | function runNormalTest(element: any) {
function runAccordionTest (line 263) | function runAccordionTest(element: React.ReactElement) {
Condensed preview — 39 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (82K chars).
[
{
"path": ".dumirc.ts",
"chars": 614,
"preview": "// more config: https://d.umijs.org/config\nimport { defineConfig } from 'dumi';\nimport path from 'path';\n\nconst basePath"
},
{
"path": ".editorconfig",
"chars": 192,
"preview": "# top-most EditorConfig file\nroot = true\n\n# Unix-style newlines with a newline ending every file\n[*.{js,css}]\nend_of_lin"
},
{
"path": ".eslintrc.js",
"chars": 571,
"preview": "const base = require('@umijs/fabric/dist/eslint');\n\nmodule.exports = {\n ...base,\n rules: {\n ...base.rules,\n 'no-"
},
{
"path": ".fatherrc.ts",
"chars": 118,
"preview": "import { defineConfig } from 'father';\n\nexport default defineConfig({\n plugins: ['@rc-component/father-plugin'],\n});\n"
},
{
"path": ".github/dependabot.yml",
"chars": 474,
"preview": "version: 2\nupdates:\n- package-ecosystem: npm\n directory: \"/\"\n schedule:\n interval: daily\n time: \"21:00\"\n open-p"
},
{
"path": ".github/workflows/react-component-ci.yml",
"chars": 138,
"preview": "name: ✅ test\non: [push, pull_request]\njobs:\n test:\n uses: react-component/rc-test/.github/workflows/test.yml@main\n "
},
{
"path": ".github/workflows/site-deploy.yml",
"chars": 857,
"preview": "name: Deploy website\non:\n push:\n tags:\n - '*'\n workflow_dispatch:\n\npermissions:\n contents: write\n\njobs:\n bui"
},
{
"path": ".gitignore",
"chars": 335,
"preview": "*.iml\n*.log\n.idea/\n.ipr\n.iws\n*~\n~*\n*.diff\n*.patch\n*.bak\n.DS_Store\nThumbs.db\n.project\n.*proj\n.svn/\n*.swp\n*.swo\n*.pyc\n*.py"
},
{
"path": ".husky/pre-commit",
"chars": 12,
"preview": "lint-staged\n"
},
{
"path": ".prettierrc",
"chars": 97,
"preview": "{\n \"singleQuote\": true,\n \"trailingComma\": \"all\",\n \"proseWrap\": \"never\",\n \"printWidth\": 100\n}\n"
},
{
"path": "HISTORY.md",
"chars": 1835,
"preview": "# History\n\n---\n\n## 2.0.0 `2020-05-08`\n\n- Remove `react-lifecycles-compat` and `prop-types`.\n- Upgrade `rc-animate` to `3"
},
{
"path": "LICENSE.md",
"chars": 1083,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2014-present yiminghe\n\nPermission is hereby granted, free of charge, to any person "
},
{
"path": "README.md",
"chars": 7409,
"preview": "# rc-collapse\n\nrc-collapse ui component for react\n\n[![NPM version][npm-image]][npm-url] [![build status][github-actions-"
},
{
"path": "assets/index.less",
"chars": 2193,
"preview": "@prefixCls: rc-collapse;\n@text-color: #666;\n@borderStyle: 1px solid #d9d9d9;\n\n@import './motion.less';\n\n#arrow {\n .comm"
},
{
"path": "assets/motion.less",
"chars": 147,
"preview": "@prefixCls: rc-collapse;\n\n.@{prefixCls} {\n &-motion {\n transition: height 0.3s, opacity 0.3s;\n }\n\n &-panel-hidden "
},
{
"path": "bunfig.toml",
"chars": 22,
"preview": "[install]\npeer = false"
},
{
"path": "docs/demo/basic.md",
"chars": 97,
"preview": "---\ntitle: Basic\nnav:\n title: Demo\n path: /demo\n---\n\n<code src=\"../examples/basic.tsx\"></code>\n"
},
{
"path": "docs/demo/custom-icon.md",
"chars": 109,
"preview": "---\ntitle: custom-icon\nnav:\n title: Demo\n path: /demo\n---\n\n<code src=\"../examples/custom-icon.tsx\"></code>\n"
},
{
"path": "docs/demo/fragment.md",
"chars": 103,
"preview": "---\ntitle: fragment\nnav:\n title: Demo\n path: /demo\n---\n\n<code src=\"../examples/fragment.tsx\"></code>\n"
},
{
"path": "docs/demo/simple.md",
"chars": 99,
"preview": "---\ntitle: simple\nnav:\n title: Demo\n path: /demo\n---\n\n<code src=\"../examples/simple.tsx\"></code>\n"
},
{
"path": "docs/examples/_util/motionUtil.ts",
"chars": 897,
"preview": "import type {\n CSSMotionProps,\n MotionEndEventHandler,\n MotionEventHandler,\n} from '@rc-component/motion';\n\nconst get"
},
{
"path": "docs/examples/basic.tsx",
"chars": 580,
"preview": "import type { CollapseProps } from 'rc-collapse';\nimport Collapse from 'rc-collapse';\nimport * as React from 'react';\nim"
},
{
"path": "docs/examples/custom-icon.tsx",
"chars": 3071,
"preview": "import Collapse, { Panel } from 'rc-collapse';\nimport * as React from 'react';\nimport '../../assets/index.less';\nimport "
},
{
"path": "docs/examples/fragment.tsx",
"chars": 580,
"preview": "import Collapse, { Panel } from 'rc-collapse';\nimport * as React from 'react';\nimport { Fragment } from 'react';\nimport "
},
{
"path": "docs/examples/simple.tsx",
"chars": 3924,
"preview": "import type { CollapseProps } from 'rc-collapse';\nimport Collapse, { Panel } from 'rc-collapse';\nimport * as React from "
},
{
"path": "docs/index.md",
"chars": 121,
"preview": "---\nhero:\n title: rc-collapse\n description: rc-collapse ui component for react\n---\n\n<embed src=\"../README.md\"></embed>"
},
{
"path": "jest.config.js",
"chars": 82,
"preview": "module.exports = {\n setupFilesAfterEnv: ['<rootDir>/tests/setupAfterEnv.ts'],\n};\n"
},
{
"path": "package.json",
"chars": 2264,
"preview": "{\n \"name\": \"@rc-component/collapse\",\n \"version\": \"1.2.0\",\n \"description\": \"rc-collapse ui component for react\",\n \"ke"
},
{
"path": "src/Collapse.tsx",
"chars": 2719,
"preview": "import { clsx } from 'clsx';\nimport { useControlledState, useEvent } from '@rc-component/util';\nimport warning from '@rc"
},
{
"path": "src/Panel.tsx",
"chars": 3727,
"preview": "import { clsx } from 'clsx';\nimport CSSMotion from '@rc-component/motion';\nimport KeyCode from '@rc-component/util/lib/K"
},
{
"path": "src/PanelContent.tsx",
"chars": 1215,
"preview": "import { clsx } from 'clsx';\nimport React from 'react';\nimport type { CollapsePanelProps } from './interface';\n\nconst Pa"
},
{
"path": "src/hooks/useItems.tsx",
"chars": 5097,
"preview": "import toArray from '@rc-component/util/lib/Children/toArray';\nimport React from 'react';\nimport type { CollapsePanelPro"
},
{
"path": "src/index.tsx",
"chars": 240,
"preview": "import Collapse from './Collapse';\n\nexport type { CollapsePanelProps, CollapseProps } from './interface';\n\nexport defaul"
},
{
"path": "src/interface.ts",
"chars": 1910,
"preview": "import type { CSSMotionProps } from '@rc-component/motion';\nimport type * as React from 'react';\n\nexport type Collapsibl"
},
{
"path": "tests/__snapshots__/index.spec.tsx.snap",
"chars": 1960,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`collapse props items should work with nested 1`] = `\n<div\n class=\""
},
{
"path": "tests/index.spec.tsx",
"chars": 31213,
"preview": "import type { RenderResult } from '@testing-library/react';\nimport { fireEvent, render } from '@testing-library/react';\n"
},
{
"path": "tests/setupAfterEnv.ts",
"chars": 36,
"preview": "import '@testing-library/jest-dom';\n"
},
{
"path": "tsconfig.json",
"chars": 473,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"esnext\",\n \"moduleResolution\": \"node\",\n \"baseUrl\": \"./\",\n \"jsx\": \"react\""
},
{
"path": "vercel.json",
"chars": 27,
"preview": "{\n \"framework\": \"umijs\"\n}\n"
}
]
About this extraction
This page contains the full source code of the react-component/collapse GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 39 files (74.8 KB), approximately 20.7k tokens, and a symbol index with 18 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.