Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4215,6 +4215,25 @@ It prevents users from clicking the button, but it can still be focused.",
"optional": true,
"type": "string",
},
{
"description": "Attributes to add to the native \`button\` element.

It is not supported to use this attribute to apply custom styling.",
"inlineType": {
"name": "(React.HTMLAttributes<HTMLAnchorElement> & React.HTMLAttributes<HTMLButtonElement>) | Record<\`data-\${string}\`, string>",
"type": "union",
"values": [
"React.HTMLAttributes<HTMLAnchorElement> & React.HTMLAttributes<HTMLButtonElement>",
"Record<\`data-\${string}\`, string>",
],
},
"name": "nativeAttributes",
"optional": true,
"systemTags": [
"core",
],
"type": "(React.HTMLAttributes<HTMLAnchorElement> & React.HTMLAttributes<HTMLButtonElement>) | Record<\`data-\${string}\`, string>",
},
{
"description": "Adds a \`rel\` attribute to the link. By default, the component sets the \`rel\` attribute to "noopener noreferrer" when \`target\` is \`"_blank"\`.
If the \`rel\` property is provided, it overrides the default behavior.",
Expand Down Expand Up @@ -4635,6 +4654,44 @@ The main action also supports the following properties of the [button](/componen
"optional": true,
"type": "ButtonDropdownProps.MainAction",
},
{
"description": "Attributes to add to the native \`button\` element.

It is not supported to use this attribute to apply custom styling.",
"inlineType": {
"name": "(React.HTMLAttributes<HTMLAnchorElement> & React.HTMLAttributes<HTMLButtonElement>) | Record<\`data-\${string}\`, string>",
"type": "union",
"values": [
"React.HTMLAttributes<HTMLAnchorElement> & React.HTMLAttributes<HTMLButtonElement>",
"Record<\`data-\${string}\`, string>",
],
},
"name": "nativeButtonAttributes",
"optional": true,
"systemTags": [
"core",
],
"type": "(React.HTMLAttributes<HTMLAnchorElement> & React.HTMLAttributes<HTMLButtonElement>) | Record<\`data-\${string}\`, string>",
},
{
"description": "Attributes to add to the native \`button\` element of the \`mainAction\`.

It is not supported to use this attribute to apply custom styling.",
"inlineType": {
"name": "(React.HTMLAttributes<HTMLAnchorElement> & React.HTMLAttributes<HTMLButtonElement>) | Record<\`data-\${string}\`, string>",
"type": "union",
"values": [
"React.HTMLAttributes<HTMLAnchorElement> & React.HTMLAttributes<HTMLButtonElement>",
"Record<\`data-\${string}\`, string>",
],
},
"name": "nativeMainActionButtonAttributes",
"optional": true,
"systemTags": [
"core",
],
"type": "(React.HTMLAttributes<HTMLAnchorElement> & React.HTMLAttributes<HTMLButtonElement>) | Record<\`data-\${string}\`, string>",
},
{
"defaultValue": "'normal'",
"description": "Determines the general styling of the button dropdown.
Expand Down Expand Up @@ -19908,6 +19965,25 @@ It prevents users from clicking the button, but it can still be focused.",
"optional": true,
"type": "string",
},
{
"description": "Attributes to add to the native \`button\` element.

It is not supported to use this attribute to apply custom styling.",
"inlineType": {
"name": "(React.HTMLAttributes<HTMLAnchorElement> & React.HTMLAttributes<HTMLButtonElement>) | Record<\`data-\${string}\`, string>",
"type": "union",
"values": [
"React.HTMLAttributes<HTMLAnchorElement> & React.HTMLAttributes<HTMLButtonElement>",
"Record<\`data-\${string}\`, string>",
],
},
"name": "nativeAttributes",
"optional": true,
"systemTags": [
"core",
],
"type": "(React.HTMLAttributes<HTMLAnchorElement> & React.HTMLAttributes<HTMLButtonElement>) | Record<\`data-\${string}\`, string>",
},
{
"defaultValue": "false",
"description": "Sets the toggle button to pressed state.",
Expand Down
4 changes: 2 additions & 2 deletions src/app-layout/visual-refresh/mobile-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export default function MobileToolbar() {
className={testutilStyles['navigation-toggle']}
ref={navigationRefs.toggle}
disabled={hasDrawerViewportOverlay}
__nativeAttributes={{ 'aria-haspopup': navigationOpen ? undefined : true }}
nativeAttributes={{ 'aria-haspopup': navigationOpen ? undefined : true }}
/>
</nav>
)}
Expand Down Expand Up @@ -97,7 +97,7 @@ export default function MobileToolbar() {
onClick={() => handleToolsClick(true)}
variant="icon"
ref={toolsRefs.toggle}
__nativeAttributes={{ 'aria-haspopup': true }}
nativeAttributes={{ 'aria-haspopup': true }}
/>
</aside>
)
Expand Down
2 changes: 1 addition & 1 deletion src/app-layout/visual-refresh/trigger-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function TriggerButton(
badge={badge}
onClick={onClick}
variant="icon"
__nativeAttributes={{
nativeAttributes={{
'aria-haspopup': true,
...(testId && {
'data-testid': testId,
Expand Down
2 changes: 1 addition & 1 deletion src/attribute-editor/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ const InternalAttributeEditor = React.forwardRef(
// Using aria-disabled="true" and tabindex="-1" instead of "disabled"
// because focus can be dynamically moved to this button by calling
// `focusAddButton()` on the ref.
__nativeAttributes={disableAddButton ? { tabIndex: -1 } : {}}
nativeAttributes={disableAddButton ? { tabIndex: -1 } : {}}
__focusable={true}
onClick={onAddButtonClick}
formAction="none"
Expand Down
82 changes: 82 additions & 0 deletions src/button-dropdown/__tests__/native-attributes.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { render } from '@testing-library/react';

import ButtonDropdown from '../../../lib/components/button-dropdown';
import createWrapper from '../../../lib/components/test-utils/dom';

describe('ButtonDropdown native attributes', () => {
const items = [{ id: 'item1', text: 'Item 1' }];

test('passes nativeTriggerAttributes to the dropdown trigger button', () => {
const { container } = render(
<ButtonDropdown
items={items}
nativeTriggerAttributes={{
'data-testid': 'dropdown-trigger',
'aria-controls': 'controlled-element',
}}
/>
);
const wrapper = createWrapper(container).findButtonDropdown()!;
const triggerButton = wrapper.findNativeButton().getElement();

expect(triggerButton).toHaveAttribute('data-testid', 'dropdown-trigger');
expect(triggerButton).toHaveAttribute('aria-controls', 'controlled-element');
});

test('passes nativeMainActionButtonAttributes to the main action button', () => {
const { container } = render(
<ButtonDropdown
items={items}
variant="primary"
mainAction={{ text: 'Main action' }}
nativeMainActionButtonAttributes={{
'data-testid': 'main-action',
'aria-controls': 'another-element',
}}
/>
);
const wrapper = createWrapper(container).findButtonDropdown()!;
const mainActionButton = wrapper.findMainAction()!.getElement();

expect(mainActionButton).toHaveAttribute('data-testid', 'main-action');
expect(mainActionButton).toHaveAttribute('aria-controls', 'another-element');
});

test('does not apply nativeMainActionButtonAttributes when no main action is present', () => {
const { container } = render(
<ButtonDropdown
items={items}
nativeMainActionButtonAttributes={{
'data-testid': 'main-action',
}}
/>
);
const wrapper = createWrapper(container).findButtonDropdown()!;

expect(wrapper.findMainAction()).toBeNull();
expect(container.querySelector('[data-testid="main-action"]')).toBeNull();
});

test('applies both nativeTriggerAttributes and nativeMainActionButtonAttributes to respective buttons', () => {
const { container } = render(
<ButtonDropdown
items={items}
variant="primary"
mainAction={{ text: 'Main action' }}
nativeTriggerAttributes={{
'data-testid': 'dropdown-trigger',
}}
nativeMainActionButtonAttributes={{
'data-testid': 'main-action',
}}
/>
);
const wrapper = createWrapper(container).findButtonDropdown()!;

expect(wrapper.findNativeButton().getElement()).toHaveAttribute('data-testid', 'dropdown-trigger');
expect(wrapper.findMainAction()!.getElement()).toHaveAttribute('data-testid', 'main-action');
});
});
4 changes: 4 additions & 0 deletions src/button-dropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const ButtonDropdown = React.forwardRef(
onItemFollow,
mainAction,
fullWidth,
nativeTriggerAttributes,
nativeMainActionButtonAttributes,
...props
}: ButtonDropdownProps,
ref: React.Ref<ButtonDropdownProps.Ref>
Expand Down Expand Up @@ -72,6 +74,8 @@ const ButtonDropdown = React.forwardRef(
onItemFollow={onItemFollow}
mainAction={mainAction}
fullWidth={fullWidth}
nativeTriggerAttributes={nativeTriggerAttributes}
nativeMainActionButtonAttributes={nativeMainActionButtonAttributes}
{...getAnalyticsMetadataAttribute({
component: analyticsComponentMetadata,
})}
Expand Down
18 changes: 18 additions & 0 deletions src/button-dropdown/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,24 @@ export interface ButtonDropdownProps extends BaseComponentProps, ExpandToViewpor
* Sets the button width to be 100% of the parent container width. Button content is centered.
*/
fullWidth?: boolean;

/**
* Attributes to add to the native `button` element.
*
* It is not supported to use this attribute to apply custom styling.
*
* @awsuiSystem core
*/
nativeTriggerAttributes?: ButtonProps['nativeAttributes'];

/**
* Attributes to add to the native `button` element of the `mainAction`.
*
* It is not supported to use this attribute to apply custom styling.
*
* @awsuiSystem core
*/
nativeMainActionButtonAttributes?: ButtonProps['nativeAttributes'];
}

export namespace ButtonDropdownProps {
Expand Down
7 changes: 6 additions & 1 deletion src/button-dropdown/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ const InternalButtonDropdown = React.forwardRef(
analyticsMetadataTransformer,
linkStyle,
fullWidth,
nativeTriggerAttributes,
nativeMainActionButtonAttributes,
...props
}: InternalButtonDropdownProps,
ref: React.Ref<ButtonDropdownProps.Ref>
Expand Down Expand Up @@ -168,7 +170,7 @@ const InternalButtonDropdown = React.forwardRef(
ariaLabel,
ariaExpanded: canBeOpened && isOpen,
formAction: 'none',
__nativeAttributes: {
nativeAttributes: {
'aria-haspopup': true,
},
};
Expand Down Expand Up @@ -237,6 +239,7 @@ const InternalButtonDropdown = React.forwardRef(
ref={mainActionRef}
{...mainActionProps}
{...mainActionIconProps}
nativeAttributes={nativeMainActionButtonAttributes}
fullWidth={canBeFullWidth}
className={clsx(
styles['trigger-button'],
Expand Down Expand Up @@ -290,6 +293,7 @@ const InternalButtonDropdown = React.forwardRef(
<InternalButton
ref={triggerRef}
{...baseTriggerProps}
nativeAttributes={{ ...baseTriggerProps.nativeAttributes, ...nativeTriggerAttributes }}
className={clsx(baseTriggerProps.className, {
[styles['main-action-trigger-full-width']]: canBeFullWidth,
})}
Expand All @@ -308,6 +312,7 @@ const InternalButtonDropdown = React.forwardRef(
ref={triggerRef}
id={triggerId}
{...baseTriggerProps}
nativeAttributes={{ ...baseTriggerProps.nativeAttributes, ...nativeTriggerAttributes }}
className={clsx(baseTriggerProps.className, {
[styles['full-width']]: canBeFullWidth,
[styles.loading]: canBeFullWidth && !!loading,
Expand Down
4 changes: 2 additions & 2 deletions src/button/__tests__/button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -813,8 +813,8 @@ describe('table grid navigation support', () => {
test('does not override explicit tab index with 0', () => {
const { setCurrentTarget } = renderWithSingleTabStopNavigation(
<div>
<InternalButton id="button1" __nativeAttributes={{ tabIndex: -2 }} />
<InternalButton id="button2" __nativeAttributes={{ tabIndex: -2 }} />
<InternalButton id="button1" nativeAttributes={{ tabIndex: -2 }} />
<InternalButton id="button2" nativeAttributes={{ tabIndex: -2 }} />
</div>
);
setCurrentTarget(getButton('#button1'));
Expand Down
14 changes: 1 addition & 13 deletions src/button/__tests__/internal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,9 @@ import { mockedFunnelInteractionId, mockFunnelMetrics } from '../../internal/ana

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

test('specific properties take precedence over nativeAttributes', () => {
const { container } = render(
<InternalButton ariaLabel="property" __nativeAttributes={{ 'aria-label': 'native attribute' }} />
);
expect(container.querySelector('button')).toHaveAttribute('aria-label', 'property');
});

test('supports providing custom attributes', () => {
const { container } = render(<InternalButton __nativeAttributes={{ 'aria-hidden': 'true' }} />);
expect(container.querySelector('button')).toHaveAttribute('aria-hidden', 'true');
});

test('supports __iconClass property', () => {
const { container } = render(
<InternalButton __iconClass="example-class" iconName="settings" __nativeAttributes={{ 'aria-expanded': 'true' }} />
<InternalButton __iconClass="example-class" iconName="settings" nativeAttributes={{ 'aria-expanded': 'true' }} />
);
expect(container.querySelector(`button .${styles.icon}`)).toHaveClass('example-class');
});
Expand Down
61 changes: 61 additions & 0 deletions src/button/__tests__/native-attributes.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { render } from '@testing-library/react';

import Button from '../../../lib/components/button';
import createWrapper from '../../../lib/components/test-utils/dom';

describe('Button native attributes', () => {
test('passes nativeAttributes to the button element', () => {
const { container } = render(
<Button
nativeAttributes={{
'data-testid': 'test-button',
'aria-controls': 'controlled-element',
}}
>
Button text
</Button>
);
const wrapper = createWrapper(container).findButton()!;

expect(wrapper.getElement()).toHaveAttribute('data-testid', 'test-button');
expect(wrapper.getElement()).toHaveAttribute('aria-controls', 'controlled-element');
});

test('passes nativeAttributes to the anchor element when href is provided', () => {
const { container } = render(
<Button
href="https://example.com"
nativeAttributes={{
'data-testid': 'test-link',
'aria-controls': 'controlled-element',
}}
>
Link text
</Button>
);
const wrapper = createWrapper(container).findButton()!;

expect(wrapper.getElement()).toHaveAttribute('data-testid', 'test-link');
expect(wrapper.getElement()).toHaveAttribute('aria-controls', 'controlled-element');
expect(wrapper.getElement().tagName).toBe('A');
});

test('overrides built-in attributes with nativeAttributes', () => {
const { container } = render(
<Button
ariaLabel="Button label"
nativeAttributes={{
'aria-label': 'Override label',
}}
>
Button text
</Button>
);
const wrapper = createWrapper(container).findButton()!;

expect(wrapper.getElement()).toHaveAccessibleName('Override label');
});
});
Loading
Loading