Skip to content

Commit 17b3159

Browse files
authored
fix: Input arrow up and down pulls up autocomplete now instead of incrementing (#4291)
## Description closes #4287 - [x] Up/Down now uses `onChange` instead of `onChangeComplete` (this should be faster) - [x] Up/Down in the Space control now works with modifiers (it seems this never worked before) - [x] Up/Down no longer opens the combobox if the input is numeric-like​⬤ ## Steps for reproduction 1. click button 2. 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: 5de6) - [ ] 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 87e2a1c commit 17b3159

File tree

2 files changed

+93
-76
lines changed

2 files changed

+93
-76
lines changed

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

Lines changed: 68 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -218,63 +218,6 @@ const useScrub = ({
218218
export const isNumericString = (input: string) =>
219219
String(input).trim().length !== 0 && Number.isNaN(Number(input)) === false;
220220

221-
const useHandleKeyDown =
222-
({
223-
ignoreEnter,
224-
ignoreUpDownNumeric,
225-
value,
226-
onChange,
227-
onKeyDown,
228-
}: {
229-
ignoreEnter: boolean;
230-
ignoreUpDownNumeric: boolean;
231-
value: CssValueInputValue;
232-
onChange: (event: {
233-
value: CssValueInputValue;
234-
altKey: boolean;
235-
shiftKey: boolean;
236-
}) => void;
237-
onKeyDown: KeyboardEventHandler<HTMLInputElement>;
238-
}) =>
239-
(event: KeyboardEvent<HTMLInputElement>) => {
240-
if (event.defaultPrevented) {
241-
// Underlying select like `unitSelect` can already prevent an event like up/down buttons
242-
return;
243-
}
244-
const meta = { altKey: event.altKey, shiftKey: event.shiftKey };
245-
246-
// Do not prevent downshift behaviour on item select
247-
if (ignoreEnter === false) {
248-
if (event.key === "Enter") {
249-
onChange({ value, ...meta });
250-
}
251-
}
252-
253-
if (
254-
ignoreUpDownNumeric === false &&
255-
(value.type === "unit" ||
256-
(value.type === "intermediate" && isNumericString(value.value))) &&
257-
value.unit !== undefined &&
258-
(event.key === "ArrowUp" || event.key === "ArrowDown")
259-
) {
260-
const inputValue =
261-
value.type === "unit" ? value.value : Number(value.value.trim());
262-
263-
onChange({
264-
value: {
265-
type: "unit",
266-
value: handleNumericInputArrowKeys(inputValue, event),
267-
unit: value.unit,
268-
},
269-
...meta,
270-
});
271-
// Prevent Downshift from opening menu on arrow up/down
272-
return;
273-
}
274-
275-
onKeyDown(event);
276-
};
277-
278221
export type IntermediateStyleValue = {
279222
type: "intermediate";
280223
value: string;
@@ -289,7 +232,13 @@ type Modifiers = {
289232
};
290233

291234
type ChangeCompleteEvent = {
292-
type: "enter" | "blur" | "scrub-end" | "unit-select" | "keyword-select";
235+
type:
236+
| "enter"
237+
| "blur"
238+
| "scrub-end"
239+
| "unit-select"
240+
| "keyword-select"
241+
| "delta";
293242
value: StyleValue;
294243
} & Modifiers;
295244

@@ -355,6 +304,7 @@ const Description = styled(Box, { width: theme.spacing[27] });
355304
* - shift key modifier increases/decreases value by 10
356305
* - option/alt key modifier increases/decreases value by 0.1
357306
* - no modifier increases/decreases value by 1
307+
* - does not open the combobox when the input is a number (CSS root variables can include numbers in their names)
358308
* - Scrub interaction
359309
* - Click outside, unit selection or escape when list is open should unfocus the unit select trigger
360310
*
@@ -612,18 +562,6 @@ export const CssValueInput = ({
612562
onChangeComplete({ value, type: "blur" });
613563
};
614564

615-
const handleKeyDown = useHandleKeyDown({
616-
// In case of the menu is really open and the selection is inside it
617-
// we do not prevent the default downshift Enter key behavior
618-
ignoreEnter:
619-
isUnitsOpen || (isOpen && !menuProps.empty && highlightedIndex !== -1),
620-
// Do not change the number value on the arrow up/down if any menu is opened
621-
ignoreUpDownNumeric: isUnitsOpen || isOpen,
622-
onChange: (event) => onChangeComplete({ ...event, type: "enter" }),
623-
value,
624-
onKeyDown: inputProps.onKeyDown,
625-
});
626-
627565
const finalPrefix =
628566
prefix ||
629567
(icon && (
@@ -692,9 +630,67 @@ export const CssValueInput = ({
692630
.filter(Boolean)
693631
.map((descr) => <Description>{descr}</Description>);
694632

633+
const handleUpDownNumeric = (event: KeyboardEvent<HTMLInputElement>) => {
634+
const isComboOpen = isOpen && !menuProps.empty;
635+
636+
if (isUnitsOpen || isComboOpen) {
637+
return;
638+
}
639+
640+
if (
641+
(value.type === "unit" ||
642+
(value.type === "intermediate" && isNumericString(value.value))) &&
643+
value.unit !== undefined &&
644+
(event.key === "ArrowUp" || event.key === "ArrowDown")
645+
) {
646+
const inputValue =
647+
value.type === "unit" ? value.value : Number(value.value.trim());
648+
649+
const meta = { altKey: event.altKey, shiftKey: event.shiftKey };
650+
const hasMeta = meta.altKey || meta.shiftKey;
651+
652+
if (hasMeta) {
653+
// @todo switch to using props.onChange instead of props.onChangeComplete
654+
// this will require modifying input-popover.tsx
655+
const newValue = {
656+
type: "unit" as const,
657+
value: handleNumericInputArrowKeys(inputValue, event),
658+
unit: value.unit,
659+
};
660+
661+
onChangeComplete({ value: newValue, ...meta, type: "delta" });
662+
} else {
663+
props.onChange({
664+
type: "unit",
665+
value: handleNumericInputArrowKeys(inputValue, event),
666+
unit: value.unit,
667+
});
668+
}
669+
event.preventDefault();
670+
}
671+
};
672+
673+
const handleMetaEnter = (event: KeyboardEvent<HTMLInputElement>) => {
674+
if (
675+
isUnitsOpen ||
676+
(isOpen && !menuProps.empty && highlightedIndex !== -1)
677+
) {
678+
return;
679+
}
680+
681+
const meta = { altKey: event.altKey, shiftKey: event.shiftKey };
682+
683+
if (event.key === "Enter") {
684+
onChangeComplete({ type: "enter", value, ...meta });
685+
}
686+
};
687+
695688
const inputPropsHandleKeyDown = composeEventHandlers(
696-
inputProps.onKeyDown,
697-
handleKeyDown
689+
composeEventHandlers(handleUpDownNumeric, inputProps.onKeyDown, {
690+
// Pass prevented events to the combobox (e.g., the Escape key doesn't work otherwise, as it's blocked by Radix)
691+
checkForDefaultPrevented: false,
692+
}),
693+
handleMetaEnter
698694
);
699695

700696
return (

packages/design-system/src/components/combobox.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,9 @@ type UseComboboxProps<Item> = Omit<UseDownshiftComboboxProps<Item>, "items"> & {
255255

256256
export const comboboxStateChangeTypes = useDownshiftCombobox.stateChangeTypes;
257257

258+
const isNumericString = (input: string) =>
259+
String(input).trim().length !== 0 && Number.isNaN(Number(input)) === false;
260+
258261
export const useCombobox = <Item,>({
259262
getItems,
260263
value,
@@ -281,7 +284,20 @@ export const useCombobox = <Item,>({
281284
selectedItem: selectedItem ?? null, // Prevent downshift warning about switching controlled mode
282285
isOpen,
283286

284-
onIsOpenChange({ isOpen, inputValue }) {
287+
onIsOpenChange(state) {
288+
const { type, isOpen, inputValue } = state;
289+
290+
// Don't open the combobox if the input is a number and the user is using the arrow keys.
291+
// This prevents the combobox from opening when the user is trying to increment or decrement a number.
292+
if (
293+
(type === comboboxStateChangeTypes.InputKeyDownArrowDown ||
294+
type === comboboxStateChangeTypes.InputKeyDownArrowUp) &&
295+
inputValue !== undefined &&
296+
isNumericString(inputValue)
297+
) {
298+
return;
299+
}
300+
285301
if (isOpen) {
286302
itemsCache.current = getItems();
287303
const matchedItems = match(
@@ -303,11 +319,16 @@ export const useCombobox = <Item,>({
303319
stateReducer,
304320
itemToString,
305321
inputValue: value ? itemToString(value) : "",
306-
onInputValueChange({ inputValue, type }) {
322+
onInputValueChange(state) {
323+
const { inputValue, type } = state;
307324
if (type === comboboxStateChangeTypes.InputChange) {
308-
setMatchedItems(
309-
match(inputValue ?? "", itemsCache.current, itemToString)
325+
const matchedItems = match(
326+
inputValue ?? "",
327+
itemsCache.current,
328+
itemToString
310329
);
330+
setIsOpen(matchedItems.length > 0);
331+
setMatchedItems(matchedItems);
311332
}
312333
},
313334
onSelectedItemChange({ selectedItem, type }) {

0 commit comments

Comments
 (0)