Skip to content

Commit

Permalink
Fix to use Element type to return React component
Browse files Browse the repository at this point in the history
  • Loading branch information
Qs-F committed Nov 23, 2023
1 parent 10aeb7e commit 810bc38
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 159 deletions.
295 changes: 150 additions & 145 deletions packages/for-ui/src/button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Children, ComponentPropsWithoutRef, ElementType, forwardRef, ReactNode, Ref, useMemo } from 'react';
import { Children, ElementType, forwardRef, ReactNode, useMemo } from 'react';
import MuiButton, { ButtonUnstyledProps as MuiButtonProps } from '@mui/base/ButtonUnstyled';
import { LoadingButtonProps } from '@mui/lab/LoadingButton';
import { Loader } from '../loader';
import { ComponentPropsWithAs, Element, Ref } from '../system/componentType';
import { fsx } from '../system/fsx';
import { walkChildren } from '../system/walkChildren';

// Iterable<ReactNode> seems to contain string but cannot be excluded, so added as sum type.
type Child = Exclude<ReactNode, Iterable<ReactNode>> | string;

export type ButtonProps<As extends ElementType = 'button'> = MuiButtonProps<As> &
ComponentPropsWithoutRef<As> & {
export type ButtonProps<As extends ElementType = 'button'> = ComponentPropsWithAs<
MuiButtonProps<As> & {
/**
* 種類を指定
*
Expand Down Expand Up @@ -97,7 +98,9 @@ export type ButtonProps<As extends ElementType = 'button'> = MuiButtonProps<As>
color?: 'primary' | 'secondary' | 'default';

className?: string;
};
},
As
>;

const extractText = (root: ReactNode): string => {
let ret = '';
Expand All @@ -118,148 +121,150 @@ const structures = ['text', 'icon', 'text-icon', 'icon-text'] as const;

type Structure = (typeof structures)[number];

const _Button = <As extends ElementType = 'button'>({
as,
variant = 'outlined',
intention: passedIntention = 'subtle',
size = 'large',
disabled = false,
loading = false,
startIcon,
endIcon,
color,
children,
_ref,
className,
...rest
}: ButtonProps<As> & { _ref?: Ref<As> }): JSX.Element => {
const component = as || 'button';
const childTexts = useMemo(() => Children.map(children, extractText) || [], [children]);
const label = childTexts.join('');
const structure: Structure = useMemo(() => {
if ((childTexts.at(0) && !childTexts.at(-1)) || (endIcon && children)) {
return 'text-icon';
}
if ((!childTexts.at(0) && childTexts.at(-1)) || (startIcon && children)) {
return 'icon-text';
}
if (!childTexts.at(0) || (startIcon && !children)) {
return 'icon';
}
return 'text';
}, [startIcon, endIcon, children, childTexts]);
type ButtonComponent = <As extends ElementType = 'button'>(props: ButtonProps<As>) => Element;

// Legacy support for color props
// If not needed, rename the passedIntention to intention.
export const Button: ButtonComponent = forwardRef(
<As extends ElementType = 'button'>(
{
as,
variant = 'outlined',
intention: passedIntention = 'subtle',
size = 'large',
disabled = false,
loading = false,
startIcon,
endIcon,
color,
children,
className,
...rest
}: ButtonProps<As>,
ref: Ref<As>,
): JSX.Element => {
const component = as || 'button';
const childTexts = useMemo(() => Children.map(children, extractText) || [], [children]);
const label = childTexts.join('');
const structure: Structure = useMemo(() => {
if ((childTexts.at(0) && !childTexts.at(-1)) || (endIcon && children)) {
return 'text-icon';
}
if ((!childTexts.at(0) && childTexts.at(-1)) || (startIcon && children)) {
return 'icon-text';
}
if (!childTexts.at(0) || (startIcon && !children)) {
return 'icon';
}
return 'text';
}, [startIcon, endIcon, children, childTexts]);

const intention = color
? (
{
primary: 'primary',
secondary: 'secondary',
default: 'primary',
} as const
)[color]
: passedIntention;
// Legacy support for color props
// If not needed, rename the passedIntention to intention.

return (
<MuiButton<As>
component={component}
ref={_ref}
disabled={disabled || loading}
aria-label={label || rest['aria-label'] || 'button'}
aria-busy={loading}
className={fsx([
`rounded-1.5 focus-visible:shadow-focused relative flex h-fit w-fit shrink-0 flex-row items-center justify-center font-sans outline-none disabled:cursor-not-allowed [&_svg]:fill-inherit`,
{
text: {
large: `px-4 py-2 gap-1`,
medium: `px-4 py-1 gap-1`,
small: `px-2 py-0.5 gap-0.5`,
},
icon: {
large: `p-2 gap-1 [&_svg]:w-6 [&_svg]:h-6`,
medium: `p-1 gap-1 [&_svg]:w-6 [&_svg]:h-6`,
small: `p-1 gap-1 [&_svg]:w-4 [&_svg]:h-4`,
},
'text-icon': {
large: `pl-4 pr-3 py-2 gap-1 [&_svg]:w-4 [&_svg]:h-4`,
medium: `pl-4 pr-2 py-1 gap-1 [&_svg]:w-4 [&_svg]:h-4`,
small: `pl-2 pr-1 py-0.5 gap-0.5 [&_svg]:w-3 [&_svg]:h-3`,
},
'icon-text': {
large: `pl-3 pr-4 py-2 gap-1 [&_svg]:w-4 [&_svg]:h-4`,
medium: `pl-2 pr-4 py-1 gap-1 [&_svg]:w-4 [&_svg]:h-4`,
small: `pl-1 pr-2 py-0.5 gap-0.5 [&_svg]:w-3 [&_svg]:h-3`,
},
}[structure][size],
{
large: `text-r`,
medium: `text-r`,
small: `text-s`,
}[size],
{
filled: `font-bold disabled:bg-shade-dark-disabled disabled:text-shade-white-disabled disabled:fill-shade-dark-disabled`,
outlined: `font-regular outline outline-1 -outline-offset-1 disabled:bg-shade-dark-disabled disabled:outline-shade-dark-disabled disabled:text-shade-white-disabled disabled:fill-shade-dark-disabled`,
text: `font-bold disabled:bg-shade-dark-disabled disabled:text-shade-white-disabled disabled:fill-shade-dark-disabled`,
}[variant],
{
subtle: {
filled: [
`bg-shade-light-default hover:bg-shade-light-hover focus-visible:bg-shade-light-hover text-shade-medium-default fill-shade-medium-default`,
structure === 'icon' && `fill-shade-dark-default`,
],
outlined: [
`bg-shade-white-default hover:bg-shade-white-hover focus-visible:bg-shade-white-hover outline-shade-medium-default text-shade-dark-default fill-shade-medium-default`,
structure === 'icon' && `fill-shade-dark-default`,
],
text: [
`bg-shade-white-default hover:bg-shade-white-hover focus-visible:bg-shade-white-hover text-shade-medium-default fill-shade-medium-default`,
structure === 'icon' && `fill-shade-dark-default`,
],
},
primary: {
filled: `bg-primary-dark-default hover:bg-primary-dark-hover focus-visible:bg-primary-dark-hover text-shade-white-default fill-shade-white-default`,
outlined: `bg-shade-white-default hover:bg-shade-white-hover focus-visible:bg-shade-white-hover outline-primary-dark-default text-primary-dark-default fill-primary-dark-default`,
text: `bg-shade-white-default hover:bg-shade-white-hover focus-visible:bg-shade-white-hover text-primary-dark-default fill-primary-dark-default`,
},
secondary: {
filled: `bg-secondary-dark-default hover:bg-secondary-dark-hover focus-visible:bg-secondary-dark-hover text-shade-white-default fill-shade-white-default`,
outlined: `bg-shade-white-default hover:bg-shade-white-hover focus-visible:bg-shade-white-hover outline-secondary-dark-default text-secondary-dark-default fill-secondary-dark-default`,
text: `bg-shade-white-default hover:bg-shade-white-hover focus-visible:bg-shade-white-hover text-secondary-dark-default fill-secondary-dark-default`,
},
shade: {
filled: `bg-shade-dark-default hover:bg-shade-dark-hover text-shade-white-default fill-shade-white-default`,
outlined: `bg-shade-white-default hover:bg-shade-white-hover outline-shade-dark-default text-shade-dark-default fill-shade-dark-default`,
text: `bg-shade-white-default hover:bg-shade-white-hover text-shade-dark-default fill-shade-dark-default`,
},
negative: {
filled: `bg-negative-dark-default hover:bg-negative-dark-hover text-shade-white-default fill-shade-white-default`,
outlined: `bg-shade-white-default hover:bg-shade-white-hover outline-negative-dark-default text-negative-dark-default fill-negative-dark-default`,
text: `bg-shade-white-default hover:bg-shade-white-hover text-negative-dark-default fill-negative-dark-default`,
},
}[intention][variant],
className,
])}
// FIXME: Avoid unintended type error, maybe MUI's problem?
{...(rest as MuiButtonProps<As>)}
>
{startIcon}
{children}
{endIcon}
{loading && (
<div className={fsx(`absolute inset-0 grid h-full w-full place-items-center`)}>
<Loader
className={fsx(
`[&:is(svg):is(svg)]:fill-shade-dark-default [&:is(svg):is(svg)]:h-6 [&:is(svg):is(svg)]:w-6`,
)}
/>
</div>
)}
</MuiButton>
);
};
const intention = color
? (
{
primary: 'primary',
secondary: 'secondary',
default: 'primary',
} as const
)[color]
: passedIntention;

export const Button = forwardRef((props: ButtonProps<ElementType>, ref: Ref<ElementType>) => (
<_Button _ref={ref} {...props} />
)) as <As extends ElementType = 'button'>(props: ButtonProps<As>) => JSX.Element;
return (
<MuiButton<As>
component={component}
ref={ref}
disabled={disabled || loading}
aria-label={label || rest['aria-label'] || 'button'}
aria-busy={loading}
className={fsx([
`rounded-1.5 focus-visible:shadow-focused relative flex h-fit w-fit shrink-0 flex-row items-center justify-center font-sans outline-none disabled:cursor-not-allowed [&_svg]:fill-inherit`,
{
text: {
large: `px-4 py-2 gap-1`,
medium: `px-4 py-1 gap-1`,
small: `px-2 py-0.5 gap-0.5`,
},
icon: {
large: `p-2 gap-1 [&_svg]:w-6 [&_svg]:h-6`,
medium: `p-1 gap-1 [&_svg]:w-6 [&_svg]:h-6`,
small: `p-1 gap-1 [&_svg]:w-4 [&_svg]:h-4`,
},
'text-icon': {
large: `pl-4 pr-3 py-2 gap-1 [&_svg]:w-4 [&_svg]:h-4`,
medium: `pl-4 pr-2 py-1 gap-1 [&_svg]:w-4 [&_svg]:h-4`,
small: `pl-2 pr-1 py-0.5 gap-0.5 [&_svg]:w-3 [&_svg]:h-3`,
},
'icon-text': {
large: `pl-3 pr-4 py-2 gap-1 [&_svg]:w-4 [&_svg]:h-4`,
medium: `pl-2 pr-4 py-1 gap-1 [&_svg]:w-4 [&_svg]:h-4`,
small: `pl-1 pr-2 py-0.5 gap-0.5 [&_svg]:w-3 [&_svg]:h-3`,
},
}[structure][size],
{
large: `text-r`,
medium: `text-r`,
small: `text-s`,
}[size],
{
filled: `font-bold disabled:bg-shade-dark-disabled disabled:text-shade-white-disabled disabled:fill-shade-dark-disabled`,
outlined: `font-regular outline outline-1 -outline-offset-1 disabled:bg-shade-dark-disabled disabled:outline-shade-dark-disabled disabled:text-shade-white-disabled disabled:fill-shade-dark-disabled`,
text: `font-bold disabled:bg-shade-dark-disabled disabled:text-shade-white-disabled disabled:fill-shade-dark-disabled`,
}[variant],
{
subtle: {
filled: [
`bg-shade-light-default hover:bg-shade-light-hover focus-visible:bg-shade-light-hover text-shade-medium-default fill-shade-medium-default`,
structure === 'icon' && `fill-shade-dark-default`,
],
outlined: [
`bg-shade-white-default hover:bg-shade-white-hover focus-visible:bg-shade-white-hover outline-shade-medium-default text-shade-dark-default fill-shade-medium-default`,
structure === 'icon' && `fill-shade-dark-default`,
],
text: [
`bg-shade-white-default hover:bg-shade-white-hover focus-visible:bg-shade-white-hover text-shade-medium-default fill-shade-medium-default`,
structure === 'icon' && `fill-shade-dark-default`,
],
},
primary: {
filled: `bg-primary-dark-default hover:bg-primary-dark-hover focus-visible:bg-primary-dark-hover text-shade-white-default fill-shade-white-default`,
outlined: `bg-shade-white-default hover:bg-shade-white-hover focus-visible:bg-shade-white-hover outline-primary-dark-default text-primary-dark-default fill-primary-dark-default`,
text: `bg-shade-white-default hover:bg-shade-white-hover focus-visible:bg-shade-white-hover text-primary-dark-default fill-primary-dark-default`,
},
secondary: {
filled: `bg-secondary-dark-default hover:bg-secondary-dark-hover focus-visible:bg-secondary-dark-hover text-shade-white-default fill-shade-white-default`,
outlined: `bg-shade-white-default hover:bg-shade-white-hover focus-visible:bg-shade-white-hover outline-secondary-dark-default text-secondary-dark-default fill-secondary-dark-default`,
text: `bg-shade-white-default hover:bg-shade-white-hover focus-visible:bg-shade-white-hover text-secondary-dark-default fill-secondary-dark-default`,
},
shade: {
filled: `bg-shade-dark-default hover:bg-shade-dark-hover text-shade-white-default fill-shade-white-default`,
outlined: `bg-shade-white-default hover:bg-shade-white-hover outline-shade-dark-default text-shade-dark-default fill-shade-dark-default`,
text: `bg-shade-white-default hover:bg-shade-white-hover text-shade-dark-default fill-shade-dark-default`,
},
negative: {
filled: `bg-negative-dark-default hover:bg-negative-dark-hover text-shade-white-default fill-shade-white-default`,
outlined: `bg-shade-white-default hover:bg-shade-white-hover outline-negative-dark-default text-negative-dark-default fill-negative-dark-default`,
text: `bg-shade-white-default hover:bg-shade-white-hover text-negative-dark-default fill-negative-dark-default`,
},
}[intention][variant],
className,
])}
// FIXME: Avoid unintended type error, maybe MUI's problem?
{...(rest as MuiButtonProps<As>)}
>
{startIcon}
{children}
{endIcon}
{loading && (
<div className={fsx(`absolute inset-0 grid h-full w-full place-items-center`)}>
<Loader
className={fsx(
`[&:is(svg):is(svg)]:fill-shade-dark-default [&:is(svg):is(svg)]:h-6 [&:is(svg):is(svg)]:w-6`,
)}
/>
</div>
)}
</MuiButton>
);
},
);
6 changes: 3 additions & 3 deletions packages/for-ui/src/chip/Chip.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ElementType, forwardRef, MouseEvent, ReactNode } from 'react';
import { ComponentProps, ElementTypeToHTMLElement, Ref } from '../system/polyComponent';
import { ComponentPropsWithAs, Element, ElementTypeToHTMLElement, Ref } from '../system/componentType';
import { FullChip } from './FullChip';
import { LimitedChip } from './LimitedChip';

export type ChipProps<As extends ElementType = 'button'> = ComponentProps<
export type ChipProps<As extends ElementType = 'button'> = ComponentPropsWithAs<
{
/**
* ユーザーに提示したい意図 (e.g. エラーならばnegative) を指定
Expand Down Expand Up @@ -46,7 +46,7 @@ export type ChipProps<As extends ElementType = 'button'> = ComponentProps<
As
>;

type ChipComponent = <As extends ElementType = 'button'>(props: ChipProps<As>) => ReactNode;
type ChipComponent = <As extends ElementType = 'button'>(props: ChipProps<As>) => Element;

export const Chip: ChipComponent = forwardRef(
<As extends ElementType = 'button'>({ clickableArea = 'full', ...props }: ChipProps<As>, ref?: Ref<As>) =>
Expand Down
8 changes: 4 additions & 4 deletions packages/for-ui/src/chip/FullChip.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { ElementType, forwardRef, ReactNode } from 'react';
import { ElementType, forwardRef } from 'react';
import { Element, Ref } from '../system/componentType';
import { fsx } from '../system/fsx';
import { Ref } from '../system/polyComponent';
import { Text } from '../text';
import { ChipProps } from './Chip';

type FullChipProps<As extends ElementType = 'button'> = Omit<ChipProps<As>, 'clickableArea'>;

type FullChipComponent = <As extends ElementType = 'button'>(props: FullChipProps<As>) => ReactNode;
type FullChipComponent = <As extends ElementType = 'button'>(props: FullChipProps<As>) => Element;

export const FullChip: FullChipComponent = forwardRef(
<As extends ElementType = 'button'>(props: FullChipProps<As>, ref: Ref<As>): JSX.Element => {
<As extends ElementType = 'button'>(props: FullChipProps<As>, ref: Ref<As>) => {
const { as, label, icon, intention = 'shade', className, ...rest } = props;
const Component: ElementType = as || 'button';
return (
Expand Down
8 changes: 4 additions & 4 deletions packages/for-ui/src/chip/LimitedChip.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { ElementType, forwardRef, ReactNode } from 'react';
import { ElementType, forwardRef } from 'react';
import { MdClose } from 'react-icons/md';
import { Element, Ref } from '../system/componentType';
import { fsx } from '../system/fsx';
import { Ref } from '../system/polyComponent';
import { Text } from '../text';
import { ChipProps } from './Chip';

type LimitedChipProps<As extends ElementType = 'span'> = Omit<ChipProps<As>, 'clickableArea'>;

type LimitedChipComponent = <As extends ElementType = 'span'>(props: LimitedChipProps<As>) => ReactNode;
type LimitedChipComponent = <As extends ElementType = 'span'>(props: LimitedChipProps<As>) => Element;

export const LimitedChip: LimitedChipComponent = forwardRef(
<As extends ElementType = 'span'>(props: LimitedChipProps<As>, ref: Ref<As>): JSX.Element => {
<As extends ElementType = 'span'>(props: LimitedChipProps<As>, ref: Ref<As>) => {
const { as, label, icon = <MdClose />, intention = 'shade', onClick, className, ...rest } = props;
const Component: ElementType = as || 'button';

Expand Down
Loading

0 comments on commit 810bc38

Please sign in to comment.