Files
web/packages/design-system/lib/components/Form/FormInput/index.tsx
Rasmus Langvad b9a62b5280 Merged in feat/use-new-input-component (pull request #3324)
feat(SW-3659): Use new input component

* Use new input component

* Update error formatter

* Merged master into feat/use-new-input-component

* Merged master into feat/use-new-input-component

* Merge branch 'master' into feat/use-new-input-component

* Merged master into feat/use-new-input-component

* Update Input stories

* Merge branch 'feat/use-new-input-component' of bitbucket.org:scandic-swap/web into feat/use-new-input-component

* Update Storybook logo

* Add some new demo icon input story

* Fix the clear content button position

* Fix broken password input icon

* Merged master into feat/use-new-input-component

* Merged master into feat/use-new-input-component

* Add aria-hidden to required asterisk

* Merge branch 'feat/use-new-input-component' of bitbucket.org:scandic-swap/web into feat/use-new-input-component

* Merge branch 'master' into feat/use-new-input-component


Approved-by: Bianca Widstam
Approved-by: Matilda Landström
2025-12-18 15:42:09 +00:00

126 lines
3.9 KiB
TypeScript

'use client'
import { forwardRef, type HTMLAttributes, type WheelEvent } from 'react'
import { Text, TextField } from 'react-aria-components'
import { Controller, useFormContext } from 'react-hook-form'
import { useIntl, type IntlShape } from 'react-intl'
import { cx } from 'class-variance-authority'
import { Error } from '../ErrorMessage/Error'
import { mergeRefs } from '../utils/mergeRefs'
import { MaterialIcon, MaterialIconProps } from '../../Icons/MaterialIcon'
import { Input } from '../../Input'
import styles from './input.module.css'
import type { FormInputProps } from './input'
const defaultErrorFormatter = (
_intl: IntlShape,
errorMessage?: string
): string => errorMessage ?? ''
export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(
function FormInput(
{
autoComplete,
className = '',
description = '',
descriptionIcon = 'info' as MaterialIconProps['icon'],
disabled = false,
errorFormatter,
hideError,
inputMode,
label,
labelPosition = 'floating',
maxLength,
name,
placeholder,
readOnly = false,
registerOptions = {},
type = 'text',
validationState,
...props
},
ref
) {
const intl = useIntl()
const { control } = useFormContext()
const formatErrorMessage = errorFormatter ?? defaultErrorFormatter
// Number input: prevent scroll from changing value
const numberAttributes: HTMLAttributes<HTMLInputElement> =
type === 'number'
? {
onWheel: (evt: WheelEvent<HTMLInputElement>) => {
evt.currentTarget.blur()
},
}
: {}
return (
<Controller
control={control}
name={name}
rules={registerOptions}
render={({ field, fieldState }) => {
const isDisabled = disabled || field.disabled
const hasError = fieldState.invalid && !hideError
const showDescription = description && !fieldState.error
return (
// Note: No aria-label needed on TextField since the Input component
// always renders a visible label that provides the accessible name
<TextField
className={cx(styles.wrapper, className)}
isDisabled={isDisabled}
isReadOnly={readOnly}
isInvalid={fieldState.invalid}
isRequired={!!registerOptions.required}
>
<Input
{...props}
{...numberAttributes}
ref={mergeRefs(field.ref, ref)}
name={field.name}
onBlur={field.onBlur}
onChange={field.onChange}
value={field.value ?? ''}
autoComplete={autoComplete}
id={field.name}
label={label}
labelPosition={labelPosition}
maxLength={maxLength}
placeholder={placeholder}
readOnly={readOnly}
disabled={isDisabled}
required={!!registerOptions.required}
type={type}
inputMode={inputMode}
// Only 'warning' is passed through; error state is handled via isInvalid
data-validation-state={validationState}
/>
{showDescription ? (
<Text className={styles.description} slot="description">
<MaterialIcon icon={descriptionIcon} size={20} />
{description}
</Text>
) : null}
{hasError && fieldState.error ? (
<Text slot="errorMessage" aria-live="polite">
<Error>
{formatErrorMessage(intl, fieldState.error.message)}
</Error>
</Text>
) : null}
</TextField>
)
}}
/>
)
}
)
FormInput.displayName = 'FormInput'