Skip to content

Commit

Permalink
[TAXTOOLS-556] Add Help Drawer component (#299)
Browse files Browse the repository at this point in the history
* Add simplified HelpDrawer and Toggle based on App3

* Add tests

* Add aria-label to close button

* Deal with focus in render, properly nest HTML in examples

* Remove id generation/handling from toggle

* No longer dangerously set HTML in footer - allow user to pass a renderable node instead

* Add footer title and content to React example
  • Loading branch information
hannaliebl authored Jan 4, 2019
1 parent eb61878 commit 5bf3453
Show file tree
Hide file tree
Showing 11 changed files with 480 additions and 1 deletion.
76 changes: 76 additions & 0 deletions packages/core/src/components/HelpDrawer/HelpDrawer.example.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import HelpDrawer from './HelpDrawer';
import HelpDrawerToggle from './HelpDrawerToggle';
import React from 'react';
import ReactDOM from 'react-dom';

class HelpDrawerExample extends React.PureComponent {
constructor(props) {
super(props);

this.state = {
showHelp: false
};
}

handleDrawerClose() {
this.setState({ showHelp: false });
}

handleDrawerOpen() {
this.setState({ showHelp: true });
}

render() {
return (
<div>
<p>
<strong>Click the link below to open the help drawer.</strong>
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat.
</p>
<HelpDrawerToggle
helpDrawerOpen={this.state.showHelp}
showDrawer={() => this.handleDrawerOpen()}
>
Toggle the help drawer.
</HelpDrawerToggle>
{this.state.showHelp && (
<HelpDrawer
footerTitle="Footer Title"
footerBody={
<p className="ds-text ds-u-margin--0">Footer content</p>
}
title="Help Drawer Title"
onCloseClick={() => this.handleDrawerClose()}
>
<strong>An Explanation</strong>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat
nulla pariatur. Excepteur sint occaecat cupidatat non proident,
sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
</HelpDrawer>
)}
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.
</p>
</div>
);
}
}

ReactDOM.render(<HelpDrawerExample />, document.getElementById('js-example'));
75 changes: 75 additions & 0 deletions packages/core/src/components/HelpDrawer/HelpDrawer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import Button from '../Button/Button';
import PropTypes from 'prop-types';
import React from 'react';

export class HelpDrawer extends React.PureComponent {
constructor(props) {
super(props);
this.titleRef = null;
}

componentDidMount() {
if (this.titleRef) this.titleRef.focus();
}

render() {
const {
ariaLabel,
title,
children,
onCloseClick,
footerBody,
footerTitle
} = this.props;
/* eslint-disable jsx-a11y/no-noninteractive-tabindex, react/no-danger */
return (
<div className="ds-c-help-drawer">
<div className="ds-c-help-drawer__header">
{/* The nested div below might seem redundant, but we need a
* separation between our sticky header, and the flex container
* so things display as expected when the body content overflows
*/}
<div className="ds-u-fill--gray-lightest ds-u-padding--2 ds-u-display--flex ds-u-align-items--start">
<h3
ref={el => (this.titleRef = el)}
tabIndex="0"
className="ds-u-text--lead ds-u-margin-y--0 ds-u-margin-right--2"
>
{title}
</h3>
<Button
aria-label={ariaLabel}
className="ds-u-margin-left--auto"
size="small"
onClick={onCloseClick}
variation="secondary"
>
Close
</Button>
</div>
</div>
<div className="ds-c-help-drawer__body ds-u-md-font-size--small ds-u-lg-font-size--base ds-u-padding--2">
{children}
</div>
<div className="ds-c-help-drawer__footer ds-u-fill--primary-alt-lightest ds-u-md-font-size--small ds-u-lg-font-size--base ds-u-padding--2">
<h4 className="ds-text ds-u-margin--0">{footerTitle}</h4>
<div className="ds-text ds-u-margin--0">{footerBody}</div>
</div>
</div>
);
}
}

HelpDrawer.defaultProps = { ariaLabel: 'Close help drawer' };
HelpDrawer.propTypes = {
/** Helps give more context to screen readers on the button that closes the Help Drawer */
ariaLabel: PropTypes.string,
children: PropTypes.node.isRequired,
footerBody: PropTypes.node,
footerTitle: PropTypes.string,
onCloseClick: PropTypes.func.isRequired,
/** Required because the title is what gets focused on mount */
title: PropTypes.string.isRequired
};

export default HelpDrawer;
112 changes: 112 additions & 0 deletions packages/core/src/components/HelpDrawer/HelpDrawer.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
@import '@cmsgov/design-system-support/src/settings/index';

/*
Help Drawer
A help drawer provides a space for medium to long-form help
content — content that's too long or not common enough to warrant
being on the page by default.
On large screens it's fixed to the side of the screen, and on
smaller screens it overlays the entire screen.
Render the drawer below the toggle bottom that triggers it.
This way the markup remains semantically sound and screen reader friendly.
Markup:
<div>
<p><strong>Note: This is just an example of the HTML markup. See the React example for a functioning example.</strong></p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
<span class="ds-u-display--block">
<a href="javascript:void(0);">Toggle the help drawer.</a>
</span>
<div class="ds-c-help-drawer">
<div class="ds-c-help-drawer__header">
<div class="ds-u-fill--gray-lightest ds-u-padding--2 ds-u-display--flex ds-u-align-items--start">
<h3 tabindex="0" class="ds-u-text--lead ds-u-margin-y--0 ds-u-margin-right--2">Help Drawer Title</h3>
<button class="ds-c-button ds-c-button--secondary ds-c-button--small ds-u-margin-left--auto" type="button">Close</button>
</div>
</div>
<div class="ds-c-help-drawer__body ds-u-md-font-size--small ds-u-lg-font-size--base ds-u-padding--2">
<strong>An Explanation</strong>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>
<div class="ds-c-help-drawer__footer ds-u-fill--primary-alt-lightest ds-u-md-font-size--small ds-u-lg-font-size--base ds-u-padding--2">
<h4 class="ds-text ds-u-margin--0">Footer title</h4>
<p class="ds-text ds-u-margin--0">Footer content</p>
</div>
</div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>
Style guide: components.help-drawer
*/

/*
`<HelpDrawer>`
@react-component HelpDrawer
Style guide: components.help-drawer.react-help-drawer
*/

/*
`<HelpDrawerToggle>`
@hide-example
@react-component HelpDrawerToggle
Style guide: components.help-drawer.react-help-drawer-toggle
*/

@keyframes slideInHelpDrawer {
from {
opacity: 0;
transform: translate3d(200px, 0, 0);
}

to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}

.ds-c-help-drawer {
background: $color-background;
bottom: 0;
box-shadow: -2px 0 0 $border-color;
display: flex; // flex layout helps stick the footer to the bottom
flex-direction: column;
overflow: auto;
position: fixed;
right: 0;
top: 0;
width: 100%;
z-index: 10;

@media (min-width: $width-md) {
animation: slideInHelpDrawer $animation-speed-2 ease-in-out both; // slide in from the right
max-width: 33%; // this equates to 4 grid columns
}

@media (min-width: $width-xl) {
max-width: $measure-base;
}
}

.ds-c-help-drawer__header {
position: sticky;
top: 0;
}

.ds-c-help-drawer__body {
// Stretch the body so that the footer sticks to the
// bottom of the screen
flex-grow: 1;

p:first-child {
// Prevent too much space at the top of the body area
margin-top: 0;
}
}
47 changes: 47 additions & 0 deletions packages/core/src/components/HelpDrawer/HelpDrawer.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import HelpDrawer from './HelpDrawer.jsx';
import React from 'react';
import renderer from 'react-test-renderer';
import { shallow } from 'enzyme';

const defaultProps = {
footerBody: (
<div>
<p>Some footer content</p>
</div>
),
footerTitle: 'Footer title',
onCloseClick: () => {},
title: 'HelpDrawer title'
};

function renderHelpDrawer(props) {
props = Object.assign({}, defaultProps, props);
const wrapper = shallow(
<HelpDrawer {...props}>
<p>content</p>
</HelpDrawer>
);
return { props, wrapper };
}

describe('HelpDrawer', () => {
it('calls props.onCloseClick on close button click', () => {
const onCloseClick = jest.fn();
const { wrapper } = renderHelpDrawer({ onCloseClick });
const closeBtn = wrapper.find('Button');
closeBtn.simulate('click');
expect(onCloseClick).toHaveBeenCalled();
});

it('renders a snapshot', () => {
const tree = renderer
.create(
<HelpDrawer {...defaultProps}>
<p>content</p>
</HelpDrawer>
)
.toJSON();

expect(tree).toMatchSnapshot();
});
});
46 changes: 46 additions & 0 deletions packages/core/src/components/HelpDrawer/HelpDrawerToggle.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import PropTypes from 'prop-types';
import React from 'react';

/**
* A link that triggers the visibility of a help drawer
*/
export class HelpDrawerToggle extends React.PureComponent {
render() {
if (!this.props.helpDrawerOpen && this.buttonRef) {
this.buttonRef.focus();
}
const blockInlineClass = `ds-u-display--${
this.props.inline ? 'inline-block' : 'block'
}`;
/* eslint-disable jsx-a11y/anchor-is-valid */
return (
// Use a <span> since a <div> may be invalid depending where this link is nested
<span className={blockInlineClass}>
<a
href="javascript:void(0);"
className={this.props.className}
ref={el => (this.buttonRef = el)}
onClick={() => this.props.showDrawer()}
>
{this.props.children}
</a>
</span>
);
}
}

/* eslint-disable react/no-unused-prop-types */
HelpDrawerToggle.propTypes = {
/** Whether or not the Help Drawer controlled by this toggle is open or closed. This value is used to re-focus the toggle that opened the drawer when the drawer closes. */
helpDrawerOpen: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
/** Additional classes for the toggle button anchor element */
className: PropTypes.string,
/** Add display inline or block to parent span */
inline: PropTypes.bool,
/** This function is called with an id that the toggle generates. It can
be used in implementing the help drawer for keeping track of the drawer the toggle controls */
showDrawer: PropTypes.func.isRequired
};

export default HelpDrawerToggle;
Loading

0 comments on commit 5bf3453

Please sign in to comment.