Skip to content
This repository was archived by the owner on Apr 21, 2026. It is now read-only.

Commit f7df786

Browse files
author
dpatanin
committed
add collapsible component to examples
1 parent 8ec6b25 commit f7df786

4 files changed

Lines changed: 378 additions & 18 deletions

File tree

src/components/Collapsible.js

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
import React, { Component } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { IoIosArrowDown, IoIosArrowForward } from 'react-icons/io';
4+
5+
/**
6+
* This Component was adapted from https://www.npmjs.com/package/react-collapsible.
7+
*/
8+
class Collapsible extends Component {
9+
constructor(props) {
10+
super(props);
11+
12+
this.timeout = undefined;
13+
14+
// Bind class methods
15+
this.handleTriggerClick = this.handleTriggerClick.bind(this);
16+
this.handleTransitionEnd = this.handleTransitionEnd.bind(this);
17+
this.continueOpenCollapsible = this.continueOpenCollapsible.bind(this);
18+
this.setInnerRef = this.setInnerRef.bind(this);
19+
20+
// Defaults the dropdown to be closed
21+
if (props.open) {
22+
this.state = {
23+
isClosed: false,
24+
shouldSwitchAutoOnNextCycle: false,
25+
height: 'auto',
26+
transition: 'none',
27+
hasBeenOpened: true,
28+
overflow: props.overflowWhenOpen,
29+
inTransition: false,
30+
};
31+
} else {
32+
this.state = {
33+
isClosed: true,
34+
shouldSwitchAutoOnNextCycle: false,
35+
height: 0,
36+
transition: `height ${props.transitionTime}ms ${props.easing}`,
37+
hasBeenOpened: false,
38+
overflow: 'hidden',
39+
inTransition: false,
40+
};
41+
}
42+
}
43+
44+
componentDidUpdate(prevProps, prevState) {
45+
if (this.state.shouldOpenOnNextCycle) {
46+
this.continueOpenCollapsible();
47+
}
48+
49+
if (
50+
prevState.height === 'auto' &&
51+
this.state.shouldSwitchAutoOnNextCycle === true
52+
) {
53+
window.clearTimeout(this.timeout);
54+
this.timeout = window.setTimeout(() => {
55+
// Set small timeout to ensure a true re-render
56+
this.setState({
57+
height: 0,
58+
overflow: 'hidden',
59+
isClosed: true,
60+
shouldSwitchAutoOnNextCycle: false,
61+
});
62+
}, 50);
63+
}
64+
65+
// If there has been a change in the open prop (controlled by accordion)
66+
if (prevProps.open !== this.props.open) {
67+
if (this.props.open === true) {
68+
this.openCollapsible();
69+
this.props.onOpening();
70+
} else {
71+
this.closeCollapsible();
72+
this.props.onClosing();
73+
}
74+
}
75+
}
76+
77+
componentWillUnmount() {
78+
window.clearTimeout(this.timeout);
79+
}
80+
81+
closeCollapsible() {
82+
this.setState({
83+
shouldSwitchAutoOnNextCycle: true,
84+
height: this.innerRef.scrollHeight,
85+
transition: `height ${
86+
this.props.transitionCloseTime
87+
? this.props.transitionCloseTime
88+
: this.props.transitionTime
89+
}ms ${this.props.easing}`,
90+
inTransition: true,
91+
});
92+
}
93+
94+
openCollapsible() {
95+
this.setState({
96+
inTransition: true,
97+
shouldOpenOnNextCycle: true,
98+
});
99+
}
100+
101+
continueOpenCollapsible() {
102+
this.setState({
103+
height: this.innerRef.scrollHeight,
104+
transition: `height ${this.props.transitionTime}ms ${this.props.easing}`,
105+
isClosed: false,
106+
hasBeenOpened: true,
107+
inTransition: true,
108+
shouldOpenOnNextCycle: false,
109+
});
110+
}
111+
112+
handleTriggerClick(event) {
113+
if (this.props.triggerDisabled || this.state.inTransition) {
114+
return;
115+
}
116+
117+
event.preventDefault();
118+
119+
if (this.props.handleTriggerClick) {
120+
this.props.handleTriggerClick(this.props.accordionPosition);
121+
} else {
122+
if (this.state.isClosed === true) {
123+
this.openCollapsible();
124+
this.props.onOpening();
125+
this.props.onTriggerOpening();
126+
} else {
127+
this.closeCollapsible();
128+
this.props.onClosing();
129+
this.props.onTriggerClosing();
130+
}
131+
}
132+
}
133+
134+
renderNonClickableTriggerElement() {
135+
if (
136+
this.props.triggerSibling &&
137+
typeof this.props.triggerSibling === 'string'
138+
) {
139+
return (
140+
<span className={`${this.props.classParentString}__trigger-sibling`}>
141+
{this.props.triggerSibling}
142+
</span>
143+
);
144+
} else if (
145+
this.props.triggerSibling &&
146+
typeof this.props.triggerSibling === 'function'
147+
) {
148+
return this.props.triggerSibling();
149+
} else if (this.props.triggerSibling) {
150+
return <this.props.triggerSibling />;
151+
}
152+
153+
return null;
154+
}
155+
156+
handleTransitionEnd(e) {
157+
// only handle transitions that origin from the container of this component
158+
if (e.target !== this.innerRef) {
159+
return;
160+
}
161+
// Switch to height auto to make the container responsive
162+
if (!this.state.isClosed) {
163+
this.setState({
164+
height: 'auto',
165+
overflow: this.props.overflowWhenOpen,
166+
inTransition: false,
167+
});
168+
this.props.onOpen();
169+
} else {
170+
this.setState({ inTransition: false });
171+
this.props.onClose();
172+
}
173+
}
174+
175+
setInnerRef(ref) {
176+
this.innerRef = ref;
177+
}
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 triggerClassString = `${
213+
this.props.classParentString
214+
}__trigger ${openClass} ${disabledClass} ${
215+
this.state.isClosed
216+
? this.props.triggerClassName
217+
: this.props.triggerOpenedClassName
218+
}`;
219+
const parentClassString = `${this.props.classParentString} ${
220+
this.state.isClosed ? this.props.className : this.props.openedClassName
221+
}`;
222+
const outerClassString = `${this.props.classParentString}__contentOuter ${this.props.contentOuterClassName}`;
223+
const innerClassString = `${this.props.classParentString}__contentInner ${this.props.contentInnerClassName}`;
224+
225+
return (
226+
<ContentContainerElement
227+
className={parentClassString.trim()}
228+
{...this.props.containerElementProps}
229+
>
230+
<TriggerElement
231+
className={triggerClassString.trim()}
232+
onClick={this.handleTriggerClick}
233+
style={this.props.triggerStyle && this.props.triggerStyle}
234+
onKeyPress={(event) => {
235+
const { key } = event;
236+
if (
237+
(key === ' ' &&
238+
this.props.triggerTagName.toLowerCase() !== 'button') ||
239+
key === 'Enter'
240+
) {
241+
this.handleTriggerClick(event);
242+
}
243+
}}
244+
tabIndex={this.props.tabIndex && this.props.tabIndex}
245+
{...this.props.triggerElementProps}
246+
>
247+
<div class="collapsible-header">
248+
{trigger}
249+
<IoIosArrowDown
250+
style={{
251+
display: this.state.isClosed ? 'none' : 'block',
252+
}}
253+
/>
254+
<IoIosArrowForward
255+
style={{
256+
display: this.state.isClosed ? 'block' : 'none',
257+
}}
258+
/>
259+
</div>
260+
</TriggerElement>
261+
262+
{this.renderNonClickableTriggerElement()}
263+
264+
<div
265+
className={outerClassString.trim()}
266+
style={dropdownStyle}
267+
onTransitionEnd={this.handleTransitionEnd}
268+
ref={this.setInnerRef}
269+
>
270+
<div className={innerClassString.trim()}>{children}</div>
271+
</div>
272+
</ContentContainerElement>
273+
);
274+
}
275+
}
276+
277+
Collapsible.propTypes = {
278+
transitionTime: PropTypes.number,
279+
transitionCloseTime: PropTypes.number,
280+
triggerTagName: PropTypes.string,
281+
easing: PropTypes.string,
282+
open: PropTypes.bool,
283+
containerElementProps: PropTypes.object,
284+
triggerElementProps: PropTypes.object,
285+
classParentString: PropTypes.string,
286+
openedClassName: PropTypes.string,
287+
triggerStyle: PropTypes.object,
288+
triggerClassName: PropTypes.string,
289+
triggerOpenedClassName: PropTypes.string,
290+
contentOuterClassName: PropTypes.string,
291+
contentInnerClassName: PropTypes.string,
292+
accordionPosition: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
293+
handleTriggerClick: PropTypes.func,
294+
onOpen: PropTypes.func,
295+
onClose: PropTypes.func,
296+
onOpening: PropTypes.func,
297+
onClosing: PropTypes.func,
298+
onTriggerOpening: PropTypes.func,
299+
onTriggerClosing: PropTypes.func,
300+
trigger: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
301+
triggerWhenOpen: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
302+
triggerDisabled: PropTypes.bool,
303+
lazyRender: PropTypes.bool,
304+
overflowWhenOpen: PropTypes.oneOf([
305+
'hidden',
306+
'visible',
307+
'auto',
308+
'scroll',
309+
'inherit',
310+
'initial',
311+
'unset',
312+
]),
313+
triggerSibling: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
314+
tabIndex: PropTypes.number,
315+
contentContainerTagName: PropTypes.string,
316+
};
317+
318+
Collapsible.defaultProps = {
319+
transitionTime: 400,
320+
transitionCloseTime: null,
321+
triggerTagName: 'span',
322+
easing: 'linear',
323+
open: false,
324+
classParentString: 'Collapsible',
325+
triggerDisabled: false,
326+
lazyRender: false,
327+
overflowWhenOpen: 'hidden',
328+
openedClassName: '',
329+
triggerStyle: null,
330+
triggerClassName: '',
331+
triggerOpenedClassName: '',
332+
contentOuterClassName: '',
333+
contentInnerClassName: '',
334+
className: '',
335+
triggerSibling: null,
336+
onOpen: () => {},
337+
onClose: () => {},
338+
onOpening: () => {},
339+
onClosing: () => {},
340+
onTriggerOpening: () => {},
341+
onTriggerClosing: () => {},
342+
tabIndex: null,
343+
contentContainerTagName: 'div',
344+
};
345+
346+
export default Collapsible;

0 commit comments

Comments
 (0)