@itwin/core-react
Version:
A react component library of iTwin.js UI general purpose components
370 lines • 15.7 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module ContextMenu
*/
import "./ContextMenu.scss";
import classnames from "classnames";
import * as React from "react";
import { Key } from "ts-key-enum";
import { ConditionalBooleanValue } from "@itwin/appui-abstract";
import { DivWithOutsideClick } from "../base/DivWithOutsideClick.js";
import { ContextMenuDirection } from "./ContextMenuDirection.js";
import { ContextMenuItem } from "./ContextMenuItem.js";
import { ContextSubMenu } from "./ContextSubMenu.js";
/** A context menu populated with [[ContextMenuItem]] components.
* Can be nested using [[ContextSubMenu]] component.
* @public
* @deprecated in 4.16.0. Use {@link https://itwinui.bentley.com/docs/dropdownmenu iTwinUI DropdownMenu} component instead.
*/
export class ContextMenu extends React.PureComponent {
_rootElement = null;
_menuElement = null;
_selectedElement = null;
_length = 0;
_hotKeyMap = new Map();
_lastChildren;
_lastDirection = ContextMenuDirection.BottomRight;
_lastSelectedIndex = 0;
_injectedChildren;
_parentWindowHeight = 0;
_parentWindowWidth = 0;
static defaultProps = {
direction: ContextMenuDirection.BottomRight,
autoflip: true,
edgeLimit: true,
hotkeySelect: true,
selectedIndex: -1,
floating: true,
};
state;
constructor(props) {
super(props);
this.state = {
selectedIndex: this.props.selectedIndex,
direction: props.direction,
ignoreNextKeyUp: props.ignoreNextKeyUp,
};
}
/** @internal */
static autoFlip = (dir, rect, windowWidth, windowHeight) => {
if (rect.right > windowWidth) {
switch (dir) {
case ContextMenuDirection.TopRight:
dir = ContextMenuDirection.TopLeft;
break;
case ContextMenuDirection.Right:
dir = ContextMenuDirection.Left;
break;
case ContextMenuDirection.BottomRight:
dir = ContextMenuDirection.BottomLeft;
break;
}
}
if (rect.left < 0) {
switch (dir) {
case ContextMenuDirection.TopLeft:
dir = ContextMenuDirection.TopRight;
break;
case ContextMenuDirection.Left:
dir = ContextMenuDirection.Right;
break;
case ContextMenuDirection.BottomLeft:
dir = ContextMenuDirection.BottomRight;
break;
}
}
if (rect.bottom > windowHeight) {
switch (dir) {
case ContextMenuDirection.BottomLeft:
dir = ContextMenuDirection.TopLeft;
break;
case ContextMenuDirection.Bottom:
dir = ContextMenuDirection.Top;
break;
case ContextMenuDirection.BottomRight:
dir = ContextMenuDirection.TopRight;
break;
}
}
if (rect.top < 0) {
switch (dir) {
case ContextMenuDirection.TopLeft:
dir = ContextMenuDirection.BottomLeft;
break;
case ContextMenuDirection.Top:
dir = ContextMenuDirection.Bottom;
break;
case ContextMenuDirection.TopRight:
dir = ContextMenuDirection.BottomRight;
break;
}
}
return dir;
};
_handleHotKeyParsed = (index, hotKey) => {
this._hotKeyMap.set(index, hotKey);
};
_handleOnOutsideClick = (event) => {
if (this.props.opened && this.props.onOutsideClick)
this.props.onOutsideClick(event);
};
render() {
const { opened, direction, onOutsideClick, onSelect, onEsc, autoflip, edgeLimit, hotkeySelect, selectedIndex, floating, parentMenu, parentSubmenu, children, className, ignoreNextKeyUp, offset = 0, ...props } = this.props;
const renderDirection = parentMenu === undefined ? this.state.direction : direction;
if (this._lastChildren !== children ||
this._lastDirection !== renderDirection ||
this._lastSelectedIndex !== this.state.selectedIndex) {
this._injectedChildren = this._injectMenuItemProps(children, renderDirection, this.state.selectedIndex);
this._lastChildren = children;
this._lastDirection = renderDirection;
this._lastSelectedIndex = this.state.selectedIndex;
}
return (React.createElement("div", { role: "presentation", className: classnames("core-context-menu", className), onKeyUp: this._handleKeyUp, onClick: this._handleClick, "data-testid": "core-context-menu-root", ...props, ref: this._rootRef },
React.createElement(DivWithOutsideClick, { onOutsideClick: this._handleOnOutsideClick },
React.createElement("div", { ref: this._menuRef, role: "menu", tabIndex: 0, "data-testid": "core-context-menu-container", className: classnames("core-context-menu-container", opened && "core-context-menu-opened", floating && "core-context-menu-floating", ContextMenu.getCSSClassNameFromDirection(renderDirection)), style: {
"--_core-context-menu-offset": `${offset}px`,
} }, this._injectedChildren))));
}
/** @internal */
static getCSSClassNameFromDirection = (direction) => {
let className = "";
if (direction === undefined)
direction = ContextMenuDirection.BottomRight;
if (direction === ContextMenuDirection.None)
return "";
switch (direction) {
case ContextMenuDirection.TopLeft:
className = "core-context-menu-top core-context-menu-left";
break;
case ContextMenuDirection.Top:
className = "core-context-menu-top";
break;
case ContextMenuDirection.TopRight:
className = "core-context-menu-top core-context-menu-right";
break;
case ContextMenuDirection.Left:
className = "core-context-menu-left";
break;
case ContextMenuDirection.Center:
className = "core-context-menu-center";
break;
case ContextMenuDirection.Right:
className = "core-context-menu-right";
break;
case ContextMenuDirection.BottomLeft:
className = "core-context-menu-bottom core-context-menu-left";
break;
case ContextMenuDirection.Bottom:
className = "core-context-menu-bottom";
break;
case ContextMenuDirection.BottomRight:
className = "core-context-menu-bottom core-context-menu-right";
break;
}
return className;
};
_injectMenuItemProps = (children, direction, selectedIndex) => {
let index = 0;
// add inheritance data to submenu children
const ch = React.Children.map(children, (child) => {
// Capture only ContextSubMenus and ContextMenuItems.
if (child &&
typeof child === "object" &&
"props" in child &&
(child.type === ContextSubMenu || child.type === ContextMenuItem) &&
React.isValidElement(child) &&
!ConditionalBooleanValue.getValue(child.props.disabled) &&
!ConditionalBooleanValue.getValue(child.props.hidden)) {
const id = index; // get separate id variable so value stays the same when onHover is called later.
const onHover = () => {
this.setState({ selectedIndex: id });
this.focus();
};
const ref = (el) => {
if (selectedIndex === id)
// only save to this._selectedElement if previously captured bool is true
this._selectedElement = el;
};
const boundHandleHotKeyParse = this._handleHotKeyParsed.bind(this, id); // bind local callback for specific index
const childProps = {
parentMenu: this,
ref,
onHover,
isSelected: selectedIndex === id,
onHotKeyParsed: boundHandleHotKeyParse,
};
if (child.type === ContextSubMenu &&
React.isValidElement(child)) {
// add direction only to sub-menus
childProps.direction = child.props.direction || direction;
}
index++;
return React.cloneElement(child, childProps);
}
else
return child; // Else, pass through unmodified
});
this._length = index;
return ch;
};
_rootRef = (el) => {
this._rootElement = el;
};
_menuRef = (el) => {
this._menuElement = el;
};
getWindow() {
const el = this._rootElement ? this._rootElement : this._menuElement;
const parentDocument = el.ownerDocument;
return parentDocument.defaultView ?? window;
}
componentDidMount() {
const parentWindow = this.getWindow();
parentWindow.addEventListener("focus", this._handleFocusChange);
parentWindow.addEventListener("mouseup", this._handleFocusChange);
this.checkRenderDirection();
if (this.props.opened)
this.focus();
}
componentWillUnmount() {
const parentWindow = this.getWindow();
parentWindow.removeEventListener("focus", this._handleFocusChange);
parentWindow.removeEventListener("mouseup", this._handleFocusChange);
}
checkRenderDirection() {
const { direction, autoflip, parentMenu } = this.props;
const parentWindow = this.getWindow();
let renderDirection = parentMenu === undefined ? this.state.direction : direction;
if (parentWindow.innerHeight === this._parentWindowHeight &&
parentWindow.innerWidth === this._parentWindowWidth)
return;
this._parentWindowHeight = parentWindow.innerHeight;
this._parentWindowWidth = parentWindow.innerWidth;
// check if menu should flip
if (parentWindow && autoflip && parentMenu === undefined) {
const menuRect = this.getRect();
renderDirection = ContextMenu.autoFlip(renderDirection, menuRect, parentWindow.innerWidth, parentWindow.innerHeight);
if (renderDirection !== this.state.direction)
this.setState({ direction: renderDirection });
}
}
focus = () => {
if (this._menuElement)
this._menuElement.focus();
};
blur = () => {
if (this._menuElement)
this._menuElement.blur();
};
getRect = () => {
let clientRect = DOMRect.fromRect({ x: 0, y: 0, width: 0, height: 0 });
if (this._menuElement) {
clientRect = this._menuElement.getBoundingClientRect();
}
return clientRect;
};
_handleFocusChange = (event) => {
if (this._rootElement &&
this.props.opened &&
event.target instanceof Node &&
this.props.onOutsideClick &&
!this._rootElement.contains(event.target))
this.props.onOutsideClick(event);
};
_handleClick = (event) => {
if (this.props.onSelect)
this.props.onSelect(event);
};
_handleKeyUp = (event) => {
if (this.state.ignoreNextKeyUp) {
this.setState({ ignoreNextKeyUp: false });
return;
}
if (event.key) {
for (const [key, value] of this._hotKeyMap) {
if (!this.props.hotkeySelect && key > this.state.selectedIndex) {
// Start search at current index.
if (event.key.toUpperCase() === value) {
this.setState({ selectedIndex: key });
return;
}
}
}
for (const [key, value] of this._hotKeyMap) {
if (event.key.toUpperCase() === value) {
this.setState({ selectedIndex: key }, () => {
if (this.props.hotkeySelect && this._selectedElement) {
this._selectedElement.select();
}
});
event.stopPropagation();
return;
}
}
}
if (event.key === Key.ArrowLeft.valueOf()) {
event.stopPropagation();
if (this.props.parentMenu && this.props.parentSubmenu) {
this.props.parentSubmenu.close();
this.props.parentMenu.focus();
}
if (this.props.onEsc)
this.props.onEsc(event);
}
if (event.key === Key.Escape.valueOf()) {
if (this.props.onEsc)
this.props.onEsc(event);
}
if ((event.key === Key.Enter.valueOf() ||
event.key === Key.ArrowRight.valueOf()) &&
this._selectedElement) {
event.stopPropagation();
if (event.key === Key.Enter.valueOf() ||
this._selectedElement instanceof ContextSubMenu) {
if (this._selectedElement.select)
this._selectedElement.select();
}
}
let { selectedIndex } = this.state;
if (event.key === Key.ArrowUp.valueOf() ||
event.key === Key.ArrowDown.valueOf()) {
event.stopPropagation();
if (selectedIndex === -1) {
selectedIndex = 0;
}
else {
if (event.key === Key.ArrowUp.valueOf()) {
if (this.state.selectedIndex === 0)
selectedIndex = this._length - 1;
else
selectedIndex--;
}
if (event.key === Key.ArrowDown.valueOf()) {
if (this.state.selectedIndex === this._length - 1)
selectedIndex = 0;
else
selectedIndex++;
}
}
}
this.setState({ selectedIndex });
};
/** @internal */
componentDidUpdate(prevProps) {
if (prevProps.selectedIndex !== this.props.selectedIndex) {
this.setState((_, props) => ({ selectedIndex: props.selectedIndex }));
}
if (!prevProps.opened && this.props.opened) {
this.setState((_, props) => ({ selectedIndex: props.selectedIndex }));
}
if (!this.props.parentMenu) {
// const direction = this.props.direction!;
// if ((!this.props.opened && prevProps.opened && direction !== this.state.direction) || prevProps.direction !== direction)
this.checkRenderDirection();
}
}
}
//# sourceMappingURL=ContextMenu.js.map