Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion packages/ra-ui-materialui/src/input/ResettableTextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export const ResettableTextField = forwardRef(
variant,
margin,
className,
'aria-required': ariaRequired,
inputProps: htmlInputPropsFromConsumer,
...rest
} = props;

Expand Down Expand Up @@ -159,11 +161,36 @@ export const ResettableTextField = forwardRef(
...InputPropsWithoutEndAdornment,
};

// Props forwarded to the native <input> element. We translate the
// `aria-required` prop here (rather than forwarding the native
// `required` attribute) because react-admin relies on JS validation,
// not the browser's native validation. This marks inputs with a
// `required()` validator as required for assistive technologies
// without triggering native browser validation. See issue #9585.
const htmlInputProps = {
...htmlInputPropsFromConsumer,
...(ariaRequired ? { 'aria-required': true } : {}),
};
const hasHtmlInputProps = Object.keys(htmlInputProps).length > 0;

// @ts-expect-error slotProps do not yet exist in MUI v5
const htmlInputSlot = rest.slotProps?.htmlInput;
// slotProps.htmlInput may be an object or, in MUI v6+, a callback
// receiving the owner state. Preserve the caller's form when merging.
const mergedHtmlInputSlot =
typeof htmlInputSlot === 'function'
? ownerState => ({
...htmlInputProps,
...htmlInputSlot(ownerState),
})
: { ...htmlInputProps, ...htmlInputSlot };

const mergedSlotProps = {
// @ts-expect-error slotProps do not yet exist in MUI v5
...rest.slotProps,
// @ts-expect-error slotProps do not yet exist in MUI v5
input: { ...inputProps, ...rest.slotProps?.input },
...(hasHtmlInputProps ? { htmlInput: mergedHtmlInputSlot } : {}),
};

return (
Expand All @@ -175,7 +202,11 @@ export const ResettableTextField = forwardRef(
margin={margin}
className={className}
{...rest}
{...(muiMajor >= 6 ? { slotProps: mergedSlotProps } : {})}
{...(muiMajor >= 6
? { slotProps: mergedSlotProps }
: hasHtmlInputProps
? { inputProps: htmlInputProps }
: {})}
inputRef={ref}
/>
);
Expand Down
61 changes: 61 additions & 0 deletions packages/ra-ui-materialui/src/input/TextInput.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,67 @@ describe('<TextInput />', () => {
});
});

describe('aria-required', () => {
it('should mark the input as required with aria-required (not the native required attribute) when a required() validator is set', () => {
render(
<AdminContext dataProvider={testDataProvider()}>
<ResourceContextProvider value="posts">
<SimpleForm onSubmit={jest.fn}>
<TextInput
{...defaultProps}
validate={required()}
/>
</SimpleForm>
</ResourceContextProvider>
</AdminContext>
);
const input = screen.getByLabelText(
'resources.posts.fields.title *'
);
expect(input).toBeRequired();
expect(input).toHaveAttribute('aria-required', 'true');
// react-admin uses JS validation, so the native required attribute
// must not be set (it would trigger native browser validation).
expect(input).not.toHaveAttribute('required');
});

it('should not mark the input as required when no required validator is set', () => {
render(
<AdminContext dataProvider={testDataProvider()}>
<ResourceContextProvider value="posts">
<SimpleForm onSubmit={jest.fn}>
<TextInput {...defaultProps} />
</SimpleForm>
</ResourceContextProvider>
</AdminContext>
);
const input = screen.getByLabelText('resources.posts.fields.title');
expect(input).not.toBeRequired();
expect(input).not.toHaveAttribute('aria-required');
});

it('should preserve caller-provided inputProps while adding aria-required', () => {
render(
<AdminContext dataProvider={testDataProvider()}>
<ResourceContextProvider value="posts">
<SimpleForm onSubmit={jest.fn}>
<TextInput
{...defaultProps}
validate={required()}
inputProps={{ 'data-custom': 'kept' }}
/>
</SimpleForm>
</ResourceContextProvider>
</AdminContext>
);
const input = screen.getByLabelText(
'resources.posts.fields.title *'
);
expect(input).toHaveAttribute('aria-required', 'true');
expect(input).toHaveAttribute('data-custom', 'kept');
});
});

it('should keep null values', async () => {
const onSuccess = jest.fn();
render(<ValueNull onSuccess={onSuccess} />);
Expand Down
1 change: 1 addition & 0 deletions packages/ra-ui-materialui/src/input/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const TextInput = (props: TextInputProps) => {
<StyledResettableTextField
id={id}
{...field}
aria-required={isRequired || undefined}
className={clsx('ra-input', `ra-input-${source}`, className)}
label={
label !== '' && label !== false ? (
Expand Down