Skip to content

Commit

Permalink
feat: add experimental combo-button and menu-button components (#13224)
Browse files Browse the repository at this point in the history
* feat: add experimental combo-button component

* docs(combo-button): add disabled and danger item to demo

* docs: add menuitem subcomponents where applicable

* docs(combo-button): fix subcomponents placing

* refactor(combo-button): outsource common logic to useAttachedMenu hook

* feat: add experimental menu-button component

* feat(combo-button, menu-button): add props.className support

* style(combo-button): remove props.kind as only primary is supported

* feat(combo-button, menu-button): add to ts index

* style(menu-button): remove support for secondary button kind

* style(menu-button): add min-width and align trigger with menu

* fix(menu): avoid column gap from empty icon grid cell

* style(combo-button): ensure container min-width of 10rem

* feat(combo-button): add i18n support

* feat(combo-button): add support for prop-controllable tooltip alignment

* feat(menu-button): add support for ref and additional props

* test(menu-button): add tests

* feat(combo-button): add support for ref and additional props

* test(combo-button): add tests

* test(menu-button): align tests

* test: update react exports snapshot

* test(menu-button): add test to verify disabled prop works

* test(combo-button): extend tests

* test(menu-button): extend tests

* docs(combo-buton): add default story

* docs(menu-button): add default story

* style(combo-button, menu-button): make lg default size

* fix(menu): remove inline style

* fix(combo-button): remove inline style

* fix(menu-button): remove inline style

* test: fix typos

* docs(menu-button): sync playground and default story

* docs(combo-button): sync playground and default story

* feat(menu): add support fort props.onOpen

* fix(combo-button, menu-button): simplify menu width styling

* fix(combo-button): fix menu width on firefox

* fix(menu): set position style immediately when calculated

* docs(menu-button): add more stories

* docs(combo-button): add "with-danger" story

* docs(menu-button): hide children, className in story

* docs(combo-button): hide children, className, translateWithId in story

* docs(combo-button): only hide certain props in playground story

* docs(menu-button): only hide certain props in playground story

---------

Co-authored-by: Francine Lucca <[email protected]>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Mar 14, 2023
1 parent d87508d commit 9ffb292
Show file tree
Hide file tree
Showing 27 changed files with 1,271 additions and 58 deletions.
37 changes: 37 additions & 0 deletions e2e/components/ComboButton/ComboButton-test.e2e.js
Original file line number Diff line number Diff line change
@@ -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('combo-button @vrt', async ({ page }) => {
await snapshotStory(page, {
component: 'ComboButton',
id: 'experimental-unstable-combobutton--default',
theme,
});
});
});
});

test('accessibility-checker @avt', async ({ page }) => {
await visitStory(page, {
component: 'ComboButton',
id: 'experimental-unstable-combobutton--default',
globals: {
theme: 'white',
},
});
await expect(page).toHaveNoACViolations('ComboButton');
});
});
37 changes: 37 additions & 0 deletions e2e/components/MenuButton/MenuButton-test.e2e.js
Original file line number Diff line number Diff line change
@@ -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('MenuButton', () => {
themes.forEach((theme) => {
test.describe(theme, () => {
test('menu-button @vrt', async ({ page }) => {
await snapshotStory(page, {
component: 'MenuButton',
id: 'experimental-unstable-menubutton--default',
theme,
});
});
});
});

test('accessibility-checker @avt', async ({ page }) => {
await visitStory(page, {
component: 'MenuButton',
id: 'experimental-unstable-menubutton--default',
globals: {
theme: 'white',
},
});
await expect(page).toHaveNoACViolations('MenuButton');
});
});
94 changes: 94 additions & 0 deletions packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -9038,6 +9038,57 @@ Map {
"8": "cool-gray",
"9": "warm-gray",
},
"unstable_ComboButton" => Object {
"$$typeof": Symbol(react.forward_ref),
"propTypes": Object {
"children": Object {
"isRequired": true,
"type": "node",
},
"className": Object {
"type": "string",
},
"disabled": Object {
"type": "bool",
},
"label": Object {
"isRequired": true,
"type": "string",
},
"onClick": Object {
"type": "func",
},
"size": Object {
"args": Array [
Array [
"sm",
"md",
"lg",
],
],
"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",
},
},
"render": [Function],
},
"unstable_FeatureFlags" => Object {
"propTypes": Object {
"children": Object {
Expand Down Expand Up @@ -9101,6 +9152,9 @@ Map {
"onClose": Object {
"type": "func",
},
"onOpen": Object {
"type": "func",
},
"open": Object {
"type": "bool",
},
Expand Down Expand Up @@ -9157,6 +9211,46 @@ Map {
},
"render": [Function],
},
"unstable_MenuButton" => Object {
"$$typeof": Symbol(react.forward_ref),
"propTypes": Object {
"children": Object {
"isRequired": true,
"type": "node",
},
"className": Object {
"type": "string",
},
"disabled": Object {
"type": "bool",
},
"kind": Object {
"args": Array [
Array [
"primary",
"tertiary",
"ghost",
],
],
"type": "oneOf",
},
"label": Object {
"isRequired": true,
"type": "string",
},
"size": Object {
"args": Array [
Array [
"sm",
"md",
"lg",
],
],
"type": "oneOf",
},
},
"render": [Function],
},
"unstable_MenuItem" => Object {
"$$typeof": Symbol(react.forward_ref),
"propTypes": Object {
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/__tests__/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
162 changes: 162 additions & 0 deletions packages/react/src/components/ComboButton/ComboButton-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* 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 './';

const prefix = 'cds';

describe('ComboButton', () => {
describe('renders as expected - Component API', () => {
it('supports a ref on the outermost element', () => {
const ref = jest.fn();
const { container } = render(
<ComboButton label="Primary action" ref={ref}>
<MenuItem label="Additional action" />
</ComboButton>
);
expect(ref).toHaveBeenCalledWith(container.firstChild);
});

it('supports a custom class name on the outermost element', () => {
const { container } = render(
<ComboButton label="Primary action" className="test">
<MenuItem label="Additional action" />
</ComboButton>
);
expect(container.firstChild).toHaveClass('test');
});

it('forwards additional props on the outermost element', () => {
const { container } = render(
<ComboButton label="Primary action" data-testid="test">
<MenuItem label="Additional action" />
</ComboButton>
);
expect(container.firstChild).toHaveAttribute('data-testid', 'test');
});

it('renders props.label on the trigger button', () => {
render(
<ComboButton label="Test">
<MenuItem label="Additional action" />
</ComboButton>
);
expect(screen.getAllByRole('button')[0]).toHaveTextContent(/^Test$/);
});

it('supports props.disabled', () => {
render(
<ComboButton label="Primary action" disabled>
<MenuItem label="Additional action" />
</ComboButton>
);

// primary action button
expect(screen.getAllByRole('button')[0]).toBeDisabled();

// trigger button
expect(screen.getAllByRole('button')[1]).toBeDisabled();
});

describe('supports props.size', () => {
const sizes = ['sm', 'md', 'lg'];

sizes.forEach((size) => {
it(`size="${size}"`, () => {
const { container } = render(
<ComboButton label="Primary action" size={size}>
<MenuItem label="Additional action" />
</ComboButton>
);

expect(container.firstChild).toHaveClass(
`${prefix}--combo-button__container--${size}`
);
});
});
});

describe('supports props.tooltipAlign', () => {
const alignments = [
'top',
'top-left',
'top-right',
'bottom',
'bottom-left',
'bottom-right',
'left',
'right',
];

alignments.forEach((alignment) => {
it(`tooltipAlign="${alignment}"`, () => {
const { container } = render(
<ComboButton label="Primary action" tooltipAlign={alignment}>
<MenuItem label="Additional action" />
</ComboButton>
);

expect(container.firstChild.lastChild).toHaveClass(
`${prefix}--popover--${alignment}`
);
});
});
});

it('supports props.translateWithId', () => {
const t = () => 'test';

render(
<ComboButton label="Primary action" translateWithId={t}>
<MenuItem label="Additional action" />
</ComboButton>
);

const triggerButton = screen.getAllByRole('button')[1];
const tooltipId = triggerButton.getAttribute('aria-labelledby');
const tooltip = document.getElementById(tooltipId);

expect(tooltip).toHaveTextContent(t());
});
});

describe('behaves as expected', () => {
it('emits props.onClick on primary action click', async () => {
const onClick = jest.fn();
render(
<ComboButton label="Test" onClick={onClick}>
<MenuItem label="Additional action" />
</ComboButton>
);

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(
<ComboButton label="Primary action">
<MenuItem label="Additional action" />
</ComboButton>
);

await userEvent.click(screen.getAllByRole('button')[1]);

expect(screen.getByRole('menu')).toBeTruthy();
expect(screen.getByRole('menuitem')).toHaveTextContent(
/^Additional action$/
);
});
});
});
Loading

0 comments on commit 9ffb292

Please sign in to comment.