From 9d52ad0fa481f84c17e43e5229cd9d3413080d8c Mon Sep 17 00:00:00 2001
From: Xin00163
Date: Wed, 11 May 2022 09:27:23 +0100
Subject: [PATCH 01/10] fix(PPDSC-2117): wip
---
package.json | 2 ++
src/tooltip/defaults.ts | 19 ++++++++++++++++++
src/tooltip/index.ts | 2 ++
src/tooltip/style-presets.ts | 12 ++++++++++++
src/tooltip/styled.tsx | 4 ++++
src/tooltip/tooltip.tsx | 29 +++++++++++++++++++++++++++
src/tooltip/types.ts | 38 ++++++++++++++++++++++++++++++++++++
src/tooltip/use-tooltip.ts | 30 ++++++++++++++++++++++++++++
yarn.lock | 13 ++++++++++++
9 files changed, 149 insertions(+)
create mode 100644 src/tooltip/defaults.ts
create mode 100644 src/tooltip/index.ts
create mode 100644 src/tooltip/style-presets.ts
create mode 100644 src/tooltip/styled.tsx
create mode 100644 src/tooltip/tooltip.tsx
create mode 100644 src/tooltip/types.ts
create mode 100644 src/tooltip/use-tooltip.ts
diff --git a/package.json b/package.json
index a06ddfe990..c49cf61237 100644
--- a/package.json
+++ b/package.json
@@ -195,6 +195,7 @@
"@emotion-icons/material-outlined": "3.8.0",
"@emotion/react": "^11.1.5",
"@emotion/styled": "^11.1.5",
+ "@popperjs/core": "^2.11.5",
"@seznam/compose-react-refs": "^1.0.5",
"aria-hidden": "^1.1.3",
"date-fns": "^2.6.0",
@@ -209,6 +210,7 @@
"react-focus-lock": "^2.5.0",
"react-hook-form": "^7.5.3",
"react-hot-toast": "^1.0.2",
+ "react-popper": "^2.3.0",
"react-range": "^1.8.12",
"react-transition-group": "^4.4.1",
"react-virtual": "^2.10.4"
diff --git a/src/tooltip/defaults.ts b/src/tooltip/defaults.ts
new file mode 100644
index 0000000000..49ce82ae75
--- /dev/null
+++ b/src/tooltip/defaults.ts
@@ -0,0 +1,19 @@
+export default {
+ legend: {
+ small: {
+ stylePreset: 'legend',
+ typographyPreset: 'utilityLabel010',
+ spaceStack: 'space030',
+ },
+ medium: {
+ stylePreset: 'legend',
+ typographyPreset: 'utilityLabel020',
+ spaceStack: 'space030',
+ },
+ large: {
+ stylePreset: 'legend',
+ typographyPreset: 'utilityLabel030',
+ spaceStack: 'space030',
+ },
+ },
+};
diff --git a/src/tooltip/index.ts b/src/tooltip/index.ts
new file mode 100644
index 0000000000..0ef19c2f76
--- /dev/null
+++ b/src/tooltip/index.ts
@@ -0,0 +1,2 @@
+export * from './tooltip';
+export * from './types';
diff --git a/src/tooltip/style-presets.ts b/src/tooltip/style-presets.ts
new file mode 100644
index 0000000000..c2249cf92d
--- /dev/null
+++ b/src/tooltip/style-presets.ts
@@ -0,0 +1,12 @@
+import {StylePreset} from '../theme/types';
+
+export default {
+ legend: {
+ base: {
+ color: '{{colors.inkContrast}}',
+ },
+ disabled: {
+ color: '{{colors.inkNonEssential}}',
+ },
+ },
+} as Record;
diff --git a/src/tooltip/styled.tsx b/src/tooltip/styled.tsx
new file mode 100644
index 0000000000..298e3ab405
--- /dev/null
+++ b/src/tooltip/styled.tsx
@@ -0,0 +1,4 @@
+import {styled} from '../utils';
+import {TooltipProps} from './types';
+
+export const StyledTooltip = styled.div``;
diff --git a/src/tooltip/tooltip.tsx b/src/tooltip/tooltip.tsx
new file mode 100644
index 0000000000..d2bb86c4f5
--- /dev/null
+++ b/src/tooltip/tooltip.tsx
@@ -0,0 +1,29 @@
+import * as React from 'react';
+import {TooltipProps} from './types';
+import defaults from './defaults';
+import stylePresets from './style-presets';
+import {withOwnTheme} from '../utils/with-own-theme';
+import {StyledTooltip} from './styled';
+import {useTooltio} from './use-tooltip';
+
+const ThemelessTooltip = React.forwardRef(
+ ({children, title, trigger, open, placement, overrides, ...props}, ref) => {
+ if (!title) {
+ return <>{children}>;
+ }
+
+ return (
+ <>
+ {React.cloneElement(children, childrenProps)}
+
+ {title}
+
+ >
+ );
+ },
+);
+
+export const Tooltip = withOwnTheme(ThemelessTooltip)({
+ defaults,
+ stylePresets,
+});
diff --git a/src/tooltip/types.ts b/src/tooltip/types.ts
new file mode 100644
index 0000000000..f4e784f093
--- /dev/null
+++ b/src/tooltip/types.ts
@@ -0,0 +1,38 @@
+import React from 'react';
+import {MQ} from '../utils/style';
+
+export type TooltipPlacement =
+ | 'top'
+ | 'top-start'
+ | 'top-end'
+ | 'right'
+ | 'right-start'
+ | 'right-end'
+ | 'bottom'
+ | 'bottom-start'
+ | 'bottom-end'
+ | 'left'
+ | 'left-start'
+ | 'left-end';
+
+export interface TooltipProps
+ extends Omit, 'title'> {
+ children?: React.ReactNode;
+ title?: React.ReactNode;
+ trigger: ('click' | 'hover' | 'focus')[];
+ placement?: TooltipPlacement;
+ open?: boolean;
+ onOpen?: (event: React.SyntheticEvent) => void;
+ onDismiss?: (event: React.SyntheticEvent) => void;
+
+ overrides?: {
+ panel?: {
+ maxWidth?: MQ;
+ minWidth?: MQ;
+ space?: MQ;
+ stylePreset?: MQ;
+ typographyPreset?: MQ;
+ };
+ zIndex?: number;
+ };
+}
diff --git a/src/tooltip/use-tooltip.ts b/src/tooltip/use-tooltip.ts
new file mode 100644
index 0000000000..78257f0fa3
--- /dev/null
+++ b/src/tooltip/use-tooltip.ts
@@ -0,0 +1,30 @@
+import React, {useState, useEffect} from 'react';
+import {usePopper} from 'react-popper';
+import {get} from '../utils/get';
+
+export interface UseTooltipProps {
+ closeOnClick?: boolean;
+ /**
+ * If `true`, the tooltip will hide while the mouse
+ * is down
+ */
+ closeOnMouseDown?: boolean;
+ /**
+ * If `true`, the tooltip will hide on pressing Esc key
+ */
+ onOpen?(): void;
+ /**
+ * Callback to run when the tooltip hides
+ */
+ onClose?(): void;
+ /**
+ * Custom `id` to use in place of `uuid`
+ */
+ id?: string;
+ /**
+ * If `true`, the tooltip will be shown (in controlled mode)
+ */
+ open?: boolean;
+}
+
+export function useTooltip(props: UseTooltipProps = {}) {}
diff --git a/yarn.lock b/yarn.lock
index fde6936db7..93b67146db 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2431,6 +2431,11 @@
schema-utils "^2.6.5"
source-map "^0.7.3"
+"@popperjs/core@^2.11.5":
+ version "2.11.5"
+ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64"
+ integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==
+
"@popperjs/core@^2.5.4", "@popperjs/core@^2.6.0":
version "2.10.2"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590"
@@ -15096,6 +15101,14 @@ react-popper@^2.2.4:
react-fast-compare "^3.0.1"
warning "^4.0.2"
+react-popper@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.3.0.tgz#17891c620e1320dce318bad9fede46a5f71c70ba"
+ integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==
+ dependencies:
+ react-fast-compare "^3.0.1"
+ warning "^4.0.2"
+
react-range@^1.8.12:
version "1.8.12"
resolved "https://registry.yarnpkg.com/react-range/-/react-range-1.8.12.tgz#61fe79421a519a4d77c76838012d895b75ead42f"
From f1c804bcfc3a056624161a7a8abdf804ab25eea7 Mon Sep 17 00:00:00 2001
From: Xin00163
Date: Wed, 18 May 2022 15:16:10 +0100
Subject: [PATCH 02/10] feat(PPDSC-2117): add tooltip
---
package.json | 3 +-
.../__snapshots__/theme.test.ts.snap | 16 +
.../__snapshots__/tooltip.test.tsx.snap | 89 ++++
src/tooltip/__tests__/tooltip.stories.tsx | 466 ++++++++++++++++++
src/tooltip/__tests__/tooltip.test.tsx | 242 +++++++++
src/tooltip/defaults.ts | 20 +-
src/tooltip/style-presets.ts | 9 +-
src/tooltip/styled.tsx | 23 +-
src/tooltip/tooltip.tsx | 111 ++++-
src/tooltip/types.ts | 40 +-
src/tooltip/use-tooltip.ts | 30 --
yarn.lock | 47 +-
12 files changed, 985 insertions(+), 111 deletions(-)
create mode 100644 src/tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap
create mode 100644 src/tooltip/__tests__/tooltip.stories.tsx
create mode 100644 src/tooltip/__tests__/tooltip.test.tsx
delete mode 100644 src/tooltip/use-tooltip.ts
diff --git a/package.json b/package.json
index c49cf61237..3ac0b3c0eb 100644
--- a/package.json
+++ b/package.json
@@ -195,7 +195,7 @@
"@emotion-icons/material-outlined": "3.8.0",
"@emotion/react": "^11.1.5",
"@emotion/styled": "^11.1.5",
- "@popperjs/core": "^2.11.5",
+ "@floating-ui/react-dom-interactions": "^0.6.0",
"@seznam/compose-react-refs": "^1.0.5",
"aria-hidden": "^1.1.3",
"date-fns": "^2.6.0",
@@ -210,7 +210,6 @@
"react-focus-lock": "^2.5.0",
"react-hook-form": "^7.5.3",
"react-hot-toast": "^1.0.2",
- "react-popper": "^2.3.0",
"react-range": "^1.8.12",
"react-transition-group": "^4.4.1",
"react-virtual": "^2.10.4"
diff --git a/src/theme/__tests__/__snapshots__/theme.test.ts.snap b/src/theme/__tests__/__snapshots__/theme.test.ts.snap
index 5920ed5af8..47a4ecea6e 100644
--- a/src/theme/__tests__/__snapshots__/theme.test.ts.snap
+++ b/src/theme/__tests__/__snapshots__/theme.test.ts.snap
@@ -2472,6 +2472,15 @@ Object {
"spaceInset": "spaceInset030",
"stylePreset": "toastNeutral",
},
+ "tooltip": Object {
+ "panel": Object {
+ "paddingBlock": "spaceInset020",
+ "paddingInline": "spaceInset020",
+ "stylePreset": "tooltipPanel",
+ "typographyPreset": "utilityLabel010",
+ },
+ "zIndex": 80,
+ },
"unorderedList": Object {
"content": Object {
"stylePreset": "inkBase",
@@ -4622,6 +4631,13 @@ Object {
"iconColor": "{{colors.inkInverse}}",
},
},
+ "tooltipPanel": Object {
+ "base": Object {
+ "backgroundColor": "{{colors.interface060}}",
+ "borderRadius": "{{borders.borderRadiusDefault}}",
+ "color": "{{colors.inkInverse}}",
+ },
+ },
"videoPlayerControlBar": Object {
"base": Object {
"backgroundColor": "{{colors.blackTint050}}",
diff --git a/src/tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap b/src/tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap
new file mode 100644
index 0000000000..bdb3dfb0a5
--- /dev/null
+++ b/src/tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap
@@ -0,0 +1,89 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Tooltip should render correct styles: default 1`] = `
+
+
+ .emotion-0 {
+ pointer-events: none;
+ z-index: 80;
+}
+
+.emotion-1 {
+ background-color: #0A0A0A;
+ color: #FFFFFF;
+ border-radius: 8px;
+ font-family: "Poppins",sans-serif;
+ font-size: 12px;
+ line-height: 1.5;
+ font-weight: 500;
+ letter-spacing: 0;
+ padding-inline: 8px;
+ padding-block: 8px;
+}
+
+
+
+`;
+
+exports[`Tooltip should render correct styles: with overrides 1`] = `
+
+
+ .emotion-0 {
+ pointer-events: none;
+ z-index: 70;
+ max-width: 80px;
+ min-width: 50px;
+}
+
+.emotion-1 {
+ background-color: #EF1703;
+ border-radius: 0;
+ color: #0A0A0A;
+ font-family: "Poppins",sans-serif;
+ font-size: 14px;
+ line-height: 1.5;
+ font-weight: 500;
+ letter-spacing: 0;
+ padding-inline: 8px;
+ padding-block: 16px;
+}
+
+
+
+`;
diff --git a/src/tooltip/__tests__/tooltip.stories.tsx b/src/tooltip/__tests__/tooltip.stories.tsx
new file mode 100644
index 0000000000..3611b130bf
--- /dev/null
+++ b/src/tooltip/__tests__/tooltip.stories.tsx
@@ -0,0 +1,466 @@
+import * as React from 'react';
+import {Button, ButtonSize} from '../../button';
+import {GridLayout, GridLayoutItem} from '../../grid-layout';
+import {StorybookSubHeading} from '../../test/storybook-comps';
+import {createTheme, ThemeProvider} from '../../theme';
+import {styled} from '../../utils';
+import {Tooltip} from '../tooltip';
+import {IconFilledTwitter} from '../../icons';
+
+export default {
+ title: 'NewsKit Light/tooltip',
+ component: () => 'None',
+};
+
+const Box = styled.div`
+ justify-content: center;
+ padding: 24px;
+ margin: 24px;
+`;
+
+const myCustomTheme = createTheme({
+ name: 'my-custom-modal-theme',
+ overrides: {
+ stylePresets: {
+ tooltipPanelCustom: {
+ base: {
+ backgroundColor: '{{colors.red080}}',
+ borderRadius: '{{borders.borderRadiusDefault}}',
+ color: '{{colors.inkInverse}}',
+ },
+ },
+ },
+ },
+});
+
+export const StoryTooltipPlacements = () => (
+ <>
+ Tooltip
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+);
+StoryTooltipPlacements.storyName = 'tooltip-placements';
+StoryTooltipPlacements.parameters = {
+ eyes: {include: false},
+};
+
+export const StoryTooltipTriggers = () => (
+ <>
+ Triggered by focus
+
+
+
+ Triggered by hover & focus
+
+
+
+ >
+);
+StoryTooltipTriggers.storyName = 'tooltip-triggers';
+StoryTooltipTriggers.parameters = {
+ eyes: {include: false},
+};
+
+export const StoryTooltipDefaultOpen = () => (
+ <>
+ Tooltip default open
+
+
+
+ >
+);
+StoryTooltipDefaultOpen.storyName = 'tooltip-default-open';
+
+export const StoryTooltipControlled = () => {
+ const [open, setOpen] = React.useState(false);
+ return (
+ <>
+ Tooltip Controlled
+
+
+
+
+ External state control - click the button below to show/hide the
+ tooltip.
+
+
+ >
+ );
+};
+StoryTooltipControlled.storyName = 'tooltip-controlled';
+StoryTooltipControlled.parameters = {
+ eyes: {include: false},
+};
+
+export const StoryTooltipAccessibility = () => (
+ <>
+
+ When title is empty, no tooltip is displayed
+
+
+
+
+ Title is not a string
+ Hello} placement="right">
+
+
+ Tooltip as a label
+
+
+
+ >
+);
+StoryTooltipAccessibility.storyName = 'tooltip-accessibility';
+StoryTooltipAccessibility.parameters = {
+ eyes: {include: false},
+};
+
+export const StoryTooltipOverrides = () => (
+ <>
+ Tooltip Overrides
+
+
+
+
+
+ >
+);
+StoryTooltipOverrides.storyName = 'tooltip-overrides';
+
+export const StoryTooltipPlacementsVisualTest = () => (
+ <>
+ Tooltip
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+);
+StoryTooltipPlacementsVisualTest.storyName = 'tooltip-placements-visual-test';
diff --git a/src/tooltip/__tests__/tooltip.test.tsx b/src/tooltip/__tests__/tooltip.test.tsx
new file mode 100644
index 0000000000..c12c2c0ec7
--- /dev/null
+++ b/src/tooltip/__tests__/tooltip.test.tsx
@@ -0,0 +1,242 @@
+import React from 'react';
+import {cleanup, fireEvent} from '@testing-library/react';
+import {renderWithTheme} from '../../test/test-utils';
+import {Tooltip} from '..';
+import {TriggerType} from '../types';
+import {Button} from '../../button';
+import {createTheme} from '../../theme';
+
+describe('Tooltip', () => {
+ const defaultProps = {
+ children: ,
+ title: 'hello',
+ defaultOpen: true,
+ };
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ describe('should render correct styles:', () => {
+ // Mocking ResizeObserver
+ const mockResizeObserver = jest.fn(() => ({
+ observe: jest.fn(),
+ disconnect: jest.fn(),
+ }));
+
+ beforeAll(() => {
+ // @ts-ignore
+ global.ResizeObserver = mockResizeObserver;
+ });
+
+ test('default', () => {
+ const {getByRole, asFragment} = renderWithTheme(Tooltip, defaultProps);
+ expect(getByRole('tooltip').textContent).toBe('hello');
+ expect(getByRole('tooltip')).toHaveStyle({
+ position: 'absolute',
+ });
+ expect(asFragment()).toMatchSnapshot();
+ });
+
+ test('not render if title is an empty string', () => {
+ const {queryByRole} = renderWithTheme(Tooltip, {
+ children: ,
+ title: '',
+ });
+ expect(queryByRole('tooltip')).not.toBeInTheDocument();
+ });
+
+ // Cannot test the exact position with unit tests but will be covered in visual tests
+ test('with different placement', () => {
+ const {getByRole} = renderWithTheme(Tooltip, {
+ ...defaultProps,
+ placement: 'bottom',
+ });
+ expect(getByRole('tooltip')).toHaveStyle({
+ position: 'absolute',
+ });
+ });
+
+ test('with overrides', () => {
+ const myCustomTheme = createTheme({
+ name: 'my-custom-tooltip-theme',
+ overrides: {
+ stylePresets: {
+ tooltipPanelCustom: {
+ base: {
+ backgroundColor: '{{colors.red060}}',
+ borderRadius: '{{borders.borderRadiusSharp}}',
+ color: '{{colors.inkContrast}}',
+ },
+ },
+ },
+ },
+ });
+ const {asFragment} = renderWithTheme(
+ Tooltip,
+ {
+ ...defaultProps,
+ overrides: {
+ minWidth: '50px',
+ maxWidth: '80px',
+ zIndex: 70,
+ panel: {
+ paddingBlock: 'space040',
+ paddingInline: 'space020',
+ stylePreset: 'tooltipPanelCustom',
+ typographyPreset: 'utilityLabel020',
+ },
+ },
+ },
+ myCustomTheme,
+ );
+ expect(asFragment()).toMatchSnapshot();
+ });
+ });
+
+ describe('with different triggers', () => {
+ test('opens on mouseover by default', () => {
+ const {getByRole, queryByRole} = renderWithTheme(Tooltip, {
+ ...defaultProps,
+ defaultOpen: false,
+ });
+ const button = getByRole('button');
+ fireEvent.mouseEnter(button);
+ expect(queryByRole('tooltip')).toBeInTheDocument();
+ });
+ test('closes on mouseleave', () => {
+ const {getByRole, queryByRole} = renderWithTheme(Tooltip, {
+ ...defaultProps,
+ defaultOpen: false,
+ });
+ const button = getByRole('button');
+
+ fireEvent.mouseEnter(button);
+ fireEvent.mouseLeave(button);
+ expect(queryByRole('tooltip')).not.toBeInTheDocument();
+ });
+ test('opens on focus', () => {
+ const {getByRole, queryByRole} = renderWithTheme(Tooltip, {
+ ...defaultProps,
+ defaultOpen: false,
+ trigger: 'focus' as TriggerType,
+ });
+ const button = getByRole('button');
+ fireEvent.focus(button);
+ expect(queryByRole('tooltip')).toBeInTheDocument();
+ });
+
+ test('closes on blur', () => {
+ const {getByRole, queryByRole} = renderWithTheme(Tooltip, {
+ ...defaultProps,
+ defaultOpen: false,
+ trigger: 'focus' as TriggerType,
+ });
+ const button = getByRole('button');
+ fireEvent.focus(button);
+ fireEvent.blur(button);
+ expect(queryByRole('tooltip')).not.toBeInTheDocument();
+ });
+
+ test('will not open on focus when focus trigger is not passed', () => {
+ const {getByRole, queryByRole} = renderWithTheme(Tooltip, {
+ ...defaultProps,
+ defaultOpen: false,
+ });
+ const button = getByRole('button');
+ fireEvent.focus(button);
+ expect(queryByRole('tooltip')).not.toBeInTheDocument();
+ });
+
+ test('dismisses with escape key', () => {
+ const {queryByRole} = renderWithTheme(Tooltip, defaultProps);
+ expect(queryByRole('tooltip')).toBeInTheDocument();
+ fireEvent.keyDown(document.body, {key: 'Escape'});
+ expect(queryByRole('tooltip')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('pass the correct a11y attributes', () => {
+ test('have role tooltip when used as a description', () => {
+ const {queryByRole} = renderWithTheme(Tooltip, defaultProps);
+ expect(queryByRole('tooltip')).toBeInTheDocument();
+ });
+ test('do not have role tooltip when used as a label', () => {
+ const {queryByRole} = renderWithTheme(Tooltip, {
+ ...defaultProps,
+ labelTooltip: true,
+ });
+ expect(queryByRole('tooltip')).not.toBeInTheDocument();
+ });
+ test('can describe the child when open and remove aria attribute when closed', () => {
+ const {getByRole} = renderWithTheme(Tooltip, {
+ ...defaultProps,
+ defaultOpen: false,
+ });
+ const button = getByRole('button');
+ fireEvent.mouseEnter(button);
+ expect(button.hasAttribute('aria-describedby')).toBe(true);
+ fireEvent.mouseLeave(button);
+ expect(button.hasAttribute('aria-describedby')).toBe(false);
+ });
+ test('can describe the exotic child when open and remove aria attribute when closed', () => {
+ const {getByRole} = renderWithTheme(Tooltip, {
+ children: ,
+ title: the title
,
+ defaultOpen: false,
+ });
+ const button = getByRole('button');
+ fireEvent.mouseEnter(button);
+ expect(button.hasAttribute('aria-describedby')).toBe(true);
+ fireEvent.mouseLeave(button);
+ expect(button.hasAttribute('aria-describedby')).toBe(false);
+ });
+ test('should label the child when open and remove aria attribute when closed', () => {
+ const {getByRole} = renderWithTheme(Tooltip, {
+ ...defaultProps,
+ defaultOpen: false,
+ labelTooltip: true,
+ });
+ const button = getByRole('button');
+ fireEvent.mouseEnter(button);
+ expect(button.hasAttribute('aria-labelledby')).toBe(true);
+ fireEvent.mouseLeave(button);
+ expect(button.hasAttribute('aria-labelledby')).toBe(false);
+ });
+ test('should label the exotic child when open and remove aria attribute when closed', () => {
+ const {getByRole} = renderWithTheme(Tooltip, {
+ children: ,
+ title: the title
,
+ defaultOpen: false,
+ labelTooltip: true,
+ });
+ const button = getByRole('button');
+ fireEvent.mouseEnter(button);
+ expect(button.hasAttribute('aria-labelledby')).toBe(true);
+ fireEvent.mouseLeave(button);
+ expect(button.hasAttribute('aria-labelledby')).toBe(false);
+ });
+ });
+
+ test('should be controllable', () => {
+ const Component = () => {
+ const [open, setOpen] = React.useState(false);
+ return (
+ <>
+
+
+
+
+ >
+ );
+ };
+
+ const {getByTestId, queryByRole} = renderWithTheme(Component);
+
+ const button = getByTestId('outside-control');
+ fireEvent.click(button);
+ expect(queryByRole('tooltip')).toBeInTheDocument();
+ });
+});
diff --git a/src/tooltip/defaults.ts b/src/tooltip/defaults.ts
index 49ce82ae75..76302f09d9 100644
--- a/src/tooltip/defaults.ts
+++ b/src/tooltip/defaults.ts
@@ -1,19 +1,11 @@
export default {
- legend: {
- small: {
- stylePreset: 'legend',
+ tooltip: {
+ zIndex: 80,
+ panel: {
+ paddingBlock: 'spaceInset020',
+ paddingInline: 'spaceInset020',
+ stylePreset: 'tooltipPanel',
typographyPreset: 'utilityLabel010',
- spaceStack: 'space030',
- },
- medium: {
- stylePreset: 'legend',
- typographyPreset: 'utilityLabel020',
- spaceStack: 'space030',
- },
- large: {
- stylePreset: 'legend',
- typographyPreset: 'utilityLabel030',
- spaceStack: 'space030',
},
},
};
diff --git a/src/tooltip/style-presets.ts b/src/tooltip/style-presets.ts
index c2249cf92d..75a921bf2b 100644
--- a/src/tooltip/style-presets.ts
+++ b/src/tooltip/style-presets.ts
@@ -1,12 +1,11 @@
import {StylePreset} from '../theme/types';
export default {
- legend: {
+ tooltipPanel: {
base: {
- color: '{{colors.inkContrast}}',
- },
- disabled: {
- color: '{{colors.inkNonEssential}}',
+ backgroundColor: '{{colors.interface060}}',
+ color: '{{colors.inkInverse}}',
+ borderRadius: '{{borders.borderRadiusDefault}}',
},
},
} as Record;
diff --git a/src/tooltip/styled.tsx b/src/tooltip/styled.tsx
index 298e3ab405..8ed0ac656f 100644
--- a/src/tooltip/styled.tsx
+++ b/src/tooltip/styled.tsx
@@ -1,4 +1,23 @@
-import {styled} from '../utils';
import {TooltipProps} from './types';
+import {
+ getTypographyPreset,
+ getStylePreset,
+ styled,
+ getResponsiveSpace,
+ getResponsiveSize,
+} from '../utils/style';
+import {logicalProps} from '../utils/logical-properties';
-export const StyledTooltip = styled.div``;
+export const StyledTooltipPanel = styled.div>`
+ ${getStylePreset('tooltip.panel', 'panel')};
+ ${getTypographyPreset('tooltip.panel', 'panel')};
+ ${getResponsiveSpace('padding', 'tooltip.panel', 'panel', 'spaceInset')};
+ ${logicalProps('tooltip.panel', 'panel')}
+`;
+
+export const StyledTooltip = styled.div>`
+ pointer-events: none;
+ ${getResponsiveSpace('zIndex', 'tooltip', '', 'zIndex')};
+ ${getResponsiveSize('maxWidth', 'tooltip', '', 'maxWidth')};
+ ${getResponsiveSize('minWidth', 'tooltip', '', 'minWidth')};
+`;
diff --git a/src/tooltip/tooltip.tsx b/src/tooltip/tooltip.tsx
index d2bb86c4f5..7e52c931e5 100644
--- a/src/tooltip/tooltip.tsx
+++ b/src/tooltip/tooltip.tsx
@@ -1,27 +1,100 @@
import * as React from 'react';
+import {
+ autoUpdate,
+ useFloating,
+ useInteractions,
+ useHover,
+ useFocus,
+ useRole,
+ useDismiss,
+ useId,
+} from '@floating-ui/react-dom-interactions';
import {TooltipProps} from './types';
+import {withOwnTheme} from '../utils/with-own-theme';
+import {StyledTooltip, StyledTooltipPanel} from './styled';
import defaults from './defaults';
import stylePresets from './style-presets';
-import {withOwnTheme} from '../utils/with-own-theme';
-import {StyledTooltip} from './styled';
-import {useTooltio} from './use-tooltip';
-
-const ThemelessTooltip = React.forwardRef(
- ({children, title, trigger, open, placement, overrides, ...props}, ref) => {
- if (!title) {
- return <>{children}>;
- }
-
- return (
- <>
- {React.cloneElement(children, childrenProps)}
-
- {title}
+import {useControlled} from '../utils/hooks';
+
+const ThemelessTooltip: React.FC = ({
+ children,
+ title,
+ placement = 'top',
+ trigger = 'hover',
+ open: openProp,
+ defaultOpen,
+ labelTooltip,
+ overrides,
+ ...props
+}) => {
+ const [open, onOpenChange] = useControlled({
+ controlledValue: openProp,
+ defaultValue: Boolean(defaultOpen),
+ });
+
+ const {x, y, reference, floating, strategy, context} = useFloating({
+ placement,
+ open,
+ onOpenChange,
+ whileElementsMounted: autoUpdate,
+ });
+
+ const {getReferenceProps, getFloatingProps} = useInteractions([
+ useHover(context, {
+ enabled: trigger.includes('hover'),
+ }),
+ useFocus(context, {enabled: trigger.includes('focus')}),
+ useRole(context, {enabled: !labelTooltip, role: 'tooltip'}),
+ useDismiss(context),
+ ]);
+
+ // If tooltip is used as a label, add aria-labelledby to childrenProps and id to StyledTooltip
+ const id = useId();
+ const nameOrDescProps = {} as {
+ 'aria-labelledby': string | null;
+ };
+
+ if (labelTooltip) {
+ nameOrDescProps['aria-labelledby'] = open ? id : null;
+ }
+
+ const childrenProps = {
+ ...nameOrDescProps,
+ ...children.props,
+ };
+
+ if (!title) {
+ return children;
+ }
+
+ return (
+ <>
+ {React.cloneElement(
+ children,
+ getReferenceProps({ref: reference, ...childrenProps}),
+ )}
+ {open && (
+
+ {title}
- >
- );
- },
-);
+ )}
+ >
+ );
+};
export const Tooltip = withOwnTheme(ThemelessTooltip)({
defaults,
diff --git a/src/tooltip/types.ts b/src/tooltip/types.ts
index f4e784f093..c9962913f2 100644
--- a/src/tooltip/types.ts
+++ b/src/tooltip/types.ts
@@ -1,38 +1,26 @@
import React from 'react';
+import {Placement} from '@floating-ui/react-dom-interactions';
import {MQ} from '../utils/style';
+import {LogicalPaddingProps} from '../utils/logical-properties';
-export type TooltipPlacement =
- | 'top'
- | 'top-start'
- | 'top-end'
- | 'right'
- | 'right-start'
- | 'right-end'
- | 'bottom'
- | 'bottom-start'
- | 'bottom-end'
- | 'left'
- | 'left-start'
- | 'left-end';
+export type TriggerType = 'hover' | 'focus';
export interface TooltipProps
- extends Omit, 'title'> {
- children?: React.ReactNode;
- title?: React.ReactNode;
- trigger: ('click' | 'hover' | 'focus')[];
- placement?: TooltipPlacement;
+ extends Omit, 'title' | 'defaultValue'> {
+ children: React.ReactElement;
+ title: React.ReactNode;
open?: boolean;
- onOpen?: (event: React.SyntheticEvent) => void;
- onDismiss?: (event: React.SyntheticEvent) => void;
-
+ defaultOpen?: boolean;
+ trigger?: TriggerType | TriggerType[];
+ placement?: Placement;
+ labelTooltip?: boolean;
overrides?: {
+ zIndex?: number;
+ maxWidth?: MQ;
+ minWidth?: MQ;
panel?: {
- maxWidth?: MQ;
- minWidth?: MQ;
- space?: MQ;
stylePreset?: MQ;
typographyPreset?: MQ;
- };
- zIndex?: number;
+ } & LogicalPaddingProps;
};
}
diff --git a/src/tooltip/use-tooltip.ts b/src/tooltip/use-tooltip.ts
deleted file mode 100644
index 78257f0fa3..0000000000
--- a/src/tooltip/use-tooltip.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import React, {useState, useEffect} from 'react';
-import {usePopper} from 'react-popper';
-import {get} from '../utils/get';
-
-export interface UseTooltipProps {
- closeOnClick?: boolean;
- /**
- * If `true`, the tooltip will hide while the mouse
- * is down
- */
- closeOnMouseDown?: boolean;
- /**
- * If `true`, the tooltip will hide on pressing Esc key
- */
- onOpen?(): void;
- /**
- * Callback to run when the tooltip hides
- */
- onClose?(): void;
- /**
- * Custom `id` to use in place of `uuid`
- */
- id?: string;
- /**
- * If `true`, the tooltip will be shown (in controlled mode)
- */
- open?: boolean;
-}
-
-export function useTooltip(props: UseTooltipProps = {}) {}
diff --git a/yarn.lock b/yarn.lock
index 93b67146db..9fb4bdea3e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1977,6 +1977,35 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
+"@floating-ui/core@^0.7.0":
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-0.7.0.tgz#f2442168d65c22daeb48cfb730cbc3a29a846ac7"
+ integrity sha512-W7+i5Suhhvv97WDTW//KqUA43f/2a4abprM1rWqtLM9lIlJ29tbFI8h232SvqunXon0WmKNEKVjbOsgBhTnbLw==
+
+"@floating-ui/dom@^0.5.0":
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-0.5.0.tgz#e4efd9e609ff3ee6778fbe4273930d6d1c220f2e"
+ integrity sha512-PS75dnMg4GdWjDFOiOs15cDzYJpukRNHqQn0ugrBlsrpk2n+y8bwZ24XrsdLSL7kxshmxxr2nTNycLnmRIvV7g==
+ dependencies:
+ "@floating-ui/core" "^0.7.0"
+
+"@floating-ui/react-dom-interactions@^0.6.0":
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.6.0.tgz#a328b4d349b4d706e45c3a6a7dce592f9cf4bd27"
+ integrity sha512-8XzQuQStUNztHvg+Rj6MdUjBsOKIb6Oe0eIs1w9LfssOT0ZEPPHVsCZgLiWoyDaotF6pirLGAXkOfQxPM7VBRQ==
+ dependencies:
+ "@floating-ui/react-dom" "^0.7.0"
+ aria-hidden "^1.1.3"
+ use-isomorphic-layout-effect "^1.1.1"
+
+"@floating-ui/react-dom@^0.7.0":
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-0.7.0.tgz#8f867b6fbf87dbfde6f55fd5f5c2c53cfdd1c873"
+ integrity sha512-mpYGykTqwtBYT+ZTQQ2OfZ6wXJNuUgmqqD9ooCgbMRgvul6InFOTtWYvtujps439hmOFiVPm4PoBkEEn5imidg==
+ dependencies:
+ "@floating-ui/dom" "^0.5.0"
+ use-isomorphic-layout-effect "^1.1.1"
+
"@gar/promisify@^1.0.1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210"
@@ -2431,11 +2460,6 @@
schema-utils "^2.6.5"
source-map "^0.7.3"
-"@popperjs/core@^2.11.5":
- version "2.11.5"
- resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64"
- integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==
-
"@popperjs/core@^2.5.4", "@popperjs/core@^2.6.0":
version "2.10.2"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590"
@@ -15101,14 +15125,6 @@ react-popper@^2.2.4:
react-fast-compare "^3.0.1"
warning "^4.0.2"
-react-popper@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.3.0.tgz#17891c620e1320dce318bad9fede46a5f71c70ba"
- integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==
- dependencies:
- react-fast-compare "^3.0.1"
- warning "^4.0.2"
-
react-range@^1.8.12:
version "1.8.12"
resolved "https://registry.yarnpkg.com/react-range/-/react-range-1.8.12.tgz#61fe79421a519a4d77c76838012d895b75ead42f"
@@ -18064,6 +18080,11 @@ use-isomorphic-layout-effect@^1.0.0:
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz#7bb6589170cd2987a152042f9084f9effb75c225"
integrity sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ==
+use-isomorphic-layout-effect@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
+ integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
+
use-latest@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.2.0.tgz#a44f6572b8288e0972ec411bdd0840ada366f232"
From 79597b4e7104b2d69133cdab256d57d57ed75732 Mon Sep 17 00:00:00 2001
From: Xin00163
Date: Wed, 18 May 2022 17:25:58 +0100
Subject: [PATCH 03/10] fix(PPDSC-2117): add forwardRef to iconButton and
update tests
---
src/icon-button/icon-button.tsx | 11 +++++--
.../__snapshots__/tooltip.test.tsx.snap | 4 +--
src/tooltip/__tests__/tooltip.stories.tsx | 9 +++---
src/tooltip/__tests__/tooltip.test.tsx | 31 +++++++------------
src/tooltip/tooltip.tsx | 15 ++++++---
5 files changed, 37 insertions(+), 33 deletions(-)
diff --git a/src/icon-button/icon-button.tsx b/src/icon-button/icon-button.tsx
index be61f0a03e..7949fa53ab 100644
--- a/src/icon-button/icon-button.tsx
+++ b/src/icon-button/icon-button.tsx
@@ -6,7 +6,10 @@ import defaults from './defaults';
import stylePresets from './style-presets';
import {withOwnTheme} from '../utils/with-own-theme';
-const ThemelessIconButton = ({overrides = {}, ...props}: IconButtonProps) => {
+const ThemelessIconButton = React.forwardRef<
+ HTMLButtonElement | HTMLAnchorElement,
+ IconButtonProps
+>(({overrides = {}, ...props}, ref) => {
const theme = useTheme();
const {size = ButtonSize.Small} = props;
@@ -15,8 +18,10 @@ const ThemelessIconButton = ({overrides = {}, ...props}: IconButtonProps) => {
...filterOutFalsyProperties(overrides),
};
- return ;
-};
+ return (
+
+ );
+});
export const IconButton: React.FC = withOwnTheme(
ThemelessIconButton,
diff --git a/src/tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap b/src/tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap
index bdb3dfb0a5..8fdeee0fd6 100644
--- a/src/tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap
+++ b/src/tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap
@@ -3,7 +3,7 @@
exports[`Tooltip should render correct styles: default 1`] = `
`;
diff --git a/src/tooltip/__tests__/tooltip.stories.tsx b/src/tooltip/__tests__/tooltip.stories.tsx
index 28a5a9edbd..29860dd703 100644
--- a/src/tooltip/__tests__/tooltip.stories.tsx
+++ b/src/tooltip/__tests__/tooltip.stories.tsx
@@ -23,7 +23,7 @@ const myCustomTheme = createTheme({
name: 'my-custom-modal-theme',
overrides: {
stylePresets: {
- tooltipPanelCustom: {
+ tooltipCustom: {
base: {
backgroundColor: '{{colors.red080}}',
borderRadius: '{{borders.borderRadiusDefault}}',
@@ -273,7 +273,7 @@ export const StoryTooltipAccessibility = () => (
Tooltip as a label
-
+
(
overrides={{
minWidth: '50px',
maxWidth: '80px',
- zIndex: 80,
- panel: {
- paddingBlock: 'space040',
- paddingInline: 'space020',
- stylePreset: 'tooltipPanelCustom',
- typographyPreset: 'utilityLabel020',
- },
+ zIndex: 70,
+ paddingBlock: 'space040',
+ paddingInline: 'space020',
+ stylePreset: 'tooltipCustom',
+ typographyPreset: 'utilityLabel020',
}}
>
{
name: 'my-custom-tooltip-theme',
overrides: {
stylePresets: {
- tooltipPanelCustom: {
+ tooltipCustom: {
base: {
backgroundColor: '{{colors.red060}}',
borderRadius: '{{borders.borderRadiusSharp}}',
@@ -73,12 +73,10 @@ describe('Tooltip', () => {
minWidth: '50px',
maxWidth: '80px',
zIndex: 70,
- panel: {
- paddingBlock: 'space040',
- paddingInline: 'space020',
- stylePreset: 'tooltipPanelCustom',
- typographyPreset: 'utilityLabel020',
- },
+ paddingBlock: 'space040',
+ paddingInline: 'space020',
+ stylePreset: 'tooltipCustom',
+ typographyPreset: 'utilityLabel020',
},
},
myCustomTheme,
@@ -157,7 +155,7 @@ describe('Tooltip', () => {
test('do not have role tooltip when used as a label', () => {
const {queryByRole} = renderWithTheme(Tooltip, {
...defaultProps,
- labelTooltip: true,
+ asLabel: true,
});
expect(queryByRole('tooltip')).not.toBeInTheDocument();
});
@@ -188,7 +186,7 @@ describe('Tooltip', () => {
const {getByRole} = renderWithTheme(Tooltip, {
...defaultProps,
defaultOpen: false,
- labelTooltip: true,
+ asLabel: true,
});
const button = getByRole('button');
fireEvent.mouseEnter(button);
@@ -201,7 +199,7 @@ describe('Tooltip', () => {
children: Add,
title: the title
,
defaultOpen: false,
- labelTooltip: true,
+ asLabel: true,
});
const button = getByRole('button');
fireEvent.mouseEnter(button);
diff --git a/src/tooltip/defaults.ts b/src/tooltip/defaults.ts
index 76302f09d9..dcf631967d 100644
--- a/src/tooltip/defaults.ts
+++ b/src/tooltip/defaults.ts
@@ -1,11 +1,9 @@
export default {
tooltip: {
zIndex: 80,
- panel: {
- paddingBlock: 'spaceInset020',
- paddingInline: 'spaceInset020',
- stylePreset: 'tooltipPanel',
- typographyPreset: 'utilityLabel010',
- },
+ paddingBlock: 'spaceInset020',
+ paddingInline: 'spaceInset020',
+ stylePreset: 'tooltip',
+ typographyPreset: 'utilityLabel010',
},
};
diff --git a/src/tooltip/style-presets.ts b/src/tooltip/style-presets.ts
index 75a921bf2b..e25f42eebc 100644
--- a/src/tooltip/style-presets.ts
+++ b/src/tooltip/style-presets.ts
@@ -1,7 +1,7 @@
import {StylePreset} from '../theme/types';
export default {
- tooltipPanel: {
+ tooltip: {
base: {
backgroundColor: '{{colors.interface060}}',
color: '{{colors.inkInverse}}',
diff --git a/src/tooltip/styled.tsx b/src/tooltip/styled.tsx
index 8ed0ac656f..b7ed206e01 100644
--- a/src/tooltip/styled.tsx
+++ b/src/tooltip/styled.tsx
@@ -8,16 +8,12 @@ import {
} from '../utils/style';
import {logicalProps} from '../utils/logical-properties';
-export const StyledTooltipPanel = styled.div>`
- ${getStylePreset('tooltip.panel', 'panel')};
- ${getTypographyPreset('tooltip.panel', 'panel')};
- ${getResponsiveSpace('padding', 'tooltip.panel', 'panel', 'spaceInset')};
- ${logicalProps('tooltip.panel', 'panel')}
-`;
-
export const StyledTooltip = styled.div>`
pointer-events: none;
${getResponsiveSpace('zIndex', 'tooltip', '', 'zIndex')};
${getResponsiveSize('maxWidth', 'tooltip', '', 'maxWidth')};
${getResponsiveSize('minWidth', 'tooltip', '', 'minWidth')};
+ ${getStylePreset('tooltip', '')};
+ ${getTypographyPreset('tooltip', '')};
+ ${logicalProps('tooltip', '')}
`;
diff --git a/src/tooltip/tooltip.tsx b/src/tooltip/tooltip.tsx
index de6945ebf3..79e244feda 100644
--- a/src/tooltip/tooltip.tsx
+++ b/src/tooltip/tooltip.tsx
@@ -9,9 +9,10 @@ import {
useDismiss,
useId,
} from '@floating-ui/react-dom-interactions';
+import composeRefs from '@seznam/compose-react-refs';
import {TooltipProps} from './types';
import {withOwnTheme} from '../utils/with-own-theme';
-import {StyledTooltip, StyledTooltipPanel} from './styled';
+import {StyledTooltip} from './styled';
import defaults from './defaults';
import stylePresets from './style-presets';
import {useControlled} from '../utils/hooks';
@@ -23,7 +24,7 @@ const ThemelessTooltip: React.FC = ({
trigger = 'hover',
open: openProp,
defaultOpen,
- labelTooltip,
+ asLabel,
overrides,
...props
}) => {
@@ -44,7 +45,7 @@ const ThemelessTooltip: React.FC = ({
enabled: trigger.includes('hover'),
}),
useFocus(context, {enabled: trigger.includes('focus')}),
- useRole(context, {enabled: !labelTooltip, role: 'tooltip'}),
+ useRole(context, {enabled: !asLabel, role: 'tooltip'}),
useDismiss(context),
]);
@@ -53,11 +54,11 @@ const ThemelessTooltip: React.FC = ({
const id = useId();
const labelOrDescProps = {} as {
- 'aria-labelledby': string | null;
- 'aria-describedby': string | null;
+ 'aria-labelledby'?: string | null;
+ 'aria-describedby'?: string | null;
};
- if (labelTooltip) {
+ if (asLabel) {
labelOrDescProps['aria-labelledby'] = open ? id : null;
} else {
labelOrDescProps['aria-describedby'] = open ? id : null;
@@ -76,7 +77,10 @@ const ThemelessTooltip: React.FC = ({
<>
{React.cloneElement(
children,
- getReferenceProps({ref: reference, ...childrenProps}),
+ getReferenceProps({
+ ref: composeRefs(reference, children.ref),
+ ...childrenProps,
+ }),
)}
{open && (
= ({
overrides={overrides}
{...props}
>
- {title}
+ {title}
)}
>
diff --git a/src/tooltip/types.ts b/src/tooltip/types.ts
index c9962913f2..95bda72b30 100644
--- a/src/tooltip/types.ts
+++ b/src/tooltip/types.ts
@@ -7,20 +7,20 @@ export type TriggerType = 'hover' | 'focus';
export interface TooltipProps
extends Omit, 'title' | 'defaultValue'> {
- children: React.ReactElement;
+ children: React.ReactElement & {
+ ref?: React.Ref;
+ };
title: React.ReactNode;
open?: boolean;
defaultOpen?: boolean;
trigger?: TriggerType | TriggerType[];
placement?: Placement;
- labelTooltip?: boolean;
+ asLabel?: boolean;
overrides?: {
zIndex?: number;
maxWidth?: MQ;
minWidth?: MQ;
- panel?: {
- stylePreset?: MQ;
- typographyPreset?: MQ;
- } & LogicalPaddingProps;
- };
+ stylePreset?: MQ;
+ typographyPreset?: MQ;
+ } & LogicalPaddingProps;
}
From 2d12d9b065f1365befa66999f95b61c21ea28da4 Mon Sep 17 00:00:00 2001
From: Xin00163
Date: Fri, 20 May 2022 15:43:18 +0100
Subject: [PATCH 06/10] fix(PPDSC-2117): address comments
---
.../__tests__/icon-button.test.tsx | 27 +-
src/tooltip/__tests__/tooltip.stories.tsx | 694 +++++++++++-------
src/tooltip/tooltip.tsx | 4 +-
3 files changed, 458 insertions(+), 267 deletions(-)
diff --git a/src/icon-button/__tests__/icon-button.test.tsx b/src/icon-button/__tests__/icon-button.test.tsx
index d5477e3e4a..0e66cd1762 100644
--- a/src/icon-button/__tests__/icon-button.test.tsx
+++ b/src/icon-button/__tests__/icon-button.test.tsx
@@ -1,5 +1,9 @@
-import React from 'react';
-import {renderToFragmentWithTheme} from '../../test/test-utils';
+import React, {createRef} from 'react';
+import {act} from 'react-test-renderer';
+import {
+ renderToFragmentWithTheme,
+ renderWithTheme,
+} from '../../test/test-utils';
import {IconButton} from '..';
import {ButtonSize, IconButtonProps} from '../../button';
import {IconFilledEmail} from '../../icons';
@@ -73,4 +77,23 @@ describe('IconButton', () => {
const fragment = renderToFragmentWithTheme(renderIconButton, props);
expect(fragment).toMatchSnapshot();
});
+
+ test('focus can be triggered with ref', async () => {
+ const iconButtonRef = createRef();
+
+ const props = {
+ ref: iconButtonRef,
+ 'aria-label': 'Test icon button',
+ children: ,
+ };
+
+ renderWithTheme(IconButton, props);
+
+ await act(async () => {
+ if (iconButtonRef && iconButtonRef.current) {
+ iconButtonRef.current.focus();
+ }
+ });
+ expect(iconButtonRef.current).toHaveFocus();
+ });
});
diff --git a/src/tooltip/__tests__/tooltip.stories.tsx b/src/tooltip/__tests__/tooltip.stories.tsx
index 29860dd703..419534c3a6 100644
--- a/src/tooltip/__tests__/tooltip.stories.tsx
+++ b/src/tooltip/__tests__/tooltip.stories.tsx
@@ -7,16 +7,22 @@ import {styled} from '../../utils';
import {Tooltip} from '../tooltip';
import {IconFilledTwitter} from '../../icons';
import {IconButton} from '../../icon-button';
+import {Flow, Stack} from '../../stack';
+import {LinkInline, LinkStandalone} from '../../link';
export default {
title: 'NewsKit Light/tooltip',
component: () => 'None',
};
-const Box = styled.div`
- justify-content: center;
- padding: 24px;
- margin: 24px;
+const StyledDiv = styled.div`
+ margin-left: 200px;
+ margin-top: 48px;
+`;
+
+const Container = styled.div`
+ max-width: 600px;
+ margin: 50px auto;
`;
const myCustomTheme = createTheme({
@@ -34,10 +40,120 @@ const myCustomTheme = createTheme({
},
});
+export const StoryTooltip = () => (
+
+ Tooltip with icon button
+
+
+
+
+
+ Tooltip with button
+
+
+ Button
+
+
+ Tooltip with inline link
+
+ Inline link
+
+ Tooltip with standalone link
+
+ Standalone link
+
+
+ When title is empty, no tooltip is displayed
+
+
+
+ Button
+
+
+ When title is not a string
+ Lorem ipsum dolor sit amet}
+ placement="right"
+ trigger={['focus', 'hover']}
+ >
+
+ Button
+
+
+
+ When title is long and without maxWidth
+
+
+
+
+ Button
+
+
+
+
+ When title is long and with maxWidth
+
+
+
+
+ Button
+
+
+
+
+);
+StoryTooltip.storyName = 'tooltip';
+StoryTooltip.parameters = {
+ eyes: {include: false},
+};
+
export const StoryTooltipPlacements = () => (
<>
- Tooltip
-
+ Tooltip Placements
+
(
justifySelf="flex-start"
alignSelf="center"
>
-
-
- left-start
-
-
-
-
-
- left
-
-
-
-
-
- left-end
-
-
+
+
+
+ left-start
+
+
+
+
+
+ left
+
+
+
+
+
+ left-end
+
+
+
(
justifySelf="flex-end"
alignSelf="center"
>
-
-
- right-start
-
-
-
-
-
- right
-
-
-
-
-
- right-end
-
-
-
+
+
+
+ right-start
+
+
+
+
+ right
+
+
+
+
+ right-end
+
+
+
(
justifySelf="center"
alignSelf="flex-start"
>
-
-
- top-start
-
-
-
-
- top
-
-
-
-
- top-end
-
-
+
+
+
+ top-start
+
+
+
+
+ top
+
+
+
+
+ top-end
+
+
+
(
justifySelf="center"
alignSelf="flex-end"
>
-
-
- bottom-start
-
-
-
-
- bottom
-
-
-
-
- bottom-end
-
-
+
+
+
+ bottom-start
+
+
+
+
+ bottom
+
+
+
+
+ bottom-end
+
+
+
-
+
>
);
StoryTooltipPlacements.storyName = 'tooltip-placements';
@@ -184,22 +341,26 @@ StoryTooltipPlacements.parameters = {
export const StoryTooltipTriggers = () => (
<>
- Triggered by focus
-
+ Triggered by focus only
+
- right
+ Button
Triggered by hover & focus
-
+
- right
+ Button
>
@@ -212,12 +373,12 @@ StoryTooltipTriggers.parameters = {
export const StoryTooltipDefaultOpen = () => (
<>
Tooltip default open
-
+
- bottom-end
+ Button
>
@@ -229,12 +390,17 @@ export const StoryTooltipControlled = () => {
return (
<>
Tooltip Controlled
-
+
- right
+ Button
@@ -250,56 +416,17 @@ StoryTooltipControlled.parameters = {
eyes: {include: false},
};
-export const StoryTooltipAccessibility = () => (
- <>
-
- When title is empty, no tooltip is displayed
-
-
-
- right
-
-
- Title is not a string
- Hello} placement="right">
-
- right
-
-
- Tooltip as a label
-
-
-
-
-
- >
-);
-StoryTooltipAccessibility.storyName = 'tooltip-accessibility';
-StoryTooltipAccessibility.parameters = {
- eyes: {include: false},
-};
-
export const StoryTooltipOverrides = () => (
<>
Tooltip Overrides
(
size={ButtonSize.Small}
overrides={{stylePreset: 'buttonOutlinedPrimary'}}
>
- right
+ Button
@@ -321,8 +448,8 @@ StoryTooltipOverrides.storyName = 'tooltip-overrides';
export const StoryTooltipPlacementsVisualTest = () => (
<>
- Tooltip
-
+ Tooltip Visual
+
(
justifySelf="flex-start"
alignSelf="center"
>
-
-
- left-start
-
-
-
-
-
- left
-
-
-
-
-
- left-end
-
-
+
+
+
+ left-start
+
+
+
+
+
+ left
+
+
+
+
+
+ left-end
+
+
+
(
justifySelf="flex-end"
alignSelf="center"
>
-
-
- right-start
-
-
-
-
-
- right
-
-
-
-
-
- right-end
-
-
-
+
+
+
+ right-start
+
+
+
+
+ right
+
+
+
+
+ right-end
+
+
+
(
justifySelf="center"
alignSelf="flex-start"
>
-
-
- top-start
-
-
-
-
- top
-
-
-
-
- top-end
-
-
+
+
+
+ top-start
+
+
+
+
+ top
+
+
+
+
+ top-end
+
+
+
(
justifySelf="center"
alignSelf="flex-end"
>
-
-
- bottom-start
-
-
-
-
- bottom
-
-
-
-
- bottom-end
-
-
+
+
+
+ bottom-start
+
+
+
+
+ bottom
+
+
+
+
+ bottom-end
+
+
+
-
+
>
);
StoryTooltipPlacementsVisualTest.storyName = 'tooltip-placements-visual-test';
diff --git a/src/tooltip/tooltip.tsx b/src/tooltip/tooltip.tsx
index 79e244feda..60ab30a62e 100644
--- a/src/tooltip/tooltip.tsx
+++ b/src/tooltip/tooltip.tsx
@@ -28,7 +28,7 @@ const ThemelessTooltip: React.FC = ({
overrides,
...props
}) => {
- const [open, onOpenChange] = useControlled({
+ const [open, setOpenState] = useControlled({
controlledValue: openProp,
defaultValue: Boolean(defaultOpen),
});
@@ -36,7 +36,7 @@ const ThemelessTooltip: React.FC = ({
const {x, y, reference, floating, strategy, context} = useFloating({
placement,
open,
- onOpenChange,
+ onOpenChange: setOpenState,
whileElementsMounted: autoUpdate,
});
From ae6b7a35426f1230bc2f011184b44ce9fa8ade73 Mon Sep 17 00:00:00 2001
From: Xin00163
Date: Mon, 23 May 2022 22:01:37 +0100
Subject: [PATCH 07/10] fix(PPDSC-2117): address reviews
---
src/button/types.ts | 2 +-
src/icon-button/types.ts | 2 +-
.../__tests__/__snapshots__/tooltip.test.tsx.snap | 14 ++++++++------
src/tooltip/__tests__/tooltip.stories.tsx | 1 -
src/tooltip/__tests__/tooltip.test.tsx | 2 +-
src/tooltip/styled.tsx | 3 ++-
src/tooltip/tooltip.tsx | 6 +++---
7 files changed, 16 insertions(+), 14 deletions(-)
diff --git a/src/button/types.ts b/src/button/types.ts
index ff9e01e456..cc749f468b 100644
--- a/src/button/types.ts
+++ b/src/button/types.ts
@@ -34,4 +34,4 @@ export const isButtonLink = (
props: ButtonOrButtonLinkProps,
): props is ButtonLinkProps => (props as ButtonLinkProps).href !== undefined;
-export type IconButtonProps = {'aria-label': string} & ButtonOrButtonLinkProps;
+export type IconButtonProps = {'aria-label'?: string} & ButtonOrButtonLinkProps;
diff --git a/src/icon-button/types.ts b/src/icon-button/types.ts
index efb115cd44..d896ceb615 100644
--- a/src/icon-button/types.ts
+++ b/src/icon-button/types.ts
@@ -1,5 +1,5 @@
import {ButtonProps} from '../button';
export interface IconButtonProps extends ButtonProps {
- 'aria-label': string;
+ 'aria-label'?: string;
}
diff --git a/src/tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap b/src/tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap
index cd85821e33..90f5b02ee9 100644
--- a/src/tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap
+++ b/src/tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap
@@ -9,6 +9,7 @@ exports[`Tooltip should render correct styles: default 1`] = `
Add
.emotion-0 {
+ margin: 0;
pointer-events: none;
z-index: 80;
background-color: #0A0A0A;
@@ -23,16 +24,15 @@ exports[`Tooltip should render correct styles: default 1`] = `
padding-block: 8px;
}
-
hello
-
+
`;
@@ -45,6 +45,9 @@ exports[`Tooltip should render correct styles: with overrides 1`] = `
Add
.emotion-0 {
+ margin: 0;
+ padding-inline: 8px;
+ padding-block: 16px;
pointer-events: none;
z-index: 70;
max-width: 80px;
@@ -61,15 +64,14 @@ exports[`Tooltip should render correct styles: with overrides 1`] = `
padding-block: 16px;
}
-
hello
-
+
`;
diff --git a/src/tooltip/__tests__/tooltip.stories.tsx b/src/tooltip/__tests__/tooltip.stories.tsx
index 419534c3a6..ae7b5a7ebc 100644
--- a/src/tooltip/__tests__/tooltip.stories.tsx
+++ b/src/tooltip/__tests__/tooltip.stories.tsx
@@ -52,7 +52,6 @@ export const StoryTooltip = () => (
diff --git a/src/tooltip/__tests__/tooltip.test.tsx b/src/tooltip/__tests__/tooltip.test.tsx
index cc7a6fa0e2..e70b8e59f7 100644
--- a/src/tooltip/__tests__/tooltip.test.tsx
+++ b/src/tooltip/__tests__/tooltip.test.tsx
@@ -170,7 +170,7 @@ describe('Tooltip', () => {
fireEvent.mouseLeave(button);
expect(button.hasAttribute('aria-describedby')).toBe(false);
});
- test('can describe the exotic child when open and remove aria attribute when closed', () => {
+ test('can render with exotic title when open and remove aria attribute when closed', () => {
const {getByRole} = renderWithTheme(Tooltip, {
children: Add,
title: the title
,
diff --git a/src/tooltip/styled.tsx b/src/tooltip/styled.tsx
index b7ed206e01..c5e334442b 100644
--- a/src/tooltip/styled.tsx
+++ b/src/tooltip/styled.tsx
@@ -7,8 +7,9 @@ import {
getResponsiveSize,
} from '../utils/style';
import {logicalProps} from '../utils/logical-properties';
+import {TextBlock} from '../text-block';
-export const StyledTooltip = styled.div>`
+export const StyledTooltip = styled(TextBlock)>`
pointer-events: none;
${getResponsiveSpace('zIndex', 'tooltip', '', 'zIndex')};
${getResponsiveSize('maxWidth', 'tooltip', '', 'maxWidth')};
diff --git a/src/tooltip/tooltip.tsx b/src/tooltip/tooltip.tsx
index 60ab30a62e..cc7a1c4802 100644
--- a/src/tooltip/tooltip.tsx
+++ b/src/tooltip/tooltip.tsx
@@ -28,7 +28,7 @@ const ThemelessTooltip: React.FC = ({
overrides,
...props
}) => {
- const [open, setOpenState] = useControlled({
+ const [open, setOpen] = useControlled({
controlledValue: openProp,
defaultValue: Boolean(defaultOpen),
});
@@ -36,7 +36,7 @@ const ThemelessTooltip: React.FC = ({
const {x, y, reference, floating, strategy, context} = useFloating({
placement,
open,
- onOpenChange: setOpenState,
+ onOpenChange: setOpen,
whileElementsMounted: autoUpdate,
});
@@ -84,6 +84,7 @@ const ThemelessTooltip: React.FC = ({
)}
{open && (
= ({
left: x ?? '',
},
})}
- data-testid="tooltip"
overrides={overrides}
{...props}
>
From c311cfd8a16bf5265fba93c28632e6e421da7688 Mon Sep 17 00:00:00 2001
From: Xin00163
Date: Tue, 24 May 2022 10:30:36 +0100
Subject: [PATCH 08/10] fix(PPDSC-2117): a11y test
---
src/tooltip/__tests__/tooltip.test.tsx | 14 +++++---------
src/tooltip/tooltip.tsx | 8 ++++++--
2 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/src/tooltip/__tests__/tooltip.test.tsx b/src/tooltip/__tests__/tooltip.test.tsx
index e70b8e59f7..d882f27f58 100644
--- a/src/tooltip/__tests__/tooltip.test.tsx
+++ b/src/tooltip/__tests__/tooltip.test.tsx
@@ -170,7 +170,7 @@ describe('Tooltip', () => {
fireEvent.mouseLeave(button);
expect(button.hasAttribute('aria-describedby')).toBe(false);
});
- test('can render with exotic title when open and remove aria attribute when closed', () => {
+ test('can describe with exotic title when open and remove aria attribute when closed', () => {
const {getByRole} = renderWithTheme(Tooltip, {
children: Add,
title: the title
,
@@ -182,19 +182,16 @@ describe('Tooltip', () => {
fireEvent.mouseLeave(button);
expect(button.hasAttribute('aria-describedby')).toBe(false);
});
- test('should label the child when open and remove aria attribute when closed', () => {
+ test('should label the child when closed', () => {
const {getByRole} = renderWithTheme(Tooltip, {
...defaultProps,
defaultOpen: false,
asLabel: true,
});
const button = getByRole('button');
- fireEvent.mouseEnter(button);
- expect(button.hasAttribute('aria-labelledby')).toBe(true);
- fireEvent.mouseLeave(button);
- expect(button.hasAttribute('aria-labelledby')).toBe(false);
+ expect(button.hasAttribute('aria-label')).toBe(true);
});
- test('should label the exotic child when open and remove aria attribute when closed', () => {
+ test('should label the child when open with an exotic title', () => {
const {getByRole} = renderWithTheme(Tooltip, {
children: Add,
title: the title
,
@@ -202,10 +199,9 @@ describe('Tooltip', () => {
asLabel: true,
});
const button = getByRole('button');
+ expect(button.hasAttribute('aria-labelledby')).toBe(false);
fireEvent.mouseEnter(button);
expect(button.hasAttribute('aria-labelledby')).toBe(true);
- fireEvent.mouseLeave(button);
- expect(button.hasAttribute('aria-labelledby')).toBe(false);
});
});
diff --git a/src/tooltip/tooltip.tsx b/src/tooltip/tooltip.tsx
index cc7a1c4802..b2d36d8036 100644
--- a/src/tooltip/tooltip.tsx
+++ b/src/tooltip/tooltip.tsx
@@ -53,13 +53,17 @@ const ThemelessTooltip: React.FC = ({
// Because of above, 'aria-describedby' has different id for reference and floating, hence manually set below as well;
const id = useId();
+ const titleIsString = typeof title === 'string';
+
const labelOrDescProps = {} as {
+ 'aria-label'?: string | null;
'aria-labelledby'?: string | null;
'aria-describedby'?: string | null;
};
if (asLabel) {
- labelOrDescProps['aria-labelledby'] = open ? id : null;
+ labelOrDescProps['aria-label'] = titleIsString ? title : null;
+ labelOrDescProps['aria-labelledby'] = open && !titleIsString ? id : null;
} else {
labelOrDescProps['aria-describedby'] = open ? id : null;
}
@@ -84,7 +88,7 @@ const ThemelessTooltip: React.FC = ({
)}
{open && (
Date: Tue, 24 May 2022 11:29:54 +0100
Subject: [PATCH 09/10] fix(PPDSC-2117): naming and comment
---
src/tooltip/tooltip.tsx | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/src/tooltip/tooltip.tsx b/src/tooltip/tooltip.tsx
index b2d36d8036..b56e8eac26 100644
--- a/src/tooltip/tooltip.tsx
+++ b/src/tooltip/tooltip.tsx
@@ -49,21 +49,22 @@ const ThemelessTooltip: React.FC = ({
useDismiss(context),
]);
- // If tooltip is used as a label, add aria-labelledby to childrenProps and id to StyledTooltip;
- // Because of above, 'aria-describedby' has different id for reference and floating, hence manually set below as well;
const id = useId();
- const titleIsString = typeof title === 'string';
+ const isTitleString = typeof title === 'string';
- const labelOrDescProps = {} as {
+ const labelOrDescProps: {
'aria-label'?: string | null;
'aria-labelledby'?: string | null;
'aria-describedby'?: string | null;
- };
+ } = {};
+ // If tooltip is used as a label, add aria-label or aria-labelledby to childrenProps and id to StyledTooltip;
+ // aria-label is used when title is string; aria-labelledby is used when it's not a string;
+ // Because of above, 'aria-describedby' has different id for reference and floating, hence manually set below as well;
if (asLabel) {
- labelOrDescProps['aria-label'] = titleIsString ? title : null;
- labelOrDescProps['aria-labelledby'] = open && !titleIsString ? id : null;
+ labelOrDescProps['aria-label'] = isTitleString ? title : null;
+ labelOrDescProps['aria-labelledby'] = open && !isTitleString ? id : null;
} else {
labelOrDescProps['aria-describedby'] = open ? id : null;
}
@@ -88,7 +89,7 @@ const ThemelessTooltip: React.FC = ({
)}
{open && (
Date: Tue, 24 May 2022 12:09:31 +0100
Subject: [PATCH 10/10] fix(PPDSC-2117): design feedback
---
src/tooltip/__tests__/tooltip.stories.tsx | 38 +++++++++++------------
1 file changed, 18 insertions(+), 20 deletions(-)
diff --git a/src/tooltip/__tests__/tooltip.stories.tsx b/src/tooltip/__tests__/tooltip.stories.tsx
index ae7b5a7ebc..fc863e359d 100644
--- a/src/tooltip/__tests__/tooltip.stories.tsx
+++ b/src/tooltip/__tests__/tooltip.stories.tsx
@@ -155,14 +155,13 @@ export const StoryTooltipPlacements = () => (
@@ -205,8 +204,8 @@ export const StoryTooltipPlacements = () => (
@@ -247,9 +246,9 @@ export const StoryTooltipPlacements = () => (
@@ -289,9 +288,9 @@ export const StoryTooltipPlacements = () => (
@@ -451,14 +450,13 @@ export const StoryTooltipPlacementsVisualTest = () => (
@@ -501,8 +499,8 @@ export const StoryTooltipPlacementsVisualTest = () => (
@@ -543,9 +541,9 @@ export const StoryTooltipPlacementsVisualTest = () => (
@@ -585,9 +583,9 @@ export const StoryTooltipPlacementsVisualTest = () => (