Skip to content

refactor: replace Toolbar with UI5 Web Component #6061

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

Merged
merged 7 commits into from
Jul 11, 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
2 changes: 1 addition & 1 deletion .storybook/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const replaceSubComps = {
ListItemBase: ['ListItemStandard', 'ListItemCustom', 'ListItemGroup'],
InputSuggestionItem: ['SuggestionItem', 'SuggestionItemGroup'],
NotificationListItemBase: ['NotificationListItem'],
ToolbarItem: ['ToolbarSeparatorV2', 'ToolbarSpacerV2', 'ToolbarButton', 'ToolbarSelect', 'ToolbarSelectOption'],
ToolbarItem: ['ToolbarSeparator', 'ToolbarSpacer', 'ToolbarButton', 'ToolbarSelect', 'ToolbarSelectOption'],
TreeItemBase: ['TreeItem', 'TreeItemCustom'],
AvatarGroupItem: ['Avatar'],
TableFeature: ['TableGrowing', 'TableSelection']
Expand Down
16 changes: 16 additions & 0 deletions docs/MigrationGuide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,22 @@ function MyComponent() {
}
```

### Toolbar

The `Toolbar` component has been replaced with the UI5 Web Components Toolbar component (which was previously exported in this project as `ToolbarV2`).
The old `Toolbar` implementation has been moved to the `@ui5/webcomponents-react-compat` package with all its subcomponents:

- `ToolbarSeparator`
- `ToolbarSpacer`
- `OverflowToolbarButton`
- `OverflowToolbarToggleButton`
- enum `ToolbarDesign`
- enum `ToolbarStyle`

Although the old `Toolbar` is still available in the `@ui5/webcomponents-react-compat` package, we strongly recommend to migrate to the new `Toolbar` instead.

As the new `Toolbar` is a completely different component, we can't offer a proper migration guide, so it's best to check the [Toolbar documentation](?path=/docs/layouts-floorplans-toolbar--docs) and update your implementation accordingly with the new components.

## Components with API Changes

### ActionSheet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@
"TitleLevel": "@ui5/webcomponents/dist/types/TitleLevel.js",
"ToastPlacement": "@ui5/webcomponents/dist/types/ToastPlacement.js",
"ToolbarAlign": "@ui5/webcomponents/dist/types/ToolbarAlign.js",
"ToolbarDesign": "@ui5/webcomponents/dist/types/ToolbarDesign.js",
"ToolbarItemOverflowBehavior": "@ui5/webcomponents/dist/types/ToolbarItemOverflowBehavior.js",
"UploadState": "@ui5/webcomponents-fiori/dist/types/UploadState.js",
"ValueState": "@ui5/webcomponents-base/dist/types/ValueState.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use client';

import { Button } from '@ui5/webcomponents-react';
import type { ButtonDomRef, ButtonPropTypes } from '@ui5/webcomponents-react';
import type { ReactNode } from 'react';
import { forwardRef } from 'react';
import { useOverflowPopoverContext } from '../../internal/OverflowPopoverContext.js';
import type { ButtonDomRef, ButtonPropTypes } from '../../webComponents/index.js';
import { Button } from '../../webComponents/index.js';

export interface OverflowToolbarButtonPropTypes extends Omit<ButtonPropTypes, 'children' | 'icon'> {
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use client';

import type { ToggleButtonDomRef, ToggleButtonPropTypes } from '@ui5/webcomponents-react';
import { ToggleButton } from '@ui5/webcomponents-react';
import type { ReactNode } from 'react';
import { forwardRef } from 'react';
import { useOverflowPopoverContext } from '../../internal/OverflowPopoverContext.js';
import type { ToggleButtonDomRef, ToggleButtonPropTypes } from '../../webComponents/index.js';
import { ToggleButton } from '../../webComponents/index.js';

export interface OverflowToolbarToggleButtonPropTypes extends Omit<ToggleButtonPropTypes, 'children' | 'icon'> {
/**
Expand Down
187 changes: 187 additions & 0 deletions packages/compat/src/components/Toolbar/OverflowPopover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js';
import PopoverPlacement from '@ui5/webcomponents/dist/types/PopoverPlacement.js';
import PopupAccessibleRole from '@ui5/webcomponents/dist/types/PopupAccessibleRole.js';
import iconOverflow from '@ui5/webcomponents-icons/dist/overflow.js';
import type {
ButtonPropTypes,
PopoverDomRef,
ToggleButtonDomRef,
ToggleButtonPropTypes
} from '@ui5/webcomponents-react';
import { Popover, ToggleButton } from '@ui5/webcomponents-react';
import { useCanRenderPortal } from '@ui5/webcomponents-react/dist/internal/ssr.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 { clsx } from 'clsx';
import type { Dispatch, FC, ReactElement, ReactNode, Ref, SetStateAction } from 'react';
import { 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 {
lastVisibleIndex: number;
classes: Record<string, string>;
children: ReactNode[];
portalContainer: Element;
overflowContentRef: Ref<HTMLDivElement>;
numberOfAlwaysVisibleItems?: number;
showMoreText: string;
overflowPopoverRef?: Ref<PopoverDomRef>;
overflowButton?: ReactElement<ToggleButtonPropTypes> | ReactElement<ButtonPropTypes>;
setIsMounted: Dispatch<SetStateAction<boolean>>;
a11yConfig?: ToolbarPropTypes['a11yConfig'];
}

const isPhone = Device.isPhone();

export const OverflowPopover: FC<OverflowPopoverProps> = (props: OverflowPopoverProps) => {
const {
lastVisibleIndex,
classes,
children,
portalContainer,
overflowContentRef,
numberOfAlwaysVisibleItems,
showMoreText,
overflowButton,
overflowPopoverRef,
setIsMounted,
a11yConfig
} = props;
const [pressed, setPressed] = useState(false);
const toggleBtnRef = useRef<ToggleButtonDomRef>(null);
const [componentRef, popoverRef] = useSyncRef(overflowPopoverRef);

useEffect(() => {
setIsMounted(true);
return () => {
setIsMounted(false);
};
}, []);

const handleToggleButtonClick = (e) => {
e.stopPropagation();
setPressed((prev) => {
if (!prev) {
if (popoverRef.current) {
popoverRef.current.opener = e.target;
}
return true;
}
return false;
});
};

const handleBeforeOpen = () => {
if (toggleBtnRef.current) {
toggleBtnRef.current.accessibilityAttributes = { expanded: true, hasPopup: 'menu' };
}
};
const handleAfterOpen = () => {
setPressed(true);
};

const handleClose = (e) => {
if (toggleBtnRef.current) {
toggleBtnRef.current.accessibilityAttributes = { expanded: false, hasPopup: 'menu' };
}
stopPropagation(e);
setPressed(false);
};

useEffect(() => {
const tagName = getUi5TagWithSuffix('ui5-toggle-button');
void customElements.whenDefined(tagName).then(() => {
if (toggleBtnRef.current) {
toggleBtnRef.current.accessibilityAttributes = { expanded: pressed, hasPopup: 'menu' };
}
});
}, []);

const clonedOverflowButtonClick = (e) => {
if (typeof overflowButton?.props?.onClick === 'function') {
overflowButton.props.onClick(e);
}
if (!e.defaultPrevented) {
handleToggleButtonClick(e);
}
};

const canRenderPortal = useCanRenderPortal();

const accessibleRole = (() => {
if (a11yConfig?.overflowPopover?.contentRole) {
return PopupAccessibleRole.None;
}
return a11yConfig?.overflowPopover?.role;
})();

const OverflowPopoverContextProvider = getOverflowPopoverContext().Provider;

return (
<OverflowPopoverContextProvider value={{ inPopover: true }}>
{overflowButton ? (
cloneElement(overflowButton, { onClick: clonedOverflowButtonClick })
) : (
<ToggleButton
ref={toggleBtnRef}
design={ButtonDesign.Transparent}
icon={iconOverflow}
onClick={handleToggleButtonClick}
pressed={pressed}
accessibleName={showMoreText}
tooltip={showMoreText}
data-component-name="ToolbarOverflowButton"
/>
)}
{canRenderPortal &&
createPortal(
<Popover
data-component-name="ToolbarOverflowPopover"
className={clsx(classes.popover, isPhone && classes.popoverPhone)}
placement={PopoverPlacement.Bottom}
ref={componentRef}
open={pressed}
onClose={handleClose}
onBeforeOpen={handleBeforeOpen}
onOpen={handleAfterOpen}
hideArrow
accessibleRole={accessibleRole}
>
<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;
})}
</div>
</Popover>,
portalContainer ?? document.body
)}
</OverflowPopoverContextProvider>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,18 @@ import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js';
import PopupAccessibleRole from '@ui5/webcomponents/dist/types/PopupAccessibleRole.js';
import { setTheme } from '@ui5/webcomponents-base/dist/config/Theme.js';
import menu2Icon from '@ui5/webcomponents-icons/dist/menu2.js';
import type { PopoverDomRef } from '@ui5/webcomponents-react';
import { Button, Input, Text, ToggleButton } from '@ui5/webcomponents-react';
import { ThemingParameters } from '@ui5/webcomponents-react-base';
import { useRef, useState } from 'react';
import type { PopoverDomRef, ToolbarPropTypes } from '../..';
import {
Button,
Input,
OverflowToolbarButton,
Text,
Toolbar,
ToggleButton,
ToolbarSeparator,
ToolbarSpacer,
ToolbarStyle,
OverflowToolbarToggleButton
} from '../..';
import { ToolbarDesign } from '../../enums/index.js';
import { ToolbarDesign } from '../../enums/ToolbarDesign.js';
import { ToolbarStyle } from '../../enums/ToolbarStyle.js';
import { OverflowToolbarButton } from '../OverflowToolbarButton/index.js';
import { OverflowToolbarToggleButton } from '../OverflowToolbarToggleButton/index.js';
import { ToolbarSeparator } from '../ToolbarSeparator/index.js';
import { ToolbarSpacer } from '../ToolbarSpacer/index.js';
import type { ToolbarPropTypes } from './index.js';
import { Toolbar } from './index.js';
import { cssVarToRgb, cypressPassThroughTestsFactory, mountWithCustomTagName } from '@/cypress/support/utils';

interface PropTypes {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,17 @@
import { ArgTypesWithNote, ControlsWithNote, DocsHeader, Footer } from '@sb/components';
import { Canvas, Description, Markdown, Meta } from '@storybook/blocks';
import MessageStripDesign from '@ui5/webcomponents/dist/types/MessageStripDesign.js';
import { MessageStrip } from '@ui5/webcomponents-react';
import * as ComponentStories from './Toolbar.stories';
import SubcomponentsSection from '@sb/docs/SubcomponentsSection.md?raw';
import {
OverflowToolbarButton,
OverflowToolbarToggleButton,
MessageStrip,
ToolbarSpacer,
ToolbarSeparator
} from '../..';
import { OverflowToolbarButton, OverflowToolbarToggleButton, ToolbarSpacer, ToolbarSeparator } from '../..';

<Meta of={ComponentStories} />

<DocsHeader
subComponents={['OverflowToolbarButton', 'OverflowToolbarToggleButton', 'ToolbarSpacer', 'ToolbarSeparator']}
/>

<MessageStrip
design={MessageStripDesign.Critical}
hideCloseButton
children={
<>
This component may be replaced by the <code>ui5-toolbar</code> web-component (currently available as{' '}
<code>ToolbarV2</code>) with our next major release. If you only need to pass components supported by{' '}
<code>ToolbarV2</code> then please consider using <code>ToolbarV2</code> instead of this component.
</>
}
/>

## Example

<Canvas of={ComponentStories.Default} />
Expand Down Expand Up @@ -140,6 +123,8 @@ You can achieve that either by leveraging the `onOverflowChange` event and retri

<details>

{' '}

<summary>Set opener ID via click handler</summary>

```jsx
Expand Down Expand Up @@ -176,9 +161,11 @@ const ToolbarComponent = () => {
```

</details>

<details>

{' '}

<summary>Set opener ID via onOverflowChange handler</summary>

```jsx
Expand Down
Loading
Loading