@rokkit/ui 1.0.0-next.137 → 1.0.0-next.139

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/ui",
3
- "version": "1.0.0-next.137",
3
+ "version": "1.0.0-next.139",
4
4
  "description": "Data driven UI components for Rokkit applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -88,7 +88,6 @@
88
88
  })
89
89
  </script>
90
90
 
91
- <!-- svelte-ignore a11y_no_static_element_interactions -->
92
91
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
93
92
  <div
94
93
  data-carousel
@@ -98,18 +98,24 @@
98
98
  trigger?.focus()
99
99
  }
100
100
 
101
+ /**
102
+ * Move DOM focus to the menu item at the given index
103
+ */
104
+ function focusDomItem(index: number) {
105
+ const menu = fabRef?.querySelector('[data-fab-menu]')
106
+ if (!menu) return
107
+ const menuItems = menu.querySelectorAll('[data-fab-item]:not([data-disabled])')
108
+ const menuItem = menuItems[index] as HTMLElement | undefined
109
+ menuItem?.focus()
110
+ }
111
+
101
112
  /**
102
113
  * Focus an item by index
103
114
  */
104
115
  function focusItem(index: number) {
105
116
  if (index < 0 || index >= flatItems.length) return
106
117
  focusedIndex = index
107
- const menu = fabRef?.querySelector('[data-fab-menu]')
108
- if (menu) {
109
- const menuItems = menu.querySelectorAll('[data-fab-item]:not([data-disabled])')
110
- const menuItem = menuItems[index] as HTMLElement | undefined
111
- menuItem?.focus()
112
- }
118
+ focusDomItem(index)
113
119
  }
114
120
 
115
121
  /**
@@ -125,42 +131,65 @@
125
131
  }
126
132
  }
127
133
 
134
+ /**
135
+ * Close menu and return focus to trigger
136
+ */
137
+ function closeAndFocusTrigger() {
138
+ close()
139
+ const trigger = fabRef?.querySelector('[data-fab-trigger]') as HTMLElement | undefined
140
+ trigger?.focus()
141
+ }
142
+
143
+ /**
144
+ * Activate the currently focused item
145
+ */
146
+ function activateFocusedItem() {
147
+ if (focusedIndex >= 0 && focusedIndex < flatItems.length) {
148
+ handleItemClick(flatItems[focusedIndex])
149
+ }
150
+ }
151
+
152
+ function wrapNext(current: number, last: number): number {
153
+ return current < last ? current + 1 : 0
154
+ }
155
+
156
+ function wrapPrev(current: number, last: number): number {
157
+ return current > 0 ? current - 1 : last
158
+ }
159
+
160
+ function nextFabIndex(key: string, current: number, last: number): number {
161
+ if (key === 'ArrowDown') return wrapNext(current, last)
162
+ if (key === 'ArrowUp') return wrapPrev(current, last)
163
+ if (key === 'Home') return 0
164
+ if (key === 'End') return last
165
+ return -1
166
+ }
167
+
168
+ function handleFabNavMove(event: KeyboardEvent): boolean {
169
+ const last = flatItems.length - 1
170
+ const next = nextFabIndex(event.key, focusedIndex, last)
171
+ if (next === -1) return false
172
+ focusItem(next)
173
+ return true
174
+ }
175
+
176
+ function isActivateKey(key: string): boolean {
177
+ return key === 'Enter' || key === ' '
178
+ }
179
+
128
180
  /**
129
181
  * Handle keyboard navigation when menu is open
130
182
  */
131
183
  function handleKeyDown(event: KeyboardEvent) {
132
184
  if (!open) return
133
-
134
- switch (event.key) {
135
- case 'Escape':
136
- event.preventDefault()
137
- close()
138
- const trigger = fabRef?.querySelector('[data-fab-trigger]') as HTMLElement | undefined
139
- trigger?.focus()
140
- break
141
- case 'ArrowDown':
142
- event.preventDefault()
143
- focusItem(focusedIndex < flatItems.length - 1 ? focusedIndex + 1 : 0)
144
- break
145
- case 'ArrowUp':
146
- event.preventDefault()
147
- focusItem(focusedIndex > 0 ? focusedIndex - 1 : flatItems.length - 1)
148
- break
149
- case 'Home':
150
- event.preventDefault()
151
- focusItem(0)
152
- break
153
- case 'End':
154
- event.preventDefault()
155
- focusItem(flatItems.length - 1)
156
- break
157
- case 'Enter':
158
- case ' ':
159
- event.preventDefault()
160
- if (focusedIndex >= 0 && focusedIndex < flatItems.length) {
161
- handleItemClick(flatItems[focusedIndex])
162
- }
163
- break
185
+ if (event.key === 'Escape') {
186
+ event.preventDefault()
187
+ closeAndFocusTrigger()
188
+ } else if (isActivateKey(event.key)) {
189
+ event.preventDefault()
190
+ activateFocusedItem()
191
+ } else if (handleFabNavMove(event)) {
192
+ event.preventDefault()
164
193
  }
165
194
  }
166
195
 
@@ -61,65 +61,95 @@
61
61
  if (!pinned) expanded = false
62
62
  }
63
63
 
64
+ function getItemHref(item: { proxy: ProxyItem; original: Record<string, unknown> }): string {
65
+ if (item.proxy.get('href') === undefined) return ''
66
+ return String(item.original[userFields?.href ?? 'href'] ?? '')
67
+ }
68
+
69
+ function resolveTargetId(item: { proxy: ProxyItem; original: Record<string, unknown> }): string {
70
+ const href = getItemHref(item)
71
+ return href.startsWith('#') ? href.slice(1) : String(item.proxy.value)
72
+ }
73
+
64
74
  function handleItemClick(item: { proxy: ProxyItem; original: Record<string, unknown> }) {
65
75
  value = item.proxy.value
66
76
  onselect?.(item.proxy.value, item.original)
67
-
68
- // Smooth scroll to target section
69
- const href =
70
- item.proxy.get('href') !== undefined
71
- ? String(item.original[userFields?.href ?? 'href'] ?? '')
72
- : ''
73
- const targetId = href.startsWith('#') ? href.slice(1) : String(item.proxy.value)
74
- const el = document.getElementById(targetId)
77
+ const el = document.getElementById(resolveTargetId(item))
75
78
  el?.scrollIntoView({ behavior: 'smooth' })
76
79
  }
77
80
 
81
+ function activateFocusedItem() {
82
+ if (focusedIndex >= 0 && focusedIndex < itemProxies.length) {
83
+ handleItemClick(itemProxies[focusedIndex])
84
+ }
85
+ }
86
+
87
+ const nextKey = $derived(isVertical ? 'ArrowDown' : 'ArrowRight')
88
+ const prevKey = $derived(isVertical ? 'ArrowUp' : 'ArrowLeft')
89
+
90
+ function wrapNext(current: number, last: number): number {
91
+ return current < last ? current + 1 : 0
92
+ }
93
+
94
+ function wrapPrev(current: number, last: number): number {
95
+ return current > 0 ? current - 1 : last
96
+ }
97
+
98
+ function getNextNavIndex(key: string, current: number, last: number): number {
99
+ if (key === nextKey) return wrapNext(current, last)
100
+ if (key === prevKey) return wrapPrev(current, last)
101
+ if (key === 'Home') return 0
102
+ if (key === 'End') return last
103
+ return -1
104
+ }
105
+
106
+ function handleNavMove(event: KeyboardEvent): boolean {
107
+ const last = itemProxies.length - 1
108
+ const next = getNextNavIndex(event.key, focusedIndex, last)
109
+ if (next === -1) return false
110
+ focusItem(next)
111
+ return true
112
+ }
113
+
114
+ function isActivateKey(key: string): boolean {
115
+ return key === 'Enter' || key === ' '
116
+ }
117
+
78
118
  function handleKeyDown(event: KeyboardEvent) {
79
- const nextKey = isVertical ? 'ArrowDown' : 'ArrowRight'
80
- const prevKey = isVertical ? 'ArrowUp' : 'ArrowLeft'
81
-
82
- switch (event.key) {
83
- case nextKey:
84
- event.preventDefault()
85
- focusItem(focusedIndex < itemProxies.length - 1 ? focusedIndex + 1 : 0)
86
- break
87
- case prevKey:
88
- event.preventDefault()
89
- focusItem(focusedIndex > 0 ? focusedIndex - 1 : itemProxies.length - 1)
90
- break
91
- case 'Home':
92
- event.preventDefault()
93
- focusItem(0)
94
- break
95
- case 'End':
96
- event.preventDefault()
97
- focusItem(itemProxies.length - 1)
98
- break
99
- case 'Enter':
100
- case ' ':
101
- event.preventDefault()
102
- if (focusedIndex >= 0 && focusedIndex < itemProxies.length) {
103
- handleItemClick(itemProxies[focusedIndex])
104
- }
105
- break
106
- case 'Escape':
107
- if (!pinned) {
108
- event.preventDefault()
109
- expanded = false
110
- }
111
- break
119
+ if (isActivateKey(event.key)) {
120
+ event.preventDefault()
121
+ activateFocusedItem()
122
+ } else if (event.key === 'Escape' && !pinned) {
123
+ event.preventDefault()
124
+ expanded = false
125
+ } else if (handleNavMove(event)) {
126
+ event.preventDefault()
112
127
  }
113
128
  }
114
129
 
130
+ function focusDomItem(index: number) {
131
+ const itemsContainer = navRef?.querySelector('[data-floating-nav-items]')
132
+ if (!itemsContainer) return
133
+ const navItems = itemsContainer.querySelectorAll('[data-floating-nav-item]')
134
+ const item = navItems[index] as HTMLElement | undefined
135
+ item?.focus()
136
+ }
137
+
115
138
  function focusItem(index: number) {
116
139
  if (index < 0 || index >= itemProxies.length) return
117
140
  focusedIndex = index
118
- const itemsContainer = navRef?.querySelector('[data-floating-nav-items]')
119
- if (itemsContainer) {
120
- const navItems = itemsContainer.querySelectorAll('[data-floating-nav-item]')
121
- const item = navItems[index] as HTMLElement | undefined
122
- item?.focus()
141
+ focusDomItem(index)
142
+ }
143
+
144
+ function matchesEntryId(item: { proxy: ProxyItem; original: Record<string, unknown> }, id: string): boolean {
145
+ return resolveTargetId(item) === id
146
+ }
147
+
148
+ function handleIntersection(entries: IntersectionObserverEntry[]) {
149
+ for (const entry of entries) {
150
+ if (!entry.isIntersecting) continue
151
+ const match = itemProxies.find((item) => matchesEntryId(item, entry.target.id))
152
+ if (match) value = match.proxy.value
123
153
  }
124
154
  }
125
155
 
@@ -127,30 +157,10 @@
127
157
  $effect(() => {
128
158
  if (!observe || itemProxies.length === 0) return
129
159
 
130
- const observer = new IntersectionObserver((entries) => {
131
- for (const entry of entries) {
132
- if (entry.isIntersecting) {
133
- const match = itemProxies.find((item) => {
134
- const href =
135
- item.proxy.get('href') !== undefined
136
- ? String(item.original[userFields?.href ?? 'href'] ?? '')
137
- : ''
138
- const targetId = href.startsWith('#') ? href.slice(1) : String(item.proxy.value)
139
- return targetId === entry.target.id
140
- })
141
- if (match) {
142
- value = match.proxy.value
143
- }
144
- }
145
- }
146
- }, observerOptions)
160
+ const observer = new IntersectionObserver(handleIntersection, observerOptions)
147
161
 
148
162
  for (const item of itemProxies) {
149
- const href =
150
- item.proxy.get('href') !== undefined
151
- ? String(item.original[userFields?.href ?? 'href'] ?? '')
152
- : ''
153
- const targetId = href.startsWith('#') ? href.slice(1) : String(item.proxy.value)
163
+ const targetId = resolveTargetId(item)
154
164
  const el = document.getElementById(targetId)
155
165
  if (el) observer.observe(el)
156
166
  }
@@ -57,7 +57,7 @@
57
57
  f,
58
58
  key,
59
59
  level,
60
- onlazyload ? async (_value, rawItem) => onlazyload(rawItem) : null
60
+ onlazyload ? (_value, rawItem) => onlazyload(rawItem) : null
61
61
  )
62
62
  })
63
63
  )
@@ -77,7 +77,6 @@
77
77
  })
78
78
  </script>
79
79
 
80
- <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
81
80
  <div
82
81
  bind:this={treeRef}
83
82
  data-tree
@@ -99,7 +99,6 @@
99
99
  {/if}
100
100
  {/snippet}
101
101
 
102
- <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
103
102
  <nav
104
103
  bind:this={listRef}
105
104
  data-list
@@ -214,7 +214,6 @@
214
214
  </button>
215
215
 
216
216
  {#if isOpen}
217
- <!-- svelte-ignore a11y_no_static_element_interactions -->
218
217
  <div bind:this={dropdownRef} data-menu-dropdown role="menu" aria-orientation="vertical">
219
218
  {#each wrapper.flatView as node (node.key)}
220
219
  {@const proxy = node.proxy}
@@ -27,6 +27,7 @@
27
27
  // @ts-nocheck
28
28
  import type { ProxyItem } from '@rokkit/states'
29
29
  import { Wrapper, ProxyTree } from '@rokkit/states'
30
+ import { SvelteSet } from 'svelte/reactivity'
30
31
  import { Navigator, Trigger } from '@rokkit/actions'
31
32
  import { DEFAULT_STATE_ICONS, resolveSnippet, ITEM_SNIPPET, GROUP_SNIPPET } from '@rokkit/core'
32
33
  import ItemContent from './ItemContent.svelte'
@@ -133,65 +134,52 @@
133
134
  return (value ?? []).some((v) => v === extractedValue)
134
135
  }
135
136
 
136
- function toggleItemSelection(extractedValue: unknown) {
137
- const currentValues = value ?? []
138
- const alreadySelected = currentValues.some((v) => v === extractedValue)
139
-
140
- let newValues: unknown[]
141
- let newItems: unknown[]
142
-
143
- if (alreadySelected) {
144
- newValues = currentValues.filter((v) => v !== extractedValue)
145
- // Rebuild selected items from remaining values
146
- newItems = []
147
- for (const [, proxy] of wrapper.lookup) {
148
- if (!proxy.hasChildren && newValues.some((v) => v === proxy.value)) {
149
- newItems.push(proxy.original)
150
- }
151
- }
152
- } else {
153
- newValues = [...currentValues, extractedValue]
154
- // Rebuild selected items from lookup to include all values
155
- newItems = []
156
- for (const [, proxy] of wrapper.lookup) {
157
- if (!proxy.hasChildren && newValues.some((v) => v === proxy.value)) {
158
- newItems.push(proxy.original)
159
- }
160
- }
137
+ function buildSelectedItems(newValues: unknown[]): unknown[] {
138
+ const result: unknown[] = []
139
+ for (const [, proxy] of wrapper.lookup) {
140
+ if (isLeafWithValue(proxy, newValues)) result.push(proxy.original)
161
141
  }
142
+ return result
143
+ }
162
144
 
145
+ function applySelection(newValues: unknown[]) {
146
+ const newItems = buildSelectedItems(newValues)
163
147
  value = newValues
164
148
  selected = newItems
165
149
  onchange?.(newValues, newItems)
166
150
  }
167
151
 
168
- function removeTag(extractedValue: unknown) {
152
+ function toggleItemSelection(extractedValue: unknown) {
169
153
  const currentValues = value ?? []
170
- const newValues = currentValues.filter((v) => v !== extractedValue)
171
- const newItems: unknown[] = []
172
- for (const [, proxy] of wrapper.lookup) {
173
- if (!proxy.hasChildren && newValues.some((v) => v === proxy.value)) {
174
- newItems.push(proxy.original)
175
- }
176
- }
177
- value = newValues
178
- selected = newItems
179
- onchange?.(newValues, newItems)
154
+ const alreadySelected = currentValues.some((v) => v === extractedValue)
155
+ const newValues = alreadySelected
156
+ ? currentValues.filter((v) => v !== extractedValue)
157
+ : [...currentValues, extractedValue]
158
+ applySelection(newValues)
159
+ }
160
+
161
+ function removeTag(extractedValue: unknown) {
162
+ const newValues = (value ?? []).filter((v) => v !== extractedValue)
163
+ applySelection(newValues)
180
164
  }
181
165
 
182
166
  // ─── Selected items for tags display ──────────────────────────────────────
183
167
 
184
- const selectedProxies = $derived.by(() => {
168
+ function isLeafWithValue(proxy: ProxyItem, vals: unknown[]): boolean {
169
+ return !proxy.hasChildren && vals.some((v) => v === proxy.value)
170
+ }
171
+
172
+ function computeSelectedProxies(): ProxyItem[] {
185
173
  const vals = value ?? []
186
174
  if (vals.length === 0) return []
187
175
  const result: ProxyItem[] = []
188
176
  for (const [, proxy] of wrapper.lookup) {
189
- if (!proxy.hasChildren && vals.some((v) => v === proxy.value)) {
190
- result.push(proxy)
191
- }
177
+ if (isLeafWithValue(proxy, vals)) result.push(proxy)
192
178
  }
193
179
  return result
194
- })
180
+ }
181
+
182
+ const selectedProxies = $derived.by(computeSelectedProxies)
195
183
 
196
184
  // ─── Trigger action ───────────────────────────────────────────────────────
197
185
 
@@ -236,7 +224,7 @@
236
224
  // ─── Helpers ──────────────────────────────────────────────────────────────
237
225
 
238
226
  const groupDividers = $derived.by(() => {
239
- const set = new Set<string>()
227
+ const set = new SvelteSet<string>()
240
228
  let foundFirst = false
241
229
  for (const node of wrapper.flatView) {
242
230
  if (node.hasChildren) {
@@ -318,7 +306,6 @@
318
306
  </button>
319
307
 
320
308
  {#if isOpen}
321
- <!-- svelte-ignore a11y_no_static_element_interactions -->
322
309
  <div
323
310
  bind:this={dropdownRef}
324
311
  data-select-dropdown
@@ -81,22 +81,18 @@
81
81
  slidingUpper = true
82
82
  }
83
83
 
84
+ function panUpperBy(dx: number) {
85
+ const currentPx = inverseLerp(rangeMode ? upper : value, min, max) * trackWidth
86
+ const minPx = rangeMode ? inverseLerp(lower, min, max) * trackWidth : 0
87
+ const newPx = clamp(currentPx + dx, minPx, trackWidth)
88
+ const snapped = snapToStep(clamp(pixelToValue(newPx), rangeMode ? lower : min, max))
89
+ if (rangeMode) upper = snapped
90
+ else value = snapped
91
+ }
92
+
84
93
  function handleUpperPanMove(event: CustomEvent) {
85
94
  if (disabled || trackWidth === 0) return
86
- const dx = event.detail.dx as number
87
- const currentPx = inverseLerp(rangeMode ? upper : value, min, max) * trackWidth
88
- const newPx = clamp(
89
- currentPx + dx,
90
- rangeMode ? inverseLerp(lower, min, max) * trackWidth : 0,
91
- trackWidth
92
- )
93
- const raw = pixelToValue(newPx)
94
- const snapped = snapToStep(clamp(raw, rangeMode ? lower : min, max))
95
- if (rangeMode) {
96
- upper = snapped
97
- } else {
98
- value = snapped
99
- }
95
+ panUpperBy(event.detail.dx as number)
100
96
  fireChange()
101
97
  }
102
98
 
@@ -111,41 +107,37 @@
111
107
  fireChange()
112
108
  }
113
109
 
114
- function handleUpperKeyDown(event: KeyboardEvent) {
115
- if (disabled) return
110
+ function nudgeUpper(delta: number) {
116
111
  const increment = step > 0 ? step : (max - min) / 10
112
+ if (rangeMode) upper = clamp(snapToStep(upper + delta * increment), lower, max)
113
+ else value = clamp(snapToStep(value + delta * increment), min, max)
114
+ }
117
115
 
118
- if (event.key === 'ArrowRight' || event.key === 'ArrowUp') {
119
- event.preventDefault()
120
- if (rangeMode) {
121
- upper = clamp(snapToStep(upper + increment), lower, max)
122
- } else {
123
- value = clamp(snapToStep(value + increment), min, max)
124
- }
125
- fireChange()
126
- } else if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') {
127
- event.preventDefault()
128
- if (rangeMode) {
129
- upper = clamp(snapToStep(upper - increment), lower, max)
130
- } else {
131
- value = clamp(snapToStep(value - increment), min, max)
132
- }
133
- fireChange()
134
- } else if (event.key === 'Home') {
135
- event.preventDefault()
136
- if (rangeMode) {
137
- upper = lower
138
- } else {
139
- value = min
140
- }
141
- fireChange()
142
- } else if (event.key === 'End') {
116
+ function jumpUpper(toEnd: boolean) {
117
+ if (rangeMode) upper = toEnd ? max : lower
118
+ else value = toEnd ? max : min
119
+ }
120
+
121
+ function isIncreaseKey(key: string): boolean {
122
+ return key === 'ArrowRight' || key === 'ArrowUp'
123
+ }
124
+
125
+ function isDecreaseKey(key: string): boolean {
126
+ return key === 'ArrowLeft' || key === 'ArrowDown'
127
+ }
128
+
129
+ function applyUpperKey(key: string): boolean {
130
+ if (isIncreaseKey(key)) { nudgeUpper(1); return true }
131
+ if (isDecreaseKey(key)) { nudgeUpper(-1); return true }
132
+ if (key === 'Home') { jumpUpper(false); return true }
133
+ if (key === 'End') { jumpUpper(true); return true }
134
+ return false
135
+ }
136
+
137
+ function handleUpperKeyDown(event: KeyboardEvent) {
138
+ if (disabled) return
139
+ if (applyUpperKey(event.key)) {
143
140
  event.preventDefault()
144
- if (rangeMode) {
145
- upper = max
146
- } else {
147
- value = max
148
- }
149
141
  fireChange()
150
142
  }
151
143
  }
@@ -175,25 +167,23 @@
175
167
  fireChange()
176
168
  }
177
169
 
178
- function handleLowerKeyDown(event: KeyboardEvent) {
179
- if (disabled) return
170
+ function nudgeLower(delta: number) {
180
171
  const increment = step > 0 ? step : (max - min) / 10
172
+ lower = clamp(snapToStep(lower + delta * increment), min, upper)
173
+ }
181
174
 
182
- if (event.key === 'ArrowRight' || event.key === 'ArrowUp') {
183
- event.preventDefault()
184
- lower = clamp(snapToStep(lower + increment), min, upper)
185
- fireChange()
186
- } else if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') {
187
- event.preventDefault()
188
- lower = clamp(snapToStep(lower - increment), min, upper)
189
- fireChange()
190
- } else if (event.key === 'Home') {
191
- event.preventDefault()
192
- lower = min
193
- fireChange()
194
- } else if (event.key === 'End') {
175
+ function applyLowerKey(key: string): boolean {
176
+ if (isIncreaseKey(key)) { nudgeLower(1); return true }
177
+ if (isDecreaseKey(key)) { nudgeLower(-1); return true }
178
+ if (key === 'Home') { lower = min; return true }
179
+ if (key === 'End') { lower = upper; return true }
180
+ return false
181
+ }
182
+
183
+ function handleLowerKeyDown(event: KeyboardEvent) {
184
+ if (disabled) return
185
+ if (applyLowerKey(event.key)) {
195
186
  event.preventDefault()
196
- lower = upper
197
187
  fireChange()
198
188
  }
199
189
  }
@@ -45,31 +45,41 @@
45
45
  onchange?.(newValue)
46
46
  }
47
47
 
48
+ function setRating(newValue: number) {
49
+ value = newValue
50
+ onchange?.(newValue)
51
+ }
52
+
53
+ function tryDigitKey(key: string): boolean {
54
+ const digit = parseInt(key, 10)
55
+ if (isNaN(digit) || digit < 0 || digit > max) return false
56
+ setRating(digit)
57
+ return true
58
+ }
59
+
60
+ function isIncreaseKey(key: string): boolean {
61
+ return key === 'ArrowRight' || key === 'ArrowUp'
62
+ }
63
+
64
+ function isDecreaseKey(key: string): boolean {
65
+ return key === 'ArrowLeft' || key === 'ArrowDown'
66
+ }
67
+
48
68
  function handleKeyDown(event: KeyboardEvent) {
49
69
  if (disabled) return
50
70
 
51
- if (event.key === 'ArrowRight' || event.key === 'ArrowUp') {
71
+ if (isIncreaseKey(event.key)) {
72
+ event.preventDefault()
73
+ setRating(Math.min(value + 1, max))
74
+ } else if (isDecreaseKey(event.key)) {
52
75
  event.preventDefault()
53
- const newValue = Math.min(value + 1, max)
54
- value = newValue
55
- onchange?.(newValue)
56
- } else if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') {
76
+ setRating(Math.max(value - 1, 0))
77
+ } else if (tryDigitKey(event.key)) {
57
78
  event.preventDefault()
58
- const newValue = Math.max(value - 1, 0)
59
- value = newValue
60
- onchange?.(newValue)
61
- } else {
62
- const digit = parseInt(event.key, 10)
63
- if (!isNaN(digit) && digit >= 0 && digit <= max) {
64
- event.preventDefault()
65
- value = digit
66
- onchange?.(digit)
67
- }
68
79
  }
69
80
  }
70
81
  </script>
71
82
 
72
- <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
73
83
  <div
74
84
  data-rating
75
85
  data-rating-disabled={disabled || undefined}
@@ -68,7 +68,7 @@
68
68
 
69
69
  {#if filters.length > 0}
70
70
  <div data-search-tags>
71
- {#each filters as filter, i}
71
+ {#each filters as filter, i (i)}
72
72
  {#if tagSnippet}
73
73
  {@render tagSnippet(filter, () => removeFilter(i))}
74
74
  {:else}