@moneyforward/mfui-components 4.3.1 → 4.6.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/Accordion/Accordion.d.ts +21 -1
- package/dist/src/Accordion/Accordion.js +28 -4
- package/dist/src/Accordion/Accordion.types.d.ts +21 -0
- package/dist/src/Badge/Badge.d.ts +1 -1
- package/dist/src/Badge/Badge.js +4 -4
- package/dist/src/Badge/Badge.types.d.ts +3 -1
- package/dist/src/Breadcrumbs/Breadcrumbs.js +1 -1
- package/dist/src/Checkbox/Checkbox.js +7 -3
- package/dist/src/CheckboxCard/CheckboxCard.js +3 -1
- package/dist/src/DataTable/DataTable.js +9 -2
- package/dist/src/DataTable/DataTable.types.d.ts +17 -2
- package/dist/src/DataTable/DataTableCell/DataTableCell.js +21 -4
- package/dist/src/DataTable/DataTableCell/DataTableCell.types.d.ts +1 -1
- package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.js +9 -5
- package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.types.d.ts +19 -1
- package/dist/src/DataTable/DataTableRow/DataTableRow.js +106 -31
- package/dist/src/DataTable/DataTableRow/DataTableRow.types.d.ts +63 -12
- package/dist/src/DataTable/DataTableRowActionContext.d.ts +34 -0
- package/dist/src/DataTable/DataTableRowActionContext.js +8 -0
- package/dist/src/DateTimeSelection/DatePicker/DatePickerCalendar/DatePickerCalendar.js +7 -3
- package/dist/src/DateTimeSelection/DatePicker/DatePickerDateCell/index.d.ts +1 -1
- package/dist/src/DateTimeSelection/DatePicker/DatePickerDateCell/index.js +0 -1
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerPopover/DateRangePickerCalendar/DateRangePickerCalendar.js +1 -1
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerPopover/utilities/createDateRangePickerPopoverTestUtility.js +2 -2
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerProvider/DateRangePickerProvider.js +13 -7
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerTrigger/DateRangePickerTrigger.js +1 -1
- package/dist/src/DateTimeSelection/FilterDatePicker/FilterDatePicker.js +1 -1
- package/dist/src/DateTimeSelection/FilterMonthPicker/FilterMonthPicker.js +1 -1
- package/dist/src/DateTimeSelection/FilterMonthRangePicker/FilterMonthRangePicker.js +1 -1
- package/dist/src/DateTimeSelection/MonthPicker/MonthPickerPanel/MonthPickerPanel.js +1 -1
- package/dist/src/DateTimeSelection/MonthPicker/constants.d.ts +2 -2
- package/dist/src/DateTimeSelection/MonthPicker/constants.js +6 -6
- package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerMonthCell/MonthRangePickerMonthCell.js +1 -1
- package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerNavigation/MonthRangePickerNavigation.js +2 -2
- package/dist/src/DateTimeSelection/TimePicker/TimePicker.js +1 -1
- package/dist/src/DateTimeSelection/shared/BasePicker/constants.d.ts +2 -2
- package/dist/src/DateTimeSelection/shared/BasePicker/constants.js +10 -10
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerPopover/BaseRangePickerCalendar/BaseRangePickerCalendar.js +1 -1
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerPopover/constants.js +4 -4
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.js +13 -7
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerTrigger/BaseRangePickerTrigger.js +2 -2
- package/dist/src/DateTimeSelection/shared/CalendarGrid/CalendarGrid.js +2 -4
- package/dist/src/DateTimeSelection/shared/utilities/japaneseCalendar.js +2 -2
- package/dist/src/Dialog/Dialog.d.ts +1 -1
- package/dist/src/Dialog/Dialog.types.d.ts +1 -1
- package/dist/src/FileDropZone/FileDropZone.js +1 -1
- package/dist/src/FileDropZone/utils/fileHelpers.js +1 -1
- package/dist/src/MainNavigation/BaseMainNavigation.d.ts +1 -1
- package/dist/src/MainNavigation/BaseMainNavigation.js +3 -3
- package/dist/src/MainNavigation/MainNavigation.js +2 -1
- package/dist/src/MainNavigation/MainNavigation.types.d.ts +9 -0
- package/dist/src/MainNavigation/NarrowViewportMainNavigation.js +3 -3
- package/dist/src/MultipleSelectBox/MultipleSelectBox.js +27 -18
- package/dist/src/MultipleSelectBox/MultipleSelectBoxTrigger/MultipleSelectBoxTrigger.js +1 -1
- package/dist/src/MultipleSelectBox/hooks/useCreateOption.js +8 -4
- package/dist/src/MultipleSelectBox/hooks/useInitialFocusOnOptionPanelOpen.js +1 -1
- package/dist/src/MultipleSelectBox/hooks/useOptionKeyboardNavigation.js +9 -4
- package/dist/src/MultipleSelectBox/hooks/useSelectedValues.js +16 -50
- package/dist/src/NavigationList/NavigationListItem/NavigationListItem.d.ts +3 -0
- package/dist/src/NavigationList/NavigationListItem/NavigationListItem.js +6 -2
- package/dist/src/NavigationList/NavigationListItem/NavigationListItem.types.d.ts +2 -1
- package/dist/src/NumberBox/NumberBox.d.ts +5 -0
- package/dist/src/NumberBox/NumberBox.js +61 -6
- package/dist/src/NumberBox/NumberBox.types.d.ts +35 -0
- package/dist/src/PageHeader/PageHeader.types.d.ts +2 -0
- package/dist/src/Pagination/Pagination.js +4 -1
- package/dist/src/Popover/Popover.js +12 -4
- package/dist/src/RadioButtonCard/RadioButtonCard.js +3 -1
- package/dist/src/SelectBox/SelectBox.js +29 -21
- package/dist/src/SelectBox/hooks/useKeyboardNavigation.js +8 -2
- package/dist/src/SelectBox/utils/getFlatOptions.js +1 -1
- package/dist/src/SidePane/SidePane.js +87 -7
- package/dist/src/SidePane/SidePane.types.d.ts +39 -0
- package/dist/src/SplitView/hooks/useSplitViewPanelVisibility.js +5 -2
- package/dist/src/SubNavigation/SubNavigation.d.ts +10 -1
- package/dist/src/SubNavigation/SubNavigation.js +93 -21
- package/dist/src/SubNavigation/SubNavigation.types.d.ts +45 -7
- package/dist/src/Tabs/Tabs.js +8 -4
- package/dist/src/Tabs/hooks/useCalculatedSelectedTabId.js +7 -5
- package/dist/src/Tabs/hooks/useTabRegistry.js +5 -5
- package/dist/src/Toast/ToastContainer/ToastContainer.js +11 -2
- package/dist/src/ToggleSwitch/index.d.ts +1 -1
- package/dist/src/ToggleSwitch/index.js +0 -1
- package/dist/src/Tooltip/Tooltip.js +8 -3
- package/dist/src/Tooltip/hooks/useTooltipDisplayController.js +77 -88
- package/dist/src/utilities/dom/containsAcrossShadowRoots.d.ts +21 -0
- package/dist/src/utilities/dom/containsAcrossShadowRoots.js +33 -0
- package/dist/src/utilities/dom/getFocusableNodes.js +1 -3
- package/dist/src/utilities/dom/useFixedColumns.js +6 -8
- package/dist/src/utilities/dom/useFocusTrap.js +1 -1
- package/dist/src/utilities/dom/useTableEdgeCellMarkers.d.ts +56 -0
- package/dist/src/utilities/dom/useTableEdgeCellMarkers.js +223 -0
- package/dist/src/utilities/react/flattenChildren.d.ts +15 -0
- package/dist/src/utilities/react/flattenChildren.js +28 -0
- package/dist/src/utilities/state/useLatest.d.ts +1 -1
- package/dist/src/utilities/state/useLatest.js +8 -2
- package/dist/styled-system/recipes/badge-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/badge-slot-recipe.js +4 -0
- package/dist/styled-system/recipes/data-table-cell-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/data-table-cell-slot-recipe.js +8 -2
- package/dist/styled-system/recipes/data-table-row-slot-recipe.js +13 -6
- package/dist/styled-system/recipes/sub-navigation-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/sub-navigation-slot-recipe.js +12 -0
- package/dist/styles.css +175 -39
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +12 -12
- /package/dist/src/{MultipleSelectBox/utils → utilities/dom}/getDeepActiveElement.d.ts +0 -0
- /package/dist/src/{MultipleSelectBox/utils → utilities/dom}/getDeepActiveElement.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AccordionProps, type AccordionItemProps } from './Accordion.types';
|
|
2
2
|
declare function AccordionBase({ children, size, width, defaultOpenItems }: AccordionProps): import("react/jsx-runtime").JSX.Element;
|
|
3
|
-
declare function AccordionItem({ children, id, enableAutoUnmount }: AccordionItemProps): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
declare function AccordionItem({ children, id, enableAutoUnmount, open, onOpenStateChanged }: AccordionItemProps): import("react/jsx-runtime").JSX.Element;
|
|
4
4
|
/**
|
|
5
5
|
* The `Accordion` component.
|
|
6
6
|
*
|
|
@@ -24,6 +24,26 @@ declare function AccordionItem({ children, id, enableAutoUnmount }: AccordionIte
|
|
|
24
24
|
* </Accordion.Item>
|
|
25
25
|
* </Accordion>
|
|
26
26
|
* ```
|
|
27
|
+
*
|
|
28
|
+
* Each `Accordion.Item` can also be controlled with the `open` and `onOpenStateChanged` props, which lets a trigger
|
|
29
|
+
* outside the Accordion tree drive it. In that case you own the `aria-expanded` and `aria-controls` attributes of
|
|
30
|
+
* your own trigger, and `enableAutoUnmount={false}` keeps the collapsed content mounted.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```jsx
|
|
34
|
+
* const [isOpen, setIsOpen] = useState(false);
|
|
35
|
+
*
|
|
36
|
+
* <>
|
|
37
|
+
* <button type="button" aria-expanded={isOpen} aria-controls="accordion-1" onClick={() => setIsOpen(!isOpen)}>
|
|
38
|
+
* Toggle
|
|
39
|
+
* </button>
|
|
40
|
+
* <Accordion>
|
|
41
|
+
* <Accordion.Item id="accordion-1" open={isOpen} enableAutoUnmount={false}>
|
|
42
|
+
* <Accordion.Target>Content for section 1</Accordion.Target>
|
|
43
|
+
* </Accordion.Item>
|
|
44
|
+
* </Accordion>
|
|
45
|
+
* </>
|
|
46
|
+
* ```
|
|
27
47
|
*/
|
|
28
48
|
export declare const Accordion: typeof AccordionBase & {
|
|
29
49
|
/**
|
|
@@ -41,12 +41,16 @@ function AccordionBase({ children, size = 'medium', width, defaultOpenItems = []
|
|
|
41
41
|
};
|
|
42
42
|
return (_jsx("div", { style: containerStyle, children: _jsx(AccordionContext.Provider, { value: contextValue, children: children }) }));
|
|
43
43
|
}
|
|
44
|
-
function AccordionItem({ children, id, enableAutoUnmount = true }) {
|
|
44
|
+
function AccordionItem({ children, id, enableAutoUnmount = true, open, onOpenStateChanged }) {
|
|
45
45
|
const { openItems, toggleItem } = useAccordionContext();
|
|
46
|
-
const
|
|
46
|
+
const isControlled = open !== undefined;
|
|
47
|
+
const isOpen = open ?? openItems[id] ?? false;
|
|
47
48
|
const toggle = useCallback(() => {
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
if (!isControlled) {
|
|
50
|
+
toggleItem(id);
|
|
51
|
+
}
|
|
52
|
+
onOpenStateChanged?.(!isOpen);
|
|
53
|
+
}, [isControlled, toggleItem, id, onOpenStateChanged, isOpen]);
|
|
50
54
|
const contextValue = useMemo(() => ({
|
|
51
55
|
isOpen,
|
|
52
56
|
toggle,
|
|
@@ -100,6 +104,26 @@ const AccordionTarget = forwardRef(({ children, className, style, ...props }, re
|
|
|
100
104
|
* </Accordion.Item>
|
|
101
105
|
* </Accordion>
|
|
102
106
|
* ```
|
|
107
|
+
*
|
|
108
|
+
* Each `Accordion.Item` can also be controlled with the `open` and `onOpenStateChanged` props, which lets a trigger
|
|
109
|
+
* outside the Accordion tree drive it. In that case you own the `aria-expanded` and `aria-controls` attributes of
|
|
110
|
+
* your own trigger, and `enableAutoUnmount={false}` keeps the collapsed content mounted.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```jsx
|
|
114
|
+
* const [isOpen, setIsOpen] = useState(false);
|
|
115
|
+
*
|
|
116
|
+
* <>
|
|
117
|
+
* <button type="button" aria-expanded={isOpen} aria-controls="accordion-1" onClick={() => setIsOpen(!isOpen)}>
|
|
118
|
+
* Toggle
|
|
119
|
+
* </button>
|
|
120
|
+
* <Accordion>
|
|
121
|
+
* <Accordion.Item id="accordion-1" open={isOpen} enableAutoUnmount={false}>
|
|
122
|
+
* <Accordion.Target>Content for section 1</Accordion.Target>
|
|
123
|
+
* </Accordion.Item>
|
|
124
|
+
* </Accordion>
|
|
125
|
+
* </>
|
|
126
|
+
* ```
|
|
103
127
|
*/
|
|
104
128
|
export const Accordion = Object.assign(AccordionBase, {
|
|
105
129
|
/**
|
|
@@ -42,6 +42,27 @@ export type AccordionItemProps = {
|
|
|
42
42
|
* @default true
|
|
43
43
|
*/
|
|
44
44
|
enableAutoUnmount?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The open state of this accordion item.
|
|
47
|
+
* You only need to use this prop if you want to control the item programmatically, for example when the trigger
|
|
48
|
+
* lives outside the Accordion tree. While this prop is defined, it takes precedence over the `defaultOpenItems` of
|
|
49
|
+
* the Accordion and over the internal state, so you have to update it yourself in response to
|
|
50
|
+
* `onOpenStateChanged`. Supply it consistently: do not switch an item between controlled and uncontrolled.
|
|
51
|
+
*
|
|
52
|
+
* When you drive the item from your own trigger outside the Accordion, you own its `aria-expanded` and
|
|
53
|
+
* `aria-controls` attributes. `Accordion.Target` renders the item `id` as its DOM `id`, so
|
|
54
|
+
* `aria-controls={id}` points at it. Note that with the default `enableAutoUnmount` the target is unmounted
|
|
55
|
+
* while collapsed, so `aria-controls` refers to an element that does not exist yet; set
|
|
56
|
+
* `enableAutoUnmount={false}` to keep the target in the DOM.
|
|
57
|
+
*/
|
|
58
|
+
open?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Callback fired when the open state of this accordion item is toggled by `Accordion.Trigger`.
|
|
61
|
+
* It is called in both controlled and uncontrolled mode.
|
|
62
|
+
*
|
|
63
|
+
* @param open - The new open state of the accordion item.
|
|
64
|
+
*/
|
|
65
|
+
onOpenStateChanged?: (open: boolean) => void;
|
|
45
66
|
};
|
|
46
67
|
/**
|
|
47
68
|
* Props for the Accordion.Trigger component
|
|
@@ -5,7 +5,7 @@ import { type BadgeProps } from './Badge.types';
|
|
|
5
5
|
* - If `dot` is `true`, the badge will be displayed as a dot.
|
|
6
6
|
* - If `count` is not `undefined`, the badge will be displayed as a number calculated by `digit` prop.
|
|
7
7
|
* - If `children` is not `undefined`, the badge will be displayed in the top-right corner of the children.
|
|
8
|
-
* - In addition, the `aria-describedby` attribute will be added to the children to link the
|
|
8
|
+
* - In addition, the `aria-describedby` attribute will be added to the children to link the visually hidden label of the badge.
|
|
9
9
|
*
|
|
10
10
|
* Also extends the props of `<span>` element.
|
|
11
11
|
*
|
package/dist/src/Badge/Badge.js
CHANGED
|
@@ -9,13 +9,13 @@ import { Typography } from '../Typography';
|
|
|
9
9
|
* - If `dot` is `true`, the badge will be displayed as a dot.
|
|
10
10
|
* - If `count` is not `undefined`, the badge will be displayed as a number calculated by `digit` prop.
|
|
11
11
|
* - If `children` is not `undefined`, the badge will be displayed in the top-right corner of the children.
|
|
12
|
-
* - In addition, the `aria-describedby` attribute will be added to the children to link the
|
|
12
|
+
* - In addition, the `aria-describedby` attribute will be added to the children to link the visually hidden label of the badge.
|
|
13
13
|
*
|
|
14
14
|
* Also extends the props of `<span>` element.
|
|
15
15
|
*
|
|
16
16
|
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span
|
|
17
17
|
*/
|
|
18
|
-
export const Badge = forwardRef(({ dot, count: number, digit, className, children, ...props }, ref) => {
|
|
18
|
+
export const Badge = forwardRef(({ dot, count: number, digit, className, children, 'aria-label': ariaLabel, ...props }, ref) => {
|
|
19
19
|
const classes = badgeSlotRecipe({ hasChildren: children !== undefined });
|
|
20
20
|
const displayContent = (() => {
|
|
21
21
|
if (number === undefined)
|
|
@@ -25,10 +25,10 @@ export const Badge = forwardRef(({ dot, count: number, digit, className, childre
|
|
|
25
25
|
const maxValue = 10 ** digit - 1;
|
|
26
26
|
return number > maxValue ? `${String(maxValue)}+` : String(number);
|
|
27
27
|
})();
|
|
28
|
-
const
|
|
28
|
+
const labelId = useId();
|
|
29
29
|
const displayBadge = dot !== undefined || number !== undefined;
|
|
30
30
|
// If the badge is not displayed, just return the children.
|
|
31
31
|
if (!displayBadge)
|
|
32
32
|
return children;
|
|
33
|
-
return (_jsxs("span", { ref: ref, className: cx(classes.root, 'mfui-Badge__root', className),
|
|
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
34
|
});
|
|
@@ -50,7 +50,9 @@ export type BadgeProps = {
|
|
|
50
50
|
* The invisible label to specify how you want the badge to be recognized and read by screen readers.
|
|
51
51
|
* This prop is required.
|
|
52
52
|
*
|
|
53
|
-
*
|
|
53
|
+
* The value is not set as an `aria-label` attribute, because it is prohibited on role-less elements.
|
|
54
|
+
* Instead, it is rendered as visually hidden text inside the badge.
|
|
55
|
+
* When `children` is specified, that text is also referenced by the `aria-describedby` attribute of the children.
|
|
54
56
|
*
|
|
55
57
|
* @example
|
|
56
58
|
* ```jsx
|
|
@@ -102,7 +102,7 @@ export const Breadcrumbs = forwardRef(({ items, wrap = false, customLinkComponen
|
|
|
102
102
|
if (wrap || !listRef.current)
|
|
103
103
|
return;
|
|
104
104
|
const observer = new ResizeObserver((entries) => {
|
|
105
|
-
const entry = entries
|
|
105
|
+
const [entry] = entries;
|
|
106
106
|
if (entry) {
|
|
107
107
|
setContainerWidth(entry.contentRect.width);
|
|
108
108
|
}
|
|
@@ -21,6 +21,13 @@ export const Checkbox = forwardRef(({ className, label, indeterminate = false, n
|
|
|
21
21
|
const checkboxGroupContext = useContext(CheckboxGroupContext);
|
|
22
22
|
const [localChecked, setLocalChecked] = useState(!!defaultChecked);
|
|
23
23
|
const [localIndeterminate, setLocalIndeterminate] = useState(indeterminate);
|
|
24
|
+
// Adjusted during render (React's documented pattern for resyncing state to a changed
|
|
25
|
+
// prop) rather than in an effect, so the UI never flashes the stale value for one frame.
|
|
26
|
+
const [prevIndeterminateProp, setPrevIndeterminateProp] = useState(indeterminate);
|
|
27
|
+
if (indeterminate !== prevIndeterminateProp) {
|
|
28
|
+
setPrevIndeterminateProp(indeterminate);
|
|
29
|
+
setLocalIndeterminate(indeterminate);
|
|
30
|
+
}
|
|
24
31
|
const classes = checkboxSlotRecipe();
|
|
25
32
|
const WrapperTag = label ? 'label' : 'span';
|
|
26
33
|
const wrapperProps = {
|
|
@@ -36,9 +43,6 @@ export const Checkbox = forwardRef(({ className, label, indeterminate = false, n
|
|
|
36
43
|
checkboxInputRef.current.indeterminate = checkboxGroupContext.values === undefined ? localIndeterminate : false;
|
|
37
44
|
}
|
|
38
45
|
}, [checkboxInputRef, localIndeterminate, checkboxGroupContext.values]);
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
setLocalIndeterminate(indeterminate);
|
|
41
|
-
}, [indeterminate]);
|
|
42
46
|
const icon = (() => {
|
|
43
47
|
if (isIndeterminate) {
|
|
44
48
|
return _jsx(CheckboxIndeterminate, {});
|
|
@@ -29,5 +29,7 @@ export const CheckboxCard = forwardRef(({ label, layout = 'vertical', renderDeta
|
|
|
29
29
|
disableResponsive,
|
|
30
30
|
isResponsive: Boolean(responsiveSlot.narrowViewport),
|
|
31
31
|
});
|
|
32
|
-
return (_jsxs("div", { className: cx(classes.root, 'mfui-CheckboxCard__root', className), children: [_jsx(FocusIndicator, { disableNestedFocus: true, children: _jsxs(Typography, { variant: "label", className: cx(classes.inputWrapper, 'mfui-CheckboxCard__inputWrapper'), as: "label", htmlFor: props.id, children: [_jsx(Checkbox, { ref: ref, ...props, className: cx(classes.input, 'mfui-CheckboxCard__input') }), _jsx("span", { className: cx(classes.label, 'mfui-CheckboxCard__label'), children: label })] }) }), responsiveSlot.narrowViewport
|
|
32
|
+
return (_jsxs("div", { className: cx(classes.root, 'mfui-CheckboxCard__root', className), children: [_jsx(FocusIndicator, { disableNestedFocus: true, children: _jsxs(Typography, { variant: "label", className: cx(classes.inputWrapper, 'mfui-CheckboxCard__inputWrapper'), as: "label", htmlFor: props.id, children: [_jsx(Checkbox, { ref: ref, ...props, className: cx(classes.input, 'mfui-CheckboxCard__input') }), _jsx("span", { className: cx(classes.label, 'mfui-CheckboxCard__label'), children: label })] }) }), responsiveSlot.narrowViewport ? (_jsxs(_Fragment, { children: [_jsx("div", { "data-mfui-content": "details", className: cx(classes.details, classes.detailsWide, 'mfui-CheckboxCard__details'), children: responsiveSlot.base(checkboxState) }), _jsx("div", { "data-mfui-content": "details", className: cx(classes.details, classes.detailsNarrow, 'mfui-CheckboxCard__details'), children: responsiveSlot.narrowViewport(checkboxState) })] })) : (
|
|
33
|
+
/* For function-based renderDetailsSlot or ResponsiveSlot with only base, use single details element */
|
|
34
|
+
responsiveSlot.base && (_jsx("div", { "data-mfui-content": "details", className: cx(classes.details, 'mfui-CheckboxCard__details'), children: responsiveSlot.base(checkboxState) })))] }));
|
|
33
35
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Children, isValidElement, useMemo, forwardRef } from 'react';
|
|
3
|
+
import { Children, isValidElement, useMemo, useRef, forwardRef } from 'react';
|
|
4
4
|
import { DataTableProvider } from './DataTableProvider';
|
|
5
5
|
import { dataTableSlotRecipe } from '../../styled-system/recipes';
|
|
6
6
|
import { DataTableBody } from './DataTableBody';
|
|
@@ -13,8 +13,15 @@ import { DataTableRow } from './DataTableRow';
|
|
|
13
13
|
import { DataTableSortButton } from './DataTableSortButton';
|
|
14
14
|
import { cx } from '../../styled-system/css';
|
|
15
15
|
import { isComponentOrWrapped } from '../utilities/react/isComponentOrWrapped';
|
|
16
|
+
import { mergeRefs } from '../utilities/dom/mergeRefs';
|
|
17
|
+
import { useTableEdgeCellMarkers } from '../utilities/dom/useTableEdgeCellMarkers';
|
|
16
18
|
const Base = forwardRef(({ className, size = 'medium', enableRowSelection = false, enableRowClickSelection = false, fixedHeader = false, showVerticalBorder = false, leftFixedColumnIndex, rightFixedColumnIndex, layout = 'default', wrapperProps, loading = false, emptyStateSlot, children, ...props }, ref) => {
|
|
17
19
|
const classes = dataTableSlotRecipe({ layout });
|
|
20
|
+
const tableRef = useRef(null);
|
|
21
|
+
// `:first-child` / `:last-child` cannot tell which grid column a cell renders in, so the
|
|
22
|
+
// table-edge spacing of the cell recipes misfires on the rows a `rowSpan` extends into.
|
|
23
|
+
// This marks the displaced cells so that spacing can be cancelled on them.
|
|
24
|
+
useTableEdgeCellMarkers(tableRef);
|
|
18
25
|
// Check if DataTable has any rows by looking for DataTable.Body with DataTable.Row children
|
|
19
26
|
const hasRows = useMemo(() => {
|
|
20
27
|
if (loading)
|
|
@@ -39,7 +46,7 @@ const Base = forwardRef(({ className, size = 'medium', enableRowSelection = fals
|
|
|
39
46
|
}, [children, loading]);
|
|
40
47
|
const shouldShowEmptyState = !hasRows && !!emptyStateSlot && !loading;
|
|
41
48
|
return (_jsx(DataTableProvider, { size: size, enableRowSelection: enableRowSelection, enableRowClickSelection: enableRowClickSelection, fixedHeader: fixedHeader, showVerticalBorder: showVerticalBorder, leftFixedColumnIndex: leftFixedColumnIndex, rightFixedColumnIndex: rightFixedColumnIndex, loading: loading, layout: layout, children: _jsxs("div", { ...wrapperProps, className: cx(classes.wrapper, 'mfui-DataTable__wrapper',
|
|
42
|
-
/* TODO: Remove in v5.0.0 */ 'mfui-DataGrid__wrapper', wrapperProps?.className), children: [_jsx("table", { ref: ref, className: cx(classes.table, 'mfui-DataTable__table',
|
|
49
|
+
/* TODO: Remove in v5.0.0 */ 'mfui-DataGrid__wrapper', wrapperProps?.className), children: [_jsx("table", { ref: mergeRefs(ref, tableRef), className: cx(classes.table, 'mfui-DataTable__table',
|
|
43
50
|
/* TODO: Remove in v5.0.0 */ 'mfui-DataGrid__table', className), ...props, children: children }), shouldShowEmptyState ? (_jsx("div", { className: cx(classes.emptyState, 'mfui-DataTable__emptyState',
|
|
44
51
|
/* TODO: Remove in v5.0.0 */ 'mfui-DataGrid__emptyState'), role: "status", "aria-live": "polite", children: emptyStateSlot })) : null] }) }));
|
|
45
52
|
});
|
|
@@ -9,6 +9,15 @@ import { type DataTableSlotRecipeVariant } from '../../styled-system/recipes';
|
|
|
9
9
|
* - `amount`: Right-aligned numeric content (amounts, balances, etc.)
|
|
10
10
|
*/
|
|
11
11
|
export type CellDataType = 'text' | 'amount';
|
|
12
|
+
/**
|
|
13
|
+
* The function that wraps the built-in selection checkbox of `DataTable.HeaderRow` or `DataTable.Row`.
|
|
14
|
+
*
|
|
15
|
+
* It receives the selection checkbox element and returns the content to render in the checkbox cell.
|
|
16
|
+
* The returned content must contain the passed element, because the row owns the checkbox itself.
|
|
17
|
+
*
|
|
18
|
+
* @param checkbox - The built-in selection checkbox element.
|
|
19
|
+
*/
|
|
20
|
+
export type DataTableSelectionCheckboxWrapper = (checkbox: ReactNode) => ReactNode;
|
|
12
21
|
/**
|
|
13
22
|
* The props for the `DataTable` component.
|
|
14
23
|
*
|
|
@@ -40,8 +49,8 @@ export type DataTableProps = {
|
|
|
40
49
|
* - Clicking the row, including its checkbox cell, calls the `checkboxProps.onChange` of the `DataTable.Row` component once.
|
|
41
50
|
* The checkbox stops responding to clicks on its own, so a single click never toggles the selection twice.
|
|
42
51
|
* The checkbox cell does not have independent hover, focus, or active styles; the checkbox icon follows the row state.
|
|
43
|
-
* - The
|
|
44
|
-
*
|
|
52
|
+
* - The checkbox stays in the tab order and is the keyboard control: pressing Enter or Space while it is focused toggles the selection.
|
|
53
|
+
* While the checkbox is focused, the focus indicator is displayed on the whole row.
|
|
45
54
|
* - The row selection is not toggled when the click originates from another interactive element inside the row,
|
|
46
55
|
* such as a button or a link, or when the row or its checkbox is disabled.
|
|
47
56
|
*
|
|
@@ -81,6 +90,9 @@ export type DataTableProps = {
|
|
|
81
90
|
* If the fixed column width might be changed with any interaction, such as disclosure, removing row or translating,
|
|
82
91
|
* please add `table-layout: fixed` to the DataTable and specify the width of the each column.
|
|
83
92
|
*
|
|
93
|
+
* **Not supported together with merged cells.** The index is counted in source order, which a
|
|
94
|
+
* `colSpan` / `rowSpan` shifts, so the fixed column is resolved on the wrong cells.
|
|
95
|
+
*
|
|
84
96
|
* @default undefined
|
|
85
97
|
*/
|
|
86
98
|
leftFixedColumnIndex?: number;
|
|
@@ -91,6 +103,9 @@ export type DataTableProps = {
|
|
|
91
103
|
* If the fixed column width might be changed with any interaction, such as disclosure, removing row or translating,
|
|
92
104
|
* please add `table-layout: fixed` to the DataTable and specify the width of the each column.
|
|
93
105
|
*
|
|
106
|
+
* **Not supported together with merged cells.** The index is counted in source order, which a
|
|
107
|
+
* `colSpan` / `rowSpan` shifts, so the fixed column is resolved on the wrong cells.
|
|
108
|
+
*
|
|
94
109
|
* @default undefined
|
|
95
110
|
*/
|
|
96
111
|
rightFixedColumnIndex?: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef } from 'react';
|
|
2
|
+
import { forwardRef, useState } from 'react';
|
|
3
3
|
import { DisclosureBasicCollapsed, DisclosureBasicExpanded, DisclosureNestedCollapsed, DisclosureNestedExpanded, } from '@moneyforward/mfui-icons-react';
|
|
4
4
|
import { dataTableCellSlotRecipe } from '../../../styled-system/recipes';
|
|
5
5
|
import { FocusIndicator } from '../../FocusIndicator';
|
|
@@ -10,6 +10,8 @@ import { cx } from '../../../styled-system/css';
|
|
|
10
10
|
import { useFixedColumns } from '../../utilities/dom/useFixedColumns';
|
|
11
11
|
import { handleCheckboxCellClick } from '../utils/handleCheckbox';
|
|
12
12
|
import { mergeRefs } from '../../utilities/dom/mergeRefs';
|
|
13
|
+
import { useIsomorphicLayoutEffect } from '../../utilities/react/useIsomorphicLayoutEffect';
|
|
14
|
+
import { useDataTableRowActionContext } from '../DataTableRowActionContext';
|
|
13
15
|
export const DataTableCell = forwardRef(({ onClickCell, children, type = 'text', className, isRowHighlighted, isRowHeader = false, disabled = false, depth = 0, columnIndex, disclosureIconProps: nestingIconProps, style, ...props }, ref) => {
|
|
14
16
|
const { size, leftFixedColumnIndex, rightFixedColumnIndex, loading, enableRowSelection, enableRowClickSelection, showVerticalBorder, layout, } = useDataTableContext();
|
|
15
17
|
const { cellRef, isFixedColumn, isEdgeFixedColumn, fixedPositionStyles } = useFixedColumns({
|
|
@@ -20,6 +22,21 @@ export const DataTableCell = forwardRef(({ onClickCell, children, type = 'text',
|
|
|
20
22
|
});
|
|
21
23
|
const isCheckboxCell = enableRowSelection && columnIndex === 0;
|
|
22
24
|
const isCheckboxCellInteractive = isCheckboxCell && !enableRowClickSelection;
|
|
25
|
+
// Take part in hosting the row action button of a clickable row. The row picks the host among
|
|
26
|
+
// the registered cells, so the button reaches a cell however the consumer composes the row.
|
|
27
|
+
const rowAction = useDataTableRowActionContext();
|
|
28
|
+
const registerRowActionCell = rowAction?.registerCell;
|
|
29
|
+
const [isRowActionHost, setIsRowActionHost] = useState(false);
|
|
30
|
+
useIsomorphicLayoutEffect(() => {
|
|
31
|
+
const element = cellRef.current;
|
|
32
|
+
if (!registerRowActionCell || !element) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
return registerRowActionCell({ element, isRowHeader, setIsHost: setIsRowActionHost });
|
|
36
|
+
// `columnIndex` changes when the consumer reorders keyed cells, and the re-registration lets
|
|
37
|
+
// the row pick the host again by the new cell positions.
|
|
38
|
+
}, [registerRowActionCell, isRowHeader, cellRef, columnIndex]);
|
|
39
|
+
const rowActionButtonProps = isRowActionHost ? rowAction?.buttonProps : undefined;
|
|
23
40
|
const classes = dataTableCellSlotRecipe({
|
|
24
41
|
size,
|
|
25
42
|
type,
|
|
@@ -27,7 +44,7 @@ export const DataTableCell = forwardRef(({ onClickCell, children, type = 'text',
|
|
|
27
44
|
fixedColumn: isFixedColumn,
|
|
28
45
|
edgeFixedColumn: isEdgeFixedColumn,
|
|
29
46
|
isCheckboxCell,
|
|
30
|
-
isCheckboxCellInteractive: isCheckboxCellInteractive
|
|
47
|
+
isCheckboxCellInteractive: isCheckboxCellInteractive ? true : undefined,
|
|
31
48
|
verticalBorder: showVerticalBorder,
|
|
32
49
|
layout,
|
|
33
50
|
// Passing `false` would emit a class for the unused `false` variant value, changing the
|
|
@@ -59,8 +76,8 @@ export const DataTableCell = forwardRef(({ onClickCell, children, type = 'text',
|
|
|
59
76
|
// A row header identifies its row, so it must be a `th` for assistive technology.
|
|
60
77
|
// `scope` is only interpreted on `th`, which is why it cannot be passed to a plain cell.
|
|
61
78
|
const Tag = isRowHeader ? 'th' : 'td';
|
|
62
|
-
return (
|
|
63
|
-
/* TODO: Remove in v5.0.0 */ 'mfui-DataGridCell__root', className), style: { ...style, ...fixedPositionStyles, zIndex: isFixedColumn ? 1 : undefined }, ...props, children: cellContent }));
|
|
79
|
+
return (_jsxs(Tag, { ref: mergeRefs(ref, cellRef), scope: isRowHeader ? 'row' : undefined, className: cx(classes.root, 'mfui-DataTableCell__root',
|
|
80
|
+
/* TODO: Remove in v5.0.0 */ 'mfui-DataGridCell__root', className), style: { ...style, ...fixedPositionStyles, zIndex: isFixedColumn ? 1 : undefined }, ...props, children: [rowActionButtonProps ? (_jsx("button", { type: "button", className: cx(classes.rowActionButton, 'mfui-DataTableCell__rowActionButton'), "data-mfui-content": "data-table-row-action-button", ...rowActionButtonProps })) : null, cellContent] }));
|
|
64
81
|
});
|
|
65
82
|
DataTableCell.displayName = 'DataTableCell';
|
|
66
83
|
function CellContent({ children, depth, disclosureIconProps, classes, }) {
|
|
@@ -109,7 +109,7 @@ export type DataTableCellProps = {
|
|
|
109
109
|
*
|
|
110
110
|
* `aria-expanded` is set automatically based on `isOpen`:
|
|
111
111
|
* on the cell button when the cell is interactive (`onClickCell`),
|
|
112
|
-
* or on the row when the row is interactive (`onClickRow`).
|
|
112
|
+
* or on the visually hidden row action button when the row is interactive (`onClickRow`).
|
|
113
113
|
*
|
|
114
114
|
* Note: the attribute on the cell button is fully derived from `isOpen`
|
|
115
115
|
* and cannot be overridden, while an `aria-expanded` passed to
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { cloneElement, forwardRef, isValidElement, useEffect } from 'react';
|
|
3
3
|
import { dataTableHeaderRowSlotRecipe } from '../../../styled-system/recipes';
|
|
4
4
|
import { useDataTableContext } from '../DataTableProvider';
|
|
5
5
|
import { DataTableHeaderCell } from '../DataTableHeaderCell';
|
|
6
6
|
import { Checkbox } from '../../Checkbox';
|
|
7
7
|
import { cx } from '../../../styled-system/css';
|
|
8
8
|
import { isComponentOrWrapped } from '../../utilities/react/isComponentOrWrapped';
|
|
9
|
+
import { flattenChildren } from '../../utilities/react/flattenChildren';
|
|
9
10
|
export const DataTableHeaderRow = forwardRef(({ children, checkboxProps, className, ...props }, ref) => {
|
|
10
11
|
const { enableRowSelection, setNumberColumns } = useDataTableContext();
|
|
11
12
|
const classes = dataTableHeaderRowSlotRecipe();
|
|
12
|
-
//
|
|
13
|
+
// Building the element before wrapping keeps everything the header row owns on the Checkbox itself.
|
|
14
|
+
const selectionCheckbox = (_jsx(Checkbox, { checked: checkboxProps?.checked, indeterminate: checkboxProps?.indeterminate, "aria-label": checkboxProps?.['aria-label'] ?? 'すべての行を選択', disabled: checkboxProps?.disabled, onChange: checkboxProps?.onChange, onClick: checkboxProps?.onClick }));
|
|
15
|
+
// Count the number of header cells, plus the selection checkbox column rendered
|
|
16
|
+
// below when `enableRowSelection` is on (it isn't part of `children`).
|
|
13
17
|
useEffect(() => {
|
|
14
|
-
const headerCells =
|
|
15
|
-
setNumberColumns(headerCells.length);
|
|
18
|
+
const headerCells = flattenChildren(children).filter((child) => isValidElement(child) && isComponentOrWrapped(child, DataTableHeaderCell));
|
|
19
|
+
setNumberColumns(headerCells.length + (enableRowSelection ? 1 : 0));
|
|
16
20
|
}, [children, enableRowSelection, setNumberColumns]);
|
|
17
21
|
return (_jsxs("tr", { className: cx(classes.root, 'mfui-DataTableHeaderRow__root',
|
|
18
|
-
/* TODO: Remove in v5.0.0 */ 'mfui-DataGridHeaderRow__root', className), ...props, ref: ref, children: [enableRowSelection ? (_jsx(DataTableHeaderCell, { columnIndex: 0, disabled: checkboxProps?.disabled, children:
|
|
22
|
+
/* TODO: Remove in v5.0.0 */ 'mfui-DataGridHeaderRow__root', className), ...props, ref: ref, children: [enableRowSelection ? (_jsx(DataTableHeaderCell, { columnIndex: 0, disabled: checkboxProps?.disabled, children: checkboxProps?.wrapper ? checkboxProps.wrapper(selectionCheckbox) : selectionCheckbox })) : null, flattenChildren(children).map((child, index) => {
|
|
19
23
|
if (!isValidElement(child)) {
|
|
20
24
|
return child;
|
|
21
25
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ComponentPropsWithRef } from 'react';
|
|
2
2
|
import { type CheckboxProps } from '../../Checkbox';
|
|
3
|
+
import { type DataTableSelectionCheckboxWrapper } from '../DataTable.types';
|
|
3
4
|
/**
|
|
4
5
|
* The props for the `DataTableHeaderRow` component.
|
|
5
6
|
*
|
|
@@ -24,6 +25,23 @@ export type DataTableHeaderRowProps = {
|
|
|
24
25
|
* @property onClick - The handler called when the checkbox is clicked (for event propagation control).
|
|
25
26
|
*
|
|
26
27
|
* @property disabled - Whether the checkbox is disabled.
|
|
28
|
+
*
|
|
29
|
+
* @property wrapper - The function that wraps the checkbox, for example with the `Tooltip` component.
|
|
30
|
+
* It receives the checkbox element and must render it as it is, because the header row owns the checkbox itself.
|
|
31
|
+
* Keep the `Tooltip` component with its default `trigger` (`['hover', 'focus']`) so that keyboard users can also
|
|
32
|
+
* read the content; a tooltip opened by a click would duplicate the action of the checkbox.
|
|
33
|
+
* When the content restates the accessible name of the checkbox, pass `hideFromScreenReader` to the `Tooltip`
|
|
34
|
+
* component to avoid announcing the same information twice.
|
|
35
|
+
*
|
|
36
|
+
* ```tsx
|
|
37
|
+
* <DataTable.HeaderRow
|
|
38
|
+
* checkboxProps={{
|
|
39
|
+
* wrapper: (checkbox) => <Tooltip content="すべて選択">{checkbox}</Tooltip>,
|
|
40
|
+
* }}
|
|
41
|
+
* >
|
|
42
|
+
* ```
|
|
27
43
|
*/
|
|
28
|
-
checkboxProps?: Pick<CheckboxProps, 'aria-label' | 'checked' | 'indeterminate' | 'onChange' | 'disabled' | 'onClick'
|
|
44
|
+
checkboxProps?: Pick<CheckboxProps, 'aria-label' | 'checked' | 'indeterminate' | 'onChange' | 'disabled' | 'onClick'> & {
|
|
45
|
+
wrapper?: DataTableSelectionCheckboxWrapper;
|
|
46
|
+
};
|
|
29
47
|
} & ComponentPropsWithRef<'tr'>;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { cloneElement, forwardRef, isValidElement, useCallback, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { DataTableRowActionContext, } from '../DataTableRowActionContext';
|
|
3
4
|
import { dataTableRowSlotRecipe } from '../../../styled-system/recipes';
|
|
4
5
|
import { DataTableCell } from '../DataTableCell';
|
|
5
6
|
import { Checkbox } from '../../Checkbox';
|
|
6
7
|
import { useDataTableContext } from '../DataTableProvider';
|
|
7
8
|
import { cx } from '../../../styled-system/css';
|
|
8
9
|
import { isComponentOrWrapped } from '../../utilities/react/isComponentOrWrapped';
|
|
10
|
+
import { flattenChildren } from '../../utilities/react/flattenChildren';
|
|
11
|
+
import { useIsomorphicLayoutEffect } from '../../utilities/react/useIsomorphicLayoutEffect';
|
|
12
|
+
const ROW_ACTION_BUTTON_SELECTOR = '[data-mfui-content="data-table-row-action-button"]';
|
|
9
13
|
const INTERACTIVE_ELEMENT_SELECTOR = [
|
|
10
14
|
'a[href]',
|
|
11
15
|
'button',
|
|
@@ -33,17 +37,32 @@ function isClickFromNestedInteractiveElement(event) {
|
|
|
33
37
|
if (!(event.target instanceof Element)) {
|
|
34
38
|
return false;
|
|
35
39
|
}
|
|
40
|
+
// A click inside a nested table (e.g. a DataTable rendered in a cell) belongs to the inner row.
|
|
41
|
+
if (event.target.closest('tr') !== event.currentTarget) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
36
44
|
const interactiveElement = event.target.closest(INTERACTIVE_ELEMENT_SELECTOR);
|
|
37
|
-
|
|
45
|
+
if (interactiveElement === null || interactiveElement === event.currentTarget) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
// The visually hidden row action button exists to trigger the row click itself,
|
|
49
|
+
// so its click must not be treated as a nested interactive element's click.
|
|
50
|
+
return !interactiveElement.matches(ROW_ACTION_BUTTON_SELECTOR);
|
|
38
51
|
}
|
|
39
|
-
export const DataTableRow = forwardRef(({ children, disabled = false, highlighted, checkboxProps, className, onClickRow, selectable, ...props }, ref) => {
|
|
52
|
+
export const DataTableRow = forwardRef(({ children, disabled = false, highlighted, checkboxProps, className, onClickRow, rowActionButtonProps, selectable, 'aria-expanded': ariaExpandedProp, ...props }, ref) => {
|
|
40
53
|
const { enableRowSelection: contextEnableRowSelection, enableRowClickSelection, layout } = useDataTableContext();
|
|
41
54
|
const selectionCheckboxRef = useRef(null);
|
|
42
55
|
const isSelectable = selectable ?? contextEnableRowSelection;
|
|
43
|
-
|
|
56
|
+
// A selected row is always highlighted, and `highlighted` stays an independent signal on top of it,
|
|
57
|
+
// so that a row can be marked (e.g. as the one opened in a detail panel) while it is not selected.
|
|
58
|
+
const isRowHighlighted = isSelectable ? highlighted === true || checkboxProps?.checked === true : highlighted;
|
|
44
59
|
// Only a row whose selection can actually be toggled becomes the trigger of the row selection.
|
|
45
60
|
const isRowClickSelectable = !!enableRowClickSelection && isSelectable && !disabled && !checkboxProps?.disabled;
|
|
46
61
|
const isInteractive = (onClickRow !== undefined || isRowClickSelectable) && !disabled;
|
|
62
|
+
// The row click itself is exposed to keyboard and screen reader users through the row action
|
|
63
|
+
// button. The row click selection is exposed through the selection checkbox instead.
|
|
64
|
+
// A disabled row keeps the button as a disabled one so that the row action stays discoverable.
|
|
65
|
+
const hasRowActionButton = onClickRow !== undefined;
|
|
47
66
|
const classes = dataTableRowSlotRecipe({
|
|
48
67
|
layout,
|
|
49
68
|
highlighted: isRowHighlighted,
|
|
@@ -51,17 +70,85 @@ export const DataTableRow = forwardRef(({ children, disabled = false, highlighte
|
|
|
51
70
|
rowClickSelectable: isRowClickSelectable,
|
|
52
71
|
disabled,
|
|
53
72
|
});
|
|
73
|
+
// Fragments are expanded so that cells wrapped in a Fragment (e.g. `<>{items.map(...)}</>`)
|
|
74
|
+
// are recognized by the cell inspection below.
|
|
75
|
+
const flatChildren = useMemo(() => flattenChildren(children), [children]);
|
|
76
|
+
const isCellElement = (child) => isValidElement(child) && isComponentOrWrapped(child, DataTableCell);
|
|
54
77
|
// When the interactive row itself toggles a disclosure (a child cell shows the disclosure icon
|
|
55
|
-
// but does not handle the click by itself), expose the expanded state on the row.
|
|
56
|
-
const disclosureIconProps =
|
|
57
|
-
.filter(
|
|
78
|
+
// but does not handle the click by itself), expose the expanded state on the row action button.
|
|
79
|
+
const disclosureIconProps = flatChildren
|
|
80
|
+
.filter(isCellElement)
|
|
58
81
|
.find((cell) => cell.props.disclosureIconProps && !cell.props.onClickCell)?.props.disclosureIconProps;
|
|
59
|
-
const
|
|
82
|
+
const derivedAriaExpanded = hasRowActionButton && disclosureIconProps ? (disclosureIconProps.isOpen ?? false) : undefined;
|
|
83
|
+
const rowActionAriaExpanded = ariaExpandedProp ?? derivedAriaExpanded;
|
|
84
|
+
// The default name is given by `aria-label`, so a consumer `aria-label` replaces it and a consumer
|
|
85
|
+
// `aria-labelledby` takes precedence over it by the accessible name computation.
|
|
86
|
+
// A row that toggles a disclosure says so, since its expanded state is exposed on the button.
|
|
87
|
+
const defaultRowActionLabel = disclosureIconProps ? '行を開閉' : '行の操作';
|
|
88
|
+
const resolvedRowActionButtonProps = useMemo(() => hasRowActionButton
|
|
89
|
+
? {
|
|
90
|
+
...rowActionButtonProps,
|
|
91
|
+
// An explicitly undefined consumer label must not leave the button without a name.
|
|
92
|
+
'aria-label': rowActionButtonProps?.['aria-label'] ?? defaultRowActionLabel,
|
|
93
|
+
...(rowActionAriaExpanded !== undefined && { 'aria-expanded': rowActionAriaExpanded }),
|
|
94
|
+
...(disabled && { disabled }),
|
|
95
|
+
}
|
|
96
|
+
: undefined, [hasRowActionButton, rowActionButtonProps, defaultRowActionLabel, rowActionAriaExpanded, disabled]);
|
|
97
|
+
// The cells register themselves through the context, so the button reaches a cell wherever the
|
|
98
|
+
// consumer renders it (intermediate components, wrappers, Fragments). The version state makes the
|
|
99
|
+
// host selection below run again whenever a cell mounts or unmounts.
|
|
100
|
+
const rowActionCellEntries = useRef(new Set());
|
|
101
|
+
const [rowActionCellsVersion, setRowActionCellsVersion] = useState(0);
|
|
102
|
+
const registerRowActionCell = useCallback((entry) => {
|
|
103
|
+
rowActionCellEntries.current.add(entry);
|
|
104
|
+
setRowActionCellsVersion((version) => version + 1);
|
|
105
|
+
return () => {
|
|
106
|
+
rowActionCellEntries.current.delete(entry);
|
|
107
|
+
setRowActionCellsVersion((version) => version + 1);
|
|
108
|
+
};
|
|
109
|
+
}, []);
|
|
110
|
+
// The first cell in the row hosts the button, skipping a row header cell so that the button label
|
|
111
|
+
// does not become part of the row header name. The selection cell, rendered by the row itself,
|
|
112
|
+
// therefore hosts it whenever the row is selectable.
|
|
113
|
+
const hasWarnedMissingRowActionHost = useRef(false);
|
|
114
|
+
useIsomorphicLayoutEffect(() => {
|
|
115
|
+
// The registration order follows the mount order, which differs from the visual order when a
|
|
116
|
+
// cell mounts later, so sort by the position of the cell element in the row.
|
|
117
|
+
const entries = [...rowActionCellEntries.current].toSorted((a, b) => a.element.cellIndex - b.element.cellIndex);
|
|
118
|
+
const host = hasRowActionButton ? (entries.find((entry) => !entry.isRowHeader) ?? entries[0]) : undefined;
|
|
119
|
+
entries.forEach((entry) => {
|
|
120
|
+
entry.setIsHost(entry === host);
|
|
121
|
+
});
|
|
122
|
+
if (hasRowActionButton && host === undefined && !hasWarnedMissingRowActionHost.current) {
|
|
123
|
+
hasWarnedMissingRowActionHost.current = true;
|
|
124
|
+
console.warn('[MFUI] DataTable.Row received onClickRow, but the row action button was not rendered because ' +
|
|
125
|
+
'the row contains no DataTable.Cell, so the row cannot be operated with a keyboard or screen reader. ' +
|
|
126
|
+
'Render the cells of the row with DataTable.Cell.');
|
|
127
|
+
}
|
|
128
|
+
}, [hasRowActionButton, rowActionCellsVersion]);
|
|
129
|
+
// The cells of a row without the button do not register, so such a row does not pay for the host selection.
|
|
130
|
+
const rowActionContextValue = useMemo(() => ({
|
|
131
|
+
buttonProps: resolvedRowActionButtonProps,
|
|
132
|
+
registerCell: hasRowActionButton ? registerRowActionCell : undefined,
|
|
133
|
+
}), [hasRowActionButton, resolvedRowActionButtonProps, registerRowActionCell]);
|
|
60
134
|
// Forwarding the interaction to the checkbox lets the consumer keep handling the row selection in
|
|
61
135
|
// `checkboxProps.onChange` only, and the change event carries the real next checked state.
|
|
62
136
|
const toggleSelectionByRowInteraction = () => {
|
|
63
137
|
selectionCheckboxRef.current?.click();
|
|
64
138
|
};
|
|
139
|
+
// The focus indicator is displayed on the whole row while the selection checkbox is focused, so the
|
|
140
|
+
// selection also toggles with Enter, which a native checkbox ignores, in addition to Space.
|
|
141
|
+
const handleKeyDownSelectionCheckbox = (event) => {
|
|
142
|
+
if (isRowClickSelectable && event.key === 'Enter') {
|
|
143
|
+
// Prevents the implicit form submission.
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
toggleSelectionByRowInteraction();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
// Building the element before wrapping keeps everything the row owns on the Checkbox itself, so that the
|
|
149
|
+
// ref-based toggle and the recipe selectors keep working with a consumer-supplied wrapper in between.
|
|
150
|
+
const selectionCheckbox = (_jsx(Checkbox, { ref: selectionCheckboxRef, className: cx(classes.selectionCheckbox, 'mfui-DataTableRow__selectionCheckbox'), checked: checkboxProps?.checked, "aria-label": checkboxProps?.['aria-label'] ?? '行を選択', disabled: disabled || checkboxProps?.disabled, onChange: checkboxProps?.onChange, onClick: checkboxProps?.onClick, onKeyDown: handleKeyDownSelectionCheckbox }));
|
|
151
|
+
// Handles pointer clicks anywhere in the row and the bubbled click of the row action button.
|
|
65
152
|
const handleClickRow = (event) => {
|
|
66
153
|
if (disabled || isClickFromNestedInteractiveElement(event)) {
|
|
67
154
|
return;
|
|
@@ -71,29 +158,17 @@ export const DataTableRow = forwardRef(({ children, disabled = false, highlighte
|
|
|
71
158
|
}
|
|
72
159
|
onClickRow?.(event);
|
|
73
160
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return (_jsxs("tr", { ref: ref, className: cx(classes.root, 'mfui-DataTableRow__root',
|
|
85
|
-
/* TODO: Remove in v5.0.0 */ 'mfui-DataGridRow__root', className), ...(ariaExpanded !== undefined && { 'aria-expanded': ariaExpanded }), ...props,
|
|
86
|
-
// When the row is clickable, the role should be 'button' in order to allow screen readers to understand the row is clickable.
|
|
87
|
-
role: isInteractive ? 'button' : 'row', tabIndex: isInteractive ? 0 : undefined, ...(disabled && { 'aria-disabled': disabled }), onClick: handleClickRow, onKeyDown: handleKeyDownRow, children: [isSelectable ? (_jsx(DataTableCell, { columnIndex: 0, isRowHighlighted: isRowHighlighted, disabled: disabled, children: _jsx(Checkbox, { ref: selectionCheckboxRef, className: cx(classes.selectionCheckbox, 'mfui-DataTableRow__selectionCheckbox'), checked: checkboxProps?.checked, "aria-label": checkboxProps?.['aria-label'] ?? '行を選択', disabled: disabled || checkboxProps?.disabled, tabIndex: isRowClickSelectable ? -1 : undefined, onChange: checkboxProps?.onChange, onClick: checkboxProps?.onClick }) })) : null, Children.map(children, (child, index) => {
|
|
88
|
-
if (!isValidElement(child)) {
|
|
161
|
+
return (_jsx(DataTableRowActionContext.Provider, { value: rowActionContextValue, children: _jsxs("tr", { ref: ref, className: cx(classes.root, 'mfui-DataTableRow__root',
|
|
162
|
+
/* TODO: Remove in v5.0.0 */ 'mfui-DataGridRow__root', className), ...(!hasRowActionButton && ariaExpandedProp !== undefined && { 'aria-expanded': ariaExpandedProp }), ...props, ...(disabled && { 'aria-disabled': disabled }), onClick: handleClickRow, children: [isSelectable ? (_jsx(DataTableCell, { columnIndex: 0, isRowHighlighted: isRowHighlighted, disabled: disabled, children: checkboxProps?.wrapper ? checkboxProps.wrapper(selectionCheckbox) : selectionCheckbox })) : null, flatChildren.map((child, index) => {
|
|
163
|
+
if (!isValidElement(child)) {
|
|
164
|
+
return child;
|
|
165
|
+
}
|
|
166
|
+
if (isComponentOrWrapped(child, DataTableCell)) {
|
|
167
|
+
// Add selectable offset to columnIndex if selectable is true
|
|
168
|
+
const columnIndex = isSelectable ? index + 1 : index;
|
|
169
|
+
return cloneElement(child, { isRowHighlighted, columnIndex, disabled });
|
|
170
|
+
}
|
|
89
171
|
return child;
|
|
90
|
-
}
|
|
91
|
-
if (isComponentOrWrapped(child, DataTableCell)) {
|
|
92
|
-
// Add selectable offset to columnIndex if selectable is true
|
|
93
|
-
const columnIndex = isSelectable ? index + 1 : index;
|
|
94
|
-
return cloneElement(child, { isRowHighlighted, columnIndex, disabled });
|
|
95
|
-
}
|
|
96
|
-
return child;
|
|
97
|
-
})] }));
|
|
172
|
+
})] }) }));
|
|
98
173
|
});
|
|
99
174
|
DataTableRow.displayName = 'DataTableRow';
|