diff --git a/ui/components/component-library/button-base/__snapshots__/button-base.test.js.snap b/ui/components/component-library/button-base/__snapshots__/button-base.test.tsx.snap similarity index 100% rename from ui/components/component-library/button-base/__snapshots__/button-base.test.js.snap rename to ui/components/component-library/button-base/__snapshots__/button-base.test.tsx.snap diff --git a/ui/components/component-library/button-base/button-base.constants.js b/ui/components/component-library/button-base/button-base.constants.js deleted file mode 100644 index 73387a710ae1..000000000000 --- a/ui/components/component-library/button-base/button-base.constants.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Size } from '../../../helpers/constants/design-system'; - -export const BUTTON_BASE_SIZES = { - SM: Size.SM, - MD: Size.MD, - LG: Size.LG, -}; diff --git a/ui/components/component-library/button-base/button-base.js b/ui/components/component-library/button-base/button-base.js deleted file mode 100644 index ec32327b00a6..000000000000 --- a/ui/components/component-library/button-base/button-base.js +++ /dev/null @@ -1,203 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -import Box from '../../ui/box'; -import { IconName, Icon, IconSize } from '../icon'; -import { Text } from '..'; - -import { - AlignItems, - Display, - JustifyContent, - TextColor, - TextVariant, - BorderRadius, - BackgroundColor, - IconColor, -} from '../../../helpers/constants/design-system'; -import { BUTTON_BASE_SIZES } from './button-base.constants'; - -export const ButtonBase = ({ - as = 'button', - block, - children, - className, - href, - ellipsis = false, - externalLink, - size = BUTTON_BASE_SIZES.MD, - startIconName, - startIconProps, - endIconName, - endIconProps, - loading, - disabled, - iconLoadingProps, - textProps, - color = TextColor.textDefault, - ...props -}) => { - const Tag = href ? 'a' : as; - if (Tag === 'a' && externalLink) { - props.target = '_blank'; - props.rel = 'noopener noreferrer'; - } - return ( - - {startIconName && ( - - )} - {/* - * If children is a string and doesn't need truncation or loading - * prevent html bloat by rendering just the string - * otherwise render with wrapper to allow truncation or loading - */} - {typeof children === 'string' && !ellipsis && !loading ? ( - children - ) : ( - - {children} - - )} - {endIconName && ( - - )} - {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, - /** - * Additional props to pass to the Text component that wraps the button children - */ - buttonTextProps: PropTypes.object, - /** - * 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, - /** - * When an `href` prop is passed, ButtonBase will automatically change the root element to be an `a` (anchor) tag - */ - href: PropTypes.string, - /** - * Used for long strings that can be cut off... - */ - ellipsis: PropTypes.bool, - /** - * Boolean indicating if the link targets external content, it will cause the link to open in a new tab - */ - externalLink: PropTypes.bool, - /** - * Add icon to start (left side) of button text passing icon name - * The name of the icon to display. Should be one of IconName - */ - startIconName: PropTypes.oneOf(Object.values(IconName)), - /** - * iconProps accepts all the props from Icon - */ - startIconProps: PropTypes.object, - /** - * Add icon to end (right side) of button text passing icon name - * The name of the icon to display. Should be one of IconName - */ - endIconName: PropTypes.oneOf(Object.values(IconName)), - /** - * iconProps accepts all the props from Icon - */ - endIconProps: PropTypes.object, - /** - * iconLoadingProps accepts all the props from Icon - */ - iconLoadingProps: PropTypes.object, - /** - * Boolean to show loading spinner in button - */ - loading: PropTypes.bool, - /** - * The size of the ButtonBase. - * Possible values could be 'Size.SM'(32px), 'Size.MD'(40px), 'Size.LG'(48px), - */ - size: PropTypes.oneOfType([ - PropTypes.shape(BUTTON_BASE_SIZES), - PropTypes.string, - ]), - /** - * textProps accepts all the props from Icon - */ - textProps: PropTypes.object, - /** - * ButtonBase accepts all the props from Box - */ - ...Box.propTypes, -}; diff --git a/ui/components/component-library/button-base/button-base.stories.js b/ui/components/component-library/button-base/button-base.stories.tsx similarity index 65% rename from ui/components/component-library/button-base/button-base.stories.js rename to ui/components/component-library/button-base/button-base.stories.tsx index 6be335435142..8beed57f7b99 100644 --- a/ui/components/component-library/button-base/button-base.stories.js +++ b/ui/components/component-library/button-base/button-base.stories.tsx @@ -1,15 +1,16 @@ import React from 'react'; +import { StoryFn, Meta } from '@storybook/react'; import { AlignItems, - Color, - DISPLAY, - FLEX_DIRECTION, - Size, + BackgroundColor, + Display, + FlexDirection, + TextColor, } from '../../../helpers/constants/design-system'; import Box from '../../ui/box/box'; -import { TextDirection, IconName } from '..'; +import { TextDirection, IconName, ValidTag } from '..'; -import { BUTTON_BASE_SIZES } from './button-base.constants'; +import { ButtonBaseSize } from './button-base.types'; import { ButtonBase } from './button-base'; import README from './README.mdx'; @@ -70,7 +71,7 @@ export default { }, size: { control: 'select', - options: Object.values(BUTTON_BASE_SIZES), + options: Object.values(ButtonBaseSize), }, marginTop: { options: marginSizeControlOptions, @@ -96,27 +97,29 @@ export default { args: { children: 'Button Base', }, -}; +} as Meta; -export const DefaultStory = (args) => ; +export const DefaultStory: StoryFn = (args) => ( + +); DefaultStory.storyName = 'Default'; -export const SizeStory = (args) => ( +export const SizeStory: StoryFn = (args) => ( <> - + Button SM - + Button MD - + Button LG @@ -125,7 +128,7 @@ export const SizeStory = (args) => ( SizeStory.storyName = 'Size'; -export const Block = (args) => ( +export const Block: StoryFn = (args) => ( <> Default Button @@ -136,22 +139,24 @@ export const Block = (args) => ( ); -export const As = (args) => ( - +export const As: StoryFn = (args) => ( + Button Element - + Anchor Element ); -export const Href = (args) => Anchor Element; +export const Href: StoryFn = (args) => ( + Anchor Element +); Href.args = { href: '/metamask', }; -export const ExternalLink = (args) => ( +export const ExternalLink: StoryFn = (args) => ( Anchor element with external link ); @@ -160,7 +165,7 @@ ExternalLink.args = { externalLink: true, }; -export const Disabled = (args) => ( +export const Disabled: StoryFn = (args) => ( Disabled Button ); @@ -168,7 +173,7 @@ Disabled.args = { disabled: true, }; -export const Loading = (args) => ( +export const Loading: StoryFn = (args) => ( Loading Button ); @@ -176,20 +181,20 @@ Loading.args = { loading: true, }; -export const StartIconName = (args) => ( +export const StartIconName: StoryFn = (args) => ( Button ); -export const EndIconName = (args) => ( +export const EndIconName: StoryFn = (args) => ( Button ); -export const Rtl = (args) => ( - +export const Rtl: StoryFn = (args) => ( + ( ); -export const Ellipsis = (args) => ( - +export const Ellipsis: StoryFn = (args) => ( + Example without ellipsis - + Example with ellipsis diff --git a/ui/components/component-library/button-base/button-base.test.js b/ui/components/component-library/button-base/button-base.test.tsx similarity index 77% rename from ui/components/component-library/button-base/button-base.test.js rename to ui/components/component-library/button-base/button-base.test.tsx index 6e3a3bf43f8b..1b2d1c03dcaa 100644 --- a/ui/components/component-library/button-base/button-base.test.js +++ b/ui/components/component-library/button-base/button-base.test.tsx @@ -1,8 +1,8 @@ /* eslint-disable jest/require-top-level-describe */ import { render } from '@testing-library/react'; import React from 'react'; -import { IconName } from '..'; -import { BUTTON_BASE_SIZES } from './button-base.constants'; +import { IconName, ValidTag } from '..'; +import { ButtonBaseSize } from './button-base.types'; import { ButtonBase } from './button-base'; describe('ButtonBase', () => { @@ -18,7 +18,7 @@ describe('ButtonBase', () => { it('should render anchor element correctly', () => { const { getByTestId, container } = render( - , + , ); expect(getByTestId('button-base')).toHaveClass('mm-button-base'); const anchor = container.getElementsByTagName('a').length; @@ -51,17 +51,8 @@ describe('ButtonBase', () => { expect(getByTestId('button-base')).toHaveAttribute( 'href', 'https://www.test.com/', - 'target', - '_blank', - 'rel', - 'noopener noreferrer', - ); - expect(getByTestId('button-base')).toHaveAttribute( - 'target', - '_blank', - 'rel', - 'noopener noreferrer', ); + expect(getByTestId('button-base')).toHaveAttribute('target', '_blank'); expect(getByTestId('button-base')).toHaveAttribute( 'rel', 'noopener noreferrer', @@ -79,28 +70,19 @@ describe('ButtonBase', () => { it('should render with different size classes', () => { const { getByTestId } = render( <> - - - + + + , ); - expect(getByTestId(BUTTON_BASE_SIZES.SM)).toHaveClass( - `mm-button-base--size-${BUTTON_BASE_SIZES.SM}`, + expect(getByTestId(ButtonBaseSize.Sm)).toHaveClass( + `mm-button-base--size-${ButtonBaseSize.Sm}`, ); - expect(getByTestId(BUTTON_BASE_SIZES.MD)).toHaveClass( - `mm-button-base--size-${BUTTON_BASE_SIZES.MD}`, + expect(getByTestId(ButtonBaseSize.Md)).toHaveClass( + `mm-button-base--size-${ButtonBaseSize.Md}`, ); - expect(getByTestId(BUTTON_BASE_SIZES.LG)).toHaveClass( - `mm-button-base--size-${BUTTON_BASE_SIZES.LG}`, + expect(getByTestId(ButtonBaseSize.Lg)).toHaveClass( + `mm-button-base--size-${ButtonBaseSize.Lg}`, ); }); diff --git a/ui/components/component-library/button-base/button-base.tsx b/ui/components/component-library/button-base/button-base.tsx new file mode 100644 index 000000000000..8c1282a69111 --- /dev/null +++ b/ui/components/component-library/button-base/button-base.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import classnames from 'classnames'; +import { IconName, Icon, IconSize, Text } from '..'; +import { + AlignItems, + Display, + JustifyContent, + TextColor, + TextVariant, + BorderRadius, + BackgroundColor, + IconColor, +} from '../../../helpers/constants/design-system'; +import type { PolymorphicRef } from '../box'; +import { + ButtonBaseProps, + ButtonBaseSize, + ButtonBaseComponent, +} from './button-base.types'; + +export const ButtonBase: ButtonBaseComponent = React.forwardRef( + ( + { + as = 'button', + block, + children, + className = '', + href, + ellipsis = false, + externalLink, + size = ButtonBaseSize.Md, + startIconName, + startIconProps, + endIconName, + endIconProps, + loading, + disabled, + iconLoadingProps, + textProps, + color = TextColor.textDefault, + iconColor = IconColor.iconDefault, + ...props + }: ButtonBaseProps, + ref?: PolymorphicRef, + ) => { + const Tag = href ? 'a' : as; + if (Tag === 'a' && externalLink) { + props.target = '_blank'; + props.rel = 'noopener noreferrer'; + } + + return ( + + {startIconName && ( + + )} + {/* + * If children is a string and doesn't need truncation or loading + * prevent html bloat by rendering just the string + * otherwise render with wrapper to allow truncation or loading + */} + {typeof children === 'string' && !ellipsis && !loading ? ( + children + ) : ( + + {children} + + )} + {endIconName && ( + + )} + {loading && ( + + )} + + ); + }, +); diff --git a/ui/components/component-library/button-base/button-base.types.ts b/ui/components/component-library/button-base/button-base.types.ts new file mode 100644 index 000000000000..5ca12677b2fc --- /dev/null +++ b/ui/components/component-library/button-base/button-base.types.ts @@ -0,0 +1,105 @@ +import { ReactNode } from 'react'; +import type { + StyleUtilityProps, + PolymorphicComponentPropWithRef, +} from '../box'; +import { IconColor } from '../../../helpers/constants/design-system'; +import { TextDirection, TextProps } from '../text'; +import { IconName, IconProps } from '../icon'; + +export enum ButtonBaseSize { + Sm = 'sm', + Md = 'md', + Lg = 'lg', +} + +export interface ButtonBaseStyleUtilityProps extends StyleUtilityProps { + /** + * The polymorphic `as` prop allows you to change the root HTML element of the Button component between `button` and `a` tag + * + */ + as?: 'button' | 'a'; + /** + * Boolean prop to quickly activate box prop display block + */ + block?: boolean; + /** + * The children to be rendered inside the ButtonBase + */ + children?: ReactNode; + /** + * Boolean to disable button + */ + disabled?: boolean; + /** + * When an `href` prop is passed, ButtonBase will automatically change the root element to be an `a` (anchor) tag + */ + href?: string; + /** + * Used for long strings that can be cut off... + */ + ellipsis?: boolean; + /** + * Boolean indicating if the link targets external content, it will cause the link to open in a new tab + */ + externalLink?: boolean; + /** + * Add icon to start (left side) of button text passing icon name + * The name of the icon to display. Should be one of IconName + */ + startIconName?: IconName; + /** + * iconProps accepts all the props from Icon + */ + startIconProps?: IconProps; + /** + * Add icon to end (right side) of button text passing icon name + * The name of the icon to display. Should be one of IconName + */ + endIconName?: IconName; + /** + * iconProps accepts all the props from Icon + */ + endIconProps?: IconProps; + /** + * iconLoadingProps accepts all the props from Icon + */ + iconLoadingProps?: IconProps; + /** + * Boolean to show loading spinner in button + */ + loading?: boolean; + /** + * The size of the ButtonBase. + * Possible values could be 'Size.SM'(32px), 'Size.MD'(40px), 'Size.LG'(48px), + */ + size?: ButtonBaseSize; + /** + * textProps are additional props to pass to the Text component that wraps the button children + */ + textProps?: TextProps<'span'>; + /** + * Specifies where to display the linked URL. + */ + target?: string; + /** + * Specifies the relationship between the current document and + * the linked URL. + */ + rel?: string; + /** + * Sets the color of the button icon. + */ + iconColor?: IconColor; + /** + * Direction of the text content within the button ("ltr" or "rtl"). + */ + textDirection?: TextDirection; +} + +export type ButtonBaseProps = + PolymorphicComponentPropWithRef; + +export type ButtonBaseComponent = ( + props: ButtonBaseProps, +) => React.ReactElement | null; diff --git a/ui/components/component-library/button-base/index.js b/ui/components/component-library/button-base/index.js deleted file mode 100644 index 3d030b89b39c..000000000000 --- a/ui/components/component-library/button-base/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { ButtonBase } from './button-base'; -export { BUTTON_BASE_SIZES } from './button-base.constants'; diff --git a/ui/components/component-library/button-base/index.ts b/ui/components/component-library/button-base/index.ts new file mode 100644 index 000000000000..44d2ed54d1ff --- /dev/null +++ b/ui/components/component-library/button-base/index.ts @@ -0,0 +1,3 @@ +export { ButtonBase } from './button-base'; +export { ButtonBaseSize } from './button-base.types'; +export type { ButtonBaseProps } from './button-base.types'; diff --git a/ui/components/component-library/index.js b/ui/components/component-library/index.js index 564643063982..588707bdab46 100644 --- a/ui/components/component-library/index.js +++ b/ui/components/component-library/index.js @@ -16,7 +16,7 @@ export { } from './badge-wrapper'; export { Box } from './box'; export { Button, BUTTON_VARIANT, BUTTON_SIZES } from './button'; -export { ButtonBase, BUTTON_BASE_SIZES } from './button-base'; +export { ButtonBase, ButtonBaseSize } from './button-base'; export { ButtonIcon, ButtonIconSize } from './button-icon'; export { ButtonLink, BUTTON_LINK_SIZES } from './button-link'; export { ButtonPrimary, BUTTON_PRIMARY_SIZES } from './button-primary'; diff --git a/ui/components/component-library/text/text.types.ts b/ui/components/component-library/text/text.types.ts index 2afc5a71436b..ca221003fed3 100644 --- a/ui/components/component-library/text/text.types.ts +++ b/ui/components/component-library/text/text.types.ts @@ -3,7 +3,6 @@ import { FontWeight, FontStyle, TextVariant, - TextAlign, TextTransform, OverflowWrap, } from '../../../helpers/constants/design-system'; @@ -52,6 +51,8 @@ export enum ValidTag { Label = 'label', Input = 'input', Header = 'header', + A = 'a', + Button = 'button', } export type ValidTagType = @@ -72,7 +73,9 @@ export type ValidTagType = | 'ul' | 'label' | 'input' - | 'header'; + | 'header' + | 'a' + | 'button'; export interface TextStyleUtilityProps extends StyleUtilityProps { /** @@ -117,11 +120,6 @@ export interface TextStyleUtilityProps extends StyleUtilityProps { * ./ui/helpers/constants/design-system.js */ textTransform?: TextTransform; - /** - * The text-align of the Text component. Should use the TextAlign enum from - * ./ui/helpers/constants/design-system.js - */ - textAlign?: TextAlign; /** * Change the dir (direction) global attribute of text to support the direction a language is written * Possible values: `LEFT_TO_RIGHT` (default), `RIGHT_TO_LEFT`, `AUTO` (user agent decides) diff --git a/ui/helpers/constants/design-system.ts b/ui/helpers/constants/design-system.ts index 89530360a6f1..602a79e2d9c7 100644 --- a/ui/helpers/constants/design-system.ts +++ b/ui/helpers/constants/design-system.ts @@ -164,6 +164,7 @@ export enum IconColor { lineaMainnetInverse = 'linea-mainnet-inverse', goerliInverse = 'goerli-inverse', sepoliaInverse = 'sepolia-inverse', + transparent = 'transparent', } export enum TypographyVariant {