wix-style-react
Version:
565 lines (499 loc) • 17.2 kB
JavaScript
import React, { Component, cloneElement } from 'react';
import PropTypes from 'prop-types';
import { st, classes } from './Sidebar.st.css';
import { SidebarItem } from './SidebarItem';
import { SidebarPersistentHeader } from './SidebarPersistentHeader';
import { SidebarPersistentFooter } from './SidebarPersistentFooter';
import { SidebarBackButton } from './SidebarBackButton';
import { SidebarContext } from './SidebarAPI';
import { dataHooks, sidebarSkins } from './constants';
import { SidebarContentWrapper } from './SidebarContentWrapper';
import { WixStyleReactContext } from '../WixStyleReactProvider/context';
const CollapsibeInnerMenuOpenChildren = props => {
const { children, waitForOtherMenuToClose, skin, isAlreadyOpen } = props;
return (
<div
data-hook={'open-inner-menu-children'}
className={
isAlreadyOpen
? ''
: st(
waitForOtherMenuToClose
? classes.innerMenuWrapperInToPlaceAfterClosingOther
: classes.innerMenuWrapperInToPlace,
)
}
>
<SidebarContentWrapper
containerClasses={st(classes.innerMenu)}
containerDataHook={dataHooks.drivenInChildren}
skin={skin}
>
{children}
</SidebarContentWrapper>
</div>
);
};
const CollapsibeInnerMenuCloseChildren = props => {
const { children } = props;
return (
<div
data-hook={'closed-inner-menu-children'}
className={st(classes.innerMenuWrapperOutOfPlace)}
>
<div
className={st(classes.innerMenu)}
data-hook={dataHooks.drivenOutChildren}
>
{children}
</div>
</div>
);
};
const CollapsibeInnerMenuCloseParent = props => {
const { isAlreadyClosed, children } = props;
const closeParentClass = st(
isAlreadyClosed ? classes.closedInnerMenu : classes.closingInnerMenu,
);
return (
<div data-hook="closed-inner-menu" className={closeParentClass}>
{children}
</div>
);
};
const CollapsibeInnerMenuOpenParent = props => {
const { isAlreadyOpen, children, waitForOtherMenuToClose } = props;
const openParentClass = waitForOtherMenuToClose
? st(classes.openingInnerMenuAfterClosingOther)
: st(isAlreadyOpen ? classes.openInnerMenu : classes.openingInnerMenu);
return (
<div data-hook={'open-inner-menu'} className={openParentClass}>
{children}
</div>
);
};
/** A sidebar navigation component */
class Sidebar extends Component {
static displayName = 'Sidebar';
static Item = SidebarItem;
static PersistentHeader = SidebarPersistentHeader;
static PersistentFooter = SidebarPersistentFooter;
static BackButton = SidebarBackButton;
static propTypes = {
/** classNames overrides */
classNames: PropTypes.shape({
sideBar: PropTypes.string,
content: PropTypes.string,
slider: PropTypes.string,
sliderOutToLeft: PropTypes.string,
sliderOutToRight: PropTypes.string,
sliderInFromLeft: PropTypes.string,
sliderInFromRight: PropTypes.string,
}),
/** The dataHook of the Sidebar */
dataHook: PropTypes.string,
/** Sidebar menu children */
children: PropTypes.node,
/** Sidebar indicator for animating out or in */
isHidden: PropTypes.bool,
/** Sets the skin of the Sidebar */
skin: PropTypes.oneOf(['dark', 'light']),
};
static defaultProps = {
skin: sidebarSkins.dark,
isHidden: false,
};
constructor(props) {
super(props);
this.itemKeyToChildren = {};
this.itemKeyToParentKey = {};
this.firstLevelItems = [];
this.state = {
persistentTopChildren: [],
drivenOutChildren: [],
onScreenChildren: [],
collapsibleOnScreenChildren: [],
drivenInChildren: [],
persistentBottomChildren: [],
selectedKey: '',
lastSelectedKey: '',
expandedInnerMenu: undefined,
};
}
_getInnerMenuCollapsibleState = options => {
const { menuToClose, menuToOpen, selected } = options;
let expandedInnerMenu = undefined;
const openMenuChildren = this.itemKeyToChildren[menuToOpen]?.children || [];
const closeMenuChildren =
this.itemKeyToChildren[menuToClose]?.children || [];
const collapsibleOnScreenChildren = this.firstLevelItems.reduce(
(accumalator, child) => {
const shouldExpand =
menuToOpen &&
child.props.itemKey === menuToOpen &&
openMenuChildren?.length > 0;
const shouldCollapse =
this.state.expandedInnerMenu &&
menuToClose &&
closeMenuChildren?.length > 0 &&
child.props.itemKey === menuToClose &&
this.state.expandedInnerMenu === menuToClose;
const waitForOtherMenuToClose =
this.state.expandedInnerMenu &&
menuToClose &&
this.state.expandedInnerMenu === menuToClose &&
menuToClose !== menuToOpen;
if (shouldExpand) {
expandedInnerMenu = menuToOpen;
return [
...accumalator,
<CollapsibeInnerMenuOpenParent
key={`open-parent-${menuToOpen}`}
isAlreadyOpen={this._isAlreadyOpen(menuToOpen)}
waitForOtherMenuToClose={waitForOtherMenuToClose}
>
{child}
</CollapsibeInnerMenuOpenParent>,
<CollapsibeInnerMenuOpenChildren
key={`open-children-${menuToOpen}`}
waitForOtherMenuToClose={waitForOtherMenuToClose}
skin={this.props.skin}
isAlreadyOpen={this._isAlreadyOpen(menuToOpen)}
>
{openMenuChildren}
</CollapsibeInnerMenuOpenChildren>,
];
}
if (shouldCollapse) {
return [
...accumalator,
<CollapsibeInnerMenuCloseParent
key={`closed-parent-${menuToClose}`}
isAlreadyClosed={this._isAlreadyClosed(selected, menuToClose)}
>
{child}
</CollapsibeInnerMenuCloseParent>,
<CollapsibeInnerMenuCloseChildren
key={`closed-children-${menuToClose}`}
>
{closeMenuChildren}
</CollapsibeInnerMenuCloseChildren>,
];
}
return [...accumalator, child];
},
[],
);
return {
collapsibleOnScreenChildren,
expandedInnerMenu,
};
};
_shouldCollapseInnerMenu = itemKey =>
this.state.expandedInnerMenu === itemKey &&
((itemKey === this.state.selectedKey && this.state.expandedInnerMenu) ||
itemKey !== this.state.selectedKey);
_shouldExpandInnerMenu = (parentKey, itemKey) =>
(parentKey === this.state.lastSelectedKey &&
!this.state.expandedInnerMenu) ||
this.itemKeyToParentKey[itemKey] === parentKey ||
parentKey !== this.state.lastSelectedKey;
_navigateTo = itemKey => {
if (this._isChild(itemKey)) {
this._selectItem(itemKey);
this.sidebarContext = this._getSidebarContext();
return;
}
if (this._isParent(itemKey)) {
this._openInnerMenu(itemKey);
this.sidebarContext = this._getSidebarContext();
return;
}
if (itemKey) {
this._closeInnerMenu(itemKey);
this.sidebarContext = this._getSidebarContext();
return;
}
};
_getSidebarContext = () => {
return {
itemClicked: this._navigateTo,
backClicked: () => {
this._closeInnerMenu();
this.sidebarContext = this._getSidebarContext();
},
getSelectedKey: () => this.state.selectedKey,
getSkin: () => this.props.skin,
getIsMenuExpanded: () => this.state.expandedInnerMenu,
};
};
sidebarContext = this._getSidebarContext();
_getInnerChildSelectedState = itemKey => {
const collapsibleInnerMenuState = this._getInnerMenuCollapsibleState({
menuToClose: this.state.lastSelectedKey,
menuToOpen: this.itemKeyToParentKey[itemKey],
selected: itemKey,
});
if (this.itemKeyToParentKey[itemKey] !== this.state.lastSelectedKey) {
return {
drivenInChildren:
this.itemKeyToChildren[this.itemKeyToParentKey[itemKey]].children,
selectedKey: itemKey,
lastSelectedKey: this.itemKeyToParentKey[itemKey],
...collapsibleInnerMenuState,
};
} else {
return {
...collapsibleInnerMenuState,
selectedKey: itemKey,
};
}
};
_getInnerMenuOpenState = itemKey => {
const { children, selectedKey } = this.itemKeyToChildren[itemKey];
const selected =
this.itemKeyToParentKey[this.state.lastSelectedKey] === itemKey
? this.state.lastSelectedKey
: selectedKey;
const parentKey = this.itemKeyToParentKey[selected];
const parentItemKeyToOpen = this._getItemToOpenKey(parentKey, itemKey);
const parentItemKeyToClose = this._getItemToCloseKey(
this.state.lastSelectedKey,
);
return {
...this._getInnerMenuCollapsibleState({
menuToClose: parentItemKeyToClose,
menuToOpen: parentItemKeyToOpen,
selected,
}),
drivenInChildren: children,
drivenOutChildren: [],
selectedKey: selected,
lastSelectedKey: itemKey,
};
};
_getItemToCloseKey = itemKey => {
if (this._shouldCollapseInnerMenu(itemKey)) {
return this.itemKeyToParentKey[itemKey] || itemKey;
}
if (this._shouldCollapseInnerMenu(this.state.lastSelectedKey)) {
return (
this.itemKeyToParentKey[this.state.lastSelectedKey] ||
this.state.lastSelectedKey
);
}
return undefined;
};
_getItemToOpenKey = (parentKey, itemKey) =>
this._shouldExpandInnerMenu(parentKey, itemKey) ? parentKey : undefined;
_getInnerMenuCloseState = (itemKey, updateCollapsibleOnlyOnChange) => {
const selectedKey = this.state.lastSelectedKey || itemKey;
const parentItemKeyToClose = this._getItemToCloseKey(selectedKey);
return {
...this._getInnerMenuCollapsibleState({
menuToClose: parentItemKeyToClose,
selected: selectedKey,
}),
selectedKey: itemKey || this.state.lastSelectedKey,
lastSelectedKey: selectedKey,
drivenInChildren: [],
drivenOutChildren: this.state.drivenInChildren,
};
};
_getItemWithKey = (item, itemKey) =>
item.props.itemKey ? item : cloneElement(item, { ...item.props, itemKey });
_getChildrenWithKeys = child =>
child.props.innerMenu?.map((innerChild, index) =>
this._getItemWithKey(innerChild, child.props.itemKey + index),
) || [];
_isParent = itemKey => this.itemKeyToChildren[itemKey];
_isChild = itemKey => this.itemKeyToParentKey[itemKey];
_isAlreadyOpen = menuToOpen =>
this.state.lastSelectedKey === menuToOpen &&
this.itemKeyToParentKey[this.props.selectedKey] === menuToOpen &&
this.state.expandedInnerMenu === menuToOpen;
_isAlreadyClosed = (selected, menuToClose) =>
this.itemKeyToParentKey[selected] !== menuToClose;
_selectItem = itemKey =>
this.setState(this._getInnerChildSelectedState(itemKey));
_openInnerMenu = itemKey =>
this.setState(this._getInnerMenuOpenState(itemKey));
_closeInnerMenu = itemKey =>
this.setState(this._getInnerMenuCloseState(itemKey));
UNSAFE_componentWillMount() {
this._setInnerMenus(this.props);
}
UNSAFE_componentWillReceiveProps(props) {
this._setInnerMenus(props);
}
_setInnerMenus(props) {
const persistentTopChildren = [];
const persistentBottomChildren = [];
const onScreenChildren = [];
const findEnabledChild = item =>
item.props.innerMenu &&
item.props.innerMenu.find(
c => c.type === SidebarItem && !c.props.disable,
);
const handleChild = child => {
if (child.type === SidebarItem) {
const enabledChild = findEnabledChild(child);
const innerChildrenWithKeys = this._getChildrenWithKeys(child);
this.itemKeyToChildren[child.props.itemKey] = {
selectedKey: enabledChild
? enabledChild.props.itemKey
: child.props.itemKey,
children: innerChildrenWithKeys,
};
if (child.props.innerMenu) {
innerChildrenWithKeys.forEach(innerChild => {
if (innerChild.type !== SidebarBackButton) {
this.itemKeyToParentKey[innerChild.props.itemKey] =
child.props.itemKey;
}
});
}
onScreenChildren.push(child);
} else if (child.type === SidebarPersistentHeader) {
persistentTopChildren.push(child);
} else if (child.type === SidebarPersistentFooter) {
persistentBottomChildren.push(child);
} else {
onScreenChildren.push(child);
}
};
if (props.children) {
if (props.children.length) {
props.children.forEach(child => {
if (child) {
if (child.length > 0) {
child.forEach(handleChild);
} else {
handleChild(child);
}
}
});
} else {
handleChild(props.children);
}
}
this.firstLevelItems = onScreenChildren.slice();
const newState = {
persistentTopChildren,
persistentBottomChildren,
onScreenChildren,
selectedKey: props.selectedKey,
};
const selectedItemParentKey = this.itemKeyToParentKey[props.selectedKey];
if (selectedItemParentKey) {
this.setState({
...newState,
drivenInChildren:
this.itemKeyToChildren[selectedItemParentKey].children,
lastSelectedKey: selectedItemParentKey,
...this._getInnerMenuCollapsibleState({
menuToClose:
this.itemKeyToParentKey[this.props.selectedKey] ||
this.props.selctedKey,
menuToOpen: selectedItemParentKey,
selected: props.selectedKey,
}),
});
} else {
const updateCollapsibleOnlyOnChange = true;
this.setState({
...newState,
drivenInChildren: [],
...this._getInnerMenuCloseState(
props.selectedKey,
updateCollapsibleOnlyOnChange,
),
});
}
}
render() {
const { isHidden, skin } = this.props;
const css = { ...classes, ...this.props.classNames };
const sliderClasses = st(
css.slider,
{
skin,
},
this.state.drivenInChildren.length !== 0 && css.sliderOutToLeft,
this.state.drivenInChildren.length === 0 &&
this.state.drivenOutChildren.length !== 0 &&
css.sliderInFromLeft,
);
const collapsibleSliderClasses = st(css.slider, {
skin,
});
const sliderOutToRightClasses = st(
css.slider,
{
skin,
},
!this.props.isHidden && css.sliderOutToRight,
);
const sliderInFromRightClasses = st(
css.slider,
{
skin,
},
!this.props.isHidden && css.sliderInFromRight,
);
const rootClasses = st(css.sideBar || classes.root, {
hidden: isHidden,
skin,
});
return (
<WixStyleReactContext.Consumer>
{({ sidebarExperimentCollapsible }) => {
return (
<SidebarContext.Provider value={this.sidebarContext}>
<div className={rootClasses} data-hook={this.props.dataHook}>
{this.state.persistentTopChildren}
<div className={st(css.content)}>
{!sidebarExperimentCollapsible &&
this.state.drivenInChildren.length === 0 &&
this.state.drivenOutChildren.length !== 0 && (
<div
className={sliderOutToRightClasses}
data-hook={dataHooks.drivenOutChildren}
>
{this.state.drivenOutChildren}
</div>
)}
<SidebarContentWrapper
containerClasses={
sidebarExperimentCollapsible
? collapsibleSliderClasses
: sliderClasses
}
skin={skin}
>
{sidebarExperimentCollapsible
? this.state.collapsibleOnScreenChildren
: this.state.onScreenChildren}
</SidebarContentWrapper>
{!sidebarExperimentCollapsible &&
this.state.drivenInChildren.length !== 0 && (
<SidebarContentWrapper
key="collapsiblle"
containerClasses={sliderInFromRightClasses}
containerDataHook={dataHooks.drivenInChildren}
skin={skin}
>
{this.state.drivenInChildren}
</SidebarContentWrapper>
)}
</div>
{this.state.persistentBottomChildren}
</div>
</SidebarContext.Provider>
);
}}
</WixStyleReactContext.Consumer>
);
}
}
export default Sidebar;