Added store support for navigation

This commit is contained in:
Aleksi Lassila
2024-01-06 14:01:17 +02:00
parent def6df4074
commit 75775b2f25
3 changed files with 65 additions and 18 deletions

View File

@@ -1,3 +1,5 @@
import { derived, get, type Readable, type Writable, writable } from 'svelte/store';
export type Registerer = (htmlElement: HTMLElement) => { destroy: () => void };
export type Direction = 'up' | 'down' | 'left' | 'right';
@@ -19,9 +21,26 @@ export class Container {
private direction: FlowDirection = 'vertical';
private focusIndex: number = 0;
static focusedObject: Writable<Container | undefined> = writable(undefined);
focusIndex: Writable<number> = writable(0);
hasFocus: Readable<boolean> = derived(Container.focusedObject, ($focusedObject) => {
console.log('Updating hasFocus', $focusedObject, this);
return $focusedObject === this;
});
hasFocusWithin: Readable<boolean> = derived(Container.focusedObject, ($focusedObject) => {
let currentContainer: Container | undefined = $focusedObject;
while (currentContainer) {
if (currentContainer === this) {
return true;
}
currentContainer = currentContainer.parent;
}
return false;
});
static focusedObject: Container;
static objects = new Map<symbol, Container>();
constructor(name: string = '') {
@@ -48,11 +67,11 @@ export class Container {
focus() {
if (this.children.length > 0) {
this.children[this.focusIndex]?.focus();
this.children[get(this.focusIndex)]?.focus();
} else if (this.htmlElement) {
this.htmlElement.focus({ preventScroll: true });
this.htmlElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
Container.focusedObject = this;
Container.focusedObject.set(this);
this.updateFocusIndex();
}
}
@@ -60,7 +79,7 @@ export class Container {
updateFocusIndex(container?: Container) {
if (container) {
const index = this.children.indexOf(container);
this.focusIndex = index === -1 ? this.focusIndex : index;
this.focusIndex.update((prev) => (index === -1 ? prev : index));
}
if (this.parent) {
this.parent.updateFocusIndex(this);
@@ -82,14 +101,14 @@ export class Container {
getFocusableNeighbor(direction: Direction): Container | undefined {
const canLoop =
(this.direction === 'vertical' &&
((direction === 'up' && this.focusIndex !== 0) ||
(direction === 'down' && this.focusIndex !== this.children.length - 1))) ||
((direction === 'up' && get(this.focusIndex) !== 0) ||
(direction === 'down' && get(this.focusIndex) !== this.children.length - 1))) ||
(this.direction === 'horizontal' &&
((direction === 'left' && this.focusIndex !== 0) ||
(direction === 'right' && this.focusIndex !== this.children.length - 1)));
((direction === 'left' && get(this.focusIndex) !== 0) ||
(direction === 'right' && get(this.focusIndex) !== this.children.length - 1)));
if (this.children.length > 0 && canLoop) {
if (direction === 'up' || direction === 'left') {
let index = this.focusIndex - 1;
let index = get(this.focusIndex) - 1;
while (index >= 0) {
if (this.children[index].isFocusable()) {
return this.children[index];
@@ -97,7 +116,7 @@ export class Container {
index--;
}
} else if (direction === 'down' || direction === 'right') {
let index = this.focusIndex + 1;
let index = get(this.focusIndex) + 1;
while (index < this.children.length) {
if (this.children[index].isFocusable()) {
return this.children[index];
@@ -114,7 +133,7 @@ export class Container {
giveFocus(direction: Direction) {
const neighbor = this.getFocusableNeighbor(direction);
console.log('Giving focus to', direction, 'neighbor: ', neighbor?.name, neighbor);
// console.log('Giving focus to', direction, 'neighbor: ', neighbor?.name, neighbor);
if (neighbor) {
neighbor.focus();
return true;
@@ -129,7 +148,7 @@ export class Container {
this.createChild().addHtmlElement(htmlElement);
if (!Container.focusedObject && this.shouldFocusByDefault()) {
if (!get(Container.focusedObject) && this.shouldFocusByDefault()) {
this.focus();
}
@@ -176,14 +195,14 @@ export class Container {
}
export function handleKeyboardNavigation(event: KeyboardEvent) {
const currentlyFocusedObject = Container.focusedObject;
const currentlyFocusedObject = get(Container.focusedObject);
if (!currentlyFocusedObject) {
console.error('No focused object!!!');
return;
}
console.log('Currently focused object: ', currentlyFocusedObject.name, currentlyFocusedObject);
// console.log('Currently focused object: ', currentlyFocusedObject.name, currentlyFocusedObject);
if (event.key === 'ArrowUp') {
if (currentlyFocusedObject.giveFocus('up')) event.preventDefault();

View File

@@ -1,15 +1,37 @@
<script lang="ts">
import CardPlaceholder from '../Card/CardPlaceholder.svelte';
import { Container } from '../../actions/focusAction';
import classNames from 'classnames';
export let size: 'dynamic' | 'md' | 'lg' = 'md';
export let orientation: 'landscape' | 'portrait' = 'landscape';
export let container: Container;
let registerer = container.createChild('carousel').setDirection('horizontal').getRegisterer();
let carousel = container.createChild('carousel').setDirection('horizontal');
let focusIndexStore = carousel.focusIndex;
let focusWithinStore = carousel.hasFocusWithin;
Container.focusedObject.subscribe((fo) => console.log('focusedObject', fo));
carousel.hasFocus.subscribe((hf) => console.log('hasFocus', hf));
let registerer = carousel.getRegisterer();
</script>
<p
class={classNames({
'bg-blue-500': $focusWithinStore
})}
>
Index: {$focusIndexStore}
</p>
{#each Array(10) as _, i (i)}
<div tabindex="0" use:registerer>
<div
tabindex="0"
use:registerer
class={classNames({
'bg-red-500': $focusIndexStore === i && $focusWithinStore
})}
>
<CardPlaceholder {size} index={i} {orientation} />
</div>
{/each}

View File

@@ -2,11 +2,17 @@
import { Container } from '../actions/focusAction';
import Carousel from '../components/Carousel/Carousel.svelte';
import CarouselPlaceholderItems from '../components/Carousel/CarouselPlaceholderItems.svelte';
import classNames from 'classnames';
export let container: Container;
const focusWithin = container.hasFocusWithin;
</script>
<div class="flex flex-col">
<div
class={classNames('flex flex-col', {
'bg-green-100': $focusWithin
})}
>
<Carousel>
<CarouselPlaceholderItems {container} />
</Carousel>