@insymetri/styleguide 0.1.44 → 0.1.46
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.
|
@@ -41,14 +41,40 @@
|
|
|
41
41
|
const showError = $derived(error || !!errorMessage)
|
|
42
42
|
|
|
43
43
|
let placeholder = $state<DateValue>(today(getLocalTimeZone()))
|
|
44
|
+
let inputEl = $state<HTMLElement | null>(null)
|
|
45
|
+
let internalValue = $state<DateValue | undefined>(value)
|
|
46
|
+
|
|
47
|
+
// Sync external value changes into internal state
|
|
48
|
+
$effect(() => {
|
|
49
|
+
internalValue = value
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
function allSegmentsEmpty() {
|
|
53
|
+
if (!inputEl) return true
|
|
54
|
+
const segments = inputEl.querySelectorAll('[data-segment]:not([data-segment="trigger"]):not([data-segment="literal"])')
|
|
55
|
+
return Array.from(segments).every(s => s.hasAttribute('data-placeholder'))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function handleValueChange(newValue: DateValue | undefined) {
|
|
59
|
+
if (newValue === undefined && !allSegmentsEmpty()) {
|
|
60
|
+
// A segment was partially cleared — restore the internal value
|
|
61
|
+
// to prevent bits-ui from wiping the date
|
|
62
|
+
internalValue = value
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
internalValue = newValue
|
|
66
|
+
value = newValue
|
|
67
|
+
onValueChange?.(newValue)
|
|
68
|
+
}
|
|
44
69
|
</script>
|
|
45
70
|
|
|
46
71
|
<div class={cn('flex flex-col gap-4', className)}>
|
|
47
|
-
<DatePicker.Root
|
|
72
|
+
<DatePicker.Root value={internalValue} bind:placeholder onValueChange={handleValueChange} {disabled}>
|
|
48
73
|
{#if label}
|
|
49
74
|
<DatePicker.Label class="text-small-emphasis text-secondary">{label}</DatePicker.Label>
|
|
50
75
|
{/if}
|
|
51
76
|
<DatePicker.Input
|
|
77
|
+
bind:ref={inputEl}
|
|
52
78
|
class={cn(
|
|
53
79
|
'flex items-center gap-4 px-12 border bg-input-bg border-input-border transition-all duration-fast hover:border-input-border-hover [&:has(:focus)]:border-accent [&:has(:focus)]:ring-3 [&:has(:focus)]:ring-primary motion-reduce:transition-none',
|
|
54
80
|
densityClasses[density.value],
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import {tick} from 'svelte'
|
|
2
3
|
import type {Snippet} from 'svelte'
|
|
3
4
|
import {IIIcon} from '../IIIcon'
|
|
4
5
|
import {cn} from '../utils/cn'
|
|
@@ -86,6 +87,55 @@
|
|
|
86
87
|
if (item) handleSelect(item)
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
let typeaheadBuffer = $state('')
|
|
91
|
+
let typeaheadTimer: ReturnType<typeof setTimeout> | undefined
|
|
92
|
+
|
|
93
|
+
function typeahead(char: string) {
|
|
94
|
+
const wasOpen = open
|
|
95
|
+
typeaheadBuffer += char.toLowerCase()
|
|
96
|
+
clearTimeout(typeaheadTimer)
|
|
97
|
+
typeaheadTimer = setTimeout(() => { typeaheadBuffer = '' }, 500)
|
|
98
|
+
|
|
99
|
+
const match = items.find(
|
|
100
|
+
i => !i.disabled && i.label.toLowerCase().startsWith(typeaheadBuffer)
|
|
101
|
+
)
|
|
102
|
+
if (match) {
|
|
103
|
+
value = match.value
|
|
104
|
+
onSelect?.(match.value)
|
|
105
|
+
if (!wasOpen) {
|
|
106
|
+
open = false
|
|
107
|
+
triggerEl?.focus()
|
|
108
|
+
} else {
|
|
109
|
+
tick().then(() => {
|
|
110
|
+
const el = floatingEl?.querySelector<HTMLElement>('[aria-selected="true"]')
|
|
111
|
+
el?.focus()
|
|
112
|
+
el?.scrollIntoView({block: 'nearest'})
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function handleTriggerKeydown(e: KeyboardEvent) {
|
|
119
|
+
if (disabled) return
|
|
120
|
+
|
|
121
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
122
|
+
e.preventDefault()
|
|
123
|
+
if (!open) open = true
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (e.key === ' ' || e.key === 'Enter') {
|
|
128
|
+
e.preventDefault()
|
|
129
|
+
if (!open) open = true
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
134
|
+
e.preventDefault()
|
|
135
|
+
typeahead(e.key)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
89
139
|
function toggle() {
|
|
90
140
|
if (disabled) return
|
|
91
141
|
open = !open
|
|
@@ -102,12 +152,14 @@
|
|
|
102
152
|
}
|
|
103
153
|
})
|
|
104
154
|
|
|
105
|
-
// Focus
|
|
155
|
+
// Focus selected item (or first) when opened (non-searchable)
|
|
106
156
|
$effect(() => {
|
|
107
157
|
if (open && floatingEl && !searchable) {
|
|
108
158
|
requestAnimationFrame(() => {
|
|
109
|
-
const
|
|
110
|
-
|
|
159
|
+
const selected = floatingEl?.querySelector<HTMLElement>('[role="option"][aria-selected="true"]:not([data-disabled])')
|
|
160
|
+
const target = selected ?? floatingEl?.querySelector<HTMLElement>('[role="option"]:not([data-disabled])')
|
|
161
|
+
target?.focus()
|
|
162
|
+
target?.scrollIntoView({block: 'nearest'})
|
|
111
163
|
})
|
|
112
164
|
}
|
|
113
165
|
})
|
|
@@ -132,6 +184,10 @@
|
|
|
132
184
|
const currentIndex = optionItems.indexOf(document.activeElement as HTMLElement)
|
|
133
185
|
|
|
134
186
|
switch (e.key) {
|
|
187
|
+
case 'Tab':
|
|
188
|
+
e.preventDefault()
|
|
189
|
+
focusItem(optionItems, currentIndex, e.shiftKey ? -1 : 1)
|
|
190
|
+
break
|
|
135
191
|
case 'ArrowDown':
|
|
136
192
|
e.preventDefault()
|
|
137
193
|
focusItem(optionItems, currentIndex, 1)
|
|
@@ -141,7 +197,6 @@
|
|
|
141
197
|
focusItem(optionItems, currentIndex, -1)
|
|
142
198
|
break
|
|
143
199
|
case 'Enter':
|
|
144
|
-
case ' ':
|
|
145
200
|
e.preventDefault()
|
|
146
201
|
;(document.activeElement as HTMLElement)?.click()
|
|
147
202
|
break
|
|
@@ -149,6 +204,12 @@
|
|
|
149
204
|
e.preventDefault()
|
|
150
205
|
close()
|
|
151
206
|
break
|
|
207
|
+
default:
|
|
208
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
209
|
+
e.preventDefault()
|
|
210
|
+
typeahead(e.key)
|
|
211
|
+
}
|
|
212
|
+
break
|
|
152
213
|
}
|
|
153
214
|
}
|
|
154
215
|
</script>
|
|
@@ -171,6 +232,7 @@
|
|
|
171
232
|
className
|
|
172
233
|
)}
|
|
173
234
|
onclick={toggle}
|
|
235
|
+
onkeydown={handleTriggerKeydown}
|
|
174
236
|
>
|
|
175
237
|
{#if renderSelected && selectedItem}
|
|
176
238
|
{@render renderSelected(selectedItem)}
|