@moneyforward/mfui-components 3.26.0 → 3.27.1

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.
Files changed (40) hide show
  1. package/dist/src/DropdownMenu/DropdownMenu.d.ts +1 -1
  2. package/dist/src/DropdownMenu/DropdownMenu.js +3 -3
  3. package/dist/src/DropdownMenu/DropdownMenu.types.d.ts +57 -5
  4. package/dist/src/DropdownMenu/DropdownMenuButtonTrigger.d.ts +46 -0
  5. package/dist/src/DropdownMenu/DropdownMenuButtonTrigger.js +56 -0
  6. package/dist/src/Sidebar/Sidebar.d.ts +63 -0
  7. package/dist/src/Sidebar/Sidebar.js +82 -0
  8. package/dist/src/Sidebar/Sidebar.types.d.ts +143 -0
  9. package/dist/src/Sidebar/Sidebar.types.js +1 -0
  10. package/dist/src/Sidebar/SidebarNavigationItem.d.ts +9 -0
  11. package/dist/src/Sidebar/SidebarNavigationItem.js +48 -0
  12. package/dist/src/Sidebar/SidebarServiceMenu.d.ts +23 -0
  13. package/dist/src/Sidebar/SidebarServiceMenu.js +20 -0
  14. package/dist/src/Sidebar/SidebarTenantMenu.d.ts +23 -0
  15. package/dist/src/Sidebar/SidebarTenantMenu.js +18 -0
  16. package/dist/src/Sidebar/SidebarUserMenu.d.ts +23 -0
  17. package/dist/src/Sidebar/SidebarUserMenu.js +18 -0
  18. package/dist/src/Sidebar/hooks/useSidebarResize.d.ts +28 -0
  19. package/dist/src/Sidebar/hooks/useSidebarResize.js +111 -0
  20. package/dist/src/Sidebar/index.d.ts +6 -0
  21. package/dist/src/Sidebar/index.js +1 -0
  22. package/dist/src/SplitView/SplitView.js +5 -3
  23. package/dist/src/SplitView/SplitView.types.d.ts +6 -0
  24. package/dist/src/SplitView/hooks/useSplitViewDrag.d.ts +1 -0
  25. package/dist/src/SplitView/hooks/useSplitViewDrag.js +11 -1
  26. package/dist/src/SplitView/hooks/useSplitViewKeyboard.d.ts +1 -0
  27. package/dist/src/SplitView/hooks/useSplitViewKeyboard.js +9 -2
  28. package/dist/src/Tooltip/hooks/useTooltipDisplayController.js +28 -1
  29. package/dist/src/index.d.ts +1 -0
  30. package/dist/src/index.js +1 -0
  31. package/dist/src/utilities/react/useIsomorphicLayoutEffect.d.ts +11 -0
  32. package/dist/src/utilities/react/useIsomorphicLayoutEffect.js +11 -0
  33. package/dist/styled-system/recipes/index.d.ts +2 -1
  34. package/dist/styled-system/recipes/index.js +1 -0
  35. package/dist/styled-system/recipes/sidebar-slot-recipe.d.ts +33 -0
  36. package/dist/styled-system/recipes/sidebar-slot-recipe.js +112 -0
  37. package/dist/styled-system/tokens/index.js +6 -6
  38. package/dist/styles.css +354 -6
  39. package/dist/tsconfig.build.tsbuildinfo +1 -1
  40. package/package.json +2 -2
@@ -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
  });
@@ -207,7 +209,7 @@ export const SplitView = forwardRef(({ className, leftPanelSlot, rightPanelSlot,
207
209
  ref.current = node;
208
210
  }
209
211
  containerRef.current = node;
210
- }, className: cx(classes.root, 'mfui-SplitView__root', className), ...props, children: [resolvedTargetPanel === 'left' ? (shouldRenderControlledPanel || isSlideOutAnimating ? (_jsx("div", { ...leftPanelProps, className: cx(classes.leftPanel, 'mfui-SplitView__leftPanel', leftPanelProps?.className), style: computedLeftPanelStyle, children: _jsx("div", { style: controlledPanelInnerStyle, children: enableAutoUnmount ? (leftPanelSlot ?? lastRightPanelRef.current) : leftPanelSlot }) })) : null) : (_jsx("div", { ...leftPanelProps, className: cx(classes.leftPanel, 'mfui-SplitView__leftPanel', leftPanelProps?.className), style: computedLeftPanelStyle, children: leftPanelSlot })), isRightPanelVisible || isSlideOutAnimating ? (_jsx("div", { ...dividerProps, className: cx(classes.divider, 'mfui-SplitView__divider', dividerProps?.className), role: "separator", "aria-orientation": "vertical", "aria-label": dividerProps?.['aria-label'] ?? '左右のパネルを調整',
212
+ }, className: cx(classes.root, 'mfui-SplitView__root', className), ...props, children: [resolvedTargetPanel === 'left' ? (shouldRenderControlledPanel || isSlideOutAnimating ? (_jsx("div", { ...leftPanelProps, className: cx(classes.leftPanel, 'mfui-SplitView__leftPanel', leftPanelProps?.className), style: computedLeftPanelStyle, children: _jsx("div", { style: controlledPanelInnerStyle, children: enableAutoUnmount ? (leftPanelSlot ?? lastRightPanelRef.current) : leftPanelSlot }) })) : null) : (_jsx("div", { ...leftPanelProps, className: cx(classes.leftPanel, 'mfui-SplitView__leftPanel', leftPanelProps?.className), style: computedLeftPanelStyle, children: leftPanelSlot })), isRightPanelVisible || isSlideOutAnimating ? (_jsx("div", { className: cx(classes.divider, 'mfui-SplitView__divider', dividerProps?.className), role: "separator", "aria-orientation": "vertical", "aria-label": dividerProps?.['aria-label'] ?? '左右のパネルを調整', "aria-valuenow": currentControlledPanelSize ?? initialPanelSizeNumber, "aria-valuemin": minPanelSizeNumber, "aria-valuemax": maxPanelSizeNumber,
211
213
  // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
212
- tabIndex: 0, style: dividerStyle, onPointerDown: handleMouseDown, onKeyDown: handleKeyDown })) : null, resolvedTargetPanel === 'right' ? (shouldRenderControlledPanel || isSlideOutAnimating ? (_jsx("div", { ...rightPanelProps, className: cx(classes.rightPanel, 'mfui-SplitView__rightPanel', rightPanelProps?.className), style: computedRightPanelStyle, children: _jsx("div", { style: controlledPanelInnerStyle, children: enableAutoUnmount ? (rightPanelSlot ?? lastRightPanelRef.current) : rightPanelSlot }) })) : null) : (_jsx("div", { ...rightPanelProps, className: cx(classes.rightPanel, 'mfui-SplitView__rightPanel', rightPanelProps?.className), style: computedRightPanelStyle, children: rightPanelSlot }))] }));
214
+ tabIndex: 0, style: dividerStyle, onPointerDown: handleMouseDown, onKeyDown: handleKeyDown, ...dividerProps })) : null, resolvedTargetPanel === 'right' ? (shouldRenderControlledPanel || isSlideOutAnimating ? (_jsx("div", { ...rightPanelProps, className: cx(classes.rightPanel, 'mfui-SplitView__rightPanel', rightPanelProps?.className), style: computedRightPanelStyle, children: _jsx("div", { style: controlledPanelInnerStyle, children: enableAutoUnmount ? (rightPanelSlot ?? lastRightPanelRef.current) : rightPanelSlot }) })) : null) : (_jsx("div", { ...rightPanelProps, className: cx(classes.rightPanel, 'mfui-SplitView__rightPanel', rightPanelProps?.className), style: computedRightPanelStyle, children: rightPanelSlot }))] }));
213
215
  });
@@ -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
  },
@@ -42,6 +42,7 @@ export * from './RadioGroup';
42
42
  export * from './SearchBox';
43
43
  export * from './SectionMessage';
44
44
  export * from './SelectBox';
45
+ export * from './Sidebar';
45
46
  export * from './SidePane';
46
47
  export * from './Skeleton';
47
48
  export * from './SplitView';
package/dist/src/index.js CHANGED
@@ -42,6 +42,7 @@ export * from './RadioGroup';
42
42
  export * from './SearchBox';
43
43
  export * from './SectionMessage';
44
44
  export * from './SelectBox';
45
+ export * from './Sidebar';
45
46
  export * from './SidePane';
46
47
  export * from './Skeleton';
47
48
  export * from './SplitView';
@@ -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';
@@ -98,3 +98,4 @@ export * from './toggle-switch-slot-recipe.js';
98
98
  export * from './navigation-list-slot-recipe.js';
99
99
  export * from './navigation-list-item-slot-recipe.js';
100
100
  export * from './form-footer-slot-recipe.js';
101
+ export * from './sidebar-slot-recipe.js';
@@ -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
+ });