@moneyforward/mfui-components 3.25.1 → 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/DateTimeSelection/DatePicker/DatePickerCalendar/DatePickerCalendar.js +5 -3
- package/dist/src/DateTimeSelection/shared/BasePicker/BasePicker.d.ts +1 -1
- package/dist/src/DateTimeSelection/shared/BasePicker/BasePicker.js +7 -7
- package/dist/src/DateTimeSelection/shared/BasePicker/BasePicker.types.d.ts +11 -0
- 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/MultipleSelectBox/MultipleSelectBox.js +31 -23
- package/dist/src/SelectBox/SelectBox.js +25 -17
- 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,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
|
+
}
|
|
@@ -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
|
/**
|