Skip to content

Commit 8b6b0b1

Browse files
Merge branch 'develop' into feat-lxlweb-improve-supersearch-a11y
2 parents 1d8eeb7 + 0ab14a8 commit 8b6b0b1

39 files changed

Lines changed: 612 additions & 268 deletions

‎lxl-web/eslint.config.js‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ export default ts.config(
3030
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
3131
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
3232
'no-undef': 'off',
33-
'svelte/no-useless-mustaches': 'warn'
33+
'svelte/no-useless-mustaches': 'warn',
34+
// TODO: Fix all files. temp disabled to not be flooded with errors.
35+
'svelte/no-navigation-without-resolve': 'warn'
3436
}
3537
},
3638
{

‎lxl-web/maintenance.html‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
</style>
3232

3333
<article>
34-
<h1>beta.libris.kb.se uppdateras</h1>
34+
<h1>libris.kb.se uppdateras</h1>
3535
<p>
36-
Vi läser in en ny kopia av katalogen. Tjänsten förväntas vara tillgänglig igen under vecka 6.
36+
Vi läser in en ny kopia av katalogen. Tjänsten förväntas vara tillgänglig igen inom kort!
3737
</p>
3838
</article>

‎lxl-web/src/hooks.server.ts‎

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import fs from 'fs';
2-
import { type HandleServerError, redirect, type RequestEvent } from '@sveltejs/kit';
2+
import {
3+
type HandleServerError,
4+
redirect,
5+
type RequestEvent,
6+
type ServerInit
7+
} from '@sveltejs/kit';
38
import { env } from '$env/dynamic/private';
49
import { defaultLocale, Locales } from '$lib/i18n/locales';
510
import { DERIVED_LENSES } from '$lib/types/display';
@@ -20,16 +25,24 @@ import { updateSettings } from '$lib/utils/userSettings.svelte';
2025

2126
type QualifierSuggestionsByLocale = Record<keyof typeof Locales, QualifierSuggestion2[]>;
2227
type Util = [VocabUtil, DisplayUtil, QualifierSuggestionsByLocale];
23-
let utilCache: Util | undefined;
24-
let initLibraries: boolean = false;
28+
let utilCache: Promise<Util> | undefined;
29+
30+
// Warm up caches immediately on startup instead of waiting for a request
31+
export const init: ServerInit = async () => {
32+
try {
33+
const [, displayUtil] = await loadUtilCached();
34+
startRefreshLibraries(displayUtil, defaultLocale);
35+
} catch (err) {
36+
// This is OK, handle() will retry
37+
console.error('Startup initialization failed:', err);
38+
}
39+
};
2540

2641
export const handle = async ({ event, resolve }) => {
2742
const [vocabUtil, displayUtil, qualifierSuggestionsByLocale] = await loadUtilCached();
2843

29-
if (!initLibraries) {
30-
initLibraries = true;
31-
await startRefreshLibraries(displayUtil, defaultLocale);
32-
}
44+
// Fallback in case startup init failed. No-op if refresh already started.
45+
startRefreshLibraries(displayUtil, defaultLocale);
3346

3447
event.locals.vocab = vocabUtil;
3548
event.locals.display = displayUtil;
@@ -192,9 +205,12 @@ function isValidMyLibraries(value: unknown): value is MyLibrariesType {
192205
return true;
193206
}
194207

195-
async function loadUtilCached() {
208+
function loadUtilCached() {
196209
if (!utilCache) {
197-
utilCache = await loadUtil();
210+
utilCache = loadUtil().catch((err) => {
211+
utilCache = undefined;
212+
throw err;
213+
});
198214
}
199215
return utilCache;
200216
}

‎lxl-web/src/lib/components/Citations.svelte‎

Lines changed: 45 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -83,52 +83,54 @@
8383
<li
8484
class="bg-page border-r-neutral border-b-neutral flex flex-col gap-2 rounded-sm border-r border-b p-4 text-xs"
8585
>
86-
<h2 class="mb-2 text-sm font-medium" id={format.key}>
87-
{format.fullName || format.name}
88-
</h2>
89-
<svelte:element
90-
this={isFileFormat ? 'pre' : 'p'}
91-
class={[
92-
'mb-2 block',
93-
isFileFormat && 'text-2xs overflow-x-scroll [scrollbar-width:thin]'
94-
]}
95-
>
96-
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
97-
{@html format.citation}
98-
</svelte:element>
99-
<div class="flex gap-2">
100-
<!-- copy button -->
101-
<button
102-
class="btn btn-accent"
103-
onclick={() =>
104-
handleCopyCitation(
105-
format.citation,
106-
isFileFormat ? 'text/plain' : 'text/html',
107-
() => {
108-
wasCopied = {};
109-
wasCopied[format.key] = true;
110-
}
111-
)}
86+
<article aria-labelledby={format.key}>
87+
<h2 class="mb-2 text-sm font-medium" id={format.key}>
88+
{format.fullName || format.name}
89+
</h2>
90+
<svelte:element
91+
this={isFileFormat ? 'pre' : 'p'}
92+
class={[
93+
'mb-2 block',
94+
isFileFormat && 'text-2xs overflow-x-scroll [scrollbar-width:thin]'
95+
]}
11296
>
113-
<BiCopy />
114-
{#if wasCopied[format.key]}
115-
{page.data.t('general.copied')}
116-
{:else}
117-
{page.data.t('general.copyToClipboard')}
118-
{/if}
119-
</button>
120-
{#if isFileFormat && id}
121-
<!-- download button -->
122-
<a
97+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
98+
{@html format.citation}
99+
</svelte:element>
100+
<div class="flex gap-2">
101+
<!-- copy button -->
102+
<button
123103
class="btn btn-accent"
124-
download={`${id}.${format.fileFormat}`}
125-
href={`/api/${page.data.locale}/cite?id=${id}&format=${format.key}`}
126-
>
127-
<BiDownload />
128-
{page.data.t('citations.saveAsFile')}</a
104+
onclick={() =>
105+
handleCopyCitation(
106+
format.citation,
107+
isFileFormat ? 'text/plain' : 'text/html',
108+
() => {
109+
wasCopied = {};
110+
wasCopied[format.key] = true;
111+
}
112+
)}
129113
>
130-
{/if}
131-
</div>
114+
<BiCopy />
115+
{#if wasCopied[format.key]}
116+
{page.data.t('general.copied')}
117+
{:else}
118+
{page.data.t('general.copyToClipboard')}
119+
{/if}
120+
</button>
121+
{#if isFileFormat && id}
122+
<!-- download button -->
123+
<a
124+
class="btn btn-accent"
125+
download={`${id}.${format.fileFormat}`}
126+
href={`/api/${page.data.locale}/cite?id=${id}&format=${format.key}`}
127+
>
128+
<BiDownload />
129+
{page.data.t('citations.saveAsFile')}</a
130+
>
131+
{/if}
132+
</div>
133+
</article>
132134
</li>
133135
{/each}
134136
</ul>

‎lxl-web/src/lib/components/Draggable.svelte‎

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
side: 'left' | 'right';
1313
minWidth?: number;
1414
maxWidth?: number;
15+
step?: number;
1516
isDragging?: boolean;
1617
disabled?: boolean;
1718
collapseWidth?: number;
@@ -25,6 +26,7 @@
2526
isDragging = $bindable(false),
2627
minWidth = 200,
2728
maxWidth = 400,
29+
step = 20,
2830
collapseWidth,
2931
side = 'right',
3032
disabled = false,
@@ -80,6 +82,51 @@
8082
document.addEventListener('pointerup', onPointerUp, { once: true });
8183
}
8284
85+
// https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/slider_role#keyboard_interactions
86+
function handleKeyDown(e: KeyboardEvent) {
87+
e.preventDefault();
88+
isDragging = true;
89+
90+
switch (e.key) {
91+
case 'ArrowRight':
92+
case 'ArrowUp':
93+
stepUp(step);
94+
break;
95+
case 'ArrowLeft':
96+
case 'ArrowDown':
97+
stepDown(step);
98+
break;
99+
case 'PageUp':
100+
stepUp(step * 2);
101+
break;
102+
case 'PageDown':
103+
stepDown(step * 2);
104+
break;
105+
case 'Home':
106+
width = minWidth;
107+
break;
108+
case 'End':
109+
width = maxWidth;
110+
break;
111+
}
112+
113+
function stepUp(s: number) {
114+
if (width + s < maxWidth) {
115+
width = width + s;
116+
} else {
117+
width = maxWidth;
118+
}
119+
}
120+
121+
function stepDown(s: number) {
122+
if (width - s > minWidth) {
123+
width = width - s;
124+
} else {
125+
width = minWidth;
126+
}
127+
}
128+
}
129+
83130
function onPointerUp() {
84131
document.removeEventListener('pointermove', throttledOnPointerMove);
85132
isDragging = false;
@@ -97,6 +144,7 @@
97144
});
98145
</script>
99146

147+
<!-- role 'slider' vs 'separator' discussion -->
100148
<!-- https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/ -->
101149
<!-- https://github.com/w3c/aria-practices/issues/130#issuecomment-2301520761 -->
102150
<div
@@ -108,14 +156,16 @@
108156
]}
109157
aria-disabled={disabled}
110158
tabindex="0"
111-
aria-label={page.data.t('panes.resizeMe')}
159+
aria-label={page.data.t('panes.resizablePane')}
112160
role="slider"
113161
aria-valuenow={width}
114162
aria-valuemin={minWidth}
115163
aria-valuemax={maxWidth}
116164
aria-orientation="vertical"
117165
aria-controls={parentId}
118166
onpointerdown={handlePointerDown}
167+
onkeydown={handleKeyDown}
168+
onkeyup={() => (isDragging = false)}
119169
></div>
120170

121171
<style lang="postcss">

‎lxl-web/src/lib/components/Holder.svelte‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@
9191
target="_blank"
9292
class="ext-link"
9393
aria-label={page.data.t('holdings.findAtLibrary')}
94-
aria-describedby={`holder-${holder[JsonLd.ID]}`}
9594
>
9695
{page.data.t('holdings.linkToLocal')}
9796
</a>
@@ -143,7 +142,10 @@
143142
{/snippet}
144143

145144
<li class={['holder', hidden && 'hidden']}>
146-
<article class="border-neutral bg-page flex flex-col rounded-sm border-b p-3">
145+
<article
146+
class="border-neutral bg-page flex flex-col rounded-sm border-b p-3"
147+
aria-labelledby={`holder-${holder[JsonLd.ID]}`}
148+
>
147149
<div class="holder-heading flex items-baseline justify-between">
148150
<h3 class="text-base font-medium sm:text-sm" id={`holder-${holder[JsonLd.ID]}`}>
149151
{holder.displayStr || holder.name || holder[JsonLd.ID]}
@@ -154,7 +156,6 @@
154156
href={getBestLink(instances[0])}
155157
target="_blank"
156158
aria-label={page.data.t('holdings.findAtLibrary')}
157-
aria-describedby={`holder-${holder[JsonLd.ID]}`}
158159
>
159160
<BiBoxArrowUpRight aria-hidden="true" class="text-link ml-2 size-4" />
160161
</a>

‎lxl-web/src/lib/components/MyLibsHoldingIndicator.svelte‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
</script>
3131

3232
<span
33-
aria-label={indicatorText}
3433
use:popover={{
3534
title: indicatorText
3635
}}

‎lxl-web/src/lib/components/ResourceHoldings.svelte‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
{/if}
6868
<span class="text-nowrap">{getLocalizedType(type)}</span>
6969
<span class="truncate font-normal opacity-90">
70-
{' · '}
70+
<span aria-hidden="true">{' · '}</span>
7171
{#if subsetLibraries && subsetHoldingByType}
7272
<span>{page.data.t('holdings.findTitle')}</span>
7373
{:else}

‎lxl-web/src/lib/components/SearchResultItem.svelte‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@
113113
<DecoratedData data={data['card-heading']} showLabels="never" />
114114
</h2>
115115
{#if data[LensType.WebCardHeaderExtra]?._display && data[LensType.WebCardHeaderExtra]?._display.length}
116-
<p class="decorated-card-heading-extra text-subtle mt-0.5 truncate text-xs">
116+
<p class="decorated-card-heading-extra text-subtle mt-0.5 line-clamp-2 text-xs">
117117
{#each data[LensType.WebCardHeaderExtra]?._display as displayObj, index (index)}
118118
<span>
119119
<DecoratedData data={displayObj} showLabels="defaultOn" />

‎lxl-web/src/lib/components/TableOfContents.svelte‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
intersectionRoot?: HTMLElement;
1717
};
1818
19-
let { items, uidPrefix, mobile = false, intersectionRoot }: Props = $props();
19+
let { items, uidPrefix = '', mobile = false, intersectionRoot }: Props = $props();
2020
let tocElement: HTMLDivElement;
2121
let observer: IntersectionObserver | undefined = $state();
2222
let visibleSections: Set<string> = new SvelteSet();
@@ -118,7 +118,7 @@
118118
</script>
119119

120120
{#snippet tocList(items: TableOfContentsItem[])}
121-
<ul>
121+
<ul aria-labelledby="toc-heading">
122122
{#each items as { id, label, children } (id)}
123123
<li class={children && '[&>ul_a]:pl-7'}>
124124
<div class={['border-l-2 border-l-neutral-200']}>
@@ -143,7 +143,7 @@
143143

144144
<div bind:this={tocElement} class="contents">
145145
{#if mobile}
146-
<h2 class="sr-only">{page.data.t('tableOfContents.onThisPage')}</h2>
146+
<h2 id="toc-heading" class="sr-only">{page.data.t('tableOfContents.onThisPage')}</h2>
147147
<div class="border-b-neutral border-b p-1 text-xs sm:text-sm">
148148
<label
149149
id={`${uidPrefix}toc-label`}
@@ -155,9 +155,6 @@
155155
type="checkbox"
156156
role="switch"
157157
bind:checked={openOnMobile}
158-
aria-label={openOnMobile
159-
? page.data.t('tableOfContents.hide')
160-
: page.data.t('tableOfContents.show')}
161158
aria-expanded={openOnMobile}
162159
aria-controls={`${uidPrefix}toc-items`}
163160
class="h-0 appearance-none focus:outline-0"
@@ -170,7 +167,7 @@
170167
</div>
171168
{:else}
172169
<header class="text-subtle mb-2 text-xs font-medium sm:text-sm">
173-
<h2>{page.data.t('tableOfContents.onThisPage')}</h2>
170+
<h2 id="toc-heading">{page.data.t('tableOfContents.onThisPage')}</h2>
174171
</header>
175172
<nav class="text-placeholder text-xs sm:text-sm">{@render tocList(itemsWithTop)}</nav>
176173
{/if}

0 commit comments

Comments
 (0)