Skip to content

Commit 053a0f0

Browse files
committed
fix(Toolbar): announce number of items in overflow popover
1 parent 6fc5199 commit 053a0f0

File tree

3 files changed

+149
-73
lines changed

3 files changed

+149
-73
lines changed

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

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import iconOverflow from '@ui5/webcomponents-icons/dist/overflow.js';
2-
import { Device, useSyncRef } from '@ui5/webcomponents-react-base';
2+
import { Device, useI18nBundle, useSyncRef } from '@ui5/webcomponents-react-base';
33
import { clsx } from 'clsx';
44
import type { Dispatch, FC, ReactElement, ReactNode, Ref, SetStateAction } from 'react';
5-
import { cloneElement, useEffect, useRef, useState } from 'react';
5+
import { isValidElement, cloneElement, useEffect, useRef, useState } from 'react';
66
import { createPortal } from 'react-dom';
77
import { ButtonDesign, PopoverPlacementType, PopupAccessibleRole } from '../../enums/index.js';
8+
import { SHOW_MORE, X_OF_Y } from '../../i18n/i18n-defaults.js';
89
import { getOverflowPopoverContext } from '../../internal/OverflowPopoverContext.js';
910
import { useCanRenderPortal } from '../../internal/ssr.js';
1011
import { stopPropagation } from '../../internal/stopPropagation.js';
@@ -25,7 +26,6 @@ interface OverflowPopoverProps {
2526
portalContainer: Element;
2627
overflowContentRef: Ref<HTMLDivElement>;
2728
numberOfAlwaysVisibleItems?: number;
28-
showMoreText: string;
2929
overflowPopoverRef?: Ref<PopoverDomRef>;
3030
overflowButton?: ReactElement<ToggleButtonPropTypes> | ReactElement<ButtonPropTypes>;
3131
setIsMounted: Dispatch<SetStateAction<boolean>>;
@@ -42,7 +42,6 @@ export const OverflowPopover: FC<OverflowPopoverProps> = (props: OverflowPopover
4242
portalContainer,
4343
overflowContentRef,
4444
numberOfAlwaysVisibleItems,
45-
showMoreText,
4645
overflowButton,
4746
overflowPopoverRef,
4847
setIsMounted,
@@ -51,6 +50,8 @@ export const OverflowPopover: FC<OverflowPopoverProps> = (props: OverflowPopover
5150
const [pressed, setPressed] = useState(false);
5251
const toggleBtnRef = useRef<ToggleButtonDomRef>(null);
5352
const [componentRef, popoverRef] = useSyncRef(overflowPopoverRef);
53+
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
54+
const showMoreText = i18nBundle.getText(SHOW_MORE);
5455

5556
useEffect(() => {
5657
setIsMounted(true);
@@ -118,6 +119,47 @@ export const OverflowPopover: FC<OverflowPopoverProps> = (props: OverflowPopover
118119

119120
const OverflowPopoverContextProvider = getOverflowPopoverContext().Provider;
120121

122+
let startIndex = null;
123+
const filteredChildrenArray = children
124+
.map((item, index, arr) => {
125+
if (index > lastVisibleIndex && index > numberOfAlwaysVisibleItems - 1) {
126+
if (startIndex === null) {
127+
startIndex = index;
128+
}
129+
const labelProp = item?.props?.['data-accessible-name'] ? 'accessibleName' : 'aria-label';
130+
let labelVal = i18nBundle.getText(X_OF_Y, index + 1 - startIndex, arr.length - startIndex);
131+
if (item?.props?.[labelProp]) {
132+
labelVal += ' ' + item.props[labelProp];
133+
}
134+
// @ts-expect-error: if props is not defined, it doesn't have an id (is not a ReactElement)
135+
if (item?.props?.id) {
136+
// @ts-expect-error: item is ReactElement
137+
return cloneElement(item, {
138+
id: `${item.props.id}-overflow`,
139+
[labelProp]: labelVal
140+
});
141+
}
142+
// @ts-expect-error: if type is not defined, it's not a spacer
143+
if (item.type?.displayName === 'ToolbarSeparator') {
144+
return cloneElement(item as ReactElement, {
145+
style: {
146+
height: '0.0625rem',
147+
margin: '0.375rem 0.1875rem',
148+
width: '100%'
149+
},
150+
'aria-label': labelVal
151+
});
152+
}
153+
if (isValidElement(item)) {
154+
return cloneElement(item, {
155+
[labelProp]: labelVal
156+
});
157+
}
158+
}
159+
return null;
160+
})
161+
.filter(Boolean);
162+
121163
return (
122164
<OverflowPopoverContextProvider value={{ inPopover: true }}>
123165
{overflowButton ? (
@@ -147,34 +189,16 @@ export const OverflowPopover: FC<OverflowPopoverProps> = (props: OverflowPopover
147189
onAfterOpen={handleAfterOpen}
148190
hideArrow
149191
accessibleRole={accessibleRole}
192+
//todo translation
193+
accessibleName={`with ${filteredChildrenArray.length} items`}
150194
>
151195
<div
152196
className={classes.popoverContent}
153197
ref={overflowContentRef}
154198
role={a11yConfig?.overflowPopover?.contentRole}
155199
data-component-name="ToolbarOverflowPopoverContent"
156200
>
157-
{children.map((item, index) => {
158-
if (index > lastVisibleIndex && index > numberOfAlwaysVisibleItems - 1) {
159-
// @ts-expect-error: if props is not defined, it doesn't have an id (is not a ReactElement)
160-
if (item?.props?.id) {
161-
// @ts-expect-error: item is ReactElement
162-
return cloneElement(item, { id: `${item.props.id}-overflow` });
163-
}
164-
// @ts-expect-error: if type is not defined, it's not a spacer
165-
if (item.type?.displayName === 'ToolbarSeparator') {
166-
return cloneElement(item as ReactElement, {
167-
style: {
168-
height: '0.0625rem',
169-
margin: '0.375rem 0.1875rem',
170-
width: '100%'
171-
}
172-
});
173-
}
174-
return item;
175-
}
176-
return null;
177-
})}
201+
{filteredChildrenArray}
178202
</div>
179203
</Popover>,
180204
portalContainer ?? document.body

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

Lines changed: 99 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,15 @@ export const Default: Story = {
4747
return (
4848
<Toolbar {...args}>
4949
<Text>Toolbar</Text>
50-
<Button design={ButtonDesign.Transparent}>Button One</Button>
51-
<Button design={ButtonDesign.Transparent}>Button Two</Button>
52-
<Input />
53-
<DatePicker />
54-
<Switch />
50+
<Button data-accessible-name design={ButtonDesign.Transparent}>
51+
Button One
52+
</Button>
53+
<Button data-accessible-name design={ButtonDesign.Transparent}>
54+
Button Two
55+
</Button>
56+
<Input data-accessible-name />
57+
<DatePicker data-accessible-name />
58+
<Switch data-accessible-name />
5559
</Toolbar>
5660
);
5761
}
@@ -63,9 +67,11 @@ export const RightAlignedItems: Story = {
6367
return (
6468
<Toolbar {...args}>
6569
<ToolbarSpacer />
66-
<Button design={ButtonDesign.Transparent}>Button</Button>
67-
<Icon name={settingsIcon} />
68-
<Icon name={downloadIcon} />
70+
<Button data-accessible-name design={ButtonDesign.Transparent}>
71+
Button
72+
</Button>
73+
<Icon data-accessible-name accessibleName="Settings" name={settingsIcon} />
74+
<Icon data-accessible-name accessibleName="Download" name={downloadIcon} />
6975
</Toolbar>
7076
);
7177
}
@@ -78,11 +84,13 @@ export const EvenlyAlignedItems: Story = {
7884
<Toolbar {...args}>
7985
<Text>Left</Text>
8086
<ToolbarSpacer />
81-
<Button design={ButtonDesign.Transparent}>Center</Button>
87+
<Button data-accessible-name design={ButtonDesign.Transparent}>
88+
Center
89+
</Button>
8290
<ToolbarSpacer />
8391
<Text>Right</Text>
84-
<Icon name={settingsIcon} />
85-
<Icon name={downloadIcon} />
92+
<Icon data-accessible-name accessibleName="Settings" name={settingsIcon} />
93+
<Icon data-accessible-name accessibleName="Download" name={downloadIcon} />
8694
</Toolbar>
8795
);
8896
}
@@ -93,16 +101,30 @@ export const WithSeparator: Story = {
93101
render(args) {
94102
return (
95103
<Toolbar {...args}>
96-
<Button design={ButtonDesign.Transparent}>Item1</Button>
97-
<Button design={ButtonDesign.Transparent}>Item2</Button>
98-
<Button design={ButtonDesign.Transparent}>Item3</Button>
104+
<Button data-accessible-name design={ButtonDesign.Transparent}>
105+
Item1
106+
</Button>
107+
<Button data-accessible-name design={ButtonDesign.Transparent}>
108+
Item2
109+
</Button>
110+
<Button data-accessible-name design={ButtonDesign.Transparent}>
111+
Item3
112+
</Button>
99113
<ToolbarSeparator />
100-
<Button design={ButtonDesign.Transparent}>Item4</Button>
101-
<Button design={ButtonDesign.Transparent}>Item5</Button>
114+
<Button data-accessible-name design={ButtonDesign.Transparent}>
115+
Item4
116+
</Button>
117+
<Button data-accessible-name design={ButtonDesign.Transparent}>
118+
Item5
119+
</Button>
102120
<ToolbarSeparator />
103-
<Button design={ButtonDesign.Transparent}>Item6</Button>
121+
<Button data-accessible-name design={ButtonDesign.Transparent}>
122+
Item6
123+
</Button>
104124
<ToolbarSeparator />
105-
<Button design={ButtonDesign.Transparent}>Item7</Button>
125+
<Button data-accessible-name design={ButtonDesign.Transparent}>
126+
Item7
127+
</Button>
106128
</Toolbar>
107129
);
108130
}
@@ -121,9 +143,18 @@ export const PopoverInOverflowPopover: Story = {
121143
<>
122144
<Toolbar {...args} style={{ width: '400px' }}>
123145
<Text>Toolbar</Text>
124-
<Button design={ButtonDesign.Transparent}>Button One</Button>
125-
<Button design={ButtonDesign.Transparent}>Button Two</Button>
126-
<Button design={ButtonDesign.Transparent} id="openMenuBtn" onClick={handlePopoverOpenerClick}>
146+
<Button data-accessible-name design={ButtonDesign.Transparent}>
147+
Button One
148+
</Button>
149+
<Button data-accessible-name design={ButtonDesign.Transparent}>
150+
Button Two
151+
</Button>
152+
<Button
153+
data-accessible-name
154+
design={ButtonDesign.Transparent}
155+
id="openMenuBtn"
156+
onClick={handlePopoverOpenerClick}
157+
>
127158
Open Popover (Menu)
128159
</Button>
129160
</Toolbar>
@@ -154,15 +185,25 @@ export const WithOverflowButton: Story = {
154185
<Slider onInput={handleInput} value={value} />
155186
<Toolbar {...args} style={{ width: `calc(100% * ${value / 100})` }}>
156187
<Text>Toolbar</Text>
157-
<Button design={ButtonDesign.Transparent}>Button One</Button>
158-
<Button design={ButtonDesign.Transparent} icon="accept" />
159-
<Button design={ButtonDesign.Transparent}>Button Two</Button>
160-
<Select style={{ width: 'auto' }} />
161-
<Switch />
162-
<Button design={ButtonDesign.Transparent}>Button Three</Button>
163-
<Button design={ButtonDesign.Transparent}>Button Four</Button>
164-
<OverflowToolbarButton icon={editIcon}>Edit</OverflowToolbarButton>
165-
<OverflowToolbarToggleButton design={ButtonDesign.Transparent} icon={favoriteIcon}>
188+
<Button data-accessible-name design={ButtonDesign.Transparent}>
189+
Button One
190+
</Button>
191+
<Button data-accessible-name design={ButtonDesign.Transparent} icon="accept" />
192+
<Button data-accessible-name design={ButtonDesign.Transparent}>
193+
Button Two
194+
</Button>
195+
<Select data-accessible-name style={{ width: 'auto' }} />
196+
<Switch data-accessible-name />
197+
<Button data-accessible-name design={ButtonDesign.Transparent}>
198+
Button Three
199+
</Button>
200+
<Button data-accessible-name design={ButtonDesign.Transparent}>
201+
Button Four
202+
</Button>
203+
<OverflowToolbarButton data-accessible-name icon={editIcon}>
204+
Edit
205+
</OverflowToolbarButton>
206+
<OverflowToolbarToggleButton data-accessible-name design={ButtonDesign.Transparent} icon={favoriteIcon}>
166207
Favorite
167208
</OverflowToolbarToggleButton>
168209
</Toolbar>
@@ -176,32 +217,54 @@ export const OverflowBtns: Story = {
176217
render(args) {
177218
return (
178219
<Toolbar {...args} style={{ width: '500px', ...args.style }}>
179-
<Button design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text always visible">
220+
<Button data-accessible-name design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text always visible">
180221
Default Button
181222
</Button>
182-
<OverflowToolbarButton design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text only visible in popover">
223+
<OverflowToolbarButton
224+
data-accessible-name
225+
design={ButtonDesign.Transparent}
226+
icon={editIcon}
227+
tooltip="Text only visible in popover"
228+
>
183229
OverflowToolbarButton (only visible in popover)
184230
</OverflowToolbarButton>
185-
<ToggleButton design={ButtonDesign.Transparent} icon={favoriteIcon} tooltip="Text always visible">
231+
<ToggleButton
232+
data-accessible-name
233+
design={ButtonDesign.Transparent}
234+
icon={favoriteIcon}
235+
tooltip="Text always visible"
236+
>
186237
Default ToggleButton
187238
</ToggleButton>
188239
<OverflowToolbarToggleButton
240+
data-accessible-name
189241
design={ButtonDesign.Transparent}
190242
icon={favoriteIcon}
191243
tooltip="Text only visible in popover"
192244
>
193245
OverflowToolbarToggleButton (only visible in popover)
194246
</OverflowToolbarToggleButton>
195-
<Button design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text always visible">
247+
<Button data-accessible-name design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text always visible">
196248
Default Button
197249
</Button>
198-
<OverflowToolbarButton design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text only visible in popover">
250+
<OverflowToolbarButton
251+
data-accessible-name
252+
design={ButtonDesign.Transparent}
253+
icon={editIcon}
254+
tooltip="Text only visible in popover"
255+
>
199256
OverflowToolbarButton (only visible in popover)
200257
</OverflowToolbarButton>
201-
<ToggleButton design={ButtonDesign.Transparent} icon={favoriteIcon} tooltip="Text always visible">
258+
<ToggleButton
259+
data-accessible-name
260+
design={ButtonDesign.Transparent}
261+
icon={favoriteIcon}
262+
tooltip="Text always visible"
263+
>
202264
Default ToggleButton
203265
</ToggleButton>
204266
<OverflowToolbarToggleButton
267+
data-accessible-name
205268
design={ButtonDesign.Transparent}
206269
icon={favoriteIcon}
207270
tooltip="Text only visible in popover"

packages/main/src/components/Toolbar/index.tsx

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
'use client';
22

3-
import {
4-
debounce,
5-
useI18nBundle,
6-
useIsomorphicLayoutEffect,
7-
useStylesheet,
8-
useSyncRef
9-
} from '@ui5/webcomponents-react-base';
3+
import { debounce, useIsomorphicLayoutEffect, useStylesheet, useSyncRef } from '@ui5/webcomponents-react-base';
104
import { clsx } from 'clsx';
115
import type { ElementType, HTMLAttributes, ReactElement, ReactNode, Ref, RefObject } from 'react';
126
import {
@@ -22,7 +16,6 @@ import {
2216
} from 'react';
2317
import type { PopupAccessibleRole } from '../../enums/index.js';
2418
import { ToolbarDesign, ToolbarStyle } from '../../enums/index.js';
25-
import { SHOW_MORE } from '../../i18n/i18n-defaults.js';
2619
import { flattenFragments } from '../../internal/utils.js';
2720
import type { CommonProps } from '../../types/index.js';
2821
import type { ButtonPropTypes, PopoverDomRef, ToggleButtonPropTypes } from '../../webComponents/index.js';
@@ -179,9 +172,6 @@ const Toolbar = forwardRef<HTMLDivElement, ToolbarPropTypes>((props, ref) => {
179172
const overflowBtnRef = useRef(null);
180173
const [minWidth, setMinWidth] = useState('0');
181174

182-
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
183-
const showMoreText = i18nBundle.getText(SHOW_MORE);
184-
185175
const toolbarClasses = clsx(
186176
classNames.outerContainer,
187177
toolbarStyle === ToolbarStyle.Clear && classNames.clear,
@@ -411,7 +401,6 @@ const Toolbar = forwardRef<HTMLDivElement, ToolbarPropTypes>((props, ref) => {
411401
portalContainer={portalContainer}
412402
overflowContentRef={overflowContentRef}
413403
numberOfAlwaysVisibleItems={numberOfAlwaysVisibleItems}
414-
showMoreText={showMoreText}
415404
overflowButton={overflowButton}
416405
setIsMounted={setIsPopoverMounted}
417406
a11yConfig={a11yConfig}

0 commit comments

Comments
 (0)