fleeting-garden/src/page/menu-hider.ts
Andras Schmelczer c40c5d97db
Some checks failed
Check & deploy / build (pull_request) Failing after 1m16s
Final clean up
2026-05-24 10:52:20 +01:00

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;
}
}