WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
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
2,632 changes: 2,632 additions & 0 deletions pages/tooltip/components-usage.page.tsx

Large diffs are not rendered by default.

931 changes: 931 additions & 0 deletions pages/tooltip/hook-comparison.page.tsx

Large diffs are not rendered by default.

652 changes: 652 additions & 0 deletions pages/tooltip/tooltip-version-2.page.tsx

Large diffs are not rendered by default.

661 changes: 661 additions & 0 deletions pages/tooltip/wrapper-component.page.tsx

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion src/internal/do-not-use/tooltip.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import InternalTooltip, { TooltipProps } from '../components/tooltip/index.js';
import { TooltipProps } from '../../tooltip/interfaces.js';
import InternalTooltip from '../../tooltip/internal.js';

export type InternalTooltipProps = TooltipProps;
export default InternalTooltip;
export { InternalTooltip };
38 changes: 38 additions & 0 deletions src/tooltip/__integ__/tooltip.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects';
import useBrowser from '@cloudscape-design/browser-test-tools/use-browser';

import createWrapper from '../../../../../lib/components/test-utils/selectors';

import tooltipStyles from '../../../../../lib/components/internal/components/tooltip/styles.selectors.js';

test(
'should not close any wrapping modals when the tooltip detects an Escape keypress',
useBrowser(async browser => {
await browser.url('/#/light/modal/with-tooltip');
const page = new BasePageObject(browser);

const openButtonSelector = createWrapper().findButton().toSelector();
await page.waitForVisible(openButtonSelector);
await page.click(openButtonSelector);

const modal = createWrapper().findModal();
const slider = modal.findContent().findSlider();
await page.waitForVisible(slider.toSelector());

// Slider on the page is set at 50% on purpose. `hoverElement` will move the
// mouse to the center of the track where the "thumb" is.
await page.hoverElement(slider.findNativeInput().toSelector());
await page.waitForVisible(`.${tooltipStyles.root}`);

// Press once to close the tooltip
await page.keys(['Escape']);
await expect(page.isDisplayed(`.${tooltipStyles.root}`)).resolves.toBe(false);
await expect(page.isDisplayed(modal.toSelector())).resolves.toBe(true);

// Press again to close the modal
await page.keys(['Escape']);
await expect(page.isDisplayed(modal.toSelector())).resolves.toBe(false);
})
);
57 changes: 57 additions & 0 deletions src/tooltip/__tests__/simple-tooltip.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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 createWrapper from '../../../lib/components/test-utils/dom';
import { SimpleTooltip } from '../simple-tooltip';

describe('SimpleTooltip', () => {
it('renders children correctly', () => {
const { container } = render(
<SimpleTooltip content="Tooltip text">
<button>Hover me</button>
</SimpleTooltip>
);

const wrapper = createWrapper(container);
expect(wrapper.findButton()!.getElement()).toHaveTextContent('Hover me');
});

it('renders tooltip content when hovered', () => {
const { container } = render(
<SimpleTooltip content="Tooltip text">
<button>Hover me</button>
</SimpleTooltip>
);

const wrapper = createWrapper(container);
expect(wrapper.getElement()).toHaveTextContent('Tooltip text');
});

it('handles React node content', () => {
const { container } = render(
<SimpleTooltip content={<strong>Bold tooltip</strong>}>
<button>Hover me</button>
</SimpleTooltip>
);

const wrapper = createWrapper(container);
expect(wrapper.getElement()).toHaveTextContent('Bold tooltip');
});

it('supports all positions', () => {
const positions: Array<'top' | 'right' | 'bottom' | 'left'> = ['top', 'right', 'bottom', 'left'];

positions.forEach(position => {
const { container } = render(
<SimpleTooltip content="Tooltip text" position={position}>
<button>Hover me</button>
</SimpleTooltip>
);

const wrapper = createWrapper(container);
expect(wrapper.getElement()).toHaveTextContent('Tooltip text');
});
});
});
103 changes: 103 additions & 0 deletions src/tooltip/__tests__/tooltip.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { act, render } from '@testing-library/react';

import InternalTooltip from '../../internal/components/tooltip';
import { TooltipProps } from '../../internal/components/tooltip';
import StatusIndicator from '../../status-indicator/internal';
import createWrapper, { ElementWrapper, PopoverWrapper } from '../../test-utils/dom';

import tooltipStyles from '../../internal/components/tooltip/styles.css.js';
import styles from '../../popover/styles.css.js';

class TooltipInternalWrapper extends PopoverWrapper {
findTooltip(): ElementWrapper | null {
return createWrapper().findByClassName(tooltipStyles.root);
}
findContent(): ElementWrapper | null {
return createWrapper().findByClassName(styles.content);
}
findArrow(): ElementWrapper | null {
return createWrapper().findByClassName(styles.arrow);
}
findHeader(): ElementWrapper | null {
return createWrapper().findByClassName(styles.header);
}
}

const dummyRef = { current: null };
function renderTooltip(props: Partial<TooltipProps>) {
const { container } = render(
<InternalTooltip
trackRef={dummyRef}
trackKey={props.trackKey}
value={props.value ?? ''}
contentAttributes={props.contentAttributes}
onDismiss={props.onDismiss}
/>
);
return new TooltipInternalWrapper(container);
}

describe('Tooltip', () => {
it('renders text correctly', () => {
const wrapper = renderTooltip({ value: 'Value' });

expect(wrapper.findContent()!.getElement()).toHaveTextContent('Value');
});

it('renders node correctly', () => {
const wrapper = renderTooltip({ value: <StatusIndicator type="success">Success</StatusIndicator> });
const statusIndicatorWrapper = createWrapper(wrapper.findContent()!.getElement()).findStatusIndicator()!;

expect(statusIndicatorWrapper.getElement()).toHaveTextContent('Success');
});

it('renders arrow', () => {
const wrapper = renderTooltip({ value: 'Value' });

expect(wrapper.findArrow()).not.toBeNull();
});

it('does not render a header', () => {
const wrapper = renderTooltip({ value: 'Value' });

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

it('contentAttributes work as expected', () => {
const wrapper = renderTooltip({ value: 'Value', contentAttributes: { title: 'test' } });

expect(wrapper.findTooltip()?.getElement()).toHaveAttribute('title', 'test');
});

it('trackKey is set correctly for strings', () => {
const wrapper = renderTooltip({ value: 'Value' });

expect(wrapper.findTooltip()?.getElement()).toHaveAttribute('data-testid', 'Value');
});

it('trackKey is set correctly for explicit value', () => {
const trackKey = 'test-track-key';
const wrapper = renderTooltip({ value: 'Value', trackKey });

expect(wrapper.findTooltip()?.getElement()).toHaveAttribute('data-testid', trackKey);
});

it('calls onDismiss when an Escape keypress is detected anywhere', () => {
const onDismiss = jest.fn();
const keydownEvent = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true });
jest.spyOn(keydownEvent, 'stopPropagation');

renderTooltip({ value: 'Value', onDismiss });
expect(onDismiss).not.toHaveBeenCalled();

act(() => {
// Dispatch the exect event instance so that we can spy stopPropagation on it.
document.body.dispatchEvent(keydownEvent);
});
expect(keydownEvent.stopPropagation).toHaveBeenCalled();
expect(onDismiss).toHaveBeenCalled();
});
});
55 changes: 55 additions & 0 deletions src/tooltip/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
'use client';
import React from 'react';

import { getBaseProps } from '../internal/base-component';
import useBaseComponent from '../internal/hooks/use-base-component';
import { applyDisplayName } from '../internal/utils/apply-display-name';
import { TooltipProps } from './interfaces';
import InternalTooltip from './internal';

export { TooltipProps };

const Tooltip = React.forwardRef(
(
{
value,
trackRef,
trackKey,
position,
className,
contentAttributes,
size,
hideOnOverscroll,
onDismiss,
...props
}: TooltipProps,
ref: React.Ref<TooltipProps.Ref>
) => {
const baseComponentProps = useBaseComponent('Tooltip', {
props: { position, size },
});
const baseProps = getBaseProps(props);

return (
<InternalTooltip
{...baseProps}
{...baseComponentProps}
ref={ref}
value={value}
trackRef={trackRef}
trackKey={trackKey}
position={position}
className={className}
contentAttributes={contentAttributes}
size={size}
hideOnOverscroll={hideOnOverscroll}
onDismiss={onDismiss}
/>
);
}
);

applyDisplayName(Tooltip, 'Tooltip');
export default Tooltip;
63 changes: 63 additions & 0 deletions src/tooltip/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import { NonCancelableEventHandler } from '../internal/events';
import { PopoverProps } from '../popover/interfaces';

export interface TooltipProps {
/**
* The content to display in the tooltip.
*/
value: React.ReactNode;

/**
* Reference to the element the tooltip is positioned against.
*/
trackRef: React.RefObject<HTMLElement | SVGElement>;

/**
* A unique key to identify the tooltip. If not provided and value is a string or number,
* the value will be used as the key.
*/
trackKey?: string | number;

/**
* The position of the tooltip relative to the tracked element.
* @default 'top'
*/
position?: 'top' | 'right' | 'bottom' | 'left';

/**
* Additional CSS class name to apply to the tooltip container.
*/
className?: string;

/**
* Additional HTML attributes to apply to the tooltip content container.
*/
contentAttributes?: React.HTMLAttributes<HTMLDivElement>;

/**
* The size of the tooltip.
* @default 'small'
*/
size?: PopoverProps['size'];

/**
* If true, the tooltip will be hidden when the page is scrolled.
*/
hideOnOverscroll?: boolean;

/**
* Callback function called when the tooltip should be dismissed.
* @internal
*/
onDismiss?: NonCancelableEventHandler;
}

export namespace TooltipProps {
export interface Ref {
focus(): void;
}
}
Loading
Loading