Dialog: Esc nur fuer den obersten Dialog, Schliessen ohne Ereignis, Fokus zurueck auch nach autoFocus
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6eb5ab3679
commit
39dfb386ac
2 files changed
+20
-6
No files matched your search
@@ -2,10 +2,14 @@ import { useEffect, useId, useRef } from 'react'
|
||||
import { useDialogFocus } from '../../hooks/useDialogFocus'
|
||||
import { shouldCloseOnKey } from '../../lib/dialogKeys'
|
||||
|
||||
// Alle gerade offenen Dialoge, der zuletzt geöffnete steht hinten: nur er reagiert auf Esc (sonst schlösse ein Esc auch die darunter liegenden)
|
||||
const openDialogs = []
|
||||
|
||||
/**
|
||||
* Dialog: weiße Box auf abgedunkeltem Hintergrund, auf dem Handy als Bottom-Sheet (Regeln in global.css, .overlay/.dialog).
|
||||
* Schließen geht nur über × und Esc, ein Klick auf den Hintergrund tut nichts (lange Eingaben sollen nicht verloren gehen).
|
||||
* Solange `busy` gilt, wirken × und Esc nicht. Esc greift nur, wenn kein innerer Baustein die Taste schon behandelt hat
|
||||
* Solange `busy` gilt, wirken × und Esc nicht. Liegen mehrere Dialoge übereinander, schließt Esc nur den obersten.
|
||||
* Esc greift nur, wenn kein innerer Baustein die Taste schon behandelt hat
|
||||
* (z. B. schließt eine offene Vorschlagsliste zuerst sich selbst): React 18 verteilt seine Ereignisse am Wurzelelement, also vor
|
||||
* dem Listener auf `document`, und ein dortiges preventDefault() ist hier als `defaultPrevented` sichtbar.
|
||||
* Mit `as="form"` ist die Box ein <form onSubmit>; die native Pflichtfeld-Prüfung bleibt aktiv (kein noValidate).
|
||||
@@ -23,12 +27,18 @@ export default function Dialog({ title, onClose, busy = false, wide = false, as
|
||||
latest.current = { busy, onClose }
|
||||
})
|
||||
useEffect(() => {
|
||||
openDialogs.push(latest)
|
||||
const onKeyDown = (e) => {
|
||||
if (openDialogs[openDialogs.length - 1] !== latest) return
|
||||
const { busy: isBusy, onClose: close } = latest.current
|
||||
if (shouldCloseOnKey(e, isBusy)) close()
|
||||
}
|
||||
document.addEventListener('keydown', onKeyDown)
|
||||
return () => document.removeEventListener('keydown', onKeyDown)
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeyDown)
|
||||
const index = openDialogs.indexOf(latest)
|
||||
if (index !== -1) openDialogs.splice(index, 1)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
@@ -44,7 +54,7 @@ export default function Dialog({ title, onClose, busy = false, wide = false, as
|
||||
>
|
||||
<div className="dialog-head">
|
||||
<h3 id={titleId}>{title}</h3>
|
||||
<button type="button" className="dialog-close" aria-label="Schließen" disabled={busy} onClick={onClose}>
|
||||
<button type="button" className="dialog-close" aria-label="Schließen" disabled={busy} onClick={() => onClose()}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
|
||||
const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
||||
|
||||
/**
|
||||
* Fokus für ein Dialog-Overlay: beim Öffnen springt der Fokus in den Dialog (`dialogRef`), beim Schließen zurück zum
|
||||
* Element, das ihn vorher hatte (falls es noch da ist). Der zurückgegebene onKeyDown gehört an das Overlay: Tab und
|
||||
* Element, das ihn vorher hatte (falls es noch da ist). Das Element wird beim ersten Render festgehalten, noch bevor
|
||||
* ein autoFocus im Dialog den Fokus nimmt. Der zurückgegebene onKeyDown gehört an das Overlay: Tab und
|
||||
* Umschalt+Tab bleiben im Dialog (sonst landet man hinter dem Overlay in der Seite).
|
||||
*/
|
||||
export function useDialogFocus(dialogRef) {
|
||||
const openerRef = useRef(undefined)
|
||||
if (openerRef.current === undefined) openerRef.current = document.activeElement
|
||||
|
||||
useEffect(() => {
|
||||
const opener = document.activeElement
|
||||
dialogRef.current?.focus()
|
||||
return () => {
|
||||
const opener = openerRef.current
|
||||
if (opener && opener.isConnected && typeof opener.focus === 'function') opener.focus()
|
||||
}
|
||||
}, [dialogRef])
|
||||
|
||||
Reference in new issue
Block a user