From 962c035eab0b6812669d97b15bdda02eb36f5f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 15 Oct 2020 01:18:23 +0200 Subject: [PATCH 1/2] Add MenuWrapper component --- src/client/components/boardComponent.tsx | 33 ++++----- src/client/components/tableComponent.tsx | 30 ++++---- src/client/components/viewMenu.tsx | 5 +- src/client/widgets/menu.tsx | 27 +------ src/client/widgets/menuWrapper.tsx | 94 ++++++++++++++++++++++++ 5 files changed, 128 insertions(+), 61 deletions(-) create mode 100644 src/client/widgets/menuWrapper.tsx diff --git a/src/client/components/boardComponent.tsx b/src/client/components/boardComponent.tsx index 510c37d54..3ba79613f 100644 --- a/src/client/components/boardComponent.tsx +++ b/src/client/components/boardComponent.tsx @@ -6,6 +6,7 @@ import { IPropertyOption } from "../board" import { BoardTree } from "../boardTree" import { CardFilter } from "../cardFilter" import ViewMenu from "../components/viewMenu" +import MenuWrapper from "../widgets/menuWrapper" import { Constants } from "../constants" import { Menu as OldMenu } from "../menu" import { Mutator } from "../mutator" @@ -29,7 +30,6 @@ type Props = { type State = { isHoverOnCover: boolean isSearching: boolean - viewMenu: boolean } class BoardComponent extends React.Component { @@ -39,7 +39,7 @@ class BoardComponent extends React.Component { 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) { @@ -93,21 +93,20 @@ class BoardComponent extends React.Component {
{ mutator.changeTitle(activeView, text) }} /> -
this.setState({ viewMenu: true })} - > - {this.state.viewMenu && - this.setState({ viewMenu: false })} - mutator={mutator} - boardTree={boardTree} - showView={showView} - />} -
-
+ +
+
+
+ +
{ this.propertiesClicked(e) }}>Properties
{ this.groupByClicked(e) }}> diff --git a/src/client/components/tableComponent.tsx b/src/client/components/tableComponent.tsx index a55508ae5..a2d00991a 100644 --- a/src/client/components/tableComponent.tsx +++ b/src/client/components/tableComponent.tsx @@ -6,6 +6,7 @@ import { IPropertyTemplate } from "../board" import { BoardTree } from "../boardTree" import { CsvExporter } from "../csvExporter" import ViewMenu from "../components/viewMenu" +import MenuWrapper from "../widgets/menuWrapper" import { Menu as OldMenu } from "../menu" import { Mutator } from "../mutator" import { IBlock } from "../octoTypes" @@ -90,21 +91,20 @@ class TableComponent extends React.Component {
{ mutator.changeTitle(activeView, text) }} /> -
this.setState({ viewMenu: true })} - > - {this.state.viewMenu && - this.setState({ viewMenu: false })} - mutator={mutator} - boardTree={boardTree} - showView={showView} - />} -
-
+ +
+
+
+ +
{ this.propertiesClicked(e) }}>Properties
{ this.filterClicked(e) }}>Filter
diff --git a/src/client/components/viewMenu.tsx b/src/client/components/viewMenu.tsx index e5b48c34f..08c1af2ee 100644 --- a/src/client/components/viewMenu.tsx +++ b/src/client/components/viewMenu.tsx @@ -11,7 +11,6 @@ type Props = { boardTree?: BoardTree board: Board, showView: (id: string) => void - onClose: () => void, } export default class ViewMenu extends React.Component { @@ -68,9 +67,9 @@ export default class ViewMenu extends React.Component { } render() { - const { onClose, boardTree } = this.props + const { boardTree } = this.props return ( - + {boardTree.views.map((view) => ())} {boardTree.views.length > 1 && } diff --git a/src/client/widgets/menu.tsx b/src/client/widgets/menu.tsx index 78de42a8c..8c12c12f0 100644 --- a/src/client/widgets/menu.tsx +++ b/src/client/widgets/menu.tsx @@ -40,7 +40,7 @@ class SubMenuOption extends React.Component {
{this.props.name}
{this.state.isOpen && - + {this.props.children} } @@ -113,7 +113,6 @@ class TextOption extends React.Component { type MenuProps = { children: React.ReactNode - onClose: () => void } export default class Menu extends React.Component { @@ -123,30 +122,6 @@ export default class Menu extends React.Component { 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 (
diff --git a/src/client/widgets/menuWrapper.tsx b/src/client/widgets/menuWrapper.tsx new file mode 100644 index 000000000..cbe9167c9 --- /dev/null +++ b/src/client/widgets/menuWrapper.tsx @@ -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 { + private node: React.RefObject; + + 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) => { + /** + * 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 ( +
+ {children && !this.state.open ? Object.values(children)[0] : null} + {children && this.state.open ? Object.values(children)[1] : null} +
+ ); + } +} From fb9c237ba056da89e45ac4210e6aec1fc09c66a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 15 Oct 2020 01:49:31 +0200 Subject: [PATCH 2/2] Migrate a couple of menus using MenuWrapper --- src/client/components/boardComponent.tsx | 95 ++++++++---------------- src/client/components/tableComponent.tsx | 59 ++++++--------- src/client/widgets/menu.tsx | 10 ++- src/client/widgets/menuWrapper.tsx | 2 +- 4 files changed, 62 insertions(+), 104 deletions(-) diff --git a/src/client/components/boardComponent.tsx b/src/client/components/boardComponent.tsx index 3ba79613f..1240527a0 100644 --- a/src/client/components/boardComponent.tsx +++ b/src/client/components/boardComponent.tsx @@ -7,6 +7,7 @@ import { BoardTree } from "../boardTree" import { CardFilter } from "../cardFilter" import ViewMenu from "../components/viewMenu" import MenuWrapper from "../widgets/menuWrapper" +import Menu from "../widgets/menu" import { Constants } from "../constants" import { Menu as OldMenu } from "../menu" import { Mutator } from "../mutator" @@ -85,7 +86,13 @@ class BoardComponent extends React.Component {
{board.icon ? -
{ this.iconClicked(e) }}>{board.icon}
+ +
{board.icon}
+ + mutator.changeIcon(board, undefined, "remove icon")}/> + mutator.changeIcon(board, BlockIcons.shared.randomIcon())}/> + +
: undefined} { mutator.changeTitle(board, text) }} />
@@ -93,20 +100,20 @@ class BoardComponent extends React.Component {
{ mutator.changeTitle(activeView, text) }} /> - -
-
-
- -
+ +
+
+
+ +
{ this.propertiesClicked(e) }}>Properties
{ this.groupByClicked(e) }}> @@ -162,7 +169,16 @@ class BoardComponent extends React.Component { onChanged={(text) => { this.propertyNameChanged(group.option, text) }} /> + + + + mutator.deletePropertyOption(boardTree, boardTree.groupByProperty, group.option)}/> + + {Constants.menuColors.map((color) => + mutator.changePropertyOptionColor(boardTree.board, group.option, color.id)} /> + )} + +
)} @@ -216,28 +232,6 @@ class BoardComponent extends React.Component { ) } - 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?: IBlock) { console.log(`showCard: ${card?.title}`) @@ -262,31 +256,6 @@ class BoardComponent extends React.Component { await mutator.changePropertyOptionValue(boardTree, boardTree.groupByProperty, option, text) } - async valueOptionClicked(e: React.MouseEvent, 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) } diff --git a/src/client/components/tableComponent.tsx b/src/client/components/tableComponent.tsx index a2d00991a..62de747c4 100644 --- a/src/client/components/tableComponent.tsx +++ b/src/client/components/tableComponent.tsx @@ -7,6 +7,7 @@ import { BoardTree } from "../boardTree" import { CsvExporter } from "../csvExporter" import ViewMenu from "../components/viewMenu" import MenuWrapper from "../widgets/menuWrapper" +import Menu from "../widgets/menu" import { Menu as OldMenu } from "../menu" import { Mutator } from "../mutator" import { IBlock } from "../octoTypes" @@ -83,7 +84,13 @@ class TableComponent extends React.Component {
{board.icon ? -
{ this.iconClicked(e) }}>{board.icon}
+ +
{board.icon}
+ + mutator.changeIcon(board, undefined, "remove icon")}/> + mutator.changeIcon(board, BlockIcons.shared.randomIcon())}/> + +
: undefined} { mutator.changeTitle(board, text) }} />
@@ -91,20 +98,20 @@ class TableComponent extends React.Component {
{ mutator.changeTitle(activeView, text) }} /> - -
-
-
- -
+ +
+
+
+ +
{ this.propertiesClicked(e) }}>Properties
{ this.filterClicked(e) }}>Filter
@@ -213,28 +220,6 @@ class TableComponent extends React.Component { ) } - 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 diff --git a/src/client/widgets/menu.tsx b/src/client/widgets/menu.tsx index 8c12c12f0..7588ee4f0 100644 --- a/src/client/widgets/menu.tsx +++ b/src/client/widgets/menu.tsx @@ -54,13 +54,17 @@ type ColorOptionProps = MenuOptionProps & { } class ColorOption extends React.Component { + handleOnClick = () => { + this.props.onClick(this.props.id) + } + render() { - const {name, icon} = this.props; + const {id, name, icon} = this.props; return ( -
+
{name}
{icon &&
} -
+
) } diff --git a/src/client/widgets/menuWrapper.tsx b/src/client/widgets/menuWrapper.tsx index cbe9167c9..42bee48b0 100644 --- a/src/client/widgets/menuWrapper.tsx +++ b/src/client/widgets/menuWrapper.tsx @@ -86,7 +86,7 @@ export default class MenuWrapper extends React.PureComponent { onClick={this.toggle} ref={this.node} > - {children && !this.state.open ? Object.values(children)[0] : null} + {children ? Object.values(children)[0] : null} {children && this.state.open ? Object.values(children)[1] : null}
);