@moneyforward/mfui-components 3.19.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.
- package/dist/src/CheckboxCard/CheckboxCard.js +1 -7
- 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/DateRangePickerProvider/DateRangePickerProvider.js +11 -12
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerTrigger/hooks/useDateRangeTriggerValueController.js +4 -3
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerTrigger/utilities/createDateRangePickerTriggerTestUtility.js +3 -1
- package/dist/src/DateTimeSelection/FilterDateRangePicker/DateRangePickerContent/DateRangePickerContent.d.ts +6 -0
- package/dist/src/DateTimeSelection/FilterDateRangePicker/DateRangePickerContent/DateRangePickerContent.js +33 -0
- package/dist/src/DateTimeSelection/FilterDateRangePicker/DateRangePickerContent/DateRangePickerContent.types.d.ts +17 -0
- package/dist/src/DateTimeSelection/FilterDateRangePicker/FilterDateRangePicker.d.ts +14 -0
- package/dist/src/DateTimeSelection/FilterDateRangePicker/FilterDateRangePicker.js +42 -0
- package/dist/src/DateTimeSelection/FilterDateRangePicker/FilterDateRangePicker.types.d.ts +78 -0
- package/dist/src/DateTimeSelection/FilterDateRangePicker/FilterDateRangePicker.types.js +1 -0
- package/dist/src/DateTimeSelection/FilterDateRangePicker/index.d.ts +2 -0
- package/dist/src/DateTimeSelection/FilterDateRangePicker/index.js +1 -0
- package/dist/src/DateTimeSelection/FilterMonthPicker/FilterMonthPicker.d.ts +14 -0
- package/dist/src/DateTimeSelection/FilterMonthPicker/FilterMonthPicker.js +75 -0
- package/dist/src/DateTimeSelection/FilterMonthPicker/FilterMonthPicker.types.d.ts +83 -0
- package/dist/src/DateTimeSelection/FilterMonthPicker/FilterMonthPicker.types.js +1 -0
- package/dist/src/DateTimeSelection/FilterMonthPicker/MonthPickerPopover/MonthPickerPopover.d.ts +6 -0
- package/dist/src/DateTimeSelection/FilterMonthPicker/MonthPickerPopover/MonthPickerPopover.js +45 -0
- package/dist/src/DateTimeSelection/FilterMonthPicker/MonthPickerPopover/MonthPickerPopover.types.d.ts +24 -0
- package/dist/src/DateTimeSelection/FilterMonthPicker/MonthPickerPopover/MonthPickerPopover.types.js +1 -0
- package/dist/src/DateTimeSelection/FilterMonthPicker/index.d.ts +2 -0
- package/dist/src/DateTimeSelection/FilterMonthPicker/index.js +1 -0
- package/dist/src/DateTimeSelection/FilterMonthRangePicker/FilterMonthRangePicker.d.ts +15 -0
- package/dist/src/DateTimeSelection/FilterMonthRangePicker/FilterMonthRangePicker.js +89 -0
- package/dist/src/DateTimeSelection/FilterMonthRangePicker/FilterMonthRangePicker.types.d.ts +75 -0
- package/dist/src/DateTimeSelection/FilterMonthRangePicker/FilterMonthRangePicker.types.js +1 -0
- package/dist/src/DateTimeSelection/FilterMonthRangePicker/MonthRangePickerPopover/MonthRangePickerPopover.d.ts +5 -0
- package/dist/src/DateTimeSelection/FilterMonthRangePicker/MonthRangePickerPopover/MonthRangePickerPopover.js +54 -0
- package/dist/src/DateTimeSelection/FilterMonthRangePicker/MonthRangePickerPopover/MonthRangePickerPopover.types.d.ts +19 -0
- package/dist/src/DateTimeSelection/FilterMonthRangePicker/MonthRangePickerPopover/MonthRangePickerPopover.types.js +1 -0
- package/dist/src/DateTimeSelection/FilterMonthRangePicker/index.d.ts +2 -0
- package/dist/src/DateTimeSelection/FilterMonthRangePicker/index.js +1 -0
- 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 +4 -0
- package/dist/src/DateTimeSelection/index.js +4 -0
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.d.ts +1 -1
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.js +26 -10
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.types.d.ts +19 -0
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.js +40 -19
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.types.d.ts +19 -0
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerTrigger/BaseRangePickerTrigger.js +4 -6
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerTrigger/hooks/useDateRangeTriggerValueController.js +4 -3
- 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/MonthGrid/MonthGrid.js +3 -3
- package/dist/src/DateTimeSelection/shared/YearSelector/YearSelector.js +1 -1
- package/dist/src/DateTimeSelection/shared/utilities/dateParsing.js +27 -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/MultilineTextBox/index.d.ts +10 -2
- package/dist/src/MultilineTextBox/index.js +9 -1
- package/dist/src/MultipleSelectBox/MultipleSelectBoxTrigger/MultipleSelectBoxTrigger.js +1 -3
- package/dist/src/RadioButtonCard/RadioButtonCard.js +1 -7
- package/dist/src/TextBox/TextBox.js +2 -6
- package/dist/src/{MultilineTextBox/MultilineTextBox.d.ts → Textarea/Textarea.d.ts} +5 -2
- package/dist/src/{MultilineTextBox/MultilineTextBox.js → Textarea/Textarea.js} +9 -5
- package/dist/src/{MultilineTextBox/MultilineTextBox.types.d.ts → Textarea/Textarea.types.d.ts} +2 -2
- package/dist/src/Textarea/Textarea.types.js +1 -0
- package/dist/src/Textarea/index.d.ts +2 -0
- package/dist/src/Textarea/index.js +1 -0
- package/dist/src/Tooltip/Tooltip.js +12 -3
- package/dist/src/Typography/Typography.js +1 -3
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/styled-system/recipes/filter-date-range-picker-slot-recipe.d.ts +33 -0
- package/dist/styled-system/recipes/filter-date-range-picker-slot-recipe.js +48 -0
- package/dist/styled-system/recipes/filter-month-picker-slot-recipe.d.ts +33 -0
- package/dist/styled-system/recipes/filter-month-picker-slot-recipe.js +44 -0
- package/dist/styled-system/recipes/filter-month-range-picker-slot-recipe.d.ts +33 -0
- package/dist/styled-system/recipes/filter-month-range-picker-slot-recipe.js +44 -0
- package/dist/styled-system/recipes/index.d.ts +5 -1
- package/dist/styled-system/recipes/index.js +5 -1
- package/dist/styled-system/recipes/textarea-slot-recipe.d.ts +52 -0
- package/dist/styled-system/recipes/textarea-slot-recipe.js +64 -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/styles.css +601 -25
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +13 -11
- package/dist/src/DateTimeSelection/shared/BasePicker/YearSelector/YearSelector.d.ts +0 -18
- package/dist/src/DateTimeSelection/shared/BasePicker/YearSelector/YearSelector.js +0 -36
- package/dist/styled-system/recipes/multiline-text-box-slot-recipe.d.ts +0 -52
- package/dist/styled-system/recipes/multiline-text-box-slot-recipe.js +0 -64
- /package/dist/src/{MultilineTextBox/MultilineTextBox.types.js → DateTimeSelection/FilterDateRangePicker/DateRangePickerContent/DateRangePickerContent.types.js} +0 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type MouseEvent, type SyntheticEvent } from 'react';
|
|
2
|
+
import { type IconButtonProps } from '../../IconButton';
|
|
3
|
+
type TimePickerOwnProps = {
|
|
4
|
+
/**
|
|
5
|
+
* Callback fired when the time value changes.
|
|
6
|
+
*
|
|
7
|
+
* @param value - The new time string (e.g., `"09:30"`) — empty string when cleared.
|
|
8
|
+
*
|
|
9
|
+
* @param event - The originating event. A `ChangeEvent<HTMLInputElement>` for
|
|
10
|
+
* input typing, a `MouseEvent` from the clear button when the value is cleared.
|
|
11
|
+
*/
|
|
12
|
+
onChange?: (value: string, event: SyntheticEvent) => void;
|
|
13
|
+
/**
|
|
14
|
+
* The display and input format. Drives the granularity of the underlying
|
|
15
|
+
* `<input type="time">` by setting `step=1` for seconds.
|
|
16
|
+
*
|
|
17
|
+
* **iOS Safari limitation**: iOS Safari ignores the `step` attribute on
|
|
18
|
+
* `<input type="time">`, so the seconds segment is never rendered or
|
|
19
|
+
* editable there. Setting `format="HH:mm:ss"` on iOS Safari still produces
|
|
20
|
+
* an HH:mm-only input; consumers that need seconds on iOS must collect
|
|
21
|
+
* them through a separate control.
|
|
22
|
+
*
|
|
23
|
+
* @default 'HH:mm'
|
|
24
|
+
*/
|
|
25
|
+
format?: 'HH:mm' | 'HH:mm:ss';
|
|
26
|
+
/**
|
|
27
|
+
* Size variant matching the Figma trigger: small (24px) / medium (32px) / large (48px).
|
|
28
|
+
*
|
|
29
|
+
* @default 'medium'
|
|
30
|
+
*/
|
|
31
|
+
size?: 'small' | 'medium' | 'large';
|
|
32
|
+
/**
|
|
33
|
+
* Whether the input is invalid (applies error border + sets `aria-invalid`).
|
|
34
|
+
*
|
|
35
|
+
* @default false
|
|
36
|
+
*/
|
|
37
|
+
invalid?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Callback fired when the clear button is clicked.
|
|
40
|
+
*/
|
|
41
|
+
onClear?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
42
|
+
/**
|
|
43
|
+
* Props for the clear IconButton. Use `aria-label` to override the default
|
|
44
|
+
* accessible name.
|
|
45
|
+
*/
|
|
46
|
+
clearButtonProps?: Pick<IconButtonProps, 'aria-label'>;
|
|
47
|
+
/**
|
|
48
|
+
* Props for the picker indicator IconButton (the clock icon). Use `aria-label`
|
|
49
|
+
* to override the default accessible name.
|
|
50
|
+
*/
|
|
51
|
+
pickerIndicatorIconButtonProps?: Pick<IconButtonProps, 'aria-label'>;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* `TimePicker` is a wrapper around `<input type="time">`, so it inherits the
|
|
55
|
+
* native input's attributes (`name`, `id`, `min`, `max`, `step`, `required`,
|
|
56
|
+
* `disabled`, `aria-*`, `onBlur`, `onFocus`, `className`, …) plus the
|
|
57
|
+
* component-specific props above. `onChange`, `size`, `type` and `aria-invalid`
|
|
58
|
+
* are intentionally omitted — they are owned by the component.
|
|
59
|
+
*/
|
|
60
|
+
export type TimePickerProps = Omit<ComponentPropsWithoutRef<'input'>, keyof TimePickerOwnProps | 'type' | 'aria-invalid'> & TimePickerOwnProps;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const TIME_PICKER_LABELS = {
|
|
2
|
+
ja: {
|
|
3
|
+
openPicker: '時刻を開く',
|
|
4
|
+
},
|
|
5
|
+
en: {
|
|
6
|
+
openPicker: 'Open time picker',
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
export function getTimePickerLabel(locale, key) {
|
|
10
|
+
const safeLocale = locale in TIME_PICKER_LABELS ? locale : 'ja';
|
|
11
|
+
return TIME_PICKER_LABELS[safeLocale][key];
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TimePicker } from './TimePicker';
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export * from './DatePicker';
|
|
2
2
|
export * from './FilterDatePicker';
|
|
3
|
+
export * from './FilterDateRangePicker';
|
|
4
|
+
export * from './FilterMonthPicker';
|
|
5
|
+
export * from './FilterMonthRangePicker';
|
|
3
6
|
export * from './MonthPicker';
|
|
4
7
|
export * from './DateRangePicker';
|
|
5
8
|
export * from './MonthRangePicker';
|
|
9
|
+
export * from './TimePicker';
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export * from './DatePicker';
|
|
2
2
|
export * from './FilterDatePicker';
|
|
3
|
+
export * from './FilterDateRangePicker';
|
|
4
|
+
export * from './FilterMonthPicker';
|
|
5
|
+
export * from './FilterMonthRangePicker';
|
|
3
6
|
export * from './MonthPicker';
|
|
4
7
|
export * from './DateRangePicker';
|
|
5
8
|
export * from './MonthRangePicker';
|
|
9
|
+
export * from './TimePicker';
|
|
@@ -3,4 +3,4 @@ import { type BaseRangePickerProps } from './BaseRangePicker.types';
|
|
|
3
3
|
* BaseRangePicker component
|
|
4
4
|
* A generic component for selecting a range of dates with configurable format
|
|
5
5
|
*/
|
|
6
|
-
export declare function BaseRangePicker({ value, defaultValue, disabled, invalid, targetDOMNode, onChange, format, onOpenStateChanged, disableAutoOpen, onBlur, allowedPlacements, enableViewportConstraint, enableClearButton, clearButtonProps, minDate, maxDate, enableAutoUnmount, minWidth, renderPopoverContent, startInputProps, endInputProps, calendarLocale, initialDisplayedMonths, initialViewingDate, }: BaseRangePickerProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export declare function BaseRangePicker({ value, defaultValue, disabled, invalid, targetDOMNode, onChange, format, onOpenStateChanged, disableAutoOpen, onBlur, allowedPlacements, enableViewportConstraint, enableClearButton, clearButtonProps, minDate, maxDate, enableAutoUnmount, minWidth, renderPopoverContent, startInputProps, endInputProps, calendarLocale, initialDisplayedMonths, initialViewingDate, customFormatValue, pendingFocusDayjsFormat, }: BaseRangePickerProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -9,8 +9,8 @@ import { dayjs } from '../../../utilities/date/dayjs';
|
|
|
9
9
|
/**
|
|
10
10
|
* Internal component that has access to BaseRangePickerContext
|
|
11
11
|
*/
|
|
12
|
-
function InternalBaseRangePicker({ startInputRef, endInputRef, disabled, invalid, format, enableClearButton, clearButtonProps, targetDOMNode, onBlur, isOpen, open, close, onOpenStateChanged, enableAutoUnmount, allowedPlacements, enableViewportConstraint, minWidth, renderPopoverContent, startInputProps = {}, endInputProps = {}, calendarLocale = 'ja', }) {
|
|
13
|
-
const { setPendingFocusDate, temporaryStart, temporaryEnd, viewingMonth, setViewingMonth } = useBaseRangePickerContext();
|
|
12
|
+
function InternalBaseRangePicker({ startInputRef, endInputRef, disabled, invalid, format, enableClearButton, clearButtonProps, targetDOMNode, onBlur, isOpen, open, close, onOpenStateChanged, enableAutoUnmount, allowedPlacements, enableViewportConstraint, minWidth, renderPopoverContent, startInputProps = {}, endInputProps = {}, calendarLocale = 'ja', pendingFocusDayjsFormat = 'YYYY-MM-DD', }) {
|
|
13
|
+
const { setPendingFocusDate, temporaryStart, temporaryEnd, startDate, endDate, viewingMonth, setViewingMonth } = useBaseRangePickerContext();
|
|
14
14
|
// Custom onOpen handler to prevent auto-focus when clicking on input elements
|
|
15
15
|
// and implement smart focus management for calendar dates
|
|
16
16
|
const handlePopoverOpen = useCallback((event) => {
|
|
@@ -31,7 +31,7 @@ function InternalBaseRangePicker({ startInputRef, endInputRef, disabled, invalid
|
|
|
31
31
|
// 3. Selected start date (if exists)
|
|
32
32
|
// 4. Selected end date (if exists and no start date)
|
|
33
33
|
// 5. Current date (today)
|
|
34
|
-
// 6. First non-disabled
|
|
34
|
+
// 6. First non-disabled cell (handled by CalendarGrid / MonthGrid automatically)
|
|
35
35
|
let dateToFocus;
|
|
36
36
|
if (temporaryStart) {
|
|
37
37
|
dateToFocus = temporaryStart;
|
|
@@ -39,16 +39,32 @@ function InternalBaseRangePicker({ startInputRef, endInputRef, disabled, invalid
|
|
|
39
39
|
else if (temporaryEnd) {
|
|
40
40
|
dateToFocus = temporaryEnd;
|
|
41
41
|
}
|
|
42
|
+
else if (startDate) {
|
|
43
|
+
dateToFocus = startDate;
|
|
44
|
+
}
|
|
45
|
+
else if (endDate) {
|
|
46
|
+
dateToFocus = endDate;
|
|
47
|
+
}
|
|
42
48
|
else {
|
|
43
|
-
// Default to today's date -
|
|
49
|
+
// Default to today's date - the grid will focus the first non-disabled cell if needed
|
|
44
50
|
dateToFocus = new Date();
|
|
45
51
|
}
|
|
46
|
-
// Set the pending focus
|
|
47
|
-
//
|
|
48
|
-
const formattedDate = dayjs(dateToFocus).format(
|
|
52
|
+
// Set the pending focus key so the popover grid can focus the matching cell when it renders.
|
|
53
|
+
// Must match DateCell `data-mfui-date` (YYYY-MM-DD) or MonthCell `data-mfui-month` (YYYY-MM).
|
|
54
|
+
const formattedDate = dayjs(dateToFocus).format(pendingFocusDayjsFormat);
|
|
49
55
|
setPendingFocusDate(formattedDate);
|
|
50
56
|
open();
|
|
51
|
-
}, [
|
|
57
|
+
}, [
|
|
58
|
+
startInputRef,
|
|
59
|
+
endInputRef,
|
|
60
|
+
temporaryStart,
|
|
61
|
+
temporaryEnd,
|
|
62
|
+
startDate,
|
|
63
|
+
endDate,
|
|
64
|
+
setPendingFocusDate,
|
|
65
|
+
open,
|
|
66
|
+
pendingFocusDayjsFormat,
|
|
67
|
+
]);
|
|
52
68
|
return (_jsx(Popover, { enableAutomaticPortalTargetResolution: true, open: isOpen, allowedPlacements: allowedPlacements, enableViewportConstraint: enableViewportConstraint, minWidth: minWidth, renderTrigger: ({ setTriggerRef, handleTriggerBlur, handleTriggerKeyDown, openPopover }) => (_jsx(BaseRangePickerTrigger, { ref: setTriggerRef, disabled: disabled, invalid: invalid, format: format, enableClearValue: enableClearButton, clearButtonProps: clearButtonProps, startInputRef: startInputRef, endInputRef: endInputRef, startInputProps: startInputProps, endInputProps: endInputProps, popoverOpenFunction: openPopover, calendarLocale: calendarLocale, onBlur: handleTriggerBlur, onKeyDown: handleTriggerKeyDown })), renderContent: () => renderPopoverContent({
|
|
53
69
|
viewingValue: viewingMonth,
|
|
54
70
|
setViewingValue: setViewingMonth,
|
|
@@ -61,12 +77,12 @@ function InternalBaseRangePicker({ startInputRef, endInputRef, disabled, invalid
|
|
|
61
77
|
* BaseRangePicker component
|
|
62
78
|
* A generic component for selecting a range of dates with configurable format
|
|
63
79
|
*/
|
|
64
|
-
export function BaseRangePicker({ value, defaultValue, disabled, invalid, targetDOMNode, onChange, format = 'YYYY/MM/DD', onOpenStateChanged, disableAutoOpen = false, onBlur, allowedPlacements, enableViewportConstraint, enableClearButton = false, clearButtonProps, minDate, maxDate, enableAutoUnmount = true, minWidth, renderPopoverContent, startInputProps, endInputProps, calendarLocale = 'ja', initialDisplayedMonths, initialViewingDate, }) {
|
|
80
|
+
export function BaseRangePicker({ value, defaultValue, disabled, invalid, targetDOMNode, onChange, format = 'YYYY/MM/DD', onOpenStateChanged, disableAutoOpen = false, onBlur, allowedPlacements, enableViewportConstraint, enableClearButton = false, clearButtonProps, minDate, maxDate, enableAutoUnmount = true, minWidth, renderPopoverContent, startInputProps, endInputProps, calendarLocale = 'ja', initialDisplayedMonths, initialViewingDate, customFormatValue, pendingFocusDayjsFormat = 'YYYY-MM-DD', }) {
|
|
65
81
|
const { isOpen, open, close } = useDisclosure({ value: false });
|
|
66
82
|
const startInputRef = useRef(null);
|
|
67
83
|
const endInputRef = useRef(null);
|
|
68
84
|
// Default renderPopoverContent implementation for backward compatibility
|
|
69
85
|
const defaultRenderPopoverContent = useCallback(() => _jsx(BaseRangePickerPopover, { calendarLocale: calendarLocale }), [calendarLocale]);
|
|
70
86
|
const finalRenderPopoverContent = renderPopoverContent ?? defaultRenderPopoverContent;
|
|
71
|
-
return (_jsx(BaseRangePickerProvider, { value: value, defaultValue: defaultValue, disabled: disabled, format: format, isOpen: isOpen, open: open, close: close, disableAutoOpen: disableAutoOpen, minDate: minDate, maxDate: maxDate, initialDisplayedMonths: initialDisplayedMonths, initialViewingDate: initialViewingDate, onChange: onChange, children: _jsx(InternalBaseRangePicker, { startInputRef: startInputRef, endInputRef: endInputRef, disabled: disabled, invalid: invalid, format: format, enableClearButton: enableClearButton, clearButtonProps: clearButtonProps, targetDOMNode: targetDOMNode, allowedPlacements: allowedPlacements, enableViewportConstraint: enableViewportConstraint, enableAutoUnmount: enableAutoUnmount, minWidth: minWidth, startInputProps: startInputProps, endInputProps: endInputProps, isOpen: isOpen, open: open, close: close, renderPopoverContent: finalRenderPopoverContent, calendarLocale: calendarLocale, onBlur: onBlur, onOpenStateChanged: onOpenStateChanged }) }));
|
|
87
|
+
return (_jsx(BaseRangePickerProvider, { value: value, defaultValue: defaultValue, disabled: disabled, format: format, isOpen: isOpen, open: open, close: close, disableAutoOpen: disableAutoOpen, minDate: minDate, maxDate: maxDate, initialDisplayedMonths: initialDisplayedMonths, initialViewingDate: initialViewingDate, customFormatValue: customFormatValue, pendingFocusDayjsFormat: pendingFocusDayjsFormat, onChange: onChange, children: _jsx(InternalBaseRangePicker, { startInputRef: startInputRef, endInputRef: endInputRef, disabled: disabled, invalid: invalid, format: format, enableClearButton: enableClearButton, clearButtonProps: clearButtonProps, targetDOMNode: targetDOMNode, allowedPlacements: allowedPlacements, enableViewportConstraint: enableViewportConstraint, enableAutoUnmount: enableAutoUnmount, minWidth: minWidth, startInputProps: startInputProps, endInputProps: endInputProps, isOpen: isOpen, open: open, close: close, renderPopoverContent: finalRenderPopoverContent, calendarLocale: calendarLocale, pendingFocusDayjsFormat: pendingFocusDayjsFormat, onBlur: onBlur, onOpenStateChanged: onOpenStateChanged }) }));
|
|
72
88
|
}
|
|
@@ -270,6 +270,16 @@ export type BaseRangePickerProps = {
|
|
|
270
270
|
* @default 'ja'
|
|
271
271
|
*/
|
|
272
272
|
calendarLocale?: BasePickerProps['calendarLocale'];
|
|
273
|
+
/**
|
|
274
|
+
* Custom function to format a Date value for display in the input fields.
|
|
275
|
+
* When provided, overrides the default dayjs formatting.
|
|
276
|
+
* Use this with `displayJapaneseCalendar` to show era-format strings in the inputs.
|
|
277
|
+
*
|
|
278
|
+
* @param date - The date to format
|
|
279
|
+
*
|
|
280
|
+
* @returns The formatted string to display
|
|
281
|
+
*/
|
|
282
|
+
customFormatValue?: (date: Date) => string;
|
|
273
283
|
/**
|
|
274
284
|
* Controls which months are initially displayed in the two-panel calendar view.
|
|
275
285
|
* - `'currentAndNext'`: Shows the value's start month on the left and the next month on the right.
|
|
@@ -307,4 +317,13 @@ export type BaseRangePickerProps = {
|
|
|
307
317
|
* @internal
|
|
308
318
|
*/
|
|
309
319
|
initialViewingDate?: Date;
|
|
320
|
+
/**
|
|
321
|
+
* dayjs format for pending grid focus strings (`pendingFocusDate` / `pendingFocusMonth`).
|
|
322
|
+
* Must match the grid: `YYYY-MM-DD` for day calendars, `YYYY-MM` for month grids.
|
|
323
|
+
*
|
|
324
|
+
* @internal
|
|
325
|
+
*
|
|
326
|
+
* @default 'YYYY-MM-DD'
|
|
327
|
+
*/
|
|
328
|
+
pendingFocusDayjsFormat?: 'YYYY-MM-DD' | 'YYYY-MM';
|
|
310
329
|
};
|
|
@@ -5,6 +5,7 @@ import { flushSync } from 'react-dom';
|
|
|
5
5
|
import { dayjs } from '../../../../utilities/date/dayjs';
|
|
6
6
|
import { useUpdateEffect } from '../../../../utilities/effect/useUpdateEffect';
|
|
7
7
|
import { useTransformedState } from '../../../../utilities/state/useTransformedState';
|
|
8
|
+
import { parseInputDate } from '../../utilities/dateParsing';
|
|
8
9
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
9
10
|
const noop = () => { };
|
|
10
11
|
// Normalizes a date to the first of the month
|
|
@@ -97,11 +98,12 @@ function autoCompletePartialRange(dates) {
|
|
|
97
98
|
}
|
|
98
99
|
return dates;
|
|
99
100
|
}
|
|
100
|
-
const useBaseRangePickerContextValue = ({ value, defaultValue, disabled = false, onChange, format = 'YYYY/MM/DD', isOpen = false, open = noop, close = noop, disableAutoOpen, minDate, maxDate, initialDisplayedMonths = 'currentAndNext', initialViewingDate, }) => {
|
|
101
|
+
const useBaseRangePickerContextValue = ({ value, defaultValue, disabled = false, onChange, format = 'YYYY/MM/DD', isOpen = false, open = noop, close = noop, disableAutoOpen, minDate, maxDate, initialDisplayedMonths = 'currentAndNext', initialViewingDate, customFormatValue, pendingFocusDayjsFormat = 'YYYY-MM-DD', }) => {
|
|
102
|
+
const formatDate = useMemo(() => customFormatValue ?? ((d) => dayjs(d).format(format)), [customFormatValue, format]);
|
|
101
103
|
const [dates, setDates] = useTransformedState(defaultValue ?? [undefined, undefined], normalizeToStartOfDay);
|
|
102
104
|
const [dateStrings, setDateStrings] = useState(() => {
|
|
103
105
|
const [startDate, endDate] = value ?? defaultValue ?? [undefined, undefined];
|
|
104
|
-
return [startDate ?
|
|
106
|
+
return [startDate ? formatDate(startDate) : '', endDate ? formatDate(endDate) : ''];
|
|
105
107
|
});
|
|
106
108
|
const [startDate, endDate] = value ?? dates;
|
|
107
109
|
const [startDateString, endDateString] = dateStrings;
|
|
@@ -134,9 +136,9 @@ const useBaseRangePickerContextValue = ({ value, defaultValue, disabled = false,
|
|
|
134
136
|
useUpdateEffect(() => {
|
|
135
137
|
if (value) {
|
|
136
138
|
setDates(value);
|
|
137
|
-
setDateStrings([value[0] ?
|
|
139
|
+
setDateStrings([value[0] ? formatDate(value[0]) : '', value[1] ? formatDate(value[1]) : '']);
|
|
138
140
|
}
|
|
139
|
-
}, [value,
|
|
141
|
+
}, [value, formatDate, setDates]);
|
|
140
142
|
const handleChange = useCallback((newDates) => {
|
|
141
143
|
let [newStartDate, newEndDate] = normalizeToStartOfDay(newDates);
|
|
142
144
|
// Swap dates if start date is after end date
|
|
@@ -145,12 +147,9 @@ const useBaseRangePickerContextValue = ({ value, defaultValue, disabled = false,
|
|
|
145
147
|
}
|
|
146
148
|
setDates([newStartDate, newEndDate]);
|
|
147
149
|
// In uncontrolled mode, update dateStrings for input display
|
|
148
|
-
setDateStrings([
|
|
149
|
-
newStartDate ? dayjs(newStartDate).format(format) : '',
|
|
150
|
-
newEndDate ? dayjs(newEndDate).format(format) : '',
|
|
151
|
-
]);
|
|
150
|
+
setDateStrings([newStartDate ? formatDate(newStartDate) : '', newEndDate ? formatDate(newEndDate) : '']);
|
|
152
151
|
onChange?.([newStartDate, newEndDate]);
|
|
153
|
-
}, [onChange,
|
|
152
|
+
}, [onChange, formatDate, setDates]);
|
|
154
153
|
const handleStartDateChange = useCallback((date, nextEndDate) => {
|
|
155
154
|
const proposedEndDate = nextEndDate !== undefined ? nextEndDate : endDate;
|
|
156
155
|
// Apply auto-completion behavior when start date is set and end date is missing
|
|
@@ -176,39 +175,61 @@ const useBaseRangePickerContextValue = ({ value, defaultValue, disabled = false,
|
|
|
176
175
|
const handleStartDateStringChange = useCallback((value) => {
|
|
177
176
|
setDateStrings([value, endDateString]);
|
|
178
177
|
// Parse and update both viewing month and temporary selection for immediate visual feedback
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
|
|
178
|
+
const parseResult = parseInputDate(value, format);
|
|
179
|
+
if (parseResult.isValid &&
|
|
180
|
+
parseResult.result &&
|
|
181
|
+
!isDateOutsideConstraints(parseResult.result, minDate, maxDate, format)) {
|
|
182
|
+
const newDate = parseResult.result;
|
|
182
183
|
setViewingMonth(newDate);
|
|
183
184
|
// Update temporary selection for immediate calendar visual feedback
|
|
184
185
|
setTemporaryStart(newDate);
|
|
185
186
|
// Set pending focus date so when calendar opens, it focuses on this date
|
|
186
|
-
setPendingFocusDate(
|
|
187
|
+
setPendingFocusDate(dayjs(newDate).format(pendingFocusDayjsFormat));
|
|
187
188
|
}
|
|
188
189
|
else if (value === '') {
|
|
189
190
|
// Clear temporary selection if input is empty
|
|
190
191
|
setTemporaryStart(undefined);
|
|
191
192
|
setPendingFocusDate(null);
|
|
192
193
|
}
|
|
193
|
-
}, [
|
|
194
|
+
}, [
|
|
195
|
+
endDateString,
|
|
196
|
+
format,
|
|
197
|
+
minDate,
|
|
198
|
+
maxDate,
|
|
199
|
+
setViewingMonth,
|
|
200
|
+
setTemporaryStart,
|
|
201
|
+
setPendingFocusDate,
|
|
202
|
+
pendingFocusDayjsFormat,
|
|
203
|
+
]);
|
|
194
204
|
const handleEndDateStringChange = useCallback((value) => {
|
|
195
205
|
setDateStrings([startDateString, value]);
|
|
196
206
|
// Parse and update both viewing month and temporary selection for immediate visual feedback
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
199
|
-
|
|
207
|
+
const parseResult = parseInputDate(value, format);
|
|
208
|
+
if (parseResult.isValid &&
|
|
209
|
+
parseResult.result &&
|
|
210
|
+
!isDateOutsideConstraints(parseResult.result, minDate, maxDate, format)) {
|
|
211
|
+
const newDate = parseResult.result;
|
|
200
212
|
setViewingMonth(newDate);
|
|
201
213
|
// Update temporary selection for immediate calendar visual feedback
|
|
202
214
|
setTemporaryEnd(newDate);
|
|
203
215
|
// Set pending focus date so when calendar opens, it focuses on this date
|
|
204
|
-
setPendingFocusDate(
|
|
216
|
+
setPendingFocusDate(dayjs(newDate).format(pendingFocusDayjsFormat));
|
|
205
217
|
}
|
|
206
218
|
else if (value === '') {
|
|
207
219
|
// Clear temporary selection if input is empty
|
|
208
220
|
setTemporaryEnd(undefined);
|
|
209
221
|
setPendingFocusDate(null);
|
|
210
222
|
}
|
|
211
|
-
}, [
|
|
223
|
+
}, [
|
|
224
|
+
startDateString,
|
|
225
|
+
format,
|
|
226
|
+
minDate,
|
|
227
|
+
maxDate,
|
|
228
|
+
setViewingMonth,
|
|
229
|
+
setTemporaryEnd,
|
|
230
|
+
setPendingFocusDate,
|
|
231
|
+
pendingFocusDayjsFormat,
|
|
232
|
+
]);
|
|
212
233
|
const handleClear = useCallback(() => {
|
|
213
234
|
// handleChange already calls setDates and setDateStrings, so we don't need to call them again
|
|
214
235
|
handleChange([undefined, undefined]);
|
|
@@ -97,4 +97,23 @@ export type BaseRangePickerProviderProps = {
|
|
|
97
97
|
* @internal
|
|
98
98
|
*/
|
|
99
99
|
initialViewingDate?: Date;
|
|
100
|
+
/**
|
|
101
|
+
* Custom function to format a Date value for display in the input fields.
|
|
102
|
+
* When provided, overrides the default dayjs formatting.
|
|
103
|
+
*
|
|
104
|
+
* @param date - The date to format
|
|
105
|
+
*
|
|
106
|
+
* @returns The formatted string to display
|
|
107
|
+
*/
|
|
108
|
+
customFormatValue?: (date: Date) => string;
|
|
109
|
+
/**
|
|
110
|
+
* dayjs format string for `pendingFocusDate` so it matches the focused grid cell's `data-mfui-*` attribute.
|
|
111
|
+
*
|
|
112
|
+
* @internal
|
|
113
|
+
*
|
|
114
|
+
* @default 'YYYY-MM-DD' — matches {@link DateCell} `data-mfui-date`
|
|
115
|
+
*
|
|
116
|
+
* @example 'YYYY-MM' — matches {@link MonthCell} `data-mfui-month` (MonthRangePicker)
|
|
117
|
+
*/
|
|
118
|
+
pendingFocusDayjsFormat?: 'YYYY-MM-DD' | 'YYYY-MM';
|
|
100
119
|
};
|
|
@@ -19,7 +19,7 @@ import { useDateRangeTriggerInputClick } from './hooks/useDateRangeTriggerInputC
|
|
|
19
19
|
*/
|
|
20
20
|
export const BaseRangePickerTrigger = forwardRef(({ enableClearValue = false, invalid = false, size = 'medium', disabled = false, wrapperProps = {}, clearButtonProps = {}, startInputProps = {}, endInputProps = {}, calendarButtonProps = {}, startInputRef, endInputRef, popoverOpenFunction, format = 'YYYY-MM-DD', calendarLocale = 'ja', ...props }, ref) => {
|
|
21
21
|
const isTouchDevice = useIsTouchDevice();
|
|
22
|
-
const { toggle, disableAutoOpen } = useBaseRangePickerContext();
|
|
22
|
+
const { toggle, disableAutoOpen, isOpen } = useBaseRangePickerContext();
|
|
23
23
|
const { handleInputClick } = useDateRangeTriggerInputClick(disableAutoOpen ?? false, popoverOpenFunction);
|
|
24
24
|
const { onClick: onClickClearButton, 'aria-label': clearAriaLabel } = clearButtonProps;
|
|
25
25
|
const { onClick: onCalendarClick, 'aria-label': calendarAriaLabel } = calendarButtonProps;
|
|
@@ -41,14 +41,12 @@ export const BaseRangePickerTrigger = forwardRef(({ enableClearValue = false, in
|
|
|
41
41
|
const finalEndPlaceholder = endInputPlaceholder ?? getBaseRangePickerTriggerLabel('END_DATE_PLACEHOLDER', calendarLocale);
|
|
42
42
|
const shouldShowClearButton = enableClearValue && (enhancedStartInputProps.value || enhancedEndInputProps.value);
|
|
43
43
|
const classes = baseRangePickerTriggerSlotRecipe({ invalid, disabled, size });
|
|
44
|
+
// Popover open: mark calendar icon active. Use `undefined` when closed — `data-active="false"` is still truthy in CSS.
|
|
45
|
+
const shouldCalendarIconActive = isOpen ? true : undefined;
|
|
44
46
|
const handleClickClearButton = useCallback((event) => {
|
|
45
47
|
handleClear();
|
|
46
48
|
onClickClearButton?.(event);
|
|
47
49
|
}, [handleClear, onClickClearButton]);
|
|
48
|
-
return (_jsxs("div", { ...wrapperProps, ...props, ref: ref, className: cx(classes.root, 'mfui-BaseRangePickerTrigger__root', wrapperProps.className), children: [_jsx("div", { className: cx(classes.inputGroup, 'mfui-BaseRangePickerTrigger__inputGroup'), children: isTouchDevice ? (
|
|
49
|
-
// Touch device: hidden inputs + display divs
|
|
50
|
-
_jsxs(_Fragment, { children: [_jsx("input", { type: "hidden", value: enhancedStartInputProps.value, name: startInputProps.name }), _jsx("input", { type: "hidden", value: enhancedEndInputProps.value, name: endInputProps.name }), _jsx("div", { className: cx(classes.input, 'mfui-BaseRangePickerTrigger__input'), onClick: !disableAutoOpen && !disabled ? handleInputClick('start', startInputProps.onClick) : undefined, children: enhancedStartInputProps.value || finalStartPlaceholder }), _jsx(Typography, { variant: "controlLabel", className: cx(classes.separator, 'mfui-BaseRangePickerTrigger__separator'), children: SEPARATOR_CONTENT }), _jsx("div", { className: cx(classes.input, 'mfui-BaseRangePickerTrigger__input'), onClick: !disableAutoOpen && !disabled ? handleInputClick('end', endInputProps.onClick) : undefined, children: enhancedEndInputProps.value || finalEndPlaceholder })] })) : (
|
|
51
|
-
// Desktop: Standard inputs with FocusIndicator
|
|
52
|
-
_jsxs(_Fragment, { children: [_jsx(FocusIndicator, { position: "inside", children: _jsx("input", { ref: startInputRef, disabled: disabled, className: cx(classes.input, 'mfui-BaseRangePickerTrigger__input'), ...enhancedStartInputProps, placeholder: finalStartPlaceholder }) }), _jsx(Typography, { variant: "controlLabel", className: cx(classes.separator, 'mfui-BaseRangePickerTrigger__separator'), children: SEPARATOR_CONTENT }), _jsx(FocusIndicator, { position: "inside", children: _jsx("input", { ref: endInputRef, disabled: disabled, className: cx(classes.input, 'mfui-BaseRangePickerTrigger__input'), ...enhancedEndInputProps, placeholder: finalEndPlaceholder }) })] })) }), shouldShowClearButton ? (_jsx(ClearButton, { size: size === 'small' ? 'small' : 'default', "aria-label": clearAriaLabel ?? getBaseRangePickerTriggerLabel('CLEAR_BUTTON_LABEL', calendarLocale), className: cx(classes.actionIcon, 'mfui-BaseRangePickerTrigger__actionIcon'), disabled: disabled, onClick: handleClickClearButton })) : null, _jsx(IconButton, { size: size === 'small' ? 'small' : 'default', "aria-label": calendarAriaLabel ?? getBaseRangePickerTriggerLabel('CALENDAR_BUTTON_LABEL', calendarLocale), className: cx(classes.actionIcon, 'mfui-BaseRangePickerTrigger__actionIcon'), disabled: disabled, onClick: onCalendarClick ?? toggle, children: _jsx(Calendar, {}) })] }));
|
|
50
|
+
return (_jsxs("div", { ...wrapperProps, ...props, ref: ref, className: cx(classes.root, 'mfui-BaseRangePickerTrigger__root', wrapperProps.className), children: [_jsx("div", { className: cx(classes.inputGroup, 'mfui-BaseRangePickerTrigger__inputGroup'), children: isTouchDevice ? (_jsxs(_Fragment, { children: [_jsx("input", { type: "hidden", value: enhancedStartInputProps.value, name: startInputProps.name }), _jsx("input", { type: "hidden", value: enhancedEndInputProps.value, name: endInputProps.name }), _jsx("div", { className: cx(classes.input, 'mfui-BaseRangePickerTrigger__input'), onClick: !disableAutoOpen && !disabled ? handleInputClick('start', startInputProps.onClick) : undefined, children: enhancedStartInputProps.value || finalStartPlaceholder }), _jsx(Typography, { variant: "controlLabel", className: cx(classes.separator, 'mfui-BaseRangePickerTrigger__separator'), children: SEPARATOR_CONTENT }), _jsx("div", { className: cx(classes.input, 'mfui-BaseRangePickerTrigger__input'), onClick: !disableAutoOpen && !disabled ? handleInputClick('end', endInputProps.onClick) : undefined, children: enhancedEndInputProps.value || finalEndPlaceholder })] })) : (_jsxs(_Fragment, { children: [_jsx(FocusIndicator, { position: "inside", children: _jsx("input", { ref: startInputRef, disabled: disabled, className: cx(classes.input, 'mfui-BaseRangePickerTrigger__input'), ...enhancedStartInputProps, placeholder: finalStartPlaceholder }) }), _jsx(Typography, { variant: "controlLabel", className: cx(classes.separator, 'mfui-BaseRangePickerTrigger__separator'), children: SEPARATOR_CONTENT }), _jsx(FocusIndicator, { position: "inside", children: _jsx("input", { ref: endInputRef, disabled: disabled, className: cx(classes.input, 'mfui-BaseRangePickerTrigger__input'), ...enhancedEndInputProps, placeholder: finalEndPlaceholder }) })] })) }), shouldShowClearButton ? (_jsx(ClearButton, { size: size === 'small' ? 'small' : 'default', "aria-label": clearAriaLabel ?? getBaseRangePickerTriggerLabel('CLEAR_BUTTON_LABEL', calendarLocale), className: cx(classes.actionIcon, 'mfui-BaseRangePickerTrigger__actionIcon'), disabled: disabled, onClick: handleClickClearButton })) : null, _jsx(IconButton, { size: size === 'small' ? 'small' : 'default', "aria-label": calendarAriaLabel ?? getBaseRangePickerTriggerLabel('CALENDAR_BUTTON_LABEL', calendarLocale), className: cx(classes.actionIcon, 'mfui-BaseRangePickerTrigger__actionIcon'), "data-active": shouldCalendarIconActive, disabled: disabled, onClick: onCalendarClick ?? toggle, children: _jsx(Calendar, {}) })] }));
|
|
53
51
|
});
|
|
54
52
|
BaseRangePickerTrigger.displayName = 'BaseRangePicker.Trigger';
|