From e040c4980462481d159f5ff9c241c185662ea688 Mon Sep 17 00:00:00 2001 From: Dennis Pitcock Date: Fri, 30 Aug 2024 15:22:50 +0200 Subject: [PATCH] adding tooltips and tests to visual refrest --- package-lock.json | 6 +- pages/app-layout/utils/drawer-ids.ts | 13 + pages/app-layout/utils/drawers.tsx | 20 +- .../app-layout-toolbar-tooltips.test.ts | 337 ++++++++++++++++++ src/app-layout/__tests__/drawers.test.tsx | 131 ++++++- .../__tests__/trigger-button.test.tsx | 251 +++++++++---- src/app-layout/visual-refresh/drawers.tsx | 59 +-- .../visual-refresh/mobile-toolbar.tsx | 1 + .../visual-refresh/trigger-button.scss | 8 + .../visual-refresh/trigger-button.tsx | 138 ++++++- 10 files changed, 858 insertions(+), 106 deletions(-) create mode 100644 pages/app-layout/utils/drawer-ids.ts create mode 100644 src/app-layout/__integ__/app-layout-toolbar-tooltips.test.ts diff --git a/package-lock.json b/package-lock.json index 8e179e47f07..c80bb5295b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15963,7 +15963,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.56.1", + "version": "1.77.8", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz", + "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15975,7 +15977,7 @@ "sass": "sass.js" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" } }, "node_modules/sass-loader": { diff --git a/pages/app-layout/utils/drawer-ids.ts b/pages/app-layout/utils/drawer-ids.ts new file mode 100644 index 00000000000..cdffe627d9f --- /dev/null +++ b/pages/app-layout/utils/drawer-ids.ts @@ -0,0 +1,13 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +export const drawerIds = { + security: 'security', + proHelp: 'pro-help', + links: 'links', + test1: 'test-1', + test2: 'test-2', + test3: 'test-3', + test4: 'test-4', + test5: 'test-5', + test6: 'test-6', +}; diff --git a/pages/app-layout/utils/drawers.tsx b/pages/app-layout/utils/drawers.tsx index 8b7de8a7f0d..0eaa8bdf578 100644 --- a/pages/app-layout/utils/drawers.tsx +++ b/pages/app-layout/utils/drawers.tsx @@ -4,6 +4,8 @@ import React from 'react'; import { AppLayoutProps, Drawer, SpaceBetween } from '~components'; +import { drawerIds } from './drawer-ids'; + import styles from '../styles.scss'; const getAriaLabels = (title: string, badge: boolean) => { @@ -31,7 +33,7 @@ export const drawerItems: Array = [ { ariaLabels: getAriaLabels('Security', false), content: , - id: 'security', + id: drawerIds.security, resizable: true, onResize: (event: any) => { // A drawer implementer may choose to listen to THEIR drawer's @@ -48,7 +50,7 @@ export const drawerItems: Array = [ content: Pro help}>Pro help., badge: true, defaultSize: 600, - id: 'pro-help', + id: drawerIds.proHelp, trigger: { iconName: 'contact', }, @@ -58,7 +60,7 @@ export const drawerItems: Array = [ resizable: true, defaultSize: 500, content: Links}>Links., - id: 'links', + id: drawerIds.links, trigger: { iconName: 'share', }, @@ -67,7 +69,7 @@ export const drawerItems: Array = [ ariaLabels: getAriaLabels('Test 1', true), content: Test 1}>Test 1., badge: true, - id: 'test-1', + id: drawerIds.test1, trigger: { iconName: 'contact', }, @@ -77,7 +79,7 @@ export const drawerItems: Array = [ resizable: true, defaultSize: 500, content: Test 2}>Test 2., - id: 'test-2', + id: drawerIds.test2, trigger: { iconName: 'share', }, @@ -86,7 +88,7 @@ export const drawerItems: Array = [ ariaLabels: getAriaLabels('Test 3', true), content: Test 3}>Test 3., badge: true, - id: 'test-3', + id: drawerIds.test3, trigger: { iconName: 'contact', }, @@ -96,7 +98,7 @@ export const drawerItems: Array = [ resizable: true, defaultSize: 500, content: Test 4}>Test 4., - id: 'test-4', + id: drawerIds.test4, trigger: { iconName: 'edit', }, @@ -106,7 +108,7 @@ export const drawerItems: Array = [ resizable: true, defaultSize: 500, content: Test 5}>Test 5., - id: 'test-5', + id: drawerIds.test5, trigger: { iconName: 'add-plus', }, @@ -116,7 +118,7 @@ export const drawerItems: Array = [ resizable: true, defaultSize: 500, content: Test 6}>Test 6., - id: 'test-6', + id: drawerIds.test6, trigger: { iconName: 'call', }, diff --git a/src/app-layout/__integ__/app-layout-toolbar-tooltips.test.ts b/src/app-layout/__integ__/app-layout-toolbar-tooltips.test.ts new file mode 100644 index 00000000000..a1aed0af078 --- /dev/null +++ b/src/app-layout/__integ__/app-layout-toolbar-tooltips.test.ts @@ -0,0 +1,337 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects'; +import useBrowser from '@cloudscape-design/browser-test-tools/use-browser'; + +import createWrapper from '../../../lib/components/test-utils/selectors'; +import { drawerIds as drawerIdObj } from '../../../lib/dev-pages/pages/app-layout/utils/drawer-ids'; +import { viewports } from './constants'; + +import visualRefreshStyles from '../../../lib/components/app-layout/visual-refresh/styles.selectors.js'; + +const wrapper = createWrapper().findAppLayout(); +class AppLayoutDrawersPage extends BasePageObject { + async openFirstDrawer() { + await this.click(wrapper.findDrawersTriggers().get(1).toSelector()); + } + + async getElementCenter(selector: string) { + const targetRect = await this.getBoundingBox(selector); + const x = Math.round(targetRect.left + targetRect.width / 2); + const y = Math.round(targetRect.top + targetRect.height / 2); + return { x, y }; + } + + async pointerDown(selector: string) { + const center = await this.getElementCenter(selector); + await (await this.browser.$(selector)).moveTo(); + await this.browser.performActions([ + { + type: 'pointer', + id: 'event', + parameters: { pointerType: 'mouse' }, + actions: [ + { type: 'pointerMove', duration: 0, origin: 'pointer', ...center }, + { type: 'pointerDown', button: 0 }, + { type: 'pause', duration: 100 }, + ], + }, + ]); + } + + async pointerUp() { + await this.browser.performActions([ + { + type: 'pointer', + id: 'event', + parameters: { pointerType: 'mouse' }, + actions: [ + { type: 'pointerUp', button: 0 }, + { type: 'pause', duration: 100 }, + ], + }, + ]); + } +} + +interface SetupTestOptions { + splitPanelPosition?: string; + size?: 'desktop' | 'mobile'; + disableContentPaddings?: string; + visualRefresh?: string; +} + +const drawerIds = Object.values(drawerIdObj); +const VISIBLE_MOBILE_TOOLBAR_TRIGGERS_LIMIT = 2; //must match the number in '../../../lib/components/app-layout/visual-refresh/drawers'; +const mobileDrawerTriggerIds = drawerIds.slice(0, VISIBLE_MOBILE_TOOLBAR_TRIGGERS_LIMIT); + +const setupTest = ( + { + splitPanelPosition = 'bottom', + size = 'desktop', + disableContentPaddings = 'false', + visualRefresh = 'false', + }: SetupTestOptions, + testFn: (page: AppLayoutDrawersPage) => Promise +) => + useBrowser(size === 'desktop' ? viewports.desktop : viewports.mobile, async browser => { + const page = new AppLayoutDrawersPage(browser); + const params = new URLSearchParams({ + visualRefresh, + splitPanelPosition, + disableContentPaddings, + }).toString(); + await browser.url(`#/light/app-layout/with-drawers?${params}`); + await page.waitForVisible(wrapper.findContentRegion().toSelector()); + await testFn(page); + }); + +describe(`theme='visual-refresh'`, () => { + describe(`desktop`, () => { + const size = 'desktop'; + + test( + 'Shows tooltip correctly for mouse interactions on desktop', + setupTest({ disableContentPaddings: 'true', visualRefresh: 'true', size }, async page => { + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + await expect( + page.isExisting(`.${visualRefreshStyles[`drawers-desktop-triggers-container`]}`) + ).resolves.toBeTruthy(); + await expect(page.isExisting(`.${visualRefreshStyles['drawers-trigger-overflow']}`)).resolves.toBeFalsy(); + await page.hoverElement(wrapper.findDrawerTriggerById(drawerIds[0]).toSelector()); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeTruthy(); + + for (const drawerId of drawerIds) { + async () => { + await page.hoverElement(wrapper.findDrawerTriggerById(drawerId).toSelector()); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeTruthy(); + await expect( + page.getElementsCount(wrapper.findByClassName(visualRefreshStyles['trigger-tooltip']).toSelector()) + ).resolves.toBe(1); + await page.hoverElement(`.${visualRefreshStyles[`drawers-desktop-triggers-container`]}`); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + await page.click(`button[data-testid='awsui-app-layout-trigger-${drawerId}']`); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + await expect(page.isExisting(wrapper.findActiveDrawer().toSelector())).resolves.toBeTruthy(); + await page.hoverElement(`.${visualRefreshStyles[`drawers-desktop-triggers-container`]}`); + await expect(page.isExisting(wrapper.findActiveDrawer().toSelector())).resolves.toBeTruthy(); + + for (const nestedDrawerId of drawerIds) { + async () => { + await page.hoverElement(wrapper.findDrawerTriggerById(nestedDrawerId).toSelector()); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeTruthy(); + await expect( + page.getElementsCount(wrapper.findByClassName(visualRefreshStyles['trigger-tooltip']).toSelector()) + ).resolves.toBe(1); + await page.hoverElement(`.${visualRefreshStyles[`drawers-desktop-triggers-container`]}`); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + }; + } + + await page.click(wrapper.findActiveDrawerCloseButton().toSelector()); + await expect(page.isExisting(wrapper.findActiveDrawer().toSelector())).resolves.toBeFalsy(); + await expect(page.isFocused(wrapper.findDrawerTriggerById(drawerId).toSelector())).resolves.toBeTruthy(); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + }; + } + }) + ); + + test( + 'tooltip shows on focus and hides on blur events', + setupTest({ disableContentPaddings: 'true', visualRefresh: 'true', size }, async page => { + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + await expect( + page.isExisting(`.${visualRefreshStyles[`drawers-desktop-triggers-container`]}`) + ).resolves.toBeTruthy(); + await expect( + page.isExisting(`.${visualRefreshStyles['drawers-desktop-triggers-container']}`) + ).resolves.toBeTruthy(); + + for (const drawerId of drawerIds) { + async () => { + await page.hoverElement(wrapper.findDrawerTriggerById(drawerId).toSelector()); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeTruthy(); + await expect( + page.getElementsCount(wrapper.findByClassName(visualRefreshStyles['trigger-tooltip']).toSelector()) + ).resolves.toBe(1); + await page.hoverElement(`.${visualRefreshStyles[`drawers-desktop-triggers-container`]}`); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + await page.click(`button[data-testid='awsui-app-layout-trigger-${drawerId}']`); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + await expect(page.isExisting(wrapper.findActiveDrawer().toSelector())).resolves.toBeTruthy(); + await page.hoverElement(`.${visualRefreshStyles[`drawers-desktop-triggers-container`]}`); + await expect(page.isExisting(wrapper.findActiveDrawer().toSelector())).resolves.toBeTruthy(); + + for (const nestedDrawerId of drawerIds) { + async () => { + await page.hoverElement(wrapper.findDrawerTriggerById(nestedDrawerId).toSelector()); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeTruthy(); + await expect( + page.getElementsCount(wrapper.findByClassName(visualRefreshStyles['trigger-tooltip']).toSelector()) + ).resolves.toBe(1); + await page.hoverElement(`.${visualRefreshStyles[`drawers-desktop-triggers-container`]}`); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + }; + } + + await page.click(wrapper.findActiveDrawerCloseButton().toSelector()); + await expect(page.isExisting(wrapper.findActiveDrawer().toSelector())).resolves.toBeFalsy(); + await expect(page.isFocused(wrapper.findDrawerTriggerById(drawerId).toSelector())).resolves.toBeTruthy(); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + }; + } + }) + ); + + test( + 'Shows tooltip correctly for keyboard (tab) interactions on desktop', + setupTest({ disableContentPaddings: 'true', visualRefresh: 'true', size }, async page => { + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + await expect( + page.isExisting(`.${visualRefreshStyles[`drawers-desktop-triggers-container`]}`) + ).resolves.toBeTruthy(); + await expect( + page.isExisting(`.${visualRefreshStyles['drawers-desktop-triggers-container']}`) + ).resolves.toBeTruthy(); + + for (const drawerId of drawerIds) { + async () => { + //best way to avoid tab navigation errors is to start with a click to open then close the drawer, asserting button is focuses + await page.click(`button[data-testid='awsui-app-layout-trigger-${drawerId}']`); //opens + await page.click(`button[data-testid='awsui-app-layout-trigger-${drawerId}']`); //close drawer + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeTruthy(); + await expect( + page.getElementsCount(wrapper.findByClassName(visualRefreshStyles['trigger-tooltip']).toSelector()) + ).resolves.toBe(1); + await page.keys('Space'); + await expect(page.isExisting(wrapper.findActiveDrawer().toSelector())).resolves.toBeTruthy(); + await page.keys('Tab'); //navigate to close button + await expect( + page.isFocused( + wrapper.findActiveDrawer().findByClassName(visualRefreshStyles['drawer-close-button']).toSelector() + ) + ).resolves.toBeTruthy(); + + //jump back to toolbar and navigate down the triggers + for (const nestedDrawerId of drawerIds) { + async () => { + await page.keys('Tab'); //navigate to next button + await expect( + page.isFocused(wrapper.findDrawerTriggerById(nestedDrawerId).toSelector()) + ).resolves.toBeTruthy(); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeTruthy(); + await expect( + page.getElementsCount(wrapper.findByClassName(visualRefreshStyles['trigger-tooltip']).toSelector()) + ).resolves.toBe(1); + }; + } + + //now navigate back up + for (const reverseNestedDrawerId of [...drawerIds.reverse().slice(1)]) { + async () => { + await page.keys('Shift+Tab'); //navigate to last button + await expect( + page.isFocused(wrapper.findDrawerTriggerById(reverseNestedDrawerId).toSelector()) + ).resolves.toBeTruthy(); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeTruthy(); + await expect( + page.getElementsCount(wrapper.findByClassName(visualRefreshStyles['trigger-tooltip']).toSelector()) + ).resolves.toBe(1); + }; + } + + await page.keys('Shift+Tab'); + await expect( + page.isFocused( + wrapper.findActiveDrawer().findByClassName(visualRefreshStyles['drawer-close-button']).toSelector() + ) + ).resolves.toBeTruthy(); + await page.keys('Space'); //close drawer + + await expect(page.isExisting(wrapper.findActiveDrawer().toSelector())).resolves.toBeFalsy(); + await expect(page.isFocused(wrapper.findDrawerTriggerById(drawerId).toSelector())).resolves.toBeTruthy(); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + }; + } + }) + ); + }); + + describe(`mobile`, () => { + const size = 'mobile'; + + test( + 'Shows tooltip correctly for pointer interactions on mobile', + setupTest({ disableContentPaddings: 'true', visualRefresh: 'true', size }, async page => { + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + await page.pause(100); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + await expect( + page.isExisting(`.${visualRefreshStyles[`drawers-mobile-triggers-container`]}`) + ).resolves.toBeTruthy(); + + for (const drawerId of mobileDrawerTriggerIds) { + async () => { + await page.pause(100); + await expect(page.isExisting(wrapper.findDrawerTriggerById(drawerId).toSelector())).resolves.toBeTruthy(); + await page.pointerDown(wrapper.findDrawerTriggerById(drawerId).toSelector()); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeTruthy(); + await expect( + page.getElementsCount(wrapper.findByClassName(visualRefreshStyles['trigger-tooltip']).toSelector()) + ).resolves.toBe(1); + await page.pointerUp(); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + await page.click(`button[data-testid='awsui-app-layout-trigger-${drawerId}']`); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + await expect(page.isExisting(wrapper.findActiveDrawer().toSelector())).resolves.toBeTruthy(); + await page.click(wrapper.findActiveDrawerCloseButton().toSelector()); + await expect(page.isExisting(wrapper.findActiveDrawer().toSelector())).resolves.toBeFalsy(); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + }; + } + }) + ); + + test( + 'Shows tooltip correctly for key interactions on mobile', + setupTest({ disableContentPaddings: 'true', visualRefresh: 'true', size }, async page => { + //open via hamburger menu o set focus in the toolbar + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + await expect( + page.isExisting(`.${visualRefreshStyles[`drawers-mobile-triggers-container`]}`) + ).resolves.toBeTruthy(); + await page.click(wrapper.findNavigationToggle().toSelector()); //opens navigation drawer + await page.click(wrapper.findNavigationClose().toSelector()); //close drawer + await expect(page.isFocused(wrapper.findNavigationToggle().toSelector())).resolves.toBeTruthy(); + await page.keys([ + 'Tab', //first and only breadcrumb + 'Tab', //first button + ]); + await expect( + page.isFocused(wrapper.findDrawerTriggerById(mobileDrawerTriggerIds[0]).toSelector()) + ).resolves.toBeTruthy(); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeTruthy(); + await expect(page.getElementsCount(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBe(1); + await page.keys('Escape'); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + await page.keys('Tab'); + await expect( + page.isFocused(wrapper.findDrawerTriggerById(mobileDrawerTriggerIds[1]).toSelector()) + ).resolves.toBeTruthy(); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeTruthy(); + await expect(page.getElementsCount(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBe(1); + await page.keys('Escape'); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + await page.keys(['Shift', 'Tab', 'Shift']); + await expect( + page.isFocused(wrapper.findDrawerTriggerById(mobileDrawerTriggerIds[0]).toSelector()) + ).resolves.toBeTruthy(); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeTruthy(); + await expect(page.getElementsCount(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBe(1); + await page.keys('Enter'); + await expect(page.isExisting(`.${visualRefreshStyles['trigger-tooltip']}`)).resolves.toBeFalsy(); + }) + ); + }); +}); diff --git a/src/app-layout/__tests__/drawers.test.tsx b/src/app-layout/__tests__/drawers.test.tsx index 9be6473929b..0862c0e9118 100644 --- a/src/app-layout/__tests__/drawers.test.tsx +++ b/src/app-layout/__tests__/drawers.test.tsx @@ -2,6 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 /* eslint simple-import-sort/imports: 0 */ import React from 'react'; +import { act, render, waitFor, fireEvent } from '@testing-library/react'; +import { KeyCode } from '@cloudscape-design/test-utils-core/utils.js'; +import createWrapper from '../../../lib/components/test-utils/dom'; + import { describeEachAppLayout, renderComponent, @@ -11,9 +15,9 @@ import { findActiveDrawerLandmark, } from './utils'; -import { act } from '@testing-library/react'; import AppLayout, { AppLayoutProps } from '../../../lib/components/app-layout'; -import visualRefreshStyles from '../../../lib/components/app-layout/visual-refresh/styles.css.js'; +import tooltipStyles from '../../../lib/components/internal/components/tooltip/styles.selectors.js'; +import visualRefreshStyles from '../../../lib/components/app-layout/visual-refresh/styles.selectors.js'; import toolbarTriggerButtonStyles from '../../../lib/components/app-layout/visual-refresh-toolbar/toolbar/trigger-button/styles.css.js'; jest.mock('../../../lib/components/internal/hooks/use-mobile', () => ({ @@ -22,6 +26,12 @@ jest.mock('../../../lib/components/internal/hooks/use-mobile', () => ({ const testIf = (condition: boolean) => (condition ? test : test.skip); +const mockEventBubble = { + bubbles: true, + isTrusted: true, + relatedTarget: null, +}; + jest.mock('@cloudscape-design/component-toolkit', () => ({ ...jest.requireActual('@cloudscape-design/component-toolkit'), useContainerQuery: () => [100, () => {}], @@ -33,6 +43,7 @@ describeEachAppLayout(({ size, theme }) => { expect(wrapper.findDrawersTriggers()).toHaveLength(1); rerender(); expect(wrapper.findDrawersTriggers()).toHaveLength(0); + expect(wrapper!.findByClassName(tooltipStyles.root)).toBeNull(); }); test('should not apply drawers treatment to the tools if the drawers array is empty', () => { @@ -184,4 +195,120 @@ describeEachAppLayout(({ size, theme }) => { drawerTrigger.click(); expect(drawerTrigger!.getElement()).not.toHaveClass(selectedClass); }); + + testIf(theme === 'refresh')('tooltip renders correctly on focus, blur, and escape key press events', async () => { + const mockDrawers = [testDrawer]; + const result = render(); + const wrapper = createWrapper(result.container); + expect(() => result.getByTestId(testDrawer.ariaLabels.drawerName)).toThrow(); + + const triggerButtonContainer = wrapper.findByClassName( + visualRefreshStyles[`drawers-${size === 'mobile' ? 'mobile' : 'desktop'}-triggers-container`] + ); + expect(triggerButtonContainer).not.toBeNull(); + const items = triggerButtonContainer?.findAllByClassName(visualRefreshStyles['trigger-wrapper']); + expect(items?.length).toEqual(mockDrawers.length); + + fireEvent.focus(items![0].getElement()); + + await waitFor(() => { + const tooltipWrapper = result.getByTestId(testDrawer.ariaLabels.drawerName); + expect(tooltipWrapper.classList.contains(tooltipStyles.root)).toBeTruthy(); + expect(tooltipWrapper.classList.contains(visualRefreshStyles['trigger-tooltip'])).toBeTruthy(); + expect(result.getByText(testDrawer.ariaLabels.drawerName)).toBeTruthy(); + }); + + fireEvent.blur(items![0].getElement()); + expect(() => result.getByTestId(testDrawer.ariaLabels.drawerName)).toThrow(); + + fireEvent.focus(items![0].getElement()); + + await waitFor(() => { + const tooltipWrapper = result.getByTestId(testDrawer.ariaLabels.drawerName); + expect(tooltipWrapper.classList.contains(tooltipStyles.root)).toBeTruthy(); + expect(tooltipWrapper.classList.contains(visualRefreshStyles['trigger-tooltip'])).toBeTruthy(); + expect(result.getByText(testDrawer.ariaLabels.drawerName)).toBeTruthy(); + }); + + fireEvent.keyDown(items![0].getElement(), { + ...mockEventBubble, + key: 'Escape', + code: KeyCode.escape, + }); + expect(() => result.getByTestId(testDrawer.ariaLabels.drawerName)).toThrow(); + }); + + testIf(theme === 'refresh')( + 'tooltip renders correctly on pointer events and is removed on escape key press', + async () => { + const mockDrawers = [testDrawer]; + const result = render(); + const wrapper = createWrapper(result.container); + expect(() => result.getByTestId(testDrawer.ariaLabels.drawerName)).toThrow(); + + const triggerButtonContainer = wrapper.findByClassName( + visualRefreshStyles[`drawers-${size === 'mobile' ? 'mobile' : 'desktop'}-triggers-container`] + ); + expect(triggerButtonContainer).not.toBeNull(); + const items = triggerButtonContainer?.findAllByClassName(visualRefreshStyles['trigger-wrapper']); + expect(items?.length).toEqual(mockDrawers.length); + + fireEvent.pointerEnter(items![0].getElement()); + + await waitFor(() => { + const tooltipWrapper = result.getByTestId(testDrawer.ariaLabels.drawerName); + expect(tooltipWrapper.classList.contains(tooltipStyles.root)).toBeTruthy(); + expect(tooltipWrapper.classList.contains(visualRefreshStyles['trigger-tooltip'])).toBeTruthy(); + expect(result.getByText(testDrawer.ariaLabels.drawerName)).toBeTruthy(); + }); + + fireEvent.pointerLeave(items![0].getElement()); + expect(() => result.getByTestId(testDrawer.ariaLabels.drawerName)).toThrow(); + + fireEvent.pointerEnter(items![0].getElement()); + + await waitFor(() => { + const tooltipWrapper = result.getByTestId(testDrawer.ariaLabels.drawerName); + expect(tooltipWrapper.classList.contains(tooltipStyles.root)).toBeTruthy(); + expect(tooltipWrapper.classList.contains(visualRefreshStyles['trigger-tooltip'])).toBeTruthy(); + expect(result.getByText(testDrawer.ariaLabels.drawerName)).toBeTruthy(); + }); + + fireEvent.keyDown(items![0].getElement(), { + ...mockEventBubble, + key: 'Escape', + code: KeyCode.escape, + }); + expect(() => result.getByTestId(testDrawer.ariaLabels.drawerName)).toThrow(); + } + ); + + testIf(theme === 'refresh')('tooltip does not render on trigger focus via close button', async () => { + const mockDrawers = [testDrawer]; + const result = render(); + const wrapper = createWrapper(result.container); + expect(() => result.getByTestId(testDrawer.ariaLabels.drawerName)).toThrow(); + + const triggerButtonContainer = wrapper.findByClassName( + visualRefreshStyles[`drawers-${size === 'mobile' ? 'mobile' : 'desktop'}-triggers-container`] + ); + expect(triggerButtonContainer).not.toBeNull(); + const drawerTrigger = triggerButtonContainer!.find( + `button[data-testid="awsui-app-layout-trigger-${testDrawer.id}"]` + ); + drawerTrigger?.click(); + + await waitFor(() => { + expect(result.getByText('Security')).toBeTruthy(); + expect(() => result.getByTestId(testDrawer.ariaLabels.drawerName)).toThrow(); + }); + + const closeButton = wrapper.findDrawer()?.find(`button[title="${testDrawer.ariaLabels.drawerName}-close-button"]`); + expect(closeButton).not.toBeNull(); + closeButton?.click(); + + await waitFor(() => { + expect(() => result.getByTestId(testDrawer.ariaLabels.drawerName)).toThrow(); + }); + }); }); diff --git a/src/app-layout/__tests__/trigger-button.test.tsx b/src/app-layout/__tests__/trigger-button.test.tsx index ad8a50d05c8..92300922655 100644 --- a/src/app-layout/__tests__/trigger-button.test.tsx +++ b/src/app-layout/__tests__/trigger-button.test.tsx @@ -1,7 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import { render } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; + +import { KeyCode } from '@cloudscape-design/test-utils-core/dist/utils.js'; import * as AllContext from '../../../lib/components/app-layout/visual-refresh/context.js'; import VisualRefreshTriggerButton, { @@ -21,12 +23,16 @@ const MockUseAppLayoutInternals = jest.spyOn(AllContext, 'useAppLayoutInternals' const mockDrawerId = 'mock-drawer-id'; const mockTestId = `awsui-app-layout-trigger-${mockDrawerId}`; +const mockTooltipText = 'Mock Tooltip'; const mockProps = { ariaLabel: 'Aria label', className: 'class-from-props', iconName: 'bug', testId: mockTestId, + tooltipText: mockTooltipText, + hasTooltip: false, + isForPreviousActiveDrawer: false, }; const mockOtherEl = { class: 'other-el-class', @@ -57,7 +63,7 @@ const testIf = (condition: boolean) => (condition ? test : test.skip); const renderVisualRefreshTriggerButton = ( props: Partial = {}, useAppLayoutInternalsValues: Partial = {}, - ref: React.Ref + ref: React.Ref = null ) => { MockUseAppLayoutInternals.mockReturnValue({ ...mockUseLayoutInternalValues, @@ -76,7 +82,7 @@ const renderVisualRefreshTriggerButton = ( const renderVisualRefreshToolbarTriggerButton = ( props: Partial = {}, - ref: React.Ref + ref: React.Ref = null ) => { const renderProps = { ...mockProps, ...props }; const { container, rerender, getByTestId, getByText } = render( @@ -95,7 +101,6 @@ describe('Visual refresh trigger-button (not in appLayoutWidget toolbar)', () => describe.each([true, false])('Toolbar trigger-button with isMobile=%s', isMobile => { describe.each([true, false])('AppLayoutInternals with hasOpenDrawer=%s', hasOpenDrawer => { testIf(!isMobile)('applies the correct class when selected', () => { - const ref: React.MutableRefObject = React.createRef(); const { wrapper } = renderVisualRefreshTriggerButton( { selected: true, @@ -103,8 +108,7 @@ describe('Visual refresh trigger-button (not in appLayoutWidget toolbar)', () => { isMobile, hasOpenDrawer, - }, - ref + } ); expect(wrapper).not.toBeNull(); const seletedButton = wrapper.findByClassName(visualRefreshStyles.selected); @@ -112,7 +116,6 @@ describe('Visual refresh trigger-button (not in appLayoutWidget toolbar)', () => }); test('renders correctly with wit badge', () => { - const ref: React.MutableRefObject = React.createRef(); const { wrapper, getByTestId } = renderVisualRefreshTriggerButton( { badge: false, @@ -120,8 +123,7 @@ describe('Visual refresh trigger-button (not in appLayoutWidget toolbar)', () => { isMobile, hasOpenDrawer, - }, - ref + } ); expect(wrapper).not.toBeNull(); @@ -132,8 +134,7 @@ describe('Visual refresh trigger-button (not in appLayoutWidget toolbar)', () => expect(wrapper.findBadge()).toBeNull(); }); - test('renders correctly with aria controls adn aria label', () => { - const ref: React.MutableRefObject = React.createRef(); + test('renders correctly with aria controls and aria label', () => { const mockAriaControls = 'mock-aria-control'; const { wrapper, getByTestId } = renderVisualRefreshTriggerButton( { @@ -142,8 +143,7 @@ describe('Visual refresh trigger-button (not in appLayoutWidget toolbar)', () => { isMobile, hasOpenDrawer, - }, - ref + } ); expect(wrapper).not.toBeNull(); @@ -181,7 +181,6 @@ describe('Visual refresh trigger-button (not in appLayoutWidget toolbar)', () => }); test.each([true, false])('Disables click events when disabled prop is %s', disabledValue => { - const ref: React.MutableRefObject = React.createRef(); const mockClickSpy = jest.fn(); const { wrapper, getByTestId } = renderVisualRefreshTriggerButton( { @@ -191,8 +190,7 @@ describe('Visual refresh trigger-button (not in appLayoutWidget toolbar)', () => { isMobile, hasOpenDrawer, - }, - ref + } ); expect(wrapper).not.toBeNull(); const button = wrapper.find('button')!; @@ -202,7 +200,6 @@ describe('Visual refresh trigger-button (not in appLayoutWidget toolbar)', () => }); test('renders an empty button when no iconName and iconSVG prop', () => { - const ref: React.MutableRefObject = React.createRef(); const { wrapper } = renderVisualRefreshTriggerButton( { iconName: '' as IconProps.Name, @@ -211,8 +208,7 @@ describe('Visual refresh trigger-button (not in appLayoutWidget toolbar)', () => { isMobile, hasOpenDrawer, - }, - ref + } ); expect(wrapper).not.toBeNull(); const button = wrapper.find('button'); @@ -223,15 +219,13 @@ describe('Visual refresh trigger-button (not in appLayoutWidget toolbar)', () => }); test('renders correctly with badge using dot class on desktop', () => { - const ref: React.MutableRefObject = React.createRef(); const { wrapper, getByTestId } = renderVisualRefreshTriggerButton( { badge: true, }, { isMobile: false, - }, - ref + } ); expect(wrapper).not.toBeNull(); @@ -242,22 +236,163 @@ describe('Visual refresh trigger-button (not in appLayoutWidget toolbar)', () => expect(wrapper.findByClassName(visualRefreshStyles.dot)).toBeTruthy(); }); - test.each([true, false] as const)('Is focusable using the forwarded ref with mobile is %s', isMobile => { - const ref: React.MutableRefObject = React.createRef(); - const { wrapper, getByTestId } = renderVisualRefreshTriggerButton( - {}, - { - isMobile, - }, - ref + describe('Shared trigger wrapper events', () => { + test.each([true, false] as const)( + 'Is focusable using the forwarded ref with no tooltip with mobile is %s', + isMobile => { + const ref: React.MutableRefObject = React.createRef(); + const { wrapper, getByTestId, getByText } = renderVisualRefreshTriggerButton( + { + hasTooltip: true, + tooltipText: mockTooltipText, + }, + { + isMobile, + }, + ref + ); + expect(getByTestId(mockTestId)).toBeTruthy(); + const button = wrapper!.find('button'); + expect(getByTestId(mockTestId)).toBeTruthy(); + expect(wrapper!.getElement().classList.contains(visualRefreshStyles['trigger-wrapper-tooltip-visible'])).toBe( + false + ); + expect(wrapper!.findByClassName(visualRefreshStyles['trigger-tooltip'])).toBeNull(); + expect(() => getByText(mockTooltipText)).toThrow(); + expect(button).toBeTruthy(); + expect(document.activeElement).not.toBe(button!.getElement()); + (ref.current as any)?.focus(mockEventBubbleWithShiftFocus); + expect(document.activeElement).toBe(button!.getElement()); + expect(getByTestId(mockTestId)).toBeTruthy(); + expect(wrapper!.findByClassName(visualRefreshStyles['trigger-tooltip'])).toBeNull(); + } ); - expect(getByTestId(mockTestId)).toBeTruthy(); - const button = wrapper!.find('button'); - expect(getByTestId(mockTestId)).toBeTruthy(); - expect(button).toBeTruthy(); - expect(document.activeElement).not.toBe(button!.getElement()); - (ref.current as any)?.focus(mockEventBubbleWithShiftFocus); - expect(document.activeElement).toBe(button!.getElement()); + + test.each([true, false] as const)( + 'Does not show tooltip on pointerEnter when hasTooltip is %s', + async hasTooltip => { + const { wrapper, getByText, getByTestId } = await renderVisualRefreshTriggerButton({ + hasTooltip, + tooltipText: mockTooltipText, + }); + expect(getByTestId(mockTestId)).toBeTruthy(); + expect(wrapper!.getElement().classList.contains(visualRefreshStyles['trigger-wrapper-tooltip-visible'])).toBe( + false + ); + expect(wrapper!.findByClassName(visualRefreshStyles['trigger-tooltip'])).toBeNull(); + expect(() => getByText(mockTooltipText)).toThrow(); + fireEvent.pointerEnter(wrapper!.getElement()); + if (hasTooltip) { + expect(getByText(mockTooltipText)).toBeTruthy(); + expect(wrapper!.getElement().classList.contains(visualRefreshStyles['trigger-wrapper-tooltip-visible'])).toBe( + true + ); + //trigger event again to assert the tooltip remains + fireEvent.pointerDown(wrapper!.getElement()); + expect(wrapper!.getElement().classList.contains(visualRefreshStyles['trigger-wrapper-tooltip-visible'])).toBe( + true + ); + } else { + expect(() => getByText(mockTooltipText)).toThrow(); + expect(wrapper!.getElement().classList.contains(visualRefreshStyles['trigger-wrapper-tooltip-visible'])).toBe( + false + ); + } + fireEvent.pointerLeave(wrapper!.getElement(), mockEventBubble); + expect(wrapper!.getElement().classList.contains(visualRefreshStyles['trigger-wrapper-tooltip-visible'])).toBe( + false + ); + expect(() => getByText(mockTooltipText)).toThrow(); + } + ); + + test('Shows tooltip on focus and removes on key escape when drawer is open on mobile', async () => { + const { wrapper, getByText, getByTestId } = await renderVisualRefreshTriggerButton({ + hasTooltip: true, + tooltipText: mockTooltipText, + }); + expect(getByTestId(mockTestId)).toBeTruthy(); + expect(wrapper!.getElement().classList.contains(visualRefreshStyles['trigger-wrapper-tooltip-visible'])).toBe( + false + ); + expect(wrapper!.findByClassName(visualRefreshStyles['trigger-tooltip'])).toBeNull(); + expect(() => getByText(mockTooltipText)).toThrow(); + fireEvent.focus(wrapper!.getElement()); + expect(getByText(mockTooltipText)).toBeTruthy(); + expect(wrapper!.getElement().classList.contains(visualRefreshStyles['trigger-wrapper-tooltip-visible'])).toBe( + true + ); + + fireEvent.keyDown(wrapper!.getElement(), { + ...mockEventBubble, + key: 'Escape', + code: KeyCode.escape, + }); + + expect(wrapper.findByClassName(visualRefreshStyles['trigger-tooltip'])).toBeNull(); + expect(wrapper!.getElement().classList.contains(visualRefreshStyles['trigger-wrapper-tooltip-visible'])).toBe( + false + ); + expect(() => getByText(mockTooltipText)).toThrow(); + }); + + test('Does not show tooltip on pointerEnter when there is no arialLabel nor tooltipText', async () => { + const { wrapper, getByTestId } = await renderVisualRefreshTriggerButton({ + ariaLabel: '', + tooltipText: '', + }); + expect(getByTestId(mockTestId)).toBeTruthy(); + expect(wrapper!.getElement().classList.contains(visualRefreshStyles['trigger-wrapper-tooltip-visible'])).toBe( + false + ); + expect(wrapper!.findByClassName(visualRefreshStyles['trigger-tooltip'])).toBeNull(); + + fireEvent.pointerEnter(wrapper!.getElement()); + expect(wrapper!.getElement().classList.contains(visualRefreshStyles['trigger-wrapper-tooltip-visible'])).toBe( + false + ); + expect(wrapper.findByClassName(visualRefreshStyles['trigger-tooltip'])).toBeNull(); + }); + + test('Does not show tooltip on focus when no ariaLabel nor tooltipText', async () => { + const { wrapper, getByTestId } = await renderVisualRefreshTriggerButton({ + ariaLabel: '', + tooltipText: '', + }); + expect(getByTestId(mockTestId)).toBeTruthy(); + expect(wrapper!.getElement().classList.contains(visualRefreshStyles['trigger-wrapper-tooltip-visible'])).toBe( + false + ); + expect(wrapper!.findByClassName(visualRefreshStyles['trigger-tooltip'])).toBeNull(); + fireEvent.focus(wrapper!.getElement()); + expect(wrapper!.getElement().classList.contains(visualRefreshStyles['trigger-wrapper-tooltip-visible'])).toBe( + false + ); + expect(wrapper.findByClassName(visualRefreshStyles['trigger-tooltip'])).toBeNull(); + }); + + test('Does not show tooltip if mobile and hasOpenDrawer', async () => { + const { wrapper, getByTestId } = await renderVisualRefreshTriggerButton( + { + hasTooltip: true, + tooltipText: mockTooltipText, + }, + { + isMobile: true, + hasOpenDrawer: true, + } + ); + expect(getByTestId(mockTestId)).toBeTruthy(); + expect(wrapper!.getElement().classList.contains(visualRefreshStyles['trigger-wrapper-tooltip-visible'])).toBe( + false + ); + expect(wrapper!.findByClassName(visualRefreshStyles['trigger-tooltip'])).toBeNull(); + fireEvent.focus(wrapper!.getElement()); + expect(wrapper!.getElement().classList.contains(visualRefreshStyles['trigger-wrapper-tooltip-visible'])).toBe( + false + ); + expect(wrapper.findByClassName(visualRefreshStyles['trigger-tooltip'])).toBeNull(); + }); }); }); @@ -265,13 +400,9 @@ describe('Visual Refresh Toolbar trigger-button', () => { beforeEach(() => jest.clearAllMocks()); test('renders correctly with badge using dot class', () => { - const ref: React.MutableRefObject = React.createRef(); - const { wrapper, getByTestId } = renderVisualRefreshToolbarTriggerButton( - { - badge: true, - }, - ref - ); + const { wrapper, getByTestId } = renderVisualRefreshToolbarTriggerButton({ + badge: true, + }); expect(wrapper).not.toBeNull(); const button = wrapper.find('button'); @@ -282,13 +413,9 @@ describe('Visual Refresh Toolbar trigger-button', () => { }); test('applies the correct class when selected', () => { - const ref: React.MutableRefObject = React.createRef(); - const { wrapper } = renderVisualRefreshToolbarTriggerButton( - { - selected: true, - }, - ref - ); + const { wrapper } = renderVisualRefreshToolbarTriggerButton({ + selected: true, + }); expect(wrapper).not.toBeNull(); const seletedButton = wrapper.findByClassName(toolbarTriggerButtonStyles.selected); expect(seletedButton).toBeTruthy(); @@ -317,15 +444,11 @@ describe('Visual Refresh Toolbar trigger-button', () => { }); test.each([true, false])('click events work as expected when disabled is %s', disabledValue => { - const ref: React.MutableRefObject = React.createRef(); const mockClickSpy = jest.fn(); - const { wrapper, getByTestId } = renderVisualRefreshToolbarTriggerButton( - { - disabled: disabledValue, - onClick: mockClickSpy, - }, - ref - ); + const { wrapper, getByTestId } = renderVisualRefreshToolbarTriggerButton({ + disabled: disabledValue, + onClick: mockClickSpy, + }); expect(wrapper).not.toBeNull(); const button = wrapper.find('button')!; expect(getByTestId(mockTestId)).toBeTruthy(); @@ -334,13 +457,9 @@ describe('Visual Refresh Toolbar trigger-button', () => { }); test('renders an empty button when no iconName and iconSVG prop', () => { - const ref: React.MutableRefObject = React.createRef(); - const { wrapper } = renderVisualRefreshToolbarTriggerButton( - { - iconName: '' as IconProps.Name, - }, - ref - ); + const { wrapper } = renderVisualRefreshToolbarTriggerButton({ + iconName: '' as IconProps.Name, + }); expect(wrapper).not.toBeNull(); const button = wrapper.find('button'); expect(button).toBeTruthy(); diff --git a/src/app-layout/visual-refresh/drawers.tsx b/src/app-layout/visual-refresh/drawers.tsx index 2a40686a551..9bbfbc6ca78 100644 --- a/src/app-layout/visual-refresh/drawers.tsx +++ b/src/app-layout/visual-refresh/drawers.tsx @@ -139,6 +139,7 @@ function ActiveDrawer() { }} ref={drawersRefs.close} variant="icon" + data-shift-focus="last-opened-toolbar-trigger-button" /> {toolsContent && ( @@ -244,6 +245,7 @@ function DesktopTriggers() { aria-orientation="vertical" > {visibleItems.map(item => { + const isForPreviousActiveDrawer = previousActiveDrawerId?.current === item.id; return ( handleDrawersClick(item.id)} - ref={item.id === previousActiveDrawerId.current ? drawersRefs.toggle : undefined} + ref={isForPreviousActiveDrawer ? drawersRefs.toggle : undefined} badge={item.badge} testId={`awsui-app-layout-trigger-${item.id}`} highContrastHeader={headerVariant === 'high-contrast'} selected={item.id === activeDrawerId} + hasTooltip={true} + tooltipText={item.ariaLabels?.drawerName} + isForPreviousActiveDrawer={isForPreviousActiveDrawer} /> ); })} @@ -348,29 +353,35 @@ export function MobileTriggers() { role="region" >
- {visibleItems.map(item => ( - handleDrawersClick(item.id)} - testId={`awsui-app-layout-trigger-${item.id}`} - highContrastHeader={headerVariant === 'high-contrast'} - selected={item.id === activeDrawerId} - /> - ))} + {visibleItems.map(item => { + const isForPreviousActiveDrawer = previousActiveDrawerId?.current === item.id; + return ( + handleDrawersClick(item.id)} + testId={`awsui-app-layout-trigger-${item.id}`} + highContrastHeader={headerVariant === 'high-contrast'} + selected={item.id === activeDrawerId} + hasTooltip={true} + tooltipText={item.ariaLabels?.drawerName} + isForPreviousActiveDrawer={isForPreviousActiveDrawer} + /> + ); + })} {overflowItems.length > 0 && ( void; badge?: boolean; highContrastHeader?: boolean; + + /** + * If the button is expected to have a tooltip. When false it will not set the event listeners + * + * defaults to false + */ + hasTooltip?: boolean; + /** + * This text allows for a customized tooltip. + * + * When falsy, the tooltip will parse the tooltip form the aria-lable + */ + tooltipText?: string; + /** + * set to true if the trigger button was used to open the last active drawer + */ + isForPreviousActiveDrawer?: boolean; } function TriggerButton( @@ -46,13 +64,119 @@ function TriggerButton( badge, selected = false, highContrastHeader, + hasTooltip = false, + tooltipText = '', + isForPreviousActiveDrawer = false, }: TriggerButtonProps, ref: React.Ref ) { - const { isMobile } = useAppLayoutInternals(); + const containerRef = React.useRef(null); + const tooltipValue = tooltipText ? tooltipText : ariaLabel ? ariaLabel : ''; + const [showTooltip, setShowTooltip] = useState(false); + const { hasOpenDrawer, isMobile } = useAppLayoutInternals(); + + const tooltipVisible = + containerRef && containerRef?.current && tooltipValue && !(isMobile && hasOpenDrawer) && showTooltip; + + const onShowTooltipSoft = (show: boolean) => { + setShowTooltip(show); + }; + + const onShowTooltipHard = (show: boolean) => { + setShowTooltip(show); + }; + + /** + * Takes the drawer being closed and the data-shift-focus value from a close button on that drawer that persists + * on the event relatedTarget to determine not to show the tooltip + * @param event + */ + const handleFocus = useCallback( + (event: KeyboardEvent | PointerEvent) => { + // Create a more descriptive variable name for the event object + const eventWithRelatedTarget = event as any; + + // Extract the condition for showing the tooltip hard into a separate function + const shouldShowTooltipHard = () => { + return eventWithRelatedTarget?.relatedTarget?.dataset?.shiftFocus !== 'last-opened-toolbar-trigger-button'; + }; + + // Extract the condition for mobile devices and open drawers into a separate function + const isMobileWithOpenDrawerCondition = () => { + return isMobile && (!hasOpenDrawer || isForPreviousActiveDrawer); + }; + // Handle the logic based on the extracted conditions + if (isMobileWithOpenDrawerCondition()) { + if (shouldShowTooltipHard()) { + onShowTooltipHard(true); + } else { + // This removes any tooltip that is already showing + onShowTooltipHard(false); + } + } else if (shouldShowTooltipHard()) { + onShowTooltipHard(true); + } else { + // This removes any tooltip that is already showing + onShowTooltipHard(false); + } + }, + [ + // To assert reference equality check + isMobile, + hasOpenDrawer, + isForPreviousActiveDrawer, + ] + ); + + useEffect(() => { + if (hasTooltip && tooltipValue) { + const close = () => { + setShowTooltip(false); + }; + + const shouldCloseTooltip = (event: PointerEvent) => { + if (event.target && containerRef && (containerRef.current as any)?.contains(event.target as HTMLElement)) { + return false; + } + return true; + }; + + const handlePointerDownEvent = (event: PointerEvent) => { + if (shouldCloseTooltip(event)) { + close(); + } + }; + + const handleKeyDownEvent = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + close(); + } + }; + + window.addEventListener('pointerdown', handlePointerDownEvent); + window.addEventListener('keydown', handleKeyDownEvent); + // console.log('listeners added', {hasTooltip, tooltipVisible}) + return () => { + window.removeEventListener('pointerdown', handlePointerDownEvent); + window.removeEventListener('keydown', handleKeyDownEvent); + }; + } + }, [containerRef, hasTooltip, tooltipValue]); return ( -
+
onShowTooltipSoft(true), + onPointerLeave: () => onShowTooltipSoft(false), + onFocus: e => handleFocus(e as any), + onBlur: () => onShowTooltipHard(false), + })} + className={clsx(styles['trigger-wrapper'], { + [styles['remove-high-contrast-header']]: !highContrastHeader, + [styles['trigger-wrapper-tooltip-visible']]: tooltipVisible, + })} + > {isMobile ? ( } )} + {tooltipVisible && ( + + )}
); }