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,
} from "solid-js";
import { Portal } from "solid-js/web";
import cx from "classnames";
import { Label } from "../base/label";
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 {
value: string;
label: string;
disabled?: boolean;
}
interface SelectInputpProps {
@@ -22,7 +30,7 @@ interface SelectInputpProps {
selectProps: JSX.InputHTMLAttributes<HTMLSelectElement>;
options: Option[];
label: JSX.Element;
altLabel?: JSX.Element;
labelProps?: InputLabelProps;
helperText?: JSX.Element;
error?: string;
required?: boolean;
@@ -36,6 +44,7 @@ interface SelectInputpProps {
disabled?: boolean;
placeholder?: string;
multiple?: boolean;
loading?: boolean;
}
export function SelectInput(props: SelectInputpProps) {
@@ -61,6 +70,7 @@ export function SelectInput(props: SelectInputpProps) {
});
},
}),
offset({ mainAxis: 2 }),
shift(),
flip(),
hide({
@@ -114,34 +124,61 @@ export function SelectInput(props: SelectInputpProps) {
return (
<>
<label
class={cx("form-control w-full", props.class)}
aria-disabled={props.disabled}
<FieldLayout
error={props.error && <InputError error={props.error} />}
label={
<InputLabel
description={""}
required={props.required}
{...props.labelProps}
>
<div class="label">
<Label label={props.label} required={props.required} />
<span class="label-text-alt block">{props.altLabel}</span>
</div>
{props.label}
</InputLabel>
}
field={
<>
<InputBase
error={!!props.error}
disabled={props.disabled}
required={props.required}
class="!justify-start"
divRef={setReference}
inputElem={
<button
type="button"
class="select select-bordered flex items-center gap-2"
ref={setReference}
formnovalidate
// TODO: Keyboard acessibililty
// Currently the popover only opens with onClick
// Options are not selectable with keyboard
tabIndex={-1}
onClick={() => {
const popover = document.getElementById(_id);
if (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}
// popovertargetaction="toggle"
>
<Show when={props.adornment && props.adornment.position === "start"}>
<Show
when={
props.adornment && props.adornment.position === "start"
}
>
{props.adornment?.content}
</Show>
{props.inlineLabel}
<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"}>
{(item) => (
<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>
)}
</For>
</Show>
</div>
<select
class="hidden"
multiple
{...props.selectProps}
required={props.required}
<Show
when={props.adornment && props.adornment.position === "end"}
>
<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}
</Show>
<Icon icon="CaretDown" class="ml-auto mr-2"></Icon>
</button>
}
/>
</>
}
/>
<Portal mount={document.body}>
<div
id={_id}
@@ -201,39 +234,34 @@ export function SelectInput(props: SelectInputpProps) {
top: `${position.y ?? 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">
<Show when={!props.loading} fallback={"Loading ...."}>
<For each={props.options}>
{(opt) => (
<>
<li>
<button
<Button
variant="ghost"
class="!justify-start"
onClick={() => handleClickOption(opt)}
disabled={opt.disabled}
classList={{
active: getValues().includes(opt.value),
active:
!opt.disabled && getValues().includes(opt.value),
}}
>
{opt.label}
</button>
</Button>
</li>
</>
)}
</For>
</Show>
</ul>
</div>
</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
"aria-readonly:cursor-no-drop",
props.class
props.class,
)}
classList={{
[cx("!border !border-semantic-1 !outline-semantic-1")]: !!props.error,
@@ -160,7 +160,7 @@ interface InputErrorProps {
export const InputError = (props: InputErrorProps) => {
const [typoClasses, rest] = splitProps(
props.typographyProps || { class: "" },
["class"]
["class"],
);
return (
<Typography