Web:Components: create new-context-menu
This commit is contained in:
parent
20dcdde09b
commit
1e4a708dd1
34
packages/asc-web-components/new-context-menu/README.md
Normal file
34
packages/asc-web-components/new-context-menu/README.md
Normal file
@ -0,0 +1,34 @@
|
||||
# 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 |
|
||||
| `style` | `obj`, `array` | - | - | - | Accepts css style |
|
||||
| `targetAreaId` | `string` | - | - | - | Id of container apply to |
|
||||
| `withBackdrop` | `bool` | - | - | `true` | Used to display backdrop |
|
432
packages/asc-web-components/new-context-menu/index.js
Normal file
432
packages/asc-web-components/new-context-menu/index.js
Normal file
@ -0,0 +1,432 @@
|
||||
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 StyledContextMenu from "./styled-new-context-menu";
|
||||
import Backdrop from "../backdrop";
|
||||
|
||||
// eslint-disable-next-line react/display-name, react/prop-types
|
||||
const Row = React.memo(({ data, index, style }) => {
|
||||
if (!data[index]) return null;
|
||||
return (
|
||||
<MenuItem
|
||||
// eslint-disable-next-line react/prop-types
|
||||
key={data[index].key}
|
||||
// eslint-disable-next-line react/prop-types
|
||||
icon={data[index].icon}
|
||||
// eslint-disable-next-line react/prop-types
|
||||
label={data[index].label}
|
||||
// eslint-disable-next-line react/prop-types
|
||||
isSeparator={data[index].isSeparator}
|
||||
// eslint-disable-next-line react/prop-types
|
||||
onClick={data[index].onClick}
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 (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={this.props.model}
|
||||
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}
|
||||
onClick={item.onClick}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
renderContextMenu() {
|
||||
const className = classNames("p-contextmenu", this.props.className);
|
||||
|
||||
const items = this.renderContextMenuItems();
|
||||
|
||||
return (
|
||||
<StyledContextMenu changeView={this.state.changeView}>
|
||||
<Backdrop visible={this.state.visible} />
|
||||
<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}
|
||||
>
|
||||
{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 menuitems */
|
||||
model: PropTypes.array,
|
||||
/** 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,
|
||||
/** 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,
|
||||
style: null,
|
||||
className: null,
|
||||
global: false,
|
||||
autoZIndex: true,
|
||||
baseZIndex: 0,
|
||||
appendTo: null,
|
||||
onShow: null,
|
||||
onHide: null,
|
||||
};
|
||||
|
||||
export default NewContextMenu;
|
@ -0,0 +1,48 @@
|
||||
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 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"),
|
||||
},
|
||||
];
|
||||
|
||||
const Template = () => {
|
||||
return <NewContextMenu model={defaultModel} />;
|
||||
};
|
||||
export const Default = Template.bind({});
|
@ -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();
|
||||
});
|
||||
});
|
@ -0,0 +1,57 @@
|
||||
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: 375px;
|
||||
max-width: 375px;
|
||||
max-height: calc(100vh - 64px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
const styledMobileView = css`
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
max-height: calc(100vh - 64px);
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
`;
|
||||
|
||||
const StyledContextMenu = styled.div`
|
||||
.p-contextmenu {
|
||||
position: absolute;
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
-moz-border-radius: 6px;
|
||||
-webkit-border-radius: 6px;
|
||||
box-shadow: 0px 12px 40px rgba(4, 15, 27, 0.12);
|
||||
-moz-box-shadow: 0px 12px 40px rgba(4, 15, 27, 0.12);
|
||||
-webkit-box-shadow: 0px 12px 40px rgba(4, 15, 27, 0.12);
|
||||
padding: 6px 0px;
|
||||
|
||||
@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;
|
@ -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,
|
||||
@ -113,7 +113,7 @@ class Row extends React.Component {
|
||||
) : (
|
||||
<div className="expandButton"> </div>
|
||||
)}
|
||||
<ContextMenu model={contextOptions} ref={this.cm}></ContextMenu>
|
||||
<NewContextMenu model={contextOptions} ref={this.cm}></NewContextMenu>
|
||||
</StyledOptionButton>
|
||||
</StyledRow>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user