diff --git a/README.md b/README.md index a4001c7..481e320 100644 --- a/README.md +++ b/README.md @@ -196,9 +196,9 @@ You can override these as you see fit and as your setup allows. This component was built with focus accessibility best-practices at top-of-mind, and provides enough flexibility to allow you to create an accessible modal window. -### Focus loop +### Focus trap -There are hidden elements at the start and end of the modal component, which, on focus, shift the user's focus to either the end or start of the content, respectively. +The modal uses the native `` element with the `showModal()` method, which automatically traps focus within the dialog. This ensures keyboard navigation stays within the modal while it is open. ### Focus on close diff --git a/package-lock.json b/package-lock.json index baa2a9f..d011b85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@wethegit/react-modal", - "version": "3.1.0", + "version": "3.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@wethegit/react-modal", - "version": "3.1.0", + "version": "3.2.0", "license": "MIT", "dependencies": { "@changesets/changelog-github": "~0.5.0", diff --git a/src/lib/components/modal-focus-bounds/index.tsx b/src/lib/components/modal-focus-bounds/index.tsx deleted file mode 100644 index e5f7b60..0000000 --- a/src/lib/components/modal-focus-bounds/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ModalFocusBounds } from "./modal-focus-bounds" diff --git a/src/lib/components/modal-focus-bounds/modal-focus-bounds.module.scss b/src/lib/components/modal-focus-bounds/modal-focus-bounds.module.scss deleted file mode 100644 index 8327c0a..0000000 --- a/src/lib/components/modal-focus-bounds/modal-focus-bounds.module.scss +++ /dev/null @@ -1,5 +0,0 @@ -.modalFocusBounds { - // This applies to the hidden element(s) that help manage user focus. - box-shadow: none; - outline: none; -} diff --git a/src/lib/components/modal-focus-bounds/modal-focus-bounds.tsx b/src/lib/components/modal-focus-bounds/modal-focus-bounds.tsx deleted file mode 100644 index 660dac0..0000000 --- a/src/lib/components/modal-focus-bounds/modal-focus-bounds.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { forwardRef } from "react" - -import styles from "./modal-focus-bounds.module.scss" - -export const ModalFocusBounds = forwardRef(function ModalFocusBounds( - _, - ref -) { - return
-}) diff --git a/src/lib/components/modal-inner/modal-inner.module.scss b/src/lib/components/modal-inner/modal-inner.module.scss index 2864462..94e207f 100644 --- a/src/lib/components/modal-inner/modal-inner.module.scss +++ b/src/lib/components/modal-inner/modal-inner.module.scss @@ -1,8 +1,12 @@ .modalInner { align-items: center; + background: transparent; + border: none; display: flex; - inset: 0; + height: 100%; justify-content: center; + max-height: 100%; + max-width: 100%; + padding: 0; width: 100%; - z-index: 1; } diff --git a/src/lib/components/modal-inner/modal-inner.tsx b/src/lib/components/modal-inner/modal-inner.tsx index ae3e826..75bc78a 100644 --- a/src/lib/components/modal-inner/modal-inner.tsx +++ b/src/lib/components/modal-inner/modal-inner.tsx @@ -2,12 +2,9 @@ import { useEffect, useRef } from "react" import { classnames } from "../../../utils/classnames" -import { ModalVisuallyHidden } from "../modal-visually-hidden" -import { ModalFocusBounds } from "../modal-focus-bounds" - import styles from "./modal-inner.module.scss" -export interface ModalInnerProps extends React.HTMLAttributes { +export interface ModalInnerProps extends React.HTMLAttributes { /** * The content of the modal. */ @@ -16,45 +13,33 @@ export interface ModalInnerProps extends React.HTMLAttributes { * The className of the modal. */ className?: string + /** + * Called when the cancel event fires (e.g. user presses Escape). + * The default browser close behaviour is always prevented so React state stays in control. + */ + onCancel?: React.ReactEventHandler } -export function ModalInner({ children, className, ...props }: ModalInnerProps) { - const modalRef = useRef(null) - const firstFocusableElement = useRef(null) - const lastFocusableElement = useRef(null) - - const focusStartingPosition = () => { - const element = firstFocusableElement.current - if (element) element.focus() - } - - const focusEndingPosition = () => { - const element = lastFocusableElement.current - if (element) element.focus() - } +export function ModalInner({ children, className, onCancel, ...props }: ModalInnerProps) { + const dialogRef = useRef(null) useEffect(() => { - focusStartingPosition() + dialogRef.current?.showModal() }, []) return ( -
{ + // Prevent the browser from closing the dialog so our React state + // remains in control. The useModal hook handles closing via Escape. + e.preventDefault() + onCancel?.(e) + }} > - - - - {children} - - - - -
+
) } diff --git a/src/lib/components/modal-visually-hidden/index.tsx b/src/lib/components/modal-visually-hidden/index.tsx deleted file mode 100644 index 2f2e5a0..0000000 --- a/src/lib/components/modal-visually-hidden/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ModalVisuallyHidden } from "./modal-visually-hidden" diff --git a/src/lib/components/modal-visually-hidden/modal-visually-hidden.module.scss b/src/lib/components/modal-visually-hidden/modal-visually-hidden.module.scss deleted file mode 100644 index 5205dac..0000000 --- a/src/lib/components/modal-visually-hidden/modal-visually-hidden.module.scss +++ /dev/null @@ -1,10 +0,0 @@ -.modalVisuallyHidden { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; -} diff --git a/src/lib/components/modal-visually-hidden/modal-visually-hidden.tsx b/src/lib/components/modal-visually-hidden/modal-visually-hidden.tsx deleted file mode 100644 index 0edce2b..0000000 --- a/src/lib/components/modal-visually-hidden/modal-visually-hidden.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { forwardRef } from "react" - -import styles from "./modal-visually-hidden.module.scss" - -export interface ModalVisuallyHiddenProps { - onFocus?: () => void -} - -export const ModalVisuallyHidden = forwardRef( - function ModalVisuallyHidden(props: ModalVisuallyHiddenProps, ref) { - return ( -
- ) - } -) diff --git a/src/lib/components/modal/modal.module.scss b/src/lib/components/modal/modal.module.scss deleted file mode 100644 index 5c39867..0000000 --- a/src/lib/components/modal/modal.module.scss +++ /dev/null @@ -1,7 +0,0 @@ -.ModalFixed { - position: fixed; -} - -.ModalAbsolute { - position: absolute; -} diff --git a/src/lib/components/modal/modal.tsx b/src/lib/components/modal/modal.tsx index 1d978c5..5169701 100644 --- a/src/lib/components/modal/modal.tsx +++ b/src/lib/components/modal/modal.tsx @@ -3,9 +3,7 @@ import ReactDOM from "react-dom" import { ModalInner } from "../modal-inner" import type { ModalInnerProps } from "../modal-inner" -import { classnames } from "../../../utils/classnames" -import styles from "./modal.module.scss" export interface ModalProps extends ModalInnerProps { /** * The modal will be appended to the passed element instead of being rendered in place @@ -14,10 +12,8 @@ export interface ModalProps extends ModalInnerProps { renderTo: HTMLElement } -export function Modal({ renderTo, className, ...props }: ModalProps) { - const classes = classnames([styles.ModalFixed, className]) - - const modalContent = +export function Modal({ renderTo, ...props }: ModalProps) { + const modalContent = if (renderTo) { return ReactDOM.createPortal(modalContent, renderTo)