mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
chore: initial Untitled UI Vite scaffold with FontAwesome Pro
This commit is contained in:
269
src/components/base/input/input.tsx
Normal file
269
src/components/base/input/input.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import { type ComponentType, type HTMLAttributes, type ReactNode, type Ref, createContext, useContext } from "react";
|
||||
import { HelpCircle, InfoCircle } from "@untitledui/icons";
|
||||
import type { InputProps as AriaInputProps, TextFieldProps as AriaTextFieldProps } from "react-aria-components";
|
||||
import { Group as AriaGroup, Input as AriaInput, TextField as AriaTextField } from "react-aria-components";
|
||||
import { HintText } from "@/components/base/input/hint-text";
|
||||
import { Label } from "@/components/base/input/label";
|
||||
import { Tooltip, TooltipTrigger } from "@/components/base/tooltip/tooltip";
|
||||
import { cx, sortCx } from "@/utils/cx";
|
||||
|
||||
export interface InputBaseProps extends TextFieldProps {
|
||||
/** Tooltip message on hover. */
|
||||
tooltip?: string;
|
||||
/**
|
||||
* Input size.
|
||||
* @default "sm"
|
||||
*/
|
||||
size?: "sm" | "md";
|
||||
/** Placeholder text. */
|
||||
placeholder?: string;
|
||||
/** Class name for the icon. */
|
||||
iconClassName?: string;
|
||||
/** Class name for the input. */
|
||||
inputClassName?: string;
|
||||
/** Class name for the input wrapper. */
|
||||
wrapperClassName?: string;
|
||||
/** Class name for the tooltip. */
|
||||
tooltipClassName?: string;
|
||||
/** Keyboard shortcut to display. */
|
||||
shortcut?: string | boolean;
|
||||
ref?: Ref<HTMLInputElement>;
|
||||
groupRef?: Ref<HTMLDivElement>;
|
||||
/** Icon component to display on the left side of the input. */
|
||||
icon?: ComponentType<HTMLAttributes<HTMLOrSVGElement>>;
|
||||
}
|
||||
|
||||
export const InputBase = ({
|
||||
ref,
|
||||
tooltip,
|
||||
shortcut,
|
||||
groupRef,
|
||||
size = "sm",
|
||||
isInvalid,
|
||||
isDisabled,
|
||||
icon: Icon,
|
||||
placeholder,
|
||||
wrapperClassName,
|
||||
tooltipClassName,
|
||||
inputClassName,
|
||||
iconClassName,
|
||||
// Omit this prop to avoid invalid HTML attribute warning
|
||||
isRequired: _isRequired,
|
||||
...inputProps
|
||||
}: Omit<InputBaseProps, "label" | "hint">) => {
|
||||
// Check if the input has a leading icon or tooltip
|
||||
const hasTrailingIcon = tooltip || isInvalid;
|
||||
const hasLeadingIcon = Icon;
|
||||
|
||||
// If the input is inside a `TextFieldContext`, use its context to simplify applying styles
|
||||
const context = useContext(TextFieldContext);
|
||||
|
||||
const inputSize = context?.size || size;
|
||||
|
||||
const sizes = sortCx({
|
||||
sm: {
|
||||
root: cx("px-3 py-2", hasTrailingIcon && "pr-9", hasLeadingIcon && "pl-10"),
|
||||
iconLeading: "left-3",
|
||||
iconTrailing: "right-3",
|
||||
shortcut: "pr-2.5",
|
||||
},
|
||||
md: {
|
||||
root: cx("px-3.5 py-2.5", hasTrailingIcon && "pr-9.5", hasLeadingIcon && "pl-10.5"),
|
||||
iconLeading: "left-3.5",
|
||||
iconTrailing: "right-3.5",
|
||||
shortcut: "pr-3",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<AriaGroup
|
||||
{...{ isDisabled, isInvalid }}
|
||||
ref={groupRef}
|
||||
className={({ isFocusWithin, isDisabled, isInvalid }) =>
|
||||
cx(
|
||||
"relative flex w-full flex-row place-content-center place-items-center rounded-lg bg-primary shadow-xs ring-1 ring-primary transition-shadow duration-100 ease-linear ring-inset",
|
||||
|
||||
isFocusWithin && !isDisabled && "ring-2 ring-brand",
|
||||
|
||||
// Disabled state styles
|
||||
isDisabled && "cursor-not-allowed bg-disabled_subtle ring-disabled",
|
||||
"group-disabled:cursor-not-allowed group-disabled:bg-disabled_subtle group-disabled:ring-disabled",
|
||||
|
||||
// Invalid state styles
|
||||
isInvalid && "ring-error_subtle",
|
||||
"group-invalid:ring-error_subtle",
|
||||
|
||||
// Invalid state with focus-within styles
|
||||
isInvalid && isFocusWithin && "ring-2 ring-error",
|
||||
isFocusWithin && "group-invalid:ring-2 group-invalid:ring-error",
|
||||
|
||||
context?.wrapperClassName,
|
||||
wrapperClassName,
|
||||
)
|
||||
}
|
||||
>
|
||||
{/* Leading icon and Payment icon */}
|
||||
{Icon && (
|
||||
<Icon
|
||||
className={cx(
|
||||
"pointer-events-none absolute size-5 text-fg-quaternary",
|
||||
isDisabled && "text-fg-disabled",
|
||||
sizes[inputSize].iconLeading,
|
||||
context?.iconClassName,
|
||||
iconClassName,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Input field */}
|
||||
<AriaInput
|
||||
{...(inputProps as AriaInputProps)}
|
||||
ref={ref}
|
||||
placeholder={placeholder}
|
||||
className={cx(
|
||||
"m-0 w-full bg-transparent text-md text-primary ring-0 outline-hidden placeholder:text-placeholder autofill:rounded-lg autofill:text-primary",
|
||||
isDisabled && "cursor-not-allowed text-disabled",
|
||||
sizes[inputSize].root,
|
||||
context?.inputClassName,
|
||||
inputClassName,
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Tooltip and help icon */}
|
||||
{tooltip && !isInvalid && (
|
||||
<Tooltip title={tooltip} placement="top">
|
||||
<TooltipTrigger
|
||||
className={cx(
|
||||
"absolute cursor-pointer text-fg-quaternary transition duration-200 hover:text-fg-quaternary_hover focus:text-fg-quaternary_hover",
|
||||
sizes[inputSize].iconTrailing,
|
||||
context?.tooltipClassName,
|
||||
tooltipClassName,
|
||||
)}
|
||||
>
|
||||
<HelpCircle className="size-4" />
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Invalid icon */}
|
||||
{isInvalid && (
|
||||
<InfoCircle
|
||||
className={cx(
|
||||
"pointer-events-none absolute size-4 text-fg-error-secondary",
|
||||
sizes[inputSize].iconTrailing,
|
||||
context?.tooltipClassName,
|
||||
tooltipClassName,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Shortcut */}
|
||||
{shortcut && (
|
||||
<div
|
||||
className={cx(
|
||||
"pointer-events-none absolute inset-y-0.5 right-0.5 z-10 flex items-center rounded-r-[inherit] bg-linear-to-r from-transparent to-bg-primary to-40% pl-8",
|
||||
sizes[inputSize].shortcut,
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cx(
|
||||
"pointer-events-none rounded px-1 py-px text-xs font-medium text-quaternary ring-1 ring-secondary select-none ring-inset",
|
||||
isDisabled && "bg-transparent text-disabled",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{typeof shortcut === "string" ? shortcut : "⌘K"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</AriaGroup>
|
||||
);
|
||||
};
|
||||
|
||||
InputBase.displayName = "InputBase";
|
||||
|
||||
interface BaseProps {
|
||||
/** Label text for the input */
|
||||
label?: string;
|
||||
/** Helper text displayed below the input */
|
||||
hint?: ReactNode;
|
||||
}
|
||||
|
||||
interface TextFieldProps
|
||||
extends BaseProps,
|
||||
AriaTextFieldProps,
|
||||
Pick<InputBaseProps, "size" | "wrapperClassName" | "inputClassName" | "iconClassName" | "tooltipClassName"> {
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const TextFieldContext = createContext<TextFieldProps>({});
|
||||
|
||||
export const TextField = ({ className, ...props }: TextFieldProps) => {
|
||||
return (
|
||||
<TextFieldContext.Provider value={props}>
|
||||
<AriaTextField
|
||||
{...props}
|
||||
data-input-wrapper
|
||||
className={(state) =>
|
||||
cx("group flex h-max w-full flex-col items-start justify-start gap-1.5", typeof className === "function" ? className(state) : className)
|
||||
}
|
||||
/>
|
||||
</TextFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
TextField.displayName = "TextField";
|
||||
|
||||
interface InputProps extends InputBaseProps, BaseProps {
|
||||
/** Whether to hide required indicator from label */
|
||||
hideRequiredIndicator?: boolean;
|
||||
}
|
||||
|
||||
export const Input = ({
|
||||
size = "sm",
|
||||
placeholder,
|
||||
icon: Icon,
|
||||
label,
|
||||
hint,
|
||||
shortcut,
|
||||
hideRequiredIndicator,
|
||||
className,
|
||||
ref,
|
||||
groupRef,
|
||||
tooltip,
|
||||
iconClassName,
|
||||
inputClassName,
|
||||
wrapperClassName,
|
||||
tooltipClassName,
|
||||
...props
|
||||
}: InputProps) => {
|
||||
return (
|
||||
<TextField aria-label={!label ? placeholder : undefined} {...props} className={className}>
|
||||
{({ isRequired, isInvalid }) => (
|
||||
<>
|
||||
{label && <Label isRequired={hideRequiredIndicator ? !hideRequiredIndicator : isRequired}>{label}</Label>}
|
||||
|
||||
<InputBase
|
||||
{...{
|
||||
ref,
|
||||
groupRef,
|
||||
size,
|
||||
placeholder,
|
||||
icon: Icon,
|
||||
shortcut,
|
||||
iconClassName,
|
||||
inputClassName,
|
||||
wrapperClassName,
|
||||
tooltipClassName,
|
||||
tooltip,
|
||||
}}
|
||||
/>
|
||||
|
||||
{hint && <HintText isInvalid={isInvalid}>{hint}</HintText>}
|
||||
</>
|
||||
)}
|
||||
</TextField>
|
||||
);
|
||||
};
|
||||
|
||||
Input.displayName = "Input";
|
||||
Reference in New Issue
Block a user