Skip to content

Commit e41e149

Browse files
feat: Add native attributes to badge, checkbox, icon, spinner (#3828)
1 parent 18e7222 commit e41e149

File tree

13 files changed

+253
-12
lines changed

13 files changed

+253
-12
lines changed

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

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3575,6 +3575,28 @@ use the \`id\` attribute, consider setting it on a parent element instead.",
35753575
"optional": true,
35763576
"type": "string",
35773577
},
3578+
{
3579+
"description": "Attributes to add to the native element.
3580+
Some attributes will be automatically combined with internal attribute values:
3581+
- \`className\` will be appended.
3582+
- Event handlers will be chained, unless the default is prevented.
3583+
3584+
We do not support using this attribute to apply custom styling.",
3585+
"inlineType": {
3586+
"name": "Omit<React.HTMLAttributes<HTMLElement>, "children"> & Record<\`data-\${string}\`, string>",
3587+
"type": "union",
3588+
"values": [
3589+
"Omit<React.HTMLAttributes<HTMLElement>, "children">",
3590+
"Record<\`data-\${string}\`, string>",
3591+
],
3592+
},
3593+
"name": "nativeAttributes",
3594+
"optional": true,
3595+
"systemTags": [
3596+
"core",
3597+
],
3598+
"type": "Omit<React.HTMLAttributes<HTMLElement>, "children"> & Record<\`data-\${string}\`, string>",
3599+
},
35783600
{
35793601
"inlineType": {
35803602
"name": "BadgeProps.Style",
@@ -7046,6 +7068,28 @@ in the native control.",
70467068
"optional": true,
70477069
"type": "string",
70487070
},
7071+
{
7072+
"description": "Attributes to add to the native \`input\` element.
7073+
Some attributes will be automatically combined with internal attribute values:
7074+
- \`className\` will be appended.
7075+
- Event handlers will be chained, unless the default is prevented.
7076+
7077+
We do not support using this attribute to apply custom styling.",
7078+
"inlineType": {
7079+
"name": "Omit<React.InputHTMLAttributes<HTMLInputElement>, "children"> & Record<\`data-\${string}\`, string>",
7080+
"type": "union",
7081+
"values": [
7082+
"Omit<React.InputHTMLAttributes<HTMLInputElement>, "children">",
7083+
"Record<\`data-\${string}\`, string>",
7084+
],
7085+
},
7086+
"name": "nativeInputAttributes",
7087+
"optional": true,
7088+
"systemTags": [
7089+
"core",
7090+
],
7091+
"type": "Omit<React.InputHTMLAttributes<HTMLInputElement>, "children"> & Record<\`data-\${string}\`, string>",
7092+
},
70497093
{
70507094
"description": "Specifies if the control is read-only, which prevents the
70517095
user from modifying the value. Should be used only inside forms.
@@ -13287,6 +13331,28 @@ use the \`id\` attribute, consider setting it on a parent element instead.",
1328713331
"optional": true,
1328813332
"type": "string",
1328913333
},
13334+
{
13335+
"description": "Attributes to add to the native element.
13336+
Some attributes will be automatically combined with internal attribute values:
13337+
- \`className\` will be appended.
13338+
- Event handlers will be chained, unless the default is prevented.
13339+
13340+
We do not support using this attribute to apply custom styling.",
13341+
"inlineType": {
13342+
"name": "Omit<React.HTMLAttributes<HTMLElement>, "children"> & Record<\`data-\${string}\`, string>",
13343+
"type": "union",
13344+
"values": [
13345+
"Omit<React.HTMLAttributes<HTMLElement>, "children">",
13346+
"Record<\`data-\${string}\`, string>",
13347+
],
13348+
},
13349+
"name": "nativeAttributes",
13350+
"optional": true,
13351+
"systemTags": [
13352+
"core",
13353+
],
13354+
"type": "Omit<React.HTMLAttributes<HTMLElement>, "children"> & Record<\`data-\${string}\`, string>",
13355+
},
1329013356
{
1329113357
"defaultValue": "'normal'",
1329213358
"description": "Specifies the size of the icon.
@@ -22022,6 +22088,28 @@ use the \`id\` attribute, consider setting it on a parent element instead.",
2202222088
"optional": true,
2202322089
"type": "string",
2202422090
},
22091+
{
22092+
"description": "Attributes to add to the native element.
22093+
Some attributes will be automatically combined with internal attribute values:
22094+
- \`className\` will be appended.
22095+
- Event handlers will be chained, unless the default is prevented.
22096+
22097+
We do not support using this attribute to apply custom styling.",
22098+
"inlineType": {
22099+
"name": "Omit<React.HTMLAttributes<HTMLElement>, "children"> & Record<\`data-\${string}\`, string>",
22100+
"type": "union",
22101+
"values": [
22102+
"Omit<React.HTMLAttributes<HTMLElement>, "children">",
22103+
"Record<\`data-\${string}\`, string>",
22104+
],
22105+
},
22106+
"name": "nativeAttributes",
22107+
"optional": true,
22108+
"systemTags": [
22109+
"core",
22110+
],
22111+
"type": "Omit<React.HTMLAttributes<HTMLElement>, "children"> & Record<\`data-\${string}\`, string>",
22112+
},
2202522113
{
2202622114
"defaultValue": "'normal'",
2202722115
"description": "Specifies the size of the spinner.",

src/badge/__tests__/badge.test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,15 @@ describe('Style API', () => {
8585
expect(getComputedStyle(badge).getPropertyValue('padding-inline')).toBe('8px');
8686
});
8787
});
88+
89+
describe('native attributes', () => {
90+
it('adds native attributes', () => {
91+
const { container } = render(<Badge nativeAttributes={{ 'data-testid': 'my-test-id' }} />);
92+
expect(container.querySelector('[data-testid="my-test-id"]')).not.toBeNull();
93+
});
94+
it('concatenates class names', () => {
95+
const { container } = render(<Badge nativeAttributes={{ className: 'additional-class' }} />);
96+
expect(container.firstChild).toHaveClass(styles.badge);
97+
expect(container.firstChild).toHaveClass('additional-class');
98+
});
99+
});

src/badge/index.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,31 @@ import clsx from 'clsx';
77
import { getBaseProps } from '../internal/base-component';
88
import useBaseComponent from '../internal/hooks/use-base-component';
99
import { applyDisplayName } from '../internal/utils/apply-display-name';
10+
import WithNativeAttributes from '../internal/utils/with-native-attributes';
1011
import { BadgeProps } from './interfaces';
1112
import { getBadgeStyles } from './style';
1213

1314
import styles from './styles.css.js';
1415

1516
export { BadgeProps };
1617

17-
export default function Badge({ color = 'grey', children, style, ...rest }: BadgeProps) {
18+
export default function Badge({ color = 'grey', children, style, nativeAttributes, ...rest }: BadgeProps) {
1819
const { __internalRootRef } = useBaseComponent('Badge', { props: { color } });
1920
const baseProps = getBaseProps(rest);
2021

2122
const className = clsx(baseProps.className, styles.badge, styles[`badge-color-${color}`]);
2223

2324
return (
24-
<span {...baseProps} {...{ className }} ref={__internalRootRef} style={getBadgeStyles(style)}>
25+
<WithNativeAttributes
26+
{...baseProps}
27+
tag="span"
28+
nativeAttributes={nativeAttributes}
29+
className={className}
30+
ref={__internalRootRef}
31+
style={getBadgeStyles(style)}
32+
>
2533
{children}
26-
</span>
34+
</WithNativeAttributes>
2735
);
2836
}
2937

src/badge/interfaces.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
import React from 'react';
44

55
import { BaseComponentProps } from '../internal/base-component';
6+
/**
7+
* @awsuiSystem core
8+
*/
9+
import { NativeAttributes } from '../internal/utils/with-native-attributes';
610

711
export interface BadgeProps extends BaseComponentProps {
812
/**
@@ -28,6 +32,18 @@ export interface BadgeProps extends BaseComponentProps {
2832
* @awsuiSystem core
2933
*/
3034
style?: BadgeProps.Style;
35+
36+
/**
37+
* Attributes to add to the native element.
38+
* Some attributes will be automatically combined with internal attribute values:
39+
* - `className` will be appended.
40+
* - Event handlers will be chained, unless the default is prevented.
41+
*
42+
* We do not support using this attribute to apply custom styling.
43+
*
44+
* @awsuiSystem core
45+
*/
46+
nativeAttributes?: NativeAttributes<React.HTMLAttributes<HTMLElement>>;
3147
}
3248

3349
export namespace BadgeProps {

src/checkbox/__tests__/checkbox.test.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,3 +319,16 @@ test('all style api properties', () => {
319319
expect(getComputedStyle(control).getPropertyValue(customCssProps.styleFocusRingBorderWidth)).toBe('2px');
320320
expect(getComputedStyle(label).getPropertyValue('color')).toBe('orange');
321321
});
322+
323+
describe('native attributes', () => {
324+
it('adds native attributes', () => {
325+
const { container } = render(<Checkbox checked={true} nativeInputAttributes={{ 'data-testid': 'my-test-id' }} />);
326+
expect(container.querySelector('[data-testid="my-test-id"]')).not.toBeNull();
327+
});
328+
it('concatenates class names', () => {
329+
const { container } = render(<Checkbox checked={true} nativeInputAttributes={{ className: 'additional-class' }} />);
330+
const input = container.querySelector('input');
331+
expect(input).toHaveClass(abstractSwitchStyles['native-input']);
332+
expect(input).toHaveClass('additional-class');
333+
});
334+
});

src/checkbox/interfaces.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
import React from 'react';
44

55
import { NonCancelableEventHandler } from '../internal/events';
6+
/**
7+
* @awsuiSystem core
8+
*/
9+
import { NativeAttributes } from '../internal/utils/with-native-attributes';
610
import { BaseCheckboxProps } from './base-checkbox';
711

812
export interface CheckboxProps extends BaseCheckboxProps {
@@ -33,6 +37,18 @@ export interface CheckboxProps extends BaseCheckboxProps {
3337
* @awsuiSystem core
3438
*/
3539
style?: CheckboxProps.Style;
40+
41+
/**
42+
* Attributes to add to the native `input` element.
43+
* Some attributes will be automatically combined with internal attribute values:
44+
* - `className` will be appended.
45+
* - Event handlers will be chained, unless the default is prevented.
46+
*
47+
* We do not support using this attribute to apply custom styling.
48+
*
49+
* @awsuiSystem core
50+
*/
51+
nativeInputAttributes?: NativeAttributes<React.InputHTMLAttributes<HTMLInputElement>>;
3652
}
3753

3854
export namespace CheckboxProps {

src/checkbox/internal.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { useFormFieldContext } from '../internal/context/form-field-context';
1616
import { fireNonCancelableEvent } from '../internal/events';
1717
import useForwardFocus from '../internal/hooks/forward-focus';
1818
import { InternalBaseComponentProps } from '../internal/hooks/use-base-component';
19+
import WithNativeAttributes from '../internal/utils/with-native-attributes';
1920
import { GeneratedAnalyticsMetadataCheckboxComponent } from './analytics-metadata/interfaces';
2021
import { CheckboxProps } from './interfaces';
2122
import { getAbstractSwitchStyles, getCheckboxIconStyles } from './style';
@@ -48,6 +49,7 @@ const InternalCheckbox = React.forwardRef<CheckboxProps.Ref, InternalProps>(
4849
showOutline,
4950
ariaControls,
5051
style,
52+
nativeInputAttributes,
5153
__internalRootRef,
5254
__injectAnalyticsComponentMetadata = false,
5355
...rest
@@ -99,8 +101,10 @@ const InternalCheckbox = React.forwardRef<CheckboxProps.Ref, InternalProps>(
99101
ariaControls={ariaControls}
100102
showOutline={showOutline}
101103
nativeControl={nativeControlProps => (
102-
<input
104+
<WithNativeAttributes<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>
103105
{...nativeControlProps}
106+
tag="input"
107+
nativeAttributes={nativeInputAttributes}
104108
ref={checkboxRef}
105109
type="checkbox"
106110
checked={checked}

src/icon/__tests__/icon.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,17 @@ describe('Icon Component', () => {
179179
});
180180
});
181181
});
182+
183+
describe.each(['plain', 'svg', 'url'])('native attributes: %s', type => {
184+
const attributes: Partial<IconProps> =
185+
type === 'plain' ? { name: 'add-plus' } : type === 'svg' ? { svg: <svg /> } : { url: 'https://example.com' };
186+
it('adds native attributes', () => {
187+
const { container } = render(<Icon nativeAttributes={{ 'data-testid': 'my-test-id' }} {...attributes} />);
188+
expect(container.querySelector('[data-testid="my-test-id"]')).not.toBeNull();
189+
});
190+
it('concatenates class names', () => {
191+
const { container } = render(<Icon nativeAttributes={{ className: 'additional-class' }} {...attributes} />);
192+
expect(container.firstChild).toHaveClass(styles.icon);
193+
expect(container.firstChild).toHaveClass('additional-class');
194+
});
195+
});

src/icon/interfaces.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
import React from 'react';
44

55
import { BaseComponentProps } from '../internal/base-component';
6+
/**
7+
* @awsuiSystem core
8+
*/
9+
import { NativeAttributes } from '../internal/utils/with-native-attributes';
610

711
export interface IconProps extends BaseComponentProps {
812
/**
@@ -69,6 +73,18 @@ export interface IconProps extends BaseComponentProps {
6973
* In most cases, they aren't needed, as the `svg` element inherits styles from the icon component.
7074
*/
7175
svg?: React.ReactNode;
76+
77+
/**
78+
* Attributes to add to the native element.
79+
* Some attributes will be automatically combined with internal attribute values:
80+
* - `className` will be appended.
81+
* - Event handlers will be chained, unless the default is prevented.
82+
*
83+
* We do not support using this attribute to apply custom styling.
84+
*
85+
* @awsuiSystem core
86+
*/
87+
nativeAttributes?: NativeAttributes<React.HTMLAttributes<HTMLElement>>;
7288
}
7389

7490
export namespace IconProps {

src/icon/internal.tsx

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { InternalIconContext } from '../icon-provider/context';
99
import { getBaseProps } from '../internal/base-component';
1010
import { InternalBaseComponentProps } from '../internal/hooks/use-base-component';
1111
import { useVisualRefresh } from '../internal/hooks/use-visual-mode';
12+
import WithNativeAttributes from '../internal/utils/with-native-attributes';
1213
import { IconProps } from './interfaces';
1314

1415
import styles from './styles.css.js';
@@ -46,6 +47,7 @@ const InternalIcon = ({
4647
ariaLabel,
4748
svg,
4849
badge,
50+
nativeAttributes,
4951
__internalRootRef,
5052
...props
5153
}: InternalIconProps) => {
@@ -94,17 +96,31 @@ const InternalIcon = ({
9496
);
9597
}
9698
return (
97-
<span {...baseProps} {...labelAttributes} ref={mergedRef} aria-hidden={!hasAriaLabel} style={inlineStyles}>
99+
<WithNativeAttributes
100+
{...baseProps}
101+
{...labelAttributes}
102+
tag="span"
103+
nativeAttributes={nativeAttributes}
104+
ref={mergedRef}
105+
aria-hidden={!hasAriaLabel}
106+
style={inlineStyles}
107+
>
98108
{svg}
99-
</span>
109+
</WithNativeAttributes>
100110
);
101111
}
102112

103113
if (url) {
104114
return (
105-
<span {...baseProps} ref={mergedRef} style={inlineStyles}>
115+
<WithNativeAttributes
116+
{...baseProps}
117+
tag="span"
118+
nativeAttributes={nativeAttributes}
119+
ref={mergedRef}
120+
style={inlineStyles}
121+
>
106122
<img src={url} alt={ariaLabel ?? alt} />
107-
</span>
123+
</WithNativeAttributes>
108124
);
109125
}
110126

@@ -134,9 +150,16 @@ const InternalIcon = ({
134150
}
135151

136152
return (
137-
<span {...baseProps} {...labelAttributes} ref={mergedRef} style={inlineStyles}>
153+
<WithNativeAttributes
154+
{...baseProps}
155+
{...labelAttributes}
156+
tag="span"
157+
nativeAttributes={nativeAttributes}
158+
ref={mergedRef}
159+
style={inlineStyles}
160+
>
138161
{validIcon ? iconMap(name) : undefined}
139-
</span>
162+
</WithNativeAttributes>
140163
);
141164
};
142165

0 commit comments

Comments
 (0)