Skip to content
This repository has been archived by the owner on Feb 1, 2024. It is now read-only.

[GUILD] Typescript Button #2123

Merged
merged 11 commits into from
May 16, 2022
1 change: 1 addition & 0 deletions src/@types/@teamleader/ui-icons.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module '@teamleader/ui-icons';
4 changes: 2 additions & 2 deletions src/components/box/Box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const borderRadiuses = {
rounded: '4px',
};

type Props = Partial<
export type BoxProps = Partial<
{
alignContent: 'center' | 'flex-start' | 'flex-end' | 'space-around' | 'space-between' | 'space-evenly';
alignItems: 'center' | 'flex-start' | 'flex-end' | 'baseline' | 'stretch';
Expand Down Expand Up @@ -121,7 +121,7 @@ const Box = forwardRef(
style,
textAlign,
...others
}: Props,
}: BoxProps,
ref,
) => {
const getBorder = (value: number) => {
Expand Down
169 changes: 61 additions & 108 deletions src/components/button/Button.js → src/components/button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
import PropTypes from 'prop-types';
import React, { forwardRef, ReactNode } from 'react';
import Box from '../box';
import LoadingSpinner from '../loadingSpinner';
import { UITextBody, UITextDisplay, UITextSmall } from '../typography';
import cx from 'classnames';
import theme from './theme.css';
import { BoxProps } from '../box/Box';

export const BUTTON_LEVELS = {
outline: 'outline',
primary: 'primary',
secondary: 'secondary',
destructive: 'destructive',
link: 'link',
timer: 'timer',
};
export enum BUTTON_LEVELS {
outline = 'outline',
primary = 'primary',
secondary = 'secondary',
destructive = 'destructive',
link = 'link',
timer = 'timer',
}

const textComponentMap = {
tiny: UITextSmall,
Expand All @@ -22,33 +22,58 @@ const textComponentMap = {
large: UITextDisplay,
};

/** @type {React.ComponentType<any>} */
interface ButtonProps extends Omit<BoxProps, 'size'> {
/** The content to display inside the button. */
children?: ReactNode;
/** A class name for the button to give custom styles. */
className?: string;
/** The color which the button should have when 'level' is set to 'outline' */
color?: 'teal' | 'neutral' | 'mint' | 'violet' | 'ruby' | 'gold' | 'aqua' | 'white';
/** A custom element to be rendered */
element?: any;
/** If true, component will be disabled. */
disabled?: boolean;
/** If true, component will be shown in an active state */
active?: boolean;
/** If true, component will take the full width available. */
fullWidth?: boolean;
/** The icon displayed inside the button. */
icon?: ReactNode;
/** The position of the icon inside the button. */
iconPlacement?: 'left' | 'right';
/** The textual label displayed inside the button. */
label?: string;
/** Determines which kind of button to be rendered. */
level?: `${BUTTON_LEVELS}`;
/** If true, component will show a loading spinner instead of label or children. */
processing?: boolean;
/** Size of the button. */
size?: 'tiny' | 'small' | 'medium' | 'large';
/** Type of the button element. */
type?: string;
}

const Button = forwardRef(
(
{
color,
level,
onMouseUp,
onMouseLeave,
color = 'teal',
children,
className,
disabled,
element,
active,
fullWidth,
disabled = false,
element = 'button',
active = false,
fullWidth = false,
icon,
iconPlacement,
iconPlacement = 'left',
label,
size,
type,
processing,
level = BUTTON_LEVELS.secondary,
size = 'medium',
type = 'button',
processing = false,
...others
},
}: ButtonProps,
ref,
) => {
const buttonRef = useRef();
useImperativeHandle(ref, () => buttonRef.current);

const getSpinnerColor = () => {
switch (level) {
case BUTTON_LEVELS.secondary:
Expand Down Expand Up @@ -85,27 +110,6 @@ const Button = forwardRef(
}
};

const handleMouseUp = (event) => {
blur();
if (onMouseUp) {
onMouseUp(event);
}
};

const handleMouseLeave = (event) => {
blur();
if (onMouseLeave) {
onMouseLeave(event);
}
};

const blur = () => {
const currentButtonRef = buttonRef.current;
if (currentButtonRef) {
currentButtonRef.blur();
}
};

const classNames = cx(
theme['reset-box-sizing'],
theme['reset-font-smoothing'],
Expand All @@ -124,22 +128,18 @@ const Button = forwardRef(
className,
);

const props = {
...others,
ref: buttonRef,
className: classNames,
disabled: element === 'button' ? disabled : null,
element: element,
onMouseUp: handleMouseUp,
onMouseLeave: handleMouseLeave,
type: element === 'button' ? type : null,
'data-teamleader-ui': 'button',
};

const Text = textComponentMap[size];

return (
<Box {...props}>
<Box
{...others}
className={classNames}
data-teamleader-ui="button"
disabled={element === 'button' ? disabled : undefined}
element={element}
ref={ref}
type={element === 'button' ? type : undefined}
>
{icon && iconPlacement === 'left' && icon}
{(label || children) && (
<Text
Expand All @@ -166,53 +166,6 @@ const Button = forwardRef(
},
);

Button.propTypes = {
/** The content to display inside the button. */
children: PropTypes.any,
/** A class name for the button to give custom styles. */
className: PropTypes.string,
/** The color which the button should have when 'level' is set to 'outline' */
color: PropTypes.oneOf(['teal', 'neutral', 'mint', 'violet', 'ruby', 'gold', 'aqua', 'white']),
/** A custom element to be rendered */
element: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
/** Determines which kind of button to be rendered. */
level: PropTypes.oneOf(Object.values(BUTTON_LEVELS)),
/** If true, component will be disabled. */
disabled: PropTypes.bool,
/** If true, component will be shown in an active state */
active: PropTypes.bool,
/** If true, component will take the full width available. */
fullWidth: PropTypes.bool,
/** The icon displayed inside the button. */
icon: PropTypes.element,
/** The position of the icon inside the button. */
iconPlacement: PropTypes.oneOf(['left', 'right']),
/** The textual label displayed inside the button. */
label: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
/** Callback function that is fired when mouse leaves the component. */
onMouseLeave: PropTypes.func,
/** Callback function that is fired when the mouse button is released. */
onMouseUp: PropTypes.func,
/** If true, component will show a loading spinner instead of label or children. */
processing: PropTypes.bool,
/** Size of the button. */
size: PropTypes.oneOf(['tiny', 'small', 'medium', 'large']),
/** Type of the button element. */
type: PropTypes.string,
};

Button.defaultProps = {
className: '',
color: 'teal',
element: 'button',
fullWidth: false,
level: 'secondary',
iconPlacement: 'left',
processing: false,
size: 'medium',
type: 'button',
};

Button.displayName = 'Button';

export default Button;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { addStoryInGroup, LOW_LEVEL_BLOCKS } from '../../../.storybook/utils';
import { IconAddMediumOutline, IconAddSmallOutline } from '@teamleader/ui-icons';
import Button from './Button';
Expand All @@ -13,9 +14,9 @@ export default {
url: 'https://www.figma.com/file/LHH25GN90ljQaBEUNMsdJn/Desktop-components?node-id=225%3A2',
},
},
};
} as ComponentMeta<typeof Button>;

export const withTextAndIcon = ({ size, ...args }) => (
export const withTextAndIcon: ComponentStory<typeof Button> = ({ size, ...args }) => (
<Button
{...args}
size={size}
Expand All @@ -29,14 +30,16 @@ withTextAndIcon.args = {

withTextAndIcon.storyName = 'With text and icon';

export const WithText = () => <Button label="Button with text" />;
export const WithText: ComponentStory<typeof Button> = () => <Button label="Button with text" />;

WithText.storyName = 'With text';

export const withIcon = () => <Button icon={<IconAddMediumOutline />} />;
export const withIcon: ComponentStory<typeof Button> = () => <Button icon={<IconAddMediumOutline />} />;

withIcon.storyName = 'With icon';

export const withCustomElement = () => <Button element="a" label="Button with custom element" />;
export const withCustomElement: ComponentStory<typeof Button> = () => (
<Button element="a" label="Button with custom element" />
);

withCustomElement.storyName = 'With custom element';