mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Delay flash announcements briefly to avoid missing the first screen
reader announcement
This commit is contained in:
@@ -29,7 +29,7 @@
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
|
||||
import FlashController, { SUCCESS_AUTOHIDE_TIMEOUT } from './flash.controller';
|
||||
import FlashController, { LIVE_REGION_ANNOUNCEMENT_DELAY, SUCCESS_AUTOHIDE_TIMEOUT } from './flash.controller';
|
||||
import { setupStimulusTest, type StimulusTestContext } from 'core-stimulus/test-helpers';
|
||||
|
||||
interface LiveRegionTestElement extends HTMLElement {
|
||||
@@ -80,8 +80,8 @@ describe('FlashController', () => {
|
||||
await ctx.nextFrame();
|
||||
|
||||
const item = ctx.screen.getByText('Saved');
|
||||
// Keep the deferred live-region update deterministic.
|
||||
vi.runOnlyPendingTimers();
|
||||
// Keep the delayed live-region update deterministic.
|
||||
vi.advanceTimersByTime(LIVE_REGION_ANNOUNCEMENT_DELAY);
|
||||
|
||||
expect(announceSpy).toHaveBeenCalledWith('Saved', { politeness: 'polite', from: item });
|
||||
});
|
||||
@@ -94,7 +94,7 @@ describe('FlashController', () => {
|
||||
await ctx.nextFrame();
|
||||
|
||||
const item = ctx.screen.getByText('Invalid input');
|
||||
vi.runOnlyPendingTimers();
|
||||
vi.advanceTimersByTime(LIVE_REGION_ANNOUNCEMENT_DELAY);
|
||||
|
||||
expect(announceSpy).toHaveBeenCalledWith('Invalid input', { politeness: 'assertive', from: item });
|
||||
});
|
||||
@@ -106,7 +106,7 @@ describe('FlashController', () => {
|
||||
const announceSpy:LiveRegionTestElement['announce'] = vi.fn((_message:string, _options:unknown) => undefined);
|
||||
stubLiveRegionAnnouncement(announceSpy);
|
||||
await ctx.nextFrame();
|
||||
vi.runOnlyPendingTimers();
|
||||
vi.advanceTimersByTime(LIVE_REGION_ANNOUNCEMENT_DELAY);
|
||||
|
||||
expect(announceSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -119,7 +119,7 @@ describe('FlashController', () => {
|
||||
await ctx.nextFrame();
|
||||
|
||||
ctx.screen.getByText('Saved').remove();
|
||||
vi.runOnlyPendingTimers();
|
||||
vi.advanceTimersByTime(LIVE_REGION_ANNOUNCEMENT_DELAY);
|
||||
|
||||
expect(announceSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -2,6 +2,9 @@ import { ApplicationController } from 'stimulus-use';
|
||||
import { announce } from '@primer/live-region-element';
|
||||
|
||||
export const SUCCESS_AUTOHIDE_TIMEOUT = 5000;
|
||||
// Match Primer's live-region registration delay. Manual screen reader
|
||||
// testing showed the first announcement can be missed without this pause.
|
||||
export const LIVE_REGION_ANNOUNCEMENT_DELAY = 150;
|
||||
|
||||
export default class FlashController extends ApplicationController {
|
||||
static values = {
|
||||
@@ -70,14 +73,14 @@ export default class FlashController extends ApplicationController {
|
||||
// Determine politeness level: 'assertive' for errors/alerts, 'polite' for other messages
|
||||
const politeness = element.dataset.politeness === 'assertive' ? 'assertive' : 'polite';
|
||||
|
||||
// Defer announcement until after the flash has been connected to the DOM.
|
||||
// Defer announcement so screen readers can observe the live region before it changes.
|
||||
window.setTimeout(() => {
|
||||
if (!element.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
void announce(message, { politeness, from: element });
|
||||
});
|
||||
}, LIVE_REGION_ANNOUNCEMENT_DELAY);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,11 +88,7 @@ export default class FlashController extends ApplicationController {
|
||||
* Clears any existing timer without restarting it.
|
||||
*/
|
||||
private pauseAutohideTimer(element:HTMLElement) {
|
||||
const timeoutId = this.autohideTimers.get(element);
|
||||
if (timeoutId) {
|
||||
window.clearTimeout(timeoutId);
|
||||
this.autohideTimers.delete(element);
|
||||
}
|
||||
this.clearAutohideTimer(element);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user