From f43ebe2a72d0b05fe06c314c225069f3cfe0e56d Mon Sep 17 00:00:00 2001 From: Austin O'Neil Date: Sun, 31 Mar 2024 18:24:51 -0700 Subject: [PATCH] focus tooltipped item when tooltip is active, allow tooltip to be dismissed with escape --- .../modus-tooltip/modus-tooltip.e2e.ts | 77 +++++++++++++++++++ .../modus-tooltip/modus-tooltip.tsx | 26 ++++++- 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/stencil-workspace/src/components/modus-tooltip/modus-tooltip.e2e.ts b/stencil-workspace/src/components/modus-tooltip/modus-tooltip.e2e.ts index 8d54e3765..145eb31ab 100644 --- a/stencil-workspace/src/components/modus-tooltip/modus-tooltip.e2e.ts +++ b/stencil-workspace/src/components/modus-tooltip/modus-tooltip.e2e.ts @@ -78,4 +78,81 @@ describe('modus-tooltip', () => { await new Promise((r) => setTimeout(r, 500)); expect(tooltip.getAttribute('data-show')).toBeNull(); }); + + it('preserves tabindex on hide', async () => { + const page = await newE2EPage(); + + await page.setContent(` + + Button + + Other button + `); + + const tooltip = await page.find('modus-tooltip'); + const button = await tooltip.find('#tooltip-button'); + await page.hover('#tooltip-button'); + await page.waitForChanges(); + expect(button.getAttribute('tabindex')).toEqual('3'); + + await page.hover('#not-tooltip-button'); + await page.waitForChanges(); + expect(button.getAttribute('tabindex')).toEqual('3'); + }); + + it('preserves lack of tabindex on hide', async () => { + const page = await newE2EPage(); + + await page.setContent(` + + Button + + Other button + `); + + const tooltip = await page.find('modus-tooltip'); + const button = await tooltip.find('#tooltip-button'); + await page.hover('#tooltip-button'); + await page.waitForChanges(); + expect(button.getAttribute('tabindex')).toEqual('-1'); + + await page.hover('#not-tooltip-button'); + await page.waitForChanges(); + expect(button.getAttribute('tabindex')).toBeNull(); + }); + + it('focuses the tooltipped element on show', async () => { + const page = await newE2EPage(); + + await page.setContent(` + + Button + + `); + + await page.hover('#tooltip-button'); + await page.waitForChanges(); + const activeElementId = await page.evaluate(() => document.activeElement!.id); + expect(activeElementId).toEqual('tooltip-button'); + }); + + it('hides the tooltip on "escape" key', async () => { + const page = await newE2EPage(); + + await page.setContent(` + + Button + + `); + + const tooltip = await page.find('modus-tooltip >>> .tooltip'); + await page.hover('#tooltip-button'); + + await page.waitForChanges(); + expect(tooltip).toHaveAttribute('data-show'); + + page.keyboard.press('Escape'); + await page.waitForChanges(); + expect(tooltip).not.toHaveAttribute('data-show'); + }); }); diff --git a/stencil-workspace/src/components/modus-tooltip/modus-tooltip.tsx b/stencil-workspace/src/components/modus-tooltip/modus-tooltip.tsx index bdcebbe6e..60561e23c 100644 --- a/stencil-workspace/src/components/modus-tooltip/modus-tooltip.tsx +++ b/stencil-workspace/src/components/modus-tooltip/modus-tooltip.tsx @@ -1,5 +1,5 @@ // eslint-disable-next-line -import { Component, Element, Fragment, h, Prop, Watch } from '@stencil/core'; +import { Component, Element, h, Prop, Watch, Fragment } from '@stencil/core'; import { createPopper, Instance } from '@popperjs/core'; import { ModusToolTipPlacement } from './modus-tooltip.models'; @@ -47,6 +47,7 @@ export class ModusTooltip { } private popperInstance: Instance; + private targetTabIndex?: string; // necessary for preserving tab index after blur private tooltipElement: HTMLDivElement; private readonly showEvents = ['mouseenter', 'mouseover', 'focus']; private readonly hideEvents = ['mouseleave', 'blur', 'click']; @@ -72,6 +73,8 @@ export class ModusTooltip { const target = this.element.firstElementChild; if (!target || !this.tooltipElement) return; + this.targetTabIndex = target.getAttribute('tabindex'); + this.popperInstance = createPopper(target, this.tooltipElement, { placement: position, modifiers: [ @@ -91,6 +94,8 @@ export class ModusTooltip { this.hideEvents.forEach((event) => { target.addEventListener(event, this.hideEventsListener); }); + + target.addEventListener('keydown', this.escapeKeyHandler); } cleanupPopper(): void { @@ -109,7 +114,17 @@ export class ModusTooltip { this.popperInstance = null; } + escapeKeyHandler = (event: KeyboardEvent) => { + if (event.code === 'Escape') { + this.hide(); + } + }; + show(): void { + const target = this.element.firstElementChild as HTMLElement; + const tabIndex = this.targetTabIndex !== '' && this.targetTabIndex != null ? (this.targetTabIndex as string) : '-1'; + target.setAttribute('tabindex', tabIndex); + target.focus(); if (this.popperInstance) { // Make the tooltip visible this.tooltipElement.setAttribute('data-show', ''); @@ -135,6 +150,15 @@ export class ModusTooltip { ...options, modifiers: [...options.modifiers, { name: 'eventListeners', enabled: false }], })); + + const target = this.element.firstElementChild as HTMLElement; + + if (this.targetTabIndex == null) { + target.removeAttribute('tabindex'); + } else { + target.setAttribute('tabindex', this.targetTabIndex); + } + target.blur(); } }