️ feat: make the button component to forward ref

This commit is contained in:
Wahyu Kurniawan 2024-02-20 22:59:31 +07:00
parent 2ac657c32e
commit 0f7c6c73c9
No known key found for this signature in database
GPG Key ID: 040A1549143A8E33

View File

@ -1,4 +1,4 @@
import React, { useCallback } from 'react'; import React, { forwardRef, useCallback } from 'react';
import type { ComponentPropsWithoutRef, ReactNode } from 'react'; import type { ComponentPropsWithoutRef, ReactNode } from 'react';
import { buttonTheme } from './Button.theme'; import { buttonTheme } from './Button.theme';
@ -64,104 +64,114 @@ export type ButtonOrLinkProps = (ButtonLinkProps | ButtonProps) &
/** /**
* A custom button component that can be used in React applications. * A custom button component that can be used in React applications.
*/ */
const Button = ({ const Button = forwardRef<
children, HTMLButtonElement | HTMLAnchorElement,
className, ButtonOrLinkProps
leftIcon, >(
rightIcon, (
fullWidth, {
iconOnly, children,
shape, className,
variant, leftIcon,
...props rightIcon,
}: ButtonOrLinkProps) => { fullWidth,
// Conditionally render between <NextLink>, <a> or <button> depending on props iconOnly,
// useCallback to prevent unnecessary re-rendering shape,
const Component = useCallback( variant,
({ children: _children, ..._props }: ButtonOrLinkProps) => { ...props
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>;
}
}, },
[], ref,
); ) => {
// 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
* Extracts specific style properties from the given props object and returns them as a new object. if (external) {
*/ const externalLinkProps = {
const styleProps = (({ target: '_blank',
variant = 'primary', rel: 'noopener',
size = 'md', href,
fullWidth = false, ...baseLinkProps,
iconOnly = false, };
shape = 'rounded', return <a {...externalLinkProps}>{_children}</a>;
as, }
}) => ({
variant,
size,
fullWidth,
iconOnly,
shape,
as,
}))({ ...props, fullWidth, iconOnly, shape, variant });
/** // Internal link
* Validates that a button component has either children or an aria-label prop. return (
*/ <Link {...baseLinkProps} to={href}>
if (typeof children === 'undefined' && !props['aria-label']) { {_children}
throw new Error( </Link>
'Button components must have either children or an aria-label prop', );
} else {
const { ...buttonProps } = _props;
// @ts-expect-error - as prop is not a valid prop for button elements
return <button {...buttonProps}>{_children}</button>;
}
},
[],
); );
}
const iconSize = useCallback(() => { /**
switch (styleProps.size) { * Extracts specific style properties from the given props object and returns them as a new object.
case 'lg': */
return { width: 20, height: 20 }; const styleProps = (({
case 'sm': variant = 'primary',
case 'xs': size = 'md',
return { width: 16, height: 16 }; fullWidth = false,
case 'md': iconOnly = false,
default: { shape = 'rounded',
return { width: 18, height: 18 }; as,
} }) => ({
variant,
size,
fullWidth,
iconOnly,
shape,
as,
}))({ ...props, fullWidth, iconOnly, 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',
);
} }
}, [styleProps.size])();
return ( const iconSize = useCallback(() => {
<Component switch (styleProps.size) {
{...props} case 'lg':
className={buttonTheme({ ...styleProps, class: className })} return { width: 20, height: 20 };
> case 'sm':
{cloneIcon(leftIcon, { ...iconSize })} case 'xs':
{children} return { width: 16, height: 16 };
{cloneIcon(rightIcon, { ...iconSize })} case 'md':
</Component> default: {
); return { width: 18, height: 18 };
}; }
}
}, [styleProps.size])();
return (
<Component
{...props}
// @ts-expect-error - ref is not a valid prop for button elements
ref={ref}
className={buttonTheme({ ...styleProps, class: className })}
>
{cloneIcon(leftIcon, { ...iconSize })}
{children}
{cloneIcon(rightIcon, { ...iconSize })}
</Component>
);
},
);
Button.displayName = 'Button'; Button.displayName = 'Button';