feat: Add active property to navigation elements, code cleanup

This commit is contained in:
Aleksi Lassila
2024-03-23 22:53:33 +02:00
parent fafb2e1345
commit 652894fcc9
7 changed files with 75 additions and 31 deletions

View File

@@ -8,6 +8,8 @@
export let focusOnMount = false;
export let debugOutline = false;
export let active = true;
export let navigationActions: NavigationActions = {};
const { registerer, ...rest } = new Selectable(name)
@@ -21,6 +23,8 @@
export let tag = 'div';
$: container.setIsActive(active);
onMount(() => {
rest.container._initializeSelectable();
@@ -37,7 +41,7 @@
<svelte:element
this={tag}
on:click
tabindex="0"
tabindex={active ? 0 : -1}
{...$$restProps}
class={classNames($$restProps.class, {
'outline-none': debugOutline === false

View File

@@ -2,7 +2,7 @@
import { getTmdbPerson } from '../../lib/apis/tmdb/tmdbApi';
import Carousel from '../../lib/components/Carousel/Carousel.svelte';
import CarouselPlaceholderItems from '../../lib/components/Carousel/CarouselPlaceholderItems.svelte';
import Poster from '../../lib/components/Poster/Poster.svelte';
import Poster from '../components/Card/Card.svelte';
import TitlePageLayout from '../../lib/components/TitlePageLayout/TitlePageLayout.svelte';
import FacebookIcon from '../../lib/components/svgs/FacebookIcon.svelte';
import ImdbIcon from '../../lib/components/svgs/ImdbIcon.svelte';

View File

@@ -2,7 +2,6 @@
import classNames from 'classnames';
import PlayButton from '../PlayButton.svelte';
import ProgressBar from '../ProgressBar.svelte';
// import { playerState } from '../VideoPlayer/VideoPlayer';
import LazyImg from '../LazyImg.svelte';
import { Star } from 'radix-icons-svelte';
import type { TitleType } from '../../types';
@@ -21,6 +20,7 @@
export let rating: number | undefined = undefined;
export let progress = 0;
export let focusable = true;
export let shadow = false;
export let size: 'dynamic' | 'md' | 'lg' | 'sm' = 'md';
export let orientation: 'portrait' | 'landscape' = 'landscape';
@@ -29,6 +29,7 @@
</script>
<Container
active={focusable}
on:click={() => {
if (openInModal) {
if (tmdbId) {

View File

@@ -8,7 +8,7 @@
import { ChevronRight, DotFilled } from 'radix-icons-svelte';
import CardPlaceholder from '../Card/CardPlaceholder.svelte';
import classNames from 'classnames';
import Poster from '../Poster/Poster.svelte';
import Card from '../Card/Card.svelte';
import { TMDB_POSTER_SMALL } from '../../constants';
export let items: Promise<ShowcaseItemProps[]> = Promise.resolve([]);
@@ -40,13 +40,13 @@
<Container class="h-screen pl-16 flex flex-col relative">
<HeroShowcaseBackground {items} index={showcaseIndex} />
<Container
class="flex-1 px-8 flex overflow-hidden z-10"
navigationActions={{
right: onNext,
left: onPrevious,
up: () => Selectable.focusLeft() || true
}}
>
/>
<div class="flex-1 px-8 flex overflow-hidden z-10">
<div class="flex flex-1">
{#await items}
<div class="flex-1 flex items-end">
@@ -70,7 +70,11 @@
{@const item = items[showcaseIndex]}
<div class="flex-1 flex items-end">
<div class="mr-8">
<Poster orientation="portrait" backdropUrl={TMDB_POSTER_SMALL + item.posterUrl} />
<Card
focusable={false}
orientation="portrait"
backdropUrl={TMDB_POSTER_SMALL + item.posterUrl}
/>
</div>
<div class="flex flex-col">
<div
@@ -120,7 +124,7 @@
<p>{error.message}</p>
{/await}
</div>
</Container>
</div>
<Container class="z-10">
<slot />
</Container>

View File

@@ -27,14 +27,14 @@
<div on:click={() => onJump(i)}>
<div
class={classNames(
'cursor-pointer transition-transform hover:scale-125 hover:opacity-50 p-2.5',
'cursor-pointer transition-transform hover:scale-125 hover:opacity-50 p-1.5 xl:p-2.5',
{
'opacity-50': i === index,
'opacity-20': i !== index
}
)}
>
<div class={'bg-zinc-200 rounded-full w-2.5 h-2.5'} />
<div class={'bg-zinc-200 rounded-full w-2 h-2 xl:w-2.5 xl:h-2.5'} />
</div>
</div>
{/each}

View File

@@ -5,7 +5,7 @@
import { settings } from '../stores/settings.store';
import type { TitleType } from '../types';
import type { ComponentProps } from 'svelte';
import Poster from '../components/Poster/Poster.svelte';
import Poster from '../components/Card/Card.svelte';
import { getJellyfinItems, type JellyfinItem } from '../apis/jellyfin/jellyfinApi';
import { jellyfinItemsStore } from '../stores/data.store';
import Carousel from '../components/Carousel/Carousel.svelte';

View File

@@ -26,6 +26,7 @@ export class Selectable {
private focusByDefault: boolean = false;
private isInitialized: boolean = false;
private navigationActions: NavigationActions = {};
private isActive: boolean = true;
private direction: FlowDirection = 'vertical';
@@ -69,14 +70,45 @@ export class Selectable {
}
focus() {
function updateFocusIndex(currentSelectable: Selectable, selectable?: Selectable) {
if (selectable) {
const index = currentSelectable.children.indexOf(selectable);
currentSelectable.focusIndex.update((prev) => (index === -1 ? prev : index));
}
if (currentSelectable.parent) {
updateFocusIndex(currentSelectable.parent, currentSelectable);
}
}
if (this.children.length > 0) {
this.children[get(this.focusIndex)]?.focus();
const focusIndex = get(this.focusIndex);
if (this.children[focusIndex]?.isFocusable()) {
this.children[focusIndex].focus();
} else {
let i = focusIndex;
while (i < this.children.length) {
if (this.children[i].isFocusable()) {
this.children[i].focus();
return;
}
i++;
}
i = focusIndex - 1;
while (i >= 0) {
if (this.children[i].isFocusable()) {
this.children[i].focus();
return;
}
i--;
}
}
} else if (this.htmlElement) {
this.htmlElement.focus({ preventScroll: true });
// this.htmlElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
this.scrollIntoView(50);
Selectable.focusedObject.set(this);
this.updateFocusIndex();
updateFocusIndex(this);
}
}
@@ -98,17 +130,12 @@ export class Selectable {
}
}
updateFocusIndex(selectable?: Selectable) {
if (selectable) {
const index = this.children.indexOf(selectable);
this.focusIndex.update((prev) => (index === -1 ? prev : index));
}
if (this.parent) {
this.parent.updateFocusIndex(this);
}
}
/**
* @returns {boolean} whether the selectable is focusable
*/
isFocusable(): boolean {
if (!this.isActive) return false;
isFocusable() {
if (this.htmlElement) {
return this.htmlElement.tabIndex >= 0;
} else {
@@ -118,20 +145,23 @@ export class Selectable {
}
}
}
return false;
}
getFocusableNeighbor(direction: Direction): Selectable | undefined {
const canLoop =
const focusIndex = get(this.focusIndex);
const canCycleSiblings =
(this.direction === 'vertical' &&
((direction === 'up' && get(this.focusIndex) !== 0) ||
(direction === 'down' && get(this.focusIndex) !== this.children.length - 1))) ||
((direction === 'up' && focusIndex !== 0) ||
(direction === 'down' && focusIndex !== this.children.length - 1))) ||
(this.direction === 'horizontal' &&
((direction === 'left' && get(this.focusIndex) !== 0) ||
(direction === 'right' && get(this.focusIndex) !== this.children.length - 1)));
((direction === 'left' && focusIndex !== 0) ||
(direction === 'right' && focusIndex !== this.children.length - 1)));
if (this.children.length > 0 && canLoop) {
if (this.children.length > 0 && canCycleSiblings) {
if (direction === 'up' || direction === 'left') {
let index = get(this.focusIndex) - 1;
let index = focusIndex - 1;
while (index >= 0) {
if (this.children[index].isFocusable()) {
return this.children[index];
@@ -139,7 +169,7 @@ export class Selectable {
index--;
}
} else if (direction === 'down' || direction === 'right') {
let index = get(this.focusIndex) + 1;
let index = focusIndex + 1;
while (index < this.children.length) {
if (this.children[index].isFocusable()) {
return this.children[index];
@@ -315,6 +345,11 @@ export class Selectable {
getNavigationActions(): NavigationActions {
return this.navigationActions;
}
setIsActive(isActive: boolean) {
this.isActive = isActive;
return this;
}
}
export function handleKeyboardNavigation(event: KeyboardEvent) {