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 |
}
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 | [](https://www.npmjs.com/package/react-collapsible) [](https://github.com/glennflanagan/react-collapsible/blob/develop/LICENSE.md) [](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 | 
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 | 
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 | 
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 |