@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.
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 +3 -1
  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 +346 -6
  39. package/dist/tsconfig.build.tsbuildinfo +1 -1
  40. package/package.json +3 -3
@@ -19,7 +19,7 @@ import { DropdownMenuHeading } from './DropdownMenuHeading';
19
19
  * </DropdownMenu>
20
20
  * ```
21
21
  */
22
- export declare function DropdownMenu({ icon, label, children, triggerProps, menuPopoverProps, disabled, targetDOMNode, enableAutoUnmount, onOpenStateChanged, onBlur, }: DropdownMenuProps<ReactElement | undefined>): import("react/jsx-runtime").JSX.Element;
22
+ export declare function DropdownMenu({ icon, label, children, triggerProps, tooltipLabel, tooltipPlacement, triggerMiddleTruncate, triggerTypographyVariant, menuPopoverProps, disabled, targetDOMNode, enableAutoUnmount, onOpenStateChanged, onBlur, }: DropdownMenuProps<ReactElement | undefined>): import("react/jsx-runtime").JSX.Element;
23
23
  export declare namespace DropdownMenu {
24
24
  var Divider: typeof DropdownMenuDivider;
25
25
  var Item: typeof DropdownMenuItem;
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useCallback, useState } from 'react';
4
- import { Button } from '../Button';
4
+ import { DropdownMenuButtonTrigger } from './DropdownMenuButtonTrigger';
5
5
  import { DropdownMenuContainer } from './DropdownMenuContainer';
6
6
  import { DropdownMenuDivider } from './DropdownMenuDivider';
7
7
  import { DropdownMenuItem } from './DropdownMenuItem';
@@ -27,7 +27,7 @@ import { DropdownMenuHeading } from './DropdownMenuHeading';
27
27
  * </DropdownMenu>
28
28
  * ```
29
29
  */
30
- export function DropdownMenu({ icon, label, children, triggerProps, menuPopoverProps, disabled, targetDOMNode, enableAutoUnmount, onOpenStateChanged, onBlur, }) {
30
+ export function DropdownMenu({ icon, label, children, triggerProps, tooltipLabel, tooltipPlacement, triggerMiddleTruncate, triggerTypographyVariant, menuPopoverProps, disabled, targetDOMNode, enableAutoUnmount, onOpenStateChanged, onBlur, }) {
31
31
  const [menuDefaultFocusPosition, setMenuDefaultFocusPosition] = useState('first');
32
32
  const handleTriggerKeyDownLocal = useCallback((event) => {
33
33
  switch (event.key) {
@@ -49,7 +49,7 @@ export function DropdownMenu({ icon, label, children, triggerProps, menuPopoverP
49
49
  handleTriggerKeyDownLocal(event);
50
50
  handleTriggerKeyDown(event);
51
51
  };
52
- return icon ? (_jsx(IconButton, { ref: setTriggerRef, "aria-expanded": isPopoverOpen, "aria-haspopup": "menu", disabled: disabled, onKeyDown: handleKeyDownCommon, onBlur: handleTriggerBlur, onClick: togglePopover, ...triggerProps, "aria-label": label, children: icon })) : (_jsx(Button, { ref: setTriggerRef, "aria-expanded": isPopoverOpen, "aria-haspopup": "menu", disabled: disabled, onKeyDown: handleKeyDownCommon, onBlur: handleTriggerBlur, onClick: togglePopover, ...triggerProps, isDropdownTrigger: true, children: label }));
52
+ return icon ? (_jsx(IconButton, { ref: setTriggerRef, "aria-expanded": isPopoverOpen, "aria-haspopup": "menu", disabled: disabled, onKeyDown: handleKeyDownCommon, onBlur: handleTriggerBlur, onClick: togglePopover, ...triggerProps, "aria-label": label, children: icon })) : (_jsx(DropdownMenuButtonTrigger, { label: label, tooltipLabel: tooltipLabel, tooltipPlacement: tooltipPlacement, triggerMiddleTruncate: triggerMiddleTruncate, triggerTypographyVariant: triggerTypographyVariant, triggerProps: triggerProps, disabled: disabled, isPopoverOpen: isPopoverOpen, setTriggerRef: setTriggerRef, togglePopover: togglePopover, handleTriggerKeyDown: handleKeyDownCommon, handleTriggerBlur: handleTriggerBlur }));
53
53
  }, renderContent: ({ closePopover }) => (_jsx(DropdownMenuProvider, { value: { closeMenuPopover: closePopover }, children: _jsx(DropdownMenuContainer, { defaultFocusItem: menuDefaultFocusPosition, onClose: closePopover, children: children }) })), enableAutoFocusOnPopover: false, enableAutoUnmount: enableAutoUnmount, contentProps: {
54
54
  className: cx(classes.popover, menuPopoverProps?.className),
55
55
  }, onBlur: onBlur, onOpenStateChanged: onOpenStateChanged }));
@@ -2,6 +2,47 @@ import { type ComponentPropsWithoutRef, type ReactElement, type ReactNode } from
2
2
  import { type ButtonProps } from '../Button';
3
3
  import { type IconButtonProps } from '../IconButton';
4
4
  import { type PopoverProps } from '../Popover';
5
+ import { type TooltipProps } from '../Tooltip';
6
+ import { type TypographyProps } from '../Typography';
7
+ /**
8
+ * The props of the default Button-based trigger that are managed by the DropdownMenu itself
9
+ * (as opposed to `triggerProps`, which is forwarded to the underlying Button).
10
+ *
11
+ * These describe trigger-presentation concerns that the Button cannot express on its own:
12
+ * a describing tooltip and middle truncation of the label.
13
+ */
14
+ export type DropdownMenuButtonTriggerProps = {
15
+ /**
16
+ * The tooltip label describing what the trigger is (e.g. `"事業者"`).
17
+ * Shown on hover/focus and wired to the trigger via `aria-describedby` for screen readers.
18
+ * When omitted, no tooltip is shown.
19
+ */
20
+ tooltipLabel?: string;
21
+ /**
22
+ * The placement of the trigger tooltip (only applied when `tooltipLabel` is set).
23
+ *
24
+ * Accepts a single placement (e.g. `"top"`) for fixed positioning, or an array
25
+ * (e.g. `["top", "bottom"]`) for priority-based positioning with fallbacks.
26
+ * When omitted, the tooltip is placed automatically.
27
+ */
28
+ tooltipPlacement?: TooltipProps['placement'];
29
+ /**
30
+ * Whether the trigger label is middle-truncated (e.g. `"Money…ward"`) when it overflows
31
+ * the available width. When omitted, the label is not truncated by the component.
32
+ *
33
+ * @default false
34
+ */
35
+ triggerMiddleTruncate?: boolean;
36
+ /**
37
+ * The Typography variant of the trigger label. Use this when the label needs a different
38
+ * type style than the default (e.g. `"condensedControlLabel"`).
39
+ *
40
+ * Only applied when `tooltipLabel` or `triggerMiddleTruncate` is set.
41
+ *
42
+ * @default 'controlLabel'
43
+ */
44
+ triggerTypographyVariant?: TypographyProps['variant'];
45
+ };
5
46
  type DropdownMenuCommonProps = {
6
47
  /**
7
48
  * The list of the dropdown menu.
@@ -26,7 +67,7 @@ type DropdownMenuCommonProps = {
26
67
  * If the icon prop is provided, the label prop will be set as the aria-label of the icon button.
27
68
  * If the icon prop is not provided, the label prop will be set as the label of the button.
28
69
  */
29
- label: string;
70
+ label?: string;
30
71
  /**
31
72
  * The properties for the popover content panel.
32
73
  *
@@ -77,7 +118,14 @@ type DropdownMenuCommonProps = {
77
118
  */
78
119
  onBlur?: (event: React.FocusEvent<HTMLElement>) => void;
79
120
  };
80
- type DropdownMenuPropsWithIcon = {
121
+ type DropdownMenuButtonTriggerPropsNever = {
122
+ [K in keyof DropdownMenuButtonTriggerProps]?: undefined;
123
+ };
124
+ type DropdownMenuPropsWithIcon = DropdownMenuButtonTriggerPropsNever & {
125
+ /**
126
+ * The aria-label of the trigger icon button of the dropdown menu.
127
+ */
128
+ label: string;
81
129
  icon: ReactElement;
82
130
  /**
83
131
  * Button component and the IconButton component.
@@ -90,6 +138,10 @@ type DropdownMenuPropsWithIcon = {
90
138
  triggerProps?: Pick<IconButtonProps, 'outlined' | 'className'>;
91
139
  };
92
140
  type DropdownMenuPropsWithoutIcon = {
141
+ /**
142
+ * The label of the trigger button of the dropdown menu.
143
+ */
144
+ label: string;
93
145
  icon?: undefined;
94
146
  /**
95
147
  * Button component and the IconButton component.
@@ -97,9 +149,9 @@ type DropdownMenuPropsWithoutIcon = {
97
149
  * Use this prop if you need to customize the style of the trigger button.
98
150
  *
99
151
  * If the icon prop is not provided, the following props are acceptable as props of the Button component:
100
- * 'size' | 'priority' | 'destructive' | 'leftIcon' | 'loading' | 'className'
152
+ * 'size' | 'priority' | 'destructive' | 'leftIcon' | 'loading' | 'className' | 'isDropdownTrigger'
101
153
  */
102
- triggerProps?: Pick<ButtonProps, 'size' | 'priority' | 'destructive' | 'leftIcon' | 'loading' | 'className'>;
103
- };
154
+ triggerProps?: Pick<ButtonProps, 'size' | 'priority' | 'destructive' | 'leftIcon' | 'loading' | 'className' | 'isDropdownTrigger'>;
155
+ } & DropdownMenuButtonTriggerProps;
104
156
  export type DropdownMenuProps<IconType = undefined> = DropdownMenuCommonProps & (IconType extends ReactElement ? DropdownMenuPropsWithIcon : DropdownMenuPropsWithoutIcon) & Pick<PopoverProps, 'targetDOMNode' | 'enableAutoUnmount'>;
105
157
  export {};
@@ -0,0 +1,46 @@
1
+ import { type FocusEventHandler, type KeyboardEventHandler, type SyntheticEvent } from 'react';
2
+ import { type ButtonProps } from '../Button';
3
+ import { type DropdownMenuButtonTriggerProps } from './DropdownMenu.types';
4
+ type DropdownMenuButtonTriggerComponentProps = DropdownMenuButtonTriggerProps & {
5
+ /**
6
+ * The accessible label / visible text of the trigger button.
7
+ */
8
+ label: string;
9
+ /**
10
+ * Props forwarded to the underlying Button for design customization.
11
+ */
12
+ triggerProps?: Pick<ButtonProps, 'size' | 'priority' | 'destructive' | 'leftIcon' | 'loading' | 'className' | 'isDropdownTrigger'>;
13
+ /**
14
+ * The disabled state of the trigger.
15
+ */
16
+ disabled?: boolean;
17
+ /**
18
+ * The current open state of the dropdown menu, used for `aria-expanded`.
19
+ */
20
+ isPopoverOpen: boolean;
21
+ /**
22
+ * Ref setter for the trigger element so the popover can position itself.
23
+ */
24
+ setTriggerRef: (element: HTMLElement | null) => void;
25
+ /**
26
+ * Toggles the open state of the dropdown menu.
27
+ */
28
+ togglePopover: (event?: SyntheticEvent) => void;
29
+ /**
30
+ * Keyboard handler for the trigger (ArrowUp / ArrowDown focus behavior already merged in).
31
+ */
32
+ handleTriggerKeyDown: KeyboardEventHandler<HTMLElement>;
33
+ /**
34
+ * Blur handler for the trigger with smart blur detection.
35
+ */
36
+ handleTriggerBlur: FocusEventHandler<HTMLElement>;
37
+ };
38
+ /**
39
+ * The default Button-based trigger of the DropdownMenu.
40
+ *
41
+ * On top of the plain Button trigger, it wires up the optional trigger features that are owned by
42
+ * the DropdownMenu rather than the Button: a describing tooltip (`tooltipLabel`) and middle
43
+ * truncation of the label (`triggerMiddleTruncate`). Design customization stays on `triggerProps`.
44
+ */
45
+ export declare function DropdownMenuButtonTrigger({ label, tooltipLabel, tooltipPlacement, triggerMiddleTruncate, triggerTypographyVariant, triggerProps, disabled, isPopoverOpen, setTriggerRef, togglePopover, handleTriggerKeyDown, handleTriggerBlur, }: DropdownMenuButtonTriggerComponentProps): import("react/jsx-runtime").JSX.Element;
46
+ export {};
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useRef } from 'react';
4
+ import { Button } from '../Button';
5
+ import { Tooltip } from '../Tooltip';
6
+ import { Typography } from '../Typography';
7
+ import { useMiddleTruncatedText } from '../utilities/dom/useMiddleTruncatedText';
8
+ const TRUNCATED_LABEL_STYLE = {
9
+ overflow: 'hidden',
10
+ textOverflow: 'ellipsis',
11
+ whiteSpace: 'nowrap',
12
+ flex: '1 0 0',
13
+ minWidth: '1px',
14
+ color: 'inherit',
15
+ };
16
+ // The tooltip wraps the trigger Button (so focus on the button bubbles up to the tooltip and shows
17
+ // it on keyboard focus). The wrapper must not shrink the full-width trigger, hence `width: 100%`.
18
+ const TOOLTIP_WRAPPER_STYLE = {
19
+ display: 'flex',
20
+ minWidth: 0,
21
+ };
22
+ /**
23
+ * The default Button-based trigger of the DropdownMenu.
24
+ *
25
+ * On top of the plain Button trigger, it wires up the optional trigger features that are owned by
26
+ * the DropdownMenu rather than the Button: a describing tooltip (`tooltipLabel`) and middle
27
+ * truncation of the label (`triggerMiddleTruncate`). Design customization stays on `triggerProps`.
28
+ */
29
+ export function DropdownMenuButtonTrigger({ label, tooltipLabel, tooltipPlacement, triggerMiddleTruncate = false, triggerTypographyVariant = 'controlLabel', triggerProps, disabled, isPopoverOpen, setTriggerRef, togglePopover, handleTriggerKeyDown, handleTriggerBlur, }) {
30
+ const labelRef = useRef(null);
31
+ // The ref is only attached when middle truncation is enabled, so the hook is a no-op otherwise.
32
+ const truncatedLabel = useMiddleTruncatedText(label, labelRef);
33
+ const sharedButtonProps = {
34
+ ref: setTriggerRef,
35
+ 'aria-expanded': isPopoverOpen,
36
+ 'aria-haspopup': 'menu',
37
+ disabled,
38
+ onKeyDown: handleTriggerKeyDown,
39
+ onBlur: handleTriggerBlur,
40
+ onClick: togglePopover,
41
+ };
42
+ // The label needs custom rendering (Typography / truncation span) only when one of the managed
43
+ // trigger features is requested. Otherwise the plain string label is passed to the Button,
44
+ // preserving the existing default-trigger behavior.
45
+ const needsCustomLabel = tooltipLabel != null || triggerMiddleTruncate;
46
+ if (!needsCustomLabel) {
47
+ return (_jsx(Button, { isDropdownTrigger: true, ...sharedButtonProps, ...triggerProps, children: label }));
48
+ }
49
+ const trigger = (_jsx(Button, { isDropdownTrigger: true, ...sharedButtonProps, ...triggerProps, "aria-label": label, children: _jsx(Typography, { variant: triggerTypographyVariant, style: TRUNCATED_LABEL_STYLE, children: triggerMiddleTruncate ? _jsx("span", { ref: labelRef, children: truncatedLabel }) : label }) }));
50
+ if (tooltipLabel == null) {
51
+ return trigger;
52
+ }
53
+ // Wrap the trigger (not its inner label) so the tooltip opens on keyboard focus of the button and
54
+ // its content is wired to the button via `aria-describedby` automatically by the Tooltip.
55
+ return (_jsx(Tooltip, { content: tooltipLabel, placement: tooltipPlacement, style: TOOLTIP_WRAPPER_STYLE, children: trigger }));
56
+ }
@@ -0,0 +1,63 @@
1
+ import { type SidebarProps } from './Sidebar.types';
2
+ import { SidebarServiceMenu } from './SidebarServiceMenu';
3
+ import { SidebarTenantMenu } from './SidebarTenantMenu';
4
+ import { SidebarUserMenu } from './SidebarUserMenu';
5
+ import { SidebarNavigationItem } from './SidebarNavigationItem';
6
+ declare function SidebarBase({ className, logoSlot, mainNavigationItems, extraNavigationSlot, footerSlot, serviceMenuSlot, footerIconMenuSlot, tenantSlot, userSlot, minWidth, maxWidth, defaultWidth, onWidthChange, mainNavigationLabel, extraNavigationLabel, resizeHandleLabel, }: SidebarProps): import("react/jsx-runtime").JSX.Element;
7
+ /**
8
+ * The sidebar component.
9
+ * A navigation layout component that combines a logo header, a navigation area,
10
+ * and a footer with icon menu and user information.
11
+ *
12
+ * **EXPERIMENTAL**: This component is experimental and may be changed or removed in the future.
13
+ *
14
+ * The width is resizable via the drag handle on the right edge. See the `WidthPersistence` story for
15
+ * an example of persisting the width across reloads via `onWidthChange` and `defaultWidth`.
16
+ *
17
+ * The following composition components are provided as sub-components:
18
+ * - `Sidebar.NavigationItem` — a single navigation item; `mainNavigationItems` renders these for you,
19
+ * but it can also be used directly inside `extraNavigationSlot`.
20
+ * - `Sidebar.ServiceMenu` — the footer service menu trigger, intended to wrap a list of services.
21
+ * - `Sidebar.TenantMenu` — the footer tenant (business) name and its menu.
22
+ * - `Sidebar.UserMenu` — the footer user (account) name and its menu.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * <Sidebar
27
+ * logoSlot={<YourLogo />}
28
+ * defaultWidth={savedWidth}
29
+ * onWidthChange={(width) => persistWidth(width)}
30
+ * mainNavigationItems={[
31
+ * { label: 'Home', icon: <HomeIcon />, href: '/', isCurrent: true },
32
+ * { label: 'Search', icon: <SearchIcon />, onClick: handleSearch },
33
+ * ]}
34
+ * serviceMenuSlot={
35
+ * <Sidebar.ServiceMenu label="サービス" icon={<ApplicationsIcon />}>
36
+ * <DropdownMenu.Item href="https://example.com">マイページ</DropdownMenu.Item>
37
+ * </Sidebar.ServiceMenu>
38
+ * }
39
+ * footerIconMenuSlot={<YourIconButtons />}
40
+ * tenantSlot={
41
+ * <Sidebar.TenantMenu name="株式会社サンプル" tooltipLabel="事業者">
42
+ * <DropdownMenu.Item href="https://example.com">事業者設定</DropdownMenu.Item>
43
+ * </Sidebar.TenantMenu>
44
+ * }
45
+ * userSlot={
46
+ * <Sidebar.UserMenu name="山田 太郎" tooltipLabel="アカウント">
47
+ * <DropdownMenu.Item onClick={handleLogout}>ログアウト</DropdownMenu.Item>
48
+ * </Sidebar.UserMenu>
49
+ * }
50
+ * />
51
+ * ```
52
+ */
53
+ export declare const Sidebar: typeof SidebarBase & {
54
+ /** @see {@link SidebarNavigationItem} */
55
+ NavigationItem: typeof SidebarNavigationItem;
56
+ /** @see {@link SidebarServiceMenu} */
57
+ ServiceMenu: typeof SidebarServiceMenu;
58
+ /** @see {@link SidebarTenantMenu} */
59
+ TenantMenu: typeof SidebarTenantMenu;
60
+ /** @see {@link SidebarUserMenu} */
61
+ UserMenu: typeof SidebarUserMenu;
62
+ };
63
+ export {};
@@ -0,0 +1,82 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cx } from '../../styled-system/css';
4
+ import { sidebarSlotRecipe } from '../../styled-system/recipes';
5
+ import { SidebarServiceMenu } from './SidebarServiceMenu';
6
+ import { SidebarTenantMenu } from './SidebarTenantMenu';
7
+ import { SidebarUserMenu } from './SidebarUserMenu';
8
+ import { SidebarNavigationItem } from './SidebarNavigationItem';
9
+ import { useSidebarResize } from './hooks/useSidebarResize';
10
+ function SidebarBase({ className, logoSlot, mainNavigationItems, extraNavigationSlot, footerSlot, serviceMenuSlot, footerIconMenuSlot, tenantSlot, userSlot, minWidth, maxWidth, defaultWidth, onWidthChange, mainNavigationLabel = 'メインナビゲーション', extraNavigationLabel = '追加ナビゲーション', resizeHandleLabel = 'サイドバーの幅を調整', }) {
11
+ const classes = sidebarSlotRecipe();
12
+ const { width, minWidth: resolvedMinWidth, maxWidth: resolvedMaxWidth, rootRef, isDragging, handlePointerDown, handleKeyDown, } = useSidebarResize({
13
+ minWidth,
14
+ maxWidth,
15
+ defaultWidth,
16
+ onWidthChange,
17
+ });
18
+ return (_jsxs("div", { ref: rootRef, className: cx(classes.root, 'mfui-Sidebar__root', className), style: { width: `${String(width)}px` }, "data-mfui-dragging": isDragging ? true : undefined, children: [_jsx("header", { className: cx(classes.header, 'mfui-Sidebar__header'), children: _jsx("div", { className: cx(classes.leftPane, 'mfui-Sidebar__leftPane'), children: _jsx("div", { className: cx(classes.logo, 'mfui-Sidebar__logo'), children: logoSlot }) }) }), _jsxs("div", { className: cx(classes.navigationWrapper, 'mfui-Sidebar__navigationWrapper'), children: [mainNavigationItems && mainNavigationItems.length > 0 ? (_jsx("nav", { "aria-label": mainNavigationLabel, className: cx(classes.mainNavigation, 'mfui-Sidebar__mainNavigation'), children: _jsx("ul", { className: cx(classes.navigationList, 'mfui-Sidebar__navigationList'), children: mainNavigationItems.map((item, index) => {
19
+ if ('type' in item) {
20
+ return (_jsx("li", { "aria-hidden": "true", className: cx(classes.navigationDivider, 'mfui-Sidebar__navigationDivider') }, index));
21
+ }
22
+ return _jsx(SidebarNavigationItem, { ...item }, index);
23
+ }) }) })) : null, extraNavigationSlot ? (_jsx("nav", { "aria-label": extraNavigationLabel, className: cx(classes.extraNavigation, 'mfui-Sidebar__extraNavigation'), children: extraNavigationSlot })) : null] }), _jsxs("div", { className: cx(classes.footer, 'mfui-Sidebar__footer'), children: [footerSlot ? _jsx("div", { className: cx(classes.footerSlot, 'mfui-Sidebar__footerSlot'), children: footerSlot }) : null, serviceMenuSlot ? (_jsx("div", { className: cx(classes.serviceMenu, 'mfui-Sidebar__serviceMenu'), children: serviceMenuSlot })) : null, _jsx("div", { className: cx(classes.footerIconMenu, 'mfui-Sidebar__footerIconMenu'), children: footerIconMenuSlot }), tenantSlot || userSlot ? (_jsxs("div", { className: cx(classes.footerUserIdMenu, 'mfui-Sidebar__footerUserIdMenu'), children: [tenantSlot ? (_jsx("div", { className: cx(classes.footerUserIdMenuItem, 'mfui-Sidebar__footerUserIdMenuItem'), children: tenantSlot })) : null, userSlot ? (_jsx("div", { className: cx(classes.footerUserIdMenuItem, 'mfui-Sidebar__footerUserIdMenuItem'), children: userSlot })) : null] })) : null] }), _jsx("div", { "data-mfui-handle": true, "data-mfui-dragging": isDragging ? true : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": resizeHandleLabel, "aria-valuenow": width, "aria-valuemin": resolvedMinWidth, "aria-valuemax": resolvedMaxWidth,
24
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
25
+ tabIndex: 0, className: cx(classes.handle, 'mfui-Sidebar__handle'), onPointerDown: handlePointerDown, onKeyDown: handleKeyDown })] }));
26
+ }
27
+ /**
28
+ * The sidebar component.
29
+ * A navigation layout component that combines a logo header, a navigation area,
30
+ * and a footer with icon menu and user information.
31
+ *
32
+ * **EXPERIMENTAL**: This component is experimental and may be changed or removed in the future.
33
+ *
34
+ * The width is resizable via the drag handle on the right edge. See the `WidthPersistence` story for
35
+ * an example of persisting the width across reloads via `onWidthChange` and `defaultWidth`.
36
+ *
37
+ * The following composition components are provided as sub-components:
38
+ * - `Sidebar.NavigationItem` — a single navigation item; `mainNavigationItems` renders these for you,
39
+ * but it can also be used directly inside `extraNavigationSlot`.
40
+ * - `Sidebar.ServiceMenu` — the footer service menu trigger, intended to wrap a list of services.
41
+ * - `Sidebar.TenantMenu` — the footer tenant (business) name and its menu.
42
+ * - `Sidebar.UserMenu` — the footer user (account) name and its menu.
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * <Sidebar
47
+ * logoSlot={<YourLogo />}
48
+ * defaultWidth={savedWidth}
49
+ * onWidthChange={(width) => persistWidth(width)}
50
+ * mainNavigationItems={[
51
+ * { label: 'Home', icon: <HomeIcon />, href: '/', isCurrent: true },
52
+ * { label: 'Search', icon: <SearchIcon />, onClick: handleSearch },
53
+ * ]}
54
+ * serviceMenuSlot={
55
+ * <Sidebar.ServiceMenu label="サービス" icon={<ApplicationsIcon />}>
56
+ * <DropdownMenu.Item href="https://example.com">マイページ</DropdownMenu.Item>
57
+ * </Sidebar.ServiceMenu>
58
+ * }
59
+ * footerIconMenuSlot={<YourIconButtons />}
60
+ * tenantSlot={
61
+ * <Sidebar.TenantMenu name="株式会社サンプル" tooltipLabel="事業者">
62
+ * <DropdownMenu.Item href="https://example.com">事業者設定</DropdownMenu.Item>
63
+ * </Sidebar.TenantMenu>
64
+ * }
65
+ * userSlot={
66
+ * <Sidebar.UserMenu name="山田 太郎" tooltipLabel="アカウント">
67
+ * <DropdownMenu.Item onClick={handleLogout}>ログアウト</DropdownMenu.Item>
68
+ * </Sidebar.UserMenu>
69
+ * }
70
+ * />
71
+ * ```
72
+ */
73
+ export const Sidebar = Object.assign(SidebarBase, {
74
+ /** @see {@link SidebarNavigationItem} */
75
+ NavigationItem: SidebarNavigationItem,
76
+ /** @see {@link SidebarServiceMenu} */
77
+ ServiceMenu: SidebarServiceMenu,
78
+ /** @see {@link SidebarTenantMenu} */
79
+ TenantMenu: SidebarTenantMenu,
80
+ /** @see {@link SidebarUserMenu} */
81
+ UserMenu: SidebarUserMenu,
82
+ });
@@ -0,0 +1,143 @@
1
+ import { type ComponentPropsWithoutRef, type ElementType, type ReactElement, type ReactNode } from 'react';
2
+ type AnchorNavigationItemProps = {
3
+ /**
4
+ * The URL to navigate to when the item is clicked.
5
+ * When provided, the item is rendered as an anchor (`<a>`) element.
6
+ */
7
+ href: string;
8
+ /**
9
+ * The target attribute for the anchor element.
10
+ */
11
+ target?: string;
12
+ /**
13
+ * Custom link component to use instead of the default anchor element.
14
+ * Useful for client-side routing libraries (e.g. next/link).
15
+ */
16
+ customLinkComponent?: ElementType;
17
+ onClick?: undefined;
18
+ };
19
+ type ButtonNavigationItemProps = {
20
+ href?: undefined;
21
+ target?: undefined;
22
+ customLinkComponent?: undefined;
23
+ /**
24
+ * The click handler for the navigation item.
25
+ * When provided without `href`, the item is rendered as a button (`<button>`) element.
26
+ */
27
+ onClick: ComponentPropsWithoutRef<'button'>['onClick'];
28
+ };
29
+ export type SidebarNavigationItem = {
30
+ /**
31
+ * The label of the navigation item.
32
+ */
33
+ label: string;
34
+ /**
35
+ * The icon of the navigation item.
36
+ */
37
+ icon: ReactElement;
38
+ /**
39
+ * Whether the navigation item is the current page.
40
+ */
41
+ isCurrent?: boolean;
42
+ /**
43
+ * Maximum number of lines before truncating with an ellipsis.
44
+ * When the label is truncated, a tooltip with the full text is shown automatically.
45
+ *
46
+ * @default 1
47
+ */
48
+ maxLines?: number;
49
+ } & (AnchorNavigationItemProps | ButtonNavigationItemProps);
50
+ /**
51
+ * A visual divider that separates groups of navigation items.
52
+ */
53
+ export type SidebarNavigationDivider = {
54
+ type: 'divider';
55
+ };
56
+ export type SidebarNavigationEntry = SidebarNavigationItem | SidebarNavigationDivider;
57
+ export type SidebarProps = {
58
+ /**
59
+ * Additional class names to apply to the component.
60
+ */
61
+ className?: string;
62
+ /**
63
+ * Elements to be displayed on the logo area at the top of the sidebar.
64
+ */
65
+ logoSlot: ReactNode;
66
+ /**
67
+ * Navigation items to be displayed in the primary navigation area.
68
+ * Use `{ type: 'divider' }` to insert a visual separator between groups of items.
69
+ */
70
+ mainNavigationItems?: SidebarNavigationEntry[];
71
+ /**
72
+ * Elements to be displayed in the secondary navigation area, below the primary navigation.
73
+ * This slot accepts any content.
74
+ */
75
+ extraNavigationSlot?: ReactNode;
76
+ /**
77
+ * Elements to be displayed above the service menu in the footer.
78
+ * This slot accepts any content.
79
+ */
80
+ footerSlot?: ReactNode;
81
+ /**
82
+ * Elements to be displayed as the service menu in the footer.
83
+ * Intended for a dropdown menu listing available services.
84
+ */
85
+ serviceMenuSlot: ReactNode;
86
+ /**
87
+ * Elements to be displayed in the footer icon menu area.
88
+ * Icon buttons with dropdowns should open upward (placement: top).
89
+ */
90
+ footerIconMenuSlot: ReactNode;
91
+ /**
92
+ * Elements to be displayed below the footer icon menu.
93
+ * Intended for the tenant (business) name and its menu.
94
+ */
95
+ tenantSlot?: ReactNode;
96
+ /**
97
+ * Elements to be displayed below the tenant area.
98
+ * Intended for the user (account) name and its menu.
99
+ */
100
+ userSlot?: ReactNode;
101
+ /**
102
+ * Minimum width in pixels when resizing.
103
+ *
104
+ * @default 180
105
+ */
106
+ minWidth?: number;
107
+ /**
108
+ * Maximum width in pixels when resizing.
109
+ *
110
+ * @default 280
111
+ */
112
+ maxWidth?: number;
113
+ /**
114
+ * The initial (or server-persisted) width in pixels.
115
+ * Use this to restore the sidebar width from your own storage (e.g. a server-side user preference).
116
+ * When provided, the sidebar opens at this width clamped to `[minWidth, maxWidth]`.
117
+ */
118
+ defaultWidth?: number;
119
+ /**
120
+ * Called whenever the user finishes resizing the sidebar, with the new width in pixels.
121
+ * Use this to persist the value in your own storage (e.g. via an API call).
122
+ */
123
+ onWidthChange: (width: number) => void;
124
+ /**
125
+ * The accessible name for the primary navigation landmark.
126
+ *
127
+ * @default 'メインナビゲーション'
128
+ */
129
+ mainNavigationLabel?: string;
130
+ /**
131
+ * The accessible name for the secondary navigation landmark.
132
+ *
133
+ * @default '追加ナビゲーション'
134
+ */
135
+ extraNavigationLabel?: string;
136
+ /**
137
+ * The accessible name for the resize handle.
138
+ *
139
+ * @default 'サイドバーの幅を調整'
140
+ */
141
+ resizeHandleLabel?: string;
142
+ };
143
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { type SidebarNavigationItem as SidebarNavigationItemType } from './Sidebar.types';
2
+ export type SidebarNavigationItemProps = SidebarNavigationItemType;
3
+ /**
4
+ * A single navigation item shown in the Sidebar.
5
+ *
6
+ * Renders as an anchor (`<a>`) when `href` is provided, or a button (`<button>`) when `onClick` is provided.
7
+ * Long labels are truncated to `maxLines` with an automatic tooltip showing the full text.
8
+ */
9
+ export declare function SidebarNavigationItem({ label, icon, isCurrent, maxLines, ...rest }: SidebarNavigationItemProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cloneElement, useRef, useState } from 'react';
4
+ import { cx } from '../../styled-system/css';
5
+ import { sidebarSlotRecipe } from '../../styled-system/recipes';
6
+ import { FocusIndicator } from '../FocusIndicator';
7
+ import { Tooltip } from '../Tooltip';
8
+ import { Typography } from '../Typography';
9
+ import { useIsomorphicLayoutEffect } from '../utilities/react/useIsomorphicLayoutEffect';
10
+ /**
11
+ * A single navigation item shown in the Sidebar.
12
+ *
13
+ * Renders as an anchor (`<a>`) when `href` is provided, or a button (`<button>`) when `onClick` is provided.
14
+ * Long labels are truncated to `maxLines` with an automatic tooltip showing the full text.
15
+ */
16
+ export function SidebarNavigationItem({ label, icon, isCurrent, maxLines, ...rest }) {
17
+ const classes = sidebarSlotRecipe();
18
+ const labelRef = useRef(null);
19
+ const [isTruncated, setIsTruncated] = useState(false);
20
+ useIsomorphicLayoutEffect(() => {
21
+ const element = labelRef.current;
22
+ if (!element)
23
+ return;
24
+ const check = () => {
25
+ setIsTruncated(element.scrollHeight > element.clientHeight);
26
+ };
27
+ check();
28
+ const observer = new ResizeObserver(check);
29
+ observer.observe(element);
30
+ return () => {
31
+ observer.disconnect();
32
+ };
33
+ // Re-check when label/maxLines change: a content change may not alter the observed
34
+ // box size, so ResizeObserver alone would leave isTruncated stale.
35
+ }, [label, maxLines]);
36
+ const isAnchor = 'href' in rest && rest.href !== undefined;
37
+ const Tag = isAnchor
38
+ ? (rest.customLinkComponent ?? 'a')
39
+ : 'button';
40
+ const href = isAnchor ? rest.href : undefined;
41
+ const target = 'target' in rest ? rest.target : undefined;
42
+ const onClick = 'onClick' in rest ? rest.onClick : undefined;
43
+ return (_jsx("li", { className: cx(classes.navigationListItem, 'mfui-Sidebar__navigationListItem'), style: maxLines !== undefined ? { '--mfui-sidebar-max-lines': maxLines } : undefined, children: _jsx(Tooltip, { content: label, disabled: !isTruncated, placement: "right", children: _jsx(FocusIndicator, { children: _jsx(Tag, { "data-mfui-focusable": true, href: href, target: target, type: isAnchor ? undefined : 'button', "aria-current": isCurrent ? 'page' : undefined, className: cx(classes.navigationLink, 'mfui-Sidebar__navigationLink'), onClick: onClick, children: _jsxs("div", { className: cx(classes.navigationLinkInner, 'mfui-Sidebar__navigationLinkInner'), children: [_jsx("span", { className: cx(classes.navigationLinkIcon, 'mfui-Sidebar__navigationLinkIcon'), children: cloneElement(icon, {
44
+ width: 18,
45
+ height: 18,
46
+ 'aria-hidden': true,
47
+ }) }), _jsx(Typography, { ref: labelRef, variant: "controlLabel", className: cx(classes.navigationLinkLabel, 'mfui-Sidebar__navigationLinkLabel'), children: label })] }) }) }) }) }));
48
+ }
@@ -0,0 +1,23 @@
1
+ import { type ReactElement, type ReactNode } from 'react';
2
+ export type SidebarServiceMenuProps = {
3
+ /**
4
+ * The label shown in the service switcher trigger.
5
+ */
6
+ label: string;
7
+ /**
8
+ * The icon shown in the service switcher trigger.
9
+ */
10
+ icon: ReactElement;
11
+ /**
12
+ * The dropdown menu items (e.g. `DropdownMenu.Item` / `DropdownMenu.Divider`).
13
+ */
14
+ children: ReactNode;
15
+ };
16
+ /**
17
+ * The service switcher menu shown in the Sidebar footer.
18
+ *
19
+ * It renders a navigation-link styled trigger (matching the sidebar navigation items) that opens a
20
+ * dropdown of services. The trigger design and accessibility are fixed; pass the label, icon, and
21
+ * menu items only.
22
+ */
23
+ export declare function SidebarServiceMenu({ label, icon, children }: SidebarServiceMenuProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { DropdownMenu } from '../DropdownMenu';
4
+ /**
5
+ * The service switcher menu shown in the Sidebar footer.
6
+ *
7
+ * It renders a navigation-link styled trigger (matching the sidebar navigation items) that opens a
8
+ * dropdown of services. The trigger design and accessibility are fixed; pass the label, icon, and
9
+ * menu items only.
10
+ */
11
+ export function SidebarServiceMenu({ label, icon, children }) {
12
+ return (_jsx(DropdownMenu, { label: label, triggerProps: {
13
+ priority: 'tertiary',
14
+ size: 'small',
15
+ isDropdownTrigger: false,
16
+ leftIcon: icon,
17
+ // Styled via the `serviceMenu` slot's `& .mfui-Sidebar__serviceTrigger` descendant selector.
18
+ className: 'mfui-Sidebar__serviceTrigger',
19
+ }, menuPopoverProps: { allowedPlacements: ['top-start'] }, children: children }));
20
+ }