|
|
|
@ -23,6 +23,7 @@ import { NAVIGATION_MENU_LANGS } from './langs';
|
|
|
|
|
|
|
|
|
|
|
|
import { navigationMenuMorfo } from '../../../morfo/components/navigation-menu';
|
|
|
|
import { navigationMenuMorfo } from '../../../morfo/components/navigation-menu';
|
|
|
|
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
|
|
|
|
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
|
|
|
|
|
|
|
|
import type { TimerHandle } from '$timer';
|
|
|
|
|
|
|
|
|
|
|
|
const attrs = createAttrs(navigationMenuMorfo);
|
|
|
|
const attrs = createAttrs(navigationMenuMorfo);
|
|
|
|
|
|
|
|
|
|
|
|
@ -62,9 +63,9 @@ export class NavigationMenuProvider {
|
|
|
|
return new NavigationMenuProvider(opts);
|
|
|
|
return new NavigationMenuProvider(opts);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private openTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
private openTimer: TimerHandle | null = null;
|
|
|
|
private closeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
private closeTimer: TimerHandle | null = null;
|
|
|
|
private skipDelayTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
private skipDelayTimer: TimerHandle | null = null;
|
|
|
|
/** True while the "skip delay" window is open — i.e. a submenu was just closed
|
|
|
|
/** True while the "skip delay" window is open — i.e. a submenu was just closed
|
|
|
|
* and the user is likely hovering another trigger right after. During this
|
|
|
|
* and the user is likely hovering another trigger right after. During this
|
|
|
|
* window, `scheduleOpen` uses the shorter `skipDelayDuration`. */
|
|
|
|
* window, `scheduleOpen` uses the shorter `skipDelayDuration`. */
|
|
|
|
@ -107,9 +108,8 @@ export class NavigationMenuProvider {
|
|
|
|
|
|
|
|
|
|
|
|
$effect(() => {
|
|
|
|
$effect(() => {
|
|
|
|
return () => {
|
|
|
|
return () => {
|
|
|
|
if (this.openTimer) clearTimeout(this.openTimer);
|
|
|
|
this.clearTimers();
|
|
|
|
if (this.closeTimer) clearTimeout(this.closeTimer);
|
|
|
|
this.clearSkipDelayTimer();
|
|
|
|
if (this.skipDelayTimer) clearTimeout(this.skipDelayTimer);
|
|
|
|
|
|
|
|
};
|
|
|
|
};
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -129,11 +129,11 @@ export class NavigationMenuProvider {
|
|
|
|
|
|
|
|
|
|
|
|
private clearTimers() {
|
|
|
|
private clearTimers() {
|
|
|
|
if (this.openTimer) {
|
|
|
|
if (this.openTimer) {
|
|
|
|
clearTimeout(this.openTimer);
|
|
|
|
this.openTimer.cancel();
|
|
|
|
this.openTimer = null;
|
|
|
|
this.openTimer = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (this.closeTimer) {
|
|
|
|
if (this.closeTimer) {
|
|
|
|
clearTimeout(this.closeTimer);
|
|
|
|
this.closeTimer.cancel();
|
|
|
|
this.closeTimer = null;
|
|
|
|
this.closeTimer = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -158,12 +158,18 @@ export class NavigationMenuProvider {
|
|
|
|
|
|
|
|
|
|
|
|
private armSkipDelay() {
|
|
|
|
private armSkipDelay() {
|
|
|
|
this.inSkipDelayWindow = true;
|
|
|
|
this.inSkipDelayWindow = true;
|
|
|
|
if (this.skipDelayTimer) clearTimeout(this.skipDelayTimer);
|
|
|
|
this.clearSkipDelayTimer();
|
|
|
|
// Skip window lasts the length of `openDelay` — a reasonable "grace
|
|
|
|
// Skip window lasts the length of `openDelay` — a reasonable "grace
|
|
|
|
// period" during which the user is presumed to still be navigating.
|
|
|
|
// period" during which the user is presumed to still be navigating.
|
|
|
|
this.skipDelayTimer = setTimeout(() => {
|
|
|
|
this.skipDelayTimer = this.soma.uix.timers.schedule(
|
|
|
|
|
|
|
|
this.skipDelayTimerKey,
|
|
|
|
|
|
|
|
this.opts.openDelay.current,
|
|
|
|
|
|
|
|
() => {
|
|
|
|
|
|
|
|
this.skipDelayTimer = null;
|
|
|
|
this.inSkipDelayWindow = false;
|
|
|
|
this.inSkipDelayWindow = false;
|
|
|
|
}, this.opts.openDelay.current);
|
|
|
|
},
|
|
|
|
|
|
|
|
{ replace: true, meta: { component: 'navigation-menu', action: 'skip-delay' } }
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
@ -186,23 +192,58 @@ export class NavigationMenuProvider {
|
|
|
|
this.openNow(value);
|
|
|
|
this.openNow(value);
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.openTimer = setTimeout(() => this.openNow(value), delay);
|
|
|
|
this.openTimer = this.soma.uix.timers.schedule(
|
|
|
|
|
|
|
|
this.openTimerKey,
|
|
|
|
|
|
|
|
delay,
|
|
|
|
|
|
|
|
() => {
|
|
|
|
|
|
|
|
this.openTimer = null;
|
|
|
|
|
|
|
|
this.openNow(value);
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
{ replace: true, meta: { component: 'navigation-menu', action: 'open-delay' } }
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Schedule a close (grace period). */
|
|
|
|
/** Schedule a close (grace period). */
|
|
|
|
scheduleClose() {
|
|
|
|
scheduleClose() {
|
|
|
|
this.clearTimers();
|
|
|
|
this.clearTimers();
|
|
|
|
this.closeTimer = setTimeout(() => this.closeNow(), this.opts.closeDelay.current);
|
|
|
|
this.closeTimer = this.soma.uix.timers.schedule(
|
|
|
|
|
|
|
|
this.closeTimerKey,
|
|
|
|
|
|
|
|
this.opts.closeDelay.current,
|
|
|
|
|
|
|
|
() => {
|
|
|
|
|
|
|
|
this.closeTimer = null;
|
|
|
|
|
|
|
|
this.closeNow();
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
{ replace: true, meta: { component: 'navigation-menu', action: 'close-delay' } }
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Cancel a pending close (e.g. pointer entered the content). */
|
|
|
|
/** Cancel a pending close (e.g. pointer entered the content). */
|
|
|
|
cancelClose() {
|
|
|
|
cancelClose() {
|
|
|
|
if (this.closeTimer) {
|
|
|
|
if (this.closeTimer) {
|
|
|
|
clearTimeout(this.closeTimer);
|
|
|
|
this.closeTimer.cancel();
|
|
|
|
this.closeTimer = null;
|
|
|
|
this.closeTimer = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private clearSkipDelayTimer() {
|
|
|
|
|
|
|
|
if (this.skipDelayTimer) {
|
|
|
|
|
|
|
|
this.skipDelayTimer.cancel();
|
|
|
|
|
|
|
|
this.skipDelayTimer = null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private get openTimerKey() {
|
|
|
|
|
|
|
|
return `soma:navigation-menu:${this.opts.id.current}:open`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private get closeTimerKey() {
|
|
|
|
|
|
|
|
return `soma:navigation-menu:${this.opts.id.current}:close`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private get skipDelayTimerKey() {
|
|
|
|
|
|
|
|
return `soma:navigation-menu:${this.opts.id.current}:skip-delay`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
registerTrigger(value: string, el: HTMLElement) {
|
|
|
|
registerTrigger(value: string, el: HTMLElement) {
|
|
|
|
this.triggerRefs.set(value, el);
|
|
|
|
this.triggerRefs.set(value, el);
|
|
|
|
return () => {
|
|
|
|
return () => {
|
|
|
|
|