@primer/react
Version:
An implementation of GitHub's Primer Design System using React
94 lines (86 loc) • 3.98 kB
JavaScript
;
var liveRegionElement = require('@primer/live-region-element');
var React = require('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 = React.useRef(true);
React.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 = React.useCallback((...args) => {
if (enabled) {
return liveRegionElement.announce(...args);
}
}, [enabled]);
React.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(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]);
const isFirstRender = useFirstRender();
React.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('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(announcementText, {
delayMs,
from: liveRegion ? liveRegion : undefined // announce will create a liveRegion if it doesn't find one
});
});
}, [announce, isFirstRender, items, listContainerRef, liveRegion]);
};
exports.useAnnouncements = useAnnouncements;