️ feat: create button component

This commit is contained in:
Wahyu Kurniawan 2024-02-19 20:13:11 +07:00
parent 082353e9bc
commit e850435c2d
No known key found for this signature in database
GPG Key ID: 040A1549143A8E33
3 changed files with 289 additions and 0 deletions

View File

@ -0,0 +1,137 @@
import { tv } from 'tailwind-variants';
import type { VariantProps } from 'tailwind-variants';
/**
* Defines the theme for a button component.
*/
export const buttonTheme = tv(
{
base: [
'inline-flex',
'items-center',
'justify-center',
'font-medium',
'whitespace-nowrap',
'focus-ring',
'disabled:cursor-not-allowed',
],
variants: {
size: {
lg: ['gap-3', 'py-4', 'px-6', 'rounded-lg', 'text-lg'],
md: ['gap-2', 'py-3', 'px-4', 'rounded-lg', 'text-base'],
sm: ['gap-1', 'py-2', 'px-2', 'rounded-md', 'text-xs'],
xs: ['gap-1', 'py-1', 'px-2', 'rounded-md', 'text-xs'],
},
fullWidth: {
true: 'w-full',
},
shape: {
default: '',
rounded: 'rounded-full',
},
variant: {
primary: [
'text-elements-on-primary',
'border',
'border-primary-700',
'bg-controls-primary',
'shadow-button',
'hover:bg-controls-primary-hovered',
'focus-visible:bg-controls-primary-hovered',
'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled',
'disabled:border-transparent',
'disabled:shadow-none',
],
secondary: [
'text-components-buttons-secondary-foreground',
'border',
'border-primary-500',
'bg-components-buttons-secondary-background',
'hover:text-components-buttons-secondary-foreground-hover',
'hover:bg-components-buttons-secondary-background-hover',
'focus-visible:text-components-buttons-secondary-foreground-focus',
'focus-visible:bg-components-buttons-secondary-background-focus',
'disabled:text-components-buttons-secondary-foreground-disabled',
'disabled:bg-components-buttons-secondary-background-disabled ',
'disabled:border-transparent',
],
tertiary: [
'text-components-buttons-tertiary-background',
'border',
'border-components-buttons-tertiary-background',
'bg-transparent',
'hover:text-components-buttons-tertiary-hover',
'hover:border-components-buttons-tertiary-hover',
'focus-visible:text-components-buttons-tertiary-focus',
'focus-visible:border-components-buttons-tertiary-focus',
'disabled:text-components-buttons-tertiary-disabled',
'disabled:border-components-buttons-tertiary-disabled',
],
'text-only': [
'text-components-buttons-text-only-background',
'border',
'border-transparent',
'bg-transparent',
'hover:text-components-buttons-text-only-foreground-hover',
'hover:bg-components-buttons-text-only-background-hover',
'focus-visible:text-components-buttons-text-only-foreground-focus',
'focus-visible:bg-components-buttons-text-only-background-focus',
'disabled:text-components-buttons-tertiary-disabled',
'disabled:bg-transparent',
],
danger: [
'text-components-button-icon-alert-foreground',
'border',
'border-transparent',
'bg-components-buttons-alert-background',
'hover:text-components-buttons-alert-foreground-hover',
'hover:bg-components-buttons-alert-background-hover',
'focus-visible:text-components-buttons-alert-foreground-focus',
'focus-visible:bg-components-buttons-alert-background-focus',
'disabled:text-components-button-icon-alert-foreground-disabled',
'disabled:bg-components-button-icon-alert-background-disabled',
],
'icon-only': [
'p-0 flex items-center justify-center',
'text-components-button-icon-text-only-foreground',
'border',
'border-transparent',
'bg-transparent',
'hover:text-components-button-icon-text-only-foreground-hover',
'hover:bg-components-button-icon-text-only-background-hover',
'focus-visible:text-components-button-icon-low-emphasis-foreground-focus',
'focus-visible:bg-components-button-icon-low-emphasis-background-focus',
'disabled:text-components-button-icon-low-emphasis-outlined-foreground-disabled',
'disabled:bg-transparent',
],
unstyled: [],
},
},
compoundVariants: [
{
size: 'md',
variant: 'icon-only',
class: ['h-11', 'w-11', 'rounded-lg'],
},
{
size: 'sm',
variant: 'icon-only',
class: ['h-8', 'w-8', 'rounded-md'],
},
],
defaultVariants: {
size: 'md',
variant: 'primary',
fullWidth: false,
},
},
{
responsiveVariants: true,
},
);
/**
* Represents the type of a button theme, which is derived from the `buttonTheme` variant props.
*/
export type ButtonTheme = VariantProps<typeof buttonTheme>;

View File

@ -0,0 +1,150 @@
import React, { useCallback } from 'react';
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
import { buttonTheme } from './Button.theme';
import type { ButtonTheme } from './Button.theme';
import { Link } from 'react-router-dom';
/**
* Represents the properties of a base button component.
*/
interface ButtonBaseProps {
/**
* The optional left icon element for a component.
* @type {ReactNode}
*/
leftIcon?: ReactNode;
/**
* The optional right icon element to display.
* @type {ReactNode}
*/
rightIcon?: ReactNode;
}
/**
* Interface for the props of a button link component.
*/
export interface ButtonLinkProps
extends Omit<ComponentPropsWithoutRef<'a'>, 'color'> {
/**
* Specifies the optional property `as` with a value of `'a'`.
* @type {'a'}
*/
as?: 'a';
/**
* Indicates whether the item is external or not.
* @type {boolean}
*/
external?: boolean;
/**
* The URL of a web page or resource.
* @type {string}
*/
href: string;
}
export interface ButtonProps
extends Omit<ComponentPropsWithoutRef<'button'>, 'color'> {
/**
* Specifies the optional property `as` with a value of `'button'`.
* @type {'button'}
*/
as?: 'button';
}
/**
* Interface representing the props for a button component.
* Extends the ComponentPropsWithoutRef<'button'> and ButtonTheme interfaces.
*/
export type ButtonOrLinkProps = (ButtonLinkProps | ButtonProps) &
ButtonBaseProps &
ButtonTheme;
/**
* A custom button component that can be used in React applications.
*/
const Button = ({
children,
className,
leftIcon,
rightIcon,
fullWidth,
shape,
variant,
...props
}: ButtonOrLinkProps) => {
// Conditionally render between <NextLink>, <a> or <button> depending on props
// useCallback to prevent unnecessary re-rendering
const Component = useCallback(
({ children: _children, ..._props }: ButtonOrLinkProps) => {
if (_props.as === 'a') {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { external, href, as, ...baseLinkProps } = _props;
// External link
if (external) {
const externalLinkProps = {
target: '_blank',
rel: 'noopener',
href,
...baseLinkProps,
};
return <a {...externalLinkProps}>{_children}</a>;
}
// Internal link
return (
<Link {...baseLinkProps} to={href}>
{_children}
</Link>
);
} else {
const { ...buttonProps } = _props;
// @ts-expect-error - as prop is not a valid prop for button elements
return <button {...buttonProps}>{_children}</button>;
}
},
[],
);
/**
* Extracts specific style properties from the given props object and returns them as a new object.
*/
const styleProps = (({
variant = 'primary',
size = 'md',
fullWidth = false,
shape = 'rounded',
as,
}) => ({
variant,
size,
fullWidth,
shape,
as,
}))({ ...props, fullWidth, shape, variant });
/**
* Validates that a button component has either children or an aria-label prop.
*/
if (typeof children === 'undefined' && !props['aria-label']) {
throw new Error(
'Button components must have either children or an aria-label prop',
);
}
return (
<Component
{...props}
className={buttonTheme({ ...styleProps, class: className })}
>
{leftIcon}
{children}
{rightIcon}
</Component>
);
};
Button.displayName = 'Button';
export { Button };

View File

@ -0,0 +1,2 @@
export * from './Button';
export * from './Button.theme';