@moneyforward/mfui-components 3.26.0 → 3.27.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/DropdownMenu/DropdownMenu.d.ts +1 -1
- package/dist/src/DropdownMenu/DropdownMenu.js +3 -3
- package/dist/src/DropdownMenu/DropdownMenu.types.d.ts +57 -5
- package/dist/src/DropdownMenu/DropdownMenuButtonTrigger.d.ts +46 -0
- package/dist/src/DropdownMenu/DropdownMenuButtonTrigger.js +56 -0
- package/dist/src/Sidebar/Sidebar.d.ts +63 -0
- package/dist/src/Sidebar/Sidebar.js +82 -0
- package/dist/src/Sidebar/Sidebar.types.d.ts +143 -0
- package/dist/src/Sidebar/Sidebar.types.js +1 -0
- package/dist/src/Sidebar/SidebarNavigationItem.d.ts +9 -0
- package/dist/src/Sidebar/SidebarNavigationItem.js +48 -0
- package/dist/src/Sidebar/SidebarServiceMenu.d.ts +23 -0
- package/dist/src/Sidebar/SidebarServiceMenu.js +20 -0
- package/dist/src/Sidebar/SidebarTenantMenu.d.ts +23 -0
- package/dist/src/Sidebar/SidebarTenantMenu.js +18 -0
- package/dist/src/Sidebar/SidebarUserMenu.d.ts +23 -0
- package/dist/src/Sidebar/SidebarUserMenu.js +18 -0
- package/dist/src/Sidebar/hooks/useSidebarResize.d.ts +28 -0
- package/dist/src/Sidebar/hooks/useSidebarResize.js +111 -0
- package/dist/src/Sidebar/index.d.ts +6 -0
- package/dist/src/Sidebar/index.js +1 -0
- package/dist/src/SplitView/SplitView.js +3 -1
- package/dist/src/SplitView/SplitView.types.d.ts +6 -0
- package/dist/src/SplitView/hooks/useSplitViewDrag.d.ts +1 -0
- package/dist/src/SplitView/hooks/useSplitViewDrag.js +11 -1
- package/dist/src/SplitView/hooks/useSplitViewKeyboard.d.ts +1 -0
- package/dist/src/SplitView/hooks/useSplitViewKeyboard.js +9 -2
- package/dist/src/Tooltip/hooks/useTooltipDisplayController.js +28 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/utilities/react/useIsomorphicLayoutEffect.d.ts +11 -0
- package/dist/src/utilities/react/useIsomorphicLayoutEffect.js +11 -0
- package/dist/styled-system/recipes/index.d.ts +2 -1
- package/dist/styled-system/recipes/index.js +1 -0
- package/dist/styled-system/recipes/sidebar-slot-recipe.d.ts +33 -0
- package/dist/styled-system/recipes/sidebar-slot-recipe.js +112 -0
- package/dist/styled-system/tokens/index.js +6 -6
- package/dist/styles.css +346 -6
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export type SidebarTenantMenuProps = {
|
|
3
|
+
/**
|
|
4
|
+
* The tenant (business) name shown in the trigger.
|
|
5
|
+
*/
|
|
6
|
+
name: string;
|
|
7
|
+
/**
|
|
8
|
+
* The tooltip label describing what the trigger is (e.g. `"事業者"`).
|
|
9
|
+
* Shown on hover/focus. When omitted, no tooltip is shown.
|
|
10
|
+
*/
|
|
11
|
+
tooltipLabel?: string;
|
|
12
|
+
/**
|
|
13
|
+
* The dropdown menu items (e.g. `DropdownMenu.Item` / `DropdownMenu.Divider`).
|
|
14
|
+
*/
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The tenant (business) menu shown in the Sidebar footer.
|
|
19
|
+
*
|
|
20
|
+
* It renders a fixed, text-only trigger showing the tenant name that opens a dropdown menu.
|
|
21
|
+
* The trigger design and accessibility are fixed; pass the name and menu items only.
|
|
22
|
+
*/
|
|
23
|
+
export declare function SidebarTenantMenu({ name, tooltipLabel, children }: SidebarTenantMenuProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { DropdownMenu } from '../DropdownMenu';
|
|
4
|
+
/**
|
|
5
|
+
* The tenant (business) menu shown in the Sidebar footer.
|
|
6
|
+
*
|
|
7
|
+
* It renders a fixed, text-only trigger showing the tenant name that opens a dropdown menu.
|
|
8
|
+
* The trigger design and accessibility are fixed; pass the name and menu items only.
|
|
9
|
+
*/
|
|
10
|
+
export function SidebarTenantMenu({ name, tooltipLabel, children }) {
|
|
11
|
+
return (_jsx(DropdownMenu, { triggerMiddleTruncate: true, label: name, tooltipLabel: tooltipLabel, tooltipPlacement: "top", triggerTypographyVariant: "condensedControlLabel", triggerProps: {
|
|
12
|
+
priority: 'tertiary',
|
|
13
|
+
size: 'small',
|
|
14
|
+
isDropdownTrigger: false,
|
|
15
|
+
// Styled via the `footerUserIdMenuItem` slot's `& .mfui-Sidebar__tenantTrigger` descendant selector.
|
|
16
|
+
className: 'mfui-Sidebar__tenantTrigger',
|
|
17
|
+
}, menuPopoverProps: { allowedPlacements: ['top-start'] }, children: children }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export type SidebarUserMenuProps = {
|
|
3
|
+
/**
|
|
4
|
+
* The user (account) name shown in the trigger.
|
|
5
|
+
*/
|
|
6
|
+
name: string;
|
|
7
|
+
/**
|
|
8
|
+
* The tooltip label describing what the trigger is (e.g. `"アカウント"`).
|
|
9
|
+
* Shown on hover/focus. When omitted, no tooltip is shown.
|
|
10
|
+
*/
|
|
11
|
+
tooltipLabel?: string;
|
|
12
|
+
/**
|
|
13
|
+
* The dropdown menu items (e.g. `DropdownMenu.Item` / `DropdownMenu.Divider`).
|
|
14
|
+
*/
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The user (account) menu shown in the Sidebar footer.
|
|
19
|
+
*
|
|
20
|
+
* It renders a fixed, text-only trigger showing the user name that opens a dropdown menu.
|
|
21
|
+
* The trigger design and accessibility are fixed; pass the name and menu items only.
|
|
22
|
+
*/
|
|
23
|
+
export declare function SidebarUserMenu({ name, tooltipLabel, children }: SidebarUserMenuProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { DropdownMenu } from '../DropdownMenu';
|
|
4
|
+
/**
|
|
5
|
+
* The user (account) menu shown in the Sidebar footer.
|
|
6
|
+
*
|
|
7
|
+
* It renders a fixed, text-only trigger showing the user name that opens a dropdown menu.
|
|
8
|
+
* The trigger design and accessibility are fixed; pass the name and menu items only.
|
|
9
|
+
*/
|
|
10
|
+
export function SidebarUserMenu({ name, tooltipLabel, children }) {
|
|
11
|
+
return (_jsx(DropdownMenu, { label: name, tooltipLabel: tooltipLabel, tooltipPlacement: "top", triggerProps: {
|
|
12
|
+
priority: 'tertiary',
|
|
13
|
+
size: 'small',
|
|
14
|
+
isDropdownTrigger: false,
|
|
15
|
+
// Styled via the `footerUserIdMenuItem` slot's `& .mfui-Sidebar__userTrigger` descendant selector.
|
|
16
|
+
className: 'mfui-Sidebar__userTrigger',
|
|
17
|
+
}, menuPopoverProps: { allowedPlacements: ['top-start'] }, children: children }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type KeyboardEvent, type PointerEvent as ReactPointerEvent } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Hook to manage the resizable width of the Sidebar via pointer dragging and keyboard control.
|
|
4
|
+
*
|
|
5
|
+
* @param options.minWidth - Minimum width in pixels (default 180)
|
|
6
|
+
*
|
|
7
|
+
* @param options.maxWidth - Maximum width in pixels (default 280)
|
|
8
|
+
*
|
|
9
|
+
* @param options.defaultWidth - Seeds the initial width only (like React's `defaultValue`); not re-synced afterwards (default 200)
|
|
10
|
+
*
|
|
11
|
+
* @param options.onWidthChange - Called with the new width when a change is committed (pointer release or keyboard step)
|
|
12
|
+
*
|
|
13
|
+
* @returns Object containing the current width, resolved bounds, refs, and resize handlers
|
|
14
|
+
*/
|
|
15
|
+
export declare function useSidebarResize({ minWidth, maxWidth, defaultWidth, onWidthChange, }?: {
|
|
16
|
+
minWidth?: number;
|
|
17
|
+
maxWidth?: number;
|
|
18
|
+
defaultWidth?: number;
|
|
19
|
+
onWidthChange?: (width: number) => void;
|
|
20
|
+
}): {
|
|
21
|
+
width: number;
|
|
22
|
+
minWidth: number;
|
|
23
|
+
maxWidth: number;
|
|
24
|
+
rootRef: import("react").RefObject<HTMLDivElement | null>;
|
|
25
|
+
isDragging: boolean;
|
|
26
|
+
handlePointerDown: (event: ReactPointerEvent<HTMLElement>) => void;
|
|
27
|
+
handleKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
|
|
28
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, } from 'react';
|
|
2
|
+
const DEFAULT_WIDTH = 200;
|
|
3
|
+
const DEFAULT_MIN_WIDTH = 180;
|
|
4
|
+
const DEFAULT_MAX_WIDTH = 280;
|
|
5
|
+
const KEYBOARD_STEP_PX = 14;
|
|
6
|
+
/**
|
|
7
|
+
* Hook to manage the resizable width of the Sidebar via pointer dragging and keyboard control.
|
|
8
|
+
*
|
|
9
|
+
* @param options.minWidth - Minimum width in pixels (default 180)
|
|
10
|
+
*
|
|
11
|
+
* @param options.maxWidth - Maximum width in pixels (default 280)
|
|
12
|
+
*
|
|
13
|
+
* @param options.defaultWidth - Seeds the initial width only (like React's `defaultValue`); not re-synced afterwards (default 200)
|
|
14
|
+
*
|
|
15
|
+
* @param options.onWidthChange - Called with the new width when a change is committed (pointer release or keyboard step)
|
|
16
|
+
*
|
|
17
|
+
* @returns Object containing the current width, resolved bounds, refs, and resize handlers
|
|
18
|
+
*/
|
|
19
|
+
export function useSidebarResize({ minWidth = DEFAULT_MIN_WIDTH, maxWidth = DEFAULT_MAX_WIDTH, defaultWidth, onWidthChange, } = {}) {
|
|
20
|
+
const rootRef = useRef(null);
|
|
21
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
22
|
+
const pointerIdRef = useRef(null);
|
|
23
|
+
const onWidthChangeRef = useRef(onWidthChange);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
onWidthChangeRef.current = onWidthChange;
|
|
26
|
+
});
|
|
27
|
+
const clamp = useCallback((value) => Math.min(Math.max(value, minWidth), maxWidth), [minWidth, maxWidth]);
|
|
28
|
+
// Validate the resolved bounds. When minWidth > maxWidth the clamp pins the width to maxWidth,
|
|
29
|
+
// which is counterintuitive, so surface it as a developer error.
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (minWidth > maxWidth) {
|
|
32
|
+
console.error(`Sidebar: minWidth (${String(minWidth)}) must be less than or equal to maxWidth (${String(maxWidth)}).`);
|
|
33
|
+
}
|
|
34
|
+
}, [minWidth, maxWidth]);
|
|
35
|
+
// Clamp the default so the initial render stays within the user-supplied bounds.
|
|
36
|
+
// `defaultWidth` seeds the initial width only (like React's `defaultValue`); it is intentionally
|
|
37
|
+
// not re-synced on later changes, so the user's drag/keyboard adjustments are never overridden.
|
|
38
|
+
const initialWidth = clamp(defaultWidth ?? DEFAULT_WIDTH);
|
|
39
|
+
const widthRef = useRef(initialWidth);
|
|
40
|
+
const [width, setWidth] = useState(initialWidth);
|
|
41
|
+
const notify = useCallback((value) => {
|
|
42
|
+
onWidthChangeRef.current?.(value);
|
|
43
|
+
}, []);
|
|
44
|
+
const applyWidth = useCallback((newWidth) => {
|
|
45
|
+
widthRef.current = newWidth;
|
|
46
|
+
setWidth(newWidth);
|
|
47
|
+
}, []);
|
|
48
|
+
// --- Drag ---
|
|
49
|
+
const handlePointerDown = useCallback((event) => {
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
52
|
+
pointerIdRef.current = event.pointerId;
|
|
53
|
+
setIsDragging(true);
|
|
54
|
+
}, []);
|
|
55
|
+
const handlePointerMove = useCallback((event) => {
|
|
56
|
+
if (pointerIdRef.current !== event.pointerId)
|
|
57
|
+
return;
|
|
58
|
+
if (!rootRef.current)
|
|
59
|
+
return;
|
|
60
|
+
const rect = rootRef.current.getBoundingClientRect();
|
|
61
|
+
const newWidth = Math.min(Math.max(event.clientX - rect.left, minWidth), maxWidth);
|
|
62
|
+
applyWidth(newWidth);
|
|
63
|
+
}, [minWidth, maxWidth, applyWidth]);
|
|
64
|
+
const handlePointerUp = useCallback((event) => {
|
|
65
|
+
if (pointerIdRef.current !== event.pointerId)
|
|
66
|
+
return;
|
|
67
|
+
setIsDragging(false);
|
|
68
|
+
pointerIdRef.current = null;
|
|
69
|
+
notify(widthRef.current);
|
|
70
|
+
}, [notify]);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!isDragging)
|
|
73
|
+
return;
|
|
74
|
+
document.addEventListener('pointermove', handlePointerMove);
|
|
75
|
+
document.addEventListener('pointerup', handlePointerUp);
|
|
76
|
+
document.addEventListener('pointercancel', handlePointerUp);
|
|
77
|
+
return () => {
|
|
78
|
+
document.removeEventListener('pointermove', handlePointerMove);
|
|
79
|
+
document.removeEventListener('pointerup', handlePointerUp);
|
|
80
|
+
document.removeEventListener('pointercancel', handlePointerUp);
|
|
81
|
+
};
|
|
82
|
+
}, [isDragging, handlePointerMove, handlePointerUp]);
|
|
83
|
+
// --- Keyboard ---
|
|
84
|
+
const handleKeyDown = useCallback((event) => {
|
|
85
|
+
let newWidth;
|
|
86
|
+
switch (event.key) {
|
|
87
|
+
case 'ArrowRight':
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
newWidth = Math.min(widthRef.current + KEYBOARD_STEP_PX, maxWidth);
|
|
90
|
+
break;
|
|
91
|
+
case 'ArrowLeft':
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
newWidth = Math.max(widthRef.current - KEYBOARD_STEP_PX, minWidth);
|
|
94
|
+
break;
|
|
95
|
+
case 'Home':
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
newWidth = minWidth;
|
|
98
|
+
break;
|
|
99
|
+
case 'End':
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
newWidth = maxWidth;
|
|
102
|
+
break;
|
|
103
|
+
// No default
|
|
104
|
+
}
|
|
105
|
+
if (newWidth !== undefined) {
|
|
106
|
+
applyWidth(newWidth);
|
|
107
|
+
notify(newWidth);
|
|
108
|
+
}
|
|
109
|
+
}, [minWidth, maxWidth, applyWidth, notify]);
|
|
110
|
+
return { width, minWidth, maxWidth, rootRef, isDragging, handlePointerDown, handleKeyDown };
|
|
111
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { Sidebar } from './Sidebar';
|
|
2
|
+
export type { SidebarProps, SidebarNavigationItem, SidebarNavigationDivider, SidebarNavigationEntry, } from './Sidebar.types';
|
|
3
|
+
export type { SidebarNavigationItemProps } from './SidebarNavigationItem';
|
|
4
|
+
export type { SidebarServiceMenuProps } from './SidebarServiceMenu';
|
|
5
|
+
export type { SidebarTenantMenuProps } from './SidebarTenantMenu';
|
|
6
|
+
export type { SidebarUserMenuProps } from './SidebarUserMenu';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Sidebar } from './Sidebar';
|
|
@@ -32,7 +32,7 @@ import { parseSizeToNumber } from './utils/parseSize';
|
|
|
32
32
|
* />
|
|
33
33
|
* ```
|
|
34
34
|
*/
|
|
35
|
-
export const SplitView = forwardRef(({ className, leftPanelSlot, rightPanelSlot, isOpen, initialPanelSize, minPanelSize, maxPanelSize, targetPanel, initialRightPanelSize, minRightPanelSize, maxRightPanelSize, leftPanelProps, rightPanelProps, dividerProps, enableAutoUnmount = true, ...props }, ref) => {
|
|
35
|
+
export const SplitView = forwardRef(({ className, leftPanelSlot, rightPanelSlot, isOpen, initialPanelSize, minPanelSize, maxPanelSize, targetPanel, initialRightPanelSize, minRightPanelSize, maxRightPanelSize, leftPanelProps, rightPanelProps, dividerProps, onWidthChange, enableAutoUnmount = true, ...props }, ref) => {
|
|
36
36
|
// Normalize legacy props to the new API
|
|
37
37
|
const resolvedTargetPanel = targetPanel ?? 'right';
|
|
38
38
|
const resolvedInitialPanelSize = initialPanelSize ?? initialRightPanelSize;
|
|
@@ -135,6 +135,7 @@ export const SplitView = forwardRef(({ className, leftPanelSlot, rightPanelSlot,
|
|
|
135
135
|
targetPanel: resolvedTargetPanel,
|
|
136
136
|
minPanelSize: minPanelSizeNumber,
|
|
137
137
|
maxPanelSize: maxPanelSizeNumber,
|
|
138
|
+
onWidthChange,
|
|
138
139
|
setControlledPanelSize,
|
|
139
140
|
setLastControlledPanelSize,
|
|
140
141
|
});
|
|
@@ -146,6 +147,7 @@ export const SplitView = forwardRef(({ className, leftPanelSlot, rightPanelSlot,
|
|
|
146
147
|
initialPanelSize: initialPanelSizeNumber,
|
|
147
148
|
minPanelSize: minPanelSizeNumber,
|
|
148
149
|
maxPanelSize: maxPanelSizeNumber,
|
|
150
|
+
onWidthChange,
|
|
149
151
|
setControlledPanelSize,
|
|
150
152
|
setLastControlledPanelSize,
|
|
151
153
|
});
|
|
@@ -52,6 +52,12 @@ type SplitViewBaseProps = {
|
|
|
52
52
|
* Props for the divider element.
|
|
53
53
|
*/
|
|
54
54
|
dividerProps?: ComponentPropsWithoutRef<'div'>;
|
|
55
|
+
/**
|
|
56
|
+
* Called when the user commits a width change by releasing a drag or pressing an arrow key.
|
|
57
|
+
*
|
|
58
|
+
* Use this to persist the controlled panel width and restore it later via `initialPanelSize`.
|
|
59
|
+
*/
|
|
60
|
+
onWidthChange?: (width: number) => void;
|
|
55
61
|
/**
|
|
56
62
|
* Enable auto unmounting of the controlled panel content when it is not displayed.
|
|
57
63
|
* When true, the controlled panel content will return null instead of hiding via CSS.
|
|
@@ -8,6 +8,7 @@ export declare function useSplitViewDrag(options: {
|
|
|
8
8
|
targetPanel: 'left' | 'right';
|
|
9
9
|
minPanelSize: number;
|
|
10
10
|
maxPanelSize: number;
|
|
11
|
+
onWidthChange?: (width: number) => void;
|
|
11
12
|
setControlledPanelSize: (size: number | undefined) => void;
|
|
12
13
|
setLastControlledPanelSize: (size: number | undefined) => void;
|
|
13
14
|
}): {
|
|
@@ -6,9 +6,14 @@ import { calculateConstrainedPanelSize } from '../utils/calculatePanelSize';
|
|
|
6
6
|
* @param options - Drag hook options
|
|
7
7
|
*/
|
|
8
8
|
export function useSplitViewDrag(options) {
|
|
9
|
-
const { containerRef, targetPanel, minPanelSize, maxPanelSize, setControlledPanelSize, setLastControlledPanelSize } = options;
|
|
9
|
+
const { containerRef, targetPanel, minPanelSize, maxPanelSize, onWidthChange, setControlledPanelSize, setLastControlledPanelSize, } = options;
|
|
10
10
|
const [isDragging, setIsDragging] = useState(false);
|
|
11
11
|
const pointerIdRef = useRef(null);
|
|
12
|
+
const latestSizeRef = useRef(undefined);
|
|
13
|
+
const onWidthChangeRef = useRef(onWidthChange);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
onWidthChangeRef.current = onWidthChange;
|
|
16
|
+
});
|
|
12
17
|
/**
|
|
13
18
|
* Handle pointer down on the divider.
|
|
14
19
|
*/
|
|
@@ -34,6 +39,7 @@ export function useSplitViewDrag(options) {
|
|
|
34
39
|
? event.clientX - containerRect.left // left panel size = pointer X from container left
|
|
35
40
|
: containerSize - (event.clientX - containerRect.left); // right panel size (legacy behavior)
|
|
36
41
|
const constrainedSize = calculateConstrainedPanelSize(rawControlledPanelSize, minPanelSize, maxPanelSize);
|
|
42
|
+
latestSizeRef.current = constrainedSize;
|
|
37
43
|
setControlledPanelSize(constrainedSize);
|
|
38
44
|
setLastControlledPanelSize(constrainedSize);
|
|
39
45
|
}, [
|
|
@@ -50,8 +56,12 @@ export function useSplitViewDrag(options) {
|
|
|
50
56
|
*/
|
|
51
57
|
const handlePointerUp = useCallback((event) => {
|
|
52
58
|
if (pointerIdRef.current === event.pointerId) {
|
|
59
|
+
if (latestSizeRef.current !== undefined) {
|
|
60
|
+
onWidthChangeRef.current?.(latestSizeRef.current);
|
|
61
|
+
}
|
|
53
62
|
setIsDragging(false);
|
|
54
63
|
pointerIdRef.current = null;
|
|
64
|
+
latestSizeRef.current = undefined;
|
|
55
65
|
}
|
|
56
66
|
}, []);
|
|
57
67
|
/**
|
|
@@ -10,6 +10,7 @@ export declare function useSplitViewKeyboard(options: {
|
|
|
10
10
|
initialPanelSize: number;
|
|
11
11
|
minPanelSize: number;
|
|
12
12
|
maxPanelSize: number;
|
|
13
|
+
onWidthChange?: (width: number) => void;
|
|
13
14
|
setControlledPanelSize: (size: number | undefined) => void;
|
|
14
15
|
setLastControlledPanelSize: (size: number | undefined) => void;
|
|
15
16
|
}): {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
2
|
import { KEYBOARD_STEP_PX } from '../constants';
|
|
3
3
|
import { calculateConstrainedPanelSize } from '../utils/calculatePanelSize';
|
|
4
4
|
/**
|
|
@@ -7,7 +7,11 @@ import { calculateConstrainedPanelSize } from '../utils/calculatePanelSize';
|
|
|
7
7
|
* @param options - Keyboard hook options
|
|
8
8
|
*/
|
|
9
9
|
export function useSplitViewKeyboard(options) {
|
|
10
|
-
const { containerRef, targetPanel, controlledPanelSize, initialPanelSize, minPanelSize, maxPanelSize, setControlledPanelSize, setLastControlledPanelSize, } = options;
|
|
10
|
+
const { containerRef, targetPanel, controlledPanelSize, initialPanelSize, minPanelSize, maxPanelSize, onWidthChange, setControlledPanelSize, setLastControlledPanelSize, } = options;
|
|
11
|
+
const onWidthChangeRef = useRef(onWidthChange);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
onWidthChangeRef.current = onWidthChange;
|
|
14
|
+
});
|
|
11
15
|
/**
|
|
12
16
|
* Handle keyboard navigation for the divider.
|
|
13
17
|
*
|
|
@@ -36,6 +40,9 @@ export function useSplitViewKeyboard(options) {
|
|
|
36
40
|
const constrainedSize = calculateConstrainedPanelSize(newSize, minPanelSize, maxPanelSize);
|
|
37
41
|
setControlledPanelSize(constrainedSize);
|
|
38
42
|
setLastControlledPanelSize(constrainedSize);
|
|
43
|
+
if (constrainedSize !== currentSize) {
|
|
44
|
+
onWidthChangeRef.current?.(constrainedSize);
|
|
45
|
+
}
|
|
39
46
|
}, [
|
|
40
47
|
containerRef,
|
|
41
48
|
targetPanel,
|
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
import { useCallback, useEffect, useId, useMemo, useRef } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useId, useMemo, useRef, } from 'react';
|
|
2
2
|
import { useUpdateEffect } from '../../utilities/effect/useUpdateEffect';
|
|
3
3
|
import { tooltipManager } from '../GlobalTooltipManager';
|
|
4
4
|
import { useOpenStateDelayed } from './useOpenStateDelayed';
|
|
5
|
+
/**
|
|
6
|
+
* Returns `true` only when the event originated from an element that is a real DOM
|
|
7
|
+
* descendant of the wrapper (i.e. the trigger itself).
|
|
8
|
+
*
|
|
9
|
+
* React dispatches synthetic events along the React tree, not the DOM tree, so events
|
|
10
|
+
* from portaled descendants (e.g. a DropdownMenu popover rendered into `document.body`)
|
|
11
|
+
* still bubble up to the wrapper's handlers. Those elements are not DOM descendants of
|
|
12
|
+
* the wrapper, so this check filters them out and prevents the tooltip from reacting to
|
|
13
|
+
* interactions that happen inside portaled content.
|
|
14
|
+
*
|
|
15
|
+
* @param event - The synthetic event dispatched to the wrapper handler.
|
|
16
|
+
*/
|
|
17
|
+
function isEventFromTrigger(event) {
|
|
18
|
+
return event.currentTarget.contains(event.target);
|
|
19
|
+
}
|
|
5
20
|
/**
|
|
6
21
|
* A hook to control the display of the tooltip.
|
|
7
22
|
*
|
|
@@ -82,6 +97,8 @@ export function useTooltipDisplayController({ trigger, wrapperProps, tooltipOpen
|
|
|
82
97
|
if (triggers.includes('hover')) {
|
|
83
98
|
Object.assign(updatedWrapperProps, {
|
|
84
99
|
onMouseEnter: (event) => {
|
|
100
|
+
if (!isEventFromTrigger(event))
|
|
101
|
+
return;
|
|
85
102
|
triggerSource.current = 'hover';
|
|
86
103
|
isHoveringTrigger.current = true;
|
|
87
104
|
cancelClose(); // Cancel any pending close
|
|
@@ -91,6 +108,8 @@ export function useTooltipDisplayController({ trigger, wrapperProps, tooltipOpen
|
|
|
91
108
|
onMouseLeave: (event) => {
|
|
92
109
|
if (triggerSource.current !== 'hover')
|
|
93
110
|
return;
|
|
111
|
+
if (!isEventFromTrigger(event))
|
|
112
|
+
return;
|
|
94
113
|
isHoveringTrigger.current = false;
|
|
95
114
|
// Only start close timer if also not hovering tooltip
|
|
96
115
|
// Use queueMicrotask to avoid race conditions with rapid mouse events
|
|
@@ -104,6 +123,8 @@ export function useTooltipDisplayController({ trigger, wrapperProps, tooltipOpen
|
|
|
104
123
|
if (triggers.includes('click')) {
|
|
105
124
|
Object.assign(updatedWrapperProps, {
|
|
106
125
|
onClick: (event) => {
|
|
126
|
+
if (!isEventFromTrigger(event))
|
|
127
|
+
return;
|
|
107
128
|
if (!open) {
|
|
108
129
|
triggerSource.current = 'click';
|
|
109
130
|
openWithManager('immediate');
|
|
@@ -119,6 +140,8 @@ export function useTooltipDisplayController({ trigger, wrapperProps, tooltipOpen
|
|
|
119
140
|
// When click is not a trigger, clicking should close the tooltip if it's open
|
|
120
141
|
Object.assign(updatedWrapperProps, {
|
|
121
142
|
onClick: (event) => {
|
|
143
|
+
if (!isEventFromTrigger(event))
|
|
144
|
+
return;
|
|
122
145
|
if (open) {
|
|
123
146
|
closeWithManager();
|
|
124
147
|
}
|
|
@@ -129,6 +152,8 @@ export function useTooltipDisplayController({ trigger, wrapperProps, tooltipOpen
|
|
|
129
152
|
if (triggers.includes('focus')) {
|
|
130
153
|
Object.assign(updatedWrapperProps, {
|
|
131
154
|
onFocus: (event) => {
|
|
155
|
+
if (!isEventFromTrigger(event))
|
|
156
|
+
return;
|
|
132
157
|
// onFocus event is triggered before onClick event when the mouse is clicked, so we need to skip it
|
|
133
158
|
if (isMouseTriggered.current)
|
|
134
159
|
return;
|
|
@@ -139,6 +164,8 @@ export function useTooltipDisplayController({ trigger, wrapperProps, tooltipOpen
|
|
|
139
164
|
onBlur: (event) => {
|
|
140
165
|
if (triggerSource.current !== 'focus')
|
|
141
166
|
return;
|
|
167
|
+
if (!isEventFromTrigger(event))
|
|
168
|
+
return;
|
|
142
169
|
closeWithManager();
|
|
143
170
|
wrapperProps.onBlur?.(event);
|
|
144
171
|
},
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.js
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* `useLayoutEffect` that falls back to `useEffect` on the server.
|
|
4
|
+
*
|
|
5
|
+
* `useLayoutEffect` does nothing during server rendering and React logs a warning when it is used
|
|
6
|
+
* there. Using this hook keeps the synchronous, pre-paint timing on the client while avoiding the
|
|
7
|
+
* SSR warning.
|
|
8
|
+
*
|
|
9
|
+
* @see https://react.dev/reference/react/useLayoutEffect#caveats
|
|
10
|
+
*/
|
|
11
|
+
export declare const useIsomorphicLayoutEffect: typeof useEffect;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* `useLayoutEffect` that falls back to `useEffect` on the server.
|
|
4
|
+
*
|
|
5
|
+
* `useLayoutEffect` does nothing during server rendering and React logs a warning when it is used
|
|
6
|
+
* there. Using this hook keeps the synchronous, pre-paint timing on the client while avoiding the
|
|
7
|
+
* SSR warning.
|
|
8
|
+
*
|
|
9
|
+
* @see https://react.dev/reference/react/useLayoutEffect#caveats
|
|
10
|
+
*/
|
|
11
|
+
export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
@@ -98,4 +98,5 @@ export * from './split-view-slot-recipe';
|
|
|
98
98
|
export * from './toggle-switch-slot-recipe';
|
|
99
99
|
export * from './navigation-list-slot-recipe';
|
|
100
100
|
export * from './navigation-list-item-slot-recipe';
|
|
101
|
-
export * from './form-footer-slot-recipe';
|
|
101
|
+
export * from './form-footer-slot-recipe';
|
|
102
|
+
export * from './sidebar-slot-recipe';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
import type { ConditionalValue } from '../types/index';
|
|
3
|
+
import type { DistributiveOmit, Pretty } from '../types/system-types';
|
|
4
|
+
|
|
5
|
+
interface SidebarSlotRecipeVariant {
|
|
6
|
+
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type SidebarSlotRecipeVariantMap = {
|
|
10
|
+
[key in keyof SidebarSlotRecipeVariant]: Array<SidebarSlotRecipeVariant[key]>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type SidebarSlotRecipeSlot = "root" | "header" | "leftPane" | "logo" | "navigationWrapper" | "mainNavigation" | "extraNavigation" | "navigationList" | "navigationListItem" | "navigationDivider" | "navigationLink" | "navigationLinkInner" | "navigationLinkIcon" | "navigationLinkLabel" | "navigationLinkLabelInner" | "footer" | "footerSlot" | "serviceMenu" | "footerIconMenu" | "footerUserIdMenu" | "footerUserIdMenuItem" | "handle"
|
|
14
|
+
|
|
15
|
+
export type SidebarSlotRecipeVariantProps = {
|
|
16
|
+
[key in keyof SidebarSlotRecipeVariant]?: ConditionalValue<SidebarSlotRecipeVariant[key]> | undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SidebarSlotRecipeRecipe {
|
|
20
|
+
__slot: SidebarSlotRecipeSlot
|
|
21
|
+
__type: SidebarSlotRecipeVariantProps
|
|
22
|
+
(props?: SidebarSlotRecipeVariantProps): Pretty<Record<SidebarSlotRecipeSlot, string>>
|
|
23
|
+
raw: (props?: SidebarSlotRecipeVariantProps) => SidebarSlotRecipeVariantProps
|
|
24
|
+
variantMap: SidebarSlotRecipeVariantMap
|
|
25
|
+
variantKeys: Array<keyof SidebarSlotRecipeVariant>
|
|
26
|
+
splitVariantProps<Props extends SidebarSlotRecipeVariantProps>(props: Props): [SidebarSlotRecipeVariantProps, Pretty<DistributiveOmit<Props, keyof SidebarSlotRecipeVariantProps>>]
|
|
27
|
+
getVariantProps: (props?: SidebarSlotRecipeVariantProps) => SidebarSlotRecipeVariantProps
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Slot class created for the MFUI Sidebar component.
|
|
32
|
+
*/
|
|
33
|
+
export declare const sidebarSlotRecipe: SidebarSlotRecipeRecipe
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { compact, getSlotCompoundVariant, memo, splitProps } from '../helpers.js';
|
|
2
|
+
import { createRecipe } from './create-recipe.js';
|
|
3
|
+
const sidebarSlotRecipeDefaultVariants = {};
|
|
4
|
+
const sidebarSlotRecipeCompoundVariants = [];
|
|
5
|
+
const sidebarSlotRecipeSlotNames = [
|
|
6
|
+
[
|
|
7
|
+
"root",
|
|
8
|
+
"Sidebar__root"
|
|
9
|
+
],
|
|
10
|
+
[
|
|
11
|
+
"header",
|
|
12
|
+
"Sidebar__header"
|
|
13
|
+
],
|
|
14
|
+
[
|
|
15
|
+
"leftPane",
|
|
16
|
+
"Sidebar__leftPane"
|
|
17
|
+
],
|
|
18
|
+
[
|
|
19
|
+
"logo",
|
|
20
|
+
"Sidebar__logo"
|
|
21
|
+
],
|
|
22
|
+
[
|
|
23
|
+
"navigationWrapper",
|
|
24
|
+
"Sidebar__navigationWrapper"
|
|
25
|
+
],
|
|
26
|
+
[
|
|
27
|
+
"mainNavigation",
|
|
28
|
+
"Sidebar__mainNavigation"
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
"extraNavigation",
|
|
32
|
+
"Sidebar__extraNavigation"
|
|
33
|
+
],
|
|
34
|
+
[
|
|
35
|
+
"navigationList",
|
|
36
|
+
"Sidebar__navigationList"
|
|
37
|
+
],
|
|
38
|
+
[
|
|
39
|
+
"navigationListItem",
|
|
40
|
+
"Sidebar__navigationListItem"
|
|
41
|
+
],
|
|
42
|
+
[
|
|
43
|
+
"navigationDivider",
|
|
44
|
+
"Sidebar__navigationDivider"
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
"navigationLink",
|
|
48
|
+
"Sidebar__navigationLink"
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"navigationLinkInner",
|
|
52
|
+
"Sidebar__navigationLinkInner"
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
"navigationLinkIcon",
|
|
56
|
+
"Sidebar__navigationLinkIcon"
|
|
57
|
+
],
|
|
58
|
+
[
|
|
59
|
+
"navigationLinkLabel",
|
|
60
|
+
"Sidebar__navigationLinkLabel"
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
"navigationLinkLabelInner",
|
|
64
|
+
"Sidebar__navigationLinkLabelInner"
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
"footer",
|
|
68
|
+
"Sidebar__footer"
|
|
69
|
+
],
|
|
70
|
+
[
|
|
71
|
+
"footerSlot",
|
|
72
|
+
"Sidebar__footerSlot"
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
"serviceMenu",
|
|
76
|
+
"Sidebar__serviceMenu"
|
|
77
|
+
],
|
|
78
|
+
[
|
|
79
|
+
"footerIconMenu",
|
|
80
|
+
"Sidebar__footerIconMenu"
|
|
81
|
+
],
|
|
82
|
+
[
|
|
83
|
+
"footerUserIdMenu",
|
|
84
|
+
"Sidebar__footerUserIdMenu"
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
"footerUserIdMenuItem",
|
|
88
|
+
"Sidebar__footerUserIdMenuItem"
|
|
89
|
+
],
|
|
90
|
+
[
|
|
91
|
+
"handle",
|
|
92
|
+
"Sidebar__handle"
|
|
93
|
+
]
|
|
94
|
+
];
|
|
95
|
+
const sidebarSlotRecipeSlotFns = /* @__PURE__ */ sidebarSlotRecipeSlotNames.map(([slotName, slotKey]) => [slotName, createRecipe(slotKey, sidebarSlotRecipeDefaultVariants, getSlotCompoundVariant(sidebarSlotRecipeCompoundVariants, slotName))]);
|
|
96
|
+
const sidebarSlotRecipeFn = memo((props = {}) => {
|
|
97
|
+
return Object.fromEntries(sidebarSlotRecipeSlotFns.map(([slotName, slotFn]) => [slotName, slotFn.recipeFn(props)]));
|
|
98
|
+
});
|
|
99
|
+
const sidebarSlotRecipeVariantKeys = [];
|
|
100
|
+
const getVariantProps = (variants) => ({ ...sidebarSlotRecipeDefaultVariants, ...compact(variants) });
|
|
101
|
+
export const sidebarSlotRecipe = /* @__PURE__ */ Object.assign(sidebarSlotRecipeFn, {
|
|
102
|
+
__recipe__: false,
|
|
103
|
+
__name__: 'sidebarSlotRecipe',
|
|
104
|
+
raw: (props) => props,
|
|
105
|
+
classNameMap: {},
|
|
106
|
+
variantKeys: sidebarSlotRecipeVariantKeys,
|
|
107
|
+
variantMap: {},
|
|
108
|
+
splitVariantProps(props) {
|
|
109
|
+
return splitProps(props, sidebarSlotRecipeVariantKeys);
|
|
110
|
+
},
|
|
111
|
+
getVariantProps
|
|
112
|
+
});
|