Skip to content

Instantly share code, notes, and snippets.

@nestarz
Last active April 4, 2023 00:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nestarz/86ebbccc2d02f4b2a2a556833edf42b0 to your computer and use it in GitHub Desktop.
Save nestarz/86ebbccc2d02f4b2a2a556833edf42b0 to your computer and use it in GitHub Desktop.
import { h } from "preact";
import { createContext, ComponentChildren } from "preact";
import { useContext } from "preact/hooks";
import { Signal } from "@preact/signals";
interface ComboboxProps {
children: ComponentChildren;
value: string | Signal<string>;
onChange: (value: string | null) => void;
nullable?: boolean;
freeSolo?: boolean;
}
interface ComboboxInputProps {
onChange: (event: Event) => void;
displayValue: (value: string | null) => string;
}
interface ComboboxOptionsProps {
children: ComponentChildren;
}
interface ComboboxOptionProps {
children: ComponentChildren;
value: string;
}
const ComboboxContext = createContext<{
value: string | Signal<string>;
onChange: (value: string | null) => void;
nullable?: boolean;
freeSolo?: boolean;
}>({
value: "",
nullable: false,
freeSolo: false,
onChange: () => {},
});
export const Combobox = ({
value,
onChange,
nullable,
freeSolo,
...props
}: ComboboxProps) => {
return (
<ComboboxContext.Provider value={{ value, nullable, freeSolo, onChange }}>
<div role="combobox" {...props} />
</ComboboxContext.Provider>
);
};
const isNullOrUndefined = (v) => v === null || v === undefined;
export const ComboboxInput = ({
onChange,
displayValue,
...props
}: ComboboxInputProps) => {
const { value, nullable, ...c } = useContext(ComboboxContext);
const getValue = (v) => (v?.props ? v.value : v);
const onBlur = (e) => {
const newValue = getValue(value);
e.target.value = isNullOrUndefined(newValue) ? "" : newValue;
};
const handleChange = (e) => {
const currValue = e.target.value;
onChange(e);
if ((c.freeSolo || nullable) && currValue === "") c.onChange(null);
else if (c.freeSolo) c.onChange(currValue);
};
return (
<input
type="text"
value={
displayValue?.(getValue(value)) ??
(isNullOrUndefined(getValue(value)) ? "" : value)
}
onChange={handleChange}
onInput={handleChange}
onBlur={onBlur}
autoComplete="off"
{...props}
/>
);
};
export const ComboboxOptions = ({ ...props }: ComboboxOptionsProps) => {
return <ul role="listbox" {...props} />;
};
export const ComboboxOption = ({ value, ...props }: ComboboxOptionProps) => {
const { onChange } = useContext(ComboboxContext);
const handleChange = (e: Event) => {
onChange(value);
e.target?.blur();
};
return (
<button type="button" role="option" onClick={handleChange} {...props} />
);
};
import { h } from "preact";
import clsx from "clsx";
import {
Combobox as Combox,
ComboboxInput,
ComboboxOption,
ComboboxOptions,
} from "./Combobox.tsx";
export const Combobox = ({
className,
value,
onChange,
onInputChange,
options,
nullable,
freeSolo,
displayValue,
}) => {
return (
<Combox
className={clsx(className, "group relative")}
value={value}
onChange={onChange}
nullable={nullable}
freeSolo={freeSolo}
>
<div className="relative flex items-center justify-center w-full cursor-default overflow-hidden rounded-lg bg-white text-left shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-teal-300 sm:text-sm">
<ComboboxInput
className="w-full border-none py-2 pl-3 text-sm leading-5 text-gray-900 focus:ring-0 outline-0"
onChange={onInputChange}
displayValue={displayValue}
/>
<div tabIndex={0}>
<svg
viewBox="0 0 10 10"
xmlns="http://www.w3.org/2000/svg"
className="stroke-black stroke-1 w-5 h-5 fill-none pr-2"
>
<path d="M1 3 L5 7 L9 3" />
</svg>
</div>
</div>
<ComboboxOptions className="hidden group-focus-within:(absolute min-w-max z-10 flex flex-col mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm)">
{(options.props ? options.value : options)?.map((item) => (
<ComboboxOption
className="relative flex items-start cursor-default select-none py-2 pl-6 pr-4 text-gray-900 cursor-pointer hover:(bg-slate-900 text-white)"
key={item}
value={item}
>
{displayValue?.(item) ?? item}
</ComboboxOption>
))}
</ComboboxOptions>
</Combox>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment