Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | 8x 8x 8x 8x 39x 39x 39x 28x 1x 1x 1x 1x 39x | import React, { ReactNode } from 'react';
import styles from './dialog.module.css';
import { clx } from '@/helpers';
type Props = {
dialogRef?: React.RefObject<HTMLDivElement | null>;
className?: string;
open?: boolean;
withPadding?: boolean;
modalMode?: boolean;
header?: ReactNode;
body?: ReactNode;
footer?: ReactNode;
large?: boolean;
/** Up to 768px wide, a `modalMode` window sizes to its content and centres, instead of filling the screen. */
fitContent?: boolean;
/** Accessible name for the window. Falls back to labelling by the header's content. */
label?: string;
/** Called on Escape. Omit for a window that has no way to close. */
onClose?: () => void;
};
/**
* @example
* <Dialog
* dialogRef={dialogRef}
* open={open}
* withPadding={withPadding}
* modalMode={modalMode}
* header={<h1>Header</h1>}
* body={<p>Body</p>}
* footer={<button>Footer</button>}
* large={large}
* />;
*
* @param {React.RefObject<HTMLDivElement>} dialogRef - Ref to the dialog element
* @param {boolean} open - If true, the dialog will be open
* @param {boolean} withPadding - If true, the dialog will have padding
* @param {boolean} modalMode - If true, the dialog will be not complete screen
* @param {ReactNode} header - The header of the dialog
* @param {ReactNode} body - The body of the dialog
* @param {ReactNode} footer - The footer of the dialog
* @param {boolean} large - If true, the dialog will be large
* @param {boolean} fitContent - If true, a small-screen modal sizes to its content
* @param {string} label - Accessible name for the window
* @param {Function} onClose - Invoked when Escape is pressed
* @returns {JSX.Element}
*/
const Dialog = (props: Props) => {
const {
dialogRef,
className,
open,
large,
fitContent,
withPadding,
modalMode,
header = <></>,
body = <></>,
footer = <></>,
label,
onClose,
} = props;
const headerId = React.useId();
// Escape closes, when there is something to close. A window a keyboard user cannot dismiss is a
// trap, and this primitive backs every page on the site.
React.useEffect(() => {
if (!open || !onClose) return undefined;
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') onClose();
};
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [open, onClose]);
return (
/**
* SDD-L06. This was a plain `<div>`: no role, no accessible name, no focus management, no
* Escape — on all eight pages of the site. A screen-reader user was never told a window had
* opened or what it was called.
*
* `aria-modal` only in `modalMode`. The macOS-window premise means most of these are *not*
* modal — the Dock and menu bar stay usable behind them — and claiming otherwise would tell
* assistive tech the rest of the page is inert when it is not. Focus is likewise not trapped,
* for the same reason.
*
* `inert` when closed is the important half, and it is what the audit could not settle
* without a runtime check: `dialog.module.css` hides a closed window with
* `transform: scale(0)`, which is purely visual. Its buttons and links stayed in the tab order
* and in the accessibility tree, so a keyboard user could tab into an invisible window and a
* screen reader would read out windows that were not on screen. `inert` removes both while
* leaving the scale transition intact, which `display: none` would not.
*/
<div
ref={dialogRef}
data-testid="dialog"
role="dialog"
aria-modal={modalMode ? true : undefined}
aria-label={label}
aria-labelledby={label ? undefined : headerId}
inert={!open}
className={clx(
className,
styles.dialog,
open ? styles.open : '',
withPadding ? styles.padding : '',
modalMode ? styles.modalMode : '',
large ? styles.large : '',
fitContent ? styles.fitContent : ''
)}
>
<header data-testid="dialog-header" id={headerId}>
{header}
</header>
{/* SDD-L05: was <main>. Layout already wraps children in one, so every page shipped
two landmarks — invalid, and it breaks the "skip to main content" idiom because a
screen-reader user gets two ambiguous "main" regions. On the blog the real article
sat inside the inner one. */}
<div className={styles.body} data-testid="dialog-body">
{body}
</div>
<footer data-testid="dialog-footer">{footer}</footer>
</div>
);
};
export default Dialog;
|