Some checks failed
Check & deploy / build (pull_request) Failing after 1m16s
139 lines
3.5 KiB
TypeScript
139 lines
3.5 KiB
TypeScript
import { appConfig } from '../config';
|
|
|
|
export class MenuHider {
|
|
private readonly desktopMediaQuery = window.matchMedia(
|
|
appConfig.menuHider.desktopMediaQuery
|
|
);
|
|
private hideTimeout: number | undefined;
|
|
private isHidden = false;
|
|
private pointerInside = false;
|
|
|
|
public constructor(
|
|
private readonly element: HTMLElement,
|
|
private readonly shouldBeHidden: () => boolean
|
|
) {
|
|
element.addEventListener('pointerenter', this.onPointerEnter);
|
|
element.addEventListener('pointerleave', this.onPointerLeave);
|
|
element.addEventListener('focusin', this.onFocusIn);
|
|
element.addEventListener('focusout', this.onFocusOut);
|
|
window.addEventListener('pointermove', this.onPointerMove, { passive: true });
|
|
document.addEventListener('fullscreenchange', this.onVisibilityContextChange);
|
|
this.desktopMediaQuery.addEventListener('change', this.onVisibilityContextChange);
|
|
|
|
this.reveal();
|
|
}
|
|
|
|
private get canAutoHide(): boolean {
|
|
return (
|
|
this.desktopMediaQuery.matches &&
|
|
this.shouldBeHidden() &&
|
|
!this.pointerInside &&
|
|
!this.element.contains(document.activeElement)
|
|
);
|
|
}
|
|
|
|
private readonly onPointerEnter = () => {
|
|
this.pointerInside = true;
|
|
this.reveal();
|
|
};
|
|
|
|
private readonly onPointerLeave = () => {
|
|
this.pointerInside = false;
|
|
this.scheduleHide();
|
|
};
|
|
|
|
private readonly onFocusIn = () => {
|
|
this.reveal();
|
|
};
|
|
|
|
private readonly onFocusOut = () => {
|
|
window.setTimeout(() => this.scheduleHide(), 0);
|
|
};
|
|
|
|
private readonly onPointerMove = (event: PointerEvent) => {
|
|
if (!this.desktopMediaQuery.matches || !this.shouldBeHidden()) {
|
|
this.reveal();
|
|
return;
|
|
}
|
|
|
|
if (this.isPointerOverDock(event.clientX, event.clientY)) {
|
|
this.pointerInside = true;
|
|
this.reveal();
|
|
return;
|
|
}
|
|
|
|
this.pointerInside = false;
|
|
|
|
if (this.isHidden) {
|
|
if (this.isNearViewportBottom(event.clientY)) {
|
|
this.reveal();
|
|
this.scheduleHide();
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.scheduleHide();
|
|
};
|
|
|
|
private readonly onVisibilityContextChange = () => {
|
|
this.scheduleHide();
|
|
};
|
|
|
|
private scheduleHide(): void {
|
|
if (!this.canAutoHide) {
|
|
this.clearHideTimeout();
|
|
this.reveal();
|
|
return;
|
|
}
|
|
|
|
if (this.hideTimeout !== undefined) {
|
|
return;
|
|
}
|
|
|
|
this.hideTimeout = window.setTimeout(() => {
|
|
this.hideTimeout = undefined;
|
|
if (this.canAutoHide) {
|
|
this.hide();
|
|
}
|
|
}, appConfig.menuHider.hideDelayMs);
|
|
}
|
|
|
|
private reveal(): void {
|
|
if (!this.isHidden && this.hideTimeout === undefined) {
|
|
return;
|
|
}
|
|
|
|
this.clearHideTimeout();
|
|
this.isHidden = false;
|
|
this.element.classList.remove('menu-hidden');
|
|
}
|
|
|
|
private hide(): void {
|
|
this.isHidden = true;
|
|
this.element.classList.add('menu-hidden');
|
|
}
|
|
|
|
private clearHideTimeout(): void {
|
|
if (this.hideTimeout === undefined) {
|
|
return;
|
|
}
|
|
|
|
window.clearTimeout(this.hideTimeout);
|
|
this.hideTimeout = undefined;
|
|
}
|
|
|
|
private isPointerOverDock(clientX: number, clientY: number): boolean {
|
|
const rect = this.element.getBoundingClientRect();
|
|
return (
|
|
clientX >= rect.left &&
|
|
clientX <= rect.right &&
|
|
clientY >= rect.top &&
|
|
clientY <= rect.bottom
|
|
);
|
|
}
|
|
|
|
private isNearViewportBottom(clientY: number): boolean {
|
|
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
return clientY >= viewportHeight - appConfig.menuHider.bottomRevealDistancePx;
|
|
}
|
|
}
|