From 722b329b1cba897e9f22adec0e5d1d14f267f352 Mon Sep 17 00:00:00 2001 From: Guilherme Datilio Ribeiro Date: Mon, 5 Feb 2024 12:15:50 -0300 Subject: [PATCH] Menu and Friends workstream (#15398) * feat: added top direction * feat: adding ellipse check * feat: adding alignments to menu * feat: added alignment working * feat: added ellipsis to combobutton * fix: added aalignments and ellipsis * fix: fixed popover * fix: fixed bugs in alignment * fix: fixed prop name * fix: added tests to ComboButton * feat: added story manipulation * test: updated snapshots * fix: changed style file re-use in other components * fix: fixed menu size when the combobutton is larger * fix: added comment * docs: added docs to menubutton * test: updated snapshots * fix: changed css and docs * feat: added menuAlignment to overflowMenu * fix: slipt css for menuAligment due to different block-sizes * test: added test for different files * fix: added new stories for MenuButton * fix: added box shadow to top alignment * fix: fixed visual changes in percy * fix: added the menuAlignment to playground * fix: fixed typo on the scss * Update _menu.scss remove comments * Update packages/react/src/components/Menu/Menu.tsx Co-authored-by: TJ Egan * Update packages/react/src/components/Menu/Menu.tsx Co-authored-by: TJ Egan * Update index.js * Update Menu.tsx * fix: fixed alignment naming * fix: removed console.log * fix: experimental property added to menualignment * test: fixed tests * fix: fixed ref and menuAlignment names * docs: fixed docs * fix: snapshots * Update packages/react/src/components/Menu/Menu.tsx * Update packages/react/src/components/ComboButton/ComboButton.mdx * fix: removed test playground code --------- Co-authored-by: Andrea N. Cardona Co-authored-by: TJ Egan Co-authored-by: Taylor Jones --- .../__snapshots__/PublicAPI-test.js.snap | 29 +++++++++ .../ComboButton/ComboButton-test.js | 45 ++++++++++++- .../components/ComboButton/ComboButton.mdx | 13 ++++ .../ComboButton/ComboButton.stories.js | 64 ++++++++++++++++++- .../react/src/components/ComboButton/index.js | 25 +++++++- packages/react/src/components/Menu/Menu.tsx | 64 +++++++++++++++++++ .../components/MenuButton/MenuButton-test.js | 26 ++++++++ .../src/components/MenuButton/MenuButton.mdx | 13 ++++ .../MenuButton/MenuButton.stories.js | 54 +++++++++++++++- .../react/src/components/MenuButton/index.js | 22 +++++++ .../OverflowMenu.featureflag.stories.js | 64 +++++++++++++++++++ .../OverflowMenu/next/OverflowMenu-test.js | 22 +++++++ .../OverflowMenu/next/OverflowMenu.mdx | 14 ++++ .../src/components/OverflowMenu/next/index.js | 34 +++++++++- .../combo-button/_combo-button.scss | 27 +++++++- .../components/menu-button/_menu-button.scss | 16 +++++ .../overflow-menu/_overflow-menu.scss | 11 ++++ .../styles/scss/utilities/_box-shadow.scss | 4 ++ 18 files changed, 536 insertions(+), 11 deletions(-) diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 99a46bc5b36a..66556fad566e 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -1372,6 +1372,19 @@ Map { "isRequired": true, "type": "string", }, + "menuAlignment": Object { + "args": Array [ + Array [ + "top", + "top-start", + "top-end", + "bottom", + "bottom-start", + "bottom-end", + ], + ], + "type": "oneOf", + }, "onClick": Object { "type": "func", }, @@ -4470,6 +4483,9 @@ Map { "label": Object { "type": "string", }, + "menuAlignment": Object { + "type": "string", + }, "mode": Object { "args": Array [ Array [ @@ -4568,6 +4584,19 @@ Map { "isRequired": true, "type": "string", }, + "menuAlignment": Object { + "args": Array [ + Array [ + "top", + "top-start", + "top-end", + "bottom", + "bottom-start", + "bottom-end", + ], + ], + "type": "oneOf", + }, "size": Object { "args": Array [ Array [ diff --git a/packages/react/src/components/ComboButton/ComboButton-test.js b/packages/react/src/components/ComboButton/ComboButton-test.js index 0189db55fc83..653889445daf 100644 --- a/packages/react/src/components/ComboButton/ComboButton-test.js +++ b/packages/react/src/components/ComboButton/ComboButton-test.js @@ -1,3 +1,4 @@ +/* eslint-disable testing-library/no-node-access */ /** * Copyright IBM Corp. 2023 * @@ -106,7 +107,6 @@ describe('ComboButton', () => { ); - // eslint-disable-next-line testing-library/no-node-access expect(container.firstChild.lastChild).toHaveClass( `${prefix}--popover--${alignment}` ); @@ -114,6 +114,33 @@ describe('ComboButton', () => { }); }); + describe('supports props.menuAlignment', () => { + const alignments = [ + 'top', + 'top-start', + 'top-end', + 'bottom', + 'bottom-start', + 'bottom-end', + ]; + + alignments.forEach((alignment) => { + it(`menuAlignment="${alignment}"`, async () => { + render( + + + + ); + + await userEvent.click(screen.getAllByRole('button')[1]); + + expect(screen.getByRole('menu')).toHaveClass( + `${prefix}--combo-button__${alignment}` + ); + }); + }); + }); + it('supports props.translateWithId', () => { const t = () => 'test'; @@ -125,7 +152,6 @@ describe('ComboButton', () => { const triggerButton = screen.getAllByRole('button')[1]; const tooltipId = triggerButton.getAttribute('aria-labelledby'); - // eslint-disable-next-line testing-library/no-node-access const tooltip = document.getElementById(tooltipId); expect(tooltip).toHaveTextContent(t()); @@ -210,5 +236,20 @@ describe('ComboButton', () => { expect(spy).toHaveBeenCalled(); spy.mockRestore(); }); + + it('supports ellipsis in ComboButton by checking the className', async () => { + render( + + + + + + ); + + expect( + screen.getByTitle('Primary action super long text to enable ellipsis') + .parentElement + ).toHaveClass(`${prefix}--combo-button__primary-action`); + }); }); }); diff --git a/packages/react/src/components/ComboButton/ComboButton.mdx b/packages/react/src/components/ComboButton/ComboButton.mdx index 8325bbc88959..78083d2425e6 100644 --- a/packages/react/src/components/ComboButton/ComboButton.mdx +++ b/packages/react/src/components/ComboButton/ComboButton.mdx @@ -27,6 +27,19 @@ disclosed list next to the primary action. These additional actions must be ``` +## Menu Alignment (experimental) + +The `menuAlignment` prop enables you to define the placement of the Menu in +relation to the `ComboButton`. For instance, setting `menuAlignment="top"` on +the `ComboButton` will render the Menu above the button. + +If it seems your specified `menuAlignment` isn't working, it's because we +prioritize ensuring the Menu remains visible. We calculate the optimal position +to display the Menu in case the provided `menuAlignment` obscures it. + +We encourage you to play around in the Storybook playground to better understand +the `menuAlignment` prop. + ## Component API diff --git a/packages/react/src/components/ComboButton/ComboButton.stories.js b/packages/react/src/components/ComboButton/ComboButton.stories.js index 0f9755520a3e..d4323b6ac49f 100644 --- a/packages/react/src/components/ComboButton/ComboButton.stories.js +++ b/packages/react/src/components/ComboButton/ComboButton.stories.js @@ -30,7 +30,7 @@ export default { export const Default = () => ( - + @@ -38,7 +38,7 @@ export const Default = () => ( export const WithDanger = () => ( - + @@ -53,12 +53,70 @@ export const WithIcons = () => ( ); +export const WithMenuAlignment = () => ( + <> +
+ + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + +
+ +); + export const Playground = (args) => { const onClick = action('onClick (MenuItem)'); return ( - + diff --git a/packages/react/src/components/ComboButton/index.js b/packages/react/src/components/ComboButton/index.js index 403cb1f159e8..6e719ae4b01b 100644 --- a/packages/react/src/components/ComboButton/index.js +++ b/packages/react/src/components/ComboButton/index.js @@ -36,6 +36,7 @@ const ComboButton = React.forwardRef(function ComboButton( label, onClick, size = 'lg', + menuAlignment = 'bottom', tooltipAlignment, translateWithId: t = defaultTranslateWithId, ...rest @@ -44,7 +45,6 @@ const ComboButton = React.forwardRef(function ComboButton( ) { const id = useId('combobutton'); const prefix = usePrefix(); - const containerRef = useRef(null); const menuRef = useRef(null); const ref = useMergedRefs([forwardRef, containerRef]); @@ -74,6 +74,11 @@ const ComboButton = React.forwardRef(function ComboButton( function handleOpen() { menuRef.current.style.inlineSize = `${width}px`; + menuRef.current.style.minInlineSize = `${width}px`; + + if (menuAlignment !== 'bottom' && menuAlignment !== 'top') { + menuRef.current.style.inlineSize = `fit-content`; + } } const containerClasses = classNames( @@ -85,6 +90,8 @@ const ComboButton = React.forwardRef(function ComboButton( className ); + const menuClasses = classNames(`${prefix}--combo-button__${menuAlignment}`); + const primaryActionClasses = classNames( `${prefix}--combo-button__primary-action` ); @@ -98,6 +105,7 @@ const ComboButton = React.forwardRef(function ComboButton( aria-owns={open ? id : null}>
{ ); }; +export const WithMenuAlignment = (args) => { + return ( + <> +
+ + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + +
+ + ); +}; + export const Playground = (args) => { return ( @@ -128,4 +185,11 @@ Playground.argTypes = { disable: true, }, }, + menuAlignment: { + options: ['bottom-start', 'bottom-end', 'top-start', 'top-end'], + control: { type: 'select' }, + description: + 'Specify how the menu should align with the button element `bottom-start` `bottom-end` `top-start` `top-end`', + default: 'bottom-start', + }, }; diff --git a/packages/react/src/components/OverflowMenu/next/OverflowMenu-test.js b/packages/react/src/components/OverflowMenu/next/OverflowMenu-test.js index 90fee14c033c..519c0783a64a 100644 --- a/packages/react/src/components/OverflowMenu/next/OverflowMenu-test.js +++ b/packages/react/src/components/OverflowMenu/next/OverflowMenu-test.js @@ -11,6 +11,8 @@ import { MenuItem } from '../../Menu'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +const prefix = 'cds'; + describe('OverflowMenu (enable-v12-overflowmenu)', () => { it('should render closed by default', () => { render( @@ -103,4 +105,24 @@ describe('OverflowMenu (enable-v12-overflowmenu)', () => { 'false' ); }); + + describe('supports props.menuAlignment', () => { + const alignments = ['top-start', 'top-end', 'bottom-start', 'bottom-end']; + + alignments.forEach((alignment) => { + it(`menuAlignment="${alignment}"`, async () => { + render( + + one + + ); + + await userEvent.click(screen.getByRole('button')); + + expect(screen.getAllByRole('menu')[0]).toHaveClass( + `${prefix}--overflow-menu__${alignment}` + ); + }); + }); + }); }); diff --git a/packages/react/src/components/OverflowMenu/next/OverflowMenu.mdx b/packages/react/src/components/OverflowMenu/next/OverflowMenu.mdx index 2f7d7013462e..ef2984a7399b 100644 --- a/packages/react/src/components/OverflowMenu/next/OverflowMenu.mdx +++ b/packages/react/src/components/OverflowMenu/next/OverflowMenu.mdx @@ -29,6 +29,20 @@ This version of the `OverflowMenu` can be enabled by using the ``` +## Menu Alignment + +The `menuAlignment` prop enables you to define the placement of the Menu in +relation to the `OverflowMenu`. For instance, setting +`menuAlignment="top-start"` on the `OverflowMenu` will render the Menu above the +button. + +If it seems your specified `menuAlignment` isn't working, it's because we +prioritize ensuring the Menu remains visible. We calculate the optimal position +to display the Menu in case the provided `menuAlignment` obscures it. + +We encourage you to play around in the Storybook playground to better understand +the `menuAlignment` prop. + ## Component API diff --git a/packages/react/src/components/OverflowMenu/next/index.js b/packages/react/src/components/OverflowMenu/next/index.js index 331be0ad6275..d9abb0707de2 100644 --- a/packages/react/src/components/OverflowMenu/next/index.js +++ b/packages/react/src/components/OverflowMenu/next/index.js @@ -26,6 +26,7 @@ const OverflowMenu = React.forwardRef(function OverflowMenu( label = 'Options', renderIcon: IconElement = OverflowMenuVertical, size = defaultSize, + menuAlignment = 'bottom-start', tooltipAlignment, ...rest }, @@ -35,14 +36,28 @@ const OverflowMenu = React.forwardRef(function OverflowMenu( const prefix = usePrefix(); const triggerRef = useRef(null); - const { open, x, y, handleClick, handleMousedown, handleClose } = - useAttachedMenu(triggerRef); + const { + open, + x, + y, + handleClick: hookOnClick, + handleMousedown, + handleClose, + } = useAttachedMenu(triggerRef); + + function handleTriggerClick() { + if (triggerRef.current) { + hookOnClick(); + } + } const containerClasses = classNames( className, `${prefix}--overflow-menu__container` ); + const menuClasses = classNames(`${prefix}--overflow-menu__${menuAlignment}`); + const triggerClasses = classNames( `${prefix}--overflow-menu`, { @@ -62,7 +77,7 @@ const OverflowMenu = React.forwardRef(function OverflowMenu( aria-haspopup aria-expanded={open} className={triggerClasses} - onClick={handleClick} + onClick={handleTriggerClick} onMouseDown={handleMousedown} ref={triggerRef} label={label} @@ -70,6 +85,9 @@ const OverflowMenu = React.forwardRef(function OverflowMenu(