Skip to content

Commit

Permalink
hover: Make closeOnMouseout more stable, improve animation (honestble…
Browse files Browse the repository at this point in the history
…eps#5015)

- Always close when mouse movement occurs outside of the hover container, in case `mouseleave` is not triggered, e.g. due to scrolling past the element
- Changes several consumer to open hover on `mouseenter` instead `mouseover` to potentially avoid potentially triggering hover multiple times
- Changes Filterline hover trigger to fix issue where wrong infocard is shown
- Make fade out CSS based
  • Loading branch information
larsjohnsen authored Jan 11, 2019
1 parent 57a3fff commit a0bef54
Show file tree
Hide file tree
Showing 12 changed files with 112 additions and 82 deletions.
1 change: 0 additions & 1 deletion lib/css/modules/_hover.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

.RESHover,
.RESHover.RESDialogSmall {
display: none;
position: absolute;
z-index: $zindex-res-hover;
}
Expand Down
5 changes: 5 additions & 0 deletions lib/css/res.scss
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,11 @@ body.RESScrollLock {
content: attr(data-text);
}

.transitionToTransparent {
transition: opacity 3s ease-in-out;
opacity: 0;
}

.RESLoadingSpinner {
color: hsl(210, 80%, 50%);
font-size: inherit; // Determines size of the icon.
Expand Down
2 changes: 1 addition & 1 deletion lib/modules/filteReddit/Filterline.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export class Filterline {
}

if (fromSelected) filter.updateByInputConstruction({ fromSelected });
filter.showInfocard(true);
else filter.showInfocard(true);
});

if (CaseClass.thingToCriterion || !CaseClass.defaultConditions) {
Expand Down
17 changes: 12 additions & 5 deletions lib/modules/filteReddit/LineFilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,23 @@ export class LineFilter extends Filter {
e.preventDefault(); // Do not show context menu
});

this.element.addEventListener('mouseenter', () => this.showInfocard()); // FIXME This causes the widget to be opened also when just scrolling through
this.element.addEventListener('click', () => this.showInfocard()); // Reset timout
this.element.addEventListener('contextmenu', () => this.showInfocard()); // Reset timeout
this.element.addEventListener('mouseenter', async () => {
// To prevent this from triggering and clearing other cards when moving the curser quickly through,
// wait a little in order to be sure about the user's intent
await new Promise(res => setTimeout(res, 150));
if (this.element.matches(':hover')) this.showInfocard();
});
// When just clicking the button, avoid showing the infocard as it can be a nuisance
this.element.addEventListener('click', () => Hover.infocard('filterline-filter').resetShowTimer());
this.element.addEventListener('contextmenu', () => Hover.infocard('filterline-filter').resetShowTimer());
}

showInfocard(immediately: ?boolean = false) {
showInfocard(immediately: boolean = false) {
const card = Hover.infocard('filterline-filter');
if (card.visible) immediately = true;
card
.target(this.element)
.options({ width: 570, openDelay: (card.visible || immediately) ? 0 : 600, pin: Hover.pin.bottom })
.options({ width: 570, openDelay: immediately ? 0 : 550, pin: Hover.pin.bottom })
.populateWith(this.populateHover.bind(this))
.begin();
}
Expand Down
145 changes: 82 additions & 63 deletions lib/modules/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { $ } from '../vendor';
import { Module } from '../core/module';
import * as Options from '../core/options';
import { getViewportSize } from '../utils';
import { NAMED_KEYS, getViewportSize, waitForEvent } from '../utils';

export const module: Module<*> = new Module('hover');

Expand Down Expand Up @@ -143,8 +143,9 @@ class Hover {
instanceID: string;

visible: boolean = false;
hideTimer: TimeoutID | null = null;
showTimer: TimeoutID | null = null;
_hideTimer: TimeoutID | null = null;
_showTimer: TimeoutID | null = null;
_closeFadeTimer: TimeoutID | null = null;
_target: HTMLElement | void;
_callback: HoverCallback | void;
_container: HTMLElement | void;
Expand All @@ -171,11 +172,12 @@ class Hover {
}

target(element: HTMLElement) {
if (this._target !== element) {
if (this._target && this._target !== element) {
this.close();
this._target = element;
}

this._target = element;

return this;
}

Expand All @@ -194,11 +196,13 @@ class Hover {
}

getContainer() {
if (!this._container) {
this._container = this._render();
const container = this._container = this._container || this._render();

if (!document.body.contains(container)) {
document.body.append(container);
}

return this._container;
return container;
}

_render() {
Expand All @@ -210,28 +214,43 @@ class Hover {

_addContainerHandlers(ele: HTMLElement) {
$(ele)
.appendTo(document.body)
.on('mouseenter', () => this._cancelHideTimer())
.on('mouseleave', () => {
.on('mouseenter', () => {
this._cancelHideTimer();
if (this._options.closeOnMouseOut) {
this._startHideTimer();
}
this._clearCloseFade();
})
.on('mouseleave', () => this._startHideTimer())
.on('click', '.RESCloseButton', () => this.close(true))
// Close on escape when inner element is focused
.on('keyup', ({ which }: KeyboardEvent) => { if (which === 27) this.close(true); });
.on('keyup', (e: KeyboardEvent) => {
if (e.key === NAMED_KEYS.Escape) this.close(true);
e.stopImmediatePropagation();
});

if (this._options.closeOnMouseOut) {
const fn = ({ target }: MouseEvent) => {
if (this.visible && !this.getCheckedTarget().contains(target) && !ele.contains(target)) this._startHideTimer();
};
document.body.addEventListener('mousemove', fn);
document.body.addEventListener('mouseover', fn); // To detect scrolling
}
}

begin() {
if (!this._enabled) return false;
this._startShowTimer();
this._addShowListeners();

if (this._options.openDelay) {
this._startShowTimer();
} else {
this.open();
}
}

open() {
if (!this._enabled) return false;

this._cancelShowTimer();
this._cancelHideTimer();
this._clearCloseFade();

const callback = this._callback;
if (callback) {
this._displayLoadIndicator();
Expand All @@ -247,7 +266,7 @@ class Hover {
this.close();
}

$(this.getContainer()).show().css({ opacity: 1 }); // nvm fade in, too much trouble
this.visible = true;
}

_displayLoadIndicator() {}
Expand Down Expand Up @@ -303,69 +322,69 @@ class Hover {
this._positionContainer({ top, left });
}

_cancelShow(fade: boolean = false) {
this.close(fade);
}

_addShowListeners() {
$(this._target).on('mouseleave.RESHover', () => {
if (!this.visible) this._cancelShow();
});
}
// Ensure a stable `this` for timer starters / cancellers, so that they can be referenced when adding / remove event listeners

_clearShowListeners() {
$(this._target).off('mouseleave.RESHover');
}
_startShowTimer = () => {
if (this._showTimer) return;
this._cancelHideTimer();
waitForEvent(this.getCheckedTarget(), 'mouseleave').then(this._cancelShowTimer);
this._showTimer = setTimeout(() => this.open(), this._options.openDelay);
};

_startShowTimer() {
if (this.showTimer !== null) this._cancelShowTimer();
this.showTimer = setTimeout(() => this._afterShowTimer(), this._options.openDelay);
}
_cancelShowTimer = () => {
if (!this._showTimer) return;
clearTimeout(this._showTimer);
this._showTimer = null;
};

_cancelShowTimer() {
if (this.showTimer !== null) clearTimeout(this.showTimer);
this.showTimer = null;
resetShowTimer() {
if (this._showTimer) {
this._cancelShowTimer();
this._startShowTimer();
}
}

_afterShowTimer() {
_startHideTimer = () => {
if (this._hideTimer) return;
this._cancelShowTimer();
this._clearShowListeners();

this.open();
this.visible = true;

$(this._target).on('mouseleave', () => this._startHideTimer());
$(this._target).on('mouseenter', () => this._cancelHideTimer());
}
this._hideTimer = setTimeout(() => this.close(true), this._options.fadeDelay);
};

_startHideTimer() {
if (this.hideTimer !== null) clearTimeout(this.hideTimer);
this.hideTimer = setTimeout(() => this._afterHideTimer(), this._options.fadeDelay);
}
_cancelHideTimer = () => {
if (!this._hideTimer) return;
clearTimeout(this._hideTimer);
this._hideTimer = null;
};

_cancelHideTimer() {
if (this.hideTimer !== null) clearTimeout(this.hideTimer);
this.hideTimer = null;
_startCloseFade() {
if (this._closeFadeTimer) return;
this._closeFadeTimer = setTimeout(() => { this.remove(); }, this._options.fadeSpeed * 1000);
this.getContainer().style.transitionDuration = `${this._options.fadeSpeed}s`;
this.getContainer().classList.add('transitionToTransparent');
}

_afterHideTimer() {
this._cancelHideTimer();
this.close(true);
this.visible = false;
_clearCloseFade() {
if (!this._closeFadeTimer) return;
clearTimeout(this._closeFadeTimer);
this.getContainer().style.transitionDuration = '';
this.getContainer().classList.remove('transitionToTransparent');
this._closeFadeTimer = null;
}

close(fade: boolean = false) {
if (!this._enabled) return false;

this._clearShowListeners();
this._cancelShowTimer();
this._cancelHideTimer();

if (!this.visible) return false;
if (fade) this._startCloseFade();
else this.remove();
}

$(this.getContainer()).fadeOut(fade ? this._options.fadeSpeed * 1000 : 0, () => {
if (document.activeElement && this.getContainer().contains(document.activeElement)) document.activeElement.blur();
});
remove() {
this._clearCloseFade();
this.getContainer().remove();
this.visible = false;
}
}

Expand Down
4 changes: 2 additions & 2 deletions lib/modules/messageMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ module.options = {
};

module.go = () => {
$('#mail, .mail-count, #NREMail, #NREMailCount').on('mouseover', onMouseOver);
$('#mail, .mail-count, #NREMail, #NREMailCount').on('mouseenter', onMouseEnter);
};

function onMouseOver(e: Event) {
function onMouseEnter(e: Event) {
Hover.dropdownList(module.moduleID)
.target(e.target)
.options({
Expand Down
4 changes: 2 additions & 2 deletions lib/modules/multiredditNavbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ module.options = {

module.go = () => {
if (module.options.sectionMenu.value) {
$('.listing-chooser .multis').on('mouseover', 'li', onMouseoverMultiLink);
$('.listing-chooser .multis').on('mouseenter', 'li', onMouseEnterMultiLink);
}
};

function onMouseoverMultiLink(e: Event) {
function onMouseEnterMultiLink(e: Event) {
const link: ?HTMLAnchorElement = (e.currentTarget.querySelector('a[href^="/me/m"]'): any);
if (!link) {
return;
Expand Down
4 changes: 2 additions & 2 deletions lib/modules/profileNavigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ module.options = {
module.go = () => {
const username = loggedInUser();
if (module.options.sectionMenu.value && username) {
$('#header .user a').on('mouseover', (e: Event) => onMouseoverProfileLink(username, e));
$('#header .user a').on('mouseenter', (e: Event) => onMouseEnterProfileLink(username, e));
}
};

function onMouseoverProfileLink(user, e) {
function onMouseEnterProfileLink(user, e) {
Hover.dropdownList(module.moduleID)
.target(e.target)
.options({
Expand Down
4 changes: 2 additions & 2 deletions lib/modules/subredditInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ module.go = () => {
!module.options.requireDirectLink.value && '.md a[href*="reddit.com/r/"]',
].filter(x => x).join(', ');

$(document.body).on('mouseover', linkSelector, handleMouseOver);
$(document.body).on('mouseenter', linkSelector, handleMouseEnter);
};

function handleMouseOver(e: Event) {
function handleMouseEnter(e: Event) {
const target = downcast(e.target, HTMLAnchorElement);
const match = regexes.subreddit.exec(target.pathname);
if (!match) return;
Expand Down
4 changes: 2 additions & 2 deletions lib/modules/userInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,11 @@ export const highlightedUsers = {};

module.go = () => {
if (module.options.hoverInfo.value) {
$(document.body).on('mouseover', usernameSelector, handleMouseOver);
$(document.body).on('mouseenter', usernameSelector, handleMouseEnter);
}
};

function handleMouseOver(e: Event) {
function handleMouseEnter(e: Event) {
const username = getUsernameFromLink(e.target);
if (!username) {
console.error(i18n('userInfoInvalidUsernameLink'));
Expand Down
2 changes: 1 addition & 1 deletion tests/subredditInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ module.exports = {
.assert.visible('.RESHover a[href$="/r/RESIntegrationTests"]')
.assert.containsText('.RESHover', 'Subreddit created:')
.click('.RESHover .RESCloseButton')
.waitForElementNotVisible('.RESHover')
.waitForElementNotPresent('.RESHover')
.end();
},
};
2 changes: 1 addition & 1 deletion tests/userInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module.exports = {
.assert.visible('.RESHover a[href$="/user/erikdesjardins/comments/"]')
.assert.containsText('.RESHover', 'Redditor since:')
.click('.RESHover .RESCloseButton')
.waitForElementNotVisible('.RESHover')
.waitForElementNotPresent('.RESHover')
.end();
},
};

0 comments on commit a0bef54

Please sign in to comment.