fix: Containers mounting asynchronously being in wrong focus order

This commit is contained in:
Aleksi Lassila
2024-04-04 17:16:44 +03:00
parent df1623eb53
commit 754227737b
6 changed files with 94 additions and 15 deletions

View File

@@ -29,7 +29,8 @@ module.exports = {
], ],
rules: { rules: {
'@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'warn',
'prefer-const': 'warn',
} }
}; };

View File

@@ -1,10 +1,10 @@
<svelte:options accessors />
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { type NavigationActions, type RevealStrategy, Selectable } from './lib/selectable'; import { type NavigationActions, type RevealStrategy, Selectable } from './lib/selectable';
import classNames from 'classnames'; import classNames from 'classnames';
export let element: HTMLElement;
export let name: string = ''; export let name: string = '';
export let direction: 'vertical' | 'horizontal' | 'grid' = 'vertical'; export let direction: 'vertical' | 'horizontal' | 'grid' = 'vertical';
export let gridCols: number = 0; export let gridCols: number = 0;
@@ -60,7 +60,6 @@
'outline-none': debugOutline === false 'outline-none': debugOutline === false
})} })}
use:registerer use:registerer
bind:this={element}
> >
<slot hasFocus={$hasFocus} hasFocusWithin={$hasFocusWithin} focusIndex={$focusIndex} /> <slot hasFocus={$hasFocus} hasFocusWithin={$hasFocusWithin} focusIndex={$focusIndex} />
</svelte:element> </svelte:element>

View File

@@ -45,7 +45,11 @@
</div> </div>
<div class="relative"> <div class="relative">
<Container childrenRevealStrategy={scrollWithOffset('left', 64 + 16)} direction="horizontal"> <Container
childrenRevealStrategy={scrollWithOffset('left', 64 + 16)}
direction="horizontal"
navigationActions={{ left: () => true }}
>
<div <div
class={classNames( class={classNames(
'flex overflow-x-scroll items-center overflow-y-visible relative scrollbar-hide', 'flex overflow-x-scroll items-center overflow-y-visible relative scrollbar-hide',

View File

@@ -3,7 +3,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Container from '../../../Container.svelte'; import Container from '../../../Container.svelte';
let element: HTMLDivElement; let element: Container;
let scrollX = 0; let scrollX = 0;
let maxScrollX = 0; let maxScrollX = 0;
let fadeLeft = false; let fadeLeft = false;
@@ -31,7 +31,7 @@
fadeLeft ? '' : 'black 0%, ' fadeLeft ? '' : 'black 0%, '
}black 5%, black 95%, ${fadeRight ? '' : 'black 100%, '}transparent 100%);`} }black 5%, black 95%, ${fadeRight ? '' : 'black 100%, '}transparent 100%);`}
on:scroll={updateScrollPosition} on:scroll={updateScrollPosition}
bind:element bind:this={element}
> >
<slot /> <slot />
</Container> </Container>

View File

@@ -47,7 +47,7 @@
</Container> </Container>
{/each} {/each}
</UICarousel> </UICarousel>
<Container revealStrategy={scrollWithOffset('all', 64)} class="flex"> <div class="flex">
{#each $tmdbSeasons as season} {#each $tmdbSeasons as season}
{#each season?.episodes || [] as episode} {#each season?.episodes || [] as episode}
<div class="mx-2"> <div class="mx-2">
@@ -55,6 +55,6 @@
</div> </div>
{/each} {/each}
{/each} {/each}
</Container> </div>
</Carousel> </Carousel>
{/if} {/if}

View File

@@ -287,6 +287,69 @@ export class Selectable {
else return undefined; else return undefined;
}; };
const getSiblingSelectable = (parent: Selectable): Selectable | undefined => {
const getElementTree = (start: HTMLElement, end: HTMLElement): HTMLElement[] => {
let element = start;
const elements: HTMLElement[] = [start];
while (element !== end) {
if (element.parentElement) element = element.parentElement;
else break;
elements.push(element);
}
return elements;
};
if (!this.htmlElement) return undefined;
const parentHtmlElement = parent.htmlElement;
if (!parentHtmlElement) return undefined;
const thisElementTree = getElementTree(this.htmlElement, parentHtmlElement);
let aboveSibling: Selectable | undefined = undefined;
for (const existingSibling of parent.children) {
// Does not contain this yet
if (!existingSibling.htmlElement) {
console.error('No html element found for', existingSibling);
continue;
}
const siblingElementTree: HTMLElement[] = getElementTree(
existingSibling.htmlElement,
parentHtmlElement
);
const commonParentElement = thisElementTree.find((element) =>
siblingElementTree.includes(element)
);
const thisSibling = thisElementTree.find(
(element) => element.parentElement && siblingElementTree.includes(element.parentElement)
);
const targetSibling = siblingElementTree.find(
(element) => element.parentElement && thisElementTree.includes(element.parentElement)
);
if (!thisSibling || !targetSibling || !commonParentElement) {
console.warn(
"Couldn't find common parent element",
thisSibling,
targetSibling,
commonParentElement
);
continue;
}
const allSiblingElements = Array.from(commonParentElement.children);
if (allSiblingElements.indexOf(targetSibling) < allSiblingElements.indexOf(thisSibling)) {
aboveSibling = existingSibling;
} else break;
}
return aboveSibling;
};
if (!this.htmlElement) { if (!this.htmlElement) {
console.error('No html element found for', this); console.error('No html element found for', this);
return; return;
@@ -300,7 +363,9 @@ export class Selectable {
? getParentSelectable(this.htmlElement.parentElement) ? getParentSelectable(this.htmlElement.parentElement)
: undefined; : undefined;
if (parentSelectable) { if (parentSelectable) {
parentSelectable.addChild(this); const aboveSibling = getSiblingSelectable(parentSelectable);
const index = aboveSibling ? parentSelectable.children.indexOf(aboveSibling) : undefined;
parentSelectable.addChild(this, index === undefined ? 0 : index + 1);
} else { } else {
console.error('No parent selectable found for', this.htmlElement); console.error('No parent selectable found for', this.htmlElement);
} }
@@ -311,7 +376,7 @@ export class Selectable {
} }
_unmountContainer() { _unmountContainer() {
// console.log('Unmounting selectable', this); console.log('Unmounting selectable', this);
const isFocusedWithin = get(this.hasFocusWithin); const isFocusedWithin = get(this.hasFocusWithin);
if (this.htmlElement) { if (this.htmlElement) {
@@ -341,6 +406,7 @@ export class Selectable {
destroy: () => { destroy: () => {
selectable.parent?.removeChild(selectable); selectable.parent?.removeChild(selectable);
Selectable.objects.delete(htmlElement); Selectable.objects.delete(htmlElement);
console.log('destroying', htmlElement, selectable);
} }
}; };
}; };
@@ -375,14 +441,23 @@ export class Selectable {
}; };
} }
private addChild(child: Selectable) { private addChild(child: Selectable, index?: number) {
this.children.push(child); if (index !== undefined) {
const parentFocusWithin = child.parent?.hasFocusWithin && get(child.parent?.hasFocusWithin);
if (parentFocusWithin && this.children.length && index <= get(this.focusIndex)) {
this.focusIndex.update((prev) => prev + 1);
}
this.children.splice(index, 0, child);
} else {
this.children.push(child);
}
child.parent = this; child.parent = this;
return this; return this;
} }
private removeChild(child: Selectable) { private removeChild(child: Selectable) {
if (this.children.indexOf(child) < get(this.focusIndex)) { if (this.children.indexOf(child) <= get(this.focusIndex)) {
this.focusIndex.update((prev) => prev - 1); this.focusIndex.update((prev) => prev - 1);
} }