Skip to content

Commit

Permalink
refactor: replace Toolbar with UI5 Web Component (#6061)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: the `Toolbar` component and its related components have been moved to the `@ui5/webcomponents-react-compat` package.
BREAKING CHANGE: the `ToolbarV2` component has been renamed to `Toolbar`
BREAKING CHANGE: the `ToolbarSpacerV2` component has been renamed to `ToolbarSpacer`
BREAKING CHANGE: the `ToolbarSeparatorV2` component has been renamed to `ToolbarSeparator`

---------

Co-authored-by: Lukas Harbarth <[email protected]>
  • Loading branch information
MarcusNotheis and Lukas742 authored Jul 11, 2024
1 parent 3822bee commit bf60767
Show file tree
Hide file tree
Showing 28 changed files with 913 additions and 113 deletions.
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

0 comments on commit bf60767

Please sign in to comment.