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: {
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'prefer-const': 'warn',
}
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -287,6 +287,69 @@ export class Selectable {
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) {
console.error('No html element found for', this);
return;
@@ -300,7 +363,9 @@ export class Selectable {
? getParentSelectable(this.htmlElement.parentElement)
: undefined;
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 {
console.error('No parent selectable found for', this.htmlElement);
}
@@ -311,7 +376,7 @@ export class Selectable {
}
_unmountContainer() {
// console.log('Unmounting selectable', this);
console.log('Unmounting selectable', this);
const isFocusedWithin = get(this.hasFocusWithin);
if (this.htmlElement) {
@@ -341,6 +406,7 @@ export class Selectable {
destroy: () => {
selectable.parent?.removeChild(selectable);
Selectable.objects.delete(htmlElement);
console.log('destroying', htmlElement, selectable);
}
};
};
@@ -375,14 +441,23 @@ export class Selectable {
};
}
private addChild(child: Selectable) {
this.children.push(child);
private addChild(child: Selectable, index?: number) {
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;
return this;
}
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);
}