mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Lens app source code (#119)
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
1 parent
936cbd53d4
commit
1d0815abd2
797 files changed
+73714
-215
No files matched your search
@@ -0,0 +1,2 @@
|
||||
export * from './menu'
|
||||
export * from './menu-picker'
|
||||
@@ -0,0 +1,62 @@
|
||||
.MenuActions {
|
||||
&.toolbar {
|
||||
--flex-gap: #{$padding / 2};
|
||||
|
||||
position: static;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
flex-direction: row;
|
||||
margin-right: var(--flex-gap) !important;
|
||||
|
||||
.Icon {
|
||||
color: $textColorAccent;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.title, .arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .MenuItem {
|
||||
background: none !important;
|
||||
padding: 0;
|
||||
|
||||
> .SubMenu {
|
||||
$offset: $margin;
|
||||
|
||||
margin-top: $offset;
|
||||
top: 100%;
|
||||
right: $padding;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: -$offset;
|
||||
height: $offset;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.MenuItem {
|
||||
position: relative;
|
||||
|
||||
&:hover > .SubMenu {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
> .SubMenu {
|
||||
margin: 0;
|
||||
transition: visibility, opacity 150ms ease-in-out;
|
||||
transition-delay: 150ms;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
right: 100%;
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import "./menu-actions.scss"
|
||||
|
||||
import React, { isValidElement } from "react";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { autobind, cssNames } from "../../utils";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { Icon, IconProps } from "../icon";
|
||||
import { Menu, MenuItem, MenuProps } from "../menu";
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
import isString from "lodash/isString";
|
||||
import { _i18n } from "../../i18n";
|
||||
|
||||
export interface MenuActionsProps extends Partial<MenuProps> {
|
||||
className?: string;
|
||||
toolbar?: boolean; // display menu as toolbar with icons
|
||||
triggerIcon?: string | IconProps | React.ReactNode;
|
||||
removeConfirmationMessage?: React.ReactNode | (() => React.ReactNode);
|
||||
updateAction?(): void;
|
||||
removeAction?(): void;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class MenuActions extends React.Component<MenuActionsProps> {
|
||||
static defaultProps: MenuActionsProps = {
|
||||
get removeConfirmationMessage() {
|
||||
return _i18n._(t`Remove item?`)
|
||||
}
|
||||
}
|
||||
|
||||
public id = uniqueId("menu_actions_");
|
||||
|
||||
@observable isOpen = !!this.props.toolbar;
|
||||
|
||||
toggle = () => {
|
||||
if (this.props.toolbar) return;
|
||||
this.isOpen = !this.isOpen;
|
||||
};
|
||||
|
||||
@autobind()
|
||||
remove() {
|
||||
const { removeAction} = this.props;
|
||||
let { removeConfirmationMessage } = this.props;
|
||||
|
||||
if (typeof removeConfirmationMessage === "function") {
|
||||
removeConfirmationMessage = removeConfirmationMessage();
|
||||
}
|
||||
ConfirmDialog.open({
|
||||
ok: removeAction,
|
||||
labelOk: _i18n._(t`Remove`),
|
||||
message: <div>{removeConfirmationMessage}</div>,
|
||||
})
|
||||
}
|
||||
|
||||
renderTriggerIcon() {
|
||||
if (this.props.toolbar) return;
|
||||
const { triggerIcon = "more_vert" } = this.props;
|
||||
if (isValidElement<HTMLElement>(triggerIcon)) {
|
||||
const className = cssNames(triggerIcon.props.className, { active: this.isOpen });
|
||||
return React.cloneElement(triggerIcon, { id: this.id, className } as any);
|
||||
}
|
||||
const iconProps: Partial<IconProps> = {
|
||||
id: this.id,
|
||||
interactive: true,
|
||||
material: isString(triggerIcon) ? triggerIcon : undefined,
|
||||
active: this.isOpen,
|
||||
...(typeof triggerIcon === "object" ? triggerIcon : {}),
|
||||
}
|
||||
if (iconProps.tooltip && this.isOpen) {
|
||||
delete iconProps.tooltip; // don't show tooltip for icon when menu is open
|
||||
}
|
||||
return (
|
||||
<Icon {...iconProps}/>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
toolbar, children, updateAction, removeAction, triggerIcon, removeConfirmationMessage,
|
||||
...menuProps
|
||||
} = this.props;
|
||||
let { className } = this.props;
|
||||
className = cssNames("MenuActions flex", className, {
|
||||
toolbar: toolbar,
|
||||
gaps: toolbar, // add spacing for .flex
|
||||
});
|
||||
const autoClose = !toolbar;
|
||||
return (
|
||||
<>
|
||||
{this.renderTriggerIcon()}
|
||||
<Menu
|
||||
htmlFor={this.id}
|
||||
isOpen={this.isOpen} open={this.toggle} close={this.toggle}
|
||||
className={className}
|
||||
usePortal={autoClose}
|
||||
closeOnScroll={autoClose}
|
||||
closeOnClickItem={autoClose}
|
||||
closeOnClickOutside={autoClose}
|
||||
{...menuProps}
|
||||
>
|
||||
{children}
|
||||
{updateAction && (
|
||||
<MenuItem onClick={updateAction}>
|
||||
<Icon material="edit" interactive={toolbar} title={_i18n._(t`Edit`)}/>
|
||||
<span className="title"><Trans>Edit</Trans></span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{removeAction && (
|
||||
<MenuItem onClick={this.remove}>
|
||||
<Icon material="delete" interactive={toolbar} title={_i18n._(t`Delete`)}/>
|
||||
<span className="title"><Trans>Remove</Trans></span>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
.MenuPicker {
|
||||
&.waiting {
|
||||
.MenuItem {
|
||||
pointer-events: none;
|
||||
opacity: .75;
|
||||
}
|
||||
}
|
||||
|
||||
> .Button {
|
||||
padding: $padding /2 $padding * 2;
|
||||
}
|
||||
|
||||
.menu-header {
|
||||
background: var(--bgc-active);
|
||||
padding: $padding;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $textColorAccent;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
background: var(--bgc-active) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.Menu {
|
||||
@include custom-scrollbar;
|
||||
|
||||
border-radius: $radius;
|
||||
box-shadow: -$unit / 2 $unit / 2 22px $boxShadow;
|
||||
margin-top: ceil($margin / 1.33);
|
||||
overflow: hidden;
|
||||
min-width: 150px;
|
||||
max-height: $unit * 35;
|
||||
}
|
||||
|
||||
.MenuItem {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding-right: $padding;
|
||||
white-space: normal;
|
||||
|
||||
&:before {
|
||||
min-width: 20px;
|
||||
font-family: "Material Icons";
|
||||
font-weight: bold;
|
||||
content: ' '
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $colorInfo;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
|
||||
&:not(:hover) {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: 'check';
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid $borderColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import './menu-picker.scss'
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Menu, MenuProps } from "./menu";
|
||||
import { Icon } from "../icon";
|
||||
import { Button } from "../button";
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
|
||||
interface Props extends Partial<MenuProps> {
|
||||
title: React.ReactNode;
|
||||
waiting?: boolean;
|
||||
}
|
||||
|
||||
export function MenuPicker(props: Props) {
|
||||
const id = useRef(uniqueId("menu_picker_")).current;
|
||||
const { className, title, waiting, children, ...menuProps } = props;
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
|
||||
const toggle = () => setOpen(!isOpen);
|
||||
|
||||
return (
|
||||
<div className={cssNames("MenuPicker", className, { waiting })}>
|
||||
<Button primary id={id}>
|
||||
{title}
|
||||
<Icon material="arrow_drop_down"/>
|
||||
</Button>
|
||||
<Menu
|
||||
htmlFor={id}
|
||||
isOpen={isOpen} open={toggle} close={toggle}
|
||||
closeOnClickItem={false}
|
||||
{...menuProps}
|
||||
>
|
||||
<div className="menu-header flex gaps">
|
||||
<span className="box grow">{title}</span>
|
||||
<Icon small material="close" onClick={toggle}/>
|
||||
</div>
|
||||
{children}
|
||||
</Menu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
.Menu {
|
||||
--bgc: #{$contentColor};
|
||||
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bgc);
|
||||
list-style: none;
|
||||
border: 1px solid $borderColor;
|
||||
z-index: 101;
|
||||
|
||||
&.portal {
|
||||
left: -1000px;
|
||||
top: -1000px;
|
||||
|
||||
&.top {
|
||||
margin-top: -$margin;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
margin-top: $margin;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.portal) {
|
||||
margin: $margin 0;
|
||||
|
||||
&.left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.top {
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.MenuItem {
|
||||
--color: #{$textColorPrimary};
|
||||
--color-active: #{$textColorAccent};
|
||||
--bgc-active: #{$menuActiveBackground};
|
||||
--spacing: #{$padding};
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color);
|
||||
padding: var(--spacing);
|
||||
white-space: nowrap;
|
||||
transition: 250ms background;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
color: var(--color-active);
|
||||
background: var(--bgc-active);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $colorInfo;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--color-active);
|
||||
background-color: var(--bgc-active);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
&.spacer {
|
||||
padding: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid $contentColor;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.Icon {
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
margin-right: $padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
import './menu.scss'
|
||||
|
||||
import React, { Fragment, ReactElement, ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { autobind, cssNames, noop } from "../../utils";
|
||||
import { Animate } from "../animate";
|
||||
import { Icon, IconProps } from "../icon";
|
||||
import debounce from "lodash/debounce"
|
||||
|
||||
export const MenuContext = React.createContext<MenuContextValue>(null);
|
||||
export type MenuContextValue = Menu;
|
||||
|
||||
interface MenuPosition {
|
||||
left?: boolean;
|
||||
top?: boolean;
|
||||
right?: boolean;
|
||||
bottom?: boolean;
|
||||
}
|
||||
|
||||
export interface MenuProps {
|
||||
isOpen?: boolean;
|
||||
open(): void;
|
||||
close(): void;
|
||||
className?: string;
|
||||
htmlFor?: string;
|
||||
autoFocus?: boolean;
|
||||
usePortal?: boolean | HTMLElement;
|
||||
closeOnClickItem?: boolean; // close menu on item click
|
||||
closeOnClickOutside?: boolean; // use false value for sub-menus
|
||||
closeOnScroll?: boolean; // applicable when usePortal={true}
|
||||
position?: MenuPosition; // applicable when usePortal={false}
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
position?: MenuPosition;
|
||||
}
|
||||
|
||||
const defaultPropsMenu: Partial<MenuProps> = {
|
||||
position: { right: true, bottom: true },
|
||||
autoFocus: false,
|
||||
usePortal: false,
|
||||
closeOnClickItem: true,
|
||||
closeOnClickOutside: true,
|
||||
closeOnScroll: false,
|
||||
};
|
||||
|
||||
@autobind()
|
||||
export class Menu extends React.Component<MenuProps, State> {
|
||||
static defaultProps = defaultPropsMenu as object;
|
||||
|
||||
public opener: HTMLElement;
|
||||
public elem: HTMLUListElement;
|
||||
protected items: { [index: number]: MenuItem } = {};
|
||||
|
||||
public state: State = {};
|
||||
|
||||
get isOpen() {
|
||||
return !!this.props.isOpen;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.usePortal) {
|
||||
const parent = this.elem.parentElement;
|
||||
const position = window.getComputedStyle(parent).position;
|
||||
if (position === 'static') parent.style.position = 'relative';
|
||||
}
|
||||
else if (this.isOpen) {
|
||||
this.refreshPosition();
|
||||
}
|
||||
this.opener = document.getElementById(this.props.htmlFor); // might not exist in sub-menus
|
||||
if (this.opener) {
|
||||
this.opener.addEventListener('click', this.toggle);
|
||||
this.opener.addEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
this.elem.addEventListener('keydown', this.onKeyDown);
|
||||
window.addEventListener('resize', this.onWindowResize);
|
||||
window.addEventListener('click', this.onClickOutside, true);
|
||||
window.addEventListener('scroll', this.onScrollOutside, true);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.opener) {
|
||||
this.opener.removeEventListener('click', this.toggle);
|
||||
this.opener.removeEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
this.elem.removeEventListener('keydown', this.onKeyDown);
|
||||
window.removeEventListener('resize', this.onWindowResize);
|
||||
window.removeEventListener('click', this.onClickOutside, true);
|
||||
window.removeEventListener('scroll', this.onScrollOutside, true);
|
||||
}
|
||||
|
||||
protected get focusableItems() {
|
||||
return Object.values(this.items).filter(item => item.isFocusable);
|
||||
}
|
||||
|
||||
protected get focusedItem() {
|
||||
return this.focusableItems.find(item => item.elem === document.activeElement);
|
||||
}
|
||||
|
||||
protected focusNextItem(reverse = false) {
|
||||
const items = this.focusableItems;
|
||||
const activeIndex = items.findIndex(item => item === this.focusedItem);
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
if (activeIndex > -1) {
|
||||
let nextItem = reverse ? items[activeIndex - 1] : items[activeIndex + 1];
|
||||
if (!nextItem) nextItem = items[activeIndex];
|
||||
nextItem.elem.focus();
|
||||
}
|
||||
else {
|
||||
items[0].elem.focus();
|
||||
}
|
||||
}
|
||||
|
||||
refreshPosition = debounce(() => {
|
||||
if (!this.props.usePortal || !this.opener) return;
|
||||
const { width, height } = this.opener.getBoundingClientRect();
|
||||
let { left, top, bottom, right } = this.opener.getBoundingClientRect();
|
||||
const withScroll = window.getComputedStyle(this.elem).position !== "fixed";
|
||||
|
||||
// window global scroll corrections
|
||||
if (withScroll) {
|
||||
left += window.pageXOffset;
|
||||
top += window.pageYOffset;
|
||||
right = left + width;
|
||||
bottom = top + height;
|
||||
}
|
||||
|
||||
// setup initial position
|
||||
const position: MenuPosition = { left: true, bottom: true };
|
||||
this.elem.style.left = left + "px"
|
||||
this.elem.style.top = bottom + "px"
|
||||
|
||||
// correct position if menu doesn't fit to viewport
|
||||
const menuPos = this.elem.getBoundingClientRect();
|
||||
if (menuPos.right > window.innerWidth) {
|
||||
this.elem.style.left = (right - this.elem.offsetWidth) + "px";
|
||||
position.right = true;
|
||||
delete position.left;
|
||||
}
|
||||
if (menuPos.bottom > window.innerHeight) {
|
||||
this.elem.style.top = (top - this.elem.offsetHeight) + "px";
|
||||
position.top = true;
|
||||
delete position.bottom;
|
||||
}
|
||||
this.setState({ position });
|
||||
}, Animate.VISIBILITY_DELAY_MS);
|
||||
|
||||
open() {
|
||||
if (this.isOpen) return;
|
||||
this.props.open();
|
||||
this.refreshPosition();
|
||||
if (this.props.autoFocus) this.focusNextItem();
|
||||
}
|
||||
|
||||
close() {
|
||||
if (!this.isOpen) return;
|
||||
this.props.close();
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.isOpen ? this.close() : this.open();
|
||||
}
|
||||
|
||||
onKeyDown(evt: KeyboardEvent) {
|
||||
if (!this.isOpen) return;
|
||||
switch (evt.code) {
|
||||
case "Escape":
|
||||
this.close();
|
||||
break;
|
||||
|
||||
case "Space":
|
||||
case "Enter":
|
||||
const focusedItem = this.focusedItem;
|
||||
if (focusedItem) {
|
||||
focusedItem.elem.click();
|
||||
evt.preventDefault();
|
||||
}
|
||||
break;
|
||||
|
||||
case "ArrowUp":
|
||||
this.focusNextItem(true);
|
||||
break;
|
||||
case "ArrowDown":
|
||||
this.focusNextItem();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onWindowResize(evt: UIEvent) {
|
||||
if (!this.isOpen) return;
|
||||
this.refreshPosition();
|
||||
}
|
||||
|
||||
onScrollOutside(evt: UIEvent) {
|
||||
if (!this.isOpen) return;
|
||||
const target = evt.target as HTMLElement;
|
||||
const { usePortal, closeOnScroll } = this.props;
|
||||
if (usePortal && closeOnScroll && !target.contains(this.elem)) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
onClickOutside(evt: MouseEvent) {
|
||||
if (!this.props.closeOnClickOutside) return;
|
||||
if (!this.isOpen || evt.target === document.body) return;
|
||||
const target = evt.target as HTMLElement;
|
||||
const clickInsideMenu = this.elem.contains(target);
|
||||
const clickOnOpener = this.opener && this.opener.contains(target);
|
||||
if (!clickInsideMenu && !clickOnOpener) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
protected bindRef(elem: HTMLUListElement) {
|
||||
this.elem = elem;
|
||||
}
|
||||
|
||||
protected bindItemRef(item: MenuItem, index: number) {
|
||||
this.items[index] = item;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { position } = this.props;
|
||||
let { className, usePortal } = this.props;
|
||||
className = cssNames('Menu', className, this.state.position || position, {
|
||||
portal: usePortal,
|
||||
});
|
||||
|
||||
let children = this.props.children as ReactElement<any>;
|
||||
if (children.type === Fragment) {
|
||||
children = children.props.children;
|
||||
}
|
||||
const menuItems = React.Children.toArray(children).map((item: ReactElement<MenuItemProps>, index) => {
|
||||
if (item.type === MenuItem) {
|
||||
return React.cloneElement(item, {
|
||||
ref: (item: MenuItem) => this.bindItemRef(item, index)
|
||||
});
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
const menu = (
|
||||
<MenuContext.Provider value={this}>
|
||||
<Animate enter={this.isOpen}>
|
||||
<ul className={className} ref={this.bindRef}>
|
||||
{menuItems}
|
||||
</ul>
|
||||
</Animate>
|
||||
</MenuContext.Provider>
|
||||
);
|
||||
if (usePortal === true) usePortal = document.body;
|
||||
return usePortal instanceof HTMLElement ? createPortal(menu, usePortal) : menu;
|
||||
}
|
||||
}
|
||||
|
||||
export function SubMenu(props: Partial<MenuProps>) {
|
||||
const { className, ...menuProps } = props;
|
||||
return (
|
||||
<Menu
|
||||
className={cssNames("SubMenu", className)}
|
||||
isOpen open={noop} close={noop}
|
||||
position={{}} // reset position, must be handled in css
|
||||
closeOnClickOutside={false}
|
||||
closeOnClickItem={false}
|
||||
{...menuProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export interface MenuItemProps extends React.HTMLProps<any> {
|
||||
icon?: string | Partial<IconProps>;
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
spacer?: boolean;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
const defaultPropsMenuItem: Partial<MenuItemProps> = {
|
||||
onClick: noop,
|
||||
};
|
||||
|
||||
@autobind()
|
||||
export class MenuItem extends React.Component<MenuItemProps> {
|
||||
static defaultProps = defaultPropsMenuItem as object;
|
||||
static contextType = MenuContext;
|
||||
|
||||
public context: MenuContextValue;
|
||||
public elem: HTMLElement;
|
||||
|
||||
get isFocusable() {
|
||||
const { disabled, spacer } = this.props;
|
||||
return !(disabled || spacer);
|
||||
}
|
||||
|
||||
get isLink() {
|
||||
return !!this.props.href;
|
||||
}
|
||||
|
||||
onClick(evt: React.MouseEvent) {
|
||||
const menu = this.context;
|
||||
const { spacer, onClick } = this.props;
|
||||
if (spacer) return;
|
||||
onClick(evt);
|
||||
if (menu.props.closeOnClickItem && !evt.defaultPrevented) {
|
||||
menu.close();
|
||||
}
|
||||
}
|
||||
|
||||
protected bindRef(elem: HTMLElement) {
|
||||
this.elem = elem;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, disabled, active, spacer, icon, children, ...props } = this.props;
|
||||
let iconProps: Partial<IconProps>;
|
||||
if (icon) {
|
||||
iconProps = {};
|
||||
if (typeof icon === "string") iconProps.material = icon;
|
||||
else Object.assign(iconProps, icon);
|
||||
}
|
||||
const elemProps: React.HTMLProps<any> = {
|
||||
tabIndex: this.isFocusable ? 0 : -1,
|
||||
...props,
|
||||
className: cssNames("MenuItem", className, { disabled, active, spacer }),
|
||||
onClick: this.onClick,
|
||||
children: icon ? <><Icon {...iconProps}/> {children}</> : children,
|
||||
ref: this.bindRef,
|
||||
}
|
||||
if (this.isLink) {
|
||||
return <a {...elemProps}/>
|
||||
}
|
||||
return <li {...elemProps}/>
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user