⚡️ feat: make the button component to forward ref
This commit is contained in:
parent
2ac657c32e
commit
0f7c6c73c9
@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { forwardRef, useCallback } from 'react';
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
|
||||
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.
|
||||
*/
|
||||
const Button = ({
|
||||
children,
|
||||
className,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
fullWidth,
|
||||
iconOnly,
|
||||
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>;
|
||||
}
|
||||
const Button = forwardRef<
|
||||
HTMLButtonElement | HTMLAnchorElement,
|
||||
ButtonOrLinkProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
className,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
fullWidth,
|
||||
iconOnly,
|
||||
shape,
|
||||
variant,
|
||||
...props
|
||||
},
|
||||
[],
|
||||
);
|
||||
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;
|
||||
|
||||
/**
|
||||
* Extracts specific style properties from the given props object and returns them as a new object.
|
||||
*/
|
||||
const styleProps = (({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
fullWidth = false,
|
||||
iconOnly = false,
|
||||
shape = 'rounded',
|
||||
as,
|
||||
}) => ({
|
||||
variant,
|
||||
size,
|
||||
fullWidth,
|
||||
iconOnly,
|
||||
shape,
|
||||
as,
|
||||
}))({ ...props, fullWidth, iconOnly, shape, variant });
|
||||
// External link
|
||||
if (external) {
|
||||
const externalLinkProps = {
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
href,
|
||||
...baseLinkProps,
|
||||
};
|
||||
return <a {...externalLinkProps}>{_children}</a>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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',
|
||||
// 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>;
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
const iconSize = useCallback(() => {
|
||||
switch (styleProps.size) {
|
||||
case 'lg':
|
||||
return { width: 20, height: 20 };
|
||||
case 'sm':
|
||||
case 'xs':
|
||||
return { width: 16, height: 16 };
|
||||
case 'md':
|
||||
default: {
|
||||
return { width: 18, height: 18 };
|
||||
}
|
||||
/**
|
||||
* Extracts specific style properties from the given props object and returns them as a new object.
|
||||
*/
|
||||
const styleProps = (({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
fullWidth = false,
|
||||
iconOnly = false,
|
||||
shape = 'rounded',
|
||||
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 (
|
||||
<Component
|
||||
{...props}
|
||||
className={buttonTheme({ ...styleProps, class: className })}
|
||||
>
|
||||
{cloneIcon(leftIcon, { ...iconSize })}
|
||||
{children}
|
||||
{cloneIcon(rightIcon, { ...iconSize })}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
const iconSize = useCallback(() => {
|
||||
switch (styleProps.size) {
|
||||
case 'lg':
|
||||
return { width: 20, height: 20 };
|
||||
case 'sm':
|
||||
case 'xs':
|
||||
return { width: 16, height: 16 };
|
||||
case 'md':
|
||||
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';
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user