feat: keyboard navigation to timeline (#17798)

* feat: improve focus

* feat: keyboard nav

* feat: improve focus

* typo

* test

* fix test

* lint

* bad merge

* lint

* inadvertent

* lint

* fix: flappy e2e test

* bad merge and fix tests

* use modulus in loop

* tests

* react to modal dialog refactor

* regression due to deferLayout

* Review comments

* Re-use change-date instead of new component

* bad merge

* Review comments

* rework moveFocus

* lint

* Fix outline

* use Date

* Finish up removing/reducing date parsing

* lint

* title

* strings

* Rework dates, rework earlier/later algorithm

* bad merge

* fix tests

* Fix race in scroll comp

* consolidate scroll methods

* Review comments

* console.log

* Edge cases in scroll compensation

* edge case, optimizations

* review comments

* lint

* lint

* More edge cases

* lint

---------

Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Min Idzelis
2025-05-28 09:55:14 -04:00
committed by GitHub
parent b5593823a2
commit f029910dc7
21 changed files with 1077 additions and 598 deletions
@@ -0,0 +1,78 @@
import type { AssetStore, TimelineAsset } from '$lib/stores/assets-store.svelte';
import { moveFocus } from '$lib/utils/focus-util';
import { InvocationTracker } from '$lib/utils/invocationTracker';
import { tick } from 'svelte';
const tracker = new InvocationTracker();
const getFocusedThumb = () => {
const current = document.activeElement as HTMLElement | undefined;
if (current && current.dataset.thumbnailFocusContainer !== undefined) {
return current;
}
};
export const focusNextAsset = () =>
moveFocus((element) => element.dataset.thumbnailFocusContainer !== undefined, 'next');
export const focusPreviousAsset = () =>
moveFocus((element) => element.dataset.thumbnailFocusContainer !== undefined, 'previous');
const queryHTMLElement = (query: string) => document.querySelector(query) as HTMLElement;
export const setFocusToAsset = (scrollToAsset: (asset: TimelineAsset) => boolean, asset: TimelineAsset) => {
const scrolled = scrollToAsset(asset);
if (scrolled) {
const element = queryHTMLElement(`[data-thumbnail-focus-container][data-asset="${asset.id}"]`);
element?.focus();
}
};
export const setFocusTo = async (
scrollToAsset: (asset: TimelineAsset) => boolean,
store: AssetStore,
direction: 'earlier' | 'later',
interval: 'day' | 'month' | 'year' | 'asset',
) => {
if (tracker.isActive()) {
// there are unfinished running invocations, so return early
return;
}
const thumb = getFocusedThumb();
if (!thumb) {
return direction === 'earlier' ? focusNextAsset() : focusPreviousAsset();
}
const invocation = tracker.startInvocation();
const id = thumb.dataset.asset;
if (!thumb || !id) {
invocation.endInvocation();
return;
}
const asset =
direction === 'earlier'
? await store.getEarlierAsset({ id }, interval)
: await store.getLaterAsset({ id }, interval);
if (!invocation.isStillValid()) {
return;
}
if (!asset) {
invocation.endInvocation();
return;
}
const scrolled = scrollToAsset(asset);
if (scrolled) {
await tick();
if (!invocation.isStillValid()) {
return;
}
const element = queryHTMLElement(`[data-thumbnail-focus-container][data-asset="${asset.id}"]`);
element?.focus();
}
invocation.endInvocation();
};