Skip to content

Commit

Permalink
helpdrawer focus trap (#1196)
Browse files Browse the repository at this point in the history
* Implement focus trap on HelpDrawer.

* Implement ESC on close support and write documentation.

* Update TS definitions (sneaking in sticky props here).

* Rework escape handler.
  • Loading branch information
zarahzachz authored Oct 27, 2021
1 parent 30834d8 commit 92181e8
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint no-alert: 0 */
import { HelpDrawer, HelpDrawerToggle } from '@design-system';
import React from 'react';
import ReactDOM from 'react-dom';
Expand Down Expand Up @@ -35,6 +36,7 @@ class HelpDrawerExample extends React.PureComponent {

{this.state.showHelp && (
<HelpDrawer
hasFocusTrap={false}
footerTitle="Footer Title"
footerBody={<p className="ds-text ds-u-margin--0">Footer content</p>}
heading="Help Drawer Heading"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ Style guide: components.help-drawer.react-help-drawer-toggle
- `Tab` to move the focus sequentially through the list of focusable items
- `Shift + Tab` to move the focus sequentially through the list of focusable items in reversed order
#### Focus Management
The property `hasFocusTrap` is an available option that will prevent keyboard focus from leaving the confines of the HelpDrawer when enabled.
When the property `hasFocusTrap` is enabled...
- Keyboard focus is trapped within the HelpDrawer component.
- Pressing the escape key will close the HelpDrawer.
A focus trap should *not* be used when it's anticipated that the user will rely on the HelpDrawer as a reference resource for doing something outside of the HelpDrawer. For instance, if the user is completing a form, HelpDrawer may contain a reference guide defining specific requirements.
However, there are times when it makes sense to trap focus within the HelpDrawer. One occassion where it can benefit the user is when the HelpDrawer is anticipated to fill the viewport of a screen, i.e., mobile layout.
### Future Work
Future design iterations and possible user research to ensure the enhanced help drawer link design isn't confused or mistaken for transitional hyperlinks, glossary terms, or tooltips.
Expand Down
35 changes: 34 additions & 1 deletion packages/design-system/src/components/HelpDrawer/HelpDrawer.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { EVENT_CATEGORY, MAX_LENGTH, sendLinkEvent } from '../analytics/SendAnalytics';
import FocusTrap from 'focus-trap-react';
import Button from '../Button/Button';
import PropTypes from 'prop-types';
import React from 'react';
Expand All @@ -12,6 +13,7 @@ export class HelpDrawer extends React.PureComponent {
this.headingRef = null;
this.eventHeadingText = '';
this.id = this.props.headingId || uniqueId('helpDrawer_');
this.handleEscapeKey = this.handleEscapeKey.bind(this);

if (process.env.NODE_ENV !== 'production') {
if (props.title) {
Expand All @@ -28,6 +30,8 @@ export class HelpDrawer extends React.PureComponent {
}

componentDidMount() {
if (this.props.hasFocusTrap) document.addEventListener('keydown', this.handleEscapeKey);

if (this.headingRef) this.headingRef.focus();

if (helpDrawerSendsAnalytics() && this.props.analytics !== false) {
Expand Down Expand Up @@ -57,6 +61,8 @@ export class HelpDrawer extends React.PureComponent {
}

componentWillUnmount() {
document.removeEventListener('keydown', this.handleEscapeKey);

if (helpDrawerSendsAnalytics() && this.props.analytics !== false) {
/* Send analytics event for helpdrawer close */
sendLinkEvent({
Expand All @@ -70,6 +76,16 @@ export class HelpDrawer extends React.PureComponent {
}
}

handleEscapeKey(evt) {
switch (evt.code) {
case 'Escape':
this.props.onCloseClick();
break;
default:
break;
}
}

render() {
const {
ariaLabel,
Expand All @@ -78,6 +94,7 @@ export class HelpDrawer extends React.PureComponent {
children,
footerBody,
footerTitle,
hasFocusTrap,
heading,
isHeaderSticky,
isFooterSticky,
Expand All @@ -87,7 +104,7 @@ export class HelpDrawer extends React.PureComponent {
const Heading = `h${this.props.headingLevel}` || `h3`;

/* eslint-disable jsx-a11y/no-noninteractive-tabindex, react/no-danger */
return (
const helpDrawerMarkup = () => (
<div
aria-labelledby={this.id}
className={classNames(className, 'ds-c-help-drawer')}
Expand Down Expand Up @@ -126,6 +143,18 @@ export class HelpDrawer extends React.PureComponent {
</div>
</div>
);

return (
<>
{hasFocusTrap ? (
<FocusTrap focusTrapOptions={{ clickOutsideDeactivates: true }}>
{helpDrawerMarkup()}
</FocusTrap>
) : (
helpDrawerMarkup()
)}
</>
);
}
}

Expand Down Expand Up @@ -157,6 +186,10 @@ HelpDrawer.propTypes = {
className: PropTypes.string,
footerBody: PropTypes.node,
footerTitle: PropTypes.string,
/**
* Enables focus trap functionality within HelpDrawer.
*/
hasFocusTrap: PropTypes.bool,
/**
* Text for the HelpDrawer title. Required because the `heading` will be focused on mount.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ $help-drawer-toggle-space: 0;

@if $ds-include-focus-styles {
// This element is focusable via a tabindex attribute.
.ds-c-help-drawer__heading:focus {
.ds-c-help-drawer__header-heading:focus {
@include focus-styles;
}
}
Expand Down
16 changes: 16 additions & 0 deletions packages/design-system/src/types/HelpDrawer/HelpDrawer.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,30 @@ export interface HelpDrawerProps {
className?: string;
footerBody?: React.ReactNode;
footerTitle?: string;
/**
* Enables focus trap functionality within HelpDrawer.
*/
hasFocusTrap: boolean;
/**
* Text for the HelpDrawer title. Required because the `heading` will be focused on mount.
*/
heading?: React.ReactNode;
/**
* A unique `id` to be used on heading element to label multiple instances of HelpDrawer.
*/
headingId: string;
/**
* Heading type to override default `<h3>`
*/
headingLevel?: HelpDrawerHeadingLevel;
/**
* Enables "sticky" position of HelpDrawer header element.
*/
isHeaderSticky: boolean;
/**
* Enables "sticky" position of HelpDrawer footer element.
*/
isFooterSticky: boolean;
onCloseClick: (...args: any[]) => any;
/**
* @hide-prop [Deprecated] This prop has been renamed to `heading`.
Expand Down

0 comments on commit 92181e8

Please sign in to comment.