@moneyforward/mfui-components 4.9.0 → 4.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/src/Badge/Badge.js +2 -1
  2. package/dist/src/DataTable/DataTable.types.d.ts +1 -0
  3. package/dist/src/DataTable/DataTableBody/DataTableBody.js +5 -2
  4. package/dist/src/DataTable/DataTableFilterButton/DataTableFilterButton.js +3 -9
  5. package/dist/src/DataTable/DataTableHeaderCell/DataTableHeaderCell.js +7 -3
  6. package/dist/src/DataTable/DataTableHeaderCell/DataTableHeaderCell.types.d.ts +18 -6
  7. package/dist/src/DataTable/hooks/useVirtualizedColumns.js +2 -1
  8. package/dist/src/DateTimeSelection/FilterDatePicker/CalendarPopover/CalendarPopover.js +1 -1
  9. package/dist/src/DateTimeSelection/FilterDatePicker/FilterDatePicker.js +2 -4
  10. package/dist/src/DateTimeSelection/FilterDateRangePicker/DateRangePickerContent/DateRangePickerContent.js +1 -1
  11. package/dist/src/DateTimeSelection/FilterDateRangePicker/FilterDateRangePicker.js +2 -4
  12. package/dist/src/DateTimeSelection/FilterMonthPicker/FilterMonthPicker.js +2 -4
  13. package/dist/src/DateTimeSelection/FilterMonthPicker/MonthPickerPopover/MonthPickerPopover.js +1 -1
  14. package/dist/src/DateTimeSelection/FilterMonthRangePicker/FilterMonthRangePicker.js +2 -3
  15. package/dist/src/DateTimeSelection/FilterMonthRangePicker/MonthRangePickerPopover/MonthRangePickerPopover.js +1 -1
  16. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.d.ts +1 -1
  17. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.js +2 -2
  18. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.types.d.ts +34 -0
  19. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.js +18 -17
  20. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.types.d.ts +8 -0
  21. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerTrigger/hooks/useDateRangeTriggerValueController.d.ts +2 -1
  22. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerTrigger/hooks/useDateRangeTriggerValueController.js +2 -1
  23. package/dist/src/DateTimeSelection/shared/DayCell/DayCell.js +2 -1
  24. package/dist/src/DateTimeSelection/shared/accessibility/constants.d.ts +1 -1
  25. package/dist/src/DateTimeSelection/shared/accessibility/constants.js +0 -4
  26. package/dist/src/DateTimeSelection/shared/accessibility/generateDateLabel.d.ts +4 -0
  27. package/dist/src/DateTimeSelection/shared/accessibility/generateDateLabel.js +4 -7
  28. package/dist/src/Dialog/Dialog.types.d.ts +17 -0
  29. package/dist/src/FileDropZone/FileDropZone.js +23 -6
  30. package/dist/src/Popover/Popover.d.ts +1 -1
  31. package/dist/src/Popover/Popover.js +118 -14
  32. package/dist/src/Popover/Popover.types.d.ts +16 -3
  33. package/dist/src/SectionMessage/SectionMessage.d.ts +6 -0
  34. package/dist/src/SectionMessage/SectionMessage.js +16 -1
  35. package/dist/src/SectionMessage/SectionMessage.types.d.ts +4 -0
  36. package/dist/src/Skeleton/Skeleton.d.ts +5 -2
  37. package/dist/src/Skeleton/Skeleton.js +5 -2
  38. package/dist/src/Skeleton/Skeleton.types.d.ts +9 -1
  39. package/dist/src/VisuallyHidden/VisuallyHidden.d.ts +17 -0
  40. package/dist/src/VisuallyHidden/VisuallyHidden.js +21 -0
  41. package/dist/src/VisuallyHidden/VisuallyHidden.types.d.ts +19 -0
  42. package/dist/src/VisuallyHidden/VisuallyHidden.types.js +1 -0
  43. package/dist/src/VisuallyHidden/index.d.ts +2 -0
  44. package/dist/src/VisuallyHidden/index.js +1 -0
  45. package/dist/src/index.d.ts +1 -0
  46. package/dist/src/index.js +1 -0
  47. package/dist/src/utilities/dom/usePopoverMenuPlacement.js +0 -5
  48. package/dist/src/utilities/env/isProduction.d.ts +14 -0
  49. package/dist/src/utilities/env/isProduction.js +22 -0
  50. package/dist/src/utilities/react/isComponentOrWrapped.js +2 -1
  51. package/dist/styled-system/recipes/badge-slot-recipe.d.ts +1 -1
  52. package/dist/styled-system/recipes/badge-slot-recipe.js +0 -4
  53. package/dist/styled-system/recipes/day-cell-slot-recipe.d.ts +1 -1
  54. package/dist/styled-system/recipes/day-cell-slot-recipe.js +0 -4
  55. package/dist/styled-system/recipes/index.d.ts +2 -1
  56. package/dist/styled-system/recipes/index.js +1 -0
  57. package/dist/styled-system/recipes/popover-slot-recipe.d.ts +1 -1
  58. package/dist/styled-system/recipes/popover-slot-recipe.js +4 -0
  59. package/dist/styled-system/recipes/visually-hidden-slot-recipe.d.ts +33 -0
  60. package/dist/styled-system/recipes/visually-hidden-slot-recipe.js +28 -0
  61. package/dist/styles.css +27 -38
  62. package/dist/tsconfig.build.tsbuildinfo +1 -1
  63. package/package.json +2 -2
@@ -3,6 +3,7 @@ import { cloneElement, forwardRef, useId } from 'react';
3
3
  import { cx } from '../../styled-system/css';
4
4
  import { badgeSlotRecipe } from '../../styled-system/recipes';
5
5
  import { Typography } from '../Typography';
6
+ import { VisuallyHidden } from '../VisuallyHidden';
6
7
  /**
7
8
  * The general-purpose Badge component.
8
9
  * This component switches the variants of looks depends on the props: dot, count and children:
@@ -30,5 +31,5 @@ export const Badge = forwardRef(({ dot, count: number, digit, className, childre
30
31
  // If the badge is not displayed, just return the children.
31
32
  if (!displayBadge)
32
33
  return children;
33
- return (_jsxs("span", { ref: ref, className: cx(classes.root, 'mfui-Badge__root', className), ...props, children: [children ? cloneElement(children, { 'aria-describedby': labelId }) : null, _jsx("span", { className: cx(classes.indicator, 'mfui-Badge__indicator'), children: number !== undefined ? (_jsx(Typography, { "aria-hidden": true, variant: "condensedBody", children: displayContent })) : null }), _jsx("span", { id: labelId, className: cx(classes.srOnlyLabel, 'mfui-Badge__srOnlyLabel'), children: ariaLabel })] }));
34
+ return (_jsxs("span", { ref: ref, className: cx(classes.root, 'mfui-Badge__root', className), ...props, children: [children ? cloneElement(children, { 'aria-describedby': labelId }) : null, _jsx("span", { className: cx(classes.indicator, 'mfui-Badge__indicator'), children: number !== undefined ? (_jsx(Typography, { "aria-hidden": true, variant: "condensedBody", children: displayContent })) : null }), _jsx(VisuallyHidden, { id: labelId, className: "mfui-Badge__srOnlyLabel", children: ariaLabel })] }));
34
35
  });
@@ -144,6 +144,7 @@ export type DataTableProps = {
144
144
  /**
145
145
  * Whether the table is loading.
146
146
  * If `true`, four rows with the skeleton cell will be shown instead of the table content.
147
+ * Each skeleton row has one cell per column, including the selection column when `enableRowSelection` is set.
147
148
  *
148
149
  * @default false
149
150
  */
@@ -21,7 +21,7 @@ function getRowRef(element) {
21
21
  return element.props.ref ?? element.ref;
22
22
  }
23
23
  export const DataTableBody = forwardRef(({ className, children, enableVirtualization = false, virtualizationOptions, ...props }, ref) => {
24
- const { numberColumns, loading, scrollElement, setVirtualizedRowCount, headerRowCount } = useDataTableContext();
24
+ const { numberColumns, loading, scrollElement, setVirtualizedRowCount, headerRowCount, enableRowSelection } = useDataTableContext();
25
25
  const classes = dataTableBodySlotRecipe();
26
26
  // The body is held in state so that its offset below the `<thead>` can be read while rendering.
27
27
  const [bodyElement, setBodyElement] = useState(null);
@@ -55,7 +55,10 @@ export const DataTableBody = forwardRef(({ className, children, enableVirtualiza
55
55
  const bodyClassName = cx(classes.root, 'mfui-DataTableBody__root',
56
56
  /* TODO: Remove in v5.0.0 */ 'mfui-DataGridBody__root', className);
57
57
  if (loading) {
58
- return (_jsx("tbody", { className: bodyClassName, ...props, ref: ref, children: Array.from({ length: NUMBER_OF_LOADING_ROWS }).map((_, rowIndex) => (_jsx(DataTableRow, { children: Array.from({ length: numberColumns }).map((_, colIndex) => (_jsx(DataTableCell, {}, colIndex))) }, rowIndex))) }));
58
+ // `numberColumns` counts the selection column, which `DataTableRow` renders itself, so only the
59
+ // consumer columns are filled in here — otherwise the skeleton row is one cell wider than the header.
60
+ const loadingCellCount = Math.max(numberColumns - (enableRowSelection ? 1 : 0), 0);
61
+ return (_jsx("tbody", { className: bodyClassName, ...props, ref: ref, children: Array.from({ length: NUMBER_OF_LOADING_ROWS }).map((_, rowIndex) => (_jsx(DataTableRow, { children: Array.from({ length: loadingCellCount }).map((_, colIndex) => (_jsx(DataTableCell, {}, colIndex))) }, rowIndex))) }));
59
62
  }
60
63
  if (!isVirtualized) {
61
64
  return (_jsx("tbody", { className: bodyClassName, ...props, ref: ref, children: children }));
@@ -1,6 +1,6 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Filter, FilterOn } from '@moneyforward/mfui-icons-react';
3
- import { forwardRef, useCallback, useId, useRef } from 'react';
3
+ import { forwardRef, useCallback, useId } from 'react';
4
4
  import { dataTableFilterButtonSlotRecipe } from '../../../styled-system/recipes';
5
5
  import { IconButton } from '../../IconButton';
6
6
  import { Popover } from '../../Popover';
@@ -11,7 +11,6 @@ import { cx } from '../../../styled-system/css';
11
11
  export const DataTableFilterButton = forwardRef(({ filtered = false, renderContent, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedBy, popoverProps, onClick, onKeyDown, onBlur, className, ...props }, ref) => {
12
12
  const filteredDescriptionId = useId();
13
13
  const popoverContentId = useId();
14
- const triggerRef = useRef(null);
15
14
  const triggerLabel = ariaLabel ?? '絞り込み';
16
15
  const dialogLabel = popoverProps?.['aria-label'] ?? '絞り込み条件';
17
16
  const describedBy = [ariaDescribedBy, filtered ? filteredDescriptionId : undefined].filter(Boolean).join(' ');
@@ -38,7 +37,7 @@ export const DataTableFilterButton = forwardRef(({ filtered = false, renderConte
38
37
  , {
39
38
  // handleTriggerKeyDown is intentionally not wired: arrow-key opening belongs to
40
39
  // menu/listbox patterns, while this trigger opens a dialog.
41
- renderTrigger: ({ setTriggerRef, togglePopover, closePopover, isPopoverOpen, handleTriggerBlur }) => (_jsxs(_Fragment, { children: [_jsx(IconButton, { ref: mergeRefs(ref, setTriggerRef, triggerRef), ...props, className: cx(classes.root, 'mfui-DataTableFilterButton__root', className), "aria-label": triggerLabel, "aria-describedby": describedBy || undefined, "aria-haspopup": "dialog", "aria-expanded": isPopoverOpen, "aria-controls": popoverContentId, onClick: (event) => {
40
+ renderTrigger: ({ setTriggerRef, togglePopover, closePopover, isPopoverOpen, handleTriggerBlur }) => (_jsxs(_Fragment, { children: [_jsx(IconButton, { ref: mergeRefs(ref, setTriggerRef), ...props, className: cx(classes.root, 'mfui-DataTableFilterButton__root', className), "aria-label": triggerLabel, "aria-describedby": describedBy || undefined, "aria-haspopup": "dialog", "aria-expanded": isPopoverOpen, "aria-controls": popoverContentId, onClick: (event) => {
42
41
  togglePopover(event);
43
42
  onClick?.(event);
44
43
  }, onKeyDown: (event) => {
@@ -53,11 +52,6 @@ export const DataTableFilterButton = forwardRef(({ filtered = false, renderConte
53
52
  }, onBlur: (event) => {
54
53
  handleTriggerBlur(event);
55
54
  onBlur?.(event);
56
- }, children: filtered ? _jsx(FilterOn, {}) : _jsx(Filter, {}) }), filtered ? (_jsx("span", { id: filteredDescriptionId, className: cx(classes.filteredDescription, 'mfui-DataTableFilterButton__filteredDescription'), children: "\u9069\u7528\u4E2D" })) : null] })), renderContent: (options) => (_jsx("div", { ref: focusInitialElement, role: "dialog", "aria-label": dialogLabel, "aria-labelledby": popoverProps?.['aria-labelledby'], tabIndex: -1, className: cx(classes.dialog, 'mfui-DataTableFilterButton__dialog'), onKeyDown: handleFocusTrapKeyDown, children: renderContent(options) })), contentProps: { id: popoverContentId }, enableAutoFocusOnPopover: false, onClose: (event) => {
57
- // Keep Escape focus restoration scoped to the filter button instead of changing the shared Popover.
58
- if (event && 'key' in event && event.key === 'Escape') {
59
- triggerRef.current?.focus();
60
- }
61
- } }));
55
+ }, children: filtered ? _jsx(FilterOn, {}) : _jsx(Filter, {}) }), filtered ? (_jsx("span", { id: filteredDescriptionId, className: cx(classes.filteredDescription, 'mfui-DataTableFilterButton__filteredDescription'), children: "\u9069\u7528\u4E2D" })) : null] })), renderContent: (options) => (_jsx("div", { ref: focusInitialElement, role: "dialog", "aria-label": dialogLabel, "aria-labelledby": popoverProps?.['aria-labelledby'], tabIndex: -1, className: cx(classes.dialog, 'mfui-DataTableFilterButton__dialog'), onKeyDown: handleFocusTrapKeyDown, children: renderContent(options) })), contentProps: { id: popoverContentId }, enableAutoFocusOnPopover: false }));
62
56
  });
63
57
  DataTableFilterButton.displayName = 'DataTableFilterButton';
@@ -28,17 +28,21 @@ export const DataTableHeaderCell = forwardRef(({ children, type = 'text', sortBu
28
28
  });
29
29
  const isCheckboxCell = enableRowSelection && columnIndex === 0;
30
30
  const isStringChild = typeof children === 'string';
31
+ // The current order is exposed by `aria-sort` on the `th`, so each icon label describes the order
32
+ // the next click applies instead. These defaults assume `onClick` cycles
33
+ // unsorted → ascending → descending → ascending; consumers with a different cycle override them
34
+ // through the `*IconProps`.
31
35
  const sortIcon = (() => {
32
36
  if (!sortOrder) {
33
37
  return (_jsx(SortUnsorted, { className: cx(classes.unsortedIcon, 'mfui-DataTableHeaderCell__unsortedIcon',
34
- /* TODO: Remove in v5.0.0 */ 'mfui-DataGridHeaderCell__unsortedIcon'), role: "img", "aria-label": unsortedIconProps?.['aria-label'] ?? '並び替えを解除' }));
38
+ /* TODO: Remove in v5.0.0 */ 'mfui-DataGridHeaderCell__unsortedIcon'), role: "img", "aria-label": unsortedIconProps?.['aria-label'] ?? '昇順で並び替え' }));
35
39
  }
36
40
  if (sortOrder === 'ascending') {
37
41
  return (_jsx(SortAscending, { className: cx(classes.sortIcon, 'mfui-DataTableHeaderCell__sortIcon',
38
- /* TODO: Remove in v5.0.0 */ 'mfui-DataGridHeaderCell__sortIcon'), role: "img", "aria-label": ascendingIconProps?.['aria-label'] ?? '昇順で並び替え' }));
42
+ /* TODO: Remove in v5.0.0 */ 'mfui-DataGridHeaderCell__sortIcon'), role: "img", "aria-label": ascendingIconProps?.['aria-label'] ?? '降順で並び替え' }));
39
43
  }
40
44
  return (_jsx(SortDescending, { className: cx(classes.sortIcon, 'mfui-DataTableHeaderCell__sortIcon',
41
- /* TODO: Remove in v5.0.0 */ 'mfui-DataGridHeaderCell__sortIcon'), role: "img", "aria-label": descendingIconProps?.['aria-label'] ?? '降順で並び替え' }));
45
+ /* TODO: Remove in v5.0.0 */ 'mfui-DataGridHeaderCell__sortIcon'), role: "img", "aria-label": descendingIconProps?.['aria-label'] ?? '昇順で並び替え' }));
42
46
  })();
43
47
  const handleClickSortButton = sortButtonProps?.onClick;
44
48
  const cellContent = (() => {
@@ -28,24 +28,36 @@ export type DataTableHeaderCellProps = {
28
28
  */
29
29
  type?: CellDataType;
30
30
  /**
31
- * The properties for the ascending icon.
31
+ * The properties for the icon shown while the column is sorted in ascending order.
32
32
  * If this prop is not passed, the default values for each property will be applied.
33
33
  *
34
- * @property aria-label - The alternative text for the ascending icon. default is '昇順で並び替え'.
34
+ * The current order is exposed by `aria-sort`, so the label describes the order the next click applies,
35
+ * not the current one. The default assumes `sortButtonProps.onClick` cycles
36
+ * unsorted → ascending → descending → ascending; override it if your cycle differs.
37
+ *
38
+ * @property aria-label - The alternative text describing the next sort action. default is '降順で並び替え'.
35
39
  */
36
40
  ascendingIconProps?: Pick<ComponentPropsWithoutRef<'svg'>, 'aria-label'>;
37
41
  /**
38
- * The properties for the descending icon.
42
+ * The properties for the icon shown while the column is sorted in descending order.
39
43
  * If this prop is not passed, the default values for each property will be applied.
40
44
  *
41
- * @property aria-label - The alternative text for the descending icon. default is '降順で並び替え'.
45
+ * The current order is exposed by `aria-sort`, so the label describes the order the next click applies,
46
+ * not the current one. The default assumes `sortButtonProps.onClick` cycles
47
+ * unsorted → ascending → descending → ascending; override it if your cycle differs
48
+ * (e.g. descending → unsorted).
49
+ *
50
+ * @property aria-label - The alternative text describing the next sort action. default is '昇順で並び替え'.
42
51
  */
43
52
  descendingIconProps?: Pick<ComponentPropsWithoutRef<'svg'>, 'aria-label'>;
44
53
  /**
45
- * The properties for the unsorted icon.
54
+ * The properties for the icon shown while the column is not sorted.
46
55
  * If this prop is not passed, the default values for each property will be applied.
47
56
  *
48
- * @property aria-label - The alternative text for the unsorted icon. default is '並び替えを解除'.
57
+ * The label describes the order the next click applies. The default assumes
58
+ * `sortButtonProps.onClick` sorts an unsorted column in ascending order first; override it if your cycle differs.
59
+ *
60
+ * @property aria-label - The alternative text describing the next sort action. default is '昇順で並び替え'.
49
61
  */
50
62
  unsortedIconProps?: Pick<ComponentPropsWithoutRef<'svg'>, 'aria-label'>;
51
63
  /**
@@ -1,5 +1,6 @@
1
1
  import { useMemo, useRef, useState } from 'react';
2
2
  import { useVirtualizer } from '@tanstack/react-virtual';
3
+ import { isProduction } from '../../utilities/env/isProduction';
3
4
  import { useIsomorphicLayoutEffect } from '../../utilities/react/useIsomorphicLayoutEffect';
4
5
  import { DEFAULT_COLUMN_WIDTH } from '../utils/resolveColumnWidths';
5
6
  /**
@@ -57,7 +58,7 @@ export function useVirtualizedColumns({ enabled, scrollElement, tableRef, column
57
58
  // so a `<colgroup>` rendered by a consumer component ends up next to the table's without any error.
58
59
  useIsomorphicLayoutEffect(() => {
59
60
  const table = tableRef.current;
60
- if (process.env['NODE_ENV'] === 'production' || !enabled || !table || hasWarnedDuplicateColgroup.current)
61
+ if (isProduction() || !enabled || !table || hasWarnedDuplicateColgroup.current)
61
62
  return;
62
63
  if (table.querySelectorAll(':scope > colgroup').length > 1) {
63
64
  hasWarnedDuplicateColgroup.current = true;
@@ -42,5 +42,5 @@ export function InternalCalendarPopover({ currentValue, defaultValue, isCalendar
42
42
  }, [currentValue, defaultValue, setViewingValue, setPendingFocusDate]);
43
43
  return (_jsx(Popover, { open: isCalendarOpen, targetDOMNode: targetDOMNode, renderTrigger: ({ setTriggerRef, togglePopover, handleTriggerKeyDown, handleTriggerBlur }) => (_jsxs("div", { ref: setTriggerRef, "data-selected": hasValue, className: cx(classes.wrapper, 'mfui-FilterDatePicker__wrapper'), children: [_jsx(FocusIndicator, { children: _jsxs("button", { ref: triggerRef, type: "button", disabled: disabled, className: cx(classes.trigger, 'mfui-FilterDatePicker__trigger', className), "data-selected": hasValue, "data-mfui-has-clear-button": enableClearButton ? hasValue : null, "aria-controls": dialogId, "aria-expanded": isCalendarOpen, "aria-haspopup": "dialog", onClick: togglePopover, onKeyDown: handleTriggerKeyDown, onBlur: handleTriggerBlur, children: [_jsx("span", { className: cx(classes.content, 'mfui-FilterDatePicker__content'), "data-mfui-content": "filter-date-picker-display-value", children: hasValue ? (_jsxs(Typography, { variant: "strongBody", children: [_jsx("span", { "data-mfui-content": "filter-date-picker-label", children: `${label}:` }), _jsx("span", { "data-mfui-content": "filter-date-picker-value", children: formattedValue })] })) : (_jsx(Typography, { variant: "controlLabel", children: _jsx("span", { "data-mfui-content": "filter-date-picker-label", children: label }) })) }), _jsx("span", { className: cx(classes.calendarIcon, 'mfui-FilterDatePicker__calendarIcon'), children: _jsx(CalendarIcon, { "aria-hidden": true }) })] }) }), enableClearButton && hasValue ? (_jsx("div", { className: cx(classes.clearButtonWrapper, 'mfui-FilterDatePicker__clearButtonWrapper'), children: _jsx(ClearButton, { "aria-label": clearButtonProps?.['aria-label'] ?? '値をクリアする', "data-mfui-content": "filter-date-picker-clear-button", disabled: disabled, onClick: handleClear }) })) : null] })), renderContent: () => (_jsx("div", { id: dialogId, role: "dialog", "aria-label": label, className: "mfui-FilterDatePicker__popoverWrapper", onKeyDown: handleOnKeyDown, children: _jsx(FirstDayOfWeekProvider, { value: firstDayOfWeek, children: _jsx(DatePickerPanel, { value: currentValue, checkDisabledDate: checkDisabledDate, minDate: minDate, maxDate: maxDate, prevMonthIconButtonProps: prevMonthIconButtonProps, nextMonthIconButtonProps: nextMonthIconButtonProps, todayButtonProps: todayButtonProps, onChange: (date) => {
44
44
  handleValueChange(date);
45
- } }) }) })), minWidth: "min-content", onOpenStateChanged: toggleCalendar, onOpen: handlePopoverOpen }));
45
+ } }) }) })), minWidth: "min-content", onOpenStateChanged: toggleCalendar, onOpen: handlePopoverOpen, returnFocusRef: triggerRef }));
46
46
  }
@@ -39,11 +39,9 @@ export function FilterDatePicker({ label, value, defaultValue, onChange, open, o
39
39
  const { isOpen: isCalendarOpen, toggle: toggleCalendar, close: closeCalendar, } = useDisclosure({
40
40
  // disabled takes precedence: never open the calendar when the trigger is disabled.
41
41
  value: disabled ? false : open,
42
- // onToggle fires for Escape and outside-click (via toggleCalendar); focus trigger on close.
42
+ // onToggle fires for Escape, outside-click, and trigger click. Focus after Escape and trigger click
43
+ // is returned by the Popover through `returnFocusRef`, and not after outside-click (see the MEMO in Popover).
43
44
  onToggle: (nextOpen) => {
44
- if (!nextOpen) {
45
- triggerRef.current?.focus();
46
- }
47
45
  onOpenStateChanged?.(nextOpen);
48
46
  },
49
47
  // onClose fires for programmatic close (date selection); focus trigger there too.
@@ -32,5 +32,5 @@ export function InternalDateRangePickerContent({ classes, triggerRef, dialogId,
32
32
  setPendingFocusDate(dayjs(dateToFocus).format('YYYY-MM-DD'));
33
33
  }, [startDate, endDate, setPendingFocusDate]);
34
34
  return (_jsx(Popover, { open: isCalendarOpen, targetDOMNode: targetDOMNode, minWidth: "min-content", allowedPlacements: ['bottom-start', 'top-start'], renderTrigger: ({ setTriggerRef, togglePopover, handleTriggerKeyDown, handleTriggerBlur }) => (_jsxs("div", { ref: setTriggerRef, "data-selected": hasValue, className: cx(classes.wrapper, 'mfui-FilterDateRangePicker__wrapper'), children: [_jsx(FocusIndicator, { children: _jsxs("button", { ref: triggerRef, type: "button", disabled: disabled, className: cx(classes.trigger, 'mfui-FilterDateRangePicker__trigger', className), "data-selected": hasValue, "data-mfui-has-clear-button": enableClearButton ? hasValue : null, "aria-controls": dialogId, "aria-expanded": isCalendarOpen, "aria-haspopup": "dialog", onClick: togglePopover, onKeyDown: handleTriggerKeyDown, onBlur: handleTriggerBlur, children: [_jsx("span", { className: cx(classes.content, 'mfui-FilterDateRangePicker__content'), "data-mfui-content": "filter-date-range-picker-display-value", children: hasValue ? (_jsxs(Typography, { variant: "strongBody", children: [_jsx("span", { "data-mfui-content": "filter-date-range-picker-label", children: `${label}:` }), _jsx("span", { "data-mfui-content": "filter-date-range-picker-value", children: formattedValue })] })) : (_jsx(Typography, { variant: "controlLabel", children: _jsx("span", { "data-mfui-content": "filter-date-range-picker-label", children: label }) })) }), _jsx("span", { className: cx(classes.calendarIcon, 'mfui-FilterDateRangePicker__calendarIcon'), children: _jsx(CalendarIcon, { "aria-hidden": true }) })] }) }), enableClearButton && hasValue ? (_jsx("div", { className: cx(classes.clearButtonWrapper, 'mfui-FilterDateRangePicker__clearButtonWrapper'), children: _jsx(ClearButton, { "aria-label": clearButtonProps?.['aria-label'] ??
35
- getBaseRangePickerTriggerLabel('CLEAR_BUTTON_LABEL', calendarLocale), "data-mfui-content": "filter-date-range-picker-clear-button", disabled: disabled, onClick: handleTriggerClear }) })) : null] })), renderContent: () => (_jsx("div", { id: dialogId, role: "dialog", "aria-label": label, className: cx(classes.popoverWrapper, 'mfui-FilterDateRangePicker__popoverWrapper'), children: _jsx(CalendarLocaleProvider, { value: calendarLocale, children: _jsx(FirstDayOfWeekProvider, { value: firstDayOfWeek, children: _jsx(DateRangePickerPopover, {}) }) }) })), onOpenStateChanged: toggleCalendar, onOpen: handlePopoverOpen }));
35
+ getBaseRangePickerTriggerLabel('CLEAR_BUTTON_LABEL', calendarLocale), "data-mfui-content": "filter-date-range-picker-clear-button", disabled: disabled, onClick: handleTriggerClear }) })) : null] })), renderContent: () => (_jsx("div", { id: dialogId, role: "dialog", "aria-label": label, className: cx(classes.popoverWrapper, 'mfui-FilterDateRangePicker__popoverWrapper'), children: _jsx(CalendarLocaleProvider, { value: calendarLocale, children: _jsx(FirstDayOfWeekProvider, { value: firstDayOfWeek, children: _jsx(DateRangePickerPopover, {}) }) }) })), onOpenStateChanged: toggleCalendar, onOpen: handlePopoverOpen, returnFocusRef: triggerRef }));
36
36
  }
@@ -26,11 +26,9 @@ export function FilterDateRangePicker({ label, value, defaultValue, onChange, op
26
26
  const { isOpen: isCalendarOpen, open: openCalendar, close: closeCalendar, toggle: toggleCalendar, } = useDisclosure({
27
27
  // disabled takes precedence: never open the calendar when the trigger is disabled.
28
28
  value: disabled ? false : open,
29
- // onToggle fires for Escape and outside-click; focus trigger on close.
29
+ // onToggle fires for Escape, outside-click, and trigger click. Focus after Escape and trigger click
30
+ // is returned by the Popover through `returnFocusRef`, and not after outside-click (see the MEMO in Popover).
30
31
  onToggle: (nextOpen) => {
31
- if (!nextOpen) {
32
- triggerRef.current?.focus();
33
- }
34
32
  onOpenStateChanged?.(nextOpen);
35
33
  },
36
34
  // onClose fires for programmatic close (Apply/Cancel buttons); focus trigger there too.
@@ -38,11 +38,9 @@ export function FilterMonthPicker({ label, value, defaultValue, onChange, open,
38
38
  const { isOpen: isPickerOpen, toggle: togglePicker, close: closePicker, } = useDisclosure({
39
39
  // disabled takes precedence: never open the picker when the trigger is disabled.
40
40
  value: disabled ? false : open,
41
- // onToggle fires for Escape and outside-click (via togglePicker); focus trigger on close.
41
+ // onToggle fires for Escape, outside-click, and trigger click. Focus after Escape and trigger click
42
+ // is returned by the Popover through `returnFocusRef`, and not after outside-click (see the MEMO in Popover).
42
43
  onToggle: (nextOpen) => {
43
- if (!nextOpen) {
44
- triggerRef.current?.focus();
45
- }
46
44
  onOpenStateChanged?.(nextOpen);
47
45
  },
48
46
  // onClose fires for programmatic close (month selection); focus trigger there too.
@@ -42,5 +42,5 @@ export function InternalMonthPickerPopover({ currentValue, defaultValue, isPicke
42
42
  }, [currentValue, defaultValue, setViewingValue, setPendingFocusDate]);
43
43
  return (_jsx(Popover, { open: isPickerOpen, targetDOMNode: targetDOMNode, renderTrigger: ({ setTriggerRef, togglePopover, handleTriggerKeyDown, handleTriggerBlur }) => (_jsxs("div", { ref: setTriggerRef, "data-selected": hasValue, className: cx(classes.wrapper, 'mfui-FilterMonthPicker__wrapper'), children: [_jsx(FocusIndicator, { children: _jsxs("button", { ref: triggerRef, type: "button", disabled: disabled, className: cx(classes.trigger, 'mfui-FilterMonthPicker__trigger', className), "data-selected": hasValue, "data-mfui-has-clear-button": enableClearButton ? hasValue : null, "aria-controls": dialogId, "aria-expanded": isPickerOpen, "aria-haspopup": "dialog", onClick: togglePopover, onKeyDown: handleTriggerKeyDown, onBlur: handleTriggerBlur, children: [_jsx("span", { className: cx(classes.content, 'mfui-FilterMonthPicker__content'), "data-mfui-content": "filter-month-picker-display-value", children: hasValue ? (_jsxs(Typography, { variant: "strongBody", children: [_jsx("span", { "data-mfui-content": "filter-month-picker-label", children: `${label}:` }), _jsx("span", { "data-mfui-content": "filter-month-picker-value", children: formattedValue })] })) : (_jsx(Typography, { variant: "controlLabel", children: _jsx("span", { "data-mfui-content": "filter-month-picker-label", children: label }) })) }), _jsx("span", { className: cx(classes.calendarIcon, 'mfui-FilterMonthPicker__calendarIcon'), children: _jsx(CalendarIcon, { "aria-hidden": true }) })] }) }), enableClearButton && hasValue ? (_jsx("div", { className: cx(classes.clearButtonWrapper, 'mfui-FilterMonthPicker__clearButtonWrapper'), children: _jsx(ClearButton, { "aria-label": clearButtonProps?.['aria-label'] ?? getBasePickerLabel('CLEAR_VALUE', calendarLocale), "data-mfui-content": "filter-month-picker-clear-button", disabled: disabled, onClick: handleClear }) })) : null] })), renderContent: () => (_jsx("div", { id: dialogId, role: "dialog", "aria-label": label, className: "mfui-FilterMonthPicker__popoverWrapper", onKeyDown: handleOnKeyDown, children: _jsx(CalendarLocaleProvider, { value: calendarLocale, children: _jsx(MonthPickerPanel, { viewingValue: viewingValue, setViewingValue: setViewingValue, value: currentValue, minMonth: minMonth, maxMonth: maxMonth, checkDisabledMonth: checkDisabledMonth, onChange: (date) => {
44
44
  handleValueChange(date);
45
- } }) }) })), minWidth: "min-content", allowedPlacements: ['bottom-start', 'top-start'], onOpenStateChanged: togglePicker, onOpen: handlePopoverOpen }));
45
+ } }) }) })), minWidth: "min-content", allowedPlacements: ['bottom-start', 'top-start'], onOpenStateChanged: togglePicker, onOpen: handlePopoverOpen, returnFocusRef: triggerRef }));
46
46
  }
@@ -37,10 +37,9 @@ export function FilterMonthRangePicker({ label, value, defaultValue, onChange, o
37
37
  }, [value, defaultValue]);
38
38
  const { isOpen: isPickerOpen, toggle: togglePicker, close: closePicker, open: openPicker, } = useDisclosure({
39
39
  value: disabled ? false : open,
40
+ // onToggle fires for Escape, outside-click, and trigger click. Focus after Escape and trigger click
41
+ // is returned by the Popover through `returnFocusRef`, and not after outside-click (see the MEMO in Popover).
40
42
  onToggle: (nextOpen) => {
41
- if (!nextOpen) {
42
- triggerRef.current?.focus();
43
- }
44
43
  onOpenStateChanged?.(nextOpen);
45
44
  },
46
45
  onClose: () => {
@@ -50,5 +50,5 @@ export function InternalMonthRangePickerPopover({ isPickerOpen, togglePicker, ha
50
50
  setViewingMonth(monthToFocus);
51
51
  setPendingFocusDate(dayjs(monthToFocus).format('YYYY-MM'));
52
52
  }, [temporaryStart, temporaryEnd, startDate, endDate, setViewingMonth, setPendingFocusDate]);
53
- return (_jsx(Popover, { open: isPickerOpen, targetDOMNode: targetDOMNode, renderTrigger: ({ setTriggerRef, togglePopover, handleTriggerKeyDown, handleTriggerBlur }) => (_jsxs("div", { ref: setTriggerRef, "data-selected": hasValue, className: cx(classes.wrapper, 'mfui-FilterMonthRangePicker__wrapper'), children: [_jsx(FocusIndicator, { children: _jsxs("button", { ref: triggerRef, type: "button", disabled: disabled, className: cx(classes.trigger, 'mfui-FilterMonthRangePicker__trigger', className), "data-selected": hasValue, "data-mfui-has-clear-button": enableClearButton ? hasValue : null, "aria-controls": dialogId, "aria-expanded": isPickerOpen, "aria-haspopup": "dialog", onClick: togglePopover, onKeyDown: handleTriggerKeyDown, onBlur: handleTriggerBlur, children: [_jsx("span", { className: cx(classes.content, 'mfui-FilterMonthRangePicker__content'), "data-mfui-content": "filter-month-range-picker-display-value", children: hasValue ? (_jsxs(Typography, { variant: "strongBody", children: [_jsx("span", { "data-mfui-content": "filter-month-range-picker-label", children: `${label}:` }), _jsx("span", { "data-mfui-content": "filter-month-range-picker-value", children: formattedValue })] })) : (_jsx(Typography, { variant: "controlLabel", children: _jsx("span", { "data-mfui-content": "filter-month-range-picker-label", children: label }) })) }), _jsx("span", { className: cx(classes.calendarIcon, 'mfui-FilterMonthRangePicker__calendarIcon'), children: _jsx(CalendarIcon, { "aria-hidden": true }) })] }) }), enableClearButton && hasValue ? (_jsx("div", { className: cx(classes.clearButtonWrapper, 'mfui-FilterMonthRangePicker__clearButtonWrapper'), children: _jsx(ClearButton, { "aria-label": clearButtonProps?.['aria-label'] ?? '値をクリアする', "data-mfui-content": "filter-month-range-picker-clear-button", disabled: disabled, onClick: handleClear }) })) : null] })), renderContent: () => (_jsx("div", { id: dialogId, role: "dialog", "aria-label": label, className: "mfui-FilterMonthRangePicker__popoverWrapper", onKeyDown: handleOnKeyDown, children: _jsx(MonthRangePickerPanel, { viewingValue: viewingMonth, setViewingValue: setViewingMonth, value: [temporaryStart, temporaryEnd], minMonth: minMonth, maxMonth: maxMonth }) })), minWidth: "min-content", allowedPlacements: ['bottom-start', 'top-start'], onOpenStateChanged: togglePicker, onOpen: handlePopoverOpen }));
53
+ return (_jsx(Popover, { open: isPickerOpen, targetDOMNode: targetDOMNode, renderTrigger: ({ setTriggerRef, togglePopover, handleTriggerKeyDown, handleTriggerBlur }) => (_jsxs("div", { ref: setTriggerRef, "data-selected": hasValue, className: cx(classes.wrapper, 'mfui-FilterMonthRangePicker__wrapper'), children: [_jsx(FocusIndicator, { children: _jsxs("button", { ref: triggerRef, type: "button", disabled: disabled, className: cx(classes.trigger, 'mfui-FilterMonthRangePicker__trigger', className), "data-selected": hasValue, "data-mfui-has-clear-button": enableClearButton ? hasValue : null, "aria-controls": dialogId, "aria-expanded": isPickerOpen, "aria-haspopup": "dialog", onClick: togglePopover, onKeyDown: handleTriggerKeyDown, onBlur: handleTriggerBlur, children: [_jsx("span", { className: cx(classes.content, 'mfui-FilterMonthRangePicker__content'), "data-mfui-content": "filter-month-range-picker-display-value", children: hasValue ? (_jsxs(Typography, { variant: "strongBody", children: [_jsx("span", { "data-mfui-content": "filter-month-range-picker-label", children: `${label}:` }), _jsx("span", { "data-mfui-content": "filter-month-range-picker-value", children: formattedValue })] })) : (_jsx(Typography, { variant: "controlLabel", children: _jsx("span", { "data-mfui-content": "filter-month-range-picker-label", children: label }) })) }), _jsx("span", { className: cx(classes.calendarIcon, 'mfui-FilterMonthRangePicker__calendarIcon'), children: _jsx(CalendarIcon, { "aria-hidden": true }) })] }) }), enableClearButton && hasValue ? (_jsx("div", { className: cx(classes.clearButtonWrapper, 'mfui-FilterMonthRangePicker__clearButtonWrapper'), children: _jsx(ClearButton, { "aria-label": clearButtonProps?.['aria-label'] ?? '値をクリアする', "data-mfui-content": "filter-month-range-picker-clear-button", disabled: disabled, onClick: handleClear }) })) : null] })), renderContent: () => (_jsx("div", { id: dialogId, role: "dialog", "aria-label": label, className: "mfui-FilterMonthRangePicker__popoverWrapper", onKeyDown: handleOnKeyDown, children: _jsx(MonthRangePickerPanel, { viewingValue: viewingMonth, setViewingValue: setViewingMonth, value: [temporaryStart, temporaryEnd], minMonth: minMonth, maxMonth: maxMonth }) })), minWidth: "min-content", allowedPlacements: ['bottom-start', 'top-start'], onOpenStateChanged: togglePicker, onOpen: handlePopoverOpen, returnFocusRef: triggerRef }));
54
54
  }
@@ -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, caution, 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;
6
+ export declare function BaseRangePicker({ value, defaultValue, disabled, invalid, caution, targetDOMNode, onChange, format, onOpenStateChanged, disableAutoOpen, disableAutoSwap, onBlur, allowedPlacements, enableViewportConstraint, enableClearButton, clearButtonProps, minDate, maxDate, enableAutoUnmount, minWidth, renderPopoverContent, startInputProps, endInputProps, calendarLocale, initialDisplayedMonths, initialViewingDate, customFormatValue, pendingFocusDayjsFormat, }: BaseRangePickerProps): import("react/jsx-runtime").JSX.Element;
@@ -77,12 +77,12 @@ function InternalBaseRangePicker({ startInputRef, endInputRef, disabled, invalid
77
77
  * BaseRangePicker component
78
78
  * A generic component for selecting a range of dates with configurable format
79
79
  */
80
- export function BaseRangePicker({ value, defaultValue, disabled, invalid, caution, 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', }) {
80
+ export function BaseRangePicker({ value, defaultValue, disabled, invalid, caution, targetDOMNode, onChange, format = 'YYYY/MM/DD', onOpenStateChanged, disableAutoOpen = false, disableAutoSwap = false, onBlur, allowedPlacements, enableViewportConstraint, enableClearButton = false, clearButtonProps, minDate, maxDate, enableAutoUnmount = true, minWidth, renderPopoverContent, startInputProps, endInputProps, calendarLocale = 'ja', initialDisplayedMonths, initialViewingDate, customFormatValue, pendingFocusDayjsFormat = 'YYYY-MM-DD', }) {
81
81
  const { isOpen, open, close } = useDisclosure({ value: false });
82
82
  const startInputRef = useRef(null);
83
83
  const endInputRef = useRef(null);
84
84
  // Default renderPopoverContent implementation for backward compatibility
85
85
  const defaultRenderPopoverContent = useCallback(() => _jsx(BaseRangePickerPopover, { calendarLocale: calendarLocale }), [calendarLocale]);
86
86
  const finalRenderPopoverContent = renderPopoverContent ?? defaultRenderPopoverContent;
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, 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 }) }));
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, pendingFocusDayjsFormat: pendingFocusDayjsFormat, 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
88
  }
@@ -119,6 +119,40 @@ export type BaseRangePickerProps = {
119
119
  * @default false - Automatically open the Popover when we click on the TextBox.
120
120
  */
121
121
  disableAutoOpen?: boolean;
122
+ /**
123
+ * Whether to keep a typed range as entered when the start date is later than the end date.
124
+ * By default, such a range is reordered before `onChange` is called, so the consumer never
125
+ * sees the reversed pair. When `true`, `onChange` receives the dates in the order the user typed,
126
+ * and the popover shows no selection while the range is reversed.
127
+ * Selecting dates on the calendar still produces an ordered range.
128
+ *
129
+ * Validate the range yourself and surface the error with `invalid` and an adjacent
130
+ * `<HelpMessage messageType="error">` associated with both inputs via
131
+ * `startInputProps['aria-describedby']` and `endInputProps['aria-describedby']` —
132
+ * color alone does not convey the state (WCAG 2.2 1.4.1).
133
+ *
134
+ * ```tsx
135
+ * const errorMessageId = useId();
136
+ * const isReversed = !!start && !!end && start > end;
137
+ *
138
+ * <DateRangePicker
139
+ * disableAutoSwap
140
+ * invalid={isReversed}
141
+ * startInputProps={{ 'aria-describedby': errorMessageId }}
142
+ * endInputProps={{ 'aria-describedby': errorMessageId }}
143
+ * />
144
+ * {isReversed && (
145
+ * <HelpMessage id={errorMessageId} messageType="error">
146
+ * 開始日は終了日以前の日付を入力してください。
147
+ * </HelpMessage>
148
+ * )}
149
+ * ```
150
+ *
151
+ * This behavior will become the default in the next major version.
152
+ *
153
+ * @default false
154
+ */
155
+ disableAutoSwap?: boolean;
122
156
  /**
123
157
  * Callback fired when the user blurs away from the trigger element or popover content.
124
158
  * This implements smart blur detection - only fires when focus leaves both the trigger and popover.
@@ -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, minDate, maxDate, initialDisplayedMonths = 'currentAndNext', initialViewingDate, customFormatValue, pendingFocusDayjsFormat = 'YYYY-MM-DD', }) => {
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, pendingFocusDayjsFormat = 'YYYY-MM-DD', }) => {
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(() => {
@@ -123,16 +123,15 @@ const useBaseRangePickerContextValue = ({ value, defaultValue, disabled = false,
123
123
  const [previewDate, setPreviewDate] = useState();
124
124
  // --- Focus Management ---
125
125
  const [pendingFocusDate, setPendingFocusDate] = useState(null);
126
+ // The committed range anchors the view on its start; the end is only a fallback
127
+ // for a range whose start has been cleared. Keeping this in one effect avoids the
128
+ // ordering hazard of two effects writing the same state in the same commit.
126
129
  useUpdateEffect(() => {
127
- if (startDate) {
128
- setViewingMonth(startDate);
130
+ const anchorDate = startDate ?? endDate;
131
+ if (anchorDate) {
132
+ setViewingMonth(anchorDate);
129
133
  }
130
- }, [startDate]);
131
- useUpdateEffect(() => {
132
- if (endDate) {
133
- setViewingMonth(endDate);
134
- }
135
- }, [endDate]);
134
+ }, [startDate, endDate]);
136
135
  useUpdateEffect(() => {
137
136
  if (value) {
138
137
  setDates(value);
@@ -141,15 +140,15 @@ const useBaseRangePickerContextValue = ({ value, defaultValue, disabled = false,
141
140
  }, [value, formatDate, setDates]);
142
141
  const handleChange = useCallback((newDates) => {
143
142
  let [newStartDate, newEndDate] = normalizeToStartOfDay(newDates);
144
- // Swap dates if start date is after end date
145
- if (newStartDate && newEndDate && dayjs(newStartDate).isAfter(newEndDate)) {
143
+ // Swap dates if start date is after end date, unless the consumer opted out to validate the typed order
144
+ if (!disableAutoSwap && newStartDate && newEndDate && dayjs(newStartDate).isAfter(newEndDate)) {
146
145
  [newStartDate, newEndDate] = [newEndDate, newStartDate];
147
146
  }
148
147
  setDates([newStartDate, newEndDate]);
149
148
  // In uncontrolled mode, update dateStrings for input display
150
149
  setDateStrings([newStartDate ? formatDate(newStartDate) : '', newEndDate ? formatDate(newEndDate) : '']);
151
150
  onChange?.([newStartDate, newEndDate]);
152
- }, [onChange, formatDate, setDates]);
151
+ }, [onChange, formatDate, setDates, disableAutoSwap]);
153
152
  const handleStartDateChange = useCallback((date, nextEndDate) => {
154
153
  const proposedEndDate = nextEndDate ?? endDate;
155
154
  // Apply auto-completion behavior when start date is set and end date is missing
@@ -294,13 +293,15 @@ const useBaseRangePickerContextValue = ({ value, defaultValue, disabled = false,
294
293
  }
295
294
  return false;
296
295
  }, [selecting, temporaryStart, temporaryEnd, previewDate]);
297
- const isRangeStart = useCallback((date) => !!temporaryStart && date.getTime() === temporaryStart.getTime(), [temporaryStart]);
298
- const isRangeEnd = useCallback((date) => !!temporaryEnd && date.getTime() === temporaryEnd.getTime(), [temporaryEnd]);
296
+ // With auto-swap disabled, a reversed range is shown with no selection so it is not mistaken for a valid one
297
+ const isReversedRange = disableAutoSwap && !!temporaryStart && !!temporaryEnd && dayjs(temporaryStart).isAfter(temporaryEnd);
298
+ const isRangeStart = useCallback((date) => !isReversedRange && !!temporaryStart && date.getTime() === temporaryStart.getTime(), [isReversedRange, temporaryStart]);
299
+ const isRangeEnd = useCallback((date) => !isReversedRange && !!temporaryEnd && date.getTime() === temporaryEnd.getTime(), [isReversedRange, temporaryEnd]);
299
300
  // Add onApply handler for Apply button with auto-completion behavior
300
301
  const handleApply = useCallback(() => {
301
- // Always swap if start > end
302
+ // Swap if start > end, unless the consumer opted out to validate the typed order
302
303
  let [newStart, newEnd] = [temporaryStart, temporaryEnd];
303
- if (newStart && newEnd && dayjs(newStart).isAfter(newEnd)) {
304
+ if (!disableAutoSwap && newStart && newEnd && dayjs(newStart).isAfter(newEnd)) {
304
305
  [newStart, newEnd] = [newEnd, newStart];
305
306
  }
306
307
  // Validate dates against constraints before applying
@@ -317,7 +318,7 @@ const useBaseRangePickerContextValue = ({ value, defaultValue, disabled = false,
317
318
  handleChange(autoCompletedRange);
318
319
  // Do not setTemporaryStart/setTemporaryEnd here; the render-time adjustment above syncs them from startDate/endDate
319
320
  close();
320
- }, [handleChange, close, temporaryStart, temporaryEnd, minDate, maxDate, format]);
321
+ }, [handleChange, close, temporaryStart, temporaryEnd, minDate, maxDate, format, disableAutoSwap]);
321
322
  const handleCancel = useCallback(() => {
322
323
  // Use flushSync to ensure all state updates happen synchronously
323
324
  flushSync(() => {
@@ -63,6 +63,14 @@ export type BaseRangePickerProviderProps = {
63
63
  * @default false - Automatically open the Popover when we click on the TextBox.
64
64
  */
65
65
  disableAutoOpen?: boolean;
66
+ /**
67
+ * Whether to keep a typed range as entered when the start date is later than the end date.
68
+ * When true, the dates are not reordered before `onChange`, and no date is marked as selected
69
+ * while the range is reversed.
70
+ *
71
+ * @default false
72
+ */
73
+ disableAutoSwap?: boolean;
66
74
  /**
67
75
  * The minimum selectable date.
68
76
  * Dates before this date will be disabled and cannot be selected.
@@ -16,7 +16,8 @@ type UseDateRangeTriggerValueControllerOptions = {
16
16
  };
17
17
  /**
18
18
  * Controller for the value of the BaseRangePicker component.
19
- * Automatically ensures dates are in chronological order by swapping them if needed on blur.
19
+ * Commits the typed dates on blur. Unless `disableAutoSwap` is set on the picker,
20
+ * a reversed range is reordered into chronological order.
20
21
  *
21
22
  * @param options - The options for the controller.
22
23
  *
@@ -33,7 +33,8 @@ const createInputProps = (baseProps, value, format, onStringChange, onDateChange
33
33
  });
34
34
  /**
35
35
  * Controller for the value of the BaseRangePicker component.
36
- * Automatically ensures dates are in chronological order by swapping them if needed on blur.
36
+ * Commits the typed dates on blur. Unless `disableAutoSwap` is set on the picker,
37
+ * a reversed range is reordered into chronological order.
37
38
  *
38
39
  * @param options - The options for the controller.
39
40
  *
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../../../../styled-system/css';
3
3
  import { dayCellSlotRecipe } from '../../../../styled-system/recipes';
4
4
  import { Typography } from '../../../Typography';
5
+ import { VisuallyHidden } from '../../../VisuallyHidden';
5
6
  import { DAY_ABBREVIATIONS, DAY_FULL_NAMES } from '../BasePicker/constants';
6
7
  import { useCalendarLocale } from '../CalendarLocale/CalendarLocaleContext';
7
8
  /**
@@ -12,5 +13,5 @@ export function DayCell({ day }) {
12
13
  const classNames = dayCellSlotRecipe();
13
14
  const abbreviation = DAY_ABBREVIATIONS[calendarLocale][day];
14
15
  const fullName = DAY_FULL_NAMES[calendarLocale][day];
15
- return (_jsxs("div", { className: cx(classNames.root, 'mfui-DayCell__root'), children: [_jsx(Typography, { "aria-hidden": true, variant: "contentHeading", className: cx(classNames.label, 'mfui-DayCell__label'), children: abbreviation }), _jsx("span", { className: cx(classNames.srOnlyLabel, 'mfui-DayCell__srOnlyLabel'), children: fullName })] }));
16
+ return (_jsxs("div", { className: cx(classNames.root, 'mfui-DayCell__root'), children: [_jsx(Typography, { "aria-hidden": true, variant: "contentHeading", className: cx(classNames.label, 'mfui-DayCell__label'), children: abbreviation }), _jsx(VisuallyHidden, { className: "mfui-DayCell__srOnlyLabel", children: fullName })] }));
16
17
  }
@@ -1,6 +1,6 @@
1
1
  import { type BasePickerProps } from '../BasePicker/BasePicker.types';
2
2
  type CalendarLocale = NonNullable<BasePickerProps['calendarLocale']>;
3
- type DatePickerA11yLabelKey = 'TODAY' | 'SELECTED' | 'IN_RANGE' | 'UNAVAILABLE' | 'RANGE_START' | 'RANGE_END';
3
+ type DatePickerA11yLabelKey = 'SELECTED' | 'IN_RANGE' | 'UNAVAILABLE' | 'RANGE_START' | 'RANGE_END';
4
4
  /**
5
5
  * Shared accessibility labels for date picker components, keyed by calendar locale.
6
6
  * These constants provide consistent text for screen readers.
@@ -4,8 +4,6 @@
4
4
  */
5
5
  export const DATE_PICKER_A11Y_LABELS = {
6
6
  ja: {
7
- /** Label for today's date */
8
- TODAY: '今日',
9
7
  /** Label for selected dates */
10
8
  SELECTED: '選択済み',
11
9
  /** Label for dates in the selected range */
@@ -18,8 +16,6 @@ export const DATE_PICKER_A11Y_LABELS = {
18
16
  RANGE_END: '終了日',
19
17
  },
20
18
  en: {
21
- /** Label for today's date */
22
- TODAY: 'Today',
23
19
  /** Label for selected dates */
24
20
  SELECTED: 'Selected',
25
21
  /** Label for dates in the selected range */
@@ -20,6 +20,10 @@ export type DateLabelOptions = {
20
20
  /**
21
21
  * Generates an accessible label for a date cell with status information
22
22
  *
23
+ * Today's date is intentionally not part of the label: `DateCell` already marks it with
24
+ * `aria-current="date"`, which screen readers announce on their own. Adding it here too
25
+ * would make today's cell announce the same fact twice.
26
+ *
23
27
  * @param options - Configuration options for the label
24
28
  *
25
29
  * @returns Formatted accessible label string
@@ -5,15 +5,15 @@ import { DATE_PICKER_A11Y_LABELS } from './constants';
5
5
  /**
6
6
  * Generates an accessible label for a date cell with status information
7
7
  *
8
+ * Today's date is intentionally not part of the label: `DateCell` already marks it with
9
+ * `aria-current="date"`, which screen readers announce on their own. Adding it here too
10
+ * would make today's cell announce the same fact twice.
11
+ *
8
12
  * @param options - Configuration options for the label
9
13
  *
10
14
  * @returns Formatted accessible label string
11
15
  */
12
16
  export function generateAccessibleDateLabel({ date, selected = false, inRange = false, disabled = false, isRangeStart = false, isRangeEnd = false, locale = 'ja', }) {
13
- const today = new Date();
14
- const isToday = date.getFullYear() === today.getFullYear() &&
15
- date.getMonth() === today.getMonth() &&
16
- date.getDate() === today.getDate();
17
17
  // Fall back to 'ja' so an unknown locale from untyped consumers cannot crash the calendar
18
18
  const safeLocale = locale in DATE_PICKER_A11Y_LABELS ? locale : 'ja';
19
19
  const labels = DATE_PICKER_A11Y_LABELS[safeLocale];
@@ -21,9 +21,6 @@ export function generateAccessibleDateLabel({ date, selected = false, inRange =
21
21
  // Pin the dayjs locale per call so the output does not depend on the global dayjs locale
22
22
  const dateText = dayjs(date).locale(safeLocale).format('D MMMM YYYY');
23
23
  const statusParts = [];
24
- if (isToday) {
25
- statusParts.push(labels.TODAY);
26
- }
27
24
  if (disabled) {
28
25
  statusParts.push(labels.UNAVAILABLE);
29
26
  }
@@ -13,6 +13,23 @@ export type DialogProps = {
13
13
  * inline text, status indicators, or icons — because the dialog uses
14
14
  * `aria-labelledby` to announce the title and screen readers will read the
15
15
  * entire subtree. Avoid placing interactive controls inside `title`.
16
+ * Note that `<Skeleton />` is hidden from assistive technologies, so a
17
+ * Skeleton-only title contributes nothing to the dialog's accessible name.
18
+ * Pair it with `<VisuallyHidden>` text to give the dialog an accessible name.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <Dialog
23
+ * title={
24
+ * <>
25
+ * <VisuallyHidden>Loading title</VisuallyHidden>
26
+ * <span style={{ display: 'inline-block', width: '240px', height: '24px' }}>
27
+ * <Skeleton />
28
+ * </span>
29
+ * </>
30
+ * }
31
+ * />
32
+ * ```
16
33
  */
17
34
  title: ReactNode;
18
35
  /**