diff --git a/src/components/DatePicker/DatePicker.stories.tsx b/src/components/DatePicker/DatePicker.stories.tsx new file mode 100644 index 0000000..8c2d58f --- /dev/null +++ b/src/components/DatePicker/DatePicker.stories.tsx @@ -0,0 +1,406 @@ +import { CalendarDate, getLocalTimeZone, today } from '@internationalized/date'; +import type { Meta, StoryObj } from '@storybook/react'; +import React, { useState } from 'react'; +import { + Calendar, + CalendarCell, + CalendarGrid, + CalendarGridBody, + CalendarGridHeader, + CalendarHeaderCell, + DatePicker as ReactAriaDatePicker, + Dialog, + Popover, + type DateValue, +} from 'react-aria-components'; +import { + Button, + DatePicker, + DatePickerCalendarButton, + DatePickerDate, + DatePickerMonth, + DatePickerYear, + ErrorText, + Legend, + RequirementBadge, + Select, + SupportText, +} from '..'; + +/** + * Defaultタイプのデートピッカー。 + * 左右キーで年月日フィールド間を移動する機能を持ちます。 + * + * SeparatedタイプはSeparatedDatePickerコンポーネントを参照してください。 + */ +const meta = { + title: 'Component/DADS v2/DatePicker/DatePicker', + tags: ['autodocs'], + component: DatePicker, + argTypes: { + size: { + type: 'string', + description: 'デートピッカーのサイズを以下から選択します。', + control: { type: 'radio' }, + options: ['lg', 'md', 'sm'], + table: { + defaultValue: { summary: 'lg' }, + type: { summary: "'lg' | 'md' | 'sm'" }, + }, + }, + isError: { + description: 'エラー状態であるかどうかを指定します。', + control: { type: 'boolean' }, + table: { + defaultValue: { summary: 'false' }, + type: { summary: 'boolean' }, + }, + }, + isDisabled: { + description: '無効状態であるかどうかを指定します。', + control: { type: 'boolean' }, + table: { + defaultValue: { summary: 'false' }, + type: { summary: 'boolean' }, + }, + }, + }, + args: { + size: 'lg', + isError: false, + isDisabled: false, + children: ({ yearRef, monthRef, dateRef }) => ( + <> + + + + > + ), + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = {}; + +/** 項目ラベルやサポートテキスト等と一緒に表示した例 */ +export const WithFieldset: Story = { + render({ ...args }) { + return ( + + + 日付※必須 + + 例:2025年01月20日 + + {({ yearRef, monthRef, dateRef }) => ( + <> + + + + > + )} + + + ); + }, +}; + +/** + * 項目ラベルやサポートテキスト等と一緒に表示した例 + * + * Defaultタイプはすべての入力フィールドがエラー状態になります。 + */ +export const Errored: Story = { + args: { + isError: true, + }, + render({ ...args }) { + return ( + + + 日付※必須 + + 例:2025年01月20日 + + {({ yearRef, monthRef, dateRef }) => ( + <> + + + + > + )} + + *年を入力してください。 + + ); + }, +}; + +/** + * Disabled状態を表示した例 + */ +export const Disabled: Story = { + args: { + isDisabled: true, + }, + render({ ...args }) { + return ( + + + 日付※任意 + + 例:2025年01月20日 + + {({ yearRef, monthRef, dateRef }) => ( + <> + + + + > + )} + + + ); + }, +}; + +/** カレンダーで日付を選択できるようにした例 */ +export const WithCalendar: Story = { + render({ size, ...args }) { + const [yearInput, setYearInput] = useState(''); + const [monthInput, setMonthInput] = useState(''); + const [dayInput, setDayInput] = useState(''); + + function handleCalendarChange(newDate: DateValue | null) { + if (newDate !== null) { + setYearInput(String(newDate.year)); + setMonthInput(String(newDate.month).padStart(2, '0')); + setDayInput(String(newDate.day).padStart(2, '0')); + } else { + setYearInput(''); + setMonthInput(''); + setDayInput(''); + } + } + + return ( + + {({ state }) => { + const updateCalendarDate = () => { + const year = Number.parseInt(yearInput); + const month = Number.parseInt(monthInput); + const day = Number.parseInt(dayInput); + + if (!Number.isNaN(year) && !Number.isNaN(month) && !Number.isNaN(day)) { + state.setValue(new CalendarDate(year, month, day)); + } else { + state.setValue(null); + } + }; + + return ( + <> + + {({ yearRef, monthRef, dateRef }) => ( + <> + setYearInput(e.target.value)} + onBlur={updateCalendarDate} + /> + setMonthInput(e.target.value)} + onBlur={updateCalendarDate} + /> + setDayInput(e.target.value)} + onBlur={updateCalendarDate} + /> + > + )} + + state.setOpen(true)} + /> + + + + {({ state: calendarState }) => ( + <> + + + calendarState.setFocusedDate( + calendarState.focusedDate.set({ + year: Number(event.target.value), + }), + ) + } + aria-label='年' + > + 2018年(平成30年) + 2019年(令和元年) + 2020年(令和2年) + 2021年(令和3年) + 2022年(令和4年) + 2023年(令和5年) + 2024年(令和6年) + 2025年(令和7年) + 2026年(令和8年) + 2027年(令和9年) + 2028年(令和10年) + 2029年(令和11年) + 2030年(令和12年) + + + calendarState.focusPreviousPage()} + > + + + + + {calendarState.focusedDate.month}月 + calendarState.focusNextPage()} + > + + + + + + + + + {(day) => ( + + {day} + + )} + + + {(date) => ( + + )} + + + + state.close()}> + 閉じる + + calendarState.selectDate(today(getLocalTimeZone()))} + > + 今日 + + + > + )} + + + + > + ); + }} + + ); + }, +}; + +/** 年月または月日のみのタイプ */ +export const Partial: Story = { + render({ ...args }) { + return ( + + + {({ yearRef, monthRef }) => ( + <> + + + > + )} + + + + {({ monthRef, dateRef }) => ( + <> + + + > + )} + + + ); + }, +}; diff --git a/src/components/DatePicker/DatePicker.tsx b/src/components/DatePicker/DatePicker.tsx new file mode 100644 index 0000000..fec648e --- /dev/null +++ b/src/components/DatePicker/DatePicker.tsx @@ -0,0 +1,77 @@ +import { type ComponentProps, type KeyboardEvent, type Ref, useRef } from 'react'; + +export type DatePickerSize = 'lg' | 'md' | 'sm'; + +export type DatePickerProps = Omit, 'children'> & { + size?: DatePickerSize; + isError?: boolean; + isDisabled?: boolean; + children: (props: { + yearRef: Ref; + monthRef: Ref; + dateRef: Ref; + }) => JSX.Element; +}; + +export const DatePicker = (props: DatePickerProps) => { + const { className, size = 'lg', isError, isDisabled, children, ...rest } = props; + + const yearRef = useRef(null); + const monthRef = useRef(null); + const dateRef = useRef(null); + + function handleKeyDown(event: KeyboardEvent) { + if (event.key === 'ArrowRight') { + moveRight(event); + } else if (event.key === 'ArrowLeft') { + moveLeft(event); + } else if (event.key.match(/^[^0-9]$/)) { + if (!event.ctrlKey && !event.metaKey) { + event.preventDefault(); + } + } + } + + function moveRight(event: KeyboardEvent) { + const input = event.target as HTMLInputElement; + if (input.selectionStart !== input.selectionEnd) { + return; + } + if (input.selectionEnd === input.value.length) { + event.preventDefault(); + if (input === yearRef.current) { + monthRef.current?.focus(); + } else if (input === monthRef.current) { + dateRef.current?.focus(); + } + } + } + + function moveLeft(event: KeyboardEvent) { + const input = event.target as HTMLInputElement; + if (input.selectionStart !== input.selectionEnd) { + return; + } + if (input.selectionStart === 0) { + event.preventDefault(); + if (input === monthRef.current) { + yearRef.current?.focus(); + } else if (input === dateRef.current) { + monthRef.current?.focus(); + } + } + } + + return ( + + {children({ yearRef, monthRef, dateRef })} + + ); +}; diff --git a/src/components/DatePicker/index.ts b/src/components/DatePicker/index.ts new file mode 100644 index 0000000..4344c21 --- /dev/null +++ b/src/components/DatePicker/index.ts @@ -0,0 +1,5 @@ +export * from './DatePicker'; +export * from './parts/DatePickerCalendarButton'; +export * from './parts/DatePickerDate'; +export * from './parts/DatePickerMonth'; +export * from './parts/DatePickerYear'; diff --git a/src/components/DatePicker/parts/DatePickerCalendarButton.tsx b/src/components/DatePicker/parts/DatePickerCalendarButton.tsx new file mode 100644 index 0000000..1c16eec --- /dev/null +++ b/src/components/DatePicker/parts/DatePickerCalendarButton.tsx @@ -0,0 +1,33 @@ +import { type ComponentProps, forwardRef } from 'react'; +import type { DatePickerSize } from '../DatePicker'; + +export type DatePickerCalendarButtonProps = ComponentProps<'button'> & { + size?: DatePickerSize; +}; + +export const DatePickerCalendarButton = forwardRef< + HTMLButtonElement, + DatePickerCalendarButtonProps +>((props, ref) => { + const { className, size = 'lg', ...rest } = props; + + return ( + + + + + + + + + ); +}); diff --git a/src/components/DatePicker/parts/DatePickerDate.tsx b/src/components/DatePicker/parts/DatePickerDate.tsx new file mode 100644 index 0000000..a27bb9a --- /dev/null +++ b/src/components/DatePicker/parts/DatePickerDate.tsx @@ -0,0 +1,23 @@ +import { type ComponentProps, forwardRef } from 'react'; + +export type DatePickerDateProps = ComponentProps<'input'> & {}; + +export const DatePickerDate = forwardRef((props, ref) => { + const { className, 'aria-disabled': disabled, readOnly, ...rest } = props; + + return ( + + 日 + + + ); +}); diff --git a/src/components/DatePicker/parts/DatePickerMonth.tsx b/src/components/DatePicker/parts/DatePickerMonth.tsx new file mode 100644 index 0000000..b4b8c77 --- /dev/null +++ b/src/components/DatePicker/parts/DatePickerMonth.tsx @@ -0,0 +1,23 @@ +import { type ComponentProps, forwardRef } from 'react'; + +export type DatePickerMonthProps = ComponentProps<'input'> & {}; + +export const DatePickerMonth = forwardRef((props, ref) => { + const { className, 'aria-disabled': disabled, readOnly, ...rest } = props; + + return ( + + 月 + + + ); +}); diff --git a/src/components/DatePicker/parts/DatePickerYear.tsx b/src/components/DatePicker/parts/DatePickerYear.tsx new file mode 100644 index 0000000..b651774 --- /dev/null +++ b/src/components/DatePicker/parts/DatePickerYear.tsx @@ -0,0 +1,23 @@ +import { type ComponentProps, forwardRef } from 'react'; + +export type DatePickerYearProps = ComponentProps<'input'> & {}; + +export const DatePickerYear = forwardRef((props, ref) => { + const { className, 'aria-disabled': disabled, readOnly, ...rest } = props; + + return ( + + 年 + + + ); +}); diff --git a/src/components/Divider/Divider.tsx b/src/components/Divider/Divider.tsx index 30b20b1..d4e7137 100644 --- a/src/components/Divider/Divider.tsx +++ b/src/components/Divider/Divider.tsx @@ -2,12 +2,6 @@ import type { ComponentProps } from 'react'; export type DividerColor = 'gray-420' | 'gray-536' | 'black'; -export const DividerColorStyle: { [key in DividerColor]: string } = { - 'gray-420': 'border-solid-gray-420', - 'gray-536': 'border-solid-gray-536', - black: 'border-black', -}; - export type DividerProps = ComponentProps<'hr'> & { color?: DividerColor; }; @@ -15,5 +9,14 @@ export type DividerProps = ComponentProps<'hr'> & { export const Divider = (props: DividerProps) => { const { className, color = 'gray-420', ...rest } = props; - return ; + return ( + + ); }; diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index 6029d35..b378613 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -2,15 +2,6 @@ import { type ComponentProps, forwardRef } from 'react'; export type InputBlockSize = 'lg' | 'md' | 'sm'; -export const InputBlockSizeStyle: { [key in InputBlockSize]: string } = { - // NOTE: - // Tailwind CSS (v3.4.4) does not have any utility classes for logical properties of sizing. - // Once it is officially released, we will replace them with classes like `bs-14`. - lg: 'h-14', - md: 'h-12', - sm: 'h-10', -}; - export type InputProps = ComponentProps<'input'> & { isError?: boolean; blockSize?: InputBlockSize; @@ -22,13 +13,16 @@ export const Input = forwardRef((props, ref) => { return ( & { size?: LabelSize; }; @@ -20,9 +14,10 @@ export const Label = (props: LabelProps) => { {children} diff --git a/src/components/LanguageSelector/parts/Menu.tsx b/src/components/LanguageSelector/parts/Menu.tsx index 901f61d..e24c3b6 100644 --- a/src/components/LanguageSelector/parts/Menu.tsx +++ b/src/components/LanguageSelector/parts/Menu.tsx @@ -11,11 +11,11 @@ export const LanguageSelectorMenu = forwardRef:nth-child(7)]:rounded-r-none - ${isCondensed ? 'max-h-[calc((32*6.5+16)/16*1rem)]' : 'max-h-[calc((44*6.5+16)/16*1rem)]'} - ${className ?? ''} - `} + min-w-fit w-auto py-2 border border-solid-gray-420 bg-white shadow-1 rounded-8 + has-[>:nth-child(7)]:rounded-r-none + ${isCondensed ? 'max-h-[calc((32*6.5+16)/16*1rem)]' : 'max-h-[calc((44*6.5+16)/16*1rem)]'} + ${className ?? ''} + `} ref={ref} {...rest} > diff --git a/src/components/Legend/Legend.tsx b/src/components/Legend/Legend.tsx index 00bcabd..b1a5e62 100644 --- a/src/components/Legend/Legend.tsx +++ b/src/components/Legend/Legend.tsx @@ -2,12 +2,6 @@ import type { ComponentProps } from 'react'; export type LegendSize = 'lg' | 'md' | 'sm'; -export const legendSizeStyle: { [key in LegendSize]: string } = { - lg: 'text-std-18B-160', - md: 'text-std-17B-170', - sm: 'text-std-16B-170', -}; - export type LegendProps = ComponentProps<'legend'> & { size?: LegendSize; }; @@ -19,9 +13,10 @@ export const Legend = (props: LegendProps) => { {children} diff --git a/src/components/NotificationBanner/parts/Body.tsx b/src/components/NotificationBanner/parts/Body.tsx index 7663bfd..93fd5d7 100644 --- a/src/components/NotificationBanner/parts/Body.tsx +++ b/src/components/NotificationBanner/parts/Body.tsx @@ -11,6 +11,7 @@ export const NotificationBannerBody = (props: Props) => { col-start-1 -col-end-1 desktop:col-start-2 text-std-16N-170 text-solid-gray-800 ${className ?? ''} `} + {...rest} > {children} diff --git a/src/components/RequirementBadge/RequirementBadge.tsx b/src/components/RequirementBadge/RequirementBadge.tsx index 0847fa8..97a56c6 100644 --- a/src/components/RequirementBadge/RequirementBadge.tsx +++ b/src/components/RequirementBadge/RequirementBadge.tsx @@ -10,10 +10,11 @@ export const RequirementBadge = (props: RequirementBadgeProps) => { return ( {children} diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx index 1ac1b3f..c44adbb 100644 --- a/src/components/Select/Select.tsx +++ b/src/components/Select/Select.tsx @@ -2,15 +2,6 @@ import { type ComponentProps, forwardRef } from 'react'; export type SelectBlockSize = 'lg' | 'md' | 'sm'; -export const SelectBlockSizeStyle: { [key in SelectBlockSize]: string } = { - // NOTE: - // Tailwind CSS (v3.4.4) does not have any utility classes for logical properties of sizing. - // Once it is officially released, we will replace them with classes like `bs-14`. - lg: 'h-14', - md: 'h-12', - sm: 'h-10', -}; - export type SelectProps = ComponentProps<'select'> & { isError?: boolean; blockSize?: SelectBlockSize; @@ -33,13 +24,16 @@ export const Select = forwardRef((props, ref) => ; + +export const Playground: Story = { + render({ ...args }) { + return ( + + + + + + ); + }, +}; + +/** 項目ラベルやサポートテキスト等と一緒に表示した例 */ +export const WithFieldset: Story = { + render({ ...args }) { + return ( + + + 日付※必須 + + 西暦、半角 + + + + + + + ); + }, +}; + +/** + * 項目ラベルやサポートテキスト等と一緒に表示した例 + * + * Separatedタイプは個別の入力フィールドがエラー状態になります。 + */ +export const Errored: Story = { + render({ ...args }) { + return ( + + + 日付※必須 + + 西暦、半角 + + + + + + *年を入力してください。 + + ); + }, +}; + +/** + * Disabled状態を表示した例 + * + * 無効化するために`disabled`属性ではなく`aria-disabled`属性を使用します。 + */ +export const Disabled: Story = { + render({ ...args }) { + return ( + + + 日付※任意 + + 西暦、半角 + + + + + + + ); + }, +}; + +/** カレンダーで日付を選択できるようにした例 */ +export const WithCalendar: Story = { + render({ size, ...args }) { + const [yearInput, setYearInput] = useState(''); + const [monthInput, setMonthInput] = useState(''); + const [dayInput, setDayInput] = useState(''); + + function handleCalendarChange(newDate: DateValue | null) { + if (newDate !== null) { + setYearInput(String(newDate.year)); + setMonthInput(String(newDate.month).padStart(2, '0')); + setDayInput(String(newDate.day).padStart(2, '0')); + } else { + setYearInput(''); + setMonthInput(''); + setDayInput(''); + } + } + + return ( + + {({ state }) => { + const updateCalendarDate = () => { + const year = Number.parseInt(yearInput); + const month = Number.parseInt(monthInput); + const day = Number.parseInt(dayInput); + + if (!Number.isNaN(year) && !Number.isNaN(month) && !Number.isNaN(day)) { + state.setValue(new CalendarDate(year, month, day)); + } else { + state.setValue(null); + } + }; + + return ( + <> + + setYearInput(e.target.value)} + onBlur={updateCalendarDate} + /> + setMonthInput(e.target.value)} + onBlur={updateCalendarDate} + /> + setDayInput(e.target.value)} + onBlur={updateCalendarDate} + /> + + state.setOpen(true)} + /> + + + + {({ state: calendarState }) => ( + <> + + + calendarState.setFocusedDate( + calendarState.focusedDate.set({ + year: Number(event.target.value), + }), + ) + } + aria-label='年' + > + 2018年(平成30年) + 2019年(令和元年) + 2020年(令和2年) + 2021年(令和3年) + 2022年(令和4年) + 2023年(令和5年) + 2024年(令和6年) + 2025年(令和7年) + 2026年(令和8年) + 2027年(令和9年) + 2028年(令和10年) + 2029年(令和11年) + 2030年(令和12年) + + + calendarState.focusPreviousPage()} + > + + + + + {calendarState.focusedDate.month}月 + calendarState.focusNextPage()} + > + + + + + + + + + {(day) => ( + + {day} + + )} + + + {(date) => ( + + )} + + + + state.close()}> + 閉じる + + calendarState.selectDate(today(getLocalTimeZone()))} + > + 今日 + + + > + )} + + + + > + ); + }} + + ); + }, +}; + +/** 年月または月日のみのタイプ */ +export const Partial: Story = { + render({ ...args }) { + return ( + <> + + + + + + + + + + + + > + ); + }, +}; diff --git a/src/components/SeparatedDatePicker/SeparatedDatePicker.tsx b/src/components/SeparatedDatePicker/SeparatedDatePicker.tsx new file mode 100644 index 0000000..be08a1c --- /dev/null +++ b/src/components/SeparatedDatePicker/SeparatedDatePicker.tsx @@ -0,0 +1,23 @@ +import type { ComponentProps } from 'react'; + +export type SeparatedDatePickerSize = 'lg' | 'md' | 'sm'; + +export type SeparatedDatePickerProps = ComponentProps<'div'> & { + size?: SeparatedDatePickerSize; +}; + +export const SeparatedDatePicker = (props: SeparatedDatePickerProps) => { + const { className, size = 'lg', children, ...rest } = props; + + return ( + + + {children} + + + ); +}; diff --git a/src/components/SeparatedDatePicker/index.ts b/src/components/SeparatedDatePicker/index.ts new file mode 100644 index 0000000..afb90bd --- /dev/null +++ b/src/components/SeparatedDatePicker/index.ts @@ -0,0 +1,5 @@ +export * from './parts/SeparatedDatePickerCalendarButton'; +export * from './parts/SeparatedDatePickerDate'; +export * from './parts/SeparatedDatePickerMonth'; +export * from './parts/SeparatedDatePickerYear'; +export * from './SeparatedDatePicker'; diff --git a/src/components/SeparatedDatePicker/parts/SeparatedDatePickerCalendarButton.tsx b/src/components/SeparatedDatePicker/parts/SeparatedDatePickerCalendarButton.tsx new file mode 100644 index 0000000..6ab4e1c --- /dev/null +++ b/src/components/SeparatedDatePicker/parts/SeparatedDatePickerCalendarButton.tsx @@ -0,0 +1,33 @@ +import { type ComponentProps, forwardRef } from 'react'; +import type { SeparatedDatePickerSize } from '../SeparatedDatePicker'; + +export type SeparatedDatePickerCalendarButtonProps = ComponentProps<'button'> & { + size?: SeparatedDatePickerSize; +}; + +export const SeparatedDatePickerCalendarButton = forwardRef< + HTMLButtonElement, + SeparatedDatePickerCalendarButtonProps +>((props, ref) => { + const { className, size = 'lg', ...rest } = props; + + return ( + + + + + + + + + ); +}); diff --git a/src/components/SeparatedDatePicker/parts/SeparatedDatePickerDate.tsx b/src/components/SeparatedDatePicker/parts/SeparatedDatePickerDate.tsx new file mode 100644 index 0000000..d85ed5d --- /dev/null +++ b/src/components/SeparatedDatePicker/parts/SeparatedDatePickerDate.tsx @@ -0,0 +1,27 @@ +import { type ComponentProps, forwardRef } from 'react'; + +export type SeparatedDatePickerDateProps = ComponentProps<'input'> & {}; + +export const SeparatedDatePickerDate = forwardRef( + (props, ref) => { + const { className, 'aria-disabled': disabled, readOnly, ...rest } = props; + + return ( + + + 日 + + + + ); + }, +); diff --git a/src/components/SeparatedDatePicker/parts/SeparatedDatePickerMonth.tsx b/src/components/SeparatedDatePicker/parts/SeparatedDatePickerMonth.tsx new file mode 100644 index 0000000..1ab6560 --- /dev/null +++ b/src/components/SeparatedDatePicker/parts/SeparatedDatePickerMonth.tsx @@ -0,0 +1,27 @@ +import { type ComponentProps, forwardRef } from 'react'; + +export type SeparatedDatePickerMonthProps = ComponentProps<'input'> & {}; + +export const SeparatedDatePickerMonth = forwardRef( + (props, ref) => { + const { className, 'aria-disabled': disabled, readOnly, ...rest } = props; + + return ( + + + 月 + + + + ); + }, +); diff --git a/src/components/SeparatedDatePicker/parts/SeparatedDatePickerYear.tsx b/src/components/SeparatedDatePicker/parts/SeparatedDatePickerYear.tsx new file mode 100644 index 0000000..7bbea21 --- /dev/null +++ b/src/components/SeparatedDatePicker/parts/SeparatedDatePickerYear.tsx @@ -0,0 +1,27 @@ +import { type ComponentProps, forwardRef } from 'react'; + +export type SeparatedDatePickerYearProps = ComponentProps<'input'> & {}; + +export const SeparatedDatePickerYear = forwardRef( + (props, ref) => { + const { className, 'aria-disabled': disabled, readOnly, ...rest } = props; + + return ( + + + 年 + + + + ); + }, +); diff --git a/src/components/Textarea/Textarea.tsx b/src/components/Textarea/Textarea.tsx index 90b7712..4acc026 100644 --- a/src/components/Textarea/Textarea.tsx +++ b/src/components/Textarea/Textarea.tsx @@ -10,12 +10,14 @@ export const Textarea = forwardRef((props, r return (
{calendarState.focusedDate.month}月