wix-style-react
Version:
wix-style-react
159 lines • 7.85 kB
JavaScript
import React, { memo, useCallback, useEffect, useMemo, useRef, useState, } from 'react';
import isEqual from 'lodash/isEqual';
import { ToggleButton } from './components/ToggleButton';
import { SidebarNextContext, } from './SidebarNextContext';
import { st, classes } from './SidebarNext.st.css';
import { dataHooks, sidebarSkins } from './constants';
import SidebarSkeleton from './components/SidebarSkeleton';
import { extractItemNodes, getItemPath, getMaxDepth, getMenuPath, pick, } from './utils';
import { useMemoizedIdentity } from './hooks/useMemoizedIdentity';
const DEFAULT_SKIN = sidebarSkins.dark;
const DEFAULT_WIDTH = 228;
const MINIMIZED_WIDTH = 54;
export const SidebarNext = memo(({ skin = DEFAULT_SKIN, width: orgWidth = DEFAULT_WIDTH, isLoading = true, dataHook, className, ariaLabel, selectedKey, header, footer, hidden = false, minimized = false, enableMinimizeToggle, minimizeToggleTooltip, onMinimizeToggleClick, zIndex, children, }) => {
const [width, setWidth] = useState(orgWidth);
const [contentWidth, setContentWidth] = useState(orgWidth);
const scrollAreaRef = useRef(null);
const structure = useMemoizedIdentity(extractItemNodes(children));
const widthForCalc = typeof width === 'number' ? `${width}px` : width;
const hiddenMarginLeft = `calc(-1 * ${widthForCalc})`;
const { selectedPath, expandTrigger, isExpanded, setExpanded } = useSidebarSelectionState(selectedKey, structure);
const maxDepth = useMemo(() => getMaxDepth(structure.items), [structure.items]);
// Sidebar with maximum depth of 1 or 2 is styled differently (legacy mode)
const legacy = maxDepth < 3;
useEffect(() => {
let timer;
if (minimized) {
setWidth(MINIMIZED_WIDTH);
timer = setTimeout(() => {
setContentWidth(MINIMIZED_WIDTH);
}, 500);
}
else {
setWidth(orgWidth);
setContentWidth(orgWidth);
}
return () => {
if (timer)
clearTimeout(timer);
};
}, [minimized, orgWidth]);
const sidebarContextValue = useMemoizedIdentity({
skin,
legacy,
hidden,
minimized,
inert: false,
level: 1,
parent: 'sidebar',
scrollAreaRef,
selectedPath,
expandTrigger,
isExpanded,
setExpanded,
});
return (React.createElement(SidebarNextContext.Provider, { value: sidebarContextValue },
React.createElement("div", { className: st(classes.root, { hidden }, className), style: {
width,
zIndex,
...(hidden ? { marginLeft: hiddenMarginLeft } : {}),
} },
React.createElement("section", { className: st(classes.section, { skin, hidden }), "aria-label": "Sidebar" // FIXME: translations?
, "data-hook": dataHook, "data-skin": skin, "data-hidden": hidden, "data-selected": selectedKey, "data-width": width, "data-minimized": minimized, "data-selected-key": selectedKey, "data-is-selected-expanded": isExpanded(selectedKey), "data-margin-left": hiddenMarginLeft, style: { width } },
enableMinimizeToggle && (React.createElement(ToggleButton, { minimized: minimized, minimizeToggleTooltip: minimizeToggleTooltip, onMinimizeToggleClick: onMinimizeToggleClick, skin: skin })),
React.createElement("header", { "data-hook": dataHooks.header }, header),
React.createElement("div", { className: classes.content },
React.createElement("nav", { ref: scrollAreaRef, "data-hook": dataHooks.scrollArea, className: st(classes.scrollArea, { skin }), "aria-label": ariaLabel, style: { width: contentWidth } }, isLoading ? (React.createElement("div", {
// ref={scrollAreaContentRef}
style: { overflow: 'hidden' } },
React.createElement(SidebarSkeleton, null))) : (React.createElement("ul", {
// ref={scrollAreaContentRef}
className: classes.listItems }, children)))),
React.createElement("footer", { "data-hook": dataHooks.footer }, footer)))));
});
const useSidebarSelectionState = (selectedKey, structure) => {
const selectedItemPath = getItemPath(structure.keyToParentKey, selectedKey);
const selectedMenuPath = getMenuPath(structure.keyToParentKey, selectedKey);
const [expandedState, setExpandedState] = useState(() => ({
expandedPath: selectedMenuPath,
userCollapsedKeys: {},
trigger: 'select',
}));
const isExpanded = useCallback((itemKey) => {
if (itemKey === undefined) {
return false;
}
const withinExpandedPath = expandedState.expandedPath.includes(itemKey);
const collapsedByUser = expandedState.userCollapsedKeys[itemKey];
return withinExpandedPath && !collapsedByUser;
}, [expandedState.expandedPath, expandedState.userCollapsedKeys]);
// We only allow toggling menus that are within the selected path. Menus
// outside of the selected path can only be expanded by changing `selectedKey`
// prop. This replicates previous two-level sidebar behavior.
const setExpanded = useCallback((itemKey, expanded) => {
setExpandedState(prev => {
if (!prev.expandedPath.includes(itemKey)) {
return prev;
}
return {
...prev,
userCollapsedKeys: {
...prev.userCollapsedKeys,
[itemKey]: !expanded,
},
trigger: 'click',
};
});
}, []);
// Handle `selectedKey` change
useEffect(() => {
if (!isEqual(expandedState.expandedPath, selectedMenuPath)) {
setExpandedState(prev => ({
expandedPath: selectedMenuPath,
userCollapsedKeys: pick(prev.userCollapsedKeys, selectedMenuPath),
trigger: 'select',
}));
}
}, [expandedState.expandedPath, selectedMenuPath]);
return {
selectedPath: selectedItemPath,
expandTrigger: expandedState.trigger,
isExpanded,
setExpanded,
};
};
SidebarNext.displayName = 'SidebarNext';
// FIXME: prop types on memoized component
// SidebarNext.propTypes = {
// /** The dataHook of the Sidebar */
// dataHook: PropTypes.string,
// /** The classes of the Sidebar */
// className: PropTypes.string,
// /** Sidebar menu children */
// children: PropTypes.node,
// /** Sets the skin of the Sidebar */
// skin: PropTypes.oneOf(['dark', 'light', 'neutral']),
// /** Renders header element */
// header: PropTypes.node,
// /** Renders footer element */
// footer: PropTypes.node,
// /** Controls if the sidebar is shown or hidden, using an animation to switch between the two */
// hidden: PropTypes.bool,
// /** The selected item key */
// selectedKey: PropTypes.string,
// /** Control whether the sidebar displays a loading animation, true by default */
// isLoading: PropTypes.bool,
// /** Define a string that labels the current element in case where a text label is not visible on the screen */
// ariaLabel: PropTypes.string,
// /** Defines the width */
// width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// /** Enables minimized mode */
// minimized: PropTypes.bool,
// /** Enables toggle button */
// enableMinimizeToggle: PropTypes.bool,
// /** Defines the tooltip content of minimization toggle. */
// minimizeToggleTooltip: PropTypes.string,
// /** Defines a callback function which is called every time a minimize toggle button is clicked. */
// onMinimizeToggleClick: PropTypes.func,
// };
//# sourceMappingURL=SidebarNextNext.js.map