Merge branch 'feature/new-context-menu' into feature/virtual-room-1.2

This commit is contained in:
Timofey 2022-02-01 16:07:27 +08:00
commit 750becafb8
16 changed files with 1392 additions and 8 deletions

View File

@ -0,0 +1,37 @@
# DropDownItem
Is a item of DropDown or ContextMenu component
### Usage
```js
import MenuItem from "@appserver/components/menu-item";
```
```jsx
<MenuItem
isSeparator={false}
isHeader={false}
label="Button 1"
icon="static/images/nav.logo.react.svg"
onClick={() => console.log("Button 1 clicked")}
/>
```
An item can act as separator, header, line, line with arrow or container.
When used as container, it will retain all styling features and positioning. To disable hover effects in container mode, you can use _noHover_ property.`
### Properties
| Props | Type | Required | Values | Default | Description |
| ------------- | :------------: | :------: | :----: | :-------------: | ---------------------------------------------------------- |
| `className` | `string` | - | - | - | Accepts class |
| `id` | `string` | - | - | - | Accepts id |
| `icon` | `string` | - | - | - | Dropdown item icon |
| `label` | `string` | - | - | `Dropdown item` | Dropdown item text |
| `isHeader` | `bool` | - | - | `false` | Tells when the dropdown item should display like header |
| `isSeparator` | `bool` | - | - | `false` | Tells when the dropdown item should display like separator |
| `noHover` | `bool` | - | - | `false` | Disable default style hover effect |
| `onClick` | `func` | - | - | - | What the dropdown item will trigger when clicked |
| `style` | `obj`, `array` | - | - | - | Accepts css style |

View File

@ -0,0 +1,167 @@
import React from "react";
import PropTypes from "prop-types";
import { ReactSVG } from "react-svg";
import DomHelpers from "../utils/domHelpers";
import { isMobile } from "react-device-detect";
import {
isTablet as isTabletUtils,
isMobile as isMobileUtils,
} from "../utils/device";
import { StyledMenuItem, StyledText, IconWrapper } from "./styled-menu-item";
import ArrowIcon from "./svg/folder-arrow.react.svg";
import ArrowMobileIcon from "./svg/folder-arrow.mobile.react.svg";
import NewContextMenu from "../new-context-menu";
//TODO: Add arrow type
const MenuItem = (props) => {
const [hover, setHover] = React.useState(false);
const [positionContextMenu, setPositionContextMenu] = React.useState(null);
const itemRef = React.useRef(null);
const cmRef = React.useRef(null);
//console.log("MenuItem render");
const {
isHeader,
isSeparator,
label,
icon,
options,
children,
onClick,
hideMenu,
className,
} = props;
const onHover = () => {
if (!cmRef.current) return;
if (hover) {
getPosition();
cmRef.current.show(new Event("click"));
} else {
cmRef.current.hide(new Event("click"));
}
};
const getPosition = () => {
if (!cmRef.current) return;
if (!itemRef.current) return;
const outerWidth = DomHelpers.getOuterWidth(itemRef.current);
const offset = DomHelpers.getOffset(itemRef.current);
setPositionContextMenu({
top: offset.top,
left: offset.left + outerWidth + 10,
width: outerWidth,
});
};
React.useEffect(() => {
onHover();
}, [hover]);
const onClickAction = (e) => {
onClick && onClick(e);
hideMenu();
};
return options ? (
<StyledMenuItem
{...props}
className={className}
onClick={onClickAction}
ref={itemRef}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
{icon && (
<IconWrapper isHeader={isHeader}>
<ReactSVG src={icon} className="drop-down-item_icon" />
</IconWrapper>
)}
{isSeparator ? (
<></>
) : label ? (
<>
<StyledText isHeader={isHeader} truncate={true}>
{label}
</StyledText>
{isMobile || isTabletUtils() || isMobileUtils() ? (
<ArrowMobileIcon className="arrow-icon" />
) : (
<ArrowIcon className="arrow-icon" />
)}
<NewContextMenu
ref={cmRef}
model={options}
withBackdrop={false}
position={positionContextMenu}
/>
</>
) : (
children && children
)}
</StyledMenuItem>
) : (
<StyledMenuItem {...props} className={className} onClick={onClickAction}>
{icon && (
<IconWrapper isHeader={isHeader}>
<ReactSVG src={icon} className="drop-down-item_icon" />
</IconWrapper>
)}
{isSeparator ? (
<></>
) : label ? (
<>
<StyledText isHeader={isHeader} truncate={true}>
{label}
</StyledText>
</>
) : (
children && children
)}
</StyledMenuItem>
);
};
MenuItem.propTypes = {
/** Tells when the menu item should display like separator */
isSeparator: PropTypes.bool,
/** Tells when the menu item should display like header */
isHeader: PropTypes.bool,
/** Accepts tab-index */
tabIndex: PropTypes.number,
/** Menu item text */
label: PropTypes.string,
/** Menu item icon */
icon: PropTypes.string,
/** Tells when the menu item should display like arrow and open context menu */
options: PropTypes.array,
/** Disable default style hover effect */
noHover: PropTypes.bool,
/** What the menu item will trigger when clicked */
onClick: PropTypes.func,
/** Children elements */
children: PropTypes.any,
/** Accepts class */
className: PropTypes.string,
/** Accepts id */
id: PropTypes.string,
/** Accepts css style */
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
/** Accepts css text-overflow */
textOverflow: PropTypes.bool,
};
MenuItem.defaultProps = {
isSeparator: false,
isHeader: false,
noHover: false,
textOverflow: false,
tabIndex: -1,
label: "",
};
export default MenuItem;

View File

@ -0,0 +1,70 @@
import React from "react";
import MenuItem from ".";
export default {
title: "Components/MenuItem",
component: MenuItem,
argTypes: {
onClick: { action: "onClick" },
},
parameters: {
docs: {
description: {
component: `Is a item of DropDown or ContextMenu component
An item can act as separator, header, line, line with arrow or container.
When used as container, it will retain all styling features and positioning. To disable hover effects in container mode, you can use _noHover_ property.`,
},
},
},
};
const Template = () => {
return (
<div style={{ width: "250px", position: "relative" }}>
<MenuItem
icon={"static/images/nav.logo.react.svg"}
label="Header(tablet or mobile)"
isHeader={true}
onClick={() => console.log("Header clicked")}
noHover={true}
/>
<MenuItem
icon={"static/images/nav.logo.react.svg"}
label="First item"
onClick={() => console.log("Button 1 clicked")}
/>
<MenuItem isSeparator={true} />
<MenuItem
icon={"static/images/nav.logo.react.svg"}
label="Item after separator"
onClick={() => console.log("Button 2 clicked")}
/>
<MenuItem onClick={() => console.log("Button 3 clicked")}>
<div>some child without styles</div>
</MenuItem>
<MenuItem
icon={"static/images/nav.logo.react.svg"}
label="Item after separator"
options={[
{
key: "key1",
icon: "static/images/nav.logo.react.svg",
label: "Item after separator",
onClick: () => console.log("Button 1 clicked"),
},
{
key: "key2",
icon: "static/images/nav.logo.react.svg",
label: "Item after separator",
onClick: () => console.log("Button 2 clicked"),
},
]}
onClick={() => console.log("Button 2 clicked")}
/>
</div>
);
};
export const Default = Template.bind({});

View File

@ -0,0 +1,76 @@
import React from "react";
import { mount, shallow } from "enzyme";
import MenuItem from ".";
const baseProps = {
isSeparator: false,
isHeader: false,
tabIndex: -1,
label: "test",
disabled: false,
icon: "static/images/nav.logo.react.svg",
noHover: false,
onClick: jest.fn(),
};
describe("<MenuItem />", () => {
it("renders without error", () => {
const wrapper = mount(<MenuItem {...baseProps} />);
expect(wrapper).toExist();
});
it("check isSeparator props", () => {
const wrapper = mount(<MenuItem {...baseProps} isSeparator={true} />);
expect(wrapper.prop("isSeparator")).toEqual(true);
});
it("check isHeader props", () => {
const wrapper = mount(<MenuItem {...baseProps} isHeader={true} />);
expect(wrapper.prop("isHeader")).toEqual(true);
});
it("check noHover props", () => {
const wrapper = mount(<MenuItem {...baseProps} noHover={true} />);
expect(wrapper.prop("noHover")).toEqual(true);
});
it("causes function onClick()", () => {
const onClick = jest.fn();
const wrapper = shallow(
<MenuItem id="test" {...baseProps} onClick={onClick} />
);
wrapper.find("#test").simulate("click");
expect(onClick).toHaveBeenCalled();
});
it("render without child", () => {
const wrapper = shallow(<MenuItem>test</MenuItem>);
expect(wrapper.props.children).toEqual(undefined);
});
it("accepts id", () => {
const wrapper = mount(<MenuItem {...baseProps} id="testId" />);
expect(wrapper.prop("id")).toEqual("testId");
});
it("accepts className", () => {
const wrapper = mount(<MenuItem {...baseProps} className="test" />);
expect(wrapper.prop("className")).toEqual("test");
});
it("accepts style", () => {
const wrapper = mount(<MenuItem {...baseProps} style={{ color: "red" }} />);
expect(wrapper.getDOMNode().style).toHaveProperty("color", "red");
});
});

View File

@ -0,0 +1,168 @@
import styled, { css } from "styled-components";
import Base from "../themes/base";
import Text from "../text/";
import { tablet } from "../utils/device";
import { isMobile } from "react-device-detect";
const styledHeaderText = css`
font-size: ${(props) => props.theme.menuItem.text.header.fontSize};
line-height: ${(props) => props.theme.menuItem.text.header.lineHeight};
`;
const styledMobileText = css`
font-size: ${(props) => props.theme.menuItem.text.mobile.fontSize};
line-height: ${(props) => props.theme.menuItem.text.mobile.lineHeight};
`;
const StyledText = styled(Text)`
width: 100%;
font-weight: ${(props) => props.theme.menuItem.text.fontWeight};
font-size: ${(props) => props.theme.menuItem.text.fontSize};
line-height: ${(props) => props.theme.menuItem.text.lineHeight};
margin: ${(props) => props.theme.menuItem.text.margin};
color: ${(props) => props.theme.menuItem.text.color};
text-align: left;
text-transform: none;
text-decoration: none;
user-select: none;
${isMobile
? (props) => (props.isHeader ? styledHeaderText : styledMobileText)
: null}
@media ${tablet} {
${(props) => (props.isHeader ? styledHeaderText : styledMobileText)}
}
`;
StyledText.defaultProps = { theme: Base };
const StyledMenuItem = styled.div`
display: ${(props) =>
props.isHeader && !isMobile
? "none"
: props.textOverflow
? "block"
: "flex"};
align-items: center;
width: 100%;
height: ${(props) =>
isMobile
? props.isHeader
? props.theme.menuItem.header.height
: props.theme.menuItem.mobile.height
: props.theme.menuItem.height};
max-height: ${(props) =>
isMobile
? props.isHeader
? props.theme.menuItem.header.height
: props.theme.menuItem.mobile.height
: props.theme.menuItem.height};
border: none;
border-bottom: ${(props) =>
isMobile && props.isHeader
? props.theme.menuItem.header.borderBottom
: props.theme.menuItem.borderBottom};
cursor: ${(props) => (isMobile && props.isHeader ? "default" : "pointer")};
margin: 0;
margin-bottom: ${(props) =>
isMobile && props.isHeader
? props.theme.menuItem.header.marginBottom
: props.theme.menuItem.marginBottom};
padding: ${(props) =>
isMobile
? props.theme.menuItem.mobile.padding
: props.theme.menuItem.padding};
box-sizing: border-box;
background: none;
outline: 0 !important;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
@media ${tablet} {
display: ${(props) => (props.textOverflow ? "block" : "flex")};
height: ${(props) =>
props.isHeader
? props.theme.menuItem.header.height
: props.theme.menuItem.mobile.height};
max-height: ${(props) =>
props.isHeader
? props.theme.menuItem.header.height
: props.theme.menuItem.mobile.height};
padding: ${(props) => props.theme.menuItem.mobile.padding};
border: none;
border-bottom: ${(props) =>
props.isHeader
? props.theme.menuItem.header.borderBottom
: props.theme.menuItem.borderBottom};
margin-bottom: ${(props) =>
props.isHeader
? props.theme.menuItem.header.marginBottom
: props.theme.menuItem.marginBottom};
cursor: ${(props) => (props.isHeader ? "default" : "pointer")};
}
.drop-down-item_icon {
path {
fill: ${(props) => !props.isHeader && props.theme.menuItem.svgFill};
}
}
&:hover {
background-color: ${(props) =>
props.noHover || props.isHeader
? props.theme.menuItem.background
: props.theme.menuItem.hover};
}
${(props) =>
props.isSeparator &&
css`
border-bottom: ${props.theme.menuItem.separator.borderBottom};
cursor: default !important;
margin: ${props.theme.menuItem.separator.margin};
height: ${props.theme.menuItem.separator.height};
width: ${props.theme.menuItem.separator.width};
&:hover {
cursor: default !important;
}
`}
.arrow-icon {
margin: 0 0 0 8px;
}
`;
StyledMenuItem.defaultProps = { theme: Base };
const IconWrapper = styled.div`
display: flex;
align-items: center;
width: ${(props) =>
props.isHeader && isMobile
? props.theme.menuItem.iconWrapper.header.width
: props.theme.menuItem.iconWrapper.width};
height: ${(props) =>
props.isHeader && isMobile
? props.theme.menuItem.iconWrapper.header.height
: props.theme.menuItem.iconWrapper.height};
@media ${tablet} {
width: ${(props) =>
props.isHeader
? props.theme.menuItem.iconWrapper.header.width
: props.theme.menuItem.iconWrapper.width};
height: ${(props) =>
props.isHeader
? props.theme.menuItem.iconWrapper.header.height
: props.theme.menuItem.iconWrapper.height};
}
svg {
&:not(:root) {
width: 100%;
height: 100%;
}
}
`;
IconWrapper.defaultProps = { theme: Base };
export { StyledMenuItem, StyledText, IconWrapper };

View File

@ -0,0 +1,3 @@
<svg width="4" height="8" viewBox="0 0 4 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5255 4L0.153211 1.10054C-0.0755111 0.820987 -0.0446084 0.400121 0.222235 0.160507C0.489078 -0.0791068 0.890813 -0.0467326 1.11954 0.232817L3.8468 3.56614C4.05107 3.8158 4.05107 4.1842 3.8468 4.43386L1.11954 7.76718C0.890813 8.04673 0.489078 8.07911 0.222235 7.83949C-0.0446084 7.59988 -0.0755111 7.17901 0.153211 6.89946L2.5255 4Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 460 B

View File

@ -0,0 +1,3 @@
<svg width="4" height="8" viewBox="0 0 4 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.146447 7.35355C-0.0488156 7.15829 -0.0488156 6.84171 0.146447 6.64645L2.79289 4L0.146446 1.35355C-0.0488158 1.15829 -0.0488158 0.841709 0.146446 0.646447C0.341708 0.451184 0.658291 0.451184 0.853553 0.646447L3.85355 3.64645C4.04882 3.84171 4.04882 4.15829 3.85355 4.35355L0.853553 7.35355C0.658291 7.54882 0.341709 7.54882 0.146447 7.35355Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 510 B

View File

@ -0,0 +1,35 @@
# ContextMenu
ContextMenu is used for a call context actions on a page.
> Implemented as part of RowContainer component.
### Usage
```js
import NewContextMenu from "@appserver/components/new-context-menu";
```
```jsx
<NewContextMenu model={defaultModel} />
```
For use within separate component it is necessary to determine active zone and events for calling and transferring options in menu.
In particular case, state is created containing options for particular Row element and passed to component when called.
ContextMenu contain MenuItem component and can take from the props model(all view)
and header(show only tablet or mobile, when view changed).
### Properties
| Props | Type | Required | Values | Default | Description |
| -------------- | :------------: | :------: | :----: | :-----------: | ------------------------ |
| `className` | `string` | - | - | - | Accepts class |
| `id` | `string` | - | - | `contextMenu` | Accepts id |
| `model` | `array` | - | - | `[]` | Items collection |
| `header` | `object` | - | - | `{}` | ContextMenu header |
| `position` | `object` | - | - | `{}` | ContextMenu position |
| `style` | `obj`, `array` | - | - | - | Accepts css style |
| `targetAreaId` | `string` | - | - | - | Id of container apply to |
| `withBackdrop` | `bool` | - | - | `true` | Used to display backdrop |

View File

@ -0,0 +1,489 @@
import React from "react";
import PropTypes from "prop-types";
import DomHelpers from "../utils/domHelpers";
import { classNames } from "../utils/classNames";
import { CSSTransition } from "react-transition-group";
import { VariableSizeList } from "react-window";
import CustomScrollbarsVirtualList from "../scrollbar/custom-scrollbars-virtual-list";
import { isMobile, isMobileOnly } from "react-device-detect";
import {
isMobile as isMobileUtils,
isTablet as isTabletUtils,
} from "../utils/device";
import Portal from "../portal";
import MenuItem from "../menu-item";
import Backdrop from "../backdrop";
import StyledContextMenu from "./styled-new-context-menu";
// eslint-disable-next-line react/display-name, react/prop-types
const Row = React.memo(({ data, index, style }) => {
// eslint-disable-next-line react/prop-types
if (!data.data[index]) return null;
return (
<MenuItem
// eslint-disable-next-line react/prop-types
key={data.data[index].key}
// eslint-disable-next-line react/prop-types
icon={data.data[index].icon}
// eslint-disable-next-line react/prop-types
label={data.data[index].label}
// eslint-disable-next-line react/prop-types
isSeparator={data.data[index].isSeparator}
// eslint-disable-next-line react/prop-types
options={data.data[index].options}
// eslint-disable-next-line react/prop-types
onClick={data.data[index].onClick}
// eslint-disable-next-line react/prop-types
hideMenu={data.hideMenu}
style={style}
/>
);
});
class NewContextMenu extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
reshow: false,
resetMenu: false,
changeView: false,
};
this.onMenuClick = this.onMenuClick.bind(this);
this.onLeafClick = this.onLeafClick.bind(this);
this.onMenuMouseEnter = this.onMenuMouseEnter.bind(this);
this.onEnter = this.onEnter.bind(this);
this.onEntered = this.onEntered.bind(this);
this.onExit = this.onExit.bind(this);
this.onExited = this.onExited.bind(this);
this.menuRef = React.createRef();
}
onMenuClick() {
this.setState({
resetMenu: false,
});
}
onMenuMouseEnter() {
this.setState({
resetMenu: false,
});
}
show(e) {
if (!(e instanceof Event)) {
e.persist();
}
e.stopPropagation();
e.preventDefault();
this.currentEvent = e;
if (this.state.visible) {
this.setState({ reshow: true });
} else {
this.setState({ visible: true }, () => {
if (this.props.onShow) {
this.props.onShow(this.currentEvent);
}
});
}
}
componentDidUpdate(prevProps, prevState) {
if (this.state.visible && prevState.reshow !== this.state.reshow) {
let event = this.currentEvent;
this.setState(
{
visible: false,
reshow: false,
rePosition: false,
changeView: false,
resetMenu: true,
},
() => this.show(event)
);
}
}
hide(e) {
if (!(e instanceof Event)) {
e.persist();
}
this.currentEvent = e;
this.setState({ visible: false, reshow: false, changeView: false }, () => {
if (this.props.onHide) {
this.props.onHide(this.currentEvent);
}
});
}
hideMenu() {
this.setState({ visible: false, reshow: false, changeView: false }, () => {
if (this.props.onHide) {
this.props.onHide(this.currentEvent);
}
});
}
onEnter() {
if (this.props.autoZIndex) {
this.menuRef.current.style.zIndex = String(
this.props.baseZIndex + DomHelpers.generateZIndex()
);
}
this.position(this.currentEvent);
}
onEntered() {
this.bindDocumentListeners();
}
onExit() {
this.currentEvent = null;
this.unbindDocumentListeners();
}
onExited() {
DomHelpers.revertZIndex();
}
position(event) {
if (this.props.position) {
let width = this.menuRef.current.offsetParent
? this.menuRef.current.offsetWidth
: DomHelpers.getHiddenElementOuterWidth(this.menuRef.current);
let viewport = DomHelpers.getViewport();
if (
this.props.position.left + width >
viewport.width - DomHelpers.calculateScrollbarWidth()
) {
this.menuRef.current.style.right =
// -1 * this.props.position.width + width + "px";
0 + "px";
} else {
this.menuRef.current.style.left = this.props.position.left + "px";
}
this.menuRef.current.style.top = this.props.position.top + "px";
return;
}
if (event) {
let left = event.pageX + 1;
let top = event.pageY + 1;
let width = this.menuRef.current.offsetParent
? this.menuRef.current.offsetWidth
: DomHelpers.getHiddenElementOuterWidth(this.menuRef.current);
let height = this.menuRef.current.offsetParent
? this.menuRef.current.offsetHeight
: DomHelpers.getHiddenElementOuterHeight(this.menuRef.current);
let viewport = DomHelpers.getViewport();
if ((isMobile || isTabletUtils()) && height > 483) {
this.setState({ changeView: true });
return;
}
if ((isMobileOnly || isMobileUtils()) && height > 210) {
this.setState({ changeView: true });
return;
}
//flip
if (left + width - document.body.scrollLeft > viewport.width) {
left -= width;
}
//flip
if (top + height - document.body.scrollTop > viewport.height) {
top -= height;
}
//fit
if (left < document.body.scrollLeft) {
left = document.body.scrollLeft;
}
//fit
if (top < document.body.scrollTop) {
top = document.body.scrollTop;
}
this.menuRef.current.style.left = left + "px";
this.menuRef.current.style.top = top + "px";
}
}
onLeafClick(e) {
this.setState({
resetMenu: true,
});
this.hide(e);
e.stopPropagation();
}
isOutsideClicked(e) {
return (
this.menuRef &&
this.menuRef.current &&
!(
this.menuRef.current.isSameNode(e.target) ||
this.menuRef.current.contains(e.target)
)
);
}
bindDocumentListeners() {
this.bindDocumentResizeListener();
this.bindDocumentClickListener();
}
unbindDocumentListeners() {
this.unbindDocumentResizeListener();
this.unbindDocumentClickListener();
}
bindDocumentClickListener() {
if (!this.documentClickListener) {
this.documentClickListener = (e) => {
if (this.isOutsideClicked(e)) {
//TODO: (&& e.button !== 2) restore after global usage
this.hide(e);
this.setState({
resetMenu: true,
});
}
};
document.addEventListener("click", this.documentClickListener);
document.addEventListener("mousedown", this.documentClickListener);
}
}
bindDocumentContextMenuListener() {
if (!this.documentContextMenuListener) {
this.documentContextMenuListener = (e) => {
this.show(e);
};
document.addEventListener(
"contextmenu",
this.documentContextMenuListener
);
}
}
bindDocumentResizeListener() {
if (!this.documentResizeListener) {
this.documentResizeListener = (e) => {
if (this.state.visible) {
this.hide(e);
}
};
window.addEventListener("resize", this.documentResizeListener);
}
}
unbindDocumentClickListener() {
if (this.documentClickListener) {
document.removeEventListener("click", this.documentClickListener);
document.removeEventListener("mousedown", this.documentClickListener);
this.documentClickListener = null;
}
}
unbindDocumentContextMenuListener() {
if (this.documentContextMenuListener) {
document.removeEventListener(
"contextmenu",
this.documentContextMenuListener
);
this.documentContextMenuListener = null;
}
}
unbindDocumentResizeListener() {
if (this.documentResizeListener) {
window.removeEventListener("resize", this.documentResizeListener);
this.documentResizeListener = null;
}
}
componentDidMount() {
if (this.props.global) {
this.bindDocumentContextMenuListener();
}
}
componentWillUnmount() {
this.unbindDocumentListeners();
this.unbindDocumentContextMenuListener();
DomHelpers.revertZIndex();
}
renderContextMenuItems() {
if (!this.props.model) return null;
if (this.state.changeView) {
const rowHeights = this.props.model.map((item, index) => {
if (!item) return 0;
if (item.isSeparator) return 13;
return 36;
});
const getItemSize = (index) => rowHeights[index];
const height = rowHeights.reduce((a, b) => a + b);
const viewport = DomHelpers.getViewport();
const listHeight =
height + 61 > viewport.height - 64 ? viewport.height - 125 : height;
return (
<VariableSizeList
height={listHeight}
width={"auto"}
itemCount={this.props.model.length}
itemSize={getItemSize}
itemData={{
data: this.props.model,
hideMenu: this.hideMenu.bind(this),
}}
outerElementType={CustomScrollbarsVirtualList}
>
{Row}
</VariableSizeList>
);
} else {
const items = this.props.model.map((item, index) => {
if (!item) return;
return (
<MenuItem
key={item.key}
icon={item.icon}
label={item.label}
isSeparator={item.isSeparator}
options={item.options}
onClick={item.onClick}
hideMenu={this.hideMenu.bind(this)}
/>
);
});
return items;
}
}
renderContextMenu() {
const className = classNames("p-contextmenu", this.props.className);
const items = this.renderContextMenuItems();
return (
<>
{this.props.withBackdrop && (
<Backdrop visible={this.state.visible} withBackground={true} />
)}
<StyledContextMenu changeView={this.state.changeView}>
<CSSTransition
nodeRef={this.menuRef}
classNames="p-contextmenu"
in={this.state.visible}
timeout={{ enter: 250, exit: 0 }}
unmountOnExit
onEnter={this.onEnter}
onEntered={this.onEntered}
onExit={this.onExit}
onExited={this.onExited}
>
<div
ref={this.menuRef}
id={this.props.id}
className={className}
style={this.props.style}
onClick={this.onMenuClick}
onMouseEnter={this.onMenuMouseEnter}
>
{this.state.changeView && (
<MenuItem
key={"header"}
isHeader={true}
icon={this.props.header.icon}
label={this.props.header.title}
/>
)}
{items}
</div>
</CSSTransition>
</StyledContextMenu>
</>
);
}
render() {
const element = this.renderContextMenu();
return <Portal element={element} appendTo={this.props.appendTo} />;
}
}
NewContextMenu.propTypes = {
/** Unique identifier of the element */
id: PropTypes.string,
/** An array of objects */
model: PropTypes.array,
/** An object of header with icon and label */
header: PropTypes.object,
/** Position of context menu */
position: PropTypes.object,
/** Inline style of the component */
style: PropTypes.object,
/** Style class of the component */
className: PropTypes.string,
/** Attaches the menu to document instead of a particular item */
global: PropTypes.bool,
/** Tell when context menu was render with backdrop */
withBackdrop: PropTypes.bool,
/** Base zIndex value to use in layering */
autoZIndex: PropTypes.bool,
/** Whether to automatically manage layering */
baseZIndex: PropTypes.number,
/** DOM element instance where the menu should be mounted */
appendTo: PropTypes.any,
/** Callback to invoke when a popup menu is shown */
onShow: PropTypes.func,
/** Callback to invoke when a popup menu is hidden */
onHide: PropTypes.func,
};
NewContextMenu.defaultProps = {
id: null,
model: null,
position: null,
header: null,
style: null,
className: null,
global: false,
autoZIndex: true,
baseZIndex: 0,
appendTo: null,
onShow: null,
onHide: null,
withBackdrop: true,
};
export default NewContextMenu;

View File

@ -0,0 +1,179 @@
import React from "react";
import NewContextMenu from ".";
export default {
title: "Components/NewContextMenu",
component: NewContextMenu,
parameters: {
docs: {
description: {
component: `Is a ContextMenu component.
ContextMenu contain MenuItem component and can take from the props model(all view)
and header(show only tablet or mobile, when view changed).
`,
},
},
},
};
const Template = (args) => {
const cm = React.useRef(null);
const defaultModel = [
{
disabled: false,
icon: "/static/images/access.edit.react.svg",
key: "edit",
label: "Edit",
onClick: () => console.log("item 1 clicked"),
},
{
disabled: false,
icon: "/static/images/eye.react.svg",
key: "preview",
label: "Preview",
onClick: () => console.log("item 2 clicked"),
},
{ isSeparator: true, key: "separator0" },
{
disabled: false,
icon: "/static/images/catalog.shared.react.svg",
key: "sharing-settings",
label: "Sharing settings",
onClick: () => console.log("item 3 clicked"),
},
{
disabled: false,
icon: "/static/images/catalog.shared.react.svg",
key: "sharing-settings1",
label: "Sharing settings",
onClick: () => console.log("item 3 clicked"),
},
{
disabled: false,
icon: "/static/images/catalog.shared.react.svg",
key: "sharing-settings2",
label: "Sharing settings",
onClick: () => console.log("item 3 clicked"),
},
{
disabled: false,
icon: "/static/images/catalog.shared.react.svg",
key: "sharing-settings3",
label: "Sharing settings",
onClick: () => console.log("item 3 clicked"),
},
{
disabled: false,
icon: "/static/images/catalog.shared.react.svg",
key: "sharing-settings4",
label: "Sharing settings",
onClick: () => console.log("item 3 clicked"),
},
{
disabled: false,
icon: "/static/images/catalog.shared.react.svg",
key: "sharing-settings5",
label: "Sharing settings",
onClick: () => console.log("item 3 clicked"),
},
{
disabled: false,
icon: "/static/images/catalog.shared.react.svg",
key: "sharing-settings6",
label: "Sharing settings",
onClick: () => console.log("item 3 clicked"),
},
{
disabled: false,
icon: "/static/images/catalog.shared.react.svg",
key: "sharing-settings7",
label: "Sharing settings",
onClick: () => console.log("item 3 clicked"),
},
{
disabled: false,
icon: "/static/images/catalog.shared.react.svg",
key: "sharing-settings8",
label: "Sharing settings",
onClick: () => console.log("item 3 clicked"),
},
{
disabled: false,
icon: "/static/images/catalog.shared.react.svg",
key: "sharing-settings9",
label: "Sharing settings",
onClick: () => console.log("item 3 clicked"),
},
{
disabled: false,
icon: "/static/images/catalog.shared.react.svg",
key: "sharing-settings10",
label: "Sharing settings",
options: [
{
key: "key1",
icon: "static/images/nav.logo.react.svg",
label: "Item after separator",
onClick: () => console.log("Button 1 clicked"),
},
{
key: "key2",
icon: "static/images/nav.logo.react.svg",
label: "Item after separator",
onClick: () => console.log("Button 2 clicked"),
},
],
onClick: () => console.log("item 3 clicked"),
},
{
disabled: false,
icon: "/static/images/catalog.shared.react.svg",
key: "sharing-settings11",
label: "Sharing settings",
onClick: () => console.log("item 3 clicked"),
},
{
disabled: false,
icon: "/static/images/catalog.shared.react.svg",
key: "sharing-settings12",
label: "Sharing settings",
onClick: () => console.log("item 3 clicked"),
},
{
disabled: false,
icon: "/static/images/catalog.shared.react.svg",
key: "sharing-settings13",
label: "Sharing settings",
onClick: () => console.log("item 3 clicked"),
},
];
const headerOptions = {
icon: "/static/images/catalog.shared.react.svg",
title: "File name",
};
return (
<div>
<NewContextMenu
model={defaultModel}
header={headerOptions}
ref={cm}
></NewContextMenu>
<div
style={{
width: "200px",
height: "200px",
backgroundColor: "red",
display: "inline-block",
}}
onContextMenu={(e) => cm.current.show(e)}
>
{""}
</div>
</div>
);
};
export const Default = Template.bind({});

View File

@ -0,0 +1,11 @@
import React from "react";
import { mount, shallow } from "enzyme";
import NewContextMenu from ".";
describe("<MenuItem />", () => {
it("renders without error", () => {
const wrapper = mount(<NewContextMenu />);
expect(wrapper).toExist();
});
});

View File

@ -0,0 +1,58 @@
import styled, { css } from "styled-components";
import { isMobile, isMobileOnly } from "react-device-detect";
import { tablet, mobile } from "../utils/device";
import Base from "../themes/base";
const styledTabletView = css`
width: ${(props) => props.theme.newContextMenu.devices.tabletWidth};
max-width: ${(props) => props.theme.newContextMenu.devices.tabletWidth};
max-height: ${(props) => props.theme.newContextMenu.devices.maxHeight};
left: ${(props) => props.theme.newContextMenu.devices.left};
right: ${(props) => props.theme.newContextMenu.devices.right};
bottom: ${(props) => props.theme.newContextMenu.devices.bottom};
margin: ${(props) => props.theme.newContextMenu.devices.margin};
`;
const styledMobileView = css`
width: ${(props) => props.theme.newContextMenu.devices.mobileWidth};
max-width: ${(props) => props.theme.newContextMenu.devices.mobileWidth};
max-height: ${(props) => props.theme.newContextMenu.devices.maxHeight};
left: ${(props) => props.theme.newContextMenu.devices.left};
bottom: ${(props) => props.theme.newContextMenu.devices.bottom};
`;
const StyledContextMenu = styled.div`
.p-contextmenu {
position: absolute;
background: ${(props) => props.theme.newContextMenu.background};
border-radius: ${(props) => props.theme.newContextMenu.borderRadius};
-moz-border-radius: ${(props) => props.theme.newContextMenu.borderRadius};
-webkit-border-radius: ${(props) =>
props.theme.newContextMenu.borderRadius};
box-shadow: ${(props) => props.theme.newContextMenu.boxShadow};
-moz-box-shadow: ${(props) => props.theme.newContextMenu.boxShadow};
-webkit-box-shadow: ${(props) => props.theme.newContextMenu.boxShadow};
padding: ${(props) => props.theme.newContextMenu.padding};
@media ${tablet} {
${(props) => props.changeView && !isMobile && styledTabletView}
}
@media ${mobile} {
${(props) => props.changeView && !isMobile && styledMobileView}
}
${(props) =>
props.changeView
? isMobileOnly
? styledMobileView
: styledTabletView
: null}
}
`;
StyledContextMenu.defaultProps = {
theme: Base,
};
export default StyledContextMenu;

View File

@ -3,7 +3,7 @@ import React from "react";
import Checkbox from "../checkbox";
import ContextMenuButton from "../context-menu-button";
import ContextMenu from "../context-menu";
import NewContextMenu from "../new-context-menu";
import {
StyledOptionButton,
StyledContentElement,
@ -72,6 +72,14 @@ class Row extends React.Component {
this.cm.current.show(e);
};
let contextMenuHeader = {};
if (children.props.item) {
contextMenuHeader = {
icon: children.props.item.icon,
title: children.props.item.title,
};
}
const { onRowClick, ...rest } = this.props;
return (
@ -114,7 +122,11 @@ class Row extends React.Component {
) : (
<div className="expandButton"> </div>
)}
<ContextMenu model={contextOptions} ref={this.cm}></ContextMenu>
<NewContextMenu
model={contextOptions}
ref={this.cm}
header={contextMenuHeader}
></NewContextMenu>
</StyledOptionButton>
</StyledRow>
);

View File

@ -2,7 +2,7 @@ import React, { useRef } from "react";
import PropTypes from "prop-types";
import { StyledTableRow } from "./StyledTableContainer";
import TableCell from "./TableCell";
import ContextMenu from "../context-menu";
import NewContextMenu from "../new-context-menu";
import ContextMenuButton from "../context-menu-button";
const TableRow = (props) => {
@ -53,11 +53,11 @@ const TableRow = (props) => {
forwardedRef={row}
className={`${selectionProp?.className} table-container_row-context-menu-wrapper`}
>
<ContextMenu
<NewContextMenu
onHide={onHideContextMenu}
ref={cm}
model={contextOptions}
></ContextMenu>
></NewContextMenu>
{renderContext ? (
<ContextMenuButton
color="#A3A9AE"

View File

@ -1719,6 +1719,69 @@ const Base = {
},
},
},
menuItem: {
iconWrapper: {
width: "16px",
height: "16px",
header: {
width: "24px",
height: "24px",
},
},
separator: {
borderBottom: `1px solid ${grayLightMid} !important`,
margin: "6px 16px 6px 16px !important",
height: "1px !important",
width: "calc(100% - 32px) !important",
},
text: {
header: {
fontSize: "15px",
lineHeight: "16px",
},
mobile: {
fontSize: "13px",
lineHeight: "36px",
},
fontSize: "12px",
lineHeight: "30px",
fontWeight: "600",
margin: "0 0 0 8px",
color: black,
},
hover: grayLight,
background: "none",
svgFill: black,
header: {
height: "55px",
borderBottom: `1px solid ${grayLightMid}`,
marginBottom: "6px",
},
height: "30px",
borderBottom: "none",
marginBottom: "0",
padding: "0 12px",
mobile: {
height: "36px",
padding: "0 16px",
},
},
newContextMenu: {
background: white,
borderRadius: "6px",
boxShadow: "0px 12px 40px rgba(4, 15, 27, 0.12)",
padding: "6px 0px",
devices: {
maxHeight: "calc(100vh - 64px)",
tabletWidth: "375px",
mobileWidth: "100vw",
left: 0,
right: 0,
bottom: 0,
margin: "0 auto",
},
},
};
export default Base;

View File

@ -4,7 +4,7 @@ import PropTypes from "prop-types";
import React from "react";
import { ReactSVG } from "react-svg";
import styled, { css } from "styled-components";
import ContextMenu from "@appserver/components/context-menu";
import NewContextMenu from "@appserver/components/new-context-menu";
import { tablet } from "@appserver/components/utils/device";
import { isDesktop } from "react-device-detect";
@ -356,6 +356,11 @@ class Tile extends React.PureComponent {
const [FilesTileContent, badges] = children;
const quickButtons = contentElement;
const contextMenuHeader = {
icon: children.props.item.icon,
title: children.props.item.title,
};
return (
<StyledTile
ref={this.tile}
@ -418,7 +423,11 @@ class Tile extends React.PureComponent {
) : (
<div className="expandButton" />
)}
<ContextMenu model={contextOptions} ref={this.cm} />
<NewContextMenu
model={contextOptions}
ref={this.cm}
header={contextMenuHeader}
/>
</StyledOptionButton>
</>
) : (
@ -473,7 +482,11 @@ class Tile extends React.PureComponent {
) : (
<div className="expandButton" />
)}
<ContextMenu model={contextOptions} ref={this.cm} />
<NewContextMenu
model={contextOptions}
ref={this.cm}
header={contextMenuHeader}
/>
</StyledOptionButton>
</StyledFileTileBottom>
</>