1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

Fix/buttonbase ts update (#20060)

* Migrate ButtonBase to TS

---------

Co-authored-by: Binij Shrestha <shresthabinij@gmail.com>
This commit is contained in:
Garrett Bear 2023-07-25 09:05:15 -07:00 committed by GitHub
parent 85465f53a7
commit 3f27d018c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 300 additions and 281 deletions

View File

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

View File

@ -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 (
<Text
as={Tag}
backgroundColor={BackgroundColor.backgroundAlternative}
color={loading ? TextColor.transparent : color}
variant={TextVariant.bodyMdMedium}
href={href}
paddingLeft={4}
paddingRight={4}
ellipsis={ellipsis}
className={classnames(
'mm-button-base',
{
[`mm-button-base--size-${size}`]:
Object.values(BUTTON_BASE_SIZES).includes(size),
'mm-button-base--loading': loading,
'mm-button-base--disabled': disabled,
'mm-button-base--block': block,
'mm-button-base--ellipsis': ellipsis,
},
className,
)}
disabled={disabled}
display={Display.InlineFlex}
justifyContent={JustifyContent.center}
alignItems={AlignItems.center}
borderRadius={BorderRadius.pill}
{...props}
>
{startIconName && (
<Icon
name={startIconName}
size={IconSize.Sm}
marginInlineEnd={1}
{...startIconProps}
color={loading ? IconColor.transparent : startIconProps?.color}
/>
)}
{/*
* 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
) : (
<Text
as="span"
ellipsis={ellipsis}
variant={TextVariant.inherit}
color={loading ? TextColor.transparent : color}
{...textProps}
>
{children}
</Text>
)}
{endIconName && (
<Icon
name={endIconName}
size={IconSize.Sm}
marginInlineStart={1}
{...endIconProps}
color={loading ? IconColor.transparent : endIconProps?.color}
/>
)}
{loading && (
<Icon
className="mm-button-base__icon-loading"
name={IconName.Loading}
color={color}
size={IconSize.Md}
{...iconLoadingProps}
/>
)}
</Text>
);
};
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,
};

View File

@ -6,7 +6,6 @@
user-select: none;
&--block {
display: block;
width: 100%;
}
@ -45,6 +44,10 @@
}
}
a.mm-button-base:hover {
color: var(--color-text-default);
}
@keyframes spinner {

View File

@ -1,15 +1,14 @@
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 { BUTTON_BASE_SIZES } from './button-base.constants';
import { Box, TextDirection, IconName } from '..';
import { ButtonBaseSize } from './button-base.types';
import { ButtonBase } from './button-base';
import README from './README.mdx';
@ -70,7 +69,7 @@ export default {
},
size: {
control: 'select',
options: Object.values(BUTTON_BASE_SIZES),
options: Object.values(ButtonBaseSize),
},
marginTop: {
options: marginSizeControlOptions,
@ -96,27 +95,29 @@ export default {
args: {
children: 'Button Base',
},
};
} as Meta<typeof ButtonBase>;
export const DefaultStory = (args) => <ButtonBase {...args} />;
export const DefaultStory: StoryFn<typeof ButtonBase> = (args) => (
<ButtonBase {...args} />
);
DefaultStory.storyName = 'Default';
export const SizeStory = (args) => (
export const SizeStory: StoryFn<typeof ButtonBase> = (args) => (
<>
<Box
display={DISPLAY.FLEX}
display={Display.Flex}
alignItems={AlignItems.baseline}
gap={1}
marginBottom={2}
>
<ButtonBase {...args} size={Size.SM}>
<ButtonBase {...args} size={ButtonBaseSize.Sm}>
Button SM
</ButtonBase>
<ButtonBase {...args} size={Size.MD}>
<ButtonBase {...args} size={ButtonBaseSize.Md}>
Button MD
</ButtonBase>
<ButtonBase {...args} size={Size.LG}>
<ButtonBase {...args} size={ButtonBaseSize.Lg}>
Button LG
</ButtonBase>
</Box>
@ -125,7 +126,7 @@ export const SizeStory = (args) => (
SizeStory.storyName = 'Size';
export const Block = (args) => (
export const Block: StoryFn<typeof ButtonBase> = (args) => (
<>
<ButtonBase {...args} marginBottom={2}>
Default Button
@ -136,8 +137,8 @@ export const Block = (args) => (
</>
);
export const As = (args) => (
<Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW} gap={2}>
export const As: StoryFn<typeof ButtonBase> = (args) => (
<Box display={Display.Flex} flexDirection={FlexDirection.Row} gap={2}>
<ButtonBase {...args}>Button Element</ButtonBase>
<ButtonBase as="a" href="#" {...args}>
Anchor Element
@ -145,13 +146,15 @@ export const As = (args) => (
</Box>
);
export const Href = (args) => <ButtonBase {...args}>Anchor Element</ButtonBase>;
export const Href: StoryFn<typeof ButtonBase> = (args) => (
<ButtonBase {...args}>Anchor Element</ButtonBase>
);
Href.args = {
href: '/metamask',
};
export const ExternalLink = (args) => (
export const ExternalLink: StoryFn<typeof ButtonBase> = (args) => (
<ButtonBase {...args}>Anchor element with external link</ButtonBase>
);
@ -160,7 +163,7 @@ ExternalLink.args = {
externalLink: true,
};
export const Disabled = (args) => (
export const Disabled: StoryFn<typeof ButtonBase> = (args) => (
<ButtonBase {...args}>Disabled Button</ButtonBase>
);
@ -168,7 +171,7 @@ Disabled.args = {
disabled: true,
};
export const Loading = (args) => (
export const Loading: StoryFn<typeof ButtonBase> = (args) => (
<ButtonBase {...args}>Loading Button</ButtonBase>
);
@ -176,20 +179,20 @@ Loading.args = {
loading: true,
};
export const StartIconName = (args) => (
export const StartIconName: StoryFn<typeof ButtonBase> = (args) => (
<ButtonBase {...args} startIconName={IconName.AddSquare}>
Button
</ButtonBase>
);
export const EndIconName = (args) => (
export const EndIconName: StoryFn<typeof ButtonBase> = (args) => (
<ButtonBase {...args} endIconName={IconName.Arrow2Right}>
Button
</ButtonBase>
);
export const Rtl = (args) => (
<Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.COLUMN} gap={2}>
export const Rtl: StoryFn<typeof ButtonBase> = (args) => (
<Box display={Display.Flex} flexDirection={FlexDirection.Column} gap={2}>
<ButtonBase
{...args}
startIconName={IconName.AddSquare}
@ -208,10 +211,14 @@ export const Rtl = (args) => (
</Box>
);
export const Ellipsis = (args) => (
<Box backgroundColor={Color.iconMuted} style={{ width: 150 }}>
export const Ellipsis: StoryFn<typeof ButtonBase> = (args) => (
<Box backgroundColor={BackgroundColor.primaryMuted} style={{ width: 150 }}>
<ButtonBase {...args}>Example without ellipsis</ButtonBase>
<ButtonBase {...args} ellipsis>
<ButtonBase
{...args}
ellipsis
textProps={{ color: TextColor.errorDefault }}
>
Example with ellipsis
</ButtonBase>
</Box>

View File

@ -2,7 +2,7 @@
import { render } from '@testing-library/react';
import React from 'react';
import { IconName } from '..';
import { BUTTON_BASE_SIZES } from './button-base.constants';
import { ButtonBaseSize } from './button-base.types';
import { ButtonBase } from './button-base';
describe('ButtonBase', () => {
@ -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(
<>
<ButtonBase
size={BUTTON_BASE_SIZES.SM}
data-testid={BUTTON_BASE_SIZES.SM}
/>
<ButtonBase
size={BUTTON_BASE_SIZES.MD}
data-testid={BUTTON_BASE_SIZES.MD}
/>
<ButtonBase
size={BUTTON_BASE_SIZES.LG}
data-testid={BUTTON_BASE_SIZES.LG}
/>
<ButtonBase size={ButtonBaseSize.Sm} data-testid={ButtonBaseSize.Sm} />
<ButtonBase size={ButtonBaseSize.Md} data-testid={ButtonBaseSize.Md} />
<ButtonBase size={ButtonBaseSize.Lg} data-testid={ButtonBaseSize.Lg} />
</>,
);
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}`,
);
});

View File

@ -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 type { TextProps } from '../text';
import {
ButtonBaseProps,
ButtonBaseSize,
ButtonBaseComponent,
} from './button-base.types';
export const ButtonBase: ButtonBaseComponent = React.forwardRef(
<C extends React.ElementType = 'button' | 'a'>(
{
as,
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<C>,
ref?: PolymorphicRef<C>,
) => {
const tag = href ? 'a' : as || 'button';
const tagProps = href && tag === 'a' ? { href, ...props } : props;
return (
<Text
as={tag}
backgroundColor={BackgroundColor.backgroundAlternative}
variant={TextVariant.bodyMdMedium}
color={loading ? TextColor.transparent : color}
ref={ref}
{...(tag === 'button' ? { disabled } : {})}
{...(href && externalLink
? { target: '_blank', rel: 'noopener noreferrer' }
: {})}
paddingLeft={4}
paddingRight={4}
ellipsis={ellipsis}
className={classnames(
'mm-button-base',
{
[`mm-button-base--size-${size}`]:
Object.values(ButtonBaseSize).includes(size),
'mm-button-base--loading': loading || false,
'mm-button-base--disabled': disabled || false,
'mm-button-base--block': block || false,
'mm-button-base--ellipsis': ellipsis,
},
className,
)}
display={Display.InlineFlex}
justifyContent={JustifyContent.center}
alignItems={AlignItems.center}
borderRadius={BorderRadius.pill}
{...(tagProps as TextProps<C>)}
>
{startIconName && (
<Icon
name={startIconName}
size={IconSize.Sm}
marginInlineEnd={1}
{...startIconProps}
color={loading ? IconColor.transparent : startIconProps?.color}
/>
)}
{/*
* 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
) : (
<Text
as="span"
ellipsis={ellipsis}
variant={TextVariant.inherit}
color={loading ? TextColor.transparent : color}
{...textProps}
>
{children}
</Text>
)}
{endIconName && (
<Icon
name={endIconName}
size={IconSize.Sm}
marginInlineStart={1}
{...endIconProps}
color={loading ? IconColor.transparent : endIconProps?.color}
/>
)}
{loading && (
<Icon
className="mm-button-base__icon-loading"
name={IconName.Loading}
color={iconColor}
size={IconSize.Md}
{...iconLoadingProps}
/>
)}
</Text>
);
},
);

View File

@ -0,0 +1,108 @@
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 type ValidButtonTagType = 'button' | 'a';
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?: ValidButtonTagType;
/**
* 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<C extends React.ElementType> =
PolymorphicComponentPropWithRef<C, ButtonBaseStyleUtilityProps>;
export type ButtonBaseComponent = <
C extends React.ElementType = 'button' | 'a',
>(
props: ButtonBaseProps<C>,
) => React.ReactElement | null;

View File

@ -1,2 +0,0 @@
export { ButtonBase } from './button-base';
export { BUTTON_BASE_SIZES } from './button-base.constants';

View File

@ -0,0 +1,3 @@
export { ButtonBase } from './button-base';
export { ButtonBaseSize } from './button-base.types';
export type { ButtonBaseProps } from './button-base.types';

View File

@ -1,11 +1,12 @@
.mm-button-link {
&:hover:not(&--disabled) {
color: var(--color-primary-default);
text-decoration: underline;
text-decoration-thickness: 2px;
text-underline-offset: 4px;
}
&:active {
&:active:not(&--disabled) {
color: var(--color-primary-alternative);
}

View File

@ -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';

View File

@ -3,7 +3,6 @@ import {
FontWeight,
FontStyle,
TextVariant,
TextAlign,
TextTransform,
OverflowWrap,
} from '../../../helpers/constants/design-system';
@ -72,7 +71,9 @@ export type ValidTagType =
| 'ul'
| 'label'
| 'input'
| 'header';
| 'header'
| 'a'
| 'button';
export interface TextStyleUtilityProps extends StyleUtilityProps {
/**
@ -117,11 +118,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)

View File

@ -164,6 +164,7 @@ export enum IconColor {
lineaMainnetInverse = 'linea-mainnet-inverse',
goerliInverse = 'goerli-inverse',
sepoliaInverse = 'sepolia-inverse',
transparent = 'transparent',
}
export enum TypographyVariant {