diff --git a/jest/jest.setup.js b/jest/jest.setup.js index aabc046..658c566 100644 --- a/jest/jest.setup.js +++ b/jest/jest.setup.js @@ -3,13 +3,13 @@ import '@testing-library/jest-dom/extend-expect' expect.extend({ toBeInTheDocument, toHaveAttribute, toHaveStyle }) -global._createElement = (id = 'foo', attrs) => { +global._createElement = (id = 'foo', parent, attrs) => { const el = document.createElement('div') el.setAttribute('id', id) for (let key in attrs) { el.setAttribute(key, attrs[key]) } - document.body.appendChild(el) + ;(parent || document.body).appendChild(el) return el } diff --git a/package.json b/package.json index ae989dc..95bf573 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^8.0.2", - "@testing-library/dom": "^8.11.0", + "@testing-library/dom": "^8.11.3", "@testing-library/jest-dom": "^5.11.6", "@testing-library/svelte": "^3.0.3", "babel-jest": "^27.3.1", diff --git a/src/Tooltip.js b/src/Tooltip.js index 02a3550..de386b6 100644 --- a/src/Tooltip.js +++ b/src/Tooltip.js @@ -13,7 +13,7 @@ class Tooltip { #boundEnterHandler = null #boundLeaveHandler = null - #boundKeyDownHandler = null + #boundWindowChangeHandler = null #target = null #content = null @@ -72,7 +72,7 @@ class Tooltip { this.#target.setAttribute('style', 'position: relative') this.#target.setAttribute('aria-describedby', 'tooltip') - disabled ? this.#disableTarget() : this.#enableTarget() + disabled ? this.#disable() : this.#enable() Tooltip.#instances.push(this) } @@ -122,14 +122,14 @@ class Tooltip { } if (hasToDisableTarget) { - this.#disableTarget() + this.#disable() } else if (hasToEnableTarget) { - this.#enableTarget() + this.#enable() } } - destroy() { - this.#removeTooltipFromTarget() + async destroy() { + await this.#removeTooltipFromTarget() this.#disableTarget() @@ -139,16 +139,32 @@ class Tooltip { this.#observer = null } + #enable() { + this.#enableTarget() + this.#enableWindow() + } + #enableTarget() { this.#boundEnterHandler = this.#onTargetEnter.bind(this) this.#boundLeaveHandler = this.#onTargetLeave.bind(this) - this.#boundKeyDownHandler = this.#onTargetKeyDown.bind(this) this.#target.addEventListener('mouseenter', this.#boundEnterHandler) this.#target.addEventListener('mouseleave', this.#boundLeaveHandler) this.#target.addEventListener('focusin', this.#boundEnterHandler) this.#target.addEventListener('focusout', this.#boundLeaveHandler) - window.addEventListener('keydown', this.#boundKeyDownHandler) + } + + #enableWindow() { + this.#boundWindowChangeHandler = this.#onWindowChange.bind(this) + + window.addEventListener('keydown', this.#boundWindowChangeHandler) + window.addEventListener('resize', this.#boundWindowChangeHandler) + window.addEventListener('scroll', this.#boundWindowChangeHandler) + } + + #disable() { + this.#disableTarget() + this.#disableWindow() } #disableTarget() { @@ -156,11 +172,17 @@ class Tooltip { this.#target.removeEventListener('mouseleave', this.#boundLeaveHandler) this.#target.removeEventListener('focusin', this.#boundEnterHandler) this.#target.removeEventListener('focusout', this.#boundLeaveHandler) - window.removeEventListener('keydown', this.#boundKeyDownHandler) this.#boundEnterHandler = null this.#boundLeaveHandler = null - this.#boundKeyDownHandler = null + } + + #disableWindow() { + window.removeEventListener('keydown', this.#boundWindowChangeHandler) + window.removeEventListener('resize', this.#boundWindowChangeHandler) + window.removeEventListener('scroll', this.#boundWindowChangeHandler) + + this.#boundWindowChangeHandler = null } #createTooltip() { @@ -345,9 +367,16 @@ class Tooltip { await this.#removeTooltipFromTarget() } - async #onTargetKeyDown(e) { - if (e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) { - await this.#onTargetLeave() + async #onWindowChange(e) { + if ( + this.#tooltip && + this.#tooltip.parentNode && + (e.type !== 'keydown' || + (e.type === 'keydown' && e.key === 'Escape') || + e.key === 'Esc' || + e.keyCode === 27) + ) { + await this.#removeTooltipFromTarget() } } } diff --git a/src/__tests__/useTooltip.test.js b/src/__tests__/useTooltip.test.js index b05440b..d9d005e 100644 --- a/src/__tests__/useTooltip.test.js +++ b/src/__tests__/useTooltip.test.js @@ -58,13 +58,27 @@ describe('useTooltip', () => { const _keyDown = async (key) => new Promise(async (resolve) => { - await fireEvent.keyDown(target, key || { key: 'Escape', code: 'Escape', charCode: 27 }) + await fireEvent.keyDown(target, key || { key: 'Escape', code: 'Escape', keyCode: 27 }) + await _sleep(1) + resolve() + }) + + const _scroll = async () => + new Promise(async (resolve) => { + await fireEvent.scroll(window) + await _sleep(1) + resolve() + }) + + const _resize = async () => + new Promise(async (resolve) => { + await fireEvent(window, new Event('resize')) await _sleep(1) resolve() }) beforeEach(() => { - target = _createElement('target', { class: 'bar' }) + target = _createElement('target', null, { class: 'bar' }) template = _createElement('template') options = { contentSelector: '#template', @@ -110,9 +124,26 @@ describe('useTooltip', () => { it('Hides tooltip on escape key down', async () => { action = useTooltip(target, options) await _enter() + expect(template).toBeInTheDocument() await _keyDown() expect(template).not.toBeInTheDocument() }) + + it('Hides tooltip on scroll', async () => { + action = useTooltip(target, options) + await _enter() + expect(template).toBeInTheDocument() + await _scroll() + expect(template).not.toBeInTheDocument() + }) + + it('Hides tooltip on resize', async () => { + action = useTooltip(target, options) + await _enter() + expect(template).toBeInTheDocument() + await _resize() + expect(template).not.toBeInTheDocument() + }) }) describe('update', () => { @@ -213,6 +244,28 @@ describe('useTooltip', () => { expect(template).toBeInTheDocument() }) + it('Triggers callback on element click within tooltip', async () => { + const newTemplate = _createElement('new-template') + const clickableElement = _createElement('clickable-element', newTemplate) + const contentActions = { + '#clickable-element': { + eventType: 'click', + callback: jest.fn(), + callbackParams: ['foo'], + }, + } + action = useTooltip(target, { + ...options, + contentSelector: '#new-template', + contentActions, + }) + const contentAction = contentActions['#clickable-element'] + await _enter() + await fireEvent.click(clickableElement) + expect(contentAction.callback).toHaveBeenCalledWith(contentAction.callbackParams[0], expect.any(Event)) + expect(newTemplate).toBeInTheDocument() + }) + it('Closes tooltip after triggering callback', async () => { action = useTooltip(target, options) options.contentActions['*'].closeOnCallback = true diff --git a/yarn.lock b/yarn.lock index 94cbf01..c7b7b69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1776,10 +1776,10 @@ lz-string "^1.4.4" pretty-format "^26.6.2" -"@testing-library/dom@^8.11.0": - version "8.11.0" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.11.0.tgz#3679dfb4db58e0d2b95e4b0929eaf45237b60d94" - integrity sha512-8Ay4UDiMlB5YWy+ZvCeRyFFofs53ebxrWnOFvCoM1HpMAX4cHyuSrCuIM9l2lVuUWUt+Gr3loz/nCwdrnG6ShQ== +"@testing-library/dom@^8.11.3": + version "8.11.3" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.11.3.tgz#38fd63cbfe14557021e88982d931e33fb7c1a808" + integrity sha512-9LId28I+lx70wUiZjLvi1DB/WT2zGOxUh46glrSNMaWVx849kKAluezVzZrXJfTKKoQTmEOutLes/bHg4Bj3aA== dependencies: "@babel/code-frame" "^7.10.4" "@babel/runtime" "^7.12.5"