@redseed/redseed-ui-vue3 8.21.0 → 8.21.2
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,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { ref, computed, onMounted, onUnmounted, useAttrs } from 'vue'
|
|
2
|
+
import { ref, computed, watch, onMounted, onUnmounted, useAttrs } from 'vue'
|
|
3
3
|
import { onClickOutside, useElementBounding } from '@vueuse/core'
|
|
4
4
|
import FormFieldSlot from './FormFieldSlot.vue'
|
|
5
5
|
import { ChevronDownIcon, CheckIcon } from '@heroicons/vue/24/outline'
|
|
@@ -55,6 +55,34 @@ function nativeChoose(event) {
|
|
|
55
55
|
emit('change', event.target.value)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Track native constraint-validation state for the desktop trigger.
|
|
60
|
+
*
|
|
61
|
+
* On desktop the native <select> is sr-only, so when the browser fires its
|
|
62
|
+
* `invalid` event (e.g. a required field left empty on form submit) the
|
|
63
|
+
* tooltip and focus would target the off-screen element. We intercept that
|
|
64
|
+
* event, prevent the default browser behaviour, and instead focus the
|
|
65
|
+
* visible <button> trigger while applying :invalid-equivalent styling
|
|
66
|
+
* through this reactive flag and displaying the validation message text.
|
|
67
|
+
*/
|
|
68
|
+
const isNativeInvalid = ref(false)
|
|
69
|
+
const nativeValidationMessage = ref('')
|
|
70
|
+
|
|
71
|
+
function handleInvalid(event) {
|
|
72
|
+
if (isMobileDevice.value) return
|
|
73
|
+
|
|
74
|
+
event.preventDefault()
|
|
75
|
+
isNativeInvalid.value = true
|
|
76
|
+
nativeValidationMessage.value = event.target.validationMessage || 'Please select an option.'
|
|
77
|
+
triggerElement.value?.focus()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Clear the invalid state as soon as the user selects a value
|
|
81
|
+
watch(model, () => {
|
|
82
|
+
isNativeInvalid.value = false
|
|
83
|
+
nativeValidationMessage.value = ''
|
|
84
|
+
})
|
|
85
|
+
|
|
58
86
|
function handleKeydown(event) {
|
|
59
87
|
if (!isOpen.value && (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter' || event.key === ' ')) {
|
|
60
88
|
if (event.repeat) return
|
|
@@ -134,9 +162,29 @@ function calculateDropdownPosition() {
|
|
|
134
162
|
*/
|
|
135
163
|
const spaceBelow = viewportHeight - anchorBounding.bottom.value
|
|
136
164
|
|
|
137
|
-
dropdownElement.value.style.
|
|
165
|
+
dropdownElement.value.style.minWidth = `${anchorBounding.width.value}px`
|
|
138
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Clamp the dropdown to the viewport horizontally.
|
|
169
|
+
* First cap maxWidth to the full viewport minus margins, then shift
|
|
170
|
+
* left if the dropdown's natural width would overflow the right edge.
|
|
171
|
+
* This keeps the popover fully visible for triggers near the right
|
|
172
|
+
* edge (e.g. fixed-width filter selects in ListControl).
|
|
173
|
+
*/
|
|
174
|
+
const viewportWidth = window.innerWidth
|
|
175
|
+
const safeMargin = 16
|
|
176
|
+
dropdownElement.value.style.maxWidth = `${viewportWidth - safeMargin * 2}px`
|
|
177
|
+
|
|
178
|
+
// Temporarily position at trigger left so we can measure actual width
|
|
139
179
|
dropdownElement.value.style.left = `${anchorBounding.left.value}px`
|
|
180
|
+
const dropdownWidth = dropdownElement.value.offsetWidth
|
|
181
|
+
const rightOverflow = (anchorBounding.left.value + dropdownWidth + safeMargin) - viewportWidth
|
|
182
|
+
|
|
183
|
+
// Shift left if the dropdown overflows the right edge
|
|
184
|
+
if (rightOverflow > 0) {
|
|
185
|
+
const clampedLeft = Math.max(safeMargin, anchorBounding.left.value - rightOverflow)
|
|
186
|
+
dropdownElement.value.style.left = `${clampedLeft}px`
|
|
187
|
+
}
|
|
140
188
|
|
|
141
189
|
if (spaceAbove <= dropdownElementHeight
|
|
142
190
|
&& spaceBelow <= dropdownElementHeight) {
|
|
@@ -170,6 +218,10 @@ onUnmounted(() => {
|
|
|
170
218
|
|
|
171
219
|
defineExpose({
|
|
172
220
|
focus() {
|
|
221
|
+
if (!isMobileDevice.value && triggerElement.value) {
|
|
222
|
+
triggerElement.value.focus()
|
|
223
|
+
return
|
|
224
|
+
}
|
|
173
225
|
if (selectElement.value) selectElement.value.focus()
|
|
174
226
|
},
|
|
175
227
|
})
|
|
@@ -196,14 +248,18 @@ defineExpose({
|
|
|
196
248
|
ref="triggerElement"
|
|
197
249
|
v-if="!isMobileDevice"
|
|
198
250
|
type="button"
|
|
199
|
-
class="
|
|
251
|
+
:class="[
|
|
252
|
+
'rsui-form-field-select__trigger',
|
|
253
|
+
'peer',
|
|
254
|
+
{ 'rsui-form-field-select__trigger--invalid': isNativeInvalid }
|
|
255
|
+
]"
|
|
200
256
|
role="combobox"
|
|
201
257
|
:aria-activedescendant="highlightedIndex >= 0 ? `${effectiveId}-option-${highlightedIndex}` : undefined"
|
|
202
258
|
:aria-controls="`${effectiveId}-listbox`"
|
|
203
259
|
:aria-describedby="ariaDescribedby"
|
|
204
260
|
:aria-expanded="isOpen"
|
|
205
261
|
:aria-haspopup="'listbox'"
|
|
206
|
-
:aria-invalid="ariaInvalid"
|
|
262
|
+
:aria-invalid="ariaInvalid || isNativeInvalid || undefined"
|
|
207
263
|
:aria-required="$attrs.required || undefined"
|
|
208
264
|
:disabled="$attrs.disabled"
|
|
209
265
|
:id="effectiveId"
|
|
@@ -214,7 +270,7 @@ defineExpose({
|
|
|
214
270
|
</button>
|
|
215
271
|
|
|
216
272
|
<select ref="selectElement"
|
|
217
|
-
|
|
273
|
+
class="peer"
|
|
218
274
|
v-model="model"
|
|
219
275
|
:aria-describedby="isMobileDevice ? ariaDescribedby : undefined"
|
|
220
276
|
:aria-invalid="isMobileDevice ? ariaInvalid : undefined"
|
|
@@ -228,6 +284,7 @@ defineExpose({
|
|
|
228
284
|
:name="$attrs.name"
|
|
229
285
|
:required="$attrs.required"
|
|
230
286
|
@change.prevent="nativeChoose"
|
|
287
|
+
@invalid="handleInvalid"
|
|
231
288
|
>
|
|
232
289
|
<option value="" disabled>
|
|
233
290
|
<slot name="default-option">
|
|
@@ -309,8 +366,10 @@ defineExpose({
|
|
|
309
366
|
<slot name="help"></slot>
|
|
310
367
|
</template>
|
|
311
368
|
|
|
312
|
-
<template #error v-if="$slots.error">
|
|
313
|
-
<slot name="error"
|
|
369
|
+
<template #error v-if="$slots.error || nativeValidationMessage">
|
|
370
|
+
<slot name="error">
|
|
371
|
+
{{ nativeValidationMessage }}
|
|
372
|
+
</slot>
|
|
314
373
|
</template>
|
|
315
374
|
</FormFieldSlot>
|
|
316
375
|
</template>
|