@redseed/redseed-ui-vue3 8.40.0 → 8.41.0
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
|
<script setup>
|
|
2
2
|
import { ref, computed, onMounted, onUnmounted, watch, useAttrs } from 'vue'
|
|
3
|
-
import { onClickOutside
|
|
3
|
+
import { onClickOutside } from '@vueuse/core'
|
|
4
4
|
import FormFieldSlot from './FormFieldSlot.vue'
|
|
5
5
|
import { ChevronDownIcon, CheckIcon } from '@heroicons/vue/24/outline'
|
|
6
6
|
import { useFormFieldA11y } from '../../composables/useFormFieldA11y.js'
|
|
@@ -326,40 +326,53 @@ onClickOutside(comboboxElement, () => {
|
|
|
326
326
|
}
|
|
327
327
|
})
|
|
328
328
|
|
|
329
|
-
const inputElementBounding = useElementBounding(inputElement)
|
|
330
|
-
|
|
331
329
|
function calculateDropdownPosition() {
|
|
332
|
-
if (!dropdownElement.value) return
|
|
330
|
+
if (!dropdownElement.value || !inputElement.value) return
|
|
333
331
|
|
|
332
|
+
const bounding = inputElement.value.getBoundingClientRect()
|
|
334
333
|
const viewportHeight = window.innerHeight
|
|
335
334
|
const dropdownElementHeight = dropdownElement.value.offsetHeight
|
|
336
|
-
const spaceAboveInput =
|
|
337
|
-
const spaceBelowInput = viewportHeight -
|
|
335
|
+
const spaceAboveInput = bounding.top
|
|
336
|
+
const spaceBelowInput = viewportHeight - bounding.bottom
|
|
338
337
|
|
|
339
|
-
dropdownElement.value.style.width = `${
|
|
340
|
-
dropdownElement.value.style.left = `${
|
|
338
|
+
dropdownElement.value.style.width = `${bounding.width}px`
|
|
339
|
+
dropdownElement.value.style.left = `${bounding.left}px`
|
|
341
340
|
|
|
342
341
|
if (spaceAboveInput <= dropdownElementHeight && spaceBelowInput <= dropdownElementHeight) {
|
|
343
342
|
dropdownElement.value.style.top = '0'
|
|
344
343
|
dropdownElement.value.style.bottom = 'auto'
|
|
345
344
|
return
|
|
346
345
|
} else if (spaceBelowInput > dropdownElementHeight) {
|
|
347
|
-
dropdownElement.value.style.top = `${
|
|
346
|
+
dropdownElement.value.style.top = `${bounding.bottom + window.scrollY}px`
|
|
348
347
|
dropdownElement.value.style.bottom = 'auto'
|
|
349
348
|
return
|
|
350
349
|
} else if (spaceAboveInput > dropdownElementHeight) {
|
|
351
350
|
dropdownElement.value.style.top = 'auto'
|
|
352
|
-
dropdownElement.value.style.bottom = `${spaceBelowInput +
|
|
351
|
+
dropdownElement.value.style.bottom = `${spaceBelowInput + bounding.height + 8 - window.scrollY}px`
|
|
353
352
|
return
|
|
354
353
|
}
|
|
355
354
|
}
|
|
356
355
|
|
|
356
|
+
function handleScroll(event) {
|
|
357
|
+
if (dropdownElement.value?.contains(event.target)) return
|
|
358
|
+
calculateDropdownPosition()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
watch(isOpen, (nowOpen) => {
|
|
362
|
+
if (nowOpen) {
|
|
363
|
+
window.addEventListener('scroll', handleScroll, { capture: true, passive: true })
|
|
364
|
+
} else {
|
|
365
|
+
window.removeEventListener('scroll', handleScroll, { capture: true })
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
|
|
357
369
|
onMounted(() => {
|
|
358
370
|
window.addEventListener('resize', calculateDropdownPosition)
|
|
359
371
|
})
|
|
360
372
|
|
|
361
373
|
onUnmounted(() => {
|
|
362
374
|
window.removeEventListener('resize', calculateDropdownPosition)
|
|
375
|
+
window.removeEventListener('scroll', handleScroll, { capture: true })
|
|
363
376
|
})
|
|
364
377
|
|
|
365
378
|
defineExpose({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref, computed, onMounted, onUnmounted, watch, useAttrs } from 'vue'
|
|
3
|
-
import { onClickOutside
|
|
3
|
+
import { onClickOutside } from '@vueuse/core'
|
|
4
4
|
import FormFieldSlot from './FormFieldSlot.vue'
|
|
5
5
|
import { MagnifyingGlassIcon } from '@heroicons/vue/24/outline'
|
|
6
6
|
import { useFormFieldA11y } from '../../composables/useFormFieldA11y.js'
|
|
@@ -196,6 +196,20 @@ function handleKeyup(event) {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
function handleKeydown(event) {
|
|
199
|
+
// Let selection modifiers pass through to the native input so keyboard
|
|
200
|
+
// text-selection shortcuts work while typing (e.g. Shift+Home, Ctrl+Shift+
|
|
201
|
+
// ArrowLeft on Windows; Cmd+Shift+ArrowLeft, Option+Shift+ArrowLeft on
|
|
202
|
+
// macOS). Without this, the preventDefault() calls below would intercept
|
|
203
|
+
// Arrow/Home/End and kill the browser's native text selection.
|
|
204
|
+
if (event.shiftKey || event.ctrlKey || event.metaKey) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
// Alt+ArrowDown is the standard ARIA combobox open shortcut — let it through.
|
|
208
|
+
// Other Alt combos (e.g. Option+Arrow on macOS) pass through to native.
|
|
209
|
+
if (event.altKey && event.key !== 'ArrowDown') {
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
199
213
|
if (event.key === 'ArrowDown') {
|
|
200
214
|
event.preventDefault()
|
|
201
215
|
if (!isOpen.value) {
|
|
@@ -231,39 +245,52 @@ onClickOutside(rootElement, () => {
|
|
|
231
245
|
// Measure the whole field box (icon prefix + input), not the inner <input> —
|
|
232
246
|
// the input sits to the right of the prefix and is narrower, so anchoring to
|
|
233
247
|
// it would leave the dropdown shifted right and narrower than the field.
|
|
234
|
-
const fieldElementBounding = useElementBounding(fieldElement)
|
|
235
|
-
|
|
236
248
|
function calculateDropdownPosition() {
|
|
237
|
-
if (!dropdownElement.value) return
|
|
249
|
+
if (!dropdownElement.value || !fieldElement.value) return
|
|
238
250
|
|
|
251
|
+
const bounding = fieldElement.value.getBoundingClientRect()
|
|
239
252
|
const viewportHeight = window.innerHeight
|
|
240
253
|
const dropdownHeight = dropdownElement.value.offsetHeight
|
|
241
|
-
const spaceAbove =
|
|
242
|
-
const spaceBelow = viewportHeight -
|
|
254
|
+
const spaceAbove = bounding.top
|
|
255
|
+
const spaceBelow = viewportHeight - bounding.bottom
|
|
243
256
|
|
|
244
257
|
dropdownElement.value.style.width = props.dropdownWidth != null
|
|
245
258
|
? (typeof props.dropdownWidth === 'number' ? `${props.dropdownWidth}px` : props.dropdownWidth)
|
|
246
|
-
: `${
|
|
247
|
-
dropdownElement.value.style.left = `${
|
|
259
|
+
: `${bounding.width}px`
|
|
260
|
+
dropdownElement.value.style.left = `${bounding.left}px`
|
|
248
261
|
|
|
249
262
|
if (spaceAbove <= dropdownHeight && spaceBelow <= dropdownHeight) {
|
|
250
263
|
dropdownElement.value.style.top = '0'
|
|
251
264
|
dropdownElement.value.style.bottom = 'auto'
|
|
252
265
|
} else if (spaceBelow > dropdownHeight) {
|
|
253
|
-
dropdownElement.value.style.top = `${
|
|
266
|
+
dropdownElement.value.style.top = `${bounding.bottom + window.scrollY}px`
|
|
254
267
|
dropdownElement.value.style.bottom = 'auto'
|
|
255
268
|
} else if (spaceAbove > dropdownHeight) {
|
|
256
269
|
dropdownElement.value.style.top = 'auto'
|
|
257
|
-
dropdownElement.value.style.bottom = `${spaceBelow +
|
|
270
|
+
dropdownElement.value.style.bottom = `${spaceBelow + bounding.height + 8 - window.scrollY}px`
|
|
258
271
|
}
|
|
259
272
|
}
|
|
260
273
|
|
|
274
|
+
function handleScroll(event) {
|
|
275
|
+
if (dropdownElement.value?.contains(event.target)) return
|
|
276
|
+
calculateDropdownPosition()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
watch(isOpen, (nowOpen) => {
|
|
280
|
+
if (nowOpen) {
|
|
281
|
+
window.addEventListener('scroll', handleScroll, { capture: true, passive: true })
|
|
282
|
+
} else {
|
|
283
|
+
window.removeEventListener('scroll', handleScroll, { capture: true })
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
|
|
261
287
|
onMounted(() => {
|
|
262
288
|
window.addEventListener('resize', calculateDropdownPosition)
|
|
263
289
|
})
|
|
264
290
|
|
|
265
291
|
onUnmounted(() => {
|
|
266
292
|
window.removeEventListener('resize', calculateDropdownPosition)
|
|
293
|
+
window.removeEventListener('scroll', handleScroll, { capture: true })
|
|
267
294
|
})
|
|
268
295
|
|
|
269
296
|
defineExpose({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref, computed, watch, onMounted, onUnmounted, useAttrs } from 'vue'
|
|
3
|
-
import { onClickOutside
|
|
3
|
+
import { onClickOutside } from '@vueuse/core'
|
|
4
4
|
import FormFieldSlot from './FormFieldSlot.vue'
|
|
5
5
|
import { ChevronDownIcon, CheckIcon } from '@heroicons/vue/24/outline'
|
|
6
6
|
import { useFormFieldA11y } from '../../composables/useFormFieldA11y.js'
|
|
@@ -144,10 +144,14 @@ onClickOutside(formFieldSelectElement, () => close())
|
|
|
144
144
|
|
|
145
145
|
// Anchor dropdown to the visible trigger (button on desktop, select on mobile)
|
|
146
146
|
const anchorElement = computed(() => triggerElement.value || selectElement.value)
|
|
147
|
-
const anchorBounding = useElementBounding(anchorElement)
|
|
148
147
|
|
|
149
148
|
function calculateDropdownPosition() {
|
|
150
|
-
if (!dropdownElement.value) return
|
|
149
|
+
if (!dropdownElement.value || !anchorElement.value) return
|
|
150
|
+
|
|
151
|
+
// Preserve scrollTop before the reset so scroll-chaining (wheel past the
|
|
152
|
+
// list's boundary chains to the page, triggering this handler) cannot snap
|
|
153
|
+
// the list back to the top when maxHeight is cleared and a reflow occurs.
|
|
154
|
+
const savedScrollTop = dropdownElement.value.scrollTop
|
|
151
155
|
|
|
152
156
|
// Reset inline positioning before measuring so a previously-applied
|
|
153
157
|
// max-height (from an earlier constrained open) doesn't pollute the
|
|
@@ -157,12 +161,13 @@ function calculateDropdownPosition() {
|
|
|
157
161
|
dropdownElement.value.style.top = ''
|
|
158
162
|
dropdownElement.value.style.bottom = ''
|
|
159
163
|
|
|
164
|
+
const bounding = anchorElement.value.getBoundingClientRect()
|
|
160
165
|
const dropdownElementHeight = dropdownElement.value.offsetHeight
|
|
161
166
|
const viewportHeight = window.innerHeight
|
|
162
|
-
const spaceAbove =
|
|
163
|
-
const spaceBelow = viewportHeight -
|
|
167
|
+
const spaceAbove = bounding.top
|
|
168
|
+
const spaceBelow = viewportHeight - bounding.bottom
|
|
164
169
|
|
|
165
|
-
dropdownElement.value.style.minWidth = `${
|
|
170
|
+
dropdownElement.value.style.minWidth = `${bounding.width}px`
|
|
166
171
|
|
|
167
172
|
/**
|
|
168
173
|
* Clamp the dropdown to the viewport horizontally.
|
|
@@ -176,13 +181,13 @@ function calculateDropdownPosition() {
|
|
|
176
181
|
dropdownElement.value.style.maxWidth = `${viewportWidth - safeMargin * 2}px`
|
|
177
182
|
|
|
178
183
|
// Temporarily position at trigger left so we can measure actual width
|
|
179
|
-
dropdownElement.value.style.left = `${
|
|
184
|
+
dropdownElement.value.style.left = `${bounding.left}px`
|
|
180
185
|
const dropdownWidth = dropdownElement.value.offsetWidth
|
|
181
|
-
const rightOverflow = (
|
|
186
|
+
const rightOverflow = (bounding.left + dropdownWidth + safeMargin) - viewportWidth
|
|
182
187
|
|
|
183
188
|
// Shift left if the dropdown overflows the right edge
|
|
184
189
|
if (rightOverflow > 0) {
|
|
185
|
-
const clampedLeft = Math.max(safeMargin,
|
|
190
|
+
const clampedLeft = Math.max(safeMargin, bounding.left - rightOverflow)
|
|
186
191
|
dropdownElement.value.style.left = `${clampedLeft}px`
|
|
187
192
|
}
|
|
188
193
|
|
|
@@ -192,18 +197,21 @@ function calculateDropdownPosition() {
|
|
|
192
197
|
const verticalOffset = 16
|
|
193
198
|
|
|
194
199
|
if (spaceBelow > dropdownElementHeight) {
|
|
195
|
-
dropdownElement.value.style.top = `${
|
|
200
|
+
dropdownElement.value.style.top = `${bounding.bottom + window.scrollY}px`
|
|
196
201
|
dropdownElement.value.style.bottom = 'auto'
|
|
202
|
+
dropdownElement.value.scrollTop = savedScrollTop
|
|
197
203
|
return
|
|
198
204
|
} else if (spaceAbove > spaceBelow) {
|
|
199
205
|
dropdownElement.value.style.top = 'auto'
|
|
200
|
-
dropdownElement.value.style.bottom = `${spaceBelow +
|
|
206
|
+
dropdownElement.value.style.bottom = `${spaceBelow + bounding.height + 8 - window.scrollY}px`
|
|
201
207
|
dropdownElement.value.style.maxHeight = `${spaceAbove - verticalOffset}px`
|
|
208
|
+
dropdownElement.value.scrollTop = savedScrollTop
|
|
202
209
|
return
|
|
203
210
|
} else {
|
|
204
|
-
dropdownElement.value.style.top = `${
|
|
211
|
+
dropdownElement.value.style.top = `${bounding.bottom + window.scrollY}px`
|
|
205
212
|
dropdownElement.value.style.bottom = 'auto'
|
|
206
213
|
dropdownElement.value.style.maxHeight = `${spaceBelow - verticalOffset}px`
|
|
214
|
+
dropdownElement.value.scrollTop = savedScrollTop
|
|
207
215
|
return
|
|
208
216
|
}
|
|
209
217
|
}
|
|
@@ -212,6 +220,19 @@ function handleResize() {
|
|
|
212
220
|
calculateDropdownPosition()
|
|
213
221
|
}
|
|
214
222
|
|
|
223
|
+
function handleScroll(event) {
|
|
224
|
+
if (dropdownElement.value?.contains(event.target)) return
|
|
225
|
+
calculateDropdownPosition()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
watch(isOpen, (nowOpen) => {
|
|
229
|
+
if (nowOpen) {
|
|
230
|
+
window.addEventListener('scroll', handleScroll, { capture: true, passive: true })
|
|
231
|
+
} else {
|
|
232
|
+
window.removeEventListener('scroll', handleScroll, { capture: true })
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
|
|
215
236
|
onMounted(() => {
|
|
216
237
|
isMobileDevice.value = 'ontouchstart' in window
|
|
217
238
|
|| (navigator.maxTouchPoints && navigator.maxTouchPoints > 0)
|
|
@@ -220,6 +241,7 @@ onMounted(() => {
|
|
|
220
241
|
|
|
221
242
|
onUnmounted(() => {
|
|
222
243
|
window.removeEventListener('resize', handleResize)
|
|
244
|
+
window.removeEventListener('scroll', handleScroll, { capture: true })
|
|
223
245
|
})
|
|
224
246
|
|
|
225
247
|
defineExpose({
|