Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 6 additions & 22 deletions packages/main/src/components/SelectDialog/SelectDialog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (`<ui5-announcement-area>`) 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.

<Canvas of={ComponentStories.SearchResultAnnouncement} sourceState="none" />

Expand All @@ -187,23 +185,15 @@ The workaround is to render your own `aria-live="polite"` region **inside** the
<summary>Show Code</summary>

```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<DialogDomRef | null>(null);
const liveSpanRef = useRef<HTMLSpanElement | null>(null);
const [open, setOpen] = useState(false);
const [searchValue, setSearchValue] = useState('');

Expand All @@ -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) => {
Expand All @@ -237,7 +223,6 @@ const SearchAnnouncementDialog = () => {
<>
<Button onClick={() => setOpen(true)}>Open SelectDialog</Button>
<SelectDialog
ref={setDialogEl}
headerText="Select Product"
open={open}
onClose={() => setOpen(false)}
Expand All @@ -248,7 +233,6 @@ const SearchAnnouncementDialog = () => {
<ListItemStandard key={item.id} description={item.id} text={item.text} />
))}
</SelectDialog>
{dialogEl && createPortal(<span ref={liveSpanRef} aria-live="polite" style={liveRegionStyle} />, dialogEl)}
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<DialogDomRef | null>(null);
const liveSpanRef = useRef<HTMLSpanElement | null>(null);
const [open, setOpen] = useState(false);
const [searchValue, setSearchValue] = useState('');

Expand All @@ -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<NonNullable<SelectDialogPropTypes['onSearch']>>[0]) => {
Expand All @@ -236,7 +220,6 @@ export const SearchResultAnnouncement: Story = {
<>
<Button onClick={() => setOpen(true)}>Open SelectDialog</Button>
<SelectDialog
ref={setDialogEl}
headerText="Select Product"
open={open}
onClose={() => setOpen(false)}
Expand All @@ -247,7 +230,6 @@ export const SearchResultAnnouncement: Story = {
<ListItemStandard key={item.id} description={item.id} text={item.text} />
))}
</SelectDialog>
{dialogEl && createPortal(<span ref={liveSpanRef} aria-live="polite" style={liveRegionStyle} />, dialogEl)}
</>
);
},
Expand Down
Loading