mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
147 lines
5.9 KiB
TypeScript
147 lines
5.9 KiB
TypeScript
import type { FC, ReactNode, Ref, RefAttributes } from "react";
|
|
import { createContext, isValidElement } from "react";
|
|
import { faChevronDown } from "@fortawesome/pro-duotone-svg-icons";
|
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
import type { SelectProps as AriaSelectProps } from "react-aria-components";
|
|
import { Button as AriaButton, ListBox as AriaListBox, Select as AriaSelect, SelectValue as AriaSelectValue } from "react-aria-components";
|
|
import { Avatar } from "@/components/base/avatar/avatar";
|
|
import { HintText } from "@/components/base/input/hint-text";
|
|
import { Label } from "@/components/base/input/label";
|
|
import { cx } from "@/utils/cx";
|
|
import { isReactComponent } from "@/utils/is-react-component";
|
|
import { ComboBox } from "./combobox";
|
|
import { Popover } from "./popover";
|
|
import { SelectItem } from "./select-item";
|
|
|
|
export type SelectItemType = {
|
|
id: string;
|
|
label?: string;
|
|
avatarUrl?: string;
|
|
isDisabled?: boolean;
|
|
supportingText?: string;
|
|
icon?: FC | ReactNode;
|
|
};
|
|
|
|
export interface CommonProps {
|
|
hint?: string;
|
|
label?: string;
|
|
tooltip?: string;
|
|
size?: "sm" | "md";
|
|
placeholder?: string;
|
|
}
|
|
|
|
interface SelectProps extends Omit<AriaSelectProps<SelectItemType>, "children" | "items">, RefAttributes<HTMLDivElement>, CommonProps {
|
|
items?: SelectItemType[];
|
|
popoverClassName?: string;
|
|
placeholderIcon?: FC | ReactNode;
|
|
children: ReactNode | ((item: SelectItemType) => ReactNode);
|
|
}
|
|
|
|
interface SelectValueProps {
|
|
isOpen: boolean;
|
|
size: "sm" | "md";
|
|
isFocused: boolean;
|
|
isDisabled: boolean;
|
|
placeholder?: string;
|
|
ref?: Ref<HTMLButtonElement>;
|
|
placeholderIcon?: FC | ReactNode;
|
|
}
|
|
|
|
export const sizes = {
|
|
sm: { root: "py-2 px-3", shortcut: "pr-2.5" },
|
|
md: { root: "py-2.5 px-3.5", shortcut: "pr-3" },
|
|
};
|
|
|
|
const SelectValue = ({ isOpen, isFocused, isDisabled, size, placeholder, placeholderIcon, ref }: SelectValueProps) => {
|
|
return (
|
|
<AriaButton
|
|
ref={ref}
|
|
className={cx(
|
|
"relative flex w-full cursor-pointer items-center rounded-lg bg-primary shadow-xs ring-1 ring-primary outline-hidden transition duration-100 ease-linear ring-inset",
|
|
(isFocused || isOpen) && "ring-2 ring-brand",
|
|
isDisabled && "cursor-not-allowed bg-disabled_subtle text-disabled",
|
|
)}
|
|
>
|
|
<AriaSelectValue<SelectItemType>
|
|
className={cx(
|
|
"flex h-max w-full items-center justify-start gap-2 truncate text-left align-middle",
|
|
|
|
// Icon styles
|
|
"*:data-icon:size-5 *:data-icon:shrink-0 *:data-icon:text-fg-quaternary in-disabled:*:data-icon:text-fg-disabled",
|
|
|
|
sizes[size].root,
|
|
)}
|
|
>
|
|
{(state) => {
|
|
const Icon = state.selectedItem?.icon || placeholderIcon;
|
|
return (
|
|
<>
|
|
{state.selectedItem?.avatarUrl ? (
|
|
<Avatar size="xs" src={state.selectedItem.avatarUrl} alt={state.selectedItem.label} />
|
|
) : isReactComponent(Icon) ? (
|
|
<Icon data-icon aria-hidden="true" />
|
|
) : isValidElement(Icon) ? (
|
|
Icon
|
|
) : null}
|
|
|
|
{state.selectedItem ? (
|
|
<section className="flex w-full gap-2 truncate">
|
|
<p className="truncate text-md font-medium text-primary">{state.selectedItem?.label}</p>
|
|
{state.selectedItem?.supportingText && <p className="text-md text-tertiary">{state.selectedItem?.supportingText}</p>}
|
|
</section>
|
|
) : (
|
|
<p className={cx("text-md text-placeholder", isDisabled && "text-disabled")}>{placeholder}</p>
|
|
)}
|
|
|
|
<FontAwesomeIcon
|
|
icon={faChevronDown}
|
|
aria-hidden="true"
|
|
className={cx("ml-auto shrink-0 text-fg-quaternary", size === "sm" ? "size-4" : "size-5")}
|
|
/>
|
|
</>
|
|
);
|
|
}}
|
|
</AriaSelectValue>
|
|
</AriaButton>
|
|
);
|
|
};
|
|
|
|
export const SelectContext = createContext<{ size: "sm" | "md" }>({ size: "sm" });
|
|
|
|
const Select = ({ placeholder = "Select", placeholderIcon, size = "sm", children, items, label, hint, tooltip, className, ...rest }: SelectProps) => {
|
|
return (
|
|
<SelectContext.Provider value={{ size }}>
|
|
<AriaSelect {...rest} className={(state) => cx("flex flex-col gap-1.5", typeof className === "function" ? className(state) : className)}>
|
|
{(state) => (
|
|
<>
|
|
{label && (
|
|
<Label isRequired={state.isRequired} tooltip={tooltip}>
|
|
{label}
|
|
</Label>
|
|
)}
|
|
|
|
<SelectValue {...state} {...{ size, placeholder }} placeholderIcon={placeholderIcon} />
|
|
|
|
<Popover size={size} className={rest.popoverClassName}>
|
|
<AriaListBox items={items} className="size-full outline-hidden">
|
|
{children}
|
|
</AriaListBox>
|
|
</Popover>
|
|
|
|
{hint && <HintText isInvalid={state.isInvalid}>{hint}</HintText>}
|
|
</>
|
|
)}
|
|
</AriaSelect>
|
|
</SelectContext.Provider>
|
|
);
|
|
};
|
|
|
|
const _Select = Select as typeof Select & {
|
|
ComboBox: typeof ComboBox;
|
|
Item: typeof SelectItem;
|
|
};
|
|
_Select.ComboBox = ComboBox;
|
|
_Select.Item = SelectItem;
|
|
|
|
export { _Select as Select };
|