Skip to content

Commit 95d39c9

Browse files
committed
Improved accessiblity and focus mangamgent
1 parent dc78bb6 commit 95d39c9

17 files changed

+378
-268
lines changed

src/lib/components/Buttons/Button.svelte

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,10 @@
155155
cursor: not-allowed;
156156
color: var(--token-color-text-default-normal);
157157
}
158+
159+
button:focus {
160+
outline: 2px solid var(--token-color-focusring);
161+
background-color: var(--bg-hover);
162+
color: var(--color-hover);
163+
}
158164
</style>

src/lib/components/Buttons/IconButton.svelte

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,10 @@
155155
cursor: not-allowed;
156156
color: var(--token-color-text-default-normal);
157157
}
158+
159+
button:focus {
160+
outline: 2px solid var(--token-color-focusring);
161+
background-color: var(--bg-hover);
162+
color: var(--color-hover);
163+
}
158164
</style>

src/lib/components/Buttons/LinkButton.svelte

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,10 @@
133133
cursor: not-allowed;
134134
color: var(--token-color-text-default-normal);
135135
}
136+
137+
a:focus {
138+
outline: 2px solid var(--token-color-focusring);
139+
background-color: var(--bg-hover);
140+
color: var(--color-hover);
141+
}
136142
</style>

src/lib/components/Buttons/LinkIconButton.svelte

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,10 @@
117117
cursor: not-allowed;
118118
color: var(--token-color-text-default-normal);
119119
}
120+
121+
a:focus {
122+
outline: 2px solid var(--token-color-focusring);
123+
background-color: var(--bg-hover);
124+
color: var(--color-hover);
125+
}
120126
</style>

src/lib/components/Buttons/SplitButton.svelte

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,11 @@
214214
color: var(--token-color-text-default-normal);
215215
}
216216
217+
.btn:focus {
218+
outline: 2px solid var(--token-color-focusring);
219+
background-color: var(--bg-hover);
220+
color: var(--color-hover);
221+
}
217222
/* Dropdown menu */
218223
.dropdown {
219224
position: absolute;
@@ -250,4 +255,9 @@
250255
.dropdown li button:active {
251256
background-color: var(--token-color-background-subtle-pressed);
252257
}
258+
259+
.dropdown li button:focus {
260+
outline: 2px solid var(--token-color-focusring);
261+
background-color: var(--token-color-background-subtle-hover);
262+
}
253263
</style>

src/lib/components/Inputs/Dropdown.svelte

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,47 @@
22
export let appearance: "subtle" | "primary" | "warning" | "danger" | "discover" = "subtle";
33
export let iconbefore: string | undefined = undefined;
44
export let actions: { label: string; onClick?: () => void; value?: string | number | object | null }[] = [];
5+
export let alwaysshowslot: boolean = false;
56
67
// Bindable selected value (kan string, number, object zijn)
78
export let value: string | number | object | null = null;
89
910
let open = false;
1011
let menuItems: HTMLButtonElement[] = [];
1112
12-
function toggleMenu() {
13-
open = !open;
13+
function closeMenu() {
14+
open = false;
15+
document.getElementById("returnfocus")?.focus();
1416
}
1517
16-
function handleAction(action: { onClick?: () => void; value?: string | number | object | null }, label: string) {
17-
if (action.value !== undefined) {
18-
value = action.value;
18+
function toggleMenu() {
19+
if (open) {
20+
closeMenu();
1921
} else {
20-
// fallback naar label als value niet is ingesteld
21-
value = label;
22+
open = true;
2223
}
24+
}
25+
26+
function handleAction(action: { onClick?: () => void; value?: string | number | object | null }, label: string) {
27+
value = action.value ?? label;
2328
2429
if (action.onClick) action.onClick();
2530
26-
open = false;
31+
closeMenu();
2732
}
2833
2934
function closeMenuOnBlur(event: FocusEvent) {
3035
const related = event.relatedTarget as HTMLElement | null;
3136
if (!related || !event.currentTarget || !(event.currentTarget as HTMLElement).contains(related)) {
3237
open = false;
38+
// We do NOT focus here because the user might be tabbing elsewhere intentionally.
3339
}
3440
}
3541
3642
function handleMenuKey(event: KeyboardEvent) {
3743
const currentIndex = menuItems.findIndex((el) => el === document.activeElement);
3844
if (event.key === "Escape") {
39-
open = false;
45+
closeMenu();
4046
} else if (event.key === "ArrowDown") {
4147
event.preventDefault();
4248
const nextIndex = (currentIndex + 1) % menuItems.length;
@@ -59,13 +65,21 @@
5965
</script>
6066

6167
<div class="dropdown-wrapper" on:focusout={closeMenuOnBlur}>
62-
<button class={`btn ${appearance} dropdown-toggle`} aria-haspopup="menu" aria-expanded={open} aria-controls="dropdown-menu" on:click={toggleMenu}>
68+
<button
69+
id="returnfocus"
70+
class={`btn ${appearance} dropdown-toggle`}
71+
aria-haspopup="menu"
72+
aria-expanded={open}
73+
aria-controls="dropdown-menu"
74+
on:click={toggleMenu}
75+
>
6376
{#if iconbefore}
6477
<span class="icon icon-before material-symbols-outlined" translate="no" aria-hidden="true">{iconbefore}</span>
6578
{/if}
6679

67-
<!-- Toon label van geselecteerde value of default slot -->
68-
{#if value !== null}
80+
{#if alwaysshowslot}
81+
<slot />
82+
{:else if value !== null}
6983
{#if actions.find((a) => a.value === value)?.label}
7084
{actions.find((a) => a.value === value)?.label}
7185
{:else}
@@ -79,7 +93,7 @@
7993
</button>
8094

8195
{#if open}
82-
<ul class="dropdown" id="dropdown-menu" role="menu" on:keydown={handleMenuKey}>
96+
<ul class="dropdown" id="dropdown-menu" role="menu" on:keydown={handleMenuKey} aria-labelledby="returnfocus">
8397
{#each actions as action, i (action.label)}
8498
<li role="none">
8599
<button role="menuitem" tabindex={i === 0 ? 0 : -1} bind:this={menuItems[i]} on:click={() => handleAction(action, action.label)}>
@@ -196,6 +210,12 @@
196210
color: var(--token-color-text-default-normal);
197211
}
198212
213+
.btn:focus {
214+
outline: 2px solid var(--token-color-focusring);
215+
background-color: var(--bg-hover);
216+
color: var(--color-hover);
217+
}
218+
199219
/* Dropdown menu */
200220
.dropdown {
201221
position: absolute;
@@ -232,4 +252,9 @@
232252
.dropdown li button:active {
233253
background-color: var(--token-color-background-subtle-pressed);
234254
}
255+
256+
.dropdown li button:focus {
257+
outline: 2px solid var(--token-color-focusring);
258+
background-color: var(--token-color-background-subtle-hover);
259+
}
235260
</style>
Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,51 @@
11
<script lang="ts">
2-
export let placeholder: string = "Your name";
3-
export let type: "text" | "email" | "password" | "url" = "text";
2+
export let id: string = "input-" + Math.random().toString(36).substring(2, 8);
3+
export let label: string;
4+
export let placeholder: string;
5+
export let type: "text" | "email" | "password" | "url" | "search" = "text";
46
export let value: string = "";
57
export let disabled: boolean = false;
68
export let invalid: boolean = false;
9+
export let invalidMessage: string = "Invalid";
10+
export let onEnter: (event: KeyboardEvent) => void = () => {};
711
</script>
812

9-
<div id="root">
10-
<input {type} {placeholder} {disabled} bind:value />
13+
<div class="input-root">
14+
<label for={id}>{label}</label>
15+
<input
16+
{id}
17+
{type}
18+
{placeholder}
19+
{disabled}
20+
bind:value
21+
aria-invalid={invalid}
22+
aria-describedby={invalid ? id + "-error" : undefined}
23+
on:keypress={(event) => {
24+
if (event.key === "Enter" && onEnter) {
25+
onEnter(event);
26+
}
27+
}}
28+
/>
1129
{#if invalid}
12-
<div id="message">
13-
<p><span class="material-symbols-outlined dropdown-arrow" aria-hidden="true">error</span> Invalid</p>
30+
<div id={id + "-error"} class="error-message">
31+
<p>
32+
<span class="material-symbols-outlined" aria-hidden="true">error</span>
33+
{invalidMessage}
34+
</p>
1435
</div>
1536
{/if}
1637
</div>
1738

1839
<style>
19-
#root {
40+
.input-root {
2041
display: flex;
2142
flex-direction: column;
22-
gap: 0px;
43+
gap: 0.25rem;
44+
}
45+
46+
label {
47+
font-size: 0.875rem;
48+
font-weight: 500;
2349
}
2450
2551
input {
@@ -28,20 +54,22 @@
2854
cursor: text;
2955
padding: 0.5rem;
3056
font-family: var(--token-font-main);
31-
background-color: var(--token-color-surface-sunken-normal);
57+
background-color: var(--token-color-surface-raised-normal);
3258
}
3359
34-
#message {
60+
.error-message {
3561
color: var(--token-color-text-danger);
3662
}
3763
3864
p {
3965
font-size: 1rem;
4066
vertical-align: middle;
67+
display: flex;
68+
align-items: center;
69+
gap: 0.25rem;
4170
}
4271
4372
span {
4473
font-size: 1.2rem;
45-
vertical-align: middle;
4674
}
4775
</style>

src/lib/components/Messaging/Toast.svelte

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import { fly } from "svelte/transition";
33
import { cubicOut } from "svelte/easing";
4+
import { onMount } from "svelte";
45
56
export let title: string;
67
export let desc: string | null = null;
@@ -11,6 +12,13 @@
1112
export let position: string = "bottom-right";
1213
1314
let timeout: ReturnType<typeof setTimeout>;
15+
let toastEl: HTMLDivElement;
16+
let previousFocus: Element | null = null;
17+
18+
onMount(() => {
19+
previousFocus = document.activeElement;
20+
toastEl?.focus();
21+
});
1422
1523
if (autoDismiss) {
1624
timeout = setTimeout(() => {
@@ -20,9 +28,16 @@
2028
2129
function close() {
2230
if (timeout) clearTimeout(timeout);
31+
if (previousFocus && typeof (previousFocus as HTMLElement).focus === "function") {
32+
(previousFocus as HTMLElement).focus();
33+
}
2334
onClose();
2435
}
2536
37+
function handleKeydown(e: KeyboardEvent) {
38+
if (e.key === "Escape") close();
39+
}
40+
2641
// Determine fly direction based on position
2742
let flyParams = { x: 0, y: 20, duration: 300, easing: cubicOut };
2843
@@ -34,7 +49,15 @@
3449
else if (position.includes("bottom")) flyParams.y = 20;
3550
</script>
3651

37-
<div class="toast {appearance}" in:fly={flyParams} out:fly={flyParams}>
52+
<div
53+
class="toast {appearance}"
54+
role={appearance === "danger" || appearance === "warning" ? "alert" : "status"}
55+
aria-live={appearance === "danger" || appearance === "warning" ? "assertive" : "polite"}
56+
bind:this={toastEl}
57+
on:keydown={handleKeydown}
58+
in:fly={flyParams}
59+
out:fly={flyParams}
60+
>
3861
{#if icon}
3962
<span class="material-symbols-outlined icon" aria-hidden="true">{icon}</span>
4063
{/if}
@@ -49,7 +72,6 @@
4972
{/if}
5073
</div>
5174
<button aria-label="Close toast" class="close-btn" on:click={close}>×</button>
52-
<!--×-->
5375
</div>
5476

5577
<style>
@@ -65,6 +87,7 @@
6587
max-width: 360px;
6688
position: relative;
6789
pointer-events: auto;
90+
outline: none;
6891
}
6992
.toast.info {
7093
background-color: var(--token-color-background-information-normal);
@@ -111,14 +134,8 @@
111134
color: var(--token-color-text-light-normal);
112135
}
113136
114-
.toast.discover .desc {
115-
color: var(--token-color-text-default);
116-
}
117-
118-
.toast.primary .desc {
119-
color: var(--token-color-text-default);
120-
}
121-
137+
.toast.discover .desc,
138+
.toast.primary .desc,
122139
.toast.danger .desc {
123140
color: var(--token-color-text-default);
124141
}
@@ -130,11 +147,10 @@
130147
cursor: pointer;
131148
background: transparent;
132149
border: none;
133-
font-size: 1.1rem;
150+
font-size: 1.5rem;
134151
color: inherit;
135152
padding: 0;
136153
line-height: 1;
137-
font-size: 1.5rem;
138154
}
139155
.close-btn:hover {
140156
color: var(--token-color-text-danger);

src/lib/components/Themes/ThemeMenu.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
<Dropdown
4747
appearance="subtle"
4848
iconbefore="format_paint"
49+
alwaysshowslot
4950
actions={[
5051
{ label: "Auto", onClick: () => setTheme("system") },
5152
{ label: "Light", onClick: () => setTheme("light") },

src/lib/metadata.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
//! This file is autogenerated! Edit in meta/scripts/GenerateMetaData.ts
22

33
export const metadata = {
4-
version: "1.7.1",
5-
packageName: "@davidnet/svelte-ui",
6-
npmUrl: "https://www.npmjs.com/package/@davidnet/svelte-ui",
7-
commitHash: "64f5f1f",
8-
fullCommitHash: "64f5f1f75adec56dd8a7ac6a164dbf4142ada0ad",
9-
commitDate: "2025-07-26T21:27:40+02:00",
10-
branch: "main",
11-
commitUrl: "https://github.com/davidnet-net/svelte-ui/commit/64f5f1f75adec56dd8a7ac6a164dbf4142ada0ad",
12-
repoUrl: "https://github.com/davidnet-net/svelte-ui"
4+
version: "1.7.1",
5+
packageName: "@davidnet/svelte-ui",
6+
npmUrl: "https://www.npmjs.com/package/@davidnet/svelte-ui",
7+
commitHash: "dc78bb6",
8+
fullCommitHash: "dc78bb6b80a69543175f6d6fa9ec9ed4017afd0d",
9+
commitDate: "2025-07-26T22:29:33+02:00",
10+
branch: "main",
11+
commitUrl: "https://github.com/davidnet-net/svelte-ui/commit/dc78bb6b80a69543175f6d6fa9ec9ed4017afd0d",
12+
repoUrl: "https://github.com/davidnet-net/svelte-ui"
1313
};

0 commit comments

Comments
 (0)