UI: fixup Select component design & api

This commit is contained in:
Johannes Kirschbauer
2024-12-20 18:13:06 +01:00
parent fd2ba1e220
commit 94ab273d74
2 changed files with 138 additions and 110 deletions

View File

@@ -7,14 +7,22 @@ import {
createMemo, createMemo,
} from "solid-js"; } from "solid-js";
import { Portal } from "solid-js/web"; import { Portal } from "solid-js/web";
import cx from "classnames";
import { Label } from "../base/label";
import { useFloating } from "../base"; import { useFloating } from "../base";
import { autoUpdate, flip, hide, shift, size } from "@floating-ui/dom"; import { autoUpdate, flip, hide, offset, shift, size } from "@floating-ui/dom";
import { Button } from "@/src/components/button";
import {
InputBase,
InputError,
InputLabel,
InputLabelProps,
} from "@/src/components/inputBase";
import { FieldLayout } from "./layout";
import Icon from "@/src/components/icon";
export interface Option { export interface Option {
value: string; value: string;
label: string; label: string;
disabled?: boolean;
} }
interface SelectInputpProps { interface SelectInputpProps {
@@ -22,7 +30,7 @@ interface SelectInputpProps {
selectProps: JSX.InputHTMLAttributes<HTMLSelectElement>; selectProps: JSX.InputHTMLAttributes<HTMLSelectElement>;
options: Option[]; options: Option[];
label: JSX.Element; label: JSX.Element;
altLabel?: JSX.Element; labelProps?: InputLabelProps;
helperText?: JSX.Element; helperText?: JSX.Element;
error?: string; error?: string;
required?: boolean; required?: boolean;
@@ -36,6 +44,7 @@ interface SelectInputpProps {
disabled?: boolean; disabled?: boolean;
placeholder?: string; placeholder?: string;
multiple?: boolean; multiple?: boolean;
loading?: boolean;
} }
export function SelectInput(props: SelectInputpProps) { export function SelectInput(props: SelectInputpProps) {
@@ -61,6 +70,7 @@ export function SelectInput(props: SelectInputpProps) {
}); });
}, },
}), }),
offset({ mainAxis: 2 }),
shift(), shift(),
flip(), flip(),
hide({ hide({
@@ -114,34 +124,61 @@ export function SelectInput(props: SelectInputpProps) {
return ( return (
<> <>
<label <FieldLayout
class={cx("form-control w-full", props.class)} error={props.error && <InputError error={props.error} />}
aria-disabled={props.disabled} label={
<InputLabel
description={""}
required={props.required}
{...props.labelProps}
> >
<div class="label"> {props.label}
<Label label={props.label} required={props.required} /> </InputLabel>
<span class="label-text-alt block">{props.altLabel}</span> }
</div> field={
<>
<InputBase
error={!!props.error}
disabled={props.disabled}
required={props.required}
class="!justify-start"
divRef={setReference}
inputElem={
<button <button
type="button" // TODO: Keyboard acessibililty
class="select select-bordered flex items-center gap-2" // Currently the popover only opens with onClick
ref={setReference} // Options are not selectable with keyboard
formnovalidate tabIndex={-1}
onClick={() => { onClick={() => {
const popover = document.getElementById(_id); const popover = document.getElementById(_id);
if (popover) { if (popover) {
popover.togglePopover(); // Show or hide the popover popover.togglePopover(); // Show or hide the popover
} }
}} }}
// TODO: Use native popover once Webkti supports it within <form> type="button"
class="flex w-full items-center gap-2"
formnovalidate
// TODO: Use native popover once Webkit supports it within <form>
// popovertarget={_id} // popovertarget={_id}
// popovertargetaction="toggle" // popovertargetaction="toggle"
> >
<Show when={props.adornment && props.adornment.position === "start"}> <Show
when={
props.adornment && props.adornment.position === "start"
}
>
{props.adornment?.content} {props.adornment?.content}
</Show> </Show>
{props.inlineLabel} {props.inlineLabel}
<div class="flex cursor-default flex-row gap-2"> <div class="flex cursor-default flex-row gap-2">
<Show
when={
getValues() &&
getValues.length !== 1 &&
getValues()[0] !== ""
}
fallback={props.placeholder}
>
<For each={getValues()} fallback={"Select"}> <For each={getValues()} fallback={"Select"}>
{(item) => ( {(item) => (
<div class="rounded-xl bg-slate-800 px-4 py-1 text-sm text-white"> <div class="rounded-xl bg-slate-800 px-4 py-1 text-sm text-white">
@@ -171,25 +208,21 @@ export function SelectInput(props: SelectInputpProps) {
</div> </div>
)} )}
</For> </For>
</Show>
</div> </div>
<select <Show
class="hidden" when={props.adornment && props.adornment.position === "end"}
multiple
{...props.selectProps}
required={props.required}
> >
<For each={props.options}>
{({ label, value }) => (
<option value={value} selected={getValues().includes(value)}>
{label}
</option>
)}
</For>
</select>
<Show when={props.adornment && props.adornment.position === "end"}>
{props.adornment?.content} {props.adornment?.content}
</Show> </Show>
<Icon icon="CaretDown" class="ml-auto mr-2"></Icon>
</button> </button>
}
/>
</>
}
/>
<Portal mount={document.body}> <Portal mount={document.body}>
<div <div
id={_id} id={_id}
@@ -201,39 +234,34 @@ export function SelectInput(props: SelectInputpProps) {
top: `${position.y ?? 0}px`, top: `${position.y ?? 0}px`,
left: `${position.x ?? 0}px`, left: `${position.x ?? 0}px`,
}} }}
class="dropdown-content z-[1] rounded-b-box bg-base-100 shadow" class="z-[1] shadow"
> >
<ul class="menu flex max-h-96 flex-col gap-1 overflow-x-hidden overflow-y-scroll"> <ul class="menu flex max-h-96 flex-col gap-1 overflow-x-hidden overflow-y-scroll">
<Show when={!props.loading} fallback={"Loading ...."}>
<For each={props.options}> <For each={props.options}>
{(opt) => ( {(opt) => (
<> <>
<li> <li>
<button <Button
variant="ghost"
class="!justify-start"
onClick={() => handleClickOption(opt)} onClick={() => handleClickOption(opt)}
disabled={opt.disabled}
classList={{ classList={{
active: getValues().includes(opt.value), active:
!opt.disabled && getValues().includes(opt.value),
}} }}
> >
{opt.label} {opt.label}
</button> </Button>
</li> </li>
</> </>
)} )}
</For> </For>
</Show>
</ul> </ul>
</div> </div>
</Portal> </Portal>
<div class="label">
{props.helperText && (
<span class="label-text text-neutral">{props.helperText}</span>
)}
{props.error && (
<span class="label-text-alt font-bold text-error-700">
{props.error}
</span>
)}
</div>
</label>
</> </>
); );
} }

View File

@@ -55,7 +55,7 @@ export const InputBase = (props: InputBaseProps) => {
// Cursor // Cursor
"aria-readonly:cursor-no-drop", "aria-readonly:cursor-no-drop",
props.class props.class,
)} )}
classList={{ classList={{
[cx("!border !border-semantic-1 !outline-semantic-1")]: !!props.error, [cx("!border !border-semantic-1 !outline-semantic-1")]: !!props.error,
@@ -160,7 +160,7 @@ interface InputErrorProps {
export const InputError = (props: InputErrorProps) => { export const InputError = (props: InputErrorProps) => {
const [typoClasses, rest] = splitProps( const [typoClasses, rest] = splitProps(
props.typographyProps || { class: "" }, props.typographyProps || { class: "" },
["class"] ["class"],
); );
return ( return (
<Typography <Typography