Browse Source

feat: zigzag fill easter egg (#6439)

David Luzar 2 years ago
parent
commit
e4d8ba226f

+ 57 - 35
src/actions/actionProperties.tsx

@@ -1,4 +1,5 @@
 import { AppState } from "../../src/types";
+import { trackEvent } from "../analytics";
 import { ButtonIconSelect } from "../components/ButtonIconSelect";
 import { ColorPicker } from "../components/ColorPicker";
 import { IconPicker } from "../components/IconPicker";
@@ -37,6 +38,7 @@ import {
   TextAlignLeftIcon,
   TextAlignCenterIcon,
   TextAlignRightIcon,
+  FillZigZagIcon,
 } from "../components/icons";
 import {
   DEFAULT_FONT_FAMILY,
@@ -294,7 +296,12 @@ export const actionChangeBackgroundColor = register({
 export const actionChangeFillStyle = register({
   name: "changeFillStyle",
   trackEvent: false,
-  perform: (elements, appState, value) => {
+  perform: (elements, appState, value, app) => {
+    trackEvent(
+      "element",
+      "changeFillStyle",
+      `${value} (${app.device.isMobile ? "mobile" : "desktop"})`,
+    );
     return {
       elements: changeProperty(elements, appState, (el) =>
         newElementWith(el, {
@@ -305,40 +312,55 @@ export const actionChangeFillStyle = register({
       commitToHistory: true,
     };
   },
-  PanelComponent: ({ elements, appState, updateData }) => (
-    <fieldset>
-      <legend>{t("labels.fill")}</legend>
-      <ButtonIconSelect
-        options={[
-          {
-            value: "hachure",
-            text: t("labels.hachure"),
-            icon: FillHachureIcon,
-          },
-          {
-            value: "cross-hatch",
-            text: t("labels.crossHatch"),
-            icon: FillCrossHatchIcon,
-          },
-          {
-            value: "solid",
-            text: t("labels.solid"),
-            icon: FillSolidIcon,
-          },
-        ]}
-        group="fill"
-        value={getFormValue(
-          elements,
-          appState,
-          (element) => element.fillStyle,
-          appState.currentItemFillStyle,
-        )}
-        onChange={(value) => {
-          updateData(value);
-        }}
-      />
-    </fieldset>
-  ),
+  PanelComponent: ({ elements, appState, updateData }) => {
+    const selectedElements = getSelectedElements(elements, appState);
+    const allElementsZigZag = selectedElements.every(
+      (el) => el.fillStyle === "zigzag",
+    );
+
+    return (
+      <fieldset>
+        <legend>{t("labels.fill")}</legend>
+        <ButtonIconSelect
+          type="button"
+          options={[
+            {
+              value: "hachure",
+              text: t("labels.hachure"),
+              icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
+              active: allElementsZigZag ? true : undefined,
+            },
+            {
+              value: "cross-hatch",
+              text: t("labels.crossHatch"),
+              icon: FillCrossHatchIcon,
+            },
+            {
+              value: "solid",
+              text: t("labels.solid"),
+              icon: FillSolidIcon,
+            },
+          ]}
+          value={getFormValue(
+            elements,
+            appState,
+            (element) => element.fillStyle,
+            appState.currentItemFillStyle,
+          )}
+          onClick={(value, event) => {
+            const nextValue =
+              event.altKey &&
+              value === "hachure" &&
+              selectedElements.every((el) => el.fillStyle === "hachure")
+                ? "zigzag"
+                : value;
+
+            updateData(nextValue);
+          }}
+        />
+      </fieldset>
+    );
+  },
 });
 
 export const actionChangeStrokeWidth = register({

+ 52 - 26
src/components/ButtonIconSelect.tsx

@@ -1,33 +1,59 @@
 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; testId?: string }[];
-  value: T | null;
-  onChange: (value: T) => void;
-  group: string;
-}) => (
+export const ButtonIconSelect = <T extends Object>(
+  props: {
+    options: {
+      value: T;
+      text: string;
+      icon: JSX.Element;
+      testId?: string;
+      /** if not supplied, defaults to value identity check */
+      active?: boolean;
+    }[];
+    value: T | null;
+    type?: "radio" | "button";
+  } & (
+    | { type?: "radio"; group: string; onChange: (value: T) => void }
+    | {
+        type: "button";
+        onClick: (
+          value: T,
+          event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
+        ) => void;
+      }
+  ),
+) => (
   <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}
+    {props.options.map((option) =>
+      props.type === "button" ? (
+        <button
+          key={option.text}
+          onClick={(event) => props.onClick(option.value, event)}
+          className={clsx({
+            active: option.active ?? props.value === option.value,
+          })}
           data-testid={option.testId}
-        />
-        {option.icon}
-      </label>
-    ))}
+          title={option.text}
+        >
+          {option.icon}
+        </button>
+      ) : (
+        <label
+          key={option.text}
+          className={clsx({ active: props.value === option.value })}
+          title={option.text}
+        >
+          <input
+            type="radio"
+            name={props.group}
+            onChange={() => props.onChange(option.value)}
+            checked={props.value === option.value}
+            data-testid={option.testId}
+          />
+          {option.icon}
+        </label>
+      ),
+    )}
   </div>
 );

+ 7 - 0
src/components/icons.tsx

@@ -1008,6 +1008,13 @@ export const UngroupIcon = React.memo(({ theme }: { theme: Theme }) =>
   ),
 );
 
+export const FillZigZagIcon = createIcon(
+  <g strokeWidth={1.25}>
+    <path d="M5.879 2.625h8.242a3.27 3.27 0 0 1 3.254 3.254v8.242a3.27 3.27 0 0 1-3.254 3.254H5.88a3.27 3.27 0 0 1-3.254-3.254V5.88A3.27 3.27 0 0 1 5.88 2.626l-.001-.001ZM4.518 16.118l7.608-12.83m.198 13.934 5.051-9.897M2.778 9.675l9.348-6.387m-7.608 12.83 12.857-8.793" />
+  </g>,
+  modifiedTablerIconProps,
+);
+
 export const FillHachureIcon = createIcon(
   <>
     <path

+ 3 - 0
src/css/styles.scss

@@ -155,6 +155,9 @@
     margin: 1px;
   }
 
+  .welcome-screen-menu-item:focus-visible,
+  .dropdown-menu-item:focus-visible,
+  button:focus-visible,
   .buttonList label:focus-within,
   input:focus-visible {
     outline: transparent;

+ 1 - 1
src/element/types.ts

@@ -9,7 +9,7 @@ import {
 import { MarkNonNullable, ValueOf } from "../utility-types";
 
 export type ChartType = "bar" | "line";
-export type FillStyle = "hachure" | "cross-hatch" | "solid";
+export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag";
 export type FontFamilyKeys = keyof typeof FONT_FAMILY;
 export type FontFamilyValues = typeof FONT_FAMILY[FontFamilyKeys];
 export type Theme = typeof THEME[keyof typeof THEME];