UNPKG

@yandex/ui

Version:

Yandex UI components

225 lines (224 loc) 11.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Menu = exports.cnMenu = void 0; var tslib_1 = require("tslib"); var react_1 = tslib_1.__importStar(require("react")); var classname_1 = require("@bem-react/classname"); var di_1 = require("@bem-react/di"); var env_1 = require("../lib/env"); var keyboard_1 = require("../lib/keyboard"); var mergeRefs_1 = require("../lib/mergeRefs"); var flatMap_1 = require("../lib/flatMap"); var render_override_1 = require("../lib/render-override"); require("./Menu.css"); var getFlattenItems = function (items) { return flatMap_1.flatMap(function (item) { return (item.items ? item.items : item); }, items); }; var isGroup = function (value) { return value.items !== undefined; }; var createCounter = function (count) { if (count === void 0) { count = 0; } return function () { return count++; }; }; exports.cnMenu = classname_1.cn('Menu'); /** * Компонент для создания меню. * @param {IMenuProps} props */ var Menu = /** @class */ (function (_super) { tslib_1.__extends(Menu, _super); function Menu() { var _this = _super !== null && _super.apply(this, arguments) || this; _this.state = { hoveredIndex: _this.props.focused ? 0 : -1 }; /** * Контейнер с ссылкой на корневой DOM элемент меню. */ _this.innerRef = react_1.createRef(); /** * Массив ссылок на пункты меню для правильного подскролла. */ _this.itemsRef = []; _this.uniqId = env_1.IS_TESTING ? '0' : String(Date.now()) + Math.floor(Math.random() * 10000); _this.mapChildren = function (_a, _b) { var Item = _a.Item, Group = _a.Group; var getNextItemCount = _b.getNextItemCount, getNextGroupCount = _b.getNextGroupCount, disabled = _b.disabled; return function (item) { if (isGroup(item)) { var groupIndex = getNextGroupCount(); return (react_1.default.createElement(Group, tslib_1.__assign({ title: item.title, key: "group-" + groupIndex }, item.unsafe_props), item.items.map(_this.mapChildren({ Item: Item, Group: Group }, { getNextItemCount: getNextItemCount, getNextGroupCount: getNextGroupCount, disabled: disabled })))); } var _a = _this.props, value = _a.value, view = _a.view; var hoveredIndex = _this.state.hoveredIndex; var values = [].concat(value); var needIconGlyph = view === 'default' && value !== undefined; var itemIndex = getNextItemCount(); _this.itemsRef[itemIndex] = react_1.createRef(); return (react_1.default.createElement(Item, tslib_1.__assign({}, item.unsafe_props, { id: item.id || "item-" + _this.uniqId + "-" + itemIndex, key: "item-" + itemIndex, "data-key": "item-" + itemIndex, needIconGlyph: needIconGlyph, checked: values.indexOf(item.value) !== -1, disabled: item.disabled || disabled, hovered: disabled ? false : hoveredIndex === itemIndex, onMouseEnter: _this.setHoveredOnMouseEnter, onMouseLeave: _this.setHoveredOnMouseLeave, onClick: _this.onMenuItemClick, type: value === undefined ? value : 'option', innerRef: _this.itemsRef[itemIndex], value: item.value, index: itemIndex }), item.content)); }; }; _this.onKeyDown = function (event) { if (!event.shiftKey && keyboard_1.isKeyCode(event.keyCode, [keyboard_1.Keys.UP, keyboard_1.Keys.DOWN])) { event.preventDefault(); var direction = event.keyCode - 39; var nextActiveIndex = _this.getNextNotDisabledIndex(direction); _this.triggerOnMenuItemChange(nextActiveIndex); Menu.scrollToItem(_this.innerRef, _this.itemsRef[nextActiveIndex]); _this.setState({ hoveredIndex: nextActiveIndex }); } if (keyboard_1.isKeyCode(event.keyCode, [keyboard_1.Keys.ENTER, keyboard_1.Keys.SPACE])) { event.preventDefault(); _this.triggerOnChange(); } }; _this.onMenuItemClick = function (_event, index) { _this.setState({ hoveredIndex: index }, function () { _this.triggerOnChange(); }); }; _this.setHoveredOnMouseEnter = function (_event, index) { _this.setState({ hoveredIndex: index }); }; _this.setHoveredOnMouseLeave = function (_event, _index) { _this.setState({ hoveredIndex: -1 }); }; return _this; } Menu.prototype.componentDidMount = function () { this.forwardRefs(); if (this.props.focused) { this.subscribeToEvents(); this.hoverAndScrollToFirstSelectedElement(); } }; Menu.prototype.componentDidUpdate = function (prevProps) { this.forwardRefs(); if (!prevProps.focused && this.props.focused) { this.subscribeToEvents(); this.hoverAndScrollToFirstSelectedElement(); } else if (prevProps.focused && !this.props.focused) { this.setState({ hoveredIndex: -1 }); this.unsubscribeFromEvents(); } else if (this.props.focused && prevProps.items.length !== this.props.items.length) { this.hoverAndScrollToFirstSelectedElement(); } }; Menu.prototype.componentWillUnmount = function () { this.unsubscribeFromEvents(); }; Menu.prototype.render = function () { var _this = this; var _a = this.props, className = _a.className, disabled = _a.disabled, focused = _a.focused, items = _a.items, onChange = _a.onChange, onActiveItemChange = _a.onActiveItemChange, value = _a.value, innerRef = _a.innerRef, renderItem = _a.renderItem, props = tslib_1.__rest(_a, ["className", "disabled", "focused", "items", "onChange", "onActiveItemChange", "value", "innerRef", "renderItem"]); return (react_1.default.createElement(di_1.ComponentRegistryConsumer, { id: exports.cnMenu() }, function (_a) { var ItemOriginal = _a.Item, Group = _a.Group; return (react_1.default.createElement(render_override_1.RenderOverrideProvider, { component: ItemOriginal, render: renderItem }, function (Item) { var getNextItemCount = createCounter(); var getNextGroupCount = createCounter(); return (react_1.default.createElement("div", tslib_1.__assign({}, props, { ref: _this.innerRef, "aria-disabled": disabled, "aria-multiselectable": Array.isArray(value), role: value !== undefined ? 'listbox' : undefined, className: exports.cnMenu(null, [className]) }), items.map(_this.mapChildren({ Item: Item, Group: Group }, { getNextItemCount: getNextItemCount, getNextGroupCount: getNextGroupCount, disabled: disabled })))); })); })); }; Menu.prototype.forwardRefs = function () { mergeRefs_1.mergeRefs(this.innerRef, this.props.innerRef); }; Menu.scrollToItem = function (menuRef, itemRef) { if (menuRef.current === null || itemRef === undefined || itemRef.current === null) { return; } var menuOffsetTop = menuRef.current.getBoundingClientRect().top; var itemOffsetTop = itemRef.current.getBoundingClientRect().top; var relativeScroll; if (itemOffsetTop < menuOffsetTop) { relativeScroll = itemOffsetTop - menuOffsetTop; } else { relativeScroll = itemOffsetTop + itemRef.current.offsetHeight - menuOffsetTop - menuRef.current.offsetHeight; if (relativeScroll < 0) { relativeScroll = 0; } } menuRef.current.scrollTop = menuRef.current.scrollTop + relativeScroll; }; Menu.prototype.subscribeToEvents = function () { document.addEventListener('keydown', this.onKeyDown); }; Menu.prototype.unsubscribeFromEvents = function () { document.removeEventListener('keydown', this.onKeyDown); }; Menu.prototype.hoverAndScrollToFirstSelectedElement = function () { // TODO: Возможно в метод `getNextNotDisabledIndex` стоит добавить предикат // и вызывать его тут, чтобы не дублировать логику по обходу элементов. var values = [].concat(this.props.value); var items = getFlattenItems(this.props.items); var hoveredIndex = items.reduce(function (prevIndex, item, index) { return (prevIndex < 0 && values.indexOf(item.value) !== -1 ? index : prevIndex); }, -1); Menu.scrollToItem(this.innerRef, this.itemsRef[hoveredIndex]); this.setState({ hoveredIndex: hoveredIndex < 0 ? 0 : hoveredIndex }); }; /** * Возвращает следующий индекс активного элемента для выделения в меню. * * @param direction Направление, может быть `1` — down, либо `-1` — up. */ Menu.prototype.getNextNotDisabledIndex = function (direction) { var hoveredIndex = this.state.hoveredIndex; var itemsChecked = 0; var items = getFlattenItems(this.props.items); while (itemsChecked < items.length) { itemsChecked++; hoveredIndex += direction; if (hoveredIndex < 0) { hoveredIndex = items.length - 1; } if (hoveredIndex >= items.length) { hoveredIndex = 0; } if (!items[hoveredIndex].disabled) { break; } } return hoveredIndex; }; /** * Вызывает событие onActiveItemChange. */ Menu.prototype.triggerOnMenuItemChange = function (id) { var onActiveItemChange = this.props.onActiveItemChange; if (onActiveItemChange === undefined || this.innerRef.current === null) { return; } var activeNode = this.innerRef.current.querySelector("[data-key=\"item-" + id + "\"]"); if (activeNode) { onActiveItemChange(activeNode.id); } }; /** * Вызывает событие onChange. */ Menu.prototype.triggerOnChange = function () { var _a = this.props, onChange = _a.onChange, value = _a.value; var hoveredIndex = this.state.hoveredIndex; if (onChange !== undefined) { var items = getFlattenItems(this.props.items); var nextValue_1 = hoveredIndex === -1 ? '' : items[hoveredIndex].value; // Если value является массивом, то обрабатываем этот случай как multiselect radio или check. if (Array.isArray(value)) { var prevValues = tslib_1.__spread(value); if (prevValues.indexOf(nextValue_1) !== -1) { prevValues = prevValues.filter(function (prevValue) { return prevValue !== nextValue_1; }); } else { prevValues.push(nextValue_1); } nextValue_1 = prevValues; } // Создаем синтетическое событие и кладем туда только значение, // возможно в будущем стоит положить настоящий DOM элемент. var syntheticEvent = { target: { value: nextValue_1 } }; onChange(syntheticEvent); } }; Menu.displayName = exports.cnMenu(); return Menu; }(react_1.PureComponent)); exports.Menu = Menu;