@moneyforward/mfui-components 4.3.0 → 4.5.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/Breadcrumbs/Breadcrumbs.js +1 -1
- package/dist/src/CheckboxCard/CheckboxCard.js +3 -1
- package/dist/src/DataTable/DataTable.js +9 -2
- package/dist/src/DataTable/DataTable.types.d.ts +15 -0
- package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.js +3 -1
- package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.types.d.ts +19 -1
- package/dist/src/DataTable/DataTableRow/DataTableRow.js +4 -1
- package/dist/src/DataTable/DataTableRow/DataTableRow.types.d.ts +23 -1
- 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/shared/CalendarGrid/CalendarGrid.js +1 -3
- package/dist/src/DateTimeSelection/shared/CalendarGrid/utilities/computeDataForStyling.js +2 -4
- package/dist/src/Dialog/Dialog.d.ts +1 -1
- package/dist/src/Dialog/Dialog.types.d.ts +1 -1
- package/dist/src/FileDropZone/FileDropZone.d.ts +5 -1
- package/dist/src/FileDropZone/FileDropZone.js +26 -6
- package/dist/src/FileDropZone/FileDropZone.types.d.ts +10 -0
- package/dist/src/MainNavigation/BaseMainNavigation.d.ts +1 -1
- package/dist/src/MainNavigation/BaseMainNavigation.js +2 -2
- package/dist/src/MainNavigation/MainNavigation.js +2 -1
- package/dist/src/MainNavigation/MainNavigation.types.d.ts +9 -0
- package/dist/src/MainNavigation/NarrowViewportMainNavigation.js +2 -2
- package/dist/src/MultipleSelectBox/MultipleSelectBox.js +27 -18
- package/dist/src/MultipleSelectBox/hooks/useInitialFocusOnOptionPanelOpen.js +1 -1
- 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 +55 -3
- package/dist/src/NumberBox/NumberBox.types.d.ts +35 -0
- package/dist/src/Pagination/Pagination.js +4 -1
- package/dist/src/Popover/Popover.js +3 -3
- package/dist/src/RadioButtonCard/RadioButtonCard.js +3 -1
- package/dist/src/SelectBox/SelectBox.js +20 -11
- 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/SidePane/hooks/useSidePaneStateController.js +3 -5
- package/dist/src/SplitView/utils/parseSize.d.ts +1 -1
- package/dist/src/SplitView/utils/parseSize.js +4 -2
- package/dist/src/SubNavigation/SubNavigation.d.ts +10 -1
- package/dist/src/SubNavigation/SubNavigation.js +14 -3
- package/dist/src/SubNavigation/SubNavigation.types.d.ts +27 -1
- package/dist/src/Tabs/TabContext.d.ts +3 -1
- package/dist/src/Tabs/TabContext.js +1 -0
- package/dist/src/Tabs/TabItem/TabItem.js +14 -3
- package/dist/src/Tabs/Tabs.js +5 -21
- package/dist/src/Tabs/hooks/useTabRegistry.d.ts +24 -0
- package/dist/src/Tabs/hooks/useTabRegistry.js +130 -0
- package/dist/src/ToggleSwitch/index.d.ts +1 -1
- package/dist/src/ToggleSwitch/index.js +0 -1
- package/dist/src/Tooltip/hooks/useOpenStateDelayed.js +2 -2
- package/dist/src/utilities/dom/containsAcrossShadowRoots.d.ts +21 -0
- package/dist/src/utilities/dom/containsAcrossShadowRoots.js +33 -0
- package/dist/src/utilities/dom/setInputFiles.d.ts +21 -0
- package/dist/src/utilities/dom/setInputFiles.js +41 -0
- package/dist/src/utilities/dom/toFileList.d.ts +20 -0
- package/dist/src/utilities/dom/toFileList.js +26 -0
- package/dist/src/utilities/dom/useFixedColumns.js +4 -2
- package/dist/src/utilities/dom/useFocusTrap.js +1 -1
- package/dist/src/utilities/dom/useOnClickOutside.js +3 -1
- package/dist/src/utilities/dom/useTableEdgeCellMarkers.d.ts +56 -0
- package/dist/src/utilities/dom/useTableEdgeCellMarkers.js +223 -0
- package/dist/styled-system/recipes/data-table-cell-slot-recipe.js +4 -2
- package/dist/styled-system/types/composition.d.ts +1 -1
- package/dist/styles.css +20 -4
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -3
- /package/dist/src/{MultipleSelectBox/utils → utilities/dom}/getDeepActiveElement.d.ts +0 -0
- /package/dist/src/{MultipleSelectBox/utils → utilities/dom}/getDeepActiveElement.js +0 -0
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { type SubNavigationProps } from './SubNavigation.types';
|
|
2
2
|
/**
|
|
3
3
|
* This component is for the navigation links under the main navigation.
|
|
4
|
+
*
|
|
5
|
+
* The root element is a `<nav>` landmark. Name it with `aria-label` or `aria-labelledby` when the
|
|
6
|
+
* page has other navigation landmarks. No name is applied by default because the right name
|
|
7
|
+
* depends on what the navigation is for on each page.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <SubNavigation navigationItems={items} aria-label="設定" />
|
|
12
|
+
* ```
|
|
4
13
|
*/
|
|
5
|
-
export declare
|
|
14
|
+
export declare const SubNavigation: import("react").ForwardRefExoticComponent<SubNavigationProps & import("react").RefAttributes<HTMLElement>>;
|
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { DisclosureBasicCollapsed, Lock } from '@moneyforward/mfui-icons-react';
|
|
3
|
+
import { forwardRef } from 'react';
|
|
3
4
|
import { cx } from '../../styled-system/css';
|
|
4
5
|
import { subNavigationSlotRecipe } from '../../styled-system/recipes';
|
|
5
6
|
import { FocusIndicator } from '../FocusIndicator';
|
|
6
7
|
import { Typography } from '../Typography';
|
|
7
8
|
/**
|
|
8
9
|
* This component is for the navigation links under the main navigation.
|
|
10
|
+
*
|
|
11
|
+
* The root element is a `<nav>` landmark. Name it with `aria-label` or `aria-labelledby` when the
|
|
12
|
+
* page has other navigation landmarks. No name is applied by default because the right name
|
|
13
|
+
* depends on what the navigation is for on each page.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <SubNavigation navigationItems={items} aria-label="設定" />
|
|
18
|
+
* ```
|
|
9
19
|
*/
|
|
10
|
-
export
|
|
20
|
+
export const SubNavigation = forwardRef(({ orientation = 'vertical', navigationItems, customLinkComponent, renderNavigationLabel, headerSlot, className, ...rest }, ref) => {
|
|
11
21
|
const hasLabelIcon = navigationItems.some(({ labelIcon }) => !!labelIcon);
|
|
12
22
|
const classes = subNavigationSlotRecipe({ orientation, hasLabelIcon });
|
|
13
|
-
return (_jsxs("nav", { className: cx(classes.root, 'mfui-SubNavigation__root', className), children: [headerSlot ? (_jsx("div", { className: cx(classes.subNavigationHeader, 'mfui-SubNavigation__subNavigationHeader'), children: headerSlot })) : null, _jsx("ul", { className: cx(classes.list, 'mfui-SubNavigation__list'), children: navigationItems.map((navigationItem, index) => (_jsx("li", { className: cx(classes.listItem, 'mfui-SubNavigation__listItem'), children: renderNavigationItem(navigationItem, customLinkComponent, classes, 0, renderNavigationLabel) }, index))) })] }));
|
|
14
|
-
}
|
|
23
|
+
return (_jsxs("nav", { ...rest, ref: ref, className: cx(classes.root, 'mfui-SubNavigation__root', className), children: [headerSlot ? (_jsx("div", { className: cx(classes.subNavigationHeader, 'mfui-SubNavigation__subNavigationHeader'), children: headerSlot })) : null, _jsx("ul", { className: cx(classes.list, 'mfui-SubNavigation__list'), children: navigationItems.map((navigationItem, index) => (_jsx("li", { className: cx(classes.listItem, 'mfui-SubNavigation__listItem'), children: renderNavigationItem(navigationItem, customLinkComponent, classes, 0, renderNavigationLabel) }, index))) })] }));
|
|
24
|
+
});
|
|
25
|
+
SubNavigation.displayName = 'SubNavigation';
|
|
15
26
|
/**
|
|
16
27
|
* Render a navigation item based on the following conditions:
|
|
17
28
|
* - a link without children
|
|
@@ -33,6 +33,32 @@ type HorizontalSubNavigationProps = BaseSubNavigationProps & {
|
|
|
33
33
|
* Common properties for both vertical and horizontal SubNavigation.
|
|
34
34
|
*/
|
|
35
35
|
type BaseSubNavigationProps = {
|
|
36
|
+
/**
|
|
37
|
+
* The accessible name of the `<nav>` landmark.
|
|
38
|
+
*
|
|
39
|
+
* Set this when the page has other navigation landmarks (e.g. `MainNavigation`, `Breadcrumbs`).
|
|
40
|
+
* Describe this page's navigation (e.g. `"設定"`). Avoid including the role name;
|
|
41
|
+
* assistive technology already announces it.
|
|
42
|
+
*/
|
|
43
|
+
'aria-label'?: string;
|
|
44
|
+
/**
|
|
45
|
+
* The ID of the element that names the `<nav>` landmark.
|
|
46
|
+
* Use this instead of `aria-label` when a visible heading already names the navigation.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* function SettingsNavigation() {
|
|
51
|
+
* const headingId = useId();
|
|
52
|
+
* return (
|
|
53
|
+
* <>
|
|
54
|
+
* <h2 id={headingId}>設定</h2>
|
|
55
|
+
* <SubNavigation navigationItems={items} aria-labelledby={headingId} />
|
|
56
|
+
* </>
|
|
57
|
+
* );
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
'aria-labelledby'?: string;
|
|
36
62
|
/**
|
|
37
63
|
* The component to use as the link element instead of the general anchor element `<a>`.
|
|
38
64
|
* The custom component must extend the HTMLAnchorElement interface.
|
|
@@ -109,7 +135,7 @@ type BaseSubNavigationProps = {
|
|
|
109
135
|
* For overriding styles if you need.
|
|
110
136
|
*/
|
|
111
137
|
className?: string;
|
|
112
|
-
}
|
|
138
|
+
} & Omit<ComponentPropsWithoutRef<'nav'>, 'children' | 'className' | 'aria-label' | 'aria-labelledby'>;
|
|
113
139
|
/**
|
|
114
140
|
* The union that discriminates whether each navigation item has children.
|
|
115
141
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type TabItemProps } from './TabItem';
|
|
2
|
+
import { type TabElementRef } from './hooks/useTabRegistry';
|
|
2
3
|
type TabContextType = {
|
|
3
4
|
selectedTabId: string | undefined;
|
|
4
5
|
onSelectedTabIdChange?: (tabId: string) => void;
|
|
@@ -6,7 +7,8 @@ type TabContextType = {
|
|
|
6
7
|
shouldShowMenu: boolean;
|
|
7
8
|
setShouldShowMenu?: (shouldShowMenu: boolean) => void;
|
|
8
9
|
targetDOMNode?: HTMLElement;
|
|
9
|
-
registerTab?: (tab: TabItemProps) => void;
|
|
10
|
+
registerTab?: (tab: TabItemProps, elementRef: TabElementRef) => void;
|
|
11
|
+
updateTab?: (tab: TabItemProps) => void;
|
|
10
12
|
unregisterTab?: (tabId: string) => void;
|
|
11
13
|
enableAutoUnmount: boolean;
|
|
12
14
|
};
|
|
@@ -10,7 +10,7 @@ import { useArrowKeyHandler } from './hooks/useArrowKeyHandler';
|
|
|
10
10
|
import { Badge } from '../../Badge';
|
|
11
11
|
import { cx } from '../../../styled-system/css';
|
|
12
12
|
export function TabItem({ id, badgeProps, children, className }) {
|
|
13
|
-
const { selectedTabId, onSelectedTabIdChange, tabs, shouldShowMenu, targetDOMNode, registerTab, unregisterTab } = useContext(TabContext);
|
|
13
|
+
const { selectedTabId, onSelectedTabIdChange, tabs, shouldShowMenu, targetDOMNode, registerTab, updateTab, unregisterTab, } = useContext(TabContext);
|
|
14
14
|
const { onKeyDown } = useArrowKeyHandler();
|
|
15
15
|
const buttonRef = useRef(null);
|
|
16
16
|
const labelRef = useRef(null);
|
|
@@ -40,12 +40,23 @@ export function TabItem({ id, badgeProps, children, className }) {
|
|
|
40
40
|
const onClick = useCallback(() => {
|
|
41
41
|
onSelectedTabIdChange?.(id);
|
|
42
42
|
}, [id, onSelectedTabIdChange]);
|
|
43
|
+
// The lifecycle of a registration and the updates to it must stay in two separate effects.
|
|
44
|
+
// Merging them back would unregister and re-register the tab whenever its label or badge
|
|
45
|
+
// changes, which moves it to the end of the registry and gives it the corner radius meant for
|
|
46
|
+
// the last tab. See https://github.com/moneyforward/mfui/issues/2883
|
|
47
|
+
//
|
|
48
|
+
// `badgeProps` and `children` are deliberately absent from the dependencies here: the update
|
|
49
|
+
// effect below carries them, so this effect only runs on mount and unmount.
|
|
43
50
|
useLayoutEffect(() => {
|
|
44
|
-
registerTab?.({ id, badgeProps, children });
|
|
51
|
+
registerTab?.({ id, badgeProps, children }, buttonRef);
|
|
45
52
|
return () => {
|
|
46
53
|
unregisterTab?.(id);
|
|
47
54
|
};
|
|
48
|
-
|
|
55
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
56
|
+
}, [id, registerTab, unregisterTab]);
|
|
57
|
+
useLayoutEffect(() => {
|
|
58
|
+
updateTab?.({ id, badgeProps, children });
|
|
59
|
+
}, [id, badgeProps, children, updateTab]);
|
|
49
60
|
useLayoutEffect(() => {
|
|
50
61
|
if (!labelRef.current) {
|
|
51
62
|
return;
|
package/dist/src/Tabs/Tabs.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { tabsSlotRecipe } from '../../styled-system/recipes';
|
|
5
5
|
import { useCalculatedSelectedTabId } from './hooks/useCalculatedSelectedTabId';
|
|
6
|
+
import { useTabRegistry } from './hooks/useTabRegistry';
|
|
6
7
|
import { TabItem } from './TabItem';
|
|
7
8
|
import { TabList } from './TabList';
|
|
8
9
|
import { TabPanel } from './TabPanel';
|
|
@@ -13,31 +14,12 @@ function Base({ selectedTabId, onSelectedTabChange, targetDOMNode, enableAutoUnm
|
|
|
13
14
|
const classNames = tabsSlotRecipe();
|
|
14
15
|
const [shouldShowMenu, setShouldShowMenu] = useState(false);
|
|
15
16
|
const wrapperRef = useRef(null);
|
|
16
|
-
const
|
|
17
|
+
const { tabs, registerTab, updateTab, unregisterTab } = useTabRegistry();
|
|
17
18
|
const { calculatedSelectedTabId, setCalculatedSelectedTab } = useCalculatedSelectedTabId({
|
|
18
19
|
selectedTabId,
|
|
19
20
|
onSelectedTabChange,
|
|
20
21
|
tabs,
|
|
21
22
|
});
|
|
22
|
-
const registeringTabs = useRef({});
|
|
23
|
-
const { registerTab, unregisterTab } = useMemo(() => {
|
|
24
|
-
const registerTab = (tab) => {
|
|
25
|
-
registeringTabs.current[tab.id] = tab;
|
|
26
|
-
setTabs(Object.values(registeringTabs.current));
|
|
27
|
-
};
|
|
28
|
-
const unregisterTab = (tabId) => {
|
|
29
|
-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
30
|
-
delete registeringTabs.current[tabId];
|
|
31
|
-
setTabs(Object.values(registeringTabs.current));
|
|
32
|
-
};
|
|
33
|
-
return {
|
|
34
|
-
registerTab,
|
|
35
|
-
unregisterTab,
|
|
36
|
-
};
|
|
37
|
-
}, []);
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
setTabs(Object.values(registeringTabs.current));
|
|
40
|
-
}, []);
|
|
41
23
|
const contextValue = useMemo(() => ({
|
|
42
24
|
selectedTabId: calculatedSelectedTabId,
|
|
43
25
|
onSelectedTabIdChange: setCalculatedSelectedTab,
|
|
@@ -46,6 +28,7 @@ function Base({ selectedTabId, onSelectedTabChange, targetDOMNode, enableAutoUnm
|
|
|
46
28
|
setShouldShowMenu,
|
|
47
29
|
targetDOMNode: getTargetDomNode(targetDOMNode, wrapperRef.current),
|
|
48
30
|
registerTab,
|
|
31
|
+
updateTab,
|
|
49
32
|
unregisterTab,
|
|
50
33
|
enableAutoUnmount,
|
|
51
34
|
}), [
|
|
@@ -56,6 +39,7 @@ function Base({ selectedTabId, onSelectedTabChange, targetDOMNode, enableAutoUnm
|
|
|
56
39
|
setShouldShowMenu,
|
|
57
40
|
targetDOMNode,
|
|
58
41
|
registerTab,
|
|
42
|
+
updateTab,
|
|
59
43
|
unregisterTab,
|
|
60
44
|
enableAutoUnmount,
|
|
61
45
|
]);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import { type TabItemProps } from '../TabItem';
|
|
3
|
+
/**
|
|
4
|
+
* A reference to the rendered tab element, used to order the registry by document position.
|
|
5
|
+
* It is a ref rather than the element itself because a `TabItem` swaps its button for a new
|
|
6
|
+
* node whenever its tooltip wrapper appears or disappears.
|
|
7
|
+
*/
|
|
8
|
+
export type TabElementRef = RefObject<HTMLElement | null>;
|
|
9
|
+
/**
|
|
10
|
+
* Custom hook to manage the tabs registered by the `TabItem` components.
|
|
11
|
+
*
|
|
12
|
+
* The published list is ordered by the position of the tabs in the document, which is how the
|
|
13
|
+
* guideline defines the `start` / `middle` / `end` styles, and it is also the order the tab
|
|
14
|
+
* dropdown and the uncontrolled default selection rely on. Ordering by registration recency
|
|
15
|
+
* instead would move a tab whose label or badge changed after mount to the end of the list.
|
|
16
|
+
*
|
|
17
|
+
* @returns The ordered tab list and the callbacks a `TabItem` uses to register itself.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useTabRegistry(): {
|
|
20
|
+
tabs: TabItemProps[];
|
|
21
|
+
registerTab: (props: TabItemProps, elementRef: TabElementRef) => void;
|
|
22
|
+
updateTab: (props: TabItemProps) => void;
|
|
23
|
+
unregisterTab: (tabId: string) => void;
|
|
24
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMemo, useRef, useState } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Compares two `badgeProps` objects field by field.
|
|
5
|
+
* Consumers usually pass an inline object literal, so comparing by reference would report
|
|
6
|
+
* a change on every render. The comparison must stay field by field, including `aria-label`,
|
|
7
|
+
* so a badge whose count and accessible label resolve after mount still propagates.
|
|
8
|
+
*
|
|
9
|
+
* @param a - The badge props of the already published tab.
|
|
10
|
+
* @param b - The badge props the tab wants to publish.
|
|
11
|
+
*
|
|
12
|
+
* @returns `true` when every field holds the same value.
|
|
13
|
+
*/
|
|
14
|
+
function areBadgePropsEqual(a, b) {
|
|
15
|
+
if (a === b)
|
|
16
|
+
return true;
|
|
17
|
+
if (!a || !b)
|
|
18
|
+
return false;
|
|
19
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
20
|
+
return [...keys].every((key) => Object.is(a[key], b[key]));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Compares two published tab lists.
|
|
24
|
+
* Returning `true` lets the registry keep the previous array identity, which is what stops a
|
|
25
|
+
* `TabItem` re-rendered with an equivalent-but-new `badgeProps` object from feeding itself.
|
|
26
|
+
*
|
|
27
|
+
* @param a - The already published tab list.
|
|
28
|
+
* @param b - The tab list the registry wants to publish.
|
|
29
|
+
*
|
|
30
|
+
* @returns `true` when both lists hold the same tabs, in the same order, with the same props.
|
|
31
|
+
*/
|
|
32
|
+
function areTabListsEqual(a, b) {
|
|
33
|
+
if (a === b)
|
|
34
|
+
return true;
|
|
35
|
+
if (a.length !== b.length)
|
|
36
|
+
return false;
|
|
37
|
+
return a.every((tab, index) => {
|
|
38
|
+
const other = b[index];
|
|
39
|
+
return (other !== undefined &&
|
|
40
|
+
tab.id === other.id &&
|
|
41
|
+
Object.is(tab.children, other.children) &&
|
|
42
|
+
tab.className === other.className &&
|
|
43
|
+
areBadgePropsEqual(tab.badgeProps, other.badgeProps));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Returns the entries ordered by the position their elements appear in the document.
|
|
48
|
+
* Entries whose element is not in the document yet keep their relative order, because
|
|
49
|
+
* `Array#toSorted` is stable and the comparator reports them as equal.
|
|
50
|
+
*
|
|
51
|
+
* @param entries - The registered entries to order.
|
|
52
|
+
*/
|
|
53
|
+
function sortByDocumentPosition(entries) {
|
|
54
|
+
return entries.toSorted((a, b) => {
|
|
55
|
+
const aElement = a.elementRef.current;
|
|
56
|
+
const bElement = b.elementRef.current;
|
|
57
|
+
if (!aElement?.isConnected || !bElement?.isConnected)
|
|
58
|
+
return 0;
|
|
59
|
+
const relation = aElement.compareDocumentPosition(bElement);
|
|
60
|
+
// eslint-disable-next-line no-bitwise
|
|
61
|
+
if (relation & Node.DOCUMENT_POSITION_FOLLOWING)
|
|
62
|
+
return -1;
|
|
63
|
+
// eslint-disable-next-line no-bitwise
|
|
64
|
+
if (relation & Node.DOCUMENT_POSITION_PRECEDING)
|
|
65
|
+
return 1;
|
|
66
|
+
return 0;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Custom hook to manage the tabs registered by the `TabItem` components.
|
|
71
|
+
*
|
|
72
|
+
* The published list is ordered by the position of the tabs in the document, which is how the
|
|
73
|
+
* guideline defines the `start` / `middle` / `end` styles, and it is also the order the tab
|
|
74
|
+
* dropdown and the uncontrolled default selection rely on. Ordering by registration recency
|
|
75
|
+
* instead would move a tab whose label or badge changed after mount to the end of the list.
|
|
76
|
+
*
|
|
77
|
+
* @returns The ordered tab list and the callbacks a `TabItem` uses to register itself.
|
|
78
|
+
*/
|
|
79
|
+
export function useTabRegistry() {
|
|
80
|
+
const [tabs, setTabs] = useState([]);
|
|
81
|
+
const entriesRef = useRef([]);
|
|
82
|
+
const { registerTab, updateTab, unregisterTab } = useMemo(() => {
|
|
83
|
+
const publish = () => {
|
|
84
|
+
entriesRef.current = sortByDocumentPosition(entriesRef.current);
|
|
85
|
+
const nextTabs = entriesRef.current.map((entry) => entry.props);
|
|
86
|
+
setTabs((previousTabs) => (areTabListsEqual(previousTabs, nextTabs) ? previousTabs : nextTabs));
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
/**
|
|
90
|
+
* Registers a tab, or replaces it in place when the id is already registered.
|
|
91
|
+
*
|
|
92
|
+
* @param props - The props of the tab to register.
|
|
93
|
+
* @param elementRef - The reference to the rendered tab element.
|
|
94
|
+
*/
|
|
95
|
+
registerTab: (props, elementRef) => {
|
|
96
|
+
const entry = entriesRef.current.find((registered) => registered.props.id === props.id);
|
|
97
|
+
if (entry) {
|
|
98
|
+
entry.props = props;
|
|
99
|
+
entry.elementRef = elementRef;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
entriesRef.current.push({ props, elementRef });
|
|
103
|
+
}
|
|
104
|
+
publish();
|
|
105
|
+
},
|
|
106
|
+
/**
|
|
107
|
+
* Updates the props of an already registered tab without changing its place in the list.
|
|
108
|
+
*
|
|
109
|
+
* @param props - The new props of the tab.
|
|
110
|
+
*/
|
|
111
|
+
updateTab: (props) => {
|
|
112
|
+
const entry = entriesRef.current.find((registered) => registered.props.id === props.id);
|
|
113
|
+
if (!entry)
|
|
114
|
+
return;
|
|
115
|
+
entry.props = props;
|
|
116
|
+
publish();
|
|
117
|
+
},
|
|
118
|
+
/**
|
|
119
|
+
* Removes a tab from the registry. The remaining tabs keep their relative order.
|
|
120
|
+
*
|
|
121
|
+
* @param tabId - The ID of the tab to remove.
|
|
122
|
+
*/
|
|
123
|
+
unregisterTab: (tabId) => {
|
|
124
|
+
entriesRef.current = entriesRef.current.filter((registered) => registered.props.id !== tabId);
|
|
125
|
+
publish();
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}, []);
|
|
129
|
+
return { tabs, registerTab, updateTab, unregisterTab };
|
|
130
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from './ToggleSwitch';
|
|
2
|
-
export * from './ToggleSwitch.types';
|
|
2
|
+
export type * from './ToggleSwitch.types';
|
|
@@ -16,8 +16,9 @@ export function useOpenStateDelayed(tooltipOpen, onActualClose) {
|
|
|
16
16
|
const openTimeout = useRef(null);
|
|
17
17
|
const closeTimeout = useRef(null);
|
|
18
18
|
const setOpenDelayed = useCallback((value) => {
|
|
19
|
+
// A pending open is cancelled either way; only the rest of the handling differs.
|
|
20
|
+
openTimeout.current && clearTimeout(openTimeout.current);
|
|
19
21
|
if (value) {
|
|
20
|
-
openTimeout.current && clearTimeout(openTimeout.current);
|
|
21
22
|
closeTimeout.current && clearTimeout(closeTimeout.current);
|
|
22
23
|
closeTimeout.current = null;
|
|
23
24
|
openTimeout.current = window.setTimeout(() => {
|
|
@@ -26,7 +27,6 @@ export function useOpenStateDelayed(tooltipOpen, onActualClose) {
|
|
|
26
27
|
}, OPEN_DELAY);
|
|
27
28
|
}
|
|
28
29
|
else {
|
|
29
|
-
openTimeout.current && clearTimeout(openTimeout.current);
|
|
30
30
|
openTimeout.current = null;
|
|
31
31
|
closeTimeout.current = window.setTimeout(() => {
|
|
32
32
|
setOpen(false);
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assigns the given files to a `<input type="file">` element.
|
|
3
|
+
*
|
|
4
|
+
* The browser only fills `input.files` by itself when the drop lands on the input element and the
|
|
5
|
+
* default action is not prevented. Components that intercept the drop on a surrounding container
|
|
6
|
+
* have to perform the assignment themselves, otherwise the input stays empty and native form
|
|
7
|
+
* serialization (`new FormData(form)`) silently omits the file.
|
|
8
|
+
*
|
|
9
|
+
* `input.files` only accepts a `FileList`, which cannot be constructed directly, so the files are
|
|
10
|
+
* copied into a fresh `DataTransfer` and its `files` list is assigned.
|
|
11
|
+
*
|
|
12
|
+
* @param input - The file input element to assign the files to
|
|
13
|
+
*
|
|
14
|
+
* @param files - The files to assign
|
|
15
|
+
*
|
|
16
|
+
* @returns `true` when the files were assigned, `false` when the environment does not support
|
|
17
|
+
* `DataTransfer` or rejected the assignment
|
|
18
|
+
*
|
|
19
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/files
|
|
20
|
+
*/
|
|
21
|
+
export declare const setInputFiles: (input: HTMLInputElement, files: Iterable<File>) => boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assigns the given files to a `<input type="file">` element.
|
|
3
|
+
*
|
|
4
|
+
* The browser only fills `input.files` by itself when the drop lands on the input element and the
|
|
5
|
+
* default action is not prevented. Components that intercept the drop on a surrounding container
|
|
6
|
+
* have to perform the assignment themselves, otherwise the input stays empty and native form
|
|
7
|
+
* serialization (`new FormData(form)`) silently omits the file.
|
|
8
|
+
*
|
|
9
|
+
* `input.files` only accepts a `FileList`, which cannot be constructed directly, so the files are
|
|
10
|
+
* copied into a fresh `DataTransfer` and its `files` list is assigned.
|
|
11
|
+
*
|
|
12
|
+
* @param input - The file input element to assign the files to
|
|
13
|
+
*
|
|
14
|
+
* @param files - The files to assign
|
|
15
|
+
*
|
|
16
|
+
* @returns `true` when the files were assigned, `false` when the environment does not support
|
|
17
|
+
* `DataTransfer` or rejected the assignment
|
|
18
|
+
*
|
|
19
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/files
|
|
20
|
+
*/
|
|
21
|
+
export const setInputFiles = (input, files) => {
|
|
22
|
+
if (typeof DataTransfer === 'undefined') {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const dataTransfer = new DataTransfer();
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
dataTransfer.items.add(file);
|
|
29
|
+
}
|
|
30
|
+
// eslint-disable-next-line no-param-reassign -- Assigning the files to the given input is the purpose of this utility
|
|
31
|
+
input.files = dataTransfer.files;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/*
|
|
36
|
+
* Some engines expose `DataTransfer` but forbid constructing it or mutating its items outside
|
|
37
|
+
* of a drag operation. Failing to sync must not break the visible file selection.
|
|
38
|
+
*/
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a `FileList` from an array of files.
|
|
3
|
+
*
|
|
4
|
+
* `FileList` cannot be constructed: the only ways to obtain one are a user selection on a file
|
|
5
|
+
* input, a drag operation, or a `DataTransfer`. Code that has to hand a `FileList` to an API it
|
|
6
|
+
* does not control, without a `DataTransfer` available, therefore has to assemble the shape by
|
|
7
|
+
* hand. Every member of the interface is implemented, so index access, `item()`, `length` and
|
|
8
|
+
* iteration all behave as they do on a native list.
|
|
9
|
+
*
|
|
10
|
+
* The result is not an instance of `FileList`, so `instanceof` is `false` and it is rejected by
|
|
11
|
+
* APIs that check the brand rather than the shape, `HTMLInputElement.files` among them. Prefer
|
|
12
|
+
* {@link setInputFiles} whenever the destination is a file input.
|
|
13
|
+
*
|
|
14
|
+
* @param files - The files the list should contain, in order
|
|
15
|
+
*
|
|
16
|
+
* @returns A `FileList` holding the given files
|
|
17
|
+
*
|
|
18
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileList
|
|
19
|
+
*/
|
|
20
|
+
export declare const toFileList: (files: readonly File[]) => FileList;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a `FileList` from an array of files.
|
|
3
|
+
*
|
|
4
|
+
* `FileList` cannot be constructed: the only ways to obtain one are a user selection on a file
|
|
5
|
+
* input, a drag operation, or a `DataTransfer`. Code that has to hand a `FileList` to an API it
|
|
6
|
+
* does not control, without a `DataTransfer` available, therefore has to assemble the shape by
|
|
7
|
+
* hand. Every member of the interface is implemented, so index access, `item()`, `length` and
|
|
8
|
+
* iteration all behave as they do on a native list.
|
|
9
|
+
*
|
|
10
|
+
* The result is not an instance of `FileList`, so `instanceof` is `false` and it is rejected by
|
|
11
|
+
* APIs that check the brand rather than the shape, `HTMLInputElement.files` among them. Prefer
|
|
12
|
+
* {@link setInputFiles} whenever the destination is a file input.
|
|
13
|
+
*
|
|
14
|
+
* @param files - The files the list should contain, in order
|
|
15
|
+
*
|
|
16
|
+
* @returns A `FileList` holding the given files
|
|
17
|
+
*
|
|
18
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileList
|
|
19
|
+
*/
|
|
20
|
+
export const toFileList = (files) => ({
|
|
21
|
+
// eslint-disable-next-line no-misused-spread -- Spreading the array to indexed keys is exactly the `[index: number]: File` half of the FileList contract
|
|
22
|
+
...files,
|
|
23
|
+
length: files.length,
|
|
24
|
+
item: (index) => files[index] ?? null,
|
|
25
|
+
[Symbol.iterator]: () => files[Symbol.iterator](),
|
|
26
|
+
});
|
|
@@ -21,12 +21,14 @@ export const useFixedColumns = ({ enableRowSelection = false, columnIndex, leftF
|
|
|
21
21
|
return 'left';
|
|
22
22
|
if (leftFixedColumnIndex !== undefined &&
|
|
23
23
|
actualColumnIndex !== undefined &&
|
|
24
|
-
actualColumnIndex <= leftFixedColumnIndex)
|
|
24
|
+
actualColumnIndex <= leftFixedColumnIndex) {
|
|
25
25
|
return 'left';
|
|
26
|
+
}
|
|
26
27
|
if (rightFixedColumnIndex !== undefined &&
|
|
27
28
|
actualColumnIndex !== undefined &&
|
|
28
|
-
actualColumnIndex >= rightFixedColumnIndex)
|
|
29
|
+
actualColumnIndex >= rightFixedColumnIndex) {
|
|
29
30
|
return 'right';
|
|
31
|
+
}
|
|
30
32
|
// Without this, "Not all code paths return a value. ts(7030)" warning occurs.
|
|
31
33
|
// eslint-disable-next-line no-useless-return
|
|
32
34
|
return;
|
|
@@ -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;
|
|
@@ -8,7 +8,9 @@ import { getRootNode } from './getRootNode';
|
|
|
8
8
|
*/
|
|
9
9
|
const getPortalNestingDepth = (portalElement) => {
|
|
10
10
|
const depthAttribute = portalElement.dataset['mfuiPopoverDepth'];
|
|
11
|
-
|
|
11
|
+
// The attribute is written by this package as a stringified integer, so plain `Number`
|
|
12
|
+
// is equivalent to the previous `Number.parseInt(_, 10)` here.
|
|
13
|
+
return depthAttribute ? Number(depthAttribute) : 0;
|
|
12
14
|
};
|
|
13
15
|
/**
|
|
14
16
|
* Helper function to get the nesting depth of a popover container element.
|