Browse Source

feat: implement custom Range component for opacity control (#9009)

Co-authored-by: dwelle <[email protected]>
Saikat Das 6 months ago
parent
commit
bd1590fc74

+ 7 - 19
packages/excalidraw/actions/actionProperties.tsx

@@ -121,6 +121,7 @@ import {
 import { LinearElementEditor } from "../element/linearElementEditor";
 import type { LocalPoint } from "../../math";
 import { pointFrom } from "../../math";
+import { Range } from "../components/Range";
 
 const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
 
@@ -630,25 +631,12 @@ export const actionChangeOpacity = register({
     };
   },
   PanelComponent: ({ elements, appState, updateData }) => (
-    <label className="control-label">
-      {t("labels.opacity")}
-      <input
-        type="range"
-        min="0"
-        max="100"
-        step="10"
-        onChange={(event) => updateData(+event.target.value)}
-        value={
-          getFormValue(
-            elements,
-            appState,
-            (element) => element.opacity,
-            true,
-            appState.currentItemOpacity,
-          ) ?? undefined
-        }
-      />
-    </label>
+    <Range
+      updateData={updateData}
+      elements={elements}
+      appState={appState}
+      testId="opacity"
+    />
   ),
 });
 

+ 59 - 0
packages/excalidraw/components/Range.scss

@@ -0,0 +1,59 @@
+@import "../css/variables.module.scss";
+
+.excalidraw {
+  --Range-track-background: var(--button-bg);
+  --Range-track-background-active: var(--color-primary);
+  --Range-thumb-background: var(--color-on-surface);
+  --Range-legend-color: var(--text-primary-color);
+
+  .range-wrapper {
+    position: relative;
+    padding-top: 10px;
+    padding-bottom: 30px;
+  }
+
+  .range-input {
+    width: 100%;
+    height: 4px;
+    -webkit-appearance: none;
+    background: var(--Range-track-background);
+    border-radius: 2px;
+    outline: none;
+  }
+
+  .range-input::-webkit-slider-thumb {
+    -webkit-appearance: none;
+    appearance: none;
+    width: 20px;
+    height: 20px;
+    background: var(--Range-thumb-background);
+    border-radius: 50%;
+    cursor: pointer;
+    border: none;
+  }
+
+  .range-input::-moz-range-thumb {
+    width: 20px;
+    height: 20px;
+    background: var(--Range-thumb-background);
+    border-radius: 50%;
+    cursor: pointer;
+    border: none;
+  }
+
+  .value-bubble {
+    position: absolute;
+    bottom: 0;
+    transform: translateX(-50%);
+    font-size: 12px;
+    color: var(--Range-legend-color);
+  }
+
+  .zero-label {
+    position: absolute;
+    bottom: 0;
+    left: 4px;
+    font-size: 12px;
+    color: var(--Range-legend-color);
+  }
+}

+ 65 - 0
packages/excalidraw/components/Range.tsx

@@ -0,0 +1,65 @@
+import React, { useEffect } from "react";
+import { getFormValue } from "../actions/actionProperties";
+import { t } from "../i18n";
+import "./Range.scss";
+
+export type RangeProps = {
+  updateData: (value: number) => void;
+  appState: any;
+  elements: any;
+  testId?: string;
+};
+
+export const Range = ({
+  updateData,
+  appState,
+  elements,
+  testId,
+}: RangeProps) => {
+  const rangeRef = React.useRef<HTMLInputElement>(null);
+  const valueRef = React.useRef<HTMLDivElement>(null);
+  const value = getFormValue(
+    elements,
+    appState,
+    (element) => element.opacity,
+    true,
+    appState.currentItemOpacity,
+  );
+  useEffect(() => {
+    if (rangeRef.current && valueRef.current) {
+      const rangeElement = rangeRef.current;
+      const valueElement = valueRef.current;
+      const inputWidth = rangeElement.offsetWidth;
+      const thumbWidth = 15; // 15 is the width of the thumb
+      const position =
+        (value / 100) * (inputWidth - thumbWidth) + thumbWidth / 2;
+      valueElement.style.left = `${position}px`;
+      rangeElement.style.background = `linear-gradient(to right, var(--color-primary) 0%, var(--color-primary) ${value}%, var(--button-bg) ${value}%, var(--button-bg) 100%)`;
+    }
+  }, [value]);
+
+  return (
+    <label className="control-label">
+      {t("labels.opacity")}
+      <div className="range-wrapper">
+        <input
+          ref={rangeRef}
+          type="range"
+          min="0"
+          max="100"
+          step="10"
+          onChange={(event) => {
+            updateData(+event.target.value);
+          }}
+          value={value}
+          className="range-input"
+          data-testid={testId}
+        />
+        <div className="value-bubble" ref={valueRef}>
+          {value !== 0 ? value : null}
+        </div>
+        <div className="zero-label">0</div>
+      </div>
+    </label>
+  );
+};

+ 3 - 0
packages/excalidraw/css/theme.scss

@@ -32,6 +32,7 @@
     0px 0px 3.1270833015441895px 0px rgba(0, 0, 0, 0.08),
     0px 7px 14px 0px rgba(0, 0, 0, 0.05);
 
+  --button-bg: var(--color-surface-mid);
   --button-hover-bg: var(--color-surface-high);
   --button-active-bg: var(--color-surface-high);
   --button-active-border: var(--color-brand-active);
@@ -171,6 +172,8 @@
     --button-destructive-bg-color: #5a0000;
     --button-destructive-color: #{$oc-red-3};
 
+    --button-bg: var(--color-surface-high);
+
     --button-gray-1: #363636;
     --button-gray-2: #272727;
     --button-gray-3: #222;

+ 1 - 1
packages/excalidraw/tests/actionStyles.test.tsx

@@ -50,7 +50,7 @@ describe("actionStyles", () => {
     // Roughness
     fireEvent.click(screen.getByTitle("Cartoonist"));
     // Opacity
-    fireEvent.change(screen.getByLabelText("Opacity"), {
+    fireEvent.change(screen.getByTestId("opacity"), {
       target: { value: "60" },
     });
 

+ 1 - 1
packages/excalidraw/tests/contextmenu.test.tsx

@@ -338,7 +338,7 @@ describe("contextMenu element", () => {
     // Roughness
     fireEvent.click(screen.getByTitle("Cartoonist"));
     // Opacity
-    fireEvent.change(screen.getByLabelText("Opacity"), {
+    fireEvent.change(screen.getByTestId("opacity"), {
       target: { value: "60" },
     });