Skip to content

Commit

Permalink
feat(Accordion): support panel's title as custom element Semantic-Org…
Browse files Browse the repository at this point in the history
…#1144 (Semantic-Org#1281)

* feat(Accordion): support panel's title as custom element Semantic-Org#1144

* refactor(Accordion) panels created using createShorthandFactory

* fix(Accordion) title and content custom elements not wrapped in div with class names

* docs(Accordion) add example use case for custom title component to Accordion docs

* refactor(Accordion): remove unused import

* Revert "docs(Accordion) add example use case for custom title component to Accordion docs"

This reverts commit 1b84e22.

* fix(Accordion): Accordion Title does not have dropdown for panels with string title

* docs(Accordion): added separate example for Accordion with custom title and content

* feat(Accordion): Accordion consistently renders dropdown icon for panels' titles

* docs(Accordion): Move custom title and content example to Usages

* docs(Accordion): reorganize panels shorthand

* feat(Accordion): support all child keys in panels
  • Loading branch information
rkostrzewski authored and harel committed Feb 25, 2017
1 parent c72a5eb commit 774c558
Show file tree
Hide file tree
Showing 12 changed files with 165 additions and 43 deletions.
14 changes: 1 addition & 13 deletions docs/app/Examples/modules/Accordion/Types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,14 @@ const AccordionTypesExamples = () => (
examplePath='modules/Accordion/Types/AccordionExampleStandard'
/>
<ComponentExample
title='Panels Prop'
description='Accordion panels can be define using the `panels` prop.'
description={<div>An Accordion can be defined using the <code>panels</code> prop.</div>}
examplePath='modules/Accordion/Types/AccordionExamplePanelsProp'
>
<Message info>
Panel objects can define an <code>active</code> key to open/close the panel.
{' '}They can also define an <code>onClick</code> key to be applied to the <code>Accordion.Title</code>.
</Message>
</ComponentExample>
<ComponentExample
title='Active Index'
description='The `activeIndex` prop controls which panel is open.'
examplePath='modules/Accordion/Types/AccordionExampleActiveIndex'
>
<Message info>
An <code>active</code> prop on an
{' '}<code>&lt;Accordion.Title&gt;</code> or <code>&lt;Accordion.Content&gt;</code>
{' '}will override the <code>&lt;Accordion&gt;</code> <code>&lt;activeIndex&gt;</code> prop.
</Message>
</ComponentExample>
<ComponentExample
title='Styled'
description='A styled accordion adds basic formatting.'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react'
import { Accordion, Label, Message } from 'semantic-ui-react'
import faker from 'faker'
import _ from 'lodash'

const panels = _.times(3, i => ({
key: `panel-${i}`,
title: <Label color='blue' content={faker.lorem.sentence()} />,
content: (
<Message
info
header={faker.lorem.sentence()}
content={faker.lorem.paragraph()}
/>
),
}))

const AccordionExamplePanelsPropWithCustomTitleAndContent = () => (
<Accordion panels={panels} />
)

export default AccordionExamplePanelsPropWithCustomTitleAndContent
28 changes: 28 additions & 0 deletions docs/app/Examples/modules/Accordion/Usage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react'

import ComponentExample from 'docs/app/Components/ComponentDoc/ComponentExample'
import ExampleSection from 'docs/app/Components/ComponentDoc/ExampleSection'

import { Message } from 'semantic-ui-react'

const AccordionUsageExamples = () => (
<ExampleSection title='Usage'>
<ComponentExample
title='Active Index'
description='The `activeIndex` prop controls which panel is open.'
examplePath='modules/Accordion/Usage/AccordionExampleActiveIndex'
>
<Message info>
An <code>active</code> prop on an
{' '}<code>&lt;Accordion.Title&gt;</code> or <code>&lt;Accordion.Content&gt;</code>
{' '}will override the <code>&lt;Accordion&gt;</code> <code>&lt;activeIndex&gt;</code> prop.
</Message>
</ComponentExample>
<ComponentExample
title='Panels Prop with custom title and content'
examplePath='modules/Accordion/Usage/AccordionExamplePanelsPropWithCustomTitleAndContent'
/>
</ExampleSection>
)

export default AccordionUsageExamples
2 changes: 2 additions & 0 deletions docs/app/Examples/modules/Accordion/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React from 'react'
import Types from './Types'
import Variations from './Variations'
import Usage from './Usage'

const AccordionExamples = () => (
<div>
<Types />
<Variations />
<Usage />
</div>
)

Expand Down
27 changes: 13 additions & 14 deletions src/modules/Accordion/Accordion.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import cx from 'classnames'
import _ from 'lodash'
import cx from 'classnames'
import React, { Children, cloneElement, PropTypes } from 'react'

import {
Expand All @@ -9,7 +9,7 @@ import {
META,
useKeyOnly,
} from '../../lib'
import Icon from '../../elements/Icon'

import AccordionContent from './AccordionContent'
import AccordionTitle from './AccordionTitle'

Expand Down Expand Up @@ -54,14 +54,16 @@ export default class Accordion extends Component {
/**
* Create simple accordion panels from an array of { text: <string>, content: <custom> } objects.
* Object can optionally define an `active` key to open/close the panel.
* Object can opitonally define a `key` key used for title and content nodes' keys.
* Mutually exclusive with children.
* TODO: AccordionPanel should be a sub-component
*/
panels: customPropTypes.every([
customPropTypes.disallow(['children']),
PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string,
active: PropTypes.bool,
title: PropTypes.string,
title: customPropTypes.contentShorthand,
content: customPropTypes.contentShorthand,
onClick: PropTypes.func,
})),
Expand Down Expand Up @@ -158,17 +160,14 @@ export default class Accordion extends Component {
if (panel.onClick) panel.onClick(e, i)
}

children.push(
<AccordionTitle key={`${panel.title}-title`} active={isActive} onClick={onClick}>
<Icon name='dropdown' />
{panel.title}
</AccordionTitle>
)
children.push(
<AccordionContent key={`${panel.title}-content`} active={isActive}>
{panel.content}
</AccordionContent>
)
// implement all methods of creating a key that are supported in factories
const key = panel.key
|| _.isFunction(panel.childKey) && panel.childKey(panel)
|| panel.childKey && panel.childKey
|| panel.title

children.push(AccordionTitle.create({ active: isActive, onClick, key: `${key}-title`, content: panel.title }))
children.push(AccordionContent.create({ active: isActive, key: `${key}-content`, content: panel.content }))
})

return children
Expand Down
17 changes: 14 additions & 3 deletions src/modules/Accordion/AccordionContent.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import cx from 'classnames'
import _ from 'lodash'
import React, { PropTypes } from 'react'
import cx from 'classnames'

import {
customPropTypes,
getElementType,
getUnhandledProps,
META,
useKeyOnly,
createShorthandFactory,
} from '../../lib'

/**
* A content sub-component for Accordion component.
*/
function AccordionContent(props) {
const { active, children, className } = props
const { active, children, className, content } = props
const classes = cx(
'content',
useKeyOnly(active, 'active'),
Expand All @@ -22,7 +24,11 @@ function AccordionContent(props) {
const rest = getUnhandledProps(AccordionContent, props)
const ElementType = getElementType(AccordionContent, props)

return <ElementType {...rest} className={classes}>{children}</ElementType>
return (
<ElementType {...rest} className={classes}>
{_.isNil(children) ? content : children}
</ElementType>
)
}

AccordionContent.propTypes = {
Expand All @@ -37,6 +43,9 @@ AccordionContent.propTypes = {

/** Additional classes. */
className: PropTypes.string,

/** Shorthand for primary content. */
content: customPropTypes.contentShorthand,
}

AccordionContent._meta = {
Expand All @@ -45,4 +54,6 @@ AccordionContent._meta = {
parent: 'Accordion',
}

AccordionContent.create = createShorthandFactory(AccordionContent, content => ({ content }))

export default AccordionContent
25 changes: 24 additions & 1 deletion src/modules/Accordion/AccordionTitle.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import _ from 'lodash'
import cx from 'classnames'
import React, { Component, PropTypes } from 'react'

Expand All @@ -7,8 +8,11 @@ import {
getUnhandledProps,
META,
useKeyOnly,
createShorthandFactory,
} from '../../lib'

import Icon from '../../elements/Icon'

/**
* A title sub-component for Accordion component.
*/
Expand All @@ -26,6 +30,9 @@ export default class AccordionTitle extends Component {
/** Additional classes. */
className: PropTypes.string,

/** Shorthand for primary content. */
content: customPropTypes.contentShorthand,

/**
* Called on click.
*
Expand All @@ -52,6 +59,7 @@ export default class AccordionTitle extends Component {
active,
children,
className,
content,
} = this.props

const classes = cx(
Expand All @@ -62,6 +70,21 @@ export default class AccordionTitle extends Component {
const rest = getUnhandledProps(AccordionTitle, this.props)
const ElementType = getElementType(AccordionTitle, this.props)

return <ElementType {...rest} className={classes} onClick={this.handleClick}>{children}</ElementType>
if (_.isNil(content)) {
return (
<ElementType {...rest} className={classes} onClick={this.handleClick}>
{children}
</ElementType>
)
}

return (
<ElementType {...rest} className={classes} onClick={this.handleClick}>
<Icon name='dropdown' />
{content}
</ElementType>
)
}
}

AccordionTitle.create = createShorthandFactory(AccordionTitle, content => ({ content }))
3 changes: 2 additions & 1 deletion src/modules/Accordion/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ interface AccordionProps {
onTitleClick?: (event: React.MouseEvent<HTMLDivElement>, index: number | number[]) => void;

/**
* Create simple accordion panels from an array of { text: <string>, content: <custom> } objects.
* Create simple accordion panels from an array of { text: <custom>, content: <custom> } objects.
* Object can optionally define an `active` key to open/close the panel.
* Object can opitonally define a `key` key used for title and content nodes' keys.
* Mutually exclusive with children.
*/
panels?: Array<any>;
Expand Down
68 changes: 57 additions & 11 deletions test/specs/modules/Accordion/Accordion-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,9 @@ describe('Accordion', () => {
.should.not.have.descendants('#do-not-find-me')
})

it('adds text and content sibling children', () => {
it('adds text title and text content sibling children', () => {
const panels = [{
text: faker.lorem.sentence(),
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
}]
const wrapper = mount(<Accordion panels={panels} />)
Expand All @@ -254,22 +254,40 @@ describe('Accordion', () => {
.should.have.className('title')
.and.contain.text(panels[0].title)

expect(wrapper.childAt(0).key()).to.equal(`${panels[0].title}-title`)

wrapper
.childAt(1)
.should.have.className('content')
.and.contain.text(panels[0].content)

expect(wrapper.childAt(1).key()).to.equal(`${panels[0].title}-content`)
})

it('allows setting the active prop', () => {
it('adds custom element title and custom element content sibling children', () => {
const panels = [{
active: true,
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
}, {
active: false,
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
key: 'panel-1',
title: (<h1>{faker.lorem.sentence()}</h1>),
content: (<h2>{faker.lorem.paragraph()}</h2>),
}]
const wrapper = mount(<Accordion panels={panels} />)

wrapper
.childAt(0)
.should.have.className('title')
.and.contain(panels[0].title)

expect(wrapper.childAt(0).key()).to.equal('panel-1-title')

wrapper
.childAt(1)
.should.have.className('content')
.and.contain(panels[0].content)

expect(wrapper.childAt(1).key()).to.equal('panel-1-content')
})

const checkIfAllowsSettingTheActiveProp = panels => {
const wrapper = shallow(<Accordion panels={panels} />)

// first panel (active)
Expand All @@ -295,12 +313,40 @@ describe('Accordion', () => {
.find('AccordionContent')
.at(1)
.should.have.prop('active', false)
}

it('allows setting the active prop', () => {
const panels = [{
active: true,
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
}, {
active: false,
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
}]

checkIfAllowsSettingTheActiveProp(panels)
})

it('allows setting the active prop for custom title and content', () => {
const panels = [{
active: true,
title: (<div>faker.lorem.sentence()</div>),
content: (<p>faker.lorem.paragraph()</p>),
}, {
active: false,
title: (<h1>faker.lorem.sentence()</h1>),
content: (<h3>faker.lorem.paragraph()</h3>),
}]

checkIfAllowsSettingTheActiveProp(panels)
})

describe('onClick', () => {
it('can be omitted', () => {
const panels = [{
text: faker.lorem.sentence(),
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
}]
const wrapper = mount(<Accordion panels={panels} />)
Expand Down
1 change: 1 addition & 0 deletions test/specs/modules/Accordion/AccordionContent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ describe('AccordionContent', () => {
common.isConformant(AccordionContent)
common.rendersChildren(AccordionContent)
common.propKeyOnlyToClassName(AccordionContent, 'active')
common.implementsCreateMethod(AccordionContent)
})
1 change: 1 addition & 0 deletions test/specs/modules/Accordion/AccordionTitle-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ describe('AccordionTitle', () => {
common.isConformant(AccordionTitle)
common.rendersChildren(AccordionTitle)
common.propKeyOnlyToClassName(AccordionTitle, 'active')
common.implementsCreateMethod(AccordionTitle)
})

0 comments on commit 774c558

Please sign in to comment.