@primer/react
Version:
An implementation of GitHub's Primer Design System using React
92 lines (85 loc) • 3.94 kB
JavaScript
import { announce } from '@primer/live-region-element';
import { useCallback, useEffect, useRef } from 'react';
// Announcements for FilteredActionList (and SelectPanel) based
// on https://github.com/github/multi-select-user-testing
// we add a delay so that it does not interrupt default screen reader announcement and queues after it
const delayMs = 500;
const useFirstRender = () => {
const firstRender = useRef(true);
useEffect(() => {
firstRender.current = false;
}, []);
return firstRender.current;
};
const getItemWithActiveDescendant = (listRef, items) => {
const listElement = listRef.current;
const activeItemElement = listElement === null || listElement === undefined ? undefined : listElement.querySelector('[data-is-active-descendant]');
if (!listElement || !(activeItemElement !== null && activeItemElement !== undefined && activeItemElement.textContent)) return;
const optionElements = listElement.querySelectorAll('[role="option"]');
const index = Array.from(optionElements).indexOf(activeItemElement);
const activeItem = items[index];
const text = activeItem === null || activeItem === undefined ? undefined : activeItem.text;
const selected = activeItem === null || activeItem === undefined ? undefined : activeItem.selected;
return {
index,
text,
selected
};
};
const useAnnouncements = (items, listContainerRef, inputRef, enabled = true) => {
const liveRegion = document.querySelector('live-region');
const announce$1 = useCallback((...args) => {
if (enabled) {
return announce(...args);
}
}, [enabled]);
useEffect(function announceInitialFocus() {
const focusHandler = () => {
// give @primer/behaviors a moment to apply active-descendant
window.requestAnimationFrame(() => {
const activeItem = getItemWithActiveDescendant(listContainerRef, items);
if (!activeItem) return;
const {
index,
text,
selected
} = activeItem;
const announcementText = [`Focus on filter text box and list of items`, `Focused item: ${text}`, `${selected ? 'selected' : 'not selected'}`, `${index + 1} of ${items.length}`].join(', ');
announce$1(announcementText, {
delayMs,
from: liveRegion ? liveRegion : undefined // announce will create a liveRegion if it doesn't find one
});
});
};
const inputElement = inputRef.current;
inputElement === null || inputElement === undefined ? undefined : inputElement.addEventListener('focus', focusHandler);
return () => inputElement === null || inputElement === undefined ? undefined : inputElement.removeEventListener('focus', focusHandler);
}, [listContainerRef, inputRef, items, liveRegion, announce$1]);
const isFirstRender = useFirstRender();
useEffect(function announceListUpdates() {
if (isFirstRender) return; // ignore on first render as announceInitialFocus will also announce
liveRegion === null || liveRegion === undefined ? undefined : liveRegion.clear(); // clear previous announcements
if (items.length === 0) {
announce$1('No matching items.', {
delayMs
});
return;
}
// give @primer/behaviors a moment to update active-descendant
window.requestAnimationFrame(() => {
const activeItem = getItemWithActiveDescendant(listContainerRef, items);
if (!activeItem) return;
const {
index,
text,
selected
} = activeItem;
const announcementText = [`List updated`, `Focused item: ${text}`, `${selected ? 'selected' : 'not selected'}`, `${index + 1} of ${items.length}`].join(', ');
announce$1(announcementText, {
delayMs,
from: liveRegion ? liveRegion : undefined // announce will create a liveRegion if it doesn't find one
});
});
}, [announce$1, isFirstRender, items, listContainerRef, liveRegion]);
};
export { useAnnouncements };