Browse Source

use icons for toggle labels (#2315)

Noel Schnierer 4 years ago
parent
commit
7491fcc3f3

+ 105 - 19
src/actions/actionProperties.tsx

@@ -12,6 +12,7 @@ import {
   canChangeSharpness,
 } from "../scene";
 import { ButtonSelect } from "../components/ButtonSelect";
+import { ButtonIconSelect } from "../components/ButtonIconSelect";
 import {
   isTextElement,
   redrawTextBoundingBox,
@@ -25,6 +26,20 @@ import { register } from "./register";
 import { newElementWith } from "../element/mutateElement";
 import { DEFAULT_FONT_SIZE, DEFAULT_FONT_FAMILY } from "../constants";
 import { randomInteger } from "../random";
+import {
+  FillHachureIcon,
+  FillCrossHatchIcon,
+  FillSolidIcon,
+  StrokeWidthIcon,
+  StrokeStyleSolidIcon,
+  StrokeStyleDashedIcon,
+  StrokeStyleDottedIcon,
+  EdgeSharpIcon,
+  EdgeRoundIcon,
+  SloppinessArchitectIcon,
+  SloppinessArtistIcon,
+  SloppinessCartoonistIcon,
+} from "../components/icons";
 
 const changeProperty = (
   elements: readonly ExcalidrawElement[],
@@ -141,11 +156,23 @@ export const actionChangeFillStyle = register({
   PanelComponent: ({ elements, appState, updateData }) => (
     <fieldset>
       <legend>{t("labels.fill")}</legend>
-      <ButtonSelect
+      <ButtonIconSelect
         options={[
-          { value: "hachure", text: t("labels.hachure") },
-          { value: "cross-hatch", text: t("labels.crossHatch") },
-          { value: "solid", text: t("labels.solid") },
+          {
+            value: "hachure",
+            text: t("labels.hachure"),
+            icon: <FillHachureIcon appearance={appState.appearance} />,
+          },
+          {
+            value: "cross-hatch",
+            text: t("labels.crossHatch"),
+            icon: <FillCrossHatchIcon appearance={appState.appearance} />,
+          },
+          {
+            value: "solid",
+            text: t("labels.solid"),
+            icon: <FillSolidIcon appearance={appState.appearance} />,
+          },
         ]}
         group="fill"
         value={getFormValue(
@@ -178,12 +205,39 @@ export const actionChangeStrokeWidth = register({
   PanelComponent: ({ elements, appState, updateData }) => (
     <fieldset>
       <legend>{t("labels.strokeWidth")}</legend>
-      <ButtonSelect
+      <ButtonIconSelect
         group="stroke-width"
         options={[
-          { value: 1, text: t("labels.thin") },
-          { value: 2, text: t("labels.bold") },
-          { value: 4, text: t("labels.extraBold") },
+          {
+            value: 1,
+            text: t("labels.thin"),
+            icon: (
+              <StrokeWidthIcon
+                appearance={appState.appearance}
+                strokeWidth={2}
+              />
+            ),
+          },
+          {
+            value: 2,
+            text: t("labels.bold"),
+            icon: (
+              <StrokeWidthIcon
+                appearance={appState.appearance}
+                strokeWidth={6}
+              />
+            ),
+          },
+          {
+            value: 4,
+            text: t("labels.extraBold"),
+            icon: (
+              <StrokeWidthIcon
+                appearance={appState.appearance}
+                strokeWidth={10}
+              />
+            ),
+          },
         ]}
         value={getFormValue(
           elements,
@@ -214,12 +268,24 @@ export const actionChangeSloppiness = register({
   PanelComponent: ({ elements, appState, updateData }) => (
     <fieldset>
       <legend>{t("labels.sloppiness")}</legend>
-      <ButtonSelect
+      <ButtonIconSelect
         group="sloppiness"
         options={[
-          { value: 0, text: t("labels.architect") },
-          { value: 1, text: t("labels.artist") },
-          { value: 2, text: t("labels.cartoonist") },
+          {
+            value: 0,
+            text: t("labels.architect"),
+            icon: <SloppinessArchitectIcon appearance={appState.appearance} />,
+          },
+          {
+            value: 1,
+            text: t("labels.artist"),
+            icon: <SloppinessArtistIcon appearance={appState.appearance} />,
+          },
+          {
+            value: 2,
+            text: t("labels.cartoonist"),
+            icon: <SloppinessCartoonistIcon appearance={appState.appearance} />,
+          },
         ]}
         value={getFormValue(
           elements,
@@ -249,12 +315,24 @@ export const actionChangeStrokeStyle = register({
   PanelComponent: ({ elements, appState, updateData }) => (
     <fieldset>
       <legend>{t("labels.strokeStyle")}</legend>
-      <ButtonSelect
+      <ButtonIconSelect
         group="strokeStyle"
         options={[
-          { value: "solid", text: t("labels.strokeStyle_solid") },
-          { value: "dashed", text: t("labels.strokeStyle_dashed") },
-          { value: "dotted", text: t("labels.strokeStyle_dotted") },
+          {
+            value: "solid",
+            text: t("labels.strokeStyle_solid"),
+            icon: <StrokeStyleSolidIcon appearance={appState.appearance} />,
+          },
+          {
+            value: "dashed",
+            text: t("labels.strokeStyle_dashed"),
+            icon: <StrokeStyleDashedIcon appearance={appState.appearance} />,
+          },
+          {
+            value: "dotted",
+            text: t("labels.strokeStyle_dotted"),
+            icon: <StrokeStyleDottedIcon appearance={appState.appearance} />,
+          },
         ]}
         value={getFormValue(
           elements,
@@ -488,11 +566,19 @@ export const actionChangeSharpness = register({
   PanelComponent: ({ elements, appState, updateData }) => (
     <fieldset>
       <legend>{t("labels.edges")}</legend>
-      <ButtonSelect
+      <ButtonIconSelect
         group="edges"
         options={[
-          { value: "sharp", text: t("labels.sharp") },
-          { value: "round", text: t("labels.round") },
+          {
+            value: "sharp",
+            text: t("labels.sharp"),
+            icon: <EdgeSharpIcon appearance={appState.appearance} />,
+          },
+          {
+            value: "round",
+            text: t("labels.round"),
+            icon: <EdgeRoundIcon appearance={appState.appearance} />,
+          },
         ]}
         value={getFormValue(
           elements,

+ 33 - 0
src/components/ButtonIconSelect.tsx

@@ -0,0 +1,33 @@
+import React from "react";
+import clsx from "clsx";
+
+// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
+export const ButtonIconSelect = <T extends Object>({
+  options,
+  value,
+  onChange,
+  group,
+}: {
+  options: { value: T; text: string; icon: JSX.Element }[];
+  value: T | null;
+  onChange: (value: T) => void;
+  group: string;
+}) => (
+  <div className="buttonList buttonListIcon">
+    {options.map((option) => (
+      <label
+        key={option.text}
+        className={clsx({ active: value === option.value })}
+        title={option.text}
+      >
+        <input
+          type="radio"
+          name={group}
+          onChange={() => onChange(option.value)}
+          checked={value === option.value ? true : false}
+        />
+        {option.icon}
+      </label>
+    ))}
+  </div>
+);

+ 177 - 0
src/components/icons.tsx

@@ -504,3 +504,180 @@ export const UngroupIcon = React.memo(
       { width: 182, height: 182 },
     ),
 );
+
+export const FillHachureIcon = React.memo(
+  ({ appearance }: { appearance: "light" | "dark" }) =>
+    createIcon(
+      <g stroke={iconFillColor(appearance)} fill="none">
+        <path d="M0 0s0 0 0 0m0 0s0 0 0 0m.133 12.04L10.63-.033M.133 12.04L10.63-.034M2.234 21.818L21.26-.07M2.234 21.818L21.26-.07m-8.395 21.852L31.89-.103M12.865 21.783L31.89-.103m-8.395 21.852L41.208 1.37M23.495 21.75L41.208 1.37m-7.083 20.343l7.216-8.302m-7.216 8.302l7.216-8.302" />
+        <path
+          d="M0 0h40M0 0h40m0 0v20m0-20v20m0 0H0m40 0H0m0 0V0m0 20V0"
+          strokeWidth={2}
+        />
+      </g>,
+      { width: 40, height: 20 },
+    ),
+);
+
+export const FillCrossHatchIcon = React.memo(
+  ({ appearance }: { appearance: "light" | "dark" }) =>
+    createIcon(
+      <g stroke={iconFillColor(appearance)} fill="none">
+        <path d="M0 0s0 0 0 0m0 0s0 0 0 0m.133 12.04L10.63-.033M.133 12.04L10.63-.034M2.234 21.818L21.26-.07M2.234 21.818L21.26-.07m-8.395 21.852L31.89-.103M12.865 21.783C17.87 16.025 22.875 10.266 31.89-.103m-8.395 21.852L41.208 1.37M23.495 21.75L41.208 1.37m-7.083 20.343l7.216-8.302m-7.216 8.302l7.216-8.302M-.09 19.92s0 0 0 0m0 0s0 0 0 0m12.04-.133L-.126 9.29m12.075 10.497L-.126 9.29m24.871 11.02C19.872 16.075 15 11.84.595-.684m24.15 20.994L.595-.684m36.19 20.861L12.636-.817m24.15 20.994L12.636-.817m30.909 16.269L24.676-.95m18.868 16.402L24.676-.95m18.833 5.771L37.472-.427m6.037 5.248L37.472-.427" />
+        <path
+          d="M0 0h40M0 0h40m0 0v20m0-20v20m0 0H0m40 0H0m0 0V0m0 20V0"
+          strokeWidth={2}
+        />
+      </g>,
+      { width: 40, height: 20 },
+    ),
+);
+
+export const FillSolidIcon = React.memo(
+  ({ appearance }: { appearance: "light" | "dark" }) =>
+    createIcon(
+      <>
+        <path d="M0 0h120v60H0" strokeWidth={0} />
+        <path
+          d="M0 0h40M0 0h40m0 0v20m0-20v20m0 0H0m40 0H0m0 0V0m0 20V0"
+          stroke={iconFillColor(appearance)}
+          strokeWidth={2}
+          fill="none"
+        />
+      </>,
+      { width: 40, height: 20 },
+    ),
+);
+
+export const StrokeWidthIcon = React.memo(
+  ({
+    appearance,
+    strokeWidth,
+  }: {
+    appearance: "light" | "dark";
+    strokeWidth: number;
+  }) =>
+    createIcon(
+      <path
+        d="M0 10h40M0 10h40"
+        stroke={iconFillColor(appearance)}
+        strokeWidth={strokeWidth}
+        fill="none"
+      />,
+      { width: 40, height: 20 },
+    ),
+);
+
+export const StrokeStyleSolidIcon = React.memo(
+  ({ appearance }: { appearance: "light" | "dark" }) =>
+    createIcon(
+      <path
+        d="M0 10h40M0 10h40"
+        stroke={iconFillColor(appearance)}
+        strokeWidth={2}
+        fill="none"
+      />,
+      { width: 40, height: 20 },
+    ),
+);
+
+export const StrokeStyleDashedIcon = React.memo(
+  ({ appearance }: { appearance: "light" | "dark" }) =>
+    createIcon(
+      <path
+        d="M3.286 9.998h32.759"
+        stroke={iconFillColor(appearance)}
+        strokeWidth={2.5}
+        fill="none"
+        strokeDasharray="12 8"
+      />,
+      { width: 40, height: 20 },
+    ),
+);
+
+export const StrokeStyleDottedIcon = React.memo(
+  ({ appearance }: { appearance: "light" | "dark" }) =>
+    createIcon(
+      <path
+        d="M0 10h40M0 10h40"
+        stroke={iconFillColor(appearance)}
+        strokeWidth={2}
+        fill="none"
+        strokeDasharray="3 6"
+      />,
+      { width: 40, height: 20 },
+    ),
+);
+
+export const SloppinessArchitectIcon = React.memo(
+  ({ appearance }: { appearance: "light" | "dark" }) =>
+    createIcon(
+      <path
+        d="M.268 17.938C4.05 15.093 19.414.725 22.96.868c3.547.143-4.149 16.266-1.41 17.928 2.738 1.662 14.866-6.632 17.84-7.958m-39.123 7.1C4.05 15.093 19.414.725 22.96.868c3.547.143-4.149 16.266-1.41 17.928 2.738 1.662 14.866-6.632 17.84-7.958"
+        stroke={iconFillColor(appearance)}
+        strokeWidth={2}
+        fill="none"
+      />,
+      { width: 40, height: 20 },
+    ),
+);
+
+export const SloppinessArtistIcon = React.memo(
+  ({ appearance }: { appearance: "light" | "dark" }) =>
+    createIcon(
+      <path
+        d="M2.663 18.134c3.963-2.578 18.855-12.098 22.675-12.68 3.82-.58-1.966 8.367.242 9.196 2.209.828 10.649-3.14 13.01-4.224M7.037 15.474c4.013-2.198 14.19-14.648 17.18-14.32 2.99.329-1.749 14.286.759 16.292 2.507 2.006 12.284-2.68 14.286-4.256"
+        stroke={iconFillColor(appearance)}
+        strokeWidth={2}
+        fill="none"
+      />,
+      { width: 40, height: 20 },
+    ),
+);
+
+export const SloppinessCartoonistIcon = React.memo(
+  ({ appearance }: { appearance: "light" | "dark" }) =>
+    createIcon(
+      <>
+        <path
+          d="M1.944 17.15C6.056 14.637 22.368 1.86 26.615 2.083c4.248.223-.992 14.695.815 16.406 1.807 1.71 8.355-5.117 10.026-6.14m-35.512 4.8C6.056 14.637 22.368 1.86 26.615 2.083c4.248.223-.992 14.695.815 16.406 1.807 1.71 8.355-5.117 10.026-6.14"
+          stroke={iconFillColor(appearance)}
+          strokeWidth={2}
+          fill="none"
+        />
+        <path
+          d="M3.114 10.534c2.737-1.395 12.854-8.814 16.42-8.368 3.568.445 2.35 10.282 4.984 11.04 2.635.756 9.019-5.416 10.822-6.5M3.114 10.535c2.737-1.395 12.854-8.814 16.42-8.368 3.568.445 2.35 10.282 4.984 11.04 2.635.756 9.019-5.416 10.822-6.5"
+          stroke={iconFillColor(appearance)}
+          strokeWidth={2}
+          fill="none"
+        />
+      </>,
+      { width: 40, height: 20 },
+    ),
+);
+
+export const EdgeSharpIcon = React.memo(
+  ({ appearance }: { appearance: "light" | "dark" }) =>
+    createIcon(
+      <path
+        d="M9.18 19.68V6.346m0 13.336V6.345m0 0h29.599m-29.6 0h29.6"
+        stroke={iconFillColor(appearance)}
+        strokeWidth={2}
+        fill="none"
+      />,
+      { width: 40, height: 20 },
+    ),
+);
+
+export const EdgeRoundIcon = React.memo(
+  ({ appearance }: { appearance: "light" | "dark" }) =>
+    createIcon(
+      <path
+        d="M9.444 19.537c.484-2.119-2.1-10.449 2.904-12.71 5.004-2.263 22.601-.72 27.121-.863M9.444 19.537c.484-2.119-2.1-10.449 2.904-12.71 5.004-2.263 22.601-.72 27.121-.863"
+        stroke={iconFillColor(appearance)}
+        strokeWidth={2}
+        fill="none"
+      />,
+      { width: 40, height: 20 },
+    ),
+);

+ 16 - 0
src/css/styles.scss

@@ -172,6 +172,22 @@
     }
   }
 
+  .buttonList.buttonListIcon {
+    label {
+      display: inline-flex;
+      justify-content: center;
+      align-items: center;
+      svg {
+        width: 36px;
+        height: 18px;
+        opacity: 0.6;
+      }
+      &.active svg {
+        opacity: 1;
+      }
+    }
+  }
+
   .App-bottom-bar {
     position: absolute;
     top: 0;

+ 7 - 7
src/tests/regressionTests.test.tsx

@@ -513,18 +513,18 @@ describe("regression tests", () => {
     // select rectangle tool to show properties menu
     UI.clickTool("rectangle");
     // english lang should display `hachure` label
-    expect(screen.queryByText(/hachure/i)).not.toBeNull();
+    expect(screen.queryByTitle(/hachure/i)).not.toBeNull();
     fireEvent.change(document.querySelector(".dropdown-select__language")!, {
       target: { value: "de-DE" },
     });
     // switching to german, `hachure` label should no longer exist
-    await waitFor(() => expect(screen.queryByText(/hachure/i)).toBeNull());
+    await waitFor(() => expect(screen.queryByTitle(/hachure/i)).toBeNull());
     // reset language
     fireEvent.change(document.querySelector(".dropdown-select__language")!, {
       target: { value: "en" },
     });
     // switching back to English
-    await waitFor(() => expect(screen.queryByText(/hachure/i)).not.toBeNull());
+    await waitFor(() => expect(screen.queryByTitle(/hachure/i)).not.toBeNull());
   });
 
   it("make a group and duplicate it", () => {
@@ -877,13 +877,13 @@ describe("regression tests", () => {
     clickLabeledElement("Background");
     clickLabeledElement("#e64980");
     // Fill style
-    fireEvent.click(screen.getByLabelText("Cross-hatch"));
+    fireEvent.click(screen.getByTitle("Cross-hatch"));
     // Stroke width
-    fireEvent.click(screen.getByLabelText("Bold"));
+    fireEvent.click(screen.getByTitle("Bold"));
     // Stroke style
-    fireEvent.click(screen.getByLabelText("Dotted"));
+    fireEvent.click(screen.getByTitle("Dotted"));
     // Roughness
-    fireEvent.click(screen.getByLabelText("Cartoonist"));
+    fireEvent.click(screen.getByTitle("Cartoonist"));
     // Opacity
     fireEvent.change(screen.getByLabelText("Opacity"), {
       target: { value: "60" },