Skip to content

Commit

Permalink
feat(panel): implement collapsed feature (#3575)
Browse files Browse the repository at this point in the history
  • Loading branch information
francoischalifour authored Mar 12, 2019
1 parent 3de59a3 commit e84b02b
Show file tree
Hide file tree
Showing 8 changed files with 632 additions and 75 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"events": "^1.1.0",
"hogan.js": "^3.0.2",
"lodash": "^4.17.5",
"preact": "^8.2.7",
"preact": "^8.3.0",
"preact-compat": "^3.18.0",
"preact-rheostat": "^2.1.1",
"prop-types": "^15.5.10",
Expand Down
62 changes: 53 additions & 9 deletions src/components/Panel/Panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,68 @@ import cx from 'classnames';
import Template from '../Template/Template';

class Panel extends Component {
state = {
collapsed: this.props.collapsed,
controlled: false,
};

static getDerivedStateFromProps(nextProps, prevState) {
if (!prevState.controlled && nextProps.collapsed !== prevState.collapsed) {
return {
collapsed: nextProps.collapsed,
};
}

return null;
}

componentDidMount() {
this.bodyRef.appendChild(this.props.bodyElement);
}

render() {
const { cssClasses, hidden, templateProps, data } = this.props;
const { cssClasses, hidden, collapsible, templateProps, data } = this.props;

return (
<div
className={cx(cssClasses.root, {
[cssClasses.noRefinementRoot]: hidden,
[cssClasses.collapsibleRoot]: collapsible,
[cssClasses.collapsedRoot]: this.state.collapsed,
})}
hidden={hidden}
>
{templateProps.templates.header && (
<Template
{...templateProps}
templateKey="header"
rootProps={{
className: cssClasses.header,
}}
data={data}
/>
<div className={cssClasses.header}>
<Template
{...templateProps}
templateKey="header"
rootTagName="span"
data={data}
/>

{collapsible && (
<button
className={cssClasses.collapseButton}
aria-expanded={!this.state.collapsed}
onClick={event => {
event.preventDefault();

this.setState(previousState => ({
controlled: true,
collapsed: !previousState.collapsed,
}));
}}
>
<Template
{...templateProps}
templateKey="collapseButtonText"
rootTagName="span"
data={{ collapsed: this.state.collapsed }}
/>
</button>
)}
</div>
)}

<div className={cssClasses.body} ref={node => (this.bodyRef = node)} />
Expand All @@ -51,6 +90,9 @@ Panel.propTypes = {
cssClasses: PropTypes.shape({
root: PropTypes.string.isRequired,
noRefinementRoot: PropTypes.string.isRequired,
collapsibleRoot: PropTypes.string.isRequired,
collapsedRoot: PropTypes.string.isRequired,
collapseButton: PropTypes.string.isRequired,
body: PropTypes.string.isRequired,
header: PropTypes.string.isRequired,
footer: PropTypes.string.isRequired,
Expand All @@ -59,6 +101,8 @@ Panel.propTypes = {
templates: PropTypes.object.isRequired,
}).isRequired,
hidden: PropTypes.bool.isRequired,
collapsed: PropTypes.bool.isRequired,
collapsible: PropTypes.bool.isRequired,
data: PropTypes.object.isRequired,
};

Expand Down
257 changes: 223 additions & 34 deletions src/components/Panel/__tests__/Panel-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import Panel from '../Panel';
const cssClasses = {
root: 'root',
noRefinementRoot: 'noRefinementRoot',
collapsibleRoot: 'collapsibleRoot',
collapsedRoot: 'collapsedRoot',
collapseButton: 'collapseButton',
body: 'body',
header: 'header',
footer: 'footer',
Expand All @@ -14,49 +17,235 @@ const getDefaultProps = () => ({
bodyElement: document.createElement('div'),
cssClasses,
hidden: false,
collapsible: false,
collapsed: false,
data: {},
templateProps: {
templates: {
header: 'Header',
footer: 'Footer',
header: '',
footer: '',
collapseButtonText: '',
},
},
});

describe('Panel', () => {
test('should render component with default props', () => {
const props = {
...getDefaultProps(),
};

const wrapper = mount(<Panel {...props} />);

expect(wrapper.find(`.${cssClasses.root}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.noRefinementRoot}`).exists()).toBe(
false
);
expect(wrapper.find(`.${cssClasses.body}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.header}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.footer}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.header}`).text()).toBe('Header');
expect(wrapper.find(`.${cssClasses.footer}`).text()).toBe('Footer');
expect(wrapper).toMatchSnapshot();
describe('default', () => {
test('should render component with default props', () => {
const props = {
...getDefaultProps(),
};

const wrapper = mount(<Panel {...props} />);

expect(wrapper.find(`.${cssClasses.root}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.noRefinementRoot}`).exists()).toBe(
false
);
expect(wrapper.find(`.${cssClasses.collapsibleRoot}`).exists()).toBe(
false
);
expect(wrapper.find(`.${cssClasses.collapsedRoot}`).exists()).toBe(false);
expect(wrapper.find(`.${cssClasses.collapseButton}`).exists()).toBe(
false
);
expect(wrapper.find(`.${cssClasses.body}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.header}`).exists()).toBe(false);
expect(wrapper.find(`.${cssClasses.footer}`).exists()).toBe(false);

expect(wrapper).toMatchSnapshot();
});
});

describe('templates', () => {
test('should render component with custom templates', () => {
const props = {
...getDefaultProps(),
templateProps: {
templates: {
header: 'Header',
footer: 'Footer',
collapseButtonText: 'Toggle',
},
},
};

const wrapper = mount(<Panel {...props} />);

expect(wrapper.find(`.${cssClasses.root}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.noRefinementRoot}`).exists()).toBe(
false
);
expect(wrapper.find(`.${cssClasses.collapsibleRoot}`).exists()).toBe(
false
);
expect(wrapper.find(`.${cssClasses.collapsedRoot}`).exists()).toBe(false);
expect(wrapper.find(`.${cssClasses.collapseButton}`).exists()).toBe(
false
);
expect(wrapper.find(`.${cssClasses.body}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.header}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.footer}`).exists()).toBe(true);
expect(
wrapper
.find(`.${cssClasses.header} span`)
.first()
.text()
).toBe('Header');
expect(wrapper.find(`.${cssClasses.footer}`).text()).toBe('Footer');

expect(wrapper).toMatchSnapshot();
});
});

test('should render component with `hidden` prop', () => {
const props = {
...getDefaultProps(),
hidden: true,
};

const wrapper = mount(<Panel {...props} />);

expect(wrapper.find(`.${cssClasses.root}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.noRefinementRoot}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.body}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.header}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.footer}`).exists()).toBe(true);
expect(wrapper.props().hidden).toBe(true);
expect(wrapper).toMatchSnapshot();
describe('hidden', () => {
test('should render component with `hidden` prop', () => {
const props = {
...getDefaultProps(),
hidden: true,
templateProps: {
templates: {
header: 'Header',
footer: 'Footer',
collapseButtonText: 'Toggle',
},
},
};

const wrapper = mount(<Panel {...props} />);

expect(wrapper.find(`.${cssClasses.root}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.noRefinementRoot}`).exists()).toBe(
true
);
expect(wrapper.find(`.${cssClasses.collapsibleRoot}`).exists()).toBe(
false
);
expect(wrapper.find(`.${cssClasses.collapsedRoot}`).exists()).toBe(false);
expect(wrapper.find(`.${cssClasses.collapseButton}`).exists()).toBe(
false
);
expect(wrapper.find(`.${cssClasses.body}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.header}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.footer}`).exists()).toBe(true);
expect(wrapper.props().hidden).toBe(true);

expect(wrapper).toMatchSnapshot();
});
});

describe('collapsible', () => {
test('should render component with `collapsible` prop', () => {
const props = {
...getDefaultProps(),
templateProps: {
templates: {
header: 'Header',
footer: 'Footer',
collapseButtonText: ({ collapsed }) =>
collapsed ? 'More' : 'Less',
},
},
collapsible: true,
};

const wrapper = mount(<Panel {...props} />);

expect(wrapper.find(`.${cssClasses.root}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.collapsibleRoot}`).exists()).toBe(
true
);
expect(wrapper.find(`.${cssClasses.collapsedRoot}`).exists()).toBe(false);
expect(wrapper.find(`.${cssClasses.collapseButton}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.collapseButton}`).text()).toBe('Less');
expect(
wrapper.find(`.${cssClasses.collapseButton}`).prop('aria-expanded')
).toBe(true);
expect(wrapper.find(`.${cssClasses.body}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.header}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.footer}`).exists()).toBe(true);

expect(wrapper).toMatchSnapshot();
});

test('should render component with `collapsible` and `collapsed` props', () => {
const props = {
...getDefaultProps(),
collapsible: true,
collapsed: true,
templateProps: {
templates: {
header: 'Header',
footer: 'Footer',
collapseButtonText: ({ collapsed }) =>
collapsed ? 'More' : 'Less',
},
},
};

const wrapper = mount(<Panel {...props} />);

expect(wrapper.find(`.${cssClasses.root}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.collapsibleRoot}`).exists()).toBe(
true
);
expect(wrapper.find(`.${cssClasses.collapsedRoot}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.collapseButton}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.collapseButton}`).text()).toBe('More');
expect(
wrapper.find(`.${cssClasses.collapseButton}`).prop('aria-expanded')
).toBe(false);
expect(wrapper.find(`.${cssClasses.body}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.header}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.footer}`).exists()).toBe(true);

expect(wrapper).toMatchSnapshot();
});

test('should collapse on button click', () => {
const props = {
...getDefaultProps(),
collapsible: true,
collapsed: false,
templateProps: {
templates: {
header: 'Header',
footer: 'Footer',
collapseButtonText: ({ collapsed }) =>
collapsed ? 'More' : 'Less',
},
},
};

const wrapper = mount(<Panel {...props} />);
const collapseButton = wrapper.find(`.${cssClasses.collapseButton}`);

// Default state
expect(wrapper.find(`.${cssClasses.collapsedRoot}`).exists()).toBe(false);
expect(wrapper.find(`.${cssClasses.collapseButton}`).text()).toBe('Less');
expect(
wrapper.find(`.${cssClasses.collapseButton}`).prop('aria-expanded')
).toBe(true);

// Collapse the panel
collapseButton.simulate('click');

// Collapsed state
expect(wrapper.find(`.${cssClasses.collapsedRoot}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.collapseButton}`).text()).toBe('More');
expect(
wrapper.find(`.${cssClasses.collapseButton}`).prop('aria-expanded')
).toBe(false);

// Un-collapse the panel
collapseButton.simulate('click');

// Back to default state
expect(wrapper.find(`.${cssClasses.collapsedRoot}`).exists()).toBe(false);
expect(wrapper.find(`.${cssClasses.collapseButton}`).text()).toBe('Less');
expect(
wrapper.find(`.${cssClasses.collapseButton}`).prop('aria-expanded')
).toBe(true);
});
});
});
Loading

0 comments on commit e84b02b

Please sign in to comment.