├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── .nvmrc
├── .storybook
├── main.js
└── preview.js
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── MIGRATION-2.x-to-3.x.md
├── README.md
├── example
├── .npmignore
├── index.html
├── index.tsx
├── package.json
└── tsconfig.json
├── package.json
├── release.config.js
├── setupTests.ts
├── src
├── __tests__
│ ├── index.test.tsx
│ ├── mocks
│ │ └── utils.ts
│ └── utils.test.tsx
├── global.d.ts
├── index.ts
├── stories
│ ├── basic.stories.tsx
│ ├── components.tsx
│ ├── div.stories.tsx
│ ├── nested.stories.tsx
│ └── unmount.stories.tsx
├── types.ts
└── utils.ts
├── tsconfig.json
└── yarn.lock
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release npm package
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | test:
11 | name: 'CI'
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 | - uses: actions/setup-node@v1
16 | with:
17 | node-version: 14
18 | - name: Install Dependencies
19 | uses: bahmutov/npm-install@v1
20 | - name: Build
21 | run: yarn build
22 | - name: Lint
23 | run: yarn lint
24 | - name: Test
25 | run: yarn test
26 |
27 | release:
28 | name: Publish to NPM
29 | needs: test
30 | # publish only when merged in master on original repo, not on PR
31 | if: github.repository == 'roginfarrer/react-collapsed' && github.ref == 'refs/heads/main'
32 | runs-on: ubuntu-latest
33 | steps:
34 | - uses: actions/checkout@v2
35 | - uses: actions/setup-node@v1
36 | with:
37 | node-version: 14
38 | - name: Install Dependencies
39 | uses: bahmutov/npm-install@v1
40 | - name: Build
41 | run: yarn build
42 | - run: npx semantic-release
43 | env:
44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .cache
5 | dist
6 | storybook-static
7 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 14.18.0
2 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "stories": [
3 | "../src/**/*.stories.mdx",
4 | "../src/**/*.stories.@(js|jsx|ts|tsx)"
5 | ],
6 | "addons": [
7 | "@storybook/addon-links",
8 | "@storybook/addon-essentials",
9 | "@storybook/addon-a11y"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | export const parameters = {
2 | actions: { argTypesRegex: "^on[A-Z].*" },
3 | controls: {
4 | matchers: {
5 | color: /(background|color)$/i,
6 | date: /Date$/,
7 | },
8 | },
9 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 2.0.0
2 |
3 | Complete rewrite using React hooks!
4 |
5 | - Ends support for React versions < 16.8.x
6 | - Library now exports a custom hook in lieu of a render prop component
7 | - Adds support for unmounting the contents of the Collapse element when closed
8 |
9 | ```js
10 | import React from 'react'
11 | import useCollapse from 'react-collapsed'
12 |
13 | function Demo() {
14 | const { getCollapseProps, getToggleProps, isOpen } = useCollapse()
15 |
16 | return (
17 | <>
18 |
19 | Collapsed content 🙈
20 | >
21 | )
22 | }
23 | ```
24 |
25 | # 1.0.0
26 |
27 | Bumped to full release! :)
28 |
29 | - `duration`, `easing`, and `delay` now support taking an object with `in` and `out` keys to configure differing in-and-out transitions
30 |
31 | # 0.2.0
32 |
33 | ### Breaking Changes
34 |
35 | - `getCollapsibleProps` => `getCollapseProps`. Renamed since it's easier to spell 😅
36 |
37 | ### Other
38 |
39 | - Slew of Flow bug fixes
40 | - Improved documentation
41 |
42 | # 0.1.3
43 |
44 | - ESLINT wasn't working properly - fixed this
45 | - Added `files` key to package.json to improve NPM load
46 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thanks for wanting to make this component better!
4 |
5 | ### Project setup
6 |
7 | 1. Fork and clone the repo
8 | 2. `yarn install` and `yarn dev` to install dependencies and spin up the demo site locally
9 | 3. Create a branch for your PR
10 |
11 | **Tip:** Keep your main branch pointing at the original repository and make pull requests from branches on your fork. To do this, run:
12 |
13 | ```bash
14 | git remote add upstream https://github.com/roginfarrer/react-collapsed.git
15 | git fetch upstream
16 | git branch --set-upstream-to=upstream/main main
17 | ```
18 |
19 | This will add the original repository as a "remote" called "upstream," Then fetch the git information from that remote, then set your local main branch to use the upstream main branch whenever you run git pull. Then you can make all of your pull request branches based on this main branch. Whenever you want to update your version of main, do a regular git pull.
20 |
21 | ### Committing and Pushing changes
22 |
23 | Please make sure to run the tests before you commit your changes. You can run `yarn test` to run them (or `yarn test:watch`). Make sure to add new tests for any new features or changes. All tests must pass for a pull request to be accepted.
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019-2020 Rogin Farrer
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MIGRATION-2.x-to-3.x.md:
--------------------------------------------------------------------------------
1 | # Migrating from 2.x to 3.x
2 |
3 | ## BREAKING CHANGES
4 |
5 | - `useCollapse` has been completely rewritten in TypeScript, and now exports types.
6 | - `useCollapse` configuration has changed:
7 | - `isOpen` -> `isExpanded`
8 | - `defaultOpen` -> `defaultExpanded`
9 | - `expandStyles.transitionDuration` and `collapseStyles.transitionDuration` have been moved to a single `duration` property
10 | - `expandStyles.transitionTimingFunction` and `collapseStyles.transitionTimingFunction` have been moved to a single `easing` property
11 | - `useCollapse` output has changed:
12 | - `isOpen` -> `isExpanded`
13 | - `mountChildren` has been removed. Event hooks are now provided to recreate this feature. [See below for more](#mountChildren)
14 | - `toggleOpen` has been replaced with `setExpanded`, which requires a boolean that sets the expanded state, or a callback that returns a boolean.
15 | - The default transition duration has been changed from `500ms` to being calculated based on the height of the collapsed content. Encouraged to leave this default since it will provide more natural animations.
16 | - The default transition curve has been changed from `cubic-bezier(0.250, 0.460, 0.450, 0.940)` to `ease-in-out`, or `cubic-bezier(0.4, 0, 0.2, 1)`
17 |
18 | See below for more detail on the above changes.
19 |
20 | ## Input
21 |
22 | The hook's property names have been changed for clarity:
23 |
24 | - `isOpen` -> `isExpanded`
25 | - `defaultOpen` -> `defaultExpanded`
26 |
27 | In 2.x, the customizing the transition duration and easing was done by setting `transitionDuration` and `transitionTimingFunction` in `expandStyles` or `collapseStyles`. Those have been both pulled out and promoted to top-level settings via `duration` and `easing`, respectively.
28 |
29 | The default value for `duration` is also no longer a fixed value. Instead, the duration is calculated based on the height of the collapsed content to create more natural transitions.
30 |
31 | The transition easing was also updated from a custom curve to a more basic `ease-in-out` curve.
32 |
33 | In summary:
34 |
35 | ```diff
36 | const collapse = useCollaspse({
37 | collapseStyles: {},
38 | expandStyles: {},
39 | collapsedHeight: number,
40 | - isOpen: boolean,
41 | - defaultOpen: boolean,
42 | + duration: number,
43 | + easing: string,
44 | + isExpanded: boolean,
45 | + defaultExpanded: boolean,
46 | + onCollapseStart() {},
47 | + onCollapseEnd() {},
48 | + onExpandStart() {},
49 | + onExpandEnd() {},
50 | })
51 | ```
52 |
53 | ## Output
54 |
55 | - `isOpen` -> `isExpanded`
56 | - `toggleOpen` -> `setExpanded`
57 | - `mountChildren` has been removed.
58 |
59 | `setExpanded` now also supports an argument to set the expanded state. Previously, to toggle the expanded state, you would just call the `toggleOpen` function:
60 |
61 | ```javascript
62 |
63 | ```
64 |
65 | Now, you must provide a boolean or a function that returns a boolean:
66 |
67 | ```javascript
68 |
71 | ```
72 |
73 | ### `mountChildren`
74 |
75 | `mountChildren` has been removed. In order to recreate the same functionality, you can hook into the `onExpandStart` and `onCollapseEnd` hooks:
76 |
77 | ```javascript
78 | function Collapse() {
79 | const [mountChildren, setMountChildren] = useState(false)
80 | const { getToggleProps, getCollapseProps } = useCollapse({
81 | onCollapseEnd() {
82 | setMountChildren(false)
83 | },
84 | onExpandStart() {
85 | setMountChildren(true)
86 | },
87 | })
88 |
89 | return (
90 |
91 |
92 |
93 | {mountChildren &&
I will only render when expanded!
}
94 |
95 |
96 | )
97 | }
98 | ```
99 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-collapsed (useCollapse)
2 |
3 | [![CI][ci-badge]][ci]
4 | ![npm bundle size (version)][minzipped-badge]
5 | [![npm version][npm-badge]][npm-version]
6 | [](https://app.netlify.com/sites/react-collapsed/deploys)
7 |
8 | A custom hook for creating accessible expand/collapse components in React. Animates the height using CSS transitions from `0` to `auto`.
9 |
10 | ## Features
11 |
12 | - Handles the height of animations of your elements, `auto` included!
13 | - You control the UI - `useCollapse` provides the necessary props, you control the styles and the elements.
14 | - Accessible out of the box - no need to worry if your collapse/expand component is accessible, since this takes care of it for you!
15 | - No animation framework required! Simply powered by CSS animations
16 | - Written in TypeScript
17 |
18 | ## Demo
19 |
20 | [See the demo site!](https://react-collapsed.netlify.app/)
21 |
22 | [CodeSandbox demo](https://codesandbox.io/s/magical-browser-vibv2?file=/src/App.tsx)
23 |
24 | ## Installation
25 |
26 | ```bash
27 | $ yarn add react-collapsed
28 | # or
29 | $ npm i react-collapsed
30 | ```
31 |
32 | ## Usage
33 |
34 | ### Simple Usage
35 |
36 | ```js
37 | import React from 'react'
38 | import useCollapse from 'react-collapsed'
39 |
40 | function Demo() {
41 | const { getCollapseProps, getToggleProps, isExpanded } = useCollapse()
42 |
43 | return (
44 |
75 | )
76 | }
77 | ```
78 |
79 | ## API
80 |
81 | ```js
82 | const { getCollapseProps, getToggleProps, isExpanded, setExpanded } =
83 | useCollapse({
84 | isExpanded: boolean,
85 | defaultExpanded: boolean,
86 | expandStyles: {},
87 | collapseStyles: {},
88 | collapsedHeight: 0,
89 | easing: string,
90 | duration: number,
91 | onCollapseStart: func,
92 | onCollapseEnd: func,
93 | onExpandStart: func,
94 | onExpandEnd: func,
95 | })
96 | ```
97 |
98 | ### `useCollapse` Config
99 |
100 | The following are optional properties passed into `useCollapse({ })`:
101 |
102 | | Prop | Type | Default | Description |
103 | | -------------------- | -------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
104 | | isExpanded | boolean | `undefined` | If true, the Collapse is expanded |
105 | | defaultExpanded | boolean | `false` | If true, the Collapse will be expanded when mounted |
106 | | expandStyles | object | `{}` | Style object applied to the collapse panel when it expands |
107 | | collapseStyles | object | `{}` | Style object applied to the collapse panel when it collapses |
108 | | collapsedHeight | number | `0` | The height of the content when collapsed |
109 | | easing | string | `cubic-bezier(0.4, 0, 0.2, 1)` | The transition timing function for the animation |
110 | | duration | number | `undefined` | The duration of the animation in milliseconds. By default, the duration is programmatically calculated based on the height of the collapsed element |
111 | | onCollapseStart | function | no-op | Handler called when the collapse animation begins |
112 | | onCollapseEnd | function | no-op | Handler called when the collapse animation ends |
113 | | onExpandStart | function | no-op | Handler called when the expand animation begins |
114 | | onExpandEnd | function | no-op | Handler called when the expand animation ends |
115 | | hasDisabledAnimation | boolean | false | If true, will disable the animation |
116 |
117 | ### What you get
118 |
119 | | Name | Description |
120 | | ---------------- | ----------------------------------------------------------------------------------------------------------- |
121 | | getCollapseProps | Function that returns a prop object, which should be spread onto the collapse element |
122 | | getToggleProps | Function that returns a prop object, which should be spread onto an element that toggles the collapse panel |
123 | | isExpanded | Whether or not the collapse is expanded (if not controlled) |
124 | | setExpanded | Sets the hook's internal isExpanded state |
125 |
126 | ## Alternative Solutions
127 |
128 | - [react-spring](https://www.react-spring.io/) - JavaScript animation based library that can potentially have smoother animations. Requires a bit more work to create an accessible collapse component.
129 | - [react-animate-height](https://github.com/Stanko/react-animate-height/) - Another library that uses CSS transitions to animate to any height. It provides components, not a hook.
130 |
131 | ## FAQ
132 |
133 |
134 | When I apply vertical padding to the component that gets getCollapseProps, the animation is janky and it doesn't collapse all the way. What gives?
135 |
136 | The collapse works by manipulating the `height` property. If an element has vertical padding, that padding expandes the size of the element, even if it has `height: 0; overflow: hidden`.
137 |
138 | To avoid this, simply move that padding from the element to an element directly nested within in.
139 |
140 | ```javascript
141 | // from
142 |
23 | In the morning I walked down the Boulevard to the rue Soufflot for
24 | coffee and brioche. It was a fine morning. The horse-chestnut trees in
25 | the Luxembourg gardens were in bloom. There was the pleasant
26 | early-morning feeling of a hot day. I read the papers with the coffee
27 | and then smoked a cigarette. The flower-women were coming up from the
28 | market and arranging their daily stock. Students went by going up to the
29 | law school, or down to the Sorbonne. The Boulevard was busy with trams
30 | and people going to work.
31 |
15 | Friends, Romans, countrymen, lend me your ears;
16 |
17 | I come to bury Caesar, not to praise him.
18 |
19 | The evil that men do lives after them;
20 |
21 | The good is oft interred with their bones;
22 |
23 | So let it be with Caesar. The noble Brutus
24 |
25 | Hath told you Caesar was ambitious:
26 |
27 | If it were so, it was a grievous fault,
28 |
29 | And grievously hath Caesar answer’d it.
30 |
31 | Here, under leave of Brutus and the rest–
32 |
33 | For Brutus is an honourable man;
34 |
35 | So are they all, all honourable men–
36 |
37 | Come I to speak in Caesar’s funeral.
38 |
39 |
40 | He was my friend, faithful and just to me:
41 |
42 | But Brutus says he was ambitious;
43 |
44 | And Brutus is an honourable man.
45 |
46 | He hath brought many captives home to Rome
47 |
48 | Whose ransoms did the general coffers fill:
49 |
50 | Did this in Caesar seem ambitious?
51 |
52 | When that the poor have cried, Caesar hath wept:
53 |
54 | Ambition should be made of sterner stuff:
55 |