Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fair-knives-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@4design/for-ui": minor
---

refactor(Button): MUI剥がし
23 changes: 13 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"npm-run-all2": "6.1.2",
"prettier": "3.2.5",
"turbo": "1.11.2",
"typescript": "5.1.6"
"typescript": "5.4.5"
},
"volta": {
"node": "16.20.2"
Expand Down
2 changes: 1 addition & 1 deletion packages/for-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"react-hook-form": "7.45.4",
"react-icons": "4.10.1",
"tailwindcss": "3.4.3",
"typescript": "5.1.6",
"typescript": "5.4.5",
"vite": "3.2.5",
"vitest": "0.33.0",
"webpack": "5.88.2",
Expand Down
14 changes: 14 additions & 0 deletions packages/for-ui/src/button/Button.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FC, ReactNode } from 'react';
import { MdOutlineCheck } from 'react-icons/md';
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
Expand Down Expand Up @@ -93,3 +94,16 @@ describe('Button', () => {
expect(element).toBeInTheDocument();
});
});

describe('Type', () => {
it('is semantically correct for React component using as props', async () => {
const Link: FC<{ to: string; children?: ReactNode }> = ({ to, children }) => <a href={to}>{children}</a>;
render(
<Button as={Link} to="https://example.com">
test
</Button>,
);
const element = await screen.findByRole('link', { name: 'test' });
expect(element).toBeInTheDocument();
});
});
101 changes: 20 additions & 81 deletions packages/for-ui/src/button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { Children, ElementType, forwardRef, MouseEvent, MouseEventHandler, ReactNode, useMemo } from 'react';
import MuiButton, { ButtonUnstyledProps as MuiButtonProps } from '@mui/base/ButtonUnstyled';
import { LoadingButtonProps } from '@mui/lab/LoadingButton';
import { Children, ElementType, forwardRef, ReactNode, useMemo } from 'react';
import { Loader } from '../loader';
import { ComponentPropsWithAs, Element, Ref } from '../system/componentType';
import { fsx } from '../system/fsx';
Expand All @@ -10,7 +8,7 @@ import { walkChildren } from '../system/walkChildren';
type Child = Exclude<ReactNode, Iterable<ReactNode>> | string;

export type ButtonProps<As extends ElementType = 'button'> = ComponentPropsWithAs<
Omit<MuiButtonProps<As>, 'href' | 'children' | 'onClick'> & {
{
/**
* 種類を指定
*
Expand Down Expand Up @@ -57,48 +55,6 @@ export type ButtonProps<As extends ElementType = 'button'> = ComponentPropsWithA

disabled?: boolean;

/**
* 先頭に表示するアイコンを指定
*
* @deprecated childrenを使用してください
* ```
* <Button>
* <MdEdit />
* 編集
* </Button>
* ```
*/
startIcon?: ReactNode;

/**
* 末尾に表示するアイコンを指定
*
* @deprecated childrenを使用してください
* ```
* <Button>
* 編集
* <MdEdit />
* </Button>
* ```
*/
endIcon?: ReactNode;

/**
* 読み込み中のアイコンを表示する場所を指定
*
* @deprecated デザインの仕様変更に伴い表示位置は固定になりました
*/
loadingPosition?: LoadingButtonProps['loadingPosition'];

/**
* colorを指定する場合に指定
*
* @deprecated intention propsを使ってください
*/
color?: 'primary' | 'secondary' | 'default';

onClick?: MouseEventHandler<HTMLElementTagNameMap[As extends keyof HTMLElementTagNameMap ? As : 'button']>;

className?: string;
},
As
Expand Down Expand Up @@ -130,56 +86,41 @@ export const Button: ButtonComponent = forwardRef(
{
as,
variant = 'outlined',
intention: passedIntention = 'subtle',
intention = 'subtle',
size = 'large',
loading = false,
startIcon,
endIcon,
color,
children,
className,
disabled,
onClick,
...rest
}: ButtonProps<As>,
ref?: Ref<As>,
): Element => {
const component = as || 'button';
const Component = as || 'button';
const childTexts = useMemo(() => Children.map(children, extractText) || [], [children]);
const structure: Structure = useMemo(() => {
if ((childTexts.at(0) && !childTexts.at(-1)) || (endIcon && children)) {
if (childTexts.at(0) && !childTexts.at(-1)) {
return 'text-icon';
}
if ((!childTexts.at(0) && childTexts.at(-1)) || (startIcon && children)) {
if (!childTexts.at(0) && childTexts.at(-1)) {
return 'icon-text';
}
if (!childTexts.at(0) || (startIcon && !children)) {
if (!childTexts.at(0)) {
return 'icon';
}
return 'text';
}, [startIcon, endIcon, children, childTexts]);

// Legacy support for color props
// If not needed, rename the passedIntention to intention.

const intention = color
? (
{
primary: 'primary',
secondary: 'secondary',
default: 'primary',
} as const
)[color]
: passedIntention;
}, [childTexts]);

return (
<MuiButton<As>
component={component}
<Component
ref={ref}
aria-disabled={loading}
aria-busy={loading}
type="button"
disabled={disabled}
aria-disabled={loading || disabled}
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`,
`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 aria-disabled:cursor-not-allowed [&_svg]:fill-inherit`,
{
text: {
large: `px-4 py-2 gap-1`,
Expand Down Expand Up @@ -208,9 +149,9 @@ export const Button: ButtonComponent = forwardRef(
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`,
filled: `font-bold aria-disabled:bg-shade-dark-disabled aria-disabled:text-shade-white-disabled aria-disabled:fill-shade-dark-disabled`,
outlined: `font-regular outline outline-1 -outline-offset-1 aria-disabled:bg-shade-dark-disabled aria-disabled:outline-shade-dark-disabled aria-disabled:text-shade-white-disabled aria-disabled:fill-shade-dark-disabled`,
text: `font-bold aria-disabled:bg-shade-dark-disabled aria-disabled:text-shade-white-disabled aria-disabled:fill-shade-dark-disabled`,
}[variant],
{
subtle: {
Expand Down Expand Up @@ -251,17 +192,15 @@ export const Button: ButtonComponent = forwardRef(
className,
])}
// FIXME: Avoid unintended type error, maybe MUI's problem?
{...(rest as MuiButtonProps<As>)}
onClick={(e: MouseEvent<HTMLElementTagNameMap[As extends keyof HTMLElementTagNameMap ? As : 'button']>) => {
{...rest}
onClick={(e) => {
if (loading) {
return;
}
onClick?.(e);
}}
>
{startIcon}
{children}
{endIcon}
{loading && (
<div className={fsx(`absolute inset-0 grid h-full w-full place-items-center`)}>
<Loader
Expand All @@ -271,7 +210,7 @@ export const Button: ButtonComponent = forwardRef(
/>
</div>
)}
</MuiButton>
</Component>
);
},
);
2 changes: 1 addition & 1 deletion packages/for-ui/src/system/componentType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type AsProps<As extends ElementType> = {
export type ComponentPropsWithAs<Props extends object, As extends ElementType> = AsProps<As> &
Props &
RefProps<As> &
PreservedOmit<ComponentPropsWithoutRef<As>, keyof (Props & AsProps<As> & RefProps<As>)>;
NoInfer<PreservedOmit<ComponentPropsWithoutRef<As>, keyof (Props & AsProps<As> & RefProps<As>)>>;

export type ElementTypeToHTMLElement<Element extends ElementType> = Element extends keyof HTMLElementTagNameMap
? HTMLElementTagNameMap[Element]
Expand Down