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:
Родитель
42dc96de4e
Коммит
2df1905cf2
|
@ -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},
|
||||
};
|
||||
|
|
Загрузка…
Ссылка в новой задаче