Skip to content

Commit fdbe231

Browse files
feat: Native attributes support for input-based components (#3834)
1 parent 4726111 commit fdbe231

File tree

22 files changed

+395
-35
lines changed

22 files changed

+395
-35
lines changed

src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3366,6 +3366,28 @@ single form field.",
33663366
"optional": true,
33673367
"type": "string",
33683368
},
3369+
{
3370+
"description": "Attributes to add to the native \`input\` element.
3371+
Some attributes will be automatically combined with internal attribute values:
3372+
- \`className\` will be appended.
3373+
- Event handlers will be chained, unless the default is prevented.
3374+
3375+
We do not support using this attribute to apply custom styling.",
3376+
"inlineType": {
3377+
"name": "Omit<React.InputHTMLAttributes<HTMLInputElement>, "children"> & Record<\`data-\${string}\`, string>",
3378+
"type": "union",
3379+
"values": [
3380+
"Omit<React.InputHTMLAttributes<HTMLInputElement>, "children">",
3381+
"Record<\`data-\${string}\`, string>",
3382+
],
3383+
},
3384+
"name": "nativeInputAttributes",
3385+
"optional": true,
3386+
"systemTags": [
3387+
"core",
3388+
],
3389+
"type": "Omit<React.InputHTMLAttributes<HTMLInputElement>, "children"> & Record<\`data-\${string}\`, string>",
3390+
},
33693391
{
33703392
"description": "Specifies an array of options that are displayed to the user as a dropdown list.
33713393
The options can be grouped using \`OptionGroup\` objects.
@@ -9708,6 +9730,28 @@ Supported values and formats are listed in the [JavaScript Intl API specificatio
97089730
"optional": true,
97099731
"type": "string",
97109732
},
9733+
{
9734+
"description": "Attributes to add to the native \`input\` element.
9735+
Some attributes will be automatically combined with internal attribute values:
9736+
- \`className\` will be appended.
9737+
- Event handlers will be chained, unless the default is prevented.
9738+
9739+
We do not support using this attribute to apply custom styling.",
9740+
"inlineType": {
9741+
"name": "Omit<React.InputHTMLAttributes<HTMLInputElement>, "children"> & Record<\`data-\${string}\`, string>",
9742+
"type": "union",
9743+
"values": [
9744+
"Omit<React.InputHTMLAttributes<HTMLInputElement>, "children">",
9745+
"Record<\`data-\${string}\`, string>",
9746+
],
9747+
},
9748+
"name": "nativeInputAttributes",
9749+
"optional": true,
9750+
"systemTags": [
9751+
"core",
9752+
],
9753+
"type": "Omit<React.InputHTMLAttributes<HTMLInputElement>, "children"> & Record<\`data-\${string}\`, string>",
9754+
},
97119755
{
97129756
"description": "Specifies the placeholder text rendered when the value is an empty string.",
97139757
"name": "placeholder",
@@ -14460,6 +14504,28 @@ single form field.",
1446014504
"optional": true,
1446114505
"type": "string",
1446214506
},
14507+
{
14508+
"description": "Attributes to add to the native \`input\` element.
14509+
Some attributes will be automatically combined with internal attribute values:
14510+
- \`className\` will be appended.
14511+
- Event handlers will be chained, unless the default is prevented.
14512+
14513+
We do not support using this attribute to apply custom styling.",
14514+
"inlineType": {
14515+
"name": "Omit<React.InputHTMLAttributes<HTMLInputElement>, "children"> & Record<\`data-\${string}\`, string>",
14516+
"type": "union",
14517+
"values": [
14518+
"Omit<React.InputHTMLAttributes<HTMLInputElement>, "children">",
14519+
"Record<\`data-\${string}\`, string>",
14520+
],
14521+
},
14522+
"name": "nativeInputAttributes",
14523+
"optional": true,
14524+
"systemTags": [
14525+
"core",
14526+
],
14527+
"type": "Omit<React.InputHTMLAttributes<HTMLInputElement>, "children"> & Record<\`data-\${string}\`, string>",
14528+
},
1446314529
{
1446414530
"description": "Specifies the placeholder text rendered when the value is an empty string.",
1446514531
"name": "placeholder",
@@ -18474,6 +18540,28 @@ Defaults to 3. Use -1 for infinite rows.",
1847418540
"optional": true,
1847518541
"type": "string",
1847618542
},
18543+
{
18544+
"description": "Attributes to add to the native \`textarea\` element.
18545+
Some attributes will be automatically combined with internal attribute values:
18546+
- \`className\` will be appended.
18547+
- Event handlers will be chained, unless the default is prevented.
18548+
18549+
We do not support using this attribute to apply custom styling.",
18550+
"inlineType": {
18551+
"name": "Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "children"> & Record<\`data-\${string}\`, string>",
18552+
"type": "union",
18553+
"values": [
18554+
"Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "children">",
18555+
"Record<\`data-\${string}\`, string>",
18556+
],
18557+
},
18558+
"name": "nativeTextareaAttributes",
18559+
"optional": true,
18560+
"systemTags": [
18561+
"core",
18562+
],
18563+
"type": "Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "children"> & Record<\`data-\${string}\`, string>",
18564+
},
1847718565
{
1847818566
"description": "Specifies the placeholder text rendered when the value is an empty string.",
1847918567
"name": "placeholder",
@@ -24726,6 +24814,28 @@ single form field.",
2472624814
"optional": true,
2472724815
"type": "string",
2472824816
},
24817+
{
24818+
"description": "Attributes to add to the native \`textarea\` element.
24819+
Some attributes will be automatically combined with internal attribute values:
24820+
- \`className\` will be appended.
24821+
- Event handlers will be chained, unless the default is prevented.
24822+
24823+
We do not support using this attribute to apply custom styling.",
24824+
"inlineType": {
24825+
"name": "Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "children"> & Record<\`data-\${string}\`, string>",
24826+
"type": "union",
24827+
"values": [
24828+
"Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "children">",
24829+
"Record<\`data-\${string}\`, string>",
24830+
],
24831+
},
24832+
"name": "nativeTextareaAttributes",
24833+
"optional": true,
24834+
"systemTags": [
24835+
"core",
24836+
],
24837+
"type": "Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "children"> & Record<\`data-\${string}\`, string>",
24838+
},
2472924839
{
2473024840
"description": "Specifies the placeholder text rendered when the value is an empty string.",
2473124841
"name": "placeholder",
@@ -25118,6 +25228,28 @@ single form field.",
2511825228
"optional": true,
2511925229
"type": "string",
2512025230
},
25231+
{
25232+
"description": "Attributes to add to the native \`input\` element.
25233+
Some attributes will be automatically combined with internal attribute values:
25234+
- \`className\` will be appended.
25235+
- Event handlers will be chained, unless the default is prevented.
25236+
25237+
We do not support using this attribute to apply custom styling.",
25238+
"inlineType": {
25239+
"name": "Omit<React.InputHTMLAttributes<HTMLInputElement>, "children"> & Record<\`data-\${string}\`, string>",
25240+
"type": "union",
25241+
"values": [
25242+
"Omit<React.InputHTMLAttributes<HTMLInputElement>, "children">",
25243+
"Record<\`data-\${string}\`, string>",
25244+
],
25245+
},
25246+
"name": "nativeInputAttributes",
25247+
"optional": true,
25248+
"systemTags": [
25249+
"core",
25250+
],
25251+
"type": "Omit<React.InputHTMLAttributes<HTMLInputElement>, "children"> & Record<\`data-\${string}\`, string>",
25252+
},
2512125253
{
2512225254
"description": "Specifies the placeholder text rendered when the value is an empty string.",
2512325255
"name": "placeholder",

src/autosuggest/__tests__/autosuggest.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,3 +485,42 @@ test('findOptionInGroup', () => {
485485
wrapper.findNativeInput().focus();
486486
expect(wrapper.findDropdown().findOptionInGroup(1, 2)).toBeTruthy();
487487
});
488+
489+
describe('native attributes', () => {
490+
it('adds native attributes', () => {
491+
const { wrapper } = renderAutosuggest(
492+
<Autosuggest {...defaultProps} nativeInputAttributes={{ 'data-testid': 'my-test-id' }} />
493+
);
494+
expect(wrapper.getElement().querySelectorAll('[data-testid="my-test-id"]')).toHaveLength(1);
495+
expect(wrapper.getElement().querySelectorAll('input[data-testid="my-test-id"]')).toHaveLength(1);
496+
});
497+
498+
it('chains autosuggest-specific handlers', () => {
499+
const onClick = jest.fn();
500+
const { wrapper } = renderAutosuggest(<Autosuggest {...defaultProps} nativeInputAttributes={{ onClick }} />);
501+
502+
wrapper.findNativeInput().click();
503+
504+
expect(onClick).toHaveBeenCalled();
505+
});
506+
507+
it('warns about overriding autosuggest-specific attributes', () => {
508+
renderAutosuggest(<Autosuggest {...defaultProps} nativeInputAttributes={{ 'aria-autocomplete': 'both' }} />);
509+
expect(warnOnce).toHaveBeenCalledTimes(1);
510+
expect(warnOnce).toHaveBeenCalledWith(
511+
'Autosuggest',
512+
'Overriding native attribute [aria-autocomplete] which has a Cloudscape-provided value'
513+
);
514+
});
515+
516+
it('warns about overriding input-specific attributes', () => {
517+
renderAutosuggest(
518+
<Autosuggest {...defaultProps} ariaRequired={true} nativeInputAttributes={{ 'aria-required': 'false' }} />
519+
);
520+
expect(warnOnce).toHaveBeenCalledTimes(1);
521+
expect(warnOnce).toHaveBeenCalledWith(
522+
'Input',
523+
'Overriding native attribute [aria-required] which has a Cloudscape-provided value'
524+
);
525+
});
526+
});

src/date-input/__tests__/date-input.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,4 +430,12 @@ describe('Date Input component', () => {
430430
container.querySelector('button')!.click();
431431
expect(createWrapper().findDateInput()!.findNativeInput().getElement()).toHaveFocus();
432432
});
433+
434+
describe('native attributes', () => {
435+
it('adds native attributes', () => {
436+
const { wrapper } = renderDateInput({ value: '', nativeInputAttributes: { 'data-testid': 'my-test-id' } });
437+
expect(wrapper.getElement().querySelectorAll('[data-testid="my-test-id"]')).toHaveLength(1);
438+
expect(wrapper.getElement().querySelectorAll('input[data-testid="my-test-id"]')).toHaveLength(1);
439+
});
440+
});
433441
});

src/input/__tests__/input.test.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,4 +502,17 @@ describe('Input', () => {
502502
fireEvent.wheel(input);
503503
expect(blurSpy).toHaveBeenCalledTimes(1);
504504
});
505+
506+
describe('native attributes', () => {
507+
it('adds native attributes', () => {
508+
const { container } = render(<Input value="" nativeInputAttributes={{ 'data-testid': 'my-test-id' }} />);
509+
expect(container.querySelectorAll('[data-testid="my-test-id"]')).toHaveLength(1);
510+
});
511+
it('concatenates class names', () => {
512+
const { container } = render(<Input value="" nativeInputAttributes={{ className: 'additional-class' }} />);
513+
const input = container.querySelector('input');
514+
expect(input).toHaveClass(styles.input);
515+
expect(input).toHaveClass('additional-class');
516+
});
517+
});
505518
});

src/input/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const Input = React.forwardRef(
4242
warning,
4343
controlId,
4444
clearAriaLabel,
45+
nativeInputAttributes,
4546
...rest
4647
}: InputProps,
4748
ref: Ref<InputProps.Ref>
@@ -97,6 +98,7 @@ const Input = React.forwardRef(
9798
warning,
9899
controlId,
99100
clearAriaLabel,
101+
nativeInputAttributes,
100102
}}
101103
className={clsx(styles.root, baseProps.className)}
102104
__inheritFormFieldProps={true}

src/input/interfaces.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
import { BaseComponentProps } from '../internal/base-component';
44
import { FormFieldValidationControlProps } from '../internal/context/form-field-context';
55
import { BaseKeyDetail, CancelableEventHandler, NonCancelableEventHandler } from '../internal/events';
6+
/**
7+
* @awsuiSystem core
8+
*/
9+
import { NativeAttributes } from '../internal/utils/with-native-attributes';
610

711
export interface BaseInputProps {
812
/**
@@ -73,6 +77,18 @@ export interface BaseInputProps {
7377
* The event `detail` contains the current value of the field.
7478
*/
7579
onChange?: NonCancelableEventHandler<InputProps.ChangeDetail>;
80+
81+
/**
82+
* Attributes to add to the native `input` element.
83+
* Some attributes will be automatically combined with internal attribute values:
84+
* - `className` will be appended.
85+
* - Event handlers will be chained, unless the default is prevented.
86+
*
87+
* We do not support using this attribute to apply custom styling.
88+
*
89+
* @awsuiSystem core
90+
*/
91+
nativeInputAttributes?: NativeAttributes<React.InputHTMLAttributes<HTMLInputElement>>;
7692
}
7793

7894
export interface InputAutoCorrect {

src/input/internal.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { FormFieldValidationControlProps, useFormFieldContext } from '../interna
1818
import { fireKeyboardEvent, fireNonCancelableEvent, NonCancelableEventHandler } from '../internal/events';
1919
import { InternalBaseComponentProps } from '../internal/hooks/use-base-component';
2020
import { useDebounceCallback } from '../internal/hooks/use-debounce-callback';
21+
import WithNativeAttributes, { SkipWarnings } from '../internal/utils/with-native-attributes';
2122
import {
2223
GeneratedAnalyticsMetadataInputClearInput,
2324
GeneratedAnalyticsMetadataInputComponent,
@@ -42,14 +43,14 @@ export interface InternalInputProps
4243
__rightIcon?: IconProps['name'];
4344
__onRightIconClick?: () => void;
4445

45-
__nativeAttributes?: React.InputHTMLAttributes<HTMLInputElement>;
4646
__noBorderRadius?: boolean;
4747

4848
__onDelayedInput?: NonCancelableEventHandler<BaseChangeDetail>;
4949
__onBlurWithDetail?: NonCancelableEventHandler<{ relatedTarget: Node | null }>;
5050

5151
__inheritFormFieldProps?: boolean;
5252
__injectAnalyticsComponentMetadata?: boolean;
53+
__skipNativeAttributesWarnings?: SkipWarnings;
5354
}
5455

5556
function InternalInput(
@@ -86,10 +87,11 @@ function InternalInput(
8687
__onBlurWithDetail,
8788
onBlur,
8889
onFocus,
89-
__nativeAttributes,
90+
nativeInputAttributes,
9091
__internalRootRef,
9192
__inheritFormFieldProps,
9293
__injectAnalyticsComponentMetadata,
94+
__skipNativeAttributesWarnings,
9395
...rest
9496
}: InternalInputProps,
9597
ref: Ref<HTMLInputElement>
@@ -154,7 +156,6 @@ function InternalInput(
154156
fireNonCancelableEvent(__onBlurWithDetail, { relatedTarget: e.relatedTarget });
155157
},
156158
onFocus: onFocus && (() => fireNonCancelableEvent(onFocus)),
157-
...__nativeAttributes,
158159
};
159160

160161
if (type === 'number') {
@@ -208,7 +209,14 @@ function InternalInput(
208209
<InternalIcon name={__leftIcon} variant={disabled || readOnly ? 'disabled' : __leftIconVariant} />
209210
</span>
210211
)}
211-
<input ref={mergedRef} {...attributes} />
212+
<WithNativeAttributes
213+
{...attributes}
214+
tag="input"
215+
componentName="Input"
216+
nativeAttributes={nativeInputAttributes}
217+
skipWarnings={__skipNativeAttributesWarnings}
218+
ref={mergedRef}
219+
/>
212220
{__rightIcon && (
213221
<span
214222
className={styles['input-icon-right']}

src/internal/components/autosuggest-input/index.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ import {
1212
InputClearLabel,
1313
InputKeyEvents,
1414
} from '../../../input/interfaces';
15-
import InternalInput, { InternalInputProps } from '../../../input/internal';
15+
import InternalInput from '../../../input/internal';
1616
import { BaseComponentProps, getBaseProps } from '../../base-component';
1717
import { FormFieldValidationControlProps, useFormFieldContext } from '../../context/form-field-context';
1818
import { BaseKeyDetail, fireCancelableEvent, fireNonCancelableEvent, NonCancelableEventHandler } from '../../events';
1919
import { InternalBaseComponentProps } from '../../hooks/use-base-component';
2020
import { KeyCode } from '../../keycode';
2121
import { nodeBelongs } from '../../utils/node-belongs';
22+
import { processAttributes } from '../../utils/with-native-attributes';
2223
import Dropdown from '../dropdown';
2324
import { ExpandToViewport } from '../dropdown/interfaces';
2425

@@ -87,6 +88,7 @@ const AutosuggestInput = React.forwardRef(
8788
dropdownFooter = null,
8889
dropdownWidth,
8990
loopFocus,
91+
nativeInputAttributes,
9092
onCloseDropdown,
9193
onDelayedInput,
9294
onPressArrowDown,
@@ -212,7 +214,7 @@ const AutosuggestInput = React.forwardRef(
212214
};
213215

214216
const expanded = open && dropdownExpanded;
215-
const nativeAttributes: InternalInputProps['__nativeAttributes'] = {
217+
const nativeAttributes: BaseInputProps['nativeInputAttributes'] = {
216218
name,
217219
placeholder,
218220
autoFocus,
@@ -277,7 +279,8 @@ const AutosuggestInput = React.forwardRef(
277279
clearAriaLabel={clearAriaLabel}
278280
ref={inputRef}
279281
autoComplete={false}
280-
__nativeAttributes={nativeAttributes}
282+
nativeInputAttributes={processAttributes(nativeAttributes, nativeInputAttributes, 'Autosuggest')}
283+
__skipNativeAttributesWarnings={Object.keys(nativeAttributes)}
281284
{...formFieldContext}
282285
/>
283286
}

0 commit comments

Comments
 (0)