Wrote color values component.
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"build:wasm": "wasm-pack build colorlib -t bundler -d pkg --release",
|
"build:wasm": "wasm-pack build colorlib -t bundler -d pkg --release",
|
||||||
|
"check": "tsc --noEmit -p tsconfig.app.json",
|
||||||
"clean": "rm -rf dist colorlib/pkg*",
|
"clean": "rm -rf dist colorlib/pkg*",
|
||||||
"cypress:open": "1>/dev/null 2>/dev/null cypress open -d &",
|
"cypress:open": "1>/dev/null 2>/dev/null cypress open -d &",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
+8
-4
@@ -1,12 +1,12 @@
|
|||||||
import { useState } from "react";
|
import { useReducer, useState } from "react";
|
||||||
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import ColorPicker from "@components/ColorPicker/ColorPicker";
|
import ColorPicker from "@components/ColorPicker/ColorPicker";
|
||||||
import ColorValues from "@components/ColorValues/ColorValues";
|
import ColorValues from "@components/ColorValues/ColorValues";
|
||||||
import { LeftMenu, RightMenu } from "@components/SideMenu";
|
import { LeftMenu, RightMenu } from "@components/SideMenu";
|
||||||
|
|
||||||
import { useMediaQuery } from "@providers/hooks";
|
import { useMediaQuery } from "@providers/hooks";
|
||||||
|
import { useSelectedColor } from "@providers/hooks";
|
||||||
|
|
||||||
import styles from "./App.module.css";
|
import styles from "./App.module.css";
|
||||||
|
|
||||||
@@ -90,6 +90,8 @@ function MobileRightNav({ onClick, isOpen }: MenuButtonProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function MobileFirstZone() {
|
function MobileFirstZone() {
|
||||||
|
const { selectedColor, selectedColorActions } = useSelectedColor();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.mobileFirstZone} aria-label="Color tools">
|
<section className={styles.mobileFirstZone} aria-label="Color tools">
|
||||||
<div
|
<div
|
||||||
@@ -112,7 +114,7 @@ function MobileFirstZone() {
|
|||||||
aria-roledescription="slide"
|
aria-roledescription="slide"
|
||||||
aria-label="Color values"
|
aria-label="Color values"
|
||||||
>
|
>
|
||||||
<ColorValues />
|
<ColorValues color={selectedColor} actions={selectedColorActions} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -190,6 +192,8 @@ function MobileContent({
|
|||||||
// Desktop Layout Components
|
// Desktop Layout Components
|
||||||
|
|
||||||
function FirstZone() {
|
function FirstZone() {
|
||||||
|
const { selectedColor, selectedColorActions } = useSelectedColor();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.firstZone} aria-label="Color tools">
|
<section className={styles.firstZone} aria-label="Color tools">
|
||||||
<div className={styles.colorPickerWrapper} aria-label="Color picker">
|
<div className={styles.colorPickerWrapper} aria-label="Color picker">
|
||||||
@@ -197,7 +201,7 @@ function FirstZone() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.colorValuesWrapper} aria-label="Color values">
|
<div className={styles.colorValuesWrapper} aria-label="Color values">
|
||||||
<ColorValues />
|
<ColorValues color={selectedColor} actions={selectedColorActions} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,29 +1,6 @@
|
|||||||
.container {
|
.colorValuesWrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
.valueItem {
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Component Editor */
|
|
||||||
|
|
||||||
.componentWrapper {
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Large - Landscape Tablets / Desktops */
|
|
||||||
/* Medium - Portrait Tablets */
|
|
||||||
/* Horizontal layout, vertically scrolling picker and palette content */
|
|
||||||
@media (min-width: 992px),
|
|
||||||
(min-width: 568px) and (max-width: 991px) and (orientation: portrait) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Medium - Landscape Phones */
|
|
||||||
/* Horizontal layout, side menu, vertical tabbed picker */
|
|
||||||
@media (min-width: 568px) and (max-width: 991px) and (orientation: landscape) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Small - Portrait Phones*/
|
|
||||||
/* Vertical layout, side menu, horizontal tabbed picker */
|
|
||||||
@media (max-width: 567px) {
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,75 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import type { KeyboardEvent } from "react";
|
||||||
|
|
||||||
import * as colorlib from "colorlib";
|
import * as colorlib from "colorlib";
|
||||||
|
|
||||||
|
import type { ColorActions } from "@hooks/color";
|
||||||
|
|
||||||
import styles from "./ColorValues.module.css";
|
import styles from "./ColorValues.module.css";
|
||||||
import SpaceEditor from "./SpaceEditor";
|
import SpaceEditor from "./SpaceEditor";
|
||||||
|
import { HexEditor } from "./ValueEditor";
|
||||||
|
|
||||||
function ColorValues({ selectedColor }: { selectedColor: colorlib.Color }) {
|
function ColorValues({
|
||||||
return <div className={styles.wrapper}></div>;
|
color,
|
||||||
|
actions,
|
||||||
|
}: {
|
||||||
|
color: colorlib.Color;
|
||||||
|
actions: ColorActions;
|
||||||
|
}) {
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
// Cycle through inputs on Enter / Shift+Enter
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const wrapper = wrapperRef.current;
|
||||||
|
if (!wrapper) return;
|
||||||
|
|
||||||
|
const inputs = Array.from(wrapper.querySelectorAll("input"));
|
||||||
|
const currentIndex = inputs.indexOf(e.target as HTMLInputElement);
|
||||||
|
|
||||||
|
if (currentIndex === -1) return;
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
// Go to previous input
|
||||||
|
const prevIndex = (currentIndex - 1 + inputs.length) % inputs.length;
|
||||||
|
inputs[prevIndex]?.focus();
|
||||||
|
} else {
|
||||||
|
// Go to next input
|
||||||
|
const nextIndex = (currentIndex + 1) % inputs.length;
|
||||||
|
inputs[nextIndex]?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.colorValuesWrapper} ref={wrapperRef}>
|
||||||
|
<SpaceEditor
|
||||||
|
space="HCL"
|
||||||
|
color={color.hcl}
|
||||||
|
actions={actions.hcl}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
<SpaceEditor
|
||||||
|
space="HSV"
|
||||||
|
color={color.hsv}
|
||||||
|
actions={actions.hsv}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
<SpaceEditor
|
||||||
|
space="RGB"
|
||||||
|
color={color.rgb}
|
||||||
|
actions={actions.rgb}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
<HexEditor
|
||||||
|
color={color.hex}
|
||||||
|
actions={actions.hex}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ColorValues;
|
export default ColorValues;
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { useReducer } from "react";
|
||||||
|
|
||||||
|
import { Color } from "colorlib";
|
||||||
|
|
||||||
|
import { colorReducer, createColorActions } from "@hooks/color";
|
||||||
|
|
||||||
|
import ColorValues from "./ColorValues";
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
color: Color.from_hex("2edd9d"),
|
||||||
|
};
|
||||||
|
|
||||||
|
function TestWrapper() {
|
||||||
|
const [state, dispatch] = useReducer(colorReducer, initialState);
|
||||||
|
const actions = createColorActions(dispatch);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "75vh",
|
||||||
|
width: "100%",
|
||||||
|
marginBottom: 35,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ColorValues color={state.color} actions={actions} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: 40,
|
||||||
|
backgroundColor: `rgb(${Math.round(state.color.rgb.r)},${Math.round(state.color.rgb.g)},${Math.round(state.color.rgb.b)})`,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("color values component tests", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.mount(<TestWrapper />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can cycle through inputs with Enter", () => {
|
||||||
|
cy.dataCy("HSV-editor").within(() => {
|
||||||
|
cy.dataCy("S-value-input").focus();
|
||||||
|
cy.dataCy("S-value-input").type("{enter}");
|
||||||
|
cy.dataCy("V-value-input").should("have.focus");
|
||||||
|
cy.dataCy("V-value-input").type("{enter}");
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.dataCy("RGB-editor").within(() => {
|
||||||
|
cy.dataCy("R-value-input").should("have.focus");
|
||||||
|
|
||||||
|
cy.dataCy("B-value-input").focus();
|
||||||
|
cy.dataCy("B-value-input").type("{enter}");
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.dataCy("hex-value-input").should("have.focus");
|
||||||
|
cy.dataCy("hex-value-input").type("{enter}");
|
||||||
|
|
||||||
|
cy.dataCy("HCL-editor").within(() => {
|
||||||
|
cy.dataCy("H-value-input").should("have.focus");
|
||||||
|
cy.dataCy("H-value-input").type("{shift}{enter}");
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.dataCy("hex-value-input").should("have.focus");
|
||||||
|
cy.dataCy("hex-value-input").type("{shift}{enter}");
|
||||||
|
|
||||||
|
cy.dataCy("RGB-editor").within(() => {
|
||||||
|
cy.dataCy("B-value-input").should("have.focus");
|
||||||
|
cy.dataCy("B-value-input").type("{esc}");
|
||||||
|
cy.dataCy("B-value-input").should("not.have.focus");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can change color values", () => {
|
||||||
|
cy.dataCy("RGB-editor").within(() => {
|
||||||
|
cy.dataCy("R-value-input").type("120");
|
||||||
|
cy.dataCy("G-value-input").type("60");
|
||||||
|
cy.dataCy("B-value-input").type("220");
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.dataCy("hex-value-input").should("have.value", "#783CDC");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
import { useReducer } from "react";
|
|
||||||
|
|
||||||
import { Color } from "colorlib";
|
|
||||||
|
|
||||||
import { roundTo } from "@/util";
|
|
||||||
import { colorReducer, createColorActions } from "@hooks/color";
|
|
||||||
|
|
||||||
import SpaceEditor from "./SpaceEditor";
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
color: Color.from_hex("2edd9d"),
|
|
||||||
};
|
|
||||||
|
|
||||||
function TestWrapper() {
|
|
||||||
const [state, dispatch] = useReducer(colorReducer, initialState);
|
|
||||||
const actions = createColorActions(dispatch);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: 300,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "20px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SpaceEditor
|
|
||||||
space="HCL"
|
|
||||||
color={state.color.hcl}
|
|
||||||
actions={actions.hcl}
|
|
||||||
/>
|
|
||||||
<SpaceEditor
|
|
||||||
space="HSV"
|
|
||||||
color={state.color.hsv}
|
|
||||||
actions={actions.hsv}
|
|
||||||
/>
|
|
||||||
<SpaceEditor
|
|
||||||
space="RGB"
|
|
||||||
color={state.color.rgb}
|
|
||||||
actions={actions.rgb}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ fontFamily: "monospace" }}>
|
|
||||||
<p data-cy="hcl-value">
|
|
||||||
HCL ({roundTo(state.color.hcl.h, 0)}, {roundTo(state.color.hcl.c, 2)},{" "}
|
|
||||||
{roundTo(state.color.hcl.l, 2)})
|
|
||||||
</p>
|
|
||||||
<p data-cy="hsv-value">
|
|
||||||
HSV ({roundTo(state.color.hsv.h, 0)}, {roundTo(state.color.hsv.s, 2)},{" "}
|
|
||||||
{roundTo(state.color.hsv.v, 2)})
|
|
||||||
</p>
|
|
||||||
<p data-cy="rgb-value">
|
|
||||||
RGB ({roundTo(state.color.rgb.r, 0)}, {roundTo(state.color.rgb.g, 0)},{" "}
|
|
||||||
{roundTo(state.color.rgb.b, 0)})
|
|
||||||
</p>
|
|
||||||
<p data-cy="hex-value">HEX: #{state.color.hex.to_code()}</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("space editor tests", () => {
|
|
||||||
it("can edit color values", () => {
|
|
||||||
cy.mount(<TestWrapper />);
|
|
||||||
|
|
||||||
// Confirm initial values
|
|
||||||
cy.dataCy("RGB-editor").within(() => {
|
|
||||||
cy.dataCy("R-value-input").should("have.value", 46);
|
|
||||||
cy.dataCy("G-value-input").should("have.value", 221);
|
|
||||||
cy.dataCy("B-value-input").should("have.value", 157);
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.dataCy("HSV-editor").within(() => {
|
|
||||||
cy.dataCy("H-value-input").should("have.value", 158);
|
|
||||||
cy.dataCy("S-value-input").should("have.value", 79);
|
|
||||||
cy.dataCy("V-value-input").should("have.value", 87);
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.dataCy("HCL-editor").within(() => {
|
|
||||||
cy.dataCy("H-value-input").should("have.value", 158);
|
|
||||||
cy.dataCy("C-value-input").should("have.value", 79);
|
|
||||||
cy.dataCy("L-value-input").should("have.value", 70);
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.dataCy("rgb-value").should("have.text", "RGB (46, 221, 157)");
|
|
||||||
cy.dataCy("hsv-value").should("have.text", "HSV (158, 0.79, 0.87)");
|
|
||||||
cy.dataCy("hcl-value").should("have.text", "HCL (158, 0.79, 0.7)");
|
|
||||||
cy.dataCy("hex-value").should("have.text", "HEX: #2EDD9D");
|
|
||||||
|
|
||||||
// Update the color values
|
|
||||||
cy.wait(50); // ensure render
|
|
||||||
cy.dataCy("HCL-editor").within(() => {
|
|
||||||
cy.dataCy("H-slider").click();
|
|
||||||
cy.dataCy("C-decrement-button").click();
|
|
||||||
cy.dataCy("L-value-input").type("25");
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.dataCy("rgb-value").should("have.text", "RGB (17, 76, 74)");
|
|
||||||
cy.dataCy("hsv-value").should("have.text", "HSV (179, 0.78, 0.3)");
|
|
||||||
cy.dataCy("hcl-value").should("have.text", "HCL (179, 0.78, 0.25)");
|
|
||||||
cy.dataCy("hex-value").should("have.text", "HEX: #104B4A");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -7,23 +7,33 @@ import type {
|
|||||||
} from "@hooks/color";
|
} from "@hooks/color";
|
||||||
|
|
||||||
import styles from "./SpaceEditor.module.css";
|
import styles from "./SpaceEditor.module.css";
|
||||||
import ValueEditor from "./ValueEditor";
|
import { ValueEditor } from "./ValueEditor";
|
||||||
|
|
||||||
type SpaceEditorProps =
|
type ColorSpaceProps =
|
||||||
| { space: "RGB"; color: colorlib.RGB; actions: RGBColorActions }
|
| { space: "RGB"; color: colorlib.RGB; actions: RGBColorActions }
|
||||||
| { space: "HSV"; color: colorlib.HSV; actions: HSVColorActions }
|
| { space: "HSV"; color: colorlib.HSV; actions: HSVColorActions }
|
||||||
| { space: "HCL"; color: colorlib.HCL; actions: HCLColorActions };
|
| { space: "HCL"; color: colorlib.HCL; actions: HCLColorActions };
|
||||||
|
|
||||||
function SpaceEditor({ space, color, actions }: SpaceEditorProps) {
|
type SpaceEditorProps = ColorSpaceProps & {
|
||||||
|
onKeyDown?: (e: React.KeyboardEvent) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SpaceEditor({ space, color, actions, onKeyDown }: SpaceEditorProps) {
|
||||||
switch (space) {
|
switch (space) {
|
||||||
case "RGB":
|
case "RGB":
|
||||||
return <RGBSpaceEditor color={color} actions={actions} />;
|
return (
|
||||||
|
<RGBSpaceEditor color={color} actions={actions} onKeyDown={onKeyDown} />
|
||||||
|
);
|
||||||
|
|
||||||
case "HSV":
|
case "HSV":
|
||||||
return <HSVSpaceEditor color={color} actions={actions} />;
|
return (
|
||||||
|
<HSVSpaceEditor color={color} actions={actions} onKeyDown={onKeyDown} />
|
||||||
|
);
|
||||||
|
|
||||||
case "HCL":
|
case "HCL":
|
||||||
return <HCLSpaceEditor color={color} actions={actions} />;
|
return (
|
||||||
|
<HCLSpaceEditor color={color} actions={actions} onKeyDown={onKeyDown} />
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
@@ -68,9 +78,11 @@ const COLOR_SPACES = {
|
|||||||
function RGBSpaceEditor({
|
function RGBSpaceEditor({
|
||||||
color,
|
color,
|
||||||
actions,
|
actions,
|
||||||
|
onKeyDown,
|
||||||
}: {
|
}: {
|
||||||
color: colorlib.RGB;
|
color: colorlib.RGB;
|
||||||
actions: RGBColorActions;
|
actions: RGBColorActions;
|
||||||
|
onKeyDown?: (e: React.KeyboardEvent) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div data-cy="RGB-editor" className={styles.spaceWrapper}>
|
<div data-cy="RGB-editor" className={styles.spaceWrapper}>
|
||||||
@@ -79,18 +91,21 @@ function RGBSpaceEditor({
|
|||||||
valueRange={COLOR_SPACES.RGB.ranges.r}
|
valueRange={COLOR_SPACES.RGB.ranges.r}
|
||||||
value={color.r}
|
value={color.r}
|
||||||
setValue={actions.setR}
|
setValue={actions.setR}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
/>
|
/>
|
||||||
<ValueEditor
|
<ValueEditor
|
||||||
componentSymbol={COLOR_SPACES.RGB.symbols.g}
|
componentSymbol={COLOR_SPACES.RGB.symbols.g}
|
||||||
valueRange={COLOR_SPACES.RGB.ranges.g}
|
valueRange={COLOR_SPACES.RGB.ranges.g}
|
||||||
value={color.g}
|
value={color.g}
|
||||||
setValue={actions.setG}
|
setValue={actions.setG}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
/>
|
/>
|
||||||
<ValueEditor
|
<ValueEditor
|
||||||
componentSymbol={COLOR_SPACES.RGB.symbols.b}
|
componentSymbol={COLOR_SPACES.RGB.symbols.b}
|
||||||
valueRange={COLOR_SPACES.RGB.ranges.b}
|
valueRange={COLOR_SPACES.RGB.ranges.b}
|
||||||
value={color.b}
|
value={color.b}
|
||||||
setValue={actions.setB}
|
setValue={actions.setB}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -99,9 +114,11 @@ function RGBSpaceEditor({
|
|||||||
function HSVSpaceEditor({
|
function HSVSpaceEditor({
|
||||||
color,
|
color,
|
||||||
actions,
|
actions,
|
||||||
|
onKeyDown,
|
||||||
}: {
|
}: {
|
||||||
color: colorlib.HSV;
|
color: colorlib.HSV;
|
||||||
actions: HSVColorActions;
|
actions: HSVColorActions;
|
||||||
|
onKeyDown?: (e: React.KeyboardEvent) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div data-cy="HSV-editor" className={styles.spaceWrapper}>
|
<div data-cy="HSV-editor" className={styles.spaceWrapper}>
|
||||||
@@ -110,12 +127,14 @@ function HSVSpaceEditor({
|
|||||||
valueRange={COLOR_SPACES.HSV.ranges.h}
|
valueRange={COLOR_SPACES.HSV.ranges.h}
|
||||||
value={color.h}
|
value={color.h}
|
||||||
setValue={actions.setH}
|
setValue={actions.setH}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
/>
|
/>
|
||||||
<ValueEditor
|
<ValueEditor
|
||||||
componentSymbol={COLOR_SPACES.HSV.symbols.s}
|
componentSymbol={COLOR_SPACES.HSV.symbols.s}
|
||||||
valueRange={COLOR_SPACES.HSV.ranges.s}
|
valueRange={COLOR_SPACES.HSV.ranges.s}
|
||||||
value={color.s}
|
value={color.s}
|
||||||
setValue={actions.setS}
|
setValue={actions.setS}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
scale={COLOR_SPACES.HSV.scales.s}
|
scale={COLOR_SPACES.HSV.scales.s}
|
||||||
/>
|
/>
|
||||||
<ValueEditor
|
<ValueEditor
|
||||||
@@ -123,6 +142,7 @@ function HSVSpaceEditor({
|
|||||||
valueRange={COLOR_SPACES.HSV.ranges.v}
|
valueRange={COLOR_SPACES.HSV.ranges.v}
|
||||||
value={color.v}
|
value={color.v}
|
||||||
setValue={actions.setV}
|
setValue={actions.setV}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
scale={COLOR_SPACES.HSV.scales.v}
|
scale={COLOR_SPACES.HSV.scales.v}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,9 +152,11 @@ function HSVSpaceEditor({
|
|||||||
function HCLSpaceEditor({
|
function HCLSpaceEditor({
|
||||||
color,
|
color,
|
||||||
actions,
|
actions,
|
||||||
|
onKeyDown,
|
||||||
}: {
|
}: {
|
||||||
color: colorlib.HCL;
|
color: colorlib.HCL;
|
||||||
actions: HCLColorActions;
|
actions: HCLColorActions;
|
||||||
|
onKeyDown?: (e: React.KeyboardEvent) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div data-cy="HCL-editor" className={styles.spaceWrapper}>
|
<div data-cy="HCL-editor" className={styles.spaceWrapper}>
|
||||||
@@ -143,12 +165,14 @@ function HCLSpaceEditor({
|
|||||||
valueRange={COLOR_SPACES.HCL.ranges.h}
|
valueRange={COLOR_SPACES.HCL.ranges.h}
|
||||||
value={color.h}
|
value={color.h}
|
||||||
setValue={actions.setH}
|
setValue={actions.setH}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
/>
|
/>
|
||||||
<ValueEditor
|
<ValueEditor
|
||||||
componentSymbol={COLOR_SPACES.HCL.symbols.c}
|
componentSymbol={COLOR_SPACES.HCL.symbols.c}
|
||||||
valueRange={COLOR_SPACES.HCL.ranges.c}
|
valueRange={COLOR_SPACES.HCL.ranges.c}
|
||||||
value={color.c}
|
value={color.c}
|
||||||
setValue={actions.setC}
|
setValue={actions.setC}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
scale={COLOR_SPACES.HCL.scales.c}
|
scale={COLOR_SPACES.HCL.scales.c}
|
||||||
/>
|
/>
|
||||||
<ValueEditor
|
<ValueEditor
|
||||||
@@ -156,6 +180,7 @@ function HCLSpaceEditor({
|
|||||||
valueRange={COLOR_SPACES.HCL.ranges.l}
|
valueRange={COLOR_SPACES.HCL.ranges.l}
|
||||||
value={color.l}
|
value={color.l}
|
||||||
setValue={actions.setL}
|
setValue={actions.setL}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
scale={COLOR_SPACES.HCL.scales.l}
|
scale={COLOR_SPACES.HCL.scales.l}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 0;
|
height: 25px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
@@ -85,3 +85,34 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hexEditor {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
font-family: monospace;
|
||||||
|
border: 1px solid black;
|
||||||
|
height: 25px;
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hexLabel {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hexValueWrapper {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hexValueWrapper input {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0 5px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import type { CSSProperties, ChangeEvent, RefObject } from "react";
|
import type { ChangeEvent, KeyboardEvent, RefObject } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
@@ -8,29 +8,37 @@ import {
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import * as colorlib from "colorlib";
|
||||||
|
|
||||||
import type { CartesianSpace, Range, Setter, Timeout } from "@/types";
|
import type { CartesianSpace, Range, Setter, Timeout } from "@/types";
|
||||||
import { Direction } from "@/types";
|
import { Direction } from "@/types";
|
||||||
import { minmax, setMeasurements, valueToPosition } from "@/util";
|
import { minmax, setMeasurements, valueToPosition } from "@/util";
|
||||||
|
import type { HexColorActions } from "@hooks/color";
|
||||||
import { useScroll } from "@hooks/scroll";
|
import { useScroll } from "@hooks/scroll";
|
||||||
import { useSlider } from "@hooks/slider";
|
import { useSlider } from "@hooks/slider";
|
||||||
import { useResize } from "@hooks/window";
|
import { useResize } from "@hooks/window";
|
||||||
|
|
||||||
import styles from "./ValueEditor.module.css";
|
import styles from "./ValueEditor.module.css";
|
||||||
|
|
||||||
|
// ------------ //
|
||||||
|
// Value Editor //
|
||||||
|
// ------------ //
|
||||||
|
|
||||||
// Component
|
// Component
|
||||||
function ValueEditor({
|
export function ValueEditor({
|
||||||
componentSymbol,
|
componentSymbol,
|
||||||
valueRange,
|
valueRange,
|
||||||
value,
|
value,
|
||||||
setValue,
|
setValue,
|
||||||
scale = 1,
|
scale = 1,
|
||||||
|
onKeyDown,
|
||||||
}: {
|
}: {
|
||||||
componentSymbol: string;
|
componentSymbol: string;
|
||||||
valueRange: Range;
|
valueRange: Range;
|
||||||
value: number;
|
value: number;
|
||||||
setValue: Setter<number>;
|
setValue: Setter<number>;
|
||||||
scale?: number;
|
scale?: number;
|
||||||
|
onKeyDown?: (e: React.KeyboardEvent) => void;
|
||||||
}) {
|
}) {
|
||||||
// Set up component state
|
// Set up component state
|
||||||
const direction = Direction.HORIZONTAL;
|
const direction = Direction.HORIZONTAL;
|
||||||
@@ -100,12 +108,14 @@ function ValueEditor({
|
|||||||
position={position}
|
position={position}
|
||||||
dimensions={dimensions}
|
dimensions={dimensions}
|
||||||
componentSymbol={componentSymbol}
|
componentSymbol={componentSymbol}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
direction="decrease"
|
direction="decrease"
|
||||||
handleValueStep={handleValueStep}
|
handleValueStep={handleValueStep}
|
||||||
componentSymbol={componentSymbol}
|
componentSymbol={componentSymbol}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Value
|
<Value
|
||||||
@@ -115,12 +125,14 @@ function ValueEditor({
|
|||||||
componentSymbol={componentSymbol}
|
componentSymbol={componentSymbol}
|
||||||
handleValueStep={handleValueStep}
|
handleValueStep={handleValueStep}
|
||||||
scale={scale}
|
scale={scale}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
direction="increase"
|
direction="increase"
|
||||||
handleValueStep={handleValueStep}
|
handleValueStep={handleValueStep}
|
||||||
componentSymbol={componentSymbol}
|
componentSymbol={componentSymbol}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -144,12 +156,22 @@ function Slider({
|
|||||||
position,
|
position,
|
||||||
dimensions,
|
dimensions,
|
||||||
componentSymbol,
|
componentSymbol,
|
||||||
|
onKeyDown,
|
||||||
}: {
|
}: {
|
||||||
sliderRef: RefObject<HTMLDivElement | null>;
|
sliderRef: RefObject<HTMLDivElement | null>;
|
||||||
position: number;
|
position: number;
|
||||||
dimensions: CartesianSpace;
|
dimensions: CartesianSpace;
|
||||||
componentSymbol: string;
|
componentSymbol: string;
|
||||||
|
onKeyDown?: (e: React.KeyboardEvent) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
(e.target as HTMLElement).blur();
|
||||||
|
} else if (onKeyDown) {
|
||||||
|
onKeyDown(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.section, styles.sliderSection)}>
|
<div className={clsx(styles.section, styles.sliderSection)}>
|
||||||
<div
|
<div
|
||||||
@@ -162,6 +184,7 @@ function Slider({
|
|||||||
aria-labelledby={`${componentSymbol}-label`}
|
aria-labelledby={`${componentSymbol}-label`}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
data-cy={`${componentSymbol}-slider`}
|
data-cy={`${componentSymbol}-slider`}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={styles.sliderBar}
|
className={styles.sliderBar}
|
||||||
@@ -177,10 +200,12 @@ function Button({
|
|||||||
direction,
|
direction,
|
||||||
componentSymbol,
|
componentSymbol,
|
||||||
handleValueStep,
|
handleValueStep,
|
||||||
|
onKeyDown,
|
||||||
}: {
|
}: {
|
||||||
direction: "increase" | "decrease";
|
direction: "increase" | "decrease";
|
||||||
componentSymbol: string;
|
componentSymbol: string;
|
||||||
handleValueStep: (step: number) => void;
|
handleValueStep: (step: number) => void;
|
||||||
|
onKeyDown?: (e: React.KeyboardEvent) => void;
|
||||||
}) {
|
}) {
|
||||||
const isIncrease = direction === "increase";
|
const isIncrease = direction === "increase";
|
||||||
const label = isIncrease ? "Increase" : "Decrease";
|
const label = isIncrease ? "Increase" : "Decrease";
|
||||||
@@ -191,6 +216,14 @@ function Button({
|
|||||||
const onClick = () => handleValueStep(step);
|
const onClick = () => handleValueStep(step);
|
||||||
const longPressProps = useLongPressRepeat(onClick);
|
const longPressProps = useLongPressRepeat(onClick);
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
(e.target as HTMLElement).blur();
|
||||||
|
} else if (onKeyDown) {
|
||||||
|
onKeyDown(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.section, styles.buttonWrapper)}>
|
<div className={clsx(styles.section, styles.buttonWrapper)}>
|
||||||
<button
|
<button
|
||||||
@@ -199,6 +232,7 @@ function Button({
|
|||||||
{...longPressProps}
|
{...longPressProps}
|
||||||
aria-label={`${label} ${componentSymbol}`}
|
aria-label={`${label} ${componentSymbol}`}
|
||||||
data-cy={dataCy}
|
data-cy={dataCy}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={icon} transform="shrink-2 down-1" />
|
<FontAwesomeIcon icon={icon} transform="shrink-2 down-1" />
|
||||||
</button>
|
</button>
|
||||||
@@ -213,6 +247,7 @@ function Value({
|
|||||||
componentSymbol,
|
componentSymbol,
|
||||||
handleValueStep,
|
handleValueStep,
|
||||||
scale,
|
scale,
|
||||||
|
onKeyDown,
|
||||||
}: {
|
}: {
|
||||||
value: number;
|
value: number;
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||||
@@ -220,6 +255,7 @@ function Value({
|
|||||||
componentSymbol: string;
|
componentSymbol: string;
|
||||||
handleValueStep: (step: number) => void;
|
handleValueStep: (step: number) => void;
|
||||||
scale: number;
|
scale: number;
|
||||||
|
onKeyDown?: (e: React.KeyboardEvent) => void;
|
||||||
}) {
|
}) {
|
||||||
const valueRef = useRef(null);
|
const valueRef = useRef(null);
|
||||||
const valueScroller = useScroll({
|
const valueScroller = useScroll({
|
||||||
@@ -228,6 +264,14 @@ function Value({
|
|||||||
onScrollDown: () => handleValueStep(-1),
|
onScrollDown: () => handleValueStep(-1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
(e.target as HTMLElement).blur();
|
||||||
|
} else if (onKeyDown) {
|
||||||
|
onKeyDown(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (valueRef.current) {
|
if (valueRef.current) {
|
||||||
valueScroller.addScrollListener();
|
valueScroller.addScrollListener();
|
||||||
@@ -252,6 +296,7 @@ function Value({
|
|||||||
aria-valuemax={valueRange.max}
|
aria-valuemax={valueRange.max}
|
||||||
aria-valuenow={value}
|
aria-valuenow={value}
|
||||||
data-cy={`${componentSymbol}-value-input`}
|
data-cy={`${componentSymbol}-value-input`}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -298,4 +343,88 @@ function useLongPressRepeat(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ValueEditor;
|
// ---------- //
|
||||||
|
// Hex Editor //
|
||||||
|
// ---------- //
|
||||||
|
|
||||||
|
const extractHexValue = (value: string): string | null => {
|
||||||
|
const match = value.match(/^#?([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})$/);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatHexString = (
|
||||||
|
color: colorlib.Hex,
|
||||||
|
preserveShortFormat: boolean = false,
|
||||||
|
): string => {
|
||||||
|
const hexValue = color.to_code();
|
||||||
|
|
||||||
|
if (preserveShortFormat) {
|
||||||
|
if (
|
||||||
|
hexValue[0] === hexValue[1] &&
|
||||||
|
hexValue[2] === hexValue[3] &&
|
||||||
|
hexValue[4] === hexValue[5]
|
||||||
|
) {
|
||||||
|
return `#${hexValue[0]}${hexValue[2]}${hexValue[4]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `#${color.to_code()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function HexEditor({
|
||||||
|
color,
|
||||||
|
actions,
|
||||||
|
onKeyDown,
|
||||||
|
}: {
|
||||||
|
color: colorlib.Hex;
|
||||||
|
actions: HexColorActions;
|
||||||
|
onKeyDown?: (e: React.KeyboardEvent) => void;
|
||||||
|
}) {
|
||||||
|
const [inputValue, setInputValue] = useState(formatHexString(color));
|
||||||
|
const [isShortHex, setIsShortHex] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInputValue(formatHexString(color, isShortHex));
|
||||||
|
}, [color]);
|
||||||
|
|
||||||
|
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setInputValue(value);
|
||||||
|
|
||||||
|
const hex = extractHexValue(value);
|
||||||
|
if (hex) {
|
||||||
|
setIsShortHex(hex.length === 3);
|
||||||
|
const newColor = colorlib.Hex.from_code(hex);
|
||||||
|
actions.setHex(newColor);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBlur = () => {
|
||||||
|
setInputValue(formatHexString(color));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
(e.target as HTMLElement).blur();
|
||||||
|
} else if (onKeyDown) {
|
||||||
|
onKeyDown(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-cy="hex-editor" className={styles.hexEditor}>
|
||||||
|
<div className={clsx(styles.section, styles.hexLabel)}>HEX</div>
|
||||||
|
<div className={clsx(styles.section, styles.hexValueWrapper)}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
data-cy="hex-value-input"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onFocus={(e) => e.target.select()}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Color } from "colorlib";
|
|||||||
|
|
||||||
import { colorReducer, createColorActions } from "@hooks/color";
|
import { colorReducer, createColorActions } from "@hooks/color";
|
||||||
|
|
||||||
import ValueEditor from "./ValueEditor";
|
import { ValueEditor } from "./ValueEditor";
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
color: Color.from_hex("000"),
|
color: Color.from_hex("000"),
|
||||||
@@ -18,7 +18,6 @@ function TestWrapper() {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: 400,
|
width: 400,
|
||||||
height: 27,
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
}}
|
}}
|
||||||
@@ -43,7 +42,7 @@ describe("component editor tests", () => {
|
|||||||
cy.clock().then((clock) => clock.restore());
|
cy.clock().then((clock) => clock.restore());
|
||||||
});
|
});
|
||||||
|
|
||||||
it.only("works with mouse events", () => {
|
it("works with mouse events", () => {
|
||||||
// Check initial state
|
// Check initial state
|
||||||
cy.dataCy("R-slider-bar")
|
cy.dataCy("R-slider-bar")
|
||||||
.should("have.css", "width", "0px")
|
.should("have.css", "width", "0px")
|
||||||
@@ -159,4 +158,35 @@ describe("component editor tests", () => {
|
|||||||
.dataCy("R-value-input")
|
.dataCy("R-value-input")
|
||||||
.should("have.value", "127");
|
.should("have.value", "127");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("works with keyboard events", () => {
|
||||||
|
// Tab through components
|
||||||
|
cy.press(Cypress.Keyboard.Keys.TAB);
|
||||||
|
cy.dataCy("R-slider").should("have.focus");
|
||||||
|
|
||||||
|
cy.press(Cypress.Keyboard.Keys.TAB);
|
||||||
|
cy.dataCy("R-decrement-button").should("have.focus");
|
||||||
|
|
||||||
|
cy.press(Cypress.Keyboard.Keys.TAB);
|
||||||
|
cy.dataCy("R-value-input").should("have.focus");
|
||||||
|
|
||||||
|
cy.press(Cypress.Keyboard.Keys.TAB);
|
||||||
|
cy.dataCy("R-increment-button").should("have.focus");
|
||||||
|
|
||||||
|
// Pressing Escape should blur focused element
|
||||||
|
cy.dataCy("R-increment-button").type("{esc}");
|
||||||
|
cy.dataCy("R-increment-button").should("not.have.focus");
|
||||||
|
|
||||||
|
cy.dataCy("R-slider").focus();
|
||||||
|
cy.dataCy("R-slider").type("{esc}");
|
||||||
|
cy.dataCy("R-slider").should("not.have.focus");
|
||||||
|
|
||||||
|
cy.dataCy("R-decrement-button").focus();
|
||||||
|
cy.dataCy("R-decrement-button").type("{esc}");
|
||||||
|
cy.dataCy("R-decrement-button").should("not.have.focus");
|
||||||
|
|
||||||
|
cy.dataCy("R-value-input").focus();
|
||||||
|
cy.dataCy("R-value-input").type("{esc}");
|
||||||
|
cy.dataCy("R-value-input").should("not.have.focus");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import type { RefObject } from "react";
|
import type { RefObject } from "react";
|
||||||
|
|
||||||
export function handleScroll(
|
export function handleScroll(
|
||||||
|
|||||||
+3
-1
@@ -4,12 +4,14 @@ import { createRoot } from "react-dom/client";
|
|||||||
|
|
||||||
import App from "./App.tsx";
|
import App from "./App.tsx";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { MediaQueryProvider } from "./providers";
|
import { MediaQueryProvider, SelectedColorProvider } from "./providers";
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<MediaQueryProvider>
|
<MediaQueryProvider>
|
||||||
|
<SelectedColorProvider>
|
||||||
<App />
|
<App />
|
||||||
|
</SelectedColorProvider>
|
||||||
</MediaQueryProvider>
|
</MediaQueryProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ interface MediaQueryContextType {
|
|||||||
isMobilePortrait: boolean;
|
isMobilePortrait: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MediaQueryContext = createContext<MediaQueryContextType | undefined>(
|
export const MediaQueryContext = createContext<
|
||||||
undefined,
|
MediaQueryContextType | undefined
|
||||||
);
|
>(undefined);
|
||||||
|
|
||||||
function MediaQueryProvider({ children }: { children: ReactNode }) {
|
export const MediaQueryProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [viewportMode, setViewportMode] = useState<ViewportMode>(
|
const [viewportMode, setViewportMode] = useState<ViewportMode>(
|
||||||
ViewportMode.DESKTOP,
|
ViewportMode.DESKTOP,
|
||||||
);
|
);
|
||||||
@@ -71,7 +71,4 @@ function MediaQueryProvider({ children }: { children: ReactNode }) {
|
|||||||
{children}
|
{children}
|
||||||
</MediaQueryContext.Provider>
|
</MediaQueryContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default MediaQueryProvider;
|
|
||||||
export { MediaQueryContext };
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
|
|
||||||
import { MediaQueryContext } from "./MediaQueryProvider";
|
import { MediaQueryContext } from "./MediaQueryProvider";
|
||||||
|
import { SelectedColorContext } from "./SelectedColorProvider";
|
||||||
|
|
||||||
function useMediaQuery() {
|
export function useMediaQuery() {
|
||||||
const context = useContext(MediaQueryContext);
|
const context = useContext(MediaQueryContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error("useMediaQuery must be used within a MediaQueryProvider");
|
throw new Error("useMediaQuery must be used within a MediaQueryProvider");
|
||||||
@@ -10,4 +11,10 @@ function useMediaQuery() {
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useMediaQuery };
|
export function useSelectedColor() {
|
||||||
|
const context = useContext(SelectedColorContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useColor must be used within a ColorProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
import MediaQueryProvider from "./MediaQueryProvider";
|
import { MediaQueryProvider } from "./MediaQueryProvider";
|
||||||
|
import { SelectedColorProvider } from "./SelectedColorProvider";
|
||||||
|
|
||||||
export { MediaQueryProvider };
|
export { MediaQueryProvider, SelectedColorProvider };
|
||||||
|
|||||||
Reference in New Issue
Block a user