From 70e0915785b66263645f84241110814cca9a3672 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Wed, 4 Jun 2025 02:55:49 +0900 Subject: [PATCH 01/17] Add `Stepper` component by Claude Code --- src/components/Stepper/Stepper.module.css | 90 ++++++++++++ src/components/Stepper/Stepper.spec.tsx | 152 ++++++++++++++++++++ src/components/Stepper/Stepper.stories.tsx | 155 +++++++++++++++++++++ src/components/Stepper/Stepper.tsx | 58 ++++++++ src/components/Stepper/StepperItem.tsx | 82 +++++++++++ src/index.ts | 2 + 6 files changed, 539 insertions(+) create mode 100644 src/components/Stepper/Stepper.module.css create mode 100644 src/components/Stepper/Stepper.spec.tsx create mode 100644 src/components/Stepper/Stepper.stories.tsx create mode 100644 src/components/Stepper/Stepper.tsx create mode 100644 src/components/Stepper/StepperItem.tsx diff --git a/src/components/Stepper/Stepper.module.css b/src/components/Stepper/Stepper.module.css new file mode 100644 index 00000000..213a029b --- /dev/null +++ b/src/components/Stepper/Stepper.module.css @@ -0,0 +1,90 @@ +.stepper { + display: flex; + flex-direction: row; + align-items: stretch; + justify-content: stretch; + margin: var(--margin-top) var(--margin-right) var(--margin-bottom) var(--margin-left); +} + +.stepperItem { + display: flex; + flex: 1; + flex-direction: column; + gap: 8px; + align-items: center; +} + +.iconArea { + display: flex; + flex-direction: row; + align-self: stretch; + justify-content: center; + width: 100%; +} + +.leftBorder, +.rightBorder { + display: flex; + flex: 1; + flex-direction: column; + gap: 10px; + align-self: stretch; + justify-content: center; +} + +.leftBorder.hidden, +.rightBorder.hidden { + display: none; +} + +.border { + flex: 1; + height: 2px; + background-color: #f6f6f6; +} + +.iconWrapper { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; +} + +/* Icon colors based on status */ +.current .iconWrapper { + color: #3959cc; +} + +.done .iconWrapper { + color: #3959cc; +} + +.undone .iconWrapper { + color: #f6f6f6; +} + +.label { + width: 100%; + font-family: 'OT-UD Shin Go Pr6N', sans-serif; + font-size: 12px; + line-height: 1.5; + text-align: center; + word-wrap: break-word; +} + +.currentLabel { + font-weight: 600; + color: #000; +} + +.doneLabel { + font-weight: 400; + color: #000; +} + +.undoneLabel { + font-weight: 400; + color: #000; +} diff --git a/src/components/Stepper/Stepper.spec.tsx b/src/components/Stepper/Stepper.spec.tsx new file mode 100644 index 00000000..24488d40 --- /dev/null +++ b/src/components/Stepper/Stepper.spec.tsx @@ -0,0 +1,152 @@ +import { render, screen } from '@testing-library/react'; +import { Stepper } from './Stepper'; +import { StepperItem } from './StepperItem'; + +describe('', () => { + it('renders children correctly', () => { + render( + + + + + , + ); + + expect(screen.getByTestId('stepper')).toBeInTheDocument(); + expect(screen.getByText('Step 1')).toBeInTheDocument(); + expect(screen.getByText('Step 2')).toBeInTheDocument(); + expect(screen.getByText('Step 3')).toBeInTheDocument(); + }); + + it('applies correct status based on currentStep', () => { + render( + + + + + , + ); + + const step0 = screen.getByTestId('step-0'); + const step1 = screen.getByTestId('step-1'); + const step2 = screen.getByTestId('step-2'); + + // Step 0 should be done (index < currentStep) + expect(step0.className).toMatch(/done/); + // Step 1 should be current (index === currentStep) + expect(step1.className).toMatch(/current/); + // Step 2 should be undone (index > currentStep) + expect(step2.className).toMatch(/undone/); + }); + + it('sets first and last child properties correctly', () => { + render( + + + + + , + ); + + const first = screen.getByTestId('first'); + const middle = screen.getByTestId('middle'); + const last = screen.getByTestId('last'); + + // First item should hide left border + const firstLeftBorder = first.querySelector('[class*="leftBorder"]'); + const firstRightBorder = first.querySelector('[class*="rightBorder"]'); + expect(firstLeftBorder?.className).toMatch(/hidden/); + expect(firstRightBorder?.className).not.toMatch(/hidden/); + + // Middle item should show both borders + const middleLeftBorder = middle.querySelector('[class*="leftBorder"]'); + const middleRightBorder = middle.querySelector('[class*="rightBorder"]'); + expect(middleLeftBorder?.className).not.toMatch(/hidden/); + expect(middleRightBorder?.className).not.toMatch(/hidden/); + + // Last item should hide right border + const lastLeftBorder = last.querySelector('[class*="leftBorder"]'); + const lastRightBorder = last.querySelector('[class*="rightBorder"]'); + expect(lastLeftBorder?.className).not.toMatch(/hidden/); + expect(lastRightBorder?.className).toMatch(/hidden/); + }); + + it('has all margins through m prop', () => { + render( + + + + , + ); + const stepper = screen.getByTestId('stepper'); + + expect(stepper).toHaveStyle('--margin-top: var(--size-spacing-xxs)'); + expect(stepper).toHaveStyle('--margin-right: var(--size-spacing-xxs)'); + expect(stepper).toHaveStyle('--margin-bottom: var(--size-spacing-xxs)'); + expect(stepper).toHaveStyle('--margin-left: var(--size-spacing-xxs)'); + }); + + it('defaults to currentStep 0 when not specified', () => { + render( + + + + , + ); + + const step0 = screen.getByTestId('step-0'); + const step1 = screen.getByTestId('step-1'); + + expect(step0.className).toMatch(/current/); + expect(step1.className).toMatch(/undone/); + }); +}); + +describe('', () => { + it('renders label correctly', () => { + render(); + expect(screen.getByText('Test Step')).toBeInTheDocument(); + }); + + it('applies correct CSS classes based on status', () => { + const { rerender } = render(); + + expect(screen.getByTestId('item').className).toMatch(/current/); + + rerender(); + expect(screen.getByTestId('item').className).toMatch(/done/); + + rerender(); + expect(screen.getByTestId('item').className).toMatch(/undone/); + }); + + it('uses custom icon when provided', () => { + render(); + + // Icon component should be rendered (we can't easily test the actual icon) + const iconWrapper = screen.getByTestId('item').querySelector('[class*="iconWrapper"]'); + expect(iconWrapper).toBeInTheDocument(); + }); + + it('uses custom done icon when status is done', () => { + render(); + + const iconWrapper = screen.getByTestId('item').querySelector('[class*="iconWrapper"]'); + expect(iconWrapper).toBeInTheDocument(); + }); + + it('hides borders correctly for first and last items', () => { + const { rerender } = render(); + + const leftBorder = screen.getByTestId('item').querySelector('[class*="leftBorder"]'); + const rightBorder = screen.getByTestId('item').querySelector('[class*="rightBorder"]'); + expect(leftBorder?.className).toMatch(/hidden/); + expect(rightBorder?.className).not.toMatch(/hidden/); + + rerender(); + const leftBorder2 = screen.getByTestId('item').querySelector('[class*="leftBorder"]'); + const rightBorder2 = screen.getByTestId('item').querySelector('[class*="rightBorder"]'); + expect(leftBorder2?.className).not.toMatch(/hidden/); + expect(rightBorder2?.className).toMatch(/hidden/); + }); +}); diff --git a/src/components/Stepper/Stepper.stories.tsx b/src/components/Stepper/Stepper.stories.tsx new file mode 100644 index 00000000..b3484df8 --- /dev/null +++ b/src/components/Stepper/Stepper.stories.tsx @@ -0,0 +1,155 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { ComponentProps } from 'react'; +import { Stepper } from './Stepper'; +import { StepperItem } from './StepperItem'; + +export default { + title: 'Navigation/Stepper', + component: Stepper, + parameters: { + docs: { + description: { + component: 'ステップ形式のナビゲーションコンポーネント。進行状況を視覚的に表示します。', + }, + }, + }, +} satisfies Meta; + +type Story = StoryObj; + +const defaultArgs = { + currentStep: 1, +} satisfies Partial>; + +export const Default: Story = { + render: (args) => ( + + + + + + ), + args: defaultArgs, +}; + +export const ThreeSteps: Story = { + render: () => ( +
+
+

Current Step: 0

+ + + + + +
+ +
+

Current Step: 1

+ + + + + +
+ +
+

Current Step: 2

+ + + + + +
+
+ ), +}; + +export const FourSteps: Story = { + render: () => ( + + + + + + + ), +}; + +export const FiveSteps: Story = { + render: () => ( + + + + + + + + ), +}; + +export const LongLabels: Story = { + render: () => ( + + + + + + ), +}; + +export const CustomIcons: Story = { + render: () => ( + + + + + + ), +}; + +export const WithMargins: Story = { + render: () => ( +
+ + + + + +
+ ), +}; + +export const DifferentWidths: Story = { + render: () => ( +
+
+

幅280px

+ + + + + +
+ +
+

幅440px

+ + + + + + +
+ +
+

幅640px

+ + + + + +
+
+ ), +}; diff --git a/src/components/Stepper/Stepper.tsx b/src/components/Stepper/Stepper.tsx new file mode 100644 index 00000000..a9d1bc03 --- /dev/null +++ b/src/components/Stepper/Stepper.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { type CSSProperties, type ReactNode, Children, cloneElement, isValidElement } from 'react'; +import styles from './Stepper.module.css'; +import { marginVariables } from '../../utils/style'; +import type { CustomDataAttributeProps } from '../../types/attributes'; +import type { IconName } from '../../types/icon'; +import type { Spacing } from '../../types/style'; + +export type StepStatus = 'current' | 'undone' | 'done'; + +export interface StepperProps extends CustomDataAttributeProps { + children: ReactNode[]; + currentStep?: number; + // Margin props + m?: Spacing; + mx?: Spacing; + my?: Spacing; + mt?: Spacing; + mr?: Spacing; + mb?: Spacing; + ml?: Spacing; +} + +export interface StepperItemProps extends CustomDataAttributeProps { + label: string; + icon?: IconName; + doneIcon?: IconName; + // Internal props (automatically set by Stepper) + status?: StepStatus; + isFirst?: boolean; + isLast?: boolean; +} + +export { StepperItem } from './StepperItem'; + +export const Stepper = ({ children, currentStep = 0, m, mx, my, mt, mr, mb, ml, ...props }: StepperProps) => { + const marginStyles = marginVariables({ m, mx, my, mt, mr, mb, ml }); + + const enhancedChildren = Children.map(children, (child, index) => { + if (!isValidElement(child)) return child; + + const status: StepStatus = index < currentStep ? 'done' : index === currentStep ? 'current' : 'undone'; + + return cloneElement(child, { + ...child.props, + status, + isFirst: index === 0, + isLast: index === children.length - 1, + }); + }); + + return ( +
+ {enhancedChildren} +
+ ); +}; diff --git a/src/components/Stepper/StepperItem.tsx b/src/components/Stepper/StepperItem.tsx new file mode 100644 index 00000000..31e680ca --- /dev/null +++ b/src/components/Stepper/StepperItem.tsx @@ -0,0 +1,82 @@ +'use client'; + +import { clsx } from 'clsx'; +import styles from './Stepper.module.css'; +import { Icon } from '../Icon/Icon'; +import type { StepperItemProps } from './Stepper'; +import type { IconName } from '../../types/icon'; + +export const StepperItem = ({ + label, + icon, + doneIcon, + status = 'undone', + isFirst = false, + isLast = false, + ...props +}: StepperItemProps) => { + const getIcon = (): IconName => { + if (status === 'done' && doneIcon) { + return doneIcon; + } + if (status === 'done') { + return 'CheckAIcon'; + } + if (status === 'current' && icon) { + return icon; + } + if (status === 'current') { + return 'UbieIcon'; // Use a valid icon name + } + // undone + if (icon) { + return icon; + } + return 'UbieIcon'; // Use a valid icon name + }; + + const itemClass = clsx({ + [styles.stepperItem]: true, + [styles.current]: status === 'current', + [styles.done]: status === 'done', + [styles.undone]: status === 'undone', + }); + + const iconAreaClass = clsx({ + [styles.iconArea]: true, + }); + + const leftBorderClass = clsx({ + [styles.leftBorder]: true, + [styles.hidden]: isFirst, + }); + + const rightBorderClass = clsx({ + [styles.rightBorder]: true, + [styles.hidden]: isLast, + }); + + const labelClass = clsx({ + [styles.label]: true, + [styles.currentLabel]: status === 'current', + [styles.doneLabel]: status === 'done', + [styles.undoneLabel]: status === 'undone', + }); + + return ( +
+
+
+
+
+
+ +
+
+
+
+
+
{label}
+
+ ); +}; diff --git a/src/index.ts b/src/index.ts index 5f5e5cfe..ec9f5b2e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,8 @@ export { Select } from './components/Select/Select'; export { Text } from './components/Text/Text'; export { TextArea } from './components/TextArea/TextArea'; export { Stack } from './components/Stack/Stack'; +export { Stepper, StepperItem } from './components/Stepper/Stepper'; export { Toggle } from './components/Toggle/Toggle'; export type { IconName } from './types/icon'; +export type { StepperProps, StepperItemProps, StepStatus } from './components/Stepper/Stepper'; From 43f0c7119c7eef5b7dbfcbedcf367af4dfcf1f82 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Wed, 4 Jun 2025 04:09:18 +0900 Subject: [PATCH 02/17] Fix `StepperItemPropso` definition place --- src/components/Stepper/Stepper.tsx | 13 +------------ src/components/Stepper/StepperItem.tsx | 13 ++++++++++++- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/Stepper/Stepper.tsx b/src/components/Stepper/Stepper.tsx index a9d1bc03..df069399 100644 --- a/src/components/Stepper/Stepper.tsx +++ b/src/components/Stepper/Stepper.tsx @@ -4,7 +4,6 @@ import { type CSSProperties, type ReactNode, Children, cloneElement, isValidElem import styles from './Stepper.module.css'; import { marginVariables } from '../../utils/style'; import type { CustomDataAttributeProps } from '../../types/attributes'; -import type { IconName } from '../../types/icon'; import type { Spacing } from '../../types/style'; export type StepStatus = 'current' | 'undone' | 'done'; @@ -22,17 +21,7 @@ export interface StepperProps extends CustomDataAttributeProps { ml?: Spacing; } -export interface StepperItemProps extends CustomDataAttributeProps { - label: string; - icon?: IconName; - doneIcon?: IconName; - // Internal props (automatically set by Stepper) - status?: StepStatus; - isFirst?: boolean; - isLast?: boolean; -} - -export { StepperItem } from './StepperItem'; +export { StepperItem, type StepperItemProps } from './StepperItem'; export const Stepper = ({ children, currentStep = 0, m, mx, my, mt, mr, mb, ml, ...props }: StepperProps) => { const marginStyles = marginVariables({ m, mx, my, mt, mr, mb, ml }); diff --git a/src/components/Stepper/StepperItem.tsx b/src/components/Stepper/StepperItem.tsx index 31e680ca..f22f31b4 100644 --- a/src/components/Stepper/StepperItem.tsx +++ b/src/components/Stepper/StepperItem.tsx @@ -3,9 +3,20 @@ import { clsx } from 'clsx'; import styles from './Stepper.module.css'; import { Icon } from '../Icon/Icon'; -import type { StepperItemProps } from './Stepper'; +import type { StepStatus } from './Stepper'; +import type { CustomDataAttributeProps } from '../../types/attributes'; import type { IconName } from '../../types/icon'; +export interface StepperItemProps extends CustomDataAttributeProps { + label: string; + icon?: IconName; + doneIcon?: IconName; + // Internal props (automatically set by Stepper) + status?: StepStatus; + isFirst?: boolean; + isLast?: boolean; +} + export const StepperItem = ({ label, icon, From 260b2aa0c00d05aa5129f039407d8bbd3f60d8d3 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Wed, 4 Jun 2025 04:13:56 +0900 Subject: [PATCH 03/17] Make border thin --- src/components/Stepper/Stepper.module.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Stepper/Stepper.module.css b/src/components/Stepper/Stepper.module.css index 213a029b..482ef3ac 100644 --- a/src/components/Stepper/Stepper.module.css +++ b/src/components/Stepper/Stepper.module.css @@ -38,7 +38,6 @@ } .border { - flex: 1; height: 2px; background-color: #f6f6f6; } From 56e6e2006cbd58ede6c55751883f351f67a4b0ca Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Wed, 4 Jun 2025 04:21:17 +0900 Subject: [PATCH 04/17] Fix default step icon --- src/components/Stepper/Stepper.module.css | 31 ++++++++++++++++++++- src/components/Stepper/StepperItem.tsx | 34 ++++++++++++++--------- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/components/Stepper/Stepper.module.css b/src/components/Stepper/Stepper.module.css index 482ef3ac..6f526bab 100644 --- a/src/components/Stepper/Stepper.module.css +++ b/src/components/Stepper/Stepper.module.css @@ -51,7 +51,36 @@ height: 24px; } -/* Icon colors based on status */ +/* Circle styles for different statuses */ +.doneCircle { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 2px; + color: #3959cc; + background-color: #fff; + border: 2px solid #3959cc; + border-radius: 50%; +} + +.currentCircle { + width: 24px; + height: 24px; + background-color: #3959cc; + border-radius: 50%; +} + +.undoneCircle { + width: 24px; + height: 24px; + background-color: transparent; + border: 2px solid #f6f6f6; + border-radius: 50%; +} + +/* Icon colors for custom icons */ .current .iconWrapper { color: #3959cc; } diff --git a/src/components/Stepper/StepperItem.tsx b/src/components/Stepper/StepperItem.tsx index f22f31b4..4dea5a22 100644 --- a/src/components/Stepper/StepperItem.tsx +++ b/src/components/Stepper/StepperItem.tsx @@ -26,24 +26,32 @@ export const StepperItem = ({ isLast = false, ...props }: StepperItemProps) => { - const getIcon = (): IconName => { + const renderIcon = () => { + // カスタムアイコンが指定されている場合はそれを使用 if (status === 'done' && doneIcon) { - return doneIcon; + return ; } - if (status === 'done') { - return 'CheckAIcon'; + if ((status === 'current' || status === 'undone') && icon) { + return ; } - if (status === 'current' && icon) { - return icon; + + // デフォルトの状態に応じた描画 + if (status === 'done') { + // 白い丸で囲まれたチェックアイコン + return ( +
+ +
+ ); } + if (status === 'current') { - return 'UbieIcon'; // Use a valid icon name - } - // undone - if (icon) { - return icon; + // 塗りつぶされた青い丸 + return
; } - return 'UbieIcon'; // Use a valid icon name + + // undone: グレーの枠の塗りつぶされていない丸 + return
; }; const itemClass = clsx({ @@ -81,7 +89,7 @@ export const StepperItem = ({
- + {renderIcon()}
From 7733c0f38d8cde4302e3c5a4e9730a58ea822dfb Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Wed, 4 Jun 2025 04:31:15 +0900 Subject: [PATCH 05/17] Fix layout problems --- src/components/Stepper/Stepper.module.css | 31 +++++++++----- src/components/Stepper/Stepper.spec.tsx | 50 +++++++++++------------ src/components/Stepper/StepperItem.tsx | 25 +++++++----- 3 files changed, 59 insertions(+), 47 deletions(-) diff --git a/src/components/Stepper/Stepper.module.css b/src/components/Stepper/Stepper.module.css index 6f526bab..f365640c 100644 --- a/src/components/Stepper/Stepper.module.css +++ b/src/components/Stepper/Stepper.module.css @@ -7,6 +7,7 @@ } .stepperItem { + position: relative; display: flex; flex: 1; flex-direction: column; @@ -14,35 +15,42 @@ align-items: center; } -.iconArea { +.borderLine { + position: absolute; + top: 12px; /* Center the line with the 24px icon (12px from top) */ + right: 0; + left: 0; + z-index: 1; display: flex; flex-direction: row; - align-self: stretch; - justify-content: center; - width: 100%; + align-items: center; +} + +.iconSpacer { + flex-shrink: 0; + width: 24px; } .leftBorder, .rightBorder { display: flex; flex: 1; - flex-direction: column; - gap: 10px; - align-self: stretch; - justify-content: center; + align-items: center; } -.leftBorder.hidden, -.rightBorder.hidden { +.border.hidden { display: none; } .border { + width: 100%; height: 2px; background-color: #f6f6f6; } .iconWrapper { + position: relative; + z-index: 2; display: flex; flex-shrink: 0; align-items: center; @@ -66,6 +74,7 @@ } .currentCircle { + position: relative; width: 24px; height: 24px; background-color: #3959cc; @@ -75,7 +84,7 @@ .undoneCircle { width: 24px; height: 24px; - background-color: transparent; + background-color: #fff; border: 2px solid #f6f6f6; border-radius: 50%; } diff --git a/src/components/Stepper/Stepper.spec.tsx b/src/components/Stepper/Stepper.spec.tsx index 24488d40..06d422f3 100644 --- a/src/components/Stepper/Stepper.spec.tsx +++ b/src/components/Stepper/Stepper.spec.tsx @@ -52,23 +52,23 @@ describe('', () => { const middle = screen.getByTestId('middle'); const last = screen.getByTestId('last'); - // First item should hide left border - const firstLeftBorder = first.querySelector('[class*="leftBorder"]'); - const firstRightBorder = first.querySelector('[class*="rightBorder"]'); - expect(firstLeftBorder?.className).toMatch(/hidden/); - expect(firstRightBorder?.className).not.toMatch(/hidden/); - - // Middle item should show both borders - const middleLeftBorder = middle.querySelector('[class*="leftBorder"]'); - const middleRightBorder = middle.querySelector('[class*="rightBorder"]'); - expect(middleLeftBorder?.className).not.toMatch(/hidden/); - expect(middleRightBorder?.className).not.toMatch(/hidden/); - - // Last item should hide right border - const lastLeftBorder = last.querySelector('[class*="leftBorder"]'); - const lastRightBorder = last.querySelector('[class*="rightBorder"]'); - expect(lastLeftBorder?.className).not.toMatch(/hidden/); - expect(lastRightBorder?.className).toMatch(/hidden/); + // First item should hide left border line + const firstLeftBorderLine = first.querySelector('[class*="leftBorder"] [class*="border"]'); + const firstRightBorderLine = first.querySelector('[class*="rightBorder"] [class*="border"]'); + expect(firstLeftBorderLine?.className).toMatch(/hidden/); + expect(firstRightBorderLine?.className).not.toMatch(/hidden/); + + // Middle item should show both border lines + const middleLeftBorderLine = middle.querySelector('[class*="leftBorder"] [class*="border"]'); + const middleRightBorderLine = middle.querySelector('[class*="rightBorder"] [class*="border"]'); + expect(middleLeftBorderLine?.className).not.toMatch(/hidden/); + expect(middleRightBorderLine?.className).not.toMatch(/hidden/); + + // Last item should hide right border line + const lastLeftBorderLine = last.querySelector('[class*="leftBorder"] [class*="border"]'); + const lastRightBorderLine = last.querySelector('[class*="rightBorder"] [class*="border"]'); + expect(lastLeftBorderLine?.className).not.toMatch(/hidden/); + expect(lastRightBorderLine?.className).toMatch(/hidden/); }); it('has all margins through m prop', () => { @@ -138,15 +138,15 @@ describe('', () => { it('hides borders correctly for first and last items', () => { const { rerender } = render(); - const leftBorder = screen.getByTestId('item').querySelector('[class*="leftBorder"]'); - const rightBorder = screen.getByTestId('item').querySelector('[class*="rightBorder"]'); - expect(leftBorder?.className).toMatch(/hidden/); - expect(rightBorder?.className).not.toMatch(/hidden/); + const leftBorderLine = screen.getByTestId('item').querySelector('[class*="leftBorder"] [class*="border"]'); + const rightBorderLine = screen.getByTestId('item').querySelector('[class*="rightBorder"] [class*="border"]'); + expect(leftBorderLine?.className).toMatch(/hidden/); + expect(rightBorderLine?.className).not.toMatch(/hidden/); rerender(); - const leftBorder2 = screen.getByTestId('item').querySelector('[class*="leftBorder"]'); - const rightBorder2 = screen.getByTestId('item').querySelector('[class*="rightBorder"]'); - expect(leftBorder2?.className).not.toMatch(/hidden/); - expect(rightBorder2?.className).toMatch(/hidden/); + const leftBorderLine2 = screen.getByTestId('item').querySelector('[class*="leftBorder"] [class*="border"]'); + const rightBorderLine2 = screen.getByTestId('item').querySelector('[class*="rightBorder"] [class*="border"]'); + expect(leftBorderLine2?.className).not.toMatch(/hidden/); + expect(rightBorderLine2?.className).toMatch(/hidden/); }); }); diff --git a/src/components/Stepper/StepperItem.tsx b/src/components/Stepper/StepperItem.tsx index 4dea5a22..7f210da1 100644 --- a/src/components/Stepper/StepperItem.tsx +++ b/src/components/Stepper/StepperItem.tsx @@ -61,17 +61,21 @@ export const StepperItem = ({ [styles.undone]: status === 'undone', }); - const iconAreaClass = clsx({ - [styles.iconArea]: true, - }); - const leftBorderClass = clsx({ [styles.leftBorder]: true, - [styles.hidden]: isFirst, }); const rightBorderClass = clsx({ [styles.rightBorder]: true, + }); + + const leftBorderLineClass = clsx({ + [styles.border]: true, + [styles.hidden]: isFirst, + }); + + const rightBorderLineClass = clsx({ + [styles.border]: true, [styles.hidden]: isLast, }); @@ -84,17 +88,16 @@ export const StepperItem = ({ return (
-
+
-
-
-
- {renderIcon()} +
+
-
+
+
{renderIcon()}
{label}
); From 45ba7890f403bfbe8e316e942ad1b83c32ea810b Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Wed, 4 Jun 2025 04:48:47 +0900 Subject: [PATCH 06/17] `borderColor` support --- src/components/Stepper/Stepper.module.css | 13 +++++++++-- src/components/Stepper/Stepper.spec.tsx | 27 ++++++++++++++++++++++ src/components/Stepper/Stepper.stories.tsx | 24 +++++++++++++++++++ src/components/Stepper/Stepper.tsx | 5 +++- src/components/Stepper/StepperItem.tsx | 5 +++- 5 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/components/Stepper/Stepper.module.css b/src/components/Stepper/Stepper.module.css index f365640c..8eb13025 100644 --- a/src/components/Stepper/Stepper.module.css +++ b/src/components/Stepper/Stepper.module.css @@ -45,7 +45,7 @@ .border { width: 100%; height: 2px; - background-color: #f6f6f6; + background-color: var(--stepper-border-color, #f6f6f6); } .iconWrapper { @@ -59,6 +59,15 @@ height: 24px; } +/* Border color variants */ +.borderColorBlue { + --stepper-border-color: #3959cc; +} + +.borderColorGray { + --stepper-border-color: #f6f6f6; +} + /* Circle styles for different statuses */ .doneCircle { display: flex; @@ -85,7 +94,7 @@ width: 24px; height: 24px; background-color: #fff; - border: 2px solid #f6f6f6; + border: 2px solid var(--stepper-border-color, #f6f6f6); border-radius: 50%; } diff --git a/src/components/Stepper/Stepper.spec.tsx b/src/components/Stepper/Stepper.spec.tsx index 06d422f3..dd00cdd2 100644 --- a/src/components/Stepper/Stepper.spec.tsx +++ b/src/components/Stepper/Stepper.spec.tsx @@ -100,6 +100,33 @@ describe('', () => { expect(step0.className).toMatch(/current/); expect(step1.className).toMatch(/undone/); }); + + it('applies border color class correctly', () => { + render( + + + + , + ); + + const step0 = screen.getByTestId('step-0'); + const step1 = screen.getByTestId('step-1'); + + expect(step0.className).toMatch(/borderColorBlue/); + expect(step1.className).toMatch(/borderColorBlue/); + }); + + it('defaults to gray border color when borderColor prop is not specified', () => { + render( + + + + , + ); + + const step0 = screen.getByTestId('step-0'); + expect(step0.className).toMatch(/borderColorGray/); + }); }); describe('', () => { diff --git a/src/components/Stepper/Stepper.stories.tsx b/src/components/Stepper/Stepper.stories.tsx index b3484df8..f30eafe6 100644 --- a/src/components/Stepper/Stepper.stories.tsx +++ b/src/components/Stepper/Stepper.stories.tsx @@ -108,6 +108,30 @@ export const CustomIcons: Story = { ), }; +export const BorderColors: Story = { + render: () => ( +
+
+

Gray Border (デフォルト)

+ + + + + +
+ +
+

Blue Border

+ + + + + +
+
+ ), +}; + export const WithMargins: Story = { render: () => (
diff --git a/src/components/Stepper/Stepper.tsx b/src/components/Stepper/Stepper.tsx index df069399..dd467796 100644 --- a/src/components/Stepper/Stepper.tsx +++ b/src/components/Stepper/Stepper.tsx @@ -7,10 +7,12 @@ import type { CustomDataAttributeProps } from '../../types/attributes'; import type { Spacing } from '../../types/style'; export type StepStatus = 'current' | 'undone' | 'done'; +export type BorderColor = 'blue' | 'gray'; export interface StepperProps extends CustomDataAttributeProps { children: ReactNode[]; currentStep?: number; + borderColor?: BorderColor; // Margin props m?: Spacing; mx?: Spacing; @@ -23,7 +25,7 @@ export interface StepperProps extends CustomDataAttributeProps { export { StepperItem, type StepperItemProps } from './StepperItem'; -export const Stepper = ({ children, currentStep = 0, m, mx, my, mt, mr, mb, ml, ...props }: StepperProps) => { +export const Stepper = ({ children, currentStep = 0, borderColor = 'gray', m, mx, my, mt, mr, mb, ml, ...props }: StepperProps) => { const marginStyles = marginVariables({ m, mx, my, mt, mr, mb, ml }); const enhancedChildren = Children.map(children, (child, index) => { @@ -36,6 +38,7 @@ export const Stepper = ({ children, currentStep = 0, m, mx, my, mt, mr, mb, ml, status, isFirst: index === 0, isLast: index === children.length - 1, + borderColor, }); }); diff --git a/src/components/Stepper/StepperItem.tsx b/src/components/Stepper/StepperItem.tsx index 7f210da1..16fb3e56 100644 --- a/src/components/Stepper/StepperItem.tsx +++ b/src/components/Stepper/StepperItem.tsx @@ -3,7 +3,7 @@ import { clsx } from 'clsx'; import styles from './Stepper.module.css'; import { Icon } from '../Icon/Icon'; -import type { StepStatus } from './Stepper'; +import type { StepStatus, BorderColor } from './Stepper'; import type { CustomDataAttributeProps } from '../../types/attributes'; import type { IconName } from '../../types/icon'; @@ -15,6 +15,7 @@ export interface StepperItemProps extends CustomDataAttributeProps { status?: StepStatus; isFirst?: boolean; isLast?: boolean; + borderColor?: BorderColor; } export const StepperItem = ({ @@ -24,6 +25,7 @@ export const StepperItem = ({ status = 'undone', isFirst = false, isLast = false, + borderColor = 'gray', ...props }: StepperItemProps) => { const renderIcon = () => { @@ -59,6 +61,7 @@ export const StepperItem = ({ [styles.current]: status === 'current', [styles.done]: status === 'done', [styles.undone]: status === 'undone', + [styles[`borderColor${borderColor.charAt(0).toUpperCase() + borderColor.slice(1)}`]]: true, }); const leftBorderClass = clsx({ From 99cb974b7543fe62b4a304b68dd0ca733e64de10 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Wed, 4 Jun 2025 04:53:48 +0900 Subject: [PATCH 07/17] Use design tokens for colors --- src/components/Stepper/Stepper.module.css | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/Stepper/Stepper.module.css b/src/components/Stepper/Stepper.module.css index 8eb13025..bcd0b232 100644 --- a/src/components/Stepper/Stepper.module.css +++ b/src/components/Stepper/Stepper.module.css @@ -45,7 +45,7 @@ .border { width: 100%; height: 2px; - background-color: var(--stepper-border-color, #f6f6f6); + background-color: var(--stepper-border-color, var(--color-ubie-black-300)); } .iconWrapper { @@ -61,11 +61,11 @@ /* Border color variants */ .borderColorBlue { - --stepper-border-color: #3959cc; + --stepper-border-color: var(--color-ubie-blue-600); } .borderColorGray { - --stepper-border-color: #f6f6f6; + --stepper-border-color: var(--color-ubie-black-300); } /* Circle styles for different statuses */ @@ -76,9 +76,9 @@ width: 24px; height: 24px; padding: 2px; - color: #3959cc; + color: var(--color-ubie-blue-600); background-color: #fff; - border: 2px solid #3959cc; + border: 2px solid var(--color-ubie-blue-600); border-radius: 50%; } @@ -86,7 +86,7 @@ position: relative; width: 24px; height: 24px; - background-color: #3959cc; + background-color: var(--color-ubie-blue-600); border-radius: 50%; } @@ -94,21 +94,21 @@ width: 24px; height: 24px; background-color: #fff; - border: 2px solid var(--stepper-border-color, #f6f6f6); + border: 2px solid var(--stepper-border-color, var(--color-ubie-black-300)); border-radius: 50%; } /* Icon colors for custom icons */ .current .iconWrapper { - color: #3959cc; + color: var(--color-ubie-blue-600); } .done .iconWrapper { - color: #3959cc; + color: var(--color-ubie-blue-600); } .undone .iconWrapper { - color: #f6f6f6; + color: var(--color-ubie-black-300); } .label { From f050fa334553ec877fe1129c6bf2547878eab4a4 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Wed, 4 Jun 2025 04:55:11 +0900 Subject: [PATCH 08/17] Run format --- src/components/Stepper/Stepper.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/Stepper/Stepper.tsx b/src/components/Stepper/Stepper.tsx index dd467796..6bb137bc 100644 --- a/src/components/Stepper/Stepper.tsx +++ b/src/components/Stepper/Stepper.tsx @@ -25,7 +25,19 @@ export interface StepperProps extends CustomDataAttributeProps { export { StepperItem, type StepperItemProps } from './StepperItem'; -export const Stepper = ({ children, currentStep = 0, borderColor = 'gray', m, mx, my, mt, mr, mb, ml, ...props }: StepperProps) => { +export const Stepper = ({ + children, + currentStep = 0, + borderColor = 'gray', + m, + mx, + my, + mt, + mr, + mb, + ml, + ...props +}: StepperProps) => { const marginStyles = marginVariables({ m, mx, my, mt, mr, mb, ml }); const enhancedChildren = Children.map(children, (child, index) => { From 23dd35bf9c3a4f53c1a20048488ba1a22e46188f Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Wed, 4 Jun 2025 04:59:50 +0900 Subject: [PATCH 09/17] Add types for `children` --- src/components/Stepper/Stepper.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/Stepper/Stepper.tsx b/src/components/Stepper/Stepper.tsx index 6bb137bc..99d15760 100644 --- a/src/components/Stepper/Stepper.tsx +++ b/src/components/Stepper/Stepper.tsx @@ -1,8 +1,9 @@ 'use client'; -import { type CSSProperties, type ReactNode, Children, cloneElement, isValidElement } from 'react'; +import { type CSSProperties, Children, cloneElement, isValidElement, ReactElement } from 'react'; import styles from './Stepper.module.css'; import { marginVariables } from '../../utils/style'; +import type { StepperItemProps, StepperItem } from './StepperItem'; import type { CustomDataAttributeProps } from '../../types/attributes'; import type { Spacing } from '../../types/style'; @@ -10,7 +11,7 @@ export type StepStatus = 'current' | 'undone' | 'done'; export type BorderColor = 'blue' | 'gray'; export interface StepperProps extends CustomDataAttributeProps { - children: ReactNode[]; + children: ReactElement[]; currentStep?: number; borderColor?: BorderColor; // Margin props @@ -23,8 +24,6 @@ export interface StepperProps extends CustomDataAttributeProps { ml?: Spacing; } -export { StepperItem, type StepperItemProps } from './StepperItem'; - export const Stepper = ({ children, currentStep = 0, @@ -60,3 +59,5 @@ export const Stepper = ({
); }; + +export { StepperItem, StepperItemProps }; From 5945cb182c5a702f6631ac7c7e2ccdac4b3175cf Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Wed, 4 Jun 2025 14:03:10 +0900 Subject: [PATCH 10/17] Fix `export` for `StepperItem` --- src/components/Stepper/Stepper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Stepper/Stepper.tsx b/src/components/Stepper/Stepper.tsx index 99d15760..cc19fb97 100644 --- a/src/components/Stepper/Stepper.tsx +++ b/src/components/Stepper/Stepper.tsx @@ -60,4 +60,4 @@ export const Stepper = ({ ); }; -export { StepperItem, StepperItemProps }; +export { StepperItem, type StepperItemProps } from './StepperItem'; From 310f26155a100c9b98344803043d658178e18f1e Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Wed, 4 Jun 2025 16:29:02 +0900 Subject: [PATCH 11/17] Fix border coloring --- src/components/Stepper/Stepper.spec.tsx | 33 ++++++++++++++++++---- src/components/Stepper/Stepper.stories.tsx | 17 ++++++++--- src/components/Stepper/Stepper.tsx | 6 ++-- src/components/Stepper/StepperItem.tsx | 15 +++++++--- 4 files changed, 53 insertions(+), 18 deletions(-) diff --git a/src/components/Stepper/Stepper.spec.tsx b/src/components/Stepper/Stepper.spec.tsx index dd00cdd2..7ca47610 100644 --- a/src/components/Stepper/Stepper.spec.tsx +++ b/src/components/Stepper/Stepper.spec.tsx @@ -101,22 +101,35 @@ describe('', () => { expect(step1.className).toMatch(/undone/); }); - it('applies border color class correctly', () => { + it('applies correct border line colors based on step position', () => { render( - + + , ); const step0 = screen.getByTestId('step-0'); const step1 = screen.getByTestId('step-1'); + const step2 = screen.getByTestId('step-2'); + + // Step 0 (done): both borders should be blue (left and right of current step) + const step0RightBorder = step0.querySelector('[class*="rightBorder"] [class*="border"]'); + expect(step0RightBorder?.className).toMatch(/borderColorBlue/); - expect(step0.className).toMatch(/borderColorBlue/); - expect(step1.className).toMatch(/borderColorBlue/); + // Step 1 (current): left border blue, right border gray + const step1LeftBorder = step1.querySelector('[class*="leftBorder"] [class*="border"]'); + const step1RightBorder = step1.querySelector('[class*="rightBorder"] [class*="border"]'); + expect(step1LeftBorder?.className).toMatch(/borderColorBlue/); + expect(step1RightBorder?.className).toMatch(/borderColorGray/); + + // Step 2 (undone): left border gray + const step2LeftBorder = step2.querySelector('[class*="leftBorder"] [class*="border"]'); + expect(step2LeftBorder?.className).toMatch(/borderColorGray/); }); - it('defaults to gray border color when borderColor prop is not specified', () => { + it('applies gray border color for lines to the right of current step', () => { render( @@ -125,7 +138,15 @@ describe('', () => { ); const step0 = screen.getByTestId('step-0'); - expect(step0.className).toMatch(/borderColorGray/); + const step1 = screen.getByTestId('step-1'); + + // Step 0 (current): right border should be gray + const step0RightBorder = step0.querySelector('[class*="rightBorder"] [class*="border"]'); + expect(step0RightBorder?.className).toMatch(/borderColorGray/); + + // Step 1 (undone): left border should be gray + const step1LeftBorder = step1.querySelector('[class*="leftBorder"] [class*="border"]'); + expect(step1LeftBorder?.className).toMatch(/borderColorGray/); }); }); diff --git a/src/components/Stepper/Stepper.stories.tsx b/src/components/Stepper/Stepper.stories.tsx index f30eafe6..b8b0172c 100644 --- a/src/components/Stepper/Stepper.stories.tsx +++ b/src/components/Stepper/Stepper.stories.tsx @@ -108,11 +108,20 @@ export const CustomIcons: Story = { ), }; -export const BorderColors: Story = { +export const ProgressStates: Story = { render: () => (
-

Gray Border (デフォルト)

+

Step 0 (開始)

+ + + + + +
+ +
+

Step 1 (進行中)

@@ -121,8 +130,8 @@ export const BorderColors: Story = {
-

Blue Border

- +

Step 2 (完了間近)

+ diff --git a/src/components/Stepper/Stepper.tsx b/src/components/Stepper/Stepper.tsx index cc19fb97..2e4a8cc1 100644 --- a/src/components/Stepper/Stepper.tsx +++ b/src/components/Stepper/Stepper.tsx @@ -8,12 +8,10 @@ import type { CustomDataAttributeProps } from '../../types/attributes'; import type { Spacing } from '../../types/style'; export type StepStatus = 'current' | 'undone' | 'done'; -export type BorderColor = 'blue' | 'gray'; export interface StepperProps extends CustomDataAttributeProps { children: ReactElement[]; currentStep?: number; - borderColor?: BorderColor; // Margin props m?: Spacing; mx?: Spacing; @@ -27,7 +25,6 @@ export interface StepperProps extends CustomDataAttributeProps { export const Stepper = ({ children, currentStep = 0, - borderColor = 'gray', m, mx, my, @@ -49,7 +46,8 @@ export const Stepper = ({ status, isFirst: index === 0, isLast: index === children.length - 1, - borderColor, + stepIndex: index, + currentStep, }); }); diff --git a/src/components/Stepper/StepperItem.tsx b/src/components/Stepper/StepperItem.tsx index 16fb3e56..29e68d8f 100644 --- a/src/components/Stepper/StepperItem.tsx +++ b/src/components/Stepper/StepperItem.tsx @@ -3,7 +3,7 @@ import { clsx } from 'clsx'; import styles from './Stepper.module.css'; import { Icon } from '../Icon/Icon'; -import type { StepStatus, BorderColor } from './Stepper'; +import type { StepStatus } from './Stepper'; import type { CustomDataAttributeProps } from '../../types/attributes'; import type { IconName } from '../../types/icon'; @@ -15,7 +15,8 @@ export interface StepperItemProps extends CustomDataAttributeProps { status?: StepStatus; isFirst?: boolean; isLast?: boolean; - borderColor?: BorderColor; + stepIndex?: number; + currentStep?: number; } export const StepperItem = ({ @@ -25,7 +26,8 @@ export const StepperItem = ({ status = 'undone', isFirst = false, isLast = false, - borderColor = 'gray', + stepIndex = 0, + currentStep = 0, ...props }: StepperItemProps) => { const renderIcon = () => { @@ -61,7 +63,6 @@ export const StepperItem = ({ [styles.current]: status === 'current', [styles.done]: status === 'done', [styles.undone]: status === 'undone', - [styles[`borderColor${borderColor.charAt(0).toUpperCase() + borderColor.slice(1)}`]]: true, }); const leftBorderClass = clsx({ @@ -72,14 +73,20 @@ export const StepperItem = ({ [styles.rightBorder]: true, }); + // 左の線: 現在のステップより左(つまり stepIndex <= currentStep)の場合は青 const leftBorderLineClass = clsx({ [styles.border]: true, [styles.hidden]: isFirst, + [styles.borderColorBlue]: !isFirst && stepIndex <= currentStep, + [styles.borderColorGray]: !isFirst && stepIndex > currentStep, }); + // 右の線: 現在のステップより左(つまり stepIndex < currentStep)の場合は青 const rightBorderLineClass = clsx({ [styles.border]: true, [styles.hidden]: isLast, + [styles.borderColorBlue]: !isLast && stepIndex < currentStep, + [styles.borderColorGray]: !isLast && stepIndex >= currentStep, }); const labelClass = clsx({ From bb293281bb92c191f64e169dab36158c493845ef Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Wed, 4 Jun 2025 16:39:29 +0900 Subject: [PATCH 12/17] Add `__internal` prefix to `StepperItem` --- src/components/Stepper/Stepper.spec.tsx | 14 +++---- src/components/Stepper/Stepper.tsx | 23 +++-------- src/components/Stepper/StepperItem.tsx | 52 ++++++++++++------------- 3 files changed, 39 insertions(+), 50 deletions(-) diff --git a/src/components/Stepper/Stepper.spec.tsx b/src/components/Stepper/Stepper.spec.tsx index 7ca47610..b76b6d6d 100644 --- a/src/components/Stepper/Stepper.spec.tsx +++ b/src/components/Stepper/Stepper.spec.tsx @@ -157,19 +157,19 @@ describe('', () => { }); it('applies correct CSS classes based on status', () => { - const { rerender } = render(); + const { rerender } = render(); expect(screen.getByTestId('item').className).toMatch(/current/); - rerender(); + rerender(); expect(screen.getByTestId('item').className).toMatch(/done/); - rerender(); + rerender(); expect(screen.getByTestId('item').className).toMatch(/undone/); }); it('uses custom icon when provided', () => { - render(); + render(); // Icon component should be rendered (we can't easily test the actual icon) const iconWrapper = screen.getByTestId('item').querySelector('[class*="iconWrapper"]'); @@ -177,21 +177,21 @@ describe('', () => { }); it('uses custom done icon when status is done', () => { - render(); + render(); const iconWrapper = screen.getByTestId('item').querySelector('[class*="iconWrapper"]'); expect(iconWrapper).toBeInTheDocument(); }); it('hides borders correctly for first and last items', () => { - const { rerender } = render(); + const { rerender } = render(); const leftBorderLine = screen.getByTestId('item').querySelector('[class*="leftBorder"] [class*="border"]'); const rightBorderLine = screen.getByTestId('item').querySelector('[class*="rightBorder"] [class*="border"]'); expect(leftBorderLine?.className).toMatch(/hidden/); expect(rightBorderLine?.className).not.toMatch(/hidden/); - rerender(); + rerender(); const leftBorderLine2 = screen.getByTestId('item').querySelector('[class*="leftBorder"] [class*="border"]'); const rightBorderLine2 = screen.getByTestId('item').querySelector('[class*="rightBorder"] [class*="border"]'); expect(leftBorderLine2?.className).not.toMatch(/hidden/); diff --git a/src/components/Stepper/Stepper.tsx b/src/components/Stepper/Stepper.tsx index 2e4a8cc1..33bba66c 100644 --- a/src/components/Stepper/Stepper.tsx +++ b/src/components/Stepper/Stepper.tsx @@ -22,18 +22,7 @@ export interface StepperProps extends CustomDataAttributeProps { ml?: Spacing; } -export const Stepper = ({ - children, - currentStep = 0, - m, - mx, - my, - mt, - mr, - mb, - ml, - ...props -}: StepperProps) => { +export const Stepper = ({ children, currentStep = 0, m, mx, my, mt, mr, mb, ml, ...props }: StepperProps) => { const marginStyles = marginVariables({ m, mx, my, mt, mr, mb, ml }); const enhancedChildren = Children.map(children, (child, index) => { @@ -43,11 +32,11 @@ export const Stepper = ({ return cloneElement(child, { ...child.props, - status, - isFirst: index === 0, - isLast: index === children.length - 1, - stepIndex: index, - currentStep, + __internal_status: status, + __internal_isFirst: index === 0, + __internal_isLast: index === children.length - 1, + __internal_stepIndex: index, + __internal_currentStep: currentStep, }); }); diff --git a/src/components/Stepper/StepperItem.tsx b/src/components/Stepper/StepperItem.tsx index 29e68d8f..87c25101 100644 --- a/src/components/Stepper/StepperItem.tsx +++ b/src/components/Stepper/StepperItem.tsx @@ -12,35 +12,35 @@ export interface StepperItemProps extends CustomDataAttributeProps { icon?: IconName; doneIcon?: IconName; // Internal props (automatically set by Stepper) - status?: StepStatus; - isFirst?: boolean; - isLast?: boolean; - stepIndex?: number; - currentStep?: number; + __internal_status?: StepStatus; + __internal_isFirst?: boolean; + __internal_isLast?: boolean; + __internal_stepIndex?: number; + __internal_currentStep?: number; } export const StepperItem = ({ label, icon, doneIcon, - status = 'undone', - isFirst = false, - isLast = false, - stepIndex = 0, - currentStep = 0, + __internal_status = 'undone', + __internal_isFirst = false, + __internal_isLast = false, + __internal_stepIndex = 0, + __internal_currentStep = 0, ...props }: StepperItemProps) => { const renderIcon = () => { // カスタムアイコンが指定されている場合はそれを使用 - if (status === 'done' && doneIcon) { + if (__internal_status === 'done' && doneIcon) { return ; } - if ((status === 'current' || status === 'undone') && icon) { + if ((__internal_status === 'current' || __internal_status === 'undone') && icon) { return ; } // デフォルトの状態に応じた描画 - if (status === 'done') { + if (__internal_status === 'done') { // 白い丸で囲まれたチェックアイコン return (
@@ -49,7 +49,7 @@ export const StepperItem = ({ ); } - if (status === 'current') { + if (__internal_status === 'current') { // 塗りつぶされた青い丸 return
; } @@ -60,9 +60,9 @@ export const StepperItem = ({ const itemClass = clsx({ [styles.stepperItem]: true, - [styles.current]: status === 'current', - [styles.done]: status === 'done', - [styles.undone]: status === 'undone', + [styles.current]: __internal_status === 'current', + [styles.done]: __internal_status === 'done', + [styles.undone]: __internal_status === 'undone', }); const leftBorderClass = clsx({ @@ -76,24 +76,24 @@ export const StepperItem = ({ // 左の線: 現在のステップより左(つまり stepIndex <= currentStep)の場合は青 const leftBorderLineClass = clsx({ [styles.border]: true, - [styles.hidden]: isFirst, - [styles.borderColorBlue]: !isFirst && stepIndex <= currentStep, - [styles.borderColorGray]: !isFirst && stepIndex > currentStep, + [styles.hidden]: __internal_isFirst, + [styles.borderColorBlue]: !__internal_isFirst && __internal_stepIndex <= __internal_currentStep, + [styles.borderColorGray]: !__internal_isFirst && __internal_stepIndex > __internal_currentStep, }); // 右の線: 現在のステップより左(つまり stepIndex < currentStep)の場合は青 const rightBorderLineClass = clsx({ [styles.border]: true, - [styles.hidden]: isLast, - [styles.borderColorBlue]: !isLast && stepIndex < currentStep, - [styles.borderColorGray]: !isLast && stepIndex >= currentStep, + [styles.hidden]: __internal_isLast, + [styles.borderColorBlue]: !__internal_isLast && __internal_stepIndex < __internal_currentStep, + [styles.borderColorGray]: !__internal_isLast && __internal_stepIndex >= __internal_currentStep, }); const labelClass = clsx({ [styles.label]: true, - [styles.currentLabel]: status === 'current', - [styles.doneLabel]: status === 'done', - [styles.undoneLabel]: status === 'undone', + [styles.currentLabel]: __internal_status === 'current', + [styles.doneLabel]: __internal_status === 'done', + [styles.undoneLabel]: __internal_status === 'undone', }); return ( From 0b9d3a308bec8cfe6bd6da13911d2cb520e2aa03 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Thu, 5 Jun 2025 10:32:19 +0900 Subject: [PATCH 13/17] Remove `__internal` prefix --- src/components/Stepper/Stepper.spec.tsx | 143 ++++++-------- src/components/Stepper/Stepper.stories.tsx | 209 ++++++++++----------- src/components/Stepper/Stepper.tsx | 50 ++--- src/components/Stepper/StepperItem.tsx | 52 ++--- src/index.ts | 4 +- 5 files changed, 212 insertions(+), 246 deletions(-) diff --git a/src/components/Stepper/Stepper.spec.tsx b/src/components/Stepper/Stepper.spec.tsx index b76b6d6d..72d4e9f5 100644 --- a/src/components/Stepper/Stepper.spec.tsx +++ b/src/components/Stepper/Stepper.spec.tsx @@ -3,14 +3,8 @@ import { Stepper } from './Stepper'; import { StepperItem } from './StepperItem'; describe('', () => { - it('renders children correctly', () => { - render( - - - - - , - ); + it('renders steps correctly', () => { + render(); expect(screen.getByTestId('stepper')).toBeInTheDocument(); expect(screen.getByText('Step 1')).toBeInTheDocument(); @@ -19,65 +13,48 @@ describe('', () => { }); it('applies correct status based on currentStep', () => { - render( - - - - - , - ); + render(); - const step0 = screen.getByTestId('step-0'); - const step1 = screen.getByTestId('step-1'); - const step2 = screen.getByTestId('step-2'); + const step0 = screen.getByText('Step 1').closest('[class*="stepperItem"]'); + const step1 = screen.getByText('Step 2').closest('[class*="stepperItem"]'); + const step2 = screen.getByText('Step 3').closest('[class*="stepperItem"]'); // Step 0 should be done (index < currentStep) - expect(step0.className).toMatch(/done/); + expect(step0?.className).toMatch(/done/); // Step 1 should be current (index === currentStep) - expect(step1.className).toMatch(/current/); + expect(step1?.className).toMatch(/current/); // Step 2 should be undone (index > currentStep) - expect(step2.className).toMatch(/undone/); + expect(step2?.className).toMatch(/undone/); }); it('sets first and last child properties correctly', () => { - render( - - - - - , - ); + render(); - const first = screen.getByTestId('first'); - const middle = screen.getByTestId('middle'); - const last = screen.getByTestId('last'); + const first = screen.getByText('First').closest('[class*="stepperItem"]'); + const middle = screen.getByText('Middle').closest('[class*="stepperItem"]'); + const last = screen.getByText('Last').closest('[class*="stepperItem"]'); // First item should hide left border line - const firstLeftBorderLine = first.querySelector('[class*="leftBorder"] [class*="border"]'); - const firstRightBorderLine = first.querySelector('[class*="rightBorder"] [class*="border"]'); + const firstLeftBorderLine = first?.querySelector('[class*="leftBorder"] [class*="border"]'); + const firstRightBorderLine = first?.querySelector('[class*="rightBorder"] [class*="border"]'); expect(firstLeftBorderLine?.className).toMatch(/hidden/); expect(firstRightBorderLine?.className).not.toMatch(/hidden/); // Middle item should show both border lines - const middleLeftBorderLine = middle.querySelector('[class*="leftBorder"] [class*="border"]'); - const middleRightBorderLine = middle.querySelector('[class*="rightBorder"] [class*="border"]'); + const middleLeftBorderLine = middle?.querySelector('[class*="leftBorder"] [class*="border"]'); + const middleRightBorderLine = middle?.querySelector('[class*="rightBorder"] [class*="border"]'); expect(middleLeftBorderLine?.className).not.toMatch(/hidden/); expect(middleRightBorderLine?.className).not.toMatch(/hidden/); // Last item should hide right border line - const lastLeftBorderLine = last.querySelector('[class*="leftBorder"] [class*="border"]'); - const lastRightBorderLine = last.querySelector('[class*="rightBorder"] [class*="border"]'); + const lastLeftBorderLine = last?.querySelector('[class*="leftBorder"] [class*="border"]'); + const lastRightBorderLine = last?.querySelector('[class*="rightBorder"] [class*="border"]'); expect(lastLeftBorderLine?.className).not.toMatch(/hidden/); expect(lastRightBorderLine?.className).toMatch(/hidden/); }); it('has all margins through m prop', () => { - render( - - - - , - ); + render(); const stepper = screen.getByTestId('stepper'); expect(stepper).toHaveStyle('--margin-top: var(--size-spacing-xxs)'); @@ -87,65 +64,49 @@ describe('', () => { }); it('defaults to currentStep 0 when not specified', () => { - render( - - - - , - ); + render(); - const step0 = screen.getByTestId('step-0'); - const step1 = screen.getByTestId('step-1'); + const step0 = screen.getByText('Step 1').closest('[class*="stepperItem"]'); + const step1 = screen.getByText('Step 2').closest('[class*="stepperItem"]'); - expect(step0.className).toMatch(/current/); - expect(step1.className).toMatch(/undone/); + expect(step0?.className).toMatch(/current/); + expect(step1?.className).toMatch(/undone/); }); it('applies correct border line colors based on step position', () => { - render( - - - - - , - ); + render(); - const step0 = screen.getByTestId('step-0'); - const step1 = screen.getByTestId('step-1'); - const step2 = screen.getByTestId('step-2'); + const step0 = screen.getByText('Step 1').closest('[class*="stepperItem"]'); + const step1 = screen.getByText('Step 2').closest('[class*="stepperItem"]'); + const step2 = screen.getByText('Step 3').closest('[class*="stepperItem"]'); // Step 0 (done): both borders should be blue (left and right of current step) - const step0RightBorder = step0.querySelector('[class*="rightBorder"] [class*="border"]'); + const step0RightBorder = step0?.querySelector('[class*="rightBorder"] [class*="border"]'); expect(step0RightBorder?.className).toMatch(/borderColorBlue/); // Step 1 (current): left border blue, right border gray - const step1LeftBorder = step1.querySelector('[class*="leftBorder"] [class*="border"]'); - const step1RightBorder = step1.querySelector('[class*="rightBorder"] [class*="border"]'); + const step1LeftBorder = step1?.querySelector('[class*="leftBorder"] [class*="border"]'); + const step1RightBorder = step1?.querySelector('[class*="rightBorder"] [class*="border"]'); expect(step1LeftBorder?.className).toMatch(/borderColorBlue/); expect(step1RightBorder?.className).toMatch(/borderColorGray/); // Step 2 (undone): left border gray - const step2LeftBorder = step2.querySelector('[class*="leftBorder"] [class*="border"]'); + const step2LeftBorder = step2?.querySelector('[class*="leftBorder"] [class*="border"]'); expect(step2LeftBorder?.className).toMatch(/borderColorGray/); }); it('applies gray border color for lines to the right of current step', () => { - render( - - - - , - ); + render(); - const step0 = screen.getByTestId('step-0'); - const step1 = screen.getByTestId('step-1'); + const step0 = screen.getByText('Step 1').closest('[class*="stepperItem"]'); + const step1 = screen.getByText('Step 2').closest('[class*="stepperItem"]'); // Step 0 (current): right border should be gray - const step0RightBorder = step0.querySelector('[class*="rightBorder"] [class*="border"]'); + const step0RightBorder = step0?.querySelector('[class*="rightBorder"] [class*="border"]'); expect(step0RightBorder?.className).toMatch(/borderColorGray/); // Step 1 (undone): left border should be gray - const step1LeftBorder = step1.querySelector('[class*="leftBorder"] [class*="border"]'); + const step1LeftBorder = step1?.querySelector('[class*="leftBorder"] [class*="border"]'); expect(step1LeftBorder?.className).toMatch(/borderColorGray/); }); }); @@ -157,41 +118,45 @@ describe('', () => { }); it('applies correct CSS classes based on status', () => { - const { rerender } = render(); + const { rerender } = render(); expect(screen.getByTestId('item').className).toMatch(/current/); - rerender(); + rerender(); expect(screen.getByTestId('item').className).toMatch(/done/); - rerender(); + rerender(); expect(screen.getByTestId('item').className).toMatch(/undone/); }); - it('uses custom icon when provided', () => { - render(); + it('renders custom icons when provided in steps', () => { + render(); - // Icon component should be rendered (we can't easily test the actual icon) - const iconWrapper = screen.getByTestId('item').querySelector('[class*="iconWrapper"]'); + // Icon component should be rendered for the first step + const step0 = screen.getByText('Home').closest('[class*="stepperItem"]'); + const iconWrapper = step0?.querySelector('[class*="iconWrapper"]'); expect(iconWrapper).toBeInTheDocument(); }); - it('uses custom done icon when status is done', () => { - render(); + it('uses custom done icon when step is completed', () => { + render( + , + ); - const iconWrapper = screen.getByTestId('item').querySelector('[class*="iconWrapper"]'); + const step0 = screen.getByText('Done Step').closest('[class*="stepperItem"]'); + const iconWrapper = step0?.querySelector('[class*="iconWrapper"]'); expect(iconWrapper).toBeInTheDocument(); }); it('hides borders correctly for first and last items', () => { - const { rerender } = render(); + const { rerender } = render(); const leftBorderLine = screen.getByTestId('item').querySelector('[class*="leftBorder"] [class*="border"]'); const rightBorderLine = screen.getByTestId('item').querySelector('[class*="rightBorder"] [class*="border"]'); expect(leftBorderLine?.className).toMatch(/hidden/); expect(rightBorderLine?.className).not.toMatch(/hidden/); - rerender(); + rerender(); const leftBorderLine2 = screen.getByTestId('item').querySelector('[class*="leftBorder"] [class*="border"]'); const rightBorderLine2 = screen.getByTestId('item').querySelector('[class*="rightBorder"] [class*="border"]'); expect(leftBorderLine2?.className).not.toMatch(/hidden/); diff --git a/src/components/Stepper/Stepper.stories.tsx b/src/components/Stepper/Stepper.stories.tsx index b8b0172c..47cc71b1 100644 --- a/src/components/Stepper/Stepper.stories.tsx +++ b/src/components/Stepper/Stepper.stories.tsx @@ -1,7 +1,6 @@ import { Meta, StoryObj } from '@storybook/react'; import { ComponentProps } from 'react'; import { Stepper } from './Stepper'; -import { StepperItem } from './StepperItem'; export default { title: 'Navigation/Stepper', @@ -23,132 +22,124 @@ const defaultArgs = { export const Default: Story = { render: (args) => ( - - - - - + ), args: defaultArgs, }; export const ThreeSteps: Story = { - render: () => ( -
-
-

Current Step: 0

- - - - - + render: () => { + const steps = [{ label: 'ステップ1' }, { label: 'ステップ2' }, { label: 'ステップ3' }]; + + return ( +
+
+

Current Step: 0

+ +
+ +
+

Current Step: 1

+ +
+ +
+

Current Step: 2

+ +
- -
-

Current Step: 1

- - - - - -
- -
-

Current Step: 2

- - - - - -
-
- ), + ); + }, }; export const FourSteps: Story = { render: () => ( - - - - - - + ), }; export const FiveSteps: Story = { render: () => ( - - - - - - - + ), }; export const LongLabels: Story = { render: () => ( - - - - - + ), }; export const CustomIcons: Story = { render: () => ( - - - - - + ), }; export const ProgressStates: Story = { - render: () => ( -
-
-

Step 0 (開始)

- - - - - + render: () => { + const steps = [{ label: 'ステップ1' }, { label: 'ステップ2' }, { label: 'ステップ3' }]; + + return ( +
+
+

Step 0 (開始)

+ +
+ +
+

Step 1 (進行中)

+ +
+ +
+

Step 2 (完了間近)

+ +
- -
-

Step 1 (進行中)

- - - - - -
- -
-

Step 2 (完了間近)

- - - - - -
-
- ), + ); + }, }; export const WithMargins: Story = { render: () => (
- - - - - +
), }; @@ -158,30 +149,32 @@ export const DifferentWidths: Story = {

幅280px

- - - - - +

幅440px

- - - - - - +

幅640px

- - - - - +
), diff --git a/src/components/Stepper/Stepper.tsx b/src/components/Stepper/Stepper.tsx index 33bba66c..2c32ba12 100644 --- a/src/components/Stepper/Stepper.tsx +++ b/src/components/Stepper/Stepper.tsx @@ -1,16 +1,23 @@ 'use client'; -import { type CSSProperties, Children, cloneElement, isValidElement, ReactElement } from 'react'; +import { type CSSProperties } from 'react'; import styles from './Stepper.module.css'; +import { StepperItem } from './StepperItem'; import { marginVariables } from '../../utils/style'; -import type { StepperItemProps, StepperItem } from './StepperItem'; import type { CustomDataAttributeProps } from '../../types/attributes'; +import type { IconName } from '../../types/icon'; import type { Spacing } from '../../types/style'; export type StepStatus = 'current' | 'undone' | 'done'; +export interface StepData { + label: string; + icon?: IconName; + doneIcon?: IconName; +} + export interface StepperProps extends CustomDataAttributeProps { - children: ReactElement[]; + steps: StepData[]; currentStep?: number; // Margin props m?: Spacing; @@ -22,29 +29,30 @@ export interface StepperProps extends CustomDataAttributeProps { ml?: Spacing; } -export const Stepper = ({ children, currentStep = 0, m, mx, my, mt, mr, mb, ml, ...props }: StepperProps) => { +export const Stepper = ({ steps, currentStep = 0, m, mx, my, mt, mr, mb, ml, ...props }: StepperProps) => { const marginStyles = marginVariables({ m, mx, my, mt, mr, mb, ml }); - const enhancedChildren = Children.map(children, (child, index) => { - if (!isValidElement(child)) return child; - - const status: StepStatus = index < currentStep ? 'done' : index === currentStep ? 'current' : 'undone'; - - return cloneElement(child, { - ...child.props, - __internal_status: status, - __internal_isFirst: index === 0, - __internal_isLast: index === children.length - 1, - __internal_stepIndex: index, - __internal_currentStep: currentStep, - }); - }); - return (
- {enhancedChildren} + {steps.map((step, index) => { + const status: StepStatus = index < currentStep ? 'done' : index === currentStep ? 'current' : 'undone'; + + return ( + + ); + })}
); }; -export { StepperItem, type StepperItemProps } from './StepperItem'; +export type { StepperItemProps } from './StepperItem'; diff --git a/src/components/Stepper/StepperItem.tsx b/src/components/Stepper/StepperItem.tsx index 87c25101..29e68d8f 100644 --- a/src/components/Stepper/StepperItem.tsx +++ b/src/components/Stepper/StepperItem.tsx @@ -12,35 +12,35 @@ export interface StepperItemProps extends CustomDataAttributeProps { icon?: IconName; doneIcon?: IconName; // Internal props (automatically set by Stepper) - __internal_status?: StepStatus; - __internal_isFirst?: boolean; - __internal_isLast?: boolean; - __internal_stepIndex?: number; - __internal_currentStep?: number; + status?: StepStatus; + isFirst?: boolean; + isLast?: boolean; + stepIndex?: number; + currentStep?: number; } export const StepperItem = ({ label, icon, doneIcon, - __internal_status = 'undone', - __internal_isFirst = false, - __internal_isLast = false, - __internal_stepIndex = 0, - __internal_currentStep = 0, + status = 'undone', + isFirst = false, + isLast = false, + stepIndex = 0, + currentStep = 0, ...props }: StepperItemProps) => { const renderIcon = () => { // カスタムアイコンが指定されている場合はそれを使用 - if (__internal_status === 'done' && doneIcon) { + if (status === 'done' && doneIcon) { return ; } - if ((__internal_status === 'current' || __internal_status === 'undone') && icon) { + if ((status === 'current' || status === 'undone') && icon) { return ; } // デフォルトの状態に応じた描画 - if (__internal_status === 'done') { + if (status === 'done') { // 白い丸で囲まれたチェックアイコン return (
@@ -49,7 +49,7 @@ export const StepperItem = ({ ); } - if (__internal_status === 'current') { + if (status === 'current') { // 塗りつぶされた青い丸 return
; } @@ -60,9 +60,9 @@ export const StepperItem = ({ const itemClass = clsx({ [styles.stepperItem]: true, - [styles.current]: __internal_status === 'current', - [styles.done]: __internal_status === 'done', - [styles.undone]: __internal_status === 'undone', + [styles.current]: status === 'current', + [styles.done]: status === 'done', + [styles.undone]: status === 'undone', }); const leftBorderClass = clsx({ @@ -76,24 +76,24 @@ export const StepperItem = ({ // 左の線: 現在のステップより左(つまり stepIndex <= currentStep)の場合は青 const leftBorderLineClass = clsx({ [styles.border]: true, - [styles.hidden]: __internal_isFirst, - [styles.borderColorBlue]: !__internal_isFirst && __internal_stepIndex <= __internal_currentStep, - [styles.borderColorGray]: !__internal_isFirst && __internal_stepIndex > __internal_currentStep, + [styles.hidden]: isFirst, + [styles.borderColorBlue]: !isFirst && stepIndex <= currentStep, + [styles.borderColorGray]: !isFirst && stepIndex > currentStep, }); // 右の線: 現在のステップより左(つまり stepIndex < currentStep)の場合は青 const rightBorderLineClass = clsx({ [styles.border]: true, - [styles.hidden]: __internal_isLast, - [styles.borderColorBlue]: !__internal_isLast && __internal_stepIndex < __internal_currentStep, - [styles.borderColorGray]: !__internal_isLast && __internal_stepIndex >= __internal_currentStep, + [styles.hidden]: isLast, + [styles.borderColorBlue]: !isLast && stepIndex < currentStep, + [styles.borderColorGray]: !isLast && stepIndex >= currentStep, }); const labelClass = clsx({ [styles.label]: true, - [styles.currentLabel]: __internal_status === 'current', - [styles.doneLabel]: __internal_status === 'done', - [styles.undoneLabel]: __internal_status === 'undone', + [styles.currentLabel]: status === 'current', + [styles.doneLabel]: status === 'done', + [styles.undoneLabel]: status === 'undone', }); return ( diff --git a/src/index.ts b/src/index.ts index ec9f5b2e..cf06ab68 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,8 +29,8 @@ export { Select } from './components/Select/Select'; export { Text } from './components/Text/Text'; export { TextArea } from './components/TextArea/TextArea'; export { Stack } from './components/Stack/Stack'; -export { Stepper, StepperItem } from './components/Stepper/Stepper'; +export { Stepper } from './components/Stepper/Stepper'; export { Toggle } from './components/Toggle/Toggle'; export type { IconName } from './types/icon'; -export type { StepperProps, StepperItemProps, StepStatus } from './components/Stepper/Stepper'; +export type { StepperProps, StepperItemProps, StepStatus, StepData } from './components/Stepper/Stepper'; From 57ae7250d0fda05f14500a8d89cb2450d42c32c2 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Thu, 5 Jun 2025 10:57:27 +0900 Subject: [PATCH 14/17] Add ineer space --- src/components/Stepper/Stepper.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Stepper/Stepper.module.css b/src/components/Stepper/Stepper.module.css index bcd0b232..44927758 100644 --- a/src/components/Stepper/Stepper.module.css +++ b/src/components/Stepper/Stepper.module.css @@ -28,7 +28,7 @@ .iconSpacer { flex-shrink: 0; - width: 24px; + width: 32px; } .leftBorder, From cd5115ccddd74ae3b600f7f5b8d83f357f7a077d Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Thu, 5 Jun 2025 11:16:03 +0900 Subject: [PATCH 15/17] Remove needless export type --- src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index cf06ab68..421f9d0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,4 +33,3 @@ export { Stepper } from './components/Stepper/Stepper'; export { Toggle } from './components/Toggle/Toggle'; export type { IconName } from './types/icon'; -export type { StepperProps, StepperItemProps, StepStatus, StepData } from './components/Stepper/Stepper'; From d82c7264da700b92766f00e00b3d07870811cd5c Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Thu, 5 Jun 2025 11:18:11 +0900 Subject: [PATCH 16/17] Remove outedated comment --- src/components/Stepper/StepperItem.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Stepper/StepperItem.tsx b/src/components/Stepper/StepperItem.tsx index 29e68d8f..0b8a5b04 100644 --- a/src/components/Stepper/StepperItem.tsx +++ b/src/components/Stepper/StepperItem.tsx @@ -11,7 +11,6 @@ export interface StepperItemProps extends CustomDataAttributeProps { label: string; icon?: IconName; doneIcon?: IconName; - // Internal props (automatically set by Stepper) status?: StepStatus; isFirst?: boolean; isLast?: boolean; From 3a7a70085397e04e43e5e0f8dda74ef0b33987dc Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Thu, 5 Jun 2025 16:53:16 +0900 Subject: [PATCH 17/17] Support line breaks --- src/components/Stepper/Stepper.module.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Stepper/Stepper.module.css b/src/components/Stepper/Stepper.module.css index 44927758..f950f085 100644 --- a/src/components/Stepper/Stepper.module.css +++ b/src/components/Stepper/Stepper.module.css @@ -118,6 +118,7 @@ line-height: 1.5; text-align: center; word-wrap: break-word; + white-space: pre-wrap; } .currentLabel {