Skip to content
Draft
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 @@ -5884,6 +5884,13 @@ This property only applies when an \`href\` is provided.",
"optional": true,
"type": "string",
},
{
"description": "The text displayed in a tooltip on hover or focus.
If \`disabledReason\` is also provided, it takes precedence over \`tooltipText\` when disabled.",
"name": "tooltipText",
"optional": true,
"type": "string",
},
{
"defaultValue": "'normal'",
"description": "Determines the general styling of the button as follows:
Expand Down Expand Up @@ -33119,6 +33126,19 @@ The dismiss button is only rendered when the \`dismissible\` property is set to
],
},
},
{
"name": "findTooltip",
"parameters": [],
"returnType": {
"isNullable": true,
"name": "ElementWrapper",
"typeArguments": [
{
"name": "HTMLElement",
},
],
},
},
{
"name": "isDisabled",
"parameters": [],
Expand Down Expand Up @@ -36283,6 +36303,22 @@ To find a specific item use the \`findBreadcrumbLink(n)\` function as chaining \
],
},
},
{
"inheritedFrom": {
"name": "ButtonWrapper.findTooltip",
},
"name": "findTooltip",
"parameters": [],
"returnType": {
"isNullable": true,
"name": "ElementWrapper",
"typeArguments": [
{
"name": "HTMLElement",
},
],
},
},
{
"inheritedFrom": {
"name": "ButtonWrapper.isDisabled",
Expand Down Expand Up @@ -45233,6 +45269,14 @@ The dismiss button is only rendered when the \`dismissible\` property is set to
"name": "ElementWrapper",
},
},
{
"name": "findTooltip",
"parameters": [],
"returnType": {
"isNullable": false,
"name": "ElementWrapper",
},
},
],
"name": "ButtonWrapper",
},
Expand Down Expand Up @@ -47504,6 +47548,17 @@ To find a specific item use the \`findBreadcrumbLink(n)\` function as chaining \
"name": "ElementWrapper",
},
},
{
"inheritedFrom": {
"name": "ButtonWrapper.findTooltip",
},
"name": "findTooltip",
"parameters": [],
"returnType": {
"isNullable": false,
"name": "ElementWrapper",
},
},
],
"name": "ToggleButtonWrapper",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ exports[`test-utils selectors 1`] = `
"awsui_content_vjswe",
"awsui_disabled-reason-tooltip_1ueyk",
"awsui_icon-left_vjswe",
"awsui_tooltip_1ueyk",
],
"button-dropdown": [
"awsui_button-dropdown_sne0l",
Expand Down
123 changes: 123 additions & 0 deletions src/button/__tests__/button-tooltip-text.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { act, fireEvent, render } from '@testing-library/react';

import Button, { ButtonProps } from '../../../lib/components/button';
import createWrapper from '../../../lib/components/test-utils/dom';

function renderButton(props: Partial<ButtonProps> = {}) {
const { container } = render(<Button {...props}>Click me</Button>);
return createWrapper(container).findButton()!;
}

describe('Button tooltipText', () => {
test('does not show a tooltip by default', () => {
const wrapper = renderButton({ tooltipText: 'Helpful text' });
expect(wrapper.findTooltip()).toBeNull();
});

test('shows a tooltip on focus', () => {
const wrapper = renderButton({ tooltipText: 'Helpful text' });

wrapper.getElement().focus();

const tooltip = wrapper.findTooltip();
expect(tooltip).not.toBeNull();
expect(tooltip!.getElement()).toHaveTextContent('Helpful text');
});

test('hides the tooltip on blur', () => {
const wrapper = renderButton({ tooltipText: 'Helpful text' });

wrapper.getElement().focus();
expect(wrapper.findTooltip()).not.toBeNull();

wrapper.getElement().blur();
expect(wrapper.findTooltip()).toBeNull();
});

test('shows the tooltip on mouse enter and hides on mouse leave', () => {
const wrapper = renderButton({ tooltipText: 'Helpful text' });

fireEvent.mouseEnter(wrapper.getElement());
expect(wrapper.findTooltip()).not.toBeNull();

fireEvent.mouseLeave(wrapper.getElement());
expect(wrapper.findTooltip()).toBeNull();
});

test('hides the tooltip on Escape key press', () => {
const wrapper = renderButton({ tooltipText: 'Helpful text' });

wrapper.getElement().focus();
expect(wrapper.findTooltip()).not.toBeNull();

act(() => {
document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
});

expect(wrapper.findTooltip()).toBeNull();
});

test('does not render a tooltip when tooltipText is not provided', () => {
const wrapper = renderButton();

wrapper.getElement().focus();
fireEvent.mouseEnter(wrapper.getElement());

expect(wrapper.findTooltip()).toBeNull();
});

describe('precedence over disabledReason', () => {
test('shows disabledReason instead of tooltipText when disabled with reason', () => {
const wrapper = renderButton({
disabled: true,
disabledReason: 'Disabled because of reason',
tooltipText: 'Helpful text',
});

wrapper.getElement().focus();

// disabledReason wins: the disabled-reason tooltip is shown, the tooltipText tooltip is not.
expect(wrapper.findDisabledReason()).not.toBeNull();
expect(wrapper.findDisabledReason()!.getElement()).toHaveTextContent('Disabled because of reason');
expect(wrapper.findTooltip()).toBeNull();
});
});

describe('inactive states', () => {
test('does not show tooltip when button is in loading state', () => {
const wrapper = renderButton({ loading: true, tooltipText: 'Helpful text' });

wrapper.getElement().focus();
fireEvent.mouseEnter(wrapper.getElement());

expect(wrapper.findTooltip()).toBeNull();
});

test('does not show tooltip when button is disabled without reason', () => {
const wrapper = renderButton({ disabled: true, tooltipText: 'Helpful text' });

// Disabled buttons can't be focused, but we still verify hover does nothing.
fireEvent.mouseEnter(wrapper.getElement());

expect(wrapper.findTooltip()).toBeNull();
});
});

describe('with href', () => {
test('shows a tooltip on focus for a link button', () => {
const { container } = render(
<Button href="#test" tooltipText="Open settings">
Settings
</Button>
);
const wrapper = createWrapper(container).findButton()!;

wrapper.getElement().focus();
expect(wrapper.findTooltip()).not.toBeNull();
expect(wrapper.findTooltip()!.getElement()).toHaveTextContent('Open settings');
});
});
});
2 changes: 2 additions & 0 deletions src/button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const Button = React.forwardRef(
ariaExpanded,
ariaHaspopup,
ariaControls,
tooltipText,
fullWidth,
form,
i18nStrings,
Expand Down Expand Up @@ -83,6 +84,7 @@ const Button = React.forwardRef(
ariaExpanded={ariaExpanded}
ariaHaspopup={ariaHaspopup}
ariaControls={ariaControls}
tooltipText={tooltipText}
fullWidth={fullWidth}
form={form}
i18nStrings={i18nStrings}
Expand Down
6 changes: 6 additions & 0 deletions src/button/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,12 @@ export interface ButtonProps extends BaseComponentProps, BaseButtonProps {
*/
variant?: ButtonProps.Variant;

/**
* The text displayed in a tooltip on hover or focus.
* If `disabledReason` is also provided, it takes precedence over `tooltipText` when disabled.
*/
tooltipText?: string;

/**
* Specifies alternate text for a custom icon. We recommend that you provide this for accessibility.
* This property is ignored if you use a predefined icon or if you set your custom icon using the `iconSvg` slot.
Expand Down
27 changes: 27 additions & 0 deletions src/button/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const InternalButton = React.forwardRef(
ariaExpanded,
ariaHaspopup,
ariaControls,
tooltipText,
fullWidth,
badge,
i18nStrings,
Expand All @@ -113,6 +114,10 @@ export const InternalButton = React.forwardRef(
const isDisabledWithReason =
(variant === 'normal' || variant === 'primary' || variant === 'icon') && !!disabledReason && disabled;

// tooltipText is shown on hover/focus when the button is fully interactive. When
// the button is disabled-with-reason, disabledReason takes precedence.
const hasTooltipText = !!tooltipText && !isNotInteractive && !isDisabledWithReason;

const hasAriaDisabled = (loading && !disabled) || (disabled && __focusable) || isDisabledWithReason;
const shouldHaveContent =
children &&
Expand Down Expand Up @@ -293,6 +298,24 @@ export const InternalButton = React.forwardRef(
</>
);

const tooltipTextProps =
hasTooltipText && !(disabled && !!disabledReason)
? {
onFocus: () => setShowTooltip(true),
onBlur: () => setShowTooltip(false),
onMouseEnter: () => setShowTooltip(true),
onMouseLeave: () => setShowTooltip(false),
}
: {};
const tooltipTextContent = hasTooltipText && showTooltip && (
<Tooltip
className={testUtilStyles.tooltip}
getTrack={() => buttonRef.current}
content={tooltipText!}
onEscape={() => setShowTooltip(false)}
/>
);

const stylePropertiesAndVariables = getButtonStyles(style);

if (isAnchor) {
Expand All @@ -310,6 +333,7 @@ export const InternalButton = React.forwardRef(
<WithNativeAttributes<HTMLAnchorElement, React.AnchorHTMLAttributes<HTMLAnchorElement>>
{...buttonProps}
{...disabledReasonProps}
{...tooltipTextProps}
tag="a"
componentName="Button"
nativeAttributes={nativeAnchorAttributes}
Expand All @@ -326,6 +350,7 @@ export const InternalButton = React.forwardRef(
>
{buttonContent}
{isDisabledWithReason && disabledReasonContent}
{tooltipTextContent}
</WithNativeAttributes>
{loading && loadingText && (
<InternalLiveRegion tagName="span" hidden={true}>
Expand All @@ -341,6 +366,7 @@ export const InternalButton = React.forwardRef(
<WithNativeAttributes<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>
{...buttonProps}
{...disabledReasonProps}
{...tooltipTextProps}
tag="button"
componentName="Button"
nativeAttributes={nativeButtonAttributes}
Expand All @@ -352,6 +378,7 @@ export const InternalButton = React.forwardRef(
>
{buttonContent}
{isDisabledWithReason && disabledReasonContent}
{tooltipTextContent}
</WithNativeAttributes>
{loading && loadingText && (
<InternalLiveRegion tagName="span" hidden={true}>
Expand Down
4 changes: 4 additions & 0 deletions src/button/test-classes/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
/* used in test-utils or tests */
}

.tooltip {
/* used in test-utils or tests */
}

.external-icon {
/* used in test-utils or tests */
}
4 changes: 4 additions & 0 deletions src/test-utils/dom/button/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ export default class ButtonWrapper extends ComponentWrapper<HTMLButtonElement> {
findDisabledReason(): ElementWrapper | null {
return createWrapper().find(`.${buttonTestUtilsStyles['disabled-reason-tooltip']}`);
}

findTooltip(): ElementWrapper | null {
return createWrapper().findByClassName(buttonTestUtilsStyles.tooltip);
}
}
Loading