15090: add primary button (#16079)

* 15090: add primary button

* updates

* add button base props

* add button base props to primary

* remove button base props and improve classname test

* update box shadow animation

* fix anchor test and update documentation

* fix button base iconProps proptype
feature/default_network_editable
Garrett Bear 2 years ago committed by GitHub
parent a993509afc
commit 12aa200ad0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      ui/components/component-library/button-base/button-base.js
  2. 11
      ui/components/component-library/button-base/button-base.test.js
  3. 57
      ui/components/component-library/button-primary/README.mdx
  4. 7
      ui/components/component-library/button-primary/button-primary.constants.js
  5. 45
      ui/components/component-library/button-primary/button-primary.js
  6. 43
      ui/components/component-library/button-primary/button-primary.scss
  7. 137
      ui/components/component-library/button-primary/button-primary.stories.js
  8. 97
      ui/components/component-library/button-primary/button-primary.test.js
  9. 1
      ui/components/component-library/button-primary/index.js
  10. 1
      ui/components/component-library/component-library-components.scss

@ -39,9 +39,9 @@ export const ButtonBase = ({
'mm-button',
`mm-button--size-${size}`,
{
'mm-button--loading': Boolean(loading),
'mm-button--disabled': Boolean(disabled),
'mm-button--block': Boolean(block),
'mm-button--loading': loading,
'mm-button--disabled': disabled,
'mm-button--block': block,
},
className,
)}
@ -117,7 +117,7 @@ ButtonBase.propTypes = {
/**
* iconProps accepts all the props from Icon
*/
iconProps: Icon.propTypes,
iconProps: PropTypes.object,
/**
* Boolean to show loading spinner in button
*/

@ -18,9 +18,9 @@ describe('ButtonBase', () => {
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');
const anchor = container.getElementsByTagName('a').length;
expect(anchor).toBe(1);
});
it('should render button as block', () => {
@ -51,6 +51,13 @@ describe('ButtonBase', () => {
);
});
it('should render with added classname', () => {
const { getByTestId } = render(
<ButtonBase data-testid="classname" className="mm-button--test" />,
);
expect(getByTestId('classname')).toHaveClass('mm-button--test');
});
it('should render with different button states', () => {
const { getByTestId } = render(
<>

@ -0,0 +1,57 @@
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import { ButtonPrimary } from './button-primary';
# ButtonPrimary
The `ButtonPrimary` is an extension of `ButtonBase` to support primary styles.
<Canvas>
<Story id="ui-components-component-library-button-primary-button-primary-stories-js--default-story" />
</Canvas>
## Props
The `ButtonPrimary` accepts all props below as well as all [Box](/docs/ui-components-ui-box-box-stories-js--default-story#props) and [ButtonBase](/docs/ui-components-component-library-button-base-button-base-stories-js--default-story#props) component props
<ArgsTable of={ButtonPrimary} />
### Size
Use the `size` prop and the `SIZES` object from `./ui/helpers/constants/design-system.js` to change the size of `ButtonPrimary`. Defaults to `SIZES.MD`
Optional: `BUTTON_SIZES` from `./button-base` object can be used instead of `SIZES`.
Possible sizes include:
- `SIZES.SM` 32px
- `SIZES.MD` 40px
- `SIZES.LG` 48px
<Canvas>
<Story id="ui-components-component-library-button-primary-button-primary-stories-js--size" />
</Canvas>
```jsx
import { SIZES } from '../../../helpers/constants/design-system';
import { ButtonPrimary } from '../ui/component-library/button/button-primary/button-primary';
<ButtonPrimary size={SIZES.SM} />
<ButtonPrimary size={SIZES.MD} />
<ButtonPrimary size={SIZES.LG} />
```
### Type
Use the `type` prop and the `BUTTON_TYPES` object from `./ui/helpers/constants/design-system.js` to change the context of `ButtonPrimary`.
<Canvas>
<Story id="ui-components-component-library-button-primary-button-primary-stories-js--type" />
</Canvas>
```jsx
import { ButtonPrimary } from '../ui/component-library/button/button-primary/button-primary';
<ButtonPrimary>Normal</ButtonPrimary>
<ButtonPrimary danger>Danger</ButtonPrimary>
```

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

@ -0,0 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { ButtonBase } from '../button-base';
import { BUTTON_PRIMARY_SIZES } from './button-primary.constants';
export const ButtonPrimary = ({
className,
danger,
size = BUTTON_PRIMARY_SIZES.MD,
...props
}) => {
return (
<ButtonBase
className={classnames(className, 'mm-button-primary', {
'mm-button-primary--type-danger': danger,
})}
size={size}
{...props}
/>
);
};
ButtonPrimary.propTypes = {
/**
* An additional className to apply to the ButtonPrimary.
*/
className: PropTypes.string,
/**
* Boolean to change button type to Danger when true
*/
danger: PropTypes.bool,
/**
* The possible size values for ButtonPrimary: 'SIZES.SM', 'SIZES.MD', 'SIZES.LG',
* Default value is 'SIZES.MD'.
*/
size: PropTypes.oneOf(Object.values(BUTTON_PRIMARY_SIZES)),
/**
* ButtonPrimary accepts all the props from ButtonBase
*/
...ButtonBase.propTypes,
};
export default ButtonPrimary;

@ -0,0 +1,43 @@
.mm-button-primary {
color: var(--color-primary-inverse);
background-color: var(--color-primary-default);
&:hover {
color: var(--color-primary-inverse);
box-shadow: var(--component-button-primary-shadow);
}
&:active {
color: var(--color-primary-inverse);
background-color: var(--color-primary-alternative);
}
&.mm-button--disabled {
&:hover {
box-shadow: none;
}
&:active {
background-color: var(--color-primary-default);
}
}
&--type-danger {
color: var(--color-error-inverse);
background-color: var(--color-error-default);
&:hover {
color: var(--color-error-inverse);
box-shadow: var(--component-button-danger-shadow);
}
&:active {
color: var(--color-error-inverse);
background-color: var(--color-error-alternative);
}
&.mm-button--disabled:active {
background-color: var(--color-error-default);
}
}
}

@ -0,0 +1,137 @@
import React from 'react';
import { ALIGN_ITEMS, DISPLAY } from '../../../helpers/constants/design-system';
import Box from '../../ui/box/box';
import { ICON_NAMES } from '../icon';
import { ButtonPrimary } from './button-primary';
import { BUTTON_PRIMARY_SIZES } from './button-primary.constants';
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/ButtonPrimary',
id: __filename,
component: ButtonPrimary,
parameters: {
docs: {
page: README,
},
},
argTypes: {
as: {
control: 'select',
options: ['button', 'a'],
table: { category: 'button base props' },
},
block: {
control: 'boolean',
table: { category: 'button base props' },
},
children: {
control: 'text',
},
className: {
control: 'text',
},
danger: {
control: 'boolean',
},
disabled: {
control: 'boolean',
table: { category: 'button base props' },
},
icon: {
control: 'select',
options: Object.values(ICON_NAMES),
table: { category: 'button base props' },
},
iconPositionRight: {
control: 'boolean',
table: { category: 'button base props' },
},
iconProps: {
control: 'object',
table: { category: 'button base props' },
},
loading: {
control: 'boolean',
table: { category: 'button base props' },
},
size: {
control: 'select',
options: Object.values(BUTTON_PRIMARY_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 Primary',
},
};
export const DefaultStory = (args) => (
<>
<ButtonPrimary {...args} />
</>
);
DefaultStory.storyName = 'Default';
export const Size = (args) => (
<Box display={DISPLAY.FLEX} alignItems={ALIGN_ITEMS.BASELINE} gap={1}>
<ButtonPrimary {...args} size={BUTTON_PRIMARY_SIZES.SM}>
Small Button
</ButtonPrimary>
<ButtonPrimary {...args} size={BUTTON_PRIMARY_SIZES.MD}>
Medium (Default) Button
</ButtonPrimary>
<ButtonPrimary {...args} size={BUTTON_PRIMARY_SIZES.LG}>
Large Button
</ButtonPrimary>
</Box>
);
export const Type = (args) => (
<Box display={DISPLAY.FLEX} gap={1}>
<ButtonPrimary {...args}>Normal</ButtonPrimary>
{/* Test Anchor tag to match exactly as button */}
<ButtonPrimary as="a" {...args} href="#" danger>
Danger
</ButtonPrimary>
</Box>
);

@ -0,0 +1,97 @@
/* eslint-disable jest/require-top-level-describe */
import { render } from '@testing-library/react';
import React from 'react';
import { ButtonPrimary } from './button-primary';
import { BUTTON_PRIMARY_SIZES } from './button-primary.constants';
describe('ButtonPrimary', () => {
it('should render button element correctly', () => {
const { getByText, getByTestId, container } = render(
<ButtonPrimary data-testid="button-primary">
Button Primary
</ButtonPrimary>,
);
expect(getByText('Button Primary')).toBeDefined();
expect(container.querySelector('button')).toBeDefined();
expect(getByTestId('button-primary')).toHaveClass('mm-button');
});
it('should render anchor element correctly', () => {
const { getByTestId, container } = render(
<ButtonPrimary as="a" data-testid="button-primary" />,
);
expect(getByTestId('button-primary')).toHaveClass('mm-button');
const anchor = container.getElementsByTagName('a').length;
expect(anchor).toBe(1);
});
it('should render button as block', () => {
const { getByTestId } = render(<ButtonPrimary block data-testid="block" />);
expect(getByTestId('block')).toHaveClass(`mm-button--block`);
});
it('should render with added classname', () => {
const { getByTestId } = render(
<ButtonPrimary data-testid="classname" className="mm-button--test" />,
);
expect(getByTestId('classname')).toHaveClass('mm-button--test');
});
it('should render with different size classes', () => {
const { getByTestId } = render(
<>
<ButtonPrimary
size={BUTTON_PRIMARY_SIZES.SM}
data-testid={BUTTON_PRIMARY_SIZES.SM}
/>
<ButtonPrimary
size={BUTTON_PRIMARY_SIZES.MD}
data-testid={BUTTON_PRIMARY_SIZES.MD}
/>
<ButtonPrimary
size={BUTTON_PRIMARY_SIZES.LG}
data-testid={BUTTON_PRIMARY_SIZES.LG}
/>
</>,
);
expect(getByTestId(BUTTON_PRIMARY_SIZES.SM)).toHaveClass(
`mm-button--size-${BUTTON_PRIMARY_SIZES.SM}`,
);
expect(getByTestId(BUTTON_PRIMARY_SIZES.MD)).toHaveClass(
`mm-button--size-${BUTTON_PRIMARY_SIZES.MD}`,
);
expect(getByTestId(BUTTON_PRIMARY_SIZES.LG)).toHaveClass(
`mm-button--size-${BUTTON_PRIMARY_SIZES.LG}`,
);
});
it('should render with different types', () => {
const { getByTestId } = render(
<>
<ButtonPrimary danger data-testid="danger" />
</>,
);
expect(getByTestId('danger')).toHaveClass('mm-button-primary--type-danger');
});
it('should render with different button states', () => {
const { getByTestId } = render(
<>
<ButtonPrimary loading data-testid="loading" />
<ButtonPrimary disabled data-testid="disabled" />
</>,
);
expect(getByTestId('loading')).toHaveClass(`mm-button--loading`);
expect(getByTestId('disabled')).toHaveClass(`mm-button--disabled`);
});
it('should render with icon', () => {
const { container } = render(
<ButtonPrimary data-testid="icon" icon="add-square-filled" />,
);
const icons = container.getElementsByClassName('icon').length;
expect(icons).toBe(1);
});
});

@ -0,0 +1 @@
export { ButtonPrimary } from './button-primary';

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

Loading…
Cancel
Save