fix(ui): use custom inline edit component

This commit is contained in:
Łukasz Mierzwa
2020-05-17 21:45:08 +01:00
committed by Łukasz Mierzwa
parent 9cfc4f317b
commit d3aaa5bff7
6 changed files with 239 additions and 24 deletions
-9
View File
@@ -4,15 +4,6 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@attently/riek": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@attently/riek/-/riek-2.0.1.tgz",
"integrity": "sha1-AFJ4WlurHKepkjbxNWJNOeZhkCQ=",
"requires": {
"debug": "^2.6.8",
"prop-types": "^15.5.10"
}
},
"@babel/code-frame": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
-1
View File
@@ -4,7 +4,6 @@
"license": "Apache-2.0",
"private": true,
"dependencies": {
"@attently/riek": "2.0.1",
"@fortawesome/fontawesome-common-types": "0.2.28",
"@fortawesome/fontawesome-svg-core": "1.2.28",
"@fortawesome/free-regular-svg-icons": "5.13.0",
+87
View File
@@ -0,0 +1,87 @@
import React, { useState, useRef, useEffect } from "react";
import PropTypes from "prop-types";
import { useOnClickOutside } from "Hooks/useOnClickOutside";
const InlineEdit = ({
className,
classNameEditing,
value,
onChange,
onEnterEditing,
onExitEditing,
}) => {
const ref = useRef(null);
const [editedValue, setEditedValue] = useState(null);
const [isEditing, setIsEditing] = useState(false);
const startEditing = () => {
if (onEnterEditing) {
onEnterEditing();
}
setIsEditing(true);
};
const doneEditing = () => {
setIsEditing(false);
setEditedValue(null);
if (onExitEditing) {
onExitEditing();
}
};
const onInput = (event) => {
setEditedValue(event.target.value.trim());
};
const onKeyDown = (event) => {
if (event.keyCode === 13) {
if (editedValue) {
onChange(editedValue);
}
doneEditing();
} else if (event.keyCode === 27) {
doneEditing();
}
};
useOnClickOutside(ref, doneEditing);
useEffect(() => {
if (isEditing && ref.current) {
ref.current.focus();
}
}, [isEditing, ref]);
if (isEditing) {
const val = editedValue === null ? value : editedValue;
return (
<input
ref={ref}
type="text"
className={classNameEditing}
value={val}
size={val.length + 1}
onChange={onInput}
onKeyDown={onKeyDown}
/>
);
}
return (
<span tabIndex={0} className={className} onClick={startEditing}>
{value}
</span>
);
};
InlineEdit.propTypes = {
className: PropTypes.string,
classNameEditing: PropTypes.string,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
onEnterEditing: PropTypes.func,
onExitEditing: PropTypes.func,
};
export { InlineEdit };
+141
View File
@@ -0,0 +1,141 @@
import React from "react";
import { act } from "react-dom/test-utils";
import { mount } from "enzyme";
import { InlineEdit } from ".";
describe("<InlineEdit />", () => {
it("renders span by default", () => {
const tree = mount(<InlineEdit value="foo" onChange={jest.fn()} />);
expect(tree.html()).toBe('<span tabindex="0">foo</span>');
});
it("renders input after click", () => {
const tree = mount(<InlineEdit value="foo" onChange={jest.fn()} />);
tree.simulate("click");
expect(tree.html()).toBe('<input type="text" size="4" value="foo">');
});
it("edit mode start calls onEnterEditing", () => {
const onEnterEditing = jest.fn();
const tree = mount(
<InlineEdit
value="foo"
onChange={jest.fn()}
onEnterEditing={onEnterEditing}
/>
);
expect(onEnterEditing).not.toHaveBeenCalled();
tree.simulate("click");
expect(tree.html()).toBe('<input type="text" size="4" value="foo">');
expect(onEnterEditing).toHaveBeenCalled();
});
it("edit mode finish calls onExitEditing", () => {
const onExitEditing = jest.fn();
const tree = mount(
<div id="root">
<button>click me</button>
<InlineEdit
value="foo"
onChange={jest.fn()}
onExitEditing={onExitEditing}
/>
</div>
);
expect(onExitEditing).not.toHaveBeenCalled();
tree.find("span").simulate("click");
expect(onExitEditing).not.toHaveBeenCalled();
act(() => {
document.dispatchEvent(
new Event("mousedown", { target: tree.find("button").getDOMNode() })
);
});
expect(tree.html()).not.toMatch(/<input/);
expect(onExitEditing).toHaveBeenCalled();
});
it("cancels edits after click outside", () => {
const tree = mount(
<div id="root">
<button>click me</button>
<InlineEdit value="foo" onChange={jest.fn()} />
</div>
);
tree.find("span").simulate("click");
expect(tree.html()).toMatch(/<input/);
act(() => {
document.dispatchEvent(
new Event("mousedown", { target: tree.find("button").getDOMNode() })
);
});
expect(tree.html()).not.toMatch(/<input/);
});
it("typing in the input changes value", () => {
const tree = mount(<InlineEdit value="foo" onChange={jest.fn()} />);
tree.find("span").simulate("click");
expect(tree.html()).toMatch(/<input/);
tree.simulate("change", { target: { value: "bar" } });
expect(tree.html()).toBe('<input type="text" size="4" value="bar">');
});
it("enter calls onChange if value was edited", () => {
const onChange = jest.fn();
const tree = mount(<InlineEdit value="foo" onChange={onChange} />);
tree.find("span").simulate("click");
expect(tree.html()).toMatch(/<input/);
tree.simulate("change", { target: { value: "bar" } });
expect(tree.html()).toBe('<input type="text" size="4" value="bar">');
tree.simulate("keyDown", { keyCode: 13 });
expect(onChange).toHaveBeenCalledWith("bar");
});
it("enter doesn't call onChange if value was not edited", () => {
const onChange = jest.fn();
const tree = mount(<InlineEdit value="foo" onChange={onChange} />);
tree.find("span").simulate("click");
expect(tree.html()).toMatch(/<input/);
tree.simulate("keyDown", { keyCode: 13 });
expect(onChange).not.toHaveBeenCalled();
});
it("esc cancels edit mode", () => {
const onChange = jest.fn();
const tree = mount(<InlineEdit value="foo" onChange={onChange} />);
tree.find("span").simulate("click");
expect(tree.html()).toMatch(/<input/);
tree.simulate("keyDown", { keyCode: 27 });
expect(tree.html()).not.toMatch(/<input/);
expect(onChange).not.toHaveBeenCalled();
});
it("unknown keyDown does nothing", () => {
const onChange = jest.fn();
const tree = mount(<InlineEdit value="foo" onChange={onChange} />);
tree.find("span").simulate("click");
expect(tree.html()).toMatch(/<input/);
tree.simulate("keyDown", { keyCode: 45 });
expect(tree.html()).toMatch(/<input/);
expect(onChange).not.toHaveBeenCalled();
});
});
@@ -3,8 +3,6 @@ import PropTypes from "prop-types";
import { useObserver } from "mobx-react";
import { RIEInput } from "@attently/riek";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner";
@@ -14,15 +12,16 @@ import { AlertStore } from "Stores/AlertStore";
import { QueryOperators } from "Common/Query";
import { TooltipWrapper } from "Components/TooltipWrapper";
import { GetClassAndStyle } from "Components/Labels/Utils";
import { InlineEdit } from "Components/InlineEdit";
const FilterInputLabel = ({ alertStore, filter }) => {
const onChange = ({ raw }) => {
const onChange = (val) => {
// if filter is empty string then remove it
if (raw === "") {
if (val === "") {
alertStore.filters.removeFilter(filter.raw);
}
// if not empty replace it
alertStore.filters.replaceFilter(filter.raw, raw);
alertStore.filters.replaceFilter(filter.raw, val);
};
const cs = GetClassAndStyle(
@@ -63,15 +62,13 @@ const FilterInputLabel = ({ alertStore, filter }) => {
title="Click to edit this filter"
className="components-filteredinputlabel-text flex-grow-1 flex-shrink-1 ml-1"
>
<RIEInput
<InlineEdit
className="cursor-text px-1"
defaultValue=""
classNameEditing="px-1 py-0 border-0 editing rounded"
value={filter.raw}
propName="raw"
change={onChange}
classEditing="py-0 border-0 editing rounded"
afterStart={alertStore.status.pause}
afterFinish={alertStore.status.resume}
onChange={onChange}
onEnterEditing={alertStore.status.pause}
onExitEditing={alertStore.status.resume}
/>
</TooltipWrapper>
<FontAwesomeIcon
@@ -49,8 +49,8 @@ const ValidateOnChange = (newRaw) => {
/>
);
const input = tree.find("RIEInput");
input.props().change({ raw: newRaw });
const input = tree.find("InlineEdit");
input.props().onChange(newRaw);
return tree;
};