UNPKG

wix-style-react

Version:
565 lines (499 loc) • 17.2 kB
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;