Skip to content

Commit 698d7f4

Browse files
feat: Allow button native element attributes to be set (#3665)
1 parent f6bc21b commit 698d7f4

File tree

16 files changed

+479
-36
lines changed

16 files changed

+479
-36
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
@@ -4233,6 +4233,50 @@ It prevents users from clicking the button, but it can still be focused.",
42334233
"optional": true,
42344234
"type": "string",
42354235
},
4236+
{
4237+
"description": "Attributes to add to the native \`a\` element (when \`href\` is provided).
4238+
Some attributes will be automatically combined with internal attribute values:
4239+
- \`className\` will be appended.
4240+
- Event handlers will be chained, unless the default is prevented.
4241+
4242+
We do not support using this attribute to apply custom styling.",
4243+
"inlineType": {
4244+
"name": "Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "children"> & Record<\`data-\${string}\`, string>",
4245+
"type": "union",
4246+
"values": [
4247+
"Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "children">",
4248+
"Record<\`data-\${string}\`, string>",
4249+
],
4250+
},
4251+
"name": "nativeAnchorAttributes",
4252+
"optional": true,
4253+
"systemTags": [
4254+
"core",
4255+
],
4256+
"type": "Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "children"> & Record<\`data-\${string}\`, string>",
4257+
},
4258+
{
4259+
"description": "Attributes to add to the native \`button\` element.
4260+
Some attributes will be automatically combined with internal attribute values:
4261+
- \`className\` will be appended.
4262+
- Event handlers will be chained, unless the default is prevented.
4263+
4264+
We do not support using this attribute to apply custom styling.",
4265+
"inlineType": {
4266+
"name": "Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> & Record<\`data-\${string}\`, string>",
4267+
"type": "union",
4268+
"values": [
4269+
"Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children">",
4270+
"Record<\`data-\${string}\`, string>",
4271+
],
4272+
},
4273+
"name": "nativeButtonAttributes",
4274+
"optional": true,
4275+
"systemTags": [
4276+
"core",
4277+
],
4278+
"type": "Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> & Record<\`data-\${string}\`, string>",
4279+
},
42364280
{
42374281
"description": "Adds a \`rel\` attribute to the link. By default, the component sets the \`rel\` attribute to "noopener noreferrer" when \`target\` is \`"_blank"\`.
42384282
If the \`rel\` property is provided, it overrides the default behavior.",
@@ -20182,6 +20226,50 @@ It prevents users from clicking the button, but it can still be focused.",
2018220226
"optional": true,
2018320227
"type": "string",
2018420228
},
20229+
{
20230+
"description": "Attributes to add to the native \`a\` element (when \`href\` is provided).
20231+
Some attributes will be automatically combined with internal attribute values:
20232+
- \`className\` will be appended.
20233+
- Event handlers will be chained, unless the default is prevented.
20234+
20235+
We do not support using this attribute to apply custom styling.",
20236+
"inlineType": {
20237+
"name": "Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "children"> & Record<\`data-\${string}\`, string>",
20238+
"type": "union",
20239+
"values": [
20240+
"Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "children">",
20241+
"Record<\`data-\${string}\`, string>",
20242+
],
20243+
},
20244+
"name": "nativeAnchorAttributes",
20245+
"optional": true,
20246+
"systemTags": [
20247+
"core",
20248+
],
20249+
"type": "Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "children"> & Record<\`data-\${string}\`, string>",
20250+
},
20251+
{
20252+
"description": "Attributes to add to the native \`button\` element.
20253+
Some attributes will be automatically combined with internal attribute values:
20254+
- \`className\` will be appended.
20255+
- Event handlers will be chained, unless the default is prevented.
20256+
20257+
We do not support using this attribute to apply custom styling.",
20258+
"inlineType": {
20259+
"name": "Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> & Record<\`data-\${string}\`, string>",
20260+
"type": "union",
20261+
"values": [
20262+
"Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children">",
20263+
"Record<\`data-\${string}\`, string>",
20264+
],
20265+
},
20266+
"name": "nativeButtonAttributes",
20267+
"optional": true,
20268+
"systemTags": [
20269+
"core",
20270+
],
20271+
"type": "Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> & Record<\`data-\${string}\`, string>",
20272+
},
2018520273
{
2018620274
"defaultValue": "false",
2018720275
"description": "Sets the toggle button to pressed state.",

src/app-layout/visual-refresh/mobile-toolbar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export default function MobileToolbar() {
6969
className={testutilStyles['navigation-toggle']}
7070
ref={navigationRefs.toggle}
7171
disabled={hasDrawerViewportOverlay}
72-
__nativeAttributes={{ 'aria-haspopup': navigationOpen ? undefined : true }}
72+
nativeButtonAttributes={{ 'aria-haspopup': navigationOpen ? undefined : true }}
7373
/>
7474
</nav>
7575
)}
@@ -97,7 +97,7 @@ export default function MobileToolbar() {
9797
onClick={() => handleToolsClick(true)}
9898
variant="icon"
9999
ref={toolsRefs.toggle}
100-
__nativeAttributes={{ 'aria-haspopup': true }}
100+
nativeButtonAttributes={{ 'aria-haspopup': true }}
101101
/>
102102
</aside>
103103
)

src/app-layout/visual-refresh/trigger-button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ function TriggerButton(
6767
badge={badge}
6868
onClick={onClick}
6969
variant="icon"
70-
__nativeAttributes={{
70+
nativeButtonAttributes={{
7171
'aria-haspopup': true,
7272
...(testId && {
7373
'data-testid': testId,

src/attribute-editor/internal.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ const InternalAttributeEditor = React.forwardRef(
152152
// Using aria-disabled="true" and tabindex="-1" instead of "disabled"
153153
// because focus can be dynamically moved to this button by calling
154154
// `focusAddButton()` on the ref.
155-
__nativeAttributes={disableAddButton ? { tabIndex: -1 } : {}}
155+
nativeButtonAttributes={disableAddButton ? { tabIndex: -1 } : {}}
156+
__skipNativeAttributesWarnings={true}
156157
__focusable={true}
157158
onClick={onAddButtonClick}
158159
formAction="none"

src/button-dropdown/internal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ const InternalButtonDropdown = React.forwardRef(
168168
ariaLabel,
169169
ariaExpanded: canBeOpened && isOpen,
170170
formAction: 'none',
171-
__nativeAttributes: {
171+
nativeButtonAttributes: {
172172
'aria-haspopup': true,
173173
},
174174
};

src/button/__tests__/button.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -830,8 +830,8 @@ describe('table grid navigation support', () => {
830830
test('does not override explicit tab index with 0', () => {
831831
const { setCurrentTarget } = renderWithSingleTabStopNavigation(
832832
<div>
833-
<InternalButton id="button1" __nativeAttributes={{ tabIndex: -2 }} />
834-
<InternalButton id="button2" __nativeAttributes={{ tabIndex: -2 }} />
833+
<InternalButton id="button1" nativeButtonAttributes={{ tabIndex: -2 }} />
834+
<InternalButton id="button2" nativeButtonAttributes={{ tabIndex: -2 }} />
835835
</div>
836836
);
837837
setCurrentTarget(getButton('#button1'));

src/button/__tests__/internal.test.tsx

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,8 @@ import { mockedFunnelInteractionId, mockFunnelMetrics } from '../../internal/ana
1212

1313
import styles from '../../../lib/components/button/styles.css.js';
1414

15-
test('specific properties take precedence over nativeAttributes', () => {
16-
const { container } = render(
17-
<InternalButton ariaLabel="property" __nativeAttributes={{ 'aria-label': 'native attribute' }} />
18-
);
19-
expect(container.querySelector('button')).toHaveAttribute('aria-label', 'property');
20-
});
21-
22-
test('supports providing custom attributes', () => {
23-
const { container } = render(<InternalButton __nativeAttributes={{ 'aria-hidden': 'true' }} />);
24-
expect(container.querySelector('button')).toHaveAttribute('aria-hidden', 'true');
25-
});
26-
2715
test('supports __iconClass property', () => {
28-
const { container } = render(
29-
<InternalButton __iconClass="example-class" iconName="settings" __nativeAttributes={{ 'aria-expanded': 'true' }} />
30-
);
16+
const { container } = render(<InternalButton __iconClass="example-class" iconName="settings" />);
3117
expect(container.querySelector(`button .${styles.icon}`)).toHaveClass('example-class');
3218
});
3319

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React from 'react';
4+
import { render } from '@testing-library/react';
5+
6+
import Button from '../../../lib/components/button';
7+
import createWrapper from '../../../lib/components/test-utils/dom';
8+
9+
import styles from '../../../lib/components/button/styles.css.js';
10+
11+
describe('Button native attributes', () => {
12+
test('passes nativeAttributes to the button element', () => {
13+
const { container } = render(
14+
<Button
15+
nativeButtonAttributes={{
16+
'data-testid': 'test-button',
17+
'aria-controls': 'controlled-element',
18+
}}
19+
>
20+
Button text
21+
</Button>
22+
);
23+
const wrapper = createWrapper(container).findButton()!;
24+
25+
expect(wrapper.getElement()).toHaveAttribute('data-testid', 'test-button');
26+
expect(wrapper.getElement()).toHaveAttribute('aria-controls', 'controlled-element');
27+
});
28+
29+
test('passes nativeAttributes to the anchor element when href is provided', () => {
30+
const { container } = render(
31+
<Button
32+
href="https://example.com"
33+
nativeAnchorAttributes={{
34+
'data-testid': 'test-link',
35+
'aria-controls': 'controlled-element',
36+
}}
37+
>
38+
Link text
39+
</Button>
40+
);
41+
const wrapper = createWrapper(container).findButton()!;
42+
43+
expect(wrapper.getElement()).toHaveAttribute('data-testid', 'test-link');
44+
expect(wrapper.getElement()).toHaveAttribute('aria-controls', 'controlled-element');
45+
expect(wrapper.getElement().tagName).toBe('A');
46+
});
47+
48+
test('overrides built-in attributes with nativeAttributes', () => {
49+
const { container } = render(
50+
<Button
51+
ariaLabel="Button label"
52+
nativeButtonAttributes={{
53+
'aria-label': 'Override label',
54+
}}
55+
>
56+
Button text
57+
</Button>
58+
);
59+
const wrapper = createWrapper(container).findButton()!;
60+
61+
expect(wrapper.getElement()).toHaveAccessibleName('Override label');
62+
});
63+
64+
test('combines built-in className with any provided in nativeAttributes', () => {
65+
const { container } = render(
66+
<Button
67+
ariaLabel="Button label"
68+
nativeButtonAttributes={{
69+
className: 'my-additional-class',
70+
}}
71+
>
72+
Button text
73+
</Button>
74+
);
75+
const wrapper = createWrapper(container).findButton()!;
76+
77+
expect(wrapper.getElement()).toHaveClass('my-additional-class');
78+
expect(wrapper.getElement()).toHaveClass(styles.button);
79+
});
80+
81+
test('events get chained', () => {
82+
const mainClick = jest.fn();
83+
const nativeClick = jest.fn();
84+
const { container } = render(
85+
<Button
86+
ariaLabel="Button label"
87+
onClick={mainClick}
88+
nativeButtonAttributes={{
89+
onClick: nativeClick,
90+
}}
91+
>
92+
Button text
93+
</Button>
94+
);
95+
const wrapper = createWrapper(container).findButton()!;
96+
wrapper.click();
97+
expect(mainClick).toHaveBeenCalled();
98+
expect(nativeClick).toHaveBeenCalled();
99+
});
100+
});

src/button/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ const Button = React.forwardRef(
4242
form,
4343
i18nStrings,
4444
style,
45+
nativeButtonAttributes,
46+
nativeAnchorAttributes,
4547
...props
4648
}: ButtonProps,
4749
ref: React.Ref<ButtonProps.Ref>
@@ -83,6 +85,8 @@ const Button = React.forwardRef(
8385
form={form}
8486
i18nStrings={i18nStrings}
8587
style={style}
88+
nativeButtonAttributes={nativeButtonAttributes}
89+
nativeAnchorAttributes={nativeAnchorAttributes}
8690
__injectAnalyticsComponentMetadata={true}
8791
>
8892
{children}

src/button/interfaces.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import React from 'react';
55
import { IconProps } from '../icon/interfaces';
66
import { BaseComponentProps } from '../internal/base-component';
77
import { BaseNavigationDetail, CancelableEventHandler, ClickDetail as _ClickDetail } from '../internal/events';
8+
/**
9+
* @awsuiSystem core
10+
*/
11+
import { NativeAttributes } from '../internal/utils/with-native-attributes';
812

913
export interface BaseButtonProps {
1014
/**
@@ -100,6 +104,30 @@ export interface BaseButtonProps {
100104
* @i18n
101105
*/
102106
i18nStrings?: ButtonProps.I18nStrings;
107+
108+
/**
109+
* Attributes to add to the native `button` element.
110+
* Some attributes will be automatically combined with internal attribute values:
111+
* - `className` will be appended.
112+
* - Event handlers will be chained, unless the default is prevented.
113+
*
114+
* We do not support using this attribute to apply custom styling.
115+
*
116+
* @awsuiSystem core
117+
*/
118+
nativeButtonAttributes?: NativeAttributes<React.ButtonHTMLAttributes<HTMLButtonElement>>;
119+
120+
/**
121+
* Attributes to add to the native `a` element (when `href` is provided).
122+
* Some attributes will be automatically combined with internal attribute values:
123+
* - `className` will be appended.
124+
* - Event handlers will be chained, unless the default is prevented.
125+
*
126+
* We do not support using this attribute to apply custom styling.
127+
*
128+
* @awsuiSystem core
129+
*/
130+
nativeAnchorAttributes?: NativeAttributes<React.AnchorHTMLAttributes<HTMLAnchorElement>>;
103131
}
104132

105133
export interface ButtonProps extends BaseComponentProps, BaseButtonProps {

0 commit comments

Comments
 (0)