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
parent
a28d727caf
commit
03af17747b
@ -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'; |
Loading…
Reference in new issue