@dorsk/tsumikit 0.34.0 → 0.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import { untrack } from 'svelte';
|
|
3
4
|
import { browser } from '../../env';
|
|
4
5
|
import Icon from '../atoms/Icon.svelte';
|
|
5
6
|
import Scrim from '../atoms/Scrim.svelte';
|
|
@@ -87,7 +88,6 @@
|
|
|
87
88
|
|
|
88
89
|
function measureLength(css: string) {
|
|
89
90
|
if (!browser || !root) return undefined;
|
|
90
|
-
console.count('measure ' + css);
|
|
91
91
|
const probe = document.createElement('div');
|
|
92
92
|
probe.style.cssText = `position:absolute;visibility:hidden;pointer-events:none;width:${css}`;
|
|
93
93
|
root.appendChild(probe);
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
$effect(() => {
|
|
129
129
|
if (!browser || restored) return;
|
|
130
130
|
restored = true;
|
|
131
|
-
lengthTick
|
|
131
|
+
lengthTick = untrack(() => lengthTick) + 1;
|
|
132
132
|
if (!widthKey) return;
|
|
133
133
|
|
|
134
134
|
const savedWidth = parseStoredWidth(
|
|
@@ -149,9 +149,8 @@
|
|
|
149
149
|
$effect(() => {
|
|
150
150
|
if (!browser) return;
|
|
151
151
|
const sync = () => {
|
|
152
|
-
console.count('sync');
|
|
153
152
|
viewportWidth = window.innerWidth;
|
|
154
|
-
lengthTick
|
|
153
|
+
lengthTick = untrack(() => lengthTick) + 1;
|
|
155
154
|
};
|
|
156
155
|
sync();
|
|
157
156
|
addEventListener('resize', sync);
|
|
@@ -163,7 +162,6 @@
|
|
|
163
162
|
fullBleed = false;
|
|
164
163
|
return;
|
|
165
164
|
}
|
|
166
|
-
console.count('bleed');
|
|
167
165
|
const query = matchMedia(`(max-width: ${fullWidthBelow})`);
|
|
168
166
|
const sync = () => {
|
|
169
167
|
fullBleed = query.matches;
|
|
@@ -175,7 +173,6 @@
|
|
|
175
173
|
|
|
176
174
|
$effect(() => {
|
|
177
175
|
if (!browser || !overlay || !open) return;
|
|
178
|
-
console.count('escape');
|
|
179
176
|
const onKeydown = (event: KeyboardEvent) => {
|
|
180
177
|
if (event.key !== 'Escape' || event.defaultPrevented) return;
|
|
181
178
|
event.preventDefault();
|
|
@@ -187,7 +184,6 @@
|
|
|
187
184
|
|
|
188
185
|
$effect(() => {
|
|
189
186
|
if (!browser || !overlay || !open || !panelEl) return;
|
|
190
|
-
console.count('focus');
|
|
191
187
|
const previous = document.activeElement as HTMLElement | null;
|
|
192
188
|
if (!panelEl.contains(previous)) panelEl.focus({ preventScroll: true });
|
|
193
189
|
return () => {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
11
|
<script lang="ts">
|
|
12
|
-
// Dropdown menu: a Popover whose panel
|
|
12
|
+
// Dropdown menu: a Popover whose panel carries the WAI-ARIA `menu` role. Items are
|
|
13
13
|
// `menuitem`s navigable with ↑/↓/Home/End, activated with Enter/Space (and
|
|
14
14
|
// click). Selecting an item runs its action and closes the menu. Focus moves
|
|
15
15
|
// to the first item when the menu opens. Escape / click-outside close it
|
|
@@ -112,13 +112,15 @@
|
|
|
112
112
|
{hitArea}
|
|
113
113
|
{disabled}
|
|
114
114
|
{gap}
|
|
115
|
+
role="menu"
|
|
116
|
+
haspopup="menu"
|
|
115
117
|
{onclose}
|
|
116
118
|
onopen={() => {
|
|
117
119
|
queueMicrotask(() => focusAt(0));
|
|
118
120
|
onopen?.();
|
|
119
121
|
}}
|
|
120
122
|
>
|
|
121
|
-
<div bind:this={listEl} role="
|
|
123
|
+
<div bind:this={listEl} role="none" class="menu" data-tsu="Menu" tabindex="-1" {onkeydown}>
|
|
122
124
|
{#each items as item (item.label)}
|
|
123
125
|
<button
|
|
124
126
|
type="button"
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// Floating panel built on the native Popover API. The platform gives us the
|
|
3
3
|
// top layer (no z-index races, renders above dialogs/sticky headers), light
|
|
4
|
-
// dismiss (click-outside), Escape-to-close and
|
|
5
|
-
//
|
|
6
|
-
// `
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// dismiss (click-outside), Escape-to-close and focus wiring between the
|
|
5
|
+
// invoker and the panel — declaratively via the `popovertarget` / `popover`
|
|
6
|
+
// attributes. We add explicit `aria-expanded` / `aria-haspopup` on the
|
|
7
|
+
// trigger (the implicit popover semantics are not exposed uniformly by
|
|
8
|
+
// assistive tech), the panel role, and smart placement: position the panel
|
|
9
|
+
// against its trigger and flip into the viewport when it would overflow. (CSS anchor positioning would replace the JS here once it
|
|
9
10
|
// ships beyond Chromium; the popover semantics above are the hard part and
|
|
10
11
|
// are broadly supported today.)
|
|
11
12
|
import { tick, type Snippet } from 'svelte';
|
|
@@ -15,6 +16,8 @@
|
|
|
15
16
|
type TriggerVariant = 'default' | 'primary' | 'ghost' | 'danger';
|
|
16
17
|
type TriggerTone = 'none' | 'accent' | 'success' | 'info' | 'warn' | 'danger';
|
|
17
18
|
type TriggerSize = 'sm' | 'md' | 'lg';
|
|
19
|
+
type PanelRole = 'dialog' | 'menu' | 'listbox' | 'group';
|
|
20
|
+
type HasPopup = 'menu' | 'dialog' | 'listbox' | true;
|
|
18
21
|
|
|
19
22
|
let {
|
|
20
23
|
placement = 'bottom-start',
|
|
@@ -32,6 +35,8 @@
|
|
|
32
35
|
block = false,
|
|
33
36
|
hitArea = 'auto',
|
|
34
37
|
disabled = false,
|
|
38
|
+
role = 'dialog',
|
|
39
|
+
haspopup = 'dialog',
|
|
35
40
|
onopen,
|
|
36
41
|
onclose
|
|
37
42
|
}: {
|
|
@@ -63,6 +68,10 @@
|
|
|
63
68
|
/** Icon-only triggers grow a 44px hit slab on coarse pointers; `compact` opts out. */
|
|
64
69
|
hitArea?: 'auto' | 'compact';
|
|
65
70
|
disabled?: boolean;
|
|
71
|
+
/** ARIA role of the panel. */
|
|
72
|
+
role?: PanelRole;
|
|
73
|
+
/** `aria-haspopup` announced on the trigger. */
|
|
74
|
+
haspopup?: HasPopup;
|
|
66
75
|
onopen?: () => void;
|
|
67
76
|
onclose?: () => void;
|
|
68
77
|
} = $props();
|
|
@@ -78,6 +87,7 @@
|
|
|
78
87
|
// reopening is instant. The panel element itself always renders — the native
|
|
79
88
|
// `popovertarget` wiring needs its id present in the DOM at all times.
|
|
80
89
|
let opened = $state(false);
|
|
90
|
+
let open = $state(false);
|
|
81
91
|
|
|
82
92
|
function reposition() {
|
|
83
93
|
if (triggerEl && panelEl) place(triggerEl, panelEl, placement, gap);
|
|
@@ -87,6 +97,7 @@
|
|
|
87
97
|
if (e.newState === 'open') {
|
|
88
98
|
const firstOpen = !opened;
|
|
89
99
|
opened = true;
|
|
100
|
+
open = true;
|
|
90
101
|
// On the first open the content snippet mounts this tick; measure the
|
|
91
102
|
// panel only once it has, so placement accounts for its real size.
|
|
92
103
|
if (firstOpen) await tick();
|
|
@@ -95,6 +106,7 @@
|
|
|
95
106
|
addEventListener('resize', reposition);
|
|
96
107
|
onopen?.();
|
|
97
108
|
} else {
|
|
109
|
+
open = false;
|
|
98
110
|
removeEventListener('scroll', reposition, true);
|
|
99
111
|
removeEventListener('resize', reposition);
|
|
100
112
|
onclose?.();
|
|
@@ -126,6 +138,8 @@
|
|
|
126
138
|
class:trigger-tone-danger={tone === 'danger'}
|
|
127
139
|
popovertarget={id}
|
|
128
140
|
aria-label={label}
|
|
141
|
+
aria-haspopup={haspopup}
|
|
142
|
+
aria-expanded={open}
|
|
129
143
|
{disabled}
|
|
130
144
|
>
|
|
131
145
|
{@render trigger()}
|
|
@@ -136,7 +150,7 @@
|
|
|
136
150
|
{id}
|
|
137
151
|
popover="auto"
|
|
138
152
|
class="pop-panel"
|
|
139
|
-
role
|
|
153
|
+
{role}
|
|
140
154
|
aria-label={label}
|
|
141
155
|
ontoggle={onToggle}
|
|
142
156
|
>
|
|
@@ -3,6 +3,8 @@ type Placement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
|
3
3
|
type TriggerVariant = 'default' | 'primary' | 'ghost' | 'danger';
|
|
4
4
|
type TriggerTone = 'none' | 'accent' | 'success' | 'info' | 'warn' | 'danger';
|
|
5
5
|
type TriggerSize = 'sm' | 'md' | 'lg';
|
|
6
|
+
type PanelRole = 'dialog' | 'menu' | 'listbox' | 'group';
|
|
7
|
+
type HasPopup = 'menu' | 'dialog' | 'listbox' | true;
|
|
6
8
|
type $$ComponentProps = {
|
|
7
9
|
placement?: Placement;
|
|
8
10
|
gap?: number;
|
|
@@ -32,6 +34,10 @@ type $$ComponentProps = {
|
|
|
32
34
|
/** Icon-only triggers grow a 44px hit slab on coarse pointers; `compact` opts out. */
|
|
33
35
|
hitArea?: 'auto' | 'compact';
|
|
34
36
|
disabled?: boolean;
|
|
37
|
+
/** ARIA role of the panel. */
|
|
38
|
+
role?: PanelRole;
|
|
39
|
+
/** `aria-haspopup` announced on the trigger. */
|
|
40
|
+
haspopup?: HasPopup;
|
|
35
41
|
onopen?: () => void;
|
|
36
42
|
onclose?: () => void;
|
|
37
43
|
};
|
package/package.json
CHANGED