UI: fixup Select component design & api
This commit is contained in:
@@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user