diff --git a/.changeset/mighty-doors-beg.md b/.changeset/mighty-doors-beg.md new file mode 100644 index 00000000000..d18f4855ea8 --- /dev/null +++ b/.changeset/mighty-doors-beg.md @@ -0,0 +1,5 @@ +--- +"@primer/react": minor +--- + +Select Panel: Add built-in "No matches" item (behind feature flag `primer_react_select_panel_with_modern_action_list`) diff --git a/.changeset/small-melons-fail.md b/.changeset/small-melons-fail.md new file mode 100644 index 00000000000..c2465b10950 --- /dev/null +++ b/.changeset/small-melons-fail.md @@ -0,0 +1,5 @@ +--- +"@primer/react": patch +--- + +SelectPanel: Add announcements for screen readers (behind feature flag `primer_react_select_panel_with_modern_action_list`) diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-colorblind-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-colorblind-linux.png new file mode 100644 index 00000000000..fab4fd093f7 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-colorblind-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-colorblind-modern-action-list--true-linux.png new file mode 100644 index 00000000000..fab4fd093f7 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-colorblind-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-dimmed-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-dimmed-linux.png new file mode 100644 index 00000000000..865ac022d45 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-dimmed-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-dimmed-modern-action-list--true-linux.png new file mode 100644 index 00000000000..865ac022d45 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-dimmed-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-high-contrast-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-high-contrast-linux.png new file mode 100644 index 00000000000..14caf14e856 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-high-contrast-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-high-contrast-modern-action-list--true-linux.png new file mode 100644 index 00000000000..14caf14e856 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-high-contrast-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-linux.png new file mode 100644 index 00000000000..fab4fd093f7 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-modern-action-list--true-linux.png new file mode 100644 index 00000000000..fab4fd093f7 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-tritanopia-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-tritanopia-linux.png new file mode 100644 index 00000000000..fab4fd093f7 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-tritanopia-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-tritanopia-modern-action-list--true-linux.png new file mode 100644 index 00000000000..fab4fd093f7 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-dark-tritanopia-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-colorblind-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-colorblind-linux.png new file mode 100644 index 00000000000..0ae224d9a54 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-colorblind-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-colorblind-modern-action-list--true-linux.png new file mode 100644 index 00000000000..0ae224d9a54 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-colorblind-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-high-contrast-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-high-contrast-linux.png new file mode 100644 index 00000000000..0b023b3f496 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-high-contrast-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-high-contrast-modern-action-list--true-linux.png new file mode 100644 index 00000000000..0b023b3f496 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-high-contrast-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-linux.png new file mode 100644 index 00000000000..0ae224d9a54 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-modern-action-list--true-linux.png new file mode 100644 index 00000000000..0ae224d9a54 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-tritanopia-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-tritanopia-linux.png new file mode 100644 index 00000000000..0ae224d9a54 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-tritanopia-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-tritanopia-modern-action-list--true-linux.png new file mode 100644 index 00000000000..0ae224d9a54 Binary files /dev/null and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-No-matches-light-tritanopia-modern-action-list--true-linux.png differ diff --git a/e2e/components/SelectPanel.test.ts b/e2e/components/SelectPanel.test.ts index e4f61864b17..45ad02fe90f 100644 --- a/e2e/components/SelectPanel.test.ts +++ b/e2e/components/SelectPanel.test.ts @@ -32,6 +32,10 @@ const scenarios = matrix({ id: 'components-selectpanel-examples--height-initial-with-underflowing-items-after-fetch', name: 'Height Initial with Underflowing Items After Fetch', }, + { + id: 'components-selectpanel-examples--no-matches', + name: 'No matches', + }, ], }) diff --git a/packages/react/src/FilteredActionList/FilteredActionListWithModernActionList.tsx b/packages/react/src/FilteredActionList/FilteredActionListWithModernActionList.tsx index ae791e63909..7c447544105 100644 --- a/packages/react/src/FilteredActionList/FilteredActionListWithModernActionList.tsx +++ b/packages/react/src/FilteredActionList/FilteredActionListWithModernActionList.tsx @@ -20,6 +20,8 @@ import type {SxProp} from '../sx' import {isValidElementType} from 'react-is' import type {RenderItemFn} from '../deprecated/ActionList/List' +import {CircleSlashIcon} from '@primer/octicons-react' +import {useAnnouncements} from './useAnnouncements' const menuScrollMargins: ScrollIntoViewOptions = {startMargin: 0, endMargin: 8} @@ -114,6 +116,7 @@ export function FilteredActionList({ }, [items]) useScrollFlash(scrollContainerRef) + useAnnouncements(items, listContainerRef, inputRef) function getItemListForEachGroup(groupId: string) { const itemsInGroup = [] @@ -151,7 +154,14 @@ export function FilteredActionList({ ) : ( - + {groupMetadata?.length ? groupMetadata.map((group, index) => { return ( @@ -168,6 +178,15 @@ export function FilteredActionList({ : items.map((item, index) => { return })} + + {items.length === 0 ? ( + + + + + No matches + + ) : undefined} )} diff --git a/packages/react/src/FilteredActionList/useAnnouncements.tsx b/packages/react/src/FilteredActionList/useAnnouncements.tsx new file mode 100644 index 00000000000..8b419d92584 --- /dev/null +++ b/packages/react/src/FilteredActionList/useAnnouncements.tsx @@ -0,0 +1,97 @@ +// Announcements for FilteredActionList (and SelectPanel) based +// on https://github.com/github/multi-select-user-testing + +import {announce} from '@primer/live-region-element' +import {useEffect, useRef} from 'react' +import type {FilteredActionListProps} from './FilteredActionListEntry' + +// 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: React.RefObject, + items: FilteredActionListProps['items'], +) => { + const listElement = listRef.current + const activeItemElement = listElement?.querySelector('[data-is-active-descendant]') + + if (!listElement || !activeItemElement?.textContent) return + + const optionElements = listElement.querySelectorAll('[role="option"]') + + const index = Array.from(optionElements).indexOf(activeItemElement) + const activeItem = items[index] + + const text = activeItem.text + const selected = activeItem.selected + + return {index, text, selected} +} + +export const useAnnouncements = ( + items: FilteredActionListProps['items'], + listContainerRef: React.RefObject, + inputRef: React.RefObject, +) => { + 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 labels`, + `Focused item: ${text}`, + `${selected ? 'selected' : 'not selected'}`, + `${index + 1} of ${items.length}`, + ].join(', ') + announce(announcementText, {delayMs}) + }) + } + + const inputElement = inputRef.current + inputElement?.addEventListener('focus', focusHandler) + return () => inputElement?.removeEventListener('focus', focusHandler) + }, + [listContainerRef, inputRef, items], + ) + + const isFirstRender = useFirstRender() + useEffect( + function announceListUpdates() { + if (isFirstRender) return // ignore on first render as announceInitialFocus will also announce + + 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} of ${items.length}`, + ].join(', ') + announce(announcementText, {delayMs}) + }) + }, + [listContainerRef, inputRef, items, isFirstRender], + ) +} diff --git a/packages/react/src/SelectPanel/SelectPanel.examples.stories.tsx b/packages/react/src/SelectPanel/SelectPanel.examples.stories.tsx index 248432e0f9a..6570019e3d6 100644 --- a/packages/react/src/SelectPanel/SelectPanel.examples.stories.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.examples.stories.tsx @@ -309,3 +309,32 @@ export const CustomItemRenderer = () => { ) } + +export const NoMatches = () => { + const [selected, setSelected] = React.useState([items[0], items[1]]) + const [filter, setFilter] = React.useState('no-matches-for-this') + const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase())) + const [open, setOpen] = useState(false) + + return ( + <> +

No matches

+ ( + + )} + open={open} + onOpenChange={setOpen} + items={filteredItems} + selected={selected} + onSelectedChange={setSelected} + filterValue={filter} + onFilterChange={setFilter} + overlayProps={{width: 'medium'}} + /> + + ) +} diff --git a/packages/react/src/SelectPanel/SelectPanel.test.tsx b/packages/react/src/SelectPanel/SelectPanel.test.tsx index 7d6adfbb3ae..2e8d04a17f4 100644 --- a/packages/react/src/SelectPanel/SelectPanel.test.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.test.tsx @@ -1,10 +1,11 @@ -import {render, screen} from '@testing-library/react' +import {render, screen, waitFor} from '@testing-library/react' import React from 'react' import {SelectPanel, type SelectPanelProps} from '../SelectPanel' import type {ItemInput, GroupedListProps} from '../deprecated/ActionList/List' import {userEvent} from '@testing-library/user-event' import ThemeProvider from '../ThemeProvider' import {FeatureFlags} from '../FeatureFlags' +import {getLiveRegion} from '../utils/testing' const renderWithFlag = (children: React.ReactNode, flag: boolean) => { return render( @@ -336,39 +337,39 @@ for (const useModernActionList of [false, true]) { }) }) - describe('filtering', () => { - function FilterableSelectPanel() { - const [selected, setSelected] = React.useState([]) - const [filter, setFilter] = React.useState('') - const [open, setOpen] = React.useState(false) - - const onSelectedChange = (selected: SelectPanelProps['items']) => { - setSelected(selected) - } + function FilterableSelectPanel() { + const [selected, setSelected] = React.useState([]) + const [filter, setFilter] = React.useState('') + const [open, setOpen] = React.useState(false) - return ( - - item.text?.includes(filter))} - placeholder="Select items" - placeholderText="Filter items" - selected={selected} - onSelectedChange={onSelectedChange} - filterValue={filter} - onFilterChange={value => { - setFilter(value) - }} - open={open} - onOpenChange={isOpen => { - setOpen(isOpen) - }} - /> - - ) + const onSelectedChange = (selected: SelectPanelProps['items']) => { + setSelected(selected) } + return ( + + item.text?.includes(filter))} + placeholder="Select items" + placeholderText="Filter items" + selected={selected} + onSelectedChange={onSelectedChange} + filterValue={filter} + onFilterChange={value => { + setFilter(value) + }} + open={open} + onOpenChange={isOpen => { + setOpen(isOpen) + }} + /> + + ) + } + + describe('filtering', () => { it('should filter the list of items when the user types into the input', async () => { const user = userEvent.setup() @@ -381,10 +382,68 @@ for (const useModernActionList of [false, true]) { await user.type(document.activeElement!, 'two') expect(screen.getAllByRole('option')).toHaveLength(1) }) + }) + + describe('screen reader announcements', () => { + // this is only implemented with the feature flag + if (!useModernActionList) return + + it('should announce initial focused item', async () => { + const user = userEvent.setup() + renderWithFlag(, useModernActionList) + + await user.click(screen.getByText('Select items')) + expect(screen.getByLabelText('Filter items')).toHaveFocus() + + // we wait because announcement is intentionally updated after a timeout to not interrupt user input + await waitFor(async () => { + expect(getLiveRegion().getMessage('polite')).toBe( + 'Focus on filter text box and list of labels, Focused item: item one, not selected, 1 of 3', + ) + }) + }) + + it('should announce filtered results', async () => { + const user = userEvent.setup() + renderWithFlag(, useModernActionList) + + await user.click(screen.getByText('Select items')) + await user.type(document.activeElement!, 'o') + expect(screen.getAllByRole('option')).toHaveLength(2) + + await waitFor( + async () => { + expect(getLiveRegion().getMessage('polite')).toBe( + 'List updated, Focused item: item one, not selected, 1 of 2', + ) + }, + {timeout: 3000}, // increased timeout because we don't want the test to compare with previous announcement + ) + + await user.type(document.activeElement!, 'ne') // now: one + expect(screen.getAllByRole('option')).toHaveLength(1) + + await waitFor(async () => { + expect(getLiveRegion().getMessage('polite')).toBe( + 'List updated, Focused item: item one, not selected, 1 of 1', + ) + }) + }) - it.todo('should announce the number of results') + it('should announce when no results are available', async () => { + const user = userEvent.setup() + renderWithFlag(, useModernActionList) - it.todo('should announce when no results are available') + await user.click(screen.getByText('Select items')) + + await user.type(document.activeElement!, 'zero') + expect(screen.queryByRole('option')).toBeNull() + expect(screen.getByText('No matches')).toBeVisible() + + await waitFor(async () => { + expect(getLiveRegion().getMessage('polite')).toBe('No matching items.') + }) + }) }) describe('with footer', () => { diff --git a/packages/react/src/SelectPanel/SelectPanel.tsx b/packages/react/src/SelectPanel/SelectPanel.tsx index edf35fafee6..8c0aea638fd 100644 --- a/packages/react/src/SelectPanel/SelectPanel.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.tsx @@ -17,6 +17,7 @@ import type {FocusZoneHookSettings} from '../hooks/useFocusZone' import {useId} from '../hooks/useId' import {useProvidedStateOrCreate} from '../hooks/useProvidedStateOrCreate' import {LiveRegion, LiveRegionOutlet, Message} from '../internal/components/LiveRegion' +import {useFeatureFlag} from '../FeatureFlags' interface SelectPanelSingleSelection { selected: ItemInput | undefined @@ -174,6 +175,8 @@ export function SelectPanel({ } }, [inputLabel, textInputProps]) + const usingModernActionList = useFeatureFlag('primer_react_select_panel_with_modern_action_list') + return ( - + {usingModernActionList ? null : ( + + )} + diff --git a/packages/react/src/live-region/__tests__/Announce.test.tsx b/packages/react/src/live-region/__tests__/Announce.test.tsx index 5586297a8dd..b3364bf9c2b 100644 --- a/packages/react/src/live-region/__tests__/Announce.test.tsx +++ b/packages/react/src/live-region/__tests__/Announce.test.tsx @@ -1,15 +1,7 @@ import {render, screen} from '@testing-library/react' import React from 'react' -import type {LiveRegionElement} from '@primer/live-region-element' import {Announce} from '../Announce' - -function getLiveRegion(): LiveRegionElement { - const liveRegion = document.querySelector('live-region') - if (liveRegion) { - return liveRegion as LiveRegionElement - } - throw new Error('No live-region found') -} +import {getLiveRegion} from '../../utils/testing' describe('Announce', () => { beforeEach(() => { diff --git a/packages/react/src/live-region/__tests__/AriaAlert.test.tsx b/packages/react/src/live-region/__tests__/AriaAlert.test.tsx index e51e4558d44..91c4d83731e 100644 --- a/packages/react/src/live-region/__tests__/AriaAlert.test.tsx +++ b/packages/react/src/live-region/__tests__/AriaAlert.test.tsx @@ -1,15 +1,7 @@ import {render, screen} from '@testing-library/react' import React from 'react' -import type {LiveRegionElement} from '@primer/live-region-element' import {AriaAlert} from '../AriaAlert' - -function getLiveRegion(): LiveRegionElement { - const liveRegion = document.querySelector('live-region') - if (liveRegion) { - return liveRegion as LiveRegionElement - } - throw new Error('No live-region found') -} +import {getLiveRegion} from '../../utils/testing' describe('AriaAlert', () => { beforeEach(() => { diff --git a/packages/react/src/live-region/__tests__/AriaStatus.test.tsx b/packages/react/src/live-region/__tests__/AriaStatus.test.tsx index 29bed2c78fb..16465c03a5c 100644 --- a/packages/react/src/live-region/__tests__/AriaStatus.test.tsx +++ b/packages/react/src/live-region/__tests__/AriaStatus.test.tsx @@ -1,16 +1,8 @@ import {render, screen} from '@testing-library/react' import React from 'react' -import type {LiveRegionElement} from '@primer/live-region-element' import {AriaStatus} from '../AriaStatus' import {userEvent} from '@testing-library/user-event' - -function getLiveRegion(): LiveRegionElement { - const liveRegion = document.querySelector('live-region') - if (liveRegion) { - return liveRegion as LiveRegionElement - } - throw new Error('No live-region found') -} +import {getLiveRegion} from '../../utils/testing' describe('AriaStatus', () => { beforeEach(() => { diff --git a/packages/react/src/utils/testing.tsx b/packages/react/src/utils/testing.tsx index 5f013cd9624..5f5f1caa257 100644 --- a/packages/react/src/utils/testing.tsx +++ b/packages/react/src/utils/testing.tsx @@ -7,6 +7,7 @@ import axe from 'axe-core' import customRules from '@github/axe-github' import {ThemeProvider} from '..' import {default as defaultTheme} from '../theme' +import type {LiveRegionElement} from '@primer/live-region-element' type ComputedStyles = Record> @@ -270,3 +271,11 @@ export function checkStoriesForAxeViolations(name: string, storyDir?: string) { }) }) } + +export function getLiveRegion(): LiveRegionElement { + const liveRegion = document.querySelector('live-region') + if (liveRegion) { + return liveRegion as LiveRegionElement + } + throw new Error('No live-region found') +}