@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 += 1;
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 += 1;
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 is a WAI-ARIA `menu`. Items are
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="menu" aria-label={label} class="menu" data-tsu="Menu" tabindex="-1" {onkeydown}>
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 correct focus/`aria-expanded`
5
- // wiring between the invoker and the panel — all declaratively via the
6
- // `popovertarget` / `popover` attributes. We add only smart placement:
7
- // position the panel against its trigger and flip into the viewport when it
8
- // would overflow. (CSS anchor positioning would replace the JS here once it
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="group"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.34.0",
3
+ "version": "0.35.1",
4
4
  "description": "Minimal, dependency-free Svelte 5 + pure-CSS UI kit. Token-driven atoms, molecules & layouts with theming out of the box.",
5
5
  "type": "module",
6
6
  "license": "MIT",