@wix/design-system
Version:
@wix/design-system
168 lines • 8.21 kB
JavaScript
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