Merge pull request #10 from mattermost/add-menu-wrapper
Add a MenuWrapper component to easily handling the menu show/hide logic and state
This commit is contained in:
commit
48fdd81867
5 changed files with 161 additions and 136 deletions
|
@ -6,6 +6,8 @@ import { BoardTree } from "../boardTree"
|
|||
import { Card } from "../card"
|
||||
import { CardFilter } from "../cardFilter"
|
||||
import ViewMenu from "../components/viewMenu"
|
||||
import MenuWrapper from "../widgets/menuWrapper"
|
||||
import Menu from "../widgets/menu"
|
||||
import { Constants } from "../constants"
|
||||
import { randomEmojiList } from "../emojiList"
|
||||
import { Menu as OldMenu } from "../menu"
|
||||
|
@ -29,7 +31,6 @@ type Props = {
|
|||
type State = {
|
||||
isHoverOnCover: boolean
|
||||
isSearching: boolean
|
||||
viewMenu: boolean
|
||||
}
|
||||
|
||||
class BoardComponent extends React.Component<Props, State> {
|
||||
|
@ -39,7 +40,7 @@ class BoardComponent extends React.Component<Props, State> {
|
|||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = { isHoverOnCover: false, isSearching: !!this.props.boardTree?.getSearchText(), viewMenu: false }
|
||||
this.state = { isHoverOnCover: false, isSearching: !!this.props.boardTree?.getSearchText()}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevPros: Props, prevState: State) {
|
||||
|
@ -85,7 +86,13 @@ class BoardComponent extends React.Component<Props, State> {
|
|||
|
||||
<div className="octo-icontitle">
|
||||
{board.icon ?
|
||||
<div className="octo-button octo-icon" onClick={(e) => { this.iconClicked(e) }}>{board.icon}</div>
|
||||
<MenuWrapper>
|
||||
<div className="octo-button octo-icon">{board.icon}</div>
|
||||
<Menu>
|
||||
<Menu.Text id='random' name='Random' onClick={() => mutator.changeIcon(board, undefined, "remove icon")}/>
|
||||
<Menu.Text id='remove' name='Remove Icon' onClick={() => mutator.changeIcon(board, BlockIcons.shared.randomIcon())}/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
: undefined}
|
||||
<Editable className="title" text={board.title} placeholderText="Untitled Board" onChanged={(text) => { mutator.changeTitle(board, text) }} />
|
||||
</div>
|
||||
|
@ -93,21 +100,20 @@ class BoardComponent extends React.Component<Props, State> {
|
|||
<div className="octo-board">
|
||||
<div className="octo-controls">
|
||||
<Editable style={{ color: "#000000", fontWeight: 600 }} text={activeView.title} placeholderText="Untitled View" onChanged={(text) => { mutator.changeTitle(activeView, text) }} />
|
||||
<div
|
||||
className="octo-button"
|
||||
style={{ color: "#000000", fontWeight: 600 }}
|
||||
onClick={() => this.setState({ viewMenu: true })}
|
||||
>
|
||||
{this.state.viewMenu &&
|
||||
<ViewMenu
|
||||
board={board}
|
||||
onClose={() => this.setState({ viewMenu: false })}
|
||||
mutator={mutator}
|
||||
boardTree={boardTree}
|
||||
showView={showView}
|
||||
/>}
|
||||
<div className="imageDropdown"></div>
|
||||
</div>
|
||||
<MenuWrapper>
|
||||
<div
|
||||
className="octo-button"
|
||||
style={{ color: "#000000", fontWeight: 600 }}
|
||||
>
|
||||
<div className="imageDropdown"></div>
|
||||
</div>
|
||||
<ViewMenu
|
||||
board={board}
|
||||
mutator={mutator}
|
||||
boardTree={boardTree}
|
||||
showView={showView}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
<div className="octo-spacer"></div>
|
||||
<div className="octo-button" onClick={(e) => { this.propertiesClicked(e) }}>Properties</div>
|
||||
<div className="octo-button" id="groupByButton" onClick={(e) => { this.groupByClicked(e) }}>
|
||||
|
@ -163,7 +169,16 @@ class BoardComponent extends React.Component<Props, State> {
|
|||
onChanged={(text) => { this.propertyNameChanged(group.option, text) }} />
|
||||
<Button text={`${group.cards.length}`} />
|
||||
<div className="octo-spacer" />
|
||||
<Button onClick={(e) => { this.valueOptionClicked(e, group.option) }}><div className="imageOptions" /></Button>
|
||||
<MenuWrapper>
|
||||
<Button><div className="imageOptions" /></Button>
|
||||
<Menu>
|
||||
<Menu.Text id='delete' name='Delete' onClick={() => mutator.deletePropertyOption(boardTree, boardTree.groupByProperty, group.option)}/>
|
||||
<Menu.Separator/>
|
||||
{Constants.menuColors.map((color) =>
|
||||
<Menu.Color key={color.id} id={color.id} name={color.name} onClick={() => mutator.changePropertyOptionColor(boardTree.board, group.option, color.id)} />
|
||||
)}
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
<Button onClick={() => { this.addCard(group.option.value) }}><div className="imageAdd" /></Button>
|
||||
</div>
|
||||
)}
|
||||
|
@ -217,28 +232,6 @@ class BoardComponent extends React.Component<Props, State> {
|
|||
)
|
||||
}
|
||||
|
||||
private iconClicked(e: React.MouseEvent) {
|
||||
const { mutator, boardTree } = this.props
|
||||
const { board } = boardTree
|
||||
|
||||
OldMenu.shared.options = [
|
||||
{ id: "random", name: "Random" },
|
||||
{ id: "remove", name: "Remove Icon" },
|
||||
]
|
||||
OldMenu.shared.onMenuClicked = (optionId: string, type?: string) => {
|
||||
switch (optionId) {
|
||||
case "remove":
|
||||
mutator.changeIcon(board, undefined, "remove icon")
|
||||
break
|
||||
case "random":
|
||||
const newIcon = BlockIcons.shared.randomIcon()
|
||||
mutator.changeIcon(board, newIcon)
|
||||
break
|
||||
}
|
||||
}
|
||||
OldMenu.shared.showAtElement(e.target as HTMLElement)
|
||||
}
|
||||
|
||||
async showCard(card?: Card) {
|
||||
console.log(`showCard: ${card?.title}`)
|
||||
|
||||
|
@ -264,31 +257,6 @@ class BoardComponent extends React.Component<Props, State> {
|
|||
await mutator.changePropertyOptionValue(boardTree, boardTree.groupByProperty, option, text)
|
||||
}
|
||||
|
||||
async valueOptionClicked(e: React.MouseEvent<HTMLElement>, option: IPropertyOption) {
|
||||
const { mutator, boardTree } = this.props
|
||||
|
||||
OldMenu.shared.options = [
|
||||
{ id: "delete", name: "Delete" },
|
||||
{ id: "", name: "", type: "separator" },
|
||||
...Constants.menuColors
|
||||
]
|
||||
OldMenu.shared.onMenuClicked = async (optionId: string, type?: string) => {
|
||||
switch (optionId) {
|
||||
case "delete":
|
||||
console.log(`Delete property value: ${option.value}`)
|
||||
await mutator.deletePropertyOption(boardTree, boardTree.groupByProperty, option)
|
||||
break
|
||||
default:
|
||||
if (type === "color") {
|
||||
// id is the color
|
||||
await mutator.changePropertyOptionColor(boardTree.board, option, optionId)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
OldMenu.shared.showAtElement(e.target as HTMLElement)
|
||||
}
|
||||
|
||||
private filterClicked(e: React.MouseEvent) {
|
||||
this.props.showFilter(e.target as HTMLElement)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import { IPropertyTemplate } from "../board"
|
|||
import { BoardTree } from "../boardTree"
|
||||
import { Card } from "../card"
|
||||
import ViewMenu from "../components/viewMenu"
|
||||
import MenuWrapper from "../widgets/menuWrapper"
|
||||
import Menu from "../widgets/menu"
|
||||
import { CsvExporter } from "../csvExporter"
|
||||
import { Menu as OldMenu } from "../menu"
|
||||
import { Mutator } from "../mutator"
|
||||
|
@ -82,7 +84,13 @@ class TableComponent extends React.Component<Props, State> {
|
|||
|
||||
<div className="octo-icontitle">
|
||||
{board.icon ?
|
||||
<div className="octo-button octo-icon" onClick={(e) => { this.iconClicked(e) }}>{board.icon}</div>
|
||||
<MenuWrapper>
|
||||
<div className="octo-button octo-icon">{board.icon}</div>
|
||||
<Menu>
|
||||
<Menu.Text id='random' name='Random' onClick={() => mutator.changeIcon(board, undefined, "remove icon")}/>
|
||||
<Menu.Text id='remove' name='Remove Icon' onClick={() => mutator.changeIcon(board, BlockIcons.shared.randomIcon())}/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
: undefined}
|
||||
<Editable className="title" text={board.title} placeholderText="Untitled Board" onChanged={(text) => { mutator.changeTitle(board, text) }} />
|
||||
</div>
|
||||
|
@ -90,21 +98,20 @@ class TableComponent extends React.Component<Props, State> {
|
|||
<div className="octo-table">
|
||||
<div className="octo-controls">
|
||||
<Editable style={{ color: "#000000", fontWeight: 600 }} text={activeView.title} placeholderText="Untitled View" onChanged={(text) => { mutator.changeTitle(activeView, text) }} />
|
||||
<div
|
||||
className="octo-button"
|
||||
style={{ color: "#000000", fontWeight: 600 }}
|
||||
onClick={() => this.setState({ viewMenu: true })}
|
||||
>
|
||||
{this.state.viewMenu &&
|
||||
<ViewMenu
|
||||
board={board}
|
||||
onClose={() => this.setState({ viewMenu: false })}
|
||||
mutator={mutator}
|
||||
boardTree={boardTree}
|
||||
showView={showView}
|
||||
/>}
|
||||
<div className="imageDropdown"></div>
|
||||
</div>
|
||||
<MenuWrapper>
|
||||
<div
|
||||
className="octo-button"
|
||||
style={{ color: "#000000", fontWeight: 600 }}
|
||||
>
|
||||
<div className="imageDropdown"></div>
|
||||
</div>
|
||||
<ViewMenu
|
||||
board={board}
|
||||
mutator={mutator}
|
||||
boardTree={boardTree}
|
||||
showView={showView}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
<div className="octo-spacer"></div>
|
||||
<div className="octo-button" onClick={(e) => { this.propertiesClicked(e) }}>Properties</div>
|
||||
<div className={hasFilter ? "octo-button active" : "octo-button"} onClick={(e) => { this.filterClicked(e) }}>Filter</div>
|
||||
|
@ -213,28 +220,6 @@ class TableComponent extends React.Component<Props, State> {
|
|||
)
|
||||
}
|
||||
|
||||
private iconClicked(e: React.MouseEvent) {
|
||||
const { mutator, boardTree } = this.props
|
||||
const { board } = boardTree
|
||||
|
||||
OldMenu.shared.options = [
|
||||
{ id: "random", name: "Random" },
|
||||
{ id: "remove", name: "Remove Icon" },
|
||||
]
|
||||
OldMenu.shared.onMenuClicked = (optionId: string, type?: string) => {
|
||||
switch (optionId) {
|
||||
case "remove":
|
||||
mutator.changeIcon(board, undefined, "remove icon")
|
||||
break
|
||||
case "random":
|
||||
const newIcon = BlockIcons.shared.randomIcon()
|
||||
mutator.changeIcon(board, newIcon)
|
||||
break
|
||||
}
|
||||
}
|
||||
OldMenu.shared.showAtElement(e.target as HTMLElement)
|
||||
}
|
||||
|
||||
private async propertiesClicked(e: React.MouseEvent) {
|
||||
const { mutator, boardTree } = this.props
|
||||
const { activeView } = boardTree
|
||||
|
|
|
@ -11,7 +11,6 @@ type Props = {
|
|||
boardTree?: BoardTree
|
||||
board: Board,
|
||||
showView: (id: string) => void
|
||||
onClose: () => void,
|
||||
}
|
||||
|
||||
export default class ViewMenu extends React.Component<Props> {
|
||||
|
@ -68,9 +67,9 @@ export default class ViewMenu extends React.Component<Props> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { onClose, boardTree } = this.props
|
||||
const { boardTree } = this.props
|
||||
return (
|
||||
<Menu onClose={onClose}>
|
||||
<Menu>
|
||||
{boardTree.views.map((view) => (<Menu.Text key={view.id} id={view.id} name={view.title} onClick={this.handleViewClick} />))}
|
||||
<Menu.Separator />
|
||||
{boardTree.views.length > 1 && <Menu.Text id="__deleteView" name="Delete View" onClick={this.handleDeleteView} />}
|
||||
|
|
|
@ -40,7 +40,7 @@ class SubMenuOption extends React.Component<SubMenuOptionProps, SubMenuState> {
|
|||
<div className='name menu-name'>{this.props.name}</div>
|
||||
<div className="imageSubmenuTriangle" style={{float: 'right'}}></div>
|
||||
{this.state.isOpen &&
|
||||
<Menu onClose={this.close}>
|
||||
<Menu>
|
||||
{this.props.children}
|
||||
</Menu>
|
||||
}
|
||||
|
@ -54,13 +54,17 @@ type ColorOptionProps = MenuOptionProps & {
|
|||
}
|
||||
|
||||
class ColorOption extends React.Component<ColorOptionProps> {
|
||||
handleOnClick = () => {
|
||||
this.props.onClick(this.props.id)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {name, icon} = this.props;
|
||||
const {id, name, icon} = this.props;
|
||||
return (
|
||||
<div className='MenuOption ColorOption menu-option'>
|
||||
<div className='MenuOption ColorOption menu-option' onClick={this.handleOnClick}>
|
||||
<div className='name'>{name}</div>
|
||||
{icon && <div className={'icon ' + icon}></div>}
|
||||
<div className='menu-colorbox'></div>
|
||||
<div className={`menu-colorbox ${id}`}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -113,7 +117,6 @@ class TextOption extends React.Component<TextOptionProps> {
|
|||
|
||||
type MenuProps = {
|
||||
children: React.ReactNode
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default class Menu extends React.Component<MenuProps> {
|
||||
|
@ -123,30 +126,6 @@ export default class Menu extends React.Component<MenuProps> {
|
|||
static Separator = SeparatorOption
|
||||
static Text = TextOption
|
||||
|
||||
onBodyClick = (e: MouseEvent) => {
|
||||
this.props.onClose()
|
||||
}
|
||||
|
||||
onBodyKeyDown = (e: KeyboardEvent) => {
|
||||
// Ignore keydown events on other elements
|
||||
if (e.target !== document.body) { return }
|
||||
if (e.keyCode === 27) {
|
||||
// ESC
|
||||
this.props.onClose()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener("click", this.onBodyClick)
|
||||
document.addEventListener("keydown", this.onBodyKeyDown)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("click", this.onBodyClick)
|
||||
document.removeEventListener("keydown", this.onBodyKeyDown)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="Menu menu noselect">
|
||||
|
|
94
src/client/widgets/menuWrapper.tsx
Normal file
94
src/client/widgets/menuWrapper.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
onToggle?: (open: boolean) => void;
|
||||
isDisabled?: boolean;
|
||||
stopPropagationOnToggle?: boolean;
|
||||
}
|
||||
|
||||
type State = {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
export default class MenuWrapper extends React.PureComponent<Props, State> {
|
||||
private node: React.RefObject<HTMLDivElement>;
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
if (!Array.isArray(props.children) || props.children.length !== 2) {
|
||||
throw new Error('MenuWrapper needs exactly 2 children');
|
||||
}
|
||||
this.state = {
|
||||
open: false,
|
||||
};
|
||||
this.node = React.createRef();
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
document.addEventListener('click', this.closeOnBlur, true);
|
||||
document.addEventListener('keyup', this.keyboardClose, true);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
document.removeEventListener('click', this.closeOnBlur, true);
|
||||
document.removeEventListener('keyup', this.keyboardClose, true);
|
||||
}
|
||||
|
||||
private keyboardClose = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
this.close();
|
||||
}
|
||||
|
||||
if (e.key === 'Tab') {
|
||||
this.closeOnBlur(e);
|
||||
}
|
||||
}
|
||||
|
||||
private closeOnBlur = (e: Event) => {
|
||||
if (this.node && this.node.current && e.target && this.node.current.contains(e.target as Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.close();
|
||||
}
|
||||
|
||||
public close = () => {
|
||||
if (this.state.open) {
|
||||
this.setState({open: false});
|
||||
}
|
||||
}
|
||||
|
||||
toggle = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
/**
|
||||
* This is only here so that we can toggle the menus in the sidebar, because the default behavior of the mobile
|
||||
* version (ie the one that uses a modal) needs propagation to close the modal after selecting something
|
||||
* We need to refactor this so that the modal is explicitly closed on toggle, but for now I am aiming to preserve the existing logic
|
||||
* so as to not break other things
|
||||
**/
|
||||
if (this.props.stopPropagationOnToggle) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
const newState = !this.state.open;
|
||||
this.setState({open: newState});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {children} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'MenuWrapper'}
|
||||
onClick={this.toggle}
|
||||
ref={this.node}
|
||||
>
|
||||
{children ? Object.values(children)[0] : null}
|
||||
{children && this.state.open ? Object.values(children)[1] : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue