Skip to content

Commit

Permalink
feat(Accordion): ability to open multiple items (#988)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
levithomason authored Dec 16, 2016
1 parent 609847d commit c7f5dc8
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 16 deletions.
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
105 changes: 104 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,108 @@ 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)
})

it('can be inclusive and can open multiple panels by clicking', () => {
const wrapper = mount(
<Accordion exclusive={false}>
<Accordion.Title />
<Accordion.Content />
<Accordion.Title />
<Accordion.Content />
</Accordion>
)
const titles = wrapper.find('AccordionTitle')
const contents = wrapper.find('AccordionContent')

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

it('can be inclusive and close multiple panels by clicking', () => {
const wrapper = mount(
<Accordion exclusive={false} defaultActiveIndex={[0, 1]}>
<Accordion.Title />
<Accordion.Content />
<Accordion.Title />
<Accordion.Content />
</Accordion>
)
const titles = wrapper.find('AccordionTitle')
const contents = wrapper.find('AccordionContent')

titles
.at(0)
.simulate('click')
.should.have.prop('active', false)
titles
.at(1)
.simulate('click')
.should.have.prop('active', false)
contents.at(0).should.have.prop('active', false)
contents.at(1).should.have.prop('active', false)
})
})

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

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

0 comments on commit c7f5dc8

Please sign in to comment.