Skip to content

Commit

Permalink
refactor(expandcollapse): Re-writing expand collapse using modern TDS…
Browse files Browse the repository at this point in the history
… components
  • Loading branch information
ryanoglesby08 committed Oct 27, 2017
1 parent 4a7d497 commit 8852dcc
Show file tree
Hide file tree
Showing 14 changed files with 879 additions and 7 deletions.
18 changes: 13 additions & 5 deletions config/styleguide.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const path = require('path')
const {version} = require('../package.json')
const { version } = require('../package.json')

const styleguidistEnv = process.env.STYLEGUIDIST_ENV || 'dev' // dev, staging, production

Expand Down Expand Up @@ -130,10 +130,18 @@ module.exports = {
{
name: 'Expand collapse',
components() {
return [
path.resolve('src/old-components/ExpandCollapse/Group.jsx'),
path.resolve('src/old-components/ExpandCollapse/Panel.jsx'),
]
return compact([
toggleByEnv(
'ExpandCollapse',
path.resolve('src/components/ExpandCollapse/ExpandCollapse.jsx'),
path.resolve('src/old-components/ExpandCollapse/Group.jsx')
),
toggleByEnv(
'ExpandCollapse',
undefined,
path.resolve('src/old-components/ExpandCollapse/Panel.jsx')
),
])
},
},
{
Expand Down
4 changes: 2 additions & 2 deletions scripts/scaffolding/Component.spec.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from 'react'
import { shallow, render } from 'enzyme'
import { shallow } from 'enzyme'

import $COMPONENT$ from '../$COMPONENT$'

describe('$COMPONENT$', () => {
const doShallow = (props = {}) => shallow(<$COMPONENT$ {...props} />)

it('renders', () => {
const $COMPONENT_CAMEL$ = render(<$COMPONENT$ />)
const $COMPONENT_CAMEL$ = doShallow()

expect($COMPONENT_CAMEL$).toMatchSnapshot()
})
Expand Down
24 changes: 24 additions & 0 deletions src/components/Clickable/Clickable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'
import PropTypes from 'prop-types'

import joinClassNames from '../../utils/joinClassNames'
import safeRest from '../../utils/safeRest'

import styles from './Clickable.modules.scss'

const Clickable = ({ dangerouslyAddClassName, children, ...rest }) => (
<button {...safeRest(rest)} className={joinClassNames(styles.clickable, dangerouslyAddClassName)}>
{children}
</button>
)

Clickable.propTypes = {
dangerouslyAddClassName: PropTypes.string,
children: PropTypes.node.isRequired,
}

Clickable.defaultProps = {
dangerouslyAddClassName: undefined,
}

export default Clickable
13 changes: 13 additions & 0 deletions src/components/Clickable/Clickable.modules.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.clickable {
composes: noSpacing from '../Spacing.modules.scss';
composes: none from '../Borders.modules.scss';

// Reset the text color because Safari tries to set its own color for buttons
composes: color from '../Typography/Text/Text.modules.scss';

appearance: none;
background: none;
box-shadow: none;

cursor: pointer;
}
37 changes: 37 additions & 0 deletions src/components/Clickable/__tests__/Clickable.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react'
import { shallow } from 'enzyme'

import Clickable from '../Clickable'

describe('Clickable', () => {
const doShallow = (props = {}) => shallow(<Clickable {...props}>Some content</Clickable>)

it('renders', () => {
const clickable = doShallow()

expect(clickable).toMatchSnapshot()
})

it('will add additional arbitrary class names', () => {
const clickable = doShallow({ dangerouslyAddClassName: 'a-class' })

expect(clickable).toHaveClassName('a-class')
})

it('passes additional attributes to the button', () => {
const clickable = doShallow({ id: 'the-id', 'data-some-attr': 'some value' })

expect(clickable).toHaveProp('id', 'the-id')
expect(clickable).toHaveProp('data-some-attr', 'some value')
})

it('does not allow custom CSS', () => {
const clickable = doShallow({
className: 'my-custom-class',
style: { color: 'hotpink' },
})

expect(clickable).not.toHaveProp('className', 'my-custom-class')
expect(clickable).not.toHaveProp('style')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Clickable renders 1`] = `
<button
className="clickable"
>
Some content
</button>
`;
97 changes: 97 additions & 0 deletions src/components/ExpandCollapse/ExpandCollapse.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React from 'react'
import PropTypes from 'prop-types'
import { childrenOfType } from 'airbnb-prop-types'
import Set from 'core-js/es6/set'

import safeRest from '../../utils/safeRest'

import PanelWrapper from './PanelWrapper/PanelWrapper'
import Panel from './Panel/Panel'

// TODO: Write some tests for this just to be sure...

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
const setDifference = (start, compare) => {
const difference = new Set(start)

compare.forEach(element => difference.delete(element))

return difference
}

const areSetsEqual = (a, b) => {
const difference1 = setDifference(a, b)
const difference2 = setDifference(b, a)

return difference1.size === 0 && difference2.size === 0
}

class ExpandCollapse extends React.Component {
constructor(props) {
super(props)

this.state = {
panels: new Set(props.open),
}
}

componentWillReceiveProps(nextProps) {
const nextPanels = new Set(nextProps.open)

if (!areSetsEqual(this.state.panels, nextPanels)) {
this.setState({ panels: nextPanels })
}
}

togglePanel = panelId => {
this.setState(({ panels }) => {
const nextPanels = new Set(panels)

if (nextPanels.has(panelId)) {
nextPanels.delete(panelId)
} else {
nextPanels.add(panelId)
}

return { panels: nextPanels }
})
}

render() {
const { children, ...rest } = this.props

return (
<div {...safeRest(rest)}>
{React.Children.map(children, (panel, index) => {
const { id, header, onToggle } = panel.props

return (
<PanelWrapper
panelId={id}
panelHeader={header}
panelOnToggle={onToggle}
open={this.state.panels.has(id)}
last={index === React.Children.count(children) - 1}
onClick={() => this.togglePanel(id)}
>
{panel}
</PanelWrapper>
)
})}
</div>
)
}
}

ExpandCollapse.propTypes = {
open: PropTypes.array,
children: childrenOfType(Panel).isRequired,
}

ExpandCollapse.defaultProps = {
open: [],
}

ExpandCollapse.Panel = Panel

export default ExpandCollapse
10 changes: 10 additions & 0 deletions src/components/ExpandCollapse/ExpandCollapse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
```
<ExpandCollapse open={["panel-1"]}>
<ExpandCollapse.Panel id="panel-1" header="First panel title">
First panel
</ExpandCollapse.Panel>
<ExpandCollapse.Panel id="panel-2" header="Second panel title">
Second panel
</ExpandCollapse.Panel>
</ExpandCollapse>
```
18 changes: 18 additions & 0 deletions src/components/ExpandCollapse/Panel/Panel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import PropTypes from 'prop-types'

// TODO: I think this breaks in React 15 w/out a wrapper?

const Panel = ({ children }) => children

Panel.propTypes = {
id: PropTypes.string.isRequired,
header: PropTypes.string.isRequired,
onToggle: PropTypes.func,
children: PropTypes.node.isRequired,
}

Panel.defaultProps = {
onToggle: undefined
}

export default Panel
16 changes: 16 additions & 0 deletions src/components/ExpandCollapse/Panel/Panel.modules.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.header {
width: 100%;
text-align: left;
}

//.open {
// .content {
// display: block;
// }
//}
//
//.closed {
// .content {
// display: none;
// }
//}
89 changes: 89 additions & 0 deletions src/components/ExpandCollapse/PanelWrapper/PanelWrapper.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react'
import PropTypes from 'prop-types'
import { childrenOfType } from 'airbnb-prop-types'

import Box from '../../Box/Box'
import Flexbox from '../../Flexbox/Flexbox'
import Clickable from '../../Clickable/Clickable'
import DecorativeIcon from '../../Icons/DecorativeIcon/DecorativeIcon'
import Text from '../../Typography/Text/Text'
import HairlineDivider from '../../Dividers/HairlineDivider/HairlineDivider'
import DimpleDivider from '../../Dividers/DimpleDivider/DimpleDivider'
import Slide from './Slide'
import Panel from '../Panel/Panel'

import styles from '../Panel/Panel.modules.scss'

class PanelWrapper extends React.Component {
constructor(props) {
super(props)

this.state = {
open: props.open,
}
}

componentWillReceiveProps(nextProps) {
const { panelOnToggle } = this.props

if (this.state.open !== nextProps.open) {
this.setState({ open: nextProps.open })

if (panelOnToggle) {
panelOnToggle(nextProps.open)
}
}
}

render() {
const { panelId, panelHeader, last, onClick, children } = this.props

return (
<div data-testid={panelId}>
<HairlineDivider />

<Clickable onClick={onClick} dangerouslyAddClassName={styles.header}>
<Box spacing="padding" vertical={3}>
<Flexbox direction="row">
<Box spacing="margin" right={3}>
<DecorativeIcon symbol="caretDown" variant="primary" />
</Box>

<Text size="medium">{panelHeader}</Text>
</Flexbox>
</Box>
</Clickable>

<Slide timeout={500} in={this.state.open}>
{() => (
<div>
<DimpleDivider />

<Box spacing="padding" vertical={3}>
<Text block>{children}</Text>
</Box>
</div>
)}
</Slide>

{last && <HairlineDivider />}
</div>
)
}
}
PanelWrapper.propTypes = {
panelId: PropTypes.string.isRequired,
panelHeader: PropTypes.string.isRequired,
panelOnToggle: PropTypes.func,
open: PropTypes.bool,
last: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
children: childrenOfType(Panel).isRequired,
}

PanelWrapper.defaultProps = {
panelOnToggle: undefined,
open: false,
}

export default PanelWrapper
Loading

0 comments on commit 8852dcc

Please sign in to comment.