Workers P3-Endprüfung W3: Fokus kehrt nach dem Anhalten auf den Knopf der Leiste zurück
Der Dialog schloss im selben Takt, in dem pausing den Knopf der Leiste sperrte; useDialogFocus konnte den Fokus deshalb nicht zurückgeben, und er blieb für Tastatur- und Screenreader-Nutzer dauerhaft auf <body>. Das Board setzt ihn jetzt selbst, sobald pausing endet (auch nach einem Fehler), und nur dann, wenn der Fokus verloren ist. Der Prüfstand-Check (Szenario I) ist grün. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f6099938d8
commit
20f51c6584
2 files changed
+40
-2
No files matched your search
@@ -30,6 +30,8 @@ export default function WorkersBoard({ api, isAdmin, stageRef }) {
|
|||||||
const ref = stageRef ?? ownRef
|
const ref = stageRef ?? ownRef
|
||||||
const aliveRef = useRef(true)
|
const aliveRef = useRef(true)
|
||||||
const sideRef = useRef(null)
|
const sideRef = useRef(null)
|
||||||
|
const toolbarRef = useRef(null)
|
||||||
|
const refocusPauseRef = useRef(false) // nach dem Umschalten bekommt der Knopf „Alles anhalten“ / „Fortsetzen“ den Fokus zurück, falls er ihn verlor
|
||||||
const [workers, setWorkers] = useState({})
|
const [workers, setWorkers] = useState({})
|
||||||
const [selectedId, setSelectedId] = useState(null)
|
const [selectedId, setSelectedId] = useState(null)
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||||
@@ -55,6 +57,19 @@ export default function WorkersBoard({ api, isAdmin, stageRef }) {
|
|||||||
|
|
||||||
const { trace, loading: traceLoading, error: traceError } = useWorkerTrace(api, selectedId)
|
const { trace, loading: traceLoading, error: traceError } = useWorkerTrace(api, selectedId)
|
||||||
|
|
||||||
|
// Fokus nach dem Umschalten: Während pausing gilt, ist der Knopf der Leiste gesperrt. Schließt der Dialog im selben Takt, in dem pausing
|
||||||
|
// beginnt, kann useDialogFocus den Fokus nicht auf den gesperrten Knopf zurückgeben, und er bliebe für immer auf <body>. Deshalb
|
||||||
|
// wird er erst hier gesetzt, wenn pausing wieder endet (auch nach einem Fehler) und der Knopf wieder bedienbar ist. Hat der Mensch
|
||||||
|
// inzwischen woanders hin gewechselt (Fokus nicht auf <body>), bleibt es dabei.
|
||||||
|
useEffect(() => {
|
||||||
|
if (pausing || !refocusPauseRef.current) return
|
||||||
|
refocusPauseRef.current = false
|
||||||
|
const active = document.activeElement
|
||||||
|
if (active && active !== document.body) return
|
||||||
|
const button = toolbarRef.current?.querySelector('[data-wk-pause]')
|
||||||
|
if (button) button.focus()
|
||||||
|
}, [pausing])
|
||||||
|
|
||||||
// Hat inzwischen jemand anderes angehalten, entfällt die Frage; sie darf nach einem späteren Fortsetzen nicht wieder auftauchen
|
// Hat inzwischen jemand anderes angehalten, entfällt die Frage; sie darf nach einem späteren Fortsetzen nicht wieder auftauchen
|
||||||
const haltedByState = Boolean(state?.paused)
|
const haltedByState = Boolean(state?.paused)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -95,6 +110,7 @@ export default function WorkersBoard({ api, isAdmin, stageRef }) {
|
|||||||
// jemand anderes angehalten hat.
|
// jemand anderes angehalten hat.
|
||||||
const setPause = async (target) => {
|
const setPause = async (target) => {
|
||||||
if (pausing) return
|
if (pausing) return
|
||||||
|
refocusPauseRef.current = true
|
||||||
setPausing(true)
|
setPausing(true)
|
||||||
setPauseError('')
|
setPauseError('')
|
||||||
try {
|
try {
|
||||||
@@ -118,10 +134,10 @@ export default function WorkersBoard({ api, isAdmin, stageRef }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="wk-toolbar">
|
<div className="wk-toolbar" ref={toolbarRef}>
|
||||||
<span className="wk-status">{paused ? PAUSED_TEXT : RUNNING_TEXT}</span>
|
<span className="wk-status">{paused ? PAUSED_TEXT : RUNNING_TEXT}</span>
|
||||||
<span className="grow" />
|
<span className="grow" />
|
||||||
<Button type="button" onClick={requestPause} disabled={pausing}>{pauseLabel(paused)}</Button>
|
<Button type="button" onClick={requestPause} disabled={pausing} data-wk-pause>{pauseLabel(paused)}</Button>
|
||||||
{isAdmin && <Button type="button" onClick={() => setSettingsOpen(true)}>Einstellungen</Button>}
|
{isAdmin && <Button type="button" onClick={() => setSettingsOpen(true)}>Einstellungen</Button>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -126,3 +126,25 @@ test('WorkersBoard: „Alles anhalten“ öffnet erst einen Bestätigungsdialog,
|
|||||||
assert.equal((s.match(/<Button\b/g) || []).length, (s.match(/<Button\b[^>]*type="button"/g) || []).length)
|
assert.equal((s.match(/<Button\b/g) || []).length, (s.match(/<Button\b[^>]*type="button"/g) || []).length)
|
||||||
assert.doesNotMatch(s, /window\.confirm|confirm\(/)
|
assert.doesNotMatch(s, /window\.confirm|confirm\(/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Endprüfung P3 (W3): Nach „Anhalten“ schließt der Dialog im selben Takt, in dem pausing beginnt und den Knopf der Leiste sperrt;
|
||||||
|
// useDialogFocus kann den Fokus dann nicht zurückgeben (focus() auf einem gesperrten Knopf tut nichts) und er bliebe auf <body>.
|
||||||
|
// Das Board gibt ihn deshalb selbst zurück, sobald pausing endet. Das echte Verhalten prüft der Prüfstand (shot.cjs, Szenario I).
|
||||||
|
test('WorkersBoard: der Fokus kehrt nach dem Umschalten auf den Knopf der Leiste zurück, sobald er wieder bedienbar ist (W3)', () => {
|
||||||
|
const s = read('WorkersBoard.jsx')
|
||||||
|
assert.match(s, /const toolbarRef = useRef\(null\)/); assert.match(s, /const refocusPauseRef = useRef\(false\)/)
|
||||||
|
assert.match(s, /<div className="wk-toolbar" ref=\{toolbarRef\}>/)
|
||||||
|
assert.match(s, /<Button type="button" onClick=\{requestPause\} disabled=\{pausing\} data-wk-pause>/)
|
||||||
|
// die Markierung wird vor setPausing(true) gesetzt, der Fokus erst im Effekt nach dem Ende von pausing (sonst wäre der Knopf noch gesperrt)
|
||||||
|
assert.match(s, /if \(pausing\) return\s*refocusPauseRef\.current = true\s*setPausing\(true\)/)
|
||||||
|
const effect = s.match(/useEffect\(\(\) => \{\s*if \(pausing \|\| !refocusPauseRef\.current\) return([\s\S]*?)\}, \[pausing\]\)/)
|
||||||
|
assert.ok(effect, 'Effekt mit [pausing]')
|
||||||
|
assert.match(effect[1], /refocusPauseRef\.current = false/)
|
||||||
|
// nur wenn der Fokus verloren ist (auf <body> oder nirgends): wer inzwischen woanders ist, wird nicht gestört
|
||||||
|
assert.match(effect[1], /const active = document\.activeElement\s*if \(active && active !== document\.body\) return/)
|
||||||
|
assert.match(effect[1], /toolbarRef\.current\?\.querySelector\('\[data-wk-pause\]'\)/); assert.match(effect[1], /button\.focus\(\)/)
|
||||||
|
// Hook vor der frühen Rückgabe
|
||||||
|
assert.ok(s.indexOf('[data-wk-pause]') < s.indexOf('if (!loaded || !state)'))
|
||||||
|
// das Gesperrt-Sein des Knopfes bleibt (kein Doppelklick während der Anfrage)
|
||||||
|
assert.match(s, /disabled=\{pausing\}/)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user