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 Tooltip as a label - - + ); diff --git a/src/tooltip/__tests__/tooltip.test.tsx b/src/tooltip/__tests__/tooltip.test.tsx index c12c2c0ec7..15948281a5 100644 --- a/src/tooltip/__tests__/tooltip.test.tsx +++ b/src/tooltip/__tests__/tooltip.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {cleanup, fireEvent} from '@testing-library/react'; +import {fireEvent} from '@testing-library/react'; import {renderWithTheme} from '../../test/test-utils'; import {Tooltip} from '..'; import {TriggerType} from '../types'; @@ -13,22 +13,18 @@ describe('Tooltip', () => { defaultOpen: true, }; - afterEach(() => { - cleanup(); + // Mocking ResizeObserver + const mockResizeObserver = jest.fn(() => ({ + observe: jest.fn(), + disconnect: jest.fn(), + })); + + beforeAll(() => { + // @ts-ignore + global.ResizeObserver = mockResizeObserver; }); 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'); @@ -37,7 +33,6 @@ describe('Tooltip', () => { }); expect(asFragment()).toMatchSnapshot(); }); - test('not render if title is an empty string', () => { const {queryByRole} = renderWithTheme(Tooltip, { children: , @@ -45,7 +40,6 @@ describe('Tooltip', () => { }); 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, { @@ -56,7 +50,6 @@ describe('Tooltip', () => { position: 'absolute', }); }); - test('with overrides', () => { const myCustomTheme = createTheme({ name: 'my-custom-tooltip-theme', @@ -94,7 +87,7 @@ describe('Tooltip', () => { }); }); - describe('with different triggers', () => { + describe('with different triggers:', () => { test('opens on mouseover by default', () => { const {getByRole, queryByRole} = renderWithTheme(Tooltip, { ...defaultProps, @@ -156,7 +149,7 @@ describe('Tooltip', () => { }); }); - describe('pass the correct a11y attributes', () => { + describe('pass the correct a11y attributes:', () => { test('have role tooltip when used as a description', () => { const {queryByRole} = renderWithTheme(Tooltip, defaultProps); expect(queryByRole('tooltip')).toBeInTheDocument(); diff --git a/src/tooltip/tooltip.tsx b/src/tooltip/tooltip.tsx index 7e52c931e5..de6945ebf3 100644 --- a/src/tooltip/tooltip.tsx +++ b/src/tooltip/tooltip.tsx @@ -48,18 +48,23 @@ const ThemelessTooltip: React.FC = ({ useDismiss(context), ]); - // If tooltip is used as a label, add aria-labelledby to childrenProps and id to StyledTooltip + // 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 nameOrDescProps = {} as { + + const labelOrDescProps = {} as { 'aria-labelledby': string | null; + 'aria-describedby': string | null; }; if (labelTooltip) { - nameOrDescProps['aria-labelledby'] = open ? id : null; + labelOrDescProps['aria-labelledby'] = open ? id : null; + } else { + labelOrDescProps['aria-describedby'] = open ? id : null; } const childrenProps = { - ...nameOrDescProps, + ...labelOrDescProps, ...children.props, }; @@ -77,6 +82,7 @@ const ThemelessTooltip: React.FC = ({ = ({ left: x ?? '', }, })} - id={id} data-testid="tooltip" overrides={overrides} {...props} From 6fddbfd07fca044f47eea2414846d9b8941a8467 Mon Sep 17 00:00:00 2001 From: Xin00163 Date: Thu, 19 May 2022 14:25:15 +0100 Subject: [PATCH 04/10] fix(PPDSC-2117): add to index.ts --- src/__tests__/__snapshots__/index.test.ts.snap | 1 + src/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/__tests__/__snapshots__/index.test.ts.snap b/src/__tests__/__snapshots__/index.test.ts.snap index b9d557133f..cb5b803c49 100644 --- a/src/__tests__/__snapshots__/index.test.ts.snap +++ b/src/__tests__/__snapshots__/index.test.ts.snap @@ -133,6 +133,7 @@ Array [ "TitleBar", "Toast", "ToastProvider", + "Tooltip", "UnorderedList", "VideoPlayer", "Visible", diff --git a/src/index.ts b/src/index.ts index d9f14f2a29..2646ffb220 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,6 +51,7 @@ export * from './theme'; export * from './theme-checker'; export * from './title-bar'; export * from './toast'; +export * from './tooltip'; export * from './typography'; export * from './unordered-list'; export * from './utils/hooks/use-media-query'; From 718a2ee8164c93f48b723b93d1db95b2afa8bc84 Mon Sep 17 00:00:00 2001 From: Xin00163 Date: Fri, 20 May 2022 11:11:30 +0100 Subject: [PATCH 05/10] fix(PPDSC-2117): address comments --- .../__snapshots__/theme.test.ts.snap | 12 +++++------ .../__snapshots__/tooltip.test.tsx.snap | 18 ++--------------- src/tooltip/__tests__/tooltip.stories.tsx | 16 +++++++-------- src/tooltip/__tests__/tooltip.test.tsx | 18 ++++++++--------- src/tooltip/defaults.ts | 10 ++++------ src/tooltip/style-presets.ts | 2 +- src/tooltip/styled.tsx | 10 +++------- src/tooltip/tooltip.tsx | 20 +++++++++++-------- src/tooltip/types.ts | 14 ++++++------- 9 files changed, 49 insertions(+), 71 deletions(-) diff --git a/src/theme/__tests__/__snapshots__/theme.test.ts.snap b/src/theme/__tests__/__snapshots__/theme.test.ts.snap index 47a4ecea6e..f1d63da808 100644 --- a/src/theme/__tests__/__snapshots__/theme.test.ts.snap +++ b/src/theme/__tests__/__snapshots__/theme.test.ts.snap @@ -2473,12 +2473,10 @@ Object { "stylePreset": "toastNeutral", }, "tooltip": Object { - "panel": Object { - "paddingBlock": "spaceInset020", - "paddingInline": "spaceInset020", - "stylePreset": "tooltipPanel", - "typographyPreset": "utilityLabel010", - }, + "paddingBlock": "spaceInset020", + "paddingInline": "spaceInset020", + "stylePreset": "tooltip", + "typographyPreset": "utilityLabel010", "zIndex": 80, }, "unorderedList": Object { @@ -4631,7 +4629,7 @@ Object { "iconColor": "{{colors.inkInverse}}", }, }, - "tooltipPanel": Object { + "tooltip": Object { "base": Object { "backgroundColor": "{{colors.interface060}}", "borderRadius": "{{borders.borderRadiusDefault}}", diff --git a/src/tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap b/src/tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap index 8fdeee0fd6..cd85821e33 100644 --- a/src/tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap +++ b/src/tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap @@ -11,9 +11,6 @@ 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; @@ -34,11 +31,7 @@ exports[`Tooltip should render correct styles: default 1`] = ` style="position: absolute;" tabindex="-1" > -
- hello -
+ hello `; @@ -56,9 +49,6 @@ exports[`Tooltip should render correct styles: with overrides 1`] = ` z-index: 70; max-width: 80px; min-width: 50px; -} - -.emotion-1 { background-color: #EF1703; border-radius: 0; color: #0A0A0A; @@ -79,11 +69,7 @@ exports[`Tooltip should render correct styles: with overrides 1`] = ` style="position: absolute;" tabindex="-1" > -
- hello -
+ hello `; 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', }} > , 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 + + + + Tooltip with inline link + + Inline link + + Tooltip with standalone link + + Standalone link + + + When title is empty, no tooltip is displayed + + + + + When title is not a string + Lorem ipsum dolor sit amet} + placement="right" + trigger={['focus', 'hover']} + > + + + + When title is long and without maxWidth + + + + + + + + When title is long and with maxWidth + + + + + + + +); +StoryTooltip.storyName = 'tooltip'; +StoryTooltip.parameters = { + eyes: {include: false}, +}; + export const StoryTooltipPlacements = () => ( <> - Tooltip - + Tooltip Placements + ( justifySelf="flex-start" alignSelf="center" > - - - -
- - - -
- - - + + + + + + + + + + + + + ( justifySelf="flex-end" alignSelf="center" > - - - -
- - - -
- - - -
+ + + + + + + + + + +
( justifySelf="center" alignSelf="flex-start" > - - - - - - - - - + + + + + + + + + + + ( justifySelf="center" alignSelf="flex-end" > - - - - - - - - - + + + + + + + + + + +
-
+ ); StoryTooltipPlacements.storyName = 'tooltip-placements'; @@ -184,22 +341,26 @@ StoryTooltipPlacements.parameters = { export const StoryTooltipTriggers = () => ( <> - Triggered by focus - + Triggered by focus only + Triggered by hover & focus - + @@ -212,12 +373,12 @@ StoryTooltipTriggers.parameters = { export const StoryTooltipDefaultOpen = () => ( <> Tooltip default open - + @@ -229,12 +390,17 @@ export const StoryTooltipControlled = () => { return ( <> Tooltip Controlled - +

@@ -250,56 +416,17 @@ 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 ( 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" > - - - -
- - - -
- - - + + + + + + + + + + + + + ( justifySelf="flex-end" alignSelf="center" > - - - -
- - - -
- - - -
+ + + + + + + + + + +
( justifySelf="center" alignSelf="flex-start" > - - - - - - - - - + + + + + + + + + + + ( justifySelf="center" alignSelf="flex-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; } -

+

`; @@ -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; } - +

`; 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: , 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: , 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: , 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 = () => (