Skip to content

Commit

Permalink
fix(Toolbar - compat): announce number of items in overflow popover (#…
Browse files Browse the repository at this point in the history
…6545)

Fixes #5926
  • Loading branch information
Lukas742 authored Oct 23, 2024
1 parent 5ce8fe9 commit 60411d6
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 75 deletions.
79 changes: 52 additions & 27 deletions packages/compat/src/components/Toolbar/OverflowPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import type {
ToggleButtonPropTypes
} from '@ui5/webcomponents-react';
import { Popover, ToggleButton } from '@ui5/webcomponents-react';
import { WITH_X_ITEMS, SHOW_MORE, X_OF_Y } from '@ui5/webcomponents-react/dist/i18n/i18n-defaults.js';
import { stopPropagation } from '@ui5/webcomponents-react/dist/internal/stopPropagation.js';
import { getUi5TagWithSuffix } from '@ui5/webcomponents-react/dist/internal/utils.js';
import { Device, useSyncRef } from '@ui5/webcomponents-react-base';
import { Device, useI18nBundle, useSyncRef } from '@ui5/webcomponents-react-base';
import { clsx } from 'clsx';
import type { Dispatch, FC, ReactElement, ReactNode, Ref, SetStateAction } from 'react';
import { cloneElement, useEffect, useRef, useState } from 'react';
import type { Dispatch, FC, HTMLAttributes, ReactElement, ReactNode, Ref, SetStateAction } from 'react';
import { isValidElement, cloneElement, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { getOverflowPopoverContext } from '../../internal/OverflowPopoverContext.js';
import type { ToolbarSeparatorPropTypes } from '../ToolbarSeparator/index.js';
import type { ToolbarPropTypes } from './index.js';

interface OverflowPopoverProps {
Expand All @@ -27,7 +27,6 @@ interface OverflowPopoverProps {
portalContainer: Element;
overflowContentRef: Ref<HTMLDivElement>;
numberOfAlwaysVisibleItems?: number;
showMoreText: string;
overflowPopoverRef?: Ref<PopoverDomRef>;
overflowButton?: ReactElement<ToggleButtonPropTypes> | ReactElement<ButtonPropTypes>;
setIsMounted: Dispatch<SetStateAction<boolean>>;
Expand All @@ -44,7 +43,6 @@ export const OverflowPopover: FC<OverflowPopoverProps> = (props: OverflowPopover
portalContainer,
overflowContentRef,
numberOfAlwaysVisibleItems,
showMoreText,
overflowButton,
overflowPopoverRef,
setIsMounted,
Expand All @@ -53,6 +51,8 @@ export const OverflowPopover: FC<OverflowPopoverProps> = (props: OverflowPopover
const [pressed, setPressed] = useState(false);
const toggleBtnRef = useRef<ToggleButtonDomRef>(null);
const [componentRef, popoverRef] = useSyncRef(overflowPopoverRef);
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
const showMoreText = i18nBundle.getText(SHOW_MORE);

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

const OverflowPopoverContextProvider = getOverflowPopoverContext().Provider;

let startIndex = null;
const filteredChildrenArray = children
.map((item, index, arr) => {
if (index > lastVisibleIndex && index > numberOfAlwaysVisibleItems - 1 && isValidElement(item)) {
if (startIndex === null) {
startIndex = index;
}
const labelProp = item?.props?.['data-accessible-name'] ? 'accessibleName' : 'aria-label';
let labelVal = i18nBundle.getText(X_OF_Y, index + 1 - startIndex, arr.length - startIndex);
if (item?.props?.[labelProp]) {
labelVal += ' ' + item.props[labelProp];
}

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: React 19
if (item?.props?.id) {
return cloneElement<HTMLAttributes<HTMLElement>>(item, {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: React 19
id: `${item.props.id}-overflow`,
[labelProp]: labelVal
});
}
// @ts-expect-error: if type is not defined, it's not a spacer
if (item.type?.displayName === 'ToolbarSeparator') {
return cloneElement(item as ReactElement, {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: React 19
style: {
height: '0.0625rem',
margin: '0.375rem 0.1875rem',
width: '100%'
},
'aria-label': labelVal
});
}
return cloneElement<HTMLAttributes<HTMLElement>>(item, {
[labelProp]: labelVal
});
}
return null;
})
.filter(Boolean);

return (
<OverflowPopoverContextProvider value={{ inPopover: true }}>
{overflowButton ? (
Expand Down Expand Up @@ -152,34 +196,15 @@ export const OverflowPopover: FC<OverflowPopoverProps> = (props: OverflowPopover
onOpen={handleAfterOpen}
hideArrow
accessibleRole={accessibleRole}
accessibleName={i18nBundle.getText(WITH_X_ITEMS, filteredChildrenArray.length)}
>
<div
className={classes.popoverContent}
ref={overflowContentRef}
role={a11yConfig?.overflowPopover?.contentRole}
data-component-name="ToolbarOverflowPopoverContent"
>
{children.map((item, index) => {
if (index > lastVisibleIndex && index > numberOfAlwaysVisibleItems - 1) {
// @ts-expect-error: if props is not defined, it doesn't have an id (is not a ReactElement)
if (item?.props?.id) {
// @ts-expect-error: item is ReactElement
return cloneElement(item, { id: `${item.props.id}-overflow` });
}
// @ts-expect-error: if type is not defined, it's not a spacer
if (item.type?.displayName === 'ToolbarSeparator') {
return cloneElement(item as ReactElement<ToolbarSeparatorPropTypes>, {
style: {
height: '0.0625rem',
margin: '0.375rem 0.1875rem',
width: '100%'
}
});
}
return item;
}
return null;
})}
{filteredChildrenArray}
</div>
</Popover>,
portalContainer ?? document.body
Expand Down
18 changes: 18 additions & 0 deletions packages/compat/src/components/Toolbar/Toolbar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ import { OverflowToolbarButton, OverflowToolbarToggleButton, ToolbarSpacer, Tool

<ControlsWithNote of={ComponentStories.Default} />

## Announce number of items in overflow popover

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`.

E.g.:

```jsx
<Toolbar>
<Text>Toolbar</Text>
<Button data-accessible-name design={ButtonDesign.Transparent}>
Button One
</Button>
<button>Button Two</button>
<Input data-accessible-name />
<input />
</Toolbar>
```

## Prevent event bubbling of Toolbar items

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.
Expand Down
135 changes: 99 additions & 36 deletions packages/compat/src/components/Toolbar/Toolbar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,15 @@ export const Default: Story = {
return (
<Toolbar {...args}>
<Text>Toolbar</Text>
<Button design={ButtonDesign.Transparent}>Button One</Button>
<Button design={ButtonDesign.Transparent}>Button Two</Button>
<Input />
<DatePicker />
<Switch />
<Button data-accessible-name design={ButtonDesign.Transparent}>
Button One
</Button>
<Button data-accessible-name design={ButtonDesign.Transparent}>
Button Two
</Button>
<Input data-accessible-name />
<DatePicker data-accessible-name />
<Switch data-accessible-name />
</Toolbar>
);
}
Expand All @@ -67,9 +71,11 @@ export const RightAlignedItems: Story = {
return (
<Toolbar {...args}>
<ToolbarSpacer />
<Button design={ButtonDesign.Transparent}>Button</Button>
<Icon name={settingsIcon} />
<Icon name={downloadIcon} />
<Button data-accessible-name design={ButtonDesign.Transparent}>
Button
</Button>
<Icon data-accessible-name accessibleName="Settings" name={settingsIcon} />
<Icon data-accessible-name accessibleName="Download" name={downloadIcon} />
</Toolbar>
);
}
Expand All @@ -82,11 +88,13 @@ export const EvenlyAlignedItems: Story = {
<Toolbar {...args}>
<Text>Left</Text>
<ToolbarSpacer />
<Button design={ButtonDesign.Transparent}>Center</Button>
<Button data-accessible-name design={ButtonDesign.Transparent}>
Center
</Button>
<ToolbarSpacer />
<Text>Right</Text>
<Icon name={settingsIcon} />
<Icon name={downloadIcon} />
<Icon data-accessible-name accessibleName="Settings" name={settingsIcon} />
<Icon data-accessible-name accessibleName="Download" name={downloadIcon} />
</Toolbar>
);
}
Expand All @@ -97,16 +105,30 @@ export const WithSeparator: Story = {
render(args) {
return (
<Toolbar {...args}>
<Button design={ButtonDesign.Transparent}>Item1</Button>
<Button design={ButtonDesign.Transparent}>Item2</Button>
<Button design={ButtonDesign.Transparent}>Item3</Button>
<Button data-accessible-name design={ButtonDesign.Transparent}>
Item1
</Button>
<Button data-accessible-name design={ButtonDesign.Transparent}>
Item2
</Button>
<Button data-accessible-name design={ButtonDesign.Transparent}>
Item3
</Button>
<ToolbarSeparator />
<Button design={ButtonDesign.Transparent}>Item4</Button>
<Button design={ButtonDesign.Transparent}>Item5</Button>
<Button data-accessible-name design={ButtonDesign.Transparent}>
Item4
</Button>
<Button data-accessible-name design={ButtonDesign.Transparent}>
Item5
</Button>
<ToolbarSeparator />
<Button design={ButtonDesign.Transparent}>Item6</Button>
<Button data-accessible-name design={ButtonDesign.Transparent}>
Item6
</Button>
<ToolbarSeparator />
<Button design={ButtonDesign.Transparent}>Item7</Button>
<Button data-accessible-name design={ButtonDesign.Transparent}>
Item7
</Button>
</Toolbar>
);
}
Expand All @@ -125,9 +147,18 @@ export const PopoverInOverflowPopover: Story = {
<>
<Toolbar {...args} style={{ width: '400px' }}>
<Text>Toolbar</Text>
<Button design={ButtonDesign.Transparent}>Button One</Button>
<Button design={ButtonDesign.Transparent}>Button Two</Button>
<Button design={ButtonDesign.Transparent} id="openMenuBtn" onClick={handlePopoverOpenerClick}>
<Button data-accessible-name design={ButtonDesign.Transparent}>
Button One
</Button>
<Button data-accessible-name design={ButtonDesign.Transparent}>
Button Two
</Button>
<Button
data-accessible-name
design={ButtonDesign.Transparent}
id="openMenuBtn"
onClick={handlePopoverOpenerClick}
>
Open Popover (Menu)
</Button>
</Toolbar>
Expand Down Expand Up @@ -158,15 +189,25 @@ export const WithOverflowButton: Story = {
<Slider onInput={handleInput} value={value} />
<Toolbar {...args} style={{ width: `calc(100% * ${value / 100})` }}>
<Text>Toolbar</Text>
<Button design={ButtonDesign.Transparent}>Button One</Button>
<Button design={ButtonDesign.Transparent} icon="accept" />
<Button design={ButtonDesign.Transparent}>Button Two</Button>
<Select style={{ width: 'auto' }} />
<Switch />
<Button design={ButtonDesign.Transparent}>Button Three</Button>
<Button design={ButtonDesign.Transparent}>Button Four</Button>
<OverflowToolbarButton icon={editIcon}>Edit</OverflowToolbarButton>
<OverflowToolbarToggleButton design={ButtonDesign.Transparent} icon={favoriteIcon}>
<Button data-accessible-name design={ButtonDesign.Transparent}>
Button One
</Button>
<Button data-accessible-name design={ButtonDesign.Transparent} icon="accept" />
<Button data-accessible-name design={ButtonDesign.Transparent}>
Button Two
</Button>
<Select data-accessible-name style={{ width: 'auto' }} />
<Switch data-accessible-name />
<Button data-accessible-name design={ButtonDesign.Transparent}>
Button Three
</Button>
<Button data-accessible-name design={ButtonDesign.Transparent}>
Button Four
</Button>
<OverflowToolbarButton data-accessible-name icon={editIcon}>
Edit
</OverflowToolbarButton>
<OverflowToolbarToggleButton data-accessible-name design={ButtonDesign.Transparent} icon={favoriteIcon}>
Favorite
</OverflowToolbarToggleButton>
</Toolbar>
Expand All @@ -180,32 +221,54 @@ export const OverflowBtns: Story = {
render(args) {
return (
<Toolbar {...args} style={{ width: '500px', ...args.style }}>
<Button design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text always visible">
<Button data-accessible-name design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text always visible">
Default Button
</Button>
<OverflowToolbarButton design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text only visible in popover">
<OverflowToolbarButton
data-accessible-name
design={ButtonDesign.Transparent}
icon={editIcon}
tooltip="Text only visible in popover"
>
OverflowToolbarButton (only visible in popover)
</OverflowToolbarButton>
<ToggleButton design={ButtonDesign.Transparent} icon={favoriteIcon} tooltip="Text always visible">
<ToggleButton
data-accessible-name
design={ButtonDesign.Transparent}
icon={favoriteIcon}
tooltip="Text always visible"
>
Default ToggleButton
</ToggleButton>
<OverflowToolbarToggleButton
data-accessible-name
design={ButtonDesign.Transparent}
icon={favoriteIcon}
tooltip="Text only visible in popover"
>
OverflowToolbarToggleButton (only visible in popover)
</OverflowToolbarToggleButton>
<Button design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text always visible">
<Button data-accessible-name design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text always visible">
Default Button
</Button>
<OverflowToolbarButton design={ButtonDesign.Transparent} icon={editIcon} tooltip="Text only visible in popover">
<OverflowToolbarButton
data-accessible-name
design={ButtonDesign.Transparent}
icon={editIcon}
tooltip="Text only visible in popover"
>
OverflowToolbarButton (only visible in popover)
</OverflowToolbarButton>
<ToggleButton design={ButtonDesign.Transparent} icon={favoriteIcon} tooltip="Text always visible">
<ToggleButton
data-accessible-name
design={ButtonDesign.Transparent}
icon={favoriteIcon}
tooltip="Text always visible"
>
Default ToggleButton
</ToggleButton>
<OverflowToolbarToggleButton
data-accessible-name
design={ButtonDesign.Transparent}
icon={favoriteIcon}
tooltip="Text only visible in popover"
Expand Down
Loading

0 comments on commit 60411d6

Please sign in to comment.