diff --git a/packages/main/src/components/SelectDialog/SelectDialog.mdx b/packages/main/src/components/SelectDialog/SelectDialog.mdx
index a76d0b98145..464adc48d46 100644
--- a/packages/main/src/components/SelectDialog/SelectDialog.mdx
+++ b/packages/main/src/components/SelectDialog/SelectDialog.mdx
@@ -172,4 +172,91 @@ 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"_).
+
+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.
+
+
+
+
+
+Show Code
+
+```tsx
+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('');
+
+ const filteredItems = useMemo(() => {
+ const query = searchValue.trim().toLowerCase();
+ if (!query) {
+ return items;
+ }
+ return items.filter((item) => item.id.toLowerCase().includes(query) || item.text.toLowerCase().includes(query));
+ }, [searchValue]);
+
+ const announceCount = (count: number) => {
+ const span = liveSpanRef.current;
+ if (!span) {
+ return;
+ }
+ span.textContent = count === 0 ? 'No results found' : `${count} results available`;
+ };
+
+ const handleSearch: SelectDialogPropTypes['onSearch'] = (event) => {
+ setSearchValue(event.detail.value);
+ announceCount(filteredItems.length);
+ };
+
+ const handleSearchReset = () => {
+ setSearchValue('');
+ announceCount(items.length);
+ };
+
+ return (
+ <>
+
+ setOpen(false)}
+ onSearch={handleSearch}
+ onSearchReset={handleSearchReset}
+ >
+ {filteredItems.map((item) => (
+
+ ))}
+
+ {dialogEl && createPortal(, dialogEl)}
+ >
+ );
+};
+```
+
+
+
+
+
+
diff --git a/packages/main/src/components/SelectDialog/SelectDialog.stories.tsx b/packages/main/src/components/SelectDialog/SelectDialog.stories.tsx
index 6a08e2bd927..1c329718b68 100644
--- a/packages/main/src/components/SelectDialog/SelectDialog.stories.tsx
+++ b/packages/main/src/components/SelectDialog/SelectDialog.stories.tsx
@@ -5,13 +5,17 @@ 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 { useEffect, useRef, useState } from 'react';
+import type { CSSProperties } from 'react';
+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';
import { FlexBox } from '../FlexBox/index.js';
import { SelectDialog } from './index.js';
+import type { SelectDialogPropTypes } from './index.js';
const meta = {
title: 'Modals & Popovers / SelectDialog',
@@ -178,3 +182,73 @@ export const MultiSelect: Story = {
);
},
};
+
+const announcementItems = 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',
+};
+
+export const SearchResultAnnouncement: Story = {
+ render: () => {
+ const [dialogEl, setDialogEl] = useState(null);
+ const liveSpanRef = useRef(null);
+ const [open, setOpen] = useState(false);
+ const [searchValue, setSearchValue] = useState('');
+
+ const filteredItems = useMemo(() => {
+ const query = searchValue.trim().toLowerCase();
+ if (!query) {
+ return announcementItems;
+ }
+ return announcementItems.filter(
+ (item) => item.id.toLowerCase().includes(query) || item.text.toLowerCase().includes(query),
+ );
+ }, [searchValue]);
+
+ const announceCount = (count: number) => {
+ const span = liveSpanRef.current;
+ if (!span) {
+ return;
+ }
+ span.textContent = count === 0 ? 'No results found' : `${count} results available`;
+ };
+
+ const handleSearch = (event: Parameters>[0]) => {
+ setSearchValue(event.detail.value);
+ announceCount(filteredItems.length);
+ };
+
+ const handleSearchReset = () => {
+ setSearchValue('');
+ announceCount(announcementItems.length);
+ };
+
+ return (
+ <>
+
+ setOpen(false)}
+ onSearch={handleSearch}
+ onSearchReset={handleSearchReset}
+ >
+ {filteredItems.map((item) => (
+
+ ))}
+
+ {dialogEl && createPortal(, dialogEl)}
+ >
+ );
+ },
+};