From d6cc8c90db46d43ceebc992e96a4b56a8bedd3df Mon Sep 17 00:00:00 2001 From: touyou <465697+touyou@users.noreply.github.com> Date: Thu, 7 Aug 2025 16:57:00 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=92=84=20UI=E3=82=84=E3=82=B9?= =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=AB=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=AE=E8=BF=BD=E5=8A=A0=E3=82=84=E6=9B=B4=E6=96=B0:=20IconB?= =?UTF-8?q?utton=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=81=AE=E3=82=B9=E3=83=88=E3=83=BC=E3=83=AA=E3=83=BC?= =?UTF-8?q?=E3=82=92=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA?= =?UTF-8?q?=E3=83=B3=E3=82=B0=E3=81=97=E3=80=81args=E3=82=92=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E3=81=97=E3=81=A6=E3=83=97=E3=83=AD=E3=83=91=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=82=92=E9=81=A9=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IconButtonのストーリーでargsを使用してプロパティを適用 - アイコンボタンの説明を更新 - スタイルの改善により、カーソルをポインタに設定 --- .../ui/icon-button/index.stories.tsx | 96 ++++++++++--------- src/components/ui/icon-button/index.tsx | 23 ++++- src/components/ui/icon-button/item.json | 2 +- 3 files changed, 71 insertions(+), 50 deletions(-) diff --git a/src/components/ui/icon-button/index.stories.tsx b/src/components/ui/icon-button/index.stories.tsx index 7c2faf73..b2c04de2 100644 --- a/src/components/ui/icon-button/index.stories.tsx +++ b/src/components/ui/icon-button/index.stories.tsx @@ -59,53 +59,59 @@ export const Disabled: Story = { }, }; -export const Variant = () => { - return ( -
- - solid - - - outline - - - ghost - -
- ); +export const Variant: Story = { + render: args => { + return ( +
+ + solid + + + outline + + + ghost + +
+ ); + }, }; -export const Size = () => { - return ( -
- - extra small - - - small - - - medium - - - large - -
- ); +export const Size: Story = { + render: args => { + return ( +
+ + extra small + + + small + + + medium + + + large + +
+ ); + }, }; -export const Theme = () => { - return ( -
- - primary - - - secondary - - - negative - -
- ); +export const Theme: Story = { + render: args => { + return ( +
+ + primary + + + secondary + + + negative + +
+ ); + }, }; diff --git a/src/components/ui/icon-button/index.tsx b/src/components/ui/icon-button/index.tsx index f19d8334..30634b51 100644 --- a/src/components/ui/icon-button/index.tsx +++ b/src/components/ui/icon-button/index.tsx @@ -7,7 +7,7 @@ import { Icon } from "@/components/ui/icon"; import { Spinner } from "@/components/ui/spinner"; const iconButtonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 relative", + "inline-flex items-center justify-center whitespace-nowrap rounded-md ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[var(--color-ring-normal)] focus-visible:ring-offset-2 relative cursor-pointer antialiased", { variants: { variant: { @@ -258,9 +258,24 @@ const iconButtonVariants = cva( } ); +type IconButtonVariants = VariantProps; export interface IconButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { + extends React.ButtonHTMLAttributes { + /** + * アイコンボタンのバリエーション + * en: Variation of the icon button + */ + variant?: IconButtonVariants["variant"]; + /** + * アイコンボタンのサイズ + * en: Size of the icon button + */ + size?: IconButtonVariants["size"]; + /** + * アイコンボタンのテーマ + * en: Theme of the icon button + */ + theme?: IconButtonVariants["theme"]; /** * ボタンを別コンポーネントの子としてレンダリングするか * en: Whether to render the button as a child component @@ -351,7 +366,7 @@ const IconButton = React.forwardRef( > {isLoading ? ( <> - + ) : ( diff --git a/src/components/ui/icon-button/item.json b/src/components/ui/icon-button/item.json index 65a78de6..ada381a1 100644 --- a/src/components/ui/icon-button/item.json +++ b/src/components/ui/icon-button/item.json @@ -3,7 +3,7 @@ "name": "icon-button", "type": "registry:component", "title": "IconButton", - "description": "アイコンボタンはアイコンのみを表示するコンパクトなボタンコンポーネントです。", + "description": "アイコンボタンはフォームの送信、ダイアログの展開、アクションのキャンセル、削除の実行など、アクションやイベントのトリガーとして使用するコンポーネントです。", "files": [ { "path": "src/components/ui/icon-button/index.tsx", From c4ed865e2a30b8f6b6c98b2ac7408160d69bd76f Mon Sep 17 00:00:00 2001 From: touyou <465697+touyou@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:50:30 +0900 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=92=84=20UI=E3=82=84=E3=82=B9?= =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=AB=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=AE=E8=BF=BD=E5=8A=A0=E3=82=84=E6=9B=B4=E6=96=B0:=20IconB?= =?UTF-8?q?utton=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=81=AE=E3=83=86=E3=83=BC=E3=83=9E=E3=82=92=E3=80=8C?= =?UTF-8?q?secondary=E3=80=8D=E3=81=8B=E3=82=89=E3=80=8Cneutral=E3=80=8D?= =?UTF-8?q?=E3=81=AB=E5=A4=89=E6=9B=B4=E3=81=97=E3=80=81=E9=96=A2=E9=80=A3?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=B9=E3=82=BF=E3=82=A4=E3=83=AB=E3=82=92?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IconButtonのテーマオプションを「neutral」に変更 - スタイルのクラス名を「neutral」に合わせて更新 - ストーリー内のテーマに関するテストを修正 --- .../ui/icon-button/index.stories.tsx | 6 +- src/components/ui/icon-button/index.tsx | 112 +++++++++++------- 2 files changed, 70 insertions(+), 48 deletions(-) diff --git a/src/components/ui/icon-button/index.stories.tsx b/src/components/ui/icon-button/index.stories.tsx index b2c04de2..9d468af2 100644 --- a/src/components/ui/icon-button/index.stories.tsx +++ b/src/components/ui/icon-button/index.stories.tsx @@ -21,7 +21,7 @@ const meta = { }, theme: { control: "select", - options: ["primary", "secondary", "negative"], + options: ["primary", "neutral", "negative"], }, isLoading: { control: "boolean", @@ -105,8 +105,8 @@ export const Theme: Story = { primary - - secondary + + neutral negative diff --git a/src/components/ui/icon-button/index.tsx b/src/components/ui/icon-button/index.tsx index 30634b51..24c1f7a5 100644 --- a/src/components/ui/icon-button/index.tsx +++ b/src/components/ui/icon-button/index.tsx @@ -7,28 +7,32 @@ import { Icon } from "@/components/ui/icon"; import { Spinner } from "@/components/ui/spinner"; const iconButtonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[var(--color-ring-normal)] focus-visible:ring-offset-2 relative cursor-pointer antialiased", + [ + "inline-flex items-center justify-center whitespace-nowrap rounded-action", + "ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[var(--color-ring-normal)] focus-visible:ring-offset-2", + "relative cursor-pointer antialiased", + ].join(" "), { variants: { variant: { - solid: "border shadow-sm", - outline: "border shadow-sm", + solid: "border", + outline: "border", ghost: "", }, size: { - xs: "w-6 h-6 p-0.5 character-1-bold-pro", - sm: "w-8 h-8 p-1 character-2-bold-pro", - md: "w-10 h-10 p-2 character-3-bold-pro", - lg: "w-12 h-12 p-2.5 character-4-bold-pro", + xs: "w-6 h-6 p-1", + sm: "w-8 h-8 p-1.5", + md: "w-10 h-10 p-2", + lg: "w-12 h-12 p-2", }, theme: { primary: "", - secondary: "", + neutral: "", negative: "", }, isLoading: { true: "cursor-not-allowed", - false: "gap-2", + false: "", }, isDisabled: { true: "cursor-not-allowed", @@ -42,8 +46,11 @@ const iconButtonVariants = cva( theme: "primary", isLoading: false, isDisabled: false, - className: - "bg-primary-500 text-white border-primary-600 hover:bg-primary-600 hover:border-primary-700 active:bg-primary-700 active:border-primary-800 active:shadow-sm", + className: [ + "bg-primary-500 text-white border-primary-600", + "hover:bg-primary-600 hover:border-primary-700", + "active:bg-primary-700 active:border-primary-800", + ].join(" "), }, { variant: "solid", @@ -53,21 +60,24 @@ const iconButtonVariants = cva( className: "bg-primary-500 text-white border-primary-600", }, - // Solid Secondary バリアント + // Solid Neutral バリアント { variant: "solid", - theme: "secondary", + theme: "neutral", isLoading: false, isDisabled: false, - className: - "bg-secondary-500 text-white border-secondary-600 hover:bg-secondary-600 hover:border-secondary-700 active:bg-secondary-700 active:border-secondary-800 active:shadow-sm", + className: [ + "bg-neutral-500 text-white border-neutral-600", + "hover:bg-neutral-600 hover:border-neutral-700", + "active:bg-neutral-700 active:border-neutral-800", + ].join(" "), }, { variant: "solid", - theme: "secondary", + theme: "neutral", isLoading: true, isDisabled: false, - className: "bg-secondary-500 text-white border-secondary-600", + className: "bg-neutral-500 text-white border-neutral-600", }, // Solid Negative バリアント @@ -76,8 +86,11 @@ const iconButtonVariants = cva( theme: "negative", isLoading: false, isDisabled: false, - className: - "bg-negative-500 text-white border-negative-600 hover:bg-negative-600 hover:border-negative-700 active:bg-negative-700 active:border-negative-800 active:shadow-sm", + className: [ + "bg-negative-500 text-white border-negative-600", + "hover:bg-negative-600 hover:border-negative-700", + "active:bg-negative-700 active:border-negative-800", + ].join(" "), }, { variant: "solid", @@ -92,31 +105,37 @@ const iconButtonVariants = cva( variant: "outline", theme: "primary", isLoading: false, - className: - "bg-white text-primary-500 border-primary-500 shadow-sm hover:bg-primary-50 active:bg-primary-100", + className: [ + "bg-white text-primary-500 border-primary-300", + "hover:bg-primary-50", + "active:bg-primary-100 active:border-primary-400 active:text-primary-600", + ].join(" "), }, { variant: "outline", theme: "primary", isLoading: true, isDisabled: false, - className: "bg-white text-primary-500 border-primary-500 shadow-sm", + className: "bg-white text-primary-500 border-primary-300", }, - // Outline Secondary バリアント + // Outline Neutral バリアント { variant: "outline", - theme: "secondary", + theme: "neutral", isLoading: false, - className: - "bg-white text-secondary-700 border-secondary-500 shadow-sm hover:bg-secondary-50 active:bg-secondary-100", + className: [ + "bg-white text-neutral-700 border-neutral-300", + "hover:bg-neutral-50", + "active:bg-neutral-100", + ].join(" "), }, { variant: "outline", - theme: "secondary", + theme: "neutral", isLoading: true, isDisabled: false, - className: "bg-white text-secondary-700 border-secondary-500 shadow-sm", + className: "bg-white text-neutral-700 border-neutral-300", }, // Outline Negative バリアント @@ -125,15 +144,18 @@ const iconButtonVariants = cva( theme: "negative", isLoading: false, isDisabled: false, - className: - "bg-white text-negative-500 border-negative-500 shadow-sm hover:bg-negative-50 active:bg-negative-100", + className: [ + "bg-white text-negative-500 border-negative-300", + "hover:bg-negative-50", + "active:bg-negative-100 active:border-negative-400 active:text-negative-600", + ].join(" "), }, { variant: "outline", theme: "negative", isLoading: true, isDisabled: false, - className: "bg-white text-negative-500 border-negative-500 shadow-sm", + className: "bg-white text-negative-500 border-negative-300", }, // Ghost Primary バリアント @@ -142,7 +164,8 @@ const iconButtonVariants = cva( theme: "primary", isLoading: false, isDisabled: false, - className: "text-primary-500 hover:bg-primary-50 active:bg-primary-100", + className: + "text-primary-500 hover:bg-primary-50 active:bg-primary-100 active:text-primary-600", }, { variant: "ghost", @@ -152,21 +175,20 @@ const iconButtonVariants = cva( className: "text-primary-500", }, - // Ghost Secondary バリアント + // Ghost neutral バリアント { variant: "ghost", - theme: "secondary", + theme: "neutral", isLoading: false, isDisabled: false, - className: - "text-secondary-700 hover:bg-secondary-50 active:bg-secondary-100", + className: "text-neutral-700 hover:bg-neutral-50 active:bg-neutral-100", }, { variant: "ghost", - theme: "secondary", + theme: "neutral", isLoading: true, isDisabled: false, - className: "text-secondary-700", + className: "text-neutral-700", }, // Ghost Negative バリアント @@ -176,7 +198,7 @@ const iconButtonVariants = cva( isLoading: false, isDisabled: false, className: - "text-negative-500 hover:bg-negative-100 active:bg-negative-100", + "text-negative-500 hover:bg-negative-50 active:bg-negative-100 active:text-negative-600", }, { variant: "ghost", @@ -196,10 +218,10 @@ const iconButtonVariants = cva( }, { variant: "solid", - theme: "secondary", + theme: "neutral", isDisabled: true, className: - "disabled:bg-secondary-200 disabled:text-white disabled:border-none", + "disabled:bg-neutral-200 disabled:text-white disabled:border-none", }, { variant: "solid", @@ -217,10 +239,10 @@ const iconButtonVariants = cva( }, { variant: "outline", - theme: "secondary", + theme: "neutral", isDisabled: true, className: - "disabled:bg-white disabled:text-secondary-200 disabled:border-secondary-100", + "disabled:bg-white disabled:text-neutral-200 disabled:border-neutral-100", }, { variant: "outline", @@ -237,9 +259,9 @@ const iconButtonVariants = cva( }, { variant: "ghost", - theme: "secondary", + theme: "neutral", isDisabled: true, - className: "disabled:text-secondary-200", + className: "disabled:text-neutral-200", }, { variant: "ghost", From e0dff15b37760cf497e31581feeabef0ec27eef5 Mon Sep 17 00:00:00 2001 From: touyou <465697+touyou@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:51:38 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=92=84=20UI=E3=82=84=E3=82=B9?= =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=AB=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=AE=E8=BF=BD=E5=8A=A0=E3=82=84=E6=9B=B4=E6=96=B0:=20IconB?= =?UTF-8?q?utton=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=81=AEvariant=E3=82=AA=E3=83=97=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IconButtonコンポーネントのvariantオプションから「outline-solid」を削除し、「outline」を追加 --- src/components/ui/icon-button/index.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/icon-button/index.stories.tsx b/src/components/ui/icon-button/index.stories.tsx index 9d468af2..17fb4c18 100644 --- a/src/components/ui/icon-button/index.stories.tsx +++ b/src/components/ui/icon-button/index.stories.tsx @@ -13,7 +13,7 @@ const meta = { argTypes: { variant: { control: "select", - options: ["solid", "outline-solid", "ghost"], + options: ["solid", "outline", "ghost"], }, size: { control: "select", From 6c87e683d2075c3f15d0287dc2c28154d031d023 Mon Sep 17 00:00:00 2001 From: touyou <465697+touyou@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:59:59 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=92=84=20UI=E3=82=84=E3=82=B9?= =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=AB=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=AE=E8=BF=BD=E5=8A=A0=E3=82=84=E6=9B=B4=E6=96=B0:=20IconB?= =?UTF-8?q?utton=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=81=ABneutral=E3=83=86=E3=83=BC=E3=83=9E=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IconButtonコンポーネントにneutralテーマを追加 - isDisabledプロパティの処理を改善 - IconButtonのテストケースを新規作成 --- src/components/ui/icon-button/index.figma.tsx | 1 + src/components/ui/icon-button/index.test.tsx | 429 ++++++++++++++++++ src/components/ui/icon-button/index.tsx | 3 +- 3 files changed, 432 insertions(+), 1 deletion(-) create mode 100644 src/components/ui/icon-button/index.test.tsx diff --git a/src/components/ui/icon-button/index.figma.tsx b/src/components/ui/icon-button/index.figma.tsx index c7cca12c..f4878bea 100644 --- a/src/components/ui/icon-button/index.figma.tsx +++ b/src/components/ui/icon-button/index.figma.tsx @@ -38,6 +38,7 @@ figma.connect( theme: figma.enum("theme", { primary: "primary", negative: "negative", + neutral: "neutral", }), // No matching props could be found for these Figma properties: icon: figma.instance("icon").getProps<{ diff --git a/src/components/ui/icon-button/index.test.tsx b/src/components/ui/icon-button/index.test.tsx new file mode 100644 index 00000000..058a559c --- /dev/null +++ b/src/components/ui/icon-button/index.test.tsx @@ -0,0 +1,429 @@ +/** + * @jest-environment jsdom + */ + +import React from 'react' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { TestContainer, EventHelpers, A11yHelpers, StyleHelpers } from '@/test/helpers' +import { IconButton } from './index' + +describe('IconButton', () => { + let testContainer: TestContainer + + beforeEach(() => { + testContainer = new TestContainer() + testContainer.setup() + }) + + afterEach(() => { + testContainer.cleanup() + }) + + describe('Basic Rendering', () => { + it('renders with default props', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(button).toBeDefined() + expect(button.tagName).toBe('BUTTON') + expect(button.type).toBe('button') + }) + + it('renders the correct icon', () => { + testContainer.render() + const container = testContainer.getContainer() + const iconSpan = container.querySelector('span[aria-hidden="true"]') + + expect(iconSpan).toBeDefined() + expect(iconSpan?.textContent).toBe('edit') + }) + + it('applies default variant, size, and theme classes', () => { + testContainer.render() + const button = testContainer.queryButton() + + // Default: variant="solid", size="md", theme="primary" + expect(StyleHelpers.hasClass(button, 'w-10')).toBe(true) + expect(StyleHelpers.hasClass(button, 'h-10')).toBe(true) + expect(StyleHelpers.hasClass(button, 'bg-primary-500')).toBe(true) + expect(StyleHelpers.hasClass(button, 'text-white')).toBe(true) + expect(StyleHelpers.hasClass(button, 'border-primary-600')).toBe(true) + }) + + it('forwards custom className', () => { + const customClass = 'my-custom-class' + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, customClass)).toBe(true) + }) + + it('forwards arbitrary props', () => { + testContainer.render() + const button = testContainer.queryByTestId('custom-button') + + expect(button).toBeDefined() + }) + }) + + describe('Variant Styling', () => { + describe('solid variant', () => { + it('applies solid primary classes', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'bg-primary-500')).toBe(true) + expect(StyleHelpers.hasClass(button, 'text-white')).toBe(true) + expect(StyleHelpers.hasClass(button, 'border-primary-600')).toBe(true) + }) + + it('applies solid neutral classes', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'bg-neutral-500')).toBe(true) + expect(StyleHelpers.hasClass(button, 'text-white')).toBe(true) + expect(StyleHelpers.hasClass(button, 'border-neutral-600')).toBe(true) + }) + + it('applies solid negative classes', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'bg-negative-500')).toBe(true) + expect(StyleHelpers.hasClass(button, 'text-white')).toBe(true) + expect(StyleHelpers.hasClass(button, 'border-negative-600')).toBe(true) + }) + }) + + describe('outline variant', () => { + it('applies outline primary classes', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'bg-white')).toBe(true) + expect(StyleHelpers.hasClass(button, 'text-primary-500')).toBe(true) + expect(StyleHelpers.hasClass(button, 'border-primary-300')).toBe(true) + }) + + it('applies outline neutral classes', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'bg-white')).toBe(true) + expect(StyleHelpers.hasClass(button, 'text-neutral-700')).toBe(true) + expect(StyleHelpers.hasClass(button, 'border-neutral-300')).toBe(true) + }) + + it('applies outline negative classes', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'bg-white')).toBe(true) + expect(StyleHelpers.hasClass(button, 'text-negative-500')).toBe(true) + expect(StyleHelpers.hasClass(button, 'border-negative-300')).toBe(true) + }) + }) + + describe('ghost variant', () => { + it('applies ghost primary classes', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'text-primary-500')).toBe(true) + // Ghost variant doesn't have background or border by default + expect(StyleHelpers.hasClass(button, 'bg-primary-500')).toBe(false) + }) + + it('applies ghost neutral classes', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'text-neutral-700')).toBe(true) + }) + + it('applies ghost negative classes', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'text-negative-500')).toBe(true) + }) + }) + }) + + describe('Size Variants', () => { + it('applies extra small size classes', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'w-6')).toBe(true) + expect(StyleHelpers.hasClass(button, 'h-6')).toBe(true) + expect(StyleHelpers.hasClass(button, 'p-1')).toBe(true) + }) + + it('applies small size classes', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'w-8')).toBe(true) + expect(StyleHelpers.hasClass(button, 'h-8')).toBe(true) + expect(StyleHelpers.hasClass(button, 'p-1.5')).toBe(true) + }) + + it('applies medium size classes (default)', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'w-10')).toBe(true) + expect(StyleHelpers.hasClass(button, 'h-10')).toBe(true) + expect(StyleHelpers.hasClass(button, 'p-2')).toBe(true) + }) + + it('applies large size classes', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'w-12')).toBe(true) + expect(StyleHelpers.hasClass(button, 'h-12')).toBe(true) + expect(StyleHelpers.hasClass(button, 'p-2')).toBe(true) + }) + }) + + describe('Loading State', () => { + it('shows spinner when loading', () => { + testContainer.render() + const container = testContainer.getContainer() + + // Spinner should be present + const spinner = container.querySelector('[data-testid="spinner"], .animate-spin') + expect(spinner).toBeDefined() + + // Icon should not be present + const iconSpan = container.querySelector('span[aria-hidden="true"]') + expect(iconSpan?.textContent).not.toBe('plus') + }) + + it('applies loading cursor style', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'cursor-not-allowed')).toBe(true) + }) + + it('is disabled when loading', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(button.disabled).toBe(true) + }) + + it('does not trigger click events when loading', () => { + const handleClick = vi.fn() + testContainer.render() + const button = testContainer.queryButton() + + EventHelpers.click(button) + + expect(handleClick).not.toHaveBeenCalled() + }) + }) + + describe('Disabled State', () => { + it('is disabled with isDisabled prop', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(button.disabled).toBe(true) + }) + + it('is disabled with disabled prop', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(button.disabled).toBe(true) + }) + + it('applies disabled cursor style', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'cursor-not-allowed')).toBe(true) + }) + + it('applies disabled styling for solid variant', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'disabled:bg-primary-200')).toBe(true) + expect(StyleHelpers.hasClass(button, 'disabled:text-white')).toBe(true) + expect(StyleHelpers.hasClass(button, 'disabled:border-none')).toBe(true) + }) + + it('applies disabled styling for outline variant', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'disabled:bg-white')).toBe(true) + expect(StyleHelpers.hasClass(button, 'disabled:text-primary-200')).toBe(true) + expect(StyleHelpers.hasClass(button, 'disabled:border-primary-100')).toBe(true) + }) + + it('applies disabled styling for ghost variant', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'disabled:text-primary-200')).toBe(true) + }) + + it('does not trigger click events when disabled', () => { + const handleClick = vi.fn() + testContainer.render() + const button = testContainer.queryButton() + + EventHelpers.click(button) + + expect(handleClick).not.toHaveBeenCalled() + }) + }) + + describe('User Interaction', () => { + it('handles click events properly', () => { + const handleClick = vi.fn() + testContainer.render() + const button = testContainer.queryButton() + + EventHelpers.click(button) + + expect(handleClick).toHaveBeenCalledTimes(1) + }) + + it('handles keyboard events', () => { + const handleKeyDown = vi.fn() + testContainer.render() + const button = testContainer.queryButton() + + EventHelpers.keyDown(button, 'Enter') + + expect(handleKeyDown).toHaveBeenCalledTimes(1) + }) + + it('handles focus and blur events', () => { + const handleFocus = vi.fn() + const handleBlur = vi.fn() + testContainer.render() + const button = testContainer.queryButton() + + EventHelpers.focus(button) + expect(handleFocus).toHaveBeenCalledTimes(1) + + EventHelpers.blur(button) + expect(handleBlur).toHaveBeenCalledTimes(1) + }) + }) + + describe('AsChild Functionality', () => { + it('renders as a Slot when asChild is true', () => { + testContainer.render( + +
Custom element
+
+ ) + const container = testContainer.getContainer() + + // When asChild is true, Slot merges props with the child element + // The div should still exist but with the button's props merged + const customElement = container.querySelector('[data-testid="custom-element"]') + expect(customElement).toBeDefined() + + // Check if the element exists and has some content + if (customElement) { + expect(customElement.textContent).toContain('Custom element') + } + }) + }) + + describe('Accessibility', () => { + it('has proper button semantics', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(button.tagName).toBe('BUTTON') + expect(button.type).toBe('button') + }) + + it('supports aria-label', () => { + const ariaLabel = 'Add item' + testContainer.render() + const button = testContainer.queryButton() + + expect(A11yHelpers.hasAriaLabel(button, ariaLabel)).toBe(true) + }) + + it('has proper focus styles', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(StyleHelpers.hasClass(button, 'focus-visible:outline-hidden')).toBe(true) + expect(StyleHelpers.hasClass(button, 'focus-visible:ring-2')).toBe(true) + }) + + it('properly hides icon from screen readers', () => { + testContainer.render() + const container = testContainer.getContainer() + const iconSpan = container.querySelector('span[aria-hidden="true"]') + + expect(iconSpan?.getAttribute('aria-hidden')).toBe('true') + }) + }) + + describe('Edge Cases', () => { + it('handles empty icon gracefully', () => { + testContainer.render() + const container = testContainer.getContainer() + const iconSpan = container.querySelector('span[aria-hidden="true"]') + + expect(iconSpan?.textContent).toBe('') + }) + + it('handles both isDisabled and isLoading being true', () => { + testContainer.render() + const button = testContainer.queryButton() + + expect(button.disabled).toBe(true) + expect(StyleHelpers.hasClass(button, 'cursor-not-allowed')).toBe(true) + }) + + it('prioritizes isLoading over normal icon display', () => { + testContainer.render() + const container = testContainer.getContainer() + + // Should show spinner instead of icon + const spinner = container.querySelector('[data-testid="spinner"], .animate-spin') + expect(spinner).toBeDefined() + + // Icon should not be the original icon + const iconSpan = container.querySelector('span[aria-hidden="true"]') + expect(iconSpan?.textContent).not.toBe('plus') + }) + + it('maintains display name for debugging', () => { + expect(IconButton.displayName).toBe('IconButton') + }) + }) + + describe('Ref Forwarding', () => { + it('forwards ref to button element', () => { + const ref = { current: null as HTMLButtonElement | null } + + testContainer.render( + + ) + + expect(ref.current).toBeInstanceOf(HTMLButtonElement) + expect(ref.current?.tagName).toBe('BUTTON') + }) + }) +}) diff --git a/src/components/ui/icon-button/index.tsx b/src/components/ui/icon-button/index.tsx index 24c1f7a5..1b6e21d5 100644 --- a/src/components/ui/icon-button/index.tsx +++ b/src/components/ui/icon-button/index.tsx @@ -352,7 +352,7 @@ const IconButton = React.forwardRef( ref ) => { // disabled状態の管理(isDisabled、disabled、またはisLoadingがtrueの場合) - const isIconButtonDisabled = isLoading || isDisabled; + const isIconButtonDisabled = isLoading || isDisabled || disabled; const Comp = asChild ? SlotPrimitive.Slot : "button"; @@ -372,6 +372,7 @@ const IconButton = React.forwardRef( return ( Date: Thu, 7 Aug 2025 18:02:15 +0900 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=92=84=20test:=20IconButton=E3=82=B3?= =?UTF-8?q?=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=AE?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0=E3=83=BB?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IconButtonコンポーネントの基本レンダリングテストを追加 - バリアントスタイリングのテストを追加 - サイズバリアントのテストを追加 - ローディング状態のテストを追加 - 無効状態のテストを追加 - ユーザーインタラクションのテストを追加 - アクセシビリティのテストを追加 - エッジケースのテストを追加 - Refフォワーディングのテストを追加 --- src/components/ui/icon-button/index.test.tsx | 794 ++++++++++--------- 1 file changed, 422 insertions(+), 372 deletions(-) diff --git a/src/components/ui/icon-button/index.test.tsx b/src/components/ui/icon-button/index.test.tsx index 058a559c..46fe62ab 100644 --- a/src/components/ui/icon-button/index.test.tsx +++ b/src/components/ui/icon-button/index.test.tsx @@ -2,428 +2,478 @@ * @jest-environment jsdom */ -import React from 'react' -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' -import { TestContainer, EventHelpers, A11yHelpers, StyleHelpers } from '@/test/helpers' -import { IconButton } from './index' - -describe('IconButton', () => { - let testContainer: TestContainer +import React from "react"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { + TestContainer, + EventHelpers, + A11yHelpers, + StyleHelpers, +} from "@/test/helpers"; +import { IconButton } from "./index"; + +describe("IconButton", () => { + let testContainer: TestContainer; beforeEach(() => { - testContainer = new TestContainer() - testContainer.setup() - }) + testContainer = new TestContainer(); + testContainer.setup(); + }); afterEach(() => { - testContainer.cleanup() - }) + testContainer.cleanup(); + }); - describe('Basic Rendering', () => { - it('renders with default props', () => { - testContainer.render() - const button = testContainer.queryButton() + describe("Basic Rendering", () => { + it("renders with default props", () => { + testContainer.render(); + const button = testContainer.queryButton(); - expect(button).toBeDefined() - expect(button.tagName).toBe('BUTTON') - expect(button.type).toBe('button') - }) + expect(button).toBeDefined(); + expect(button.tagName).toBe("BUTTON"); + expect(button.type).toBe("button"); + }); - it('renders the correct icon', () => { - testContainer.render() - const container = testContainer.getContainer() - const iconSpan = container.querySelector('span[aria-hidden="true"]') + it("renders the correct icon", () => { + testContainer.render(); + const container = testContainer.getContainer(); + const iconSpan = container.querySelector('span[aria-hidden="true"]'); - expect(iconSpan).toBeDefined() - expect(iconSpan?.textContent).toBe('edit') - }) + expect(iconSpan).toBeDefined(); + expect(iconSpan?.textContent).toBe("edit"); + }); - it('applies default variant, size, and theme classes', () => { - testContainer.render() - const button = testContainer.queryButton() + it("applies default variant, size, and theme classes", () => { + testContainer.render(); + const button = testContainer.queryButton(); // Default: variant="solid", size="md", theme="primary" - expect(StyleHelpers.hasClass(button, 'w-10')).toBe(true) - expect(StyleHelpers.hasClass(button, 'h-10')).toBe(true) - expect(StyleHelpers.hasClass(button, 'bg-primary-500')).toBe(true) - expect(StyleHelpers.hasClass(button, 'text-white')).toBe(true) - expect(StyleHelpers.hasClass(button, 'border-primary-600')).toBe(true) - }) - - it('forwards custom className', () => { - const customClass = 'my-custom-class' - testContainer.render() - const button = testContainer.queryButton() - - expect(StyleHelpers.hasClass(button, customClass)).toBe(true) - }) - - it('forwards arbitrary props', () => { - testContainer.render() - const button = testContainer.queryByTestId('custom-button') - - expect(button).toBeDefined() - }) - }) - - describe('Variant Styling', () => { - describe('solid variant', () => { - it('applies solid primary classes', () => { - testContainer.render() - const button = testContainer.queryButton() - - expect(StyleHelpers.hasClass(button, 'bg-primary-500')).toBe(true) - expect(StyleHelpers.hasClass(button, 'text-white')).toBe(true) - expect(StyleHelpers.hasClass(button, 'border-primary-600')).toBe(true) - }) - - it('applies solid neutral classes', () => { - testContainer.render() - const button = testContainer.queryButton() - - expect(StyleHelpers.hasClass(button, 'bg-neutral-500')).toBe(true) - expect(StyleHelpers.hasClass(button, 'text-white')).toBe(true) - expect(StyleHelpers.hasClass(button, 'border-neutral-600')).toBe(true) - }) - - it('applies solid negative classes', () => { - testContainer.render() - const button = testContainer.queryButton() - - expect(StyleHelpers.hasClass(button, 'bg-negative-500')).toBe(true) - expect(StyleHelpers.hasClass(button, 'text-white')).toBe(true) - expect(StyleHelpers.hasClass(button, 'border-negative-600')).toBe(true) - }) - }) - - describe('outline variant', () => { - it('applies outline primary classes', () => { - testContainer.render() - const button = testContainer.queryButton() - - expect(StyleHelpers.hasClass(button, 'bg-white')).toBe(true) - expect(StyleHelpers.hasClass(button, 'text-primary-500')).toBe(true) - expect(StyleHelpers.hasClass(button, 'border-primary-300')).toBe(true) - }) - - it('applies outline neutral classes', () => { - testContainer.render() - const button = testContainer.queryButton() - - expect(StyleHelpers.hasClass(button, 'bg-white')).toBe(true) - expect(StyleHelpers.hasClass(button, 'text-neutral-700')).toBe(true) - expect(StyleHelpers.hasClass(button, 'border-neutral-300')).toBe(true) - }) - - it('applies outline negative classes', () => { - testContainer.render() - const button = testContainer.queryButton() - - expect(StyleHelpers.hasClass(button, 'bg-white')).toBe(true) - expect(StyleHelpers.hasClass(button, 'text-negative-500')).toBe(true) - expect(StyleHelpers.hasClass(button, 'border-negative-300')).toBe(true) - }) - }) - - describe('ghost variant', () => { - it('applies ghost primary classes', () => { - testContainer.render() - const button = testContainer.queryButton() - - expect(StyleHelpers.hasClass(button, 'text-primary-500')).toBe(true) + expect(StyleHelpers.hasClass(button, "w-10")).toBe(true); + expect(StyleHelpers.hasClass(button, "h-10")).toBe(true); + expect(StyleHelpers.hasClass(button, "bg-primary-500")).toBe(true); + expect(StyleHelpers.hasClass(button, "text-white")).toBe(true); + expect(StyleHelpers.hasClass(button, "border-primary-600")).toBe(true); + }); + + it("forwards custom className", () => { + const customClass = "my-custom-class"; + testContainer.render(); + const button = testContainer.queryButton(); + + expect(StyleHelpers.hasClass(button, customClass)).toBe(true); + }); + + it("forwards arbitrary props", () => { + testContainer.render( + + ); + const button = testContainer.queryByTestId("custom-button"); + + expect(button).toBeDefined(); + }); + }); + + describe("Variant Styling", () => { + describe("solid variant", () => { + it("applies solid primary classes", () => { + testContainer.render( + + ); + const button = testContainer.queryButton(); + + expect(StyleHelpers.hasClass(button, "bg-primary-500")).toBe(true); + expect(StyleHelpers.hasClass(button, "text-white")).toBe(true); + expect(StyleHelpers.hasClass(button, "border-primary-600")).toBe(true); + }); + + it("applies solid neutral classes", () => { + testContainer.render( + + ); + const button = testContainer.queryButton(); + + expect(StyleHelpers.hasClass(button, "bg-neutral-500")).toBe(true); + expect(StyleHelpers.hasClass(button, "text-white")).toBe(true); + expect(StyleHelpers.hasClass(button, "border-neutral-600")).toBe(true); + }); + + it("applies solid negative classes", () => { + testContainer.render( + + ); + const button = testContainer.queryButton(); + + expect(StyleHelpers.hasClass(button, "bg-negative-500")).toBe(true); + expect(StyleHelpers.hasClass(button, "text-white")).toBe(true); + expect(StyleHelpers.hasClass(button, "border-negative-600")).toBe(true); + }); + }); + + describe("outline variant", () => { + it("applies outline primary classes", () => { + testContainer.render( + + ); + const button = testContainer.queryButton(); + + expect(StyleHelpers.hasClass(button, "bg-white")).toBe(true); + expect(StyleHelpers.hasClass(button, "text-primary-500")).toBe(true); + expect(StyleHelpers.hasClass(button, "border-primary-300")).toBe(true); + }); + + it("applies outline neutral classes", () => { + testContainer.render( + + ); + const button = testContainer.queryButton(); + + expect(StyleHelpers.hasClass(button, "bg-white")).toBe(true); + expect(StyleHelpers.hasClass(button, "text-neutral-700")).toBe(true); + expect(StyleHelpers.hasClass(button, "border-neutral-300")).toBe(true); + }); + + it("applies outline negative classes", () => { + testContainer.render( + + ); + const button = testContainer.queryButton(); + + expect(StyleHelpers.hasClass(button, "bg-white")).toBe(true); + expect(StyleHelpers.hasClass(button, "text-negative-500")).toBe(true); + expect(StyleHelpers.hasClass(button, "border-negative-300")).toBe(true); + }); + }); + + describe("ghost variant", () => { + it("applies ghost primary classes", () => { + testContainer.render( + + ); + const button = testContainer.queryButton(); + + expect(StyleHelpers.hasClass(button, "text-primary-500")).toBe(true); // Ghost variant doesn't have background or border by default - expect(StyleHelpers.hasClass(button, 'bg-primary-500')).toBe(false) - }) - - it('applies ghost neutral classes', () => { - testContainer.render() - const button = testContainer.queryButton() - - expect(StyleHelpers.hasClass(button, 'text-neutral-700')).toBe(true) - }) - - it('applies ghost negative classes', () => { - testContainer.render() - const button = testContainer.queryButton() - - expect(StyleHelpers.hasClass(button, 'text-negative-500')).toBe(true) - }) - }) - }) - - describe('Size Variants', () => { - it('applies extra small size classes', () => { - testContainer.render() - const button = testContainer.queryButton() - - expect(StyleHelpers.hasClass(button, 'w-6')).toBe(true) - expect(StyleHelpers.hasClass(button, 'h-6')).toBe(true) - expect(StyleHelpers.hasClass(button, 'p-1')).toBe(true) - }) - - it('applies small size classes', () => { - testContainer.render() - const button = testContainer.queryButton() - - expect(StyleHelpers.hasClass(button, 'w-8')).toBe(true) - expect(StyleHelpers.hasClass(button, 'h-8')).toBe(true) - expect(StyleHelpers.hasClass(button, 'p-1.5')).toBe(true) - }) - - it('applies medium size classes (default)', () => { - testContainer.render() - const button = testContainer.queryButton() - - expect(StyleHelpers.hasClass(button, 'w-10')).toBe(true) - expect(StyleHelpers.hasClass(button, 'h-10')).toBe(true) - expect(StyleHelpers.hasClass(button, 'p-2')).toBe(true) - }) - - it('applies large size classes', () => { - testContainer.render() - const button = testContainer.queryButton() - - expect(StyleHelpers.hasClass(button, 'w-12')).toBe(true) - expect(StyleHelpers.hasClass(button, 'h-12')).toBe(true) - expect(StyleHelpers.hasClass(button, 'p-2')).toBe(true) - }) - }) - - describe('Loading State', () => { - it('shows spinner when loading', () => { - testContainer.render() - const container = testContainer.getContainer() - - // Spinner should be present - const spinner = container.querySelector('[data-testid="spinner"], .animate-spin') - expect(spinner).toBeDefined() - - // Icon should not be present - const iconSpan = container.querySelector('span[aria-hidden="true"]') - expect(iconSpan?.textContent).not.toBe('plus') - }) - - it('applies loading cursor style', () => { - testContainer.render() - const button = testContainer.queryButton() + expect(StyleHelpers.hasClass(button, "bg-primary-500")).toBe(false); + }); + + it("applies ghost neutral classes", () => { + testContainer.render( + + ); + const button = testContainer.queryButton(); + + expect(StyleHelpers.hasClass(button, "text-neutral-700")).toBe(true); + }); + + it("applies ghost negative classes", () => { + testContainer.render( + + ); + const button = testContainer.queryButton(); + + expect(StyleHelpers.hasClass(button, "text-negative-500")).toBe(true); + }); + }); + }); + + describe("Size Variants", () => { + it("applies extra small size classes", () => { + testContainer.render(); + const button = testContainer.queryButton(); + + expect(StyleHelpers.hasClass(button, "w-6")).toBe(true); + expect(StyleHelpers.hasClass(button, "h-6")).toBe(true); + expect(StyleHelpers.hasClass(button, "p-1")).toBe(true); + }); + + it("applies small size classes", () => { + testContainer.render(); + const button = testContainer.queryButton(); + + expect(StyleHelpers.hasClass(button, "w-8")).toBe(true); + expect(StyleHelpers.hasClass(button, "h-8")).toBe(true); + expect(StyleHelpers.hasClass(button, "p-1.5")).toBe(true); + }); + + it("applies medium size classes (default)", () => { + testContainer.render(); + const button = testContainer.queryButton(); + + expect(StyleHelpers.hasClass(button, "w-10")).toBe(true); + expect(StyleHelpers.hasClass(button, "h-10")).toBe(true); + expect(StyleHelpers.hasClass(button, "p-2")).toBe(true); + }); + + it("applies large size classes", () => { + testContainer.render(); + const button = testContainer.queryButton(); + + expect(StyleHelpers.hasClass(button, "w-12")).toBe(true); + expect(StyleHelpers.hasClass(button, "h-12")).toBe(true); + expect(StyleHelpers.hasClass(button, "p-2")).toBe(true); + }); + }); + + describe("Loading State", () => { + it("shows spinner when loading", () => { + testContainer.render(); + const container = testContainer.getContainer(); - expect(StyleHelpers.hasClass(button, 'cursor-not-allowed')).toBe(true) - }) - - it('is disabled when loading', () => { - testContainer.render() - const button = testContainer.queryButton() + // Spinner should be present + const spinner = container.querySelector( + '[data-testid="spinner"], .animate-spin' + ); + expect(spinner).toBeDefined(); - expect(button.disabled).toBe(true) - }) + // Icon should not be present + const iconSpan = container.querySelector('span[aria-hidden="true"]'); + expect(iconSpan?.textContent).not.toBe("plus"); + }); - it('does not trigger click events when loading', () => { - const handleClick = vi.fn() - testContainer.render() - const button = testContainer.queryButton() + it("applies loading cursor style", () => { + testContainer.render(); + const button = testContainer.queryButton(); - EventHelpers.click(button) + expect(StyleHelpers.hasClass(button, "cursor-not-allowed")).toBe(true); + }); - expect(handleClick).not.toHaveBeenCalled() - }) - }) + it("is disabled when loading", () => { + testContainer.render(); + const button = testContainer.queryButton(); - describe('Disabled State', () => { - it('is disabled with isDisabled prop', () => { - testContainer.render() - const button = testContainer.queryButton() + expect(button.disabled).toBe(true); + }); - expect(button.disabled).toBe(true) - }) + it("does not trigger click events when loading", () => { + const handleClick = vi.fn(); + testContainer.render( + + ); + const button = testContainer.queryButton(); - it('is disabled with disabled prop', () => { - testContainer.render() - const button = testContainer.queryButton() + EventHelpers.click(button); - expect(button.disabled).toBe(true) - }) + expect(handleClick).not.toHaveBeenCalled(); + }); + }); - it('applies disabled cursor style', () => { - testContainer.render() - const button = testContainer.queryButton() + describe("Disabled State", () => { + it("is disabled with isDisabled prop", () => { + testContainer.render(); + const button = testContainer.queryButton(); - expect(StyleHelpers.hasClass(button, 'cursor-not-allowed')).toBe(true) - }) + expect(button.disabled).toBe(true); + }); - it('applies disabled styling for solid variant', () => { - testContainer.render() - const button = testContainer.queryButton() + it("is disabled with disabled prop", () => { + testContainer.render(); + const button = testContainer.queryButton(); - expect(StyleHelpers.hasClass(button, 'disabled:bg-primary-200')).toBe(true) - expect(StyleHelpers.hasClass(button, 'disabled:text-white')).toBe(true) - expect(StyleHelpers.hasClass(button, 'disabled:border-none')).toBe(true) - }) + expect(button.disabled).toBe(true); + }); - it('applies disabled styling for outline variant', () => { - testContainer.render() - const button = testContainer.queryButton() + it("applies disabled cursor style", () => { + testContainer.render(); + const button = testContainer.queryButton(); - expect(StyleHelpers.hasClass(button, 'disabled:bg-white')).toBe(true) - expect(StyleHelpers.hasClass(button, 'disabled:text-primary-200')).toBe(true) - expect(StyleHelpers.hasClass(button, 'disabled:border-primary-100')).toBe(true) - }) + expect(StyleHelpers.hasClass(button, "cursor-not-allowed")).toBe(true); + }); - it('applies disabled styling for ghost variant', () => { - testContainer.render() - const button = testContainer.queryButton() + it("applies disabled styling for solid variant", () => { + testContainer.render( + + ); + const button = testContainer.queryButton(); + + expect(StyleHelpers.hasClass(button, "disabled:bg-primary-200")).toBe( + true + ); + expect(StyleHelpers.hasClass(button, "disabled:text-white")).toBe(true); + expect(StyleHelpers.hasClass(button, "disabled:border-none")).toBe(true); + }); + + it("applies disabled styling for outline variant", () => { + testContainer.render( + + ); + const button = testContainer.queryButton(); + + expect(StyleHelpers.hasClass(button, "disabled:bg-white")).toBe(true); + expect(StyleHelpers.hasClass(button, "disabled:text-primary-200")).toBe( + true + ); + expect(StyleHelpers.hasClass(button, "disabled:border-primary-100")).toBe( + true + ); + }); + + it("applies disabled styling for ghost variant", () => { + testContainer.render( + + ); + const button = testContainer.queryButton(); - expect(StyleHelpers.hasClass(button, 'disabled:text-primary-200')).toBe(true) - }) + expect(StyleHelpers.hasClass(button, "disabled:text-primary-200")).toBe( + true + ); + }); - it('does not trigger click events when disabled', () => { - const handleClick = vi.fn() - testContainer.render() - const button = testContainer.queryButton() + it("does not trigger click events when disabled", () => { + const handleClick = vi.fn(); + testContainer.render( + + ); + const button = testContainer.queryButton(); - EventHelpers.click(button) + EventHelpers.click(button); - expect(handleClick).not.toHaveBeenCalled() - }) - }) + expect(handleClick).not.toHaveBeenCalled(); + }); + }); - describe('User Interaction', () => { - it('handles click events properly', () => { - const handleClick = vi.fn() - testContainer.render() - const button = testContainer.queryButton() + describe("User Interaction", () => { + it("handles click events properly", () => { + const handleClick = vi.fn(); + testContainer.render(); + const button = testContainer.queryButton(); - EventHelpers.click(button) + EventHelpers.click(button); - expect(handleClick).toHaveBeenCalledTimes(1) - }) + expect(handleClick).toHaveBeenCalledTimes(1); + }); - it('handles keyboard events', () => { - const handleKeyDown = vi.fn() - testContainer.render() - const button = testContainer.queryButton() + it("handles keyboard events", () => { + const handleKeyDown = vi.fn(); + testContainer.render( + + ); + const button = testContainer.queryButton(); - EventHelpers.keyDown(button, 'Enter') + EventHelpers.keyDown(button, "Enter"); - expect(handleKeyDown).toHaveBeenCalledTimes(1) - }) + expect(handleKeyDown).toHaveBeenCalledTimes(1); + }); - it('handles focus and blur events', () => { - const handleFocus = vi.fn() - const handleBlur = vi.fn() - testContainer.render() - const button = testContainer.queryButton() + it("handles focus and blur events", () => { + const handleFocus = vi.fn(); + const handleBlur = vi.fn(); + testContainer.render( + + ); + const button = testContainer.queryButton(); - EventHelpers.focus(button) - expect(handleFocus).toHaveBeenCalledTimes(1) + EventHelpers.focus(button); + expect(handleFocus).toHaveBeenCalledTimes(1); - EventHelpers.blur(button) - expect(handleBlur).toHaveBeenCalledTimes(1) - }) - }) + EventHelpers.blur(button); + expect(handleBlur).toHaveBeenCalledTimes(1); + }); + }); - describe('AsChild Functionality', () => { - it('renders as a Slot when asChild is true', () => { + describe("AsChild Functionality", () => { + it("renders as a Slot when asChild is true", () => { testContainer.render(
Custom element
- ) - const container = testContainer.getContainer() - + ); + const container = testContainer.getContainer(); + // When asChild is true, Slot merges props with the child element // The div should still exist but with the button's props merged - const customElement = container.querySelector('[data-testid="custom-element"]') - expect(customElement).toBeDefined() - + const customElement = container.querySelector( + '[data-testid="custom-element"]' + ); + expect(customElement).toBeDefined(); + // Check if the element exists and has some content if (customElement) { - expect(customElement.textContent).toContain('Custom element') + expect(customElement.textContent).toContain("Custom element"); } - }) - }) - - describe('Accessibility', () => { - it('has proper button semantics', () => { - testContainer.render() - const button = testContainer.queryButton() - - expect(button.tagName).toBe('BUTTON') - expect(button.type).toBe('button') - }) - - it('supports aria-label', () => { - const ariaLabel = 'Add item' - testContainer.render() - const button = testContainer.queryButton() - - expect(A11yHelpers.hasAriaLabel(button, ariaLabel)).toBe(true) - }) - - it('has proper focus styles', () => { - testContainer.render() - const button = testContainer.queryButton() - - expect(StyleHelpers.hasClass(button, 'focus-visible:outline-hidden')).toBe(true) - expect(StyleHelpers.hasClass(button, 'focus-visible:ring-2')).toBe(true) - }) - - it('properly hides icon from screen readers', () => { - testContainer.render() - const container = testContainer.getContainer() - const iconSpan = container.querySelector('span[aria-hidden="true"]') - - expect(iconSpan?.getAttribute('aria-hidden')).toBe('true') - }) - }) - - describe('Edge Cases', () => { - it('handles empty icon gracefully', () => { - testContainer.render() - const container = testContainer.getContainer() - const iconSpan = container.querySelector('span[aria-hidden="true"]') - - expect(iconSpan?.textContent).toBe('') - }) - - it('handles both isDisabled and isLoading being true', () => { - testContainer.render() - const button = testContainer.queryButton() - - expect(button.disabled).toBe(true) - expect(StyleHelpers.hasClass(button, 'cursor-not-allowed')).toBe(true) - }) - - it('prioritizes isLoading over normal icon display', () => { - testContainer.render() - const container = testContainer.getContainer() - + }); + }); + + describe("Accessibility", () => { + it("has proper button semantics", () => { + testContainer.render(); + const button = testContainer.queryButton(); + + expect(button.tagName).toBe("BUTTON"); + expect(button.type).toBe("button"); + }); + + it("supports aria-label", () => { + const ariaLabel = "Add item"; + testContainer.render(); + const button = testContainer.queryButton(); + + expect(A11yHelpers.hasAriaLabel(button, ariaLabel)).toBe(true); + }); + + it("has proper focus styles", () => { + testContainer.render(); + const button = testContainer.queryButton(); + + expect( + StyleHelpers.hasClass(button, "focus-visible:outline-hidden") + ).toBe(true); + expect(StyleHelpers.hasClass(button, "focus-visible:ring-2")).toBe(true); + }); + + it("properly hides icon from screen readers", () => { + testContainer.render(); + const container = testContainer.getContainer(); + const iconSpan = container.querySelector('span[aria-hidden="true"]'); + + expect(iconSpan?.getAttribute("aria-hidden")).toBe("true"); + }); + }); + + describe("Edge Cases", () => { + it("handles empty icon gracefully", () => { + testContainer.render(); + const container = testContainer.getContainer(); + const iconSpan = container.querySelector('span[aria-hidden="true"]'); + + expect(iconSpan?.textContent).toBe(""); + }); + + it("handles both isDisabled and isLoading being true", () => { + testContainer.render(); + const button = testContainer.queryButton(); + + expect(button.disabled).toBe(true); + expect(StyleHelpers.hasClass(button, "cursor-not-allowed")).toBe(true); + }); + + it("prioritizes isLoading over normal icon display", () => { + testContainer.render(); + const container = testContainer.getContainer(); + // Should show spinner instead of icon - const spinner = container.querySelector('[data-testid="spinner"], .animate-spin') - expect(spinner).toBeDefined() - + const spinner = container.querySelector( + '[data-testid="spinner"], .animate-spin' + ); + expect(spinner).toBeDefined(); + // Icon should not be the original icon - const iconSpan = container.querySelector('span[aria-hidden="true"]') - expect(iconSpan?.textContent).not.toBe('plus') - }) - - it('maintains display name for debugging', () => { - expect(IconButton.displayName).toBe('IconButton') - }) - }) - - describe('Ref Forwarding', () => { - it('forwards ref to button element', () => { - const ref = { current: null as HTMLButtonElement | null } - - testContainer.render( - - ) - - expect(ref.current).toBeInstanceOf(HTMLButtonElement) - expect(ref.current?.tagName).toBe('BUTTON') - }) - }) -}) + const iconSpan = container.querySelector('span[aria-hidden="true"]'); + expect(iconSpan?.textContent).not.toBe("plus"); + }); + + it("maintains display name for debugging", () => { + expect(IconButton.displayName).toBe("IconButton"); + }); + }); + + describe("Ref Forwarding", () => { + it("forwards ref to button element", () => { + const ref = { current: null as HTMLButtonElement | null }; + + testContainer.render(); + + expect(ref.current).toBeInstanceOf(HTMLButtonElement); + expect(ref.current?.tagName).toBe("BUTTON"); + }); + }); +}); From f36bf7efd8624c388d37365ede1bcfadfe1be8de Mon Sep 17 00:00:00 2001 From: touyou <465697+touyou@users.noreply.github.com> Date: Thu, 7 Aug 2025 18:06:49 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=92=84=20UI=E3=82=84=E3=82=B9?= =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=AB=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=AE=E8=BF=BD=E5=8A=A0=E3=82=84=E6=9B=B4=E6=96=B0:=20IconB?= =?UTF-8?q?utton=E3=81=8A=E3=82=88=E3=81=B3Modal=E3=82=B3=E3=83=B3?= =?UTF-8?q?=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=AE=E3=83=86?= =?UTF-8?q?=E3=83=BC=E3=83=9E=E3=82=92=E3=80=8Csecondary=E3=80=8D=E3=81=8B?= =?UTF-8?q?=E3=82=89=E3=80=8Cneutral=E3=80=8D=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IconButtonコンポーネントのテーマを「neutral」に変更 - Modalコンポーネントのテーマを「neutral」に変更 --- src/components/ui/inline-message/index.tsx | 2 +- src/components/ui/input/index.tsx | 2 +- src/components/ui/modal/index.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ui/inline-message/index.tsx b/src/components/ui/inline-message/index.tsx index 6488270f..39f6a64e 100644 --- a/src/components/ui/inline-message/index.tsx +++ b/src/components/ui/inline-message/index.tsx @@ -132,7 +132,7 @@ const InlineMessage = React.forwardRef( icon="close" size="sm" variant="ghost" - theme="secondary" + theme="neutral" onClick={onClose} aria-label="閉じる" className="shrink-0" diff --git a/src/components/ui/input/index.tsx b/src/components/ui/input/index.tsx index 613263f1..91f3d430 100644 --- a/src/components/ui/input/index.tsx +++ b/src/components/ui/input/index.tsx @@ -311,7 +311,7 @@ const Input = React.forwardRef( From 754ecf75a5c1159dca2766fc470edb4cde8277bc Mon Sep 17 00:00:00 2001 From: touyou <465697+touyou@users.noreply.github.com> Date: Fri, 15 Aug 2025 15:31:44 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=92=84=20style:=20icon-button?= =?UTF-8?q?=E3=81=AE=E3=82=B9=E3=82=BF=E3=82=A4=E3=83=AB=E3=82=92=E6=94=B9?= =?UTF-8?q?=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "antialiased"クラスを削除し、スタイルを簡素化 --- src/components/ui/icon-button/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/icon-button/index.tsx b/src/components/ui/icon-button/index.tsx index 1b6e21d5..333f34bc 100644 --- a/src/components/ui/icon-button/index.tsx +++ b/src/components/ui/icon-button/index.tsx @@ -10,7 +10,7 @@ const iconButtonVariants = cva( [ "inline-flex items-center justify-center whitespace-nowrap rounded-action", "ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[var(--color-ring-normal)] focus-visible:ring-offset-2", - "relative cursor-pointer antialiased", + "relative cursor-pointer", ].join(" "), { variants: {