├── .github └── dependabot.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.yaml ├── LICENSE ├── README.md ├── babel.config.js ├── dist ├── cjs │ ├── components │ │ ├── Accordion.cjs │ │ ├── AccordionItem.cjs │ │ ├── AccordionProvider.cjs │ │ ├── ControlledAccordion.cjs │ │ └── withAccordionItem.cjs │ ├── hooks │ │ ├── useAccordion.cjs │ │ ├── useAccordionContext.cjs │ │ ├── useAccordionItem.cjs │ │ ├── useAccordionItemEffect.cjs │ │ ├── useAccordionProvider.cjs │ │ ├── useAccordionState.cjs │ │ ├── useHeightTransition.cjs │ │ ├── useId.cjs │ │ └── useMergeRef.cjs │ ├── index.cjs │ └── utils │ │ ├── bem.cjs │ │ ├── constants.cjs │ │ ├── mergeProps.cjs │ │ └── useIsomorphicLayoutEffect.cjs └── esm │ ├── components │ ├── Accordion.mjs │ ├── AccordionItem.mjs │ ├── AccordionProvider.mjs │ ├── ControlledAccordion.mjs │ └── withAccordionItem.mjs │ ├── hooks │ ├── useAccordion.mjs │ ├── useAccordionContext.mjs │ ├── useAccordionItem.mjs │ ├── useAccordionItemEffect.mjs │ ├── useAccordionProvider.mjs │ ├── useAccordionState.mjs │ ├── useHeightTransition.mjs │ ├── useId.mjs │ └── useMergeRef.mjs │ ├── index.mjs │ └── utils │ ├── bem.mjs │ ├── constants.mjs │ ├── mergeProps.mjs │ └── useIsomorphicLayoutEffect.mjs ├── eslint.config.mjs ├── example ├── .eslintrc.js ├── README.md ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ └── index.tsx ├── public │ ├── favicon.ico │ └── vercel.svg ├── styles │ ├── Home.module.css │ └── globals.css └── tsconfig.json ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.mjs ├── src ├── __tests__ │ ├── components │ │ ├── Accordion.test.tsx │ │ ├── AccordionItem.test.tsx │ │ ├── AccordionMock.test.tsx │ │ └── ControlledAccordion.test.tsx │ ├── globals.ts │ ├── hooks │ │ ├── useAccordionProvider.test.ts │ │ ├── useAccordionState.test.tsx │ │ └── useId.test.ts │ ├── ssr.test.ts │ └── utils.tsx ├── components │ ├── Accordion.tsx │ ├── AccordionItem.tsx │ ├── AccordionProvider.tsx │ ├── ControlledAccordion.tsx │ └── withAccordionItem.tsx ├── hooks │ ├── useAccordion.ts │ ├── useAccordionContext.ts │ ├── useAccordionItem.ts │ ├── useAccordionItemEffect.ts │ ├── useAccordionProvider.ts │ ├── useAccordionState.ts │ ├── useHeightTransition.ts │ ├── useId.ts │ └── useMergeRef.ts ├── index.ts └── utils │ ├── bem.ts │ ├── constants.ts │ ├── mergeProps.ts │ └── useIsomorphicLayoutEffect.ts ├── tsconfig.json ├── types ├── components │ ├── Accordion.d.ts │ ├── AccordionItem.d.ts │ ├── AccordionProvider.d.ts │ ├── ControlledAccordion.d.ts │ └── withAccordionItem.d.ts ├── hooks │ ├── useAccordion.d.ts │ ├── useAccordionContext.d.ts │ ├── useAccordionItem.d.ts │ ├── useAccordionItemEffect.d.ts │ ├── useAccordionProvider.d.ts │ ├── useAccordionState.d.ts │ ├── useHeightTransition.d.ts │ ├── useId.d.ts │ └── useMergeRef.d.ts ├── index.d.ts └── utils │ ├── bem.d.ts │ ├── constants.d.ts │ ├── mergeProps.d.ts │ └── useIsomorphicLayoutEffect.d.ts └── website ├── .gitignore ├── .prettierrc.yaml ├── README.md ├── babel.config.js ├── docs ├── api │ ├── components │ │ ├── Accordion.md │ │ ├── AccordionItem.md │ │ ├── AccordionProvider.md │ │ ├── ControlledAccordion.md │ │ └── _category_.json │ └── hooks │ │ ├── _category_.json │ │ ├── useAccordion.md │ │ ├── useAccordionItem.md │ │ ├── useAccordionItemEffect.md │ │ ├── useAccordionProvider.md │ │ ├── useAccordionState.md │ │ └── useHeightTransition.md └── docs │ ├── allow-multiple.mdx │ ├── animation.mdx │ ├── controlling-state.mdx │ ├── customising-header.mdx │ ├── disabling-item.mdx │ ├── getting-started.mdx │ ├── headless-ui │ ├── _category_.json │ ├── accordion-item.mdx │ ├── accordion.mdx │ ├── example.mdx │ ├── intro.md │ └── styles.mdx │ ├── initial-expanded.mdx │ ├── item-render-prop.mdx │ ├── nested.mdx │ ├── on-state-change.mdx │ └── styling.mdx ├── docusaurus.config.ts ├── gh-pages.sh ├── package-lock.json ├── package.json ├── sidebars.js ├── src ├── components │ ├── AccessingState │ │ └── index.tsx │ ├── ControllingState │ │ └── index.tsx │ ├── CustomisingHeader │ │ ├── index.tsx │ │ └── styles.module.css │ ├── DisableItem │ │ └── index.tsx │ ├── HeadlessUI │ │ ├── Accordion.tsx │ │ ├── AccordionItem.tsx │ │ ├── AccordionItemBare.tsx │ │ ├── AccordionItemMemo.tsx │ │ └── Example.tsx │ ├── HomepageFeatures │ │ ├── index.tsx │ │ └── styles.module.css │ ├── InitialEntered │ │ └── index.tsx │ ├── Multiple │ │ └── index.tsx │ ├── Nested │ │ └── index.tsx │ ├── Starter │ │ ├── ArrayMap.tsx │ │ └── Basic.tsx │ ├── StateChange │ │ └── index.tsx │ ├── UseStateHook │ │ └── index.tsx │ └── accordion │ │ ├── index.tsx │ │ └── styles.module.css ├── css │ └── custom.css ├── html │ └── skeleton.html ├── pages │ ├── index.module.css │ └── index.tsx ├── theme │ └── Footer │ │ ├── index.module.css │ │ └── index.tsx └── utils │ └── index.ts ├── static ├── .nojekyll └── img │ ├── chevron-down.svg │ └── favicon.ico └── tsconfig.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' 9 | directory: '/' 10 | versioning-strategy: increase 11 | schedule: 12 | interval: 'weekly' 13 | groups: 14 | all-dependencies: 15 | patterns: ['*'] 16 | - package-ecosystem: 'npm' 17 | directory: '/website' 18 | versioning-strategy: increase 19 | schedule: 20 | interval: 'weekly' 21 | groups: 22 | all-dependencies: 23 | patterns: ['*'] 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules/ 3 | 4 | # builds 5 | build/ 6 | .next/ 7 | _next/ 8 | example/out/ 9 | 10 | # tests 11 | coverage/ 12 | 13 | # logs 14 | logs 15 | *.log 16 | 17 | # typescript 18 | *.tsbuildinfo 19 | 20 | # misc 21 | .DS_Store 22 | .npm 23 | .env 24 | .eslintcache -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | static/ 3 | coverage/ 4 | dist/ 5 | types/ 6 | example/out/ 7 | .next/ 8 | _next/ 9 | .docusaurus/ -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | trailingComma: none 2 | singleQuote: true 3 | printWidth: 100 4 | overrides: 5 | - files: '*.md' 6 | options: 7 | printWidth: 70 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Zheng Song 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-Accordion 2 | 3 | > An unstyled, accessible accordion library for React apps and design systems. 4 | 5 | **[Examples and Docs](https://szhsin.github.io/react-accordion/)** 6 | 7 | [![NPM](https://img.shields.io/npm/v/@szhsin/react-accordion.svg)](https://www.npmjs.com/package/@szhsin/react-accordion) 8 | [![bundlephobia](https://img.shields.io/bundlephobia/minzip/@szhsin/react-accordion)](https://bundlephobia.com/package/@szhsin/react-accordion) 9 | [![bundlejs](https://deno.bundlejs.com/?q=%40szhsin%2Freact-accordion&treeshake=%5B*%5D&config=%7B%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%2C%22react-dom%22%5D%7D%7D&badge=simple)](https://bundlejs.com/?q=%40szhsin%2Freact-accordion&treeshake=%5B*%5D&config=%7B%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%2C%22react-dom%22%5D%7D%7D&bundle) 10 | 11 | ## Features 12 | 13 | - Unstyled React accordion components 14 | - Headless usage via React hooks 15 | - [WAI-ARIA compliant](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/) 16 | - Fully keyboard accessible 17 | - Supports animations 18 | - Works in controlled and uncontrolled modes 19 | - Control to expand/collapse individual or all items 20 | - Compatible with React 18+ concurrent rendering 21 | - Supports server-side rendering (SSR) 22 | - Lightweight and tree-shakable [(~3kB)](https://bundlephobia.com/package/@szhsin/react-accordion) 23 | - Fully typed API with TypeScript 24 | 25 | ![react accordion](https://user-images.githubusercontent.com/41896553/236674264-2412dd3b-48b1-4df1-ab31-40d191e188de.gif) 26 | 27 | ## Install 28 | 29 | with npm 30 | 31 | ```bash 32 | npm install @szhsin/react-accordion 33 | ``` 34 | 35 | or with Yarn 36 | 37 | ```bash 38 | yarn add @szhsin/react-accordion 39 | ``` 40 | 41 | ## Usage 42 | 43 | ```jsx 44 | import { Accordion, AccordionItem } from '@szhsin/react-accordion'; 45 | 46 | export default function Example() { 47 | return ( 48 | 49 | 50 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed 51 | do eiusmod tempor incididunt ut labore et dolore magna aliqua. 52 | 53 | 54 | 55 | Quisque eget luctus mi, vehicula mollis lorem. Proin fringilla 56 | vel erat quis sodales. Nam ex enim, eleifend venenatis lectus 57 | vitae, accumsan auctor mi. 58 | 59 | 60 | 61 | Suspendisse massa risus, pretium id interdum in, dictum sit 62 | amet ante. Fusce vulputate purus sed tempus feugiat. 63 | 64 | 65 | ); 66 | } 67 | ``` 68 | 69 | **[Edit on CodeSandbox](https://codesandbox.io/s/react-accordion-css-module-eqvnzg)**
70 | **[Visit more examples and docs](https://szhsin.github.io/react-accordion/)**

71 | 72 | ## License 73 | 74 | [MIT](https://github.com/szhsin/react-accordion/blob/master/LICENSE) Licensed. 75 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | targets: 'defaults', 3 | assumptions: { 4 | constantReexports: true, 5 | ignoreFunctionLength: true, 6 | ignoreToPrimitiveHint: true, 7 | iterableIsArray: true, 8 | noDocumentAll: true, 9 | noIncompleteNsImportDetection: true, 10 | noNewArrows: true, 11 | objectRestNoSymbols: true, 12 | pureGetters: true, 13 | setComputedProperties: true, 14 | setSpreadProperties: true, 15 | skipForOfIteratorClosing: true 16 | }, 17 | shouldPrintComment: (val) => /[@#]__PURE__/.test(val), 18 | plugins: ['pure-annotations'], 19 | presets: [ 20 | [ 21 | '@babel/preset-env', 22 | { 23 | bugfixes: true, 24 | include: [ 25 | '@babel/plugin-transform-nullish-coalescing-operator', 26 | '@babel/plugin-transform-optional-catch-binding' 27 | ], 28 | exclude: ['@babel/plugin-transform-typeof-symbol'] 29 | } 30 | ], 31 | ['@babel/preset-react', { runtime: 'automatic' }], 32 | ['@babel/preset-typescript', { isTSX: true, allExtensions: true }] 33 | ] 34 | }; 35 | -------------------------------------------------------------------------------- /dist/cjs/components/Accordion.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var useAccordionProvider = require('../hooks/useAccordionProvider.cjs'); 5 | var ControlledAccordion = require('./ControlledAccordion.cjs'); 6 | var jsxRuntime = require('react/jsx-runtime'); 7 | 8 | const Accordion = /*#__PURE__*/React.forwardRef(({ 9 | allowMultiple, 10 | initialEntered, 11 | mountOnEnter, 12 | unmountOnExit, 13 | transition, 14 | transitionTimeout, 15 | onStateChange, 16 | ...rest 17 | }, ref) => { 18 | const providerValue = useAccordionProvider.useAccordionProvider({ 19 | allowMultiple, 20 | initialEntered, 21 | mountOnEnter, 22 | unmountOnExit, 23 | transition, 24 | transitionTimeout, 25 | onStateChange 26 | }); 27 | return /*#__PURE__*/jsxRuntime.jsx(ControlledAccordion.ControlledAccordion, { 28 | ...rest, 29 | ref: ref, 30 | providerValue: providerValue 31 | }); 32 | }); 33 | Accordion.displayName = 'Accordion'; 34 | 35 | exports.Accordion = Accordion; 36 | -------------------------------------------------------------------------------- /dist/cjs/components/AccordionItem.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var constants = require('../utils/constants.cjs'); 5 | var bem = require('../utils/bem.cjs'); 6 | var mergeProps = require('../utils/mergeProps.cjs'); 7 | var useAccordionItem = require('../hooks/useAccordionItem.cjs'); 8 | var useHeightTransition = require('../hooks/useHeightTransition.cjs'); 9 | var useMergeRef = require('../hooks/useMergeRef.cjs'); 10 | var withAccordionItem = require('./withAccordionItem.cjs'); 11 | var jsxRuntime = require('react/jsx-runtime'); 12 | 13 | const getRenderNode = (nodeOrFunc, props) => typeof nodeOrFunc === 'function' ? nodeOrFunc(props) : nodeOrFunc; 14 | const WrappedItem = /*#__PURE__*/React.memo(({ 15 | forwardedRef, 16 | itemRef, 17 | state, 18 | toggle, 19 | className, 20 | disabled, 21 | header, 22 | headingTag: Heading = 'h3', 23 | headingProps, 24 | buttonProps, 25 | contentProps, 26 | panelProps, 27 | children, 28 | ...rest 29 | }) => { 30 | const itemState = { 31 | state, 32 | toggle, 33 | disabled 34 | }; 35 | const { 36 | buttonProps: _buttonProps, 37 | panelProps: _panelProps 38 | } = useAccordionItem.useAccordionItem(itemState); 39 | const [transitionStyle, _panelRef] = useHeightTransition.useHeightTransition(state); 40 | const panelRef = useMergeRef.useMergeRef(panelProps?.ref, _panelRef); 41 | const { 42 | status, 43 | isMounted, 44 | isEnter 45 | } = state; 46 | return /*#__PURE__*/jsxRuntime.jsxs("div", { 47 | ...rest, 48 | ref: useMergeRef.useMergeRef(forwardedRef, itemRef), 49 | className: bem.bem(constants.ACCORDION_BLOCK, 'item', { 50 | status, 51 | expanded: isEnter 52 | })(className, state), 53 | children: [/*#__PURE__*/jsxRuntime.jsx(Heading, { 54 | ...headingProps, 55 | style: { 56 | margin: 0, 57 | ...headingProps?.style 58 | }, 59 | className: bem.bem(constants.ACCORDION_BLOCK, 'item-heading')(headingProps?.className, state), 60 | children: /*#__PURE__*/jsxRuntime.jsx("button", { 61 | ...mergeProps.mergeProps(_buttonProps, buttonProps), 62 | type: "button", 63 | className: bem.bem(constants.ACCORDION_BLOCK, 'item-btn')(buttonProps?.className, state), 64 | children: getRenderNode(header, itemState) 65 | }) 66 | }), isMounted && /*#__PURE__*/jsxRuntime.jsx("div", { 67 | ...contentProps, 68 | style: { 69 | display: status === 'exited' ? 'none' : undefined, 70 | ...transitionStyle, 71 | ...contentProps?.style 72 | }, 73 | className: bem.bem(constants.ACCORDION_BLOCK, 'item-content')(contentProps?.className, state), 74 | children: /*#__PURE__*/jsxRuntime.jsx("div", { 75 | ...mergeProps.mergeProps(_panelProps, panelProps), 76 | ref: panelRef, 77 | className: bem.bem(constants.ACCORDION_BLOCK, 'item-panel')(panelProps?.className, state), 78 | children: getRenderNode(children, itemState) 79 | }) 80 | })] 81 | }); 82 | }); 83 | WrappedItem.displayName = 'AccordionItem'; 84 | const AccordionItem = /*#__PURE__*/withAccordionItem.withAccordionItem(WrappedItem); 85 | 86 | exports.AccordionItem = AccordionItem; 87 | -------------------------------------------------------------------------------- /dist/cjs/components/AccordionProvider.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var constants = require('../utils/constants.cjs'); 4 | var jsxRuntime = require('react/jsx-runtime'); 5 | 6 | const AccordionProvider = props => /*#__PURE__*/jsxRuntime.jsx(constants.AccordionContext.Provider, { 7 | ...props 8 | }); 9 | 10 | exports.AccordionProvider = AccordionProvider; 11 | -------------------------------------------------------------------------------- /dist/cjs/components/ControlledAccordion.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var constants = require('../utils/constants.cjs'); 5 | var bem = require('../utils/bem.cjs'); 6 | var mergeProps = require('../utils/mergeProps.cjs'); 7 | var AccordionProvider = require('./AccordionProvider.cjs'); 8 | var useAccordion = require('../hooks/useAccordion.cjs'); 9 | var jsxRuntime = require('react/jsx-runtime'); 10 | 11 | const ControlledAccordion = /*#__PURE__*/React.forwardRef(({ 12 | providerValue, 13 | className, 14 | ...rest 15 | }, ref) => { 16 | const { 17 | accordionProps 18 | } = useAccordion.useAccordion(); 19 | return /*#__PURE__*/jsxRuntime.jsx(AccordionProvider.AccordionProvider, { 20 | value: providerValue, 21 | children: /*#__PURE__*/jsxRuntime.jsx("div", { 22 | ...mergeProps.mergeProps(accordionProps, rest), 23 | ref: ref, 24 | className: bem.bem(constants.ACCORDION_BLOCK)(className) 25 | }) 26 | }); 27 | }); 28 | ControlledAccordion.displayName = 'ControlledAccordion'; 29 | 30 | exports.ControlledAccordion = ControlledAccordion; 31 | -------------------------------------------------------------------------------- /dist/cjs/components/withAccordionItem.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var useAccordionItemEffect = require('../hooks/useAccordionItemEffect.cjs'); 5 | var jsxRuntime = require('react/jsx-runtime'); 6 | 7 | const withAccordionItem = WrappedItem => { 8 | const WithAccordionItem = /*#__PURE__*/React.forwardRef(({ 9 | itemKey, 10 | initialEntered, 11 | ...rest 12 | }, ref) => /*#__PURE__*/jsxRuntime.jsx(WrappedItem, { 13 | forwardedRef: ref, 14 | ...rest, 15 | ...useAccordionItemEffect.useAccordionItemEffect({ 16 | itemKey, 17 | initialEntered, 18 | disabled: rest.disabled 19 | }) 20 | })); 21 | WithAccordionItem.displayName = 'WithAccordionItem'; 22 | return WithAccordionItem; 23 | }; 24 | 25 | exports.withAccordionItem = withAccordionItem; 26 | -------------------------------------------------------------------------------- /dist/cjs/hooks/useAccordion.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var constants = require('../utils/constants.cjs'); 4 | 5 | const getAccordion = node => { 6 | do { 7 | node = node.parentElement; 8 | } while (node && !node.hasAttribute(constants.ACCORDION_ATTR)); 9 | return node; 10 | }; 11 | const getNextIndex = (moveUp, current, length) => moveUp ? current > 0 ? current - 1 : length - 1 : (current + 1) % length; 12 | const moveFocus = (moveUp, e) => { 13 | const { 14 | activeElement 15 | } = document; 16 | if (!activeElement || !activeElement.hasAttribute(constants.ACCORDION_BTN_ATTR) || getAccordion(activeElement) !== e.currentTarget) return; 17 | const nodes = e.currentTarget.querySelectorAll(`[${constants.ACCORDION_BTN_ATTR}]`); 18 | const { 19 | length 20 | } = nodes; 21 | for (let i = 0; i < length; i++) { 22 | if (nodes[i] === activeElement) { 23 | let next = getNextIndex(moveUp, i, length); 24 | while (getAccordion(nodes[i]) !== getAccordion(nodes[next])) next = getNextIndex(moveUp, next, length); 25 | if (i !== next) { 26 | e.preventDefault(); 27 | nodes[next].focus(); 28 | } 29 | break; 30 | } 31 | } 32 | }; 33 | const useAccordion = () => { 34 | const accordionProps = { 35 | [constants.ACCORDION_ATTR]: '', 36 | onKeyDown: e => e.key === 'ArrowUp' ? moveFocus(true, e) : e.key === 'ArrowDown' && moveFocus(false, e) 37 | }; 38 | return { 39 | accordionProps 40 | }; 41 | }; 42 | 43 | exports.useAccordion = useAccordion; 44 | -------------------------------------------------------------------------------- /dist/cjs/hooks/useAccordionContext.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var constants = require('../utils/constants.cjs'); 5 | 6 | const getItemState = (providerValue, key, itemInitialEntered) => { 7 | const { 8 | stateMap, 9 | mountOnEnter, 10 | initialEntered 11 | } = providerValue; 12 | const _initialEntered = itemInitialEntered != null ? itemInitialEntered : initialEntered; 13 | return stateMap.get(key) || { 14 | status: _initialEntered ? 'entered' : mountOnEnter ? 'unmounted' : 'exited', 15 | isMounted: !mountOnEnter, 16 | isEnter: _initialEntered, 17 | isResolved: true 18 | }; 19 | }; 20 | const useAccordionContext = () => { 21 | const context = React.useContext(constants.AccordionContext); 22 | if (process.env.NODE_ENV !== 'production' && !context.stateMap) { 23 | throw new Error('[React-Accordion] Cannot find a above this AccordionItem.'); 24 | } 25 | return context; 26 | }; 27 | 28 | exports.getItemState = getItemState; 29 | exports.useAccordionContext = useAccordionContext; 30 | -------------------------------------------------------------------------------- /dist/cjs/hooks/useAccordionItem.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var constants = require('../utils/constants.cjs'); 4 | var useId = require('./useId.cjs'); 5 | 6 | const useAccordionItem = ({ 7 | state, 8 | toggle, 9 | disabled 10 | }) => { 11 | const buttonId = useId.useId(); 12 | const panelId = buttonId && buttonId + '-'; 13 | const buttonProps = { 14 | id: buttonId, 15 | 'aria-controls': panelId, 16 | 'aria-expanded': state.isEnter, 17 | onClick: toggle 18 | }; 19 | disabled ? buttonProps.disabled = true : buttonProps[constants.ACCORDION_BTN_ATTR] = ''; 20 | const panelProps = { 21 | id: panelId, 22 | 'aria-labelledby': buttonId, 23 | role: 'region' 24 | }; 25 | return { 26 | buttonProps, 27 | panelProps 28 | }; 29 | }; 30 | 31 | exports.useAccordionItem = useAccordionItem; 32 | -------------------------------------------------------------------------------- /dist/cjs/hooks/useAccordionItemEffect.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var useAccordionContext = require('./useAccordionContext.cjs'); 5 | 6 | const useAccordionItemEffect = ({ 7 | itemKey, 8 | initialEntered, 9 | disabled 10 | } = {}) => { 11 | const itemRef = React.useRef(null); 12 | const context = useAccordionContext.useAccordionContext(); 13 | const key = itemKey != null ? itemKey : itemRef.current; 14 | const state = useAccordionContext.getItemState(context, key, initialEntered); 15 | const { 16 | setItem, 17 | deleteItem, 18 | toggle 19 | } = context; 20 | React.useEffect(() => { 21 | if (disabled) return; 22 | const key = itemKey != null ? itemKey : itemRef.current; 23 | setItem(key, { 24 | initialEntered 25 | }); 26 | return () => void deleteItem(key); 27 | }, [setItem, deleteItem, itemKey, initialEntered, disabled]); 28 | return { 29 | itemRef, 30 | state, 31 | toggle: React.useCallback(toEnter => toggle(key, toEnter), [toggle, key]) 32 | }; 33 | }; 34 | 35 | exports.useAccordionItemEffect = useAccordionItemEffect; 36 | -------------------------------------------------------------------------------- /dist/cjs/hooks/useAccordionProvider.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var reactTransitionState = require('react-transition-state'); 4 | 5 | const getTransition = (transition, name) => transition === true || !!(transition && transition[name]); 6 | const useAccordionProvider = ({ 7 | transition, 8 | transitionTimeout, 9 | ...rest 10 | } = {}) => { 11 | const transitionMap = reactTransitionState.useTransitionMap({ 12 | timeout: transitionTimeout, 13 | enter: getTransition(transition, 'enter'), 14 | exit: getTransition(transition, 'exit'), 15 | preEnter: getTransition(transition, 'preEnter'), 16 | preExit: getTransition(transition, 'preExit'), 17 | ...rest 18 | }); 19 | return { 20 | mountOnEnter: !!rest.mountOnEnter, 21 | initialEntered: !!rest.initialEntered, 22 | ...transitionMap 23 | }; 24 | }; 25 | 26 | exports.useAccordionProvider = useAccordionProvider; 27 | -------------------------------------------------------------------------------- /dist/cjs/hooks/useAccordionState.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var useAccordionContext = require('./useAccordionContext.cjs'); 4 | 5 | const useAccordionState = () => { 6 | const context = useAccordionContext.useAccordionContext(); 7 | return { 8 | getItemState: (key, { 9 | initialEntered 10 | } = {}) => useAccordionContext.getItemState(context, key, initialEntered), 11 | toggle: context.toggle, 12 | toggleAll: context.toggleAll 13 | }; 14 | }; 15 | 16 | exports.useAccordionState = useAccordionState; 17 | -------------------------------------------------------------------------------- /dist/cjs/hooks/useHeightTransition.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var useIsomorphicLayoutEffect = require('../utils/useIsomorphicLayoutEffect.cjs'); 5 | 6 | const useHeightTransition = ({ 7 | status, 8 | isResolved 9 | }) => { 10 | const [height, setHeight] = React.useState(); 11 | const elementRef = React.useRef(null); 12 | useIsomorphicLayoutEffect.useLayoutEffect(() => { 13 | (status === 'preEnter' || status === 'preExit') && setHeight(elementRef.current.getBoundingClientRect().height); 14 | }, [status]); 15 | const style = { 16 | height: status === 'preEnter' || status === 'exiting' ? 0 : status === 'entering' || status === 'preExit' ? height : undefined, 17 | overflow: isResolved ? undefined : 'hidden' 18 | }; 19 | return [style, elementRef]; 20 | }; 21 | 22 | exports.useHeightTransition = useHeightTransition; 23 | -------------------------------------------------------------------------------- /dist/cjs/hooks/useId.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var constants = require('../utils/constants.cjs'); 5 | 6 | let current = 0; 7 | const useIdShim = () => { 8 | const [id, setId] = React.useState(); 9 | React.useEffect(() => setId(++current), []); 10 | return id && `${constants.ACCORDION_PREFIX}-${id}`; 11 | }; 12 | const useId = React.useId || useIdShim; 13 | 14 | exports.useId = useId; 15 | -------------------------------------------------------------------------------- /dist/cjs/hooks/useMergeRef.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | 5 | function setRef(ref, instance) { 6 | typeof ref === 'function' ? ref(instance) : ref.current = instance; 7 | } 8 | function useMergeRef(refA, refB) { 9 | return React.useMemo(() => { 10 | if (!refA) return refB; 11 | if (!refB) return refA; 12 | return instance => { 13 | setRef(refA, instance); 14 | setRef(refB, instance); 15 | }; 16 | }, [refA, refB]); 17 | } 18 | 19 | exports.useMergeRef = useMergeRef; 20 | -------------------------------------------------------------------------------- /dist/cjs/index.cjs: -------------------------------------------------------------------------------- 1 | 2 | 'use client'; 3 | 'use strict'; 4 | 5 | var Accordion = require('./components/Accordion.cjs'); 6 | var AccordionItem = require('./components/AccordionItem.cjs'); 7 | var AccordionProvider = require('./components/AccordionProvider.cjs'); 8 | var ControlledAccordion = require('./components/ControlledAccordion.cjs'); 9 | var withAccordionItem = require('./components/withAccordionItem.cjs'); 10 | var useAccordion = require('./hooks/useAccordion.cjs'); 11 | var useAccordionItem = require('./hooks/useAccordionItem.cjs'); 12 | var useAccordionItemEffect = require('./hooks/useAccordionItemEffect.cjs'); 13 | var useAccordionProvider = require('./hooks/useAccordionProvider.cjs'); 14 | var useAccordionState = require('./hooks/useAccordionState.cjs'); 15 | var useHeightTransition = require('./hooks/useHeightTransition.cjs'); 16 | var useMergeRef = require('./hooks/useMergeRef.cjs'); 17 | 18 | 19 | 20 | exports.Accordion = Accordion.Accordion; 21 | exports.AccordionItem = AccordionItem.AccordionItem; 22 | exports.AccordionProvider = AccordionProvider.AccordionProvider; 23 | exports.ControlledAccordion = ControlledAccordion.ControlledAccordion; 24 | exports.withAccordionItem = withAccordionItem.withAccordionItem; 25 | exports.useAccordion = useAccordion.useAccordion; 26 | exports.useAccordionItem = useAccordionItem.useAccordionItem; 27 | exports.useAccordionItemEffect = useAccordionItemEffect.useAccordionItemEffect; 28 | exports.useAccordionProvider = useAccordionProvider.useAccordionProvider; 29 | exports.useAccordionState = useAccordionState.useAccordionState; 30 | exports.useHeightTransition = useHeightTransition.useHeightTransition; 31 | exports.useMergeRef = useMergeRef.useMergeRef; 32 | -------------------------------------------------------------------------------- /dist/cjs/utils/bem.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const bem = (block, element, modifiers) => (className, props) => { 4 | const blockElement = element ? `${block}__${element}` : block; 5 | let classString = blockElement; 6 | modifiers && Object.keys(modifiers).forEach(name => { 7 | const value = modifiers[name]; 8 | if (value) classString += ` ${blockElement}--${value === true ? name : `${name}-${value}`}`; 9 | }); 10 | let expandedClassName = typeof className === 'function' ? className(props) : className; 11 | if (typeof expandedClassName === 'string') { 12 | expandedClassName = expandedClassName.trim(); 13 | if (expandedClassName) classString += ` ${expandedClassName}`; 14 | } 15 | return classString; 16 | }; 17 | 18 | exports.bem = bem; 19 | -------------------------------------------------------------------------------- /dist/cjs/utils/constants.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | 5 | const ACCORDION_BLOCK = 'szh-accordion'; 6 | const ACCORDION_PREFIX = 'szh-adn'; 7 | const ACCORDION_ATTR = `data-${ACCORDION_PREFIX}`; 8 | const ACCORDION_BTN_ATTR = `data-${ACCORDION_PREFIX}-btn`; 9 | const AccordionContext = /*#__PURE__*/React.createContext({}); 10 | 11 | exports.ACCORDION_ATTR = ACCORDION_ATTR; 12 | exports.ACCORDION_BLOCK = ACCORDION_BLOCK; 13 | exports.ACCORDION_BTN_ATTR = ACCORDION_BTN_ATTR; 14 | exports.ACCORDION_PREFIX = ACCORDION_PREFIX; 15 | exports.AccordionContext = AccordionContext; 16 | -------------------------------------------------------------------------------- /dist/cjs/utils/mergeProps.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mergeProps = (target, source) => { 4 | if (!source) return target; 5 | const result = { 6 | ...target 7 | }; 8 | Object.keys(source).forEach(key => { 9 | const targetProp = target[key]; 10 | const sourceProp = source[key]; 11 | if (typeof sourceProp === 'function' && targetProp) { 12 | result[key] = (...e) => { 13 | targetProp(...e); 14 | sourceProp(...e); 15 | }; 16 | } else { 17 | result[key] = sourceProp; 18 | } 19 | }); 20 | return result; 21 | }; 22 | 23 | exports.mergeProps = mergeProps; 24 | -------------------------------------------------------------------------------- /dist/cjs/utils/useIsomorphicLayoutEffect.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | 5 | const useIsomorphicLayoutEffect = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined' ? React.useLayoutEffect : React.useEffect; 6 | 7 | exports.useLayoutEffect = useIsomorphicLayoutEffect; 8 | -------------------------------------------------------------------------------- /dist/esm/components/Accordion.mjs: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | import { useAccordionProvider } from '../hooks/useAccordionProvider.mjs'; 3 | import { ControlledAccordion } from './ControlledAccordion.mjs'; 4 | import { jsx } from 'react/jsx-runtime'; 5 | 6 | const Accordion = /*#__PURE__*/forwardRef(({ 7 | allowMultiple, 8 | initialEntered, 9 | mountOnEnter, 10 | unmountOnExit, 11 | transition, 12 | transitionTimeout, 13 | onStateChange, 14 | ...rest 15 | }, ref) => { 16 | const providerValue = useAccordionProvider({ 17 | allowMultiple, 18 | initialEntered, 19 | mountOnEnter, 20 | unmountOnExit, 21 | transition, 22 | transitionTimeout, 23 | onStateChange 24 | }); 25 | return /*#__PURE__*/jsx(ControlledAccordion, { 26 | ...rest, 27 | ref: ref, 28 | providerValue: providerValue 29 | }); 30 | }); 31 | Accordion.displayName = 'Accordion'; 32 | 33 | export { Accordion }; 34 | -------------------------------------------------------------------------------- /dist/esm/components/AccordionItem.mjs: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { ACCORDION_BLOCK } from '../utils/constants.mjs'; 3 | import { bem } from '../utils/bem.mjs'; 4 | import { mergeProps } from '../utils/mergeProps.mjs'; 5 | import { useAccordionItem } from '../hooks/useAccordionItem.mjs'; 6 | import { useHeightTransition } from '../hooks/useHeightTransition.mjs'; 7 | import { useMergeRef } from '../hooks/useMergeRef.mjs'; 8 | import { withAccordionItem } from './withAccordionItem.mjs'; 9 | import { jsxs, jsx } from 'react/jsx-runtime'; 10 | 11 | const getRenderNode = (nodeOrFunc, props) => typeof nodeOrFunc === 'function' ? nodeOrFunc(props) : nodeOrFunc; 12 | const WrappedItem = /*#__PURE__*/memo(({ 13 | forwardedRef, 14 | itemRef, 15 | state, 16 | toggle, 17 | className, 18 | disabled, 19 | header, 20 | headingTag: Heading = 'h3', 21 | headingProps, 22 | buttonProps, 23 | contentProps, 24 | panelProps, 25 | children, 26 | ...rest 27 | }) => { 28 | const itemState = { 29 | state, 30 | toggle, 31 | disabled 32 | }; 33 | const { 34 | buttonProps: _buttonProps, 35 | panelProps: _panelProps 36 | } = useAccordionItem(itemState); 37 | const [transitionStyle, _panelRef] = useHeightTransition(state); 38 | const panelRef = useMergeRef(panelProps?.ref, _panelRef); 39 | const { 40 | status, 41 | isMounted, 42 | isEnter 43 | } = state; 44 | return /*#__PURE__*/jsxs("div", { 45 | ...rest, 46 | ref: useMergeRef(forwardedRef, itemRef), 47 | className: bem(ACCORDION_BLOCK, 'item', { 48 | status, 49 | expanded: isEnter 50 | })(className, state), 51 | children: [/*#__PURE__*/jsx(Heading, { 52 | ...headingProps, 53 | style: { 54 | margin: 0, 55 | ...headingProps?.style 56 | }, 57 | className: bem(ACCORDION_BLOCK, 'item-heading')(headingProps?.className, state), 58 | children: /*#__PURE__*/jsx("button", { 59 | ...mergeProps(_buttonProps, buttonProps), 60 | type: "button", 61 | className: bem(ACCORDION_BLOCK, 'item-btn')(buttonProps?.className, state), 62 | children: getRenderNode(header, itemState) 63 | }) 64 | }), isMounted && /*#__PURE__*/jsx("div", { 65 | ...contentProps, 66 | style: { 67 | display: status === 'exited' ? 'none' : undefined, 68 | ...transitionStyle, 69 | ...contentProps?.style 70 | }, 71 | className: bem(ACCORDION_BLOCK, 'item-content')(contentProps?.className, state), 72 | children: /*#__PURE__*/jsx("div", { 73 | ...mergeProps(_panelProps, panelProps), 74 | ref: panelRef, 75 | className: bem(ACCORDION_BLOCK, 'item-panel')(panelProps?.className, state), 76 | children: getRenderNode(children, itemState) 77 | }) 78 | })] 79 | }); 80 | }); 81 | WrappedItem.displayName = 'AccordionItem'; 82 | const AccordionItem = /*#__PURE__*/withAccordionItem(WrappedItem); 83 | 84 | export { AccordionItem }; 85 | -------------------------------------------------------------------------------- /dist/esm/components/AccordionProvider.mjs: -------------------------------------------------------------------------------- 1 | import { AccordionContext } from '../utils/constants.mjs'; 2 | import { jsx } from 'react/jsx-runtime'; 3 | 4 | const AccordionProvider = props => /*#__PURE__*/jsx(AccordionContext.Provider, { 5 | ...props 6 | }); 7 | 8 | export { AccordionProvider }; 9 | -------------------------------------------------------------------------------- /dist/esm/components/ControlledAccordion.mjs: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | import { ACCORDION_BLOCK } from '../utils/constants.mjs'; 3 | import { bem } from '../utils/bem.mjs'; 4 | import { mergeProps } from '../utils/mergeProps.mjs'; 5 | import { AccordionProvider } from './AccordionProvider.mjs'; 6 | import { useAccordion } from '../hooks/useAccordion.mjs'; 7 | import { jsx } from 'react/jsx-runtime'; 8 | 9 | const ControlledAccordion = /*#__PURE__*/forwardRef(({ 10 | providerValue, 11 | className, 12 | ...rest 13 | }, ref) => { 14 | const { 15 | accordionProps 16 | } = useAccordion(); 17 | return /*#__PURE__*/jsx(AccordionProvider, { 18 | value: providerValue, 19 | children: /*#__PURE__*/jsx("div", { 20 | ...mergeProps(accordionProps, rest), 21 | ref: ref, 22 | className: bem(ACCORDION_BLOCK)(className) 23 | }) 24 | }); 25 | }); 26 | ControlledAccordion.displayName = 'ControlledAccordion'; 27 | 28 | export { ControlledAccordion }; 29 | -------------------------------------------------------------------------------- /dist/esm/components/withAccordionItem.mjs: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | import { useAccordionItemEffect } from '../hooks/useAccordionItemEffect.mjs'; 3 | import { jsx } from 'react/jsx-runtime'; 4 | 5 | const withAccordionItem = WrappedItem => { 6 | const WithAccordionItem = /*#__PURE__*/forwardRef(({ 7 | itemKey, 8 | initialEntered, 9 | ...rest 10 | }, ref) => /*#__PURE__*/jsx(WrappedItem, { 11 | forwardedRef: ref, 12 | ...rest, 13 | ...useAccordionItemEffect({ 14 | itemKey, 15 | initialEntered, 16 | disabled: rest.disabled 17 | }) 18 | })); 19 | WithAccordionItem.displayName = 'WithAccordionItem'; 20 | return WithAccordionItem; 21 | }; 22 | 23 | export { withAccordionItem }; 24 | -------------------------------------------------------------------------------- /dist/esm/hooks/useAccordion.mjs: -------------------------------------------------------------------------------- 1 | import { ACCORDION_ATTR, ACCORDION_BTN_ATTR } from '../utils/constants.mjs'; 2 | 3 | const getAccordion = node => { 4 | do { 5 | node = node.parentElement; 6 | } while (node && !node.hasAttribute(ACCORDION_ATTR)); 7 | return node; 8 | }; 9 | const getNextIndex = (moveUp, current, length) => moveUp ? current > 0 ? current - 1 : length - 1 : (current + 1) % length; 10 | const moveFocus = (moveUp, e) => { 11 | const { 12 | activeElement 13 | } = document; 14 | if (!activeElement || !activeElement.hasAttribute(ACCORDION_BTN_ATTR) || getAccordion(activeElement) !== e.currentTarget) return; 15 | const nodes = e.currentTarget.querySelectorAll(`[${ACCORDION_BTN_ATTR}]`); 16 | const { 17 | length 18 | } = nodes; 19 | for (let i = 0; i < length; i++) { 20 | if (nodes[i] === activeElement) { 21 | let next = getNextIndex(moveUp, i, length); 22 | while (getAccordion(nodes[i]) !== getAccordion(nodes[next])) next = getNextIndex(moveUp, next, length); 23 | if (i !== next) { 24 | e.preventDefault(); 25 | nodes[next].focus(); 26 | } 27 | break; 28 | } 29 | } 30 | }; 31 | const useAccordion = () => { 32 | const accordionProps = { 33 | [ACCORDION_ATTR]: '', 34 | onKeyDown: e => e.key === 'ArrowUp' ? moveFocus(true, e) : e.key === 'ArrowDown' && moveFocus(false, e) 35 | }; 36 | return { 37 | accordionProps 38 | }; 39 | }; 40 | 41 | export { useAccordion }; 42 | -------------------------------------------------------------------------------- /dist/esm/hooks/useAccordionContext.mjs: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { AccordionContext } from '../utils/constants.mjs'; 3 | 4 | const getItemState = (providerValue, key, itemInitialEntered) => { 5 | const { 6 | stateMap, 7 | mountOnEnter, 8 | initialEntered 9 | } = providerValue; 10 | const _initialEntered = itemInitialEntered != null ? itemInitialEntered : initialEntered; 11 | return stateMap.get(key) || { 12 | status: _initialEntered ? 'entered' : mountOnEnter ? 'unmounted' : 'exited', 13 | isMounted: !mountOnEnter, 14 | isEnter: _initialEntered, 15 | isResolved: true 16 | }; 17 | }; 18 | const useAccordionContext = () => { 19 | const context = useContext(AccordionContext); 20 | if (process.env.NODE_ENV !== 'production' && !context.stateMap) { 21 | throw new Error('[React-Accordion] Cannot find a above this AccordionItem.'); 22 | } 23 | return context; 24 | }; 25 | 26 | export { getItemState, useAccordionContext }; 27 | -------------------------------------------------------------------------------- /dist/esm/hooks/useAccordionItem.mjs: -------------------------------------------------------------------------------- 1 | import { ACCORDION_BTN_ATTR } from '../utils/constants.mjs'; 2 | import { useId } from './useId.mjs'; 3 | 4 | const useAccordionItem = ({ 5 | state, 6 | toggle, 7 | disabled 8 | }) => { 9 | const buttonId = useId(); 10 | const panelId = buttonId && buttonId + '-'; 11 | const buttonProps = { 12 | id: buttonId, 13 | 'aria-controls': panelId, 14 | 'aria-expanded': state.isEnter, 15 | onClick: toggle 16 | }; 17 | disabled ? buttonProps.disabled = true : buttonProps[ACCORDION_BTN_ATTR] = ''; 18 | const panelProps = { 19 | id: panelId, 20 | 'aria-labelledby': buttonId, 21 | role: 'region' 22 | }; 23 | return { 24 | buttonProps, 25 | panelProps 26 | }; 27 | }; 28 | 29 | export { useAccordionItem }; 30 | -------------------------------------------------------------------------------- /dist/esm/hooks/useAccordionItemEffect.mjs: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect, useCallback } from 'react'; 2 | import { useAccordionContext, getItemState } from './useAccordionContext.mjs'; 3 | 4 | const useAccordionItemEffect = ({ 5 | itemKey, 6 | initialEntered, 7 | disabled 8 | } = {}) => { 9 | const itemRef = useRef(null); 10 | const context = useAccordionContext(); 11 | const key = itemKey != null ? itemKey : itemRef.current; 12 | const state = getItemState(context, key, initialEntered); 13 | const { 14 | setItem, 15 | deleteItem, 16 | toggle 17 | } = context; 18 | useEffect(() => { 19 | if (disabled) return; 20 | const key = itemKey != null ? itemKey : itemRef.current; 21 | setItem(key, { 22 | initialEntered 23 | }); 24 | return () => void deleteItem(key); 25 | }, [setItem, deleteItem, itemKey, initialEntered, disabled]); 26 | return { 27 | itemRef, 28 | state, 29 | toggle: useCallback(toEnter => toggle(key, toEnter), [toggle, key]) 30 | }; 31 | }; 32 | 33 | export { useAccordionItemEffect }; 34 | -------------------------------------------------------------------------------- /dist/esm/hooks/useAccordionProvider.mjs: -------------------------------------------------------------------------------- 1 | import { useTransitionMap } from 'react-transition-state'; 2 | 3 | const getTransition = (transition, name) => transition === true || !!(transition && transition[name]); 4 | const useAccordionProvider = ({ 5 | transition, 6 | transitionTimeout, 7 | ...rest 8 | } = {}) => { 9 | const transitionMap = useTransitionMap({ 10 | timeout: transitionTimeout, 11 | enter: getTransition(transition, 'enter'), 12 | exit: getTransition(transition, 'exit'), 13 | preEnter: getTransition(transition, 'preEnter'), 14 | preExit: getTransition(transition, 'preExit'), 15 | ...rest 16 | }); 17 | return { 18 | mountOnEnter: !!rest.mountOnEnter, 19 | initialEntered: !!rest.initialEntered, 20 | ...transitionMap 21 | }; 22 | }; 23 | 24 | export { useAccordionProvider }; 25 | -------------------------------------------------------------------------------- /dist/esm/hooks/useAccordionState.mjs: -------------------------------------------------------------------------------- 1 | import { useAccordionContext, getItemState } from './useAccordionContext.mjs'; 2 | 3 | const useAccordionState = () => { 4 | const context = useAccordionContext(); 5 | return { 6 | getItemState: (key, { 7 | initialEntered 8 | } = {}) => getItemState(context, key, initialEntered), 9 | toggle: context.toggle, 10 | toggleAll: context.toggleAll 11 | }; 12 | }; 13 | 14 | export { useAccordionState }; 15 | -------------------------------------------------------------------------------- /dist/esm/hooks/useHeightTransition.mjs: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from 'react'; 2 | import { useLayoutEffect as useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect.mjs'; 3 | 4 | const useHeightTransition = ({ 5 | status, 6 | isResolved 7 | }) => { 8 | const [height, setHeight] = useState(); 9 | const elementRef = useRef(null); 10 | useIsomorphicLayoutEffect(() => { 11 | (status === 'preEnter' || status === 'preExit') && setHeight(elementRef.current.getBoundingClientRect().height); 12 | }, [status]); 13 | const style = { 14 | height: status === 'preEnter' || status === 'exiting' ? 0 : status === 'entering' || status === 'preExit' ? height : undefined, 15 | overflow: isResolved ? undefined : 'hidden' 16 | }; 17 | return [style, elementRef]; 18 | }; 19 | 20 | export { useHeightTransition }; 21 | -------------------------------------------------------------------------------- /dist/esm/hooks/useId.mjs: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { ACCORDION_PREFIX } from '../utils/constants.mjs'; 3 | 4 | let current = 0; 5 | const useIdShim = () => { 6 | const [id, setId] = useState(); 7 | useEffect(() => setId(++current), []); 8 | return id && `${ACCORDION_PREFIX}-${id}`; 9 | }; 10 | const useId = React.useId || useIdShim; 11 | 12 | export { useId }; 13 | -------------------------------------------------------------------------------- /dist/esm/hooks/useMergeRef.mjs: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | 3 | function setRef(ref, instance) { 4 | typeof ref === 'function' ? ref(instance) : ref.current = instance; 5 | } 6 | function useMergeRef(refA, refB) { 7 | return useMemo(() => { 8 | if (!refA) return refB; 9 | if (!refB) return refA; 10 | return instance => { 11 | setRef(refA, instance); 12 | setRef(refB, instance); 13 | }; 14 | }, [refA, refB]); 15 | } 16 | 17 | export { useMergeRef }; 18 | -------------------------------------------------------------------------------- /dist/esm/index.mjs: -------------------------------------------------------------------------------- 1 | 2 | 'use client'; 3 | export { Accordion } from './components/Accordion.mjs'; 4 | export { AccordionItem } from './components/AccordionItem.mjs'; 5 | export { AccordionProvider } from './components/AccordionProvider.mjs'; 6 | export { ControlledAccordion } from './components/ControlledAccordion.mjs'; 7 | export { withAccordionItem } from './components/withAccordionItem.mjs'; 8 | export { useAccordion } from './hooks/useAccordion.mjs'; 9 | export { useAccordionItem } from './hooks/useAccordionItem.mjs'; 10 | export { useAccordionItemEffect } from './hooks/useAccordionItemEffect.mjs'; 11 | export { useAccordionProvider } from './hooks/useAccordionProvider.mjs'; 12 | export { useAccordionState } from './hooks/useAccordionState.mjs'; 13 | export { useHeightTransition } from './hooks/useHeightTransition.mjs'; 14 | export { useMergeRef } from './hooks/useMergeRef.mjs'; 15 | -------------------------------------------------------------------------------- /dist/esm/utils/bem.mjs: -------------------------------------------------------------------------------- 1 | const bem = (block, element, modifiers) => (className, props) => { 2 | const blockElement = element ? `${block}__${element}` : block; 3 | let classString = blockElement; 4 | modifiers && Object.keys(modifiers).forEach(name => { 5 | const value = modifiers[name]; 6 | if (value) classString += ` ${blockElement}--${value === true ? name : `${name}-${value}`}`; 7 | }); 8 | let expandedClassName = typeof className === 'function' ? className(props) : className; 9 | if (typeof expandedClassName === 'string') { 10 | expandedClassName = expandedClassName.trim(); 11 | if (expandedClassName) classString += ` ${expandedClassName}`; 12 | } 13 | return classString; 14 | }; 15 | 16 | export { bem }; 17 | -------------------------------------------------------------------------------- /dist/esm/utils/constants.mjs: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const ACCORDION_BLOCK = 'szh-accordion'; 4 | const ACCORDION_PREFIX = 'szh-adn'; 5 | const ACCORDION_ATTR = `data-${ACCORDION_PREFIX}`; 6 | const ACCORDION_BTN_ATTR = `data-${ACCORDION_PREFIX}-btn`; 7 | const AccordionContext = /*#__PURE__*/createContext({}); 8 | 9 | export { ACCORDION_ATTR, ACCORDION_BLOCK, ACCORDION_BTN_ATTR, ACCORDION_PREFIX, AccordionContext }; 10 | -------------------------------------------------------------------------------- /dist/esm/utils/mergeProps.mjs: -------------------------------------------------------------------------------- 1 | const mergeProps = (target, source) => { 2 | if (!source) return target; 3 | const result = { 4 | ...target 5 | }; 6 | Object.keys(source).forEach(key => { 7 | const targetProp = target[key]; 8 | const sourceProp = source[key]; 9 | if (typeof sourceProp === 'function' && targetProp) { 10 | result[key] = (...e) => { 11 | targetProp(...e); 12 | sourceProp(...e); 13 | }; 14 | } else { 15 | result[key] = sourceProp; 16 | } 17 | }); 18 | return result; 19 | }; 20 | 21 | export { mergeProps }; 22 | -------------------------------------------------------------------------------- /dist/esm/utils/useIsomorphicLayoutEffect.mjs: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useEffect } from 'react'; 2 | 3 | const useIsomorphicLayoutEffect = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined' ? useLayoutEffect : useEffect; 4 | 5 | export { useIsomorphicLayoutEffect as useLayoutEffect }; 6 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js'; 4 | import globals from 'globals'; 5 | import tseslint from 'typescript-eslint'; 6 | import prettier from 'eslint-config-prettier'; 7 | import jest from 'eslint-plugin-jest'; 8 | import react from 'eslint-plugin-react'; 9 | import reactHooks from 'eslint-plugin-react-hooks'; 10 | import reactHooksAddons from 'eslint-plugin-react-hooks-addons'; 11 | 12 | export default tseslint.config( 13 | eslint.configs.recommended, 14 | ...tseslint.configs.recommendedTypeChecked, 15 | jest.configs['flat/recommended'], 16 | jest.configs['flat/style'], 17 | react.configs.flat.recommended, 18 | reactHooksAddons.configs.recommended, 19 | prettier, 20 | { 21 | files: ['**/*.js', '**/*.mjs'], 22 | ...tseslint.configs.disableTypeChecked 23 | }, 24 | { 25 | ignores: [ 26 | '**/coverage/', 27 | '**/dist/', 28 | '**/example/', 29 | '**/types/', 30 | '**/build/', 31 | '**/static/', 32 | '**/.docusaurus/' 33 | ] 34 | }, 35 | { 36 | languageOptions: { 37 | ecmaVersion: 'latest', 38 | sourceType: 'module', 39 | parserOptions: { 40 | projectService: { 41 | allowDefaultProject: ['*.js', '*.mjs'] 42 | }, 43 | tsconfigRootDir: import.meta.dirname, 44 | ecmaFeatures: { 45 | jsx: true 46 | } 47 | }, 48 | globals: { 49 | ...globals.browser, 50 | ...globals.node, 51 | ...globals.jest 52 | } 53 | }, 54 | plugins: { 55 | jest, 56 | react, 57 | 'react-hooks': reactHooks 58 | }, 59 | settings: { 60 | react: { 61 | version: 'detect' 62 | } 63 | }, 64 | rules: { 65 | 'no-console': ['error', { allow: ['warn', 'error'] }], 66 | 'react/react-in-jsx-scope': 0, 67 | 'react/jsx-uses-react': 0, 68 | 'react/prop-types': 0, 69 | 'react-hooks/rules-of-hooks': 'error', 70 | 'react-hooks/exhaustive-deps': 'error', 71 | '@typescript-eslint/ban-ts-comment': 0, 72 | '@typescript-eslint/no-unused-expressions': [ 73 | 'error', 74 | { allowShortCircuit: true, allowTernary: true } 75 | ] 76 | } 77 | } 78 | ); 79 | -------------------------------------------------------------------------------- /example/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const config = require('../.eslintrc'); 2 | 3 | module.exports = { 4 | ...config, 5 | extends: [...config.extends, 'plugin:@next/next/recommended'], 6 | rules: { ...config.rules, 'no-console': 0 }, 7 | overrides: [ 8 | { 9 | files: ['*.ts', '*.tsx'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['@typescript-eslint'], 12 | extends: ['plugin:@typescript-eslint/recommended'] 13 | } 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | \*\*\***WARNING**\*\*\* 2 | 3 | _The purpose of this project is to assist with local development, rather than to demonstrate how to use the main library. The examples here do not always follow best practices for using the main library, and some may not even function properly._ 4 | 5 | If you're seeking proper examples, please refer to the [website](../website/) directory. 6 | 7 | ## Getting Started 8 | 9 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 10 | 11 | Run the development server: 12 | 13 | ```bash 14 | npm run dev 15 | ``` 16 | 17 | Open [http://localhost:8080](http://localhost:8080) with your browser to see the result. 18 | -------------------------------------------------------------------------------- /example/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /example/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 8080", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@szhsin/react-accordion": "file:..", 13 | "next": "^15.0.4", 14 | "react": "file:../node_modules/react", 15 | "react-dom": "file:../node_modules/react-dom" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^20", 19 | "@types/react": "file:../node_modules/@types/react", 20 | "@types/react-dom": "file:../node_modules/@types/react-dom", 21 | "eslint": "^8", 22 | "eslint-config-next": "^15.0.4", 23 | "typescript": "^5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css'; 2 | import type { AppProps } from 'next/app'; 3 | 4 | function MyApp({ Component, pageProps }: AppProps) { 5 | return ; 6 | } 7 | 8 | export default MyApp; 9 | -------------------------------------------------------------------------------- /example/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { forwardRef } from 'react'; 3 | import { Accordion, AccordionItem as Item, AccordionItemProps } from '@szhsin/react-accordion'; 4 | import styles from '../styles/Home.module.css'; 5 | 6 | const AccordionItem = forwardRef((props, ref) => ( 7 | 13 | )); 14 | 15 | AccordionItem.displayName = 'MyAccordionItem'; 16 | 17 | const Home: NextPage = () => { 18 | return ( 19 |
20 | 21 | 22 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt 23 | ut labore et dolore magna aliqua. 24 | 25 | 26 | 27 | Quisque eget luctus mi, vehicula mollis lorem. Proin fringilla vel erat quis sodales. Nam 28 | ex enim, eleifend venenatis lectus vitae, accumsan auctor mi. 29 | 30 | 31 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 32 | incididunt ut labore et dolore magna aliqua. 33 | 34 | 35 | 36 | Quisque eget luctus mi, vehicula mollis lorem. Proin fringilla vel erat quis sodales. 37 | Nam ex enim, eleifend venenatis lectus vitae, accumsan auctor mi. Quisque eget luctus 38 | mi, vehicula mollis lorem. Proin fringilla vel erat quis sodales. Nam ex enim, 39 | eleifend venenatis lectus vitae, accumsan auctor mi. Quisque eget luctus mi, vehicula 40 | mollis lorem. Proin fringilla vel erat quis sodales. Nam ex enim, eleifend venenatis 41 | lectus vitae, accumsan auctor mi. Quisque eget luctus mi, vehicula mollis lorem. Proin 42 | fringilla vel erat quis sodales. Nam ex enim, eleifend venenatis lectus vitae, 43 | accumsan auctor mi. Quisque eget luctus mi, vehicula mollis lorem. Proin fringilla vel 44 | erat quis sodales. Nam ex enim, eleifend venenatis lectus vitae, accumsan auctor mi. 45 | 46 | 47 | 48 | 49 | 50 | Suspendisse massa risus, pretium id interdum in, dictum sit amet ante. Fusce vulputate 51 | purus sed tempus feugiat. 52 | 53 | 54 |
55 |
56 | ); 57 | }; 58 | 59 | export default Home; 60 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szhsin/react-accordion/284d9e49ca23410b5b1ca94b93aa3cf61db15266/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /example/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .itemContent { 2 | transition: height 0.3s ease-in-out; 3 | } 4 | 5 | .itemPanel { 6 | padding: 1rem; 7 | } 8 | -------------------------------------------------------------------------------- /example/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | height: 2000px; 6 | font-family: 7 | -apple-system, 8 | BlinkMacSystemFont, 9 | Segoe UI, 10 | Roboto, 11 | Oxygen, 12 | Ubuntu, 13 | Cantarell, 14 | Fira Sans, 15 | Droid Sans, 16 | Helvetica Neue, 17 | sans-serif; 18 | } 19 | 20 | * { 21 | box-sizing: border-box; 22 | } 23 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jsdom', 3 | testMatch: ['**/*.test.[jt]s?(x)'], 4 | setupFilesAfterEnv: ['@testing-library/jest-dom'], 5 | clearMocks: true, 6 | collectCoverage: true 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@szhsin/react-accordion", 3 | "version": "1.4.1", 4 | "description": "The complete accordion solution for React.", 5 | "author": "Zheng Song", 6 | "license": "MIT", 7 | "repository": "szhsin/react-accordion", 8 | "homepage": "https://szhsin.github.io/react-accordion/", 9 | "main": "./dist/cjs/index.cjs", 10 | "module": "./dist/esm/index.mjs", 11 | "types": "./types/index.d.ts", 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "sideEffects": false, 16 | "files": [ 17 | "dist/", 18 | "types/" 19 | ], 20 | "keywords": [ 21 | "react", 22 | "accordion", 23 | "component", 24 | "hook", 25 | "unstyled", 26 | "headless UI", 27 | "design system", 28 | "accessibility", 29 | "wai-aria" 30 | ], 31 | "scripts": { 32 | "start": "run-p watch \"types -- --watch\"", 33 | "eg": "npm run dev --prefix example", 34 | "bundle": "rollup -c", 35 | "watch": "rollup -c -w", 36 | "clean": "rm -Rf dist types", 37 | "types": "tsc", 38 | "lint": "eslint .", 39 | "lint:fix": "eslint --fix .", 40 | "pret": "prettier -c .", 41 | "pret:fix": "prettier -w .", 42 | "test": "jest", 43 | "test:watch": "jest --watch --verbose", 44 | "test:cov": "jest --coverage=true", 45 | "postbuild": "rm -Rf types/__tests__", 46 | "build": "run-s pret clean types lint bundle" 47 | }, 48 | "exports": { 49 | ".": { 50 | "types": "./types/index.d.ts", 51 | "require": "./dist/cjs/index.cjs", 52 | "default": "./dist/esm/index.mjs" 53 | }, 54 | "./package.json": "./package.json" 55 | }, 56 | "peerDependencies": { 57 | "react": "^16.14 || ^17.0 || ^18.0 || ^19.0", 58 | "react-dom": "^16.14 || ^17.0 || ^18.0 || ^19.0" 59 | }, 60 | "dependencies": { 61 | "react-transition-state": "^2.3.1" 62 | }, 63 | "devDependencies": { 64 | "@babel/core": "^7.27.4", 65 | "@babel/preset-env": "^7.27.2", 66 | "@babel/preset-react": "^7.27.1", 67 | "@babel/preset-typescript": "^7.27.1", 68 | "@eslint/js": "^9.28.0", 69 | "@rollup/plugin-babel": "^6.0.4", 70 | "@rollup/plugin-node-resolve": "^16.0.1", 71 | "@testing-library/jest-dom": "^6.6.3", 72 | "@testing-library/react": "^16.3.0", 73 | "@types/jest": "^29.5.14", 74 | "@types/react": "^19.1.6", 75 | "@types/react-dom": "^19.1.5", 76 | "babel-plugin-pure-annotations": "^0.1.2", 77 | "eslint": "^9.28.0", 78 | "eslint-config-prettier": "^10.1.5", 79 | "eslint-plugin-jest": "^28.12.0", 80 | "eslint-plugin-react": "^7.37.5", 81 | "eslint-plugin-react-hooks": "^5.2.0", 82 | "eslint-plugin-react-hooks-addons": "^0.5.0", 83 | "globals": "^16.2.0", 84 | "jest": "^29.7.0", 85 | "jest-environment-jsdom": "^29.7.0", 86 | "npm-run-all": "^4.1.5", 87 | "prettier": "^3.5.3", 88 | "react": "^19.1.0", 89 | "react-dom": "^19.1.0", 90 | "rollup": "^4.41.1", 91 | "rollup-plugin-add-directive": "^1.0.0", 92 | "typescript": "^5.8.3", 93 | "typescript-eslint": "^8.33.0" 94 | }, 95 | "overrides": { 96 | "whatwg-url@11.0.0": { 97 | "tr46": "^4" 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 4 | import { babel } from '@rollup/plugin-babel'; 5 | import { addDirective } from 'rollup-plugin-add-directive'; 6 | 7 | /** 8 | * @type {import('rollup').RollupOptions} 9 | */ 10 | export default { 11 | external: ['react', 'react-dom', 'react/jsx-runtime', 'react-transition-state'], 12 | plugins: [ 13 | nodeResolve({ extensions: ['.ts', '.tsx', '.js', '.jsx'] }), 14 | babel({ 15 | babelHelpers: 'bundled', 16 | extensions: ['.ts', '.tsx', '.js', '.jsx'] 17 | }), 18 | addDirective({ pattern: 'index' }) 19 | ], 20 | treeshake: { 21 | moduleSideEffects: false, 22 | propertyReadSideEffects: false 23 | }, 24 | input: 'src/index.ts', 25 | output: [ 26 | { 27 | dir: 'dist/cjs', 28 | format: 'cjs', 29 | interop: 'default', 30 | entryFileNames: '[name].cjs', 31 | preserveModules: true 32 | }, 33 | { 34 | dir: 'dist/esm', 35 | format: 'es', 36 | entryFileNames: '[name].mjs', 37 | preserveModules: true 38 | } 39 | ] 40 | }; 41 | -------------------------------------------------------------------------------- /src/__tests__/components/Accordion.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, fireEvent } from '@testing-library/react'; 2 | import { render, getAccordion } from '../utils'; 3 | import { Accordion, AccordionItem } from '../../'; 4 | 5 | test('Accordion should forward props and ref', () => { 6 | const ref = jest.fn(); 7 | const onKeyDown = jest.fn(); 8 | render( 9 | getAccordion({ 10 | props: { 'data-testid': 'accordion', className: 'my-accordion', onKeyDown }, 11 | ref 12 | }) 13 | ); 14 | expect(screen.getByTestId('accordion')).toHaveClass('szh-accordion my-accordion'); 15 | expect(ref).toHaveBeenCalled(); 16 | const button = screen.getByRole('button', { name: 'header 1' }); 17 | button.focus(); 18 | fireEvent.keyDown(button, { key: 'ArrowDown' }); 19 | expect(onKeyDown).toHaveBeenCalled(); 20 | }); 21 | 22 | test('Accordion should expand and collapse items', () => { 23 | render(getAccordion()); 24 | expect(screen.queryByRole('region', { name: 'header 1' })).toBeNull(); 25 | expect(screen.queryByRole('region', { name: 'header 2' })).toBeNull(); 26 | expect(screen.queryByRole('region', { name: 'header 3' })).toBeNull(); 27 | 28 | fireEvent.click(screen.getByRole('button', { name: 'header 1' })); 29 | expect(screen.queryByRole('region', { name: 'header 1' })).toBeVisible(); 30 | expect(screen.queryByRole('region', { name: 'header 2' })).toBeNull(); 31 | expect(screen.queryByRole('region', { name: 'header 3' })).toBeNull(); 32 | 33 | fireEvent.click(screen.getByRole('button', { name: 'header 2' })); 34 | expect(screen.queryByRole('region', { name: 'header 1' })).toBeNull(); 35 | expect(screen.queryByRole('region', { name: 'header 2' })).toBeVisible(); 36 | expect(screen.queryByRole('region', { name: 'header 3' })).toBeNull(); 37 | }); 38 | 39 | test('Accordion should allow multiple items to expand', () => { 40 | render(getAccordion({ props: { allowMultiple: true }, item1Props: { initialEntered: true } })); 41 | expect(screen.queryByRole('region', { name: 'header 1' })).toBeVisible(); 42 | expect(screen.queryByRole('region', { name: 'header 2' })).toBeNull(); 43 | expect(screen.queryByRole('region', { name: 'header 3' })).toBeNull(); 44 | 45 | fireEvent.click(screen.getByRole('button', { name: 'header 2' })); 46 | expect(screen.queryByRole('region', { name: 'header 1' })).toBeVisible(); 47 | expect(screen.queryByRole('region', { name: 'header 2' })).toBeVisible(); 48 | expect(screen.queryByRole('region', { name: 'header 3' })).toBeNull(); 49 | }); 50 | 51 | test('Accordion should support keyboard interaction', () => { 52 | const onKeyDown = jest.fn(); 53 | render( 54 | 55 | item 1 56 | 57 | 58 | item 2.1 59 | item 2.2 60 | 61 | 62 | 63 | disabled item is excluded from keyboard navigation 64 | 65 | 66 | 67 | item 3.1 68 | 69 | 70 | 71 | ); 72 | 73 | // Top level navigation 74 | let currentFocus = screen.getByRole('button', { name: 'header 1' }); 75 | currentFocus.focus(); 76 | expect(currentFocus).toHaveFocus(); 77 | 78 | fireEvent.keyDown(currentFocus, { key: 'ArrowDown' }); 79 | currentFocus = screen.getByRole('button', { name: 'header 2' }); 80 | expect(currentFocus).toHaveFocus(); 81 | 82 | fireEvent.keyDown(currentFocus, { key: 'ArrowDown' }); 83 | currentFocus = screen.getByRole('button', { name: 'header 3' }); 84 | expect(currentFocus).toHaveFocus(); 85 | 86 | fireEvent.keyDown(currentFocus, { key: 'ArrowDown' }); 87 | currentFocus = screen.getByRole('button', { name: 'header 1' }); 88 | expect(currentFocus).toHaveFocus(); 89 | 90 | fireEvent.keyDown(currentFocus, { key: 'ArrowUp' }); 91 | currentFocus = screen.getByRole('button', { name: 'header 3' }); 92 | expect(currentFocus).toHaveFocus(); 93 | 94 | // Nested level navigation 95 | currentFocus = screen.getByRole('button', { name: 'header 2.1' }); 96 | currentFocus.focus(); 97 | expect(currentFocus).toHaveFocus(); 98 | 99 | fireEvent.keyDown(currentFocus, { key: 'ArrowDown' }); 100 | currentFocus = screen.getByRole('button', { name: 'header 2.2' }); 101 | expect(currentFocus).toHaveFocus(); 102 | 103 | fireEvent.keyDown(currentFocus, { key: 'ArrowDown' }); 104 | currentFocus = screen.getByRole('button', { name: 'header 2.1' }); 105 | expect(currentFocus).toHaveFocus(); 106 | 107 | fireEvent.keyDown(currentFocus, { key: 'ArrowUp' }); 108 | currentFocus = screen.getByRole('button', { name: 'header 2.2' }); 109 | expect(currentFocus).toHaveFocus(); 110 | // eslint-disable-next-line 111 | expect(onKeyDown.mock.lastCall[0].isDefaultPrevented()).toBe(true); 112 | 113 | // Nested level navigation through a single item 114 | currentFocus = screen.getByRole('button', { name: 'header 3.1' }); 115 | currentFocus.focus(); 116 | expect(currentFocus).toHaveFocus(); 117 | 118 | fireEvent.keyDown(currentFocus, { key: 'ArrowDown' }); 119 | expect(currentFocus).toHaveFocus(); 120 | // eslint-disable-next-line 121 | expect(onKeyDown.mock.lastCall[0].isDefaultPrevented()).toBe(false); 122 | }); 123 | -------------------------------------------------------------------------------- /src/__tests__/components/AccordionItem.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, fireEvent, act } from '@testing-library/react'; 2 | import { TransitionState } from 'react-transition-state'; 3 | import { render, getAccordion } from '../utils'; 4 | import { AccordionItem, AccordionItemProps, ItemState } from '../../'; 5 | 6 | describe('AccordionItem', () => { 7 | test('should throw when rendered without provider', () => { 8 | expect(() => { 9 | render(item); 10 | }).toThrow('Cannot find a above this AccordionItem'); 11 | }); 12 | 13 | test('should forward props and ref to item', () => { 14 | const ref = jest.fn(); 15 | render( 16 | getAccordion({ item1Props: { 'data-testid': 'item1', className: 'item-1' }, item1Ref: ref }) 17 | ); 18 | expect(screen.getByTestId('item1')).toHaveClass( 19 | 'szh-accordion__item szh-accordion__item--status-exited item-1' 20 | ); 21 | expect(ref).toHaveBeenCalled(); 22 | }); 23 | 24 | test('should forward props and ref to heading', () => { 25 | const ref = jest.fn(); 26 | render( 27 | getAccordion({ 28 | item1Props: { 29 | headingProps: { 30 | ref, 31 | 'data-testid': 'heading1', 32 | className: 'heading-1', 33 | style: { color: 'green' } 34 | } 35 | } 36 | }) 37 | ); 38 | const heading = screen.getByRole('heading', { name: 'header 1', level: 3 }); 39 | expect(heading).toHaveClass('szh-accordion__item-heading heading-1'); 40 | expect(heading).toHaveAttribute('data-testid', 'heading1'); 41 | expect(heading).toHaveStyle({ color: 'green', margin: 0 }); 42 | expect(ref).toHaveBeenCalled(); 43 | }); 44 | 45 | test('should forward props and ref to button', () => { 46 | const ref = jest.fn(); 47 | render( 48 | getAccordion({ 49 | item1Props: { 50 | buttonProps: { 51 | ref, 52 | 'data-testid': 'button1', 53 | className: 'button-1', 54 | style: { color: 'blue' } 55 | } 56 | } 57 | }) 58 | ); 59 | const button = screen.getByRole('button', { name: 'header 1' }); 60 | expect(button).toHaveClass('szh-accordion__item-btn button-1'); 61 | expect(button).toHaveAttribute('data-testid', 'button1'); 62 | expect(button).toHaveStyle({ color: 'blue' }); 63 | expect(button).toHaveTextContent('header 1'); 64 | expect(ref).toHaveBeenCalled(); 65 | }); 66 | 67 | test('should forward props and ref to content', () => { 68 | const ref = jest.fn(); 69 | render( 70 | getAccordion({ 71 | item2Props: { 72 | contentProps: { 73 | ref, 74 | 'data-testid': 'content2', 75 | className: 'content-2', 76 | style: { color: 'green' } 77 | } 78 | } 79 | }) 80 | ); 81 | const content = screen.getByTestId('content2'); 82 | expect(content).toHaveClass('szh-accordion__item-content content-2'); 83 | expect(content).toHaveStyle({ color: 'green' }); 84 | expect(ref).toHaveBeenCalled(); 85 | }); 86 | 87 | test('should forward props and ref to panel', () => { 88 | const ref = jest.fn(); 89 | render( 90 | getAccordion({ 91 | item2Props: { 92 | panelProps: { 93 | ref, 94 | 'data-testid': 'panel2', 95 | className: 'panel-2', 96 | style: { color: 'green' } 97 | } 98 | } 99 | }) 100 | ); 101 | const panel = screen.getByTestId('panel2'); 102 | expect(panel).toHaveClass('szh-accordion__item-panel panel-2'); 103 | expect(panel).toHaveStyle({ color: 'green' }); 104 | expect(panel).toHaveTextContent('item 2'); 105 | expect(ref).toHaveBeenCalled(); 106 | }); 107 | }); 108 | 109 | describe('className function', () => { 110 | const className = jest.fn(); 111 | test.each([ 112 | [{ className }], 113 | [{ headingProps: { className } }], 114 | [{ buttonProps: { className } }], 115 | [{ contentProps: { className } }], 116 | [{ panelProps: { className } }] 117 | ])('should receive modifier params %#', (item1Props: AccordionItemProps) => { 118 | render( 119 | getAccordion({ 120 | item1Props 121 | }) 122 | ); 123 | const params = expect.objectContaining({ 124 | status: 'exited', 125 | isEnter: false, 126 | isMounted: true, 127 | isResolved: true 128 | }) as TransitionState; 129 | expect(className).toHaveBeenNthCalledWith(1, params); 130 | expect(className).toHaveBeenLastCalledWith(params); 131 | fireEvent.click(screen.getByRole('button', { name: 'header 1' })); 132 | expect(className).toHaveBeenLastCalledWith( 133 | expect.objectContaining({ 134 | status: 'entered', 135 | isEnter: true, 136 | isMounted: true, 137 | isResolved: true 138 | }) 139 | ); 140 | }); 141 | }); 142 | 143 | describe('Header and children render prop', () => { 144 | const getRenderPropParam = (state: Partial) => ({ 145 | state: expect.objectContaining(state) as ItemState['state'], 146 | toggle: expect.any(Function) as ItemState['toggle'] 147 | }); 148 | 149 | let toggle!: ItemState['toggle']; 150 | const header = jest.fn().mockImplementation(({ toggle: _toggle }: ItemState) => { 151 | toggle = _toggle; 152 | return 'custom header'; 153 | }); 154 | const children = jest.fn().mockReturnValue('custom children'); 155 | 156 | test.each([ 157 | { 158 | props: {}, 159 | item1Props: { header, children }, 160 | expectedState1: { 161 | status: 'exited', 162 | isEnter: false, 163 | isMounted: true, 164 | isResolved: true 165 | }, 166 | expectedState2: { status: 'entered' } 167 | }, 168 | { 169 | props: {}, 170 | item1Props: { header, children, initialEntered: true }, 171 | expectedState1: { 172 | status: 'entered', 173 | isEnter: true, 174 | isMounted: true, 175 | isResolved: true 176 | }, 177 | expectedState2: { status: 'exited' } 178 | }, 179 | { 180 | props: { initialEntered: true }, 181 | item1Props: { header, children }, 182 | expectedState1: { 183 | status: 'entered', 184 | isEnter: true, 185 | isMounted: true, 186 | isResolved: true 187 | }, 188 | expectedState2: { status: 'exited' } 189 | }, 190 | { 191 | props: { initialEntered: true }, 192 | item1Props: { header, children, initialEntered: false }, 193 | expectedState1: { 194 | status: 'exited', 195 | isEnter: false, 196 | isMounted: true, 197 | isResolved: true 198 | }, 199 | expectedState2: { status: 'entered' } 200 | }, 201 | { 202 | props: { mountOnEnter: true }, 203 | item1Props: { header, children }, 204 | expectedState1: { 205 | status: 'unmounted', 206 | isEnter: false, 207 | isMounted: false, 208 | isResolved: true 209 | }, 210 | expectedState2: { status: 'entered' } 211 | } 212 | ] as const)('scenario %#', ({ props, item1Props, expectedState1, expectedState2 }) => { 213 | render( 214 | getAccordion({ 215 | props, 216 | item1Props 217 | }) 218 | ); 219 | screen.getByRole('button', { name: 'custom header' }); 220 | const params = getRenderPropParam(expectedState1); 221 | expect(header).toHaveBeenNthCalledWith(1, params); 222 | expect(header).toHaveBeenLastCalledWith(params); 223 | 224 | const prevToggle = toggle; 225 | act(() => { 226 | toggle(); 227 | }); 228 | expect(header).toHaveBeenLastCalledWith(getRenderPropParam(expectedState2)); 229 | expect(children).toHaveBeenLastCalledWith(getRenderPropParam(expectedState2)); 230 | expect(screen.getByRole('region', { name: 'custom header', hidden: true })).toHaveTextContent( 231 | 'custom children' 232 | ); 233 | expect(toggle).toBe(prevToggle); 234 | }); 235 | }); 236 | 237 | test('AccordionItem should set DOM attributes', () => { 238 | const onClick = jest.fn(); 239 | render( 240 | getAccordion({ 241 | item1Props: { 242 | buttonProps: { 'data-testid': 'button', onClick }, 243 | panelProps: { 'data-testid': 'panel' } 244 | }, 245 | item2Props: { 246 | disabled: true 247 | } 248 | }) 249 | ); 250 | 251 | const button = screen.getByTestId('button'); 252 | const panel = screen.getByTestId('panel'); 253 | expect(button).toHaveAttribute('aria-controls', panel.id); 254 | expect(button).toHaveAttribute('aria-expanded', 'false'); 255 | expect(panel).toHaveAttribute('aria-labelledby', button.id); 256 | expect(panel).toHaveAttribute('role', 'region'); 257 | 258 | fireEvent.click(button); 259 | expect(button).toHaveAttribute('aria-expanded', 'true'); 260 | expect(onClick).toHaveBeenCalled(); 261 | 262 | expect(screen.getByRole('button', { name: 'header 2' })).toBeDisabled(); 263 | }); 264 | 265 | test('Heading level can be customised', () => { 266 | render( 267 | getAccordion({ 268 | item1Props: { 269 | headingTag: 'h1' 270 | }, 271 | item2Props: { 272 | headingTag: 'h2' 273 | } 274 | }) 275 | ); 276 | 277 | expect(screen.getByRole('heading', { level: 1, name: 'header 1' })).toBeInTheDocument(); 278 | expect(screen.getByRole('heading', { level: 2, name: 'header 2' })).toBeInTheDocument(); 279 | expect(screen.getByRole('heading', { level: 3, name: 'header 3' })).toBeInTheDocument(); 280 | }); 281 | 282 | test('AccordionItem should lazily mount content when mountOnEnter is true', () => { 283 | render( 284 | getAccordion({ 285 | props: { mountOnEnter: true }, 286 | item1Props: { 287 | 'data-testid': 'item', 288 | contentProps: { 'data-testid': 'content' } 289 | } 290 | }) 291 | ); 292 | 293 | expect(screen.getByTestId('item')).toHaveClass('szh-accordion__item--status-unmounted'); 294 | expect(screen.queryByTestId('content')).not.toBeInTheDocument(); 295 | 296 | fireEvent.click(screen.getByRole('button', { name: 'header 1' })); 297 | expect(screen.getByTestId('item')).toHaveClass('szh-accordion__item--status-entered'); 298 | expect(screen.getByTestId('content')).toBeInTheDocument(); 299 | }); 300 | 301 | test('AccordionItem should not render when the state of other items is updated', () => { 302 | const children1 = jest.fn(); 303 | const children2 = jest.fn(); 304 | render( 305 | getAccordion({ 306 | props: { allowMultiple: true }, 307 | item1Props: { 308 | children: children1 309 | }, 310 | item2Props: { 311 | children: children2 312 | } 313 | }) 314 | ); 315 | 316 | expect(children1).toHaveBeenCalled(); 317 | expect(children2).toHaveBeenCalled(); 318 | 319 | jest.clearAllMocks(); 320 | fireEvent.click(screen.getByRole('button', { name: 'header 1' })); 321 | expect(children1).toHaveBeenCalled(); 322 | expect(children2).not.toHaveBeenCalled(); 323 | 324 | jest.clearAllMocks(); 325 | fireEvent.click(screen.getByRole('button', { name: 'header 2' })); 326 | expect(children1).not.toHaveBeenCalled(); 327 | expect(children2).toHaveBeenCalled(); 328 | }); 329 | -------------------------------------------------------------------------------- /src/__tests__/components/AccordionMock.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '../utils'; 2 | import { Accordion, AccordionProps, useAccordionProvider } from '../../'; 3 | 4 | jest.mock('../../hooks/useAccordionProvider'); 5 | 6 | test('Accordion should forward props to useAccordionProvider', () => { 7 | const props: AccordionProps = { 8 | allowMultiple: true, 9 | initialEntered: true, 10 | mountOnEnter: false, 11 | unmountOnExit: false, 12 | transition: true, 13 | transitionTimeout: 300, 14 | onStateChange: jest.fn() 15 | }; 16 | render(Accordion); 17 | expect(useAccordionProvider).toHaveBeenCalledWith(props); 18 | }); 19 | -------------------------------------------------------------------------------- /src/__tests__/components/ControlledAccordion.test.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { screen, fireEvent } from '@testing-library/react'; 3 | import { render } from '../utils'; 4 | import { ControlledAccordion, AccordionItem, useAccordionProvider } from '../../'; 5 | 6 | const Accordion = () => { 7 | const value = useAccordionProvider(); 8 | const ref = useRef(null); 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 | item 1 16 | 17 | 18 | item 2 19 | 20 | item 3 21 | 22 |
23 | ); 24 | }; 25 | 26 | test('ControlledAccordion', () => { 27 | render(); 28 | expect(screen.queryByRole('region', { name: 'header 1' })).toBeNull(); 29 | expect(screen.queryByRole('region', { name: 'header 2' })).toBeVisible(); 30 | 31 | fireEvent.click(screen.getByRole('button', { name: 'Toggle' })); 32 | expect(screen.queryByRole('region', { name: 'header 2' })).toBeNull(); 33 | expect(screen.queryByRole('region', { name: 'header 1' })).toBeVisible(); 34 | }); 35 | -------------------------------------------------------------------------------- /src/__tests__/globals.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /src/__tests__/hooks/useAccordionProvider.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import { useTransitionMap } from 'react-transition-state'; 3 | import { useAccordionProvider } from '../../'; 4 | 5 | jest.mock('react-transition-state', () => ({ 6 | useTransitionMap: jest.fn().mockReturnValue({ mock: true }) 7 | })); 8 | 9 | describe('useAccordionProvider', () => { 10 | test('can be called without params', () => { 11 | const { result } = renderHook(() => useAccordionProvider()); 12 | expect(result.current).toEqual({ mock: true, mountOnEnter: false, initialEntered: false }); 13 | expect(useTransitionMap).toHaveBeenCalledWith({ 14 | enter: false, 15 | exit: false, 16 | preEnter: false, 17 | preExit: false 18 | }); 19 | }); 20 | 21 | test('should forward options', () => { 22 | const { result } = renderHook(() => 23 | useAccordionProvider({ 24 | mountOnEnter: true, 25 | initialEntered: true, 26 | transition: true, 27 | transitionTimeout: 300 28 | }) 29 | ); 30 | expect(result.current).toEqual({ mock: true, mountOnEnter: true, initialEntered: true }); 31 | expect(useTransitionMap).toHaveBeenCalledWith({ 32 | mountOnEnter: true, 33 | initialEntered: true, 34 | enter: true, 35 | exit: true, 36 | preEnter: true, 37 | preExit: true, 38 | timeout: 300 39 | }); 40 | }); 41 | 42 | test('should accept individual transition flag', () => { 43 | const { result } = renderHook(() => 44 | useAccordionProvider({ 45 | transition: { enter: true, preExit: true } 46 | }) 47 | ); 48 | expect(result.current).toEqual({ mock: true, mountOnEnter: false, initialEntered: false }); 49 | expect(useTransitionMap).toHaveBeenCalledWith({ 50 | enter: true, 51 | exit: false, 52 | preEnter: false, 53 | preExit: true 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/__tests__/hooks/useAccordionState.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, fireEvent } from '@testing-library/react'; 2 | import { render, getAccordion } from '../utils'; 3 | import { useAccordionState } from '../../'; 4 | 5 | const MyComponent = ({ itemKey }: { itemKey: string }) => { 6 | const { getItemState, toggle, toggleAll } = useAccordionState(); 7 | return ( 8 | <> 9 |
{getItemState(itemKey).status}
10 | 13 | 16 | 17 | ); 18 | }; 19 | 20 | test('useAccordionState toggle item', () => { 21 | render( 22 | getAccordion({ 23 | item1Props: { 24 | itemKey: 'item1', 25 | children: 26 | } 27 | }) 28 | ); 29 | expect(screen.getByTestId('item-state')).toHaveTextContent('exited'); 30 | fireEvent.click(screen.getByTestId('toggle-item')); 31 | expect(screen.getByTestId('item-state')).toHaveTextContent('entered'); 32 | }); 33 | 34 | test('useAccordionState toggle all', () => { 35 | render( 36 | getAccordion({ 37 | props: { 38 | allowMultiple: true 39 | }, 40 | item2Props: { 41 | initialEntered: true, 42 | children: 43 | } 44 | }) 45 | ); 46 | expect(screen.queryByRole('region', { name: 'header 1' })).toBeNull(); 47 | expect(screen.queryByRole('region', { name: 'header 2' })).toBeVisible(); 48 | expect(screen.queryByRole('region', { name: 'header 3' })).toBeNull(); 49 | 50 | fireEvent.click(screen.getByTestId('toggle-all')); 51 | expect(screen.queryByRole('region', { name: 'header 1' })).toBeVisible(); 52 | expect(screen.queryByRole('region', { name: 'header 2' })).toBeNull(); 53 | expect(screen.queryByRole('region', { name: 'header 3' })).toBeVisible(); 54 | }); 55 | -------------------------------------------------------------------------------- /src/__tests__/hooks/useId.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import { useId } from '../../hooks/useId'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 5 | jest.mock('react', () => ({ 6 | ...jest.requireActual('react'), 7 | useId: undefined 8 | })); 9 | 10 | test('useId', () => { 11 | const id1 = renderHook(() => useId()); 12 | const id2 = renderHook(() => useId()); 13 | expect(id1.result.current).toBe('szh-adn-1'); 14 | expect(id2.result.current).toBe('szh-adn-2'); 15 | id1.rerender(); 16 | id2.rerender(); 17 | expect(id1.result.current).toBe('szh-adn-1'); 18 | expect(id2.result.current).toBe('szh-adn-2'); 19 | }); 20 | -------------------------------------------------------------------------------- /src/__tests__/ssr.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | import { renderToString } from 'react-dom/server'; 6 | import { getAccordion } from './utils'; 7 | 8 | describe('Server rendering', () => { 9 | test('defalut state', () => { 10 | const htmlString = renderToString(getAccordion()); 11 | expect(htmlString).toContain('szh-accordion__item--status-exited'); 12 | expect(htmlString).toContain('item 1'); 13 | }); 14 | 15 | test('mountOnEnter', () => { 16 | const htmlString = renderToString(getAccordion({ props: { mountOnEnter: true } })); 17 | expect(htmlString).toContain('szh-accordion__item--status-unmounted'); 18 | expect(htmlString).not.toContain('szh-accordion__item--status-exited'); 19 | expect(htmlString).not.toContain('item 1'); 20 | }); 21 | 22 | test('initialEntered', () => { 23 | const htmlString = renderToString(getAccordion({ props: { initialEntered: true } })); 24 | expect(htmlString).toContain('szh-accordion__item--status-entered'); 25 | expect(htmlString).not.toContain('szh-accordion__item--status-exited'); 26 | expect(htmlString).toContain('item 1'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/__tests__/utils.tsx: -------------------------------------------------------------------------------- 1 | import { Ref, ReactElement, StrictMode } from 'react'; 2 | import { render as nonStrictRender, RenderOptions, RenderResult } from '@testing-library/react'; 3 | import { Accordion, AccordionProps, AccordionItem, AccordionItemProps } from '../'; 4 | 5 | export { nonStrictRender }; 6 | 7 | export const render: ( 8 | ui: ReactElement, 9 | options?: Omit 10 | ) => RenderResult = (ui, options) => nonStrictRender(ui, { wrapper: StrictMode, ...options }); 11 | 12 | export const getAccordion = ({ 13 | ref, 14 | item1Ref, 15 | props, 16 | item1Props, 17 | item2Props 18 | }: { 19 | ref?: Ref; 20 | item1Ref?: Ref; 21 | props?: AccordionProps; 22 | item1Props?: AccordionItemProps; 23 | item2Props?: AccordionItemProps; 24 | } = {}) => ( 25 | 26 | 27 | {item1Props?.children || 'item 1'} 28 | 29 | 30 | {item2Props?.children || 'item 2'} 31 | 32 | item 3 33 | 34 | ); 35 | -------------------------------------------------------------------------------- /src/components/Accordion.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | import { AccordionProviderOptions } from '../utils/constants'; 3 | import { useAccordionProvider } from '../hooks/useAccordionProvider'; 4 | import { ControlledAccordion, ControlledAccordionProps } from './ControlledAccordion'; 5 | 6 | interface AccordionProps 7 | extends AccordionProviderOptions, 8 | Omit {} 9 | 10 | const Accordion = forwardRef( 11 | ( 12 | { 13 | allowMultiple, 14 | initialEntered, 15 | mountOnEnter, 16 | unmountOnExit, 17 | transition, 18 | transitionTimeout, 19 | onStateChange, 20 | ...rest 21 | }, 22 | ref 23 | ) => { 24 | const providerValue = useAccordionProvider({ 25 | allowMultiple, 26 | initialEntered, 27 | mountOnEnter, 28 | unmountOnExit, 29 | transition, 30 | transitionTimeout, 31 | onStateChange 32 | }); 33 | return ; 34 | } 35 | ); 36 | 37 | Accordion.displayName = 'Accordion'; 38 | 39 | export { Accordion, AccordionProps }; 40 | -------------------------------------------------------------------------------- /src/components/AccordionItem.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, ForwardedRef, memo } from 'react'; 2 | import { TransitionState } from 'react-transition-state'; 3 | import { ACCORDION_BLOCK, ElementProps, ItemState, ItemStateOptions } from '../utils/constants'; 4 | import { bem } from '../utils/bem'; 5 | import { mergeProps } from '../utils/mergeProps'; 6 | import { useAccordionItem } from '../hooks/useAccordionItem'; 7 | import { useHeightTransition } from '../hooks/useHeightTransition'; 8 | import { useMergeRef } from '../hooks/useMergeRef'; 9 | import { withAccordionItem, ItemStateProps } from './withAccordionItem'; 10 | 11 | interface ItemElementProps extends ElementProps { 12 | ref?: ForwardedRef; 13 | } 14 | 15 | type NodeOrFunc = ReactNode | ((props: ItemState) => ReactNode); 16 | 17 | interface AccordionItemProps 18 | extends ItemStateOptions, 19 | ElementProps { 20 | header?: NodeOrFunc; 21 | children?: NodeOrFunc; 22 | headingTag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; 23 | headingProps?: ItemElementProps; 24 | buttonProps?: ItemElementProps; 25 | contentProps?: ItemElementProps; 26 | panelProps?: ItemElementProps; 27 | } 28 | 29 | interface WrappedItemProps 30 | extends ItemStateProps, 31 | Omit {} 32 | 33 | const getRenderNode:

( 34 | nodeOrFunc: ReactNode | ((props: P) => ReactNode), 35 | props: P 36 | ) => ReactNode = (nodeOrFunc, props) => 37 | typeof nodeOrFunc === 'function' ? nodeOrFunc(props) : nodeOrFunc; 38 | 39 | const WrappedItem = memo( 40 | ({ 41 | forwardedRef, 42 | itemRef, 43 | state, 44 | toggle, 45 | className, 46 | disabled, 47 | header, 48 | headingTag: Heading = 'h3', 49 | headingProps, 50 | buttonProps, 51 | contentProps, 52 | panelProps, 53 | children, 54 | ...rest 55 | }: WrappedItemProps) => { 56 | const itemState: ItemState = { state, toggle, disabled }; 57 | const { buttonProps: _buttonProps, panelProps: _panelProps } = useAccordionItem(itemState); 58 | const [transitionStyle, _panelRef] = useHeightTransition(state); 59 | const panelRef = useMergeRef(panelProps?.ref, _panelRef); 60 | const { status, isMounted, isEnter } = state; 61 | 62 | return ( 63 |

68 | 73 | 80 | 81 | 82 | {isMounted && ( 83 |
92 |
97 | {getRenderNode(children, itemState)} 98 |
99 |
100 | )} 101 |
102 | ); 103 | } 104 | ); 105 | 106 | WrappedItem.displayName = 'AccordionItem'; 107 | const AccordionItem = withAccordionItem(WrappedItem); 108 | 109 | export { AccordionItem, AccordionItemProps }; 110 | -------------------------------------------------------------------------------- /src/components/AccordionProvider.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { AccordionContext, AccordionProviderValue } from '../utils/constants'; 3 | 4 | const AccordionProvider = (props: { value: AccordionProviderValue; children?: ReactNode }) => ( 5 | 6 | ); 7 | 8 | export { AccordionProvider }; 9 | -------------------------------------------------------------------------------- /src/components/ControlledAccordion.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, forwardRef } from 'react'; 2 | import { ACCORDION_BLOCK, AccordionProviderValue, ElementProps } from '../utils/constants'; 3 | import { bem } from '../utils/bem'; 4 | import { mergeProps } from '../utils/mergeProps'; 5 | import { AccordionProvider } from './AccordionProvider'; 6 | import { useAccordion } from '../hooks/useAccordion'; 7 | 8 | interface ControlledAccordionProps extends ElementProps { 9 | providerValue: AccordionProviderValue; 10 | children?: ReactNode; 11 | } 12 | 13 | const ControlledAccordion = forwardRef( 14 | ({ providerValue, className, ...rest }, ref) => { 15 | const { accordionProps } = useAccordion(); 16 | return ( 17 | 18 |
23 | 24 | ); 25 | } 26 | ); 27 | 28 | ControlledAccordion.displayName = 'ControlledAccordion'; 29 | 30 | export { ControlledAccordion, ControlledAccordionProps }; 31 | -------------------------------------------------------------------------------- /src/components/withAccordionItem.tsx: -------------------------------------------------------------------------------- 1 | import type { RefObject, ForwardedRef, MemoExoticComponent, JSX } from 'react'; 2 | import { forwardRef } from 'react'; 3 | import type { ItemState, ItemStateOptions } from '../utils/constants'; 4 | import { useAccordionItemEffect } from '../hooks/useAccordionItemEffect'; 5 | 6 | interface ItemStateProps extends ItemState { 7 | itemRef: RefObject; 8 | forwardedRef: ForwardedRef; 9 | } 10 | 11 | const withAccordionItem =

( 12 | WrappedItem: MemoExoticComponent<(props: ItemStateProps) => JSX.Element> 13 | ) => { 14 | const WithAccordionItem = forwardRef(({ itemKey, initialEntered, ...rest }, ref) => ( 15 | ({ itemKey, initialEntered, disabled: rest.disabled })} 19 | /> 20 | )); 21 | 22 | WithAccordionItem.displayName = 'WithAccordionItem'; 23 | return WithAccordionItem; 24 | }; 25 | 26 | export { withAccordionItem, ItemStateProps }; 27 | -------------------------------------------------------------------------------- /src/hooks/useAccordion.ts: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes, KeyboardEvent } from 'react'; 2 | import { ACCORDION_ATTR, ACCORDION_BTN_ATTR } from '../utils/constants'; 3 | 4 | const getAccordion = (node: Element) => { 5 | do { 6 | node = node.parentElement!; 7 | } while (node && !node.hasAttribute(ACCORDION_ATTR)); 8 | return node; 9 | }; 10 | 11 | const getNextIndex = (moveUp: boolean, current: number, length: number) => 12 | moveUp ? (current > 0 ? current - 1 : length - 1) : (current + 1) % length; 13 | 14 | const moveFocus = (moveUp: boolean, e: KeyboardEvent) => { 15 | const { activeElement } = document; 16 | if ( 17 | !activeElement || 18 | !activeElement.hasAttribute(ACCORDION_BTN_ATTR) || 19 | getAccordion(activeElement) !== e.currentTarget 20 | ) 21 | return; 22 | 23 | const nodes = e.currentTarget.querySelectorAll(`[${ACCORDION_BTN_ATTR}]`); 24 | const { length } = nodes; 25 | for (let i = 0; i < length; i++) { 26 | if (nodes[i] === activeElement) { 27 | let next = getNextIndex(moveUp, i, length); 28 | while (getAccordion(nodes[i]) !== getAccordion(nodes[next])) 29 | next = getNextIndex(moveUp, next, length); 30 | if (i !== next) { 31 | e.preventDefault(); 32 | nodes[next].focus(); 33 | } 34 | break; 35 | } 36 | } 37 | }; 38 | 39 | const useAccordion = () => { 40 | const accordionProps: HTMLAttributes = { 41 | [ACCORDION_ATTR]: '', 42 | onKeyDown: (e) => 43 | e.key === 'ArrowUp' ? moveFocus(true, e) : e.key === 'ArrowDown' && moveFocus(false, e) 44 | }; 45 | return { 46 | accordionProps 47 | }; 48 | }; 49 | 50 | export { useAccordion }; 51 | -------------------------------------------------------------------------------- /src/hooks/useAccordionContext.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { TransitionState } from 'react-transition-state'; 3 | import { AccordionContext, AccordionProviderValue, ItemKey } from '../utils/constants'; 4 | 5 | const getItemState = ( 6 | providerValue: AccordionProviderValue, 7 | key: ItemKey, 8 | itemInitialEntered?: boolean 9 | ): TransitionState => { 10 | const { stateMap, mountOnEnter, initialEntered } = providerValue; 11 | const _initialEntered = itemInitialEntered ?? initialEntered; 12 | return ( 13 | stateMap.get(key) || { 14 | status: _initialEntered ? 'entered' : mountOnEnter ? 'unmounted' : 'exited', 15 | isMounted: !mountOnEnter, 16 | isEnter: _initialEntered, 17 | isResolved: true 18 | } 19 | ); 20 | }; 21 | 22 | const useAccordionContext = () => { 23 | const context = useContext(AccordionContext); 24 | if (process.env.NODE_ENV !== 'production' && !context.stateMap) { 25 | throw new Error( 26 | '[React-Accordion] Cannot find a above this AccordionItem.' 27 | ); 28 | } 29 | return context as AccordionProviderValue; 30 | }; 31 | 32 | export { useAccordionContext, getItemState }; 33 | -------------------------------------------------------------------------------- /src/hooks/useAccordionItem.ts: -------------------------------------------------------------------------------- 1 | import { MouseEventHandler, HTMLAttributes, ButtonHTMLAttributes } from 'react'; 2 | import { ACCORDION_BTN_ATTR, ItemState } from '../utils/constants'; 3 | import { useId } from './useId'; 4 | 5 | const useAccordionItem = ({ state, toggle, disabled }: ItemState) => { 6 | const buttonId = useId(); 7 | const panelId = buttonId && buttonId + '-'; 8 | 9 | const buttonProps: ButtonHTMLAttributes = { 10 | id: buttonId, 11 | 'aria-controls': panelId, 12 | 'aria-expanded': state.isEnter, 13 | onClick: toggle as unknown as MouseEventHandler 14 | }; 15 | disabled 16 | ? (buttonProps.disabled = true) 17 | : ((buttonProps as { [x: string]: string })[ACCORDION_BTN_ATTR] = ''); 18 | 19 | const panelProps: HTMLAttributes = { 20 | id: panelId, 21 | 'aria-labelledby': buttonId, 22 | role: 'region' 23 | }; 24 | 25 | return { 26 | buttonProps, 27 | panelProps 28 | }; 29 | }; 30 | 31 | export { useAccordionItem }; 32 | -------------------------------------------------------------------------------- /src/hooks/useAccordionItemEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useCallback } from 'react'; 2 | import { ItemStateOptions } from '../utils/constants'; 3 | import { useAccordionContext, getItemState } from './useAccordionContext'; 4 | 5 | const useAccordionItemEffect = ({ 6 | itemKey, 7 | initialEntered, 8 | disabled 9 | }: ItemStateOptions = {}) => { 10 | const itemRef = useRef(null); 11 | const context = useAccordionContext(); 12 | const key = itemKey ?? itemRef.current!; 13 | const state = getItemState(context, key, initialEntered); 14 | const { setItem, deleteItem, toggle } = context; 15 | 16 | useEffect(() => { 17 | if (disabled) return; 18 | const key = itemKey ?? itemRef.current!; 19 | setItem(key, { initialEntered }); 20 | return () => void deleteItem(key); 21 | }, [setItem, deleteItem, itemKey, initialEntered, disabled]); 22 | 23 | return { 24 | itemRef, 25 | state, 26 | toggle: useCallback((toEnter?: boolean) => toggle(key, toEnter), [toggle, key]) 27 | }; 28 | }; 29 | 30 | export { useAccordionItemEffect }; 31 | -------------------------------------------------------------------------------- /src/hooks/useAccordionProvider.ts: -------------------------------------------------------------------------------- 1 | import { useTransitionMap } from 'react-transition-state'; 2 | import { 3 | TransitionProp, 4 | AccordionProviderOptions, 5 | AccordionProviderValue, 6 | ItemKey 7 | } from '../utils/constants'; 8 | 9 | const getTransition = ( 10 | transition: TransitionProp | undefined, 11 | name: 'enter' | 'exit' | 'preEnter' | 'preExit' 12 | ): boolean => transition === true || !!(transition && transition[name]); 13 | 14 | const useAccordionProvider: (options?: AccordionProviderOptions) => AccordionProviderValue = ({ 15 | transition, 16 | transitionTimeout, 17 | ...rest 18 | } = {}) => { 19 | const transitionMap = useTransitionMap({ 20 | timeout: transitionTimeout, 21 | enter: getTransition(transition, 'enter'), 22 | exit: getTransition(transition, 'exit'), 23 | preEnter: getTransition(transition, 'preEnter'), 24 | preExit: getTransition(transition, 'preExit'), 25 | ...rest 26 | }); 27 | 28 | return { 29 | mountOnEnter: !!rest.mountOnEnter, 30 | initialEntered: !!rest.initialEntered, 31 | ...transitionMap 32 | }; 33 | }; 34 | 35 | export { useAccordionProvider }; 36 | -------------------------------------------------------------------------------- /src/hooks/useAccordionState.ts: -------------------------------------------------------------------------------- 1 | import { ItemKey } from '../utils/constants'; 2 | import { useAccordionContext, getItemState } from './useAccordionContext'; 3 | 4 | const useAccordionState = () => { 5 | const context = useAccordionContext(); 6 | return { 7 | getItemState: (key: ItemKey, { initialEntered }: { initialEntered?: boolean } = {}) => 8 | getItemState(context, key, initialEntered), 9 | toggle: context.toggle, 10 | toggleAll: context.toggleAll 11 | }; 12 | }; 13 | 14 | export { useAccordionState }; 15 | -------------------------------------------------------------------------------- /src/hooks/useHeightTransition.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties, useState, useRef } from 'react'; 2 | import { TransitionState } from 'react-transition-state'; 3 | import { useLayoutEffect } from '../utils/useIsomorphicLayoutEffect'; 4 | 5 | const useHeightTransition = ({ status, isResolved }: TransitionState) => { 6 | const [height, setHeight] = useState(); 7 | const elementRef = useRef(null); 8 | 9 | useLayoutEffect(() => { 10 | (status === 'preEnter' || status === 'preExit') && 11 | setHeight(elementRef.current!.getBoundingClientRect().height); 12 | }, [status]); 13 | 14 | const style: CSSProperties = { 15 | height: 16 | status === 'preEnter' || status === 'exiting' 17 | ? 0 18 | : status === 'entering' || status === 'preExit' 19 | ? height 20 | : undefined, 21 | overflow: isResolved ? undefined : 'hidden' 22 | }; 23 | 24 | return [style, elementRef] as const; 25 | }; 26 | 27 | export { useHeightTransition }; 28 | -------------------------------------------------------------------------------- /src/hooks/useId.ts: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { ACCORDION_PREFIX } from '../utils/constants'; 3 | 4 | let current = 0; 5 | 6 | const useIdShim = () => { 7 | const [id, setId] = useState(); 8 | useEffect(() => setId(++current), []); 9 | return (id && `${ACCORDION_PREFIX}-${id}`) as string | undefined; 10 | }; 11 | 12 | const useId: () => string | undefined = React.useId || useIdShim; 13 | 14 | export { useId }; 15 | -------------------------------------------------------------------------------- /src/hooks/useMergeRef.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, RefCallback, MutableRefObject } from 'react'; 2 | 3 | // Adapted from material-ui 4 | // https://github.com/mui-org/material-ui/blob/f996027d00e7e4bff3fc040786c1706f9c6c3f82/packages/material-ui-utils/src/useForkRef.ts 5 | 6 | type Ref = RefCallback | MutableRefObject; 7 | 8 | function setRef(ref: Ref, instance: T) { 9 | typeof ref === 'function' ? ref(instance) : (ref.current = instance); 10 | } 11 | 12 | function useMergeRef(refA?: Ref | null, refB?: Ref | null) { 13 | return useMemo(() => { 14 | if (!refA) return refB; 15 | if (!refB) return refA; 16 | 17 | return (instance: T | null) => { 18 | setRef(refA, instance); 19 | setRef(refB, instance); 20 | }; 21 | }, [refA, refB]); 22 | } 23 | 24 | export { useMergeRef }; 25 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Accordion } from './components/Accordion'; 2 | export { AccordionItem } from './components/AccordionItem'; 3 | export { AccordionProvider } from './components/AccordionProvider'; 4 | export { ControlledAccordion } from './components/ControlledAccordion'; 5 | export { withAccordionItem } from './components/withAccordionItem'; 6 | export { useAccordion } from './hooks/useAccordion'; 7 | export { useAccordionItem } from './hooks/useAccordionItem'; 8 | export { useAccordionItemEffect } from './hooks/useAccordionItemEffect'; 9 | export { useAccordionProvider } from './hooks/useAccordionProvider'; 10 | export { useAccordionState } from './hooks/useAccordionState'; 11 | export { useHeightTransition } from './hooks/useHeightTransition'; 12 | export { useMergeRef } from './hooks/useMergeRef'; 13 | export type { AccordionProps } from './components/Accordion'; 14 | export type { AccordionItemProps } from './components/AccordionItem'; 15 | export type { ControlledAccordionProps } from './components/ControlledAccordion'; 16 | export type { ItemStateProps } from './components/withAccordionItem'; 17 | export type { 18 | AccordionProviderOptions, 19 | AccordionProviderValue, 20 | ItemState, 21 | ItemStateOptions, 22 | TransitionProp 23 | } from './utils/constants'; 24 | -------------------------------------------------------------------------------- /src/utils/bem.ts: -------------------------------------------------------------------------------- 1 | import { ClassNameProp, Modifiers } from './constants'; 2 | 3 | /** 4 | * Generate className following BEM methodology: http://getbem.com/naming/ 5 | * Modifier value can be one of the types: boolean, string 6 | */ 7 | const bem = 8 | (block: string, element?: string, modifiers?: Modifiers) => 9 |

(className?: ClassNameProp

, props?: P) => { 10 | const blockElement = element ? `${block}__${element}` : block; 11 | 12 | let classString = blockElement; 13 | modifiers && 14 | Object.keys(modifiers).forEach((name) => { 15 | const value = modifiers[name]; 16 | if (value) classString += ` ${blockElement}--${value === true ? name : `${name}-${value}`}`; 17 | }); 18 | 19 | let expandedClassName = typeof className === 'function' ? className(props!) : className; 20 | if (typeof expandedClassName === 'string') { 21 | expandedClassName = expandedClassName.trim(); 22 | if (expandedClassName) classString += ` ${expandedClassName}`; 23 | } 24 | 25 | return classString; 26 | }; 27 | 28 | export { bem }; 29 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import { createContext, HTMLAttributes } from 'react'; 2 | import { 3 | TransitionState, 4 | TransitionMapResult, 5 | TransitionMapOptions, 6 | TransitionOptions 7 | } from 'react-transition-state'; 8 | 9 | export const ACCORDION_BLOCK = 'szh-accordion'; 10 | export const ACCORDION_PREFIX = 'szh-adn'; 11 | export const ACCORDION_ATTR: string = `data-${ACCORDION_PREFIX}`; 12 | export const ACCORDION_BTN_ATTR = `data-${ACCORDION_PREFIX}-btn`; 13 | 14 | export type Modifiers = { 15 | readonly [index: string]: boolean | string; 16 | }; 17 | export type ClassNameProp

= string | ((props: P) => string); 18 | export interface ElementProps 19 | extends Omit, 'className' | 'children'> { 20 | className?: P extends undefined ? string : ClassNameProp

; 21 | 'data-testid'?: string | number; 22 | } 23 | 24 | export type ItemKey = Element | string | number; 25 | export type TransitionProp = 26 | | boolean 27 | | { 28 | enter?: boolean; 29 | exit?: boolean; 30 | preEnter?: boolean; 31 | preExit?: boolean; 32 | }; 33 | 34 | export interface ItemState { 35 | readonly state: TransitionState; 36 | readonly toggle: (toEnter?: boolean) => void; 37 | disabled?: boolean; 38 | } 39 | 40 | export interface ItemStateOptions { 41 | itemKey?: string | number; 42 | initialEntered?: boolean; 43 | disabled?: boolean; 44 | } 45 | 46 | export interface AccordionProviderOptions 47 | extends Omit< 48 | TransitionMapOptions, 49 | 'enter' | 'exit' | 'preEnter' | 'preExit' | 'timeout' 50 | > { 51 | transition?: TransitionProp; 52 | transitionTimeout?: TransitionOptions['timeout']; 53 | } 54 | 55 | export interface AccordionProviderValue extends TransitionMapResult { 56 | mountOnEnter: boolean; 57 | initialEntered: boolean; 58 | } 59 | 60 | export const AccordionContext = createContext>({}); 61 | -------------------------------------------------------------------------------- /src/utils/mergeProps.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | 4 | const mergeProps = (target, source) => { 5 | if (!source) return target; 6 | 7 | const result = { ...target }; 8 | Object.keys(source).forEach((key) => { 9 | const targetProp = target[key]; 10 | const sourceProp = source[key]; 11 | if (typeof sourceProp === 'function' && targetProp) { 12 | result[key] = (...e) => { 13 | targetProp(...e); 14 | sourceProp(...e); 15 | }; 16 | } else { 17 | result[key] = sourceProp; 18 | } 19 | }); 20 | 21 | return result; 22 | }; 23 | 24 | export { mergeProps }; 25 | -------------------------------------------------------------------------------- /src/utils/useIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect } from 'react'; 2 | 3 | // Get around a warning when using useLayoutEffect on the server. 4 | // https://github.com/reduxjs/react-redux/blob/b48d087d76f666e1c6c5a9713bbec112a1631841/src/utils/useIsomorphicLayoutEffect.js#L12 5 | // https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85 6 | // https://github.com/facebook/react/issues/14927#issuecomment-549457471 7 | 8 | const useIsomorphicLayoutEffect = 9 | typeof window !== 'undefined' && 10 | typeof window.document !== 'undefined' && 11 | typeof window.document.createElement !== 'undefined' 12 | ? useLayoutEffect 13 | : useEffect; 14 | 15 | export { useIsomorphicLayoutEffect as useLayoutEffect }; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDeclarationOnly": true, 4 | "declaration": true, 5 | "declarationDir": "./types", 6 | "jsx": "react-jsx", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "esModuleInterop": true 11 | }, 12 | "exclude": ["example/", "website/", "types/"] 13 | } 14 | -------------------------------------------------------------------------------- /types/components/Accordion.d.ts: -------------------------------------------------------------------------------- 1 | import { AccordionProviderOptions } from '../utils/constants'; 2 | import { ControlledAccordionProps } from './ControlledAccordion'; 3 | interface AccordionProps extends AccordionProviderOptions, Omit { 4 | } 5 | declare const Accordion: import("react").ForwardRefExoticComponent>; 6 | export { Accordion, AccordionProps }; 7 | -------------------------------------------------------------------------------- /types/components/AccordionItem.d.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode, ForwardedRef } from 'react'; 2 | import { TransitionState } from 'react-transition-state'; 3 | import { ElementProps, ItemState, ItemStateOptions } from '../utils/constants'; 4 | interface ItemElementProps extends ElementProps { 5 | ref?: ForwardedRef; 6 | } 7 | type NodeOrFunc = ReactNode | ((props: ItemState) => ReactNode); 8 | interface AccordionItemProps extends ItemStateOptions, ElementProps { 9 | header?: NodeOrFunc; 10 | children?: NodeOrFunc; 11 | headingTag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; 12 | headingProps?: ItemElementProps; 13 | buttonProps?: ItemElementProps; 14 | contentProps?: ItemElementProps; 15 | panelProps?: ItemElementProps; 16 | } 17 | declare const AccordionItem: import("react").ForwardRefExoticComponent>; 18 | export { AccordionItem, AccordionItemProps }; 19 | -------------------------------------------------------------------------------- /types/components/AccordionProvider.d.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { AccordionProviderValue } from '../utils/constants'; 3 | declare const AccordionProvider: (props: { 4 | value: AccordionProviderValue; 5 | children?: ReactNode; 6 | }) => import("react/jsx-runtime").JSX.Element; 7 | export { AccordionProvider }; 8 | -------------------------------------------------------------------------------- /types/components/ControlledAccordion.d.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { AccordionProviderValue, ElementProps } from '../utils/constants'; 3 | interface ControlledAccordionProps extends ElementProps { 4 | providerValue: AccordionProviderValue; 5 | children?: ReactNode; 6 | } 7 | declare const ControlledAccordion: import("react").ForwardRefExoticComponent>; 8 | export { ControlledAccordion, ControlledAccordionProps }; 9 | -------------------------------------------------------------------------------- /types/components/withAccordionItem.d.ts: -------------------------------------------------------------------------------- 1 | import type { RefObject, ForwardedRef, MemoExoticComponent, JSX } from 'react'; 2 | import type { ItemState, ItemStateOptions } from '../utils/constants'; 3 | interface ItemStateProps extends ItemState { 4 | itemRef: RefObject; 5 | forwardedRef: ForwardedRef; 6 | } 7 | declare const withAccordionItem:

(WrappedItem: MemoExoticComponent<(props: ItemStateProps) => JSX.Element>) => import("react").ForwardRefExoticComponent & import("react").RefAttributes>; 8 | export { withAccordionItem, ItemStateProps }; 9 | -------------------------------------------------------------------------------- /types/hooks/useAccordion.d.ts: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from 'react'; 2 | declare const useAccordion: () => { 3 | accordionProps: HTMLAttributes; 4 | }; 5 | export { useAccordion }; 6 | -------------------------------------------------------------------------------- /types/hooks/useAccordionContext.d.ts: -------------------------------------------------------------------------------- 1 | import { TransitionState } from 'react-transition-state'; 2 | import { AccordionProviderValue, ItemKey } from '../utils/constants'; 3 | declare const getItemState: (providerValue: AccordionProviderValue, key: ItemKey, itemInitialEntered?: boolean) => TransitionState; 4 | declare const useAccordionContext: () => AccordionProviderValue; 5 | export { useAccordionContext, getItemState }; 6 | -------------------------------------------------------------------------------- /types/hooks/useAccordionItem.d.ts: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes, ButtonHTMLAttributes } from 'react'; 2 | import { ItemState } from '../utils/constants'; 3 | declare const useAccordionItem: ({ state, toggle, disabled }: ItemState) => { 4 | buttonProps: ButtonHTMLAttributes; 5 | panelProps: HTMLAttributes; 6 | }; 7 | export { useAccordionItem }; 8 | -------------------------------------------------------------------------------- /types/hooks/useAccordionItemEffect.d.ts: -------------------------------------------------------------------------------- 1 | import { ItemStateOptions } from '../utils/constants'; 2 | declare const useAccordionItemEffect: ({ itemKey, initialEntered, disabled }?: ItemStateOptions) => { 3 | itemRef: import("react").RefObject; 4 | state: Readonly<{ 5 | status: import("react-transition-state").TransitionStatus; 6 | isMounted: boolean; 7 | isEnter: boolean; 8 | isResolved: boolean; 9 | }>; 10 | toggle: (toEnter?: boolean) => void; 11 | }; 12 | export { useAccordionItemEffect }; 13 | -------------------------------------------------------------------------------- /types/hooks/useAccordionProvider.d.ts: -------------------------------------------------------------------------------- 1 | import { AccordionProviderOptions, AccordionProviderValue } from '../utils/constants'; 2 | declare const useAccordionProvider: (options?: AccordionProviderOptions) => AccordionProviderValue; 3 | export { useAccordionProvider }; 4 | -------------------------------------------------------------------------------- /types/hooks/useAccordionState.d.ts: -------------------------------------------------------------------------------- 1 | import { ItemKey } from '../utils/constants'; 2 | declare const useAccordionState: () => { 3 | getItemState: (key: ItemKey, { initialEntered }?: { 4 | initialEntered?: boolean; 5 | }) => Readonly<{ 6 | status: import("react-transition-state").TransitionStatus; 7 | isMounted: boolean; 8 | isEnter: boolean; 9 | isResolved: boolean; 10 | }>; 11 | toggle: (key: ItemKey, toEnter?: boolean) => void; 12 | toggleAll: (toEnter?: boolean) => void; 13 | }; 14 | export { useAccordionState }; 15 | -------------------------------------------------------------------------------- /types/hooks/useHeightTransition.d.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react'; 2 | import { TransitionState } from 'react-transition-state'; 3 | declare const useHeightTransition: ({ status, isResolved }: TransitionState) => readonly [CSSProperties, import("react").RefObject]; 4 | export { useHeightTransition }; 5 | -------------------------------------------------------------------------------- /types/hooks/useId.d.ts: -------------------------------------------------------------------------------- 1 | declare const useId: () => string | undefined; 2 | export { useId }; 3 | -------------------------------------------------------------------------------- /types/hooks/useMergeRef.d.ts: -------------------------------------------------------------------------------- 1 | import { RefCallback, MutableRefObject } from 'react'; 2 | type Ref = RefCallback | MutableRefObject; 3 | declare function useMergeRef(refA?: Ref | null, refB?: Ref | null): Ref | null | undefined; 4 | export { useMergeRef }; 5 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export { Accordion } from './components/Accordion'; 2 | export { AccordionItem } from './components/AccordionItem'; 3 | export { AccordionProvider } from './components/AccordionProvider'; 4 | export { ControlledAccordion } from './components/ControlledAccordion'; 5 | export { withAccordionItem } from './components/withAccordionItem'; 6 | export { useAccordion } from './hooks/useAccordion'; 7 | export { useAccordionItem } from './hooks/useAccordionItem'; 8 | export { useAccordionItemEffect } from './hooks/useAccordionItemEffect'; 9 | export { useAccordionProvider } from './hooks/useAccordionProvider'; 10 | export { useAccordionState } from './hooks/useAccordionState'; 11 | export { useHeightTransition } from './hooks/useHeightTransition'; 12 | export { useMergeRef } from './hooks/useMergeRef'; 13 | export type { AccordionProps } from './components/Accordion'; 14 | export type { AccordionItemProps } from './components/AccordionItem'; 15 | export type { ControlledAccordionProps } from './components/ControlledAccordion'; 16 | export type { ItemStateProps } from './components/withAccordionItem'; 17 | export type { AccordionProviderOptions, AccordionProviderValue, ItemState, ItemStateOptions, TransitionProp } from './utils/constants'; 18 | -------------------------------------------------------------------------------- /types/utils/bem.d.ts: -------------------------------------------------------------------------------- 1 | import { ClassNameProp, Modifiers } from './constants'; 2 | /** 3 | * Generate className following BEM methodology: http://getbem.com/naming/ 4 | * Modifier value can be one of the types: boolean, string 5 | */ 6 | declare const bem: (block: string, element?: string, modifiers?: Modifiers) =>

(className?: ClassNameProp

, props?: P) => string; 7 | export { bem }; 8 | -------------------------------------------------------------------------------- /types/utils/constants.d.ts: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from 'react'; 2 | import { TransitionState, TransitionMapResult, TransitionMapOptions, TransitionOptions } from 'react-transition-state'; 3 | export declare const ACCORDION_BLOCK = "szh-accordion"; 4 | export declare const ACCORDION_PREFIX = "szh-adn"; 5 | export declare const ACCORDION_ATTR: string; 6 | export declare const ACCORDION_BTN_ATTR = "data-szh-adn-btn"; 7 | export type Modifiers = { 8 | readonly [index: string]: boolean | string; 9 | }; 10 | export type ClassNameProp

= string | ((props: P) => string); 11 | export interface ElementProps extends Omit, 'className' | 'children'> { 12 | className?: P extends undefined ? string : ClassNameProp

; 13 | 'data-testid'?: string | number; 14 | } 15 | export type ItemKey = Element | string | number; 16 | export type TransitionProp = boolean | { 17 | enter?: boolean; 18 | exit?: boolean; 19 | preEnter?: boolean; 20 | preExit?: boolean; 21 | }; 22 | export interface ItemState { 23 | readonly state: TransitionState; 24 | readonly toggle: (toEnter?: boolean) => void; 25 | disabled?: boolean; 26 | } 27 | export interface ItemStateOptions { 28 | itemKey?: string | number; 29 | initialEntered?: boolean; 30 | disabled?: boolean; 31 | } 32 | export interface AccordionProviderOptions extends Omit, 'enter' | 'exit' | 'preEnter' | 'preExit' | 'timeout'> { 33 | transition?: TransitionProp; 34 | transitionTimeout?: TransitionOptions['timeout']; 35 | } 36 | export interface AccordionProviderValue extends TransitionMapResult { 37 | mountOnEnter: boolean; 38 | initialEntered: boolean; 39 | } 40 | export declare const AccordionContext: import("react").Context>; 41 | -------------------------------------------------------------------------------- /types/utils/mergeProps.d.ts: -------------------------------------------------------------------------------- 1 | declare const mergeProps: (target: any, source: any) => any; 2 | export { mergeProps }; 3 | -------------------------------------------------------------------------------- /types/utils/useIsomorphicLayoutEffect.d.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | declare const useIsomorphicLayoutEffect: typeof useEffect; 3 | export { useIsomorphicLayoutEffect as useLayoutEffect }; 4 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /website/.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | trailingComma: none 2 | singleQuote: true 3 | printWidth: 72 4 | overrides: 5 | - files: '*.md' 6 | options: 7 | proseWrap: never 8 | - files: '*.mdx' 9 | options: 10 | proseWrap: always 11 | printWidth: 90 12 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ npm install 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ npm start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ npm run build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | -------------------------------------------------------------------------------- /website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')] 3 | }; 4 | -------------------------------------------------------------------------------- /website/docs/api/components/Accordion.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Accordion 6 | 7 | | Prop | Type | Description | 8 | | --- | --- | --- | 9 | | `allowMultiple` | boolean | Allow multiple accordion items to expand at once. | 10 | | `initialEntered` | boolean | Make all accordion items expanded when initially mounted. | 11 | | `mountOnEnter` | boolean | Lazily mount `children` of accordion items until they are expanded. Use this option if you don't need to server-side render accordion item contents. | 12 | | `unmountOnExit` | boolean | Unmount `children` of accordion items after they are collapsed. | 13 | | `transition` | [View on GitHub](https://github.com/szhsin/react-accordion/blob/7eddacda0928b23bde05ad2299d9b5e27efd4995/types/utils/constants.d.ts#L16) | Enable or disable transition. Accept a single boolean value or each individual transition stage in an object. | 14 | | `transitionTimeout` | [View on GitHub](https://github.com/szhsin/react-accordion/blob/7eddacda0928b23bde05ad2299d9b5e27efd4995/types/utils/constants.d.ts#L32) | Set transition duration. Accept a single number or individual "enter" or "exit" stage in an object. | 15 | | `onStateChange` | (event: \{ key: ItemKey; current: TransitionState \}) => void | Event to notify state changes in any accordion items. | 16 | -------------------------------------------------------------------------------- /website/docs/api/components/AccordionItem.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # AccordionItem 6 | 7 | | Prop | Type | Description | 8 | | --- | --- | --- | 9 | | `itemKey` | string \| number | Set an explicit key which can be used to expand or close item. | 10 | | `initialEntered` | boolean | Make the accordion item expanded when initially mounted. This prop takes precedence over the `initialEntered` prop of `Accordion` component. | 11 | | `disabled` | boolean | If true, disable the accordion item and exclude it from keyboard navigation. | 12 | | `header` | node \| function | Set item header. Support a function form which will receive item states. | 13 | | `children` | node \| function | Set item content. Support a function form which will receive item states. | 14 | | `headingTag` | string | Set heading tag; value can be `h1` to `h6`. | 15 | | `headingProps` | object | Set props and attributes on the heading element. | 16 | | `buttonProps` | object | Set props and attributes on the button element. | 17 | | `contentProps` | object | Set props and attributes on the content element. | 18 | | `panelProps` | object | Set props and attributes on the panel element. | 19 | -------------------------------------------------------------------------------- /website/docs/api/components/AccordionProvider.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # AccordionProvider 6 | 7 | | Prop | Type | Description | 8 | | --- | --- | --- | 9 | | `value` | object | Provide this prop with the return object from [useAccordionProvider](../hooks/useAccordionProvider#return-object). | 10 | -------------------------------------------------------------------------------- /website/docs/api/components/ControlledAccordion.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # ControlledAccordion 6 | 7 | | Prop | Type | Description | 8 | | --- | --- | --- | 9 | | `providerValue` | object | Provide this prop with the return object from [useAccordionProvider](../hooks/useAccordionProvider#return-object). | 10 | -------------------------------------------------------------------------------- /website/docs/api/components/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "position": 1, 3 | "label": "Components", 4 | "collapsed": false 5 | } 6 | -------------------------------------------------------------------------------- /website/docs/api/hooks/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "position": 2, 3 | "label": "Hooks", 4 | "collapsed": false 5 | } 6 | -------------------------------------------------------------------------------- /website/docs/api/hooks/useAccordion.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # useAccordion 6 | 7 | ## Return object 8 | 9 | | Prop | Type | Description | 10 | | --- | --- | --- | 11 | | `accordionProps` | object | An object to be spread to the accordion container element. | 12 | -------------------------------------------------------------------------------- /website/docs/api/hooks/useAccordionItem.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # useAccordionItem 6 | 7 | ## Parameter object 8 | 9 | | Prop | Type | Description | 10 | | --- | --- | --- | 11 | | `state` | TransitionState | The item `state` returned from [useAccordionItemEffect](./useAccordionItemEffect). | 12 | | `toggle` | (toEnter?: boolean) => void | The `toggle` function returned from [useAccordionItemEffect](./useAccordionItemEffect). | 13 | | `disabled` | boolean | Whether the accordion item is disabled. | 14 | 15 | ## Return object 16 | 17 | | Prop | Type | Description | 18 | | --- | --- | --- | 19 | | `buttonProps` | object | An object to be spread to the item button element. | 20 | | `panelProps` | object | An object to be spread to the item panel element. | 21 | -------------------------------------------------------------------------------- /website/docs/api/hooks/useAccordionItemEffect.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # useAccordionItemEffect 6 | 7 | ## Parameter object 8 | 9 | | Prop | Type | Description | 10 | | --- | --- | --- | 11 | | `itemKey` | string \| number | An explicit key which can be used to expand or close item. | 12 | | `initialEntered` | boolean | Make the accordion item expanded when initially mounted. This prop takes precedence over the `initialEntered` prop of `Accordion` component. | 13 | | `disabled` | boolean | Whether the accordion item is disabled. | 14 | 15 | ## Return object 16 | 17 | | Prop | Type | Description | 18 | | --- | --- | --- | 19 | | `itemRef` | object | A React `ref` object to be attached to the item element. | 20 | | `state` | TransitionState | The item state. | 21 | | `toggle` | (toEnter?: boolean) => void | A function to toggle the item state. | 22 | 23 | :::caution NOTE 24 | 25 | As its name suggests, this hook is not pure and should be used only once in each accordion item. 26 | 27 | ::: 28 | -------------------------------------------------------------------------------- /website/docs/api/hooks/useAccordionProvider.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # useAccordionProvider 6 | 7 | ## Parameter object 8 | 9 | | Prop | Type | Description | 10 | | --- | --- | --- | 11 | | `allowMultiple` | boolean | Allow multiple accordion items to expand at once. | 12 | | `initialEntered` | boolean | Make all accordion items expanded when initially mounted. | 13 | | `mountOnEnter` | boolean | Lazily mount `children` of accordion items until they are expanded. Use this option if you don't need to server-side render accordion item contents. | 14 | | `unmountOnExit` | boolean | Unmount `children` of accordion items after they are collapsed. | 15 | | `transition` | [View on GitHub](https://github.com/szhsin/react-accordion/blob/7eddacda0928b23bde05ad2299d9b5e27efd4995/types/utils/constants.d.ts#L16) | Enable or disable transition. Accept a single boolean value or each individual transition stage in an object. | 16 | | `transitionTimeout` | [View on GitHub](https://github.com/szhsin/react-accordion/blob/7eddacda0928b23bde05ad2299d9b5e27efd4995/types/utils/constants.d.ts#L32) | Set transition duration. Accept a single number or individual "enter" or "exit" stage in an object. | 17 | | `onStateChange` | (event: \{ key: ItemKey; current: TransitionState \}) => void | Event to notify state changes in any accordion items. | 18 | 19 | ## Return object 20 | 21 | | Prop | Type | Description | 22 | | --- | --- | --- | 23 | | `toggle` | (key: ItemKey, toEnter?: boolean) => void | A function to toggle item state by providing an item key. | 24 | | `toggleAll` | (toEnter?: boolean ) => void | A function to toggle all item states at the same time. | 25 | 26 | :::caution NOTE 27 | 28 | The return object contains some other properties which are considered implementation details and should not be used externally. 29 | 30 | ::: 31 | -------------------------------------------------------------------------------- /website/docs/api/hooks/useAccordionState.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # useAccordionState 6 | 7 | ## Return object 8 | 9 | | Prop | Type | Description | 10 | | --- | --- | --- | 11 | | `getItemState` | (key: ItemKey, \{ initialEntered?: boolean \} => TransitionState | A function that can be use to retrieve item state. | 12 | | `toggle` | (key: ItemKey, toEnter?: boolean ) => void | A function that can be use to toggle item state. | 13 | | `toggleAll` | (toEnter?: boolean ) => void | A function that can be use to toggle all item states at the same time. | 14 | -------------------------------------------------------------------------------- /website/docs/api/hooks/useHeightTransition.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # useHeightTransition 6 | 7 | ## Parameter list 8 | 9 | | Position | Type | Description | 10 | | --- | --- | --- | 11 | | #1 | TransitionState | The item `state` returned from [useAccordionItemEffect](./useAccordionItemEffect). | 12 | 13 | ## Return tuple 14 | 15 | | Position | Type | Description | 16 | | --- | --- | --- | 17 | | #1 | CSSProperties | A style object to be applied to the content element. | 18 | | #2 | object | A React `ref` object to be attached to the panel element. | 19 | -------------------------------------------------------------------------------- /website/docs/docs/allow-multiple.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | import CodeBlock from '@theme/CodeBlock'; 6 | import source from '!!raw-loader!@site/src/components/Multiple'; 7 | import { transformCodeBlock } from '@site/src/utils'; 8 | import Multiple from '@site/src/components/Multiple'; 9 | 10 | # Allowing multiple 11 | 12 | To allow multiple accordion items to expand at once, set the `allowMultiple` prop of the 13 | `Accordion` component. 14 | 15 | 16 | 17 | {transformCodeBlock(source)} 18 | -------------------------------------------------------------------------------- /website/docs/docs/animation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 11 3 | --- 4 | 5 | import CodeBlock from '@theme/CodeBlock'; 6 | import skeleton from '!!raw-loader!@site/src/html/skeleton.html'; 7 | 8 | # Animation 9 | 10 | Accordion supports expanding and collapsing animation with full state transition cycle, 11 | thanks to the [react-transition-state](https://github.com/szhsin/react-transition-state) 12 | library. Because how an accordion should be animated is part of styling, no default 13 | animation is included in the package. Generally, you can follow the steps below to enable 14 | animation: 15 | 16 | 1. set `transition` and `transitionTimeout` props on the Accordion component. 17 | 18 | ```jsx 19 | 20 | {/* Accordion items ... */} 21 | 22 | ``` 23 | 24 | If you use `ControlledAccordion` component, set the transition props with the 25 | `useAccordionProvider` hook. 26 | 27 | ```jsx 28 | const providerValue = useAccordionProvider({ 29 | transition: true, 30 | transitionTimeout: 250 31 | }); 32 | 33 | 34 | {/* Accordion items ... */} 35 | ; 36 | ``` 37 | 38 | 2. add the `height` transition css to the item content DOM element of each accordion item, 39 | which is the element with class selector `szh-accordion__item-content`. 40 | 41 | ```css 42 | transition: height 0.25s cubic-bezier(0, 0, 0, 1); 43 | ``` 44 | 45 | :::info 46 | 47 | The transition duration in CSS should be equal to the value of `transitionTimeout` prop in 48 | the first step. 49 | 50 | ::: 51 | 52 | The transition of height is to create the accordion effect when items are expanded and 53 | collapsed. Of course, you are free to animate other CSS properties to suit the needs of 54 | your app or design system. For example, you may animate `opacity` to create fading in/out 55 | effects. 56 | 57 | Depending on the styling solution in your project, the methods of applying CSS transition 58 | are slightly different. You can find some [CodeSandbox examples](styling#examples) in the 59 | styling page which all have animation enabled. 60 | 61 | :::caution IMPORTANT 62 | 63 | To ensure a smooth height animation on accordion items, do not add outer **vertical 64 | margin** to the children of an accordion item to create space around the item. Instead, 65 | use padding on the parent DOM element which contains the children. This is the element 66 | with class selector `szh-accordion__item-panel`. 67 | 68 | ```html 69 | 70 |

71 |
72 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor... 73 |
74 |
75 | ``` 76 | 77 | ```html 78 | 79 |
80 |
81 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor... 82 |
83 |
84 | ``` 85 | 86 | In case outer vertical margin of the children of an accordion item cannot be practically 87 | removed, add a vertical padding on the parent DOM element. It can be a small non-visible 88 | padding if you don't actual need one: 89 | 90 | ```html 91 |
92 |
93 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor... 94 |
95 |
96 | ``` 97 | 98 | ::: 99 | -------------------------------------------------------------------------------- /website/docs/docs/controlling-state.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | import CodeBlock from '@theme/CodeBlock'; 6 | import { transformCodeBlock } from '@site/src/utils'; 7 | import controllingStateSource from '!!raw-loader!@site/src/components/ControllingState'; 8 | import useStateHookSource from '!!raw-loader!@site/src/components/UseStateHook'; 9 | import ControllingState from '@site/src/components/ControllingState'; 10 | import UseStateHook from '@site/src/components/UseStateHook'; 11 | 12 | # Controlling state 13 | 14 | ## From above `Accordion` 15 | 16 | If you want to programmatically open or close accordion items from a component that is 17 | above `Accordion` in the React tree, you could use the `ControlledAccordion` and 18 | `useAccordionProvider`. 19 | 20 | The value returned from `useAccordionProvider` contains a `toggle` function which can be 21 | used to open or close any accordion items. This value should also be given to the 22 | `providerValue` prop of the `ControlledAccordion` component. 23 | 24 | The `toggle` function accepts two parameters. The first parameter is the `itemKey` prop of 25 | any accordion items to toggle. The second parameter specifies whether to open or close an 26 | item using a `boolean` value, or to toggle between the two states if the parameter is 27 | omitted. 28 | 29 | 30 | 31 | {transformCodeBlock(controllingStateSource)} 32 | 33 | :::caution NOTE 34 | 35 | The `itemKey` prop of `AccordionItem` is not required to be globally unique, but it should 36 | be unique among its sibling `AccordionItem` components. 37 | 38 | Also, you don't need to specify the `itemKey` prop for an item if you don't want to 39 | programmatically toggle it. 40 | 41 | ::: 42 | 43 | ## From underneath `Accordion` 44 | 45 | To programmatically open or close accordion items or access the state from a component 46 | that is underneath `Accordion` in the React tree, use the `useAccordionState` hook. 47 | 48 | 49 | 50 | {transformCodeBlock(useStateHookSource)} 51 | 52 | :::tip 53 | 54 | If you want to access and control state of the _**current**_ item, there is a simpler way 55 | to achieve it using the [render prop pattern](./item-render-prop). 56 | 57 | ::: 58 | -------------------------------------------------------------------------------- /website/docs/docs/customising-header.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | import CodeBlock from '@theme/CodeBlock'; 6 | import source from '!!raw-loader!@site/src/components/CustomisingHeader'; 7 | import styles from '!!raw-loader!@site/src/components/CustomisingHeader/styles.module.css'; 8 | import { transformCodeBlock } from '@site/src/utils'; 9 | import CustomisingHeader from '@site/src/components/CustomisingHeader'; 10 | 11 | # Customising header 12 | 13 | You may provide the `header` prop of `AccordionItem` with any JSX elements or React 14 | components, allowing the item header be to freely customised. 15 | 16 | 17 | 18 | {transformCodeBlock(source)} 19 | 20 | 21 | {styles} 22 | 23 | 24 | :::info 25 | 26 | You may use the 27 | [`headingTag`](https://github.com/szhsin/react-accordion/blob/2314e7131613cf8f776ed7f6995d9041871c2a43/types/components/AccordionItem.d.ts#L11) 28 | prop to set a heading level which is appropriate for the information structure of the 29 | page. By default a `h3` tag is rendered. 30 | 31 | ::: 32 | -------------------------------------------------------------------------------- /website/docs/docs/disabling-item.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 7 3 | --- 4 | 5 | import CodeBlock from '@theme/CodeBlock'; 6 | import source from '!!raw-loader!@site/src/components/DisableItem'; 7 | import { transformCodeBlock } from '@site/src/utils'; 8 | import Example from '@site/src/components/DisableItem'; 9 | 10 | # Disabling items 11 | 12 | Accordion items can be made disabled by adding the `disabled` prop. Disabled items cannot 13 | be toggled by clicking the header and are excluded from keyboard navigation. 14 | 15 | 16 | 17 | {transformCodeBlock(source)} 18 | -------------------------------------------------------------------------------- /website/docs/docs/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | import Tabs from '@theme/Tabs'; 6 | import TabItem from '@theme/TabItem'; 7 | import CodeBlock from '@theme/CodeBlock'; 8 | import sourceBasic from '!!raw-loader!@site/src/components/Starter/Basic'; 9 | import sourceArrayMap from '!!raw-loader!@site/src/components/Starter/ArrayMap'; 10 | import { transformCodeBlock } from '@site/src/utils'; 11 | import Basic from '@site/src/components/Starter/Basic'; 12 | 13 | # Getting Started 14 | 15 | ## Install 16 | 17 | 18 | 19 | 20 | ```bash 21 | npm install @szhsin/react-accordion 22 | ``` 23 | 24 | 25 | 26 | 27 | ```bash 28 | yarn add @szhsin/react-accordion 29 | ``` 30 | 31 | 32 | 33 | 34 | ```bash 35 | pnpm add @szhsin/react-accordion 36 | ``` 37 | 38 | 39 | 40 | 41 | ```bash 42 | bun add @szhsin/react-accordion 43 | ``` 44 | 45 | 46 | 47 | 48 | ## Usage 49 | 50 | An accordion is created by wrapping any number of `AccordionItem` components inside an 51 | `Accordion` component. 52 | 53 | By default, only one accordion item can be expanded at one time. 54 | 55 | 56 | 57 | :::tip 58 | 59 | When focus is on an accordion header, you may use the `ArrowDown` and `ArrowUp` keys to 60 | move focus to the next or previous accordion header. 61 | 62 | ::: 63 | 64 | {transformCodeBlock(sourceBasic)} 65 | 66 | **[Try on CodeSandbox](https://codesandbox.io/s/react-accordion-css-module-eqvnzg)** 67 | 68 | Or, if your data is already in an array: 69 | 70 | {transformCodeBlock(sourceArrayMap)} 71 | 72 | :::info 73 | 74 | Although the `header` prop of `AccordionItem` looks like some simple text content, you may 75 | provide it with any JSX elements or React components, allowing the item header to be 76 | [freely customised](./customising-header). 77 | 78 | ::: 79 | 80 | ## Accessibility 81 | 82 | This library implements WAI-ARIA roles, states, properties, and keyboard interaction, 83 | which is compliant with the 84 | [Accordion Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/). 85 | -------------------------------------------------------------------------------- /website/docs/docs/headless-ui/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "position": 12, 3 | "label": "Headless UI", 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /website/docs/docs/headless-ui/accordion-item.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | import CodeBlock from '@theme/CodeBlock'; 6 | import sourceBare from '!!raw-loader!@site/src/components/HeadlessUI/AccordionItemBare'; 7 | import source from '!!raw-loader!@site/src/components/HeadlessUI/AccordionItem'; 8 | 9 | # Creating AccordionItem 10 | 11 | Second, create an `AccordionItem` component with the `useAccordionItem` and 12 | `useAccordionItemEffect` hook exported from the library. 13 | 14 | ## Without height transition 15 | 16 | Use the following code if you don't need the height transition for expanding and 17 | collapsing items. 18 | 19 | {sourceBare} 20 | 21 | ## Height transition 22 | 23 | Use the `useHeightTransition` hook and add a wrapping `div` around the accordion item 24 | children if you want the height transition for expanding and collapsing items. 25 | 26 | {source} 27 | -------------------------------------------------------------------------------- /website/docs/docs/headless-ui/accordion.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | import CodeBlock from '@theme/CodeBlock'; 6 | import source from '!!raw-loader!@site/src/components/HeadlessUI/Accordion'; 7 | 8 | # Creating Accordion 9 | 10 | First, we create an `Accordion` component with `useAccordion`, `useAccordionProvider`, and 11 | `AccordionProvider` exported from the library. 12 | 13 | {source} 14 | -------------------------------------------------------------------------------- /website/docs/docs/headless-ui/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | import CodeBlock from '@theme/CodeBlock'; 6 | import source from '!!raw-loader!@site/src/components/HeadlessUI/Example'; 7 | import Example from '@site/src/components/HeadlessUI/Example'; 8 | 9 | # Putting together 10 | 11 | Now we can use our own `Accordion` and `AccordionItem` components as normal. They are 12 | WAI-ARIA compliant and support keyboard navigation! 13 | 14 | 15 | 16 | {source} 17 | -------------------------------------------------------------------------------- /website/docs/docs/headless-ui/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | sidebar_label: When to Use 4 | --- 5 | 6 | # Headless UI 7 | 8 | The `Accordion` and `AccordionItem` components are unstyled and render HTML markups that are compliant with the [Accordion Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/). They are built on top of some React Hooks which can be used directly in your code. There are a few reasons that you might want to do it: 9 | 10 | - You want to fully control what HTML markups are rendered into DOM. 11 | - The `Accordion` and `AccordionItem` components set some default class selectors on the HTML markups, and you don't want or need the selectors. 12 | - You could cut out some functionalities which are not needed in your components, e.g. the accordion item height transition implementation. It will help reduce bundle size after tree-shaking. 13 | 14 | :::info 15 | 16 | The components are always easier to setup and use, but the hooks provide more freedom for customisation. 17 | 18 | ::: 19 | -------------------------------------------------------------------------------- /website/docs/docs/headless-ui/styles.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | import CodeBlock from '@theme/CodeBlock'; 6 | import styles from '!!raw-loader!@site/src/components/accordion/styles.module.css'; 7 | 8 | # Styles 9 | 10 | Here are the styles for the two components we have created. 11 | 12 | 13 | {styles} 14 | 15 | -------------------------------------------------------------------------------- /website/docs/docs/initial-expanded.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | import CodeBlock from '@theme/CodeBlock'; 6 | import source from '!!raw-loader!@site/src/components/InitialEntered'; 7 | import { transformCodeBlock } from '@site/src/utils'; 8 | import InitialEntered from '@site/src/components/InitialEntered'; 9 | 10 | # Expanding items initially 11 | 12 | You could use the `initialEntered` prop of `AccordionItem` to expand items when accordion 13 | first mounts. In the following example, the first and last item are expanded on mount. 14 | 15 | 16 | 17 | {transformCodeBlock(source)} 18 | 19 | :::info 20 | 21 | The `Accordion` component also has an `initialEntered` prop which can make every accordion 22 | item expanded when initially mounted. 23 | 24 | When the `initialEntered` prop is specified on an individual `AccordionItem` at the same 25 | time, it overrides the `initialEntered` prop of `Accordion` component. 26 | 27 | ::: 28 | -------------------------------------------------------------------------------- /website/docs/docs/item-render-prop.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | import CodeBlock from '@theme/CodeBlock'; 6 | import source from '!!raw-loader!@site/src/components/AccessingState'; 7 | import { transformCodeBlock } from '@site/src/utils'; 8 | import AccessingState from '@site/src/components/AccessingState'; 9 | 10 | # Item render prop 11 | 12 | Both the `header` and `children` props of `AccordionItem` component support the 13 | [render prop](https://reactjs.org/docs/render-props.html) pattern, which can be used to 14 | access item state, along with a `toggle` function to open or close the current item. 15 | 16 | 17 | 18 | {transformCodeBlock(source)} 19 | 20 | :::tip 21 | 22 | While this technique is useful for rendering dynamic content based on item state, in most 23 | cases you won't need it for styling purpose. The `className` prop itself accepts a render 24 | prop form which can be used to apply class selectors based on state. Please see the 25 | [styling docs](./styling). 26 | 27 | ::: 28 | -------------------------------------------------------------------------------- /website/docs/docs/nested.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 8 3 | --- 4 | 5 | import CodeBlock from '@theme/CodeBlock'; 6 | import source from '!!raw-loader!@site/src/components/Nested'; 7 | import { transformCodeBlock } from '@site/src/utils'; 8 | import Nested from '@site/src/components/Nested'; 9 | 10 | # Nested accordion 11 | 12 | `AccordionItem` can have a nested accordion. When navigating through items with keyboard 13 | arrow keys, focus will stay within the same level. Use the `tab` key to move to other 14 | levels. 15 | 16 | 17 | 18 | {transformCodeBlock(source)} 19 | -------------------------------------------------------------------------------- /website/docs/docs/on-state-change.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 9 3 | --- 4 | 5 | import CodeBlock from '@theme/CodeBlock'; 6 | import source from '!!raw-loader!@site/src/components/StateChange'; 7 | import { transformCodeBlock } from '@site/src/utils'; 8 | import StateChange from '@site/src/components/StateChange'; 9 | 10 | # State changing event 11 | 12 | The `onStateChange` event of `Accordion` can be used to listen to item state updates. The 13 | event object has a `key` prop identifying which item's state has changed. 14 | 15 | 16 | 17 | {transformCodeBlock(source)} 18 | 19 | :::tip 20 | 21 | Open the browser console to see the logs. 22 | 23 | ::: 24 | -------------------------------------------------------------------------------- /website/docs/docs/styling.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 10 3 | --- 4 | 5 | import CodeBlock from '@theme/CodeBlock'; 6 | import skeleton from '!!raw-loader!@site/src/html/skeleton.html'; 7 | 8 | # Styling 9 | 10 | The unstyled `Accordion` and `AccordionItem` components render the following HTML markups 11 | into DOM. 12 | 13 | {skeleton} 14 | 15 | Each tag has some global class selectors attached. Depending on the styling system in your 16 | project, you may not use those class selectors. 17 | 18 | Each tag accepts a `className` prop which can be used to append custom class selectors. 19 | The `className` prop also supports a function form that receives relevant state as its 20 | parameters. 21 | 22 | ## Examples 23 | 24 | Please checkout the sandbox examples below for styling with some popular front-end stacks: 25 | 26 | - [CSS Module](https://codesandbox.io/s/react-accordion-css-module-eqvnzg) 27 | - [CSS/SASS](https://codesandbox.io/s/react-accordion-sass-o5jujp) 28 | - [styled-components](https://codesandbox.io/s/react-accordion-styled-k8s60p) 29 | - [Tailwind CSS](https://codesandbox.io/s/react-accordion-tailwindcss-sbfnqt) 30 | - [@emotion/react](https://codesandbox.io/s/react-accordion-emotion-react-ftuosh) 31 | -------------------------------------------------------------------------------- /website/docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@docusaurus/types'; 2 | import type * as Preset from '@docusaurus/preset-classic'; 3 | import { themes as prismThemes } from 'prism-react-renderer'; 4 | const lightCodeTheme = prismThemes.github; 5 | const darkCodeTheme = prismThemes.vsDark; 6 | 7 | const config: Config = { 8 | title: 'React Accordion', 9 | tagline: 10 | 'An unstyled, accessible accordion library for React apps and design systems', 11 | url: 'https://szhsin.github.io', 12 | baseUrl: '/react-accordion/', 13 | onBrokenLinks: 'throw', 14 | onBrokenMarkdownLinks: 'warn', 15 | favicon: 'img/favicon.ico', 16 | 17 | // GitHub pages deployment config. 18 | // If you aren't using GitHub pages, you don't need these. 19 | organizationName: 'szhsin', // Usually your GitHub org/user name. 20 | projectName: 'react-accordion', // Usually your repo name. 21 | trailingSlash: false, 22 | 23 | // Even if you don't use internalization, you can use this field to set useful 24 | // metadata like html lang. For example, if your site is Chinese, you may want 25 | // to replace "en" with "zh-Hans". 26 | i18n: { 27 | defaultLocale: 'en', 28 | locales: ['en'] 29 | }, 30 | 31 | presets: [ 32 | [ 33 | 'classic', 34 | { 35 | docs: { 36 | routeBasePath: '/', 37 | sidebarPath: require.resolve('./sidebars.js'), 38 | // Please change this to your repo. 39 | // Remove this to remove the "edit this page" links. 40 | editUrl: 41 | 'https://github.com/szhsin/react-accordion/tree/master/website/' 42 | }, 43 | blog: false, 44 | theme: { 45 | customCss: require.resolve('./src/css/custom.css') 46 | } 47 | } satisfies Preset.Options 48 | ] 49 | ], 50 | 51 | themeConfig: { 52 | navbar: { 53 | title: 'React Accordion', 54 | items: [ 55 | { 56 | type: 'doc', 57 | docId: 'docs/getting-started', 58 | position: 'left', 59 | label: 'Docs' 60 | }, 61 | { 62 | type: 'doc', 63 | docId: 'api/components/Accordion', 64 | position: 'left', 65 | label: 'API' 66 | }, 67 | { 68 | href: 'https://github.com/szhsin/react-accordion', 69 | label: 'GitHub', 70 | position: 'right' 71 | } 72 | ] 73 | }, 74 | prism: { 75 | theme: lightCodeTheme, 76 | darkTheme: darkCodeTheme, 77 | additionalLanguages: ['bash'] 78 | }, 79 | colorMode: { 80 | defaultMode: 'dark' 81 | } 82 | } satisfies Preset.ThemeConfig 83 | }; 84 | 85 | export default config; 86 | -------------------------------------------------------------------------------- /website/gh-pages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | check_str=$(pwd | grep "/website") 6 | if [ -z "$check_str" ]; then 7 | echo "Not in /website" 8 | exit 1 9 | fi 10 | 11 | npm run build 12 | 13 | tmpdir="$HOME/gh-pages" 14 | rm -Rf "$tmpdir" 15 | mkdir "$tmpdir" 16 | mv build "$tmpdir" 17 | cd .. 18 | 19 | git checkout gh-pages 20 | check_str=$(git branch | grep "*" | grep "gh-pages") 21 | if [ -z "$check_str" ]; then 22 | echo "Not on branch gh-pages" 23 | exit 1 24 | fi 25 | 26 | rm -Rf api/ assets/ category/ docs/ img/ 27 | cp -Rf "$tmpdir/build/" . 28 | git add . 29 | git commit -m "Updates" 30 | rm -Rf "$tmpdir" 31 | echo "Ready to push gh-pages" 32 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "^3.7.0", 19 | "@docusaurus/preset-classic": "^3.7.0", 20 | "@mdx-js/react": "^3.1.0", 21 | "@szhsin/react-accordion": "file:..", 22 | "clsx": "^1.2.1", 23 | "prism-react-renderer": "^2.4.1", 24 | "react": "file:../node_modules/react", 25 | "react-dom": "file:../node_modules/react-dom" 26 | }, 27 | "devDependencies": { 28 | "@docusaurus/module-type-aliases": "^3.7.0", 29 | "@docusaurus/tsconfig": "^3.7.0", 30 | "@docusaurus/types": "^3.7.0", 31 | "@types/react": "file:../node_modules/@types/react", 32 | "raw-loader": "^4.0.2", 33 | "typescript": "^5.8.3" 34 | }, 35 | "overrides": { 36 | "react": "$react", 37 | "react-dom": "$react-dom" 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.5%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | }, 51 | "engines": { 52 | "node": ">=16.14" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /website/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | docsSidebar: [{ type: 'autogenerated', dirName: 'docs' }], 17 | apiSidebar: [{ type: 'autogenerated', dirName: 'api' }] 18 | }; 19 | 20 | module.exports = sidebars; 21 | -------------------------------------------------------------------------------- /website/src/components/AccessingState/index.tsx: -------------------------------------------------------------------------------- 1 | import { Accordion, AccordionItem } from '../accordion'; 2 | 3 | export default function Example() { 4 | return ( 5 | 6 | `Item expanded: ${state.isEnter}`} 10 | > 11 | {({ toggle }) => ( 12 | <> 13 |

14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, 15 | sed do eiusmod tempor incididunt ut labore et dolore magna 16 | aliqua. 17 |

18 | 19 | {/* `toggle` function is also available from the render prop */} 20 | {/* highlight-next-line */} 21 | 24 | 25 | )} 26 |
27 | 28 | 29 | Quisque eget luctus mi, vehicula mollis lorem. Proin fringilla 30 | vel erat quis sodales. Nam ex enim, eleifend venenatis lectus 31 | vitae, accumsan auctor mi. 32 | 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /website/src/components/ControllingState/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ControlledAccordion, 3 | AccordionItem, 4 | useAccordionProvider 5 | } from '../accordion'; 6 | 7 | export default function Example() { 8 | // highlight-start 9 | const providerValue = useAccordionProvider({ 10 | allowMultiple: true, 11 | transition: true, 12 | transitionTimeout: 250 13 | }); 14 | // Destructuring `toggle` and `toggleAll` from `providerValue` 15 | const { toggle, toggleAll } = providerValue; 16 | // highlight-end 17 | 18 | return ( 19 |
20 |
21 | 29 | 37 | 44 | 51 | 59 |
60 | 61 | 66 | 72 |

73 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed 74 | do eiusmod tempor incididunt ut labore et dolore magna 75 | aliqua. 76 |

77 | 85 |
86 | 87 | 88 | Quisque eget luctus mi, vehicula mollis lorem. Proin fringilla 89 | vel erat quis sodales. Nam ex enim, eleifend venenatis lectus 90 | vitae, accumsan auctor mi. 91 | 92 | 93 | 99 | Suspendisse massa risus, pretium id interdum in, dictum sit 100 | amet ante. Fusce vulputate purus sed tempus feugiat. 101 | 102 |
103 |
104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /website/src/components/CustomisingHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import { Accordion, AccordionItem } from '../accordion'; 2 | import styles from './styles.module.css'; 3 | 4 | export default function Example() { 5 | return ( 6 | 7 | 11 |

What is Lorem Ipsum?

12 |

13 | Lorem ipsum is a placeholder text commonly used to 14 | demonstrate the visual form of a document. 15 |

16 |
17 | } 18 | // highlight-end 19 | > 20 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 21 | eiusmod tempor incididunt ut labore et dolore magna aliqua. 22 | 23 | 24 | 25 | Quisque eget luctus mi, vehicula mollis lorem. Proin fringilla 26 | vel erat quis sodales. Nam ex enim, eleifend venenatis lectus 27 | vitae, accumsan auctor mi. 28 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /website/src/components/CustomisingHeader/styles.module.css: -------------------------------------------------------------------------------- 1 | .title { 2 | font-size: 1.5rem; 3 | font-weight: bold; 4 | } 5 | 6 | .description { 7 | color: var(--ifm-color-gray-600); 8 | margin: 0; 9 | } 10 | -------------------------------------------------------------------------------- /website/src/components/DisableItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { Accordion, AccordionItem } from '../accordion'; 2 | 3 | export default function Example() { 4 | return ( 5 | 6 | 7 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 8 | eiusmod tempor incididunt ut labore et dolore magna aliqua. 9 | 10 | 11 | 12 | Quisque eget luctus mi, vehicula mollis lorem. Proin fringilla 13 | vel erat quis sodales. Nam ex enim, eleifend venenatis lectus 14 | vitae, accumsan auctor mi. 15 | 16 | 17 | 18 | Suspendisse massa risus, pretium id interdum in, dictum sit amet 19 | ante. Fusce vulputate purus sed tempus feugiat. 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /website/src/components/HeadlessUI/Accordion.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useAccordion, 3 | useAccordionProvider, 4 | AccordionProvider 5 | } from '@szhsin/react-accordion'; 6 | import styles from '../accordion/styles.module.css'; 7 | 8 | const Accordion = ({ children }: { children: React.ReactNode }) => { 9 | const providerValue = useAccordionProvider({ 10 | // Omit these two options if you don't want to implement any transition 11 | // highlight-start 12 | transition: true, 13 | transitionTimeout: 250 14 | // highlight-end 15 | }); 16 | const { accordionProps } = useAccordion(); 17 | 18 | return ( 19 | 20 |
21 | {children} 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default Accordion; 28 | -------------------------------------------------------------------------------- /website/src/components/HeadlessUI/AccordionItem.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useAccordionItem, 3 | useAccordionItemEffect, 4 | // highlight-next-line 5 | useHeightTransition 6 | } from '@szhsin/react-accordion'; 7 | import ChevronDown from '@site/static/img/chevron-down.svg'; 8 | import styles from '../accordion/styles.module.css'; 9 | 10 | const AccordionItem = ({ 11 | header, 12 | children, 13 | itemKey, 14 | initialEntered, 15 | disabled 16 | }: { 17 | header: React.ReactNode; 18 | children: React.ReactNode; 19 | itemKey?: string | number; 20 | initialEntered?: boolean; 21 | disabled?: boolean; 22 | }) => { 23 | const { itemRef, state, toggle } = 24 | useAccordionItemEffect({ 25 | itemKey, 26 | initialEntered, 27 | disabled 28 | }); 29 | const { buttonProps, panelProps } = useAccordionItem({ 30 | state, 31 | toggle, 32 | disabled 33 | }); 34 | 35 | // highlight-start 36 | const [transitionStyle, panelRef] = 37 | useHeightTransition(state); 38 | // highlight-end 39 | 40 | const { status, isMounted, isEnter } = state; 41 | 42 | return ( 43 |
44 |

45 | 53 |

54 | {isMounted && ( 55 | // Add an extra `div` around the panel `div` for the 56 | // height transition to work as intended 57 | // highlight-next-line 58 |
66 |
72 | {children} 73 |
74 | {/* Closing tag of the extra `div` */} 75 | {/* highlight-next-line */} 76 |
77 | )} 78 |
79 | ); 80 | }; 81 | 82 | export default AccordionItem; 83 | -------------------------------------------------------------------------------- /website/src/components/HeadlessUI/AccordionItemBare.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useAccordionItem, 3 | useAccordionItemEffect 4 | } from '@szhsin/react-accordion'; 5 | import ChevronDown from '@site/static/img/chevron-down.svg'; 6 | import styles from '../accordion/styles.module.css'; 7 | 8 | const AccordionItem = ({ 9 | header, 10 | children, 11 | itemKey, 12 | initialEntered, 13 | disabled 14 | }: { 15 | header: React.ReactNode; 16 | children: React.ReactNode; 17 | itemKey?: string | number; 18 | initialEntered?: boolean; 19 | disabled?: boolean; 20 | }) => { 21 | const { itemRef, state, toggle } = 22 | useAccordionItemEffect({ 23 | itemKey, 24 | initialEntered, 25 | disabled 26 | }); 27 | const { buttonProps, panelProps } = useAccordionItem({ 28 | state, 29 | toggle, 30 | disabled 31 | }); 32 | const { status, isMounted, isEnter } = state; 33 | 34 | return ( 35 |
36 | {/* Choose a heading level that is appropriate for the information 37 | architecture of your page */} 38 | {/* highlight-next-line */} 39 |

40 | 48 |

49 | {isMounted && ( 50 |
57 | {children} 58 |
59 | )} 60 |
61 | ); 62 | }; 63 | 64 | export default AccordionItem; 65 | -------------------------------------------------------------------------------- /website/src/components/HeadlessUI/AccordionItemMemo.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { 3 | useAccordionItem, 4 | useHeightTransition, 5 | withAccordionItem 6 | } from '@szhsin/react-accordion'; 7 | import type { 8 | ItemStateProps, 9 | ItemStateOptions 10 | } from '@szhsin/react-accordion'; 11 | import ChevronDown from '@site/static/img/chevron-down.svg'; 12 | import styles from '../accordion/styles.module.css'; 13 | 14 | interface Props { 15 | header: React.ReactNode; 16 | children: React.ReactNode; 17 | } 18 | 19 | const MemoItem = memo( 20 | ({ 21 | itemRef, 22 | state, 23 | toggle, 24 | disabled, 25 | header, 26 | children 27 | }: ItemStateProps & Props) => { 28 | const { buttonProps, panelProps } = useAccordionItem({ 29 | state, 30 | toggle, 31 | disabled 32 | }); 33 | 34 | const [transitionStyle, panelRef] = 35 | useHeightTransition(state); 36 | 37 | const { status, isMounted, isEnter } = state; 38 | 39 | return ( 40 |
41 |

42 | 50 |

51 | {isMounted && ( 52 |
60 |
65 | {children} 66 |
67 |
68 | )} 69 |
70 | ); 71 | } 72 | ); 73 | 74 | MemoItem.displayName = `AccordionItem`; 75 | const AccordionItem = withAccordionItem< 76 | ItemStateOptions & Props, 77 | HTMLDivElement 78 | >(MemoItem); 79 | 80 | export default AccordionItem; 81 | -------------------------------------------------------------------------------- /website/src/components/HeadlessUI/Example.tsx: -------------------------------------------------------------------------------- 1 | import Accordion from './Accordion'; 2 | import AccordionItem from './AccordionItem'; 3 | 4 | export default function Example() { 5 | return ( 6 | 7 | 8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 9 | eiusmod tempor incididunt ut labore et dolore magna aliqua. 10 | 11 | 12 | 13 | Quisque eget luctus mi, vehicula mollis lorem. Proin fringilla 14 | vel erat quis sodales. Nam ex enim, eleifend venenatis lectus 15 | vitae, accumsan auctor mi. 16 | 17 | 18 | 19 | Suspendisse massa risus, pretium id interdum in, dictum sit amet 20 | ante. Fusce vulputate purus sed tempus feugiat. 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from '@docusaurus/Link'; 2 | import styles from './styles.module.css'; 3 | 4 | type FeatureItem = { 5 | title: string; 6 | description: JSX.Element; 7 | }; 8 | 9 | const featureList: FeatureItem[] = [ 10 | { 11 | title: 'Unstyled', 12 | description: ( 13 | <> 14 | Providing behaviour and data/state management without enforcing 15 | any styles. Freely customising for your React app or design 16 | system. 17 | 18 | ) 19 | }, 20 | { 21 | title: 'Headless UI', 22 | description: ( 23 | <> 24 | While the components render markup with good default settings, 25 | the React Hooks give you complete control of render outputs. 26 | 27 | ) 28 | }, 29 | { 30 | title: 'WAI-ARIA Compliant', 31 | description: ( 32 | <> 33 | Fully accessible and compliant with the{' '} 34 | 35 | Accordion Pattern 36 | 37 | . Supports keyboard navigation. 38 | 39 | ) 40 | }, 41 | { 42 | title: 'Animation', 43 | description: ( 44 | <> 45 | Supports open and close animation with full state transition 46 | cycle, thanks to the{' '} 47 | 48 | react-transition-state 49 | {' '} 50 | library. 51 | 52 | ) 53 | } 54 | ]; 55 | 56 | function Feature({ title, description }: FeatureItem) { 57 | return ( 58 |
59 |

{title}

60 |

{description}

61 |
62 | ); 63 | } 64 | 65 | export default function HomepageFeatures(): JSX.Element { 66 | return ( 67 |
68 | {featureList.map((props, idx) => ( 69 | 70 | ))} 71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .featureList { 2 | display: grid; 3 | grid-template-columns: repeat(4, 1fr); 4 | justify-items: center; 5 | gap: 2rem; 6 | padding: 4rem 1rem 3rem; 7 | max-width: 1400px; 8 | margin: 0 auto; 9 | text-align: center; 10 | } 11 | 12 | .feature { 13 | max-width: 18rem; 14 | } 15 | 16 | .feature h3 { 17 | font-size: 1.5rem; 18 | } 19 | 20 | .feature p { 21 | margin-bottom: 1rem; 22 | } 23 | 24 | @media (max-width: 900px) { 25 | .featureList { 26 | grid-template-columns: repeat(2, 1fr); 27 | } 28 | } 29 | 30 | @media (max-width: 480px) { 31 | .featureList { 32 | display: block; 33 | padding-top: 2rem; 34 | } 35 | .feature { 36 | margin: 0 auto; 37 | padding-top: 2rem; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /website/src/components/InitialEntered/index.tsx: -------------------------------------------------------------------------------- 1 | import { Accordion, AccordionItem } from '../accordion'; 2 | 3 | export default function Example() { 4 | return ( 5 | 6 | 7 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 8 | eiusmod tempor incididunt ut labore et dolore magna aliqua. 9 | 10 | 11 | 12 | Quisque eget luctus mi, vehicula mollis lorem. Proin fringilla 13 | vel erat quis sodales. Nam ex enim, eleifend venenatis lectus 14 | vitae, accumsan auctor mi. 15 | 16 | 17 | 18 | Suspendisse massa risus, pretium id interdum in, dictum sit amet 19 | ante. Fusce vulputate purus sed tempus feugiat. 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /website/src/components/Multiple/index.tsx: -------------------------------------------------------------------------------- 1 | import { Accordion, AccordionItem } from '../accordion'; 2 | 3 | export default function Example() { 4 | return ( 5 | 6 | 7 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 8 | eiusmod tempor incididunt ut labore et dolore magna aliqua. 9 | 10 | 11 | 12 | Quisque eget luctus mi, vehicula mollis lorem. Proin fringilla 13 | vel erat quis sodales. Nam ex enim, eleifend venenatis lectus 14 | vitae, accumsan auctor mi. 15 | 16 | 17 | 18 | Suspendisse massa risus, pretium id interdum in, dictum sit amet 19 | ante. Fusce vulputate purus sed tempus feugiat. 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /website/src/components/Nested/index.tsx: -------------------------------------------------------------------------------- 1 | import { Accordion, AccordionItem } from '../accordion'; 2 | 3 | export default function Example() { 4 | return ( 5 | 6 | 7 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 8 | eiusmod tempor incididunt ut labore et dolore magna aliqua. 9 | 10 | 11 | 12 | 13 | 14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed 15 | do eiusmod tempor incididunt ut labore et dolore magna 16 | aliqua. 17 | 18 | 19 | 20 | Suspendisse massa risus, pretium id interdum in, dictum sit 21 | amet ante. Fusce vulputate purus sed tempus feugiat. 22 | 23 | 24 | 25 | 26 | 27 | Suspendisse massa risus, pretium id interdum in, dictum sit amet 28 | ante. Fusce vulputate purus sed tempus feugiat. 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /website/src/components/Starter/ArrayMap.tsx: -------------------------------------------------------------------------------- 1 | import { Accordion, AccordionItem } from '../accordion'; 2 | 3 | const items = [ 4 | { 5 | header: 'What is Lorem Ipsum?', 6 | content: 'Lorem ipsum dolor sit amet, consectetur adipiscing...' 7 | }, 8 | { 9 | header: 'Where does it come from?', 10 | content: 'Quisque eget luctus mi, vehicula mollis lorem...' 11 | }, 12 | { 13 | header: 'Why do we use it?', 14 | content: 'Suspendisse massa risus, pretium id interdum in...' 15 | } 16 | ]; 17 | 18 | export default function Example() { 19 | return ( 20 | 21 | {items.map(({ header, content }, i) => ( 22 | 23 | {content} 24 | 25 | ))} 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /website/src/components/Starter/Basic.tsx: -------------------------------------------------------------------------------- 1 | import { Accordion, AccordionItem } from '../accordion'; 2 | 3 | export default function Example() { 4 | return ( 5 | 6 | 7 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 8 | eiusmod tempor incididunt ut labore et dolore magna aliqua. 9 | 10 | 11 | 12 | Quisque eget luctus mi, vehicula mollis lorem. Proin fringilla 13 | vel erat quis sodales. Nam ex enim, eleifend venenatis lectus 14 | vitae, accumsan auctor mi. 15 | 16 | 17 | 18 | Suspendisse massa risus, pretium id interdum in, dictum sit amet 19 | ante. Fusce vulputate purus sed tempus feugiat. 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /website/src/components/StateChange/index.tsx: -------------------------------------------------------------------------------- 1 | import { Accordion, AccordionItem } from '../accordion'; 2 | 3 | const items = [ 4 | { 5 | header: 'What is Lorem Ipsum?', 6 | content: 'Lorem ipsum dolor sit amet, consectetur adipiscing...' 7 | }, 8 | { 9 | header: 'Where does it come from?', 10 | content: 'Quisque eget luctus mi, vehicula mollis lorem...' 11 | }, 12 | { 13 | header: 'Why do we use it?', 14 | content: 'Suspendisse massa risus, pretium id interdum in...' 15 | } 16 | ]; 17 | 18 | /* eslint-disable no-console */ 19 | 20 | export default function Example() { 21 | return ( 22 | { 25 | if (current.isResolved) 26 | console.log( 27 | `${key as string} is expanded: ${current.isEnter}` 28 | ); 29 | }} 30 | // highlight-end 31 | > 32 | {items.map(({ header, content }, i) => ( 33 | 40 | {content} 41 | 42 | ))} 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /website/src/components/UseStateHook/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Accordion, 3 | AccordionItem, 4 | useAccordionState 5 | } from '../accordion'; 6 | 7 | const MyItem = () => { 8 | // highlight-next-line 9 | const { getItemState, toggle, toggleAll } = useAccordionState(); 10 | 11 | return ( 12 | 13 |

14 | {/* Accessing item state by providing an `itemKey` */} 15 | {/* highlight-next-line */} 16 | Next item expanded: {getItemState('next').isEnter.toString()} 17 |

18 | 19 |
20 | {/* highlight-next-line */} 21 | 24 | 25 | {/* highlight-next-line */} 26 | 29 |
30 |
31 | ); 32 | }; 33 | 34 | export default function Example() { 35 | return ( 36 | 37 | {/* highlight-next-line */} 38 | 39 | 40 | 45 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 46 | eiusmod tempor incididunt ut labore et dolore magna aliqua. 47 | 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /website/src/components/accordion/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactAccordion from '@szhsin/react-accordion'; 2 | import ChevronDown from '@site/static/img/chevron-down.svg'; 3 | import styles from './styles.module.css'; 4 | 5 | const Accordion = (props: ReactAccordion.AccordionProps) => ( 6 | 12 | ); 13 | 14 | const ControlledAccordion = ( 15 | props: ReactAccordion.ControlledAccordionProps 16 | ) => ( 17 | 21 | ); 22 | 23 | const AccordionItem = (props: ReactAccordion.AccordionItemProps) => ( 24 | ( 27 | <> 28 | {typeof props.header === 'function' 29 | ? props.header(renderProps) 30 | : props.header} 31 | 32 | 33 | )} 34 | className={styles.item} 35 | buttonProps={{ 36 | className: ({ isEnter }) => 37 | isEnter ? styles.buttonExpanded : styles.button 38 | }} 39 | contentProps={{ className: styles.content }} 40 | panelProps={{ className: styles.panel }} 41 | /> 42 | ); 43 | 44 | export * from '@szhsin/react-accordion'; 45 | export { Accordion, ControlledAccordion, AccordionItem }; 46 | -------------------------------------------------------------------------------- /website/src/components/accordion/styles.module.css: -------------------------------------------------------------------------------- 1 | .accordion { 2 | margin-bottom: 1.5rem; 3 | } 4 | 5 | .item { 6 | border: 1px solid var(--ifm-color-emphasis-300); 7 | border-top-width: 0; 8 | } 9 | 10 | .item:first-of-type { 11 | border-top-width: 1px; 12 | border-top-left-radius: 6px; 13 | border-top-right-radius: 6px; 14 | } 15 | 16 | .item:last-of-type { 17 | border-bottom-left-radius: 6px; 18 | border-bottom-right-radius: 6px; 19 | } 20 | 21 | .item:first-of-type .button { 22 | border-top-left-radius: 5px; 23 | border-top-right-radius: 5px; 24 | } 25 | 26 | .item:last-of-type .button:not(.buttonExpanded) { 27 | border-bottom-left-radius: 5px; 28 | border-bottom-right-radius: 5px; 29 | } 30 | 31 | .button { 32 | cursor: pointer; 33 | display: flex; 34 | align-items: center; 35 | width: 100%; 36 | margin: 0; 37 | padding: 1rem 1.25rem; 38 | font-size: 1rem; 39 | font-weight: 400; 40 | text-align: left; 41 | color: var(--ifm-font-color-base); 42 | background-color: transparent; 43 | border: none; 44 | transition: background-color 0.25s ease-in-out; 45 | } 46 | 47 | .buttonExpanded { 48 | composes: button; 49 | box-shadow: inset 0 -1px 0 0 var(--ifm-color-emphasis-300); 50 | color: var(--ifm-color-primary); 51 | background-color: var(--accordion-background); 52 | } 53 | 54 | .button:hover { 55 | box-shadow: 0 0 0 1px var(--ifm-color-primary); 56 | } 57 | 58 | .button:disabled { 59 | box-shadow: none; 60 | cursor: auto; 61 | color: #808080; 62 | } 63 | 64 | .button:focus { 65 | position: relative; 66 | outline: none; 67 | } 68 | 69 | .button:focus-visible { 70 | box-shadow: 0 0 0 3px var(--ifm-color-primary); 71 | } 72 | 73 | @supports not selector(:focus-visible) { 74 | .button:focus { 75 | box-shadow: 0 0 0 3px var(--ifm-color-primary); 76 | } 77 | } 78 | 79 | .chevron { 80 | flex-shrink: 0; 81 | margin-left: auto; 82 | transition: transform 0.25s cubic-bezier(0, 0, 0, 1); 83 | } 84 | 85 | .buttonExpanded .chevron { 86 | transform: rotate(180deg); 87 | } 88 | 89 | .content { 90 | transition: height 0.25s cubic-bezier(0, 0, 0, 1); 91 | } 92 | 93 | .panel { 94 | padding: 1rem 1.25rem; 95 | } 96 | -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | --accordion-background: #d1efde; 19 | } 20 | 21 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 22 | [data-theme='dark'] { 23 | --ifm-color-primary: #25c2a0; 24 | --ifm-color-primary-dark: #21af90; 25 | --ifm-color-primary-darker: #1fa588; 26 | --ifm-color-primary-darkest: #1a8870; 27 | --ifm-color-primary-light: #29d5b0; 28 | --ifm-color-primary-lighter: #32d8b4; 29 | --ifm-color-primary-lightest: #4fddbf; 30 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 31 | --accordion-background: #072720; 32 | } 33 | 34 | [data-theme='dark'] pre { 35 | background-color: #1a1f27; 36 | } 37 | 38 | .theme-code-block-highlighted-line { 39 | box-shadow: inset 3px 0 0 0 var(--ifm-color-primary); 40 | } 41 | 42 | [data-theme='dark'] .theme-code-block-highlighted-line { 43 | background-color: #242c37; 44 | } 45 | 46 | .btn { 47 | cursor: pointer; 48 | padding: 0.375rem 0.75rem; 49 | } 50 | 51 | .buttons { 52 | display: flex; 53 | flex-wrap: wrap; 54 | } 55 | 56 | .buttons .btn { 57 | margin: 0 1rem 1rem 0; 58 | } 59 | -------------------------------------------------------------------------------- /website/src/html/skeleton.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 |

6 | 10 |

11 |
12 |
13 | 14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 15 | eiusmod tempor incididunt ut labore et dolore magna aliqua. 16 |
17 |
18 |
19 | 20 | 21 | 22 |
23 | -------------------------------------------------------------------------------- /website/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | .heroBanner { 2 | padding: 4rem 0; 3 | text-align: center; 4 | position: relative; 5 | } 6 | 7 | .container { 8 | padding: 0; 9 | } 10 | 11 | [data-theme='dark'] .heroBanner { 12 | color: white; 13 | background-color: #205d3b; 14 | } 15 | 16 | @media screen and (max-width: 996px) { 17 | .heroBanner { 18 | padding: 2rem; 19 | } 20 | } 21 | 22 | @media screen and (max-width: 480px) { 23 | .heroBanner { 24 | padding: 2rem 1rem; 25 | } 26 | .heroTitle { 27 | font-size: 2rem; 28 | } 29 | } 30 | 31 | .buttons { 32 | display: flex; 33 | flex-direction: column; 34 | align-items: center; 35 | } 36 | 37 | .install { 38 | margin-bottom: 1.5rem; 39 | } 40 | -------------------------------------------------------------------------------- /website/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Link from '@docusaurus/Link'; 3 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 4 | import Layout from '@theme/Layout'; 5 | import CodeBlock from '@theme/CodeBlock'; 6 | import HomepageFeatures from '@site/src/components/HomepageFeatures'; 7 | import styles from './index.module.css'; 8 | 9 | export default function Home(): JSX.Element { 10 | const { siteConfig } = useDocusaurusContext(); 11 | return ( 12 | 13 |
14 |
15 |

16 | {siteConfig.title} 17 |

18 |

{siteConfig.tagline}

19 |
20 | 21 | npm install @szhsin/react-accordion 22 | 23 | 27 | Getting Started 28 | 29 |
30 |
31 |
32 |
33 | 34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /website/src/theme/Footer/index.module.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | text-align: center; 3 | padding: 2rem 1rem; 4 | font-size: 14px; 5 | line-height: 1.5; 6 | color: var(--ifm-color-gray-500); 7 | } 8 | 9 | .footer a { 10 | width: auto; 11 | font-size: 16px; 12 | } 13 | 14 | .license { 15 | margin-top: 16px; 16 | } 17 | -------------------------------------------------------------------------------- /website/src/theme/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import LinkItem from '@theme/Footer/LinkItem'; 3 | import styles from './index.module.css'; 4 | 5 | export default function Footer() { 6 | return ( 7 |
8 | 14 |
15 | Released under the MIT License. 16 |
17 |
Copyright © {new Date().getFullYear()} Zheng Song.
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /website/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const transformCodeBlock = (source: string) => 2 | source.replace( 3 | "from '../accordion'", 4 | "from '@szhsin/react-accordion'" 5 | ); 6 | -------------------------------------------------------------------------------- /website/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szhsin/react-accordion/284d9e49ca23410b5b1ca94b93aa3cf61db15266/website/static/.nojekyll -------------------------------------------------------------------------------- /website/static/img/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szhsin/react-accordion/284d9e49ca23410b5b1ca94b93aa3cf61db15266/website/static/img/favicon.ico -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | --------------------------------------------------------------------------------