Skip to content

Commit c7f5dc8

Browse files
authored
feat(Accordion): ability to open multiple items (#988)
* Add ability for activeIndex in Accordions to be an array Allowing multiple open panels at once, need help with transforming activeIndex prop to an array. * feat(Accordion): ability to open multiple items * docs(Accordion): add missing period * fix(Accordion): remove redundant defaultActiveIndex logic * refactor(Accordion): consolidate active index checks * test(Accordion): fix exclusive test, use array defaultActiveIndex * test(Accordion): silence console for known prop type warning * docs(ExampleSection): add whitespace to bottom of page * test(Accordion): add inclusive tests for opening and closing panels (#1023) test(Accordion): add inclusive tests for opening and closing panels
1 parent 609847d commit c7f5dc8

File tree

5 files changed

+159
-16
lines changed

5 files changed

+159
-16
lines changed

docs/app/Components/ComponentDoc/ExampleSection.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React, { PropTypes } from 'react'
33
import { Grid, Header } from 'src'
44

55
const headerStyle = { marginBottom: '1.5em' }
6-
const sectionStyle = { background: '#fff', boxShadow: '0 2px 2px rgba(0, 0, 0, 0.1)' }
6+
const sectionStyle = { background: '#fff', boxShadow: '0 2px 2px rgba(0, 0, 0, 0.1)', paddingBottom: '5em' }
77

88
const ExampleSection = ({ title, children, ...rest }) => (
99
<Grid padded style={sectionStyle} {...rest}>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import _ from 'lodash'
2+
import faker from 'faker'
3+
import React from 'react'
4+
import { Accordion } from 'semantic-ui-react'
5+
6+
const panels = _.times(3, () => ({
7+
title: faker.lorem.sentence(),
8+
content: faker.lorem.paragraphs(),
9+
}))
10+
11+
const AccordionExampleExclusive = () => (
12+
<Accordion panels={panels} exclusive={false} fluid />
13+
)
14+
15+
export default AccordionExampleExclusive

docs/app/Examples/modules/Accordion/Variations/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ const AccordionTypesExamples = () => (
1414
description='An accordion can be formatted to appear on dark backgrounds.'
1515
examplePath='modules/Accordion/Variations/AccordionExampleInverted'
1616
/>
17+
<ComponentExample
18+
title='Exclusive'
19+
description='An accordion can have multiple panels open at the same time.'
20+
examplePath='modules/Accordion/Variations/AccordionExampleExclusive'
21+
/>
1722
</ExampleSection>
1823
)
1924

src/modules/Accordion/Accordion.js

+34-14
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import AccordionTitle from './AccordionTitle'
1818
* An accordion allows users to toggle the display of sections of content
1919
*/
2020
export default class Accordion extends Component {
21+
static defaultProps = {
22+
exclusive: true,
23+
}
24+
2125
static autoControlledProps = [
2226
'activeIndex',
2327
]
@@ -27,7 +31,10 @@ export default class Accordion extends Component {
2731
as: customPropTypes.as,
2832

2933
/** Index of the currently active panel. */
30-
activeIndex: PropTypes.number,
34+
activeIndex: PropTypes.oneOfType([
35+
PropTypes.number,
36+
PropTypes.arrayOf(PropTypes.number),
37+
]),
3138

3239
/** Primary content. */
3340
children: PropTypes.node,
@@ -36,7 +43,13 @@ export default class Accordion extends Component {
3643
className: PropTypes.string,
3744

3845
/** Initial activeIndex value. */
39-
defaultActiveIndex: PropTypes.number,
46+
defaultActiveIndex: PropTypes.oneOfType([
47+
PropTypes.number,
48+
PropTypes.arrayOf(PropTypes.number),
49+
]),
50+
51+
/** Only allow one panel open at a time */
52+
exclusive: PropTypes.bool,
4053

4154
/** Format to take up the width of it's container. */
4255
fluid: PropTypes.bool,
@@ -84,23 +97,33 @@ export default class Accordion extends Component {
8497
// The default prop should always win on first render.
8598
// This default check should then be removed.
8699
if (typeof this.props.defaultActiveIndex === 'undefined') {
87-
this.trySetState({ activeIndex: -1 })
100+
this.trySetState({ activeIndex: this.props.exclusive ? -1 : [-1] })
88101
}
89102
}
90103

91104
handleTitleClick = (e, index) => {
92-
const { onTitleClick } = this.props
105+
const { onTitleClick, exclusive } = this.props
93106
const { activeIndex } = this.state
94107

95-
this.trySetState({
96-
activeIndex: index === activeIndex ? -1 : index,
97-
})
108+
let newIndex
109+
if (exclusive) {
110+
newIndex = index === activeIndex ? -1 : index
111+
} else {
112+
// check to see if index is in array, and remove it, if not then add it
113+
newIndex = _.includes(activeIndex, index) ? _.without(activeIndex, index) : [...activeIndex, index]
114+
}
115+
this.trySetState({ activeIndex: newIndex })
98116
if (onTitleClick) onTitleClick(e, index)
99117
}
100118

119+
isIndexActive = (index) => {
120+
const { exclusive } = this.props
121+
const { activeIndex } = this.state
122+
return exclusive ? activeIndex === index : _.includes(activeIndex, index)
123+
}
124+
101125
renderChildren = () => {
102126
const { children } = this.props
103-
const { activeIndex } = this.state
104127
let titleIndex = 0
105128
let contentIndex = 0
106129

@@ -110,7 +133,7 @@ export default class Accordion extends Component {
110133

111134
if (isTitle) {
112135
const currentIndex = titleIndex
113-
const isActive = _.has(child, 'props.active') ? child.props.active : activeIndex === currentIndex
136+
const isActive = _.has(child, 'props.active') ? child.props.active : this.isIndexActive(titleIndex)
114137
const onClick = (e) => {
115138
this.handleTitleClick(e, currentIndex)
116139
if (child.props.onClick) child.props.onClick(e, currentIndex)
@@ -120,8 +143,7 @@ export default class Accordion extends Component {
120143
}
121144

122145
if (isContent) {
123-
const currentIndex = contentIndex
124-
const isActive = _.has(child, 'props.active') ? child.props.active : activeIndex === currentIndex
146+
const isActive = _.has(child, 'props.active') ? child.props.active : this.isIndexActive(contentIndex)
125147
contentIndex++
126148
return cloneElement(child, { ...child.props, active: isActive })
127149
}
@@ -132,12 +154,10 @@ export default class Accordion extends Component {
132154

133155
renderPanels = () => {
134156
const { panels } = this.props
135-
const { activeIndex } = this.state
136157
const children = []
137158

138159
_.each(panels, (panel, i) => {
139-
const isActive = _.has(panel, 'active') ? panel.active : activeIndex === i
140-
160+
const isActive = _.has(panel, 'active') ? panel.active : this.isIndexActive(i)
141161
const onClick = (e) => {
142162
this.handleTitleClick(e, i)
143163
if (panel.onClick) panel.onClick(e, i)

test/specs/modules/Accordion/Accordion-test.js

+104-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Accordion from 'src/modules/Accordion/Accordion'
55
import AccordionContent from 'src/modules/Accordion/AccordionContent'
66
import AccordionTitle from 'src/modules/Accordion/AccordionTitle'
77
import * as common from 'test/specs/commonTests'
8-
import { sandbox } from 'test/utils'
8+
import { consoleUtil, sandbox } from 'test/utils'
99

1010
describe('Accordion', () => {
1111
common.isConformant(Accordion)
@@ -100,6 +100,108 @@ describe('Accordion', () => {
100100
wrapper.childAt(4).should.have.prop('active', true)
101101
wrapper.childAt(5).should.have.prop('active', true)
102102
})
103+
104+
it('can be an array', () => {
105+
const wrapper = shallow(
106+
<Accordion exclusive={false}>
107+
<Accordion.Title />
108+
<Accordion.Content />
109+
<Accordion.Title />
110+
<Accordion.Content />
111+
<Accordion.Title />
112+
<Accordion.Content />
113+
</Accordion>
114+
)
115+
wrapper.setProps({ activeIndex: [0, 1] })
116+
wrapper.childAt(0).should.have.prop('active', true)
117+
wrapper.childAt(1).should.have.prop('active', true)
118+
wrapper.childAt(2).should.have.prop('active', true)
119+
wrapper.childAt(3).should.have.prop('active', true)
120+
121+
wrapper.setProps({ activeIndex: [1, 2] })
122+
wrapper.childAt(2).should.have.prop('active', true)
123+
wrapper.childAt(3).should.have.prop('active', true)
124+
wrapper.childAt(4).should.have.prop('active', true)
125+
wrapper.childAt(5).should.have.prop('active', true)
126+
})
127+
128+
it('can be inclusive and makes Accordion.Content at activeIndex - 1 "active"', () => {
129+
const contents = shallow(
130+
<Accordion exclusive={false} defaultActiveIndex={[0]}>
131+
<Accordion.Title />
132+
<Accordion.Content />
133+
<Accordion.Title />
134+
<Accordion.Content />
135+
</Accordion>
136+
)
137+
.find('AccordionTitle')
138+
139+
contents.at(0).should.have.prop('active', true)
140+
contents.at(1).should.have.prop('active', false)
141+
})
142+
143+
it('can be inclusive and allows multiple open', () => {
144+
const contents = shallow(
145+
<Accordion exclusive={false} defaultActiveIndex={[0, 1]}>
146+
<Accordion.Title />
147+
<Accordion.Content />
148+
<Accordion.Title />
149+
<Accordion.Content />
150+
</Accordion>
151+
)
152+
.find('AccordionTitle')
153+
154+
contents.at(0).should.have.prop('active', true)
155+
contents.at(1).should.have.prop('active', true)
156+
})
157+
158+
it('can be inclusive and can open multiple panels by clicking', () => {
159+
const wrapper = mount(
160+
<Accordion exclusive={false}>
161+
<Accordion.Title />
162+
<Accordion.Content />
163+
<Accordion.Title />
164+
<Accordion.Content />
165+
</Accordion>
166+
)
167+
const titles = wrapper.find('AccordionTitle')
168+
const contents = wrapper.find('AccordionContent')
169+
170+
titles
171+
.at(0)
172+
.simulate('click')
173+
.should.have.prop('active', true)
174+
titles
175+
.at(1)
176+
.simulate('click')
177+
.should.have.prop('active', true)
178+
contents.at(0).should.have.prop('active', true)
179+
contents.at(1).should.have.prop('active', true)
180+
})
181+
182+
it('can be inclusive and close multiple panels by clicking', () => {
183+
const wrapper = mount(
184+
<Accordion exclusive={false} defaultActiveIndex={[0, 1]}>
185+
<Accordion.Title />
186+
<Accordion.Content />
187+
<Accordion.Title />
188+
<Accordion.Content />
189+
</Accordion>
190+
)
191+
const titles = wrapper.find('AccordionTitle')
192+
const contents = wrapper.find('AccordionContent')
193+
194+
titles
195+
.at(0)
196+
.simulate('click')
197+
.should.have.prop('active', false)
198+
titles
199+
.at(1)
200+
.simulate('click')
201+
.should.have.prop('active', false)
202+
contents.at(0).should.have.prop('active', false)
203+
contents.at(1).should.have.prop('active', false)
204+
})
103205
})
104206

105207
describe('defaultActiveIndex', () => {
@@ -131,6 +233,7 @@ describe('Accordion', () => {
131233

132234
describe('panels', () => {
133235
it('does not render children', () => {
236+
consoleUtil.disableOnce()
134237
shallow(
135238
<Accordion panels={[]}>
136239
<div id='do-not-find-me' />

0 commit comments

Comments
 (0)