mui / mui/base-ui

[field] Field.Root invalid prop not resettable

Open
#3,777 4 comments 0 reactions 0 assignees View on GitHub
component: field type: enhancement
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.