125 lines
4.7 KiB
JavaScript
125 lines
4.7 KiB
JavaScript
|
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
|
||
|
import { createPriorityQueue, useTimeout } from '@fluentui/react-utilities';
|
||
|
import * as React from 'react';
|
||
|
/** The duration the message needs to be in present in DOM for screen readers to register a change and announce */ const MESSAGE_DURATION = 500;
|
||
|
const VISUALLY_HIDDEN_STYLES = {
|
||
|
clip: 'rect(0px, 0px, 0px, 0px)',
|
||
|
height: '1px',
|
||
|
margin: '-1px',
|
||
|
width: '1px',
|
||
|
position: 'absolute',
|
||
|
overflow: 'hidden',
|
||
|
textWrap: 'nowrap'
|
||
|
};
|
||
|
/* INTERNAL: implementation of the announcer using a live region element */ export const useDomAnnounce_unstable = ()=>{
|
||
|
const { targetDocument } = useFluent();
|
||
|
const timeoutRef = React.useRef(undefined);
|
||
|
const [setAnnounceTimeout, clearAnnounceTimeout] = useTimeout();
|
||
|
const elementRef = React.useRef(null);
|
||
|
const order = React.useRef(0);
|
||
|
// investigate alert implementation later
|
||
|
// const [alertList, setAlertList] = React.useState<string[]>([]);
|
||
|
const batchMessages = React.useRef([]);
|
||
|
const [messageQueue] = React.useState(()=>createPriorityQueue((a, b)=>{
|
||
|
if (a.priority !== b.priority) {
|
||
|
return b.priority - a.priority;
|
||
|
}
|
||
|
return a.createdAt - b.createdAt;
|
||
|
}));
|
||
|
const queueMessage = React.useCallback(()=>{
|
||
|
if (timeoutRef.current || !elementRef.current) {
|
||
|
return;
|
||
|
}
|
||
|
const runCycle = ()=>{
|
||
|
if (!elementRef.current) {
|
||
|
return;
|
||
|
}
|
||
|
if (targetDocument && messageQueue.peek()) {
|
||
|
// need a wrapping element for Narrator/Edge, which currently does not pick up text-only live region changes
|
||
|
// consistently
|
||
|
// if this is fixed, we can set textContent to the string directly
|
||
|
const wrappingEl = targetDocument.createElement('span');
|
||
|
wrappingEl.innerText = messageQueue.all().filter((msg)=>msg.message.trim().length > 0).reduce((prevText, currMsg)=>prevText + currMsg.message + '. ', '');
|
||
|
elementRef.current.innerText = '';
|
||
|
elementRef.current.appendChild(wrappingEl);
|
||
|
messageQueue.clear();
|
||
|
batchMessages.current = [];
|
||
|
// begin new cycle to clear (or update) messages
|
||
|
timeoutRef.current = setAnnounceTimeout(()=>{
|
||
|
runCycle();
|
||
|
}, MESSAGE_DURATION);
|
||
|
} else {
|
||
|
elementRef.current.textContent = '';
|
||
|
clearAnnounceTimeout();
|
||
|
timeoutRef.current = undefined;
|
||
|
}
|
||
|
};
|
||
|
runCycle();
|
||
|
}, [
|
||
|
clearAnnounceTimeout,
|
||
|
messageQueue,
|
||
|
setAnnounceTimeout,
|
||
|
targetDocument
|
||
|
]);
|
||
|
const announce = React.useCallback((message, options = {})=>{
|
||
|
const { alert = false, priority = 0, batchId } = options;
|
||
|
// check if message is an alert
|
||
|
if (alert) {
|
||
|
// TODO: alert implementation
|
||
|
// setAlertList([...alertList, message]);
|
||
|
}
|
||
|
const liveMessage = {
|
||
|
message,
|
||
|
createdAt: order.current++,
|
||
|
priority,
|
||
|
batchId
|
||
|
};
|
||
|
// check if batchId exists
|
||
|
if (batchId) {
|
||
|
// update associated msg if it does
|
||
|
const batchMessage = batchMessages.current.find((msg)=>msg.batchId === batchId);
|
||
|
if (batchMessage) {
|
||
|
// replace existing message in queue
|
||
|
messageQueue.remove(batchMessage.message);
|
||
|
// update list of existing batchIds w/ most recent message
|
||
|
batchMessage.message = liveMessage;
|
||
|
} else {
|
||
|
// update list of existing batchIds, add new if doesn't already exist
|
||
|
batchMessages.current = [
|
||
|
...batchMessages.current,
|
||
|
{
|
||
|
batchId,
|
||
|
message: liveMessage
|
||
|
}
|
||
|
];
|
||
|
}
|
||
|
}
|
||
|
// add new message
|
||
|
messageQueue.enqueue(liveMessage);
|
||
|
queueMessage();
|
||
|
}, [
|
||
|
messageQueue,
|
||
|
queueMessage
|
||
|
]);
|
||
|
React.useEffect(()=>{
|
||
|
if (!targetDocument) {
|
||
|
return;
|
||
|
}
|
||
|
const element = targetDocument.createElement('div');
|
||
|
element.setAttribute('aria-live', 'assertive');
|
||
|
Object.assign(element.style, VISUALLY_HIDDEN_STYLES);
|
||
|
targetDocument.body.append(element);
|
||
|
elementRef.current = element;
|
||
|
return ()=>{
|
||
|
element.remove();
|
||
|
elementRef.current = null;
|
||
|
clearAnnounceTimeout();
|
||
|
timeoutRef.current = undefined;
|
||
|
};
|
||
|
}, [
|
||
|
clearAnnounceTimeout,
|
||
|
targetDocument
|
||
|
]);
|
||
|
return announce;
|
||
|
};
|