├── dist └── .gitkeep ├── .npmignore ├── example ├── img │ ├── becomes.png │ ├── example.gif │ ├── browserstack-logo.png │ └── browserstack-logo.svg ├── src │ ├── sass │ │ ├── base │ │ │ ├── _mixins.scss │ │ │ ├── _variables.scss │ │ │ ├── _clearfix.scss │ │ │ └── _reset.scss │ │ ├── main.scss │ │ └── components │ │ │ └── _Collapsible.scss │ ├── examples │ │ └── ZeroHeightCollapsible.js │ └── index.js ├── package.json └── index.html ├── .prettierrc ├── .gitignore ├── .babelrc ├── src ├── setInTransition.js └── Collapsible.js ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── main.yml ├── webpack.config.js ├── LICENSE.md ├── index.d.ts ├── package.json ├── __tests__ └── index.js └── README.md /dist/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example/ 2 | .history -------------------------------------------------------------------------------- /example/img/becomes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glennflanagan/react-collapsible/HEAD/example/img/becomes.png -------------------------------------------------------------------------------- /example/img/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glennflanagan/react-collapsible/HEAD/example/img/example.gif -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /example/img/browserstack-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glennflanagan/react-collapsible/HEAD/example/img/browserstack-logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/*.js 3 | node_modules 4 | npm-debug.log 5 | yarn-error.log 6 | 7 | # Parcel builds for example 8 | example/.cache 9 | example/dist 10 | 11 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env", "@babel/react"], 3 | "plugins": [["@babel/plugin-proposal-class-properties", {}, ""]] // required to avoid duplicate plugin naming clash with parcel. 4 | } 5 | -------------------------------------------------------------------------------- /example/src/sass/base/_mixins.scss: -------------------------------------------------------------------------------- 1 | @function strip-unit($num) { 2 | @return $num / ($num * 0 + 1); 3 | } 4 | 5 | 6 | @function rem($sizeValue: 1.6) { 7 | 8 | $value: strip-unit($sizeValue / $BASE_FONT_SIZE); 9 | 10 | @return $value + rem; 11 | } -------------------------------------------------------------------------------- /example/src/sass/base/_variables.scss: -------------------------------------------------------------------------------- 1 | $yellow: rgb(248, 159, 0); 2 | $cyan: rgb(0, 172, 157); 3 | $grey: rgb(51, 51, 51); 4 | $black: rgb(38, 38, 38); 5 | $base: rgb(255, 255, 255); 6 | $lightGrey: rgb(235, 235, 235); 7 | 8 | $BASE_FONT_SIZE: 16px; 9 | 10 | $breakpointMega: 1600px; 11 | $breakpointLarge: 990px; 12 | $breakpointMed: 767px; 13 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "parcel index.html --open --log-level 3" 8 | }, 9 | "dependencies": { 10 | "styled-components": "^5.2.1" 11 | }, 12 | "devDependencies": { 13 | "@babel/core": "^7.12.10", 14 | "parcel-bundler": "^1.12.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/setInTransition.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://github.com/glennflanagan/react-collapsible/issues/177 3 | * 4 | * If the inner content of the collapsible has a scrollHeight of 0 5 | * `onTransitionEnd` will not fire. 6 | * 7 | * To fix this we do set 'inTransition' to false if the element has 0 height value. 8 | */ 9 | const setInTransition = (innerRefScrollHeight) => innerRefScrollHeight !== 0; 10 | 11 | export default setInTransition; 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | jest: true, 6 | node: true, 7 | }, 8 | extends: ['eslint:recommended', 'plugin:react/recommended'], 9 | parser: '@babel/eslint-parser', 10 | parserOptions: { 11 | ecmaFeatures: { 12 | jsx: true, 13 | }, 14 | ecmaVersion: 12, 15 | sourceType: 'module', 16 | }, 17 | plugins: ['react'], 18 | rules: { 19 | indent: ['error', 2, { SwitchCase: 1 }], 20 | 'linebreak-style': ['error', 'unix'], 21 | quotes: ['error', 'single', { avoidEscape: true }], 22 | semi: ['error', 'always'], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 13 | [React Collapsible JSFiddle Template](https://jsfiddle.net/982f3LL7) 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'production', 5 | entry: './src/Collapsible.js', 6 | output: { 7 | filename: 'index.js', 8 | path: path.resolve(__dirname, 'dist'), 9 | libraryTarget: 'umd', 10 | /** 11 | * Makes UMD build available on both browsers and Node.js 12 | * https://webpack.js.org/configuration/output/#outputglobalobject 13 | */ 14 | globalObject: 'this', 15 | }, 16 | externals: ['react'], 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.m?js$/, 21 | exclude: /(node_modules|bower_components)/, 22 | use: { 23 | loader: 'babel-loader', 24 | }, 25 | }, 26 | ], 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /example/src/sass/base/_clearfix.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * For modern browsers 3 | * 1. The space content is one way to avoid an Opera bug when the 4 | * contenteditable attribute is included anywhere else in the document. 5 | * Otherwise it causes space to appear at the top and bottom of elements 6 | * that are clearfixed. 7 | * 2. The use of `table` rather than `block` is only necessary if using 8 | * `:before` to contain the top-margins of child elements. 9 | */ 10 | .cf:before, 11 | .cf:after { 12 | content: " "; /* 1 */ 13 | display: table; /* 2 */ 14 | } 15 | 16 | .cf:after { 17 | clear: both; 18 | } 19 | 20 | /** 21 | * For IE 6/7 only 22 | * Include this rule to trigger hasLayout and contain floats. 23 | */ 24 | .cf { 25 | *zoom: 1; 26 | } -------------------------------------------------------------------------------- /example/src/sass/main.scss: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Roboto:400,700,700italic,400italic); 2 | @import "base/reset"; 3 | @import "base/clearfix"; 4 | @import "base/variables"; 5 | @import "base/mixins"; 6 | 7 | html { 8 | font-size: $BASE_FONT_SIZE; 9 | } 10 | 11 | body { 12 | background-color: $grey; 13 | font-family: 'Roboto', sans-serif; 14 | padding-top: 50px; 15 | } 16 | 17 | h1 { 18 | color: white; 19 | text-align: center; 20 | font-size: 30px; 21 | margin-bottom: 50px; 22 | } 23 | 24 | h2 { 25 | font-weight: 800; 26 | text-transform: uppercase; 27 | margin-top: 20px; 28 | margin-bottom: 20px; 29 | } 30 | 31 | strong { 32 | font-weight: bold; 33 | } 34 | 35 | #root { 36 | max-width: 960px; 37 | margin-left: auto; 38 | margin-right: auto; 39 | } 40 | 41 | @import "components/Collapsible"; 42 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React Responsive Collapsible Section Component Example 7 | 8 | 13 | 14 | 15 | 16 | 20 | 21 | 22 |

React Responsive Collapsible Section Component

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the develop branch 7 | on: 8 | push: 9 | branches: [ develop, master ] 10 | pull_request: 11 | 12 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 13 | jobs: 14 | # This workflow contains a single job called "build" 15 | test: 16 | # The type of runner that the job will run on 17 | runs-on: ubuntu-latest 18 | 19 | # Steps represent a sequence of tasks that will be executed as part of the job 20 | steps: 21 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 22 | - uses: actions/checkout@v2 23 | 24 | - name: Install 25 | run: yarn 26 | 27 | - name: Run Tests 28 | run: yarn test 29 | -------------------------------------------------------------------------------- /example/src/examples/ZeroHeightCollapsible.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Collapsible from '../../../src/Collapsible'; 4 | 5 | import styled from 'styled-components'; 6 | 7 | const Wrap = styled.div` 8 | margin: 30px 0; 9 | 10 | /** 11 | * Overwrite the contentInner padding + border 12 | * to ensure zero height. 13 | */ 14 | .Collapsible__contentInner { 15 | padding: 0; 16 | border: 0; 17 | } 18 | `; 19 | 20 | const Title = styled.p` 21 | color: white; 22 | margin-bottom: 15px; 23 | `; 24 | 25 | const ZeroHeightCollapsible = () => { 26 | const [open, setOpen] = useState(false); 27 | 28 | const handleTriggerClick = () => { 29 | setOpen(!open); 30 | }; 31 | 32 | return ( 33 | 34 | Empty or Zero Height collapsible 35 | 39 | 40 | ); 41 | }; 42 | 43 | export default ZeroHeightCollapsible; 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Glenn Flanagan 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 | -------------------------------------------------------------------------------- /example/src/sass/base/_reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | * { 51 | -webkit-box-sizing: border-box; 52 | -moz-box-sizing: border-box; 53 | box-sizing: border-box; 54 | } -------------------------------------------------------------------------------- /example/src/sass/components/_Collapsible.scss: -------------------------------------------------------------------------------- 1 | // The main container element 2 | .Collapsible { 3 | background-color: $base; 4 | } 5 | 6 | 7 | //The content within the collaspable area 8 | .Collapsible__contentInner { 9 | padding: 10px; 10 | border: 1px solid $lightGrey; 11 | border-top: 0; 12 | 13 | p { 14 | margin-bottom: 10px; 15 | font-size: 14px; 16 | line-height: 20px; 17 | 18 | &:last-child { 19 | margin-bottom: 0; 20 | } 21 | } 22 | } 23 | 24 | //The link which when clicked opens the collapsable area 25 | .Collapsible__trigger { 26 | display: block; 27 | font-weight: 400; 28 | text-decoration: none; 29 | position: relative; 30 | border: 1px solid white; 31 | padding: 10px; 32 | background: $cyan; 33 | color: white; 34 | 35 | 36 | &:after { 37 | font-family: 'FontAwesome'; 38 | content: '\f107'; 39 | position: absolute; 40 | right: 10px; 41 | top: 10px; 42 | display: block; 43 | transition: transform 300ms; 44 | } 45 | 46 | &.is-open { 47 | &:after { 48 | transform: rotateZ(180deg); 49 | } 50 | } 51 | 52 | &.is-disabled { 53 | opacity: 0.5; 54 | background-color: grey; 55 | } 56 | } 57 | 58 | .CustomTriggerCSS { 59 | background-color: lightcoral; 60 | transition: background-color 200ms ease; 61 | 62 | 63 | } 64 | 65 | .CustomTriggerCSS--open { 66 | background-color: darkslateblue; 67 | } 68 | 69 | .Collapsible__custom-sibling { 70 | padding: 5px; 71 | font-size: 12px; 72 | background-color: #CBB700; 73 | color: black; 74 | } 75 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | declare class Collapsible extends React.Component {} 4 | 5 | export interface CollapsibleProps extends React.HTMLProps { 6 | transitionTime?: number; 7 | transitionCloseTime?: number | null; 8 | triggerTagName?: string; 9 | easing?: string; 10 | open?: boolean; 11 | containerElementProps?: object; 12 | classParentString?: string; 13 | openedClassName?: string; 14 | triggerStyle?: null | React.CSSProperties; 15 | triggerClassName?: string; 16 | triggerOpenedClassName?: string; 17 | triggerElementProps?: object; 18 | contentElementId?: string; 19 | contentOuterClassName?: string; 20 | contentInnerClassName?: string; 21 | accordionPosition?: string | number; 22 | handleTriggerClick?: (accordionPosition?: string | number) => void; 23 | onOpen?: () => void; 24 | onClose?: () => void; 25 | onOpening?: () => void; 26 | onClosing?: () => void; 27 | onTriggerOpening?: () => void; 28 | onTriggerClosing?: () => void; 29 | trigger: string | React.ReactElement; 30 | triggerWhenOpen?: string | React.ReactElement; 31 | triggerDisabled?: boolean; 32 | lazyRender?: boolean; 33 | overflowWhenOpen?: 34 | | "hidden" 35 | | "visible" 36 | | "auto" 37 | | "scroll" 38 | | "inherit" 39 | | "initial" 40 | | "unset"; 41 | contentHiddenWhenClosed?: boolean; 42 | triggerSibling?: string | React.ReactElement; 43 | className?: string; 44 | tabIndex?: number; 45 | contentContainerTagName?: string; 46 | } 47 | 48 | export default Collapsible; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-collapsible", 3 | "version": "2.10.0", 4 | "description": "React component to wrap content in Collapsible element with trigger to open and close.", 5 | "keywords": [ 6 | "react-component", 7 | "react", 8 | "collapse", 9 | "collapsible", 10 | "accordion" 11 | ], 12 | "main": "dist/index.js", 13 | "types": "./index.d.ts", 14 | "author": "Glenn Flanagan ", 15 | "contributors": [ 16 | "Karl Taylor ", 17 | "Rebeca Sarai " 18 | ], 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@babel/core": "^7.11.4", 22 | "@babel/eslint-parser": "^7.12.1", 23 | "@babel/plugin-proposal-class-properties": "^7.10.4", 24 | "@babel/preset-env": "^7.11.0", 25 | "@babel/preset-react": "^7.10.4", 26 | "@types/react": "^16.13.1", 27 | "babel-loader": "^8.1.0", 28 | "babel-preset-react": "^6.11.1", 29 | "enzyme": "^3.3.0", 30 | "enzyme-adapter-react-16": "^1.1.1", 31 | "eslint": "^7.18.0", 32 | "eslint-plugin-react": "^7.22.0", 33 | "jest": "^26.0.1", 34 | "prop-types": "^15.7.2", 35 | "react": "^16.13.1", 36 | "react-dom": "^16.13.1", 37 | "webpack": "^4.44.1", 38 | "webpack-cli": "^3.3.12" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "https://github.com/glennflanagan/react-collapsible" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/glennflanagan/react-collapsible/issues" 46 | }, 47 | "scripts": { 48 | "build": "webpack", 49 | "dev": "webpack --watch", 50 | "prepare": "npm run build", 51 | "test": "jest" 52 | }, 53 | "peerDependencies": { 54 | "react": "~15 || ~16 || ~17 || ~18", 55 | "react-dom": "~15 || ~16 || ~17 || ~18" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Collapsible from '../../dist'; 4 | 5 | import ZeroHeightCollapsible from './examples/ZeroHeightCollapsible'; 6 | 7 | const triggerSiblingExample = () => ( 8 |
9 | This is a sibling to the trigger which wont cause the Collapsible to open! 10 |
11 | ); 12 | 13 | const App = () => { 14 | return ( 15 |
16 | 17 |

18 | This is the collapsible content. It can be any element or React 19 | component you like. 20 |

21 |

22 | It can even be another Collapsible component. Check out the next 23 | section! 24 |

25 |
26 | 27 | 28 |

Would you look at that!

29 |

See; you can nest as many Collapsible components as you like.

30 | 31 | 32 |

33 | And there's no limit to how many levels deep you go. Or how many you 34 | have on the same level. 35 |

36 | 37 | 38 |

39 | It just keeps going and going! Well, actually we've stopped here. 40 | But that's only because I'm running out of things to type. 41 |

42 |
43 | 44 |

45 | And would you look at that! This one is open by default. Sexy 46 | huh!? 47 |

48 |

49 | You can pass the prop of open={true} which will make the 50 | Collapsible open by default. 51 |

52 |
53 | 57 |

Whoosh! That was fast right?

58 |

59 | You can control the time it takes to animate (transition) by 60 | passing the prop transitionTime a value in milliseconds. This one 61 | was set to transitionTime={100} 62 |

63 |
64 |
65 |
66 | 67 | 72 |

73 | Well maybe not. But did you see that little wiggle at the end. That is 74 | using a CSS cubic-beizer for the easing! 75 |

76 |

77 | You can pass any string into the prop easing that you would declare in 78 | a CSS transition-timing-function. This means you have complete control 79 | over how that Collapsible appears. 80 |

81 |
82 | 83 | 88 |

89 | That's correct. This collapsible section will animate to the height it 90 | needs to and then set it's height back to auto. 91 |

92 |

93 | This means that no matter what width you stretch that viewport to, the 94 | Collapsible it will respond to it. 95 |

96 |

97 | And no matter what height the content within it is, it will change 98 | height too. 99 |

100 |

CSS Styles

101 |

102 | All of the style of the Collapsible (apart from the overflow and 103 | transition) are controlled by your own CSS too. 104 |

105 |

106 | By default the top-level CSS class is Collapsible, but you have 107 | control over this too so you can easily add it into your own project. 108 | Neato! 109 |

110 |

111 | So by setting the prop of 112 | classParentString={"MyNamespacedClass"} then the top-level 113 | class will become MyNamespacedClass. 114 |

115 |
116 | 117 | 124 |

125 | Add the prop of{' '} 126 | lazyRender and the 127 | content will only be rendered when the trigger is pressed 128 |

129 | 130 | 131 | 132 | 133 | 134 | 135 |
136 | 137 | 141 |

142 | Use the `triggerTagName` prop to set the trigger wrapping 143 | element. 144 |

145 |

146 | Defaults to span. 147 |

148 |
149 | 150 | 157 |

158 | This is the collapsible content. It can be any element or React 159 | component you like. 160 |

161 |
162 | 163 | 168 |

This one has it's trigger disabled in the open position. Nifty.

169 |

170 | You also get the is-disabled CSS class so you can 171 | style it. 172 |

173 |
174 | 175 | 179 |

This one has it's trigger disabled in the open position. Nifty.

180 |

181 | You also get the is-disabled CSS class so you can 182 | style it. 183 |

184 |
185 | 186 | 190 |

191 | Adds a style attribute to the span trigger. 192 |

193 |
194 | 199 |

200 | Some element attributes (id & lang) have been passed 201 | as properties using containerElementProps. 202 |

203 |
204 | 205 | 206 | 207 | 208 | 209 | 210 |
211 |
212 | 219 |
220 |
221 | 222 | 223 |
224 | ); 225 | }; 226 | 227 | ReactDOM.render(, document.querySelector('#root')); 228 | -------------------------------------------------------------------------------- /__tests__/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import React from 'react'; 3 | import { configure, shallow, mount } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | 6 | import Collapsible from '../src/Collapsible'; 7 | 8 | configure({ adapter: new Adapter() }); 9 | 10 | const dummyEvent = { preventDefault: () => {} }; 11 | 12 | class CollapsibleStateContainer extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { open: !this.props.changeOpenTo }; 16 | } 17 | componentDidMount() { 18 | this.setState({ open: this.props.changeOpenTo }); 19 | } 20 | render() { 21 | return ; 22 | } 23 | } 24 | 25 | describe('', () => { 26 | it('renders an element with the class `.Collapsible`.', () => { 27 | const wrapper = shallow(); 28 | expect(wrapper.is('.Collapsible')).toEqual(true); 29 | }); 30 | 31 | it('renders Collapsible with trigger text.', () => { 32 | const wrapper = shallow(); 33 | expect(wrapper.find('span').text()).toEqual('Hello World'); 34 | }); 35 | 36 | it('given a closed Collapsible fires the onOpening prop when clicked to open', () => { 37 | const mockOnOpening = jest.fn(); 38 | const collapsible = mount( 39 | 40 | ); 41 | const trigger = collapsible.find('.Collapsible__trigger'); 42 | 43 | expect(trigger).toHaveLength(1); 44 | trigger.simulate('click', dummyEvent); 45 | expect(mockOnOpening.mock.calls).toHaveLength(1); 46 | }); 47 | 48 | it('given an open Collapsible fires the onClosing prop when clicked to close', () => { 49 | const mockOnClosing = jest.fn(); 50 | const collapsible = mount( 51 | 52 | ); 53 | const trigger = collapsible.find('.Collapsible__trigger'); 54 | 55 | expect(trigger).toHaveLength(1); 56 | trigger.simulate('click', dummyEvent); 57 | expect(mockOnClosing.mock.calls).toHaveLength(1); 58 | }); 59 | 60 | it('given a closed Collapsible it fires the onOpen prop after the transistion', () => { 61 | const mockOnOpen = jest.fn(); 62 | const collapsible = shallow( 63 | 64 | Some Content 65 | 66 | ); 67 | const outer = collapsible.find('.Collapsible__contentOuter'); 68 | 69 | expect(outer).toHaveLength(1); 70 | outer.simulate('transitionEnd', dummyEvent); 71 | expect(mockOnOpen.mock.calls).toHaveLength(1); 72 | }); 73 | 74 | it('given an open Collapsible it fires the onClose prop after the transistion', () => { 75 | const mockOnClose = jest.fn(); 76 | const collapsible = shallow( 77 | 78 | Some Content 79 | 80 | ); 81 | const outer = collapsible.find('.Collapsible__contentOuter'); 82 | 83 | expect(outer).toHaveLength(1); 84 | outer.simulate('transitionEnd', dummyEvent); 85 | expect(mockOnClose.mock.calls).toHaveLength(1); 86 | }); 87 | 88 | it('given a Collapsible with the handleTriggerClick prop, the handleTriggerClick prop gets fired', () => { 89 | const mockHandleTriggerClick = jest.fn(); 90 | const collapsible = shallow( 91 | 95 | ); 96 | const trigger = collapsible.find('.Collapsible__trigger'); 97 | 98 | expect(trigger).toHaveLength(1); 99 | trigger.simulate('click', dummyEvent); 100 | expect(mockHandleTriggerClick.mock.calls).toHaveLength(1); 101 | }); 102 | 103 | describe('onTriggerOpening prop', () => { 104 | it('is called when a closed Collapsible is triggered', () => { 105 | const mockOnTriggerOpening = jest.fn(); 106 | const collapsible = mount( 107 | 111 | ); 112 | const trigger = collapsible.find('.Collapsible__trigger'); 113 | 114 | expect(trigger).toHaveLength(1); 115 | trigger.simulate('click', dummyEvent); 116 | expect(mockOnTriggerOpening.mock.calls).toHaveLength(1); 117 | }); 118 | 119 | it("is not called when a closed collapsible's open prop changes to true", () => { 120 | const mockOnTriggerOpening = jest.fn(); 121 | const collapsible = mount( 122 | 127 | ); 128 | const trigger = collapsible.find('.Collapsible__trigger'); 129 | 130 | expect(trigger).toHaveLength(1); 131 | expect(mockOnTriggerOpening.mock.calls).toHaveLength(0); 132 | }); 133 | }); 134 | 135 | describe('onTriggerClosing prop', () => { 136 | it('is called when an open Collapsible is triggered', () => { 137 | const mockOnTriggerClosing = jest.fn(); 138 | const collapsible = mount( 139 | 144 | ); 145 | const trigger = collapsible.find('.Collapsible__trigger'); 146 | 147 | expect(trigger).toHaveLength(1); 148 | trigger.simulate('click', dummyEvent); 149 | expect(mockOnTriggerClosing.mock.calls).toHaveLength(1); 150 | }); 151 | 152 | it("is not called when an open collapsible's open prop changes to false", () => { 153 | const mockOnTriggerClosing = jest.fn(); 154 | const collapsible = mount( 155 | 160 | ); 161 | const trigger = collapsible.find('.Collapsible__trigger'); 162 | 163 | expect(trigger).toHaveLength(1); 164 | expect(mockOnTriggerClosing.mock.calls).toHaveLength(0); 165 | }); 166 | }); 167 | 168 | describe('Zero Height Collapsibles', () => { 169 | it('opens correctly even if height is 0', () => { 170 | const wrapper = mount(); 171 | 172 | expect(wrapper.state().height).toBe(0); 173 | expect(wrapper.state().isClosed).toBe(true); 174 | 175 | wrapper.find('.Collapsible__trigger').simulate('click'); 176 | 177 | expect(wrapper.state().isClosed).toBe(false); 178 | }); 179 | 180 | it('closes correctly even if height is 0', () => { 181 | jest.useFakeTimers(); 182 | const mockFn = jest.fn(); 183 | const wrapper = mount( 184 | 185 | ); 186 | 187 | expect(wrapper.state().height).toBe('auto'); // defaults to auto when open 188 | expect(wrapper.state().isClosed).toBe(false); 189 | 190 | wrapper.find('.Collapsible__trigger').simulate('click'); 191 | 192 | expect(mockFn.mock.calls).toHaveLength(1); 193 | 194 | wrapper.setProps({ open: false }); 195 | 196 | jest.runAllTimers(); 197 | 198 | expect(wrapper.state().isClosed).toBe(true); 199 | expect(wrapper.props().open).toBe(false); 200 | }); 201 | }); 202 | 203 | describe('Trigger Siblings', () => { 204 | it('Renders a trigger sibling as a string', () => { 205 | const string = 'Hello world'; 206 | const wrapper = mount(); 207 | 208 | const elementText = wrapper.find('.Collapsible__trigger-sibling').text(); 209 | 210 | expect(elementText).toBe(string); 211 | }); 212 | 213 | it('Renders a react function', () => { 214 | const string = 'Hello world'; 215 | const ReactFn = () =>
{string}
; 216 | const wrapper = mount(); 217 | 218 | const element = wrapper.find('[test-id="test-react-fn"]'); 219 | 220 | expect(element.text()).toBe(string); 221 | }); 222 | 223 | it('Renders a react component', () => { 224 | const string = 'Hello world'; 225 | const ReactFn = () =>
{string}
; 226 | const wrapper = mount(} />); 227 | 228 | const element = wrapper.find('[test-id="test-react-fn"]'); 229 | 230 | expect(element.text()).toBe(string); 231 | }); 232 | 233 | it('Renders a function', () => { 234 | const string = 'Hello world'; 235 | const wrapper = mount( 236 |
{string}
} 238 | /> 239 | ); 240 | 241 | const element = wrapper.find('[test-id="test-react-fn"]'); 242 | 243 | expect(element.text()).toBe(string); 244 | }); 245 | }); 246 | }); 247 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Npm Version](https://img.shields.io/npm/v/react-collapsible.svg?style=flat-square)](https://www.npmjs.com/package/react-collapsible) [![License](https://img.shields.io/npm/l/react-collapsible.svg?style=flat-square)](https://github.com/glennflanagan/react-collapsible/blob/develop/LICENSE.md) [![Downloads Per Week](https://img.shields.io/npm/dw/react-collapsible.svg?style=flat-square)](https://npmcharts.com/compare/react-collapsible) 2 | 3 | # React Responsive Collapsible Section Component (Collapsible) 4 | 5 | React component to wrap content in Collapsible element with trigger to open and close. 6 | 7 | ![Alt text](example/img/example.gif) 8 | 9 | It's like an accordion, but where any number of sections can be open at the same time. 10 | 11 | Supported by [Browserstack](https://www.browserstack.com). 12 | 13 | ![Browserstack Logo](example/img/browserstack-logo.png 'Browserstack') 14 | 15 | --- 16 | 17 | ## Migrating from v1.x to v2.x 18 | 19 | Version 2 is 100% API complete to version 1. However, there is a breaking change in the `onOpen` and `onClose` callbacks. These methods now fire at the end of the collapsing animation. There is also the addition of `onOpening` and `onClosing` callbacks which fire at the beginning of the animation. 20 | 21 | To migrate to v2 from v1 simply change the `onOpen` prop to `onOpening` and `onClose` to `onClosing`. 22 | 23 | ## Installation 24 | 25 | Install via npm or yarn 26 | 27 | ```bash 28 | npm install react-collapsible --save 29 | 30 | yarn add react-collapsible 31 | ``` 32 | 33 | ## Usage 34 | 35 | Collapsible can receive any HTML elements or React component as it's children. Collapsible will wrap the contents, as well as generate a trigger element which will control showing and hiding. 36 | 37 | ```javascript 38 | import React from 'react'; 39 | import Collapsible from 'react-collapsible'; 40 | 41 | const App = () => { 42 | return ( 43 | 44 |

45 | This is the collapsible content. It can be any element or React 46 | component you like. 47 |

48 |

49 | It can even be another Collapsible component. Check out the next 50 | section! 51 |

52 |
53 | ); 54 | }; 55 | 56 | export default App; 57 | ``` 58 | 59 | With a little CSS becomes 60 | 61 | ![Alt text](example/img/becomes.png) 62 | 63 | ## Properties _(Options)_ 64 | 65 | ### **contentContainerTagName** | _string_ | default: `div` 66 | 67 | Tag Name for the Collapsible Root Element. 68 | 69 | ### **containerElementProps** | _object_ 70 | 71 | Pass props (or attributes) to the top div element. Useful for inserting `id`. 72 | 73 | ### **trigger** | _string_ or _React Element_ | **required** 74 | 75 | The text or element to appear in the trigger link. 76 | 77 | ### **triggerTagName** | _string_ | default: span 78 | 79 | The tag name of the element wrapping the trigger text or element. 80 | 81 | ### **triggerStyle** | _object_ | default: null 82 | 83 | Adds a style attribute to the trigger. 84 | 85 | ### **triggerWhenOpen** | _string_ or _React Element_ 86 | 87 | Optional trigger text or element to change to when the Collapsible is open. 88 | 89 | ### **triggerDisabled** | _boolean_ | default: false 90 | 91 | Disables the trigger handler if `true`. Note: this has no effect other than applying the `.is-disabled` CSS class if you've provided a `handleTriggerClick` prop. 92 | 93 | ### **triggerElementProps** | _object_ 94 | 95 | Pass props (or attributes) to the trigger wrapping element. Useful for inserting `role` when using `tabIndex`. 96 | 97 | As an alternative to an auto generated id (which is not guaranteed to be unique in extremely fast builds) that is used as the TriggerElement id, and also as a separate `aria-labelledby` attribute, a custom id can be assigned by providing `triggerElementProps` with an object containing an `id` key and value, e.g. `{id: 'some-value'}`. 98 | 99 | ### **contentElementId** | _string_ 100 | 101 | Allows for an alternative to an auto generated id (which is not guaranteed to be unique in extremely fast builds) that is used as part of the component id and the `aria-controls` attribute of the component. 102 | 103 | ### **transitionTime** | _number_ | default: 400 104 | 105 | The number of milliseconds for the open/close transition to take. 106 | 107 | ### **transitionCloseTime** | _number_ | default: null 108 | 109 | The number of milliseconds for the close transition to take. 110 | 111 | ### **easing** | _string_ | default: 'linear' 112 | 113 | The CSS easing method you wish to apply to the open/close transition. This string can be any valid value of CSS `transition-timing-function`. For reference view the [MDN documentation](https://developer.mozilla.org/en/docs/Web/CSS/transition-timing-function). 114 | 115 | ### **open** | _bool_ | default: false 116 | 117 | Set to true if you want the Collapsible to begin in the open state. You can also use this prop to manage the state from a parent component. 118 | 119 | ### **accordionPosition** | _string_ 120 | 121 | Unique key used to identify the `Collapse` instance when used in an accordion. 122 | 123 | ### **handleTriggerClick** | _function_ 124 | 125 | Define this to override the click handler for the trigger link. Takes one parameter, which is `props.accordionPosition`. 126 | 127 | ### **onOpen** | _function_ 128 | 129 | Is called when the Collapsible has opened. 130 | 131 | ### **onClose** | _function_ 132 | 133 | Is called when the Collapsible has closed. 134 | 135 | ### **onOpening** | _function_ 136 | 137 | Is called when the Collapsible is opening. 138 | 139 | ### **onClosing** | _function_ 140 | 141 | Is called when the Collapsible is closing. 142 | 143 | ### **onTriggerOpening** | _function_ 144 | 145 | Is called when the Collapsible open trigger is clicked. Like onOpening except it isn't called when the open prop is updated. 146 | 147 | ### **onTriggerClosing** | _function_ 148 | 149 | Is called when the Collapsible close trigger is clicked. Like onClosing except it isn't called when the open prop is updated. 150 | 151 | ### **lazyRender** | _bool_ | default: false 152 | 153 | Set this to true to postpone rendering of all of the content of the Collapsible until before it's opened for the first time 154 | 155 | ### **overflowWhenOpen** | _enum_ | default: 'hidden' 156 | 157 | The CSS overflow property once the Collapsible is open. This can be any one of the valid CSS values of `'hidden'`, `'visible'`, `'auto'`, `'scroll'`, `'inherit'`, `'initial'`, or `'unset'` 158 | 159 | ### **contentHiddenWhenClosed** | _bool_ | default: false 160 | 161 | Set this to true to add the html hidden attribute to the content when the collapsible is fully closed. 162 | 163 | ### **triggerSibling** | _element_ | default: null 164 | 165 | Escape hatch to add arbitrary content on the trigger without triggering expand/collapse. It's up to you to style it as needed. This is inserted in component tree and DOM directly 166 | after `.Collapsible__trigger` 167 | 168 | ### **tabIndex** | _number_ | default: null 169 | 170 | A `tabIndex` prop adds the `tabIndex` attribute to the trigger element which in turn allows the Collapsible trigger to gain focus. 171 | 172 | ## CSS Class String Props 173 | 174 | ### **classParentString** | _string_ | default: Collapsible 175 | 176 | Use this to overwrite the parent CSS class for the Collapsible component parts. Read more in the CSS section below. 177 | 178 | ### **className** | _string_ 179 | 180 | `.Collapsible` element (root) when closed 181 | 182 | ### **openedClassName** | _string_ 183 | 184 | `.Collapsible` element (root) when open 185 | 186 | ### **triggerClassName** | _string_ 187 | 188 | `.Collapsible__trigger` element (root) when closed 189 | 190 | ### **triggerOpenedClassName** | _string_ 191 | 192 | `.Collapsible__trigger` element (root) when open 193 | 194 | ### **contentOuterClassName** | _string_ 195 | 196 | `.Collapsible__contentOuter` element 197 | 198 | ### **contentInnerClassName** | _string_ 199 | 200 | `.Collapsible__contentInner` element 201 | 202 | ## CSS Styles 203 | 204 | In theory you don't need any CSS to get this to work, but let's face it, it'd be pretty rubbish without it. 205 | 206 | By default the parent CSS class name is `.Collapsible` but this can be changed by setting the `classParentString` property on the component. 207 | 208 | The CSS class names follow a [type of BEM pattern](http://getbem.com/introduction/) of CSS naming. Below is a list of the CSS classes available on the component. 209 | 210 | ### `.Collapsible` 211 | 212 | The parent element for the components. 213 | 214 | ### `.Collapsible__trigger` 215 | 216 | The trigger link that controls the opening and closing of the component. 217 | The state of the component is also reflected on this element with the modifier classes; 218 | 219 | - `is-closed` | Closed state 220 | - `is-open` | Open setState 221 | - `is-disabled` | Trigger is disabled 222 | 223 | ### `.Collapsible__contentOuter` 224 | 225 | The outer container that hides the content. This is set to `overflow: hidden` within the javascript but everything else about it is for you to change. 226 | 227 | ### `.Collapsible__contentInner` 228 | 229 | This is a container for the content passed into the component. This keeps everything nice and neat and allows the component to do all it's whizzy calculations. 230 | 231 | If you're using a CSS framework such as Foundation or Bootstrap, you probably want to use their classes instead of styling `.Collapsible`. See Properties above. 232 | 233 | ## Example 234 | 235 | Examples of `` components can be found in the `./example` folder. To get the example running: 236 | 237 | ``` 238 | cd example && yarn && yarn start 239 | ``` 240 | 241 | This will run a [parceljs](https://parceljs.org) app. 242 | 243 | ## Issues 244 | 245 | Please create an issue for any bug or feature requests. 246 | 247 | Here is a plain [JSFiddle](https://jsfiddle.net/sm7n31p1/1/) to use for replicating bugs. 248 | 249 | ## Licence 250 | 251 | React Responsive Collapsible Section Component is [MIT licensed](LICENSE.md) 252 | -------------------------------------------------------------------------------- /src/Collapsible.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import setInTransition from './setInTransition'; 5 | 6 | class Collapsible extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.timeout = undefined; 11 | 12 | this.contentId = 13 | props.contentElementId || `collapsible-content-${Date.now()}`; 14 | 15 | this.triggerId = 16 | props.triggerElementProps.id || `collapsible-trigger-${Date.now()}`; 17 | 18 | // Defaults the dropdown to be closed 19 | if (props.open) { 20 | this.state = { 21 | isClosed: false, 22 | shouldSwitchAutoOnNextCycle: false, 23 | height: 'auto', 24 | transition: 'none', 25 | hasBeenOpened: true, 26 | overflow: props.overflowWhenOpen, 27 | inTransition: false, 28 | }; 29 | } else { 30 | this.state = { 31 | isClosed: true, 32 | shouldSwitchAutoOnNextCycle: false, 33 | height: 0, 34 | transition: `height ${props.transitionTime}ms ${props.easing}`, 35 | hasBeenOpened: false, 36 | overflow: 'hidden', 37 | inTransition: false, 38 | }; 39 | } 40 | } 41 | 42 | componentDidUpdate(prevProps, prevState) { 43 | if (this.state.shouldOpenOnNextCycle) { 44 | this.continueOpenCollapsible(); 45 | } 46 | 47 | if ( 48 | (prevState.height === 'auto' || prevState.height === 0) && 49 | this.state.shouldSwitchAutoOnNextCycle === true 50 | ) { 51 | window.clearTimeout(this.timeout); 52 | this.timeout = window.setTimeout(() => { 53 | // Set small timeout to ensure a true re-render 54 | this.setState({ 55 | height: 0, 56 | overflow: 'hidden', 57 | isClosed: true, 58 | shouldSwitchAutoOnNextCycle: false, 59 | }); 60 | }, 50); 61 | } 62 | 63 | // If there has been a change in the open prop (controlled by accordion) 64 | if (prevProps.open !== this.props.open) { 65 | if (this.props.open === true) { 66 | this.openCollapsible(); 67 | this.props.onOpening(); 68 | } else { 69 | this.closeCollapsible(); 70 | this.props.onClosing(); 71 | } 72 | } 73 | } 74 | 75 | componentWillUnmount() { 76 | window.clearTimeout(this.timeout); 77 | } 78 | 79 | closeCollapsible() { 80 | const { innerRef } = this; 81 | 82 | this.setState({ 83 | shouldSwitchAutoOnNextCycle: true, 84 | height: innerRef.scrollHeight, 85 | transition: `height ${ 86 | this.props.transitionCloseTime 87 | ? this.props.transitionCloseTime 88 | : this.props.transitionTime 89 | }ms ${this.props.easing}`, 90 | inTransition: setInTransition(innerRef.scrollHeight), 91 | }); 92 | } 93 | 94 | openCollapsible() { 95 | this.setState({ 96 | inTransition: setInTransition(this.innerRef.scrollHeight), 97 | shouldOpenOnNextCycle: true, 98 | }); 99 | } 100 | 101 | continueOpenCollapsible = () => { 102 | const { innerRef } = this; 103 | 104 | this.setState({ 105 | height: innerRef.scrollHeight, 106 | transition: `height ${this.props.transitionTime}ms ${this.props.easing}`, 107 | isClosed: false, 108 | hasBeenOpened: true, 109 | inTransition: setInTransition(innerRef.scrollHeight), 110 | shouldOpenOnNextCycle: false, 111 | }); 112 | }; 113 | 114 | handleTriggerClick = (event) => { 115 | if (this.props.triggerDisabled || this.state.inTransition) { 116 | return; 117 | } 118 | 119 | event.preventDefault(); 120 | 121 | if (this.props.handleTriggerClick) { 122 | this.props.handleTriggerClick(this.props.accordionPosition); 123 | } else { 124 | if (this.state.isClosed === true) { 125 | this.openCollapsible(); 126 | this.props.onOpening(); 127 | this.props.onTriggerOpening(); 128 | } else { 129 | this.closeCollapsible(); 130 | this.props.onClosing(); 131 | this.props.onTriggerClosing(); 132 | } 133 | } 134 | }; 135 | 136 | renderNonClickableTriggerElement() { 137 | const { triggerSibling, classParentString } = this.props; 138 | if (!triggerSibling) return null; 139 | 140 | const triggerSiblingType = typeof triggerSibling; 141 | 142 | switch (triggerSiblingType) { 143 | case 'string': 144 | return ( 145 | 146 | {triggerSibling} 147 | 148 | ); 149 | case 'function': 150 | return triggerSibling(); 151 | case 'object': 152 | return triggerSibling; 153 | default: 154 | return null; 155 | } 156 | } 157 | 158 | handleTransitionEnd = (e) => { 159 | // only handle transitions that origin from the container of this component 160 | if (e.target !== this.innerRef) { 161 | return; 162 | } 163 | // Switch to height auto to make the container responsive 164 | if (!this.state.isClosed) { 165 | this.setState({ 166 | height: 'auto', 167 | overflow: this.props.overflowWhenOpen, 168 | inTransition: false, 169 | }); 170 | this.props.onOpen(); 171 | } else { 172 | this.setState({ inTransition: false }); 173 | this.props.onClose(); 174 | } 175 | }; 176 | 177 | setInnerRef = (ref) => (this.innerRef = ref); 178 | 179 | render() { 180 | const dropdownStyle = { 181 | height: this.state.height, 182 | WebkitTransition: this.state.transition, 183 | msTransition: this.state.transition, 184 | transition: this.state.transition, 185 | overflow: this.state.overflow, 186 | }; 187 | 188 | var openClass = this.state.isClosed ? 'is-closed' : 'is-open'; 189 | var disabledClass = this.props.triggerDisabled ? 'is-disabled' : ''; 190 | 191 | //If user wants different text when tray is open 192 | var trigger = 193 | this.state.isClosed === false && this.props.triggerWhenOpen !== undefined 194 | ? this.props.triggerWhenOpen 195 | : this.props.trigger; 196 | 197 | const ContentContainerElement = this.props.contentContainerTagName; 198 | 199 | // If user wants a trigger wrapping element different than 'span' 200 | const TriggerElement = this.props.triggerTagName; 201 | 202 | // Don't render children until the first opening of the Collapsible if lazy rendering is enabled 203 | var children = 204 | this.props.lazyRender && 205 | !this.state.hasBeenOpened && 206 | this.state.isClosed && 207 | !this.state.inTransition 208 | ? null 209 | : this.props.children; 210 | 211 | // Construct CSS classes strings 212 | const { classParentString, contentOuterClassName, contentInnerClassName } = 213 | this.props; 214 | 215 | const triggerClassString = `${classParentString}__trigger ${openClass} ${disabledClass} ${ 216 | this.state.isClosed 217 | ? this.props.triggerClassName 218 | : this.props.triggerOpenedClassName 219 | }`; 220 | 221 | const parentClassString = `${classParentString} ${ 222 | this.state.isClosed ? this.props.className : this.props.openedClassName 223 | }`; 224 | 225 | const outerClassString = `${classParentString}__contentOuter ${contentOuterClassName}`; 226 | const innerClassString = `${classParentString}__contentInner ${contentInnerClassName}`; 227 | 228 | return ( 229 | 233 | { 239 | const { key } = event; 240 | if ( 241 | (key === ' ' && 242 | this.props.triggerTagName.toLowerCase() !== 'button') || 243 | key === 'Enter' 244 | ) { 245 | this.handleTriggerClick(event); 246 | } 247 | }} 248 | tabIndex={this.props.tabIndex && this.props.tabIndex} 249 | aria-expanded={!this.state.isClosed} 250 | aria-disabled={this.props.triggerDisabled} 251 | aria-controls={this.contentId} 252 | role="button" // Since our default TriggerElement is not a button 253 | {...this.props.triggerElementProps} 254 | > 255 | {trigger} 256 | 257 | 258 | {this.renderNonClickableTriggerElement()} 259 | 260 | 276 | 277 | ); 278 | } 279 | } 280 | 281 | Collapsible.propTypes = { 282 | transitionTime: PropTypes.number, 283 | transitionCloseTime: PropTypes.number, 284 | triggerTagName: PropTypes.string, 285 | easing: PropTypes.string, 286 | open: PropTypes.bool, 287 | containerElementProps: PropTypes.object, 288 | triggerElementProps: PropTypes.object, 289 | contentElementId: PropTypes.string, 290 | classParentString: PropTypes.string, 291 | className: PropTypes.string, 292 | openedClassName: PropTypes.string, 293 | triggerStyle: PropTypes.object, 294 | triggerClassName: PropTypes.string, 295 | triggerOpenedClassName: PropTypes.string, 296 | contentOuterClassName: PropTypes.string, 297 | contentInnerClassName: PropTypes.string, 298 | accordionPosition: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 299 | handleTriggerClick: PropTypes.func, 300 | onOpen: PropTypes.func, 301 | onClose: PropTypes.func, 302 | onOpening: PropTypes.func, 303 | onClosing: PropTypes.func, 304 | onTriggerOpening: PropTypes.func, 305 | onTriggerClosing: PropTypes.func, 306 | trigger: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), 307 | triggerWhenOpen: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), 308 | triggerDisabled: PropTypes.bool, 309 | lazyRender: PropTypes.bool, 310 | overflowWhenOpen: PropTypes.oneOf([ 311 | 'hidden', 312 | 'visible', 313 | 'auto', 314 | 'scroll', 315 | 'inherit', 316 | 'initial', 317 | 'unset', 318 | ]), 319 | contentHiddenWhenClosed: PropTypes.bool, 320 | triggerSibling: PropTypes.oneOfType([ 321 | PropTypes.string, 322 | PropTypes.element, 323 | PropTypes.func, 324 | ]), 325 | tabIndex: PropTypes.number, 326 | contentContainerTagName: PropTypes.string, 327 | children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), 328 | }; 329 | 330 | Collapsible.defaultProps = { 331 | transitionTime: 400, 332 | transitionCloseTime: null, 333 | triggerTagName: 'span', 334 | easing: 'linear', 335 | open: false, 336 | classParentString: 'Collapsible', 337 | triggerDisabled: false, 338 | lazyRender: false, 339 | overflowWhenOpen: 'hidden', 340 | contentHiddenWhenClosed: false, 341 | openedClassName: '', 342 | triggerStyle: null, 343 | triggerClassName: '', 344 | triggerOpenedClassName: '', 345 | contentOuterClassName: '', 346 | contentInnerClassName: '', 347 | className: '', 348 | triggerSibling: null, 349 | onOpen: () => {}, 350 | onClose: () => {}, 351 | onOpening: () => {}, 352 | onClosing: () => {}, 353 | onTriggerOpening: () => {}, 354 | onTriggerClosing: () => {}, 355 | tabIndex: null, 356 | contentContainerTagName: 'div', 357 | triggerElementProps: {}, 358 | }; 359 | 360 | export default Collapsible; 361 | -------------------------------------------------------------------------------- /example/img/browserstack-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | browserstack-logo 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 46 | 47 | --------------------------------------------------------------------------------