From 92181e8b70cf7e55d17585b4eee261732e7b223d Mon Sep 17 00:00:00 2001
From: Sarah
Date: Wed, 27 Oct 2021 15:29:52 -0400
Subject: [PATCH] helpdrawer focus trap (#1196)
* Implement focus trap on HelpDrawer.
* Implement ESC on close support and write documentation.
* Update TS definitions (sneaking in sticky props here).
* Rework escape handler.
---
.../HelpDrawer/HelpDrawer.example.jsx | 2 ++
.../HelpDrawer/_HelpDrawer.docs.scss | 12 +++++++
.../src/components/HelpDrawer/HelpDrawer.jsx | 35 ++++++++++++++++++-
.../src/styles/components/_HelpDrawer.scss | 2 +-
.../src/types/HelpDrawer/HelpDrawer.d.ts | 16 +++++++++
5 files changed, 65 insertions(+), 2 deletions(-)
diff --git a/packages/design-system-docs/src/pages/components/HelpDrawer/HelpDrawer.example.jsx b/packages/design-system-docs/src/pages/components/HelpDrawer/HelpDrawer.example.jsx
index 7f3fc38648..6d040c7cd0 100644
--- a/packages/design-system-docs/src/pages/components/HelpDrawer/HelpDrawer.example.jsx
+++ b/packages/design-system-docs/src/pages/components/HelpDrawer/HelpDrawer.example.jsx
@@ -1,3 +1,4 @@
+/* eslint no-alert: 0 */
import { HelpDrawer, HelpDrawerToggle } from '@design-system';
import React from 'react';
import ReactDOM from 'react-dom';
@@ -35,6 +36,7 @@ class HelpDrawerExample extends React.PureComponent {
{this.state.showHelp && (
Footer content
}
heading="Help Drawer Heading"
diff --git a/packages/design-system-docs/src/pages/components/HelpDrawer/_HelpDrawer.docs.scss b/packages/design-system-docs/src/pages/components/HelpDrawer/_HelpDrawer.docs.scss
index 5d13bdd6c6..bc5fc02f47 100644
--- a/packages/design-system-docs/src/pages/components/HelpDrawer/_HelpDrawer.docs.scss
+++ b/packages/design-system-docs/src/pages/components/HelpDrawer/_HelpDrawer.docs.scss
@@ -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.
diff --git a/packages/design-system/src/components/HelpDrawer/HelpDrawer.jsx b/packages/design-system/src/components/HelpDrawer/HelpDrawer.jsx
index 039774f8c0..3a3fe35813 100644
--- a/packages/design-system/src/components/HelpDrawer/HelpDrawer.jsx
+++ b/packages/design-system/src/components/HelpDrawer/HelpDrawer.jsx
@@ -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';
@@ -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) {
@@ -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) {
@@ -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({
@@ -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,
@@ -78,6 +94,7 @@ export class HelpDrawer extends React.PureComponent {
children,
footerBody,
footerTitle,
+ hasFocusTrap,
heading,
isHeaderSticky,
isFooterSticky,
@@ -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 = () => (
);
+
+ return (
+ <>
+ {hasFocusTrap ? (
+
+ {helpDrawerMarkup()}
+
+ ) : (
+ helpDrawerMarkup()
+ )}
+ >
+ );
}
}
@@ -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.
*/
diff --git a/packages/design-system/src/styles/components/_HelpDrawer.scss b/packages/design-system/src/styles/components/_HelpDrawer.scss
index 7c22fbb40d..3a4692d223 100644
--- a/packages/design-system/src/styles/components/_HelpDrawer.scss
+++ b/packages/design-system/src/styles/components/_HelpDrawer.scss
@@ -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;
}
}
diff --git a/packages/design-system/src/types/HelpDrawer/HelpDrawer.d.ts b/packages/design-system/src/types/HelpDrawer/HelpDrawer.d.ts
index f72b51f5c3..a2bc64dd10 100644
--- a/packages/design-system/src/types/HelpDrawer/HelpDrawer.d.ts
+++ b/packages/design-system/src/types/HelpDrawer/HelpDrawer.d.ts
@@ -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 ``
*/
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`.