diff --git a/packages/main/src/components/SelectDialog/SelectDialog.mdx b/packages/main/src/components/SelectDialog/SelectDialog.mdx index 464adc48d46..aae19409dca 100644 --- a/packages/main/src/components/SelectDialog/SelectDialog.mdx +++ b/packages/main/src/components/SelectDialog/SelectDialog.mdx @@ -174,11 +174,9 @@ const MultiSelectDialog = () => { ### Announcing search result count -A common a11y requirement is to announce the number of matches when the user filters the list (e.g. _"5 results available"_, _"No results found"_). +A common a11y requirement is to announce the number of matches when the user filters the list (e.g. _"5 results available"_, _"No results found"_). The result count and the timing of the announcement (on every keystroke vs. on commit, debouncing) depend on your data and UX, so this is wired up in application code rather than by the component. -The framework's `InvisibleMessage.announce(...)` cannot be used here: its live region (``) is appended to `document.body`, outside the dialog. When the dialog is open, some screen readers do not announce updates from live regions rendered outside it, so the message is silently dropped. See [UI5/webcomponents#13613](https://github.com/UI5/webcomponents/issues/13613) for details. - -The workaround is to render your own `aria-live="polite"` region **inside** the dialog's DOM via `createPortal`, and write the message into it from your search handler. +Use the framework's [Invisible Messaging](https://ui5.github.io/webcomponents/docs/advanced/accessibility/#invisible-messaging) — call `announce(...)` from your search handler to announce the result count politely. @@ -187,23 +185,15 @@ The workaround is to render your own `aria-live="polite"` region **inside** the Show Code ```tsx +import announce from '@ui5/webcomponents-base/dist/util/InvisibleMessage.js'; +import InvisibleMessageMode from '@ui5/webcomponents-base/dist/types/InvisibleMessageMode.js'; + const items = Array.from({ length: 40 }, (_unused, index) => ({ id: `P-${index.toString().padStart(3, '0')}`, text: ['Gaming Laptop', 'Business Laptop', 'Gaming PC', 'Business PC'][index % 4], })); -const liveRegionStyle: CSSProperties = { - position: 'absolute', - clip: 'rect(1px,1px,1px,1px)', - userSelect: 'none', - left: '-1000px', - top: '-1000px', - pointerEvents: 'none', -}; - const SearchAnnouncementDialog = () => { - const [dialogEl, setDialogEl] = useState(null); - const liveSpanRef = useRef(null); const [open, setOpen] = useState(false); const [searchValue, setSearchValue] = useState(''); @@ -216,11 +206,7 @@ const SearchAnnouncementDialog = () => { }, [searchValue]); const announceCount = (count: number) => { - const span = liveSpanRef.current; - if (!span) { - return; - } - span.textContent = count === 0 ? 'No results found' : `${count} results available`; + announce(count === 0 ? 'No results found' : `${count} results available`, InvisibleMessageMode.Polite); }; const handleSearch: SelectDialogPropTypes['onSearch'] = (event) => { @@ -237,7 +223,6 @@ const SearchAnnouncementDialog = () => { <> setOpen(false)} @@ -248,7 +233,6 @@ const SearchAnnouncementDialog = () => { ))} - {dialogEl && createPortal(, dialogEl)} ); }; diff --git a/packages/main/src/components/SelectDialog/SelectDialog.stories.tsx b/packages/main/src/components/SelectDialog/SelectDialog.stories.tsx index 1c329718b68..211ca6e06f6 100644 --- a/packages/main/src/components/SelectDialog/SelectDialog.stories.tsx +++ b/packages/main/src/components/SelectDialog/SelectDialog.stories.tsx @@ -5,11 +5,10 @@ import Pc2 from '@sb/demoImages/PC2.jpg'; import { isChromatic } from '@sb/utils.js'; import type { Meta, StoryObj } from '@storybook/react-vite'; import ListSelectionMode from '@ui5/webcomponents/dist/types/ListSelectionMode.js'; -import type { CSSProperties } from 'react'; +import InvisibleMessageMode from '@ui5/webcomponents-base/dist/types/InvisibleMessageMode.js'; +import announce from '@ui5/webcomponents-base/dist/util/InvisibleMessage.js'; import { useEffect, useMemo, useRef, useState } from 'react'; -import { createPortal } from 'react-dom'; import { Button } from '../../webComponents/Button/index.js'; -import type { DialogDomRef } from '../../webComponents/Dialog/index.js'; import { Label } from '../../webComponents/Label/index.js'; import { ListItemStandard } from '../../webComponents/ListItemStandard/index.js'; import { Text } from '../../webComponents/Text/index.js'; @@ -188,19 +187,8 @@ const announcementItems = Array.from({ length: 40 }, (_unused, index) => ({ text: ['Gaming Laptop', 'Business Laptop', 'Gaming PC', 'Business PC'][index % 4], })); -const liveRegionStyle: CSSProperties = { - position: 'absolute', - clip: 'rect(1px,1px,1px,1px)', - userSelect: 'none', - left: '-1000px', - top: '-1000px', - pointerEvents: 'none', -}; - export const SearchResultAnnouncement: Story = { render: () => { - const [dialogEl, setDialogEl] = useState(null); - const liveSpanRef = useRef(null); const [open, setOpen] = useState(false); const [searchValue, setSearchValue] = useState(''); @@ -215,11 +203,7 @@ export const SearchResultAnnouncement: Story = { }, [searchValue]); const announceCount = (count: number) => { - const span = liveSpanRef.current; - if (!span) { - return; - } - span.textContent = count === 0 ? 'No results found' : `${count} results available`; + announce(count === 0 ? 'No results found' : `${count} results available`, InvisibleMessageMode.Polite); }; const handleSearch = (event: Parameters>[0]) => { @@ -236,7 +220,6 @@ export const SearchResultAnnouncement: Story = { <> setOpen(false)} @@ -247,7 +230,6 @@ export const SearchResultAnnouncement: Story = { ))} - {dialogEl && createPortal(, dialogEl)} ); },