@moneyforward/mfui-components 3.20.0 → 3.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/src/DateTimeSelection/DatePicker/DatePicker.js +2 -11
  2. package/dist/src/DateTimeSelection/DateRangePicker/DateRangePicker.d.ts +1 -1
  3. package/dist/src/DateTimeSelection/DateRangePicker/DateRangePicker.js +10 -2
  4. package/dist/src/DateTimeSelection/DateRangePicker/DateRangePicker.types.d.ts +14 -0
  5. package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerPopover/DateRangePickerPopover.js +13 -5
  6. package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerPopover/utilities/createDateRangePickerPopoverTestUtility.js +2 -2
  7. package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerTrigger/utilities/createDateRangePickerTriggerTestUtility.js +3 -1
  8. package/dist/src/DateTimeSelection/MonthPicker/MonthPicker.d.ts +1 -1
  9. package/dist/src/DateTimeSelection/MonthPicker/MonthPicker.js +10 -2
  10. package/dist/src/DateTimeSelection/MonthPicker/MonthPicker.types.d.ts +14 -0
  11. package/dist/src/DateTimeSelection/MonthPicker/MonthPickerPanel/MonthCell/MonthCell.js +2 -25
  12. package/dist/src/DateTimeSelection/MonthPicker/MonthPickerPanel/MonthPickerPanel.js +1 -1
  13. package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePicker.d.ts +1 -1
  14. package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePicker.js +14 -5
  15. package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePicker.types.d.ts +21 -0
  16. package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerMonthCell/MonthRangePickerMonthCell.d.ts +1 -0
  17. package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerMonthCell/MonthRangePickerMonthCell.js +10 -3
  18. package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerNavigation/MonthRangePickerNavigation.js +16 -3
  19. package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerPanel/MonthRangePickerPanel.js +3 -1
  20. package/dist/src/DateTimeSelection/TimePicker/TimePicker.d.ts +43 -0
  21. package/dist/src/DateTimeSelection/TimePicker/TimePicker.js +85 -0
  22. package/dist/src/DateTimeSelection/TimePicker/TimePicker.types.d.ts +61 -0
  23. package/dist/src/DateTimeSelection/TimePicker/TimePicker.types.js +1 -0
  24. package/dist/src/DateTimeSelection/TimePicker/constants.d.ts +4 -0
  25. package/dist/src/DateTimeSelection/TimePicker/constants.js +12 -0
  26. package/dist/src/DateTimeSelection/TimePicker/index.d.ts +2 -0
  27. package/dist/src/DateTimeSelection/TimePicker/index.js +1 -0
  28. package/dist/src/DateTimeSelection/index.d.ts +1 -0
  29. package/dist/src/DateTimeSelection/index.js +1 -0
  30. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.d.ts +1 -1
  31. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.js +2 -2
  32. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.types.d.ts +10 -0
  33. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.js +24 -20
  34. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.types.d.ts +9 -0
  35. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerTrigger/hooks/useDateRangeTriggerValueController.js +4 -4
  36. package/dist/src/DateTimeSelection/shared/CalendarLocale/CalendarLocaleContext.d.ts +1 -1
  37. package/dist/src/DateTimeSelection/shared/CalendarLocale/CalendarLocaleContext.js +1 -1
  38. package/dist/src/DateTimeSelection/shared/DayCell/DayCell.js +2 -3
  39. package/dist/src/DateTimeSelection/shared/YearSelector/YearSelector.js +1 -1
  40. package/dist/src/DateTimeSelection/shared/utilities/dateParsing.js +16 -9
  41. package/dist/src/DateTimeSelection/shared/utilities/japaneseCalendar.d.ts +36 -8
  42. package/dist/src/DateTimeSelection/shared/utilities/japaneseCalendar.js +82 -15
  43. package/dist/src/DateTimeSelection/shared/utilities/monthCellMonthFormat.d.ts +14 -0
  44. package/dist/src/DateTimeSelection/shared/utilities/monthCellMonthFormat.js +35 -0
  45. package/dist/styled-system/recipes/index.d.ts +1 -0
  46. package/dist/styled-system/recipes/index.js +1 -0
  47. package/dist/styled-system/recipes/time-picker-slot-recipe.d.ts +44 -0
  48. package/dist/styled-system/recipes/time-picker-slot-recipe.js +62 -0
  49. package/dist/styles.css +168 -1
  50. package/dist/tsconfig.build.tsbuildinfo +1 -1
  51. package/package.json +5 -5
  52. package/dist/src/DateTimeSelection/shared/BasePicker/YearSelector/YearSelector.d.ts +0 -18
  53. package/dist/src/DateTimeSelection/shared/BasePicker/YearSelector/YearSelector.js +0 -36
@@ -4,7 +4,7 @@ import { BasePicker } from '../shared/BasePicker';
4
4
  import { DatePickerPanel } from './DatePickerPanel';
5
5
  import { CalendarLocaleProvider } from '../shared/CalendarLocale/CalendarLocaleContext';
6
6
  import { DisplayJapaneseCalendarProvider } from '../shared/CalendarLocale/DisplayJapaneseCalendarContext';
7
- import { dateToEnglishEraShort } from '../shared/utilities/japaneseCalendar';
7
+ import { dateToEnglishEraShort, dateToJapaneseEra } from '../shared/utilities/japaneseCalendar';
8
8
  /**
9
9
  * DatePicker component
10
10
  *
@@ -15,16 +15,7 @@ export function DatePicker({ format = 'YYYY/MM/DD', checkDisabledDate, minDate,
15
15
  const customFormatValue = useMemo(() => {
16
16
  if (!displayJapaneseCalendar)
17
17
  return;
18
- if (calendarLocale === 'en') {
19
- return (date) => dateToEnglishEraShort(date);
20
- }
21
- const formatter = new Intl.DateTimeFormat('ja-JP-u-ca-japanese', {
22
- era: 'long',
23
- year: 'numeric',
24
- month: 'narrow',
25
- day: 'numeric',
26
- });
27
- return (date) => formatter.format(date);
18
+ return calendarLocale === 'en' ? dateToEnglishEraShort : dateToJapaneseEra;
28
19
  }, [displayJapaneseCalendar, calendarLocale]);
29
20
  return (_jsx(BasePicker, { ...props, format: format, baseFormat: "YYYY-MM-DD", calendarLocale: calendarLocale, customFormatValue: customFormatValue, renderPopoverContent: ({ value, onChange }) => (_jsx(DisplayJapaneseCalendarProvider, { value: displayJapaneseCalendar, children: _jsx(CalendarLocaleProvider, { value: calendarLocale, children: _jsx(DatePickerPanel, { value: value, checkDisabledDate: checkDisabledDate, minDate: minDate, maxDate: maxDate, prevMonthIconButtonProps: prevMonthIconButtonProps, nextMonthIconButtonProps: nextMonthIconButtonProps, todayButtonProps: todayButtonProps, onChange: onChange }) }) })) }));
30
21
  }
@@ -5,4 +5,4 @@ import { type DateRangePickerProps } from './DateRangePicker.types';
5
5
  *
6
6
  * @param props - DateRangePicker props
7
7
  */
8
- export declare function DateRangePicker({ format, calendarLocale, ...restProps }: DateRangePickerProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function DateRangePicker({ format, calendarLocale, displayJapaneseCalendar, ...restProps }: DateRangePickerProps): import("react/jsx-runtime").JSX.Element;
@@ -6,15 +6,23 @@ import { jsx as _jsx } from "react/jsx-runtime";
6
6
  * All logic and props are inherited from BaseRangePicker.
7
7
  * See BaseRangePicker for implementation details.
8
8
  */
9
+ import { useMemo } from 'react';
9
10
  import { BaseRangePicker } from '../shared/BaseRangePicker';
10
11
  import { CalendarLocaleProvider } from '../shared/CalendarLocale/CalendarLocaleContext';
12
+ import { DisplayJapaneseCalendarProvider } from '../shared/CalendarLocale/DisplayJapaneseCalendarContext';
11
13
  import { DateRangePickerPopover } from './DateRangePickerPopover/DateRangePickerPopover';
14
+ import { dateToEnglishEraShort, dateToJapaneseEra } from '../shared/utilities/japaneseCalendar';
12
15
  /**
13
16
  * DateRangePicker component for selecting date ranges.
14
17
  * Extends BaseRangePicker with date-specific functionality.
15
18
  *
16
19
  * @param props - DateRangePicker props
17
20
  */
18
- export function DateRangePicker({ format = 'YYYY/MM/DD', calendarLocale = 'ja', ...restProps }) {
19
- return (_jsx(BaseRangePicker, { ...restProps, format: format, calendarLocale: calendarLocale, renderPopoverContent: () => (_jsx(CalendarLocaleProvider, { value: calendarLocale, children: _jsx(DateRangePickerPopover, {}) })) }));
21
+ export function DateRangePicker({ format = 'YYYY/MM/DD', calendarLocale = 'ja', displayJapaneseCalendar = false, ...restProps }) {
22
+ const customFormatValue = useMemo(() => {
23
+ if (!displayJapaneseCalendar)
24
+ return;
25
+ return calendarLocale === 'en' ? dateToEnglishEraShort : dateToJapaneseEra;
26
+ }, [displayJapaneseCalendar, calendarLocale]);
27
+ return (_jsx(BaseRangePicker, { ...restProps, format: format, calendarLocale: calendarLocale, customFormatValue: customFormatValue, renderPopoverContent: () => (_jsx(DisplayJapaneseCalendarProvider, { value: displayJapaneseCalendar, children: _jsx(CalendarLocaleProvider, { value: calendarLocale, children: _jsx(DateRangePickerPopover, {}) }) })) }));
20
28
  }
@@ -10,4 +10,18 @@ export type DateRangePickerProps = BaseRangePickerProps & {
10
10
  * @default 'ja'
11
11
  */
12
12
  calendarLocale?: BasePickerProps['calendarLocale'];
13
+ /**
14
+ * Whether to display the date in Japanese calendar format (e.g., "令和7年1月15日")
15
+ * When enabled, the text inputs will show dates in Japanese calendar format
16
+ * while internal value handling remains unchanged.
17
+ *
18
+ * @default false
19
+ *
20
+ * @remarks
21
+ * - When combined with `calendarLocale="en"`, dates are romanized with
22
+ * zero-padded month/day (e.g., "Reiwa 7/01/15").
23
+ * - Pre-Meiji dates (year < 1868) fall back to the Gregorian year
24
+ * (e.g., "1867/12/31").
25
+ */
26
+ displayJapaneseCalendar?: boolean;
13
27
  };
@@ -13,6 +13,8 @@ import { useFocusTrap } from '../../../utilities/dom/useFocusTrap';
13
13
  import { getFocusableNodes } from '../../../utilities/dom/getFocusableNodes';
14
14
  import { getBasePickerLabel } from '../../shared/BasePicker/constants';
15
15
  import { useCalendarLocale } from '../../shared/CalendarLocale/CalendarLocaleContext';
16
+ import { useDisplayJapaneseCalendar } from '../../shared/CalendarLocale/DisplayJapaneseCalendarContext';
17
+ import { dateToEnglishEraMonthShort, dateToJapaneseEraMonth } from '../../shared/utilities/japaneseCalendar';
16
18
  // Utility function to add months to a date
17
19
  const addMonths = (date, months) => new Date(date.getFullYear(), date.getMonth() + months, 1);
18
20
  /**
@@ -38,6 +40,7 @@ const addMonths = (date, months) => new Date(date.getFullYear(), date.getMonth()
38
40
  export function DateRangePickerPopover() {
39
41
  const { viewingMonth, setViewingMonth, setPendingFocusDate, isDateDisabled, minDate, maxDate } = useBaseRangePickerContext();
40
42
  const calendarLocale = useCalendarLocale();
43
+ const displayJapaneseCalendar = useDisplayJapaneseCalendar();
41
44
  const rightViewingMonth = useMemo(() => addMonths(viewingMonth, 1), [viewingMonth]);
42
45
  // Check if navigation buttons should be disabled based on constraints
43
46
  const isPreviousMonthDisabled = useCallback(() => {
@@ -129,14 +132,19 @@ export function DateRangePickerPopover() {
129
132
  // Localized labels
130
133
  const previousMonthLabel = useMemo(() => getBasePickerLabel('GO_TO_PREV_MONTH', calendarLocale), [calendarLocale]);
131
134
  const nextMonthLabel = useMemo(() => getBasePickerLabel('GO_TO_NEXT_MONTH', calendarLocale), [calendarLocale]);
132
- // Localized month format functions
133
- const formatMonth = useMemo(() => (date) => {
135
+ // Localized month labels (Gregorian vs Japanese-calendar era, matching trigger formatting)
136
+ const formatMonth = useMemo(() => {
137
+ const firstOfMonth = (d) => new Date(d.getFullYear(), d.getMonth(), 1);
138
+ if (displayJapaneseCalendar) {
139
+ const formatEra = calendarLocale === 'ja' ? dateToJapaneseEraMonth : dateToEnglishEraMonthShort;
140
+ return (date) => formatEra(firstOfMonth(date));
141
+ }
134
142
  if (calendarLocale === 'ja') {
135
- return `${String(date.getFullYear())}年${String(date.getMonth() + 1)}月`;
143
+ return (date) => `${String(firstOfMonth(date).getFullYear())}年${String(firstOfMonth(date).getMonth() + 1)}月`;
136
144
  }
137
145
  const monthFormatter = new Intl.DateTimeFormat('en-US', { month: 'long' });
138
- return `${monthFormatter.format(date)} ${String(date.getFullYear())}`;
139
- }, [calendarLocale]);
146
+ return (date) => `${monthFormatter.format(firstOfMonth(date))} ${String(firstOfMonth(date).getFullYear())}`;
147
+ }, [calendarLocale, displayJapaneseCalendar]);
140
148
  return (_jsxs("div", { className: cx(classNames.root, 'mfui-DateRangePickerPopover__root'), onKeyDown: handleOnKeyDown, children: [_jsxs("div", { className: cx(classNames.navigation, 'mfui-DateRangePickerPopover__navigation'), children: [_jsxs("div", { className: cx(classNames.navigationColumn, 'mfui-DateRangePickerPopover__navigationColumn'), children: [_jsx(IconButton, { "aria-label": previousMonthLabel, disabled: isPreviousMonthDisabled(), onClick: () => {
141
149
  if (!isPreviousMonthDisabled()) {
142
150
  setViewingMonth(addMonths(viewingMonth, -1));
@@ -51,8 +51,8 @@ export function createDateRangePickerPopoverTestUtility({ user }) {
51
51
  return getAllDateCells().filter((element) => element.dataset['mfuiInRange'] === 'true');
52
52
  }
53
53
  function getLeftMonthLabel() {
54
- // Look for month labels - handles both Japanese (YYYY年MM月) and English (Month YYYY) formats
55
- const monthLabels = screen.getAllByText(/年.*月|\w+ \d{4}/);
54
+ // Month navigation headings: Gregorian JA/EN, Japanese-era JA, or romanized-era EN (e.g. Reiwa 7/01)
55
+ const monthLabels = screen.getAllByText(/年[0-90-9]*月|\d{4}年\d{1,2}月|\b(?:January|February|March|April|May|June|July|August|September|October|November|December)\b \d{4}|\b(?:Reiwa|Heisei|Showa|Taisho|Meiji)\s+\d{1,2}\/\d{2}|(?:令和|平成|昭和|大正|明治)(?:元|[0-90-9]{1,2})年[0-90-9]{1,2}月/);
56
56
  return monthLabels[0]?.textContent ?? null;
57
57
  }
58
58
  // Actions
@@ -12,7 +12,9 @@ export function createDateRangePickerTriggerTestUtility({ user }) {
12
12
  return screen.queryByRole('button', { name: LOCALE.CLEAR_BUTTON_LABEL });
13
13
  }
14
14
  function getCalendarButton() {
15
- return screen.getByRole('button', { name: LOCALE.CALENDAR_BUTTON_LABEL });
15
+ return screen.getByRole('button', {
16
+ name: new RegExp(`^(?:${LOCALE.CALENDAR_BUTTON_LABEL}|Open calendar)$`),
17
+ });
16
18
  }
17
19
  // Value Queries
18
20
  function getStartValue() {
@@ -5,4 +5,4 @@ import { type MonthPickerProps } from './MonthPicker.types';
5
5
  *
6
6
  * @param props - MonthPicker props including minMonth and maxMonth constraints
7
7
  */
8
- export declare function MonthPicker({ format, minMonth, maxMonth, checkDisabledMonth, calendarLocale, ...restProps }: MonthPickerProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function MonthPicker({ format, minMonth, maxMonth, checkDisabledMonth, calendarLocale, displayJapaneseCalendar, ...restProps }: MonthPickerProps): import("react/jsx-runtime").JSX.Element;
@@ -6,15 +6,23 @@ import { jsx as _jsx } from "react/jsx-runtime";
6
6
  * All logic and props are inherited from BasePicker, except for the popover content and format.
7
7
  * See BasePicker for implementation details.
8
8
  */
9
+ import { useMemo } from 'react';
9
10
  import { BasePicker } from '../shared/BasePicker';
10
11
  import { CalendarLocaleProvider } from '../shared/CalendarLocale/CalendarLocaleContext';
12
+ import { DisplayJapaneseCalendarProvider } from '../shared/CalendarLocale/DisplayJapaneseCalendarContext';
11
13
  import { MonthPickerPanel } from './MonthPickerPanel/MonthPickerPanel';
14
+ import { dateToEnglishEraMonthShort, dateToJapaneseEraMonth } from '../shared/utilities/japaneseCalendar';
12
15
  /**
13
16
  * MonthPicker component for selecting months.
14
17
  * Extends BasePicker with month-specific functionality.
15
18
  *
16
19
  * @param props - MonthPicker props including minMonth and maxMonth constraints
17
20
  */
18
- export function MonthPicker({ format = 'YYYY/MM', minMonth, maxMonth, checkDisabledMonth, calendarLocale = 'ja', ...restProps }) {
19
- return (_jsx(BasePicker, { ...restProps, format: format, baseFormat: "YYYY-MM", calendarLocale: calendarLocale, renderPopoverContent: ({ viewingValue, setViewingValue, value, onChange }) => (_jsx(CalendarLocaleProvider, { value: calendarLocale, children: _jsx(MonthPickerPanel, { viewingValue: viewingValue, setViewingValue: setViewingValue, value: value, minMonth: minMonth, maxMonth: maxMonth, checkDisabledMonth: checkDisabledMonth, onChange: onChange }) })) }));
21
+ export function MonthPicker({ format = 'YYYY/MM', minMonth, maxMonth, checkDisabledMonth, calendarLocale = 'ja', displayJapaneseCalendar = false, ...restProps }) {
22
+ const customFormatValue = useMemo(() => {
23
+ if (!displayJapaneseCalendar)
24
+ return;
25
+ return calendarLocale === 'en' ? dateToEnglishEraMonthShort : dateToJapaneseEraMonth;
26
+ }, [displayJapaneseCalendar, calendarLocale]);
27
+ return (_jsx(BasePicker, { ...restProps, format: format, baseFormat: "YYYY-MM", calendarLocale: calendarLocale, customFormatValue: customFormatValue, renderPopoverContent: ({ viewingValue, setViewingValue, value, onChange }) => (_jsx(DisplayJapaneseCalendarProvider, { value: displayJapaneseCalendar, children: _jsx(CalendarLocaleProvider, { value: calendarLocale, children: _jsx(MonthPickerPanel, { viewingValue: viewingValue, setViewingValue: setViewingValue, value: value, minMonth: minMonth, maxMonth: maxMonth, checkDisabledMonth: checkDisabledMonth, onChange: onChange }) }) })) }));
20
28
  }
@@ -39,4 +39,18 @@ export type MonthPickerProps = Omit<BasePickerProps, 'renderPopoverContent' | 'b
39
39
  * // Displays: "Jan", "Feb", etc. and English navigation labels
40
40
  */
41
41
  calendarLocale?: BasePickerProps['calendarLocale'];
42
+ /**
43
+ * Whether to display the month in Japanese calendar format (e.g., "令和7年1月")
44
+ * When enabled, the text input will show months in Japanese calendar format
45
+ * while internal value handling remains unchanged.
46
+ *
47
+ * @default false
48
+ *
49
+ * @remarks
50
+ * - When combined with `calendarLocale="en"`, months are romanized with
51
+ * a zero-padded month number (e.g., "Reiwa 7/01").
52
+ * - Pre-Meiji months (year < 1868) fall back to the Gregorian year
53
+ * (e.g., "1867/12").
54
+ */
55
+ displayJapaneseCalendar?: boolean;
42
56
  };
@@ -2,38 +2,15 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useMemo } from 'react';
3
3
  import { MonthCell } from '../../../shared/MonthCell';
4
4
  import { useCalendarLocale } from '../../../shared/CalendarLocale/CalendarLocaleContext';
5
+ import { getMonthCellMonthFormat } from '../../../shared/utilities/monthCellMonthFormat';
5
6
  import { useMonthPickerKeyboardNavigation } from '../../utilities/useMonthPickerKeyboardNavigation';
6
- // English month abbreviations wrapped in square brackets to escape them in dayjs format strings
7
- const ENGLISH_MONTH_ABBREVIATIONS = [
8
- '[Jan]',
9
- '[Feb]',
10
- '[Mar]',
11
- '[Apr]',
12
- '[May]',
13
- '[Jun]',
14
- '[Jul]',
15
- '[Aug]',
16
- '[Sep]',
17
- '[Oct]',
18
- '[Nov]',
19
- '[Dec]',
20
- ];
21
7
  /**
22
8
  * MonthPickerMonthCell is a wrapper around the shared MonthCell component.
23
9
  * It handles month selection and integrates with the MonthPicker context.
24
10
  */
25
11
  export function MonthPickerMonthCell({ onKeyDown, minMonth, maxMonth, checkDisabledMonth, date, ...rest }) {
26
12
  const locale = useCalendarLocale();
27
- // Generate month format string based on locale
28
- const monthFormat = useMemo(() => {
29
- const monthIndex = date.getMonth();
30
- if (locale === 'en') {
31
- // Return English month abbreviation escaped for dayjs
32
- return ENGLISH_MONTH_ABBREVIATIONS[monthIndex];
33
- }
34
- // Japanese format: "1月", "2月", etc. (using dayjs format)
35
- return 'M月';
36
- }, [locale, date]);
13
+ const monthFormat = useMemo(() => getMonthCellMonthFormat(locale, date), [locale, date]);
37
14
  const handleKeyDown = useMonthPickerKeyboardNavigation(onKeyDown, minMonth, maxMonth, checkDisabledMonth);
38
15
  return _jsx(MonthCell, { ...rest, date: date, monthFormat: monthFormat, onKeyDown: handleKeyDown });
39
16
  }
@@ -109,7 +109,7 @@ export function MonthPickerPanel({ viewingValue, setViewingValue, value, onChang
109
109
  setPendingFocusDate(null);
110
110
  }, [viewingValue, pendingFocusDate, setPendingFocusDate, baseFormat]);
111
111
  const classNames = monthPickerPanelSlotRecipe();
112
- return (_jsxs("div", { ...props, className: cx(classNames.root, 'mfui-MonthPickerPanel__root', className), children: [_jsxs("div", { className: cx(classNames.navigation, 'mfui-MonthPickerPanel__navigation'), children: [!isPreviousYearDisabled ? (_jsx(IconButton, { "aria-label": getMonthPickerLabel(locale ?? 'ja', 'prevYear'), onClick: handlePreviousYear, children: _jsx(PickerBefore, {}) })) : (_jsx(YearNavigationPlaceholder, { className: cx(classNames.yearNavigationPlaceholder, 'mfui-MonthPickerPanel__yearNavigationPlaceholder') })), _jsx(YearSelector, { value: viewingValue, triggerWrapperProps: { className: cx(classNames.yearSelector, 'mfui-MonthPickerPanel__yearSelector') }, filterYear: shouldFilterYears ? filterYear : undefined, onChange: handleYearSelect }), !isNextYearDisabled ? (_jsx(IconButton, { "aria-label": getMonthPickerLabel(locale ?? 'ja', 'nextYear'), onClick: handleNextYear, children: _jsx(PickerAfter, {}) })) : (_jsx(YearNavigationPlaceholder, { className: cx(classNames.yearNavigationPlaceholder, 'mfui-MonthPickerPanel__yearNavigationPlaceholder') }))] }), _jsx(MonthPickerMonthGrid, { viewingValue: viewingValue, value: value, minMonth: minMonth, maxMonth: maxMonth, checkDisabledMonth: checkDisabledMonth, onChange: onChange })] }));
112
+ return (_jsxs("div", { ...props, className: cx(classNames.root, 'mfui-MonthPickerPanel__root', className), children: [_jsxs("div", { className: cx(classNames.navigation, 'mfui-MonthPickerPanel__navigation'), children: [!isPreviousYearDisabled ? (_jsx(IconButton, { "aria-label": getMonthPickerLabel(locale, 'prevYear'), onClick: handlePreviousYear, children: _jsx(PickerBefore, {}) })) : (_jsx(YearNavigationPlaceholder, { className: cx(classNames.yearNavigationPlaceholder, 'mfui-MonthPickerPanel__yearNavigationPlaceholder') })), _jsx(YearSelector, { value: viewingValue, triggerWrapperProps: { className: cx(classNames.yearSelector, 'mfui-MonthPickerPanel__yearSelector') }, filterYear: shouldFilterYears ? filterYear : undefined, onChange: handleYearSelect }), !isNextYearDisabled ? (_jsx(IconButton, { "aria-label": getMonthPickerLabel(locale, 'nextYear'), onClick: handleNextYear, children: _jsx(PickerAfter, {}) })) : (_jsx(YearNavigationPlaceholder, { className: cx(classNames.yearNavigationPlaceholder, 'mfui-MonthPickerPanel__yearNavigationPlaceholder') }))] }), _jsx(MonthPickerMonthGrid, { viewingValue: viewingValue, value: value, minMonth: minMonth, maxMonth: maxMonth, checkDisabledMonth: checkDisabledMonth, onChange: onChange })] }));
113
113
  }
114
114
  // Create a placeholder div that matches IconButton dimensions to prevent layout shift
115
115
  function YearNavigationPlaceholder({ className }) {
@@ -14,4 +14,4 @@ import { type MonthRangePickerProps } from './MonthRangePicker.types';
14
14
  * />
15
15
  * ```
16
16
  */
17
- export declare function MonthRangePicker({ format, minMonth, maxMonth, startInputProps, endInputProps, initialDisplayedMonths, ...restProps }: MonthRangePickerProps): import("react/jsx-runtime").JSX.Element;
17
+ export declare function MonthRangePicker({ format, minMonth, maxMonth, startInputProps, endInputProps, initialDisplayedMonths, displayJapaneseCalendar, calendarLocale, ...restProps }: MonthRangePickerProps): import("react/jsx-runtime").JSX.Element;
@@ -12,8 +12,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
12
12
  * - Keyboard navigation support
13
13
  * - Apply/Cancel workflow
14
14
  */
15
+ import { useMemo } from 'react';
15
16
  import { BaseRangePicker } from '../shared/BaseRangePicker';
17
+ import { CalendarLocaleProvider } from '../shared/CalendarLocale/CalendarLocaleContext';
18
+ import { DisplayJapaneseCalendarProvider } from '../shared/CalendarLocale/DisplayJapaneseCalendarContext';
16
19
  import { MonthRangePickerPanel } from './MonthRangePickerPanel';
20
+ import { dateToEnglishEraMonthShort, dateToJapaneseEraMonth } from '../shared/utilities/japaneseCalendar';
17
21
  /**
18
22
  * MonthRangePicker component for selecting month ranges.
19
23
  *
@@ -29,7 +33,12 @@ import { MonthRangePickerPanel } from './MonthRangePickerPanel';
29
33
  * />
30
34
  * ```
31
35
  */
32
- export function MonthRangePicker({ format = 'YYYY/MM', minMonth, maxMonth, startInputProps = {}, endInputProps = {}, initialDisplayedMonths, ...restProps }) {
36
+ export function MonthRangePicker({ format = 'YYYY/MM', minMonth, maxMonth, startInputProps = {}, endInputProps = {}, initialDisplayedMonths, displayJapaneseCalendar = false, calendarLocale = 'ja', ...restProps }) {
37
+ const customFormatValue = useMemo(() => {
38
+ if (!displayJapaneseCalendar)
39
+ return;
40
+ return calendarLocale === 'en' ? dateToEnglishEraMonthShort : dateToJapaneseEraMonth;
41
+ }, [displayJapaneseCalendar, calendarLocale]);
33
42
  // MonthRangePicker panels are grouped by year (left = viewingYear, right = viewingYear + 1).
34
43
  // When `previousAndCurrent` is requested, we shift by a full year so the left panel
35
44
  // shows the previous year. This applies regardless of whether `value` is set.
@@ -51,11 +60,11 @@ export function MonthRangePicker({ format = 'YYYY/MM', minMonth, maxMonth, start
51
60
  // Same-year value + previousAndCurrent: show previous year on the left
52
61
  return new Date(startDate.getFullYear() - 1, 0, 1);
53
62
  })();
54
- return (_jsx(BaseRangePicker, { ...restProps, format: format, pendingFocusDayjsFormat: "YYYY-MM", initialDisplayedMonths: initialDisplayedMonths, initialViewingDate: initialViewingDate, startInputProps: {
55
- placeholder: '開始月',
63
+ return (_jsx(BaseRangePicker, { ...restProps, format: format, pendingFocusDayjsFormat: "YYYY-MM", initialDisplayedMonths: initialDisplayedMonths, initialViewingDate: initialViewingDate, customFormatValue: customFormatValue, startInputProps: {
64
+ placeholder: calendarLocale === 'en' ? 'Start month' : '開始月',
56
65
  ...startInputProps,
57
66
  }, endInputProps: {
58
- placeholder: '終了月',
67
+ placeholder: calendarLocale === 'en' ? 'End month' : '終了月',
59
68
  ...endInputProps,
60
- }, minDate: minMonth, maxDate: maxMonth, minWidth: "min-content", renderPopoverContent: ({ viewingValue, setViewingValue, value }) => (_jsx(MonthRangePickerPanel, { viewingValue: viewingValue, setViewingValue: setViewingValue, value: value, minMonth: minMonth, maxMonth: maxMonth })) }));
69
+ }, minDate: minMonth, maxDate: maxMonth, minWidth: "min-content", calendarLocale: calendarLocale, renderPopoverContent: ({ viewingValue, setViewingValue, value }) => (_jsx(DisplayJapaneseCalendarProvider, { value: displayJapaneseCalendar, children: _jsx(CalendarLocaleProvider, { value: calendarLocale, children: _jsx(MonthRangePickerPanel, { viewingValue: viewingValue, setViewingValue: setViewingValue, value: value, minMonth: minMonth, maxMonth: maxMonth }) }) })) }));
61
70
  }
@@ -1,3 +1,4 @@
1
+ import { type BasePickerProps } from '../shared/BasePicker';
1
2
  import { type BaseRangePickerProps } from '../shared/BaseRangePicker';
2
3
  /**
3
4
  * The props for the MonthRangePicker component
@@ -25,4 +26,24 @@ export type MonthRangePickerProps = BaseRangePickerProps & {
25
26
  * ```
26
27
  */
27
28
  maxMonth?: Date;
29
+ /**
30
+ * Whether to display the month in Japanese calendar format (e.g., "令和7年1月")
31
+ * When enabled, the text inputs will show months in Japanese calendar format
32
+ * while internal value handling remains unchanged.
33
+ *
34
+ * @default false
35
+ *
36
+ * @remarks
37
+ * - When combined with `calendarLocale="en"`, months are romanized with
38
+ * a zero-padded month number (e.g., "Reiwa 7/01").
39
+ * - Pre-Meiji months (year < 1868) fall back to the Gregorian year
40
+ * (e.g., "1867/12").
41
+ */
42
+ displayJapaneseCalendar?: boolean;
43
+ /**
44
+ * The locale of the calendar.
45
+ *
46
+ * @default 'ja'
47
+ */
48
+ calendarLocale?: BasePickerProps['calendarLocale'];
28
49
  };
@@ -1,4 +1,5 @@
1
1
  import { type MonthRangePickerMonthCellProps } from './MonthRangePickerMonthCell.types';
2
+ import 'dayjs/locale/en';
2
3
  /**
3
4
  * MonthRangePickerMonthCell component that wraps MonthCell with range selection visual states.
4
5
  *
@@ -1,8 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useMemo } from 'react';
3
3
  import { MonthCell } from '../../shared/MonthCell';
4
+ import { useCalendarLocale } from '../../shared/CalendarLocale/CalendarLocaleContext';
5
+ import { getMonthCellMonthFormat } from '../../shared/utilities/monthCellMonthFormat';
4
6
  import { dayjs } from '../../../utilities/date/dayjs';
5
7
  import { MONTH_RANGE_PICKER_A11Y_LABELS } from './constants';
8
+ import 'dayjs/locale/en';
6
9
  /**
7
10
  * MonthRangePickerMonthCell component that wraps MonthCell with range selection visual states.
8
11
  *
@@ -17,11 +20,15 @@ import { MONTH_RANGE_PICKER_A11Y_LABELS } from './constants';
17
20
  * @param props - MonthRangePickerMonthCell props
18
21
  */
19
22
  export function MonthRangePickerMonthCell({ isInRange, isRangeStart, isRangeEnd, onMouseEnter, onMouseLeave, handleKeyboardNavigation, ...monthCellProps }) {
23
+ const calendarLocale = useCalendarLocale();
20
24
  // Calculate if this month is selected (either start or end of range)
21
25
  const selected = isRangeStart || isRangeEnd || monthCellProps.selected || false;
26
+ const monthFormat = useMemo(() => getMonthCellMonthFormat(calendarLocale, monthCellProps.date), [calendarLocale, monthCellProps.date]);
22
27
  // Create accessible label for range selection
23
28
  const accessibleLabel = useMemo(() => {
24
- const monthText = dayjs(monthCellProps.date).format('MMMM YYYY');
29
+ const monthText = calendarLocale === 'en'
30
+ ? dayjs(monthCellProps.date).locale('en').format('MMMM YYYY')
31
+ : dayjs(monthCellProps.date).format('MMMM YYYY');
25
32
  const statusParts = [];
26
33
  if (monthCellProps.disabled) {
27
34
  statusParts.push(MONTH_RANGE_PICKER_A11Y_LABELS.UNAVAILABLE);
@@ -41,6 +48,6 @@ export function MonthRangePickerMonthCell({ isInRange, isRangeStart, isRangeEnd,
41
48
  statusParts.push(MONTH_RANGE_PICKER_A11Y_LABELS.IN_RANGE);
42
49
  }
43
50
  return statusParts.length > 0 ? `${monthText}, ${statusParts.join(', ')}` : monthText;
44
- }, [monthCellProps.date, monthCellProps.disabled, selected, isInRange, isRangeStart, isRangeEnd]);
45
- return (_jsx("div", { onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, children: _jsx(MonthCell, { ...monthCellProps, selected: selected, inRange: isInRange, "aria-label": accessibleLabel, "aria-pressed": selected ? 'true' : undefined, "aria-selected": selected ? 'true' : 'false', onKeyDown: handleKeyboardNavigation }) }));
51
+ }, [calendarLocale, monthCellProps.date, monthCellProps.disabled, selected, isInRange, isRangeStart, isRangeEnd]);
52
+ return (_jsx("div", { onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, children: _jsx(MonthCell, { ...monthCellProps, monthFormat: monthFormat, selected: selected, inRange: isInRange, "aria-label": accessibleLabel, "aria-pressed": selected ? 'true' : undefined, "aria-selected": selected ? 'true' : 'false', onKeyDown: handleKeyboardNavigation }) }));
46
53
  }
@@ -5,11 +5,14 @@ import { cx } from '../../../../styled-system/css';
5
5
  import { monthRangePickerNavigationSlotRecipe } from '../../../../styled-system/recipes';
6
6
  import { IconButton } from '../../../IconButton';
7
7
  import { Typography } from '../../../Typography';
8
+ import { useCalendarLocale } from '../../shared/CalendarLocale/CalendarLocaleContext';
9
+ import { useDisplayJapaneseCalendar } from '../../shared/CalendarLocale/DisplayJapaneseCalendarContext';
10
+ import { yearToEraLabel } from '../../shared/utilities/japaneseCalendar';
8
11
  // Constants for year navigation limits
9
12
  const MINIMUM_VIEWABLE_YEAR = 1900;
10
13
  const MAXIMUM_VIEWABLE_YEAR = 2100;
11
- const LABEL_PREV_YEAR = '前年';
12
- const LABEL_NEXT_YEAR = '翌年';
14
+ const LABEL_PREV_YEAR = { ja: '前年', en: 'Previous year' };
15
+ const LABEL_NEXT_YEAR = { ja: '翌年', en: 'Next year' };
13
16
  /**
14
17
  * Year navigation component for MonthRangePicker.
15
18
  *
@@ -21,8 +24,18 @@ const LABEL_NEXT_YEAR = '翌年';
21
24
  * @param props - MonthRangePickerNavigation props
22
25
  */
23
26
  export function MonthRangePickerNavigation({ viewingValue, setViewingValue, minMonth, maxMonth, }) {
27
+ const displayJapaneseCalendar = useDisplayJapaneseCalendar();
28
+ const calendarLocale = useCalendarLocale();
24
29
  const currentYear = viewingValue.getFullYear();
25
30
  const nextYear = currentYear + 1;
31
+ const formatYear = (year) => {
32
+ if (displayJapaneseCalendar) {
33
+ return yearToEraLabel(year, calendarLocale);
34
+ }
35
+ return calendarLocale === 'en' ? String(year) : `${String(year)}年`;
36
+ };
37
+ const currentYearLabel = formatYear(currentYear);
38
+ const nextYearLabel = formatYear(nextYear);
26
39
  // Calculate if year navigation should be disabled based on min/max constraints
27
40
  const { isPreviousYearDisabled, isNextYearDisabled } = useMemo(() => {
28
41
  const baseMinYearDisabled = currentYear <= MINIMUM_VIEWABLE_YEAR;
@@ -65,5 +78,5 @@ export function MonthRangePickerNavigation({ viewingValue, setViewingValue, minM
65
78
  }
66
79
  }, [isNextYearDisabled, viewingValue, nextYear, setViewingValue]);
67
80
  const classNames = monthRangePickerNavigationSlotRecipe();
68
- return (_jsxs("div", { className: cx(classNames.root, 'mfui-MonthRangePickerNavigation__root'), children: [_jsxs("div", { className: cx(classNames.column, 'mfui-MonthRangePickerNavigation__column'), children: [_jsx(IconButton, { "aria-label": LABEL_PREV_YEAR, disabled: isPreviousYearDisabled, onClick: handlePreviousYear, children: _jsx(PickerBefore, {}) }), _jsxs(Typography, { variant: "contentHeading", children: [currentYear, "\u5E74"] }), _jsx("div", { className: cx(classNames.blankIconButton, 'mfui-MonthRangePickerNavigation__blankIconButton') })] }), _jsxs("div", { className: cx(classNames.column, 'mfui-MonthRangePickerNavigation__column'), children: [_jsx("div", { className: cx(classNames.blankIconButton, 'mfui-MonthRangePickerNavigation__blankIconButton') }), _jsxs(Typography, { variant: "contentHeading", children: [nextYear, "\u5E74"] }), _jsx(IconButton, { "aria-label": LABEL_NEXT_YEAR, disabled: isNextYearDisabled, onClick: handleNextYear, children: _jsx(PickerAfter, {}) })] })] }));
81
+ return (_jsxs("div", { className: cx(classNames.root, 'mfui-MonthRangePickerNavigation__root'), children: [_jsxs("div", { className: cx(classNames.column, 'mfui-MonthRangePickerNavigation__column'), children: [_jsx(IconButton, { "aria-label": LABEL_PREV_YEAR[calendarLocale], disabled: isPreviousYearDisabled, onClick: handlePreviousYear, children: _jsx(PickerBefore, {}) }), _jsx(Typography, { variant: "contentHeading", children: currentYearLabel }), _jsx("div", { className: cx(classNames.blankIconButton, 'mfui-MonthRangePickerNavigation__blankIconButton') })] }), _jsxs("div", { className: cx(classNames.column, 'mfui-MonthRangePickerNavigation__column'), children: [_jsx("div", { className: cx(classNames.blankIconButton, 'mfui-MonthRangePickerNavigation__blankIconButton') }), _jsx(Typography, { variant: "contentHeading", children: nextYearLabel }), _jsx(IconButton, { "aria-label": LABEL_NEXT_YEAR[calendarLocale], disabled: isNextYearDisabled, onClick: handleNextYear, children: _jsx(PickerAfter, {}) })] })] }));
69
82
  }
@@ -7,6 +7,7 @@ import { BaseRangePickerFooter } from '../../shared/BaseRangePicker/BaseRangePic
7
7
  import { MonthRangePickerNavigation } from '../MonthRangePickerNavigation';
8
8
  import { MonthRangePickerContent } from '../MonthRangePickerContent';
9
9
  import { useBaseRangePickerContext } from '../../shared/BaseRangePicker/BaseRangePickerProvider';
10
+ import { useCalendarLocale } from '../../shared/CalendarLocale/CalendarLocaleContext';
10
11
  /**
11
12
  * MonthRangePickerPanel component for selecting month ranges.
12
13
  *
@@ -32,6 +33,7 @@ import { useBaseRangePickerContext } from '../../shared/BaseRangePicker/BaseRang
32
33
  */
33
34
  export function MonthRangePickerPanel({ viewingValue, setViewingValue, value, minMonth, maxMonth, }) {
34
35
  const { setPendingFocusDate, handlePreview } = useBaseRangePickerContext();
36
+ const calendarLocale = useCalendarLocale();
35
37
  // Cross-Panel Keyboard Navigation for MonthRangePicker
36
38
  const handleKeyboardNavigation = useCallback((event) => {
37
39
  const { key, currentTarget } = event;
@@ -85,5 +87,5 @@ export function MonthRangePickerPanel({ viewingValue, setViewingValue, value, mi
85
87
  }
86
88
  }, [viewingValue, setViewingValue, setPendingFocusDate, handlePreview]);
87
89
  const classNames = monthRangePickerPanelSlotRecipe();
88
- return (_jsxs("div", { className: cx(classNames.root, 'mfui-MonthRangePickerPanel__root'), children: [_jsx(MonthRangePickerNavigation, { viewingValue: viewingValue, setViewingValue: setViewingValue, minMonth: minMonth, maxMonth: maxMonth }), _jsx(MonthRangePickerContent, { viewingValue: viewingValue, value: value, minMonth: minMonth, maxMonth: maxMonth, handleKeyboardNavigation: handleKeyboardNavigation }), _jsx(BaseRangePickerFooter, {})] }));
90
+ return (_jsxs("div", { className: cx(classNames.root, 'mfui-MonthRangePickerPanel__root'), children: [_jsx(MonthRangePickerNavigation, { viewingValue: viewingValue, setViewingValue: setViewingValue, minMonth: minMonth, maxMonth: maxMonth }), _jsx(MonthRangePickerContent, { viewingValue: viewingValue, value: value, minMonth: minMonth, maxMonth: maxMonth, handleKeyboardNavigation: handleKeyboardNavigation }), _jsx(BaseRangePickerFooter, { calendarLocale: calendarLocale })] }));
89
91
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * TimePicker is a styled wrapper around the native `<input type="time">`.
3
+ *
4
+ * The browser owns the picker UI: typing into the segmented control, the
5
+ * UA-provided dropdown, and Escape-to-revert semantics all come from the
6
+ * platform. The trailing clock icon opens the native picker — on desktop via
7
+ * `HTMLInputElement.showPicker()`, on iOS Safari (where `showPicker` is not
8
+ * reliably implemented) by dispatching a click on the underlying input, which
9
+ * is what iOS Safari uses to open its native time picker.
10
+ *
11
+ * The `value` prop follows the same string format as the HTML `<input type="time">`
12
+ * element: `"HH:mm"` for hours and minutes, `"HH:mm:ss"` for hours, minutes, and seconds.
13
+ *
14
+ * The component does not render its own `<label>`. Consumers are expected to
15
+ * provide an accessible name via a parent `<label htmlFor>`, `aria-labelledby`,
16
+ * or `aria-label`.
17
+ *
18
+ * @example
19
+ * // Controlled
20
+ * const [time, setTime] = useState<string>('');
21
+ * <TimePicker value={time} onChange={setTime} />
22
+ *
23
+ * @example
24
+ * // With seconds
25
+ * <TimePicker format="HH:mm:ss" />
26
+ */
27
+ export declare const TimePicker: import("react").ForwardRefExoticComponent<Omit<Omit<import("react").DetailedHTMLProps<import("react").InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "ref">, "type" | "aria-invalid" | keyof {
28
+ onChange?: (value: string, event: import("react").SyntheticEvent) => void;
29
+ format?: "HH:mm" | "HH:mm:ss";
30
+ size?: "small" | "medium" | "large";
31
+ invalid?: boolean;
32
+ onClear?: (event: import("react").MouseEvent<HTMLButtonElement>) => void;
33
+ clearButtonProps?: Pick<import("../..").IconButtonProps, "aria-label">;
34
+ pickerIndicatorIconButtonProps?: Pick<import("../..").IconButtonProps, "aria-label">;
35
+ }> & {
36
+ onChange?: (value: string, event: import("react").SyntheticEvent) => void;
37
+ format?: "HH:mm" | "HH:mm:ss";
38
+ size?: "small" | "medium" | "large";
39
+ invalid?: boolean;
40
+ onClear?: (event: import("react").MouseEvent<HTMLButtonElement>) => void;
41
+ clearButtonProps?: Pick<import("../..").IconButtonProps, "aria-label">;
42
+ pickerIndicatorIconButtonProps?: Pick<import("../..").IconButtonProps, "aria-label">;
43
+ } & import("react").RefAttributes<HTMLInputElement>>;
@@ -0,0 +1,85 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, useCallback, useRef, useState } from 'react';
4
+ import { Time } from '@moneyforward/mfui-icons-react';
5
+ import { cx } from '../../../styled-system/css';
6
+ import { timePickerSlotRecipe } from '../../../styled-system/recipes';
7
+ import { FocusIndicator } from '../../FocusIndicator';
8
+ import { InternalIconButton as IconButton } from '../../IconButton/IconButton';
9
+ import { ClearButton } from '../../shared/ClearButton';
10
+ import { mergeRefs } from '../../utilities/dom/mergeRefs';
11
+ import { getTimePickerLabel } from './constants';
12
+ /**
13
+ * TimePicker is a styled wrapper around the native `<input type="time">`.
14
+ *
15
+ * The browser owns the picker UI: typing into the segmented control, the
16
+ * UA-provided dropdown, and Escape-to-revert semantics all come from the
17
+ * platform. The trailing clock icon opens the native picker — on desktop via
18
+ * `HTMLInputElement.showPicker()`, on iOS Safari (where `showPicker` is not
19
+ * reliably implemented) by dispatching a click on the underlying input, which
20
+ * is what iOS Safari uses to open its native time picker.
21
+ *
22
+ * The `value` prop follows the same string format as the HTML `<input type="time">`
23
+ * element: `"HH:mm"` for hours and minutes, `"HH:mm:ss"` for hours, minutes, and seconds.
24
+ *
25
+ * The component does not render its own `<label>`. Consumers are expected to
26
+ * provide an accessible name via a parent `<label htmlFor>`, `aria-labelledby`,
27
+ * or `aria-label`.
28
+ *
29
+ * @example
30
+ * // Controlled
31
+ * const [time, setTime] = useState<string>('');
32
+ * <TimePicker value={time} onChange={setTime} />
33
+ *
34
+ * @example
35
+ * // With seconds
36
+ * <TimePicker format="HH:mm:ss" />
37
+ */
38
+ export const TimePicker = forwardRef(({ value, defaultValue, onChange, format = 'HH:mm', size = 'medium', disabled = false, invalid = false, step, onClear, clearButtonProps, pickerIndicatorIconButtonProps, className, ...inputProps }, ref) => {
39
+ const inputRef = useRef(null);
40
+ const mergedInputRef = mergeRefs(ref, inputRef);
41
+ // Track the live value internally so the clear button can read it whether
42
+ // the consumer is using `value` (controlled) or `defaultValue` (uncontrolled).
43
+ const [internalValue, setInternalValue] = useState(value ?? defaultValue ?? '');
44
+ const effectiveValue = value === undefined ? internalValue : value;
45
+ const hasValue = effectiveValue !== '';
46
+ const classes = timePickerSlotRecipe({ size, disabled, invalid });
47
+ const pickerLabel = pickerIndicatorIconButtonProps?.['aria-label'] ?? getTimePickerLabel('ja', 'openPicker');
48
+ const resolvedStep = step ?? (format === 'HH:mm:ss' ? 1 : undefined);
49
+ const handleClear = useCallback((event) => {
50
+ if (!inputRef.current)
51
+ return;
52
+ // eslint-disable-next-line @typescript-eslint/unbound-method
53
+ const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
54
+ setter?.call(inputRef.current, '');
55
+ setInternalValue('');
56
+ onChange?.('', event);
57
+ inputRef.current.focus();
58
+ onClear?.(event);
59
+ }, [onChange, onClear]);
60
+ // Try the spec-compliant `showPicker()` API first. On iOS Safari (where
61
+ // `showPicker` is not reliably implemented and frequently throws), fall
62
+ // back to dispatching a click on the input element — iOS Safari opens its
63
+ // native time picker in response to a user-initiated click on the input.
64
+ const triggerPicker = useCallback(() => {
65
+ const input = inputRef.current;
66
+ if (!input || disabled)
67
+ return;
68
+ if (typeof input.showPicker === 'function') {
69
+ try {
70
+ input.showPicker();
71
+ return;
72
+ }
73
+ catch {
74
+ // Fall through to the click fallback below.
75
+ }
76
+ }
77
+ input.click();
78
+ }, [disabled]);
79
+ return (_jsx(FocusIndicator, { children: _jsxs("div", { className: cx(classes.root, 'mfui-TimePicker__root', className), children: [_jsx("input", { ...inputProps, ref: mergedInputRef, type: "time", className: cx(classes.input, 'mfui-TimePicker__input'), value: value, defaultValue: defaultValue, disabled: disabled, step: resolvedStep, "aria-invalid": invalid || undefined, onChange: (event) => {
80
+ setInternalValue(event.target.value);
81
+ onChange?.(event.target.value, event);
82
+ } }), hasValue && !disabled ? (_jsx(ClearButton, { size: size === 'small' ? 'small' : 'default', "aria-label": clearButtonProps?.['aria-label'] ?? '値をクリアする', className: cx(classes.clearButton, 'mfui-TimePicker__clearButton'), disabled: disabled, onClick: handleClear })) : null, _jsx(IconButton, { size: size === 'small' ? 'small' : 'default', "aria-label": pickerLabel, className: cx(classes.pickerButton, 'mfui-TimePicker__pickerButton'), disabled: disabled, onClick: () => {
83
+ triggerPicker();
84
+ }, children: _jsx(Time, {}) })] }) }));
85
+ });