mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: implement custom Range component for opacity control (#9009)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
d29c3db7f6
commit
bd1590fc74
6 changed files with 136 additions and 21 deletions
|
@ -121,6 +121,7 @@ import {
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import type { LocalPoint } from "../../math";
|
import type { LocalPoint } from "../../math";
|
||||||
import { pointFrom } from "../../math";
|
import { pointFrom } from "../../math";
|
||||||
|
import { Range } from "../components/Range";
|
||||||
|
|
||||||
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
||||||
|
|
||||||
|
@ -630,25 +631,12 @@ export const actionChangeOpacity = register({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<label className="control-label">
|
<Range
|
||||||
{t("labels.opacity")}
|
updateData={updateData}
|
||||||
<input
|
elements={elements}
|
||||||
type="range"
|
appState={appState}
|
||||||
min="0"
|
testId="opacity"
|
||||||
max="100"
|
/>
|
||||||
step="10"
|
|
||||||
onChange={(event) => updateData(+event.target.value)}
|
|
||||||
value={
|
|
||||||
getFormValue(
|
|
||||||
elements,
|
|
||||||
appState,
|
|
||||||
(element) => element.opacity,
|
|
||||||
true,
|
|
||||||
appState.currentItemOpacity,
|
|
||||||
) ?? undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
59
packages/excalidraw/components/Range.scss
Normal file
59
packages/excalidraw/components/Range.scss
Normal file
|
@ -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
packages/excalidraw/components/Range.tsx
Normal file
65
packages/excalidraw/components/Range.tsx
Normal file
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -32,6 +32,7 @@
|
||||||
0px 0px 3.1270833015441895px 0px rgba(0, 0, 0, 0.08),
|
0px 0px 3.1270833015441895px 0px rgba(0, 0, 0, 0.08),
|
||||||
0px 7px 14px 0px rgba(0, 0, 0, 0.05);
|
0px 7px 14px 0px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
--button-bg: var(--color-surface-mid);
|
||||||
--button-hover-bg: var(--color-surface-high);
|
--button-hover-bg: var(--color-surface-high);
|
||||||
--button-active-bg: var(--color-surface-high);
|
--button-active-bg: var(--color-surface-high);
|
||||||
--button-active-border: var(--color-brand-active);
|
--button-active-border: var(--color-brand-active);
|
||||||
|
@ -171,6 +172,8 @@
|
||||||
--button-destructive-bg-color: #5a0000;
|
--button-destructive-bg-color: #5a0000;
|
||||||
--button-destructive-color: #{$oc-red-3};
|
--button-destructive-color: #{$oc-red-3};
|
||||||
|
|
||||||
|
--button-bg: var(--color-surface-high);
|
||||||
|
|
||||||
--button-gray-1: #363636;
|
--button-gray-1: #363636;
|
||||||
--button-gray-2: #272727;
|
--button-gray-2: #272727;
|
||||||
--button-gray-3: #222;
|
--button-gray-3: #222;
|
||||||
|
|
|
@ -50,7 +50,7 @@ describe("actionStyles", () => {
|
||||||
// Roughness
|
// Roughness
|
||||||
fireEvent.click(screen.getByTitle("Cartoonist"));
|
fireEvent.click(screen.getByTitle("Cartoonist"));
|
||||||
// Opacity
|
// Opacity
|
||||||
fireEvent.change(screen.getByLabelText("Opacity"), {
|
fireEvent.change(screen.getByTestId("opacity"), {
|
||||||
target: { value: "60" },
|
target: { value: "60" },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -338,7 +338,7 @@ describe("contextMenu element", () => {
|
||||||
// Roughness
|
// Roughness
|
||||||
fireEvent.click(screen.getByTitle("Cartoonist"));
|
fireEvent.click(screen.getByTitle("Cartoonist"));
|
||||||
// Opacity
|
// Opacity
|
||||||
fireEvent.change(screen.getByLabelText("Opacity"), {
|
fireEvent.change(screen.getByTestId("opacity"), {
|
||||||
target: { value: "60" },
|
target: { value: "60" },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue