From 27babf2059d82c5e4c79d5e651e2a40027507615 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Sat, 15 Feb 2025 14:35:58 +0000 Subject: [PATCH 1/5] improve composeEventHandlers --- .../sections/advanced/add-styles-input.tsx | 33 ++++++------ .../css-value-input/css-value-input.tsx | 10 ++-- apps/builder/app/shared/event-utils.test.ts | 46 +++++++++++++++++ apps/builder/app/shared/event-utils.ts | 50 ++++++------------- 4 files changed, 84 insertions(+), 55 deletions(-) create mode 100644 apps/builder/app/shared/event-utils.test.ts diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/add-styles-input.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/add-styles-input.tsx index c5c001ae14be..c057a88f038f 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/add-styles-input.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/add-styles-input.tsx @@ -178,31 +178,34 @@ export const AddStylesInput = forwardRef< setItem({ property: "", label: "" }); }; - const handleKeys = (event: KeyboardEvent) => { - // Dropdown might handle enter or escape. - if (event.defaultPrevented) { - return; - } + const handleEnter = (event: KeyboardEvent) => { if (event.key === "Enter") { clear(); onSubmit(item.property); - return; } - // When user hits backspace and there is nothing in the input - we hide the input - const abortByBackspace = - event.key === "Backspace" && combobox.inputValue === ""; + }; - if (event.key === "Escape" || abortByBackspace) { + const handleEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { clear(); onClose(); - event.preventDefault(); } }; - const handleKeyDown = composeEventHandlers(inputProps.onKeyDown, handleKeys, { - // Pass prevented events to the combobox (e.g., the Escape key doesn't work otherwise, as it's blocked by Radix) - checkForDefaultPrevented: false, - }); + const handleDelete = (event: KeyboardEvent) => { + // When user hits backspace and there is nothing in the input - we hide the input + if (event.key === "Backspace" && combobox.inputValue === "") { + clear(); + onClose(); + } + }; + + const handleKeyDown = composeEventHandlers([ + inputProps.onKeyDown, + handleEnter, + handleEscape, + handleDelete, + ]); return ( diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx index 02658c70b9f6..8ebb097a399b 100644 --- a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx @@ -277,7 +277,9 @@ type CssValueInputProps = Pick< onChange: (value: CssValueInputValue | undefined) => void; onChangeComplete: (event: ChangeCompleteEvent) => void; onHighlight: (value: StyleValue | undefined) => void; + // Does not reset intermediate changes. onAbort: () => void; + // Resets the value to default even if it has intermediate changes. onReset: () => void; icon?: ReactNode; showSuffix?: boolean; @@ -798,7 +800,7 @@ export const CssValueInput = ({ } }; - const handleMetaEnter = (event: KeyboardEvent) => { + const handleEnter = (event: KeyboardEvent) => { if ( isUnitsOpen || (isOpen && !menuProps.empty && highlightedIndex !== -1) @@ -860,11 +862,11 @@ export const CssValueInput = ({ }, [inputRef]); const inputPropsHandleKeyDown = composeEventHandlers( - composeEventHandlers(handleUpDownNumeric, inputProps.onKeyDown, { + [handleUpDownNumeric, inputProps.onKeyDown, handleEnter], + { // Pass prevented events to the combobox (e.g., the Escape key doesn't work otherwise, as it's blocked by Radix) checkForDefaultPrevented: false, - }), - handleMetaEnter + } ); const suffixRef = useRef(null); diff --git a/apps/builder/app/shared/event-utils.test.ts b/apps/builder/app/shared/event-utils.test.ts new file mode 100644 index 000000000000..8a8131512dbf --- /dev/null +++ b/apps/builder/app/shared/event-utils.test.ts @@ -0,0 +1,46 @@ +import { describe, test, expect, vi } from "vitest"; +import { composeEventHandlers } from "./event-utils"; + +describe("composeEventHandlers", () => { + test("executes handlers in sequence", () => { + const handler1 = vi.fn(); + const handler2 = vi.fn(); + const event = {}; + + const composed = composeEventHandlers([handler1, handler2]); + composed(event); + + expect(handler1).toHaveBeenCalledWith(event); + expect(handler2).toHaveBeenCalledWith(event); + }); + + test("stops execution if event.defaultPrevented is true", () => { + const handler1 = vi.fn((event) => { + event.defaultPrevented = true; + }); + const handler2 = vi.fn(); + const event = {}; + + const composed = composeEventHandlers([handler1, handler2]); + composed(event); + + expect(handler1).toHaveBeenCalled(); + expect(handler2).not.toHaveBeenCalled(); + }); + + test("continues execution when checkForDefaultPrevented is false", () => { + const handler1 = vi.fn((event) => { + event.defaultPrevented = true; + }); + const handler2 = vi.fn(); + const event = {}; + + const composed = composeEventHandlers([handler1, handler2], { + checkForDefaultPrevented: false, + }); + composed(event); + + expect(handler1).toHaveBeenCalled(); + expect(handler2).toHaveBeenCalled(); + }); +}); diff --git a/apps/builder/app/shared/event-utils.ts b/apps/builder/app/shared/event-utils.ts index 0f2ba93b548e..02aaf002919e 100644 --- a/apps/builder/app/shared/event-utils.ts +++ b/apps/builder/app/shared/event-utils.ts @@ -1,42 +1,20 @@ /* -https://github.com/radix-ui/primitives/blob/main/packages/core/primitive/src/primitive.tsx - -MIT License - -Copyright (c) 2022 WorkOS - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -export const composeEventHandlers = ( - originalEventHandler?: (event: E) => void, - ourEventHandler?: (event: E) => void, + * Inspired by + * https://github.com/radix-ui/primitives/blob/main/packages/core/primitive/src/primitive.tsx + */ +export const composeEventHandlers = ( + handlers: Array<(event: CustomEvent) => void>, { checkForDefaultPrevented = true } = {} ) => { - return function handleEvent(event: E) { - originalEventHandler?.(event); - - if ( - checkForDefaultPrevented === false || - !(event as unknown as Event).defaultPrevented - ) { - return ourEventHandler?.(event); + return function handleEvent(event: CustomEvent) { + for (const handler of handlers) { + handler?.(event); + if ( + checkForDefaultPrevented && + (event as unknown as Event).defaultPrevented + ) { + break; + } } }; }; From 733ddbd9c2983b32c8cb403f9531e51347c4c3a0 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Sat, 15 Feb 2025 15:18:35 +0000 Subject: [PATCH 2/5] handle reset by backspace in advanced --- .../style-panel/sections/advanced/advanced.tsx | 4 ++++ .../css-value-input/css-value-input-container.tsx | 3 +++ .../shared/css-value-input/css-value-input.tsx | 10 +++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx index 024328dacd99..0b46b5196236 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx @@ -178,6 +178,7 @@ const AdvancedPropertyValue = ({ autoFocus, property, onChangeComplete, + onReset, inputRef: inputRefProp, }: { autoFocus?: boolean; @@ -185,6 +186,7 @@ const AdvancedPropertyValue = ({ onChangeComplete: ComponentProps< typeof CssValueInputContainer >["onChangeComplete"]; + onReset: ComponentProps["onReset"]; inputRef?: RefObject; }) => { const styleDecl = useComputedStyleDecl(property); @@ -245,6 +247,7 @@ const AdvancedPropertyValue = ({ }} deleteProperty={deleteProperty} onChangeComplete={onChangeComplete} + onReset={onReset} /> ); }; @@ -340,6 +343,7 @@ const AdvancedProperty = memo( autoFocus={autoFocus} property={property} onChangeComplete={onChangeComplete} + onReset={onReset} inputRef={valueInputRef} /> diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input-container.tsx b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input-container.tsx index 3d39d69a5a9f..3ad97e56c190 100644 --- a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input-container.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input-container.tsx @@ -16,6 +16,7 @@ type CssValueInputContainerProps = { | "onChangeComplete" > & { onChangeComplete?: ComponentProps["onChangeComplete"]; + onReset?: ComponentProps["onReset"]; }; export const CssValueInputContainer = ({ @@ -23,6 +24,7 @@ export const CssValueInputContainer = ({ setValue, deleteProperty, onChangeComplete, + onReset, ...props }: CssValueInputContainerProps) => { const [intermediateValue, setIntermediateValue] = useState< @@ -63,6 +65,7 @@ export const CssValueInputContainer = ({ }} onReset={() => { deleteProperty(property); + onReset?.(); }} /> ); diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx index 8ebb097a399b..e866a9a4524d 100644 --- a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx @@ -815,6 +815,14 @@ export const CssValueInput = ({ } }; + const handleDelete = (event: KeyboardEvent) => { + if (event.key === "Backspace" && inputProps.value === "") { + event.preventDefault(); + closeMenu(); + onReset(); + } + }; + const { abort, ...autoScrollProps } = useMemo(() => { return getAutoScrollProps(); }, []); @@ -862,7 +870,7 @@ export const CssValueInput = ({ }, [inputRef]); const inputPropsHandleKeyDown = composeEventHandlers( - [handleUpDownNumeric, inputProps.onKeyDown, handleEnter], + [handleUpDownNumeric, inputProps.onKeyDown, handleEnter, handleDelete], { // Pass prevented events to the combobox (e.g., the Escape key doesn't work otherwise, as it's blocked by Radix) checkForDefaultPrevented: false, From 6f31f126b105690d3ce79c9a94dfd86a489b3b5c Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Sat, 15 Feb 2025 16:04:40 +0000 Subject: [PATCH 3/5] Delete property as a text for all advanced properties --- .../app/builder/features/style-panel/property-label.tsx | 6 +++--- .../features/style-panel/sections/advanced/advanced.tsx | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/property-label.tsx b/apps/builder/app/builder/features/style-panel/property-label.tsx index 787799d363f3..b5c8800d94f8 100644 --- a/apps/builder/app/builder/features/style-panel/property-label.tsx +++ b/apps/builder/app/builder/features/style-panel/property-label.tsx @@ -73,6 +73,7 @@ export const PropertyInfo = ({ description, styles, onReset, + resetType = "reset", }: { title: string; code?: string; @@ -80,6 +81,7 @@ export const PropertyInfo = ({ description: ReactNode; styles: ComputedStyleDecl[]; onReset: () => void; + resetType?: "reset" | "delete"; }) => { const breakpoints = useStore($breakpoints); const instances = useStore($instances); @@ -203,9 +205,7 @@ export const PropertyInfo = ({ css={{ gridTemplateColumns: "1fr max-content 1fr" }} onClick={onReset} > - {styles[0].property.startsWith("--") - ? "Delete variable" - : "Reset value"} + {resetType === "delete" ? "Delete property" : "Reset value"} )} diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx index 0b46b5196236..d64ba9a95bf1 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx @@ -157,6 +157,7 @@ const AdvancedPropertyLabel = ({ setIsOpen(false); onReset?.(); }} + resetType="delete" /> } > From a35acd3515b53f6aea1bf63d89e7b00e6c7ed140 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Sat, 15 Feb 2025 16:15:47 +0000 Subject: [PATCH 4/5] setIntermediateValue(undefined) on reset --- .../shared/css-value-input/css-value-input-container.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input-container.tsx b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input-container.tsx index 3ad97e56c190..e4af6f57c761 100644 --- a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input-container.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input-container.tsx @@ -64,6 +64,7 @@ export const CssValueInputContainer = ({ deleteProperty(property, { isEphemeral: true }); }} onReset={() => { + setIntermediateValue(undefined); deleteProperty(property); onReset?.(); }} From 6fb99cbf68ed14fc1cf7a3935a3874283e4eda40 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Sat, 15 Feb 2025 16:19:53 +0000 Subject: [PATCH 5/5] comment why on preventDefault --- .../style-panel/shared/css-value-input/css-value-input.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx index e866a9a4524d..cd9b120c8a67 100644 --- a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx @@ -817,8 +817,10 @@ export const CssValueInput = ({ const handleDelete = (event: KeyboardEvent) => { if (event.key === "Backspace" && inputProps.value === "") { + // - allows to close the menu + // - prevents baspace from deleting the value AFTER its already reseted to default, e.g. we get "aut" instead of "auto" event.preventDefault(); - closeMenu(); + //closeMenu(); onReset(); } };