Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix tooltips disappearing after trying to interact during their fade out animation #33289

Merged
merged 4 commits into from Mar 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 11 additions & 4 deletions js/src/tooltip.js
Expand Up @@ -279,11 +279,14 @@ class Tooltip extends BaseComponent {

if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
container.appendChild(tip)
EventHandler.trigger(this._element, this.constructor.Event.INSERTED)
}

EventHandler.trigger(this._element, this.constructor.Event.INSERTED)

this._popper = Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))
if (this._popper) {
this._popper.update()
} else {
this._popper = Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))
}

tip.classList.add(CLASS_NAME_SHOW)

Expand Down Expand Up @@ -329,6 +332,10 @@ class Tooltip extends BaseComponent {

const tip = this.getTipElement()
const complete = () => {
if (this._isWithActiveTrigger()) {
return
}

if (this._hoverState !== HOVER_STATE_SHOW && tip.parentNode) {
tip.parentNode.removeChild(tip)
}
Expand Down Expand Up @@ -646,7 +653,7 @@ class Tooltip extends BaseComponent {
if (event) {
context._activeTrigger[
event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER
] = false
] = context._element.contains(event.relatedTarget)
GeoSot marked this conversation as resolved.
Show resolved Hide resolved
}

if (context._isWithActiveTrigger()) {
Expand Down
94 changes: 94 additions & 0 deletions js/tests/unit/tooltip.spec.js
Expand Up @@ -708,6 +708,100 @@ describe('Tooltip', () => {
tooltipEl.dispatchEvent(createEvent('mouseover'))
})

it('should not hide tooltip if leave event occurs and interaction remains inside trigger', done => {
fixtureEl.innerHTML = [
'<a href="#" rel="tooltip" title="Another tooltip">',
'<b>Trigger</b>',
'the tooltip',
'</a>'
]

const tooltipEl = fixtureEl.querySelector('a')
const tooltip = new Tooltip(tooltipEl)
const triggerChild = tooltipEl.querySelector('b')

spyOn(tooltip, 'hide').and.callThrough()

tooltipEl.addEventListener('mouseover', () => {
const moveMouseToChildEvent = createEvent('mouseout')
Object.defineProperty(moveMouseToChildEvent, 'relatedTarget', {
value: triggerChild
})

tooltipEl.dispatchEvent(moveMouseToChildEvent)
})

tooltipEl.addEventListener('mouseout', () => {
expect(tooltip.hide).not.toHaveBeenCalled()
done()
})

tooltipEl.dispatchEvent(createEvent('mouseover'))
})

it('should properly maintain tooltip state if leave event occurs and enter event occurs during hide transition', done => {
// Style this tooltip to give it plenty of room for popper to do what it wants
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip" data-bs-placement="top" style="position:fixed;left:50%;top:50%;">Trigger</a>'

const tooltipEl = fixtureEl.querySelector('a')
const tooltip = new Tooltip(tooltipEl)

spyOn(window, 'getComputedStyle').and.returnValue({
transitionDuration: '0.15s',
transitionDelay: '0s'
})

setTimeout(() => {
expect(tooltip._popper).not.toBeNull()
expect(tooltip.getTipElement().getAttribute('data-popper-placement')).toBe('top')
tooltipEl.dispatchEvent(createEvent('mouseout'))

setTimeout(() => {
expect(tooltip.getTipElement().classList.contains('show')).toEqual(false)
tooltipEl.dispatchEvent(createEvent('mouseover'))
}, 100)

setTimeout(() => {
expect(tooltip._popper).not.toBeNull()
expect(tooltip.getTipElement().getAttribute('data-popper-placement')).toBe('top')
done()
}, 200)
}, 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RyanBerliner can you leave a comment on top of first time-out explaining its purpose with zero delay? Or replace zero timeout with 1milisec

I suppose you wrote it like this to give a minimum time to Tooltip to be rendered

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tbh, I know it's not a good reason, but I did it this way because it matches styling from this existing tooltip test. And given that the following is true:

setTimout(() => console.log('here 1'), 0)
console.log('here 2')

------ Outputs ------

"here 2"
"here 1"

I figured that it's the preferred syntax in this case. I can change it, but if I do it should probably be changed in the other tests as well for consistency?

Copy link
Member

@GeoSot GeoSot Mar 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.
Personally I would prefer at least the zero to be 1 but I think,cc/ @XhmikosR the monster of code consistency, will give us his opinion

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to streamline these but in a separate PR after this. For now, I just want us to merge this and any other beta3 PRs so that we release beta3 next week :)


tooltipEl.dispatchEvent(createEvent('mouseover'))
})

it('should only trigger inserted event if a new tooltip element was created', done => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">'

const tooltipEl = fixtureEl.querySelector('a')
const tooltip = new Tooltip(tooltipEl)

spyOn(window, 'getComputedStyle').and.returnValue({
transitionDuration: '0.15s',
transitionDelay: '0s'
})

const insertedFunc = jasmine.createSpy()
tooltipEl.addEventListener('inserted.bs.tooltip', insertedFunc)

setTimeout(() => {
expect(insertedFunc).toHaveBeenCalledTimes(1)
tooltip.hide()

setTimeout(() => {
tooltip.show()
}, 100)

setTimeout(() => {
expect(insertedFunc).toHaveBeenCalledTimes(1)
done()
}, 200)
}, 0)

tooltip.show()
})

it('should show a tooltip with custom class provided in data attributes', done => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip" data-bs-custom-class="custom-class">'

Expand Down