Skip to content

Commit

Permalink
fix: combobox
Browse files Browse the repository at this point in the history
  • Loading branch information
riccardoperra committed Jan 7, 2024
1 parent e499f33 commit 1377384
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 39 deletions.
17 changes: 16 additions & 1 deletion packages/kit/src/components/Combobox/Combobox.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,18 @@ export const comboboxInput = style({
appearance: "none",
background: "transparent",
outline: "none",
border: 0,
display: "inline-flex",
minWidth: 0,
flex: 1,
width: "100%",
});

export const comboboxTrigger = style({
border: 0,
background: "transparent",
outline: "none",
padding: 0,
color: themeVars.foreground,
});

export const itemIndicator = style({
Expand All @@ -194,3 +203,9 @@ export const itemIndicator = style({
width: selectThemeVars.indicatorSize,
flexShrink: 0,
});

export const comboboxInputWorkaround = style({
width: 0,
height: 0,
opacity: 0,
});
127 changes: 91 additions & 36 deletions packages/kit/src/components/Combobox/Combobox.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { Combobox as KCombobox } from "@kobalte/core";
import { Combobox as KCombobox, createControllableBooleanSignal } from "@kobalte/core";
import { Accessor, JSX, JSXElement, Show, createSignal, splitProps } from "solid-js";
import { CheckIcon } from "../../icons/CheckIcon";
import { SelectorIcon } from "../../icons/SelectorIcon";
import { mergeClasses } from "../../utils/css";
import { BaseFieldProps, createBaseFieldProps } from "../Field/createBaseFieldProps";
import { highlight } from "../../utils/highlight/highlight";
import {
createFieldErrorMessageProps,
FieldWithErrorMessageSupport,
createFieldErrorMessageProps,
} from "../Field/FieldError/createFieldErrorMessageProps";
import { createFieldLabelProps } from "../Field/FieldLabel/createFieldLabelProps";
import { createFieldMessageProps } from "../Field/FieldMessage/createFieldMessageProps";
import { BaseFieldProps, createBaseFieldProps } from "../Field/createBaseFieldProps";
import * as styles from "./Combobox.css";
import { highlight } from "../../utils/highlight/highlight";

void highlight;

Expand All @@ -29,10 +29,6 @@ export type ComboboxProps<Option, OptGroup = never> = KCombobox.ComboboxRootProp
valueComponent?: (state: Accessor<Option>) => JSXElement;
};

function ComboboxContent(props: KCombobox.ComboboxContentProps) {
return <KCombobox.Content class={styles.content} {...props} />;
}

export function ComboboxItem<T>(
props: KCombobox.ComboboxItemProps & {
itemLabel?: (item: T) => JSXElement;
Expand All @@ -54,44 +50,96 @@ export function ComboboxItem<T>(
export function Combobox<Option, OptGroup = never>(
props: ComboboxProps<Option, OptGroup>,
) {
const [local, internal, others] = splitProps(
props,
[
"aria-label",
"children",
"size",
"theme",
"errorMessage",
"description",
"label",
"itemLabel",
"valueComponent",
],
["options", "value"],
);
const [local, others] = splitProps(props, [
"aria-label",
"size",
"theme",
"errorMessage",
"description",
"label",
"itemLabel",
"value",
"onChange",
"open",
"onOpenChange",
"onInputChange",
]);
const baseFieldProps = createBaseFieldProps(props);
const labelProps = createFieldLabelProps<"label">({});
const descriptionProps = createFieldMessageProps({});
const errorProps = createFieldErrorMessageProps(props);

const [open, setOpen] = createControllableBooleanSignal({
value: () => local.open,
defaultValue: () => local.open,
onChange(value) {
local.onOpenChange?.(value);
},
});

const [textValue, setTextValue] = createSignal("");
let control!: HTMLInputElement;

const onInputFocus = (event: FocusEvent) => {
if (isChangingValue) {
setOpen(false);
} else {
setOpen(true);
}
};

let controlWorkaround!: HTMLInputElement;

type TriggerMode = KCombobox.ComboboxRootOptions<any, any>["triggerMode"];

const handleOpenChange = (open: boolean, mode: TriggerMode) => {
setOpen(open);
if (!open && mode === "focus") {
// If open is false and mode is focus, then portal will be closed. Currently
// there is a behavior where input is automatically focused triggering the modal opening.
// As a workaround, i'm focusing another input in order to don't retrigger the portal content opening.
setTimeout(() => {
controlWorkaround.focus();
setOpen(false);
});
}
};

const handleOnChange = (value: Option & Option[]) => {
local.onChange?.(value as Option & Option[]);
// On handle change we set `isChangingValue` to true in order to force close the modal.
// Read `onInputFocus`;
isChangingValue = true;
setTimeout(() => (isChangingValue = false), 0);
};

let isChangingValue = false;

return (
<KCombobox.Root
data-cui="combobox"
{...(others as Record<string, unknown>)}
options={internal.options}
value={internal.value}
class={styles.field}
itemComponent={itemProps => (
<ComboboxItem
item={itemProps.item}
textValue={textValue()}
itemLabel={local.itemLabel}
textValue={textValue()}
/>
)}
open={open()}
onInputChange={text => {
setTextValue(text);
local.onInputChange?.(text);
}}
onChange={handleOnChange}
onOpenChange={(isOpen, mode) => {
handleOpenChange(isOpen, mode);
local.onOpenChange?.(isOpen, mode);
}}
{...(others as Record<string, unknown>)}
triggerMode={"input"}
>
<Show when={local.label} keyed={false}>
<Show when={local.label}>
<KCombobox.Label {...labelProps}>{local.label}</KCombobox.Label>
</Show>

Expand All @@ -101,31 +149,38 @@ export function Combobox<Option, OptGroup = never>(
aria-label={local["aria-label"]}
>
<KCombobox.Input
onInput={el => setTextValue(el.currentTarget.value)}
onFocus={onInputFocus}
ref={control}
class={styles.comboboxInput}
/>
<input
aria-hidden="true"
ref={controlWorkaround}
class={styles.comboboxInputWorkaround}
/>

<KCombobox.Trigger>
<KCombobox.Icon>
<SelectorIcon />
</KCombobox.Icon>
</KCombobox.Trigger>
</KCombobox.Control>

<Show when={local.description} keyed={false}>
<KCombobox.Portal>
<KCombobox.Content class={styles.content}>
<KCombobox.Listbox />
</KCombobox.Content>
</KCombobox.Portal>
<Show when={local.description}>
<KCombobox.Description {...descriptionProps}>
{local.description}
</KCombobox.Description>
</Show>
<Show when={local.errorMessage} keyed={false}>
<Show when={local.errorMessage}>
<KCombobox.ErrorMessage {...errorProps}>
{local.errorMessage}
</KCombobox.ErrorMessage>
</Show>
<KCombobox.Portal>
<ComboboxContent>
<KCombobox.Listbox />
</ComboboxContent>
</KCombobox.Portal>
</KCombobox.Root>
);
}
2 changes: 0 additions & 2 deletions packages/storybook/src/stories/Combobox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ export default meta;
export const ComboboxStory: Story = {
name: "Combobox",
args: {
triggerMode: "focus",
// TODO is this correct?
"aria-label": "Fruit",
placeholder: "Insert a value...",
label: "Input label",
Expand Down

0 comments on commit 1377384

Please sign in to comment.