feat: Add active property to navigation elements, code cleanup
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user