@yandex/ui
Version:
Yandex UI components
225 lines (224 loc) • 11.8 kB
JavaScript
;
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;