Improve ISAC libraries, roles, and public profiles
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { tick } from 'svelte';
|
||||
import DatabaseItemCard from './DatabaseItemCard.svelte';
|
||||
import ItemIcon from './ItemIcon.svelte';
|
||||
import DatabaseFacts from './DatabaseFacts.svelte';
|
||||
import type { DatabaseRelatedItem } from '$lib/database/items';
|
||||
|
||||
let {
|
||||
@@ -13,14 +14,45 @@
|
||||
emptyLabel?: string;
|
||||
} = $props();
|
||||
let selected = $state<DatabaseRelatedItem | null>(null);
|
||||
let trigger = $state<HTMLButtonElement | null>(null);
|
||||
let closeButton = $state<HTMLButtonElement | null>(null);
|
||||
let dialog = $state<HTMLDivElement | null>(null);
|
||||
const openPreview = async (item: DatabaseRelatedItem, button: HTMLButtonElement) => {
|
||||
selected = item;
|
||||
trigger = button;
|
||||
await tick();
|
||||
closeButton?.focus();
|
||||
};
|
||||
const closePreview = async () => {
|
||||
const previousTrigger = trigger;
|
||||
selected = null;
|
||||
trigger = null;
|
||||
await tick();
|
||||
previousTrigger?.focus();
|
||||
};
|
||||
const trapDialogFocus = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'Tab' || !dialog) return;
|
||||
const focusable = [...dialog.querySelectorAll<HTMLElement>('a[href],button:not(:disabled),[tabindex]:not([tabindex="-1"])')]
|
||||
.filter((element) => !element.hasAttribute('hidden'));
|
||||
const first = focusable[0];
|
||||
const last = focusable.at(-1);
|
||||
if (!first || !last) return;
|
||||
if (event.shiftKey && document.activeElement === first) {
|
||||
event.preventDefault();
|
||||
last.focus();
|
||||
} else if (!event.shiftKey && document.activeElement === last) {
|
||||
event.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={(event) => { if (event.key === 'Escape') selected = null; }}/>
|
||||
<svelte:window onkeydown={(event) => { if (event.key === 'Escape' && selected) void closePreview(); }}/>
|
||||
|
||||
<div class="item-icon-rail" aria-label={`${label}: ${items.length ? items.map((item) => item.name).join(', ') : emptyLabel}`}>
|
||||
{#if items.length}
|
||||
{#each items as item}
|
||||
<button type="button" title={`View ${item.name}`} aria-label={`View ${item.name} details`} onclick={() => selected = item}>
|
||||
<button type="button" title={`View ${item.name}`} aria-label={`View ${item.name} details`} aria-haspopup={item.preview ? 'dialog' : undefined} onclick={(event) => void openPreview(item, event.currentTarget)}>
|
||||
<ItemIcon src={item.iconUrl} alt=""/>
|
||||
</button>
|
||||
{/each}
|
||||
@@ -30,22 +62,10 @@
|
||||
</div>
|
||||
|
||||
{#if selected?.preview}
|
||||
<div class="database-preview-backdrop" role="presentation" onclick={(event) => { if (event.target === event.currentTarget) selected = null; }}>
|
||||
<div class="database-preview-dialog" role="dialog" aria-modal="true" aria-labelledby={`database-preview-${selected.id}`}>
|
||||
<header><div><small>DATABASE QUICK VIEW</small><strong id={`database-preview-${selected.id}`}>{selected.preview.name}</strong></div><button type="button" aria-label="Close details" onclick={() => selected = null}>×</button></header>
|
||||
<article class="database-item-card">
|
||||
<header>
|
||||
<div class="database-item-icon"><ItemIcon src={selected.preview.iconUrl} alt=""/></div>
|
||||
<div><small>{selected.preview.kicker}</small><h2>{selected.preview.name}</h2></div>
|
||||
</header>
|
||||
{#if selected.preview.badges?.length}<div class="database-item-badges">{#each selected.preview.badges as badge}<span class={badge.tone ?? ''}>{badge.label}</span>{/each}</div>{/if}
|
||||
<DatabaseFacts facts={selected.preview.facts} layout={selected.preview.factLayout}/>
|
||||
{#if selected.preview.lines?.length}<div class="database-item-lines">{#each selected.preview.lines as line}<div><span>{line.label}</span><b>{line.value}</b></div>{/each}</div>{/if}
|
||||
{#if selected.preview.description}<p class:data-description={selected.preview.descriptionStyle === 'data'}>{selected.preview.description}</p>{/if}
|
||||
{#if selected.preview.secondaryDescription}<p class="secondary-description" class:data-description={selected.preview.descriptionStyle === 'data'}>{selected.preview.secondaryDescription}</p>{/if}
|
||||
{#if selected.preview.afterLines?.length}<div class="database-item-lines after-description">{#each selected.preview.afterLines as line}<div><span>{line.label}</span><b>{line.value}</b></div>{/each}</div>{/if}
|
||||
{#if selected.preview.actions?.length}<footer class="database-item-actions">{#each selected.preview.actions as action}<a href={action.href}>{action.label} →</a>{/each}</footer>{/if}
|
||||
</article>
|
||||
<div class="database-preview-backdrop" role="presentation" onclick={(event) => { if (event.target === event.currentTarget) void closePreview(); }}>
|
||||
<div class="database-preview-dialog" bind:this={dialog} role="dialog" aria-modal="true" aria-labelledby={`database-preview-${selected.id}`} tabindex="-1" onkeydown={trapDialogFocus}>
|
||||
<header><div><small>DATABASE QUICK VIEW</small><strong id={`database-preview-${selected.id}`}>{selected.preview.name}</strong></div><button type="button" bind:this={closeButton} aria-label="Close details" onclick={() => void closePreview()}>×</button></header>
|
||||
<DatabaseItemCard item={selected.preview}/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user