UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

94 lines (86 loc) 3.98 kB
'use strict'; 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;