From f1c7893e85877554e12ae819623b1b694230a5ec Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Fri, 24 Feb 2023 11:19:50 +0100 Subject: [PATCH 01/43] feat: add experimental combo-button component --- .../components/ComboButton/ComboButton.mdx | 36 ++++ .../ComboButton/ComboButton.stories.js | 46 +++++ .../components/ComboButton/docs/overview.mdx | 13 ++ .../react/src/components/ComboButton/index.js | 164 ++++++++++++++++++ .../components/ContextMenu/useContextMenu.mdx | 2 +- packages/react/src/components/Menu/Menu.js | 1 + packages/react/src/index.js | 1 + packages/styles/scss/components/_index.scss | 1 + .../combo-button/_combo-button.scss | 30 ++++ .../scss/components/combo-button/_index.scss | 11 ++ 10 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 packages/react/src/components/ComboButton/ComboButton.mdx create mode 100644 packages/react/src/components/ComboButton/ComboButton.stories.js create mode 100644 packages/react/src/components/ComboButton/docs/overview.mdx create mode 100644 packages/react/src/components/ComboButton/index.js create mode 100644 packages/styles/scss/components/combo-button/_combo-button.scss create mode 100644 packages/styles/scss/components/combo-button/_index.scss diff --git a/packages/react/src/components/ComboButton/ComboButton.mdx b/packages/react/src/components/ComboButton/ComboButton.mdx new file mode 100644 index 000000000000..13c7627d82af --- /dev/null +++ b/packages/react/src/components/ComboButton/ComboButton.mdx @@ -0,0 +1,36 @@ +import { ArgsTable, Canvas, Story } from '@storybook/addon-docs'; + +# ComboButton + +[Source code](https://github.com/carbon-design-system/carbon/tree/main/packages/react/src/components/ComboButton) + + + + +- [Overview](#overview) +- [Component API](#component-api) +- [Feedback](#feedback) + + + +## Overview + +A `ComboButton` can be used to offer additional, secondary actions in a disclosed list next to the primary action. These additional actions must be `MenuItem`s passed as `children`. The primary action's label is passed as `props.label`. + +```jsx + + + + + +``` + +## Component API + + + +## Feedback + +Help us improve this component by providing feedback, asking questions on Slack, +or updating this file on +[GitHub](https://github.com/carbon-design-system/carbon/edit/main/packages/react/src/components/ComboButton/ComboButton.mdx). diff --git a/packages/react/src/components/ComboButton/ComboButton.stories.js b/packages/react/src/components/ComboButton/ComboButton.stories.js new file mode 100644 index 000000000000..078c1e7c8c26 --- /dev/null +++ b/packages/react/src/components/ComboButton/ComboButton.stories.js @@ -0,0 +1,46 @@ +/** + * Copyright IBM Corp. 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { action } from '@storybook/addon-actions'; + +import { MenuItem } from '../Menu'; + +import { ComboButton } from './'; +import mdx from './ComboButton.mdx'; + +export default { + title: 'Experimental/unstable__ComboButton', + component: ComboButton, + parameters: { + docs: { + page: mdx, + }, + }, +}; + +export const Playground = (args) => { + const onClick = action('onClick (MenuItem)'); + + return ( + + + + + + ); +}; + +Playground.argTypes = { + label: { + defaultValue: 'Primary action', + }, +}; + +Playground.args = { + onClick: action('onClick'), +}; diff --git a/packages/react/src/components/ComboButton/docs/overview.mdx b/packages/react/src/components/ComboButton/docs/overview.mdx new file mode 100644 index 000000000000..84455cd61e8d --- /dev/null +++ b/packages/react/src/components/ComboButton/docs/overview.mdx @@ -0,0 +1,13 @@ +## Live demo + + diff --git a/packages/react/src/components/ComboButton/index.js b/packages/react/src/components/ComboButton/index.js new file mode 100644 index 000000000000..4154d79aa136 --- /dev/null +++ b/packages/react/src/components/ComboButton/index.js @@ -0,0 +1,164 @@ +/** + * Copyright IBM Corp. 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { ChevronDown } from '@carbon/icons-react'; +import { Button } from '../Button'; +import { IconButton } from '../IconButton'; +import { Menu } from '../Menu'; + +import { useId } from '../../internal/useId'; +import { usePrefix } from '../../internal/usePrefix'; + +const defaultSize = 'md'; +const spacing = 4; // top and bottom spacing between the button and the menu. in px + +function ComboButton({ + children, + disabled, + kind, + label, + onClick, + size = defaultSize, +}) { + const id = useId('combobutton'); + const [open, setOpen] = useState(false); + const [position, setPosition] = useState([ + [0, 0], + [0, 0], + ]); + const [width, setWidth] = useState(0); + const containerRef = useRef(null); + const prefix = usePrefix(); + + function openMenu() { + if (containerRef.current) { + const { + left, + top, + right, + bottom, + width: w, + } = containerRef.current.getBoundingClientRect(); + setPosition([ + [left, right], + [top - spacing, bottom + spacing], + ]); + setWidth(Math.floor(w)); + } + + setOpen(true); + } + + function closeMenu() { + setOpen(false); + } + + function handleTriggerClick() { + if (open) { + closeMenu(); + } else { + openMenu(); + } + } + + function handleTriggerMousedown(e) { + // prevent default for mousedown on trigger element to avoid + // the "blur" event from firing on the menu as this would close + // it and immediately re-open since "click" event is fired after + // "blur" event. + e.preventDefault(); + } + + function handlePrimaryActionClick(e) { + if (onClick) { + onClick(e); + } + } + + const containerClasses = classNames(`${prefix}--combo-button__container`, { + [`${prefix}--combo-button__container--open`]: open, + }); + + const triggerClasses = classNames(`${prefix}--combo-button__trigger`); + + return ( +
+ + + + + + {children} + +
+ ); +} + +ComboButton.propTypes = { + /** + * A collection of MenuItems to be rendered as additional actions for this ComboButton. + */ + children: PropTypes.node.isRequired, + + /** + * Specify whether the combo button should be disabled, or not. + */ + disabled: PropTypes.bool, + + /** + * Specify the type of button to be used as the base for the primary action and the trigger button. + */ + kind: PropTypes.oneOf(['primary', 'secondary', 'ghost', 'tertiary']), + + /** + * Provide the label to be renderd on the primary action button. + */ + label: PropTypes.string.isRequired, + + /** + * Provide an optional function to be called when the primary action element is clicked. + */ + onClick: PropTypes.func, + + /** + * Specify the size of the Menu. + */ + size: PropTypes.oneOf(['sm', 'md', 'lg']), +}; + +export { ComboButton }; diff --git a/packages/react/src/components/ContextMenu/useContextMenu.mdx b/packages/react/src/components/ContextMenu/useContextMenu.mdx index ecf4656f41ff..9b272b9db43c 100644 --- a/packages/react/src/components/ContextMenu/useContextMenu.mdx +++ b/packages/react/src/components/ContextMenu/useContextMenu.mdx @@ -2,7 +2,7 @@ import { ArgsTable, Canvas, Story } from '@storybook/addon-docs'; # useContextMenu -[Source code](https://github.com/carbon-design-system/carbon/tree/main/packages/react/src/components/Menu) +[Source code](https://github.com/carbon-design-system/carbon/tree/main/packages/react/src/components/ContextMenu) diff --git a/packages/react/src/components/Menu/Menu.js b/packages/react/src/components/Menu/Menu.js index 85880be18677..3f6f97add869 100644 --- a/packages/react/src/components/Menu/Menu.js +++ b/packages/react/src/components/Menu/Menu.js @@ -239,6 +239,7 @@ const Menu = React.forwardRef(function Menu( onBlur={handleBlur} // eslint-disable-next-line react/forbid-dom-props style={{ + ...rest.style, left: `${position[0]}px`, top: `${position[1]}px`, }}> diff --git a/packages/react/src/index.js b/packages/react/src/index.js index d54843d5f1ea..5f2baef8ba8d 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -271,6 +271,7 @@ export { MenuItemSelectable as unstable_MenuItemSelectable, } from './components/Menu'; export { OverflowMenuV2 as unstable_OverflowMenuV2 } from './components/OverflowMenuV2'; +export { ComboButton as unstable_ComboButton } from './components/ComboButton'; export { PageSelector as unstable_PageSelector, Pagination as unstable_Pagination, diff --git a/packages/styles/scss/components/_index.scss b/packages/styles/scss/components/_index.scss index c104962401c3..70cb3dc65a86 100644 --- a/packages/styles/scss/components/_index.scss +++ b/packages/styles/scss/components/_index.scss @@ -12,6 +12,7 @@ @use 'checkbox'; @use 'code-snippet'; @use 'combo-box'; +@use 'combo-button'; @use 'contained-list'; @use 'content-switcher'; @use 'copy-button'; diff --git a/packages/styles/scss/components/combo-button/_combo-button.scss b/packages/styles/scss/components/combo-button/_combo-button.scss new file mode 100644 index 000000000000..7ad2aeb52800 --- /dev/null +++ b/packages/styles/scss/components/combo-button/_combo-button.scss @@ -0,0 +1,30 @@ +// +// Copyright IBM Corp. 2023 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +@use '../../config' as *; +@use '../../motion' as *; +@use '../../utilities/convert' as *; + +/// Combo Button styles +/// @access public +/// @group combo-button +@mixin combo-button { + .#{$prefix}--combo-button__container { + display: inline-flex; + column-gap: rem(1px); + } + + .#{$prefix}--combo-button__trigger svg { + transition: transform $duration-fast-02 motion(standard, productive); + } + + .#{$prefix}--combo-button__container--open + .#{$prefix}--combo-button__trigger + svg { + transform: rotate(180deg); + } +} diff --git a/packages/styles/scss/components/combo-button/_index.scss b/packages/styles/scss/components/combo-button/_index.scss new file mode 100644 index 000000000000..4d1a361c4a99 --- /dev/null +++ b/packages/styles/scss/components/combo-button/_index.scss @@ -0,0 +1,11 @@ +// +// Copyright IBM Corp. 2023 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +@forward 'combo-button'; +@use 'combo-button'; + +@include combo-button.combo-button; From 93a3553e7a9d8241b9b625dd1cbde160789e49a0 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Fri, 24 Feb 2023 15:41:11 +0100 Subject: [PATCH 02/43] docs(combo-button): add disabled and danger item to demo --- .../react/src/components/ComboButton/ComboButton.stories.js | 6 ++++-- packages/react/src/components/ComboButton/index.js | 5 ++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/react/src/components/ComboButton/ComboButton.stories.js b/packages/react/src/components/ComboButton/ComboButton.stories.js index 078c1e7c8c26..1ce65f551422 100644 --- a/packages/react/src/components/ComboButton/ComboButton.stories.js +++ b/packages/react/src/components/ComboButton/ComboButton.stories.js @@ -8,7 +8,7 @@ import React from 'react'; import { action } from '@storybook/addon-actions'; -import { MenuItem } from '../Menu'; +import { MenuItem, MenuItemDivider } from '../Menu'; import { ComboButton } from './'; import mdx from './ComboButton.mdx'; @@ -30,7 +30,9 @@ export const Playground = (args) => { - + + + ); }; diff --git a/packages/react/src/components/ComboButton/index.js b/packages/react/src/components/ComboButton/index.js index 4154d79aa136..d29b58ded193 100644 --- a/packages/react/src/components/ComboButton/index.js +++ b/packages/react/src/components/ComboButton/index.js @@ -17,16 +17,15 @@ import { Menu } from '../Menu'; import { useId } from '../../internal/useId'; import { usePrefix } from '../../internal/usePrefix'; -const defaultSize = 'md'; const spacing = 4; // top and bottom spacing between the button and the menu. in px function ComboButton({ children, disabled, - kind, + kind = 'primary', label, onClick, - size = defaultSize, + size = 'md', }) { const id = useId('combobutton'); const [open, setOpen] = useState(false); From c3704eb239dd6ce172309b0f5a9b4a80fd498d79 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Fri, 24 Feb 2023 15:41:55 +0100 Subject: [PATCH 03/43] docs: add menuitem subcomponents where applicable --- .../ComboButton/ComboButton.stories.js | 4 ++++ .../ContextMenu/useContextMenu.stories.js | 16 +++++++++++++++- .../OverflowMenuV2/OverflowMenuv2.stories.js | 15 ++++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/ComboButton/ComboButton.stories.js b/packages/react/src/components/ComboButton/ComboButton.stories.js index 1ce65f551422..aa9ad9eae129 100644 --- a/packages/react/src/components/ComboButton/ComboButton.stories.js +++ b/packages/react/src/components/ComboButton/ComboButton.stories.js @@ -19,6 +19,10 @@ export default { parameters: { docs: { page: mdx, + subcomponents: { + MenuItem, + MenuItemDivider, + }, }, }, }; diff --git a/packages/react/src/components/ContextMenu/useContextMenu.stories.js b/packages/react/src/components/ContextMenu/useContextMenu.stories.js index 109a88bafedf..c147687a4b49 100644 --- a/packages/react/src/components/ContextMenu/useContextMenu.stories.js +++ b/packages/react/src/components/ContextMenu/useContextMenu.stories.js @@ -13,7 +13,14 @@ import CodeSnippet from '../CodeSnippet'; import UnorderedList from '../UnorderedList'; import ListItem from '../ListItem'; -import { Menu, MenuItem, MenuItemDivider, MenuItemRadioGroup } from '../Menu'; +import { + Menu, + MenuItem, + MenuItemDivider, + MenuItemGroup, + MenuItemRadioGroup, + MenuItemSelectable, +} from '../Menu'; import { useContextMenu } from './'; import mdx from './useContextMenu.mdx'; @@ -21,6 +28,13 @@ import mdx from './useContextMenu.mdx'; export default { title: 'Experimental/unstable__useContextMenu', component: useContextMenu, + subcomponents: { + MenuItem, + MenuItemSelectable, + MenuItemGroup, + MenuItemRadioGroup, + MenuItemDivider, + }, parameters: { docs: { page: mdx, diff --git a/packages/react/src/components/OverflowMenuV2/OverflowMenuv2.stories.js b/packages/react/src/components/OverflowMenuV2/OverflowMenuv2.stories.js index 2c74202ecfeb..16f1c7351cdf 100644 --- a/packages/react/src/components/OverflowMenuV2/OverflowMenuv2.stories.js +++ b/packages/react/src/components/OverflowMenuV2/OverflowMenuv2.stories.js @@ -10,13 +10,26 @@ import { action } from '@storybook/addon-actions'; import { ArrowsVertical } from '@carbon/icons-react'; -import { MenuItem, MenuItemRadioGroup, MenuItemDivider } from '../Menu'; +import { + MenuItem, + MenuItemDivider, + MenuItemGroup, + MenuItemRadioGroup, + MenuItemSelectable, +} from '../Menu'; import { OverflowMenuV2 } from './'; export default { title: 'Experimental/unstable__OverflowMenuV2', component: OverflowMenuV2, + subcomponents: { + MenuItem, + MenuItemSelectable, + MenuItemGroup, + MenuItemRadioGroup, + MenuItemDivider, + }, }; export const _OverflowMenuV2 = () => { From 59ad788670459125ae2e5f16a1a96d234997d71c Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Fri, 24 Feb 2023 15:49:37 +0100 Subject: [PATCH 04/43] docs(combo-button): fix subcomponents placing --- .../ComboButton/ComboButton.stories.js | 8 ++++---- .../ContextMenu/useContextMenu.stories.js | 16 +--------------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/packages/react/src/components/ComboButton/ComboButton.stories.js b/packages/react/src/components/ComboButton/ComboButton.stories.js index aa9ad9eae129..1a09554cde34 100644 --- a/packages/react/src/components/ComboButton/ComboButton.stories.js +++ b/packages/react/src/components/ComboButton/ComboButton.stories.js @@ -16,13 +16,13 @@ import mdx from './ComboButton.mdx'; export default { title: 'Experimental/unstable__ComboButton', component: ComboButton, + subcomponents: { + MenuItem, + MenuItemDivider, + }, parameters: { docs: { page: mdx, - subcomponents: { - MenuItem, - MenuItemDivider, - }, }, }, }; diff --git a/packages/react/src/components/ContextMenu/useContextMenu.stories.js b/packages/react/src/components/ContextMenu/useContextMenu.stories.js index c147687a4b49..109a88bafedf 100644 --- a/packages/react/src/components/ContextMenu/useContextMenu.stories.js +++ b/packages/react/src/components/ContextMenu/useContextMenu.stories.js @@ -13,14 +13,7 @@ import CodeSnippet from '../CodeSnippet'; import UnorderedList from '../UnorderedList'; import ListItem from '../ListItem'; -import { - Menu, - MenuItem, - MenuItemDivider, - MenuItemGroup, - MenuItemRadioGroup, - MenuItemSelectable, -} from '../Menu'; +import { Menu, MenuItem, MenuItemDivider, MenuItemRadioGroup } from '../Menu'; import { useContextMenu } from './'; import mdx from './useContextMenu.mdx'; @@ -28,13 +21,6 @@ import mdx from './useContextMenu.mdx'; export default { title: 'Experimental/unstable__useContextMenu', component: useContextMenu, - subcomponents: { - MenuItem, - MenuItemSelectable, - MenuItemGroup, - MenuItemRadioGroup, - MenuItemDivider, - }, parameters: { docs: { page: mdx, From c8b56b6a13c76a0c9bbef529b87cef7955ed4d01 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Fri, 24 Feb 2023 17:35:48 +0100 Subject: [PATCH 05/43] refactor(combo-button): outsource common logic to useAttachedMenu hook --- .../react/src/components/ComboButton/index.js | 66 +++++----------- .../src/components/OverflowMenuV2/index.js | 56 +++----------- .../react/src/internal/useAttachedMenu.js | 76 +++++++++++++++++++ 3 files changed, 105 insertions(+), 93 deletions(-) create mode 100644 packages/react/src/internal/useAttachedMenu.js diff --git a/packages/react/src/components/ComboButton/index.js b/packages/react/src/components/ComboButton/index.js index d29b58ded193..162995abc09f 100644 --- a/packages/react/src/components/ComboButton/index.js +++ b/packages/react/src/components/ComboButton/index.js @@ -16,6 +16,7 @@ import { Menu } from '../Menu'; import { useId } from '../../internal/useId'; import { usePrefix } from '../../internal/usePrefix'; +import { useAttachedMenu } from '../../internal/useAttachedMenu'; const spacing = 4; // top and bottom spacing between the button and the menu. in px @@ -28,54 +29,27 @@ function ComboButton({ size = 'md', }) { const id = useId('combobutton'); - const [open, setOpen] = useState(false); - const [position, setPosition] = useState([ - [0, 0], - [0, 0], - ]); - const [width, setWidth] = useState(0); - const containerRef = useRef(null); const prefix = usePrefix(); - function openMenu() { - if (containerRef.current) { - const { - left, - top, - right, - bottom, - width: w, - } = containerRef.current.getBoundingClientRect(); - setPosition([ - [left, right], - [top - spacing, bottom + spacing], - ]); - setWidth(Math.floor(w)); - } - - setOpen(true); - } - - function closeMenu() { - setOpen(false); - } + const containerRef = useRef(null); + const [width, setWidth] = useState(0); + const { + open, + x, + y, + handleClick: hookOnClick, + handleMousedown: handleTriggerMousedown, + handleClose, + } = useAttachedMenu(containerRef); function handleTriggerClick() { - if (open) { - closeMenu(); - } else { - openMenu(); + if (containerRef.current) { + const { width: w } = containerRef.current.getBoundingClientRect(); + setWidth(w); + hookOnClick(); } } - function handleTriggerMousedown(e) { - // prevent default for mousedown on trigger element to avoid - // the "blur" event from firing on the menu as this would close - // it and immediately re-open since "click" event is fired after - // "blur" event. - e.preventDefault(); - } - function handlePrimaryActionClick(e) { if (onClick) { onClick(e); @@ -119,9 +93,9 @@ function ComboButton({ label="Additional actions" size={size} open={open} - onClose={closeMenu} - x={position[0]} - y={position[1]}> + onClose={handleClose} + x={x} + y={[y[0] - spacing, y[1] + spacing]}> {children} @@ -135,7 +109,7 @@ ComboButton.propTypes = { children: PropTypes.node.isRequired, /** - * Specify whether the combo button should be disabled, or not. + * Specify whether the ComboButton should be disabled, or not. */ disabled: PropTypes.bool, @@ -155,7 +129,7 @@ ComboButton.propTypes = { onClick: PropTypes.func, /** - * Specify the size of the Menu. + * Specify the size of the buttons and menu. */ size: PropTypes.oneOf(['sm', 'md', 'lg']), }; diff --git a/packages/react/src/components/OverflowMenuV2/index.js b/packages/react/src/components/OverflowMenuV2/index.js index 9c8885b188e7..13674400e65a 100644 --- a/packages/react/src/components/OverflowMenuV2/index.js +++ b/packages/react/src/components/OverflowMenuV2/index.js @@ -5,13 +5,16 @@ * LICENSE file in the root directory of this source tree. */ -import React, { useState, useRef } from 'react'; +import React, { useRef } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { OverflowMenuVertical } from '@carbon/icons-react'; -import { useId } from '../../internal/useId'; + import { Menu } from '../Menu'; + +import { useId } from '../../internal/useId'; import { usePrefix } from '../../internal/usePrefix'; +import { useAttachedMenu } from '../../internal/useAttachedMenu'; const defaultSize = 'md'; @@ -23,46 +26,11 @@ function OverflowMenuV2({ ...rest }) { const id = useId('overflowmenu'); - const [open, setOpen] = useState(false); - const [position, setPosition] = useState([ - [0, 0], - [0, 0], - ]); - const triggerRef = useRef(null); const prefix = usePrefix(); - function openMenu() { - if (triggerRef.current) { - const { left, top, right, bottom } = - triggerRef.current.getBoundingClientRect(); - setPosition([ - [left, right], - [top, bottom], - ]); - } - - setOpen(true); - } - - function closeMenu() { - setOpen(false); - } - - function handleClick() { - if (open) { - closeMenu(); - } else { - openMenu(); - } - } - - function handleMousedown(e) { - // prevent default for mousedown on trigger element to avoid - // the "blur" event from firing on the menu as this would close - // it and immediately re-open since "click" event is fired after - // "blur" event. - e.preventDefault(); - } + const triggerRef = useRef(null); + const { open, x, y, handleClick, handleMousedown, handleClose } = + useAttachedMenu(triggerRef); const containerClasses = classNames(`${prefix}--overflow-menu__container`); @@ -88,13 +56,7 @@ function OverflowMenuV2({ ref={triggerRef}> - + {children} diff --git a/packages/react/src/internal/useAttachedMenu.js b/packages/react/src/internal/useAttachedMenu.js new file mode 100644 index 000000000000..d6c4becc5d69 --- /dev/null +++ b/packages/react/src/internal/useAttachedMenu.js @@ -0,0 +1,76 @@ +/** + * Copyright IBM Corp. 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { useState } from 'react'; + +/** + * @typedef {object} useAttachedMenuReturn + * @property {boolean} open Whether the menu is open or not + * @property {[number, number]} x The x position of the menu + * @property {[number, number]} y The y position of the menu + * @property {Function} handleClick A function to be called when the trigger element receives a click event + * @property {Function} handleMousedown A function to be called when the trigger element recives a mousedown event + * @property {Function} handleClose A function to be called when the menu emits onClose + */ + +/** + * This hook contains common code to be used when a menu should be visually attached to an anchor based on a click event. + * + * @param {Element|object} anchor The element or ref the menu should visually be attached to. + * @returns {useAttachedMenuReturn} + */ +export function useAttachedMenu(anchor) { + const [open, setOpen] = useState(false); + const [position, setPosition] = useState([ + [-1, -1], + [-1, -1], + ]); + + function openMenu() { + const anchorEl = anchor?.current || anchor; + + if (anchorEl) { + const { left, top, right, bottom } = anchorEl.getBoundingClientRect(); + + setPosition([ + [left, right], + [top, bottom], + ]); + } + + setOpen(true); + } + + function closeMenu() { + setOpen(false); + } + + function handleClick() { + if (open) { + closeMenu(); + } else { + openMenu(); + } + } + + function handleMousedown(e) { + // prevent default for mousedown on trigger element to avoid + // the "blur" event from firing on the menu as this would close + // it and immediately re-open since "click" event is fired after + // "blur" event. + e.preventDefault(); + } + + return { + open, + x: position[0], + y: position[1], + handleClick, + handleMousedown, + handleClose: closeMenu, + }; +} From aa1045f170d106d585f2eef049b5364a0dafd8ab Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Fri, 24 Feb 2023 17:37:08 +0100 Subject: [PATCH 06/43] feat: add experimental menu-button component --- .../src/components/MenuButton/MenuButton.mdx | 36 ++++++ .../MenuButton/MenuButton.stories.js | 48 ++++++++ .../components/MenuButton/docs/overview.mdx | 13 ++ .../react/src/components/MenuButton/index.js | 116 ++++++++++++++++++ packages/react/src/index.js | 1 + packages/styles/scss/components/_index.scss | 1 + .../scss/components/menu-button/_index.scss | 11 ++ .../components/menu-button/_menu-button.scss | 23 ++++ 8 files changed, 249 insertions(+) create mode 100644 packages/react/src/components/MenuButton/MenuButton.mdx create mode 100644 packages/react/src/components/MenuButton/MenuButton.stories.js create mode 100644 packages/react/src/components/MenuButton/docs/overview.mdx create mode 100644 packages/react/src/components/MenuButton/index.js create mode 100644 packages/styles/scss/components/menu-button/_index.scss create mode 100644 packages/styles/scss/components/menu-button/_menu-button.scss diff --git a/packages/react/src/components/MenuButton/MenuButton.mdx b/packages/react/src/components/MenuButton/MenuButton.mdx new file mode 100644 index 000000000000..e63b4bbd1371 --- /dev/null +++ b/packages/react/src/components/MenuButton/MenuButton.mdx @@ -0,0 +1,36 @@ +import { ArgsTable, Canvas, Story } from '@storybook/addon-docs'; + +# MenuButton + +[Source code](https://github.com/carbon-design-system/carbon/tree/main/packages/react/src/components/MenuButton) + + + + +- [Overview](#overview) +- [Component API](#component-api) +- [Feedback](#feedback) + + + +## Overview + +A `MenuButton` can be used to group a set of actions that are related. These actions must be `MenuItem`s passed as `children`. The trigger buttons's label is passed as `props.label`. + +```jsx + + + + + +``` + +## Component API + + + +## Feedback + +Help us improve this component by providing feedback, asking questions on Slack, +or updating this file on +[GitHub](https://github.com/carbon-design-system/carbon/edit/main/packages/react/src/components/MenuButton/MenuButton.mdx). diff --git a/packages/react/src/components/MenuButton/MenuButton.stories.js b/packages/react/src/components/MenuButton/MenuButton.stories.js new file mode 100644 index 000000000000..804a3ab43e5a --- /dev/null +++ b/packages/react/src/components/MenuButton/MenuButton.stories.js @@ -0,0 +1,48 @@ +/** + * Copyright IBM Corp. 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { action } from '@storybook/addon-actions'; + +import { MenuItem, MenuItemDivider } from '../Menu'; + +import { MenuButton } from './'; +import mdx from './MenuButton.mdx'; + +export default { + title: 'Experimental/unstable__MenuButton', + component: MenuButton, + subcomponents: { + MenuItem, + MenuItemDivider, + }, + parameters: { + docs: { + page: mdx, + }, + }, +}; + +export const Playground = (args) => { + const onClick = action('onClick (MenuItem)'); + + return ( + + + + + + + + ); +}; + +Playground.argTypes = { + label: { + defaultValue: 'Actions', + }, +}; diff --git a/packages/react/src/components/MenuButton/docs/overview.mdx b/packages/react/src/components/MenuButton/docs/overview.mdx new file mode 100644 index 000000000000..b5612b853b75 --- /dev/null +++ b/packages/react/src/components/MenuButton/docs/overview.mdx @@ -0,0 +1,13 @@ +## Live demo + + diff --git a/packages/react/src/components/MenuButton/index.js b/packages/react/src/components/MenuButton/index.js new file mode 100644 index 000000000000..7a62cbd2339d --- /dev/null +++ b/packages/react/src/components/MenuButton/index.js @@ -0,0 +1,116 @@ +/** + * Copyright IBM Corp. 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { ChevronDown } from '@carbon/icons-react'; +import { Button } from '../Button'; +import { Menu } from '../Menu'; + +import { useId } from '../../internal/useId'; +import { usePrefix } from '../../internal/usePrefix'; +import { useAttachedMenu } from '../../internal/useAttachedMenu'; + +const spacing = 4; // top and bottom spacing between the button and the menu. in px + +function MenuButton({ + children, + disabled, + kind = 'primary', + label, + size = 'md', +}) { + const id = useId('MenuButton'); + const prefix = usePrefix(); + + const containerRef = useRef(null); + const [width, setWidth] = useState(0); + const { + open, + x, + y, + handleClick: hookOnClick, + handleMousedown, + handleClose, + } = useAttachedMenu(containerRef); + + function handleClick() { + if (containerRef.current) { + const { width: w } = containerRef.current.getBoundingClientRect(); + setWidth(w); + hookOnClick(); + } + } + + const triggerClasses = classNames(`${prefix}--menu-button__trigger`, { + [`${prefix}--menu-button__trigger--open`]: open, + }); + + return ( +
+ + + {children} + +
+ ); +} + +MenuButton.propTypes = { + /** + * A collection of MenuItems to be rendered as actions for this MenuButton. + */ + children: PropTypes.node.isRequired, + + /** + * Specify whether the MenuButton should be disabled, or not. + */ + disabled: PropTypes.bool, + + /** + * Specify the type of button to be used as the base for the trigger button. + */ + kind: PropTypes.oneOf(['primary', 'secondary', 'ghost', 'tertiary']), + + /** + * Provide the label to be renderd on the trigger button. + */ + label: PropTypes.string.isRequired, + + /** + * Specify the size of the button and menu. + */ + size: PropTypes.oneOf(['sm', 'md', 'lg']), +}; + +export { MenuButton }; diff --git a/packages/react/src/index.js b/packages/react/src/index.js index 5f2baef8ba8d..47a6afb042f1 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -272,6 +272,7 @@ export { } from './components/Menu'; export { OverflowMenuV2 as unstable_OverflowMenuV2 } from './components/OverflowMenuV2'; export { ComboButton as unstable_ComboButton } from './components/ComboButton'; +export { MenuButton as unstable_MenuButton } from './components/MenuButton'; export { PageSelector as unstable_PageSelector, Pagination as unstable_Pagination, diff --git a/packages/styles/scss/components/_index.scss b/packages/styles/scss/components/_index.scss index 70cb3dc65a86..b8ebb05f418a 100644 --- a/packages/styles/scss/components/_index.scss +++ b/packages/styles/scss/components/_index.scss @@ -41,6 +41,7 @@ @use 'list'; @use 'list-box'; @use 'loading'; +@use 'menu-button'; @use 'menu'; @use 'modal'; @use 'multiselect'; diff --git a/packages/styles/scss/components/menu-button/_index.scss b/packages/styles/scss/components/menu-button/_index.scss new file mode 100644 index 000000000000..9da89c0ce1c5 --- /dev/null +++ b/packages/styles/scss/components/menu-button/_index.scss @@ -0,0 +1,11 @@ +// +// Copyright IBM Corp. 2023 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +@forward 'menu-button'; +@use 'menu-button'; + +@include menu-button.menu-button; diff --git a/packages/styles/scss/components/menu-button/_menu-button.scss b/packages/styles/scss/components/menu-button/_menu-button.scss new file mode 100644 index 000000000000..8eccbfd93411 --- /dev/null +++ b/packages/styles/scss/components/menu-button/_menu-button.scss @@ -0,0 +1,23 @@ +// +// Copyright IBM Corp. 2023 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +@use '../../config' as *; +@use '../../motion' as *; +@use '../../utilities/convert' as *; + +/// Menu Button styles +/// @access public +/// @group menu-button +@mixin menu-button { + .#{$prefix}--menu-button__trigger svg { + transition: transform $duration-fast-02 motion(standard, productive); + } + + .#{$prefix}--menu-button__trigger--open svg { + transform: rotate(180deg); + } +} From 1f06c2f9aea03c95edada0cd8236b4c7c2d9a8de Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Wed, 1 Mar 2023 18:10:51 +0100 Subject: [PATCH 07/43] feat(combo-button, menu-button): add props.className support --- .../react/src/components/ComboButton/index.js | 16 ++++++++++++--- .../react/src/components/MenuButton/index.js | 20 ++++++++++++++----- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/react/src/components/ComboButton/index.js b/packages/react/src/components/ComboButton/index.js index 162995abc09f..58e5c528f1bd 100644 --- a/packages/react/src/components/ComboButton/index.js +++ b/packages/react/src/components/ComboButton/index.js @@ -22,6 +22,7 @@ const spacing = 4; // top and bottom spacing between the button and the menu. in function ComboButton({ children, + className, disabled, kind = 'primary', label, @@ -56,9 +57,13 @@ function ComboButton({ } } - const containerClasses = classNames(`${prefix}--combo-button__container`, { - [`${prefix}--combo-button__container--open`]: open, - }); + const containerClasses = classNames( + `${prefix}--combo-button__container`, + { + [`${prefix}--combo-button__container--open`]: open, + }, + className + ); const triggerClasses = classNames(`${prefix}--combo-button__trigger`); @@ -108,6 +113,11 @@ ComboButton.propTypes = { */ children: PropTypes.node.isRequired, + /** + * Additional CSS class names. + */ + className: PropTypes.string, + /** * Specify whether the ComboButton should be disabled, or not. */ diff --git a/packages/react/src/components/MenuButton/index.js b/packages/react/src/components/MenuButton/index.js index 7a62cbd2339d..e026e320d7c0 100644 --- a/packages/react/src/components/MenuButton/index.js +++ b/packages/react/src/components/MenuButton/index.js @@ -21,6 +21,7 @@ const spacing = 4; // top and bottom spacing between the button and the menu. in function MenuButton({ children, + className, disabled, kind = 'primary', label, @@ -48,12 +49,16 @@ function MenuButton({ } } - const triggerClasses = classNames(`${prefix}--menu-button__trigger`, { - [`${prefix}--menu-button__trigger--open`]: open, - }); + const triggerClasses = classNames( + `${prefix}--menu-button__trigger`, + { + [`${prefix}--menu-button__trigger--open`]: open, + }, + className + ); return ( -
+ <>
- + ); } @@ -92,6 +97,11 @@ MenuButton.propTypes = { */ children: PropTypes.node.isRequired, + /** + * Additional CSS class names. + */ + className: PropTypes.string, + /** * Specify whether the MenuButton should be disabled, or not. */ From 5f841178df359c6ea1f286c632405c4ee1a5b68a Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Wed, 1 Mar 2023 18:15:17 +0100 Subject: [PATCH 08/43] style(combo-button): remove props.kind as only primary is supported --- packages/react/src/components/ComboButton/index.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/react/src/components/ComboButton/index.js b/packages/react/src/components/ComboButton/index.js index 58e5c528f1bd..3d9fb1332db5 100644 --- a/packages/react/src/components/ComboButton/index.js +++ b/packages/react/src/components/ComboButton/index.js @@ -24,7 +24,6 @@ function ComboButton({ children, className, disabled, - kind = 'primary', label, onClick, size = 'md', @@ -71,7 +70,6 @@ function ComboButton({
Date: Thu, 2 Mar 2023 09:33:19 +0100 Subject: [PATCH 15/43] feat(combo-button): add support for prop-controllable tooltip alignment --- .../__snapshots__/PublicAPI-test.js.snap | 15 +++++++++++++++ .../react/src/components/ComboButton/index.js | 16 ++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 88ebd7f2c3d0..66a1a3c14bd4 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -9042,6 +9042,21 @@ Map { ], "type": "oneOf", }, + "tooltipAlign": Object { + "args": Array [ + Array [ + "top", + "top-left", + "top-right", + "bottom", + "bottom-left", + "bottom-right", + "left", + "right", + ], + ], + "type": "oneOf", + }, "translateWithId": Object { "type": "func", }, diff --git a/packages/react/src/components/ComboButton/index.js b/packages/react/src/components/ComboButton/index.js index fe7987b4b368..c8d0e238e106 100644 --- a/packages/react/src/components/ComboButton/index.js +++ b/packages/react/src/components/ComboButton/index.js @@ -34,6 +34,7 @@ function ComboButton({ label, onClick, size = 'md', + tooltipAlign, translateWithId: t = defaultTranslateWithId, }) { const id = useId('combobutton'); @@ -92,6 +93,7 @@ function ComboButton({ label={t('carbon.combo-button.additional-actions')} size={size} disabled={disabled} + align={tooltipAlign} aria-haspopup aria-expanded={open} onClick={handleTriggerClick} @@ -148,6 +150,20 @@ ComboButton.propTypes = { */ size: PropTypes.oneOf(['sm', 'md', 'lg']), + /** + * Specify how the trigger tooltip should be aligned. + */ + tooltipAlign: PropTypes.oneOf([ + 'top', + 'top-left', + 'top-right', + 'bottom', + 'bottom-left', + 'bottom-right', + 'left', + 'right', + ]), + /** * Optional method that takes in a message id and returns an * internationalized string. From f162faa4c43499efa65c645b9d8739f2ccbfa65d Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Thu, 2 Mar 2023 09:41:08 +0100 Subject: [PATCH 16/43] feat(menu-button): add support for ref and additional props --- .../__snapshots__/PublicAPI-test.js.snap | 2 + .../react/src/components/MenuButton/index.js | 37 +++++++++++-------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 66a1a3c14bd4..e026da933cac 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -9182,6 +9182,7 @@ Map { "render": [Function], }, "unstable_MenuButton" => Object { + "$$typeof": Symbol(react.forward_ref), "propTypes": Object { "children": Object { "isRequired": true, @@ -9218,6 +9219,7 @@ Map { "type": "oneOf", }, }, + "render": [Function], }, "unstable_MenuItem" => Object { "$$typeof": Symbol(react.forward_ref), diff --git a/packages/react/src/components/MenuButton/index.js b/packages/react/src/components/MenuButton/index.js index 645b0a9d3f93..4224764bf3c9 100644 --- a/packages/react/src/components/MenuButton/index.js +++ b/packages/react/src/components/MenuButton/index.js @@ -13,26 +13,32 @@ import { ChevronDown } from '@carbon/icons-react'; import { Button } from '../Button'; import { Menu } from '../Menu'; +import { useAttachedMenu } from '../../internal/useAttachedMenu'; import { useId } from '../../internal/useId'; +import { useMergedRefs } from '../../internal/useMergedRefs'; import { usePrefix } from '../../internal/usePrefix'; -import { useAttachedMenu } from '../../internal/useAttachedMenu'; const spacing = 4; // top and bottom spacing between the button and the menu. in px const validButtonKinds = ['primary', 'tertiary', 'ghost']; const defaultButtonKind = 'primary'; -function MenuButton({ - children, - className, - disabled, - kind = defaultButtonKind, - label, - size = 'md', -}) { +const MenuButton = React.forwardRef(function MenuButton( + { + children, + className, + disabled, + kind = defaultButtonKind, + label, + size = 'md', + ...rest + }, + forwardRef +) { const id = useId('MenuButton'); const prefix = usePrefix(); - const containerRef = useRef(null); + const triggerRef = useRef(null); + const ref = useMergedRefs([forwardRef, triggerRef]); const [width, setWidth] = useState(0); const { open, @@ -41,11 +47,11 @@ function MenuButton({ handleClick: hookOnClick, handleMousedown, handleClose, - } = useAttachedMenu(containerRef); + } = useAttachedMenu(triggerRef); function handleClick() { - if (containerRef.current) { - const { width: w } = containerRef.current.getBoundingClientRect(); + if (triggerRef.current) { + const { width: w } = triggerRef.current.getBoundingClientRect(); setWidth(w); hookOnClick(); } @@ -64,7 +70,8 @@ function MenuButton({ return ( <> Date: Thu, 2 Mar 2023 10:14:43 +0100 Subject: [PATCH 18/43] feat(combo-button): add support for ref and additional props --- .../__snapshots__/PublicAPI-test.js.snap | 2 ++ .../react/src/components/ComboButton/index.js | 34 +++++++++++-------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index e026da933cac..c3428cd87a1a 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -9014,6 +9014,7 @@ Map { "9": "warm-gray", }, "unstable_ComboButton" => Object { + "$$typeof": Symbol(react.forward_ref), "propTypes": Object { "children": Object { "isRequired": true, @@ -9061,6 +9062,7 @@ Map { "type": "func", }, }, + "render": [Function], }, "unstable_FeatureFlags" => Object { "propTypes": Object { diff --git a/packages/react/src/components/ComboButton/index.js b/packages/react/src/components/ComboButton/index.js index c8d0e238e106..3cacc8ddd223 100644 --- a/packages/react/src/components/ComboButton/index.js +++ b/packages/react/src/components/ComboButton/index.js @@ -14,9 +14,10 @@ import { Button } from '../Button'; import { IconButton } from '../IconButton'; import { Menu } from '../Menu'; +import { useAttachedMenu } from '../../internal/useAttachedMenu'; import { useId } from '../../internal/useId'; +import { useMergedRefs } from '../../internal/useMergedRefs'; import { usePrefix } from '../../internal/usePrefix'; -import { useAttachedMenu } from '../../internal/useAttachedMenu'; const spacing = 4; // top and bottom spacing between the button and the menu. in px const defaultTranslations = { @@ -27,20 +28,25 @@ function defaultTranslateWithId(messageId) { return defaultTranslations[messageId]; } -function ComboButton({ - children, - className, - disabled, - label, - onClick, - size = 'md', - tooltipAlign, - translateWithId: t = defaultTranslateWithId, -}) { +const ComboButton = React.forwardRef(function ComboButton( + { + children, + className, + disabled, + label, + onClick, + size = 'md', + tooltipAlign, + translateWithId: t = defaultTranslateWithId, + ...rest + }, + forwardRef +) { const id = useId('combobutton'); const prefix = usePrefix(); const containerRef = useRef(null); + const ref = useMergedRefs([forwardRef, containerRef]); const [width, setWidth] = useState(0); const { open, @@ -80,7 +86,7 @@ function ComboButton({ const triggerClasses = classNames(`${prefix}--combo-button__trigger`); return ( -
+
); -} +}); ComboButton.propTypes = { /** From 9e83508ab1f529fe371c8325ef73753f4e2f6e1e Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Thu, 2 Mar 2023 10:22:12 +0100 Subject: [PATCH 19/43] test(combo-button): add tests --- .../ComboButton/ComboButton-test.e2e.js | 37 ++++++++ .../ComboButton/ComboButton-test.js | 95 +++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 e2e/components/ComboButton/ComboButton-test.e2e.js create mode 100644 packages/react/src/components/ComboButton/ComboButton-test.js diff --git a/e2e/components/ComboButton/ComboButton-test.e2e.js b/e2e/components/ComboButton/ComboButton-test.e2e.js new file mode 100644 index 000000000000..22bda4304366 --- /dev/null +++ b/e2e/components/ComboButton/ComboButton-test.e2e.js @@ -0,0 +1,37 @@ +/** + * Copyright IBM Corp. 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const { expect, test } = require('@playwright/test'); +const { themes } = require('../../test-utils/env'); +const { snapshotStory, visitStory } = require('../../test-utils/storybook'); + +test.describe('ComboButton', () => { + themes.forEach((theme) => { + test.describe(theme, () => { + test('menu-button @vrt', async ({ page }) => { + await snapshotStory(page, { + component: 'ComboButton', + id: 'experimental-unstable-combobutton--playground', + theme, + }); + }); + }); + }); + + test('accessibility-checker @avt', async ({ page }) => { + await visitStory(page, { + component: 'ComboButton', + id: 'experimental-unstable-combobutton--playground', + globals: { + theme: 'white', + }, + }); + await expect(page).toHaveNoACViolations('ComboButton'); + }); +}); diff --git a/packages/react/src/components/ComboButton/ComboButton-test.js b/packages/react/src/components/ComboButton/ComboButton-test.js new file mode 100644 index 000000000000..d1c8b40102ba --- /dev/null +++ b/packages/react/src/components/ComboButton/ComboButton-test.js @@ -0,0 +1,95 @@ +/** + * Copyright IBM Corp. 2016, 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { MenuItem } from '../Menu'; + +import { ComboButton } from './'; + +describe('ComboButton', () => { + it('should support a ref on the outermost element', () => { + const ref = jest.fn(); + const { container } = render( + + + + ); + expect(ref).toHaveBeenCalledWith(container.firstChild); + }); + + it('should support a custom class name on the outermost element', () => { + const { container } = render( + + + + ); + expect(container.firstChild).toHaveClass('test'); + }); + + it('should forward additional props on the outermost element', () => { + const { container } = render( + + + + ); + expect(container.firstChild).toHaveAttribute('data-testid', 'test'); + }); + + it('should render props.label on the trigger button', () => { + render( + + + + ); + expect(screen.getAllByRole('button')[0]).toHaveTextContent(/^Test$/); + }); + + it('should emit props.onClick on primary action click', async () => { + const onClick = jest.fn(); + render( + + + + ); + + expect(onClick).toHaveBeenCalledTimes(0); + await userEvent.click(screen.getAllByRole('button')[0]); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('should open a menu on click on the trigger button', async () => { + render( + + + + ); + + await userEvent.click(screen.getAllByRole('button')[1]); + + expect(screen.getByRole('menu')).toBeTruthy(); + expect(screen.getByRole('menuitem')).toHaveTextContent( + /^Additional action$/ + ); + }); + + it('should support being disabled', () => { + render( + + + + ); + + // primary action button + expect(screen.getAllByRole('button')[0]).toBeDisabled(); + + // trigger button + expect(screen.getAllByRole('button')[1]).toBeDisabled(); + }); +}); From 9702941ff3b83d7705f6317b693ec8282e9f89ca Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Thu, 2 Mar 2023 10:24:04 +0100 Subject: [PATCH 20/43] test(menu-button): align tests --- .../components/MenuButton/MenuButton-test.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/react/src/components/MenuButton/MenuButton-test.js b/packages/react/src/components/MenuButton/MenuButton-test.js index 4b024d552c17..ab8fc3ab4735 100644 --- a/packages/react/src/components/MenuButton/MenuButton-test.js +++ b/packages/react/src/components/MenuButton/MenuButton-test.js @@ -16,44 +16,44 @@ import { MenuButton } from './'; describe('MenuButton', () => { it('should support a ref on the outermost element', () => { const ref = jest.fn(); - render( + const { container } = render( ); - expect(ref).toHaveBeenCalledWith(screen.getByRole('button')); + expect(ref).toHaveBeenCalledWith(container.firstChild); }); it('should support a custom class name on the outermost element', () => { - render( + const { container } = render( ); - expect(screen.getByRole('button')).toHaveClass('test'); + expect(container.firstChild).toHaveClass('test'); }); it('should forward additional props on the outermost element', () => { - render( + const { container } = render( ); - expect(screen.getByRole('button')).toHaveAttribute('data-testid', 'test'); + expect(container.firstChild).toHaveAttribute('data-testid', 'test'); }); it('should render props.label on the trigger button', () => { render( - + ); expect(screen.getByRole('button')).toHaveTextContent(/^Test$/); }); - it('opens a menu on click', async () => { + it('should open a menu on click', async () => { render( - + ); From c820c0c70f01befaaca03a502eb635716283528c Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Thu, 2 Mar 2023 11:04:24 +0100 Subject: [PATCH 21/43] test: update react exports snapshot --- packages/react/src/__tests__/index-test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react/src/__tests__/index-test.js b/packages/react/src/__tests__/index-test.js index 8cf6b6628a68..475bd9305453 100644 --- a/packages/react/src/__tests__/index-test.js +++ b/packages/react/src/__tests__/index-test.js @@ -221,9 +221,11 @@ describe('Carbon Components React', () => { "UnorderedList", "VStack", "types", + "unstable_ComboButton", "unstable_FeatureFlags", "unstable_LayoutDirection", "unstable_Menu", + "unstable_MenuButton", "unstable_MenuItem", "unstable_MenuItemDivider", "unstable_MenuItemGroup", From 2c45fb17cb41aebbcf7ee2fd5a15038057a83d01 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Thu, 2 Mar 2023 11:11:20 +0100 Subject: [PATCH 22/43] test(menu-button): add test to verify disabled prop works --- .../react/src/components/MenuButton/MenuButton-test.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/react/src/components/MenuButton/MenuButton-test.js b/packages/react/src/components/MenuButton/MenuButton-test.js index ab8fc3ab4735..0d0b50c522eb 100644 --- a/packages/react/src/components/MenuButton/MenuButton-test.js +++ b/packages/react/src/components/MenuButton/MenuButton-test.js @@ -63,4 +63,14 @@ describe('MenuButton', () => { expect(screen.getByRole('menu')).toBeTruthy(); expect(screen.getByRole('menuitem')).toHaveTextContent(/^Action$/); }); + + it('should support being disabled', () => { + render( + + + + ); + + expect(screen.getByRole('button')).toBeDisabled(); + }); }); From c0a3f23bb5e8db5c47d4f0d8f577f8c598a56084 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Fri, 3 Mar 2023 09:48:11 +0100 Subject: [PATCH 23/43] test(combo-button): extend tests --- .../ComboButton/ComboButton-test.e2e.js | 2 +- .../ComboButton/ComboButton-test.js | 199 ++++++++++++------ 2 files changed, 134 insertions(+), 67 deletions(-) diff --git a/e2e/components/ComboButton/ComboButton-test.e2e.js b/e2e/components/ComboButton/ComboButton-test.e2e.js index 22bda4304366..0a1609925872 100644 --- a/e2e/components/ComboButton/ComboButton-test.e2e.js +++ b/e2e/components/ComboButton/ComboButton-test.e2e.js @@ -14,7 +14,7 @@ const { snapshotStory, visitStory } = require('../../test-utils/storybook'); test.describe('ComboButton', () => { themes.forEach((theme) => { test.describe(theme, () => { - test('menu-button @vrt', async ({ page }) => { + test('combo-button @vrt', async ({ page }) => { await snapshotStory(page, { component: 'ComboButton', id: 'experimental-unstable-combobutton--playground', diff --git a/packages/react/src/components/ComboButton/ComboButton-test.js b/packages/react/src/components/ComboButton/ComboButton-test.js index d1c8b40102ba..cf73b17c75a0 100644 --- a/packages/react/src/components/ComboButton/ComboButton-test.js +++ b/packages/react/src/components/ComboButton/ComboButton-test.js @@ -13,83 +13,150 @@ import { MenuItem } from '../Menu'; import { ComboButton } from './'; +const prefix = 'cds'; + describe('ComboButton', () => { - it('should support a ref on the outermost element', () => { - const ref = jest.fn(); - const { container } = render( - - - - ); - expect(ref).toHaveBeenCalledWith(container.firstChild); - }); + describe('renders as expected - Component API', () => { + it('supports a ref on the outermost element', () => { + const ref = jest.fn(); + const { container } = render( + + + + ); + expect(ref).toHaveBeenCalledWith(container.firstChild); + }); - it('should support a custom class name on the outermost element', () => { - const { container } = render( - - - - ); - expect(container.firstChild).toHaveClass('test'); - }); + it('supports a custom class name on the outermost element', () => { + const { container } = render( + + + + ); + expect(container.firstChild).toHaveClass('test'); + }); - it('should forward additional props on the outermost element', () => { - const { container } = render( - - - - ); - expect(container.firstChild).toHaveAttribute('data-testid', 'test'); - }); + it('forwards additional props on the outermost element', () => { + const { container } = render( + + + + ); + expect(container.firstChild).toHaveAttribute('data-testid', 'test'); + }); - it('should render props.label on the trigger button', () => { - render( - - - - ); - expect(screen.getAllByRole('button')[0]).toHaveTextContent(/^Test$/); - }); + it('renders props.label on the trigger button', () => { + render( + + + + ); + expect(screen.getAllByRole('button')[0]).toHaveTextContent(/^Test$/); + }); - it('should emit props.onClick on primary action click', async () => { - const onClick = jest.fn(); - render( - - - - ); - - expect(onClick).toHaveBeenCalledTimes(0); - await userEvent.click(screen.getAllByRole('button')[0]); - expect(onClick).toHaveBeenCalledTimes(1); - }); + it('supports props.disabled', () => { + render( + + + + ); + + // primary action button + expect(screen.getAllByRole('button')[0]).toBeDisabled(); + + // trigger button + expect(screen.getAllByRole('button')[1]).toBeDisabled(); + }); + + describe('supports prop.size', () => { + const sizes = ['sm', 'md', 'lg']; - it('should open a menu on click on the trigger button', async () => { - render( - - - - ); + sizes.forEach((size) => { + it(`size="${size}"`, () => { + const { container } = render( + + + + ); - await userEvent.click(screen.getAllByRole('button')[1]); + expect(container.firstChild).toHaveClass( + `${prefix}--combo-button__container--${size}` + ); + }); + }); + }); - expect(screen.getByRole('menu')).toBeTruthy(); - expect(screen.getByRole('menuitem')).toHaveTextContent( - /^Additional action$/ - ); + describe('supports prop.tooltipAlign', () => { + const alignments = [ + 'top', + 'top-left', + 'top-right', + 'bottom', + 'bottom-left', + 'bottom-right', + 'left', + 'right', + ]; + + alignments.forEach((alignment) => { + it(`tooltipAlign="${alignment}"`, () => { + const { container } = render( + + + + ); + + expect(container.firstChild.lastChild).toHaveClass( + `${prefix}--popover--${alignment}` + ); + }); + }); + }); + + it('supports props.translateWithId', () => { + const t = () => 'test'; + + render( + + + + ); + + const triggerButton = screen.getAllByRole('button')[1]; + const tooltipId = triggerButton.getAttribute('aria-labelledby'); + const tooltip = document.getElementById(tooltipId); + + expect(tooltip).toHaveTextContent(t()); + }); }); - it('should support being disabled', () => { - render( - - - - ); + describe('behaves as expected', () => { + it('emits props.onClick on primary action click', async () => { + const onClick = jest.fn(); + render( + + + + ); + + expect(onClick).toHaveBeenCalledTimes(0); + await userEvent.click(screen.getAllByRole('button')[0]); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('opens a menu on click on the trigger button', async () => { + render( + + + + ); - // primary action button - expect(screen.getAllByRole('button')[0]).toBeDisabled(); + await userEvent.click(screen.getAllByRole('button')[1]); - // trigger button - expect(screen.getAllByRole('button')[1]).toBeDisabled(); + expect(screen.getByRole('menu')).toBeTruthy(); + expect(screen.getByRole('menuitem')).toHaveTextContent( + /^Additional action$/ + ); + }); }); }); From e049aa020dffe3e0cf07518e34739f8383495aa7 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Fri, 3 Mar 2023 09:57:37 +0100 Subject: [PATCH 24/43] test(menu-button): extend tests --- .../components/MenuButton/MenuButton-test.js | 137 +++++++++++------- 1 file changed, 88 insertions(+), 49 deletions(-) diff --git a/packages/react/src/components/MenuButton/MenuButton-test.js b/packages/react/src/components/MenuButton/MenuButton-test.js index 0d0b50c522eb..75f88445e211 100644 --- a/packages/react/src/components/MenuButton/MenuButton-test.js +++ b/packages/react/src/components/MenuButton/MenuButton-test.js @@ -13,64 +13,103 @@ import { MenuItem } from '../Menu'; import { MenuButton } from './'; +const prefix = 'cds'; + describe('MenuButton', () => { - it('should support a ref on the outermost element', () => { - const ref = jest.fn(); - const { container } = render( - - - - ); - expect(ref).toHaveBeenCalledWith(container.firstChild); - }); + describe('renders as expected - Component API', () => { + it('supports a ref on the outermost element', () => { + const ref = jest.fn(); + const { container } = render( + + + + ); + expect(ref).toHaveBeenCalledWith(container.firstChild); + }); - it('should support a custom class name on the outermost element', () => { - const { container } = render( - - - - ); - expect(container.firstChild).toHaveClass('test'); - }); + it('supports a custom class name on the outermost element', () => { + const { container } = render( + + + + ); + expect(container.firstChild).toHaveClass('test'); + }); - it('should forward additional props on the outermost element', () => { - const { container } = render( - - - - ); - expect(container.firstChild).toHaveAttribute('data-testid', 'test'); - }); + it('forwards additional props on the outermost element', () => { + const { container } = render( + + + + ); + expect(container.firstChild).toHaveAttribute('data-testid', 'test'); + }); - it('should render props.label on the trigger button', () => { - render( - - - - ); - expect(screen.getByRole('button')).toHaveTextContent(/^Test$/); - }); + it('renders props.label on the trigger button', () => { + render( + + + + ); + expect(screen.getByRole('button')).toHaveTextContent(/^Test$/); + }); + + it('supports props.disabled', () => { + render( + + + + ); - it('should open a menu on click', async () => { - render( - - - - ); + expect(screen.getByRole('button')).toBeDisabled(); + }); - await userEvent.click(screen.getByRole('button')); + describe('supports prop.size', () => { + // Button component doesn't apply any size class for `lg` + const sizes = ['sm', 'md']; - expect(screen.getByRole('menu')).toBeTruthy(); - expect(screen.getByRole('menuitem')).toHaveTextContent(/^Action$/); + sizes.forEach((size) => { + it(`size="${size}"`, () => { + const { container } = render( + + + + ); + + expect(container.firstChild).toHaveClass(`${prefix}--btn--${size}`); + }); + }); + }); + + describe('supports prop.kind', () => { + const kinds = ['primary', 'tertiary', 'ghost']; + + kinds.forEach((kind) => { + it(`kind="${kind}"`, () => { + const { container } = render( + + + + ); + + expect(container.firstChild).toHaveClass(`${prefix}--btn--${kind}`); + }); + }); + }); }); - it('should support being disabled', () => { - render( - - - - ); + describe('behaves as expected', () => { + it('opens a menu on click', async () => { + render( + + + + ); + + await userEvent.click(screen.getByRole('button')); - expect(screen.getByRole('button')).toBeDisabled(); + expect(screen.getByRole('menu')).toBeTruthy(); + expect(screen.getByRole('menuitem')).toHaveTextContent(/^Action$/); + }); }); }); From 629f7982e0c57210dec04a48cc11ab137798a808 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Fri, 3 Mar 2023 10:04:05 +0100 Subject: [PATCH 25/43] docs(combo-buton): add default story --- .../ComboButton/ComboButton-test.e2e.js | 4 ++-- .../ComboButton/ComboButton.stories.js | 24 ++++++++++++++++--- .../components/ComboButton/docs/overview.mdx | 2 +- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/e2e/components/ComboButton/ComboButton-test.e2e.js b/e2e/components/ComboButton/ComboButton-test.e2e.js index 0a1609925872..56dc75346093 100644 --- a/e2e/components/ComboButton/ComboButton-test.e2e.js +++ b/e2e/components/ComboButton/ComboButton-test.e2e.js @@ -17,7 +17,7 @@ test.describe('ComboButton', () => { test('combo-button @vrt', async ({ page }) => { await snapshotStory(page, { component: 'ComboButton', - id: 'experimental-unstable-combobutton--playground', + id: 'experimental-unstable-combobutton--default', theme, }); }); @@ -27,7 +27,7 @@ test.describe('ComboButton', () => { test('accessibility-checker @avt', async ({ page }) => { await visitStory(page, { component: 'ComboButton', - id: 'experimental-unstable-combobutton--playground', + id: 'experimental-unstable-combobutton--default', globals: { theme: 'white', }, diff --git a/packages/react/src/components/ComboButton/ComboButton.stories.js b/packages/react/src/components/ComboButton/ComboButton.stories.js index 1a09554cde34..70a670fab163 100644 --- a/packages/react/src/components/ComboButton/ComboButton.stories.js +++ b/packages/react/src/components/ComboButton/ComboButton.stories.js @@ -27,6 +27,16 @@ export default { }, }; +export const Default = () => ( + + + + + + + +); + export const Playground = (args) => { const onClick = action('onClick (MenuItem)'); @@ -34,14 +44,22 @@ export const Playground = (args) => { - - - + ); }; Playground.argTypes = { + children: { + control: { + disable: true, + }, + }, + className: { + control: { + disable: true, + }, + }, label: { defaultValue: 'Primary action', }, diff --git a/packages/react/src/components/ComboButton/docs/overview.mdx b/packages/react/src/components/ComboButton/docs/overview.mdx index 84455cd61e8d..fa1ff29e53f9 100644 --- a/packages/react/src/components/ComboButton/docs/overview.mdx +++ b/packages/react/src/components/ComboButton/docs/overview.mdx @@ -7,7 +7,7 @@ variants={[ { label: 'Default', - variant: 'experimental-unstable-combobutton--playground' + variant: 'experimental-unstable-combobutton--default' }, ]} /> From 3c2df128246ed698f384e718b91fd0e3a3d2d855 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Fri, 3 Mar 2023 10:06:56 +0100 Subject: [PATCH 26/43] docs(menu-button): add default story --- .../MenuButton/MenuButton-test.e2e.js | 4 ++-- .../MenuButton/MenuButton.stories.js | 18 ++++++++++++++++++ .../components/MenuButton/docs/overview.mdx | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/e2e/components/MenuButton/MenuButton-test.e2e.js b/e2e/components/MenuButton/MenuButton-test.e2e.js index d9fe0676bb7a..b11456cb0ff6 100644 --- a/e2e/components/MenuButton/MenuButton-test.e2e.js +++ b/e2e/components/MenuButton/MenuButton-test.e2e.js @@ -17,7 +17,7 @@ test.describe('MenuButton', () => { test('menu-button @vrt', async ({ page }) => { await snapshotStory(page, { component: 'MenuButton', - id: 'experimental-unstable-menubutton--playground', + id: 'experimental-unstable-menubutton--default', theme, }); }); @@ -27,7 +27,7 @@ test.describe('MenuButton', () => { test('accessibility-checker @avt', async ({ page }) => { await visitStory(page, { component: 'MenuButton', - id: 'experimental-unstable-menubutton--playground', + id: 'experimental-unstable-menubutton--default', globals: { theme: 'white', }, diff --git a/packages/react/src/components/MenuButton/MenuButton.stories.js b/packages/react/src/components/MenuButton/MenuButton.stories.js index 804a3ab43e5a..3660cd13cc26 100644 --- a/packages/react/src/components/MenuButton/MenuButton.stories.js +++ b/packages/react/src/components/MenuButton/MenuButton.stories.js @@ -27,6 +27,14 @@ export default { }, }; +export const Default = () => ( + + + + + +); + export const Playground = (args) => { const onClick = action('onClick (MenuItem)'); @@ -42,6 +50,16 @@ export const Playground = (args) => { }; Playground.argTypes = { + children: { + control: { + disable: true, + }, + }, + className: { + control: { + disable: true, + }, + }, label: { defaultValue: 'Actions', }, diff --git a/packages/react/src/components/MenuButton/docs/overview.mdx b/packages/react/src/components/MenuButton/docs/overview.mdx index b5612b853b75..57746b222302 100644 --- a/packages/react/src/components/MenuButton/docs/overview.mdx +++ b/packages/react/src/components/MenuButton/docs/overview.mdx @@ -7,7 +7,7 @@ variants={[ { label: 'Default', - variant: 'experimental-unstable-menubutton--playground' + variant: 'experimental-unstable-menubutton--default' }, ]} /> From 87b683d3ae01032c08d4766b4a0ef9a4d9e8be0b Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Fri, 3 Mar 2023 10:07:36 +0100 Subject: [PATCH 27/43] style(combo-button, menu-button): make lg default size --- packages/react/src/components/ComboButton/index.js | 2 +- packages/react/src/components/MenuButton/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/ComboButton/index.js b/packages/react/src/components/ComboButton/index.js index 3cacc8ddd223..4de3a901dc41 100644 --- a/packages/react/src/components/ComboButton/index.js +++ b/packages/react/src/components/ComboButton/index.js @@ -35,7 +35,7 @@ const ComboButton = React.forwardRef(function ComboButton( disabled, label, onClick, - size = 'md', + size = 'lg', tooltipAlign, translateWithId: t = defaultTranslateWithId, ...rest diff --git a/packages/react/src/components/MenuButton/index.js b/packages/react/src/components/MenuButton/index.js index ca1691557beb..deec7e08628c 100644 --- a/packages/react/src/components/MenuButton/index.js +++ b/packages/react/src/components/MenuButton/index.js @@ -29,7 +29,7 @@ const MenuButton = React.forwardRef(function MenuButton( disabled, kind = defaultButtonKind, label, - size = 'md', + size = 'lg', ...rest }, forwardRef From 9b19326c06f1164153154c1effd539b49efd17ff Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Fri, 3 Mar 2023 16:41:23 +0100 Subject: [PATCH 28/43] fix(menu): remove inline style --- packages/react/src/components/Menu/Menu.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/react/src/components/Menu/Menu.js b/packages/react/src/components/Menu/Menu.js index 3f6f97add869..19848d2e02c2 100644 --- a/packages/react/src/components/Menu/Menu.js +++ b/packages/react/src/components/Menu/Menu.js @@ -211,6 +211,13 @@ const Menu = React.forwardRef(function Menu( // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); + useEffect(() => { + if (menu.current && position[0] >= 0 && position[1] >= 0) { + menu.current.style.left = `${position[0]}px`; + menu.current.style.top = `${position[1]}px`; + } + }, [position]); + const classNames = cx( className, `${prefix}--menu`, @@ -236,13 +243,7 @@ const Menu = React.forwardRef(function Menu( aria-label={label} tabIndex={-1} onKeyDown={handleKeyDown} - onBlur={handleBlur} - // eslint-disable-next-line react/forbid-dom-props - style={{ - ...rest.style, - left: `${position[0]}px`, - top: `${position[1]}px`, - }}> + onBlur={handleBlur}> {children} From 93bde1eb8ca064d24cbeeb3d91b94952baefd3d2 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Fri, 3 Mar 2023 16:41:37 +0100 Subject: [PATCH 29/43] fix(combo-button): remove inline style --- packages/react/src/components/ComboButton/index.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/react/src/components/ComboButton/index.js b/packages/react/src/components/ComboButton/index.js index 4de3a901dc41..56c7df026922 100644 --- a/packages/react/src/components/ComboButton/index.js +++ b/packages/react/src/components/ComboButton/index.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import React, { useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; @@ -46,6 +46,7 @@ const ComboButton = React.forwardRef(function ComboButton( const prefix = usePrefix(); const containerRef = useRef(null); + const menuRef = useRef(null); const ref = useMergedRefs([forwardRef, containerRef]); const [width, setWidth] = useState(0); const { @@ -71,6 +72,12 @@ const ComboButton = React.forwardRef(function ComboButton( } } + useEffect(() => { + if (menuRef.current && width) { + menuRef.current.style.width = `${width}px`; + } + }, [width]); + const containerClasses = classNames( `${prefix}--combo-button__container`, `${prefix}--combo-button__container--${size}`, @@ -108,11 +115,8 @@ const ComboButton = React.forwardRef(function ComboButton( Date: Fri, 3 Mar 2023 16:41:47 +0100 Subject: [PATCH 30/43] fix(menu-button): remove inline style --- packages/react/src/components/MenuButton/index.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/react/src/components/MenuButton/index.js b/packages/react/src/components/MenuButton/index.js index deec7e08628c..69f8ff0105e0 100644 --- a/packages/react/src/components/MenuButton/index.js +++ b/packages/react/src/components/MenuButton/index.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import React, { useRef, useState } from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; @@ -38,6 +38,7 @@ const MenuButton = React.forwardRef(function MenuButton( const prefix = usePrefix(); const triggerRef = useRef(null); + const menuRef = useRef(null); const ref = useMergedRefs([forwardRef, triggerRef]); const [width, setWidth] = useState(0); const { @@ -57,6 +58,12 @@ const MenuButton = React.forwardRef(function MenuButton( } } + useEffect(() => { + if (menuRef.current && width) { + menuRef.current.style.width = `${width}px`; + } + }, [width]); + const triggerClasses = classNames( `${prefix}--menu-button__trigger`, { @@ -85,11 +92,8 @@ const MenuButton = React.forwardRef(function MenuButton( {label} Date: Mon, 6 Mar 2023 09:11:35 +0100 Subject: [PATCH 31/43] test: fix typos --- packages/react/src/components/ComboButton/ComboButton-test.js | 4 ++-- packages/react/src/components/MenuButton/MenuButton-test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react/src/components/ComboButton/ComboButton-test.js b/packages/react/src/components/ComboButton/ComboButton-test.js index cf73b17c75a0..8ff643d1c320 100644 --- a/packages/react/src/components/ComboButton/ComboButton-test.js +++ b/packages/react/src/components/ComboButton/ComboButton-test.js @@ -68,7 +68,7 @@ describe('ComboButton', () => { expect(screen.getAllByRole('button')[1]).toBeDisabled(); }); - describe('supports prop.size', () => { + describe('supports props.size', () => { const sizes = ['sm', 'md', 'lg']; sizes.forEach((size) => { @@ -86,7 +86,7 @@ describe('ComboButton', () => { }); }); - describe('supports prop.tooltipAlign', () => { + describe('supports props.tooltipAlign', () => { const alignments = [ 'top', 'top-left', diff --git a/packages/react/src/components/MenuButton/MenuButton-test.js b/packages/react/src/components/MenuButton/MenuButton-test.js index 75f88445e211..f95541549ab1 100644 --- a/packages/react/src/components/MenuButton/MenuButton-test.js +++ b/packages/react/src/components/MenuButton/MenuButton-test.js @@ -64,7 +64,7 @@ describe('MenuButton', () => { expect(screen.getByRole('button')).toBeDisabled(); }); - describe('supports prop.size', () => { + describe('supports props.size', () => { // Button component doesn't apply any size class for `lg` const sizes = ['sm', 'md']; @@ -81,7 +81,7 @@ describe('MenuButton', () => { }); }); - describe('supports prop.kind', () => { + describe('supports props.kind', () => { const kinds = ['primary', 'tertiary', 'ghost']; kinds.forEach((kind) => { From 29d051f00dc31db5c59986bfd23277600661a2b1 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Mon, 6 Mar 2023 09:14:04 +0100 Subject: [PATCH 32/43] docs(menu-button): sync playground and default story --- .../react/src/components/MenuButton/MenuButton.stories.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react/src/components/MenuButton/MenuButton.stories.js b/packages/react/src/components/MenuButton/MenuButton.stories.js index 3660cd13cc26..d01124b2a1aa 100644 --- a/packages/react/src/components/MenuButton/MenuButton.stories.js +++ b/packages/react/src/components/MenuButton/MenuButton.stories.js @@ -31,7 +31,9 @@ export const Default = () => ( - + + + ); From f0d091d809c0bf015a38ec5e761fa1edd5790d9d Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Mon, 6 Mar 2023 09:14:11 +0100 Subject: [PATCH 33/43] docs(combo-button): sync playground and default story --- .../react/src/components/ComboButton/ComboButton.stories.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react/src/components/ComboButton/ComboButton.stories.js b/packages/react/src/components/ComboButton/ComboButton.stories.js index 70a670fab163..12a2e743ce37 100644 --- a/packages/react/src/components/ComboButton/ComboButton.stories.js +++ b/packages/react/src/components/ComboButton/ComboButton.stories.js @@ -44,7 +44,9 @@ export const Playground = (args) => { - + + + ); }; From 5350c7aec276b8a0ae9d6e22196744d27a1ab84a Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Mon, 6 Mar 2023 09:23:36 +0100 Subject: [PATCH 34/43] feat(menu): add support fort props.onOpen --- .../__tests__/__snapshots__/PublicAPI-test.js.snap | 3 +++ packages/react/src/components/Menu/Menu.js | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index c3428cd87a1a..843e5852bb23 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -9127,6 +9127,9 @@ Map { "onClose": Object { "type": "func", }, + "onOpen": Object { + "type": "func", + }, "open": Object { "type": "bool", }, diff --git a/packages/react/src/components/Menu/Menu.js b/packages/react/src/components/Menu/Menu.js index 19848d2e02c2..7b6fb4900a92 100644 --- a/packages/react/src/components/Menu/Menu.js +++ b/packages/react/src/components/Menu/Menu.js @@ -31,6 +31,7 @@ const Menu = React.forwardRef(function Menu( className, label, onClose, + onOpen, open, size = 'sm', target = document.body, @@ -81,6 +82,10 @@ const Menu = React.forwardRef(function Menu( focusReturn.current = document.activeElement; setPosition(calculatePosition()); menu.current.focus(); + + if (onOpen) { + onOpen(); + } } } @@ -273,6 +278,11 @@ Menu.propTypes = { */ onClose: PropTypes.func, + /** + * Provide an optional function to be called when the Menu is opened. + */ + onOpen: PropTypes.func, + /** * Whether the Menu is open or not. */ From 64b949a02b3a85e4a9a933a1f3980316569a7f8b Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Mon, 6 Mar 2023 09:24:15 +0100 Subject: [PATCH 35/43] fix(combo-button, menu-button): simplify menu width styling --- packages/react/src/components/ComboButton/index.js | 11 +++++------ packages/react/src/components/MenuButton/index.js | 11 +++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/react/src/components/ComboButton/index.js b/packages/react/src/components/ComboButton/index.js index 56c7df026922..abdc02c6107d 100644 --- a/packages/react/src/components/ComboButton/index.js +++ b/packages/react/src/components/ComboButton/index.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useRef, useState } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; @@ -72,11 +72,9 @@ const ComboButton = React.forwardRef(function ComboButton( } } - useEffect(() => { - if (menuRef.current && width) { - menuRef.current.style.width = `${width}px`; - } - }, [width]); + function handleOpen() { + menuRef.current.style.width = `${width}px`; + } const containerClasses = classNames( `${prefix}--combo-button__container`, @@ -121,6 +119,7 @@ const ComboButton = React.forwardRef(function ComboButton( size={size} open={open} onClose={handleClose} + onOpen={handleOpen} x={x} y={[y[0] - spacing, y[1] + spacing]}> {children} diff --git a/packages/react/src/components/MenuButton/index.js b/packages/react/src/components/MenuButton/index.js index 69f8ff0105e0..b15b959c356f 100644 --- a/packages/react/src/components/MenuButton/index.js +++ b/packages/react/src/components/MenuButton/index.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import React, { useRef, useState, useEffect } from 'react'; +import React, { useRef, useState } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; @@ -58,11 +58,9 @@ const MenuButton = React.forwardRef(function MenuButton( } } - useEffect(() => { - if (menuRef.current && width) { - menuRef.current.style.width = `${width}px`; - } - }, [width]); + function handleOpen() { + menuRef.current.style.width = `${width}px`; + } const triggerClasses = classNames( `${prefix}--menu-button__trigger`, @@ -98,6 +96,7 @@ const MenuButton = React.forwardRef(function MenuButton( size={size} open={open} onClose={handleClose} + onOpen={handleOpen} x={x} y={[y[0] - spacing, y[1] + spacing]}> {children} From d0d4137b89c5f910195bd0350e64c0dbdfe3b717 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Mon, 6 Mar 2023 10:31:01 +0100 Subject: [PATCH 36/43] fix(combo-button): fix menu width on firefox --- .../react/src/components/ComboButton/index.js | 15 ++++++++------- .../components/combo-button/_combo-button.scss | 4 ++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/react/src/components/ComboButton/index.js b/packages/react/src/components/ComboButton/index.js index abdc02c6107d..85ed51b55c6c 100644 --- a/packages/react/src/components/ComboButton/index.js +++ b/packages/react/src/components/ComboButton/index.js @@ -92,13 +92,14 @@ const ComboButton = React.forwardRef(function ComboButton( return (
- +
+ +
Date: Mon, 6 Mar 2023 10:36:09 +0100 Subject: [PATCH 37/43] fix(menu): set position style immediately when calculated --- packages/react/src/components/Menu/Menu.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/react/src/components/Menu/Menu.js b/packages/react/src/components/Menu/Menu.js index 7b6fb4900a92..16558b5cdff8 100644 --- a/packages/react/src/components/Menu/Menu.js +++ b/packages/react/src/components/Menu/Menu.js @@ -80,7 +80,12 @@ const Menu = React.forwardRef(function Menu( function handleOpen() { if (menu.current) { focusReturn.current = document.activeElement; - setPosition(calculatePosition()); + + const pos = calculatePosition(); + menu.current.style.left = `${pos[0]}px`; + menu.current.style.top = `${pos[1]}px`; + setPosition(pos); + menu.current.focus(); if (onOpen) { @@ -216,13 +221,6 @@ const Menu = React.forwardRef(function Menu( // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); - useEffect(() => { - if (menu.current && position[0] >= 0 && position[1] >= 0) { - menu.current.style.left = `${position[0]}px`; - menu.current.style.top = `${position[1]}px`; - } - }, [position]); - const classNames = cx( className, `${prefix}--menu`, From ff5a5407ab3804f56c30d997cccdff486e74d0b1 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Fri, 10 Mar 2023 16:31:06 +0100 Subject: [PATCH 38/43] docs(menu-button): add more stories --- .../MenuButton/MenuButton.stories.js | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/MenuButton/MenuButton.stories.js b/packages/react/src/components/MenuButton/MenuButton.stories.js index d01124b2a1aa..3839d92d304d 100644 --- a/packages/react/src/components/MenuButton/MenuButton.stories.js +++ b/packages/react/src/components/MenuButton/MenuButton.stories.js @@ -32,11 +32,32 @@ export const Default = () => ( + +); + +export const WithDanger = () => ( + + + + ); +export const WithDividers = () => ( + + + + + + + + + + +); + export const Playground = (args) => { const onClick = action('onClick (MenuItem)'); @@ -45,8 +66,6 @@ export const Playground = (args) => { - - ); }; From c2f475cd2f6fd3845a975e491ca9bb77e589e5e7 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Fri, 10 Mar 2023 16:32:29 +0100 Subject: [PATCH 39/43] docs(combo-button): add "with-danger" story --- .../src/components/ComboButton/ComboButton.stories.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/react/src/components/ComboButton/ComboButton.stories.js b/packages/react/src/components/ComboButton/ComboButton.stories.js index 12a2e743ce37..f2919d7e630b 100644 --- a/packages/react/src/components/ComboButton/ComboButton.stories.js +++ b/packages/react/src/components/ComboButton/ComboButton.stories.js @@ -32,6 +32,14 @@ export const Default = () => ( + +); + +export const WithDanger = () => ( + + + + From 2b4593a1f508c536acfcab23627dd552bb0f0300 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Mon, 13 Mar 2023 08:30:51 +0100 Subject: [PATCH 40/43] docs(menu-button): hide children, className in story --- .../MenuButton/MenuButton.stories.js | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/react/src/components/MenuButton/MenuButton.stories.js b/packages/react/src/components/MenuButton/MenuButton.stories.js index 3839d92d304d..43c01c4f48e2 100644 --- a/packages/react/src/components/MenuButton/MenuButton.stories.js +++ b/packages/react/src/components/MenuButton/MenuButton.stories.js @@ -25,6 +25,18 @@ export default { page: mdx, }, }, + argTypes: { + children: { + table: { + disable: true, + }, + }, + className: { + table: { + disable: true, + }, + }, + }, }; export const Default = () => ( @@ -71,16 +83,6 @@ export const Playground = (args) => { }; Playground.argTypes = { - children: { - control: { - disable: true, - }, - }, - className: { - control: { - disable: true, - }, - }, label: { defaultValue: 'Actions', }, From c43dd2e06247adaf096e8d782d57dffe504d6ad4 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Mon, 13 Mar 2023 08:31:09 +0100 Subject: [PATCH 41/43] docs(combo-button): hide children, className, translateWithId in story --- .../ComboButton/ComboButton.stories.js | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/react/src/components/ComboButton/ComboButton.stories.js b/packages/react/src/components/ComboButton/ComboButton.stories.js index f2919d7e630b..6ebd5dab0a6b 100644 --- a/packages/react/src/components/ComboButton/ComboButton.stories.js +++ b/packages/react/src/components/ComboButton/ComboButton.stories.js @@ -25,6 +25,23 @@ export default { page: mdx, }, }, + argTypes: { + children: { + table: { + disable: true, + }, + }, + className: { + table: { + disable: true, + }, + }, + translateWithId: { + table: { + disable: true, + }, + }, + }, }; export const Default = () => ( @@ -60,16 +77,6 @@ export const Playground = (args) => { }; Playground.argTypes = { - children: { - control: { - disable: true, - }, - }, - className: { - control: { - disable: true, - }, - }, label: { defaultValue: 'Primary action', }, From b323368401446ed1a8518850d96a8df80892daf1 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Tue, 14 Mar 2023 09:42:52 +0100 Subject: [PATCH 42/43] docs(combo-button): only hide certain props in playground story --- .../ComboButton/ComboButton.stories.js | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/react/src/components/ComboButton/ComboButton.stories.js b/packages/react/src/components/ComboButton/ComboButton.stories.js index 6ebd5dab0a6b..ca696bfa0319 100644 --- a/packages/react/src/components/ComboButton/ComboButton.stories.js +++ b/packages/react/src/components/ComboButton/ComboButton.stories.js @@ -25,23 +25,6 @@ export default { page: mdx, }, }, - argTypes: { - children: { - table: { - disable: true, - }, - }, - className: { - table: { - disable: true, - }, - }, - translateWithId: { - table: { - disable: true, - }, - }, - }, }; export const Default = () => ( @@ -77,6 +60,21 @@ export const Playground = (args) => { }; Playground.argTypes = { + children: { + table: { + disable: true, + }, + }, + className: { + table: { + disable: true, + }, + }, + translateWithId: { + table: { + disable: true, + }, + }, label: { defaultValue: 'Primary action', }, From 0c0da20d617c4a3a6b7d336ab5d749ca43fb7ad6 Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Tue, 14 Mar 2023 09:42:59 +0100 Subject: [PATCH 43/43] docs(menu-button): only hide certain props in playground story --- .../MenuButton/MenuButton.stories.js | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/react/src/components/MenuButton/MenuButton.stories.js b/packages/react/src/components/MenuButton/MenuButton.stories.js index 43c01c4f48e2..67ff145c577a 100644 --- a/packages/react/src/components/MenuButton/MenuButton.stories.js +++ b/packages/react/src/components/MenuButton/MenuButton.stories.js @@ -25,18 +25,6 @@ export default { page: mdx, }, }, - argTypes: { - children: { - table: { - disable: true, - }, - }, - className: { - table: { - disable: true, - }, - }, - }, }; export const Default = () => ( @@ -83,6 +71,16 @@ export const Playground = (args) => { }; Playground.argTypes = { + children: { + table: { + disable: true, + }, + }, + className: { + table: { + disable: true, + }, + }, label: { defaultValue: 'Actions', },