fix(react): a11y attrs on DTMobileFilterOverlay
Add the screen-reader semantics that were missing:
- role="dialog" + aria-modal="true" on the content panel
- aria-labelledby pointing at the heading span (id via useId)
- aria-label="Close {heading}" on the ✕ button (was just text)
- aria-hidden="true" on the click-target backdrop
Without these, screen readers can't identify the mobile filter panel
as a dialog, can't announce its label, and the close button reads as
just "X". useId so multiple overlay instances on a page don't collide
on heading IDs.
Surfaced during dt-shopify pre-launch §10 verification — two
MobileFilterMenu.test.tsx assertions were dropped to make the suite
green pending this fix; storefront can put them back now.
Revert: git revert <THIS_COMMIT_SHA>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
* .dt-filter-overlay-content
|
* .dt-filter-overlay-content
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useCallback, useRef, type ReactNode, type CSSProperties } from 'react';
|
import { useEffect, useCallback, useRef, useId, type ReactNode, type CSSProperties } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import type { DTVariant } from '@dangerousthings/tokens';
|
import type { DTVariant } from '@dangerousthings/tokens';
|
||||||
import { cx } from '../utils/cx';
|
import { cx } from '../utils/cx';
|
||||||
@@ -40,6 +40,7 @@ export function DTMobileFilterOverlay({
|
|||||||
}: DTMobileFilterOverlayProps) {
|
}: DTMobileFilterOverlayProps) {
|
||||||
const panelRef = useRef<HTMLDivElement>(null);
|
const panelRef = useRef<HTMLDivElement>(null);
|
||||||
const triggerRef = useRef<HTMLElement | null>(null);
|
const triggerRef = useRef<HTMLElement | null>(null);
|
||||||
|
const headingId = useId();
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
@@ -105,8 +106,18 @@ export function DTMobileFilterOverlay({
|
|||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div className={cx('dt-filter-overlay', getVariantClass(variant), className)} style={style}>
|
<div className={cx('dt-filter-overlay', getVariantClass(variant), className)} style={style}>
|
||||||
<div className="dt-filter-overlay-backdrop" onClick={onDismiss} />
|
<div
|
||||||
<div ref={panelRef} className="dt-filter-overlay-content">
|
className="dt-filter-overlay-backdrop"
|
||||||
|
onClick={onDismiss}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
ref={panelRef}
|
||||||
|
className="dt-filter-overlay-content"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby={headingId}
|
||||||
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -116,7 +127,7 @@ export function DTMobileFilterOverlay({
|
|||||||
padding: '16px',
|
padding: '16px',
|
||||||
borderBottom: '1px solid rgba(var(--color-primary-rgb), 0.2)',
|
borderBottom: '1px solid rgba(var(--color-primary-rgb), 0.2)',
|
||||||
}}>
|
}}>
|
||||||
<span style={{ fontWeight: 700, fontSize: '1.125rem', letterSpacing: '0.05em' }}>
|
<span id={headingId} style={{ fontWeight: 700, fontSize: '1.125rem', letterSpacing: '0.05em' }}>
|
||||||
{heading}
|
{heading}
|
||||||
{activeFilterCount !== undefined && activeFilterCount > 0 && (
|
{activeFilterCount !== undefined && activeFilterCount > 0 && (
|
||||||
<span
|
<span
|
||||||
@@ -144,6 +155,7 @@ export function DTMobileFilterOverlay({
|
|||||||
<button
|
<button
|
||||||
onClick={onDismiss}
|
onClick={onDismiss}
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-label={`Close ${heading}`}
|
||||||
style={{
|
style={{
|
||||||
background: 'none',
|
background: 'none',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
|||||||
Reference in New Issue
Block a user