From a28d727caf1ac83ed01f991531f7c27a5ac0704c Mon Sep 17 00:00:00 2001 From: Garrett Bear Date: Wed, 9 Nov 2022 13:55:13 -0800 Subject: [PATCH] Feat/16290/add button component (#16305) * add button component * add all button props * update tests * add button type prop * fix base button size const * add href prop to button base * Update ui/components/component-library/button/README.mdx Co-authored-by: George Marshall * Update ui/components/component-library/button/README.mdx Co-authored-by: George Marshall * update tests * Update ui/components/component-library/button-primary/button-primary.js Co-authored-by: George Marshall * Update ui/components/component-library/button/README.mdx Co-authored-by: George Marshall * Update ui/components/component-library/button/button.stories.js Co-authored-by: George Marshall * Update ui/components/component-library/button/button.js Co-authored-by: George Marshall * Update ui/components/component-library/button/button.stories.js Co-authored-by: George Marshall * update button props on readme * linting issue fix Co-authored-by: George Marshall --- .../component-library/button-link/README.mdx | 6 +- .../button-link/button-link.stories.js | 2 +- .../button-link/button-link.test.js | 2 +- .../button-primary/README.mdx | 6 +- .../button-primary/button-primary.js | 2 +- .../button-primary/button-primary.stories.js | 2 +- .../button-primary/button-primary.test.js | 2 +- .../button-secondary/README.mdx | 6 +- .../button-secondary/button-secondary.js | 2 +- .../button-secondary.stories.js | 2 +- .../button-secondary/button-secondary.test.js | 2 +- .../component-library/button/README.mdx | 178 +++++++++++++++ .../button/__snapshots__/button.test.js.snap | 51 +++++ .../button/button.constants.js | 14 ++ .../component-library/button/button.js | 34 +++ .../button/button.stories.js | 212 ++++++++++++++++++ .../component-library/button/button.test.js | 157 +++++++++++++ .../component-library/button/index.js | 2 + 18 files changed, 665 insertions(+), 17 deletions(-) create mode 100644 ui/components/component-library/button/README.mdx create mode 100644 ui/components/component-library/button/__snapshots__/button.test.js.snap create mode 100644 ui/components/component-library/button/button.constants.js create mode 100644 ui/components/component-library/button/button.js create mode 100644 ui/components/component-library/button/button.stories.js create mode 100644 ui/components/component-library/button/button.test.js create mode 100644 ui/components/component-library/button/index.js diff --git a/ui/components/component-library/button-link/README.mdx b/ui/components/component-library/button-link/README.mdx index d1a743912..2c33d5dad 100644 --- a/ui/components/component-library/button-link/README.mdx +++ b/ui/components/component-library/button-link/README.mdx @@ -43,12 +43,12 @@ import { ButtonLink } from '../ui/component-library/button/button-link/button-li ``` -### Type +### Danger -Use the `type` prop and the `BUTTON_TYPES` object from `./ui/helpers/constants/design-system.js` to change the context of `ButtonLink`. +Use the `danger` boolean prop to change the `ButtonPrimary` to danger color. - + ```jsx diff --git a/ui/components/component-library/button-link/button-link.stories.js b/ui/components/component-library/button-link/button-link.stories.js index e721459f4..d068ff0ff 100644 --- a/ui/components/component-library/button-link/button-link.stories.js +++ b/ui/components/component-library/button-link/button-link.stories.js @@ -139,7 +139,7 @@ export const Size = (args) => ( ); -export const Type = (args) => ( +export const Danger = (args) => ( Normal {/* Test Anchor tag to match exactly as button */} diff --git a/ui/components/component-library/button-link/button-link.test.js b/ui/components/component-library/button-link/button-link.test.js index 473714fb0..2c246f969 100644 --- a/ui/components/component-library/button-link/button-link.test.js +++ b/ui/components/component-library/button-link/button-link.test.js @@ -53,7 +53,7 @@ describe('ButtonLink', () => { ); }); - it('should render with different types', () => { + it('should render as danger', () => { const { getByTestId } = render( <> diff --git a/ui/components/component-library/button-primary/README.mdx b/ui/components/component-library/button-primary/README.mdx index 69a110540..372339ea9 100644 --- a/ui/components/component-library/button-primary/README.mdx +++ b/ui/components/component-library/button-primary/README.mdx @@ -41,12 +41,12 @@ import { ButtonPrimary } from '../ui/component-library/button/button-primary/but ``` -### Type +### Danger -Use the `type` prop and the `BUTTON_TYPES` object from `./ui/helpers/constants/design-system.js` to change the context of `ButtonPrimary`. +Use the `danger` boolean prop to change the `ButtonPrimary` to danger color. - + ```jsx diff --git a/ui/components/component-library/button-primary/button-primary.js b/ui/components/component-library/button-primary/button-primary.js index 78dc3e93d..9cfadcea8 100644 --- a/ui/components/component-library/button-primary/button-primary.js +++ b/ui/components/component-library/button-primary/button-primary.js @@ -28,7 +28,7 @@ ButtonPrimary.propTypes = { */ className: PropTypes.string, /** - * Boolean to change button type to Danger when true + * When true, `ButtonPrimary` color becomes Danger. */ danger: PropTypes.bool, /** diff --git a/ui/components/component-library/button-primary/button-primary.stories.js b/ui/components/component-library/button-primary/button-primary.stories.js index 8f9c3cafc..114b07d56 100644 --- a/ui/components/component-library/button-primary/button-primary.stories.js +++ b/ui/components/component-library/button-primary/button-primary.stories.js @@ -122,7 +122,7 @@ export const Size = (args) => ( ); -export const Type = (args) => ( +export const Danger = (args) => ( Normal {/* Test Anchor tag to match exactly as button */} diff --git a/ui/components/component-library/button-primary/button-primary.test.js b/ui/components/component-library/button-primary/button-primary.test.js index 34751ae7f..a325a04b1 100644 --- a/ui/components/component-library/button-primary/button-primary.test.js +++ b/ui/components/component-library/button-primary/button-primary.test.js @@ -66,7 +66,7 @@ describe('ButtonPrimary', () => { ); }); - it('should render with different types', () => { + it('should render as danger', () => { const { getByTestId } = render( <> diff --git a/ui/components/component-library/button-secondary/README.mdx b/ui/components/component-library/button-secondary/README.mdx index 16a5a74a6..fba6ebb61 100644 --- a/ui/components/component-library/button-secondary/README.mdx +++ b/ui/components/component-library/button-secondary/README.mdx @@ -41,12 +41,12 @@ import { ButtonSecondary } from '../ui/component-library/button/button-secondary ``` -### Type +### Danger -Use the `type` prop and the `BUTTON_TYPES` object from `./ui/helpers/constants/design-system.js` to change the context of `ButtonSecondary`. +Use the `danger` boolean prop to change the `ButtonPrimary` to danger color. - + ```jsx diff --git a/ui/components/component-library/button-secondary/button-secondary.js b/ui/components/component-library/button-secondary/button-secondary.js index c8d4ed608..86a9ca8e7 100644 --- a/ui/components/component-library/button-secondary/button-secondary.js +++ b/ui/components/component-library/button-secondary/button-secondary.js @@ -28,7 +28,7 @@ ButtonSecondary.propTypes = { */ className: PropTypes.string, /** - * Boolean to change button type to Danger when true + * When true, ButtonSecondary color becomes Danger. */ danger: PropTypes.bool, /** diff --git a/ui/components/component-library/button-secondary/button-secondary.stories.js b/ui/components/component-library/button-secondary/button-secondary.stories.js index 399cb412e..6ae2ed76d 100644 --- a/ui/components/component-library/button-secondary/button-secondary.stories.js +++ b/ui/components/component-library/button-secondary/button-secondary.stories.js @@ -122,7 +122,7 @@ export const Size = (args) => ( ); -export const Type = (args) => ( +export const Danger = (args) => ( Normal {/* Test Anchor tag to match exactly as button */} diff --git a/ui/components/component-library/button-secondary/button-secondary.test.js b/ui/components/component-library/button-secondary/button-secondary.test.js index 61f910ab9..5d7eb1a4d 100644 --- a/ui/components/component-library/button-secondary/button-secondary.test.js +++ b/ui/components/component-library/button-secondary/button-secondary.test.js @@ -68,7 +68,7 @@ describe('ButtonSecondary', () => { ); }); - it('should render with different types', () => { + it('should render as danger', () => { const { getByTestId } = render( <> diff --git a/ui/components/component-library/button/README.mdx b/ui/components/component-library/button/README.mdx new file mode 100644 index 000000000..9c6c18f0d --- /dev/null +++ b/ui/components/component-library/button/README.mdx @@ -0,0 +1,178 @@ +import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; +import { Button } from './button'; + +# Button + +The `Button` is used for user actions it unifies `ButtonPrimary`, `ButtonSecondary` and `ButtonLink` + + + + + +## Props + +The `Button` accepts all props below as well as all [ButtonPrimary](/ui-components-component-library-button-primary-button-primary-stories-js--default-story), [ButtonSecondary](/ui-components-component-library-button-secondary-button-secondary-stories-js--default-story), [ButtonLink](/ui-components-component-library-button-link-button-link-stories-js--default-story), and [Box](/docs/ui-components-ui-box-box-stories-js--default-story#props) component props + + + +### Type + +Use the `type` prop and the `BUTTON_TYPES` object from `./button.constants.js` to change the `Button` type. + +Possible types include: + +- `BUTTON_TYPES.PRIMARY` +- `BUTTON_TYPES.SECONDARY` +- `BUTTON_TYPES.LINK` + + + + + +```jsx +import { Button, BUTTON_TYPES } from '../ui/component-library/button'; + + + + +``` + +### Size + +Use the `size` prop and the `SIZES` object from `./ui/helpers/constants/design-system.js` to change the size of `Button`. Defaults to `SIZES.MD` + +Optional: `BUTTON_SIZES` from `./button` object can be used instead of `SIZES`. + +Possible sizes include: + +- `SIZES.AUTO` inherits the font-size of the parent element. +- `SIZES.SM` 32px +- `SIZES.MD` 40px +- `SIZES.LG` 48px + + + + + +```jsx +import { SIZES } from '../../../helpers/constants/design-system'; +import { Button } from '../ui/component-library/button/button/button'; + + + +``` + +### Href + +When an `href` is passed the tag element will switch to an `anchor`(`a`) tag. + + + + + +```jsx +import { Button } from '../ui/component-library/button/button/button'; + +; +``` + +### Block + +Use boolean `block` prop to quickly enable a full width block button + + + + + +```jsx +import { DISPLAY } from '../../../helpers/constants/design-system'; +import { Button } from '../ui/component-library'; + + + +``` + +### As + +Use the `as` box prop to change the element of `Button`. Defaults to `button`. + +When an `href` prop is passed it will change the element to an anchor(`a`) tag. + +Button `as` options: + +- `button` +- `a` + + + + + +```jsx +import { Button } from '../ui/component-library'; + + + + +``` + +### Disabled + +Use the boolean `disabled` prop to disable button + + + + + +```jsx +import { Button } from '../ui/component-library'; + +; +``` + +### Loading + +Use the boolean `loading` prop to set loading spinner + + + + + +```jsx +import { Button } from '../ui/component-library'; + +; +``` + +### Icon + +Use the `icon` prop and the `ICON_NAMES` object from `./ui/components/component-library/icon` to select icon. + + + + + +```jsx +import { Button } from '../ui/component-library'; +import { ICON_NAMES } from '../icon'; + +; +``` diff --git a/ui/components/component-library/button/__snapshots__/button.test.js.snap b/ui/components/component-library/button/__snapshots__/button.test.js.snap new file mode 100644 index 000000000..a2da87852 --- /dev/null +++ b/ui/components/component-library/button/__snapshots__/button.test.js.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Button should render button element correctly 1`] = ` +
+ +
+`; + +exports[`Button should render with different button types 1`] = ` +
+ + + +
+`; diff --git a/ui/components/component-library/button/button.constants.js b/ui/components/component-library/button/button.constants.js new file mode 100644 index 000000000..803673f31 --- /dev/null +++ b/ui/components/component-library/button/button.constants.js @@ -0,0 +1,14 @@ +import { SIZES } from '../../../helpers/constants/design-system'; + +export const BUTTON_SIZES = { + SM: SIZES.SM, + MD: SIZES.MD, + LG: SIZES.LG, + AUTO: SIZES.AUTO, +}; + +export const BUTTON_TYPES = { + PRIMARY: 'primary', + SECONDARY: 'secondary', + LINK: 'link', +}; diff --git a/ui/components/component-library/button/button.js b/ui/components/component-library/button/button.js new file mode 100644 index 000000000..eaaa5486b --- /dev/null +++ b/ui/components/component-library/button/button.js @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { ButtonPrimary } from '../button-primary'; +import { ButtonSecondary } from '../button-secondary'; +import { ButtonLink } from '../button-link'; + +import { BUTTON_TYPES } from './button.constants'; + +export const Button = ({ type, ...props }) => { + switch (type) { + case BUTTON_TYPES.PRIMARY: + return ; + case BUTTON_TYPES.SECONDARY: + return ; + case BUTTON_TYPES.LINK: + return ; + default: + return ; + } +}; + +Button.propTypes = { + /** + * Select the type of Button. + * Possible values could be 'BUTTON_TYPES.PRIMARY', 'BUTTON_TYPES.SECONDARY', 'BUTTON_TYPES.LINK' + * Button will default to `BUTTON_TYPES.PRIMARY` + */ + type: PropTypes.oneOf(Object.values(BUTTON_TYPES)), + /** + * Button accepts all the props from ButtonPrimary (same props as ButtonSecondary & ButtonLink) + */ + ...ButtonPrimary.propTypes, +}; diff --git a/ui/components/component-library/button/button.stories.js b/ui/components/component-library/button/button.stories.js new file mode 100644 index 000000000..cce693001 --- /dev/null +++ b/ui/components/component-library/button/button.stories.js @@ -0,0 +1,212 @@ +import React from 'react'; +import { + ALIGN_ITEMS, + DISPLAY, + FLEX_DIRECTION, + SIZES, + TEXT, +} from '../../../helpers/constants/design-system'; +import { ICON_NAMES } from '../icon'; +import { BUTTON_LINK_SIZES } from '../button-link/button-link.constants'; +import Box from '../../ui/box/box'; +import { Text } from '../text'; +import README from './README.mdx'; +import { Button, BUTTON_TYPES } from '.'; + +const marginSizeControlOptions = [ + undefined, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 'auto', +]; + +export default { + title: 'Components/ComponentLibrary/Button', + id: __filename, + component: Button, + parameters: { + docs: { + page: README, + }, + controls: { sort: 'alpha' }, + }, + argTypes: { + as: { + control: 'select', + options: ['button', 'a'], + }, + block: { + control: 'boolean', + }, + children: { + control: 'text', + }, + className: { + control: 'text', + }, + danger: { + control: 'boolean', + }, + disabled: { + control: 'boolean', + }, + href: { + control: 'text', + }, + icon: { + control: 'select', + options: Object.values(ICON_NAMES), + }, + iconPositionRight: { + control: 'boolean', + }, + iconProps: { + control: 'object', + }, + loading: { + control: 'boolean', + }, + size: { + control: 'select', + options: Object.values(BUTTON_LINK_SIZES), + }, + type: { + options: Object.values(BUTTON_TYPES), + control: 'select', + }, + marginTop: { + options: marginSizeControlOptions, + control: 'select', + table: { category: 'box props' }, + }, + marginRight: { + options: marginSizeControlOptions, + control: 'select', + table: { category: 'box props' }, + }, + marginBottom: { + options: marginSizeControlOptions, + control: 'select', + table: { category: 'box props' }, + }, + marginLeft: { + options: marginSizeControlOptions, + control: 'select', + table: { category: 'box props' }, + }, + }, + args: { + children: 'Button', + }, +}; + +export const DefaultStory = (args) => + + +
+); + +export const Size = (args) => ( + <> + + + + + + + {' '} + inherits the font-size of the parent element. Auto size only used for + ButtonLink. + + +); + +export const Danger = (args) => ( + + + {/* Test Anchor tag to match exactly as button */} + + +); + +export const Href = (args) => ; + +Href.args = { + href: '/metamask', +}; + +export const Block = (args) => ( + <> + + + +); + +export const As = (args) => ( + + + + +); + +export const Disabled = (args) => ; + +Disabled.args = { + disabled: true, +}; + +export const Loading = (args) => ; + +Loading.args = { + loading: true, +}; + +export const Icon = (args) => ( + +); diff --git a/ui/components/component-library/button/button.test.js b/ui/components/component-library/button/button.test.js new file mode 100644 index 000000000..8f4f288e8 --- /dev/null +++ b/ui/components/component-library/button/button.test.js @@ -0,0 +1,157 @@ +/* eslint-disable jest/require-top-level-describe */ +import { render } from '@testing-library/react'; +import React from 'react'; +import { BUTTON_SIZES, BUTTON_TYPES } from './button.constants'; +import { Button } from './button'; + +describe('Button', () => { + it('should render button element correctly', () => { + const { getByTestId, getByText, container } = render( + , + ); + expect(getByText('Button')).toBeDefined(); + expect(container.querySelector('button')).toBeDefined(); + expect(getByTestId('button')).toHaveClass('mm-button'); + expect(container).toMatchSnapshot(); + }); + + it('should render anchor element correctly', () => { + const { getByTestId, container } = render( + , + ); + expect(getByTestId('button')).toHaveClass('mm-button'); + const anchor = container.getElementsByTagName('a').length; + expect(anchor).toBe(1); + }); + + it('should render anchor element correctly by href only being passed', () => { + const { getByTestId, container } = render( + , + ); + expect(getByTestId('button')).toHaveClass('mm-button'); + const anchor = container.getElementsByTagName('a').length; + expect(anchor).toBe(1); + }); + + it('should render button as block', () => { + const { getByTestId } = render( + + + , + ); + expect(getByTestId(BUTTON_TYPES.PRIMARY)).toHaveClass( + `mm-button-${BUTTON_TYPES.PRIMARY}`, + ); + expect(getByTestId(BUTTON_TYPES.SECONDARY)).toHaveClass( + `mm-button-${BUTTON_TYPES.SECONDARY}`, + ); + expect(getByTestId(BUTTON_TYPES.LINK)).toHaveClass( + `mm-button-${BUTTON_TYPES.LINK}`, + ); + expect(container).toMatchSnapshot(); + }); + + it('should render with different size classes', () => { + const { getByTestId } = render( + <> + + + + + , + ); + expect(getByTestId(BUTTON_SIZES.AUTO)).toHaveClass( + `mm-button--size-${BUTTON_SIZES.AUTO}`, + ); + expect(getByTestId(BUTTON_SIZES.SM)).toHaveClass( + `mm-button--size-${BUTTON_SIZES.SM}`, + ); + expect(getByTestId(BUTTON_SIZES.MD)).toHaveClass( + `mm-button--size-${BUTTON_SIZES.MD}`, + ); + expect(getByTestId(BUTTON_SIZES.LG)).toHaveClass( + `mm-button--size-${BUTTON_SIZES.LG}`, + ); + }); + + it('should render with added classname', () => { + const { getByTestId } = render( + , + ); + expect(getByTestId('classname')).toHaveClass('mm-button--test'); + }); + + it('should render with different button states', () => { + const { getByTestId } = render( + <> + + + , + ); + expect(getByTestId('loading')).toHaveClass(`mm-button--loading`); + expect(getByTestId('disabled')).toHaveClass(`mm-button--disabled`); + }); + it('should render with icon', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId('base-button-icon')).toBeDefined(); + }); +}); + +it('should render as danger', () => { + const { getByTestId } = render( + <> + + , + ); + + expect(getByTestId('danger')).toHaveClass('mm-button-primary--type-danger'); +}); diff --git a/ui/components/component-library/button/index.js b/ui/components/component-library/button/index.js new file mode 100644 index 000000000..1a9c9819b --- /dev/null +++ b/ui/components/component-library/button/index.js @@ -0,0 +1,2 @@ +export { Button } from './button'; +export { BUTTON_TYPES } from './button.constants';