Last active
April 4, 2023 00:39
-
-
Save nestarz/86ebbccc2d02f4b2a2a556833edf42b0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} /> | |
); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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