@moneyforward/mfui-components 4.10.3 → 4.10.4
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/DateRangePicker/DateRangePickerProvider/DateRangePickerProvider.js +3 -8
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.js +29 -41
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.js +7 -27
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.types.d.ts +0 -10
- package/package.json +1 -1
|
@@ -53,6 +53,7 @@ const useDateRangePickerContextValue = ({ value, defaultValue, disabled = false,
|
|
|
53
53
|
const [temporaryEnd, setTemporaryEnd] = useState(endDate);
|
|
54
54
|
const [previewDate, setPreviewDate] = useState();
|
|
55
55
|
// --- Focus Management ---
|
|
56
|
+
// Moves DOM focus into the grid; typing must never set it (see BaseRangePickerProvider)
|
|
56
57
|
const [pendingFocusDate, setPendingFocusDate] = useState(null);
|
|
57
58
|
useUpdateEffect(() => {
|
|
58
59
|
if (startDate) {
|
|
@@ -98,15 +99,12 @@ const useDateRangePickerContextValue = ({ value, defaultValue, disabled = false,
|
|
|
98
99
|
setViewingMonth(parsedDate);
|
|
99
100
|
// Update temporary selection for immediate calendar visual feedback
|
|
100
101
|
setTemporaryStart(parsedDate);
|
|
101
|
-
// Set pending focus date so when calendar opens, it focuses on this date
|
|
102
|
-
setPendingFocusDate(dayjs(parsedDate).format('YYYY-MM-DD'));
|
|
103
102
|
}
|
|
104
103
|
else if (value === '') {
|
|
105
104
|
// Clear temporary selection if input is empty
|
|
106
105
|
setTemporaryStart(undefined);
|
|
107
|
-
setPendingFocusDate(null);
|
|
108
106
|
}
|
|
109
|
-
}, [endDateString, format, minDate, maxDate, setViewingMonth, setTemporaryStart
|
|
107
|
+
}, [endDateString, format, minDate, maxDate, setViewingMonth, setTemporaryStart]);
|
|
110
108
|
const handleEndDateStringChange = useCallback((value) => {
|
|
111
109
|
setDateStrings([startDateString, value]);
|
|
112
110
|
// Parse and update both viewing month and temporary selection for immediate visual feedback
|
|
@@ -115,15 +113,12 @@ const useDateRangePickerContextValue = ({ value, defaultValue, disabled = false,
|
|
|
115
113
|
setViewingMonth(parsedDate);
|
|
116
114
|
// Update temporary selection for immediate calendar visual feedback
|
|
117
115
|
setTemporaryEnd(parsedDate);
|
|
118
|
-
// Set pending focus date so when calendar opens, it focuses on this date
|
|
119
|
-
setPendingFocusDate(dayjs(parsedDate).format('YYYY-MM-DD'));
|
|
120
116
|
}
|
|
121
117
|
else if (value === '') {
|
|
122
118
|
// Clear temporary selection if input is empty
|
|
123
119
|
setTemporaryEnd(undefined);
|
|
124
|
-
setPendingFocusDate(null);
|
|
125
120
|
}
|
|
126
|
-
}, [startDateString, format, minDate, maxDate, setViewingMonth, setTemporaryEnd
|
|
121
|
+
}, [startDateString, format, minDate, maxDate, setViewingMonth, setTemporaryEnd]);
|
|
127
122
|
const handleClear = useCallback(() => {
|
|
128
123
|
// handleChange already calls setDates and setDateStrings, so we don't need to call them again
|
|
129
124
|
handleChange([undefined, undefined]);
|
|
@@ -11,6 +11,20 @@ import { dayjs } from '../../../utilities/date/dayjs';
|
|
|
11
11
|
*/
|
|
12
12
|
function InternalBaseRangePicker({ startInputRef, endInputRef, disabled, invalid, caution, format, enableClearButton, clearButtonProps, targetDOMNode, onBlur, isOpen, open, close, onOpenStateChanged, enableAutoUnmount, allowedPlacements, enableViewportConstraint, minWidth, renderPopoverContent, startInputProps = {}, endInputProps = {}, calendarLocale = 'ja', pendingFocusDayjsFormat = 'YYYY-MM-DD', }) {
|
|
13
13
|
const { setPendingFocusDate, temporaryStart, temporaryEnd, startDate, endDate, viewingMonth, setViewingMonth } = useBaseRangePickerContext();
|
|
14
|
+
// Moves DOM focus into the popover grid, onto the most relevant date
|
|
15
|
+
const focusGridDate = useCallback(() => {
|
|
16
|
+
// Determine which date to focus on with priority:
|
|
17
|
+
// 1. Temporary start date (if exists - from typed input)
|
|
18
|
+
// 2. Temporary end date (if exists and no start date - from typed input)
|
|
19
|
+
// 3. Selected start date (if exists)
|
|
20
|
+
// 4. Selected end date (if exists and no start date)
|
|
21
|
+
// 5. Current date (today)
|
|
22
|
+
// 6. First non-disabled cell (handled by CalendarGrid / MonthGrid automatically)
|
|
23
|
+
const dateToFocus = temporaryStart ?? temporaryEnd ?? startDate ?? endDate ?? new Date();
|
|
24
|
+
// Set the pending focus key so the popover grid can focus the matching cell when it renders.
|
|
25
|
+
// Must match DateCell `data-mfui-date` (YYYY-MM-DD) or MonthCell `data-mfui-month` (YYYY-MM).
|
|
26
|
+
setPendingFocusDate(dayjs(dateToFocus).format(pendingFocusDayjsFormat));
|
|
27
|
+
}, [temporaryStart, temporaryEnd, startDate, endDate, setPendingFocusDate, pendingFocusDayjsFormat]);
|
|
14
28
|
// Custom onOpen handler to prevent auto-focus when clicking on input elements
|
|
15
29
|
// and implement smart focus management for calendar dates
|
|
16
30
|
const handlePopoverOpen = useCallback((event) => {
|
|
@@ -25,47 +39,21 @@ function InternalBaseRangePicker({ startInputRef, endInputRef, disabled, invalid
|
|
|
25
39
|
}
|
|
26
40
|
// For all other cases (calendar button, keyboard navigation, etc.),
|
|
27
41
|
// implement smart focus management for the calendar
|
|
28
|
-
|
|
29
|
-
// 1. Temporary start date (if exists - from typed input)
|
|
30
|
-
// 2. Temporary end date (if exists and no start date - from typed input)
|
|
31
|
-
// 3. Selected start date (if exists)
|
|
32
|
-
// 4. Selected end date (if exists and no start date)
|
|
33
|
-
// 5. Current date (today)
|
|
34
|
-
// 6. First non-disabled cell (handled by CalendarGrid / MonthGrid automatically)
|
|
35
|
-
let dateToFocus;
|
|
36
|
-
if (temporaryStart) {
|
|
37
|
-
dateToFocus = temporaryStart;
|
|
38
|
-
}
|
|
39
|
-
else if (temporaryEnd) {
|
|
40
|
-
dateToFocus = temporaryEnd;
|
|
41
|
-
}
|
|
42
|
-
else if (startDate) {
|
|
43
|
-
dateToFocus = startDate;
|
|
44
|
-
}
|
|
45
|
-
else if (endDate) {
|
|
46
|
-
dateToFocus = endDate;
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
// Default to today's date - the grid will focus the first non-disabled cell if needed
|
|
50
|
-
dateToFocus = new Date();
|
|
51
|
-
}
|
|
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);
|
|
55
|
-
setPendingFocusDate(formattedDate);
|
|
42
|
+
focusGridDate();
|
|
56
43
|
open();
|
|
57
|
-
}, [
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
44
|
+
}, [startInputRef, endInputRef, focusGridDate, open]);
|
|
45
|
+
// Popover fires `onOpen` only on a closed -> open transition, so Up/Down while the popover is
|
|
46
|
+
// already open (e.g. opened by clicking an input) has to move focus into the grid here.
|
|
47
|
+
const createTriggerKeyDownHandler = useCallback((handleTriggerKeyDown) => (event) => {
|
|
48
|
+
if (isOpen && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
|
|
49
|
+
event.stopPropagation();
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
focusGridDate();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
handleTriggerKeyDown(event);
|
|
55
|
+
}, [isOpen, focusGridDate]);
|
|
56
|
+
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, caution: caution, format: format, enableClearValue: enableClearButton, clearButtonProps: clearButtonProps, startInputRef: startInputRef, endInputRef: endInputRef, startInputProps: startInputProps, endInputProps: endInputProps, popoverOpenFunction: openPopover, calendarLocale: calendarLocale, onBlur: handleTriggerBlur, onKeyDown: createTriggerKeyDownHandler(handleTriggerKeyDown) })), renderContent: () => renderPopoverContent({
|
|
69
57
|
viewingValue: viewingMonth,
|
|
70
58
|
setViewingValue: setViewingMonth,
|
|
71
59
|
value: [temporaryStart, temporaryEnd],
|
|
@@ -84,5 +72,5 @@ export function BaseRangePicker({ value, defaultValue, disabled, invalid, cautio
|
|
|
84
72
|
// Default renderPopoverContent implementation for backward compatibility
|
|
85
73
|
const defaultRenderPopoverContent = useCallback(() => _jsx(BaseRangePickerPopover, { calendarLocale: calendarLocale }), [calendarLocale]);
|
|
86
74
|
const finalRenderPopoverContent = renderPopoverContent ?? defaultRenderPopoverContent;
|
|
87
|
-
return (_jsx(BaseRangePickerProvider, { value: value, defaultValue: defaultValue, disabled: disabled, format: format, isOpen: isOpen, open: open, close: close, disableAutoOpen: disableAutoOpen, disableAutoSwap: disableAutoSwap, minDate: minDate, maxDate: maxDate, initialDisplayedMonths: initialDisplayedMonths, initialViewingDate: initialViewingDate, customFormatValue: customFormatValue,
|
|
75
|
+
return (_jsx(BaseRangePickerProvider, { value: value, defaultValue: defaultValue, disabled: disabled, format: format, isOpen: isOpen, open: open, close: close, disableAutoOpen: disableAutoOpen, disableAutoSwap: disableAutoSwap, minDate: minDate, maxDate: maxDate, initialDisplayedMonths: initialDisplayedMonths, initialViewingDate: initialViewingDate, customFormatValue: customFormatValue, onChange: onChange, children: _jsx(InternalBaseRangePicker, { startInputRef: startInputRef, endInputRef: endInputRef, disabled: disabled, invalid: invalid, caution: caution, 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 }) }));
|
|
88
76
|
}
|
|
@@ -98,7 +98,7 @@ function autoCompletePartialRange(dates) {
|
|
|
98
98
|
}
|
|
99
99
|
return dates;
|
|
100
100
|
}
|
|
101
|
-
const useBaseRangePickerContextValue = ({ value, defaultValue, disabled = false, onChange, format = 'YYYY/MM/DD', isOpen = false, open = noop, close = noop, disableAutoOpen, disableAutoSwap = false, minDate, maxDate, initialDisplayedMonths = 'currentAndNext', initialViewingDate, customFormatValue,
|
|
101
|
+
const useBaseRangePickerContextValue = ({ value, defaultValue, disabled = false, onChange, format = 'YYYY/MM/DD', isOpen = false, open = noop, close = noop, disableAutoOpen, disableAutoSwap = false, minDate, maxDate, initialDisplayedMonths = 'currentAndNext', initialViewingDate, customFormatValue, }) => {
|
|
102
102
|
const formatDate = useMemo(() => customFormatValue ?? ((d) => dayjs(d).format(format)), [customFormatValue, format]);
|
|
103
103
|
const [dates, setDates] = useTransformedState(defaultValue ?? [undefined, undefined], normalizeToStartOfDay);
|
|
104
104
|
const [dateStrings, setDateStrings] = useState(() => {
|
|
@@ -122,6 +122,10 @@ const useBaseRangePickerContextValue = ({ value, defaultValue, disabled = false,
|
|
|
122
122
|
const [temporaryEnd, setTemporaryEnd] = useState(endDate);
|
|
123
123
|
const [previewDate, setPreviewDate] = useState();
|
|
124
124
|
// --- Focus Management ---
|
|
125
|
+
// `pendingFocusDate` moves real DOM focus into the grid, so it must only be set by deliberate
|
|
126
|
+
// navigation (opening the popover, Up/Down). Typing must never write it: the typed date is
|
|
127
|
+
// highlighted through `viewingMonth` and `temporaryStart`/`temporaryEnd` instead, so the caret
|
|
128
|
+
// stays in the input and a partially typed date is not committed on blur.
|
|
125
129
|
const [pendingFocusDate, setPendingFocusDate] = useState(null);
|
|
126
130
|
// The committed range anchors the view on its start; the end is only a fallback
|
|
127
131
|
// for a range whose start has been cleared. Keeping this in one effect avoids the
|
|
@@ -182,24 +186,12 @@ const useBaseRangePickerContextValue = ({ value, defaultValue, disabled = false,
|
|
|
182
186
|
setViewingMonth(newDate);
|
|
183
187
|
// Update temporary selection for immediate calendar visual feedback
|
|
184
188
|
setTemporaryStart(newDate);
|
|
185
|
-
// Set pending focus date so when calendar opens, it focuses on this date
|
|
186
|
-
setPendingFocusDate(dayjs(newDate).format(pendingFocusDayjsFormat));
|
|
187
189
|
}
|
|
188
190
|
else if (value === '') {
|
|
189
191
|
// Clear temporary selection if input is empty
|
|
190
192
|
setTemporaryStart(undefined);
|
|
191
|
-
setPendingFocusDate(null);
|
|
192
193
|
}
|
|
193
|
-
}, [
|
|
194
|
-
endDateString,
|
|
195
|
-
format,
|
|
196
|
-
minDate,
|
|
197
|
-
maxDate,
|
|
198
|
-
setViewingMonth,
|
|
199
|
-
setTemporaryStart,
|
|
200
|
-
setPendingFocusDate,
|
|
201
|
-
pendingFocusDayjsFormat,
|
|
202
|
-
]);
|
|
194
|
+
}, [endDateString, format, minDate, maxDate, setViewingMonth, setTemporaryStart]);
|
|
203
195
|
const handleEndDateStringChange = useCallback((value) => {
|
|
204
196
|
setDateStrings([startDateString, value]);
|
|
205
197
|
// Parse and update both viewing month and temporary selection for immediate visual feedback
|
|
@@ -211,24 +203,12 @@ const useBaseRangePickerContextValue = ({ value, defaultValue, disabled = false,
|
|
|
211
203
|
setViewingMonth(newDate);
|
|
212
204
|
// Update temporary selection for immediate calendar visual feedback
|
|
213
205
|
setTemporaryEnd(newDate);
|
|
214
|
-
// Set pending focus date so when calendar opens, it focuses on this date
|
|
215
|
-
setPendingFocusDate(dayjs(newDate).format(pendingFocusDayjsFormat));
|
|
216
206
|
}
|
|
217
207
|
else if (value === '') {
|
|
218
208
|
// Clear temporary selection if input is empty
|
|
219
209
|
setTemporaryEnd(undefined);
|
|
220
|
-
setPendingFocusDate(null);
|
|
221
210
|
}
|
|
222
|
-
}, [
|
|
223
|
-
startDateString,
|
|
224
|
-
format,
|
|
225
|
-
minDate,
|
|
226
|
-
maxDate,
|
|
227
|
-
setViewingMonth,
|
|
228
|
-
setTemporaryEnd,
|
|
229
|
-
setPendingFocusDate,
|
|
230
|
-
pendingFocusDayjsFormat,
|
|
231
|
-
]);
|
|
211
|
+
}, [startDateString, format, minDate, maxDate, setViewingMonth, setTemporaryEnd]);
|
|
232
212
|
const handleClear = useCallback(() => {
|
|
233
213
|
// handleChange already calls setDates and setDateStrings, so we don't need to call them again
|
|
234
214
|
handleChange([undefined, undefined]);
|
|
@@ -114,14 +114,4 @@ export type BaseRangePickerProviderProps = {
|
|
|
114
114
|
* @returns The formatted string to display
|
|
115
115
|
*/
|
|
116
116
|
customFormatValue?: (date: Date) => string;
|
|
117
|
-
/**
|
|
118
|
-
* dayjs format string for `pendingFocusDate` so it matches the focused grid cell's `data-mfui-*` attribute.
|
|
119
|
-
*
|
|
120
|
-
* @internal
|
|
121
|
-
*
|
|
122
|
-
* @default 'YYYY-MM-DD' — matches {@link DateCell} `data-mfui-date`
|
|
123
|
-
*
|
|
124
|
-
* @example 'YYYY-MM' — matches {@link MonthCell} `data-mfui-month` (MonthRangePicker)
|
|
125
|
-
*/
|
|
126
|
-
pendingFocusDayjsFormat?: 'YYYY-MM-DD' | 'YYYY-MM';
|
|
127
117
|
};
|