eclipsesource / eclipsesource/jsonforms
react-material: date/time pickers wire `autoFocus`, `onFocus` and `onBlur` to an aria-hidden input
- Dominant language
- TypeScript
- Stars
- 2.7k
- Forks
- 424
- Avg merge
- 17d 8h
- Merged PRs (30d)
- 1
Description
### Describe the bug
The React Material date, time and date-time controls pass `autoFocus`, `onFocus` and `onBlur` through `slotProps.textField.inputProps`, which targets the picker's hidden mirror `` rather than the field the user interacts with. As a result `options.focus` and `showUnfocusedDescription` do not work on these controls, and `createOnBlurHandler` never runs.
Since `@mui/x-date-pickers` v8, `enableAccessibleFieldDOMStructure` defaults to `true`, so `DatePicker` renders the accessible field DOM: one contenteditable `` per date section, plus a single `` that only mirrors the value.
```html
1980
06
04
```
Rendering `MaterialDateControl` and driving it with real DOM `focus()` / `blur()` on the section spans shows:
- With `options: { focus: true }`, `document.activeElement` is the `aria-hidden` / `tabindex="-1"` input, not a date section.
- Focusing and blurring the section spans (what a user reaches with Tab) fires neither `onFocus` nor `onBlur`. The hidden input is a sibling of the sections container, not an ancestor, so focus events never bubble through it.
Concretely this breaks three things:
1. `options.focus` does not focus date, time or date-time controls.
2. `showUnfocusedDescription` never takes effect, because `onFocus` never fires and the control stays unfocused as far as `isDescriptionHidden` is concerned.
3. `createOnBlurHandler` is dead code, and so is the `key` / `updateChild` remount it drives. The behavior it implemented, clearing the data and resetting the visible field when the typed text does not parse, no longer happens.
Data entry itself still works: field edits reach `setValue` with the default `changeImportance: 'accept'`, so the picker's `onAccept` still fires and `createOnChangeHandler` still commits. Only the focus, description and invalid-input-reset behaviors are affected.
The existing tests do not catch any of this because they locate the hidden input with `find('input')` and then assert React props on it or simulate events directly on it, so they pass whether or not the wiring reaches the real field.
### Expected behavior
- `options: { focus: true }` puts the caret on the first editable date section, the way it focuses the input of a plain text control.
- `onFocus` and `onBlur` fire when the user enters and leaves the field, so `showUnfocusedDescription` behaves the same as on other controls.
- No dead focus/blur wiring is left pointing at an element the user cannot reach.
### Steps to reproduce the issue
1. Start the React Material example app (`packages/material-renderers`, `pnpm run dev`).
2. Pick an example containing a date control, or use this UI schema against a `{ "type": "string", "format": "date" }` property:
```json
{
"type": "Control",
"scope": "#/properties/birthDate",
"options": { "focus": true, "showUnfocusedDescription": false }
}
```
3. Reload the form. The date field does not receive the caret: no date section is highlighted and typing goes nowhere until you click the field.
4. Click into the date field. The control's description does not appear on focus, and it does not disappear again when you Tab away.
5. Inspect the field in the browser devtools. The focusable elements are the `MuiPickersSectionList` spans, while the `` carrying the JSON Forms `autoFocus` / `onFocus` / `onBlur` is `aria-hidden="true"` with `tabindex="-1"`.
### Screenshots
_No response_
### Which Version of JSON Forms are you using?
v3.9.0-alpha.0 / v3.9.0-alpha.1
### Package
React Material Renderers
### Additional context
Use the picker's own `autoFocus` prop, which resolves to `pickerContext.autoFocus && !pickerContext.open` and focuses the first editable section, and put `onFocus` / `onBlur` on the `textField` slot itself, where `PickersTextField` forwards them to the field root. Both were confirmed to reach the section spans. The `htmlInput` props then only carry `type: 'text'`.
`createOnBlurHandler` reads `e.target.value`, which does not exist on the sections container, so it cannot be rehomed and should be dropped from the three controls. It is exported from the package, so keep and deprecate the util itself rather than deleting it.
Changes for MaterialDateControl.tsx (apply the same to MaterialDateTimeControl.tsx and MaterialTimeControl.tsx)
```diff
-import React, { useCallback, useMemo, useState } from 'react';
+import React, { useMemo, useState } from 'react';
...
import {
- createOnBlurHandler,
createOnChangeHandler,
getData,
useFocus,
useInputVariant,
} from '../util';
@@
- const [key, setKey] = useState(0);
const [open, setOpen] = useState(false);
@@
- const updateChild = useCallback(() => setKey((key) => key + 1), []);
-
const onChange = useMemo(
() => createOnChangeHandler(path, handleChange, saveFormat),
[path, handleChange, saveFormat]
);
-
- const onBlurHandler = useMemo(
- () =>
- createOnBlurHandler(
- path,
- handleChange,
- format,
- saveFormat,
- updateChild,
- onBlur
- ),
- [path, handleChange, format, saveFormat, updateChild, onBlur]
- );
@@
setOpen(true)}
onClose={() => setOpen(false)}
- key={key}
label={label}
value={value}
onAccept={onChange}
format={format}
views={views}
disabled={!enabled}
closeOnSelect={closeOnSelect}
+ autoFocus={appliedUiSchemaOptions.focus}
slotProps={{
actionBar: ...,
textField: {
id: id + '-input',
required: required && !appliedUiSchemaOptions.hideRequiredAsterisk,
error: !isValid,
fullWidth: !appliedUiSchemaOptions.trim,
variant: inputVariant,
- inputProps: {
- autoFocus: appliedUiSchemaOptions.focus,
- type: 'text',
- onFocus: onFocus,
- onBlur: onBlurHandler,
- },
+ onFocus: onFocus,
+ onBlur: onBlur,
+ inputProps: { type: 'text' },
InputLabelProps: data ? { shrink: true } : undefined,
},
}}
/>
```
After #2614 the last two prop lines become `slotProps: { htmlInput: { type: 'text' }, inputLabel: data ? { shrink: true } : undefined }`. `onFocus` / `onBlur` stay at the `textField` slot level and `autoFocus` stays on the picker either way.
Deprecate createOnBlurHandler in util/datejs.tsx
```diff
+/**
+ * @deprecated Since @mui/x-date-pickers v8 the accessible field DOM structure is the
+ * default, so blur events on the field no longer carry input values and this handler
+ * can no longer be attached to anything the user can reach. Rely on the picker's
+ * onChange/onAccept callbacks instead.
+ */
export const createOnBlurHandler =
(
path: string,
```
If the invalid-input reset from point 3 is still wanted, it should hook the picker's validation error rather than a blur event, since the field no longer surfaces unparseable text to the outside.
The date, time and date-time tests should be reworked to drive the picker through its component props or the section spans instead of the hidden input, so a regression here fails the build.
Contributor guide
Research direction
Start with packages/material-renderers/MaterialDateControl.tsx, MaterialDateTimeControl.tsx, and MaterialTimeControl.tsx, then inspect createOnBlurHandler in util/datejs.tsx and the existing date, time, and date-time tests. Exercise the picker through its section spans and component props rather than the hidden input. Done means focus, blur, autofocus, and description behavior work on the accessible field, with no dead control wiring and regression coverage for the three controls.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend, testing
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100