@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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns whether `node` is `ancestor` itself or a descendant of it, crossing shadow boundaries.
|
|
3
|
+
*
|
|
4
|
+
* `Node.contains()` walks the node tree only, so an element inside a shadow root is *not*
|
|
5
|
+
* reported as contained by the shadow host or by any of the host's ancestors. This function
|
|
6
|
+
* follows the chain of shadow hosts upwards, so focus inside a web component rendered within
|
|
7
|
+
* `ancestor` still counts as being inside it.
|
|
8
|
+
*
|
|
9
|
+
* Pair it with `getDeepActiveElement`, which pierces shadow roots downwards: a shadow-piercing
|
|
10
|
+
* lookup compared with the non-piercing `Node.contains()` reports the deepest focused element as
|
|
11
|
+
* outside every ancestor it actually lives in.
|
|
12
|
+
*
|
|
13
|
+
* @param ancestor - The node expected to contain `node`. A nullish ancestor contains nothing.
|
|
14
|
+
*
|
|
15
|
+
* @param node - The node to look for. A nullish node is never contained.
|
|
16
|
+
*
|
|
17
|
+
* @returns `true` when `node` is inside `ancestor`, across any number of shadow boundaries.
|
|
18
|
+
*
|
|
19
|
+
* @see https://dom.spec.whatwg.org/#dom-node-contains
|
|
20
|
+
*/
|
|
21
|
+
export declare const containsAcrossShadowRoots: (ancestor: Node | null | undefined, node: Node | null | undefined) => boolean;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns whether `node` is `ancestor` itself or a descendant of it, crossing shadow boundaries.
|
|
3
|
+
*
|
|
4
|
+
* `Node.contains()` walks the node tree only, so an element inside a shadow root is *not*
|
|
5
|
+
* reported as contained by the shadow host or by any of the host's ancestors. This function
|
|
6
|
+
* follows the chain of shadow hosts upwards, so focus inside a web component rendered within
|
|
7
|
+
* `ancestor` still counts as being inside it.
|
|
8
|
+
*
|
|
9
|
+
* Pair it with `getDeepActiveElement`, which pierces shadow roots downwards: a shadow-piercing
|
|
10
|
+
* lookup compared with the non-piercing `Node.contains()` reports the deepest focused element as
|
|
11
|
+
* outside every ancestor it actually lives in.
|
|
12
|
+
*
|
|
13
|
+
* @param ancestor - The node expected to contain `node`. A nullish ancestor contains nothing.
|
|
14
|
+
*
|
|
15
|
+
* @param node - The node to look for. A nullish node is never contained.
|
|
16
|
+
*
|
|
17
|
+
* @returns `true` when `node` is inside `ancestor`, across any number of shadow boundaries.
|
|
18
|
+
*
|
|
19
|
+
* @see https://dom.spec.whatwg.org/#dom-node-contains
|
|
20
|
+
*/
|
|
21
|
+
export const containsAcrossShadowRoots = (ancestor, node) => {
|
|
22
|
+
if (!ancestor || !node)
|
|
23
|
+
return false;
|
|
24
|
+
let current = node;
|
|
25
|
+
while (current) {
|
|
26
|
+
if (ancestor.contains(current))
|
|
27
|
+
return true;
|
|
28
|
+
const rootNode = current.getRootNode();
|
|
29
|
+
// Only a ShadowRoot has a host to continue from; a Document ends the walk.
|
|
30
|
+
current = rootNode instanceof ShadowRoot ? rootNode.host : null;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
};
|
|
@@ -26,9 +26,7 @@ const isNonTabbableRadio = (node) => {
|
|
|
26
26
|
}
|
|
27
27
|
const getRadio = (selector) => node.ownerDocument.querySelector(`input[type="radio"]${selector}`);
|
|
28
28
|
let roving = getRadio(`[name="${node.name}"]:checked`);
|
|
29
|
-
|
|
30
|
-
roving = getRadio(`[name="${node.name}"]`);
|
|
31
|
-
}
|
|
29
|
+
roving ??= getRadio(`[name="${node.name}"]`);
|
|
32
30
|
return roving !== node;
|
|
33
31
|
};
|
|
34
32
|
// This function ensures that the node is focusable by checking if it is disabled, hidden, or a non-tabbable radio button.
|
|
@@ -4,7 +4,6 @@ import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
|
4
4
|
*/
|
|
5
5
|
export const useFixedColumns = ({ enableRowSelection = false, columnIndex, leftFixedColumnIndex, rightFixedColumnIndex, }) => {
|
|
6
6
|
const cellRef = useRef(null);
|
|
7
|
-
const [fixedPositionStyles, setFixedPositionStyles] = useState();
|
|
8
7
|
const isCheckboxColumn = enableRowSelection && columnIndex === 0;
|
|
9
8
|
// Adjust column index when row selection is enabled to account for the checkbox column
|
|
10
9
|
const actualColumnIndex = useMemo(() => {
|
|
@@ -79,19 +78,18 @@ export const useFixedColumns = ({ enableRowSelection = false, columnIndex, leftF
|
|
|
79
78
|
resizeObserver.disconnect();
|
|
80
79
|
};
|
|
81
80
|
}, []);
|
|
82
|
-
|
|
81
|
+
// Fully derivable from render-time values (no external system involved), so this is a
|
|
82
|
+
// plain memo rather than an effect writing to its own state.
|
|
83
|
+
const fixedPositionStyles = useMemo(() => {
|
|
83
84
|
if (isCheckboxColumn && leftFixedColumnIndex !== undefined) {
|
|
84
|
-
|
|
85
|
-
left: 0,
|
|
86
|
-
});
|
|
87
|
-
return;
|
|
85
|
+
return { left: 0 };
|
|
88
86
|
}
|
|
89
87
|
if (!cellRect)
|
|
90
88
|
return;
|
|
91
|
-
|
|
89
|
+
return {
|
|
92
90
|
left: fixedDirection === 'left' ? cellRect.left : undefined,
|
|
93
91
|
right: fixedDirection === 'right' ? cellRect.right : undefined,
|
|
94
|
-
}
|
|
92
|
+
};
|
|
95
93
|
}, [cellRect, fixedDirection, isCheckboxColumn, leftFixedColumnIndex]);
|
|
96
94
|
const isFixedColumn = useMemo(() => fixedDirection !== undefined, [fixedDirection]);
|
|
97
95
|
const isEdgeFixedColumn = useMemo(() => {
|
|
@@ -18,7 +18,7 @@ export function useFocusTrap({ onKeyDown, onPressingTabOnNotFocusableElement } =
|
|
|
18
18
|
const wrapper = event.currentTarget;
|
|
19
19
|
const focusingElement = wrapper.ownerDocument.activeElement;
|
|
20
20
|
const focusableNodes = getFocusableNodes(wrapper);
|
|
21
|
-
const firstFocusableNode = focusableNodes
|
|
21
|
+
const [firstFocusableNode] = focusableNodes;
|
|
22
22
|
const lastFocusableNode = focusableNodes.at(-1);
|
|
23
23
|
const focusingOnFirstNode = focusingElement === firstFocusableNode;
|
|
24
24
|
const focusingOnLastNode = focusingElement === lastFocusableNode;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marks a cell that is `:first-child` in its row but does **not** render in the table's first
|
|
3
|
+
* grid column, because a `rowSpan` from an earlier row occupies that column.
|
|
4
|
+
*
|
|
5
|
+
* The value is always `"false"`: the attribute only ever cancels the table-edge affordance, it
|
|
6
|
+
* never grants one. A cell that genuinely sits at the edge carries no attribute at all, so the
|
|
7
|
+
* class list of every cell in a table without `rowSpan` stays untouched.
|
|
8
|
+
*
|
|
9
|
+
* The DataTable cell recipes match this attribute, and the name has to be written there as a
|
|
10
|
+
* literal because PandaCSS extracts selector keys statically and cannot resolve an imported
|
|
11
|
+
* constant. `useTableEdgeCellMarkers.test.tsx` asserts that the two sides stay in sync.
|
|
12
|
+
*/
|
|
13
|
+
export declare const TABLE_START_EDGE_ATTRIBUTE = "data-mfui-table-start-edge";
|
|
14
|
+
/**
|
|
15
|
+
* The `:last-child` mirror of {@link TABLE_START_EDGE_ATTRIBUTE}: the cell is last in its row but
|
|
16
|
+
* the table has further grid columns that a `rowSpan` from an earlier row occupies.
|
|
17
|
+
*/
|
|
18
|
+
export declare const TABLE_END_EDGE_ATTRIBUTE = "data-mfui-table-end-edge";
|
|
19
|
+
/** The only value either marker attribute is ever set to. */
|
|
20
|
+
export declare const TABLE_EDGE_MARKER_VALUE = "false";
|
|
21
|
+
type TableEdgeAttribute = typeof TABLE_START_EDGE_ATTRIBUTE | typeof TABLE_END_EDGE_ATTRIBUTE;
|
|
22
|
+
type DisplacedEdgeCell = {
|
|
23
|
+
cell: HTMLTableCellElement;
|
|
24
|
+
attribute: TableEdgeAttribute;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Finds the cells whose position in source order disagrees with their rendered grid column.
|
|
28
|
+
*
|
|
29
|
+
* Each row group is walked into an occupancy grid, so that every cell gets its real `colStart` /
|
|
30
|
+
* `colEnd` rather than its `cellIndex`. A row's first cell is displaced when it starts after
|
|
31
|
+
* column 0, and its last cell is displaced when it ends before the table's last column. The
|
|
32
|
+
* table's column count is the widest row across all row groups, since the header and the body
|
|
33
|
+
* share the same columns.
|
|
34
|
+
*
|
|
35
|
+
* @param table - The `table` element to analyse.
|
|
36
|
+
*
|
|
37
|
+
* @returns One entry per displaced cell edge, in document order.
|
|
38
|
+
*/
|
|
39
|
+
export declare const findDisplacedEdgeCells: (table: HTMLTableElement) => DisplacedEdgeCell[];
|
|
40
|
+
/**
|
|
41
|
+
* Marks the cells of a table whose source-order position disagrees with their rendered grid
|
|
42
|
+
* column, so that CSS can cancel the table-edge spacing `:first-child` / `:last-child` would
|
|
43
|
+
* otherwise give them.
|
|
44
|
+
*
|
|
45
|
+
* Markers are recomputed on mount, on every structural mutation inside the table, and cleared on
|
|
46
|
+
* unmount. They are deliberately not tied to any component re-rendering: memoized rows,
|
|
47
|
+
* key-recreated cells, virtualization and consumer state owned below the table all still produce
|
|
48
|
+
* correct markers, because each of them necessarily mutates the table DOM.
|
|
49
|
+
*
|
|
50
|
+
* Server-rendered HTML carries no markers, so a table using `rowSpan` keeps the misaligned
|
|
51
|
+
* spacing until hydration.
|
|
52
|
+
*
|
|
53
|
+
* @param tableRef - A ref to the `table` element to keep marked.
|
|
54
|
+
*/
|
|
55
|
+
export declare const useTableEdgeCellMarkers: (tableRef: React.RefObject<HTMLTableElement | null>) => void;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import { useIsomorphicLayoutEffect } from '../react/useIsomorphicLayoutEffect';
|
|
3
|
+
/**
|
|
4
|
+
* Marks a cell that is `:first-child` in its row but does **not** render in the table's first
|
|
5
|
+
* grid column, because a `rowSpan` from an earlier row occupies that column.
|
|
6
|
+
*
|
|
7
|
+
* The value is always `"false"`: the attribute only ever cancels the table-edge affordance, it
|
|
8
|
+
* never grants one. A cell that genuinely sits at the edge carries no attribute at all, so the
|
|
9
|
+
* class list of every cell in a table without `rowSpan` stays untouched.
|
|
10
|
+
*
|
|
11
|
+
* The DataTable cell recipes match this attribute, and the name has to be written there as a
|
|
12
|
+
* literal because PandaCSS extracts selector keys statically and cannot resolve an imported
|
|
13
|
+
* constant. `useTableEdgeCellMarkers.test.tsx` asserts that the two sides stay in sync.
|
|
14
|
+
*/
|
|
15
|
+
export const TABLE_START_EDGE_ATTRIBUTE = 'data-mfui-table-start-edge';
|
|
16
|
+
/**
|
|
17
|
+
* The `:last-child` mirror of {@link TABLE_START_EDGE_ATTRIBUTE}: the cell is last in its row but
|
|
18
|
+
* the table has further grid columns that a `rowSpan` from an earlier row occupies.
|
|
19
|
+
*/
|
|
20
|
+
export const TABLE_END_EDGE_ATTRIBUTE = 'data-mfui-table-end-edge';
|
|
21
|
+
/** The only value either marker attribute is ever set to. */
|
|
22
|
+
export const TABLE_EDGE_MARKER_VALUE = 'false';
|
|
23
|
+
/** Mutation targets that can change the grid. Anything inside a cell cannot. */
|
|
24
|
+
const STRUCTURAL_TAG_NAMES = new Set(['TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR']);
|
|
25
|
+
/**
|
|
26
|
+
* The row groups of a table, each as its own list of rows.
|
|
27
|
+
*
|
|
28
|
+
* A `rowspan` is clamped to its own row group, so the occupancy grid is built per group. These
|
|
29
|
+
* collections only contain the table's own rows, which is what keeps a nested table invisible
|
|
30
|
+
* here — unlike `querySelectorAll('tr')`.
|
|
31
|
+
*
|
|
32
|
+
* @param table - The `table` element to read the row groups of.
|
|
33
|
+
*
|
|
34
|
+
* @returns One list of rows per row group, in document order.
|
|
35
|
+
*/
|
|
36
|
+
const getSectionRows = (table) => {
|
|
37
|
+
const sections = [table.tHead, ...table.tBodies, table.tFoot].filter((section) => section != null);
|
|
38
|
+
if (sections.length > 0) {
|
|
39
|
+
return sections.map((section) => [...section.rows]);
|
|
40
|
+
}
|
|
41
|
+
// A `tr` written directly under `table` has no row group to belong to.
|
|
42
|
+
return table.rows.length > 0 ? [[...table.rows]] : [];
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Resolves the grid columns of one row group by walking it into an occupancy grid, so that the
|
|
46
|
+
* edge cells of every row are described by where they render rather than by their `cellIndex`.
|
|
47
|
+
*
|
|
48
|
+
* @param rows - The rows of a single row group, in document order.
|
|
49
|
+
*
|
|
50
|
+
* @returns The edge cells and their grid columns, one entry per non-empty row.
|
|
51
|
+
*/
|
|
52
|
+
const collectRowEdges = (rows) => {
|
|
53
|
+
// `occupied[rowIndex][column]` is true when a `rowSpan` from an earlier row reaches into it.
|
|
54
|
+
const occupied = rows.map(() => []);
|
|
55
|
+
const rowEdges = [];
|
|
56
|
+
rows.forEach((row, rowIndex) => {
|
|
57
|
+
const cells = [...row.cells];
|
|
58
|
+
const [firstCell] = cells;
|
|
59
|
+
const lastCell = cells.at(-1);
|
|
60
|
+
if (!firstCell || !lastCell)
|
|
61
|
+
return;
|
|
62
|
+
let column = 0;
|
|
63
|
+
let firstCellStart = 0;
|
|
64
|
+
let lastCellEnd = 0;
|
|
65
|
+
cells.forEach((cell, cellIndex) => {
|
|
66
|
+
while (occupied[rowIndex]?.[column] === true) {
|
|
67
|
+
column += 1;
|
|
68
|
+
}
|
|
69
|
+
const colSpan = Math.max(cell.colSpan, 1);
|
|
70
|
+
// `rowSpan={0}` means "every remaining row of this row group".
|
|
71
|
+
const rowSpan = cell.rowSpan === 0 ? rows.length - rowIndex : Math.max(cell.rowSpan, 1);
|
|
72
|
+
const columnEnd = column + colSpan;
|
|
73
|
+
const lastCoveredRow = Math.min(rowIndex + rowSpan, rows.length);
|
|
74
|
+
for (let coveredRow = rowIndex + 1; coveredRow < lastCoveredRow; coveredRow += 1) {
|
|
75
|
+
const occupiedColumns = occupied[coveredRow] ?? [];
|
|
76
|
+
for (let coveredColumn = column; coveredColumn < columnEnd; coveredColumn += 1) {
|
|
77
|
+
occupiedColumns[coveredColumn] = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (cellIndex === 0) {
|
|
81
|
+
firstCellStart = column;
|
|
82
|
+
}
|
|
83
|
+
lastCellEnd = columnEnd;
|
|
84
|
+
column = columnEnd;
|
|
85
|
+
});
|
|
86
|
+
rowEdges.push({ firstCell, lastCell, start: firstCellStart, end: lastCellEnd });
|
|
87
|
+
});
|
|
88
|
+
return rowEdges;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Finds the cells whose position in source order disagrees with their rendered grid column.
|
|
92
|
+
*
|
|
93
|
+
* Each row group is walked into an occupancy grid, so that every cell gets its real `colStart` /
|
|
94
|
+
* `colEnd` rather than its `cellIndex`. A row's first cell is displaced when it starts after
|
|
95
|
+
* column 0, and its last cell is displaced when it ends before the table's last column. The
|
|
96
|
+
* table's column count is the widest row across all row groups, since the header and the body
|
|
97
|
+
* share the same columns.
|
|
98
|
+
*
|
|
99
|
+
* @param table - The `table` element to analyse.
|
|
100
|
+
*
|
|
101
|
+
* @returns One entry per displaced cell edge, in document order.
|
|
102
|
+
*/
|
|
103
|
+
export const findDisplacedEdgeCells = (table) => {
|
|
104
|
+
const rowEdges = getSectionRows(table).flatMap(collectRowEdges);
|
|
105
|
+
const totalColumns = rowEdges.reduce((widest, { end }) => Math.max(widest, end), 0);
|
|
106
|
+
const displacedCells = [];
|
|
107
|
+
for (const { firstCell, lastCell, start, end } of rowEdges) {
|
|
108
|
+
if (start > 0) {
|
|
109
|
+
displacedCells.push({ cell: firstCell, attribute: TABLE_START_EDGE_ATTRIBUTE });
|
|
110
|
+
}
|
|
111
|
+
if (end < totalColumns) {
|
|
112
|
+
displacedCells.push({ cell: lastCell, attribute: TABLE_END_EDGE_ATTRIBUTE });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return displacedCells;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Whether any cell of this table carries a `rowspan` attribute.
|
|
119
|
+
*
|
|
120
|
+
* `colspan` is deliberately not probed. It is not that a `rowSpan` is the only way to displace a
|
|
121
|
+
* cell — a ragged table, whose rows disagree on total width, displaces its short rows' edge cells
|
|
122
|
+
* without any `rowSpan`, and {@link findDisplacedEdgeCells} resolves those correctly. Gating on
|
|
123
|
+
* `rowspan` is a scope decision: it is what guarantees that every table without one renders
|
|
124
|
+
* byte-identically to before this hook existed. Widen the gate if a ragged table is ever reported.
|
|
125
|
+
*
|
|
126
|
+
* @param table - The `table` element to probe.
|
|
127
|
+
*
|
|
128
|
+
* @returns Whether the table itself contains a `rowspan`.
|
|
129
|
+
*/
|
|
130
|
+
const hasRowSpanAttribute = (table) => {
|
|
131
|
+
// The subtree query is asked first because it settles the common case — a table with no
|
|
132
|
+
// `rowSpan` anywhere, which is most of them — in a single pass. The answer is only ambiguous
|
|
133
|
+
// when it finds something, since the match may belong to a nested table.
|
|
134
|
+
if (table.querySelector('[rowspan]') === null)
|
|
135
|
+
return false;
|
|
136
|
+
// Without a nested table the query above cannot have reached outside this table, so it was
|
|
137
|
+
// already correctly scoped. Only a nested table forces the explicit walk over our own rows.
|
|
138
|
+
if (table.querySelector('table') === null)
|
|
139
|
+
return true;
|
|
140
|
+
return getSectionRows(table).some((rows) => rows.some((row) => [...row.cells].some((cell) => cell.hasAttribute('rowspan'))));
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Whether a mutation record can have changed the grid of this table.
|
|
144
|
+
*
|
|
145
|
+
* @param record - The record to classify.
|
|
146
|
+
* @param table - The `table` element being observed.
|
|
147
|
+
*
|
|
148
|
+
* @returns Whether the markers have to be recomputed because of this record.
|
|
149
|
+
*/
|
|
150
|
+
const isGridMutation = (record, table) => {
|
|
151
|
+
const { target } = record;
|
|
152
|
+
if (!(target instanceof Element))
|
|
153
|
+
return false;
|
|
154
|
+
// A nested table's mutations cannot change this table's grid.
|
|
155
|
+
if (target.closest('table') !== table)
|
|
156
|
+
return false;
|
|
157
|
+
// An attribute record targets the cell whose `rowspan` / `colspan` changed.
|
|
158
|
+
if (record.type === 'attributes')
|
|
159
|
+
return true;
|
|
160
|
+
// Rows and cells are added, removed and reordered on a row, a row group or the table itself.
|
|
161
|
+
// A `childList` record on a cell is content churn — a skeleton swap, a checkbox toggle, an
|
|
162
|
+
// inline edit — and cannot move any cell to a different column.
|
|
163
|
+
return STRUCTURAL_TAG_NAMES.has(target.tagName);
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* Marks the cells of a table whose source-order position disagrees with their rendered grid
|
|
167
|
+
* column, so that CSS can cancel the table-edge spacing `:first-child` / `:last-child` would
|
|
168
|
+
* otherwise give them.
|
|
169
|
+
*
|
|
170
|
+
* Markers are recomputed on mount, on every structural mutation inside the table, and cleared on
|
|
171
|
+
* unmount. They are deliberately not tied to any component re-rendering: memoized rows,
|
|
172
|
+
* key-recreated cells, virtualization and consumer state owned below the table all still produce
|
|
173
|
+
* correct markers, because each of them necessarily mutates the table DOM.
|
|
174
|
+
*
|
|
175
|
+
* Server-rendered HTML carries no markers, so a table using `rowSpan` keeps the misaligned
|
|
176
|
+
* spacing until hydration.
|
|
177
|
+
*
|
|
178
|
+
* @param tableRef - A ref to the `table` element to keep marked.
|
|
179
|
+
*/
|
|
180
|
+
export const useTableEdgeCellMarkers = (tableRef) => {
|
|
181
|
+
const markedCellsRef = useRef([]);
|
|
182
|
+
useIsomorphicLayoutEffect(() => {
|
|
183
|
+
const table = tableRef.current;
|
|
184
|
+
if (!table)
|
|
185
|
+
return;
|
|
186
|
+
const clearMarkers = () => {
|
|
187
|
+
markedCellsRef.current.forEach(({ cell, attribute }) => {
|
|
188
|
+
cell.removeAttribute(attribute);
|
|
189
|
+
});
|
|
190
|
+
markedCellsRef.current = [];
|
|
191
|
+
};
|
|
192
|
+
const updateMarkers = () => {
|
|
193
|
+
// Recompute when the table has a `rowSpan`, or when markers are still set from an earlier
|
|
194
|
+
// run — the second half is what clears them once the last `rowSpan` is removed.
|
|
195
|
+
if (markedCellsRef.current.length === 0 && !hasRowSpanAttribute(table))
|
|
196
|
+
return;
|
|
197
|
+
clearMarkers();
|
|
198
|
+
const displacedCells = findDisplacedEdgeCells(table);
|
|
199
|
+
displacedCells.forEach(({ cell, attribute }) => {
|
|
200
|
+
cell.setAttribute(attribute, TABLE_EDGE_MARKER_VALUE);
|
|
201
|
+
});
|
|
202
|
+
markedCellsRef.current = displacedCells;
|
|
203
|
+
};
|
|
204
|
+
updateMarkers();
|
|
205
|
+
const observer = new MutationObserver((records) => {
|
|
206
|
+
if (!records.some((record) => isGridMutation(record, table)))
|
|
207
|
+
return;
|
|
208
|
+
updateMarkers();
|
|
209
|
+
});
|
|
210
|
+
observer.observe(table, {
|
|
211
|
+
childList: true,
|
|
212
|
+
subtree: true,
|
|
213
|
+
attributes: true,
|
|
214
|
+
// Our own `data-mfui-*` writes are excluded by the filter, so the observer cannot
|
|
215
|
+
// retrigger itself.
|
|
216
|
+
attributeFilter: ['rowspan', 'colspan'],
|
|
217
|
+
});
|
|
218
|
+
return () => {
|
|
219
|
+
observer.disconnect();
|
|
220
|
+
clearMarkers();
|
|
221
|
+
};
|
|
222
|
+
}, [tableRef]);
|
|
223
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flattens React children by expanding Fragments so that elements wrapped in a
|
|
3
|
+
* Fragment (e.g. `<>{items.map(...)}</>`) become directly visible to
|
|
4
|
+
* `Children`-based inspection such as `isComponentOrWrapped`.
|
|
5
|
+
*
|
|
6
|
+
* The keys assigned by `Children.toArray` are combined with the keys of the
|
|
7
|
+
* enclosing Fragments so that they stay unique after flattening.
|
|
8
|
+
*
|
|
9
|
+
* @param children - The React children to flatten
|
|
10
|
+
*
|
|
11
|
+
* @param parentKeys - The accumulated keys of the enclosing Fragments (internal use for recursion)
|
|
12
|
+
*
|
|
13
|
+
* @returns The flattened children with Fragments expanded
|
|
14
|
+
*/
|
|
15
|
+
export declare function flattenChildren(children: React.ReactNode, parentKeys?: readonly (string | number)[]): React.ReactNode[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Children, Fragment, cloneElement, isValidElement } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Flattens React children by expanding Fragments so that elements wrapped in a
|
|
4
|
+
* Fragment (e.g. `<>{items.map(...)}</>`) become directly visible to
|
|
5
|
+
* `Children`-based inspection such as `isComponentOrWrapped`.
|
|
6
|
+
*
|
|
7
|
+
* The keys assigned by `Children.toArray` are combined with the keys of the
|
|
8
|
+
* enclosing Fragments so that they stay unique after flattening.
|
|
9
|
+
*
|
|
10
|
+
* @param children - The React children to flatten
|
|
11
|
+
*
|
|
12
|
+
* @param parentKeys - The accumulated keys of the enclosing Fragments (internal use for recursion)
|
|
13
|
+
*
|
|
14
|
+
* @returns The flattened children with Fragments expanded
|
|
15
|
+
*/
|
|
16
|
+
export function flattenChildren(children, parentKeys = []) {
|
|
17
|
+
return Children.toArray(children).flatMap((child, index) => {
|
|
18
|
+
if (isValidElement(child) && child.type === Fragment) {
|
|
19
|
+
return flattenChildren(child.props.children, [...parentKeys, child.key ?? index]);
|
|
20
|
+
}
|
|
21
|
+
if (isValidElement(child) && parentKeys.length > 0) {
|
|
22
|
+
// ':' cannot appear in the combined segments (React escapes it in user keys), so it
|
|
23
|
+
// keeps structurally different key paths from composing into the same string.
|
|
24
|
+
return cloneElement(child, { key: [...parentKeys, child.key].join(':') });
|
|
25
|
+
}
|
|
26
|
+
return child;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @param value - Any value that you want to track
|
|
7
7
|
*
|
|
8
|
-
* @returns The React's RefObject of that value, which
|
|
8
|
+
* @returns The React's RefObject of that value, which holds the latest committed value.
|
|
9
9
|
*
|
|
10
10
|
* @see [React Hooks FAQ](https://legacy.reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function)
|
|
11
11
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useRef } from 'react';
|
|
2
|
+
import { useIsomorphicLayoutEffect } from '../react/useIsomorphicLayoutEffect';
|
|
2
3
|
/**
|
|
3
4
|
* React state hook that returns the latest state as described in the React hooks FAQ.
|
|
4
5
|
*
|
|
@@ -6,7 +7,7 @@ import { useRef } from 'react';
|
|
|
6
7
|
*
|
|
7
8
|
* @param value - Any value that you want to track
|
|
8
9
|
*
|
|
9
|
-
* @returns The React's RefObject of that value, which
|
|
10
|
+
* @returns The React's RefObject of that value, which holds the latest committed value.
|
|
10
11
|
*
|
|
11
12
|
* @see [React Hooks FAQ](https://legacy.reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function)
|
|
12
13
|
*
|
|
@@ -14,6 +15,11 @@ import { useRef } from 'react';
|
|
|
14
15
|
*/
|
|
15
16
|
export const useLatest = (value) => {
|
|
16
17
|
const ref = useRef(value);
|
|
17
|
-
|
|
18
|
+
// Committed in a layout effect rather than assigned during render (React's rules
|
|
19
|
+
// disallow render-time mutation). Callers only read `.current` from callbacks and
|
|
20
|
+
// effects, which all run after this commits.
|
|
21
|
+
useIsomorphicLayoutEffect(() => {
|
|
22
|
+
ref.current = value;
|
|
23
|
+
});
|
|
18
24
|
return ref;
|
|
19
25
|
};
|
|
@@ -10,7 +10,7 @@ type BadgeSlotRecipeVariantMap = {
|
|
|
10
10
|
[key in keyof BadgeSlotRecipeVariant]: Array<BadgeSlotRecipeVariant[key]>
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
type BadgeSlotRecipeSlot = "root" | "indicator"
|
|
13
|
+
type BadgeSlotRecipeSlot = "root" | "indicator" | "srOnlyLabel"
|
|
14
14
|
|
|
15
15
|
export type BadgeSlotRecipeVariantProps = {
|
|
16
16
|
[key in keyof BadgeSlotRecipeVariant]?: ConditionalValue<BadgeSlotRecipeVariant[key]> | undefined
|
|
@@ -10,6 +10,10 @@ const badgeSlotRecipeSlotNames = [
|
|
|
10
10
|
[
|
|
11
11
|
"indicator",
|
|
12
12
|
"Badge__indicator"
|
|
13
|
+
],
|
|
14
|
+
[
|
|
15
|
+
"srOnlyLabel",
|
|
16
|
+
"Badge__srOnlyLabel"
|
|
13
17
|
]
|
|
14
18
|
];
|
|
15
19
|
const badgeSlotRecipeSlotFns = /* @__PURE__ */ badgeSlotRecipeSlotNames.map(([slotName, slotKey]) => [slotName, createRecipe(slotKey, badgeSlotRecipeDefaultVariants, getSlotCompoundVariant(badgeSlotRecipeCompoundVariants, slotName))]);
|
|
@@ -19,7 +19,7 @@ type DataTableCellSlotRecipeVariantMap = {
|
|
|
19
19
|
[key in keyof DataTableCellSlotRecipeVariant]: Array<DataTableCellSlotRecipeVariant[key]>
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
type DataTableCellSlotRecipeSlot = "root" | "defaultCell" | "interactiveCell" | "disclosureIconWrapper" | "disclosureSpacing" | "skeletonCell"
|
|
22
|
+
type DataTableCellSlotRecipeSlot = "root" | "defaultCell" | "interactiveCell" | "disclosureIconWrapper" | "disclosureSpacing" | "skeletonCell" | "rowActionButton"
|
|
23
23
|
|
|
24
24
|
export type DataTableCellSlotRecipeVariantProps = {
|
|
25
25
|
[key in keyof DataTableCellSlotRecipeVariant]?: DataTableCellSlotRecipeVariant[key] | undefined
|
|
@@ -35,8 +35,10 @@ const dataTableCellSlotRecipeCompoundVariants = [
|
|
|
35
35
|
"layout": "default",
|
|
36
36
|
"css": {
|
|
37
37
|
"root": {
|
|
38
|
-
"tr:last-child >
|
|
39
|
-
"
|
|
38
|
+
"tr:last-child > &": {
|
|
39
|
+
"_before": {
|
|
40
|
+
"height": "[100%]"
|
|
41
|
+
}
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
44
|
}
|
|
@@ -66,6 +68,10 @@ const dataTableCellSlotRecipeSlotNames = [
|
|
|
66
68
|
[
|
|
67
69
|
"skeletonCell",
|
|
68
70
|
"DataTableCell__skeletonCell"
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
"rowActionButton",
|
|
74
|
+
"DataTableCell__rowActionButton"
|
|
69
75
|
]
|
|
70
76
|
];
|
|
71
77
|
const dataTableCellSlotRecipeSlotFns = /* @__PURE__ */ dataTableCellSlotRecipeSlotNames.map(([slotName, slotKey]) => [slotName, createRecipe(slotKey, dataTableCellSlotRecipeDefaultVariants, getSlotCompoundVariant(dataTableCellSlotRecipeCompoundVariants, slotName))]);
|
|
@@ -13,12 +13,8 @@ const dataTableRowSlotRecipeCompoundVariants = [
|
|
|
13
13
|
"backgroundColor": "mfui.color.selected.background.hovered"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
|
-
"
|
|
17
|
-
"backgroundColor": "mfui.color.selected.background.hovered"
|
|
18
|
-
"boxShadow": "[0px 0px 0px 2px {colors.mfui.color.primary.border.none} inset, 0px 0px 0px 3px {colors.mfui.color.base.background.none} inset]"
|
|
19
|
-
},
|
|
20
|
-
"_focusInputInside": {
|
|
21
|
-
"boxShadow": "[none]"
|
|
16
|
+
"&:has([data-mfui-content=\"data-table-row-action-button\"]:focus-visible)": {
|
|
17
|
+
"backgroundColor": "mfui.color.selected.background.hovered"
|
|
22
18
|
},
|
|
23
19
|
"_active": {
|
|
24
20
|
"&:not(:has(button:hover, input:hover, select:hover, textarea:hover, [tabindex]:hover, [data-mfui-content=\"data-table-checkbox-cell\"]:hover))": {
|
|
@@ -27,6 +23,17 @@ const dataTableRowSlotRecipeCompoundVariants = [
|
|
|
27
23
|
}
|
|
28
24
|
}
|
|
29
25
|
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"highlighted": true,
|
|
29
|
+
"rowClickSelectable": true,
|
|
30
|
+
"css": {
|
|
31
|
+
"root": {
|
|
32
|
+
"&:has(.mfui-DataTableRow__selectionCheckbox input[type=\"checkbox\"]:focus-visible)": {
|
|
33
|
+
"backgroundColor": "mfui.color.selected.background.hovered"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
30
37
|
}
|
|
31
38
|
];
|
|
32
39
|
const dataTableRowSlotRecipeSlotNames = [
|
|
@@ -11,7 +11,7 @@ type SubNavigationSlotRecipeVariantMap = {
|
|
|
11
11
|
[key in keyof SubNavigationSlotRecipeVariant]: Array<SubNavigationSlotRecipeVariant[key]>
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
type SubNavigationSlotRecipeSlot = "root" | "subNavigationHeader" | "list" | "listItem" | "link" | "labelIcon" | "label" | "statusSlot" | "details" | "summary" | "disclosureIcon" | "nestedListItem"
|
|
14
|
+
type SubNavigationSlotRecipeSlot = "root" | "subNavigationHeader" | "list" | "listItem" | "link" | "labelContainer" | "labelIcon" | "labelContent" | "label" | "description" | "statusSlot" | "details" | "summary" | "disclosureIcon" | "nestedListItem"
|
|
15
15
|
|
|
16
16
|
export type SubNavigationSlotRecipeVariantProps = {
|
|
17
17
|
[key in keyof SubNavigationSlotRecipeVariant]?: ConditionalValue<SubNavigationSlotRecipeVariant[key]> | undefined
|
|
@@ -23,14 +23,26 @@ const subNavigationSlotRecipeSlotNames = [
|
|
|
23
23
|
"link",
|
|
24
24
|
"SubNavigation__link"
|
|
25
25
|
],
|
|
26
|
+
[
|
|
27
|
+
"labelContainer",
|
|
28
|
+
"SubNavigation__labelContainer"
|
|
29
|
+
],
|
|
26
30
|
[
|
|
27
31
|
"labelIcon",
|
|
28
32
|
"SubNavigation__labelIcon"
|
|
29
33
|
],
|
|
34
|
+
[
|
|
35
|
+
"labelContent",
|
|
36
|
+
"SubNavigation__labelContent"
|
|
37
|
+
],
|
|
30
38
|
[
|
|
31
39
|
"label",
|
|
32
40
|
"SubNavigation__label"
|
|
33
41
|
],
|
|
42
|
+
[
|
|
43
|
+
"description",
|
|
44
|
+
"SubNavigation__description"
|
|
45
|
+
],
|
|
34
46
|
[
|
|
35
47
|
"statusSlot",
|
|
36
48
|
"SubNavigation__statusSlot"
|