refactor: change tag context menu according to UX design (#17)

* refactor: change tag context menu according to UX design

* Update alignPortal.scss
This commit is contained in:
kunzheng 2020-02-09 23:23:20 -08:00 коммит произвёл GitHub
Родитель 42dc96de4e
Коммит 2df1905cf2
6 изменённых файлов: 139 добавлений и 112 удалений

Просмотреть файл

@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
.align-portal {
position: fixed;
z-index: 1000;
}

Просмотреть файл

@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import React from "react";
import ReactDOM from "react-dom";
import Align, { AlignProps } from "rc-align";
import "./alignPortal.scss";
export class AlignPortal extends React.Component<AlignProps> {
private portalElement: Element;
constructor(props: AlignProps) {
super(props);
this.portalElement = document.createElement("div");
this.portalElement.classList.add("align-portal");
}
public componentDidMount() {
document.body.appendChild(this.portalElement);
}
public componentWillUnmount() {
document.body.removeChild(this.portalElement);
}
public render() {
const { children } = this.props;
return (
children &&
ReactDOM.createPortal(
<Align {...this.props}>
{children}
</Align>
, this.portalElement)
);
}
}

Просмотреть файл

@ -2,24 +2,25 @@
// Licensed under the MIT license.
import React from "react";
import { AlignPortal } from "../alignPortal/alignPortal";
import { ITag, FieldType, FieldFormat } from "../../../../models/applicationState";
import "./tagTypeFormat.scss";
import "./tagContextMenu.scss";
import "./tagInputItem.scss";
/**
* Properties for Tag Type Format component
* Properties for TagContextMenu
* @member tag - ITag
*/
export interface ITagTypeFormatProps {
export interface ITagContextMenuProps {
tag: ITag;
onChange?: (oldTag: ITag, newTag: ITag) => void;
}
/**
* State for Tag Type Format
* State for TagContextMenu
* @member tag - ITag
*/
export interface ITagFormatState {
export interface ITagContextMenuState {
tag: ITag;
showFormat?: boolean;
showType?: boolean;
@ -28,7 +29,7 @@ export interface ITagFormatState {
/**
* Generic modal that displays a message
*/
export default class TagTypeFormat extends React.Component<ITagTypeFormatProps, ITagFormatState> {
export default class TagContextMenu extends React.Component<ITagContextMenuProps, ITagContextMenuState> {
private static filterFormat(type: FieldType): FieldFormat[] {
switch (type) {
@ -63,61 +64,85 @@ export default class TagTypeFormat extends React.Component<ITagTypeFormatProps,
}
}
public state: ITagFormatState = {
public state: ITagContextMenuState = {
tag: this.props.tag,
showFormat: false,
showType: false,
};
private typeRef = React.createRef<HTMLDivElement>();
private formatRef = React.createRef<HTMLDivElement>();
public render() {
const tag = this.state.tag;
const types = Object.keys(FieldType);
const formats = TagTypeFormat.filterFormat(tag.type);
const formats = TagContextMenu.filterFormat(tag.type);
const dropdownIconClass = [
"ms-Icon", "ms-Icon--ChevronDown", "field-background-color", "icon-color", "pr-1",
].join(" ");
const align = {
// Align top right of source node (dropdown) with top left of target node (tag name row)
points: ["tr", "br"],
// Offset source node by 0px in x and 3px in y
offset: [0, 3],
// Auto adjust position when source node is overflowed
overflow: {adjustX: true, adjustY: true},
};
return (
<div className = "field-background field-background-color">
<div className = "tag-field justify-content-start">
<div className = "row-4 tag-field-item">
<div onClick={this.handleTypeShow} className = "field-background-container">
<div
ref={this.typeRef}
className="field-background-container"
onClick={this.handleTypeShow}>
<span className="type-selected">{tag.type}</span>
<span className={dropdownIconClass}></span>
</div>
<div className = {this.showHideType()}>
<ol className = "format-items-list">
{
types.filter((type) => {
return FieldType[type] !== tag.type;
}).map((type) => {
return(
this.getTypeListItem(this, type)
);
})
}
</ol>
</div>
<AlignPortal align={align} target={() => this.typeRef.current} monitorWindowResize={true}>
{
this.state.showType &&
<div className={["tag-input-portal", "format-list", "format-items-list"].join(" ")}>
{
types.filter((type) => {
return FieldType[type] !== tag.type;
}).map((type) => {
return (
this.getTypeListItem(this, type)
);
})
}
</div>
}
</AlignPortal>
</div>
<div className = "horizontal-line"></div>
<div className = "row-4 tag-field-item">
<div onClick={this.handleFormatShow} className = "field-background-container">
<div
ref={this.formatRef}
className = "field-background-container"
onClick={this.handleFormatShow}>
<span>{tag.format}</span>
<span className={dropdownIconClass}></span>
</div>
<div className = {this.showHideFormat()}>
<ol className = "format-items-list">
{
formats.filter((format) => {
return format !== tag.format;
}).map((format) => {
return (
this.getFormatListItem(this, format)
);
})
}
</ol>
</div>
<AlignPortal align={align} target={() => this.formatRef.current}>
{
this.state.showFormat &&
<div className = {["tag-input-portal", "format-list", "format-items-list"].join(" ")}>
{
formats.filter((format) => {
return format !== tag.format;
}).map((format) => {
return (
this.getFormatListItem(this, format)
);
})
}
</div>
}
</AlignPortal>
</div>
</div>
</div>
@ -144,25 +169,10 @@ export default class TagTypeFormat extends React.Component<ITagTypeFormatProps,
}
}
private showHideType = () => {
let formatHideClass = ["format-items-hide"];
if (this.state.showType) {
formatHideClass = [];
}
return formatHideClass.join(" ");
}
private handleFormatShow = (e) => {
if (e.type === "click") {
this.setState({showFormat: !this.state.showFormat, showType: false}); }
this.setState({showFormat: !this.state.showFormat, showType: false});
}
private showHideFormat = () => {
let formatHideClass = ["format-items-hide", "format-list"];
if (this.state.showFormat) {
formatHideClass = ["format-list"];
}
return formatHideClass.join(" ");
}
private handleFormatChange = (event) => {

Просмотреть файл

@ -33,7 +33,7 @@
}
.format-list {
width: 100%;
width: $tagInputWidth - $tagColorWidth;
border: white 1px;
}
@ -42,7 +42,7 @@
}
.format-items-list {
width: 100%;
width: $tagInputWidth - $tagColorWidth;
list-style-type: none;
padding: 0px;
margin: 0px;

Просмотреть файл

@ -167,7 +167,7 @@ $tagColorWidth: 12px;
}
&-input {
&-potal {
&-portal {
position: absolute;
z-index: 1;
box-shadow: 0px 5px 10px $darker-5;

Просмотреть файл

@ -4,6 +4,7 @@
import React, { KeyboardEvent, RefObject } from "react";
import ReactDOM from "react-dom";
import Align from "rc-align";
import { AlignPortal } from "../alignPortal/alignPortal";
import { randomIntInRange } from "../../../../common/utils";
import { IRegion, ITag, ILabel, FieldType, FieldFormat } from "../../../../models/applicationState";
import { ColorPicker } from "../colorPicker";
@ -13,7 +14,7 @@ import TagInputItem, { ITagInputItemProps, ITagClickProps } from "./tagInputItem
import TagInputToolbar from "./tagInputToolbar";
import { toast } from "react-toastify";
import { strings } from "../../../../common/strings";
import TagTypeFormat from "./tagTypeFormat";
import TagContextMenu from "./tagContentMenu";
// tslint:disable-next-line:no-var-requires
const tagColors = require("../../common/tagColors.json");
@ -63,8 +64,6 @@ export interface ITagInputState {
searchQuery: string;
selectedTag: ITag;
editingTag: ITag;
colorPortalElement: Element;
fieldPortalElement: Element;
editingTagNode: Element;
}
@ -86,12 +85,9 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
selectedTag: null,
editingTag: null,
editingTagNode: null,
colorPortalElement: defaultDOMNode(),
fieldPortalElement: defaultDOMNode(),
};
private tagItemRefs: Map<string, TagInputItem> = new Map<string, TagInputItem>();
private portalDiv = document.createElement("div");
private inputRef: RefObject<HTMLInputElement>;
@ -161,18 +157,6 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
);
}
public componentDidMount() {
document.body.appendChild(this.portalDiv);
this.setState({
colorPortalElement: ReactDOM.findDOMNode(this.portalDiv) as Element,
fieldPortalElement: ReactDOM.findDOMNode(this.portalDiv) as Element,
});
}
public componentWillUnmount() {
document.body.removeChild(this.portalDiv);
}
public componentDidUpdate(prevProps: ITagInputProps) {
if (prevProps.tags !== this.props.tags) {
this.setState({
@ -322,48 +306,36 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
private getColorPickerPortal = () => {
return (
<div>
{
ReactDOM.createPortal(
<Align align={this.getColorAlignConfig()} target={this.getEditingTagNode}>
<div className="tag-input-potal">
{
this.state.showColorPicker &&
<ColorPicker
color={this.state.editingTag && this.state.editingTag.color}
colors={tagColors}
onEditColor={this.handleColorChange}
show={this.state.showColorPicker}
/>
}
</div>
</Align>
, this.state.colorPortalElement)
}
</div>
<AlignPortal align={this.getColorAlignConfig()} target={this.getEditingTagNode}>
<div className="tag-input-portal">
{
this.state.showColorPicker &&
<ColorPicker
color={this.state.editingTag && this.state.editingTag.color}
colors={tagColors}
onEditColor={this.handleColorChange}
show={this.state.showColorPicker}
/>
}
</div>
</AlignPortal>
);
}
private getTagFieldPortal = () => {
return (
<div>
{
ReactDOM.createPortal(
<Align align={this.getFieldAlignConfig()} target={this.getEditingTagNameNode}>
<div className="tag-input-potal" style = {{overflow: "hidden"}}>
{
this.state.showDropDown &&
<TagTypeFormat
key={this.state.editingTag.name}
tag={this.state.editingTag}
onChange={this.props.onTagChanged}
/>
}
</div>
</Align>
, this.state.fieldPortalElement)
}
</div>
<Align align={this.getFieldAlignConfig()} target={this.getEditingTagNameNode} monitorWindowResize={true}>
<div className="tag-input-portal" style = {{overflow: "hidden"}}>
{
this.state.showDropDown &&
<TagContextMenu
key={this.state.editingTag.name}
tag={this.state.editingTag}
onChange={this.props.onTagChanged}
/>
}
</div>
</Align>
);
}
@ -384,8 +356,8 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
return {
// Align top right of source node (dropdown) with top left of target node (tag name row)
points: ["tr", "br"],
// Offset source node by 6px in x and 3px in y
offset: [6, 3],
// Offset source node by 0px in x and 3px in y
offset: [0, 3],
// Auto adjust position when source node is overflowed
overflow: {adjustX: true, adjustY: true},
};