Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add experimental combo-button and menu-button components #13224

Merged
merged 47 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
f1c7893
feat: add experimental combo-button component
janhassel Feb 24, 2023
93a3553
docs(combo-button): add disabled and danger item to demo
janhassel Feb 24, 2023
c3704eb
docs: add menuitem subcomponents where applicable
janhassel Feb 24, 2023
59ad788
docs(combo-button): fix subcomponents placing
janhassel Feb 24, 2023
c8b56b6
refactor(combo-button): outsource common logic to useAttachedMenu hook
janhassel Feb 24, 2023
aa1045f
feat: add experimental menu-button component
janhassel Feb 24, 2023
1f06c2f
feat(combo-button, menu-button): add props.className support
janhassel Mar 1, 2023
5f84117
style(combo-button): remove props.kind as only primary is supported
janhassel Mar 1, 2023
7236a8a
feat(combo-button, menu-button): add to ts index
janhassel Mar 1, 2023
858e58e
style(menu-button): remove support for secondary button kind
janhassel Mar 1, 2023
a4e75fa
style(menu-button): add min-width and align trigger with menu
janhassel Mar 1, 2023
ae3bdae
fix(menu): avoid column gap from empty icon grid cell
janhassel Mar 1, 2023
215273e
style(combo-button): ensure container min-width of 10rem
janhassel Mar 1, 2023
dc60aca
feat(combo-button): add i18n support
janhassel Mar 2, 2023
3479b43
feat(combo-button): add support for prop-controllable tooltip alignment
janhassel Mar 2, 2023
f162faa
feat(menu-button): add support for ref and additional props
janhassel Mar 2, 2023
3265678
test(menu-button): add tests
janhassel Mar 2, 2023
5e963d6
feat(combo-button): add support for ref and additional props
janhassel Mar 2, 2023
9e83508
test(combo-button): add tests
janhassel Mar 2, 2023
9702941
test(menu-button): align tests
janhassel Mar 2, 2023
c820c0c
test: update react exports snapshot
janhassel Mar 2, 2023
2c45fb1
test(menu-button): add test to verify disabled prop works
janhassel Mar 2, 2023
c10a2e9
Merge branch 'main' into 3371
janhassel Mar 2, 2023
c0a3f23
test(combo-button): extend tests
janhassel Mar 3, 2023
e049aa0
test(menu-button): extend tests
janhassel Mar 3, 2023
629f798
docs(combo-buton): add default story
janhassel Mar 3, 2023
3c2df12
docs(menu-button): add default story
janhassel Mar 3, 2023
87b683d
style(combo-button, menu-button): make lg default size
janhassel Mar 3, 2023
9b19326
fix(menu): remove inline style
janhassel Mar 3, 2023
93bde1e
fix(combo-button): remove inline style
janhassel Mar 3, 2023
2495a1b
fix(menu-button): remove inline style
janhassel Mar 3, 2023
d16dc5d
test: fix typos
janhassel Mar 6, 2023
29d051f
docs(menu-button): sync playground and default story
janhassel Mar 6, 2023
f0d091d
docs(combo-button): sync playground and default story
janhassel Mar 6, 2023
5350c7a
feat(menu): add support fort props.onOpen
janhassel Mar 6, 2023
64b949a
fix(combo-button, menu-button): simplify menu width styling
janhassel Mar 6, 2023
d0d4137
fix(combo-button): fix menu width on firefox
janhassel Mar 6, 2023
fa79692
fix(menu): set position style immediately when calculated
janhassel Mar 6, 2023
fba15c0
Merge branch 'main' into 3371
janhassel Mar 9, 2023
ff5a540
docs(menu-button): add more stories
janhassel Mar 10, 2023
c2f475c
docs(combo-button): add "with-danger" story
janhassel Mar 10, 2023
2b4593a
docs(menu-button): hide children, className in story
janhassel Mar 13, 2023
c43dd2e
docs(combo-button): hide children, className, translateWithId in story
janhassel Mar 13, 2023
b323368
docs(combo-button): only hide certain props in playground story
janhassel Mar 14, 2023
0c0da20
docs(menu-button): only hide certain props in playground story
janhassel Mar 14, 2023
fd17d3f
Merge branch 'main' into 3371
francinelucca Mar 14, 2023
d0df7d3
Merge branch 'main' into 3371
kodiakhq[bot] Mar 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions packages/react/src/components/ComboButton/ComboButton.mdx
Original file line number Diff line number Diff line change
@@ -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)

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Overview](#overview)
- [Component API](#component-api)
- [Feedback](#feedback)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## 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
<ComboButton label="Primary action">
<MenuItem label="Second action" />
<MenuItem label="Third action" />
<MenuItem label="Fourth action" />
</ComboButton>
```

## Component API

<ArgsTable />

## 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).
Original file line number Diff line number Diff line change
@@ -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 (
<ComboButton {...args}>
<MenuItem label="Second action" onClick={onClick} />
<MenuItem label="Third action" onClick={onClick} />
<MenuItem label="Fourth action" onClick={onClick} />
</ComboButton>
);
};

Playground.argTypes = {
label: {
defaultValue: 'Primary action',
},
};

Playground.args = {
onClick: action('onClick'),
};
janhassel marked this conversation as resolved.
Show resolved Hide resolved
13 changes: 13 additions & 0 deletions packages/react/src/components/ComboButton/docs/overview.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Live demo

<StorybookDemo
themeSelector
tall
url="https://react.carbondesignsystem.com"
variants={[
{
label: 'Default',
variant: 'experimental-unstable-combobutton--playground'
},
]}
/>
164 changes: 164 additions & 0 deletions packages/react/src/components/ComboButton/index.js
Original file line number Diff line number Diff line change
@@ -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,
});
janhassel marked this conversation as resolved.
Show resolved Hide resolved

const triggerClasses = classNames(`${prefix}--combo-button__trigger`);

return (
<div className={containerClasses} ref={containerRef}>
<Button
size={size}
kind={kind}
disabled={disabled}
onClick={handlePrimaryActionClick}>
{label}
</Button>
<IconButton
className={triggerClasses}
label="Additional actions"
size={size}
kind={kind}
disabled={disabled}
aria-haspopup
aria-expanded={open}
onClick={handleTriggerClick}
onMouseDown={handleTriggerMousedown}
aria-owns={id}>
<ChevronDown />
</IconButton>
<Menu
id={id}
// eslint-disable-next-line react/forbid-component-props
style={{
width: `${width}px`,
}}
francinelucca marked this conversation as resolved.
Show resolved Hide resolved
label="Additional actions"
size={size}
open={open}
onClose={closeMenu}
x={position[0]}
y={position[1]}>
{children}
</Menu>
</div>
);
}

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 };
Original file line number Diff line number Diff line change
Expand Up @@ -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)

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/components/Menu/Menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
}}>
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/styles/scss/components/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
@use 'checkbox';
@use 'code-snippet';
@use 'combo-box';
@use 'combo-button';
@use 'contained-list';
@use 'content-switcher';
@use 'copy-button';
Expand Down
30 changes: 30 additions & 0 deletions packages/styles/scss/components/combo-button/_combo-button.scss
Original file line number Diff line number Diff line change
@@ -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);
}
}
11 changes: 11 additions & 0 deletions packages/styles/scss/components/combo-button/_index.scss
Original file line number Diff line number Diff line change
@@ -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;