Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Toolbar - compat): announce number of items in overflow popover #6545

Merged
merged 6 commits into from
Oct 23, 2024
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
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
Loading