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