@moneyforward/mfui-components 3.15.0 → 3.16.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/FormFooter/FormFooter.types.d.ts +2 -1
- package/dist/src/MultipleSelectBox/MultipleSelectBox.js +2 -2
- package/dist/src/MultipleSelectBox/MultipleSelectBoxTrigger/MultipleSelectBoxTrigger.js +1 -1
- package/dist/src/MultipleSelectBox/hooks/useApplyControls.d.ts +1 -0
- package/dist/src/MultipleSelectBox/hooks/useApplyControls.js +16 -0
- package/dist/src/SidePane/SidePane.d.ts +3 -0
- package/dist/src/SidePane/SidePane.js +4 -2
- package/dist/src/SidePane/SidePane.types.d.ts +14 -0
- package/dist/src/SubNavigation/SubNavigation.d.ts +1 -1
- package/dist/src/SubNavigation/SubNavigation.js +17 -15
- package/dist/src/SubNavigation/SubNavigation.types.d.ts +58 -0
- package/dist/src/SubNavigation/index.d.ts +1 -1
- package/dist/src/Tag/Tag.types.d.ts +17 -1
- package/dist/styled-system/recipes/form-footer-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/form-footer-slot-recipe.js +2 -1
- package/dist/styled-system/recipes/side-pane-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/side-pane-slot-recipe.js +8 -0
- package/dist/styled-system/recipes/sub-navigation-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/sub-navigation-slot-recipe.js +4 -0
- package/dist/styles.css +32 -17
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
-
export type FormFooterPosition = 'fixed' | 'stacking';
|
|
2
|
+
export type FormFooterPosition = 'fixed' | 'stacking' | 'sticky';
|
|
3
3
|
export type FormFooterSectionPosition = 'fill' | 'center';
|
|
4
4
|
export type FormFooterProps = {
|
|
5
5
|
/**
|
|
@@ -11,6 +11,7 @@ export type FormFooterProps = {
|
|
|
11
11
|
*
|
|
12
12
|
* - `"stacking"`: Displayed inline at the bottom of its container. Use for creation forms where preventing mid-session abandonment is important.
|
|
13
13
|
* - `"fixed"`: Pinned to the bottom of the viewport at all times with a separator border. Use for edit screens where users return frequently.
|
|
14
|
+
* - `"sticky"`: Sits inline below the last form field, and pins to the bottom of the nearest scrolling ancestor (e.g. inside `SidePane`) when the form overflows. Use inside scrollable containers such as `SidePane`.
|
|
14
15
|
*
|
|
15
16
|
* @default "stacking"
|
|
16
17
|
*/
|
|
@@ -59,7 +59,7 @@ placeholder, emptyMessage, disabled, invalid, targetDOMNode, name, onChange, val
|
|
|
59
59
|
onToggle: (nextValue) => {
|
|
60
60
|
onOpenStateChanged?.(nextValue);
|
|
61
61
|
if (nextValue && enableApplyControls) {
|
|
62
|
-
|
|
62
|
+
openWithSelectedOptions(localSelectedOptions);
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
65
|
onClose: () => {
|
|
@@ -107,7 +107,7 @@ placeholder, emptyMessage, disabled, invalid, targetDOMNode, name, onChange, val
|
|
|
107
107
|
});
|
|
108
108
|
// Disable infinite scroll when there's an error
|
|
109
109
|
const enableInfiniteScroll = baseEnabledInfiniteScroll && !infiniteScrollError;
|
|
110
|
-
const { temporaryValues, updateTemporaryValues, initializeTemporaryValues, handleCancelButtonClick, handleApplyButtonClick, } = useApplyControls({
|
|
110
|
+
const { temporaryValues, updateTemporaryValues, openWithSelectedOptions, initializeTemporaryValues, handleCancelButtonClick, handleApplyButtonClick, } = useApplyControls({
|
|
111
111
|
options: flattenedOptions,
|
|
112
112
|
onValuesChange: updateSelectedOptions,
|
|
113
113
|
closeOptionPanel,
|
|
@@ -33,7 +33,7 @@ placeholder, renderDisplayValue, updateSelectedValues, clearButtonProps, disable
|
|
|
33
33
|
return (_jsx(Typography, { variant: textVariant, className: cx(classes.placeholder, 'mfui-MultipleSelectBoxTrigger__placeholder'), "data-mfui-content": "multiple-select-box-trigger-placeholder", children: placeholder }));
|
|
34
34
|
}
|
|
35
35
|
if (showDisplayValueAsTag) {
|
|
36
|
-
return (_jsx("div", { className: cx(classes.tagList, 'mfui-MultipleSelectBoxTrigger__tagList'), "data-mfui-content": "multiple-select-box-trigger-selected-options", children: selectedOptions.map((option, index) => (_jsx(Tag, { className: cx(classes.tagItem, 'mfui-MultipleSelectBoxTrigger__tagItem'), label: option.label, onClose: (event) => {
|
|
36
|
+
return (_jsx("div", { className: cx(classes.tagList, 'mfui-MultipleSelectBoxTrigger__tagList'), "data-mfui-content": "multiple-select-box-trigger-selected-options", children: selectedOptions.map((option, index) => (_jsx(Tag, { className: cx(classes.tagItem, 'mfui-MultipleSelectBoxTrigger__tagItem'), label: option.label, disabled: disabled, onClose: (event) => {
|
|
37
37
|
event.stopPropagation();
|
|
38
38
|
onDeselectTag(option.value);
|
|
39
39
|
} }, option.value ?? `tag-${option.label}-${String(index)}`))) }));
|
|
@@ -23,6 +23,7 @@ type UseApplyControlsProps<T extends AllowedValueTypes = string, AdditionalProps
|
|
|
23
23
|
export declare function useApplyControls<T extends AllowedValueTypes = string, AdditionalProps extends Record<string, unknown> = Record<string, never>>({ options, onValuesChange, closeOptionPanel }: UseApplyControlsProps<T, AdditionalProps>): {
|
|
24
24
|
temporaryValues: Set<MultipleSelectBoxOption<T, AdditionalProps>["value"]>;
|
|
25
25
|
updateTemporaryValues: (values: MultipleSelectBoxOption<T, AdditionalProps>["value"][]) => void;
|
|
26
|
+
openWithSelectedOptions: (selectedOptions: MultipleSelectBoxOption<T, AdditionalProps>[]) => void;
|
|
26
27
|
initializeTemporaryValues: () => void;
|
|
27
28
|
handleCancelButtonClick: () => void;
|
|
28
29
|
handleApplyButtonClick: () => void;
|
|
@@ -33,6 +33,21 @@ export function useApplyControls({ options, onValuesChange, closeOptionPanel })
|
|
|
33
33
|
return newMap;
|
|
34
34
|
});
|
|
35
35
|
}, [options]);
|
|
36
|
+
// Initialize temporary state from full option objects when the panel opens.
|
|
37
|
+
// This must receive full option objects (not just value keys) so that previously-applied
|
|
38
|
+
// options are preserved even when they are not in the currently-visible filtered list.
|
|
39
|
+
const openWithSelectedOptions = useCallback((selectedOptions) => {
|
|
40
|
+
const newMap = new Map();
|
|
41
|
+
const newValues = new Set();
|
|
42
|
+
for (const option of selectedOptions) {
|
|
43
|
+
if (option.value != null) {
|
|
44
|
+
newMap.set(option.value, option);
|
|
45
|
+
newValues.add(option.value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
setTemporaryValues(newValues);
|
|
49
|
+
setTemporaryOptionsMap(newMap);
|
|
50
|
+
}, []);
|
|
36
51
|
// Initialize temporary selection state (called when the panel closes without applying)
|
|
37
52
|
const initializeTemporaryValues = useCallback(() => {
|
|
38
53
|
setTemporaryValues(new Set());
|
|
@@ -52,6 +67,7 @@ export function useApplyControls({ options, onValuesChange, closeOptionPanel })
|
|
|
52
67
|
return {
|
|
53
68
|
temporaryValues,
|
|
54
69
|
updateTemporaryValues,
|
|
70
|
+
openWithSelectedOptions,
|
|
55
71
|
initializeTemporaryValues,
|
|
56
72
|
handleCancelButtonClick,
|
|
57
73
|
handleApplyButtonClick,
|
|
@@ -17,6 +17,9 @@ export declare const SidePane: import("react").ForwardRefExoticComponent<{
|
|
|
17
17
|
enableModal?: boolean;
|
|
18
18
|
disableBackdropClose?: boolean;
|
|
19
19
|
enableAutoUnmount?: boolean;
|
|
20
|
+
formFooterProps?: Pick<import("..").FormFooterProps, "optionsSlot" | "actionsSlot"> & {
|
|
21
|
+
position?: Exclude<import("..").FormFooterProps["position"], "fixed">;
|
|
22
|
+
};
|
|
20
23
|
insideProps?: {
|
|
21
24
|
className?: string;
|
|
22
25
|
};
|
|
@@ -10,13 +10,14 @@ import { useSidePaneStateController } from './hooks/useSidePaneStateController';
|
|
|
10
10
|
import { useFocusTrap } from '../utilities/dom/useFocusTrap';
|
|
11
11
|
import { compatibleForwardRef } from '../utilities/react/compatibleForwardRef';
|
|
12
12
|
import { Portal, TargetDomNodeProvider, useAutomaticTargetDomNode } from '../Portal';
|
|
13
|
+
import { FormFooter } from '../FormFooter';
|
|
13
14
|
/**
|
|
14
15
|
* The general purpose SidePane component.
|
|
15
16
|
* This component extends the props of `<div>` element with `role="dialog"`.
|
|
16
17
|
*
|
|
17
18
|
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
|
|
18
19
|
*/
|
|
19
|
-
export const SidePane = compatibleForwardRef(({ open, title, renderHeading, onClose, className, children, actionSlot, position = 'right', enableModal = false, enableAutoUnmount = true, disableBackdropClose = false, closeButtonProps, targetDOMNode, insideProps, ...props }, ref) => {
|
|
20
|
+
export const SidePane = compatibleForwardRef(({ open, title, renderHeading, onClose, className, children, actionSlot, formFooterProps, position = 'right', enableModal = false, enableAutoUnmount = true, disableBackdropClose = false, closeButtonProps, targetDOMNode, insideProps, ...props }, ref) => {
|
|
20
21
|
const classes = sidePaneSlotRecipe({ position, nonModal: !enableModal });
|
|
21
22
|
const { localOpen, handleCloseSidePane, handleStopCloseSidePane, sidePaneElement, setSidePaneElement } = useSidePaneStateController({
|
|
22
23
|
open,
|
|
@@ -53,9 +54,10 @@ export const SidePane = compatibleForwardRef(({ open, title, renderHeading, onCl
|
|
|
53
54
|
}, [closeButtonProps?.isCollapseIcon, position]);
|
|
54
55
|
const targetElement = useAutomaticTargetDomNode(targetDOMNode, sidePaneElement);
|
|
55
56
|
const shouldRenderContent = enableAutoUnmount ? localOpen : true;
|
|
57
|
+
const resolvedFormFooterPosition = formFooterProps?.position ?? 'sticky';
|
|
56
58
|
const headingNode = title ? (_jsx(Heading, { variant: "pageHeading2", tag: "h2", children: title })) : null;
|
|
57
59
|
return (_jsx(TargetDomNodeProvider, { targetDomNode: sidePaneElement, children: shouldRenderContent ? (_jsx(Portal, { targetDOMNode: targetElement, children: _jsxs("div", { ref: setSidePaneElement, role: "dialog", ...props, className: cx(classes.root, 'mfui-SidePane__root', className), tabIndex: -1, style: {
|
|
58
60
|
...props.style,
|
|
59
61
|
...(!enableAutoUnmount && !localOpen && { display: 'none' }),
|
|
60
|
-
}, onKeyDown: handleOnKeyDown, children: [enableModal ? (_jsx("div", { "data-mfui-content": "backdrop", className: cx(classes.backdrop, 'mfui-SidePane__backdrop'), onClick: handleBackdropClick })) : null, _jsxs("div", { "data-mfui-content": "inside", className: cx(classes.inside, 'mfui-SidePane__inside', insideProps?.className), onClick: handleStopCloseSidePane, children: [_jsxs("header", { className: cx(classes.header, 'mfui-SidePane__header'), children: [renderHeading ? (renderHeading({ headingNode })) : (_jsx("div", { className: cx(classes.title, 'mfui-SidePane__title'), children: headingNode })), _jsxs("div", { className: cx(classes.actionSlotWrapper, 'mfui-SidePane__actionSlotWrapper'), children: [!!actionSlot && (_jsx("div", { className: cx(classes.actionSlot, 'mfui-SidePane__actionSlot'), children: actionSlot })), _jsx("div", { className: cx(classes.closeButtonWrapper, 'mfui-SidePane__closeButtonWrapper'), children: _jsx(IconButton, { "aria-label": closeButtonProps?.['aria-label'] ?? '閉じる', onClick: handleCloseSidePane, children: closeButtonIcon }) })] })] }),
|
|
62
|
+
}, onKeyDown: handleOnKeyDown, children: [enableModal ? (_jsx("div", { "data-mfui-content": "backdrop", className: cx(classes.backdrop, 'mfui-SidePane__backdrop'), onClick: handleBackdropClick })) : null, _jsxs("div", { "data-mfui-content": "inside", className: cx(classes.inside, 'mfui-SidePane__inside', insideProps?.className), onClick: handleStopCloseSidePane, children: [_jsxs("header", { className: cx(classes.header, 'mfui-SidePane__header'), children: [renderHeading ? (renderHeading({ headingNode })) : (_jsx("div", { className: cx(classes.title, 'mfui-SidePane__title'), children: headingNode })), _jsxs("div", { className: cx(classes.actionSlotWrapper, 'mfui-SidePane__actionSlotWrapper'), children: [!!actionSlot && (_jsx("div", { className: cx(classes.actionSlot, 'mfui-SidePane__actionSlot'), children: actionSlot })), _jsx("div", { className: cx(classes.closeButtonWrapper, 'mfui-SidePane__closeButtonWrapper'), children: _jsx(IconButton, { "aria-label": closeButtonProps?.['aria-label'] ?? '閉じる', onClick: handleCloseSidePane, children: closeButtonIcon }) })] })] }), _jsxs("main", { className: cx(classes.content, 'mfui-SidePane__content'), children: [_jsx("div", { className: cx(classes.contentBody, 'mfui-SidePane__contentBody'), children: children }), resolvedFormFooterPosition === 'stacking' ? (_jsx("div", { className: cx(classes.footer, 'mfui-SidePane__footer'), children: _jsx(FormFooter, { sectionPosition: "fill", ...formFooterProps, position: resolvedFormFooterPosition }) })) : null] }), formFooterProps && resolvedFormFooterPosition !== 'stacking' ? (_jsx("div", { className: cx(classes.footer, 'mfui-SidePane__footer'), children: _jsx(FormFooter, { sectionPosition: "fill", ...formFooterProps, position: resolvedFormFooterPosition }) })) : null] })] }) })) : null }));
|
|
61
63
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
2
|
import { type SidePaneSlotRecipeVariant } from '../../styled-system/recipes';
|
|
3
|
+
import { type FormFooterProps } from '../FormFooter';
|
|
3
4
|
export type OnCloseFunction = () => void;
|
|
4
5
|
export type SidePaneRenderOptions = {
|
|
5
6
|
/**
|
|
@@ -109,6 +110,19 @@ export type SidePaneProps = {
|
|
|
109
110
|
* @default true
|
|
110
111
|
*/
|
|
111
112
|
enableAutoUnmount?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Props to render a `FormFooter` inside the SidePane.
|
|
115
|
+
*
|
|
116
|
+
* - `position: 'sticky'` (default): the footer is always pinned to the pane bottom, outside the scrollable content area.
|
|
117
|
+
* - `position: 'stacking'`: the footer flows after the content inside the scroll area.
|
|
118
|
+
*
|
|
119
|
+
* `position: 'fixed'` is not supported here because it anchors to the viewport rather than the pane.
|
|
120
|
+
*
|
|
121
|
+
* @default undefined
|
|
122
|
+
*/
|
|
123
|
+
formFooterProps?: Pick<FormFooterProps, 'optionsSlot' | 'actionsSlot'> & {
|
|
124
|
+
position?: Exclude<FormFooterProps['position'], 'fixed'>;
|
|
125
|
+
};
|
|
112
126
|
/**
|
|
113
127
|
* Additional props to apply to the inside container element of the SidePane.
|
|
114
128
|
* This element wraps the header and content sections.
|
|
@@ -2,4 +2,4 @@ import { type SubNavigationProps } from './SubNavigation.types';
|
|
|
2
2
|
/**
|
|
3
3
|
* This component is for the navigation links under the main navigation.
|
|
4
4
|
*/
|
|
5
|
-
export declare function SubNavigation({ orientation, navigationItems, customLinkComponent, className, }: SubNavigationProps): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
export declare function SubNavigation({ orientation, navigationItems, customLinkComponent, renderNavigationLabel, headerSlot, className, }: SubNavigationProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { DisclosureBasicCollapsed, Lock } from '@moneyforward/mfui-icons-react';
|
|
3
3
|
import { cx } from '../../styled-system/css';
|
|
4
4
|
import { subNavigationSlotRecipe } from '../../styled-system/recipes';
|
|
@@ -7,10 +7,10 @@ import { Typography } from '../Typography';
|
|
|
7
7
|
/**
|
|
8
8
|
* This component is for the navigation links under the main navigation.
|
|
9
9
|
*/
|
|
10
|
-
export function SubNavigation({ orientation = 'vertical', navigationItems, customLinkComponent, className, }) {
|
|
10
|
+
export function SubNavigation({ orientation = 'vertical', navigationItems, customLinkComponent, renderNavigationLabel, headerSlot, className, }) {
|
|
11
11
|
const hasLabelIcon = navigationItems.some(({ labelIcon }) => !!labelIcon);
|
|
12
12
|
const classes = subNavigationSlotRecipe({ orientation, hasLabelIcon });
|
|
13
|
-
return (
|
|
13
|
+
return (_jsxs("nav", { className: cx(classes.root, 'mfui-SubNavigation__root', className), children: [headerSlot ? (_jsx("div", { className: cx(classes.subNavigationHeader, 'mfui-SubNavigation__subNavigationHeader'), children: headerSlot })) : null, _jsx("ul", { className: cx(classes.list, 'mfui-SubNavigation__list'), children: navigationItems.map((navigationItem, index) => (_jsx("li", { className: cx(classes.listItem, 'mfui-SubNavigation__listItem'), children: renderNavigationItem(navigationItem, customLinkComponent, classes, 0, renderNavigationLabel) }, index))) })] }));
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* Render a navigation item based on the following conditions:
|
|
@@ -28,25 +28,27 @@ export function SubNavigation({ orientation = 'vertical', navigationItems, custo
|
|
|
28
28
|
*
|
|
29
29
|
* @param nestLevel - The nest level of the navigation item.
|
|
30
30
|
*
|
|
31
|
+
* @param renderNavigationLabel - Custom render function for each navigation item's label.
|
|
32
|
+
*
|
|
31
33
|
* @returns The rendered navigation item.
|
|
32
34
|
*/
|
|
33
|
-
const renderNavigationItem = (navigationItem, customLinkComponent, classes, nestLevel) => {
|
|
35
|
+
const renderNavigationItem = (navigationItem, customLinkComponent, classes, nestLevel, renderNavigationLabel) => {
|
|
34
36
|
const { label, href, labelIcon, statusSlot, locked, lockIconProps, children } = navigationItem;
|
|
35
37
|
if (children === undefined) {
|
|
36
|
-
return (_jsx(NavigationLink, { navigationItem: navigationItem, customLinkComponent: customLinkComponent, classes: classes }));
|
|
38
|
+
return (_jsx(NavigationLink, { navigationItem: navigationItem, customLinkComponent: customLinkComponent, classes: classes, renderNavigationLabel: renderNavigationLabel }));
|
|
37
39
|
}
|
|
38
40
|
if (href) {
|
|
39
41
|
// The the parent item will be rendered as a link with separated disclosure icon button.
|
|
40
|
-
return (_jsxs(_Fragment, { children: [_jsx(NavigationLink, { navigationItem: navigationItem, customLinkComponent: customLinkComponent, classes: classes }), _jsx(NestedNavigationList, { summarySlot: _jsx("div", { "data-mfui-content": "sub-navigation-disclosure-icon", className: cx(classes.disclosureIcon, 'mfui-SubNavigation__disclosureIcon'), children: _jsx(DisclosureBasicCollapsed, { "aria-label": label }) }), nestedNavigationItems: children, customLinkComponent: customLinkComponent, classes: classes, nestLevel: nestLevel + 1 })] }));
|
|
42
|
+
return (_jsxs(_Fragment, { children: [_jsx(NavigationLink, { navigationItem: navigationItem, customLinkComponent: customLinkComponent, classes: classes, renderNavigationLabel: renderNavigationLabel }), _jsx(NestedNavigationList, { summarySlot: _jsx("div", { "data-mfui-content": "sub-navigation-disclosure-icon", className: cx(classes.disclosureIcon, 'mfui-SubNavigation__disclosureIcon'), children: _jsx(DisclosureBasicCollapsed, { "aria-label": label }) }), nestedNavigationItems: children, customLinkComponent: customLinkComponent, classes: classes, nestLevel: nestLevel + 1, renderNavigationLabel: renderNavigationLabel })] }));
|
|
41
43
|
}
|
|
42
44
|
// The the parent item will be rendered as a disclosure button with an icon.
|
|
43
|
-
return (_jsx(NestedNavigationList, { summarySlot: _jsxs(_Fragment, { children: [_jsx(NavigationLabelGroup, { label, labelIcon, locked, lockIconProps, statusSlot, classes }), _jsx("div", { "data-mfui-content": "sub-navigation-disclosure-icon", className: cx(classes.disclosureIcon, 'mfui-SubNavigation__disclosureIcon'), children: _jsx(DisclosureBasicCollapsed, { "aria-label": "" }) })] }), nestedNavigationItems: children, customLinkComponent: customLinkComponent, classes: classes, nestLevel: nestLevel + 1 }));
|
|
45
|
+
return (_jsx(NestedNavigationList, { summarySlot: _jsxs(_Fragment, { children: [_jsx(NavigationLabelGroup, { label, labelIcon, locked, lockIconProps, statusSlot, classes, renderNavigationLabel }), _jsx("div", { "data-mfui-content": "sub-navigation-disclosure-icon", className: cx(classes.disclosureIcon, 'mfui-SubNavigation__disclosureIcon'), children: _jsx(DisclosureBasicCollapsed, { "aria-label": "" }) })] }), nestedNavigationItems: children, customLinkComponent: customLinkComponent, classes: classes, nestLevel: nestLevel + 1, renderNavigationLabel: renderNavigationLabel }));
|
|
44
46
|
};
|
|
45
47
|
/**
|
|
46
48
|
* This is a group of a text label, an optional icon, and a status.
|
|
47
49
|
* This is used for all navigation links and parent items for disclosure buttons in SubNavigation.
|
|
48
50
|
*/
|
|
49
|
-
function NavigationLabelGroup({ label, labelIcon, locked, lockIconProps, statusSlot, classes, }) {
|
|
51
|
+
function NavigationLabelGroup({ label, labelIcon, locked, lockIconProps, statusSlot, classes, renderNavigationLabel, }) {
|
|
50
52
|
// If locked=true, display <Lock /> icon in preference to any other status of the statusSlot option.
|
|
51
53
|
const renderStatus = () => {
|
|
52
54
|
if (locked) {
|
|
@@ -57,31 +59,31 @@ function NavigationLabelGroup({ label, labelIcon, locked, lockIconProps, statusS
|
|
|
57
59
|
}
|
|
58
60
|
return null;
|
|
59
61
|
};
|
|
60
|
-
return (_jsxs(_Fragment, { children: [labelIcon ? _jsx("div", { className: cx(classes.labelIcon, 'mfui-SubNavigation__labelIcon'), children: labelIcon }) : null, _jsx(Typography, { variant: "strongControlLabel", className: cx(classes.label, 'mfui-SubNavigation__label'), children: label }), renderStatus()] }));
|
|
62
|
+
return (_jsxs(_Fragment, { children: [labelIcon ? _jsx("div", { className: cx(classes.labelIcon, 'mfui-SubNavigation__labelIcon'), children: labelIcon }) : null, renderNavigationLabel ? (renderNavigationLabel(label)) : (_jsx(Typography, { variant: "strongControlLabel", className: cx(classes.label, 'mfui-SubNavigation__label'), children: label })), renderStatus()] }));
|
|
61
63
|
}
|
|
62
64
|
/**
|
|
63
65
|
* This is a link that has NavigationLabelGroup.
|
|
64
66
|
* This is used for all navigation links in SubNavigation.
|
|
65
67
|
*/
|
|
66
|
-
function NavigationLink({ navigationItem, customLinkComponent, classes, }) {
|
|
68
|
+
function NavigationLink({ navigationItem, customLinkComponent, classes, renderNavigationLabel, }) {
|
|
67
69
|
const { label, href, isCurrent, isExternal, labelIcon, statusSlot, locked, lockIconProps } = navigationItem;
|
|
68
70
|
if (navigationItem.children === undefined && navigationItem.onClick) {
|
|
69
71
|
const { onClick } = navigationItem;
|
|
70
72
|
return (_jsx(FocusIndicator, { children: _jsx("button", { type: "button", className: cx(classes.link, 'mfui-SubNavigation__link'), "aria-current": isCurrent ? 'page' : undefined, onClick: () => {
|
|
71
73
|
onClick(navigationItem.href);
|
|
72
|
-
}, children: _jsx(NavigationLabelGroup, { label, labelIcon, locked, lockIconProps, statusSlot, classes }) }) }));
|
|
74
|
+
}, children: _jsx(NavigationLabelGroup, { label, labelIcon, locked, lockIconProps, statusSlot, classes, renderNavigationLabel }) }) }));
|
|
73
75
|
}
|
|
74
76
|
if (customLinkComponent && !isExternal) {
|
|
75
77
|
const Tag = customLinkComponent;
|
|
76
|
-
return (_jsx(FocusIndicator, { children: _jsx(Tag, { href: href, className: cx(classes.link, 'mfui-SubNavigation__link'), "aria-current": isCurrent ? 'page' : undefined, children: _jsx(NavigationLabelGroup, { label, labelIcon, locked, lockIconProps, statusSlot, classes }) }) }));
|
|
78
|
+
return (_jsx(FocusIndicator, { children: _jsx(Tag, { href: href, className: cx(classes.link, 'mfui-SubNavigation__link'), "aria-current": isCurrent ? 'page' : undefined, children: _jsx(NavigationLabelGroup, { label, labelIcon, locked, lockIconProps, statusSlot, classes, renderNavigationLabel }) }) }));
|
|
77
79
|
}
|
|
78
|
-
return (_jsx(FocusIndicator, { children: _jsx("a", { href: href, target: isExternal ? '_blank' : undefined, className: cx(classes.link, 'mfui-SubNavigation__link'), "aria-current": isCurrent ? 'page' : undefined, rel: "noreferrer", children: _jsx(NavigationLabelGroup, { label, labelIcon, locked, lockIconProps, statusSlot, classes }) }) }));
|
|
80
|
+
return (_jsx(FocusIndicator, { children: _jsx("a", { href: href, target: isExternal ? '_blank' : undefined, className: cx(classes.link, 'mfui-SubNavigation__link'), "aria-current": isCurrent ? 'page' : undefined, rel: "noreferrer", children: _jsx(NavigationLabelGroup, { label, labelIcon, locked, lockIconProps, statusSlot, classes, renderNavigationLabel }) }) }));
|
|
79
81
|
}
|
|
80
82
|
/**
|
|
81
83
|
* This is a details element that contains a list of nested navigation links.
|
|
82
84
|
*/
|
|
83
|
-
function NestedNavigationList({ summarySlot, nestedNavigationItems, customLinkComponent, classes, nestLevel, }) {
|
|
85
|
+
function NestedNavigationList({ summarySlot, nestedNavigationItems, customLinkComponent, classes, nestLevel, renderNavigationLabel, }) {
|
|
84
86
|
// Check whether all descendants have isCurrent=true by calling itself recursively.
|
|
85
87
|
const isCurrentPathInChildren = (children) => children?.some(({ isCurrent, children }) => isCurrent || (children && isCurrentPathInChildren(children))) || false;
|
|
86
|
-
return (_jsxs("details", { open: isCurrentPathInChildren(nestedNavigationItems), className: cx(classes.details, 'mfui-SubNavigation__details'), children: [_jsx(FocusIndicator, { children: _jsx("summary", { className: cx(classes.summary, 'mfui-SubNavigation__summary'), children: summarySlot }) }), _jsx("ul", { className: cx(classes.list, 'mfui-SubNavigation__list'), children: nestedNavigationItems.map((nestedNavigationItem, index) => (_jsx("li", { className: cx(classes.nestedListItem, 'mfui-SubNavigation__nestedListItem'), "data-mfui-nest-level": nestLevel, children: nestedNavigationItem.children === undefined ? (_jsx(NavigationLink, { navigationItem: nestedNavigationItem, customLinkComponent: customLinkComponent, classes: classes })) : (renderNavigationItem(nestedNavigationItem, customLinkComponent, classes, nestLevel + 1)) }, index))) })] }));
|
|
88
|
+
return (_jsxs("details", { open: isCurrentPathInChildren(nestedNavigationItems), className: cx(classes.details, 'mfui-SubNavigation__details'), children: [_jsx(FocusIndicator, { children: _jsx("summary", { className: cx(classes.summary, 'mfui-SubNavigation__summary'), children: summarySlot }) }), _jsx("ul", { className: cx(classes.list, 'mfui-SubNavigation__list'), children: nestedNavigationItems.map((nestedNavigationItem, index) => (_jsx("li", { className: cx(classes.nestedListItem, 'mfui-SubNavigation__nestedListItem'), "data-mfui-nest-level": nestLevel, children: nestedNavigationItem.children === undefined ? (_jsx(NavigationLink, { navigationItem: nestedNavigationItem, customLinkComponent: customLinkComponent, classes: classes, renderNavigationLabel: renderNavigationLabel })) : (renderNavigationItem(nestedNavigationItem, customLinkComponent, classes, nestLevel + 1, renderNavigationLabel)) }, index))) })] }));
|
|
87
89
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactNode, type ElementType, type ComponentPropsWithoutRef } from 'react';
|
|
2
|
+
export type SubNavigationRenderLabelFunction = (label: string) => ReactNode;
|
|
2
3
|
export type SubNavigationProps = VerticalSubNavigationProps | HorizontalSubNavigationProps;
|
|
3
4
|
type VerticalSubNavigationProps = BaseSubNavigationProps & {
|
|
4
5
|
/**
|
|
@@ -47,6 +48,63 @@ type BaseSubNavigationProps = {
|
|
|
47
48
|
* e.g. next/link
|
|
48
49
|
*/
|
|
49
50
|
customLinkComponent?: ElementType;
|
|
51
|
+
/**
|
|
52
|
+
* Custom render function for each navigation item's label.
|
|
53
|
+
* Receives the item's raw `label` string and returns a ReactNode rendered inside
|
|
54
|
+
* the default `<Typography variant="strongControlLabel">` wrapper.
|
|
55
|
+
* When omitted, the label string is rendered directly (default behavior).
|
|
56
|
+
*
|
|
57
|
+
* Use this to truncate long labels or otherwise customize how labels are displayed
|
|
58
|
+
* without losing the built-in semantic markup.
|
|
59
|
+
*
|
|
60
|
+
* To show a tooltip on both hover and focus, wrap the entire link with a `Tooltip`
|
|
61
|
+
* via `customLinkComponent` instead — placing a `Tooltip` inside `renderNavigationLabel`
|
|
62
|
+
* only works for hover because the tooltip trigger ends up inside the focusable `<a>`,
|
|
63
|
+
* and focus events do not propagate inward to child elements.
|
|
64
|
+
*
|
|
65
|
+
* @param label - The label string of the navigation item.
|
|
66
|
+
*
|
|
67
|
+
* @returns ReactNode - Custom content rendered inside the label wrapper.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```tsx
|
|
71
|
+
* // Truncate labels that exceed 2 lines
|
|
72
|
+
* <SubNavigation
|
|
73
|
+
* navigationItems={items}
|
|
74
|
+
* renderNavigationLabel={(label) => (
|
|
75
|
+
* <span
|
|
76
|
+
* style={{
|
|
77
|
+
* display: '-webkit-box',
|
|
78
|
+
* WebkitLineClamp: 2,
|
|
79
|
+
* WebkitBoxOrient: 'vertical',
|
|
80
|
+
* overflow: 'hidden',
|
|
81
|
+
* }}
|
|
82
|
+
* >
|
|
83
|
+
* {label}
|
|
84
|
+
* </span>
|
|
85
|
+
* )}
|
|
86
|
+
* />
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
renderNavigationLabel?: SubNavigationRenderLabelFunction;
|
|
90
|
+
/**
|
|
91
|
+
* Content displayed above the navigation list, rendered inside a padded wrapper.
|
|
92
|
+
* Use this to add a feature shortcut, promotional content, or other header-level
|
|
93
|
+
* UI at the top of the navigation.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```tsx
|
|
97
|
+
* <SubNavigation
|
|
98
|
+
* navigationItems={items}
|
|
99
|
+
* headerSlot={
|
|
100
|
+
* <Button priority="primary" href="/upgrade">
|
|
101
|
+
* Upgrade plan
|
|
102
|
+
* </Button>
|
|
103
|
+
* }
|
|
104
|
+
* />
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
headerSlot?: ReactNode;
|
|
50
108
|
/**
|
|
51
109
|
* For overriding styles if you need.
|
|
52
110
|
*/
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { SubNavigation } from './SubNavigation';
|
|
2
|
-
export type { SubNavigationProps } from './SubNavigation.types';
|
|
2
|
+
export type { SubNavigationProps, SubNavigationRenderLabelFunction } from './SubNavigation.types';
|
|
@@ -54,7 +54,23 @@ export type TagProps = {
|
|
|
54
54
|
*/
|
|
55
55
|
onClick?: undefined;
|
|
56
56
|
/**
|
|
57
|
-
* This
|
|
57
|
+
* Optional flag to disable the close button. This option is only available when "onClose" is provided.
|
|
58
|
+
*
|
|
59
|
+
* @default false
|
|
60
|
+
*/
|
|
61
|
+
disabled?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The handler to close the tag. When this prop is provided, the close button will be shown.
|
|
64
|
+
* Required in this variant to allow the disabled prop.
|
|
65
|
+
*/
|
|
66
|
+
onClose: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
67
|
+
} | {
|
|
68
|
+
/**
|
|
69
|
+
* Optional click event handler. If provided, the Tag will be rendered as a button element.
|
|
70
|
+
*/
|
|
71
|
+
onClick?: undefined;
|
|
72
|
+
/**
|
|
73
|
+
* This prop is not available when neither "onClick" nor "onClose" is provided.
|
|
58
74
|
*
|
|
59
75
|
* @default false
|
|
60
76
|
*/
|
|
@@ -3,7 +3,7 @@ import type { ConditionalValue } from '../types/index';
|
|
|
3
3
|
import type { DistributiveOmit, Pretty } from '../types/system-types';
|
|
4
4
|
|
|
5
5
|
interface FormFooterSlotRecipeVariant {
|
|
6
|
-
position: "stacking" | "fixed"
|
|
6
|
+
position: "stacking" | "fixed" | "sticky"
|
|
7
7
|
sectionPosition: "center" | "fill"
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -11,7 +11,7 @@ type SidePaneSlotRecipeVariantMap = {
|
|
|
11
11
|
[key in keyof SidePaneSlotRecipeVariant]: Array<SidePaneSlotRecipeVariant[key]>
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
type SidePaneSlotRecipeSlot = "root" | "backdrop" | "inside" | "header" | "title" | "actionSlotWrapper" | "actionSlot" | "closeButtonWrapper" | "content"
|
|
14
|
+
type SidePaneSlotRecipeSlot = "root" | "backdrop" | "inside" | "header" | "title" | "actionSlotWrapper" | "actionSlot" | "closeButtonWrapper" | "content" | "contentBody" | "footer"
|
|
15
15
|
|
|
16
16
|
export type SidePaneSlotRecipeVariantProps = {
|
|
17
17
|
[key in keyof SidePaneSlotRecipeVariant]?: ConditionalValue<SidePaneSlotRecipeVariant[key]> | undefined
|
|
@@ -38,6 +38,14 @@ const sidePaneSlotRecipeSlotNames = [
|
|
|
38
38
|
[
|
|
39
39
|
"content",
|
|
40
40
|
"SidePane__content"
|
|
41
|
+
],
|
|
42
|
+
[
|
|
43
|
+
"contentBody",
|
|
44
|
+
"SidePane__contentBody"
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
"footer",
|
|
48
|
+
"SidePane__footer"
|
|
41
49
|
]
|
|
42
50
|
];
|
|
43
51
|
const sidePaneSlotRecipeSlotFns = /* @__PURE__ */ sidePaneSlotRecipeSlotNames.map(([slotName, slotKey]) => [slotName, createRecipe(slotKey, sidePaneSlotRecipeDefaultVariants, getSlotCompoundVariant(sidePaneSlotRecipeCompoundVariants, slotName))]);
|
|
@@ -11,7 +11,7 @@ type SubNavigationSlotRecipeVariantMap = {
|
|
|
11
11
|
[key in keyof SubNavigationSlotRecipeVariant]: Array<SubNavigationSlotRecipeVariant[key]>
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
type SubNavigationSlotRecipeSlot = "root" | "list" | "listItem" | "link" | "labelIcon" | "label" | "statusSlot" | "details" | "summary" | "disclosureIcon" | "nestedListItem"
|
|
14
|
+
type SubNavigationSlotRecipeSlot = "root" | "subNavigationHeader" | "list" | "listItem" | "link" | "labelIcon" | "label" | "statusSlot" | "details" | "summary" | "disclosureIcon" | "nestedListItem"
|
|
15
15
|
|
|
16
16
|
export type SubNavigationSlotRecipeVariantProps = {
|
|
17
17
|
[key in keyof SubNavigationSlotRecipeVariant]?: ConditionalValue<SubNavigationSlotRecipeVariant[key]> | undefined
|
package/dist/styles.css
CHANGED
|
@@ -2366,16 +2366,25 @@
|
|
|
2366
2366
|
.mfui-hERVbP {
|
|
2367
2367
|
flex: 1 1 0%;
|
|
2368
2368
|
overflow: auto;
|
|
2369
|
-
padding-inline: var(--mfui-spacing-mfui\.size\.padding\.main-content\.horizontal\.comfort);
|
|
2370
2369
|
flex-shrink: 0;
|
|
2371
|
-
|
|
2372
|
-
|
|
2370
|
+
display: flex;
|
|
2371
|
+
flex-direction: column;
|
|
2373
2372
|
font-family: var(--mfui-fonts-mfui\.typography\.font-family\.body);
|
|
2374
2373
|
font-weight: var(--mfui-font-weights-mfui\.typography\.font-weight\.body);
|
|
2375
2374
|
font-size: var(--mfui-font-sizes-mfui\.typography\.font-size\.body);
|
|
2376
2375
|
line-height: var(--mfui-line-heights-mfui\.typography\.line-height\.body);
|
|
2377
2376
|
}
|
|
2378
2377
|
|
|
2378
|
+
.mfui-jVWTKD {
|
|
2379
|
+
padding-inline: var(--mfui-spacing-mfui\.size\.padding\.main-content\.horizontal\.comfort);
|
|
2380
|
+
padding-block-start: var(--mfui-spacing-mfui\.size\.padding\.main-content\.top\.comfort);
|
|
2381
|
+
padding-block-end: var(--mfui-spacing-mfui\.size\.padding\.main-content\.bottom\.comfort);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
.mfui-iAowQz {
|
|
2385
|
+
flex-shrink: 0;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2379
2388
|
.mfui-lbCmDn {
|
|
2380
2389
|
border-style: solid;
|
|
2381
2390
|
border-color: var(--mfui-colors-mfui\.color\.neutral\.border\.none);
|
|
@@ -2964,6 +2973,13 @@
|
|
|
2964
2973
|
background-color: var(--mfui-colors-mfui\.color\.base\.background\.none);
|
|
2965
2974
|
}
|
|
2966
2975
|
|
|
2976
|
+
.mfui-XFuwI {
|
|
2977
|
+
padding-block: var(--mfui-spacing-mfui\.size\.padding\.control-container\.vertical\.comfort);
|
|
2978
|
+
padding-inline: var(--mfui-spacing-mfui\.size\.padding\.sub-navigation\.horizontal\.comfort);
|
|
2979
|
+
display: flex;
|
|
2980
|
+
flex-direction: column;
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2967
2983
|
.mfui-erQjxT {
|
|
2968
2984
|
list-style: none;
|
|
2969
2985
|
}
|
|
@@ -5147,8 +5163,10 @@ li:last-child > .mfui-cXGJls {
|
|
|
5147
5163
|
.mfui-iEbmF {
|
|
5148
5164
|
padding-inline: var(--mfui-spacing-mfui\.size\.padding\.main-content\.horizontal\.comfort);
|
|
5149
5165
|
display: flex;
|
|
5150
|
-
flex-direction:
|
|
5166
|
+
flex-direction: row;
|
|
5167
|
+
flex-wrap: wrap;
|
|
5151
5168
|
align-items: flex-start;
|
|
5169
|
+
column-gap: var(--mfui-spacing-mfui\.size\.spacing\.paragraph\.horizontal\.comfort);
|
|
5152
5170
|
}
|
|
5153
5171
|
|
|
5154
5172
|
.mfui-fpqRil {
|
|
@@ -5157,10 +5175,9 @@ li:last-child > .mfui-cXGJls {
|
|
|
5157
5175
|
}
|
|
5158
5176
|
|
|
5159
5177
|
.mfui-fpqRil,.mfui-eTbFbc {
|
|
5160
|
-
flex:
|
|
5178
|
+
flex: 1 0 auto;
|
|
5161
5179
|
display: flex;
|
|
5162
5180
|
align-items: center;
|
|
5163
|
-
width: 100%;
|
|
5164
5181
|
height: var(--mfui-sizes-mfui\.size\.dimension\.control-container\.height\.comfort);
|
|
5165
5182
|
}
|
|
5166
5183
|
|
|
@@ -5177,15 +5194,6 @@ li:last-child > .mfui-cXGJls {
|
|
|
5177
5194
|
}
|
|
5178
5195
|
.mfui-jDhUBB,.mfui-ggXIiJ,.mfui-clKroC {
|
|
5179
5196
|
display: none;
|
|
5180
|
-
}
|
|
5181
|
-
.mfui-iEbmF {
|
|
5182
|
-
gap: var(--mfui-spacing-mfui\.size\.spacing\.paragraph\.horizontal\.comfort);
|
|
5183
|
-
flex-direction: row;
|
|
5184
|
-
}
|
|
5185
|
-
.mfui-fpqRil,.mfui-eTbFbc {
|
|
5186
|
-
flex: 1 0 0;
|
|
5187
|
-
width: auto;
|
|
5188
|
-
min-width: 0;
|
|
5189
5197
|
}
|
|
5190
5198
|
}
|
|
5191
5199
|
|
|
@@ -7124,16 +7132,23 @@ li:last-child > .mfui-cXGJls {
|
|
|
7124
7132
|
|
|
7125
7133
|
.mfui-jAKHYi {
|
|
7126
7134
|
position: fixed;
|
|
7135
|
+
left: 0;
|
|
7136
|
+
right: 0;
|
|
7137
|
+
}
|
|
7138
|
+
|
|
7139
|
+
.mfui-jAKHYi,.mfui-erGtCj {
|
|
7127
7140
|
z-index: 1;
|
|
7128
7141
|
background-color: var(--mfui-colors-mfui\.color\.base\.background\.none);
|
|
7129
7142
|
bottom: 0;
|
|
7130
|
-
left: 0;
|
|
7131
|
-
right: 0;
|
|
7132
7143
|
border-top-width: var(--mfui-border-widths-mfui\.size\.border\.fixed-cell\.horizontal\.comfort);
|
|
7133
7144
|
border-top-style: solid;
|
|
7134
7145
|
border-top-color: var(--mfui-colors-mfui\.color\.neutral\.sub-border\.none);
|
|
7135
7146
|
}
|
|
7136
7147
|
|
|
7148
|
+
.mfui-erGtCj {
|
|
7149
|
+
position: sticky;
|
|
7150
|
+
}
|
|
7151
|
+
|
|
7137
7152
|
.mfui-hhFgaB {
|
|
7138
7153
|
max-width: var(--mfui-sizes-mfui\.layout\.area\.horizontal\.fixed);
|
|
7139
7154
|
width: 100%;
|