Merged in feat/3685-new-textarea-component (pull request #3392)

feat(SW-3685): Add new TextArea and FormTextArea components

* Add new TextArea and FormTextArea components

* Update example form with description

* Merge branch 'master' into feat/3685-new-textarea-component

* Formatting new files with new prettier config

* Added custom controls for the text area story


Approved-by: Linus Flood
This commit is contained in:
Rasmus Langvad
2026-01-07 17:04:30 +00:00
parent d0546926a9
commit 4980cc830d
17 changed files with 752 additions and 304 deletions

View File

@@ -0,0 +1,102 @@
"use client"
import { forwardRef } 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 { TextArea } from "../../TextArea"
import styles from "./textarea.module.css"
import type { FormTextAreaProps } from "./textarea"
import { MaterialIcon, MaterialIconProps } from "../../Icons/MaterialIcon"
const defaultErrorFormatter = (
_intl: IntlShape,
errorMessage?: string
): string => errorMessage ?? ""
export const FormTextArea = forwardRef<HTMLTextAreaElement, FormTextAreaProps>(
function FormTextArea(
{
className = "",
description = "",
descriptionIcon = "info" as MaterialIconProps["icon"],
disabled = false,
errorFormatter,
hideError,
label,
name,
placeholder,
readOnly = false,
registerOptions = {},
...props
},
ref
) {
const intl = useIntl()
const { control } = useFormContext()
const formatErrorMessage = errorFormatter ?? defaultErrorFormatter
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 (
<TextField
className={cx(styles.wrapper, className)}
isDisabled={isDisabled}
isReadOnly={readOnly}
isInvalid={fieldState.invalid}
isRequired={!!registerOptions.required}
>
<TextArea
{...props}
ref={mergeRefs(field.ref, ref)}
name={field.name}
onBlur={field.onBlur}
onChange={field.onChange}
value={field.value ?? ""}
id={field.name}
label={label}
placeholder={placeholder}
readOnly={readOnly}
disabled={isDisabled}
required={!!registerOptions.required}
/>
{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>
)
}}
/>
)
}
)
FormTextArea.displayName = "FormTextArea"
// Default export for backwards compatibility
export default FormTextArea

View File

@@ -0,0 +1,16 @@
.wrapper {
display: flex;
flex-direction: column;
}
.description {
display: flex;
align-items: center;
gap: var(--Space-x05);
margin-top: var(--Space-x1);
font-size: var(--Body-Supporting-text-Size);
font-family: var(--Body-Supporting-text-Font-family, "Fira Sans");
font-style: normal;
font-weight: var(--Body-Supporting-text-Font-weight);
letter-spacing: var(--Body-Supporting-text-Letter-spacing);
}

View File

@@ -0,0 +1,20 @@
import type { RegisterOptions } from "react-hook-form"
import type { IntlShape } from "react-intl"
import type { TextAreaProps } from "../../TextArea/types"
import { MaterialIconProps } from "../../Icons/MaterialIcon"
export interface FormTextAreaProps extends TextAreaProps {
/** Helper text displayed below the textarea (hidden when there's an error) */
description?: string
/** Icon to display with the description text. Defaults to 'info' */
descriptionIcon?: MaterialIconProps["icon"]
/** Field name for react-hook-form registration */
name: string
/** react-hook-form validation rules */
registerOptions?: RegisterOptions
/** Hide the error message (useful when showing errors elsewhere) */
hideError?: boolean
/** Custom formatter for error messages with i18n support */
errorFormatter?: (intl: IntlShape, errorMessage?: string) => string
}