mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
fix(admin-bulk-actions): trap focus + label modal dialog (a11y)
Bulk-action modal had role=dialog on the overlay but no focus trap, no aria-labelledby, no scroll lock, and an ad-hoc Escape listener that conflicted with anything else listening to document keydown. Move role=dialog/aria-modal onto the focus-trapped content element, wire aria-labelledby to the h3 title, and replace the ad-hoc keydown effect with useFocusTrap (Tab cycle + Esc + body scroll lock + focus restore on close). Loading state suppresses Esc but keeps focus trapped, so Tab still cycles inside the progress view.
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
|||||||
} from '../api/adminBulkActions';
|
} from '../api/adminBulkActions';
|
||||||
import { usePlatform } from '../platform/hooks/usePlatform';
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
import { useCurrency } from '../hooks/useCurrency';
|
import { useCurrency } from '../hooks/useCurrency';
|
||||||
|
import { useFocusTrap } from '@/hooks/useFocusTrap';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
// ============ Types ============
|
// ============ Types ============
|
||||||
@@ -802,16 +803,10 @@ function ActionModal({
|
|||||||
}, [modal.open, modal.action]);
|
}, [modal.open, modal.action]);
|
||||||
|
|
||||||
// Escape key handler — only when not loading
|
// Escape key handler — only when not loading
|
||||||
useEffect(() => {
|
// Focus trap + scroll lock + Escape close (only while not loading).
|
||||||
if (!modal.open) return;
|
const dialogRef = useFocusTrap<HTMLDivElement>(modal.open, {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
onEscape: modal.loading ? undefined : onClose,
|
||||||
if (e.key === 'Escape' && !modal.loading) {
|
});
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
|
||||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
||||||
}, [modal.open, modal.loading, onClose]);
|
|
||||||
|
|
||||||
// Count active paid subscriptions among selected ones
|
// Count active paid subscriptions among selected ones
|
||||||
const activePaidCount = useMemo(() => {
|
const activePaidCount = useMemo(() => {
|
||||||
@@ -1118,8 +1113,6 @@ function ActionModal({
|
|||||||
paddingLeft: 'max(1rem, env(safe-area-inset-left))',
|
paddingLeft: 'max(1rem, env(safe-area-inset-left))',
|
||||||
paddingRight: 'max(1rem, env(safe-area-inset-right))',
|
paddingRight: 'max(1rem, env(safe-area-inset-right))',
|
||||||
}}
|
}}
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
>
|
>
|
||||||
{/* Backdrop — no close on click during loading */}
|
{/* Backdrop — no close on click during loading */}
|
||||||
<div
|
<div
|
||||||
@@ -1127,11 +1120,21 @@ function ActionModal({
|
|||||||
onClick={handleBackdropClick}
|
onClick={handleBackdropClick}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Modal */}
|
{/* Modal — role/aria-modal live on the focus-trapped element so AT
|
||||||
<div className="relative max-h-[calc(100dvh-2rem)] w-full max-w-md overflow-y-auto rounded-2xl border border-dark-700 bg-dark-900 p-6 shadow-2xl">
|
announces and Tab cycles correctly. */}
|
||||||
|
<div
|
||||||
|
ref={dialogRef}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="bulk-action-modal-title"
|
||||||
|
tabIndex={-1}
|
||||||
|
className="relative max-h-[calc(100dvh-2rem)] w-full max-w-md overflow-y-auto rounded-2xl border border-dark-700 bg-dark-900 p-6 shadow-2xl"
|
||||||
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-5 flex items-center justify-between">
|
<div className="mb-5 flex items-center justify-between">
|
||||||
<h3 className="text-lg font-bold text-dark-100">{t(actionLabelKeys[modal.action])}</h3>
|
<h3 id="bulk-action-modal-title" className="text-lg font-bold text-dark-100">
|
||||||
|
{t(actionLabelKeys[modal.action])}
|
||||||
|
</h3>
|
||||||
{!modal.loading && (
|
{!modal.loading && (
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
|||||||
Reference in New Issue
Block a user