Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Accordion): ability to open multiple items #988

Merged
merged 9 commits into from
Dec 16, 2016
2 changes: 1 addition & 1 deletion docs/app/Components/ComponentDoc/ExampleSection.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { PropTypes } from 'react'
import { Grid, Header } from 'src'

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

const ExampleSection = ({ title, children, ...rest }) => (
<Grid padded style={sectionStyle} {...rest}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import _ from 'lodash'
import faker from 'faker'
import React from 'react'
import { Accordion } from 'semantic-ui-react'

const panels = _.times(3, () => ({
title: faker.lorem.sentence(),
content: faker.lorem.paragraphs(),
}))

const AccordionExampleExclusive = () => (
<Accordion panels={panels} exclusive={false} fluid />
)

export default AccordionExampleExclusive
5 changes: 5 additions & 0 deletions docs/app/Examples/modules/Accordion/Variations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ const AccordionTypesExamples = () => (
description='An accordion can be formatted to appear on dark backgrounds.'
examplePath='modules/Accordion/Variations/AccordionExampleInverted'
/>
<ComponentExample
title='Exclusive'
description='An accordion can have multiple panels open at the same time.'
examplePath='modules/Accordion/Variations/AccordionExampleExclusive'
/>
</ExampleSection>
)

Expand Down
48 changes: 34 additions & 14 deletions src/modules/Accordion/Accordion.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import AccordionTitle from './AccordionTitle'
* An accordion allows users to toggle the display of sections of content
*/
export default class Accordion extends Component {
static defaultProps = {
exclusive: true,
}

static autoControlledProps = [
'activeIndex',
]
Expand All @@ -27,7 +31,10 @@ export default class Accordion extends Component {
as: customPropTypes.as,

/** Index of the currently active panel. */
activeIndex: PropTypes.number,
activeIndex: PropTypes.oneOfType([
PropTypes.number,
PropTypes.arrayOf(PropTypes.number),
]),

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

/** Initial activeIndex value. */
defaultActiveIndex: PropTypes.number,
defaultActiveIndex: PropTypes.oneOfType([
PropTypes.number,
PropTypes.arrayOf(PropTypes.number),
]),

/** Only allow one panel open at a time */
exclusive: PropTypes.bool,

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

handleTitleClick = (e, index) => {
const { onTitleClick } = this.props
const { onTitleClick, exclusive } = this.props
const { activeIndex } = this.state

this.trySetState({
activeIndex: index === activeIndex ? -1 : index,
})
let newIndex
if (exclusive) {
newIndex = index === activeIndex ? -1 : index
} else {
// check to see if index is in array, and remove it, if not then add it
newIndex = _.includes(activeIndex, index) ? _.without(activeIndex, index) : [...activeIndex, index]
}
this.trySetState({ activeIndex: newIndex })
if (onTitleClick) onTitleClick(e, index)
}

isIndexActive = (index) => {
const { exclusive } = this.props
const { activeIndex } = this.state
return exclusive ? activeIndex === index : _.includes(activeIndex, index)
}

renderChildren = () => {
const { children } = this.props
const { activeIndex } = this.state
let titleIndex = 0
let contentIndex = 0

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

if (isTitle) {
const currentIndex = titleIndex
const isActive = _.has(child, 'props.active') ? child.props.active : activeIndex === currentIndex
const isActive = _.has(child, 'props.active') ? child.props.active : this.isIndexActive(titleIndex)
const onClick = (e) => {
this.handleTitleClick(e, currentIndex)
if (child.props.onClick) child.props.onClick(e, currentIndex)
Expand All @@ -120,8 +143,7 @@ export default class Accordion extends Component {
}

if (isContent) {
const currentIndex = contentIndex
const isActive = _.has(child, 'props.active') ? child.props.active : activeIndex === currentIndex
const isActive = _.has(child, 'props.active') ? child.props.active : this.isIndexActive(contentIndex)
contentIndex++
return cloneElement(child, { ...child.props, active: isActive })
}
Expand All @@ -132,12 +154,10 @@ export default class Accordion extends Component {

renderPanels = () => {
const { panels } = this.props
const { activeIndex } = this.state
const children = []

_.each(panels, (panel, i) => {
const isActive = _.has(panel, 'active') ? panel.active : activeIndex === i

const isActive = _.has(panel, 'active') ? panel.active : this.isIndexActive(i)
const onClick = (e) => {
this.handleTitleClick(e, i)
if (panel.onClick) panel.onClick(e, i)
Expand Down
57 changes: 56 additions & 1 deletion test/specs/modules/Accordion/Accordion-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Accordion from 'src/modules/Accordion/Accordion'
import AccordionContent from 'src/modules/Accordion/AccordionContent'
import AccordionTitle from 'src/modules/Accordion/AccordionTitle'
import * as common from 'test/specs/commonTests'
import { sandbox } from 'test/utils'
import { consoleUtil, sandbox } from 'test/utils'

describe('Accordion', () => {
common.isConformant(Accordion)
Expand Down Expand Up @@ -100,6 +100,60 @@ describe('Accordion', () => {
wrapper.childAt(4).should.have.prop('active', true)
wrapper.childAt(5).should.have.prop('active', true)
})

it('can be an array', () => {
const wrapper = shallow(
<Accordion exclusive={false}>
<Accordion.Title />
<Accordion.Content />
<Accordion.Title />
<Accordion.Content />
<Accordion.Title />
<Accordion.Content />
</Accordion>
)
wrapper.setProps({ activeIndex: [0, 1] })
wrapper.childAt(0).should.have.prop('active', true)
wrapper.childAt(1).should.have.prop('active', true)
wrapper.childAt(2).should.have.prop('active', true)
wrapper.childAt(3).should.have.prop('active', true)

wrapper.setProps({ activeIndex: [1, 2] })
wrapper.childAt(2).should.have.prop('active', true)
wrapper.childAt(3).should.have.prop('active', true)
wrapper.childAt(4).should.have.prop('active', true)
wrapper.childAt(5).should.have.prop('active', true)
})

it('can be inclusive and makes Accordion.Content at activeIndex - 1 "active"', () => {
const contents = shallow(
<Accordion exclusive={false} defaultActiveIndex={[0]}>
<Accordion.Title />
<Accordion.Content />
<Accordion.Title />
<Accordion.Content />
</Accordion>
)
.find('AccordionTitle')

contents.at(0).should.have.prop('active', true)
contents.at(1).should.have.prop('active', false)
})

it('can be inclusive and allows multiple open', () => {
const contents = shallow(
<Accordion exclusive={false} defaultActiveIndex={[0, 1]}>
<Accordion.Title />
<Accordion.Content />
<Accordion.Title />
<Accordion.Content />
</Accordion>
)
.find('AccordionTitle')

contents.at(0).should.have.prop('active', true)
contents.at(1).should.have.prop('active', true)
})
})

describe('defaultActiveIndex', () => {
Expand Down Expand Up @@ -131,6 +185,7 @@ describe('Accordion', () => {

describe('panels', () => {
it('does not render children', () => {
consoleUtil.disableOnce()
shallow(
<Accordion panels={[]}>
<div id='do-not-find-me' />
Expand Down