Skip to content

Commit 60411d6

Browse files
authored
fix(Toolbar - compat): announce number of items in overflow popover (#6545)
Fixes #5926
1 parent 5ce8fe9 commit 60411d6

File tree

4 files changed

+170
-75
lines changed

4 files changed

+170
-75
lines changed

packages/compat/src/components/Toolbar/OverflowPopover.tsx

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ import type {
99
ToggleButtonPropTypes
1010
} from '@ui5/webcomponents-react';
1111
import { Popover, ToggleButton } from '@ui5/webcomponents-react';
12+
import { WITH_X_ITEMS, SHOW_MORE, X_OF_Y } from '@ui5/webcomponents-react/dist/i18n/i18n-defaults.js';
1213
import { stopPropagation } from '@ui5/webcomponents-react/dist/internal/stopPropagation.js';
1314
import { getUi5TagWithSuffix } from '@ui5/webcomponents-react/dist/internal/utils.js';
14-
import { Device, useSyncRef } from '@ui5/webcomponents-react-base';
15+
import { Device, useI18nBundle, useSyncRef } from '@ui5/webcomponents-react-base';
1516
import { clsx } from 'clsx';
16-
import type { Dispatch, FC, ReactElement, ReactNode, Ref, SetStateAction } from 'react';
17-
import { cloneElement, useEffect, useRef, useState } from 'react';
17+
import type { Dispatch, FC, HTMLAttributes, ReactElement, ReactNode, Ref, SetStateAction } from 'react';
18+
import { isValidElement, cloneElement, useEffect, useRef, useState } from 'react';
1819
import { createPortal } from 'react-dom';
1920
import { getOverflowPopoverContext } from '../../internal/OverflowPopoverContext.js';
20-
import type { ToolbarSeparatorPropTypes } from '../ToolbarSeparator/index.js';
2121
import type { ToolbarPropTypes } from './index.js';
2222

2323
interface OverflowPopoverProps {
@@ -27,7 +27,6 @@ interface OverflowPopoverProps {
2727
portalContainer: Element;
2828
overflowContentRef: Ref<HTMLDivElement>;
2929
numberOfAlwaysVisibleItems?: number;
30-
showMoreText: string;
3130
overflowPopoverRef?: Ref<PopoverDomRef>;
3231
overflowButton?: ReactElement<ToggleButtonPropTypes> | ReactElement<ButtonPropTypes>;
3332
setIsMounted: Dispatch<SetStateAction<boolean>>;
@@ -44,7 +43,6 @@ export const OverflowPopover: FC<OverflowPopoverProps> = (props: OverflowPopover
4443
portalContainer,
4544
overflowContentRef,
4645
numberOfAlwaysVisibleItems,
47-
showMoreText,
4846
overflowButton,
4947
overflowPopoverRef,
5048
setIsMounted,
@@ -53,6 +51,8 @@ export const OverflowPopover: FC<OverflowPopoverProps> = (props: OverflowPopover
5351
const [pressed, setPressed] = useState(false);
5452
const toggleBtnRef = useRef<ToggleButtonDomRef>(null);
5553
const [componentRef, popoverRef] = useSyncRef(overflowPopoverRef);
54+
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
55+
const showMoreText = i18nBundle.getText(SHOW_MORE);
5656

5757
useEffect(() => {
5858
setIsMounted(true);
@@ -123,6 +123,50 @@ export const OverflowPopover: FC<OverflowPopoverProps> = (props: OverflowPopover
123123

124124
const OverflowPopoverContextProvider = getOverflowPopoverContext().Provider;
125125

126+
let startIndex = null;
127+
const filteredChildrenArray = children
128+
.map((item, index, arr) => {
129+
if (index > lastVisibleIndex && index > numberOfAlwaysVisibleItems - 1 && isValidElement(item)) {
130+
if (startIndex === null) {
131+
startIndex = index;
132+
}
133+
const labelProp = item?.props?.['data-accessible-name'] ? 'accessibleName' : 'aria-label';
134+
let labelVal = i18nBundle.getText(X_OF_Y, index + 1 - startIndex, arr.length - startIndex);
135+
if (item?.props?.[labelProp]) {
136+
labelVal += ' ' + item.props[labelProp];
137+
}
138+
139+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
140+
// @ts-ignore: React 19
141+
if (item?.props?.id) {
142+
return cloneElement<HTMLAttributes<HTMLElement>>(item, {
143+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
144+
// @ts-ignore: React 19
145+
id: `${item.props.id}-overflow`,
146+
[labelProp]: labelVal
147+
});
148+
}
149+
// @ts-expect-error: if type is not defined, it's not a spacer
150+
if (item.type?.displayName === 'ToolbarSeparator') {
151+
return cloneElement(item as ReactElement, {
152+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
153+
// @ts-ignore: React 19
154+
style: {
155+
height: '0.0625rem',
156+
margin: '0.375rem 0.1875rem',
157+
width: '100%'
158+
},
159+
'aria-label': labelVal
160+
});
161+
}
162+
return cloneElement<HTMLAttributes<HTMLElement>>(item, {
163+
[labelProp]: labelVal
164+
});
165+
}
166+
return null;
167+
})
168+
.filter(Boolean);
169+
126170
return (
127171
<OverflowPopoverContextProvider value={{ inPopover: true }}>
128172
{overflowButton ? (
@@ -152,34 +196,15 @@ export const OverflowPopover: FC<OverflowPopoverProps> = (props: OverflowPopover
152196
onOpen={handleAfterOpen}
153197
hideArrow
154198
accessibleRole={accessibleRole}
199+
accessibleName={i18nBundle.getText(WITH_X_ITEMS, filteredChildrenArray.length)}
155200
>
156201
<div
157202
className={classes.popoverContent}
158203
ref={overflowContentRef}
159204
role={a11yConfig?.overflowPopover?.contentRole}
160205
data-component-name="ToolbarOverflowPopoverContent"
161206
>
162-
{children.map((item, index) => {
163-
if (index > lastVisibleIndex && index > numberOfAlwaysVisibleItems - 1) {
164-
// @ts-expect-error: if props is not defined, it doesn't have an id (is not a ReactElement)
165-
if (item?.props?.id) {
166-
// @ts-expect-error: item is ReactElement
167-
return cloneElement(item, { id: `${item.props.id}-overflow` });
168-
}
169-
// @ts-expect-error: if type is not defined, it's not a spacer
170-
if (item.type?.displayName === 'ToolbarSeparator') {
171-
return cloneElement(item as ReactElement<ToolbarSeparatorPropTypes>, {
172-
style: {
173-
height: '0.0625rem',
174-
margin: '0.375rem 0.1875rem',
175-
width: '100%'
176-
}
177-
});
178-
}
179-
return item;
180-
}
181-
return null;
182-
})}
207+
{filteredChildrenArray}
183208
</div>
184209
</Popover>,
185210
portalContainer ?? document.body

packages/compat/src/components/Toolbar/Toolbar.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,24 @@ import { OverflowToolbarButton, OverflowToolbarToggleButton, ToolbarSpacer, Tool
2020

2121
<ControlsWithNote of={ComponentStories.Default} />
2222

23+
## Announce number of items in overflow popover
24+
25+
To set the `aria-label` correctly it's necessary to add the `data-accessible-name` data-attribute for each web component that relies on `accessibleName` instead of `aria-label`.
26+
27+
E.g.:
28+
29+
```jsx
30+
<Toolbar>
31+
<Text>Toolbar</Text>
32+
<Button data-accessible-name design={ButtonDesign.Transparent}>
33+
Button One
34+
</Button>
35+
<button>Button Two</button>
36+
<Input data-accessible-name />
37+
<input />
38+
</Toolbar>
39+
```
40+
2341
## Prevent event bubbling of Toolbar items
2442

2543
Per default, if the `active` prop is "true" and an actionable element like a button is clicked, the `onClick` event of the `Toolbar` is also fired.

packages/compat/src/components/Toolbar/Toolbar.stories.tsx

Lines changed: 99 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,15 @@ export const Default: Story = {
5151
return (
5252
<Toolbar {...args}>
5353
<Text>Toolbar</Text>
54-
<Button design={ButtonDesign.Transparent}>Button One</Button>
55-
<Button design={ButtonDesign.Transparent}>Button Two</Button>
56-
<Input />
57-
<DatePicker />
58-
<Switch />
54+
<Button data-accessible-name design={ButtonDesign.Transparent}>
55+
Button One
56+
</Button>
57+
<Button data-accessible-name design={ButtonDesign.Transparent}>
58+
Button Two
59+
</Button>
60+
<Input data-accessible-name />
61+
<DatePicker data-accessible-name />
62+
<Switch data-accessible-name />
5963
</Toolbar>
6064
);
6165
}
@@ -67,9 +71,11 @@ export const RightAlignedItems: Story = {
6771
return (
6872
<Toolbar {...args}>
6973
<ToolbarSpacer />
70-
<Button design={ButtonDesign.Transparent}>Button</Button>
71-
<Icon name={settingsIcon} />
72-
<Icon name={downloadIcon} />
74+
<Button data-accessible-name design={ButtonDesign.Transparent}>
75+
Button
76+
</Button>
77+
<Icon data-accessible-name accessibleName="Settings" name={settingsIcon} />
78+
<Icon data-accessible-name accessibleName="Download" name={downloadIcon} />
7379
</Toolbar>
7480
);
7581
}
@@ -82,11 +88,13 @@ export const EvenlyAlignedItems: Story = {
8288
<Toolbar {...args}>
8389
<Text>Left</Text>
8490
<ToolbarSpacer />
85-
<Button design={ButtonDesign.Transparent}>Center</Button>
91+
<Button data-accessible-name design={ButtonDesign.Transparent}>
92+
Center
93+
</Button>
8694
<ToolbarSpacer />
8795
<Text>Right</Text>
88-
<Icon name={settingsIcon} />
89-
<Icon name={downloadIcon} />
96+
<Icon data-accessible-name accessibleName="Settings" name={settingsIcon} />
97+
<Icon data-accessible-name accessibleName="Download" name={downloadIcon} />
9098
</Toolbar>
9199
);
92100
}
@@ -97,16 +105,30 @@ export const WithSeparator: Story = {
97105
render(args) {
98106
return (
99107
<Toolbar {...args}>
100-
<Button design={ButtonDesign.Transparent}>Item1</Button>
101-
<Button design={ButtonDesign.Transparent}>Item2</Button>
102-
<Button design={ButtonDesign.Transparent}>Item3</Button>
108+
<Button data-accessible-name design={ButtonDesign.Transparent}>
109+
Item1
110+
</Button>
111+
<Button data-accessible-name design={ButtonDesign.Transparent}>
112+
Item2
113+
</Button>
114+
<Button data-accessible-name design={ButtonDesign.Transparent}>
115+
Item3
116+
</Button>
103117
<ToolbarSeparator />
104-
<Button design={ButtonDesign.Transparent}>Item4</Button>
105-
<Button design={ButtonDesign.Transparent}>Item5</Button>
118+
<Button data-accessible-name design={ButtonDesign.Transparent}>
119+
Item4
120+
</Button>
121+
<Button data-accessible-name design={ButtonDesign.Transparent}>
122+
Item5
123+
</Button>
106124
<ToolbarSeparator />
107-
<Button design={ButtonDesign.Transparent}>Item6</Button>
125+
<Button data-accessible-name design={ButtonDesign.Transparent}>
126+
Item6
127+
</Button>
108128
<ToolbarSeparator />
109-
<Button design={ButtonDesign.Transparent}>Item7</Button>
129+
<Button data-accessible-name design={ButtonDesign.Transparent}>
130+
Item7
131+
</Button>
110132
</Toolbar>
111133
);
112134
}
@@ -125,9 +147,18 @@ export const PopoverInOverflowPopover: Story = {
125147
<>
126148
<Toolbar {...args} style={{ width: '400px' }}>
127149
<Text>Toolbar</Text>
128-
<Button design={ButtonDesign.Transparent}>Button One</Button>
129-
<Button design={ButtonDesign.Transparent}>Button Two</Button>
130-
<Button design={ButtonDesign.Transparent} id="openMenuBtn" onClick={handlePopoverOpenerClick}>
150+
<Button data-accessible-name design={ButtonDesign.Transparent}>
151+
Button One
152+
</Button>
153+
<Button data-accessible-name design={ButtonDesign.Transparent}>
154+
Button Two
155+
</Button>
156+
<Button
157+
data-accessible-name
158+
design={ButtonDesign.Transparent}
159+
id="openMenuBtn"
160+
onClick={handlePopoverOpenerClick}
161+
>
131162
Open Popover (Menu)
132163
</Button>
133164
</Toolbar>
@@ -158,15 +189,25 @@ export const WithOverflowButton: Story = {
158189
<Slider onInput={handleInput} value={value} />
159190
<Toolbar {...args} style={{ width: `calc(100% * ${value / 100})` }}>
160191
<Text>Toolbar</Text>
161-
<Button design={ButtonDesign.Transparent}>Button One</Button>
162-
<Button design={ButtonDesign.Transparent} icon="accept" />
163-
<Button design={ButtonDesign.Transparent}>Button Two</Button>
164-
<Select style={{ width: 'auto' }} />
165-
<Switch />
166-
<Button design={ButtonDesign.Transparent}>Button Three</Button>
167-
<Button design={ButtonDesign.Transparent}>Button Four</Button>
168-
<OverflowToolbarButton icon={editIcon}>Edit</OverflowToolbarButton>
169-
<OverflowToolbarToggleButton design={ButtonDesign.Transparent} icon={favoriteIcon}>
192+
<Button data-accessible-name design={ButtonDesign.Transparent}>
193+
Button One
194+
</Button>
195+
<Button data-accessible-name design={ButtonDesign.Transparent} icon="accept" />
196+
<Button data-accessible-name design={ButtonDesign.Transparent}>
197+
Button Two
198+
</Button>
199+
<Select data-accessible-name style={{ width: 'auto' }} />
200+
<Switch data-accessible-name />
201+
<Button data-accessible-name design={ButtonDesign.Transparent}>
202+
Button Three
203+
</Button>
204+
<Button data-accessible-name design={ButtonDesign.Transparent}>
205+
Button Four
206+
</Button>
207+
<OverflowToolbarButton data-accessible-name icon={editIcon}>
208+
Edit
209+
</OverflowToolbarButton>
210+
<OverflowToolbarToggleButton data-accessible-name design={ButtonDesign.Transparent} icon={favoriteIcon}>
170211
Favorite
171212
</OverflowToolbarToggleButton>
172213
</Toolbar>
@@ -180,32 +221,54 @@ export const OverflowBtns: Story = {
180221
render(args) {
181222
return (
182223
<Toolbar {...args} style={{ width: '500px', ...args.style }}>
183-
<Button design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text always visible">
224+
<Button data-accessible-name design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text always visible">
184225
Default Button
185226
</Button>
186-
<OverflowToolbarButton design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text only visible in popover">
227+
<OverflowToolbarButton
228+
data-accessible-name
229+
design={ButtonDesign.Transparent}
230+
icon={editIcon}
231+
tooltip="Text only visible in popover"
232+
>
187233
OverflowToolbarButton (only visible in popover)
188234
</OverflowToolbarButton>
189-
<ToggleButton design={ButtonDesign.Transparent} icon={favoriteIcon} tooltip="Text always visible">
235+
<ToggleButton
236+
data-accessible-name
237+
design={ButtonDesign.Transparent}
238+
icon={favoriteIcon}
239+
tooltip="Text always visible"
240+
>
190241
Default ToggleButton
191242
</ToggleButton>
192243
<OverflowToolbarToggleButton
244+
data-accessible-name
193245
design={ButtonDesign.Transparent}
194246
icon={favoriteIcon}
195247
tooltip="Text only visible in popover"
196248
>
197249
OverflowToolbarToggleButton (only visible in popover)
198250
</OverflowToolbarToggleButton>
199-
<Button design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text always visible">
251+
<Button data-accessible-name design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text always visible">
200252
Default Button
201253
</Button>
202-
<OverflowToolbarButton design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text only visible in popover">
254+
<OverflowToolbarButton
255+
data-accessible-name
256+
design={ButtonDesign.Transparent}
257+
icon={editIcon}
258+
tooltip="Text only visible in popover"
259+
>
203260
OverflowToolbarButton (only visible in popover)
204261
</OverflowToolbarButton>
205-
<ToggleButton design={ButtonDesign.Transparent} icon={favoriteIcon} tooltip="Text always visible">
262+
<ToggleButton
263+
data-accessible-name
264+
design={ButtonDesign.Transparent}
265+
icon={favoriteIcon}
266+
tooltip="Text always visible"
267+
>
206268
Default ToggleButton
207269
</ToggleButton>
208270
<OverflowToolbarToggleButton
271+
data-accessible-name
209272
design={ButtonDesign.Transparent}
210273
icon={favoriteIcon}
211274
tooltip="Text only visible in popover"

0 commit comments

Comments
 (0)