Skip to content

Commit 6eb989d

Browse files
authored
feat: Backspace to reset the value in CSS Value Input (#4878)
## Description 1. improved composeEventHandlers implementation 2. delete value text, then hit backspace - same as reset 3. in advanced panel all property tooltips now show "Delete property" wording instead of "Reset value" <img width="363" alt="image" src="https://github.com/user-attachments/assets/f4ad7ec4-892c-4774-9143-ac2b26c55cde" /> ## Steps for reproduction 1. click button 4. expect xyz ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 0000) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file
1 parent 769b246 commit 6eb989d

File tree

7 files changed

+106
-58
lines changed

7 files changed

+106
-58
lines changed

apps/builder/app/builder/features/style-panel/property-label.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,15 @@ export const PropertyInfo = ({
7373
description,
7474
styles,
7575
onReset,
76+
resetType = "reset",
7677
}: {
7778
title: string;
7879
code?: string;
7980
link?: string;
8081
description: ReactNode;
8182
styles: ComputedStyleDecl[];
8283
onReset: () => void;
84+
resetType?: "reset" | "delete";
8385
}) => {
8486
const breakpoints = useStore($breakpoints);
8587
const instances = useStore($instances);
@@ -203,9 +205,7 @@ export const PropertyInfo = ({
203205
css={{ gridTemplateColumns: "1fr max-content 1fr" }}
204206
onClick={onReset}
205207
>
206-
{styles[0].property.startsWith("--")
207-
? "Delete variable"
208-
: "Reset value"}
208+
{resetType === "delete" ? "Delete property" : "Reset value"}
209209
</Button>
210210
)}
211211
</Flex>

apps/builder/app/builder/features/style-panel/sections/advanced/add-styles-input.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -178,31 +178,34 @@ export const AddStylesInput = forwardRef<
178178
setItem({ property: "", label: "" });
179179
};
180180

181-
const handleKeys = (event: KeyboardEvent) => {
182-
// Dropdown might handle enter or escape.
183-
if (event.defaultPrevented) {
184-
return;
185-
}
181+
const handleEnter = (event: KeyboardEvent) => {
186182
if (event.key === "Enter") {
187183
clear();
188184
onSubmit(item.property);
189-
return;
190185
}
191-
// When user hits backspace and there is nothing in the input - we hide the input
192-
const abortByBackspace =
193-
event.key === "Backspace" && combobox.inputValue === "";
186+
};
194187

195-
if (event.key === "Escape" || abortByBackspace) {
188+
const handleEscape = (event: KeyboardEvent) => {
189+
if (event.key === "Escape") {
196190
clear();
197191
onClose();
198-
event.preventDefault();
199192
}
200193
};
201194

202-
const handleKeyDown = composeEventHandlers(inputProps.onKeyDown, handleKeys, {
203-
// Pass prevented events to the combobox (e.g., the Escape key doesn't work otherwise, as it's blocked by Radix)
204-
checkForDefaultPrevented: false,
205-
});
195+
const handleDelete = (event: KeyboardEvent) => {
196+
// When user hits backspace and there is nothing in the input - we hide the input
197+
if (event.key === "Backspace" && combobox.inputValue === "") {
198+
clear();
199+
onClose();
200+
}
201+
};
202+
203+
const handleKeyDown = composeEventHandlers([
204+
inputProps.onKeyDown,
205+
handleEnter,
206+
handleEscape,
207+
handleDelete,
208+
]);
206209

207210
return (
208211
<ComboboxRoot open={combobox.isOpen}>

apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ const AdvancedPropertyLabel = ({
157157
setIsOpen(false);
158158
onReset?.();
159159
}}
160+
resetType="delete"
160161
/>
161162
}
162163
>
@@ -178,13 +179,15 @@ const AdvancedPropertyValue = ({
178179
autoFocus,
179180
property,
180181
onChangeComplete,
182+
onReset,
181183
inputRef: inputRefProp,
182184
}: {
183185
autoFocus?: boolean;
184186
property: StyleProperty;
185187
onChangeComplete: ComponentProps<
186188
typeof CssValueInputContainer
187189
>["onChangeComplete"];
190+
onReset: ComponentProps<typeof CssValueInputContainer>["onReset"];
188191
inputRef?: RefObject<HTMLInputElement>;
189192
}) => {
190193
const styleDecl = useComputedStyleDecl(property);
@@ -245,6 +248,7 @@ const AdvancedPropertyValue = ({
245248
}}
246249
deleteProperty={deleteProperty}
247250
onChangeComplete={onChangeComplete}
251+
onReset={onReset}
248252
/>
249253
);
250254
};
@@ -340,6 +344,7 @@ const AdvancedProperty = memo(
340344
autoFocus={autoFocus}
341345
property={property}
342346
onChangeComplete={onChangeComplete}
347+
onReset={onReset}
343348
inputRef={valueInputRef}
344349
/>
345350
</Box>

apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input-container.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ type CssValueInputContainerProps = {
1616
| "onChangeComplete"
1717
> & {
1818
onChangeComplete?: ComponentProps<typeof CssValueInput>["onChangeComplete"];
19+
onReset?: ComponentProps<typeof CssValueInput>["onReset"];
1920
};
2021

2122
export const CssValueInputContainer = ({
2223
property,
2324
setValue,
2425
deleteProperty,
2526
onChangeComplete,
27+
onReset,
2628
...props
2729
}: CssValueInputContainerProps) => {
2830
const [intermediateValue, setIntermediateValue] = useState<
@@ -62,7 +64,9 @@ export const CssValueInputContainer = ({
6264
deleteProperty(property, { isEphemeral: true });
6365
}}
6466
onReset={() => {
67+
setIntermediateValue(undefined);
6568
deleteProperty(property);
69+
onReset?.();
6670
}}
6771
/>
6872
);

apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,9 @@ type CssValueInputProps = Pick<
277277
onChange: (value: CssValueInputValue | undefined) => void;
278278
onChangeComplete: (event: ChangeCompleteEvent) => void;
279279
onHighlight: (value: StyleValue | undefined) => void;
280+
// Does not reset intermediate changes.
280281
onAbort: () => void;
282+
// Resets the value to default even if it has intermediate changes.
281283
onReset: () => void;
282284
icon?: ReactNode;
283285
showSuffix?: boolean;
@@ -798,7 +800,7 @@ export const CssValueInput = ({
798800
}
799801
};
800802

801-
const handleMetaEnter = (event: KeyboardEvent<HTMLInputElement>) => {
803+
const handleEnter = (event: KeyboardEvent<HTMLInputElement>) => {
802804
if (
803805
isUnitsOpen ||
804806
(isOpen && !menuProps.empty && highlightedIndex !== -1)
@@ -813,6 +815,16 @@ export const CssValueInput = ({
813815
}
814816
};
815817

818+
const handleDelete = (event: KeyboardEvent<HTMLInputElement>) => {
819+
if (event.key === "Backspace" && inputProps.value === "") {
820+
// - allows to close the menu
821+
// - prevents baspace from deleting the value AFTER its already reseted to default, e.g. we get "aut" instead of "auto"
822+
event.preventDefault();
823+
//closeMenu();
824+
onReset();
825+
}
826+
};
827+
816828
const { abort, ...autoScrollProps } = useMemo(() => {
817829
return getAutoScrollProps();
818830
}, []);
@@ -860,11 +872,11 @@ export const CssValueInput = ({
860872
}, [inputRef]);
861873

862874
const inputPropsHandleKeyDown = composeEventHandlers(
863-
composeEventHandlers(handleUpDownNumeric, inputProps.onKeyDown, {
875+
[handleUpDownNumeric, inputProps.onKeyDown, handleEnter, handleDelete],
876+
{
864877
// Pass prevented events to the combobox (e.g., the Escape key doesn't work otherwise, as it's blocked by Radix)
865878
checkForDefaultPrevented: false,
866-
}),
867-
handleMetaEnter
879+
}
868880
);
869881

870882
const suffixRef = useRef<HTMLDivElement | null>(null);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, test, expect, vi } from "vitest";
2+
import { composeEventHandlers } from "./event-utils";
3+
4+
describe("composeEventHandlers", () => {
5+
test("executes handlers in sequence", () => {
6+
const handler1 = vi.fn();
7+
const handler2 = vi.fn();
8+
const event = {};
9+
10+
const composed = composeEventHandlers([handler1, handler2]);
11+
composed(event);
12+
13+
expect(handler1).toHaveBeenCalledWith(event);
14+
expect(handler2).toHaveBeenCalledWith(event);
15+
});
16+
17+
test("stops execution if event.defaultPrevented is true", () => {
18+
const handler1 = vi.fn((event) => {
19+
event.defaultPrevented = true;
20+
});
21+
const handler2 = vi.fn();
22+
const event = {};
23+
24+
const composed = composeEventHandlers([handler1, handler2]);
25+
composed(event);
26+
27+
expect(handler1).toHaveBeenCalled();
28+
expect(handler2).not.toHaveBeenCalled();
29+
});
30+
31+
test("continues execution when checkForDefaultPrevented is false", () => {
32+
const handler1 = vi.fn((event) => {
33+
event.defaultPrevented = true;
34+
});
35+
const handler2 = vi.fn();
36+
const event = {};
37+
38+
const composed = composeEventHandlers([handler1, handler2], {
39+
checkForDefaultPrevented: false,
40+
});
41+
composed(event);
42+
43+
expect(handler1).toHaveBeenCalled();
44+
expect(handler2).toHaveBeenCalled();
45+
});
46+
});
Lines changed: 14 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,20 @@
11
/*
2-
https://github.com/radix-ui/primitives/blob/main/packages/core/primitive/src/primitive.tsx
3-
4-
MIT License
5-
6-
Copyright (c) 2022 WorkOS
7-
8-
Permission is hereby granted, free of charge, to any person obtaining a copy
9-
of this software and associated documentation files (the "Software"), to deal
10-
in the Software without restriction, including without limitation the rights
11-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12-
copies of the Software, and to permit persons to whom the Software is
13-
furnished to do so, subject to the following conditions:
14-
15-
The above copyright notice and this permission notice shall be included in all
16-
copies or substantial portions of the Software.
17-
18-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24-
SOFTWARE.
25-
*/
26-
27-
export const composeEventHandlers = <E>(
28-
originalEventHandler?: (event: E) => void,
29-
ourEventHandler?: (event: E) => void,
2+
* Inspired by
3+
* https://github.com/radix-ui/primitives/blob/main/packages/core/primitive/src/primitive.tsx
4+
*/
5+
export const composeEventHandlers = <CustomEvent>(
6+
handlers: Array<(event: CustomEvent) => void>,
307
{ checkForDefaultPrevented = true } = {}
318
) => {
32-
return function handleEvent(event: E) {
33-
originalEventHandler?.(event);
34-
35-
if (
36-
checkForDefaultPrevented === false ||
37-
!(event as unknown as Event).defaultPrevented
38-
) {
39-
return ourEventHandler?.(event);
9+
return function handleEvent(event: CustomEvent) {
10+
for (const handler of handlers) {
11+
handler?.(event);
12+
if (
13+
checkForDefaultPrevented &&
14+
(event as unknown as Event).defaultPrevented
15+
) {
16+
break;
17+
}
4018
}
4119
};
4220
};

0 commit comments

Comments
 (0)