From c927a5d05761d0a80f886b2b7627e600df38c467 Mon Sep 17 00:00:00 2001 From: Material Web Team Date: Mon, 23 Nov 2020 11:39:59 -0800 Subject: [PATCH] feat(tooltip): Set up rich tooltip to persist if mouse leaves anchor and enters rich tooltip. BREAKING CHANGE: Added adapter methods: - setAnchorAttribute(attr: string, value: string): void; - registerEventHandler( evtType: K, handler: SpecificEventListener): void; - deregisterEventHandler( evtType: K, handler: SpecificEventListener): void; Rich tooltips are currently in development and is not yet ready for use. PiperOrigin-RevId: 343894231 --- packages/mdc-tooltip/adapter.ts | 17 + packages/mdc-tooltip/component.ts | 13 + packages/mdc-tooltip/constants.ts | 1 + packages/mdc-tooltip/foundation.ts | 36 +- packages/mdc-tooltip/test/component.test.ts | 392 +++++++++++-------- packages/mdc-tooltip/test/foundation.test.ts | 82 ++++ 6 files changed, 377 insertions(+), 164 deletions(-) diff --git a/packages/mdc-tooltip/adapter.ts b/packages/mdc-tooltip/adapter.ts index dcd8ba57014..afff934c2a6 100644 --- a/packages/mdc-tooltip/adapter.ts +++ b/packages/mdc-tooltip/adapter.ts @@ -88,11 +88,28 @@ export interface MDCTooltipAdapter { */ getAnchorAttribute(attr: string): string|null; + /** + * Sets an attribute on the anchor element. + */ + setAnchorAttribute(attr: string, value: string): void; + /** * @return true if the text direction is right-to-left. */ isRTL(): boolean; + /** + * Registers an event listener to the root element. + */ + registerEventHandler( + evtType: K, handler: SpecificEventListener): void; + + /** + * Deregisters an event listener to the root element. + */ + deregisterEventHandler( + evtType: K, handler: SpecificEventListener): void; + /** * Registers an event listener to the document body. */ diff --git a/packages/mdc-tooltip/component.ts b/packages/mdc-tooltip/component.ts index 692804463c6..194c98df005 100644 --- a/packages/mdc-tooltip/component.ts +++ b/packages/mdc-tooltip/component.ts @@ -134,7 +134,20 @@ export class MDCTooltip extends MDCComponent { getAnchorAttribute: (attr) => { return this.anchorElem ? this.anchorElem.getAttribute(attr) : null; }, + setAnchorAttribute: (attr, value) => { + this.anchorElem?.setAttribute(attr, value); + }, isRTL: () => getComputedStyle(this.root).direction === 'rtl', + registerEventHandler: (evt, handler) => { + if (this.root instanceof HTMLElement) { + this.root.addEventListener(evt, handler); + } + }, + deregisterEventHandler: (evt, handler) => { + if (this.root instanceof HTMLElement) { + this.root.removeEventListener(evt, handler); + } + }, registerDocumentEventHandler: (evt, handler) => { document.body.addEventListener(evt, handler); }, diff --git a/packages/mdc-tooltip/constants.ts b/packages/mdc-tooltip/constants.ts index 7cc28c92f53..10f9f79814c 100644 --- a/packages/mdc-tooltip/constants.ts +++ b/packages/mdc-tooltip/constants.ts @@ -22,6 +22,7 @@ */ enum CssClasses { + RICH = 'mdc-tooltip--rich', SHOWN = 'mdc-tooltip--shown', SHOWING = 'mdc-tooltip--showing', SHOWING_TRANSITION = 'mdc-tooltip--showing-transition', diff --git a/packages/mdc-tooltip/foundation.ts b/packages/mdc-tooltip/foundation.ts index 2736b86e842..7df60ceac01 100644 --- a/packages/mdc-tooltip/foundation.ts +++ b/packages/mdc-tooltip/foundation.ts @@ -30,6 +30,7 @@ import {AnchorBoundaryType, CssClasses, numbers, XPosition, YPosition} from './c import {ShowTooltipOptions} from './types'; const { + RICH, SHOWN, SHOWING, SHOWING_TRANSITION, @@ -57,7 +58,10 @@ export class MDCTooltipFoundation extends MDCFoundation { getAnchorBoundingRect: () => ({top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0}), getAnchorAttribute: () => null, + setAnchorAttribute: () => null, isRTL: () => false, + registerEventHandler: () => undefined, + deregisterEventHandler: () => undefined, registerDocumentEventHandler: () => undefined, deregisterDocumentEventHandler: () => undefined, registerWindowEventHandler: () => undefined, @@ -66,6 +70,7 @@ export class MDCTooltipFoundation extends MDCFoundation { }; } + private isRich!: boolean; // assigned in init() private isShown = false; private anchorGap = numbers.BOUNDED_ANCHOR_GAP; private xTooltipPos = XPosition.DETECTED; @@ -83,6 +88,8 @@ export class MDCTooltipFoundation extends MDCFoundation { private readonly animFrame: AnimationFrame; private readonly documentClickHandler: SpecificEventListener<'click'>; private readonly documentKeydownHandler: SpecificEventListener<'keydown'>; + private readonly richTooltipMouseEnterHandler: + SpecificEventListener<'mouseenter'>; private readonly windowScrollHandler: SpecificEventListener<'scroll'>; private readonly windowResizeHandler: SpecificEventListener<'resize'>; @@ -98,6 +105,10 @@ export class MDCTooltipFoundation extends MDCFoundation { this.handleKeydown(evt); }; + this.richTooltipMouseEnterHandler = () => { + this.handleRichTooltipMouseEnter(); + }; + this.windowScrollHandler = () => { this.handleWindowChangeEvent(); }; @@ -107,6 +118,10 @@ export class MDCTooltipFoundation extends MDCFoundation { }; } + init() { + this.isRich = this.adapter.hasClass(RICH); + } + handleAnchorMouseEnter() { if (this.isShown) { // Covers the instance where a user hovers over the anchor to reveal the @@ -155,6 +170,10 @@ export class MDCTooltipFoundation extends MDCFoundation { } } + private handleRichTooltipMouseEnter() { + this.show(); + } + /** * On window resize or scroll, check the anchor position and size and * repostion tooltip if necessary. @@ -181,9 +200,14 @@ export class MDCTooltipFoundation extends MDCFoundation { if (!showTooltipOptions.hideFromScreenreader) { this.adapter.setAttribute('aria-hidden', 'false'); } + if (this.isRich) { + this.adapter.setAnchorAttribute('aria-expanded', 'true'); + this.adapter.registerEventHandler( + 'mouseenter', this.richTooltipMouseEnterHandler); + } this.adapter.removeClass(HIDE); this.adapter.addClass(SHOWING); - if (this.isTooltipMultiline()) { + if (this.isTooltipMultiline() && !this.isRich) { this.adapter.addClass(MULTILINE_TOOLTIP); } this.anchorRect = this.adapter.getAnchorBoundingRect(); @@ -218,6 +242,11 @@ export class MDCTooltipFoundation extends MDCFoundation { this.isShown = false; this.adapter.setAttribute('aria-hidden', 'true'); + if (this.isRich) { + this.adapter.setAnchorAttribute('aria-expanded', 'false'); + this.adapter.deregisterEventHandler( + 'mouseenter', this.richTooltipMouseEnterHandler); + } this.clearAllAnimationClasses(); this.adapter.addClass(HIDE); this.adapter.addClass(HIDE_TRANSITION); @@ -543,6 +572,11 @@ export class MDCTooltipFoundation extends MDCFoundation { this.adapter.removeClass(HIDE); this.adapter.removeClass(HIDE_TRANSITION); + if (this.isRich) { + this.adapter.deregisterEventHandler( + 'mouseenter', this.richTooltipMouseEnterHandler); + } + this.adapter.deregisterDocumentEventHandler( 'click', this.documentClickHandler); this.adapter.deregisterDocumentEventHandler( diff --git a/packages/mdc-tooltip/test/component.test.ts b/packages/mdc-tooltip/test/component.test.ts index 0c17c58796a..e0e3ff312ae 100644 --- a/packages/mdc-tooltip/test/component.test.ts +++ b/packages/mdc-tooltip/test/component.test.ts @@ -39,9 +39,9 @@ function setupTestWithMockFoundation(fixture: HTMLElement) { describe('MDCTooltip', () => { let fixture: HTMLElement; setUpMdcTestEnvironment(); - - beforeEach(() => { - fixture = getFixture(`
+ describe('plain tooltip tests', () => { + beforeEach(() => { + fixture = getFixture(`
@@ -51,148 +51,156 @@ describe('MDCTooltip', () => {
`); - document.body.appendChild(fixture); - }); + document.body.appendChild(fixture); + }); - afterEach(() => { - document.body.removeChild(fixture); - }); + afterEach(() => { + document.body.removeChild(fixture); + }); - it('attachTo returns a component instance', () => { - expect(MDCTooltip.attachTo( - fixture.querySelector('.mdc-tooltip') as HTMLElement)) - .toEqual(jasmine.any(MDCTooltip)); - }); + it('attachTo returns a component instance', () => { + expect(MDCTooltip.attachTo( + fixture.querySelector('.mdc-tooltip') as HTMLElement)) + .toEqual(jasmine.any(MDCTooltip)); + }); - it('attachTo throws an error when anchor element is missing', () => { - const container = - fixture.querySelector('[aria-describedby]') as HTMLElement; - container.parentElement!.removeChild(container); - expect( - () => MDCTooltip.attachTo( - container.querySelector('.mdc-tooltip') as HTMLElement)) - .toThrow(); - }); + it('attachTo throws an error when anchor element is missing', () => { + const container = + fixture.querySelector('[aria-describedby]') as HTMLElement; + container.parentElement!.removeChild(container); + expect( + () => MDCTooltip.attachTo( + container.querySelector('.mdc-tooltip') as HTMLElement)) + .toThrow(); + }); - it('#initialSyncWithDOM registers mouseenter event handler on the anchor element', - () => { - const {anchorElem, mockFoundation, component} = - setupTestWithMockFoundation(fixture); - emitEvent(anchorElem, 'mouseenter'); - expect(mockFoundation.handleAnchorMouseEnter).toHaveBeenCalled(); - component.destroy(); - }); - - it('#destroy deregisters mouseenter event handler on the anchor element', - () => { - const {anchorElem, mockFoundation, component} = - setupTestWithMockFoundation(fixture); - component.destroy(); - emitEvent(anchorElem, 'mouseenter'); - expect(mockFoundation.handleAnchorMouseEnter).not.toHaveBeenCalled(); - }); - - it('#initialSyncWithDOM registers focus event handler on the anchor element', - () => { - const {anchorElem, mockFoundation, component} = - setupTestWithMockFoundation(fixture); - emitEvent(anchorElem, 'focus'); - expect(mockFoundation.handleAnchorFocus).toHaveBeenCalled(); - component.destroy(); - }); - - it('#destroy deregisters focus event handler on the anchor element', () => { - const {anchorElem, mockFoundation, component} = - setupTestWithMockFoundation(fixture); - component.destroy(); - emitEvent(anchorElem, 'focus'); - expect(mockFoundation.handleAnchorFocus).not.toHaveBeenCalled(); - }); + it('#initialSyncWithDOM registers mouseenter event handler on the anchor element', + () => { + const {anchorElem, mockFoundation, component} = + setupTestWithMockFoundation(fixture); + emitEvent(anchorElem, 'mouseenter'); + expect(mockFoundation.handleAnchorMouseEnter).toHaveBeenCalled(); + component.destroy(); + }); - it('#initialSyncWithDOM registers mouseleave event handler on the anchor element', - () => { - const {anchorElem, mockFoundation, component} = - setupTestWithMockFoundation(fixture); - emitEvent(anchorElem, 'mouseleave'); - expect(mockFoundation.handleAnchorMouseLeave).toHaveBeenCalled(); - component.destroy(); - }); - - it('#destroy deregisters mouseleave event handler on the anchor element', - () => { - const {anchorElem, mockFoundation, component} = - setupTestWithMockFoundation(fixture); - component.destroy(); - emitEvent(anchorElem, 'mouseleave'); - expect(mockFoundation.handleAnchorMouseLeave).not.toHaveBeenCalled(); - }); - - it('#initialSyncWithDOM registers blur event handler on the anchor element', - () => { - const {anchorElem, mockFoundation, component} = - setupTestWithMockFoundation(fixture); - emitEvent(anchorElem, 'blur'); - expect(mockFoundation.handleAnchorBlur).toHaveBeenCalled(); - component.destroy(); - }); - - it('#destroy deregisters blur event handler on the anchor element', () => { - const {anchorElem, mockFoundation, component} = - setupTestWithMockFoundation(fixture); - component.destroy(); - emitEvent(anchorElem, 'blur'); - expect(mockFoundation.handleAnchorBlur).not.toHaveBeenCalled(); - }); + it('#destroy deregisters mouseenter event handler on the anchor element', + () => { + const {anchorElem, mockFoundation, component} = + setupTestWithMockFoundation(fixture); + component.destroy(); + emitEvent(anchorElem, 'mouseenter'); + expect(mockFoundation.handleAnchorMouseEnter).not.toHaveBeenCalled(); + }); - it('#initialSyncWithDOM registers transitionend event handler on the tooltip', - () => { - const {mockFoundation, component} = setupTestWithMockFoundation(fixture); - emitEvent(component.root, 'transitionend'); - expect(mockFoundation.handleTransitionEnd).toHaveBeenCalled(); - component.destroy(); - }); - - it('#destroy deregisters transitionend event handler on the tooltip', () => { - const {mockFoundation, component} = setupTestWithMockFoundation(fixture); - component.destroy(); - emitEvent(component.root, 'transitionend'); - expect(mockFoundation.handleTransitionEnd).not.toHaveBeenCalled(); - }); + it('#initialSyncWithDOM registers focus event handler on the anchor element', + () => { + const {anchorElem, mockFoundation, component} = + setupTestWithMockFoundation(fixture); + emitEvent(anchorElem, 'focus'); + expect(mockFoundation.handleAnchorFocus).toHaveBeenCalled(); + component.destroy(); + }); - it('#setTooltipPosition fowards to MDCFoundation#setTooltipPosition', () => { - const {mockFoundation, component} = setupTestWithMockFoundation(fixture); - component.setTooltipPosition( - {xPos: XPosition.CENTER, yPos: YPosition.ABOVE}); - expect(mockFoundation.setTooltipPosition) - .toHaveBeenCalledWith({xPos: XPosition.CENTER, yPos: YPosition.ABOVE}); - component.destroy(); - }); + it('#destroy deregisters focus event handler on the anchor element', () => { + const {anchorElem, mockFoundation, component} = + setupTestWithMockFoundation(fixture); + component.destroy(); + emitEvent(anchorElem, 'focus'); + expect(mockFoundation.handleAnchorFocus).not.toHaveBeenCalled(); + }); + + it('#initialSyncWithDOM registers mouseleave event handler on the anchor element', + () => { + const {anchorElem, mockFoundation, component} = + setupTestWithMockFoundation(fixture); + emitEvent(anchorElem, 'mouseleave'); + expect(mockFoundation.handleAnchorMouseLeave).toHaveBeenCalled(); + component.destroy(); + }); + + it('#destroy deregisters mouseleave event handler on the anchor element', + () => { + const {anchorElem, mockFoundation, component} = + setupTestWithMockFoundation(fixture); + component.destroy(); + emitEvent(anchorElem, 'mouseleave'); + expect(mockFoundation.handleAnchorMouseLeave).not.toHaveBeenCalled(); + }); + + it('#initialSyncWithDOM registers blur event handler on the anchor element', + () => { + const {anchorElem, mockFoundation, component} = + setupTestWithMockFoundation(fixture); + emitEvent(anchorElem, 'blur'); + expect(mockFoundation.handleAnchorBlur).toHaveBeenCalled(); + component.destroy(); + }); + + it('#destroy deregisters blur event handler on the anchor element', () => { + const {anchorElem, mockFoundation, component} = + setupTestWithMockFoundation(fixture); + component.destroy(); + emitEvent(anchorElem, 'blur'); + expect(mockFoundation.handleAnchorBlur).not.toHaveBeenCalled(); + }); + + it('#initialSyncWithDOM registers transitionend event handler on the tooltip', + () => { + const {mockFoundation, component} = + setupTestWithMockFoundation(fixture); + emitEvent(component.root, 'transitionend'); + expect(mockFoundation.handleTransitionEnd).toHaveBeenCalled(); + component.destroy(); + }); + + it('#destroy deregisters transitionend event handler on the tooltip', + () => { + const {mockFoundation, component} = + setupTestWithMockFoundation(fixture); + component.destroy(); + emitEvent(component.root, 'transitionend'); + expect(mockFoundation.handleTransitionEnd).not.toHaveBeenCalled(); + }); + + it('#setTooltipPosition forwards to MDCFoundation#setTooltipPosition', + () => { + const {mockFoundation, component} = + setupTestWithMockFoundation(fixture); + component.setTooltipPosition( + {xPos: XPosition.CENTER, yPos: YPosition.ABOVE}); + expect(mockFoundation.setTooltipPosition).toHaveBeenCalledWith({ + xPos: XPosition.CENTER, + yPos: YPosition.ABOVE + }); + component.destroy(); + }); + + it('#setAnchorBoundaryType forwards to MDCFoundation#setAnchorBoundaryType', + () => { + const {mockFoundation, component} = + setupTestWithMockFoundation(fixture); + component.setAnchorBoundaryType(AnchorBoundaryType.UNBOUNDED); + expect(mockFoundation.setAnchorBoundaryType) + .toHaveBeenCalledWith(AnchorBoundaryType.UNBOUNDED); + component.destroy(); + }); + + it('sets aria-hidden to false when showing tooltip on an anchor annotated with `aria-describedby`', + () => { + const tooltipElem = fixture.querySelector('#tt0')!; + const anchorElem = + fixture.querySelector('[aria-describedby]')!; + MDCTooltip.attachTo(tooltipElem); + + emitEvent(anchorElem, 'mouseenter'); + jasmine.clock().tick(numbers.SHOW_DELAY_MS); + expect(tooltipElem.getAttribute('aria-hidden')).toEqual('false'); + }); - it('#setAnchorBoundaryType fowards to MDCFoundation#setAnchorBoundaryType', - () => { - const {mockFoundation, component} = setupTestWithMockFoundation(fixture); - component.setAnchorBoundaryType(AnchorBoundaryType.UNBOUNDED); - expect(mockFoundation.setAnchorBoundaryType) - .toHaveBeenCalledWith(AnchorBoundaryType.UNBOUNDED); - component.destroy(); - }); - - it('sets aria-hidden to false when showing tooltip on an anchor annotated with `aria-describedby`', - () => { - const tooltipElem = fixture.querySelector('#tt0')!; - const anchorElem = - fixture.querySelector('[aria-describedby]')!; - MDCTooltip.attachTo(tooltipElem); - - emitEvent(anchorElem, 'mouseenter'); - jasmine.clock().tick(numbers.SHOW_DELAY_MS); - expect(tooltipElem.getAttribute('aria-hidden')).toEqual('false'); - }); - - it('leaves aria-hidden as true when showing tooltip on an anchor annotated with `data-tooltip-id`', - () => { - document.body.removeChild(fixture); - fixture = getFixture(`
+ it('leaves aria-hidden as true when showing tooltip on an anchor annotated with `data-tooltip-id`', + () => { + document.body.removeChild(fixture); + fixture = getFixture(`
@@ -205,20 +213,20 @@ describe('MDCTooltip', () => {
`); - document.body.appendChild(fixture); - const tooltipElem = fixture.querySelector('#tt0')!; - const anchorElem = - fixture.querySelector('[data-tooltip-id]')!; - MDCTooltip.attachTo(tooltipElem); - - emitEvent(anchorElem, 'mouseenter'); - jasmine.clock().tick(numbers.SHOW_DELAY_MS); - expect(tooltipElem.getAttribute('aria-hidden')).toEqual('true'); - }); - - it('detects tooltip labels that span multiple lines', () => { - document.body.removeChild(fixture); - fixture = getFixture(`
+ document.body.appendChild(fixture); + const tooltipElem = fixture.querySelector('#tt0')!; + const anchorElem = + fixture.querySelector('[data-tooltip-id]')!; + MDCTooltip.attachTo(tooltipElem); + + emitEvent(anchorElem, 'mouseenter'); + jasmine.clock().tick(numbers.SHOW_DELAY_MS); + expect(tooltipElem.getAttribute('aria-hidden')).toEqual('true'); + }); + + it('detects tooltip labels that span multiple lines', () => { + document.body.removeChild(fixture); + fixture = getFixture(`
@@ -232,17 +240,75 @@ describe('MDCTooltip', () => {
`); - document.body.appendChild(fixture); - const tooltipElem = fixture.querySelector('#tt0')!; - // Add a max-width and min-height since styles are not loaded in - // this test. - tooltipElem.style.maxWidth = `${numbers.MAX_WIDTH}px`; - tooltipElem.style.minHeight = `${numbers.MIN_HEIGHT}px`; - const anchorElem = fixture.querySelector('[data-tooltip-id]')!; - MDCTooltip.attachTo(tooltipElem); - - emitEvent(anchorElem, 'mouseenter'); - jasmine.clock().tick(numbers.SHOW_DELAY_MS); - expect(tooltipElem.classList).toContain(CssClasses.MULTILINE_TOOLTIP); + document.body.appendChild(fixture); + const tooltipElem = fixture.querySelector('#tt0')!; + // Add a max-width and min-height since styles are not loaded in + // this test. + tooltipElem.style.maxWidth = `${numbers.MAX_WIDTH}px`; + tooltipElem.style.minHeight = `${numbers.MIN_HEIGHT}px`; + const anchorElem = + fixture.querySelector('[data-tooltip-id]')!; + MDCTooltip.attachTo(tooltipElem); + + emitEvent(anchorElem, 'mouseenter'); + jasmine.clock().tick(numbers.SHOW_DELAY_MS); + expect(tooltipElem.classList).toContain(CssClasses.MULTILINE_TOOLTIP); + }); + }); + + describe('rich tooltip tests', () => { + beforeEach(() => { + fixture = getFixture(`
+ + +
`); + document.body.appendChild(fixture); + }); + + afterEach(() => { + document.body.removeChild(fixture); + }); + + it('attachTo returns a component instance', () => { + expect(MDCTooltip.attachTo( + fixture.querySelector('.mdc-tooltip--rich') as HTMLElement)) + .toEqual(jasmine.any(MDCTooltip)); + }); + + it('sets aria-expanded on anchor to true when showing rich tooltip`', + () => { + const tooltipElem = fixture.querySelector('#tt0')!; + const anchorElem = + fixture.querySelector('[aria-describedby]')!; + MDCTooltip.attachTo(tooltipElem); + + emitEvent(anchorElem, 'mouseenter'); + jasmine.clock().tick(numbers.SHOW_DELAY_MS); + + expect(anchorElem.getAttribute('aria-expanded')).toEqual('true'); + }); + + it('aria-expanded remains true on anchor when mouseleave anchor and mouseenter rich tooltip`', + () => { + const tooltipElem = fixture.querySelector('#tt0')!; + const anchorElem = + fixture.querySelector('[aria-describedby]')!; + MDCTooltip.attachTo(tooltipElem); + + emitEvent(anchorElem, 'mouseenter'); + jasmine.clock().tick(numbers.SHOW_DELAY_MS); + emitEvent(anchorElem, 'mouseleave'); + emitEvent(tooltipElem, 'mouseenter'); + + expect(anchorElem.getAttribute('aria-expanded')).toEqual('true'); + }); }); }); diff --git a/packages/mdc-tooltip/test/foundation.test.ts b/packages/mdc-tooltip/test/foundation.test.ts index 069c8f3638a..e84ebb6a08f 100644 --- a/packages/mdc-tooltip/test/foundation.test.ts +++ b/packages/mdc-tooltip/test/foundation.test.ts @@ -48,7 +48,10 @@ describe('MDCTooltipFoundation', () => { 'getTooltipSize', 'getAnchorBoundingRect', 'getAnchorAttribute', + 'setAnchorAttribute', 'isRTL', + 'registerEventHandler', + 'deregisterEventHandler', 'registerDocumentEventHandler', 'deregisterDocumentEventHandler', 'registerWindowEventHandler', @@ -65,6 +68,20 @@ describe('MDCTooltipFoundation', () => { expect(mockAdapter.addClass).toHaveBeenCalledWith(CssClasses.SHOWING); }); + it('#show sets aria-expanded="true" on anchor element for rich tooltip', + () => { + const {foundation, mockAdapter} = + setUpFoundationTest(MDCTooltipFoundation); + mockAdapter.hasClass.withArgs(CssClasses.RICH).and.returnValue(true); + foundation.init(); + + foundation.show(); + + expect(mockAdapter.hasClass).toHaveBeenCalledWith(CssClasses.RICH); + expect(mockAdapter.setAnchorAttribute) + .toHaveBeenCalledWith('aria-expanded', 'true'); + }); + it('#show leaves aria-hidden="true" attribute on tooltips intended to be hidden from screenreader', () => { const {foundation, mockAdapter} = @@ -107,6 +124,34 @@ describe('MDCTooltipFoundation', () => { .not.toHaveBeenCalledWith(CssClasses.SHOWING_TRANSITION); }); + it('#show registers mouseenter event listener on the tooltip for rich tooltip', + () => { + const {foundation, mockAdapter} = + setUpFoundationTest(MDCTooltipFoundation); + mockAdapter.hasClass.withArgs(CssClasses.RICH).and.returnValue(true); + foundation.init(); + + foundation.show(); + + expect(mockAdapter.registerEventHandler) + .toHaveBeenCalledWith('mouseenter', jasmine.any(Function)); + }); + + it('#hide deregisters mouseenter event listeners on the tooltip for rich tooltip', + () => { + const {foundation, mockAdapter} = + setUpFoundationTest(MDCTooltipFoundation); + mockAdapter.hasClass.withArgs(CssClasses.RICH).and.returnValue(true); + foundation.init(); + + foundation.show(); + foundation.hide(); + + expect(mockAdapter.deregisterEventHandler) + .toHaveBeenCalledWith('mouseenter', jasmine.any(Function)); + }); + + it('#show registers click and keydown event listeners on the document', () => { const {foundation, mockAdapter} = @@ -423,6 +468,24 @@ describe('MDCTooltipFoundation', () => { expect(foundation.showTimeout).toEqual(null); }); + it(`#handleRichTooltipMouseEnter shows the tooltip immediately`, + () => { + const {foundation, mockAdapter} = + setUpFoundationTest(MDCTooltipFoundation); + mockAdapter.hasClass.withArgs(CssClasses.RICH).and.returnValue(true); + foundation.init(); + + foundation.handleRichTooltipMouseEnter(); + + expect(foundation.showTimeout).toEqual(null); + expect(mockAdapter.setAnchorAttribute) + .toHaveBeenCalledWith('aria-expanded', 'true'); + expect(mockAdapter.setAttribute) + .toHaveBeenCalledWith('aria-hidden', 'false'); + expect(mockAdapter.removeClass).toHaveBeenCalledWith(CssClasses.HIDE); + expect(mockAdapter.addClass).toHaveBeenCalledWith(CssClasses.SHOWING); + }); + it(`does not re-animate a tooltip already shown in the dom (from focus)`, () => { const {foundation, mockAdapter} = @@ -491,6 +554,21 @@ describe('MDCTooltipFoundation', () => { expect(mockAdapter.addClass).not.toHaveBeenCalledWith(CssClasses.SHOWING); }); + it('#handleRichTooltipMouseEnter keeps tooltip visible', () => { + const {foundation, mockAdapter} = setUpFoundationTest(MDCTooltipFoundation); + mockAdapter.hasClass.withArgs(CssClasses.RICH).and.returnValue(true); + foundation.init(); + + foundation.handleAnchorMouseLeave(); + expect(foundation.hideTimeout).not.toEqual(null); + foundation.handleRichTooltipMouseEnter(); + + expect(foundation.hideTimeout).toEqual(null); + expect(mockAdapter.setAttribute) + .not.toHaveBeenCalledWith('aria-hidden', 'true'); + expect(foundation.isShown).toBeTrue(); + }); + it('#hide clears any pending showTimeout', () => { const {foundation, mockAdapter} = setUpFoundationTest(MDCTooltipFoundation); foundation.handleAnchorMouseEnter(); @@ -835,6 +913,8 @@ describe('MDCTooltipFoundation', () => { () => { const {foundation, mockAdapter} = setUpFoundationTest(MDCTooltipFoundation); + mockAdapter.hasClass.withArgs(CssClasses.RICH).and.returnValue(true); + foundation.init(); foundation.handleAnchorMouseEnter(); foundation.handleAnchorMouseLeave(); foundation.destroy(); @@ -850,6 +930,8 @@ describe('MDCTooltipFoundation', () => { expect(mockAdapter.removeClass).toHaveBeenCalledWith(CssClasses.HIDE); expect(mockAdapter.removeClass) .toHaveBeenCalledWith(CssClasses.HIDE_TRANSITION); + expect(mockAdapter.deregisterEventHandler) + .toHaveBeenCalledWith('mouseenter', jasmine.any(Function)); expect(mockAdapter.deregisterDocumentEventHandler) .toHaveBeenCalledWith('click', jasmine.any(Function)); expect(mockAdapter.deregisterDocumentEventHandler)