@moneyforward/mfui-components 3.20.0 → 3.22.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.
- package/dist/src/DateTimeSelection/DatePicker/DatePicker.js +2 -11
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePicker.d.ts +1 -1
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePicker.js +10 -2
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePicker.types.d.ts +14 -0
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerPopover/DateRangePickerPopover.js +13 -5
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerPopover/utilities/createDateRangePickerPopoverTestUtility.js +2 -2
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerTrigger/utilities/createDateRangePickerTriggerTestUtility.js +3 -1
- package/dist/src/DateTimeSelection/MonthPicker/MonthPicker.d.ts +1 -1
- package/dist/src/DateTimeSelection/MonthPicker/MonthPicker.js +10 -2
- package/dist/src/DateTimeSelection/MonthPicker/MonthPicker.types.d.ts +14 -0
- package/dist/src/DateTimeSelection/MonthPicker/MonthPickerPanel/MonthCell/MonthCell.js +2 -25
- package/dist/src/DateTimeSelection/MonthPicker/MonthPickerPanel/MonthPickerPanel.js +1 -1
- package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePicker.d.ts +1 -1
- package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePicker.js +14 -5
- package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePicker.types.d.ts +21 -0
- package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerMonthCell/MonthRangePickerMonthCell.d.ts +1 -0
- package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerMonthCell/MonthRangePickerMonthCell.js +10 -3
- package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerNavigation/MonthRangePickerNavigation.js +16 -3
- package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerPanel/MonthRangePickerPanel.js +3 -1
- package/dist/src/DateTimeSelection/TimePicker/TimePicker.d.ts +43 -0
- package/dist/src/DateTimeSelection/TimePicker/TimePicker.js +85 -0
- package/dist/src/DateTimeSelection/TimePicker/TimePicker.types.d.ts +61 -0
- package/dist/src/DateTimeSelection/TimePicker/TimePicker.types.js +1 -0
- package/dist/src/DateTimeSelection/TimePicker/constants.d.ts +4 -0
- package/dist/src/DateTimeSelection/TimePicker/constants.js +12 -0
- package/dist/src/DateTimeSelection/TimePicker/index.d.ts +2 -0
- package/dist/src/DateTimeSelection/TimePicker/index.js +1 -0
- package/dist/src/DateTimeSelection/index.d.ts +1 -0
- package/dist/src/DateTimeSelection/index.js +1 -0
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.d.ts +1 -1
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.js +2 -2
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.types.d.ts +10 -0
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.js +24 -20
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.types.d.ts +9 -0
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerTrigger/hooks/useDateRangeTriggerValueController.js +4 -4
- package/dist/src/DateTimeSelection/shared/CalendarLocale/CalendarLocaleContext.d.ts +1 -1
- package/dist/src/DateTimeSelection/shared/CalendarLocale/CalendarLocaleContext.js +1 -1
- package/dist/src/DateTimeSelection/shared/DayCell/DayCell.js +2 -3
- package/dist/src/DateTimeSelection/shared/YearSelector/YearSelector.js +1 -1
- package/dist/src/DateTimeSelection/shared/utilities/dateParsing.js +16 -9
- package/dist/src/DateTimeSelection/shared/utilities/japaneseCalendar.d.ts +36 -8
- package/dist/src/DateTimeSelection/shared/utilities/japaneseCalendar.js +82 -15
- package/dist/src/DateTimeSelection/shared/utilities/monthCellMonthFormat.d.ts +14 -0
- package/dist/src/DateTimeSelection/shared/utilities/monthCellMonthFormat.js +35 -0
- package/dist/src/FormFooter/FormFooter.types.d.ts +2 -3
- package/dist/src/SidePane/SidePane.d.ts +1 -1
- package/dist/src/SidePane/SidePane.types.d.ts +1 -3
- package/dist/src/Tooltip/Tooltip.js +12 -3
- package/dist/src/Tooltip/hooks/useTooltipDisplayController.d.ts +1 -0
- package/dist/src/Tooltip/hooks/useTooltipDisplayController.js +1 -0
- package/dist/styled-system/recipes/form-footer-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/form-footer-slot-recipe.js +0 -1
- package/dist/styled-system/recipes/index.d.ts +1 -0
- package/dist/styled-system/recipes/index.js +1 -0
- package/dist/styled-system/recipes/time-picker-slot-recipe.d.ts +44 -0
- package/dist/styled-system/recipes/time-picker-slot-recipe.js +62 -0
- package/dist/styled-system/tokens/index.js +892 -44
- package/dist/styled-system/tokens/tokens.d.ts +4 -4
- package/dist/styles.css +195 -35
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/dist/src/DateTimeSelection/shared/BasePicker/YearSelector/YearSelector.d.ts +0 -18
- 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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerPopover/DateRangePickerPopover.js
CHANGED
|
@@ -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
|
|
133
|
-
const formatMonth = useMemo(() =>
|
|
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
|
-
//
|
|
55
|
-
const monthLabels = screen.getAllByText(
|
|
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', {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,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 =
|
|
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, {}) }),
|
|
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
|
}
|
package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerPanel/MonthRangePickerPanel.js
CHANGED
|
@@ -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
|
+
});
|