diff --git a/packages/core/src/components/HelpDrawer/HelpDrawer.example.jsx b/packages/core/src/components/HelpDrawer/HelpDrawer.example.jsx new file mode 100644 index 0000000000..3f30cb094e --- /dev/null +++ b/packages/core/src/components/HelpDrawer/HelpDrawer.example.jsx @@ -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 ( +
+

+ Click the link below to open the help drawer. +

+

+ 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. +

+ this.handleDrawerOpen()} + > + Toggle the help drawer. + + {this.state.showHelp && ( + Footer content

+ } + title="Help Drawer Title" + onCloseClick={() => this.handleDrawerClose()} + > + An Explanation +

+ 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. +

+
+ )} +

+ 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. +

+
+ ); + } +} + +ReactDOM.render(, document.getElementById('js-example')); diff --git a/packages/core/src/components/HelpDrawer/HelpDrawer.jsx b/packages/core/src/components/HelpDrawer/HelpDrawer.jsx new file mode 100644 index 0000000000..93d6b938ae --- /dev/null +++ b/packages/core/src/components/HelpDrawer/HelpDrawer.jsx @@ -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 ( +
+
+ {/* 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 + */} +
+

(this.titleRef = el)} + tabIndex="0" + className="ds-u-text--lead ds-u-margin-y--0 ds-u-margin-right--2" + > + {title} +

+ +
+
+
+ {children} +
+
+

{footerTitle}

+
{footerBody}
+
+
+ ); + } +} + +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; diff --git a/packages/core/src/components/HelpDrawer/HelpDrawer.scss b/packages/core/src/components/HelpDrawer/HelpDrawer.scss new file mode 100644 index 0000000000..dd0699c8a5 --- /dev/null +++ b/packages/core/src/components/HelpDrawer/HelpDrawer.scss @@ -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: +
+

Note: This is just an example of the HTML markup. See the React example for a functioning example.

+

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.

+ + Toggle the help drawer. + +
+
+
+

Help Drawer Title

+ +
+
+
+ An Explanation +

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.

+
+ +
+

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.

+
+ +Style guide: components.help-drawer +*/ + +/* +`` + +@react-component HelpDrawer + +Style guide: components.help-drawer.react-help-drawer +*/ + +/* +`` + +@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; + } +} diff --git a/packages/core/src/components/HelpDrawer/HelpDrawer.test.jsx b/packages/core/src/components/HelpDrawer/HelpDrawer.test.jsx new file mode 100644 index 0000000000..a43b012790 --- /dev/null +++ b/packages/core/src/components/HelpDrawer/HelpDrawer.test.jsx @@ -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: ( +
+

Some footer content

+
+ ), + footerTitle: 'Footer title', + onCloseClick: () => {}, + title: 'HelpDrawer title' +}; + +function renderHelpDrawer(props) { + props = Object.assign({}, defaultProps, props); + const wrapper = shallow( + +

content

+
+ ); + 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( + +

content

+
+ ) + .toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/packages/core/src/components/HelpDrawer/HelpDrawerToggle.jsx b/packages/core/src/components/HelpDrawer/HelpDrawerToggle.jsx new file mode 100644 index 0000000000..34258c023c --- /dev/null +++ b/packages/core/src/components/HelpDrawer/HelpDrawerToggle.jsx @@ -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 since a
may be invalid depending where this link is nested + + (this.buttonRef = el)} + onClick={() => this.props.showDrawer()} + > + {this.props.children} + + + ); + } +} + +/* 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; diff --git a/packages/core/src/components/HelpDrawer/HelpDrawerToggle.test.jsx b/packages/core/src/components/HelpDrawer/HelpDrawerToggle.test.jsx new file mode 100644 index 0000000000..7044f92e4e --- /dev/null +++ b/packages/core/src/components/HelpDrawer/HelpDrawerToggle.test.jsx @@ -0,0 +1,42 @@ +import HelpDrawerToggle from './HelpDrawerToggle.jsx'; +import React from 'react'; +import renderer from 'react-test-renderer'; +import { shallow } from 'enzyme'; + +const defaultProps = { + helpDrawerOpen: false, + inline: true, + showDrawer: () => {} +}; + +function renderHelpDrawerToggle(props) { + props = Object.assign({}, defaultProps, props); + const wrapper = shallow( + +

content

+
+ ); + return { props, wrapper }; +} + +describe('HelpDrawerToggle', () => { + it('calls props.showDrawer on link click', () => { + const showDrawer = jest.fn(); + const { wrapper } = renderHelpDrawerToggle({ showDrawer }); + const link = wrapper.find('a'); + link.simulate('click'); + expect(showDrawer).toHaveBeenCalled(); + }); + + it('renders a snapshot', () => { + const tree = renderer + .create( + +

link content

+
+ ) + .toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/packages/core/src/components/HelpDrawer/__snapshots__/HelpDrawer.test.jsx.snap b/packages/core/src/components/HelpDrawer/__snapshots__/HelpDrawer.test.jsx.snap new file mode 100644 index 0000000000..fabf8db7e3 --- /dev/null +++ b/packages/core/src/components/HelpDrawer/__snapshots__/HelpDrawer.test.jsx.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HelpDrawer renders a snapshot 1`] = ` +
+
+
+

+ HelpDrawer title +

+ +
+
+
+

+ content +

+
+
+

+ Footer title +

+
+
+

+ Some footer content +

+
+
+
+
+`; diff --git a/packages/core/src/components/HelpDrawer/__snapshots__/HelpDrawerToggle.test.jsx.snap b/packages/core/src/components/HelpDrawer/__snapshots__/HelpDrawerToggle.test.jsx.snap new file mode 100644 index 0000000000..2c0a765ffd --- /dev/null +++ b/packages/core/src/components/HelpDrawer/__snapshots__/HelpDrawerToggle.test.jsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HelpDrawerToggle renders a snapshot 1`] = ` + + +

+ link content +

+
+
+`; diff --git a/packages/core/src/components/__tests__/index.test.js b/packages/core/src/components/__tests__/index.test.js index 55d0fec88b..66580cebc1 100644 --- a/packages/core/src/components/__tests__/index.test.js +++ b/packages/core/src/components/__tests__/index.test.js @@ -34,7 +34,14 @@ function getDirectories(paths) { return paths.filter(filePath => fs.lstatSync(filePath).isDirectory()); } -const ignoredComponents = ['Mask', 'ReviewLink', 'Step', 'SubStep', 'StepLink']; +const ignoredComponents = [ + 'Mask', + 'ReviewLink', + 'Step', + 'SubStep', + 'StepLink', + 'HelpDrawerToggle' +]; describe('Components index', () => { it("exports all components except ones we don't want to expose", () => { diff --git a/packages/core/src/components/_index.scss b/packages/core/src/components/_index.scss index e27b11e6cf..5b48d70648 100644 --- a/packages/core/src/components/_index.scss +++ b/packages/core/src/components/_index.scss @@ -6,6 +6,7 @@ @import 'ChoiceList/Select'; @import 'Dialog/Dialog'; @import 'FormLabel/FormLabel'; +@import 'HelpDrawer/HelpDrawer'; @import 'List/List'; @import 'MonthPicker/MonthPicker'; @import 'Review/Review'; diff --git a/packages/core/src/components/index.js b/packages/core/src/components/index.js index ee858bd2fd..d84d0240f9 100644 --- a/packages/core/src/components/index.js +++ b/packages/core/src/components/index.js @@ -10,6 +10,7 @@ export * from './ChoiceList/Select'; export * from './DateField/DateField'; export * from './Dialog/Dialog'; export * from './FormLabel/FormLabel'; +export * from './HelpDrawer/HelpDrawer'; export * from './MonthPicker/MonthPicker'; export * from './Review/Review'; export * from './SkipNav/SkipNav';