[field] Field.Root invalid prop not resettable
- Dominant language
- TypeScript
- Stars
- 10.9k
- Forks
- 543
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 101
Description
# Bug report
Field.Root invalid prop not resettable
## Current behavior
`Field.Root`'s `invalid` prop can be set to `true | false | undefined`. However, when resetting from `true` or `false` to `undefined`, the associated `data-invalid` or `data-valid` attribute still persists on the HTML. We can remount with `key={resetKey}`, but ideally changing to a value of `undefined` would remove the `data-invalid`/`data-valid` attribute entirely
This would make it consistent with the `dirty` and `touched` props which behave in a way that adds/removes attributes. Obviously, this would be the desired behavior across all Form controls (i.e., TextArea, Select, etc).
Here's an example of an abstraction on top of `Field.Root` that does what I'm talking about. Also note the presence of the `validating` prop.
```jsx
import * as React from 'react'
import { Field } from '@base-ui/react/field'
import type { FieldRootProps as _FieldRootProps } from '@base-ui/react/field'
import { cn } from '@/utils'
export type FieldRootProps = _FieldRootProps & {
validating?: boolean
}
const baseClasses = `group/root`
/* ========================================================================
======================================================================== */
export const FieldRoot = ({
name,
dirty,
touched,
disabled,
invalid,
validate,
validationMode,
validationDebounceTime,
className,
style,
render,
ref,
validating,
...otherProps
}: FieldRootProps) => {
const internalRef = React.useRef(null)
/* ======================
useEffect()
====================== */
React.useEffect(() => {
const root = internalRef.current
if (!root) return
const label = root.querySelector("[data-slot='field-label']")
const control = root.querySelector("[data-slot='field-control']")
const error = root.querySelector("[data-slot='field-error']")
const description = root.querySelector("[data-slot='field-description']")
if (validating) {
root.setAttribute('data-validating', '')
root.removeAttribute('data-valid')
root.removeAttribute('data-invalid')
if (label) {
label.setAttribute('data-validating', '')
label.removeAttribute('data-valid')
label.removeAttribute('data-invalid')
}
if (control) {
control.setAttribute('data-validating', '')
control.removeAttribute('data-valid')
control.removeAttribute('data-invalid')
}
if (error) {
error.setAttribute('data-validating', '')
error.removeAttribute('data-valid')
error.removeAttribute('data-invalid')
}
if (description) {
description.setAttribute('data-validating', '')
description.removeAttribute('data-valid')
description.removeAttribute('data-invalid')
}
return
}
if (!validating) {
root.removeAttribute('data-validating')
if (label) label.removeAttribute('data-validating')
if (control) control.removeAttribute('data-validating')
if (error) error.removeAttribute('data-validating')
if (description) description.removeAttribute('data-validating')
}
// Otherwise, check invalid for true | false | undefined.
if (invalid === true) {
if (label) {
label.setAttribute('data-invalid', '')
label.removeAttribute('data-valid')
}
if (control) {
control.setAttribute('data-invalid', '')
control.removeAttribute('data-valid')
}
if (error) {
error.setAttribute('data-invalid', '')
error.removeAttribute('data-valid')
}
if (description) {
description.setAttribute('data-invalid', '')
description.removeAttribute('data-valid')
}
return
}
if (invalid === false) {
if (label) {
label.setAttribute('data-valid', '')
label.removeAttribute('data-invalid')
}
if (control) {
control.setAttribute('data-valid', '')
control.removeAttribute('data-invalid')
}
if (error) {
error.setAttribute('data-valid', '')
error.removeAttribute('data-invalid')
}
if (description) {
description.setAttribute('data-valid', '')
description.removeAttribute('data-invalid')
}
return
}
// Otherwise, invalid is undefined
if (label) {
label.removeAttribute('data-valid')
label.removeAttribute('data-invalid')
}
if (control) {
control.removeAttribute('data-valid')
control.removeAttribute('data-invalid')
}
if (error) {
error.removeAttribute('data-valid')
error.removeAttribute('data-invalid')
}
if (description) {
description.removeAttribute('data-valid')
description.removeAttribute('data-invalid')
}
}, [invalid, validating])
/* ======================
return
====================== */
return (
{
if (typeof className === 'function') {
className = className(fieldRootState) || ''
}
return cn(baseClasses, className)
}}
ref={(node) => {
if (ref && 'current' in ref) {
ref.current = node
} else if (typeof ref === 'function') {
ref?.(node)
}
internalRef.current = node
}}
dirty={dirty}
disabled={disabled}
invalid={invalid}
name={name}
render={render}
style={style}
touched={touched}
validate={validate}
validationDebounceTime={validationDebounceTime}
validationMode={validationMode}
{...otherProps}
/>
)
}
```
Contributor guide
Assessment
This issue has not been assessed yet.