Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<dialog>` 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

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion src/lib/components/modal-focus-bounds/index.tsx

This file was deleted.

This file was deleted.

10 changes: 0 additions & 10 deletions src/lib/components/modal-focus-bounds/modal-focus-bounds.tsx

This file was deleted.

8 changes: 6 additions & 2 deletions src/lib/components/modal-inner/modal-inner.module.scss
Original file line number Diff line number Diff line change
@@ -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;
}
51 changes: 18 additions & 33 deletions src/lib/components/modal-inner/modal-inner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement> {
export interface ModalInnerProps extends React.HTMLAttributes<HTMLDialogElement> {
/**
* The content of the modal.
*/
Expand All @@ -16,45 +13,33 @@ export interface ModalInnerProps extends React.HTMLAttributes<HTMLDivElement> {
* 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<HTMLDialogElement>
}

export function ModalInner({ children, className, ...props }: ModalInnerProps) {
const modalRef = useRef<HTMLDivElement>(null)
const firstFocusableElement = useRef<HTMLDivElement>(null)
const lastFocusableElement = useRef<HTMLDivElement>(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<HTMLDialogElement>(null)

useEffect(() => {
focusStartingPosition()
dialogRef.current?.showModal()
}, [])

return (
<div
<dialog
className={classnames([styles.modalInner, className])}
ref={modalRef}
role="dialog"
aria-modal="true"
tabIndex={0}
ref={dialogRef}
{...props}
onCancel={(e) => {
// 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)
}}
>
<ModalVisuallyHidden onFocus={focusEndingPosition} />

<ModalFocusBounds ref={firstFocusableElement} />

{children}

<ModalFocusBounds ref={lastFocusableElement} />

<ModalVisuallyHidden onFocus={focusStartingPosition} />
</div>
</dialog>
)
}
1 change: 0 additions & 1 deletion src/lib/components/modal-visually-hidden/index.tsx

This file was deleted.

This file was deleted.

15 changes: 0 additions & 15 deletions src/lib/components/modal-visually-hidden/modal-visually-hidden.tsx

This file was deleted.

7 changes: 0 additions & 7 deletions src/lib/components/modal/modal.module.scss

This file was deleted.

8 changes: 2 additions & 6 deletions src/lib/components/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = <ModalInner className={classes} {...props} />
export function Modal({ renderTo, ...props }: ModalProps) {
const modalContent = <ModalInner {...props} />

if (renderTo) {
return ReactDOM.createPortal(modalContent, renderTo)
Expand Down
Loading