Skip to content

Instantly share code, notes, and snippets.

@nestarz
Last active April 4, 2023 00:39
Show Gist options
  • Save nestarz/86ebbccc2d02f4b2a2a556833edf42b0 to your computer and use it in GitHub Desktop.
Save nestarz/86ebbccc2d02f4b2a2a556833edf42b0 to your computer and use it in GitHub Desktop.

Revisions

  1. nestarz revised this gist Apr 4, 2023. 1 changed file with 102 additions and 0 deletions.
    102 changes: 102 additions & 0 deletions Combobox.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,102 @@
    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} />
    );
    };
  2. nestarz created this gist Apr 4, 2023.
    57 changes: 57 additions & 0 deletions combox.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,57 @@
    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>
    );
    };