-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(expandcollapse): Re-writing expand collapse using modern TDS…
… components
- Loading branch information
1 parent
4a7d497
commit 8852dcc
Showing
14 changed files
with
879 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
}) | ||
}) |
9 changes: 9 additions & 0 deletions
9
src/components/Clickable/__tests__/__snapshots__/Clickable.spec.jsx.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
89
src/components/ExpandCollapse/PanelWrapper/PanelWrapper.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.