Skip to content

Commit 24c5ef8

Browse files
authored
fix: various bug fixes (#456)
* fix(number-field): only format when enabled * fix(number-field): don't trigger `onRawValueChange` on mount when NaN * fix(select): correct type definition & empty value for multiselect * fix(text-field): clear input when controlled value set to undefined * fix(combobox): correct type definition & empty value for multiselect * fix(skeleton): correct data-animate & data-visible attribute value * fix(combobox): close list on outside click * fix(navigation-menu): incorrect animation after closed
1 parent 1872bcb commit 24c5ef8

16 files changed

+58
-29
lines changed

apps/docs/app.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ export default defineConfig({
113113
},
114114
prerender: {
115115
routes: ["/docs/core/overview/introduction"],
116-
crawlLinks: true
117-
}
116+
crawlLinks: true,
117+
},
118118
},
119119

120120
extensions: ["mdx", "md"],

biome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
33
"files": {
4-
"ignore": ["./tsconfig.json", "*/netlify/*"]
4+
"ignore": ["./tsconfig.json", "*/netlify/*", "**/package.json"]
55
},
66
"vcs": {
77
"enabled": true,

packages/core/src/combobox/combobox-base.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -596,9 +596,8 @@ export function ComboboxBase<
596596
focusStrategy: FocusStrategy | boolean,
597597
triggerMode?: ComboboxTriggerMode,
598598
) => {
599-
600599
// If set to only open manually, ignore other triggers
601-
if (local.triggerMode === 'manual' && triggerMode !== 'manual') {
600+
if (local.triggerMode === "manual" && triggerMode !== "manual") {
602601
return;
603602
}
604603

packages/core/src/combobox/combobox-content.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,14 @@ export function ComboboxContent<T extends ValidComponent = "div">(
8282
"onFocusOutside",
8383
]);
8484

85-
const close = () => {
85+
const dismiss = () => {
8686
context.resetInputValue(
8787
context.listState().selectionManager().selectedKeys(),
8888
);
8989
context.close();
90+
setTimeout(() => {
91+
context.close();
92+
});
9093
};
9194

9295
const onFocusOutside = (e: FocusOutsideEvent) => {
@@ -165,7 +168,7 @@ export function ComboboxContent<T extends ValidComponent = "div">(
165168
local.style,
166169
)}
167170
onFocusOutside={onFocusOutside}
168-
onDismiss={close}
171+
onDismiss={dismiss}
169172
{...context.dataset()}
170173
{...others}
171174
/>

packages/core/src/combobox/combobox-input.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface ComboboxInputCommonProps<
3737
ref: T | ((el: T) => void);
3838
onInput: JSX.EventHandlerUnion<T, InputEvent>;
3939
onKeyDown: JSX.EventHandlerUnion<T, KeyboardEvent>;
40+
onClick: JSX.EventHandlerUnion<T, MouseEvent>;
4041
onFocus: JSX.EventHandlerUnion<T, FocusEvent>;
4142
onBlur: JSX.EventHandlerUnion<T, FocusEvent>;
4243
onTouchEnd: JSX.EventHandlerUnion<T, TouchEvent>;
@@ -93,6 +94,7 @@ export function ComboboxInput<T extends ValidComponent = "input">(
9394
[
9495
"ref",
9596
"disabled",
97+
"onClick",
9698
"onInput",
9799
"onKeyDown",
98100
"onFocus",
@@ -113,6 +115,14 @@ export function ComboboxInput<T extends ValidComponent = "input">(
113115

114116
const { fieldProps } = createFormControlField(formControlFieldProps);
115117

118+
const onClick: JSX.EventHandlerUnion<HTMLInputElement, MouseEvent> = (e) => {
119+
callHandler(e, local.onClick);
120+
121+
if (context.triggerMode() === "focus" && !context.isOpen()) {
122+
context.open(false, "focus");
123+
}
124+
};
125+
116126
const onInput: JSX.EventHandlerUnion<HTMLInputElement, InputEvent> = (e) => {
117127
callHandler(e, local.onInput);
118128

@@ -315,6 +325,7 @@ export function ComboboxInput<T extends ValidComponent = "input">(
315325
aria-required={formControlContext.isRequired() || undefined}
316326
aria-disabled={formControlContext.isDisabled() || undefined}
317327
aria-readonly={formControlContext.isReadOnly() || undefined}
328+
onClick={onClick}
318329
onInput={onInput}
319330
onKeyDown={onKeyDown}
320331
onFocus={onFocus}

packages/core/src/combobox/combobox-root.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515

1616
export interface ComboboxSingleSelectionOptions<T> {
1717
/** The controlled value of the combobox. */
18-
value?: T;
18+
value?: T | null;
1919

2020
/**
2121
* The value of the combobox when initially rendered.
@@ -24,7 +24,7 @@ export interface ComboboxSingleSelectionOptions<T> {
2424
defaultValue?: T;
2525

2626
/** Event handler called when the value changes. */
27-
onChange?: (value: T) => void;
27+
onChange?: (value: T | null) => void;
2828

2929
/** Whether the combobox allow multiple selection. */
3030
multiple?: false;
@@ -100,10 +100,10 @@ export function ComboboxRoot<
100100

101101
const onChange = (value: Option[]) => {
102102
if (local.multiple) {
103-
local.onChange?.(value as any);
103+
local.onChange?.((value ?? []) as any);
104104
} else {
105105
// use `null` as "no value" because `undefined` mean the component is "uncontrolled".
106-
local.onChange?.((value[0] ?? null) as any);
106+
local.onChange?.((value[0] ?? null) as any); // TODO: maybe return undefined? breaking change!
107107
}
108108
};
109109

packages/core/src/menu/menu-content-base.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,8 @@ export function MenuContentBase<T extends ValidComponent = "div">(
290290

291291
createEffect(() => onCleanup(context.registerContentId(local.id!)));
292292

293+
onCleanup(() => context.setContentRef(undefined));
294+
293295
const commonAttributes: Omit<MenuContentBaseRenderProps, keyof MenuDataSet> =
294296
{
295297
ref: mergeRefs((el) => {

packages/core/src/menu/menu-context.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface MenuContextValue {
2525
triggerId: Accessor<string | undefined>;
2626
contentId: Accessor<string | undefined>;
2727
setTriggerRef: (el: HTMLElement) => void;
28-
setContentRef: (el: HTMLElement) => void;
28+
setContentRef: (el: HTMLElement | undefined) => void;
2929
open: (focusStrategy: FocusStrategy | boolean) => void;
3030
close: (recursively?: boolean) => void;
3131
toggle: (focusStrategy: FocusStrategy | boolean) => void;

packages/core/src/navigation-menu/navigation-menu-root.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ export function NavigationMenuRoot<T extends ValidComponent = "ul">(
168168
element: () => viewportRef() ?? null,
169169
});
170170

171+
createEffect(() => {
172+
if (!viewportPresent()) {
173+
context.setPreviousMenu(undefined);
174+
}
175+
});
176+
171177
const context: NavigationMenuContextValue = {
172178
dataset,
173179
delayDuration: () => local.delayDuration!,

packages/core/src/navigation-menu/navigation-menu-trigger.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@ export function NavigationMenuTrigger<T extends ValidComponent = "button">(
6969
if (context.dataset()["data-expanded"] === "") return;
7070

7171
timeoutId = window.setTimeout(() => {
72-
context.setAutoFocusMenu(true);
7372
menuContext?.triggerRef()?.focus();
73+
setTimeout(() => {
74+
context.setAutoFocusMenu(true);
75+
});
7476
}, context.delayDuration());
7577
};
7678

packages/core/src/navigation-menu/navigation-menu-viewport.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,12 @@ export function NavigationMenuViewport<T extends ValidComponent = "li">(
117117
);
118118

119119
const height = createMemo((prev) => {
120+
if (ref() === undefined || !context.viewportPresent()) return undefined;
120121
if (size.height() === 0) return prev;
121122
return size.height();
122123
});
123124
const width = createMemo((prev) => {
125+
if (ref() === undefined || !context.viewportPresent()) return undefined;
124126
if (size.width() === 0) return prev;
125127
return size.width();
126128
});

packages/core/src/number-field/number-field-input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export function NumberFieldInput<T extends ValidComponent = "input">(
169169
>
170170
as={local.as || "input"}
171171
value={
172-
Number.isNaN(context.rawValue())
172+
Number.isNaN(context.rawValue()) || context.value() === undefined
173173
? ""
174174
: context.formatNumber(context.rawValue())
175175
}

packages/core/src/number-field/number-field-root.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@ export function NumberFieldRoot<T extends ValidComponent = "div">(
185185
return new NumberFormatter(locale(), local.formatOptions);
186186
});
187187

188+
const formatNumber = (number: number) =>
189+
local.format ? numberFormatter().format(number) : number.toString();
190+
188191
const parseRawValue = (value: string | number | undefined) =>
189192
local.format && typeof value !== "number"
190193
? numberParser().parse(value ?? "")
@@ -203,14 +206,12 @@ export function NumberFieldRoot<T extends ValidComponent = "div">(
203206
value: () => local.value,
204207
defaultValue: () => local.defaultValue ?? local.rawValue,
205208
onChange: (value) => {
206-
local.onChange?.(
207-
typeof value === "number" ? numberFormatter().format(value) : value,
208-
);
209+
local.onChange?.(typeof value === "number" ? formatNumber(value) : value);
209210
local.onRawValueChange?.(parseRawValue(value));
210211
},
211212
});
212213

213-
local.onRawValueChange?.(parseRawValue(value()));
214+
if (value() !== undefined) local.onRawValueChange?.(parseRawValue(value()));
214215

215216
function isAllowedInput(char: string): boolean {
216217
if (local.allowedInput !== undefined) return local.allowedInput.test(char);
@@ -267,7 +268,7 @@ export function NumberFieldRoot<T extends ValidComponent = "div">(
267268
setValue,
268269
rawValue: () => parseRawValue(value()),
269270
generateId: createGenerateId(() => access(formControlProps.id)!),
270-
formatNumber: (number: number) => numberFormatter().format(number),
271+
formatNumber,
271272
format: () => {
272273
if (!local.format) return;
273274
let rawValue = context.rawValue();

packages/core/src/select/select-root.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515

1616
export interface SelectSingleSelectionOptions<T> {
1717
/** The controlled value of the select. */
18-
value?: T;
18+
value?: T | null;
1919

2020
/**
2121
* The value of the select when initially rendered.
@@ -24,7 +24,7 @@ export interface SelectSingleSelectionOptions<T> {
2424
defaultValue?: T;
2525

2626
/** Event handler called when the value changes. */
27-
onChange?: (value: T) => void;
27+
onChange?: (value: T | null) => void;
2828

2929
/** Whether the select allow multiple selection. */
3030
multiple?: false;
@@ -101,10 +101,10 @@ export function SelectRoot<
101101

102102
const onChange = (value: Option[]) => {
103103
if (local.multiple) {
104-
local.onChange?.(value as any);
104+
local.onChange?.((value ?? []) as any);
105105
} else {
106106
// use `null` as "no value" because `undefined` mean the component is "uncontrolled".
107-
local.onChange?.((value[0] ?? null) as any);
107+
local.onChange?.((value[0] ?? null) as any); // TODO: maybe return undefined? breaking change!
108108
}
109109
};
110110

packages/core/src/skeleton/skeleton-root.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ export interface SkeletonRootCommonProps<T extends HTMLElement = HTMLElement> {
4646

4747
export interface SkeletonRootRenderProps extends SkeletonRootCommonProps {
4848
role: "group";
49-
"data-animate": boolean;
50-
"data-visible": boolean;
49+
"data-animate": boolean | undefined;
50+
"data-visible": boolean | undefined;
5151
}
5252

5353
export type SkeletonRootProps<
@@ -82,8 +82,8 @@ export function Skeleton<T extends ValidComponent = "div">(
8282
<Polymorphic<SkeletonRootRenderProps>
8383
as="div"
8484
role="group"
85-
data-animate={local.animate}
86-
data-visible={local.visible}
85+
data-animate={local.animate || undefined}
86+
data-visible={local.visible || undefined}
8787
style={combineStyle(
8888
{
8989
"border-radius": local.circle

packages/core/src/text-field/text-field-root.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
OverrideComponentProps,
32
type ValidationState,
43
access,
54
createGenerateId,
@@ -108,8 +107,12 @@ export function TextFieldRoot<T extends ValidComponent = "div">(
108107
FORM_CONTROL_PROP_NAMES,
109108
);
110109

110+
// Disable reactivity to only track controllability on first run
111+
// Our current implementation breaks with undefined (stops tracking controlled value)
112+
const initialValue = local.value;
113+
111114
const [value, setValue] = createControllableSignal({
112-
value: () => local.value,
115+
value: () => (initialValue === undefined ? undefined : local.value ?? ""),
113116
defaultValue: () => local.defaultValue,
114117
onChange: (value) => local.onChange?.(value),
115118
});

0 commit comments

Comments
 (0)