const EVENT_KEY = `.${DATA_KEY}`
const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`
+const EVENT_MOUSEOVER = `mouseover${EVENT_KEY}`
+const EVENT_MOUSEOUT = `mouseout${EVENT_KEY}`
+const EVENT_FOCUSIN = `focusin${EVENT_KEY}`
+const EVENT_FOCUSOUT = `focusout${EVENT_KEY}`
const EVENT_HIDE = `hide${EVENT_KEY}`
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
const EVENT_SHOW = `show${EVENT_KEY}`
this._config = this._getConfig(config)
this._timeout = null
+ this._hasMouseInteraction = false
+ this._hasKeyboardInteraction = false
this._setListeners()
}
EventHandler.trigger(this._element, EVENT_SHOWN)
- if (this._config.autohide) {
- this._timeout = setTimeout(() => {
- this.hide()
- }, this._config.delay)
- }
+ this._maybeScheduleHide()
}
this._element.classList.remove(CLASS_NAME_HIDE)
return config
}
+ _maybeScheduleHide() {
+ if (!this._config.autohide) {
+ return
+ }
+
+ if (this._hasMouseInteraction || this._hasKeyboardInteraction) {
+ return
+ }
+
+ this._timeout = setTimeout(() => {
+ this.hide()
+ }, this._config.delay)
+ }
+
+ _onInteraction(event, isInteracting) {
+ switch (event.type) {
+ case 'mouseover':
+ case 'mouseout':
+ this._hasMouseInteraction = isInteracting
+ break
+ case 'focusin':
+ case 'focusout':
+ this._hasKeyboardInteraction = isInteracting
+ break
+ default:
+ break
+ }
+
+ if (isInteracting) {
+ this._clearTimeout()
+ return
+ }
+
+ const nextElement = event.relatedTarget
+ if (this._element === nextElement || this._element.contains(nextElement)) {
+ return
+ }
+
+ this._maybeScheduleHide()
+ }
+
_setListeners() {
EventHandler.on(this._element, EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, () => this.hide())
+ EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true))
+ EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false))
+ EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true))
+ EventHandler.on(this._element, EVENT_FOCUSOUT, event => this._onInteraction(event, false))
}
_clearTimeout() {
import Toast from '../../src/toast'
/** Test helpers */
-import { getFixture, clearFixture, jQueryMock } from '../helpers/fixture'
+import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture'
describe('Toast', () => {
let fixtureEl
toast.show()
})
+
+ it('should clear timeout if toast is interacted with mouse', done => {
+ fixtureEl.innerHTML = [
+ '<div class="toast">',
+ ' <div class="toast-body">',
+ ' a simple toast',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const toastEl = fixtureEl.querySelector('.toast')
+ const toast = new Toast(toastEl)
+ const spy = spyOn(toast, '_clearTimeout').and.callThrough()
+
+ setTimeout(() => {
+ spy.calls.reset()
+
+ toastEl.addEventListener('mouseover', () => {
+ expect(toast._clearTimeout).toHaveBeenCalledTimes(1)
+ expect(toast._timeout).toBeNull()
+ done()
+ })
+
+ const mouseOverEvent = createEvent('mouseover')
+ toastEl.dispatchEvent(mouseOverEvent)
+ }, toast._config.delay / 2)
+
+ toast.show()
+ })
+
+ it('should clear timeout if toast is interacted with keyboard', done => {
+ fixtureEl.innerHTML = [
+ '<button id="outside-focusable">outside focusable</button>',
+ '<div class="toast">',
+ ' <div class="toast-body">',
+ ' a simple toast',
+ ' <button>with a button</button>',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const toastEl = fixtureEl.querySelector('.toast')
+ const toast = new Toast(toastEl)
+ const spy = spyOn(toast, '_clearTimeout').and.callThrough()
+
+ setTimeout(() => {
+ spy.calls.reset()
+
+ toastEl.addEventListener('focusin', () => {
+ expect(toast._clearTimeout).toHaveBeenCalledTimes(1)
+ expect(toast._timeout).toBeNull()
+ done()
+ })
+
+ const insideFocusable = toastEl.querySelector('button')
+ insideFocusable.focus()
+ }, toast._config.delay / 2)
+
+ toast.show()
+ })
+
+ it('should still auto hide after being interacted with mouse and keyboard', done => {
+ fixtureEl.innerHTML = [
+ '<button id="outside-focusable">outside focusable</button>',
+ '<div class="toast">',
+ ' <div class="toast-body">',
+ ' a simple toast',
+ ' <button>with a button</button>',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const toastEl = fixtureEl.querySelector('.toast')
+ const toast = new Toast(toastEl)
+
+ setTimeout(() => {
+ toastEl.addEventListener('mouseover', () => {
+ const insideFocusable = toastEl.querySelector('button')
+ insideFocusable.focus()
+ })
+
+ toastEl.addEventListener('focusin', () => {
+ const mouseOutEvent = createEvent('mouseout')
+ toastEl.dispatchEvent(mouseOutEvent)
+ })
+
+ toastEl.addEventListener('mouseout', () => {
+ const outsideFocusable = document.getElementById('outside-focusable')
+ outsideFocusable.focus()
+ })
+
+ toastEl.addEventListener('focusout', () => {
+ expect(toast._timeout).not.toBeNull()
+ done()
+ })
+
+ const mouseOverEvent = createEvent('mouseover')
+ toastEl.dispatchEvent(mouseOverEvent)
+ }, toast._config.delay / 2)
+
+ toast.show()
+ })
+
+ it('should not auto hide if focus leaves but mouse pointer remains inside', done => {
+ fixtureEl.innerHTML = [
+ '<button id="outside-focusable">outside focusable</button>',
+ '<div class="toast">',
+ ' <div class="toast-body">',
+ ' a simple toast',
+ ' <button>with a button</button>',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const toastEl = fixtureEl.querySelector('.toast')
+ const toast = new Toast(toastEl)
+
+ setTimeout(() => {
+ toastEl.addEventListener('mouseover', () => {
+ const insideFocusable = toastEl.querySelector('button')
+ insideFocusable.focus()
+ })
+
+ toastEl.addEventListener('focusin', () => {
+ const outsideFocusable = document.getElementById('outside-focusable')
+ outsideFocusable.focus()
+ })
+
+ toastEl.addEventListener('focusout', () => {
+ expect(toast._timeout).toBeNull()
+ done()
+ })
+
+ const mouseOverEvent = createEvent('mouseover')
+ toastEl.dispatchEvent(mouseOverEvent)
+ }, toast._config.delay / 2)
+
+ toast.show()
+ })
+
+ it('should not auto hide if mouse pointer leaves but focus remains inside', done => {
+ fixtureEl.innerHTML = [
+ '<button id="outside-focusable">outside focusable</button>',
+ '<div class="toast">',
+ ' <div class="toast-body">',
+ ' a simple toast',
+ ' <button>with a button</button>',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const toastEl = fixtureEl.querySelector('.toast')
+ const toast = new Toast(toastEl)
+
+ setTimeout(() => {
+ toastEl.addEventListener('mouseover', () => {
+ const insideFocusable = toastEl.querySelector('button')
+ insideFocusable.focus()
+ })
+
+ toastEl.addEventListener('focusin', () => {
+ const mouseOutEvent = createEvent('mouseout')
+ toastEl.dispatchEvent(mouseOutEvent)
+ })
+
+ toastEl.addEventListener('mouseout', () => {
+ expect(toast._timeout).toBeNull()
+ done()
+ })
+
+ const mouseOverEvent = createEvent('mouseover')
+ toastEl.dispatchEvent(mouseOverEvent)
+ }, toast._config.delay / 2)
+
+ toast.show()
+ })
})
describe('hide', () => {