Feat/15088/add button icon (#16277)

* 15088: add button icon

* button icon story updates

* add primary type

* update button icon docs

* add test

* button icon updates

* button icon and test updates

* button icon border radius and test update

* remove padding prop

* button icon updates

* Update ui/components/component-library/button-icon/button-icon.stories.js

Co-authored-by: George Marshall <george.marshall@consensys.net>

* Update ui/components/component-library/button-icon/button-icon.stories.js

Co-authored-by: George Marshall <george.marshall@consensys.net>

* Update ui/components/component-library/button-icon/button-icon.stories.js

Co-authored-by: George Marshall <george.marshall@consensys.net>

* add aria label for storybook demo

Co-authored-by: George Marshall <george.marshall@consensys.net>
feature/default_network_editable
Garrett Bear 2 years ago committed by GitHub
parent a28d727caf
commit 03af17747b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 144
      ui/components/component-library/button-icon/README.mdx
  2. 16
      ui/components/component-library/button-icon/__snapshots__/button-icon.test.js.snap
  3. 6
      ui/components/component-library/button-icon/button-icon.constants.js
  4. 101
      ui/components/component-library/button-icon/button-icon.js
  5. 32
      ui/components/component-library/button-icon/button-icon.scss
  6. 186
      ui/components/component-library/button-icon/button-icon.stories.js
  7. 146
      ui/components/component-library/button-icon/button-icon.test.js
  8. 2
      ui/components/component-library/button-icon/index.js
  9. 1
      ui/components/component-library/component-library-components.scss

@ -0,0 +1,144 @@
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import { ButtonIcon } from './button-icon';
# ButtonIcon
The `ButtonIcon` is used for icons associated with a user action.
<Canvas>
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--default-story" />
</Canvas>
## Props
The `ButtonIcon` accepts all props below as well as all [Box](/docs/ui-components-ui-box-box-stories-js--default-story#props) component props
<ArgsTable of={ButtonIcon} />
### Icon<span style={{ color: 'red' }}>\*</span>
Use the required `icon` prop with `ICON_NAMES` object from `./ui/components/component-library/icon` to select icon.
<Canvas>
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--icon" />
</Canvas>
```jsx
import { ButtonIcon } from '../ui/component-library';
import { ICON_NAMES } from '../icon';
<ButtonIcon icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close" />;
```
### Size
Use the `size` prop and the `SIZES` object from `./ui/helpers/constants/design-system.js`
to change the size of `ButtonIcon`. Defaults to `SIZES.SM`
Optional: `BUTTON_ICON_SIZES` from `./button-icon` object can be used instead of `SIZES`.
Possible sizes include:
- `SIZES.SM` 24px
- `SIZES.LG` 32px
<Canvas>
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--size" />
</Canvas>
```jsx
import { SIZES } from '../../../helpers/constants/design-system';
import { ButtonIcon } from '../ui/component-library';
<ButtonIcon size={SIZES.SM} icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close"/>
<ButtonIcon size={SIZES.LG} icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close"/>
```
### Aria Label
Use the `ariaLabel` prop to set the name of the ButtonIcon for proper accessibility
<Canvas>
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--aria-label" />
</Canvas>
```jsx
import { ButtonIcon } from '../ui/component-library';
<ButtonIcon as="button" icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close"/>
<ButtonIcon as="a" href="https://metamask.io/" target="_blank" icon={ICON_NAMES.EXPORT} color={COLORS.PRIMARY_DEFAULT} ariaLabel="Visit MetaMask.io"/>
```
### As
Use the `as` box prop to change the element of `ButtonIcon`. Defaults to `button`.
Button `as` options:
- `button`
- `a`
<Canvas>
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--as" />
</Canvas>
```jsx
import { ButtonIcon } from '../ui/component-library';
<ButtonIcon as="button" icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close"/>
<ButtonIcon as="a" href="https://metamask.io/" target="_blank" icon={ICON_NAMES.EXPORT} color={COLORS.PRIMARY_DEFAULT} ariaLabel="Visit MetaMask.io"/>
```
### Href
When an `href` prop is passed it will change the element to an anchor(`a`) tag.
<Canvas>
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--href" />
</Canvas>
```jsx
import { ButtonIcon } from '../ui/component-library';
<ButtonIcon
href="https://metamask.io/"
target="_blank"
icon={ICON_NAMES.EXPORT}
color={COLORS.PRIMARY_DEFAULT}
ariaLabel="Visit MetaMask.io"
/>;
```
### Color
Use the `color` prop and the `COLORS` object to change the color of the `ButtonIcon`. Defaults to `COLORS.ICON_DEFAULT`.
<Canvas>
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--color" />
</Canvas>
```jsx
import { ButtonIcon } from '../ui/component-library';
<ButtonIcon
icon={ICON_NAMES.EXPORT}
color={COLORS.PRIMARY_DEFAULT}
ariaLabel="Visit MetaMask.io"
/>;
```
### Disabled
Use the boolean `disabled` prop to disable button
<Canvas>
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--disabled" />
</Canvas>
```jsx
import { ButtonIcon } from '../ui/component-library';
<ButtonIcon icon={ICON_NAMES.CLOSE_OUTLINE} disabled ariaLabel="Close" />;
```

@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ButtonIcon should render button element correctly 1`] = `
<div>
<button
aria-label="add"
class="box mm-button-icon mm-button-icon--size-lg box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-icon-default box--background-color-transparent box--rounded-lg"
data-testid="button-icon"
>
<div
class="box icon icon--size-lg box--flex-direction-row box--color-inherit"
style="mask-image: url('./images/icons/icon-add-square-filled.svg;"
/>
</button>
</div>
`;

@ -0,0 +1,6 @@
import { SIZES } from '../../../helpers/constants/design-system';
export const BUTTON_ICON_SIZES = {
SM: SIZES.SM,
LG: SIZES.LG,
};

@ -0,0 +1,101 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import {
ALIGN_ITEMS,
BORDER_RADIUS,
COLORS,
DISPLAY,
JUSTIFY_CONTENT,
} from '../../../helpers/constants/design-system';
import Box from '../../ui/box';
import { Icon } from '../icon';
import { BUTTON_ICON_SIZES } from './button-icon.constants';
export const ButtonIcon = ({
ariaLabel,
as = 'button',
className,
color = COLORS.ICON_DEFAULT,
href,
size = BUTTON_ICON_SIZES.LG,
icon,
disabled,
iconProps,
...props
}) => {
const Tag = href ? 'a' : as;
return (
<Box
aria-label={ariaLabel}
as={Tag}
className={classnames(
'mm-button-icon',
`mm-button-icon--size-${size}`,
{
'mm-button-icon--disabled': disabled,
},
className,
)}
color={color}
disabled={disabled}
display={DISPLAY.INLINE_FLEX}
justifyContent={JUSTIFY_CONTENT.CENTER}
alignItems={ALIGN_ITEMS.CENTER}
borderRadius={BORDER_RADIUS.LG}
backgroundColor={COLORS.TRANSPARENT}
href={href}
{...props}
>
<Icon name={icon} size={size} {...iconProps} />
</Box>
);
};
ButtonIcon.propTypes = {
/**
* String that adds an accessible name for ButtonIcon
*/
ariaLabel: PropTypes.string.isRequired,
/**
* The polymorphic `as` prop allows you to change the root HTML element of the Button component between `button` and `a` tag
*/
as: PropTypes.string,
/**
* An additional className to apply to the ButtonIcon.
*/
className: PropTypes.string,
/**
* The color of the ButtonIcon component should use the COLOR object from
* ./ui/helpers/constants/design-system.js
*/
color: PropTypes.oneOf(Object.values(COLORS)),
/**
* Boolean to disable button
*/
disabled: PropTypes.bool,
/**
* When an `href` prop is passed, ButtonIcon will automatically change the root element to be an `a` (anchor) tag
*/
href: PropTypes.string,
/**
* The name of the icon to display. Should be one of ICON_NAMES
*/
icon: PropTypes.string.isRequired, // Can't set PropTypes.oneOf(ICON_NAMES) because ICON_NAMES is an environment variable
/**
* iconProps accepts all the props from Icon
*/
iconProps: PropTypes.object,
/**
* The size of the ButtonIcon.
* Possible values could be 'SIZES.SM', 'SIZES.LG',
*/
size: PropTypes.oneOf(Object.values(BUTTON_ICON_SIZES)),
/**
* ButtonIcon accepts all the props from Box
*/
...Box.propTypes,
};

@ -0,0 +1,32 @@
.mm-button-icon {
--button-icon-size: var(--size, 24px);
--button-icon-opacity-hover: 0.5; // TODO: replace with design tokens
--button-icon-opacity-disabled: 0.3; // TODO: replace with design tokens
height: var(--button-icon-size);
width: var(--button-icon-size);
padding: 0;
cursor: pointer;
// ButtonIcon default states
&:active,
&:hover {
opacity: var(--button-icon-opacity-hover);
}
&--disabled,
&:disabled {
opacity: var(--button-icon-opacity-disabled);
cursor: not-allowed;
}
// ButtonIcon Sizes
&--size-sm {
--button-icon-size: 24px;
}
&--size-lg {
--button-icon-size: 32px;
}
}

@ -0,0 +1,186 @@
import React from 'react';
import {
ALIGN_ITEMS,
COLORS,
DISPLAY,
FLEX_DIRECTION,
SIZES,
} from '../../../helpers/constants/design-system';
import Box from '../../ui/box/box';
import { ICON_NAMES } from '../icon';
import { BUTTON_ICON_SIZES } from './button-icon.constants';
import { ButtonIcon } from './button-icon';
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/ButtonIcon',
id: __filename,
component: ButtonIcon,
parameters: {
docs: {
page: README,
},
},
argTypes: {
ariaLabel: {
control: 'text',
},
as: {
control: 'select',
options: ['button', 'a'],
},
className: {
control: 'text',
},
color: {
control: 'select',
options: Object.values(COLORS),
},
disabled: {
control: 'boolean',
},
href: {
control: 'string',
},
icon: {
control: 'select',
options: Object.values(ICON_NAMES),
},
size: {
control: 'select',
options: Object.values(BUTTON_ICON_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' },
},
},
};
export const DefaultStory = (args) => <ButtonIcon {...args} />;
DefaultStory.args = {
icon: ICON_NAMES.CLOSE_OUTLINE,
ariaLabel: 'Close',
};
DefaultStory.storyName = 'Default';
export const Icon = (args) => (
<ButtonIcon {...args} icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close" />
);
export const Size = (args) => (
<Box
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.BASELINE}
gap={1}
marginBottom={2}
>
<ButtonIcon
{...args}
size={SIZES.SM}
icon={ICON_NAMES.CLOSE_OUTLINE}
ariaLabel="Close"
/>
<ButtonIcon
{...args}
size={SIZES.LG}
color={COLORS.PRIMARY}
icon={ICON_NAMES.CLOSE_OUTLINE}
ariaLabel="Close"
/>
</Box>
);
export const AriaLabel = (args) => (
<>
<ButtonIcon
as="button"
icon={ICON_NAMES.CLOSE_OUTLINE}
ariaLabel="Close"
{...args}
/>
<ButtonIcon
as="a"
href="https://metamask.io/"
target="_blank"
color={COLORS.PRIMARY_DEFAULT}
icon={ICON_NAMES.EXPORT}
ariaLabel="Visit MetaMask.io"
{...args}
/>
</>
);
export const As = (args) => (
<Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW} gap={2}>
<ButtonIcon {...args} icon={ICON_NAMES.CLOSE_OUTLINE} />
<ButtonIcon
as="a"
href="#"
{...args}
color={COLORS.PRIMARY_DEFAULT}
icon={ICON_NAMES.EXPORT}
/>
</Box>
);
export const Href = (args) => (
<ButtonIcon icon={ICON_NAMES.EXPORT} {...args} target="_blank" />
);
Href.args = {
href: 'https://metamask.io/',
color: COLORS.PRIMARY_DEFAULT,
};
export const Color = (args) => (
<ButtonIcon {...args} icon={ICON_NAMES.CLOSE_OUTLINE} />
);
Color.args = {
color: COLORS.PRIMARY_DEFAULT,
};
export const Disabled = (args) => (
<ButtonIcon {...args} icon={ICON_NAMES.CLOSE_OUTLINE} />
);
Disabled.args = {
disabled: true,
};

@ -0,0 +1,146 @@
/* eslint-disable jest/require-top-level-describe */
import { render } from '@testing-library/react';
import React from 'react';
import { COLORS } from '../../../helpers/constants/design-system';
import { BUTTON_ICON_SIZES } from './button-icon.constants';
import { ButtonIcon } from './button-icon';
describe('ButtonIcon', () => {
it('should render button element correctly', () => {
const { getByTestId, container } = render(
<ButtonIcon
data-testid="button-icon"
icon="add-square-filled"
ariaLabel="add"
/>,
);
expect(container.querySelector('button')).toBeDefined();
expect(getByTestId('button-icon')).toHaveClass('mm-button-icon');
expect(container).toMatchSnapshot();
});
it('should render anchor element correctly', () => {
const { getByTestId, container } = render(
<ButtonIcon
as="a"
data-testid="button-icon"
icon="add-square-filled"
ariaLabel="add"
/>,
);
expect(getByTestId('button-icon')).toHaveClass('mm-button-icon');
const anchor = container.getElementsByTagName('a').length;
expect(anchor).toBe(1);
});
it('should render anchor element correctly using href', () => {
const { getByTestId, getByRole } = render(
<ButtonIcon
href="/metamask"
data-testid="button-icon"
icon="add-square-filled"
ariaLabel="add"
/>,
);
expect(getByTestId('button-icon')).toHaveClass('mm-button-icon');
expect(getByRole('link')).toBeDefined();
});
it('should render with different size classes', () => {
const { getByTestId } = render(
<>
<ButtonIcon
icon="add-square-filled"
ariaLabel="add"
size={BUTTON_ICON_SIZES.SM}
data-testid={BUTTON_ICON_SIZES.SM}
/>
<ButtonIcon
icon="add-square-filled"
ariaLabel="add"
size={BUTTON_ICON_SIZES.LG}
data-testid={BUTTON_ICON_SIZES.LG}
/>
</>,
);
expect(getByTestId(BUTTON_ICON_SIZES.SM)).toHaveClass(
`mm-button-icon--size-${BUTTON_ICON_SIZES.SM}`,
);
expect(getByTestId(BUTTON_ICON_SIZES.LG)).toHaveClass(
`mm-button-icon--size-${BUTTON_ICON_SIZES.LG}`,
);
});
it('should render with different colors', () => {
const { getByTestId } = render(
<>
<ButtonIcon
icon="add-square-filled"
ariaLabel="add"
color={COLORS.ICON_DEFAULT}
data-testid={COLORS.ICON_DEFAULT}
/>
<ButtonIcon
icon="add-square-filled"
ariaLabel="add"
color={COLORS.ERROR_DEFAULT}
data-testid={COLORS.ERROR_DEFAULT}
/>
</>,
);
expect(getByTestId(COLORS.ICON_DEFAULT)).toHaveClass(
`box--color-${COLORS.ICON_DEFAULT}`,
);
expect(getByTestId(COLORS.ERROR_DEFAULT)).toHaveClass(
`box--color-${COLORS.ERROR_DEFAULT}`,
);
});
it('should render with added classname', () => {
const { getByTestId } = render(
<ButtonIcon
data-testid="classname"
className="mm-button-icon--test"
icon="add-square-filled"
ariaLabel="add"
/>,
);
expect(getByTestId('classname')).toHaveClass('mm-button-icon--test');
});
it('should render with different button states', () => {
const { getByTestId } = render(
<>
<ButtonIcon
disabled
data-testid="disabled"
icon="add-square-filled"
ariaLabel="add"
/>
</>,
);
expect(getByTestId('disabled')).toHaveClass(`mm-button-icon--disabled`);
expect(getByTestId('disabled')).toBeDisabled();
});
it('should render with icon', () => {
const { getByTestId } = render(
<ButtonIcon
data-testid="icon"
icon="add-square-filled"
ariaLabel="add"
iconProps={{ 'data-testid': 'button-icon' }}
/>,
);
expect(getByTestId('button-icon')).toBeDefined();
});
it('should render with aria-label', () => {
const { getByLabelText } = render(
<ButtonIcon icon="add-square-filled" ariaLabel="add" />,
);
expect(getByLabelText('add')).toBeDefined();
});
});

@ -0,0 +1,2 @@
export { ButtonIcon } from './button-icon';
export { BUTTON_ICON_SIZES } from './button-icon.constants';

@ -6,6 +6,7 @@
@import 'avatar-with-badge/avatar-with-badge';
@import 'base-avatar/base-avatar';
@import 'button-base/button-base';
@import 'button-icon/button-icon';
@import 'button-link/button-link';
@import 'button-primary/button-primary';
@import 'button-secondary/button-secondary';

Loading…
Cancel
Save