fix: Containers mounting asynchronously being in wrong focus order
This commit is contained in:
@@ -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',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user