Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/components/ui/input/InputSelector.tsx
T

172 lines
5.8 KiB
TypeScript

import { Ref, forwardRef, useImperativeHandle, useRef, ReactNode, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { Colors } from "~/application/enums/shared/Colors";
import ColorBadge from "../badges/ColorBadge";
import HintTooltip from "../tooltips/HintTooltip";
import EntityIcon from "~/components/layouts/icons/EntityIcon";
import { Select } from "~/ui/components/Select";
import { PlusIcon } from "~/components/ui/icons/vendor";
export interface RefInputSelector {
focus: () => void;
}
interface Props {
name?: string;
title?: string;
value?: string | number | undefined;
defaultValue?: string | number | undefined;
disabled?: boolean;
options: { name: string | ReactNode; value: string | number | undefined; color?: Colors; disabled?: boolean }[];
setValue?: React.Dispatch<React.SetStateAction<string | number | undefined>>;
className?: string;
withSearch?: boolean;
withLabel?: boolean;
withColors?: boolean;
selectPlaceholder?: string;
onNew?: () => void;
onNewRoute?: string;
required?: boolean;
help?: string;
hint?: ReactNode;
icon?: string;
borderless?: boolean;
darkMode?: boolean;
autoFocus?: boolean;
readOnly?: boolean;
isLoading?: boolean;
}
const InputSelector = (
{
name,
title,
value,
defaultValue,
options,
disabled,
setValue,
className,
// withSearch — Subframe Select has built-in typeahead; legacy in-list search dropped.
withLabel = true,
withColors = false,
selectPlaceholder,
onNew,
required,
onNewRoute,
help,
hint,
icon,
borderless,
// darkMode — handled by the global Subframe theme.
autoFocus,
readOnly,
isLoading,
}: Props,
ref: Ref<RefInputSelector>
) => {
const { t } = useTranslation();
const container = useRef<HTMLDivElement>(null);
const [selected, setSelected] = useState<{ name: string | ReactNode; value: string | number | undefined; color?: Colors }>();
useEffect(() => {
const found = options?.find((f) => f.value === value);
setSelected(found);
}, [options, value]);
useEffect(() => {
if (selected && setValue && value !== selected?.value) {
setValue(selected?.value);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selected]);
useImperativeHandle(ref, () => ({ focus }));
function focus() {
setTimeout(() => {
const button = container.current?.querySelector("button");
button?.focus();
button?.click();
}, 1);
}
const showLabel = Boolean(withLabel && title);
const newLabel = String(t("shared.new"));
// Rich label content rendered into Subframe's label slot (spans only → inline-safe).
const labelContent: ReactNode = showLabel ? (
<span className="flex w-full items-center justify-between gap-2">
<span className="flex items-center space-x-1 truncate">
<span className="truncate">
{title}
{required && <span className="ml-1 text-red-600">*</span>}
</span>
{help && <HintTooltip text={help} />}
</span>
{hint}
</span>
) : null;
const placeholderText = isLoading ? `${t("shared.loading")}...` : `${selectPlaceholder ?? t("shared.select")}...`;
return (
<div className={className}>
<div className="flex items-end gap-2">
<Select
ref={container}
className="w-full max-w-none"
name={name}
value={value !== undefined ? value.toString() : undefined}
defaultValue={defaultValue !== undefined ? defaultValue.toString() : undefined}
onValueChange={(e) => {
const found = options?.find((f) => f.value?.toString() === e);
setSelected(found);
}}
disabled={disabled || readOnly}
required={required}
borderless={borderless}
showLabel={showLabel}
label={labelContent}
placeholder={placeholderText}
icon={icon ? <EntityIcon className="text-muted-foreground h-5 w-5" icon={icon} /> : undefined}
>
{options.map((item, idx) => (
<Select.Item key={idx} disabled={item.disabled} value={item.value?.toString() ?? ""}>
<div className="flex items-center space-x-2">
{withColors && item.color !== undefined && <ColorBadge color={item.color} />}
<div className="truncate">{item.name}</div>
</div>
</Select.Item>
))}
</Select>
{onNew && (
<button
aria-label={newLabel}
title={newLabel}
type="button"
onClick={onNew}
autoFocus={autoFocus}
className="border-border bg-secondary hover:bg-secondary/90 text-foreground/80 focus-visible:border-border focus-visible:ring-ring relative inline-flex h-9 w-10 shrink-0 items-center justify-center rounded-md border text-sm font-medium focus:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2"
>
<PlusIcon className="h-4 w-4" aria-hidden="true" />
</button>
)}
{onNewRoute && (
<Link
to={onNewRoute}
aria-label={newLabel}
title={newLabel}
className="border-border bg-secondary hover:bg-secondary/90 text-foreground/80 focus-visible:border-border focus-visible:ring-ring relative inline-flex h-9 w-10 shrink-0 items-center justify-center rounded-md border text-sm font-medium focus:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2"
>
<PlusIcon className="h-4 w-4" aria-hidden="true" />
</Link>
)}
</div>
</div>
);
};
export default forwardRef(InputSelector);