@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.
- package/dist/src/Badge/Badge.js +2 -1
- package/dist/src/DataTable/DataTable.types.d.ts +1 -0
- package/dist/src/DataTable/DataTableBody/DataTableBody.js +5 -2
- package/dist/src/DataTable/DataTableFilterButton/DataTableFilterButton.js +3 -9
- package/dist/src/DataTable/DataTableHeaderCell/DataTableHeaderCell.js +7 -3
- package/dist/src/DataTable/DataTableHeaderCell/DataTableHeaderCell.types.d.ts +18 -6
- package/dist/src/DataTable/hooks/useVirtualizedColumns.js +2 -1
- package/dist/src/DateTimeSelection/FilterDatePicker/CalendarPopover/CalendarPopover.js +1 -1
- package/dist/src/DateTimeSelection/FilterDatePicker/FilterDatePicker.js +2 -4
- package/dist/src/DateTimeSelection/FilterDateRangePicker/DateRangePickerContent/DateRangePickerContent.js +1 -1
- package/dist/src/DateTimeSelection/FilterDateRangePicker/FilterDateRangePicker.js +2 -4
- package/dist/src/DateTimeSelection/FilterMonthPicker/FilterMonthPicker.js +2 -4
- package/dist/src/DateTimeSelection/FilterMonthPicker/MonthPickerPopover/MonthPickerPopover.js +1 -1
- package/dist/src/DateTimeSelection/FilterMonthRangePicker/FilterMonthRangePicker.js +2 -3
- package/dist/src/DateTimeSelection/FilterMonthRangePicker/MonthRangePickerPopover/MonthRangePickerPopover.js +1 -1
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.d.ts +1 -1
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.js +2 -2
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.types.d.ts +34 -0
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.js +18 -17
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.types.d.ts +8 -0
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerTrigger/hooks/useDateRangeTriggerValueController.d.ts +2 -1
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerTrigger/hooks/useDateRangeTriggerValueController.js +2 -1
- package/dist/src/DateTimeSelection/shared/DayCell/DayCell.js +2 -1
- package/dist/src/DateTimeSelection/shared/accessibility/constants.d.ts +1 -1
- package/dist/src/DateTimeSelection/shared/accessibility/constants.js +0 -4
- package/dist/src/DateTimeSelection/shared/accessibility/generateDateLabel.d.ts +4 -0
- package/dist/src/DateTimeSelection/shared/accessibility/generateDateLabel.js +4 -7
- package/dist/src/Dialog/Dialog.types.d.ts +17 -0
- package/dist/src/FileDropZone/FileDropZone.js +23 -6
- package/dist/src/Popover/Popover.d.ts +1 -1
- package/dist/src/Popover/Popover.js +118 -14
- package/dist/src/Popover/Popover.types.d.ts +16 -3
- package/dist/src/SectionMessage/SectionMessage.d.ts +6 -0
- package/dist/src/SectionMessage/SectionMessage.js +16 -1
- package/dist/src/SectionMessage/SectionMessage.types.d.ts +4 -0
- package/dist/src/Skeleton/Skeleton.d.ts +5 -2
- package/dist/src/Skeleton/Skeleton.js +5 -2
- package/dist/src/Skeleton/Skeleton.types.d.ts +9 -1
- package/dist/src/VisuallyHidden/VisuallyHidden.d.ts +17 -0
- package/dist/src/VisuallyHidden/VisuallyHidden.js +21 -0
- package/dist/src/VisuallyHidden/VisuallyHidden.types.d.ts +19 -0
- package/dist/src/VisuallyHidden/VisuallyHidden.types.js +1 -0
- package/dist/src/VisuallyHidden/index.d.ts +2 -0
- package/dist/src/VisuallyHidden/index.js +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/utilities/dom/usePopoverMenuPlacement.js +0 -5
- package/dist/src/utilities/env/isProduction.d.ts +14 -0
- package/dist/src/utilities/env/isProduction.js +22 -0
- package/dist/src/utilities/react/isComponentOrWrapped.js +2 -1
- package/dist/styled-system/recipes/badge-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/badge-slot-recipe.js +0 -4
- package/dist/styled-system/recipes/day-cell-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/day-cell-slot-recipe.js +0 -4
- package/dist/styled-system/recipes/index.d.ts +2 -1
- package/dist/styled-system/recipes/index.js +1 -0
- package/dist/styled-system/recipes/popover-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/popover-slot-recipe.js +4 -0
- package/dist/styled-system/recipes/visually-hidden-slot-recipe.d.ts +33 -0
- package/dist/styled-system/recipes/visually-hidden-slot-recipe.js +28 -0
- package/dist/styles.css +27 -38
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
package/dist/src/Badge/Badge.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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.
|
package/dist/src/DateTimeSelection/FilterMonthPicker/MonthPickerPopover/MonthPickerPopover.js
CHANGED
|
@@ -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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
298
|
-
const
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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(
|
|
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 = '
|
|
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
|
/**
|