fix(LOY-514): Fix validation error tracking in SignupForm * Fix issue with form submit handling * Fix * Remove browser validation * Add automatic tracking of validatione rrors Approved-by: Rasmus Langvad Approved-by: Matilda Landström
175 lines
5.6 KiB
TypeScript
175 lines
5.6 KiB
TypeScript
import { cx } from "class-variance-authority"
|
|
import {
|
|
type ForwardedRef,
|
|
forwardRef,
|
|
useId,
|
|
useImperativeHandle,
|
|
useRef,
|
|
} from "react"
|
|
import { Input as AriaInput, Label as AriaLabel } from "react-aria-components"
|
|
|
|
import { InputLabel } from "../InputLabel"
|
|
|
|
import styles from "./input.module.css"
|
|
|
|
import { MaterialIcon } from "../Icons/MaterialIcon"
|
|
import { Typography } from "../Typography"
|
|
import type { InputProps } from "./types"
|
|
import { clearInput, useInputHasValue } from "./utils"
|
|
|
|
const InputComponent = forwardRef(function AriaInputWithLabelComponent(
|
|
{
|
|
label,
|
|
labelPosition = "floating",
|
|
leftIcon,
|
|
rightIcon,
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
onRightIconClick,
|
|
showClearContentIcon,
|
|
placeholder,
|
|
id,
|
|
required,
|
|
"data-validation-state": validationState,
|
|
...props
|
|
}: InputProps & { "data-validation-state"?: string },
|
|
ref: ForwardedRef<HTMLInputElement>
|
|
) {
|
|
// Create an internal ref that we can access
|
|
const internalRef = useRef<HTMLInputElement>(null)
|
|
|
|
// Generate a unique ID for the input
|
|
// This is used to ensure the input is properly associated with the label
|
|
// when the label is positioned above the input
|
|
const generatedId = useId()
|
|
|
|
// Forward the ref properly
|
|
useImperativeHandle(ref, () => internalRef.current!, [])
|
|
|
|
// Track whether input has a value (for showing/hiding clear button)
|
|
const hasValue = useInputHasValue(props.value, internalRef)
|
|
|
|
const onClearContent = () => {
|
|
clearInput({
|
|
inputRef: internalRef,
|
|
onChange: props.onChange,
|
|
value: props.value,
|
|
})
|
|
}
|
|
// When labelPosition is 'top', restructure to have label outside container
|
|
// We need an ID for proper label-input association
|
|
if (labelPosition === "top") {
|
|
const inputId = id || generatedId
|
|
|
|
return (
|
|
<>
|
|
<InputLabel
|
|
required={required}
|
|
className={cx(
|
|
styles.labelAbove,
|
|
leftIcon && styles.labelAboveWithLeftIcon
|
|
)}
|
|
>
|
|
{label}
|
|
</InputLabel>
|
|
<div className={styles.inputContainer}>
|
|
{leftIcon && (
|
|
<div className={styles.leftIconContainer}>{leftIcon}</div>
|
|
)}
|
|
|
|
<label
|
|
htmlFor={inputId}
|
|
className={cx(
|
|
styles.container,
|
|
leftIcon && styles.containerWithLeftIcon,
|
|
rightIcon && styles.containerWithRightIcon
|
|
)}
|
|
data-validation-state={validationState}
|
|
>
|
|
<Typography variant="Body/Paragraph/mdRegular">
|
|
<AriaInput
|
|
{...props}
|
|
id={inputId}
|
|
// Avoid duplicating label text in placeholder when label is positioned above
|
|
// Screen readers would announce the label twice (once as label, once as placeholder)
|
|
// Only use placeholder if explicitly provided, otherwise use empty string
|
|
placeholder={placeholder ?? ""}
|
|
className={cx(
|
|
styles.input,
|
|
styles.inputTopLabel,
|
|
props.className
|
|
)}
|
|
ref={internalRef}
|
|
/>
|
|
</Typography>
|
|
</label>
|
|
{showClearContentIcon && hasValue && (
|
|
<div className={styles.rightIconContainer}>
|
|
<button
|
|
type="button"
|
|
className={styles.rightIconButton}
|
|
onClick={onClearContent}
|
|
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
|
aria-label="Clear content"
|
|
>
|
|
<MaterialIcon icon="cancel" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
{rightIcon && !(showClearContentIcon && hasValue) && (
|
|
<div className={styles.rightIconContainer}>{rightIcon}</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
|
|
// Floating label (default behavior) - label inside container
|
|
return (
|
|
<div className={styles.inputContainer}>
|
|
{leftIcon && <div className={styles.leftIconContainer}>{leftIcon}</div>}
|
|
<AriaLabel
|
|
className={cx(
|
|
styles.container,
|
|
leftIcon && styles.containerWithLeftIcon,
|
|
rightIcon && styles.containerWithRightIcon
|
|
)}
|
|
data-validation-state={validationState}
|
|
>
|
|
<Typography variant="Body/Paragraph/mdRegular">
|
|
<AriaInput
|
|
{...props}
|
|
id={id}
|
|
// For floating labels, only set placeholder if explicitly provided
|
|
// The label itself acts as the placeholder, so we don't want to duplicate it
|
|
// This ensures the label only floats when focused or has value
|
|
placeholder={placeholder}
|
|
className={cx(styles.input, props.className)}
|
|
ref={internalRef}
|
|
/>
|
|
</Typography>
|
|
<InputLabel required={required}>{label}</InputLabel>
|
|
</AriaLabel>
|
|
{showClearContentIcon && hasValue && (
|
|
<div className={styles.rightIconContainer}>
|
|
<button
|
|
type="button"
|
|
className={styles.rightIconButton}
|
|
onClick={onClearContent}
|
|
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
|
aria-label="Clear content"
|
|
>
|
|
<MaterialIcon icon="cancel" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
{rightIcon && !(showClearContentIcon && hasValue) && (
|
|
<div className={styles.rightIconContainer}>{rightIcon}</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})
|
|
|
|
export const Input = InputComponent as React.ForwardRefExoticComponent<
|
|
InputProps & React.RefAttributes<HTMLInputElement>
|
|
>
|