diff --git a/apps/docs/app.config.ts b/apps/docs/app.config.ts index 8627a488..c586bf91 100644 --- a/apps/docs/app.config.ts +++ b/apps/docs/app.config.ts @@ -113,8 +113,8 @@ export default defineConfig({ }, prerender: { routes: ["/docs/core/overview/introduction"], - crawlLinks: true - } + crawlLinks: true, + }, }, extensions: ["mdx", "md"], diff --git a/biome.json b/biome.json index 7233a3e9..2c60fb59 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,7 @@ { "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "files": { - "ignore": ["./tsconfig.json", "*/netlify/*"] + "ignore": ["./tsconfig.json", "*/netlify/*", "**/package.json"] }, "vcs": { "enabled": true, diff --git a/packages/core/src/combobox/combobox-base.tsx b/packages/core/src/combobox/combobox-base.tsx index 776aea1c..ae36f2bc 100644 --- a/packages/core/src/combobox/combobox-base.tsx +++ b/packages/core/src/combobox/combobox-base.tsx @@ -596,9 +596,8 @@ export function ComboboxBase< focusStrategy: FocusStrategy | boolean, triggerMode?: ComboboxTriggerMode, ) => { - // If set to only open manually, ignore other triggers - if (local.triggerMode === 'manual' && triggerMode !== 'manual') { + if (local.triggerMode === "manual" && triggerMode !== "manual") { return; } diff --git a/packages/core/src/combobox/combobox-content.tsx b/packages/core/src/combobox/combobox-content.tsx index ab33365e..b3d8561f 100644 --- a/packages/core/src/combobox/combobox-content.tsx +++ b/packages/core/src/combobox/combobox-content.tsx @@ -82,11 +82,14 @@ export function ComboboxContent( "onFocusOutside", ]); - const close = () => { + const dismiss = () => { context.resetInputValue( context.listState().selectionManager().selectedKeys(), ); context.close(); + setTimeout(() => { + context.close(); + }); }; const onFocusOutside = (e: FocusOutsideEvent) => { @@ -165,7 +168,7 @@ export function ComboboxContent( local.style, )} onFocusOutside={onFocusOutside} - onDismiss={close} + onDismiss={dismiss} {...context.dataset()} {...others} /> diff --git a/packages/core/src/combobox/combobox-input.tsx b/packages/core/src/combobox/combobox-input.tsx index 01730b81..fd21950b 100644 --- a/packages/core/src/combobox/combobox-input.tsx +++ b/packages/core/src/combobox/combobox-input.tsx @@ -37,6 +37,7 @@ export interface ComboboxInputCommonProps< ref: T | ((el: T) => void); onInput: JSX.EventHandlerUnion; onKeyDown: JSX.EventHandlerUnion; + onClick: JSX.EventHandlerUnion; onFocus: JSX.EventHandlerUnion; onBlur: JSX.EventHandlerUnion; onTouchEnd: JSX.EventHandlerUnion; @@ -93,6 +94,7 @@ export function ComboboxInput( [ "ref", "disabled", + "onClick", "onInput", "onKeyDown", "onFocus", @@ -113,6 +115,14 @@ export function ComboboxInput( const { fieldProps } = createFormControlField(formControlFieldProps); + const onClick: JSX.EventHandlerUnion = (e) => { + callHandler(e, local.onClick); + + if (context.triggerMode() === "focus" && !context.isOpen()) { + context.open(false, "focus"); + } + }; + const onInput: JSX.EventHandlerUnion = (e) => { callHandler(e, local.onInput); @@ -315,6 +325,7 @@ export function ComboboxInput( aria-required={formControlContext.isRequired() || undefined} aria-disabled={formControlContext.isDisabled() || undefined} aria-readonly={formControlContext.isReadOnly() || undefined} + onClick={onClick} onInput={onInput} onKeyDown={onKeyDown} onFocus={onFocus} diff --git a/packages/core/src/combobox/combobox-root.tsx b/packages/core/src/combobox/combobox-root.tsx index cbe6618e..4a5f14b5 100644 --- a/packages/core/src/combobox/combobox-root.tsx +++ b/packages/core/src/combobox/combobox-root.tsx @@ -15,7 +15,7 @@ import { export interface ComboboxSingleSelectionOptions { /** The controlled value of the combobox. */ - value?: T; + value?: T | null; /** * The value of the combobox when initially rendered. @@ -24,7 +24,7 @@ export interface ComboboxSingleSelectionOptions { defaultValue?: T; /** Event handler called when the value changes. */ - onChange?: (value: T) => void; + onChange?: (value: T | null) => void; /** Whether the combobox allow multiple selection. */ multiple?: false; @@ -100,10 +100,10 @@ export function ComboboxRoot< const onChange = (value: Option[]) => { if (local.multiple) { - local.onChange?.(value as any); + local.onChange?.((value ?? []) as any); } else { // use `null` as "no value" because `undefined` mean the component is "uncontrolled". - local.onChange?.((value[0] ?? null) as any); + local.onChange?.((value[0] ?? null) as any); // TODO: maybe return undefined? breaking change! } }; diff --git a/packages/core/src/menu/menu-content-base.tsx b/packages/core/src/menu/menu-content-base.tsx index b788cca6..a6005673 100644 --- a/packages/core/src/menu/menu-content-base.tsx +++ b/packages/core/src/menu/menu-content-base.tsx @@ -290,6 +290,8 @@ export function MenuContentBase( createEffect(() => onCleanup(context.registerContentId(local.id!))); + onCleanup(() => context.setContentRef(undefined)); + const commonAttributes: Omit = { ref: mergeRefs((el) => { diff --git a/packages/core/src/menu/menu-context.tsx b/packages/core/src/menu/menu-context.tsx index b3cfd3ec..17136c37 100644 --- a/packages/core/src/menu/menu-context.tsx +++ b/packages/core/src/menu/menu-context.tsx @@ -25,7 +25,7 @@ export interface MenuContextValue { triggerId: Accessor; contentId: Accessor; setTriggerRef: (el: HTMLElement) => void; - setContentRef: (el: HTMLElement) => void; + setContentRef: (el: HTMLElement | undefined) => void; open: (focusStrategy: FocusStrategy | boolean) => void; close: (recursively?: boolean) => void; toggle: (focusStrategy: FocusStrategy | boolean) => void; diff --git a/packages/core/src/navigation-menu/navigation-menu-root.tsx b/packages/core/src/navigation-menu/navigation-menu-root.tsx index 2b43286f..91097010 100644 --- a/packages/core/src/navigation-menu/navigation-menu-root.tsx +++ b/packages/core/src/navigation-menu/navigation-menu-root.tsx @@ -168,6 +168,12 @@ export function NavigationMenuRoot( element: () => viewportRef() ?? null, }); + createEffect(() => { + if (!viewportPresent()) { + context.setPreviousMenu(undefined); + } + }); + const context: NavigationMenuContextValue = { dataset, delayDuration: () => local.delayDuration!, diff --git a/packages/core/src/navigation-menu/navigation-menu-trigger.tsx b/packages/core/src/navigation-menu/navigation-menu-trigger.tsx index 7e047bf1..dd8f44f2 100644 --- a/packages/core/src/navigation-menu/navigation-menu-trigger.tsx +++ b/packages/core/src/navigation-menu/navigation-menu-trigger.tsx @@ -69,8 +69,10 @@ export function NavigationMenuTrigger( if (context.dataset()["data-expanded"] === "") return; timeoutId = window.setTimeout(() => { - context.setAutoFocusMenu(true); menuContext?.triggerRef()?.focus(); + setTimeout(() => { + context.setAutoFocusMenu(true); + }); }, context.delayDuration()); }; diff --git a/packages/core/src/navigation-menu/navigation-menu-viewport.tsx b/packages/core/src/navigation-menu/navigation-menu-viewport.tsx index 331330e4..090262a6 100644 --- a/packages/core/src/navigation-menu/navigation-menu-viewport.tsx +++ b/packages/core/src/navigation-menu/navigation-menu-viewport.tsx @@ -117,10 +117,12 @@ export function NavigationMenuViewport( ); const height = createMemo((prev) => { + if (ref() === undefined || !context.viewportPresent()) return undefined; if (size.height() === 0) return prev; return size.height(); }); const width = createMemo((prev) => { + if (ref() === undefined || !context.viewportPresent()) return undefined; if (size.width() === 0) return prev; return size.width(); }); diff --git a/packages/core/src/number-field/number-field-input.tsx b/packages/core/src/number-field/number-field-input.tsx index 1e9e0ad1..07d6ce66 100644 --- a/packages/core/src/number-field/number-field-input.tsx +++ b/packages/core/src/number-field/number-field-input.tsx @@ -169,7 +169,7 @@ export function NumberFieldInput( > as={local.as || "input"} value={ - Number.isNaN(context.rawValue()) + Number.isNaN(context.rawValue()) || context.value() === undefined ? "" : context.formatNumber(context.rawValue()) } diff --git a/packages/core/src/number-field/number-field-root.tsx b/packages/core/src/number-field/number-field-root.tsx index fcb5de71..26421dc9 100644 --- a/packages/core/src/number-field/number-field-root.tsx +++ b/packages/core/src/number-field/number-field-root.tsx @@ -185,6 +185,9 @@ export function NumberFieldRoot( return new NumberFormatter(locale(), local.formatOptions); }); + const formatNumber = (number: number) => + local.format ? numberFormatter().format(number) : number.toString(); + const parseRawValue = (value: string | number | undefined) => local.format && typeof value !== "number" ? numberParser().parse(value ?? "") @@ -203,14 +206,12 @@ export function NumberFieldRoot( value: () => local.value, defaultValue: () => local.defaultValue ?? local.rawValue, onChange: (value) => { - local.onChange?.( - typeof value === "number" ? numberFormatter().format(value) : value, - ); + local.onChange?.(typeof value === "number" ? formatNumber(value) : value); local.onRawValueChange?.(parseRawValue(value)); }, }); - local.onRawValueChange?.(parseRawValue(value())); + if (value() !== undefined) local.onRawValueChange?.(parseRawValue(value())); function isAllowedInput(char: string): boolean { if (local.allowedInput !== undefined) return local.allowedInput.test(char); @@ -267,7 +268,7 @@ export function NumberFieldRoot( setValue, rawValue: () => parseRawValue(value()), generateId: createGenerateId(() => access(formControlProps.id)!), - formatNumber: (number: number) => numberFormatter().format(number), + formatNumber, format: () => { if (!local.format) return; let rawValue = context.rawValue(); diff --git a/packages/core/src/select/select-root.tsx b/packages/core/src/select/select-root.tsx index 334feb84..8a209230 100644 --- a/packages/core/src/select/select-root.tsx +++ b/packages/core/src/select/select-root.tsx @@ -15,7 +15,7 @@ import { export interface SelectSingleSelectionOptions { /** The controlled value of the select. */ - value?: T; + value?: T | null; /** * The value of the select when initially rendered. @@ -24,7 +24,7 @@ export interface SelectSingleSelectionOptions { defaultValue?: T; /** Event handler called when the value changes. */ - onChange?: (value: T) => void; + onChange?: (value: T | null) => void; /** Whether the select allow multiple selection. */ multiple?: false; @@ -101,10 +101,10 @@ export function SelectRoot< const onChange = (value: Option[]) => { if (local.multiple) { - local.onChange?.(value as any); + local.onChange?.((value ?? []) as any); } else { // use `null` as "no value" because `undefined` mean the component is "uncontrolled". - local.onChange?.((value[0] ?? null) as any); + local.onChange?.((value[0] ?? null) as any); // TODO: maybe return undefined? breaking change! } }; diff --git a/packages/core/src/skeleton/skeleton-root.tsx b/packages/core/src/skeleton/skeleton-root.tsx index 61bcf26d..bb7cc13b 100644 --- a/packages/core/src/skeleton/skeleton-root.tsx +++ b/packages/core/src/skeleton/skeleton-root.tsx @@ -46,8 +46,8 @@ export interface SkeletonRootCommonProps { export interface SkeletonRootRenderProps extends SkeletonRootCommonProps { role: "group"; - "data-animate": boolean; - "data-visible": boolean; + "data-animate": boolean | undefined; + "data-visible": boolean | undefined; } export type SkeletonRootProps< @@ -82,8 +82,8 @@ export function Skeleton( as="div" role="group" - data-animate={local.animate} - data-visible={local.visible} + data-animate={local.animate || undefined} + data-visible={local.visible || undefined} style={combineStyle( { "border-radius": local.circle diff --git a/packages/core/src/text-field/text-field-root.tsx b/packages/core/src/text-field/text-field-root.tsx index 64a22806..f6295ca6 100644 --- a/packages/core/src/text-field/text-field-root.tsx +++ b/packages/core/src/text-field/text-field-root.tsx @@ -1,5 +1,4 @@ import { - OverrideComponentProps, type ValidationState, access, createGenerateId, @@ -108,8 +107,12 @@ export function TextFieldRoot( FORM_CONTROL_PROP_NAMES, ); + // Disable reactivity to only track controllability on first run + // Our current implementation breaks with undefined (stops tracking controlled value) + const initialValue = local.value; + const [value, setValue] = createControllableSignal({ - value: () => local.value, + value: () => (initialValue === undefined ? undefined : local.value ?? ""), defaultValue: () => local.defaultValue, onChange: (value) => local.onChange?.(value), });