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
126 lines
3.9 KiB
TypeScript
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'
|