mirror of https://github.com/twbs/bootstrap.git
Prevent toast autohiding if focusing or hovering (#33221)
This commit is contained in:
parent
c3ad760cd7
commit
9c3ab6557e
|
@ -26,6 +26,10 @@ const DATA_KEY = 'bs.toast'
|
||||||
const EVENT_KEY = `.${DATA_KEY}`
|
const EVENT_KEY = `.${DATA_KEY}`
|
||||||
|
|
||||||
const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_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_HIDE = `hide${EVENT_KEY}`
|
||||||
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
|
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
|
||||||
const EVENT_SHOW = `show${EVENT_KEY}`
|
const EVENT_SHOW = `show${EVENT_KEY}`
|
||||||
|
@ -62,6 +66,8 @@ class Toast extends BaseComponent {
|
||||||
|
|
||||||
this._config = this._getConfig(config)
|
this._config = this._getConfig(config)
|
||||||
this._timeout = null
|
this._timeout = null
|
||||||
|
this._hasMouseInteraction = false
|
||||||
|
this._hasKeyboardInteraction = false
|
||||||
this._setListeners()
|
this._setListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,11 +106,7 @@ class Toast extends BaseComponent {
|
||||||
|
|
||||||
EventHandler.trigger(this._element, EVENT_SHOWN)
|
EventHandler.trigger(this._element, EVENT_SHOWN)
|
||||||
|
|
||||||
if (this._config.autohide) {
|
this._maybeScheduleHide()
|
||||||
this._timeout = setTimeout(() => {
|
|
||||||
this.hide()
|
|
||||||
}, this._config.delay)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._element.classList.remove(CLASS_NAME_HIDE)
|
this._element.classList.remove(CLASS_NAME_HIDE)
|
||||||
|
@ -159,8 +161,53 @@ class Toast extends BaseComponent {
|
||||||
return config
|
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() {
|
_setListeners() {
|
||||||
EventHandler.on(this._element, EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, () => this.hide())
|
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() {
|
_clearTimeout() {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Toast from '../../src/toast'
|
import Toast from '../../src/toast'
|
||||||
|
|
||||||
/** Test helpers */
|
/** Test helpers */
|
||||||
import { getFixture, clearFixture, jQueryMock } from '../helpers/fixture'
|
import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture'
|
||||||
|
|
||||||
describe('Toast', () => {
|
describe('Toast', () => {
|
||||||
let fixtureEl
|
let fixtureEl
|
||||||
|
@ -210,6 +210,182 @@ describe('Toast', () => {
|
||||||
|
|
||||||
toast.show()
|
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', () => {
|
describe('hide', () => {
|
||||||
|
|
Loading…
Reference in New Issue