UNPKG

@wix/design-system

Version:

@wix/design-system

168 lines 8.21 kB
import React from 'react'; import { st, classes } from './ComposerSidebar.st.css.js'; import { dataHooks } from './constants'; import Box from '../Box'; import ToggleButton from '../ToggleButton'; import Text from '../Text'; const isItemEnabled = item => !item.disabled; const getNextEnabledIndex = (items, currentIndex, direction) => { const len = items.length; let next = (currentIndex + direction + len) % len; let steps = 0; while (steps < len && !isItemEnabled(items[next])) { next = (next + direction + len) % len; steps++; } return steps < len ? next : currentIndex; }; const getFirstEnabledIndex = items => { for (let i = 0; i < items.length; i++) { if (isItemEnabled(items[i])) return i; } return -1; }; const getLastEnabledIndex = items => { for (let i = items.length - 1; i >= 0; i--) { if (isItemEnabled(items[i])) return i; } return -1; }; const getPreferredFocusIndex = (items, selectedIndex) => { if (items.length === 0) return -1; const preferred = Math.min(selectedIndex, items.length - 1); return isItemEnabled(items[preferred]) ? preferred : getFirstEnabledIndex(items); }; /** ComposerSidebar */ class ComposerSidebar extends React.PureComponent { constructor() { super(...arguments); this.state = { focusedIndex: null }; this._sectionContainerRef = React.createRef(); this._itemRefs = {}; this._getSelectedIndex = () => { const { items, selectedId } = this.props; if (selectedId == null) return 0; const index = items.findIndex(item => item.id === selectedId); return index >= 0 ? index : 0; }; this._getEffectiveFocusedIndex = () => { const { items } = this.props; const { focusedIndex } = this.state; if (items.length === 0) return -1; if (focusedIndex !== null) return focusedIndex; return getPreferredFocusIndex(items, this._getSelectedIndex()); }; this._handleKeyDown = e => { const { items } = this.props; if (items.length === 0) return; const effectiveIndex = this._getEffectiveFocusedIndex(); const currentIndex = effectiveIndex >= 0 ? effectiveIndex : getFirstEnabledIndex(items); let nextIndex = currentIndex; switch (e.key) { case 'ArrowDown': nextIndex = getNextEnabledIndex(items, currentIndex, 1); break; case 'ArrowUp': nextIndex = getNextEnabledIndex(items, currentIndex, -1); break; case 'Home': nextIndex = getFirstEnabledIndex(items); break; case 'End': nextIndex = getLastEnabledIndex(items); break; default: return; } if (nextIndex !== -1 && nextIndex !== currentIndex && isItemEnabled(items[nextIndex])) { this.setState({ focusedIndex: nextIndex }, () => { const itemEl = this._itemRefs[nextIndex]; itemEl?.querySelector?.('button')?.focus(); }); e.preventDefault(); } }; this._handleFocusOut = e => { const container = this._sectionContainerRef.current; if (!container) return; const relatedTarget = e.relatedTarget; if (!relatedTarget || container.contains(relatedTarget)) return; this.setState({ focusedIndex: null }); }; this._renderSection = ({ section: { items: sectionItems, sectionTitle }, showSectionTitles = false, }) => { const { selectedId, labelPlacement } = this.props; return (React.createElement(React.Fragment, null, showSectionTitles ? (React.createElement(Text, { className: classes.title, size: "small", weight: "bold", ellipsis: true }, sectionTitle ?? 'Undefined')) : null, React.createElement("ul", { role: "menu", "data-hook": "composer-sidebar-items-container", "data-selected-id": selectedId, "data-label-placement": labelPlacement, className: classes.itemsContainer }, sectionItems.map(item => this._renderItem(item))))); }; this._renderSections = sections => { const { labelPlacement, size, width } = this.props; const withSections = sections.length > 1 || (sections.length === 1 && sections[0].sectionTitle); return (React.createElement("div", { ref: this._sectionContainerRef, role: withSections ? 'menu' : undefined, direction: "vertical", onKeyDown: this._handleKeyDown, onBlur: this._handleFocusOut, className: st(classes.sectionContainer, { withSections, labelPlacement, size, }), style: width ? { width } : undefined }, withSections ? sections.map((section, index) => (React.createElement("div", { key: index, role: "menuitem", className: classes.section }, this._renderSection({ section, showSectionTitles: true, })))) : this._renderSection({ section: sections[0] }))); }; this._separateToSections = items => { const sectionTitles = Array.from(new Set(items.map(({ sectionTitle }) => sectionTitle ? sectionTitle : undefined))); const sections = sectionTitles.map(sectionTitle => ({ sectionTitle, items: items.filter(({ sectionTitle: itemSectionTitle }) => (!sectionTitle && !itemSectionTitle) || sectionTitle === itemSectionTitle), })); return sections; }; } _renderItem(item) { const { items, selectedId, labelPlacement, size, onClick, className, labelTooltipProps, ellipsis, } = this.props; const { label, id, icon, disabled, sectionTitle, ...rest } = item; const itemIndex = items.findIndex(i => i.id === id); const selected = selectedId === id; const onClickHandler = item.onClick || onClick; const effectiveFocusedIndex = this._getEffectiveFocusedIndex(); const isFocused = effectiveFocusedIndex === itemIndex; return (React.createElement("li", { ref: el => { this._itemRefs[itemIndex] = el; }, role: "menuitem", "data-hook": `composer-sidebar-item-container-${id}`, key: `sidebar-item-${id}`, "data-section-title": sectionTitle, className: st(classes.item, { labelPlacement, size }, className) }, React.createElement(ToggleButton, { ...rest, tabIndex: isFocused ? 0 : -1, onFocus: () => this.setState({ focusedIndex: itemIndex }), className: st(classes.sidebarItemButton, { labelPlacement, size }), dataHook: `composer-sidebar-item-${id}`, onClick: e => onClickHandler(e, { id, label }), shape: "round", size: size, border: true, skin: "inverted", labelEllipsis: ellipsis, disabled: disabled, selected: selected, labelValue: label, labelPlacement: labelPlacement, tooltipProps: labelTooltipProps }, icon))); } render() { const { items, className, dataHook, labelPlacement, width } = this.props; const sections = this._separateToSections(items); return (React.createElement(Box, { height: "100%", maxHeight: "100%", width: width, className: st(classes.root, { labelPlacement }, className), dataHook: dataHook || dataHooks.composerSidebarContainer }, sections.length ? this._renderSections(sections) : null)); } } ComposerSidebar.displayName = 'ComposerSidebar'; ComposerSidebar.defaultProps = { labelPlacement: 'end', selectedId: null, items: [], size: 'medium', onClick: () => { }, labelTooltipProps: { placement: 'right', }, ellipsis: true, }; export default ComposerSidebar; //# sourceMappingURL=ComposerSidebar.js.map