105 lines
3.7 KiB
TypeScript
105 lines
3.7 KiB
TypeScript
import React, { useLayoutEffect, useRef } from 'react';
|
|
import { twMerge } from 'tailwind-merge';
|
|
import { InputContainer } from './InputContainer';
|
|
import { InputControl } from './InputControl';
|
|
import { InputStartDecorator, InputEndDecorator } from './InputDecorator';
|
|
import { InputError } from './InputError';
|
|
import { InputGroup } from './InputGroup';
|
|
import { Label } from './Label';
|
|
|
|
type InputProps = {
|
|
label?: string;
|
|
hiddenLabel?: boolean;
|
|
className?: string;
|
|
inputClassName?: string;
|
|
error?: string;
|
|
initialValue?: string;
|
|
name: string;
|
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
startDecorator?: React.ReactNode;
|
|
endDecorator?: React.ReactNode;
|
|
} & React.InputHTMLAttributes<HTMLInputElement>;
|
|
|
|
const makeObserverFunction = (ref: React.RefObject<HTMLDivElement>, varPrefix: string) => {
|
|
return (entries: ResizeObserverEntry[]) => {
|
|
const {
|
|
contentRect: { width },
|
|
} = entries[0];
|
|
if (ref.current) {
|
|
ref.current.style.setProperty(`--${varPrefix}-width`, width + 'px');
|
|
}
|
|
};
|
|
};
|
|
|
|
export function TextInput({
|
|
label,
|
|
className,
|
|
hiddenLabel,
|
|
error,
|
|
initialValue,
|
|
inputClassName,
|
|
id,
|
|
startDecorator,
|
|
endDecorator,
|
|
...inputProps
|
|
}: InputProps) {
|
|
const isError = Boolean(error);
|
|
|
|
const errorLabel = isError && id ? `${id}-error` : undefined;
|
|
|
|
const inputControlRef = useRef<HTMLInputElement>(null);
|
|
const startDecoratorRef = useRef<HTMLDivElement>(null);
|
|
const endDecoratorRef = useRef<HTMLDivElement>(null);
|
|
|
|
useLayoutEffect(() => {
|
|
const startResizeObserver = new ResizeObserver(makeObserverFunction(inputControlRef, 'start-decorator'));
|
|
const endResizeObserver = new ResizeObserver(makeObserverFunction(inputControlRef, 'end-decorator'));
|
|
if (startDecoratorRef.current) {
|
|
startResizeObserver.observe(startDecoratorRef.current);
|
|
}
|
|
if (endDecoratorRef.current) {
|
|
endResizeObserver.observe(endDecoratorRef.current);
|
|
}
|
|
return () => {
|
|
startResizeObserver.disconnect();
|
|
endResizeObserver.disconnect();
|
|
};
|
|
}, [inputControlRef, startDecoratorRef, endDecoratorRef]);
|
|
|
|
const input = (
|
|
<InputControl
|
|
id={id}
|
|
defaultValue={initialValue}
|
|
aria-invalid={error !== undefined}
|
|
aria-errormessage={errorLabel}
|
|
aria-label={hiddenLabel ? label : undefined}
|
|
className={twMerge(
|
|
// we want a 0.8rem padding on both sides of the decorator. (0.8rem + 0.8rem = 1.6rem)
|
|
// in case a decorator doesn't exist, we only need 0.8rem padding, so we default the variable to -0.8rem
|
|
'pl-[calc(var(--start-decorator-width,-0.8rem)+1.6rem)] pr-[calc(var(--end-decorator-width,-0.8rem)+1.6rem)]',
|
|
inputClassName,
|
|
)}
|
|
ref={inputControlRef}
|
|
{...inputProps}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<InputContainer className={className}>
|
|
{!hiddenLabel && label && <Label htmlFor={id} label={label} />}
|
|
{startDecorator || endDecorator ? (
|
|
<InputGroup>
|
|
{startDecorator && (
|
|
<InputStartDecorator ref={startDecoratorRef}>{startDecorator}</InputStartDecorator>
|
|
)}
|
|
{input}
|
|
{endDecorator && <InputEndDecorator ref={endDecoratorRef}>{endDecorator}</InputEndDecorator>}
|
|
</InputGroup>
|
|
) : (
|
|
input
|
|
)}
|
|
{error && <InputError id={errorLabel}>{error}</InputError>}
|
|
</InputContainer>
|
|
);
|
|
}
|