Skip to content

Commit ca35e1a

Browse files
gethinwebsterat-susie
authored andcommitted
feat: Add native attributes support for link, toggle, toggle button (#3838)
1 parent 3074fe1 commit ca35e1a

File tree

11 files changed

+150
-28
lines changed

11 files changed

+150
-28
lines changed

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

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15356,6 +15356,28 @@ use the \`id\` attribute, consider setting it on a parent element instead.",
1535615356
"optional": true,
1535715357
"type": "string",
1535815358
},
15359+
{
15360+
"description": "Attributes to add to the native element.
15361+
Some attributes will be automatically combined with internal attribute values:
15362+
- \`className\` will be appended.
15363+
- Event handlers will be chained, unless the default is prevented.
15364+
15365+
We do not support using this attribute to apply custom styling.",
15366+
"inlineType": {
15367+
"name": "Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "children"> & Record<\`data-\${string}\`, string>",
15368+
"type": "union",
15369+
"values": [
15370+
"Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "children">",
15371+
"Record<\`data-\${string}\`, string>",
15372+
],
15373+
},
15374+
"name": "nativeAttributes",
15375+
"optional": true,
15376+
"systemTags": [
15377+
"core",
15378+
],
15379+
"type": "Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "children"> & Record<\`data-\${string}\`, string>",
15380+
},
1535915381
{
1536015382
"description": "Adds a \`rel\` attribute to the link. If the \`rel\` property is provided, it overrides the default behaviour.
1536115383
By default, the component sets the \`rel\` attribute to "noopener noreferrer" when \`external\` is \`true\` or \`target\` is \`"_blank"\`.",
@@ -25424,6 +25446,28 @@ use the \`id\` attribute, consider setting it on a parent element instead.",
2542425446
"optional": true,
2542525447
"type": "string",
2542625448
},
25449+
{
25450+
"description": "Attributes to add to the native \`input\` element.
25451+
Some attributes will be automatically combined with internal attribute values:
25452+
- \`className\` will be appended.
25453+
- Event handlers will be chained, unless the default is prevented.
25454+
25455+
We do not support using this attribute to apply custom styling.",
25456+
"inlineType": {
25457+
"name": "Omit<React.InputHTMLAttributes<HTMLInputElement>, "children"> & Record<\`data-\${string}\`, string>",
25458+
"type": "union",
25459+
"values": [
25460+
"Omit<React.InputHTMLAttributes<HTMLInputElement>, "children">",
25461+
"Record<\`data-\${string}\`, string>",
25462+
],
25463+
},
25464+
"name": "nativeInputAttributes",
25465+
"optional": true,
25466+
"systemTags": [
25467+
"core",
25468+
],
25469+
"type": "Omit<React.InputHTMLAttributes<HTMLInputElement>, "children"> & Record<\`data-\${string}\`, string>",
25470+
},
2542725471
{
2542825472
"description": "Specifies if the control is read-only, which prevents the
2542925473
user from modifying the value. Should be used only inside forms.
@@ -25894,28 +25938,6 @@ It prevents users from clicking the button, but it can still be focused.",
2589425938
"optional": true,
2589525939
"type": "string",
2589625940
},
25897-
{
25898-
"description": "Attributes to add to the native \`a\` element (when \`href\` is provided).
25899-
Some attributes will be automatically combined with internal attribute values:
25900-
- \`className\` will be appended.
25901-
- Event handlers will be chained, unless the default is prevented.
25902-
25903-
We do not support using this attribute to apply custom styling.",
25904-
"inlineType": {
25905-
"name": "Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "children"> & Record<\`data-\${string}\`, string>",
25906-
"type": "union",
25907-
"values": [
25908-
"Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "children">",
25909-
"Record<\`data-\${string}\`, string>",
25910-
],
25911-
},
25912-
"name": "nativeAnchorAttributes",
25913-
"optional": true,
25914-
"systemTags": [
25915-
"core",
25916-
],
25917-
"type": "Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "children"> & Record<\`data-\${string}\`, string>",
25918-
},
2591925941
{
2592025942
"description": "Attributes to add to the native \`button\` element.
2592125943
Some attributes will be automatically combined with internal attribute values:

src/link/__tests__/index.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,3 +424,21 @@ describe('Style API', () => {
424424
expect(getComputedStyle(link).getPropertyValue(customCssProps.styleFocusRingBorderWidth)).toBe('4px');
425425
});
426426
});
427+
428+
describe('native attributes', () => {
429+
it('adds native attributes', () => {
430+
const { container } = render(<Link href="#" nativeAttributes={{ 'data-testid': 'my-test-id' }} />);
431+
expect(container.querySelectorAll('[data-testid="my-test-id"]')).toHaveLength(1);
432+
expect(container.querySelectorAll('a[data-testid="my-test-id"]')).toHaveLength(1);
433+
});
434+
it('adds native attributes (button link)', () => {
435+
const { container } = render(<Link nativeAttributes={{ 'data-testid': 'my-test-id' }} />);
436+
expect(container.querySelectorAll('[data-testid="my-test-id"]')).toHaveLength(1);
437+
expect(container.querySelectorAll('a[data-testid="my-test-id"]')).toHaveLength(1);
438+
});
439+
it('concatenates class names', () => {
440+
const { container } = render(<Link href="#" nativeAttributes={{ className: 'additional-class' }} />);
441+
expect(container.firstChild).toHaveClass(styles.link);
442+
expect(container.firstChild).toHaveClass('additional-class');
443+
});
444+
});

src/link/interfaces.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import {
99
ClickDetail as _ClickDetail,
1010
NonCancelableEventHandler,
1111
} from '../internal/events';
12+
/**
13+
* @awsuiSystem core
14+
*/
15+
import { NativeAttributes } from '../internal/utils/with-native-attributes';
1216

1317
export interface LinkProps extends BaseComponentProps {
1418
/**
@@ -111,6 +115,18 @@ export interface LinkProps extends BaseComponentProps {
111115
* @awsuiSystem core
112116
*/
113117
style?: LinkProps.Style;
118+
119+
/**
120+
* Attributes to add to the native element.
121+
* Some attributes will be automatically combined with internal attribute values:
122+
* - `className` will be appended.
123+
* - Event handlers will be chained, unless the default is prevented.
124+
*
125+
* We do not support using this attribute to apply custom styling.
126+
*
127+
* @awsuiSystem core
128+
*/
129+
nativeAttributes?: NativeAttributes<React.AnchorHTMLAttributes<HTMLAnchorElement>>;
114130
}
115131

116132
export namespace LinkProps {

src/link/internal.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { InternalBaseComponentProps } from '../internal/hooks/use-base-component
2525
import { useVisualRefresh } from '../internal/hooks/use-visual-mode';
2626
import { KeyCode } from '../internal/keycode';
2727
import { checkSafeUrl } from '../internal/utils/check-safe-url';
28+
import WithNativeAttributes from '../internal/utils/with-native-attributes';
2829
import { LinkProps } from './interfaces';
2930
import { getLinkStyles } from './style';
3031

@@ -50,6 +51,7 @@ const InternalLink = React.forwardRef(
5051
onFollow,
5152
onClick,
5253
children,
54+
nativeAttributes,
5355
__internalRootRef,
5456
style,
5557
...props
@@ -210,29 +212,35 @@ const InternalLink = React.forwardRef(
210212

211213
if (isButton) {
212214
return (
213-
<a
215+
<WithNativeAttributes
214216
{...sharedProps}
217+
tag="a"
218+
componentName="Link"
219+
nativeAttributes={nativeAttributes}
215220
role="button"
216221
tabIndex={tabIndex}
217222
onKeyDown={handleButtonKeyDown}
218223
onClick={handleButtonClick}
219224
>
220225
{content}
221-
</a>
226+
</WithNativeAttributes>
222227
);
223228
}
224229

225230
return (
226-
<a
231+
<WithNativeAttributes
227232
{...sharedProps}
233+
tag="a"
234+
componentName="Link"
235+
nativeAttributes={nativeAttributes}
228236
tabIndex={tabIndex}
229237
target={anchorTarget}
230238
rel={anchorRel}
231239
href={href}
232240
onClick={handleLinkClick}
233241
>
234242
{content}
235-
</a>
243+
</WithNativeAttributes>
236244
);
237245
}
238246
);

src/toggle-button/__tests__/toggle-button.test.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import createWrapper from '../../../lib/components/test-utils/dom';
99
import ToggleButton, { ToggleButtonProps } from '../../../lib/components/toggle-button';
1010
import { getToggleIcon } from '../../../lib/components/toggle-button/util';
1111

12+
import styles from '../../../lib/components/button/styles.css.js';
13+
1214
jest.mock('@cloudscape-design/component-toolkit/internal', () => ({
1315
...jest.requireActual('@cloudscape-design/component-toolkit/internal'),
1416
warnOnce: jest.fn(),
@@ -133,4 +135,21 @@ describe('ToggleButton Component', () => {
133135
expect(getToggleIcon(true, 'star')).toBe('star');
134136
});
135137
});
138+
139+
describe('native attributes', () => {
140+
it('adds native attributes', () => {
141+
const { container } = render(
142+
<ToggleButton pressed={true} nativeButtonAttributes={{ 'data-testid': 'my-test-id' }} />
143+
);
144+
expect(container.querySelectorAll('[data-testid="my-test-id"]')).toHaveLength(1);
145+
expect(container.querySelectorAll('button[data-testid="my-test-id"]')).toHaveLength(1);
146+
});
147+
it('concatenates class names', () => {
148+
const { container } = render(
149+
<ToggleButton pressed={true} nativeButtonAttributes={{ className: 'additional-class' }} />
150+
);
151+
expect(container.firstChild).toHaveClass(styles.button);
152+
expect(container.firstChild).toHaveClass('additional-class');
153+
});
154+
});
136155
});

src/toggle-button/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const ToggleButton = React.forwardRef(
3131
ariaDescribedby,
3232
ariaControls,
3333
pressed = false,
34+
nativeButtonAttributes,
3435
onChange,
3536
...props
3637
}: ToggleButtonProps,
@@ -65,6 +66,7 @@ const ToggleButton = React.forwardRef(
6566
pressedIconUrl={pressedIconUrl}
6667
pressedIconSvg={pressedIconSvg}
6768
pressed={pressed}
69+
nativeButtonAttributes={nativeButtonAttributes}
6870
onChange={onChange}
6971
>
7072
{children}

src/toggle-button/interfaces.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { IconProps } from '../icon/interfaces';
77
import { BaseComponentProps } from '../internal/base-component';
88
import { NonCancelableEventHandler } from '../internal/events';
99

10-
export interface ToggleButtonProps extends BaseComponentProps, BaseButtonProps {
10+
export interface ToggleButtonProps extends BaseComponentProps, Omit<BaseButtonProps, 'nativeAnchorAttributes'> {
1111
/** Determines the general styling of the toggle button as follows:
1212
* * `normal` for secondary buttons.
1313
* * `icon` to display an icon only (no text).

src/toggle-button/internal.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const InternalToggleButton = React.forwardRef(
2424
iconUrl: defaultIconUrl,
2525
pressedIconUrl,
2626
variant,
27+
nativeButtonAttributes,
2728
onChange,
2829
className,
2930
...rest
@@ -60,6 +61,7 @@ export const InternalToggleButton = React.forwardRef(
6061
}}
6162
{...rest}
6263
ref={ref}
64+
nativeButtonAttributes={nativeButtonAttributes}
6365
/>
6466
);
6567
}

src/toggle/__tests__/toggle.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,3 +232,17 @@ test('all style api properties', () => {
232232
expect(getComputedStyle(toggleHandle).getPropertyValue('background-color')).toBe('blue');
233233
expect(getComputedStyle(toggleLabel).getPropertyValue('color')).toBe('orange');
234234
});
235+
236+
describe('native attributes', () => {
237+
it('adds native attributes', () => {
238+
const { container } = render(<Toggle checked={true} nativeInputAttributes={{ 'data-testid': 'my-test-id' }} />);
239+
expect(container.querySelectorAll('[data-testid="my-test-id"]')).toHaveLength(1);
240+
expect(container.querySelectorAll('input[data-testid="my-test-id"]')).toHaveLength(1);
241+
});
242+
it('concatenates class names', () => {
243+
const { container } = render(<Toggle checked={true} nativeInputAttributes={{ className: 'additional-class' }} />);
244+
const input = container.querySelector('input');
245+
expect(input).toHaveClass(abstractSwitchStyles['native-input']);
246+
expect(input).toHaveClass('additional-class');
247+
});
248+
});

src/toggle/interfaces.ts

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

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

812
export interface ToggleProps extends BaseCheckboxProps {
913
/**
@@ -22,6 +26,18 @@ export interface ToggleProps extends BaseCheckboxProps {
2226
* @awsuiSystem core
2327
*/
2428
style?: ToggleProps.Style;
29+
30+
/**
31+
* Attributes to add to the native `input` element.
32+
* Some attributes will be automatically combined with internal attribute values:
33+
* - `className` will be appended.
34+
* - Event handlers will be chained, unless the default is prevented.
35+
*
36+
* We do not support using this attribute to apply custom styling.
37+
*
38+
* @awsuiSystem core
39+
*/
40+
nativeInputAttributes?: NativeAttributes<React.InputHTMLAttributes<HTMLInputElement>>;
2541
}
2642

2743
export namespace ToggleProps {

0 commit comments

Comments
 (0)