From 46d970e3627ebb64bd9b684eda918dac476e9c34 Mon Sep 17 00:00:00 2001 From: Garrett Bear Date: Tue, 4 Oct 2022 09:55:51 -0700 Subject: [PATCH] 15087: Add Button Base (#15998) * 15087: Add Button Base --- test/env.js | 8 + .../component-library/button-base/README.mdx | 128 +++++++++++++ .../button-base/button-base.js | 138 ++++++++++++++ .../button-base/button-base.scss | 79 ++++++++ .../button-base/button-base.stories.js | 168 ++++++++++++++++++ .../button-base/button-base.test.js | 75 ++++++++ .../button-base/button.constants.js | 8 + .../component-library/button-base/index.js | 2 + .../component-library-components.scss | 1 + .../component-library/text/README.mdx | 1 + .../component-library/text/text.scss | 8 + ui/helpers/constants/design-system.js | 3 +- 12 files changed, 618 insertions(+), 1 deletion(-) create mode 100644 ui/components/component-library/button-base/README.mdx create mode 100644 ui/components/component-library/button-base/button-base.js create mode 100644 ui/components/component-library/button-base/button-base.scss create mode 100644 ui/components/component-library/button-base/button-base.stories.js create mode 100644 ui/components/component-library/button-base/button-base.test.js create mode 100644 ui/components/component-library/button-base/button.constants.js create mode 100644 ui/components/component-library/button-base/index.js diff --git a/test/env.js b/test/env.js index 38e4a6fed..0fb3e9378 100644 --- a/test/env.js +++ b/test/env.js @@ -1 +1,9 @@ process.env.METAMASK_ENV = 'test'; + +/** + * Used for testing components that use the Icon component + * 'ui/components/component-library/icon/icon.js' + */ +process.env.ICON_NAMES = { + LOADING_FILLED: 'loading-filled', +}; diff --git a/ui/components/component-library/button-base/README.mdx b/ui/components/component-library/button-base/README.mdx new file mode 100644 index 000000000..f7a4ff0f7 --- /dev/null +++ b/ui/components/component-library/button-base/README.mdx @@ -0,0 +1,128 @@ +import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; +import { ButtonBase } from './button-base'; + +### This is a base component. It should not be used in your feature code directly but as a "base" for other UI components + +# ButtonBase + +The `ButtonBase` is the base component for buttons. + + + + + +## Props + +The `ButtonBase` accepts all props below as well as all [Box](/docs/ui-components-ui-box-box-stories-js--default-story#props) component props + + + +### Size + +Use the `size` prop and the `SIZES` object from `./ui/helpers/constants/design-system.js` +to change the size of `ButtonBase`. Defaults to `SIZES.MD` + +Optional: `BUTTON_SIZES` from `./button-base` 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 { ButtonBase } from '../ui/component-library'; + + + + + +``` + +### Block + +Use boolean `block` prop to quickly enable a full width block button + + + + + +```jsx +import { DISPLAY } from '../../../helpers/constants/design-system'; +import { ButtonBase } from '../ui/component-library'; + +Default Button +Block Button +``` + +### As + +Use the `as` box prop to change the element of `ButtonBase`. Defaults to `button`. + +Button `as` options: + +- `button` +- `a` + + + + + +```jsx +import { ButtonBase } from '../ui/component-library'; + + +Button Element + + Anchor Element + +``` + +### Disabled + +Use the boolean `disabled` prop to disable button + + + + + +```jsx +import { ButtonBase } from '../ui/component-library'; + +Disabled Button; +``` + +### Loading + +Use the boolean `loading` prop to set loading spinner + + + + + +```jsx +import { ButtonBase } from '../ui/component-library'; + +Loading Button; +``` + +### Icon + +Use the `icon` prop and the `ICON_NAMES` object from `./ui/components/component-library/icon` to select icon. + + + + + +```jsx +import { ButtonBase } from '../ui/component-library'; +import { ICON_NAMES } from '../icon'; + +Button; +``` diff --git a/ui/components/component-library/button-base/button-base.js b/ui/components/component-library/button-base/button-base.js new file mode 100644 index 000000000..eeb19c1a2 --- /dev/null +++ b/ui/components/component-library/button-base/button-base.js @@ -0,0 +1,138 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +import Box from '../../ui/box'; +import { Icon, ICON_NAMES } from '../icon'; +import { Text } from '../text'; + +import { + ALIGN_ITEMS, + DISPLAY, + JUSTIFY_CONTENT, + TEXT_COLORS, + TEXT, + SIZES, + FLEX_DIRECTION, +} from '../../../helpers/constants/design-system'; +import { BUTTON_SIZES } from './button.constants'; + +export const ButtonBase = ({ + as = 'button', + block, + children, + className, + size = BUTTON_SIZES.MD, + icon, + iconPositionRight, + loading, + disabled, + iconProps, + ...props +}) => { + return ( + + + {icon && ( + + )} + {children} + + {loading && ( + + )} + + ); +}; + +ButtonBase.propTypes = { + /** + * The polymorphic `as` prop allows you to change the root HTML element of the Button component between `button` and `a` tag + */ + as: PropTypes.string, + /** + * Boolean prop to quickly activate box prop display block + */ + block: PropTypes.bool, + /** + * The children to be rendered inside the ButtonBase + */ + children: PropTypes.node, + /** + * An additional className to apply to the ButtonBase. + */ + className: PropTypes.string, + /** + * Boolean to disable button + */ + disabled: PropTypes.bool, + /** + * Add icon to left side of button text passing icon name + * The name of the icon to display. Should be one of ICON_NAMES + */ + icon: PropTypes.string, // Can't set PropTypes.oneOf(ICON_NAMES) because ICON_NAMES is an environment variable + /** + * Boolean that when true will position the icon on right of children + * Icon default position left + */ + iconPositionRight: PropTypes.bool, + /** + * iconProps accepts all the props from Icon + */ + iconProps: Icon.propTypes, + /** + * Boolean to show loading spinner in button + */ + loading: PropTypes.bool, + /** + * The size of the ButtonBase. + * Possible values could be 'SIZES.AUTO', 'SIZES.SM', 'SIZES.MD', 'SIZES.LG', + */ + size: PropTypes.oneOf(Object.values(BUTTON_SIZES)), + /** + * Addition style properties to apply to the button. + */ + style: PropTypes.object, + /** + * ButtonBase accepts all the props from Box + */ + ...Box.propTypes, +}; diff --git a/ui/components/component-library/button-base/button-base.scss b/ui/components/component-library/button-base/button-base.scss new file mode 100644 index 000000000..9e4bd0282 --- /dev/null +++ b/ui/components/component-library/button-base/button-base.scss @@ -0,0 +1,79 @@ +.mm-button { + position: relative; + height: 40px; + padding: 0; + border-radius: 999px; + cursor: pointer; + color: var(--color-text-default); + background-color: var(--brand-colors-grey-grey100); + vertical-align: middle; + user-select: none; + + &:active, + &:hover { + color: var(--color-text-default); + } + + &--block { + display: block; + width: 100%; + } + + &__content { + height: 100%; + } + + + &--size-sm { + height: 32px; + } + + &--size-md { + height: 40px; + } + + &--size-lg { + height: 48px; + } + + &--size-auto { + height: auto; + background-color: transparent; + border-radius: 0; + vertical-align: top; + font-family: inherit; + font-weight: inherit; + font-size: inherit; + line-height: inherit; + letter-spacing: inherit; + } + + &--loading { + cursor: not-allowed; + } + + &--loading &__content { + color: transparent; + } + + &--disabled, + &:disabled { + opacity: 0.3; + cursor: not-allowed; + } + + &__icon-loading { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + animation: spinner 1.2s linear infinite; + } +} + + + +@keyframes spinner { + to { transform: translate(-50%, -50%) rotate(360deg); } +} + diff --git a/ui/components/component-library/button-base/button-base.stories.js b/ui/components/component-library/button-base/button-base.stories.js new file mode 100644 index 000000000..320223fa8 --- /dev/null +++ b/ui/components/component-library/button-base/button-base.stories.js @@ -0,0 +1,168 @@ +import React from 'react'; +import { + ALIGN_ITEMS, + DISPLAY, + FLEX_DIRECTION, + SIZES, + TEXT, +} from '../../../helpers/constants/design-system'; +import Box from '../../ui/box/box'; +import { ICON_NAMES } from '../icon'; +import { Text } from '../text'; +import { BUTTON_SIZES } from './button.constants'; +import { ButtonBase } from './button-base'; +import README from './README.mdx'; + +const marginSizeControlOptions = [ + undefined, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 'auto', +]; + +export default { + title: 'Components/ComponentLibrary/ButtonBase', + id: __filename, + component: ButtonBase, + parameters: { + docs: { + page: README, + }, + }, + argTypes: { + as: { + control: 'select', + options: ['button', 'a'], + }, + block: { + control: 'boolean', + }, + children: { + control: 'text', + }, + className: { + control: 'text', + }, + disabled: { + control: 'boolean', + }, + icon: { + control: 'select', + options: Object.values(ICON_NAMES), + }, + loading: { + control: 'boolean', + }, + size: { + control: 'select', + options: Object.values(BUTTON_SIZES), + }, + 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 Base', + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; + +export const Size = (args) => ( + <> + + + Button SM + + + Button MD + + + Button LG + + + + + Button Auto + {' '} + inherits the font-size of the parent element. + + +); + +export const Block = (args) => ( + <> + + Default Button + + + Block Button + + +); + +export const As = (args) => ( + + Button Element + + Anchor Element + + +); + +export const Disabled = (args) => ( + Disabled Button +); + +Disabled.args = { + disabled: true, +}; + +export const Loading = (args) => ( + Loading Button +); + +Loading.args = { + loading: true, +}; + +export const Icon = (args) => ( + + Button + +); diff --git a/ui/components/component-library/button-base/button-base.test.js b/ui/components/component-library/button-base/button-base.test.js new file mode 100644 index 000000000..5c0360433 --- /dev/null +++ b/ui/components/component-library/button-base/button-base.test.js @@ -0,0 +1,75 @@ +/* eslint-disable jest/require-top-level-describe */ +import { render } from '@testing-library/react'; +import React from 'react'; +import { BUTTON_SIZES } from './button.constants'; +import { ButtonBase } from './button-base'; + +describe('ButtonBase', () => { + it('should render button element correctly', () => { + const { getByTestId, getByText, container } = render( + Button base, + ); + expect(getByText('Button base')).toBeDefined(); + expect(container.querySelector('button')).toBeDefined(); + expect(getByTestId('button-base')).toHaveClass('mm-button'); + }); + + it('should render anchor element correctly', () => { + const { getByTestId, container } = render( + , + ); + expect(getByTestId('button-base')).toBeDefined(); + expect(container.querySelector('a')).toBeDefined(); + expect(getByTestId('button-base')).toHaveClass('mm-button'); + }); + + it('should render button as block', () => { + const { getByTestId } = render(); + expect(getByTestId('block')).toHaveClass(`mm-button--block`); + }); + + 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 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(); + }); +}); diff --git a/ui/components/component-library/button-base/button.constants.js b/ui/components/component-library/button-base/button.constants.js new file mode 100644 index 000000000..bcf67e6f1 --- /dev/null +++ b/ui/components/component-library/button-base/button.constants.js @@ -0,0 +1,8 @@ +import { SIZES } from '../../../helpers/constants/design-system'; + +export const BUTTON_SIZES = { + SM: SIZES.SM, + MD: SIZES.MD, + LG: SIZES.LG, + AUTO: SIZES.AUTO, +}; diff --git a/ui/components/component-library/button-base/index.js b/ui/components/component-library/button-base/index.js new file mode 100644 index 000000000..789a9db9d --- /dev/null +++ b/ui/components/component-library/button-base/index.js @@ -0,0 +1,2 @@ +export { ButtonBase } from './button-base'; +export { BUTTON_SIZES } from './button.constants'; diff --git a/ui/components/component-library/component-library-components.scss b/ui/components/component-library/component-library-components.scss index 09c0d7a28..835f1d842 100644 --- a/ui/components/component-library/component-library-components.scss +++ b/ui/components/component-library/component-library-components.scss @@ -2,5 +2,6 @@ @import 'avatar-network/avatar-network'; @import 'avatar-token/avatar-token'; @import 'base-avatar/base-avatar'; +@import 'button-base/button-base'; @import 'icon/icon'; @import 'text/text'; diff --git a/ui/components/component-library/text/README.mdx b/ui/components/component-library/text/README.mdx index 197cebb2b..f98b529e8 100644 --- a/ui/components/component-library/text/README.mdx +++ b/ui/components/component-library/text/README.mdx @@ -35,6 +35,7 @@ import { TEXT } from '../../../helpers/constants/design-system'; body-md body-sm body-xs +inherit ``` ### Color diff --git a/ui/components/component-library/text/text.scss b/ui/components/component-library/text/text.scss index 5d1ad4e01..972279570 100644 --- a/ui/components/component-library/text/text.scss +++ b/ui/components/component-library/text/text.scss @@ -82,6 +82,14 @@ $text-variants: ( } } + &--inherit { + font-family: inherit; + font-weight: inherit; + font-size: inherit; + line-height: inherit; + letter-spacing: inherit; + } + &--ellipsis { text-overflow: ellipsis; white-space: nowrap; diff --git a/ui/helpers/constants/design-system.js b/ui/helpers/constants/design-system.js index 4749a45af..58c142e11 100644 --- a/ui/helpers/constants/design-system.js +++ b/ui/helpers/constants/design-system.js @@ -161,6 +161,7 @@ export const TEXT = { BODY_MD: 'body-md', BODY_SM: 'body-sm', BODY_XS: 'body-xs', + INHERIT: 'inherit', }; const NONE = 'none'; @@ -172,7 +173,7 @@ export const SIZES = { MD: 'md', LG: 'lg', XL: 'xl', - AUTO: 'auto', // Used for Text and Icon components to inherit the parent elements font-size + AUTO: 'auto', // Used for Text, Icon, and Button components to inherit the parent elements font-size NONE, };