1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

15087: Add Button Base (#15998)

* 15087: Add Button Base
This commit is contained in:
Garrett Bear 2022-10-04 09:55:51 -07:00 committed by GitHub
parent c8067e9351
commit 46d970e362
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 618 additions and 1 deletions

View File

@ -1 +1,9 @@
process.env.METAMASK_ENV = 'test'; 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',
};

View File

@ -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.
<Canvas>
<Story id="ui-components-component-library-button-base-button-base-stories-js--default-story" />
</Canvas>
## 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
<ArgsTable of={ButtonBase} />
### 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
<Canvas>
<Story id="ui-components-component-library-button-base-button-base-stories-js--size" />
</Canvas>
```jsx
import { SIZES } from '../../../helpers/constants/design-system';
import { ButtonBase } from '../ui/component-library';
<ButtonBase size={SIZES.AUTO} />
<ButtonBase size={SIZES.SM} />
<ButtonBase size={SIZES.MD} />
<ButtonBase size={SIZES.LG} />
```
### Block
Use boolean `block` prop to quickly enable a full width block button
<Canvas>
<Story id="ui-components-component-library-button-base-button-base-stories-js--block" />
</Canvas>
```jsx
import { DISPLAY } from '../../../helpers/constants/design-system';
import { ButtonBase } from '../ui/component-library';
<ButtonBase>Default Button</ButtonBase>
<ButtonBase block>Block Button</ButtonBase>
```
### As
Use the `as` box prop to change the element of `ButtonBase`. Defaults to `button`.
Button `as` options:
- `button`
- `a`
<Canvas>
<Story id="ui-components-component-library-button-base-button-base-stories-js--as" />
</Canvas>
```jsx
import { ButtonBase } from '../ui/component-library';
<ButtonBase as="button">Button Element</ButtonBase>
<ButtonBase as="a" href="#">
Anchor Element
</ButtonBase>
```
### Disabled
Use the boolean `disabled` prop to disable button
<Canvas>
<Story id="ui-components-component-library-button-base-button-base-stories-js--disabled" />
</Canvas>
```jsx
import { ButtonBase } from '../ui/component-library';
<ButtonBase disabled>Disabled Button</ButtonBase>;
```
### Loading
Use the boolean `loading` prop to set loading spinner
<Canvas>
<Story id="ui-components-component-library-button-base-button-base-stories-js--loading" />
</Canvas>
```jsx
import { ButtonBase } from '../ui/component-library';
<ButtonBase loading>Loading Button</ButtonBase>;
```
### Icon
Use the `icon` prop and the `ICON_NAMES` object from `./ui/components/component-library/icon` to select icon.
<Canvas>
<Story id="ui-components-component-library-button-base-button-base-stories-js--icon" />
</Canvas>
```jsx
import { ButtonBase } from '../ui/component-library';
import { ICON_NAMES } from '../icon';
<ButtonBase icon={ICON_NAMES.ADD_SQUARE_FILLED}>Button</ButtonBase>;
```

View File

@ -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 (
<Box
as={as}
paddingLeft={size === BUTTON_SIZES.AUTO ? 0 : 4}
paddingRight={size === BUTTON_SIZES.AUTO ? 0 : 4}
className={classnames(
'mm-button',
`mm-button--size-${size}`,
{
'mm-button--loading': Boolean(loading),
'mm-button--disabled': Boolean(disabled),
'mm-button--block': Boolean(block),
},
className,
)}
disabled={disabled}
display={DISPLAY.INLINE_FLEX}
justifyContent={JUSTIFY_CONTENT.CENTER}
alignItems={ALIGN_ITEMS.CENTER}
{...props}
>
<Text
as="span"
className="mm-button__content"
alignItems={ALIGN_ITEMS.CENTER}
justifyContent={JUSTIFY_CONTENT.CENTER}
flexDirection={
iconPositionRight ? FLEX_DIRECTION.ROW_REVERSE : FLEX_DIRECTION.ROW
}
gap={2}
variant={size === BUTTON_SIZES.AUTO ? TEXT.INHERIT : TEXT.BODY_MD}
color={TEXT_COLORS.INHERIT}
>
{icon && (
<Icon
name={icon}
size={size === BUTTON_SIZES.AUTO ? SIZES.AUTO : SIZES.SM}
{...iconProps}
/>
)}
{children}
</Text>
{loading && (
<Icon
className="mm-button__icon-loading"
name={ICON_NAMES.LOADING_FILLED}
size={size === BUTTON_SIZES.AUTO ? SIZES.AUTO : SIZES.MD}
/>
)}
</Box>
);
};
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,
};

View File

@ -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); }
}

View File

@ -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) => <ButtonBase {...args} />;
DefaultStory.storyName = 'Default';
export const Size = (args) => (
<>
<Box
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.BASELINE}
gap={1}
marginBottom={2}
>
<ButtonBase {...args} size={SIZES.SM}>
Button SM
</ButtonBase>
<ButtonBase {...args} size={SIZES.MD}>
Button MD
</ButtonBase>
<ButtonBase {...args} size={SIZES.LG}>
Button LG
</ButtonBase>
</Box>
<Text variant={TEXT.BODY_SM}>
<ButtonBase {...args} size={SIZES.AUTO}>
Button Auto
</ButtonBase>{' '}
inherits the font-size of the parent element.
</Text>
</>
);
export const Block = (args) => (
<>
<ButtonBase {...args} marginBottom={2}>
Default Button
</ButtonBase>
<ButtonBase {...args} block marginBottom={2}>
Block Button
</ButtonBase>
</>
);
export const As = (args) => (
<Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW} gap={2}>
<ButtonBase {...args}>Button Element</ButtonBase>
<ButtonBase as="a" href="#" {...args}>
Anchor Element
</ButtonBase>
</Box>
);
export const Disabled = (args) => (
<ButtonBase {...args}>Disabled Button</ButtonBase>
);
Disabled.args = {
disabled: true,
};
export const Loading = (args) => (
<ButtonBase {...args}>Loading Button</ButtonBase>
);
Loading.args = {
loading: true,
};
export const Icon = (args) => (
<ButtonBase {...args} icon={ICON_NAMES.ADD_SQUARE_FILLED}>
Button
</ButtonBase>
);

View File

@ -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(
<ButtonBase data-testid="button-base">Button base</ButtonBase>,
);
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(
<ButtonBase as="a" data-testid="button-base" />,
);
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(<ButtonBase block data-testid="block" />);
expect(getByTestId('block')).toHaveClass(`mm-button--block`);
});
it('should render with different size classes', () => {
const { getByTestId } = render(
<>
<ButtonBase size={BUTTON_SIZES.AUTO} data-testid={BUTTON_SIZES.AUTO} />
<ButtonBase size={BUTTON_SIZES.SM} data-testid={BUTTON_SIZES.SM} />
<ButtonBase size={BUTTON_SIZES.MD} data-testid={BUTTON_SIZES.MD} />
<ButtonBase size={BUTTON_SIZES.LG} data-testid={BUTTON_SIZES.LG} />
</>,
);
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(
<>
<ButtonBase loading data-testid="loading" />
<ButtonBase disabled data-testid="disabled" />
</>,
);
expect(getByTestId('loading')).toHaveClass(`mm-button--loading`);
expect(getByTestId('disabled')).toHaveClass(`mm-button--disabled`);
});
it('should render with icon', () => {
const { getByTestId } = render(
<ButtonBase
data-testid="icon"
icon="add-square-filled"
iconProps={{ 'data-testid': 'base-button-icon' }}
/>,
);
expect(getByTestId('base-button-icon')).toBeDefined();
});
});

View File

@ -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,
};

View File

@ -0,0 +1,2 @@
export { ButtonBase } from './button-base';
export { BUTTON_SIZES } from './button.constants';

View File

@ -2,5 +2,6 @@
@import 'avatar-network/avatar-network'; @import 'avatar-network/avatar-network';
@import 'avatar-token/avatar-token'; @import 'avatar-token/avatar-token';
@import 'base-avatar/base-avatar'; @import 'base-avatar/base-avatar';
@import 'button-base/button-base';
@import 'icon/icon'; @import 'icon/icon';
@import 'text/text'; @import 'text/text';

View File

@ -35,6 +35,7 @@ import { TEXT } from '../../../helpers/constants/design-system';
<Text variant={TEXT.BODY_MD}>body-md</Text> <Text variant={TEXT.BODY_MD}>body-md</Text>
<Text variant={TEXT.BODY_SM}>body-sm</Text> <Text variant={TEXT.BODY_SM}>body-sm</Text>
<Text variant={TEXT.BODY_XS}>body-xs</Text> <Text variant={TEXT.BODY_XS}>body-xs</Text>
<Text variant={TEXT.INHERIT}>inherit</Text>
``` ```
### Color ### Color

View File

@ -82,6 +82,14 @@ $text-variants: (
} }
} }
&--inherit {
font-family: inherit;
font-weight: inherit;
font-size: inherit;
line-height: inherit;
letter-spacing: inherit;
}
&--ellipsis { &--ellipsis {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;

View File

@ -161,6 +161,7 @@ export const TEXT = {
BODY_MD: 'body-md', BODY_MD: 'body-md',
BODY_SM: 'body-sm', BODY_SM: 'body-sm',
BODY_XS: 'body-xs', BODY_XS: 'body-xs',
INHERIT: 'inherit',
}; };
const NONE = 'none'; const NONE = 'none';
@ -172,7 +173,7 @@ export const SIZES = {
MD: 'md', MD: 'md',
LG: 'lg', LG: 'lg',
XL: 'xl', 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, NONE,
}; };