From 063ceb215f858a8e2a5bde85d8f9ca24240894c6 Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Mon, 21 Sep 2020 15:20:41 +0900
Subject: Separate input component

---
 src/settings/components/ui/TextArea.tsx | 42 +++++++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)
 create mode 100644 src/settings/components/ui/TextArea.tsx

(limited to 'src/settings/components/ui/TextArea.tsx')

diff --git a/src/settings/components/ui/TextArea.tsx b/src/settings/components/ui/TextArea.tsx
new file mode 100644
index 0000000..f2b832f
--- /dev/null
+++ b/src/settings/components/ui/TextArea.tsx
@@ -0,0 +1,42 @@
+import React from "react";
+import "./Input.scss";
+
+interface Props extends React.AllHTMLAttributes<HTMLElement> {
+  name: string;
+  error?: string;
+  label: string;
+  value: string;
+  onValueChange?: (name: string, value: string) => void;
+  onBlur?: (e: React.FocusEvent<Element>) => void;
+}
+
+class TextArea extends React.Component<Props> {
+  renderTextArea(props: Props) {
+    const inputClassName = props.error ? "input-error" : "";
+    const pp = { ...props };
+    delete pp.onValueChange;
+    return (
+      <div className="settings-ui-input">
+        <label htmlFor={props.id}>{props.label}</label>
+        <textarea
+          className={inputClassName}
+          onChange={this.bindOnChange.bind(this)}
+          {...pp}
+        />
+        <p className="settings-ui-input-error">{this.props.error}</p>
+      </div>
+    );
+  }
+
+  render() {
+    return this.renderTextArea(this.props);
+  }
+
+  bindOnChange(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
+    if (this.props.onValueChange) {
+      this.props.onValueChange(e.target.name, e.target.value);
+    }
+  }
+}
+
+export default TextArea;
-- 
cgit v1.2.3


From c28ae6b82860693b8e6b012a939fb8a3dd320e23 Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Mon, 21 Sep 2020 16:08:44 +0900
Subject: Introduce styled-components on settings ui

---
 src/settings/components/site.scss            |  8 +--
 src/settings/components/ui/AddButton.scss    | 13 -----
 src/settings/components/ui/AddButton.tsx     | 31 +++++++-----
 src/settings/components/ui/DeleteButton.scss | 13 -----
 src/settings/components/ui/DeleteButton.tsx  | 30 ++++++-----
 src/settings/components/ui/Input.scss        | 29 -----------
 src/settings/components/ui/Radio.tsx         | 59 +++++++++------------
 src/settings/components/ui/Text.tsx          | 70 ++++++++++++++-----------
 src/settings/components/ui/TextArea.tsx      | 76 ++++++++++++++++------------
 9 files changed, 145 insertions(+), 184 deletions(-)
 delete mode 100644 src/settings/components/ui/AddButton.scss
 delete mode 100644 src/settings/components/ui/DeleteButton.scss
 delete mode 100644 src/settings/components/ui/Input.scss

(limited to 'src/settings/components/ui/TextArea.tsx')

diff --git a/src/settings/components/site.scss b/src/settings/components/site.scss
index c0c4f9e..e8415e8 100644
--- a/src/settings/components/site.scss
+++ b/src/settings/components/site.scss
@@ -1,12 +1,6 @@
 .vimvixen-settings-form {
   padding: 2px;
-
-  textarea[name=json] {
-    font-family: monospace;
-    width: 100%;
-    min-height: 64ex;
-    resize: vertical;
-  }
+  font-family: system-ui;
 
   fieldset {
     margin: 0;
diff --git a/src/settings/components/ui/AddButton.scss b/src/settings/components/ui/AddButton.scss
deleted file mode 100644
index beb5688..0000000
--- a/src/settings/components/ui/AddButton.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-.ui-add-button {
-  border: none;
-  padding: 4;
-  display: inline;
-  background: none;
-  font-weight: bold;
-  color: green;
-  cursor: pointer;
-
-  &:hover {
-    color: darkgreen;
-  }
-}
diff --git a/src/settings/components/ui/AddButton.tsx b/src/settings/components/ui/AddButton.tsx
index c15a732..8cf4300 100644
--- a/src/settings/components/ui/AddButton.tsx
+++ b/src/settings/components/ui/AddButton.tsx
@@ -1,19 +1,24 @@
-import "./AddButton.scss";
 import React from "react";
+import styled from "styled-components";
 
-type Props = React.AllHTMLAttributes<HTMLInputElement>;
+const Button = styled.input`
+  border: none;
+  padding: 4;
+  display: inline;
+  background: none;
+  font-weight: bold;
+  color: green;
+  cursor: pointer;
 
-class AddButton extends React.Component<Props> {
-  render() {
-    return (
-      <input
-        className="ui-add-button"
-        type="button"
-        value="&#x271a;"
-        {...this.props}
-      />
-    );
+  &:hover {
+    color: darkgreen;
   }
-}
+`;
+
+type Props = React.InputHTMLAttributes<HTMLInputElement>;
+
+const AddButton: React.FC<Props> = (props) => (
+  <Button type="button" value="&#x271a;" {...props} />
+);
 
 export default AddButton;
diff --git a/src/settings/components/ui/DeleteButton.scss b/src/settings/components/ui/DeleteButton.scss
deleted file mode 100644
index 5932a72..0000000
--- a/src/settings/components/ui/DeleteButton.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-
-.ui-delete-button {
-  border: none;
-  padding: 4;
-  display: inline;
-  background: none;
-  color: red;
-  cursor: pointer;
-
-  &:hover {
-    color: darkred;
-  }
-}
diff --git a/src/settings/components/ui/DeleteButton.tsx b/src/settings/components/ui/DeleteButton.tsx
index df8976e..ce0183b 100644
--- a/src/settings/components/ui/DeleteButton.tsx
+++ b/src/settings/components/ui/DeleteButton.tsx
@@ -1,19 +1,23 @@
-import "./DeleteButton.scss";
 import React from "react";
+import styled from "styled-components";
 
-type Props = React.AllHTMLAttributes<HTMLInputElement>;
+const Button = styled.input`
+  border: none;
+  padding: 4;
+  display: inline;
+  background: none;
+  color: red;
+  cursor: pointer;
 
-class DeleteButton extends React.Component<Props> {
-  render() {
-    return (
-      <input
-        className="ui-delete-button"
-        type="button"
-        value="&#x2716;"
-        {...this.props}
-      />
-    );
+  &:hover {
+    color: darkred;
   }
-}
+`;
+
+type Props = React.InputHTMLAttributes<HTMLInputElement>;
+
+const DeleteButton: React.FC<Props> = (props) => (
+  <Button type="button" value="&#x2716;" {...props} />
+);
 
 export default DeleteButton;
diff --git a/src/settings/components/ui/Input.scss b/src/settings/components/ui/Input.scss
deleted file mode 100644
index ad4daf8..0000000
--- a/src/settings/components/ui/Input.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-.settings-ui-input {
-  page-break-inside: avoid;
-
-  * {
-    page-break-inside: avoid;
-  }
-
-  label {
-    font-weight: bold;
-    min-width: 14rem;
-    display: inline-block;
-  }
-
-  input[type='text'] {
-    padding: 4px;
-    width: 8rem;
-  }
-
-  input.input-crror,
-  textarea.input-error {
-    box-shadow: 0 0 2px red;
-  }
-
-  &-error {
-    font-weight: bold;
-    color: red;
-    min-height: 1.5em;
-  }
-}
diff --git a/src/settings/components/ui/Radio.tsx b/src/settings/components/ui/Radio.tsx
index 20d4ad6..c0d4dd9 100644
--- a/src/settings/components/ui/Radio.tsx
+++ b/src/settings/components/ui/Radio.tsx
@@ -1,44 +1,33 @@
 import React from "react";
-import "./Input.scss";
+import styled from "styled-components";
 
-interface Props extends React.AllHTMLAttributes<HTMLElement> {
-  name: string;
-  error?: string;
+const Container = styled.div`
+  font-family: system-ui;
+`;
+
+interface Props extends React.InputHTMLAttributes<HTMLInputElement> {
   label: string;
-  value: string;
   onValueChange?: (name: string, value: string) => void;
-  onBlur?: (e: React.FocusEvent<Element>) => void;
 }
 
-class Input extends React.Component<Props> {
-  renderRadio(props: Props) {
-    const inputClassName = props.error ? "input-error" : "";
-    const pp = { ...props };
-    delete pp.onValueChange;
-    return (
-      <div className="settings-ui-input">
-        <label>
-          <input
-            type="radio"
-            className={inputClassName}
-            onChange={this.bindOnChange.bind(this)}
-            {...pp}
-          />
-          {props.label}
-        </label>
-      </div>
-    );
-  }
+const Radio: React.FC<Props> = (props) => {
+  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    if (props.onValueChange) {
+      props.onValueChange(e.target.name, e.target.value);
+    }
+  };
 
-  render() {
-    return this.renderRadio(this.props);
-  }
+  const pp = { ...props };
+  delete pp.onValueChange;
 
-  bindOnChange(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
-    if (this.props.onValueChange) {
-      this.props.onValueChange(e.target.name, e.target.value);
-    }
-  }
-}
+  return (
+    <Container>
+      <label htmlFor={props.id}>
+        <input type="radio" onChange={onChange} {...pp} />
+        {props.label}
+      </label>
+    </Container>
+  );
+};
 
-export default Input;
+export default Radio;
diff --git a/src/settings/components/ui/Text.tsx b/src/settings/components/ui/Text.tsx
index 7ecef88..700b08a 100644
--- a/src/settings/components/ui/Text.tsx
+++ b/src/settings/components/ui/Text.tsx
@@ -1,42 +1,54 @@
 import React from "react";
-import "./Input.scss";
+import styled from "styled-components";
 
-interface Props extends React.AllHTMLAttributes<HTMLElement> {
+const Container = styled.div`
+  page-break-inside: avoid;
+`;
+
+const Input = styled.input<{ hasError: boolean }>`
+  padding: 4px;
+  width: 8rem;
+  box-shadow: ${({ hasError }) => (hasError ? "0 0 2px red" : "none")};
+`;
+
+const Label = styled.label`
+  font-weight: bold;
+  min-width: 14rem;
+  display: inline-block;
+`;
+
+interface Props extends React.HTMLAttributes<HTMLElement> {
   name: string;
   error?: string;
   label: string;
   value: string;
   onValueChange?: (name: string, value: string) => void;
-  onBlur?: (e: React.FocusEvent<Element>) => void;
 }
 
-class Input extends React.Component<Props> {
-  renderText(props: Props) {
-    const inputClassName = props.error ? "input-error" : "";
-    const pp = { ...props };
-    delete pp.onValueChange;
-    return (
-      <div className="settings-ui-input">
-        <label htmlFor={props.id}>{props.label}</label>
-        <input
+const Text: React.FC<Props> = (props) => {
+  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    if (props.onValueChange) {
+      props.onValueChange(e.target.name, e.target.value);
+    }
+  };
+
+  const pp = { ...props };
+  delete pp.onValueChange;
+
+  return (
+    <Container>
+      <Label>
+        {props.label}
+        <br />
+        <Input
           type="text"
-          className={inputClassName}
-          onChange={this.bindOnChange.bind(this)}
+          hasError={props.error !== undefined}
+          onChange={onChange}
           {...pp}
         />
-      </div>
-    );
-  }
-
-  render() {
-    return this.renderText(this.props);
-  }
-
-  bindOnChange(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
-    if (this.props.onValueChange) {
-      this.props.onValueChange(e.target.name, e.target.value);
-    }
-  }
-}
+      </Label>
+    </Container>
+  );
+};
 
-export default Input;
+export default Text;
diff --git a/src/settings/components/ui/TextArea.tsx b/src/settings/components/ui/TextArea.tsx
index f2b832f..58b7bd3 100644
--- a/src/settings/components/ui/TextArea.tsx
+++ b/src/settings/components/ui/TextArea.tsx
@@ -1,42 +1,54 @@
 import React from "react";
-import "./Input.scss";
+import styled from "styled-components";
 
-interface Props extends React.AllHTMLAttributes<HTMLElement> {
-  name: string;
+const Container = styled.div`
+  page-break-inside: avoid;
+`;
+
+const Label = styled.label`
+  font-weight: bold;
+  min-width: 14rem;
+  display: inline-block;
+`;
+
+const ErrorableTextArea = styled.textarea<{ hasError: boolean }>`
+  box-shadow: ${({ hasError }) => (hasError ? "0 0 2px red" : "none")};
+  font-family: monospace;
+  font-family: monospace;
+  width: 100%;
+  min-height: 64ex;
+  resize: vertical;
+`;
+
+const ErrorMessage = styled.p`
+  font-weight: bold;
+  color: red;
+  min-height: 1.5em;
+`;
+
+interface Props extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
   error?: string;
   label: string;
-  value: string;
   onValueChange?: (name: string, value: string) => void;
-  onBlur?: (e: React.FocusEvent<Element>) => void;
 }
 
-class TextArea extends React.Component<Props> {
-  renderTextArea(props: Props) {
-    const inputClassName = props.error ? "input-error" : "";
-    const pp = { ...props };
-    delete pp.onValueChange;
-    return (
-      <div className="settings-ui-input">
-        <label htmlFor={props.id}>{props.label}</label>
-        <textarea
-          className={inputClassName}
-          onChange={this.bindOnChange.bind(this)}
-          {...pp}
-        />
-        <p className="settings-ui-input-error">{this.props.error}</p>
-      </div>
-    );
-  }
-
-  render() {
-    return this.renderTextArea(this.props);
-  }
-
-  bindOnChange(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
-    if (this.props.onValueChange) {
-      this.props.onValueChange(e.target.name, e.target.value);
+const TextArea: React.FC<Props> = (props) => {
+  const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+    if (props.onValueChange) {
+      props.onValueChange(e.target.name, e.target.value);
     }
-  }
-}
+  };
+
+  const hasError = typeof props.error !== "undefined" && props.error !== "";
+  const pp = { ...props };
+  delete pp.onValueChange;
+  return (
+    <Container>
+      <Label htmlFor={props.id}>{props.label}</Label>
+      <ErrorableTextArea hasError={hasError} onChange={onChange} {...pp} />
+      {hasError ? <ErrorMessage>{props.error}</ErrorMessage> : null}
+    </Container>
+  );
+};
 
 export default TextArea;
-- 
cgit v1.2.3


From 1f014295a54f1e16b8bd94da729fb0afd143f7fe Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Mon, 21 Sep 2020 23:27:44 +0900
Subject: Fix component test

---
 src/settings/components/form/BlacklistForm.tsx       |  4 ++--
 src/settings/components/ui/TextArea.tsx              |  4 +++-
 test/settings/components/form/BlacklistForm.test.tsx | 20 ++++++++++----------
 .../components/form/SearchEngineForm.test.tsx        | 10 +++++++---
 test/settings/components/ui/TextArea.test.tsx        |  2 +-
 5 files changed, 23 insertions(+), 17 deletions(-)

(limited to 'src/settings/components/ui/TextArea.tsx')

diff --git a/src/settings/components/form/BlacklistForm.tsx b/src/settings/components/form/BlacklistForm.tsx
index 4d794f4..d301f2c 100644
--- a/src/settings/components/form/BlacklistForm.tsx
+++ b/src/settings/components/form/BlacklistForm.tsx
@@ -40,13 +40,13 @@ class BlacklistForm extends React.Component<Props> {
   render() {
     return (
       <>
-        <Grid>
+        <Grid role="list">
           {this.props.value.items.map((item, index) => {
             if (item.partial) {
               return null;
             }
             return (
-              <GridRow key={index}>
+              <GridRow role="listitem" key={index}>
                 <GridCell>
                   <Input
                     data-index={index}
diff --git a/src/settings/components/ui/TextArea.tsx b/src/settings/components/ui/TextArea.tsx
index 58b7bd3..9dcd5be 100644
--- a/src/settings/components/ui/TextArea.tsx
+++ b/src/settings/components/ui/TextArea.tsx
@@ -46,7 +46,9 @@ const TextArea: React.FC<Props> = (props) => {
     <Container>
       <Label htmlFor={props.id}>{props.label}</Label>
       <ErrorableTextArea hasError={hasError} onChange={onChange} {...pp} />
-      {hasError ? <ErrorMessage>{props.error}</ErrorMessage> : null}
+      {hasError ? (
+        <ErrorMessage role="alert">{props.error}</ErrorMessage>
+      ) : null}
     </Container>
   );
 };
diff --git a/test/settings/components/form/BlacklistForm.test.tsx b/test/settings/components/form/BlacklistForm.test.tsx
index e34802a..8727c59 100644
--- a/test/settings/components/form/BlacklistForm.test.tsx
+++ b/test/settings/components/form/BlacklistForm.test.tsx
@@ -17,16 +17,16 @@ describe("settings/form/BlacklistForm", () => {
         />
       ).root;
 
-      const rows = root.findAllByProps({
-        className: "form-blacklist-form-row",
-      });
+      const rows = root
+        .findAllByType("div")
+        .filter((instance) => instance.props.role === "listitem");
       expect(rows).to.have.lengthOf(2);
-      expect(
-        rows[0].findByProps({ className: "column-url" }).props.value
-      ).to.equal("*.slack.com");
-      expect(
-        rows[1].findByProps({ className: "column-url" }).props.value
-      ).to.equal("www.google.com/maps");
+      expect(rows[0].findByProps({ name: "url" }).props.value).to.equal(
+        "*.slack.com"
+      );
+      expect(rows[1].findByProps({ name: "url" }).props.value).to.equal(
+        "www.google.com/maps"
+      );
 
       expect(() => root.findByType(AddButton)).not.throw();
     });
@@ -113,7 +113,7 @@ describe("settings/form/BlacklistForm", () => {
       });
 
       const button = document.querySelector(
-        "input[type=button].ui-add-button"
+        "input[type=button][name=add]"
       ) as HTMLButtonElement;
       ReactTestUtils.Simulate.click(button);
     });
diff --git a/test/settings/components/form/SearchEngineForm.test.tsx b/test/settings/components/form/SearchEngineForm.test.tsx
index 5f835cc..7b10274 100644
--- a/test/settings/components/form/SearchEngineForm.test.tsx
+++ b/test/settings/components/form/SearchEngineForm.test.tsx
@@ -21,12 +21,16 @@ describe("settings/form/SearchForm", () => {
         />
       ).root;
 
-      const names = root.findAllByProps({ name: "name" });
+      const names = root
+        .findAllByType("input")
+        .filter((instance) => instance.props.name === "name");
       expect(names).to.have.lengthOf(2);
       expect(names[0].props.value).to.equal("google");
       expect(names[1].props.value).to.equal("yahoo");
 
-      const urls = root.findAllByProps({ name: "url" });
+      const urls = root
+        .findAllByType("input")
+        .filter((instance) => instance.props.name === "url");
       expect(urls).to.have.lengthOf(2);
       expect(urls[0].props.value).to.equal("google.com");
       expect(urls[1].props.value).to.equal("yahoo.com");
@@ -139,7 +143,7 @@ describe("settings/form/SearchForm", () => {
       });
 
       const button = document.querySelector(
-        "input[type=button].ui-add-button"
+        "input[type=button][name=add]"
       ) as HTMLInputElement;
       ReactTestUtils.Simulate.click(button);
     });
diff --git a/test/settings/components/ui/TextArea.test.tsx b/test/settings/components/ui/TextArea.test.tsx
index 50313cf..232c7c0 100644
--- a/test/settings/components/ui/TextArea.test.tsx
+++ b/test/settings/components/ui/TextArea.test.tsx
@@ -32,7 +32,7 @@ describe("settings/ui/TextArea", () => {
 
     const label = document.querySelector("label")!;
     const textarea = document.querySelector("textarea")!;
-    const error = document.querySelector(".settings-ui-input-error")!;
+    const error = document.querySelector("[role=alert]")!;
     expect(label.textContent).to.contain("myfield");
     expect(textarea.nodeName).to.contain("TEXTAREA");
     expect(textarea.name).to.contain("myname");
-- 
cgit v1.2.3

