Skip to content

Commit 4402632

Browse files
authored
feat(FieldError): expose elementType prop to control host element (adobe#9759)
* feat(FieldError): expose elementType prop to control host element Fixes adobe#9757 — Option C: expose elementType on FieldErrorProps so users can set elementType="div" when wrapping block-level children (e.g. <ul>) without triggering the DOMElement render mismatch warning. * test(FieldError): replace html-validation test with warn suppression tests
1 parent 1ce0a54 commit 4402632

File tree

2 files changed

+71
-4
lines changed

2 files changed

+71
-4
lines changed

packages/react-aria-components/src/FieldError.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@ export interface FieldErrorProps extends RenderProps<FieldErrorRenderProps>, DOM
2424
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. A function may be provided to compute the class based on component state.
2525
* @default 'react-aria-FieldError'
2626
*/
27-
className?: ClassNameOrFunction<FieldErrorRenderProps>
27+
className?: ClassNameOrFunction<FieldErrorRenderProps>,
28+
/**
29+
* The HTML element type to render. Defaults to `'span'`.
30+
* Set to `'div'` when using block-level children (e.g. `<ul>`) to avoid invalid HTML.
31+
* @default 'span'
32+
*/
33+
elementType?: string
2834
}
2935

3036
/**
@@ -41,9 +47,10 @@ export const FieldError = forwardRef(function FieldError(props: FieldErrorProps,
4147

4248
const FieldErrorInner = forwardRef((props: FieldErrorProps, ref: ForwardedRef<HTMLElement>) => {
4349
let validation = useContext(FieldErrorContext)!;
44-
let domProps = filterDOMProps(props, {global: true})!;
50+
let {elementType, ...restProps} = props;
51+
let domProps = filterDOMProps(restProps, {global: true})!;
4552
let renderProps = useRenderProps({
46-
...props,
53+
...restProps,
4754
defaultClassName: 'react-aria-FieldError',
4855
defaultChildren: validation.validationErrors.length === 0 ? undefined : validation.validationErrors.join(' '),
4956
values: validation
@@ -53,5 +60,5 @@ const FieldErrorInner = forwardRef((props: FieldErrorProps, ref: ForwardedRef<HT
5360
return null;
5461
}
5562

56-
return <Text slot="errorMessage" {...domProps} {...renderProps} ref={ref} />;
63+
return <Text slot="errorMessage" elementType={elementType} {...domProps} {...renderProps} ref={ref} />;
5764
});

packages/react-aria-components/test/FieldError.test.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,64 @@ describe('FieldError', function () {
2020
const element = getByTestId(TEST_ID);
2121
expect(element).toHaveTextContent('An error');
2222
});
23+
24+
it('renders as span by default', async () => {
25+
const {getByText} = render(
26+
<TextField isInvalid>
27+
<Label>Email</Label>
28+
<Input />
29+
<FieldError>An error</FieldError>
30+
</TextField>
31+
);
32+
33+
const element = getByText('An error');
34+
expect(element.tagName).toBe('SPAN');
35+
});
36+
37+
it('supports elementType prop', async () => {
38+
const {getByText} = render(
39+
<TextField isInvalid>
40+
<Label>Email</Label>
41+
<Input />
42+
<FieldError elementType="div">An error</FieldError>
43+
</TextField>
44+
);
45+
46+
const element = getByText('An error');
47+
expect(element.tagName).toBe('DIV');
48+
});
49+
50+
it('does not warn when render prop returns element matching elementType', async () => {
51+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
52+
53+
render(
54+
<TextField isInvalid>
55+
<Label>Email</Label>
56+
<Input />
57+
<FieldError elementType="div" render={(props) => <div {...props}><ul><li>Error</li></ul></div>}>
58+
Error
59+
</FieldError>
60+
</TextField>
61+
);
62+
63+
expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('Unexpected DOM element'));
64+
warnSpy.mockRestore();
65+
});
66+
67+
it('warns when render prop returns element that does not match elementType', async () => {
68+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
69+
70+
render(
71+
<TextField isInvalid>
72+
<Label>Email</Label>
73+
<Input />
74+
<FieldError render={(props) => <div {...props} />}>
75+
Error
76+
</FieldError>
77+
</TextField>
78+
);
79+
80+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Unexpected DOM element'));
81+
warnSpy.mockRestore();
82+
});
2383
});

0 commit comments

Comments
 (0)