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
|
||||
*/
|
||||
|
||||
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 type { DTVariant } from '@dangerousthings/tokens';
|
||||
import { cx } from '../utils/cx';
|
||||
@@ -40,6 +40,7 @@ export function DTMobileFilterOverlay({
|
||||
}: DTMobileFilterOverlayProps) {
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const triggerRef = useRef<HTMLElement | null>(null);
|
||||
const headingId = useId();
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
@@ -105,8 +106,18 @@ export function DTMobileFilterOverlay({
|
||||
|
||||
return createPortal(
|
||||
<div className={cx('dt-filter-overlay', getVariantClass(variant), className)} style={style}>
|
||||
<div className="dt-filter-overlay-backdrop" onClick={onDismiss} />
|
||||
<div ref={panelRef} className="dt-filter-overlay-content">
|
||||
<div
|
||||
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 */}
|
||||
<div
|
||||
style={{
|
||||
@@ -116,7 +127,7 @@ export function DTMobileFilterOverlay({
|
||||
padding: '16px',
|
||||
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}
|
||||
{activeFilterCount !== undefined && activeFilterCount > 0 && (
|
||||
<span
|
||||
@@ -144,6 +155,7 @@ export function DTMobileFilterOverlay({
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
type="button"
|
||||
aria-label={`Close ${heading}`}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
|
||||
Reference in New Issue
Block a user