From a688200572ec42e9f5739098640d9eb489d4d35e Mon Sep 17 00:00:00 2001 From: Marcus Notheis Date: Wed, 10 Jul 2024 15:44:56 +0200 Subject: [PATCH 1/7] refactor: replace `Toolbar` with UI5 Web Component 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` --- .../OverflowToolbarButton/index.tsx | 42 ++ .../OverflowToolbarToggleButton/index.tsx | 44 ++ .../components/Toolbar/OverflowPopover.tsx | 187 ++++++ .../src/components/Toolbar/Toolbar.cy.tsx | 616 ++++++++++++++++++ .../src/components/Toolbar/Toolbar.mdx | 13 +- .../src/components/Toolbar/Toolbar.module.css | 137 ++++ .../components/Toolbar/Toolbar.stories.tsx | 31 +- .../compat/src/components/Toolbar/index.tsx | 428 ++++++++++++ .../ToolbarSeparator.module.css | 5 + .../src/components/ToolbarSeparator/index.tsx | 25 + .../src/components/ToolbarSpacer/index.tsx | 15 + packages/compat/src/enums/ToolbarDesign.ts | 6 + packages/compat/src/enums/ToolbarStyle.ts | 4 + packages/compat/src/index.ts | 9 +- .../src/internal/OverflowPopoverContext.ts | 18 + packages/main/src/enums/index.ts | 2 - packages/main/src/index.ts | 5 - packages/main/src/webComponents/index.ts | 15 +- 18 files changed, 1560 insertions(+), 42 deletions(-) create mode 100644 packages/compat/src/components/OverflowToolbarButton/index.tsx create mode 100644 packages/compat/src/components/OverflowToolbarToggleButton/index.tsx create mode 100644 packages/compat/src/components/Toolbar/OverflowPopover.tsx create mode 100644 packages/compat/src/components/Toolbar/Toolbar.cy.tsx rename packages/{main => compat}/src/components/Toolbar/Toolbar.mdx (97%) create mode 100644 packages/compat/src/components/Toolbar/Toolbar.module.css rename packages/{main => compat}/src/components/Toolbar/Toolbar.stories.tsx (90%) create mode 100644 packages/compat/src/components/Toolbar/index.tsx create mode 100644 packages/compat/src/components/ToolbarSeparator/ToolbarSeparator.module.css create mode 100644 packages/compat/src/components/ToolbarSeparator/index.tsx create mode 100644 packages/compat/src/components/ToolbarSpacer/index.tsx create mode 100644 packages/compat/src/enums/ToolbarDesign.ts create mode 100644 packages/compat/src/enums/ToolbarStyle.ts create mode 100644 packages/compat/src/internal/OverflowPopoverContext.ts diff --git a/packages/compat/src/components/OverflowToolbarButton/index.tsx b/packages/compat/src/components/OverflowToolbarButton/index.tsx new file mode 100644 index 00000000000..1b35588bda8 --- /dev/null +++ b/packages/compat/src/components/OverflowToolbarButton/index.tsx @@ -0,0 +1,42 @@ +'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'; + +export interface OverflowToolbarButtonPropTypes extends Omit { + /** + * Defines the text of the component which is only visible in the overflow area of a `Toolbar`. + * + * **Note:** Although this slot accepts HTML Elements, it is strongly recommended that you only use text in order to preserve the intended design. + */ + children?: ReactNode | ReactNode[]; + /** + * Defines the icon to be displayed as graphical element within the component. The SAP-icons font provides numerous options. + * + * Example: See all the available icons in the Icon Explorer. + */ + icon: string; +} + +/** + * The `OverflowToolbarButton` represents a push button that shows its text only when in the overflow area of a `Toolbar`. + * + * __Note:__ This component is only compatible with the `Toolbar` component and __not__ with `ToolbarV2`. + */ +const OverflowToolbarButton = forwardRef((props, ref) => { + const { children, ...rest } = props; + const { inPopover } = useOverflowPopoverContext(); + + return ( + + ); +}); + +OverflowToolbarButton.displayName = 'OverflowToolbarButton'; + +export { OverflowToolbarButton }; diff --git a/packages/compat/src/components/OverflowToolbarToggleButton/index.tsx b/packages/compat/src/components/OverflowToolbarToggleButton/index.tsx new file mode 100644 index 00000000000..d4619f99277 --- /dev/null +++ b/packages/compat/src/components/OverflowToolbarToggleButton/index.tsx @@ -0,0 +1,44 @@ +'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'; + +export interface OverflowToolbarToggleButtonPropTypes extends Omit { + /** + * Defines the text of the component which is only visible in the overflow area of a `Toolbar`. + * + * **Note:** Although this slot accepts HTML Elements, it is strongly recommended that you only use text in order to preserve the intended design. + */ + children?: ReactNode | ReactNode[]; + /** + * Defines the icon to be displayed as graphical element within the component. The SAP-icons font provides numerous options. + * + * Example: See all the available icons in the Icon Explorer. + */ + icon: string; +} + +/** + * The `OverflowToolbarToggleButton` represents a toggle button that shows its text only when in the overflow area of a `Toolbar`. + * + * __Note:__ This component is only compatible with the `Toolbar` component and __not__ with `ToolbarV2`. + */ +const OverflowToolbarToggleButton = forwardRef( + (props, ref) => { + const { children, ...rest } = props; + const { inPopover } = useOverflowPopoverContext(); + + return ( + + {inPopover ? children : ''} + + ); + } +); + +OverflowToolbarToggleButton.displayName = 'OverflowToolbarToggleButton'; + +export { OverflowToolbarToggleButton }; diff --git a/packages/compat/src/components/Toolbar/OverflowPopover.tsx b/packages/compat/src/components/Toolbar/OverflowPopover.tsx new file mode 100644 index 00000000000..019eff9d66d --- /dev/null +++ b/packages/compat/src/components/Toolbar/OverflowPopover.tsx @@ -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; + children: ReactNode[]; + portalContainer: Element; + overflowContentRef: Ref; + numberOfAlwaysVisibleItems?: number; + showMoreText: string; + overflowPopoverRef?: Ref; + overflowButton?: ReactElement | ReactElement; + setIsMounted: Dispatch>; + a11yConfig?: ToolbarPropTypes['a11yConfig']; +} + +const isPhone = Device.isPhone(); + +export const OverflowPopover: FC = (props: OverflowPopoverProps) => { + const { + lastVisibleIndex, + classes, + children, + portalContainer, + overflowContentRef, + numberOfAlwaysVisibleItems, + showMoreText, + overflowButton, + overflowPopoverRef, + setIsMounted, + a11yConfig + } = props; + const [pressed, setPressed] = useState(false); + const toggleBtnRef = useRef(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 ( + + {overflowButton ? ( + cloneElement(overflowButton, { onClick: clonedOverflowButtonClick }) + ) : ( + + )} + {canRenderPortal && + createPortal( + +
+ {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, { + style: { + height: '0.0625rem', + margin: '0.375rem 0.1875rem', + width: '100%' + } + }); + } + return item; + } + return null; + })} +
+
, + portalContainer ?? document.body + )} +
+ ); +}; diff --git a/packages/compat/src/components/Toolbar/Toolbar.cy.tsx b/packages/compat/src/components/Toolbar/Toolbar.cy.tsx new file mode 100644 index 00000000000..97a4a482c40 --- /dev/null +++ b/packages/compat/src/components/Toolbar/Toolbar.cy.tsx @@ -0,0 +1,616 @@ +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 { 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 { + onOverflowChange: (event: { + toolbarElements: HTMLElement[]; + overflowElements: HTMLCollection; + target: HTMLElement; + }) => void; +} + +const OverflowTestComponent = (props: PropTypes) => { + const { onOverflowChange } = props; + const [width, setWidth] = useState(undefined); + const [additionalChildren, setAdditionalChildren] = useState([]); + const [eventProperties, setEventProperties] = useState({ + toolbarElements: [], + overflowElements: undefined, + target: undefined + }); + + const handleOverflowChange = (e) => { + onOverflowChange(e); + setEventProperties(e); + }; + return ( + <> + { + setWidth(e.target.value); + }} + /> + + ]); + }} + > + Add + + + + + Item1 + + + + Item2 + + + Item3 + + + {additionalChildren} + + +
+ toolbarElements: {eventProperties.toolbarElements.length} + overflowElements: {eventProperties.overflowElements?.length} + + ); +}; + +describe('Toolbar', () => { + it('default', () => { + cy.mount(); + }); + + it('boolean/undefined children', () => { + cy.mount( + + Item1 + {false} + {undefined} + <>{false} + <> + {false} + {undefined} + + + ); + cy.findByText('Item1').should('be.visible'); + }); + + it('support Fragments', () => { + cy.mount( + + <> + Item1 + Item2 + Item3 + + <> + Item4 + + + ); + cy.findByText('Item1').should('be.visible'); + cy.findByText('Item2').should('be.visible'); + cy.findByText('Item3').should('be.visible'); + cy.findByText('Item4').should('be.visible'); + }); + + it('overflow menu', () => { + const onOverflowChange = cy.spy().as('overflowChangeSpy'); + cy.viewport(300, 500); + cy.mount(); + cy.get('@overflowChangeSpy').should('have.been.calledOnce'); + cy.findByTestId('toolbarElements').should('have.text', 2); + cy.findByTestId('overflowElements').should('have.text', 4); + cy.findByText('Item1').should('be.visible'); + cy.get('[data-testid="toolbar-item2"]').should('not.be.visible'); + cy.get('[data-testid="toolbar-item3"]').should('not.be.visible'); + + cy.get(`[ui5-toggle-button]`).click().as('open'); + + cy.findByText('Item1').should('be.visible'); + cy.get('[data-testid="toolbar-item2"]').should('be.visible'); + cy.get('[data-testid="toolbar-item3"]').should('be.visible'); + + cy.viewport(500, 500); + + // fuzzy - remount component instead + // cy.get(`[ui5-toggle-button]`).click(); + cy.mount(); + cy.get('[ui5-popover]').should('not.have.attr', 'open'); + + cy.get('@overflowChangeSpy').should('have.callCount', 2); + cy.findByTestId('toolbarElements').should('have.text', 3); + cy.findByTestId('overflowElements').should('have.text', 3); + + cy.findByTestId('input').shadow().find('input').type('100'); + cy.findByTestId('input').trigger('change'); + cy.findByTestId('input').shadow().find('input').clear({ force: true }); + + cy.get('@overflowChangeSpy').should('have.callCount', 3); + cy.findByTestId('toolbarElements').should('have.text', 0); + cy.findByTestId('overflowElements').should('have.text', 6); + + cy.get('[data-testid="toolbar-item"]').should('not.be.visible'); + cy.get('[data-testid="toolbar-item2"]').should('not.be.visible'); + cy.get('[data-testid="toolbar-item3"]').should('not.be.visible'); + + cy.findByTestId('input').shadow().find('input').type('2000', { force: true }); + cy.findByTestId('input').trigger('change'); + + cy.get('@overflowChangeSpy').should('have.callCount', 4); + cy.findByTestId('toolbarElements').should('have.text', 6); + cy.findByTestId('overflowElements').should('not.have.text'); + + cy.get('[data-testid="toolbar-item"]').should('be.visible'); + cy.get('[data-testid="toolbar-item2"]').should('be.visible'); + cy.get('[data-testid="toolbar-item3"]').should('be.visible'); + + cy.findByTestId('input').shadow().find('input').clear({ force: true }); + cy.findByTestId('input').trigger('change'); + + cy.get('@overflowChangeSpy').should('have.callCount', 5); + cy.findByTestId('toolbarElements').should('have.text', 3); + cy.findByTestId('overflowElements').should('have.text', 3); + + cy.findByText('Add').click(); + + cy.get('@overflowChangeSpy').should('have.callCount', 6); + cy.findByTestId('toolbarElements').should('have.text', 3); + cy.findByTestId('overflowElements').should('have.text', 4); + + cy.findByText('Add').click(); + cy.findByText('Add').click(); + cy.findByText('Add').click(); + cy.findByText('Add').click(); + cy.findByText('Add').click(); + + cy.get('@overflowChangeSpy').should('have.callCount', 11); + cy.findByTestId('toolbarElements').should('have.text', 3); + cy.findByTestId('overflowElements').should('have.text', 9); + + cy.findByText('Remove').click(); + + cy.get('@overflowChangeSpy').should('have.callCount', 12); + cy.findByTestId('toolbarElements').should('have.text', 3); + cy.findByTestId('overflowElements').should('have.text', 8); + + cy.findByText('Remove').click(); + cy.findByText('Remove').click(); + cy.findByText('Remove').click(); + cy.findByText('Remove').click(); + cy.findByText('Remove').click(); + + cy.get('@overflowChangeSpy').should('have.callCount', 17); + cy.findByTestId('toolbarElements').should('have.text', 3); + cy.findByTestId('overflowElements').should('have.text', 3); + + cy.get(`[ui5-toggle-button]`).click(); + + // ToolbarSpacers should not be visible in the popover + cy.get('[data-component-name="ToolbarOverflowPopover"]') + .findByTestId('spacer2') + .should('not.be.visible', { timeout: 100 }); + cy.findByTestId('spacer1').should('exist'); + + // ToolbarSeparator should be displayed with horizontal line + cy.get('[data-component-name="ToolbarOverflowPopover"]').findByTestId('separator').should('be.visible'); + }); + + it('Toolbar click', () => { + const click = cy.spy().as('onClickSpy'); + cy.mount( + + Text + + + ); + cy.findByTestId('tb').click(); + cy.get('@onClickSpy').should('have.been.calledOnce'); + + cy.findByTestId('tb').type('{enter}', { force: true }); + cy.get('@onClickSpy').should('have.been.calledTwice'); + + cy.findByTestId('tb').type(' ', { force: true }); + cy.get('@onClickSpy').should('have.been.calledThrice'); + + cy.findByTestId('input').trigger('keydown', { code: 'Enter' }); + cy.findByTestId('input').trigger('keydown', { code: 'Space' }); + cy.get('@onClickSpy').should('have.been.calledThrice'); + + cy.mount( + + Text + + ); + + cy.findByTestId('tb').click(); + cy.get('@onClickSpy').should('have.been.calledThrice'); + + cy.findByTestId('tb').trigger('keydown', { code: 'Enter' }); + cy.get('@onClickSpy').should('have.been.calledThrice'); + + cy.findByTestId('tb').trigger('keydown', { code: 'Space' }); + cy.get('@onClickSpy').should('have.been.calledThrice'); + }); + + it('ToolbarSpacer', () => { + cy.mount( + + Item1 + + Item2 + Item3 + + ); + cy.findByTestId('spacer').should('have.class', 'spacer').should('have.css', 'flex-grow', '1'); + }); + + it('ToolbarSeparator', () => { + cy.mount( + + Item1 + + Item2 + Item3 + + ); + cy.findByRole('separator').should('be.visible'); + }); + + it('toolbarStyle', () => { + cy.mount( + + Item1 + Item2 + + ); + cy.findByTestId('tb').should('have.css', 'border-bottom-style', 'solid'); + cy.mount( + + Item1 + Item2 + + ); + cy.findByTestId('tb').should('have.css', 'border-bottom-style', 'none'); + }); + + Object.values(ToolbarDesign).forEach((design: ToolbarPropTypes['design']) => { + it(`Design: ${design}`, () => { + cy.mount( + + Item1 + Item2 + + ); + let height = '44px'; //2.75rem + let background = 'rgba(0, 0, 0, 0)'; // transparent + let color = 'rgb(0, 0, 0)'; + + switch (design) { + case 'Info': + height = '32px'; // 2rem + background = cssVarToRgb(ThemingParameters.sapInfobar_NonInteractive_Background); + color = cssVarToRgb(ThemingParameters.sapList_TextColor); + break; + case 'Solid': + background = cssVarToRgb(ThemingParameters.sapToolbar_Background); + break; + } + cy.findByTestId('tb') + .should('have.css', 'height', height) + .should('have.css', 'background-color', background) + .should('have.css', 'color', color); + }); + }); + + it('Design: Info (active)', () => { + cy.mount( + + Item1 + Item2 + + ); + cy.findByTestId('tb') + .should('have.css', 'background-color', cssVarToRgb(ThemingParameters.sapInfobar_Background)) + .should('have.css', 'color', cssVarToRgb(ThemingParameters.sapInfobar_TextColor)); + }); + + it('always visible items', () => { + cy.mount( + + + Item1 + + + Item2 + + + Item3 + + + ); + cy.wait(200); + cy.findAllByTestId('tbi').each(($el) => { + cy.wrap($el).should('not.be.visible'); + }); + cy.get(`[ui5-toggle-button]`).click(); + cy.get('[data-component-name="ToolbarOverflowPopover"]') + .findAllByTestId('tbi') + .each(($el) => { + cy.wrap($el).should('be.visible'); + }); + + cy.mount( + + + Item1 + + + Item2 + + + Item3 + + + ); + cy.wait(200); + cy.findAllByTestId('tbiV').each(($el) => { + cy.wrap($el).should('be.visible'); + }); + cy.get('[data-testid="tbi"]').should('not.be.visible'); + cy.get(`[ui5-toggle-button]`).click(); + cy.get('[data-component-name="ToolbarOverflowPopover"]').findByTestId('tbi').should('be.visible'); + cy.get('[data-component-name="ToolbarOverflowPopover"]').findAllByTestId('tbiV').should('not.exist'); + }); + + it('close on interaction', () => { + const TestComp = () => { + const popoverRef = useRef(null); + return ( + + + + ); + }; + + cy.mount(); + cy.get(`[ui5-toggle-button]`).click(); + cy.wait(200); + cy.get('[data-component-name="ToolbarOverflowPopover"]').findByText('Close').click(); + cy.get('[data-component-name="ToolbarOverflowPopover"]').should('not.be.visible'); + }); + + it('a11y', () => { + cy.mount( + + + + + ); + + cy.get(`[ui5-toggle-button]`) + .find('button') + .should('have.attr', 'aria-expanded', 'false') + .should('have.attr', 'aria-haspopup', 'menu') + .click(); + + cy.get(`[ui5-toggle-button]`).find('button').should('have.attr', 'aria-expanded', 'true'); + }); + + it('custom overflow button', () => { + cy.mount( + } + > + + + + ); + cy.get('[data-component-name="ToolbarOverflowButton"]').should('not.exist'); + cy.findByTestId('btn').should('be.visible').click(); + cy.get('[data-component-name="ToolbarOverflowPopover"]').should('be.visible'); + }); + + it('OverflowToolbarToggleButton & OverflowToolbarButton', () => { + [OverflowToolbarToggleButton, OverflowToolbarButton].forEach((Comp) => { + cy.mount( + + + + Edit2 + + + ); + + cy.findByText('Edit1').should('be.visible').should('have.attr', 'has-icon'); + cy.findByText('Edit1').should('not.have.attr', 'icon-only'); + cy.findByText('Edit2', { timeout: 100 }).should('not.exist'); + cy.findByTestId('ob').should('be.visible').should('have.attr', 'icon-only'); + cy.findByTestId('ob').should('have.attr', 'has-icon'); + + cy.mount( + + + + Edit2 + + + ); + + cy.get('[ui5-toggle-button][icon="overflow"]').click(); + cy.get('[ui5-popover]').findByText('Edit1').should('be.visible').should('have.attr', 'has-icon'); + cy.get('[ui5-popover]').findByText('Edit1').should('not.have.attr', 'icon-only'); + cy.get('[ui5-popover]').findByText('Edit2').should('be.visible').should('have.attr', 'has-icon'); + cy.get('[ui5-popover]').findByText('Edit2').should('not.have.attr', 'icon-only'); + }); + }); + + it('recalc on children change', () => { + const TestComp = (props: ToolbarPropTypes) => { + const [actions, setActions] = useState([]); + return ( + <> + , + , + , + , + , + , + , + + ]); + }} + > + add + + + {actions} + + ); + }; + const overflowChange = cy.spy().as('overflowChange'); + cy.mount(); + + cy.get('[ui5-toggle-button]').should('not.exist'); + cy.findByText('add').click(); + cy.get('[ui5-toggle-button]').should('be.visible'); + cy.wait(50); + cy.findByText('remove').click(); + cy.get('[ui5-toggle-button]').should('not.exist'); + cy.get('@overflowChange').should('have.been.calledOnce'); + }); + + it('Toolbar active use outline or shadow', () => { + cy.mount( + + Text + + ); + cy.findByTestId('tb').should('have.css', 'outlineStyle', 'none'); + cy.findByTestId('tb').should('have.css', 'boxShadow', 'none'); + + cy.findByTestId('tb').realClick(); + cy.findByTestId('tb').should('have.css', 'outlineStyle', 'none'); + cy.findByTestId('tb').should('have.css', 'boxShadow', 'rgb(0, 50, 165) 0px 0px 0px 2px inset'); + + cy.wait(500).then(() => { + cy.findByTestId('tb').blur(); + void setTheme('sap_fiori_3'); + }); + + cy.findByTestId('tb').should('have.css', 'outlineStyle', 'none'); + cy.findByTestId('tb').should('have.css', 'boxShadow', 'none'); + + cy.findByTestId('tb').click(); + cy.findByTestId('tb').should('have.css', 'outlineStyle', 'dotted'); + cy.findByTestId('tb').should('have.css', 'boxShadow', 'none'); + }); + + it('unique ids for overflow', () => { + cy.viewport(100, 500); + cy.mount( + +
Text1
+
Text2 no id
+ +
+ ); + + cy.get('#1').should('have.length', 1); + cy.get('#1-overflow').should('have.length', 1); + cy.findAllByText('Text2 no id').should('have.length', 2).and('not.have.attr', 'id'); + cy.get('#3').should('have.length', 1); + cy.get('#3-overflow').should('have.length', 1); + }); + + it('a11y - role & contentRole', () => { + cy.viewport(100, 500); + cy.mount( + +
Text1
+
Text2
+ +
+ ); + cy.get('section[role="alertdialog"]').should('exist'); + + cy.mount( + +
Text1
+
Text2
+ +
+ ); + cy.get('section').should('not.have.attr', 'role'); + cy.get('[data-component-name="ToolbarOverflowPopoverContent"]').should('have.attr', 'role', 'menu'); + + cy.mount( + +
Text1
+
Text2
+ +
+ ); + cy.get('section').should('not.have.attr', 'role'); + cy.get('[data-component-name="ToolbarOverflowPopoverContent"]').should('have.attr', 'role', 'menu'); + }); + + mountWithCustomTagName(Toolbar); + cypressPassThroughTestsFactory(Toolbar); +}); diff --git a/packages/main/src/components/Toolbar/Toolbar.mdx b/packages/compat/src/components/Toolbar/Toolbar.mdx similarity index 97% rename from packages/main/src/components/Toolbar/Toolbar.mdx rename to packages/compat/src/components/Toolbar/Toolbar.mdx index 125e8b1646a..3661ed0e3e2 100644 --- a/packages/main/src/components/Toolbar/Toolbar.mdx +++ b/packages/compat/src/components/Toolbar/Toolbar.mdx @@ -1,15 +1,10 @@ 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 '../..'; @@ -140,6 +135,7 @@ You can achieve that either by leveraging the `onOverflowChange` event and retri
+{' '} Set opener ID via click handler ```jsx @@ -176,9 +172,10 @@ const ToolbarComponent = () => { ```
- +
+{' '} Set opener ID via onOverflowChange handler ```jsx diff --git a/packages/compat/src/components/Toolbar/Toolbar.module.css b/packages/compat/src/components/Toolbar/Toolbar.module.css new file mode 100644 index 00000000000..80740722b48 --- /dev/null +++ b/packages/compat/src/components/Toolbar/Toolbar.module.css @@ -0,0 +1,137 @@ +.outerContainer { + box-sizing: border-box; + width: 100%; + max-width: 100%; + height: var(--_ui5wcr-ToolbarHeight); + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + border-block-end: 0.0625rem solid var(--sapGroup_TitleBorderColor); + overflow: hidden; +} + +.hasOverflow { + .toolbar { + max-width: calc(100% - 44px); + } +} + +.clear { + border-block-end: none; +} + +.active { + cursor: pointer; + + &:hover { + background-color: var(--sapList_Hover_Background); + } + + &:focus { + outline: var(--_ui5wcr_Toolbar_FocusOutline); + outline-offset: -0.1875rem; + box-shadow: var(--_ui5wcr_Toolbar_FocusShadow); + } + + &:active { + background-color: var(--sapActiveColor); + } +} + +.info { + height: 2rem; + background-color: var(--sapInfobar_NonInteractive_Background); + color: var(--sapList_TextColor); + + &.active { + outline-color: var(--sapContent_ContrastFocusColor); + background-color: var(--sapInfobar_Background); + color: var(--sapInfobar_TextColor); + + &:hover { + background-color: var(--sapInfobar_Hover_Background); + } + + &:active { + background-color: var(--sapInfobar_Active_Background); + } + } +} + +.solid { + background-color: var(--sapToolbar_Background); +} + +.transparent { + background-color: transparent; +} + +.toolbar { + display: flex; + align-items: center; + width: 100%; + max-width: 100%; + + > :first-child:not(:global(.spacer)) { + margin-inline: 0.5rem 0.25rem; + } + + > :last-child:not(:global(.spacer)) { + margin-inline: 0.25rem 0.5rem; + } + + > *:not(:first-child):not(:last-child):not(:global(.spacer)) { + margin-inline: 0.25rem; + } +} + +.overflowButtonContainer { + display: flex; + margin-inline: 0 0.5rem; +} + +.popover { + &[ui5-popover]::part(content) { + padding: 0; + } +} + +.popoverPhone { + width: calc(100% - 10px); + max-width: calc(100% - 10px); + inset-inline-start: 5px !important; +} + +.popoverContent { + padding: var(--_ui5wcr-ToolbarPopoverContentPadding); + display: flex; + flex-direction: column; + + > [ui5-toggle-button], + > [ui5-button] { + margin-block: 0.25rem; + } + + > [ui5-button]::part(button), + > [ui5-toggle-button]::part(button) { + justify-content: flex-start; + } + + > [ui5-button][icon-only]::part(button), + > [ui5-toggle-button][icon-only]::part(button) { + padding: revert; + } + + &:last-child { + margin-block-end: 0; + } + + &:first-child { + margin-block-start: 0; + } +} + +.childContainer { + display: flex; +} diff --git a/packages/main/src/components/Toolbar/Toolbar.stories.tsx b/packages/compat/src/components/Toolbar/Toolbar.stories.tsx similarity index 90% rename from packages/main/src/components/Toolbar/Toolbar.stories.tsx rename to packages/compat/src/components/Toolbar/Toolbar.stories.tsx index ccabda56677..111e311da56 100644 --- a/packages/main/src/components/Toolbar/Toolbar.stories.tsx +++ b/packages/compat/src/components/Toolbar/Toolbar.stories.tsx @@ -4,21 +4,24 @@ import downloadIcon from '@ui5/webcomponents-icons/dist/download.js'; import editIcon from '@ui5/webcomponents-icons/dist/edit.js'; import favoriteIcon from '@ui5/webcomponents-icons/dist/favorite.js'; import settingsIcon from '@ui5/webcomponents-icons/dist/settings.js'; +import { + Button, + DatePicker, + Icon, + Input, + Menu, + MenuItem, + Select, + Slider, + Switch, + Text, + ToggleButton +} from '@ui5/webcomponents-react'; import { useState } from 'react'; -import { ToolbarDesign, ToolbarStyle } from '../../enums/index.js'; -import { Button } from '../../webComponents/Button/index.js'; -import { DatePicker } from '../../webComponents/DatePicker/index.js'; -import { Icon } from '../../webComponents/Icon/index.js'; -import { Input } from '../../webComponents/Input/index.js'; -import { Menu } from '../../webComponents/Menu/index.js'; -import { MenuItem } from '../../webComponents/MenuItem/index.js'; -import { Select } from '../../webComponents/Select/index.js'; -import { Slider } from '../../webComponents/Slider/index.js'; -import { Switch } from '../../webComponents/Switch/index.js'; -import { Text } from '../../webComponents/Text/index.js'; -import { ToggleButton } from '../../webComponents/ToggleButton/index.js'; -import { OverflowToolbarButton } from '../OverflowToolbarButton'; -import { OverflowToolbarToggleButton } from '../OverflowToolbarToggleButton'; +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 { Toolbar } from './index.js'; diff --git a/packages/compat/src/components/Toolbar/index.tsx b/packages/compat/src/components/Toolbar/index.tsx new file mode 100644 index 00000000000..f443218a1cd --- /dev/null +++ b/packages/compat/src/components/Toolbar/index.tsx @@ -0,0 +1,428 @@ +'use client'; + +import type PopupAccessibleRole from '@ui5/webcomponents/dist/types/PopupAccessibleRole.js'; +import type { ButtonPropTypes, CommonProps, PopoverDomRef, ToggleButtonPropTypes } from '@ui5/webcomponents-react'; +import { SHOW_MORE } from '@ui5/webcomponents-react/dist/i18n/i18n-defaults.js'; +import { flattenFragments } from '@ui5/webcomponents-react/dist/internal/utils.js'; +import { + debounce, + useI18nBundle, + useIsomorphicLayoutEffect, + useStylesheet, + useSyncRef +} from '@ui5/webcomponents-react-base'; +import { clsx } from 'clsx'; +import type { ElementType, HTMLAttributes, ReactElement, ReactNode, Ref, RefObject } from 'react'; +import { + Children, + cloneElement, + createRef, + forwardRef, + useCallback, + useEffect, + useMemo, + useRef, + useState +} from 'react'; +import { ToolbarDesign } from '../../enums/ToolbarDesign.js'; +import { ToolbarStyle } from '../../enums/ToolbarStyle.js'; +import { OverflowPopover } from './OverflowPopover.js'; +import { classNames, styleData } from './Toolbar.module.css.js'; + +export interface ToolbarPropTypes extends Omit { + /** + * Defines the content of the `Toolbar`. + * + * __Note:__ Although this prop accepts all `ReactNode` types, it is strongly recommended to not pass `string`, `number` or a React Portal to it. + * + * __Note:__ Only components displayed inside the Toolbar are supported as children, i.e. elements positioned outside the normal flow of the document (like dialogs or popovers), can cause undesired behavior. + */ + children?: ReactNode | ReactNode[]; + /** + * Defines the button shown when the `Toolbar` goes into overflow. + * + * __Note:__ It is strongly recommended that you only use `ToggleButton` in icon only mode in order to preserve the intended design. + * + * __Note:__ Per default a `ToggleButton` with the `"overflow"` icon and all necessary a11y attributes will be rendered. + */ + overflowButton?: ReactElement | ReactElement; + /** + * Defines the visual style of the `Toolbar`. + * + * __Note:__ The visual styles are theme-dependent. + */ + toolbarStyle?: ToolbarStyle | keyof typeof ToolbarStyle; + /** + * Defines the `Toolbar` design.
+ * Note: Design settings are theme-dependent. + */ + design?: ToolbarDesign | keyof typeof ToolbarDesign; + /** + * Indicates that the whole `Toolbar` is clickable. The Press event is fired only if `active` is set to true. + */ + active?: boolean; + /** + * Sets the components outer HTML tag. + * + * __Note:__ For TypeScript the types of `ref` are bound to the default tag name, if you change it you are responsible to set the respective types yourself. + */ + as?: keyof HTMLElementTagNameMap; + /** + * Defines where modals are rendered into via `React.createPortal`. + * + * You can find out more about this [here](https://sap.github.io/ui5-webcomponents-react/?path=/docs/knowledge-base-working-with-portals--page). + * + * Defaults to: `document.body` + */ + portalContainer?: Element; + /** + * Defines the number of items inside the toolbar which should always be visible. + * _E.g.: `numberOfAlwaysVisibleItems={3}` would always show the first three items, no matter the size of the toolbar._ + * + * __Note__: To preserve the intended design, it's not recommended to overwrite the `min-width` when using this prop. + */ + numberOfAlwaysVisibleItems?: number; + /** + * Exposes the React Ref of the overflow popover. + * This can be useful, for example, when wanting to close the popover on click or selection of a child element. + */ + overflowPopoverRef?: Ref; + /** + * Defines internally used a11y properties. + * + * __Note:__ When setting `contentRole` of the `overflowPopover`, the `role` is set to `"None"`. + */ + a11yConfig?: { + overflowPopover?: { + /** + * Defines the `accessibleRole` of the overflow `Popover`. + */ + role?: PopupAccessibleRole | keyof typeof PopupAccessibleRole; + /** + * Defines the `role` of the content div inside the overflow `Popover`. + * + * __Note:__ When setting `contentRole`, the `role` is set to `"None"`. + */ + contentRole?: HTMLAttributes['role']; + }; + }; + /** + * Fired if the `active` prop is set to true and the user clicks or presses Enter/Space on the `Toolbar`. + */ + onClick?: (event: CustomEvent) => void; + /** + * Fired when the content of the overflow popover has changed. + */ + onOverflowChange?: (event: { + toolbarElements: HTMLElement[]; + overflowElements: HTMLCollection | undefined; + target: HTMLElement; + }) => void; +} + +function getSpacerWidths(ref) { + if (!ref) { + return 0; + } + + let spacerWidths = 0; + if (ref.dataset.componentName === 'ToolbarSpacer') { + spacerWidths += ref.offsetWidth; + } + return spacerWidths + getSpacerWidths(ref.previousElementSibling); +} + +const OVERFLOW_BUTTON_WIDTH = 36 + 8 + 8; // width + padding end + spacing start + +/** + * + * __Note:__ The `Toolbar` component may be replaced by the `ui5-toolbar` web-component (currently available as `ToolbarV2`) with our next major release. If you only need to pass components supported by `ToolbarV2` then please consider using `ToolbarV2` instead of this component. + * + * ___ + * + * Horizontal container most commonly used to display buttons, labels, selects and various other input controls. + * + * The content of the `Toolbar` moves into the overflow area from right to left when the available space is not enough in the visible area of the container. + * It can be accessed by the user through the overflow button that opens it in a popover. + * + * __Note:__ The overflow popover is mounted only when the overflow button is displayed, i.e., any child component of the popover will be remounted, when moved into it. + * + * __Note:__ To prevent duplicate child `id`s in the DOM, all child `id`s get an `-overflow` suffix. This is especially important when popovers are opened by id. + */ +const Toolbar = forwardRef((props, ref) => { + const { + children, + toolbarStyle = ToolbarStyle.Standard, + design = ToolbarDesign.Auto, + active = false, + style, + className, + onClick, + slot, + as = 'div', + portalContainer, + numberOfAlwaysVisibleItems = 0, + onOverflowChange, + overflowPopoverRef, + overflowButton, + a11yConfig, + ...rest + } = props; + + useStylesheet(styleData, Toolbar.displayName); + const [componentRef, outerContainer] = useSyncRef(ref); + const controlMetaData = useRef([]); + const [lastVisibleIndex, setLastVisibleIndex] = useState(null); + const [isPopoverMounted, setIsPopoverMounted] = useState(false); + const contentRef = useRef(null); + const overflowContentRef = useRef(null); + const overflowBtnRef = useRef(null); + const [minWidth, setMinWidth] = useState('0'); + + const i18nBundle = useI18nBundle('@ui5/webcomponents-react'); + const showMoreText = i18nBundle.getText(SHOW_MORE); + + const toolbarClasses = clsx( + classNames.outerContainer, + toolbarStyle === ToolbarStyle.Clear && classNames.clear, + active && classNames.active, + design === ToolbarDesign.Solid && classNames.solid, + design === ToolbarDesign.Transparent && classNames.transparent, + design === ToolbarDesign.Info && classNames.info, + className + ); + const flatChildren = useMemo(() => { + return flattenFragments(children, 10); + }, [children]); + + const childrenWithRef = useMemo(() => { + controlMetaData.current = []; + + return flatChildren.map((item, index) => { + const itemRef: RefObject = createRef(); + // @ts-expect-error: if type is not defined, it's not a spacer + const isSpacer = item?.type?.displayName === 'ToolbarSpacer'; + controlMetaData.current.push({ + ref: itemRef, + isSpacer + }); + if (isSpacer) { + return item; + } + return ( +
+ {item} +
+ ); + }); + }, [flatChildren, controlMetaData, classNames.childContainer]); + + const overflowNeeded = + (lastVisibleIndex || lastVisibleIndex === 0) && + Children.count(childrenWithRef) !== lastVisibleIndex + 1 && + numberOfAlwaysVisibleItems < Children.count(flatChildren); + + useEffect(() => { + let lastElementResizeObserver; + const lastElement = contentRef.current.children[numberOfAlwaysVisibleItems - 1]; + const debouncedObserverFn = debounce(() => { + const spacerWidth = getSpacerWidths(lastElement); + const isRtl = outerContainer.current?.matches(':dir(rtl)'); + if (isRtl) { + setMinWidth( + `${lastElement.offsetParent.offsetWidth - lastElement.offsetLeft + OVERFLOW_BUTTON_WIDTH - spacerWidth}px` + ); + } else { + setMinWidth( + `${ + lastElement.offsetLeft + lastElement.getBoundingClientRect().width + OVERFLOW_BUTTON_WIDTH - spacerWidth + }px` + ); + } + }, 200); + if (numberOfAlwaysVisibleItems && overflowNeeded && lastElement) { + lastElementResizeObserver = new ResizeObserver(debouncedObserverFn); + lastElementResizeObserver.observe(contentRef.current); + } + return () => { + debouncedObserverFn.cancel(); + lastElementResizeObserver?.disconnect(); + }; + }, [numberOfAlwaysVisibleItems, overflowNeeded]); + + const requestAnimationFrameRef = useRef(undefined); + const calculateVisibleItems = useCallback(() => { + requestAnimationFrameRef.current = requestAnimationFrame(() => { + if (!outerContainer.current) return; + const availableWidth = outerContainer.current.getBoundingClientRect().width; + let consumedWidth = 0; + let lastIndex = null; + if (availableWidth - OVERFLOW_BUTTON_WIDTH <= 0) { + lastIndex = -1; + } else { + let prevItemsAreSpacer = true; + controlMetaData.current.forEach((item, index) => { + const currentMeta = controlMetaData.current[index] as { ref: RefObject }; + if (currentMeta && currentMeta.ref && currentMeta.ref.current) { + let nextWidth = currentMeta.ref.current.getBoundingClientRect().width; + nextWidth += index === 0 || index === controlMetaData.current.length - 1 ? 4 : 8; // first & last element = padding: 4px + if (index === controlMetaData.current.length - 1) { + if (consumedWidth + nextWidth <= availableWidth - 8) { + lastIndex = index; + } else if (index === 0 || prevItemsAreSpacer) { + lastIndex = index - 1; + } + } else { + if (consumedWidth + nextWidth <= availableWidth - OVERFLOW_BUTTON_WIDTH) { + lastIndex = index; + } + if ( + consumedWidth < availableWidth - OVERFLOW_BUTTON_WIDTH && + consumedWidth + nextWidth >= availableWidth - OVERFLOW_BUTTON_WIDTH + ) { + lastIndex = index - 1; + } + } + if (prevItemsAreSpacer && !item.isSpacer) { + prevItemsAreSpacer = false; + } + consumedWidth += nextWidth; + } + }); + } + setLastVisibleIndex(lastIndex); + }); + }, [overflowNeeded]); + + useEffect(() => { + const observer = new ResizeObserver(calculateVisibleItems); + + if (outerContainer.current) { + observer.observe(outerContainer.current); + } + return () => { + cancelAnimationFrame(requestAnimationFrameRef.current); + observer.disconnect(); + }; + }, [calculateVisibleItems]); + + useEffect(() => { + if (Children.count(children) > 0) { + calculateVisibleItems(); + } + }, [children]); + + useIsomorphicLayoutEffect(() => { + calculateVisibleItems(); + }, [calculateVisibleItems]); + + const handleToolbarClick = (e) => { + if (active && typeof onClick === 'function') { + const isSpaceEnterDown = e.type === 'keydown' && (e.code === 'Enter' || e.code === 'Space'); + if (isSpaceEnterDown && e.target !== e.currentTarget) { + return; + } + if (e.type === 'click' || isSpaceEnterDown) { + if (isSpaceEnterDown) { + e.preventDefault(); + } + onClick(e); + } + } + }; + + const prevChildren = useRef(flatChildren); + const debouncedOverflowChange = useRef(undefined); + + useEffect(() => { + if (typeof onOverflowChange === 'function') { + debouncedOverflowChange.current = debounce(onOverflowChange, 60); + } + }, [onOverflowChange]); + + useEffect(() => { + const haveChildrenChanged = prevChildren.current.length !== flatChildren.length; + if ((lastVisibleIndex !== null || haveChildrenChanged) && typeof debouncedOverflowChange.current === 'function') { + prevChildren.current = flatChildren; + const toolbarChildren = contentRef.current?.children; + let toolbarElements = []; + let overflowElements; + if (isPopoverMounted) { + overflowElements = overflowContentRef.current?.children; + } + if (toolbarChildren?.length > 0) { + toolbarElements = Array.from(toolbarChildren).filter((item, index) => index <= lastVisibleIndex); + } + debouncedOverflowChange.current({ + toolbarElements, + overflowElements, + target: outerContainer.current + }); + } + return () => { + if (debouncedOverflowChange.current) { + debouncedOverflowChange.current.cancel(); + } + }; + }, [lastVisibleIndex, flatChildren.length, isPopoverMounted]); + + const CustomTag = as as ElementType; + const styleWithMinWidth = minWidth !== '0' ? { minWidth, ...style } : style; + return ( + +
+ {overflowNeeded && + Children.map(childrenWithRef, (item, index) => { + if (index >= lastVisibleIndex + 1 && index > numberOfAlwaysVisibleItems - 1) { + return cloneElement(item as ReactElement, { + style: { visibility: 'hidden', position: 'absolute', pointerEvents: 'none' } + }); + } + return item; + })} + {!overflowNeeded && childrenWithRef} +
+ {overflowNeeded && ( +
+ + {flatChildren} + +
+ )} +
+ ); +}); + +Toolbar.displayName = 'Toolbar'; +export { Toolbar }; diff --git a/packages/compat/src/components/ToolbarSeparator/ToolbarSeparator.module.css b/packages/compat/src/components/ToolbarSeparator/ToolbarSeparator.module.css new file mode 100644 index 00000000000..6c975fcd93b --- /dev/null +++ b/packages/compat/src/components/ToolbarSeparator/ToolbarSeparator.module.css @@ -0,0 +1,5 @@ +.separator { + width: 0.0625rem; + height: var(--_ui5wcr-ToolbarSeparatorHeight); + background: var(--sapToolbar_SeparatorColor); +} diff --git a/packages/compat/src/components/ToolbarSeparator/index.tsx b/packages/compat/src/components/ToolbarSeparator/index.tsx new file mode 100644 index 00000000000..847af73987b --- /dev/null +++ b/packages/compat/src/components/ToolbarSeparator/index.tsx @@ -0,0 +1,25 @@ +'use client'; + +import type { CommonProps } from '@ui5/webcomponents-react'; +import { useStylesheet } from '@ui5/webcomponents-react-base'; +import { clsx } from 'clsx'; +import { forwardRef } from 'react'; +import { classNames, styleData } from './ToolbarSeparator.module.css.js'; + +export type ToolbarSeparatorPropTypes = CommonProps; + +/** + * Creates a visual separator between the preceding and succeeding `Toolbar` item. + * + * __Note:__ This component is only compatible with the `Toolbar` component from the `@ui5/webcomponents-react-compat` package. + */ +const ToolbarSeparator = forwardRef((props, ref) => { + const { style, className, ...rest } = props; + + useStylesheet(styleData, ToolbarSeparator.displayName); + const separatorClasses = clsx(classNames.separator, className); + + return
; +}); +ToolbarSeparator.displayName = 'ToolbarSeparator'; +export { ToolbarSeparator }; diff --git a/packages/compat/src/components/ToolbarSpacer/index.tsx b/packages/compat/src/components/ToolbarSpacer/index.tsx new file mode 100644 index 00000000000..edcb79a7b49 --- /dev/null +++ b/packages/compat/src/components/ToolbarSpacer/index.tsx @@ -0,0 +1,15 @@ +import type { CommonProps } from '@ui5/webcomponents-react'; +import { forwardRef } from 'react'; + +export type ToolbarSpacerPropTypes = CommonProps; +/** + * Adds horizontal space between the items used within a `Toolbar`. + * + * __Note:__ This component is only compatible with the `Toolbar` component from the `@ui5/webcomponents-react-compat` package. + */ +const ToolbarSpacer = forwardRef((props, ref) => { + return ; +}); + +ToolbarSpacer.displayName = 'ToolbarSpacer'; +export { ToolbarSpacer }; diff --git a/packages/compat/src/enums/ToolbarDesign.ts b/packages/compat/src/enums/ToolbarDesign.ts new file mode 100644 index 00000000000..63ca9fdd0ea --- /dev/null +++ b/packages/compat/src/enums/ToolbarDesign.ts @@ -0,0 +1,6 @@ +export enum ToolbarDesign { + Auto = 'Auto', + Info = 'Info', + Solid = 'Solid', + Transparent = 'Transparent' +} diff --git a/packages/compat/src/enums/ToolbarStyle.ts b/packages/compat/src/enums/ToolbarStyle.ts new file mode 100644 index 00000000000..4e44261cdfb --- /dev/null +++ b/packages/compat/src/enums/ToolbarStyle.ts @@ -0,0 +1,4 @@ +export enum ToolbarStyle { + Clear = 'Clear', + Standard = 'Standard' +} diff --git a/packages/compat/src/index.ts b/packages/compat/src/index.ts index eddc7052577..dae78941495 100644 --- a/packages/compat/src/index.ts +++ b/packages/compat/src/index.ts @@ -1,8 +1,15 @@ +export * from './components/Loader/index.js'; +export * from './components/OverflowToolbarButton/index.js'; +export * from './components/OverflowToolbarToggleButton/index.js'; export * from './components/Table/index.js'; export * from './components/TableCell/index.js'; export * from './components/TableColumn/index.js'; export * from './components/TableGroupRow/index.js'; export * from './components/TableRow/index.js'; -export * from './components/Loader/index.js'; +export * from './components/Toolbar/index.js'; +export * from './components/ToolbarSeparator/index.js'; +export * from './components/ToolbarSpacer/index.js'; export { LoaderType } from './enums/LoaderType.js'; +export { ToolbarDesign } from './enums/ToolbarDesign.js'; +export { ToolbarStyle } from './enums/ToolbarStyle.js'; diff --git a/packages/compat/src/internal/OverflowPopoverContext.ts b/packages/compat/src/internal/OverflowPopoverContext.ts new file mode 100644 index 00000000000..53e52bdfe34 --- /dev/null +++ b/packages/compat/src/internal/OverflowPopoverContext.ts @@ -0,0 +1,18 @@ +import { createContext, useContext } from 'react'; + +const SYMBOL = Symbol.for('@ui5/webcomponents-react/OverflowPopoverContext'); + +interface IOverflowPopoverContext { + inPopover: boolean; +} + +const OverflowPopoverContext = createContext({ inPopover: false }); + +export function getOverflowPopoverContext(): typeof OverflowPopoverContext { + globalThis[SYMBOL] ??= OverflowPopoverContext; + return globalThis[SYMBOL]; +} + +export function useOverflowPopoverContext() { + return useContext(getOverflowPopoverContext()); +} diff --git a/packages/main/src/enums/index.ts b/packages/main/src/enums/index.ts index 92c5eba4f9b..122177b9e66 100644 --- a/packages/main/src/enums/index.ts +++ b/packages/main/src/enums/index.ts @@ -21,7 +21,5 @@ export * from './ObjectPageMode.js'; export * from './Size.js'; export * from './TextAlign.js'; export * from './Theme.js'; -export * from './ToolbarDesign.js'; -export * from './ToolbarStyle.js'; export * from './ValueColor.js'; export * from './VerticalAlign.js'; diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts index 76d05d179b8..142fe435c8f 100644 --- a/packages/main/src/index.ts +++ b/packages/main/src/index.ts @@ -23,16 +23,11 @@ export * from './components/ObjectPage/index.js'; export * from './components/ObjectPageSection/index.js'; export * from './components/ObjectPageSubSection/index.js'; export * from './components/ObjectStatus/index.js'; -export * from './components/OverflowToolbarButton/index.js'; -export * from './components/OverflowToolbarToggleButton/index.js'; export * from './components/ResponsiveGridLayout/index.js'; export * from './components/SelectDialog/index.js'; export * from './components/SplitterElement/index.js'; export * from './components/SplitterLayout/index.js'; export * from './components/ThemeProvider/index.js'; -export * from './components/Toolbar/index.js'; -export * from './components/ToolbarSeparator/index.js'; -export * from './components/ToolbarSpacer/index.js'; export * from './components/VariantManagement/index.js'; export * from './components/VariantManagement/VariantItem.js'; export { withWebComponent } from './internal/withWebComponent.js'; diff --git a/packages/main/src/webComponents/index.ts b/packages/main/src/webComponents/index.ts index 83d31615c18..f5edac18668 100644 --- a/packages/main/src/webComponents/index.ts +++ b/packages/main/src/webComponents/index.ts @@ -108,18 +108,9 @@ export * from './Tokenizer/index.js'; export * from './ToolbarButton/index.js'; export * from './ToolbarSelect/index.js'; export * from './ToolbarSelectOption/index.js'; -export { ToolbarSeparator as ToolbarSeparatorV2 } from './ToolbarSeparator/index.js'; -export type { - ToolbarSeparatorPropTypes as ToolbarSeparatorV2PropTypes, - ToolbarSeparatorDomRef as ToolbarSeparatorV2DomRef -} from './ToolbarSeparator/index.js'; -export { ToolbarSpacer as ToolbarSpacerV2 } from './ToolbarSpacer/index.js'; -export type { - ToolbarSpacerPropTypes as ToolbarSpacerV2PropTypes, - ToolbarSpacerDomRef as ToolbarSpacerV2DomRef -} from './ToolbarSpacer/index.js'; -export { Toolbar as ToolbarV2 } from './Toolbar/index.js'; -export type { ToolbarPropTypes as ToolbarV2PropTypes, ToolbarDomRef as ToolbarV2DomRef } from './Toolbar/index.js'; +export * from './ToolbarSeparator/index.js'; +export * from './ToolbarSpacer/index.js'; +export * from './Toolbar/index.js'; export * from './Text/index.js'; export * from './Tree/index.js'; export * from './TreeItem/index.js'; From ef404ad0d188e966578b98da6d1b8afe690c9261 Mon Sep 17 00:00:00 2001 From: Marcus Notheis Date: Wed, 10 Jul 2024 17:20:04 +0200 Subject: [PATCH 2/7] component cleanup --- .../OverflowToolbarButton/index.tsx | 42 -- .../OverflowToolbarToggleButton/index.tsx | 44 -- .../src/components/Toolbar/Toolbar.cy.tsx | 620 ------------------ 3 files changed, 706 deletions(-) delete mode 100644 packages/main/src/components/OverflowToolbarButton/index.tsx delete mode 100644 packages/main/src/components/OverflowToolbarToggleButton/index.tsx delete mode 100644 packages/main/src/components/Toolbar/Toolbar.cy.tsx diff --git a/packages/main/src/components/OverflowToolbarButton/index.tsx b/packages/main/src/components/OverflowToolbarButton/index.tsx deleted file mode 100644 index 0eeb424bf39..00000000000 --- a/packages/main/src/components/OverflowToolbarButton/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client'; - -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 { - /** - * Defines the text of the component which is only visible in the overflow area of a `Toolbar`. - * - * **Note:** Although this slot accepts HTML Elements, it is strongly recommended that you only use text in order to preserve the intended design. - */ - children?: ReactNode | ReactNode[]; - /** - * Defines the icon to be displayed as graphical element within the component. The SAP-icons font provides numerous options. - * - * Example: See all the available icons in the Icon Explorer. - */ - icon: string; -} - -/** - * The `OverflowToolbarButton` represents a push button that shows its text only when in the overflow area of a `Toolbar`. - * - * __Note:__ This component is only compatible with the `Toolbar` component and __not__ with `ToolbarV2`. - */ -const OverflowToolbarButton = forwardRef((props, ref) => { - const { children, ...rest } = props; - const { inPopover } = useOverflowPopoverContext(); - - return ( - - ); -}); - -OverflowToolbarButton.displayName = 'OverflowToolbarButton'; - -export { OverflowToolbarButton }; diff --git a/packages/main/src/components/OverflowToolbarToggleButton/index.tsx b/packages/main/src/components/OverflowToolbarToggleButton/index.tsx deleted file mode 100644 index 0c358370847..00000000000 --- a/packages/main/src/components/OverflowToolbarToggleButton/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client'; - -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 { - /** - * Defines the text of the component which is only visible in the overflow area of a `Toolbar`. - * - * **Note:** Although this slot accepts HTML Elements, it is strongly recommended that you only use text in order to preserve the intended design. - */ - children?: ReactNode | ReactNode[]; - /** - * Defines the icon to be displayed as graphical element within the component. The SAP-icons font provides numerous options. - * - * Example: See all the available icons in the Icon Explorer. - */ - icon: string; -} - -/** - * The `OverflowToolbarToggleButton` represents a toggle button that shows its text only when in the overflow area of a `Toolbar`. - * - * __Note:__ This component is only compatible with the `Toolbar` component and __not__ with `ToolbarV2`. - */ -const OverflowToolbarToggleButton = forwardRef( - (props, ref) => { - const { children, ...rest } = props; - const { inPopover } = useOverflowPopoverContext(); - - return ( - - {inPopover ? children : ''} - - ); - } -); - -OverflowToolbarToggleButton.displayName = 'OverflowToolbarToggleButton'; - -export { OverflowToolbarToggleButton }; diff --git a/packages/main/src/components/Toolbar/Toolbar.cy.tsx b/packages/main/src/components/Toolbar/Toolbar.cy.tsx deleted file mode 100644 index d0b9d5d876d..00000000000 --- a/packages/main/src/components/Toolbar/Toolbar.cy.tsx +++ /dev/null @@ -1,620 +0,0 @@ -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 { 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 { cssVarToRgb, cypressPassThroughTestsFactory, mountWithCustomTagName } from '@/cypress/support/utils'; - -interface PropTypes { - onOverflowChange: (event: { - toolbarElements: HTMLElement[]; - overflowElements: HTMLCollection; - target: HTMLElement; - }) => void; -} - -const OverflowTestComponent = (props: PropTypes) => { - const { onOverflowChange } = props; - const [width, setWidth] = useState(undefined); - const [additionalChildren, setAdditionalChildren] = useState([]); - const [eventProperties, setEventProperties] = useState({ - toolbarElements: [], - overflowElements: undefined, - target: undefined - }); - - const handleOverflowChange = (e) => { - onOverflowChange(e); - setEventProperties(e); - }; - return ( - <> - { - setWidth(e.target.value); - }} - /> - - ]); - }} - > - Add - - - - - Item1 - - - - Item2 - - - Item3 - - - {additionalChildren} - - -
- toolbarElements: {eventProperties.toolbarElements.length} - overflowElements: {eventProperties.overflowElements?.length} - - ); -}; - -describe('Toolbar', () => { - it('default', () => { - cy.mount(); - }); - - it('boolean/undefined children', () => { - cy.mount( - - Item1 - {false} - {undefined} - <>{false} - <> - {false} - {undefined} - - - ); - cy.findByText('Item1').should('be.visible'); - }); - - it('support Fragments', () => { - cy.mount( - - <> - Item1 - Item2 - Item3 - - <> - Item4 - - - ); - cy.findByText('Item1').should('be.visible'); - cy.findByText('Item2').should('be.visible'); - cy.findByText('Item3').should('be.visible'); - cy.findByText('Item4').should('be.visible'); - }); - - it('overflow menu', () => { - const onOverflowChange = cy.spy().as('overflowChangeSpy'); - cy.viewport(300, 500); - cy.mount(); - cy.get('@overflowChangeSpy').should('have.been.calledOnce'); - cy.findByTestId('toolbarElements').should('have.text', 2); - cy.findByTestId('overflowElements').should('have.text', 4); - cy.findByText('Item1').should('be.visible'); - cy.get('[data-testid="toolbar-item2"]').should('not.be.visible'); - cy.get('[data-testid="toolbar-item3"]').should('not.be.visible'); - - cy.get(`[ui5-toggle-button]`).click().as('open'); - - cy.findByText('Item1').should('be.visible'); - cy.get('[data-testid="toolbar-item2"]').should('be.visible'); - cy.get('[data-testid="toolbar-item3"]').should('be.visible'); - - cy.viewport(500, 500); - - // fuzzy - remount component instead - // cy.get(`[ui5-toggle-button]`).click(); - cy.mount(); - cy.get('[ui5-popover]').should('not.have.attr', 'open'); - - cy.get('@overflowChangeSpy').should('have.callCount', 2); - cy.findByTestId('toolbarElements').should('have.text', 3); - cy.findByTestId('overflowElements').should('have.text', 3); - - cy.findByTestId('input').shadow().find('input').type('100'); - cy.findByTestId('input').trigger('change'); - cy.findByTestId('input').shadow().find('input').clear({ force: true }); - - cy.get('@overflowChangeSpy').should('have.callCount', 3); - cy.findByTestId('toolbarElements').should('have.text', 0); - cy.findByTestId('overflowElements').should('have.text', 6); - - cy.get('[data-testid="toolbar-item"]').should('not.be.visible'); - cy.get('[data-testid="toolbar-item2"]').should('not.be.visible'); - cy.get('[data-testid="toolbar-item3"]').should('not.be.visible'); - - cy.findByTestId('input').shadow().find('input').type('2000', { force: true }); - cy.findByTestId('input').trigger('change'); - - cy.get('@overflowChangeSpy').should('have.callCount', 4); - cy.findByTestId('toolbarElements').should('have.text', 6); - cy.findByTestId('overflowElements').should('not.have.text'); - - cy.get('[data-testid="toolbar-item"]').should('be.visible'); - cy.get('[data-testid="toolbar-item2"]').should('be.visible'); - cy.get('[data-testid="toolbar-item3"]').should('be.visible'); - - cy.findByTestId('input').shadow().find('input').clear({ force: true }); - cy.findByTestId('input').trigger('change'); - - cy.get('@overflowChangeSpy').should('have.callCount', 5); - cy.findByTestId('toolbarElements').should('have.text', 3); - cy.findByTestId('overflowElements').should('have.text', 3); - - cy.findByText('Add').click(); - - cy.get('@overflowChangeSpy').should('have.callCount', 6); - cy.findByTestId('toolbarElements').should('have.text', 3); - cy.findByTestId('overflowElements').should('have.text', 4); - - cy.findByText('Add').click(); - cy.findByText('Add').click(); - cy.findByText('Add').click(); - cy.findByText('Add').click(); - cy.findByText('Add').click(); - - cy.get('@overflowChangeSpy').should('have.callCount', 11); - cy.findByTestId('toolbarElements').should('have.text', 3); - cy.findByTestId('overflowElements').should('have.text', 9); - - cy.findByText('Remove').click(); - - cy.get('@overflowChangeSpy').should('have.callCount', 12); - cy.findByTestId('toolbarElements').should('have.text', 3); - cy.findByTestId('overflowElements').should('have.text', 8); - - cy.findByText('Remove').click(); - cy.findByText('Remove').click(); - cy.findByText('Remove').click(); - cy.findByText('Remove').click(); - cy.findByText('Remove').click(); - - cy.get('@overflowChangeSpy').should('have.callCount', 17); - cy.findByTestId('toolbarElements').should('have.text', 3); - cy.findByTestId('overflowElements').should('have.text', 3); - - cy.get(`[ui5-toggle-button]`).click(); - - // ToolbarSpacers should not be visible in the popover - cy.get('[data-component-name="ToolbarOverflowPopover"]') - .findByTestId('spacer2') - .should('not.be.visible', { timeout: 100 }); - cy.findByTestId('spacer1').should('exist'); - - // ToolbarSeparator should be displayed with horizontal line - cy.get('[data-component-name="ToolbarOverflowPopover"]').findByTestId('separator').should('be.visible'); - }); - - it('Toolbar click', () => { - const click = cy.spy().as('onClickSpy'); - cy.mount( - - Text - - - ); - cy.findByTestId('tb').click(); - cy.get('@onClickSpy').should('have.been.calledOnce'); - - cy.findByTestId('tb').type('{enter}', { force: true }); - cy.get('@onClickSpy').should('have.been.calledTwice'); - - cy.findByTestId('tb').type(' ', { force: true }); - cy.get('@onClickSpy').should('have.been.calledThrice'); - - cy.findByTestId('input').trigger('keydown', { code: 'Enter' }); - cy.findByTestId('input').trigger('keydown', { code: 'Space' }); - cy.get('@onClickSpy').should('have.been.calledThrice'); - - cy.mount( - - Text - - ); - - cy.findByTestId('tb').click(); - cy.get('@onClickSpy').should('have.been.calledThrice'); - - cy.findByTestId('tb').trigger('keydown', { code: 'Enter' }); - cy.get('@onClickSpy').should('have.been.calledThrice'); - - cy.findByTestId('tb').trigger('keydown', { code: 'Space' }); - cy.get('@onClickSpy').should('have.been.calledThrice'); - }); - - it('ToolbarSpacer', () => { - cy.mount( - - Item1 - - Item2 - Item3 - - ); - cy.findByTestId('spacer').should('have.class', 'spacer').should('have.css', 'flex-grow', '1'); - }); - - it('ToolbarSeparator', () => { - cy.mount( - - Item1 - - Item2 - Item3 - - ); - cy.findByRole('separator').should('be.visible'); - }); - - it('toolbarStyle', () => { - cy.mount( - - Item1 - Item2 - - ); - cy.findByTestId('tb').should('have.css', 'border-bottom-style', 'solid'); - cy.mount( - - Item1 - Item2 - - ); - cy.findByTestId('tb').should('have.css', 'border-bottom-style', 'none'); - }); - - Object.values(ToolbarDesign).forEach((design: ToolbarPropTypes['design']) => { - it(`Design: ${design}`, () => { - cy.mount( - - Item1 - Item2 - - ); - let height = '44px'; //2.75rem - let background = 'rgba(0, 0, 0, 0)'; // transparent - let color = 'rgb(0, 0, 0)'; - - switch (design) { - case 'Info': - height = '32px'; // 2rem - background = cssVarToRgb(ThemingParameters.sapInfobar_NonInteractive_Background); - color = cssVarToRgb(ThemingParameters.sapList_TextColor); - break; - case 'Solid': - background = cssVarToRgb(ThemingParameters.sapToolbar_Background); - break; - } - cy.findByTestId('tb') - .should('have.css', 'height', height) - .should('have.css', 'background-color', background) - .should('have.css', 'color', color); - }); - }); - - it('Design: Info (active)', () => { - cy.mount( - - Item1 - Item2 - - ); - cy.findByTestId('tb') - .should('have.css', 'background-color', cssVarToRgb(ThemingParameters.sapInfobar_Background)) - .should('have.css', 'color', cssVarToRgb(ThemingParameters.sapInfobar_TextColor)); - }); - - it('always visible items', () => { - cy.mount( - - - Item1 - - - Item2 - - - Item3 - - - ); - cy.wait(200); - cy.findAllByTestId('tbi').each(($el) => { - cy.wrap($el).should('not.be.visible'); - }); - cy.get(`[ui5-toggle-button]`).click(); - cy.get('[data-component-name="ToolbarOverflowPopover"]') - .findAllByTestId('tbi') - .each(($el) => { - cy.wrap($el).should('be.visible'); - }); - - cy.mount( - - - Item1 - - - Item2 - - - Item3 - - - ); - cy.wait(200); - cy.findAllByTestId('tbiV').each(($el) => { - cy.wrap($el).should('be.visible'); - }); - cy.get('[data-testid="tbi"]').should('not.be.visible'); - cy.get(`[ui5-toggle-button]`).click(); - cy.get('[data-component-name="ToolbarOverflowPopover"]').findByTestId('tbi').should('be.visible'); - cy.get('[data-component-name="ToolbarOverflowPopover"]').findAllByTestId('tbiV').should('not.exist'); - }); - - it('close on interaction', () => { - const TestComp = () => { - const popoverRef = useRef(null); - return ( - - - - ); - }; - - cy.mount(); - cy.get(`[ui5-toggle-button]`).click(); - cy.wait(200); - cy.get('[data-component-name="ToolbarOverflowPopover"]').findByText('Close').click(); - cy.get('[data-component-name="ToolbarOverflowPopover"]').should('not.be.visible'); - }); - - it('a11y', () => { - cy.mount( - - - - - ); - - cy.get(`[ui5-toggle-button]`) - .find('button') - .should('have.attr', 'aria-expanded', 'false') - .should('have.attr', 'aria-haspopup', 'menu') - .click(); - - cy.get(`[ui5-toggle-button]`).find('button').should('have.attr', 'aria-expanded', 'true'); - }); - - it('custom overflow button', () => { - cy.mount( - } - > - - - - ); - cy.get('[data-component-name="ToolbarOverflowButton"]').should('not.exist'); - cy.findByTestId('btn').should('be.visible').click(); - cy.get('[data-component-name="ToolbarOverflowPopover"]').should('be.visible'); - }); - - it('OverflowToolbarToggleButton & OverflowToolbarButton', () => { - [OverflowToolbarToggleButton, OverflowToolbarButton].forEach((Comp) => { - cy.mount( - - - - Edit2 - - - ); - - cy.findByText('Edit1').should('be.visible').should('have.attr', 'has-icon'); - cy.findByText('Edit1').should('not.have.attr', 'icon-only'); - cy.findByText('Edit2', { timeout: 100 }).should('not.exist'); - cy.findByTestId('ob').should('be.visible').should('have.attr', 'icon-only'); - cy.findByTestId('ob').should('have.attr', 'has-icon'); - - cy.mount( - - - - Edit2 - - - ); - - cy.get('[ui5-toggle-button][icon="overflow"]').click(); - cy.get('[ui5-popover]').findByText('Edit1').should('be.visible').should('have.attr', 'has-icon'); - cy.get('[ui5-popover]').findByText('Edit1').should('not.have.attr', 'icon-only'); - cy.get('[ui5-popover]').findByText('Edit2').should('be.visible').should('have.attr', 'has-icon'); - cy.get('[ui5-popover]').findByText('Edit2').should('not.have.attr', 'icon-only'); - }); - }); - - it('recalc on children change', () => { - const TestComp = (props: ToolbarPropTypes) => { - const [actions, setActions] = useState([]); - return ( - <> - , - , - , - , - , - , - , - - ]); - }} - > - add - - - {actions} - - ); - }; - const overflowChange = cy.spy().as('overflowChange'); - cy.mount(); - - cy.get('[ui5-toggle-button]').should('not.exist'); - cy.findByText('add').click(); - cy.get('[ui5-toggle-button]').should('be.visible'); - cy.wait(50); - cy.findByText('remove').click(); - cy.get('[ui5-toggle-button]').should('not.exist'); - cy.get('@overflowChange').should('have.been.calledOnce'); - }); - - it('Toolbar active use outline or shadow', () => { - cy.mount( - - Text - - ); - cy.findByTestId('tb').should('have.css', 'outlineStyle', 'none'); - cy.findByTestId('tb').should('have.css', 'boxShadow', 'none'); - - cy.findByTestId('tb').realClick(); - cy.findByTestId('tb').should('have.css', 'outlineStyle', 'none'); - cy.findByTestId('tb').should('have.css', 'boxShadow', 'rgb(0, 50, 165) 0px 0px 0px 2px inset'); - - cy.wait(500).then(() => { - cy.findByTestId('tb').blur(); - void setTheme('sap_fiori_3'); - }); - - cy.findByTestId('tb').should('have.css', 'outlineStyle', 'none'); - cy.findByTestId('tb').should('have.css', 'boxShadow', 'none'); - - cy.findByTestId('tb').click(); - cy.findByTestId('tb').should('have.css', 'outlineStyle', 'dotted'); - cy.findByTestId('tb').should('have.css', 'boxShadow', 'none'); - }); - - it('unique ids for overflow', () => { - cy.viewport(100, 500); - cy.mount( - -
Text1
-
Text2 no id
- -
- ); - - cy.get('#1').should('have.length', 1); - cy.get('#1-overflow').should('have.length', 1); - cy.findAllByText('Text2 no id').should('have.length', 2).and('not.have.attr', 'id'); - cy.get('#3').should('have.length', 1); - cy.get('#3-overflow').should('have.length', 1); - }); - - it('a11y - role & contentRole', () => { - cy.viewport(100, 500); - cy.mount( - -
Text1
-
Text2
- -
- ); - cy.get('section[role="alertdialog"]').should('exist'); - - cy.mount( - -
Text1
-
Text2
- -
- ); - cy.get('section').should('not.have.attr', 'role'); - cy.get('[data-component-name="ToolbarOverflowPopoverContent"]').should('have.attr', 'role', 'menu'); - - cy.mount( - -
Text1
-
Text2
- -
- ); - cy.get('section').should('not.have.attr', 'role'); - cy.get('[data-component-name="ToolbarOverflowPopoverContent"]').should('have.attr', 'role', 'menu'); - }); - - mountWithCustomTagName(Toolbar); - cypressPassThroughTestsFactory(Toolbar); -}); From e3e9448af60d897b7f61a86c7b62252dffb4e1b2 Mon Sep 17 00:00:00 2001 From: Marcus Notheis Date: Wed, 10 Jul 2024 17:36:31 +0200 Subject: [PATCH 3/7] cleanup imports --- docs/MigrationGuide.mdx | 14 ++++++++++++++ .../src/components/ObjectPageTitle/index.tsx | 10 +++++----- .../main/src/components/SelectDialog/index.tsx | 3 +-- packages/main/src/components/Toolbar/index.tsx | 17 ++++++++--------- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/docs/MigrationGuide.mdx b/docs/MigrationGuide.mdx index 8abda876c14..f90e559a753 100644 --- a/docs/MigrationGuide.mdx +++ b/docs/MigrationGuide.mdx @@ -424,6 +424,20 @@ 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. + ## Components with API Changes ### ActionSheet diff --git a/packages/main/src/components/ObjectPageTitle/index.tsx b/packages/main/src/components/ObjectPageTitle/index.tsx index a429f38c767..802066eeb7f 100644 --- a/packages/main/src/components/ObjectPageTitle/index.tsx +++ b/packages/main/src/components/ObjectPageTitle/index.tsx @@ -4,7 +4,7 @@ import { debounce, Device, useStylesheet, useSyncRef } from '@ui5/webcomponents- import { clsx } from 'clsx'; import type { MouseEventHandler, ReactElement, ReactNode, RefObject } from 'react'; import { Children, cloneElement, forwardRef, isValidElement, useCallback, useEffect, useRef, useState } from 'react'; -import { FlexBoxAlignItems, FlexBoxJustifyContent, ToolbarDesign, ToolbarStyle } from '../../enums/index.js'; +import { FlexBoxAlignItems, FlexBoxJustifyContent } from '../../enums/index.js'; import { stopPropagation } from '../../internal/stopPropagation.js'; import { flattenFragments } from '../../internal/utils.js'; import type { CommonProps } from '../../types/index.js'; @@ -250,8 +250,8 @@ const ObjectPageTitle = forwardRef((pr data-component-name="ObjectPageTitleNavActions" onOverflowChange={navigationActionsToolbarProps?.onOverflowChange} overflowPopoverRef={navActionsOverflowRef} - design={ToolbarDesign.Auto} - toolbarStyle={ToolbarStyle.Clear} + design="Auto" + toolbarStyle="Clear" active > @@ -283,8 +283,8 @@ const ObjectPageTitle = forwardRef((pr role={undefined} {...actionsToolbarProps} overflowButton={actionsToolbarProps?.overflowButton} - design={ToolbarDesign.Auto} - toolbarStyle={ToolbarStyle.Clear} + design="Auto" + toolbarStyle="Clear" active className={clsx(classNames.toolbar, actionsToolbarProps?.className)} onClick={handleActionsToolbarClick} diff --git a/packages/main/src/components/SelectDialog/index.tsx b/packages/main/src/components/SelectDialog/index.tsx index 46d90b5c558..eff1c14b41b 100644 --- a/packages/main/src/components/SelectDialog/index.tsx +++ b/packages/main/src/components/SelectDialog/index.tsx @@ -9,7 +9,6 @@ import { enrichEventWithDetails, useI18nBundle, useStylesheet, useSyncRef } from import { clsx } from 'clsx'; import type { ReactNode } from 'react'; import { forwardRef, useEffect, useState } from 'react'; -import { ToolbarDesign } from '../../enums/index.js'; import { CANCEL, CLEAR, RESET, SEARCH, SELECT, SELECTED } from '../../i18n/i18n-defaults.js'; import type { Ui5CustomEvent } from '../../types/index.js'; import type { @@ -335,7 +334,7 @@ const SelectDialog = forwardRef((props, ref
{selectionMode === ListSelectionMode.Multiple && (!!selectedItems.length || numberOfSelectedItems > 0) && ( - + {`${i18nBundle.getText(SELECTED)}: ${numberOfSelectedItems ?? selectedItems.length}`} )} diff --git a/packages/main/src/components/Toolbar/index.tsx b/packages/main/src/components/Toolbar/index.tsx index 3486c7bb6dc..9b1633c33f7 100644 --- a/packages/main/src/components/Toolbar/index.tsx +++ b/packages/main/src/components/Toolbar/index.tsx @@ -21,7 +21,6 @@ import { useRef, useState } from 'react'; -import { ToolbarDesign, ToolbarStyle } from '../../enums/index.js'; import { SHOW_MORE } from '../../i18n/i18n-defaults.js'; import { flattenFragments } from '../../internal/utils.js'; import type { CommonProps } from '../../types/index.js'; @@ -51,12 +50,12 @@ export interface ToolbarPropTypes extends Omit * Note: Design settings are theme-dependent. */ - design?: ToolbarDesign | keyof typeof ToolbarDesign; + design?: 'Auto' | 'Info' | 'Solid' | 'Transparent'; /** * Indicates that the whole `Toolbar` is clickable. The Press event is fired only if `active` is set to true. */ @@ -152,8 +151,8 @@ const OVERFLOW_BUTTON_WIDTH = 36 + 8 + 8; // width + padding end + spacing start const Toolbar = forwardRef((props, ref) => { const { children, - toolbarStyle = ToolbarStyle.Standard, - design = ToolbarDesign.Auto, + toolbarStyle = 'Standard', + design = 'Auto', active = false, style, className, @@ -184,11 +183,11 @@ const Toolbar = forwardRef((props, ref) => { const toolbarClasses = clsx( classNames.outerContainer, - toolbarStyle === ToolbarStyle.Clear && classNames.clear, + toolbarStyle === 'Clear' && classNames.clear, active && classNames.active, - design === ToolbarDesign.Solid && classNames.solid, - design === ToolbarDesign.Transparent && classNames.transparent, - design === ToolbarDesign.Info && classNames.info, + design === 'Solid' && classNames.solid, + design === 'Transparent' && classNames.transparent, + design === 'Info' && classNames.info, className ); const flatChildren = useMemo(() => { From ba45adf730f9aa7c168efcab5958816e2ce9ed16 Mon Sep 17 00:00:00 2001 From: Marcus Notheis Date: Wed, 10 Jul 2024 17:39:05 +0200 Subject: [PATCH 4/7] more cleanup --- .../main/src/components/FilterBar/FilterDialog.tsx | 10 ++-------- packages/main/src/components/FilterBar/index.tsx | 3 +-- packages/main/src/enums/ToolbarDesign.ts | 6 ------ packages/main/src/enums/ToolbarStyle.ts | 4 ---- 4 files changed, 3 insertions(+), 20 deletions(-) delete mode 100644 packages/main/src/enums/ToolbarDesign.ts delete mode 100644 packages/main/src/enums/ToolbarStyle.ts diff --git a/packages/main/src/components/FilterBar/FilterDialog.tsx b/packages/main/src/components/FilterBar/FilterDialog.tsx index 584a3e4a52e..94bd64f1b89 100644 --- a/packages/main/src/components/FilterBar/FilterDialog.tsx +++ b/packages/main/src/components/FilterBar/FilterDialog.tsx @@ -9,13 +9,7 @@ import { enrichEventWithDetails, useI18nBundle, useIsomorphicId, useStylesheet } import type { Dispatch, ReactElement, RefObject, SetStateAction } from 'react'; import { Children, cloneElement, useEffect, useReducer, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; -import { - FlexBoxDirection, - FlexBoxJustifyContent, - MessageBoxAction, - MessageBoxType, - ToolbarStyle -} from '../../enums/index.js'; +import { FlexBoxDirection, FlexBoxJustifyContent, MessageBoxAction, MessageBoxType } from '../../enums/index.js'; import { ACTIVE, ALL, @@ -525,7 +519,7 @@ export const FilterDialog = (props: FilterDialogPropTypes) => { } > - +