From b081ea290bdc8c96494f4cdf58f43040308a1537 Mon Sep 17 00:00:00 2001 From: Jared White Date: Tue, 7 May 2024 11:52:25 -0700 Subject: [PATCH] feat: various ARIA enhancements for Overlay (Dialog & Drawer) (#85) * feat: various ARIA enhancements for Overlay (Dialog & Drawer) * test: resolve focus trap issue --- src/global/text.scss | 4 ++ src/overlays/Dialog.tsx | 28 ++++------ src/overlays/Drawer.tsx | 15 ++---- src/overlays/Overlay.scss | 15 ++++++ src/overlays/Overlay.tsx | 57 +++++++++++++++------ src/overlays/__mocks__/tabbable.js | 14 +++++ src/overlays/__stories__/Dialog.stories.tsx | 6 +-- src/overlays/__stories__/Drawer.stories.tsx | 6 +-- 8 files changed, 95 insertions(+), 50 deletions(-) create mode 100644 src/overlays/__mocks__/tabbable.js diff --git a/src/global/text.scss b/src/global/text.scss index 0ee35c1..82ca007 100644 --- a/src/global/text.scss +++ b/src/global/text.scss @@ -12,6 +12,10 @@ .text-heading-xs { margin-block: 0; color: var(--seeds-text-color-darker); + + &:focus-visible { + outline: none; + } } .text-heading-4xl, diff --git a/src/overlays/Dialog.tsx b/src/overlays/Dialog.tsx index 9f23807..f91c75a 100644 --- a/src/overlays/Dialog.tsx +++ b/src/overlays/Dialog.tsx @@ -1,6 +1,14 @@ import React, { useEffect, useRef } from "react" -import Overlay, { OverlayContent, OverlayContentProps, OverlayFooter, OverlayFooterProps, OverlayHeader, OverlayHeaderProps } from "./Overlay" +import Overlay, { + OverlayProps, + OverlayContent, + OverlayContentProps, + OverlayFooter, + OverlayFooterProps, + OverlayHeader, + OverlayHeaderProps, +} from "./Overlay" import "./Dialog.scss" @@ -25,17 +33,7 @@ const DialogFooter = (props: OverlayFooterProps) => { return } -export interface DialogProps { - children: React.ReactNode - /** Additional class name */ - className?: string - /** If this dialog is open */ - isOpen: boolean - /** Function to call when hitting ESC or clicking background overlay */ - onClose: () => void - /** Additional class name for the Overlay */ - overlayClassName?: string -} +export interface DialogProps extends OverlayProps {} const Dialog = (props: DialogProps) => { if (!props.isOpen) return null @@ -44,11 +42,7 @@ const Dialog = (props: DialogProps) => { if (props.className) classNames.push(props.className) return ( - + {props.children} ) diff --git a/src/overlays/Drawer.tsx b/src/overlays/Drawer.tsx index 8663b81..37e458d 100644 --- a/src/overlays/Drawer.tsx +++ b/src/overlays/Drawer.tsx @@ -7,6 +7,7 @@ import Overlay, { OverlayContentProps, OverlayHeader, OverlayHeaderProps, + OverlayProps, } from "./Overlay" import "./Drawer.scss" @@ -37,18 +38,9 @@ const DrawerFooter = (props: OverlayFooterProps) => { return } -export interface DrawerProps { - children: React.ReactNode - /** Additional class name */ - className?: string - /** If this Drawer is open */ - isOpen: boolean - /** Function to call when hitting ESC or clicking background overlay */ - onClose: () => void +export interface DrawerProps extends OverlayProps { /** If this Drawer renders nested above another Drawer */ nested?: boolean - /** Additional class name for the Overlay */ - overlayClassName?: string } const Drawer = (props: DrawerProps) => { @@ -60,9 +52,8 @@ const Drawer = (props: DrawerProps) => { return ( {props.children} diff --git a/src/overlays/Overlay.scss b/src/overlays/Overlay.scss index 77d6e71..aea3192 100644 --- a/src/overlays/Overlay.scss +++ b/src/overlays/Overlay.scss @@ -75,12 +75,27 @@ justify-content: space-between; } + &:not(.has-close-button-end) { + justify-content: start; + flex-direction: row-reverse; + } + > button { padding: 0; background-color: transparent; background-image: none; border: none; + border-radius: var(--seeds-rounded); cursor: pointer; + + &:focus { + outline: none; + } + + &:focus-visible { + outline: var(--seeds-focus-ring-outline); + box-shadow: var(--seeds-focus-ring-box-shadow); + } } > h1 { diff --git a/src/overlays/Overlay.tsx b/src/overlays/Overlay.tsx index 6b19c09..8b5e572 100644 --- a/src/overlays/Overlay.tsx +++ b/src/overlays/Overlay.tsx @@ -8,13 +8,14 @@ import usePortal from "../hooks/usePortal" import "./Overlay.scss" - export interface OverlayHeaderProps { children: React.ReactNode /** Place the close button at the inline end of the header */ closeButtonEnd?: boolean /** Additional class name */ className?: string + /** Element ID */ + id?: string } const OverlayHeader = (props: OverlayHeaderProps) => { @@ -23,24 +24,30 @@ const OverlayHeader = (props: OverlayHeaderProps) => { if (props.className) classNames.push(props.className) const headerRef = useRef(null) - // Focus on the heading on render so that it is read by screen readers - useEffect(() => { - headerRef.current?.querySelector(".seeds-overlay-heading")?.focus() - }, []) const closeButton = ( - ) return (
- {!props.closeButtonEnd && closeButton} - + {props.children} - {props.closeButtonEnd && closeButton} + {closeButton}
) } @@ -49,13 +56,19 @@ export interface OverlayContentProps { children: React.ReactNode /** Additional class name */ className?: string + /** Element ID */ + id?: string } const OverlayContent = (props: OverlayContentProps) => { const classNames = ["seeds-overlay-content"] if (props.className) classNames.push(props.className) - return
{props.children}
+ return ( +
+ {props.children} +
+ ) } export interface OverlayFooterProps { @@ -71,14 +84,20 @@ const OverlayFooter = (props: OverlayFooterProps) => { return
{props.children}
} - export interface OverlayProps { children: React.ReactNode - /** Function to call when clicking the close icon */ + /** Function to call when hitting ESC or clicking background overlay */ onClose: () => void + /** If this Overlay is open */ + isOpen?: boolean + /** Additional class name for the Overlay */ overlayClassName?: string /** Additional class name */ className?: string + /** An ID of a heading element that titles the Overlay */ + ariaLabelledBy?: string + /** An ID for content content */ + ariaDescribedBy?: string } const Overlay = (props: OverlayProps) => { @@ -101,10 +120,18 @@ const Overlay = (props: OverlayProps) => { -