Files
NapCatQQ/.pnpm-store/v3/files/ab/fbcb012ca7f752cb2885508046a368d7a399ad9169b726a4c98ccf9e89fbccfff4db658f2bcbec9873726e52fa0dfbb363c344f02f7246c9ed7f0d6b166e39
T
TanTan e311ca3275
Build NapCat Artifacts / Type Check & Test (push) Waiting to run
docs: 仓库链接指向本镜像仓库
2026-11-13 09:25:58 +08:00

174 lines
5.3 KiB
Plaintext

/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
type Assertiveness = 'assertive' | 'polite';
/* Inspired by https://github.com/AlmeroSteyn/react-aria-live */
const LIVEREGION_TIMEOUT_DELAY = 7000;
let liveAnnouncer: LiveAnnouncer | null = null;
type Message = string | {'aria-labelledby': string};
/**
* Announces the message using screen reader technology.
*/
export function announce(
message: Message,
assertiveness: Assertiveness = 'assertive',
timeout: number = LIVEREGION_TIMEOUT_DELAY
): void {
if (!liveAnnouncer) {
liveAnnouncer = new LiveAnnouncer();
// wait for the live announcer regions to be added to the dom, then announce
// otherwise Safari won't announce the message if it's added too quickly
// found most times less than 100ms were not consistent when announcing with Safari
// IS_REACT_ACT_ENVIRONMENT is used by React 18. Previous versions checked for the `jest` global.
// https://github.com/reactwg/react-18/discussions/102
// if we're in a test environment, announce without waiting
// @ts-ignore
if (!(typeof IS_REACT_ACT_ENVIRONMENT === 'boolean' ? IS_REACT_ACT_ENVIRONMENT : typeof jest !== 'undefined')) {
setTimeout(() => {
if (liveAnnouncer?.isAttached()) {
liveAnnouncer?.announce(message, assertiveness, timeout);
}
}, 100);
} else {
liveAnnouncer.announce(message, assertiveness, timeout);
}
} else {
liveAnnouncer.announce(message, assertiveness, timeout);
}
}
/**
* Stops all queued announcements.
*/
export function clearAnnouncer(assertiveness: Assertiveness): void {
if (liveAnnouncer) {
liveAnnouncer.clear(assertiveness);
}
}
/**
* Removes the announcer from the DOM.
*/
export function destroyAnnouncer(): void {
if (liveAnnouncer) {
liveAnnouncer.destroy();
liveAnnouncer = null;
}
}
// LiveAnnouncer is implemented using vanilla DOM, not React. That's because as of React 18
// ReactDOM.render is deprecated, and the replacement, ReactDOM.createRoot is moved into a
// subpath import `react-dom/client`. That makes it hard for us to support multiple React versions.
// As a global API, we can't use portals without introducing a breaking API change. LiveAnnouncer
// is simple enough to implement without React, so that's what we do here.
// See this discussion for more details: https://github.com/reactwg/react-18/discussions/125#discussioncomment-2382638
class LiveAnnouncer {
node: HTMLElement | null = null;
assertiveLog: HTMLElement | null = null;
politeLog: HTMLElement | null = null;
constructor() {
if (typeof document !== 'undefined') {
this.node = document.createElement('div');
this.node.dataset.liveAnnouncer = 'true';
// copied from VisuallyHidden
Object.assign(this.node.style, {
border: 0,
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
height: '1px',
margin: '-1px',
overflow: 'hidden',
padding: 0,
position: 'absolute',
width: '1px',
whiteSpace: 'nowrap'
});
this.assertiveLog = this.createLog('assertive');
this.node.appendChild(this.assertiveLog);
this.politeLog = this.createLog('polite');
this.node.appendChild(this.politeLog);
document.body.prepend(this.node);
}
}
isAttached() {
return this.node?.isConnected;
}
createLog(ariaLive: string) {
let node = document.createElement('div');
node.setAttribute('role', 'log');
node.setAttribute('aria-live', ariaLive);
node.setAttribute('aria-relevant', 'additions');
return node;
}
destroy() {
if (!this.node) {
return;
}
document.body.removeChild(this.node);
this.node = null;
}
announce(message: Message, assertiveness = 'assertive', timeout = LIVEREGION_TIMEOUT_DELAY) {
if (!this.node) {
return;
}
let node = document.createElement('div');
if (typeof message === 'object') {
// To read an aria-labelledby, the element must have an appropriate role, such as img.
node.setAttribute('role', 'img');
node.setAttribute('aria-labelledby', message['aria-labelledby']);
} else {
node.textContent = message;
}
if (assertiveness === 'assertive') {
this.assertiveLog?.appendChild(node);
} else {
this.politeLog?.appendChild(node);
}
if (message !== '') {
setTimeout(() => {
node.remove();
}, timeout);
}
}
clear(assertiveness: Assertiveness) {
if (!this.node) {
return;
}
if ((!assertiveness || assertiveness === 'assertive') && this.assertiveLog) {
this.assertiveLog.innerHTML = '';
}
if ((!assertiveness || assertiveness === 'polite') && this.politeLog) {
this.politeLog.innerHTML = '';
}
}
}