@redseed/redseed-ui-vue3 8.21.1 → 8.22.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 +1 -1
- package/src/components/ActivityFeeds/FeedItem.vue +6 -1
- package/src/components/Breadcrumbs/Breadcrumbs.vue +27 -13
- package/src/components/Button/ButtonSlot.vue +16 -3
- package/src/components/Card/Card.vue +5 -0
- package/src/components/Card/CardHeader.vue +5 -1
- package/src/components/Card/CheckboxCard.vue +7 -10
- package/src/components/Card/MetricCard.vue +5 -2
- package/src/components/Card/RadioCard.vue +9 -2
- package/src/components/CardGroup/CardGroup.vue +43 -0
- package/src/components/Disclosure/Disclosure.vue +4 -0
- package/src/components/DropdownMenu/DropdownMenu.vue +47 -4
- package/src/components/DropdownMenu/DropdownOption.vue +9 -1
- package/src/components/FormField/FormFieldCheckbox.vue +2 -2
- package/src/components/FormField/FormFieldCombobox.vue +35 -15
- package/src/components/FormField/FormFieldRadioGroup.vue +2 -1
- package/src/components/FormField/FormFieldSelect.vue +74 -6
- package/src/components/FormField/FormFieldSlot.vue +2 -2
- package/src/components/FormField/FormFieldTextarea.vue +1 -0
- package/src/components/FormField/FormFieldUploaderWrapper.vue +9 -1
- package/src/components/Link/LinkSlot.vue +30 -3
- package/src/components/LinkedList/LinkedListItem.vue +3 -1
- package/src/components/List/ListItem.vue +10 -5
- package/src/components/Modal/Modal.vue +31 -2
- package/src/components/Pagination/PaginationItem.vue +7 -3
- package/src/components/Pagination/PaginationItemNext.vue +2 -1
- package/src/components/Pagination/PaginationItemPrevious.vue +2 -1
- package/src/components/Progress/ProgressBar.vue +6 -1
- package/src/components/Progress/ProgressCircle.vue +7 -2
- package/src/components/Progress/ProgressTrackerStep.vue +5 -0
- package/src/components/Table/Table.vue +12 -3
- package/src/components/Table/TdUser.vue +1 -1
- package/src/components/Table/Th.vue +5 -4
- package/src/components/Table/Tr.vue +5 -5
- package/src/components/Toggle/Toggle.vue +5 -3
- package/src/components/Tooltip/Tooltip.vue +21 -12
- package/src/composables/useFormFieldA11y.js +1 -1
package/package.json
CHANGED
|
@@ -40,7 +40,11 @@ function handleClick() {
|
|
|
40
40
|
'rsui-feed-item--padded': padded,
|
|
41
41
|
}
|
|
42
42
|
]"
|
|
43
|
+
:tabindex="clickable ? 0 : undefined"
|
|
44
|
+
:role="clickable ? 'button' : undefined"
|
|
43
45
|
@click="handleClick"
|
|
46
|
+
@keydown.enter.self.prevent="!$event.repeat && handleClick()"
|
|
47
|
+
@keydown.space.self.prevent="!$event.repeat && handleClick()"
|
|
44
48
|
>
|
|
45
49
|
|
|
46
50
|
<div class="rsui-feed-item__avatar">
|
|
@@ -48,7 +52,7 @@ function handleClick() {
|
|
|
48
52
|
<slot name="icon">
|
|
49
53
|
<IconCircleBackground lg invert>
|
|
50
54
|
<slot name="avatar">
|
|
51
|
-
<UserIcon />
|
|
55
|
+
<UserIcon aria-hidden="true" />
|
|
52
56
|
</slot>
|
|
53
57
|
</IconCircleBackground>
|
|
54
58
|
</slot>
|
|
@@ -56,6 +60,7 @@ function handleClick() {
|
|
|
56
60
|
<div v-if="status !== false"
|
|
57
61
|
class="rsui-feed-item__avatar-indicator"
|
|
58
62
|
>
|
|
63
|
+
<span class="sr-only">{{ status }}</span>
|
|
59
64
|
<slot name="avatar-indicator"
|
|
60
65
|
:status="status"
|
|
61
66
|
>
|
|
@@ -14,22 +14,36 @@ const props = defineProps({
|
|
|
14
14
|
}
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
+
const isLastItem = (index) => index === props.items.length - 1
|
|
18
|
+
|
|
17
19
|
function clickItem(item) {
|
|
18
20
|
if (typeof item.action === 'function') item.action()
|
|
19
21
|
}
|
|
20
22
|
</script>
|
|
21
23
|
<template>
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
<nav aria-label="Breadcrumb" class="rsui-breadcrumbs">
|
|
25
|
+
<ol class="rsui-breadcrumbs__list">
|
|
26
|
+
<li v-for="(item, index) in items"
|
|
27
|
+
:key="index"
|
|
28
|
+
:class="[
|
|
29
|
+
'rsui-breadcrumbs__item',
|
|
30
|
+
{ 'rsui-breadcrumbs__item--action': typeof item.action === 'function' }
|
|
31
|
+
]"
|
|
32
|
+
:aria-current="isLastItem(index) ? 'page' : undefined"
|
|
33
|
+
>
|
|
34
|
+
<span v-if="typeof item.action === 'function'"
|
|
35
|
+
role="button"
|
|
36
|
+
tabindex="0"
|
|
37
|
+
@click="clickItem(item)"
|
|
38
|
+
@keydown.enter.prevent="!$event.repeat && clickItem(item)"
|
|
39
|
+
@keydown.space.prevent="!$event.repeat && clickItem(item)"
|
|
40
|
+
>
|
|
41
|
+
{{ item.label }}
|
|
42
|
+
</span>
|
|
43
|
+
<span v-if="typeof item.action !== 'function'">
|
|
44
|
+
{{ item.label }}
|
|
45
|
+
</span>
|
|
46
|
+
</li>
|
|
47
|
+
</ol>
|
|
48
|
+
</nav>
|
|
35
49
|
</template>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { ref, computed } from 'vue'
|
|
2
|
+
import { ref, computed, onMounted, watchEffect } from 'vue'
|
|
3
3
|
|
|
4
4
|
const props = defineProps({
|
|
5
5
|
xs: {
|
|
@@ -42,8 +42,8 @@ defineEmits(['click'])
|
|
|
42
42
|
// button element ref
|
|
43
43
|
const buttonElementRef = ref(null)
|
|
44
44
|
|
|
45
|
-
// check if the button
|
|
46
|
-
const iconOnly = computed(() => buttonElementRef.value && !buttonElementRef.value.textContent)
|
|
45
|
+
// check if the button has no visible text (icon-only state)
|
|
46
|
+
const iconOnly = computed(() => buttonElementRef.value && !buttonElementRef.value.textContent.trim())
|
|
47
47
|
|
|
48
48
|
// button sizes
|
|
49
49
|
const sizes = {
|
|
@@ -96,6 +96,15 @@ const buttonSlotClass = computed(() => [
|
|
|
96
96
|
'rsui-button-slot--right': props.alignment === 'right',
|
|
97
97
|
},
|
|
98
98
|
])
|
|
99
|
+
|
|
100
|
+
// warn in development when an icon-only button lacks an accessible name
|
|
101
|
+
onMounted(() => {
|
|
102
|
+
watchEffect(() => {
|
|
103
|
+
if (process.env.NODE_ENV !== 'production' && iconOnly.value && !buttonElementRef.value.getAttribute('aria-label') && !buttonElementRef.value.getAttribute('aria-labelledby')) {
|
|
104
|
+
console.warn('[RSUI] Icon-only button detected without aria-label. Add aria-label for accessibility.')
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
})
|
|
99
108
|
</script>
|
|
100
109
|
<template>
|
|
101
110
|
<button
|
|
@@ -104,6 +113,10 @@ const buttonSlotClass = computed(() => [
|
|
|
104
113
|
type="button"
|
|
105
114
|
@click="$emit('click', $event)"
|
|
106
115
|
>
|
|
116
|
+
<span v-if="$slots.icon" aria-hidden="true">
|
|
117
|
+
<slot name="icon"></slot>
|
|
118
|
+
</span>
|
|
119
|
+
|
|
107
120
|
<slot></slot>
|
|
108
121
|
</button>
|
|
109
122
|
</template>
|
|
@@ -55,6 +55,10 @@ const props = defineProps({
|
|
|
55
55
|
type: Boolean,
|
|
56
56
|
default: false,
|
|
57
57
|
},
|
|
58
|
+
ariaLabel: {
|
|
59
|
+
type: String,
|
|
60
|
+
default: undefined,
|
|
61
|
+
},
|
|
58
62
|
})
|
|
59
63
|
|
|
60
64
|
const emit = defineEmits(['click'])
|
|
@@ -121,6 +125,7 @@ function onClick() {
|
|
|
121
125
|
role="button"
|
|
122
126
|
:tabindex="disabled ? undefined : 0"
|
|
123
127
|
:title="$attrs.title"
|
|
128
|
+
:aria-label="!$slots['aria-label'] ? ariaLabel : undefined"
|
|
124
129
|
:aria-disabled="disabled ? true : undefined"
|
|
125
130
|
@click="onClick"
|
|
126
131
|
@keydown.enter.prevent="!$event.repeat && onClick()"
|
|
@@ -74,6 +74,10 @@ function handleMoreActionsClick() {
|
|
|
74
74
|
'rsui-card-header--clickable': isClickable,
|
|
75
75
|
}
|
|
76
76
|
]"
|
|
77
|
+
:role="isClickable ? 'button' : undefined"
|
|
78
|
+
:tabindex="isClickable ? 0 : undefined"
|
|
79
|
+
@keydown.enter.self.prevent="isClickable && !$event.repeat && $emit('click', $event)"
|
|
80
|
+
@keydown.space.self.prevent="isClickable && !$event.repeat && $emit('click', $event)"
|
|
77
81
|
>
|
|
78
82
|
<div :class="[
|
|
79
83
|
'rsui-card-header__header',
|
|
@@ -141,7 +145,7 @@ function handleMoreActionsClick() {
|
|
|
141
145
|
<slot name="more-actions"
|
|
142
146
|
:handleMoreActionsClick="handleMoreActionsClick"
|
|
143
147
|
>
|
|
144
|
-
<ButtonTertiary @click="handleMoreActionsClick">
|
|
148
|
+
<ButtonTertiary aria-label="More actions" @click="handleMoreActionsClick">
|
|
145
149
|
<slot name="more-actions-label">
|
|
146
150
|
<Icon>
|
|
147
151
|
<EllipsisVerticalIcon />
|
|
@@ -4,6 +4,8 @@ import Card from './Card.vue'
|
|
|
4
4
|
import CardHeader from './CardHeader.vue'
|
|
5
5
|
import FormFieldCheckbox from '../FormField/FormFieldCheckbox.vue'
|
|
6
6
|
|
|
7
|
+
const checkboxId = _.uniqueId('checkbox-card-')
|
|
8
|
+
|
|
7
9
|
const props = defineProps({
|
|
8
10
|
checked: {
|
|
9
11
|
type: Boolean,
|
|
@@ -35,7 +37,6 @@ watch(() => props.checked, (val) => {
|
|
|
35
37
|
isChecked.value = val
|
|
36
38
|
})
|
|
37
39
|
|
|
38
|
-
|
|
39
40
|
function toggleChecked() {
|
|
40
41
|
if (props.disabled) return
|
|
41
42
|
isChecked.value = !isChecked.value
|
|
@@ -43,11 +44,6 @@ function toggleChecked() {
|
|
|
43
44
|
emit(isChecked.value ? 'check' : 'uncheck', isChecked.value)
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
function onTitleClick() {
|
|
47
|
-
if (props.disabled) return
|
|
48
|
-
toggleChecked()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
47
|
function onFieldInput() {
|
|
52
48
|
emit('change', isChecked.value)
|
|
53
49
|
emit(isChecked.value ? 'check' : 'uncheck', isChecked.value)
|
|
@@ -73,16 +69,17 @@ function onFieldInput() {
|
|
|
73
69
|
>
|
|
74
70
|
<div class="rsui-checkbox-card__title-row">
|
|
75
71
|
<div class="rsui-checkbox-card__field">
|
|
76
|
-
<FormFieldCheckbox v-model="isChecked" :disabled="disabled" @input="onFieldInput" />
|
|
72
|
+
<FormFieldCheckbox :id="checkboxId" v-model="isChecked" :disabled="disabled" @input="onFieldInput" />
|
|
77
73
|
</div>
|
|
78
|
-
<
|
|
74
|
+
<label :for="checkboxId"
|
|
75
|
+
:class="[
|
|
79
76
|
'rsui-checkbox-card__title-text',
|
|
80
77
|
{ 'rsui-checkbox-card__title-text--strikethrough': strikethrough }
|
|
81
78
|
]"
|
|
82
|
-
@click.stop
|
|
79
|
+
@click.stop
|
|
83
80
|
>
|
|
84
81
|
<slot name="title"></slot>
|
|
85
|
-
</
|
|
82
|
+
</label>
|
|
86
83
|
</div>
|
|
87
84
|
|
|
88
85
|
<template #subtitle v-if="$slots.subtitle">
|
|
@@ -28,6 +28,9 @@ const props = defineProps({
|
|
|
28
28
|
:clickable="props.clickable"
|
|
29
29
|
:hoverable="props.hoverable"
|
|
30
30
|
>
|
|
31
|
+
<template v-if="$slots['aria-label']" #aria-label>
|
|
32
|
+
<slot name="aria-label"></slot>
|
|
33
|
+
</template>
|
|
31
34
|
<FlexContainer flexCol justifyCenter>
|
|
32
35
|
<div v-if="labelFirst"
|
|
33
36
|
:class="[
|
|
@@ -37,7 +40,7 @@ const props = defineProps({
|
|
|
37
40
|
{ 'rsui-metric-card--right': alignment === 'right' },
|
|
38
41
|
]"
|
|
39
42
|
>
|
|
40
|
-
<div v-if="$slots.icon" class="rsui-metric-card__icon">
|
|
43
|
+
<div v-if="$slots.icon" class="rsui-metric-card__icon" aria-hidden="true">
|
|
41
44
|
<slot name="icon"></slot>
|
|
42
45
|
</div>
|
|
43
46
|
<div class="rsui-metric-card__label">
|
|
@@ -63,7 +66,7 @@ const props = defineProps({
|
|
|
63
66
|
{ 'rsui-metric-card--right': alignment === 'right' },
|
|
64
67
|
]"
|
|
65
68
|
>
|
|
66
|
-
<div v-if="$slots.icon" class="rsui-metric-card__icon">
|
|
69
|
+
<div v-if="$slots.icon" class="rsui-metric-card__icon" aria-hidden="true">
|
|
67
70
|
<slot name="icon"></slot>
|
|
68
71
|
</div>
|
|
69
72
|
<div class="rsui-metric-card__label">
|
|
@@ -29,6 +29,8 @@ const props = defineProps({
|
|
|
29
29
|
},
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
+
const titleId = _.uniqueId('radio-card-title-')
|
|
33
|
+
|
|
32
34
|
const isSelected = ref(props.selected)
|
|
33
35
|
|
|
34
36
|
watch(() => props.selected, () => {
|
|
@@ -90,11 +92,16 @@ function handleClick(event) {
|
|
|
90
92
|
<template>
|
|
91
93
|
<div :class="componentClass"
|
|
92
94
|
@click="handleSelect"
|
|
95
|
+
@keydown.enter.self.prevent="!$event.repeat && handleSelect()"
|
|
96
|
+
@keydown.space.self.prevent="!$event.repeat && handleSelect()"
|
|
93
97
|
role="radio"
|
|
94
|
-
|
|
98
|
+
:aria-checked="isSelected ? 'true' : 'false'"
|
|
99
|
+
:aria-disabled="disabled || undefined"
|
|
100
|
+
:aria-labelledby="titleId"
|
|
101
|
+
:tabindex="disabled ? -1 : 0"
|
|
95
102
|
>
|
|
96
103
|
<div :class="titleRowClass">
|
|
97
|
-
<div class="rsui-radio-card__title">
|
|
104
|
+
<div :id="titleId" class="rsui-radio-card__title">
|
|
98
105
|
<slot name="title"></slot>
|
|
99
106
|
</div>
|
|
100
107
|
</div>
|
|
@@ -187,6 +187,43 @@ function cleanupDragState() {
|
|
|
187
187
|
dragFromIndex.value = null
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
function onHandleKeyDown(e) {
|
|
191
|
+
if (e.repeat) return
|
|
192
|
+
const child = getCardChild(e.target)
|
|
193
|
+
if (!child) return
|
|
194
|
+
const container = cardsContainerElement.value
|
|
195
|
+
if (!container) return
|
|
196
|
+
const children = Array.from(container.children)
|
|
197
|
+
const index = children.indexOf(child)
|
|
198
|
+
if (index === -1) return
|
|
199
|
+
|
|
200
|
+
if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
|
|
201
|
+
e.preventDefault()
|
|
202
|
+
if (index > 0) {
|
|
203
|
+
emit('reorder', { fromIndex: index, toIndex: index - 1 })
|
|
204
|
+
// Re-focus the handle after DOM update
|
|
205
|
+
requestAnimationFrame(() => {
|
|
206
|
+
const updatedChildren = Array.from(container.children)
|
|
207
|
+
const handle = updatedChildren[index - 1]?.querySelector('.rsui-card-group__drag-handle')
|
|
208
|
+
if (handle) handle.focus()
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
|
|
214
|
+
e.preventDefault()
|
|
215
|
+
if (index < children.length - 1) {
|
|
216
|
+
emit('reorder', { fromIndex: index, toIndex: index + 1 })
|
|
217
|
+
// Re-focus the handle after DOM update
|
|
218
|
+
requestAnimationFrame(() => {
|
|
219
|
+
const updatedChildren = Array.from(container.children)
|
|
220
|
+
const handle = updatedChildren[index + 1]?.querySelector('.rsui-card-group__drag-handle')
|
|
221
|
+
if (handle) handle.focus()
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
190
227
|
function syncHandles() {
|
|
191
228
|
const container = cardsContainerElement.value
|
|
192
229
|
if (!container) return
|
|
@@ -196,14 +233,19 @@ function syncHandles() {
|
|
|
196
233
|
if (props.reorderable && !hasHandle) {
|
|
197
234
|
const handle = document.createElement('div')
|
|
198
235
|
handle.className = `rsui-card-group__drag-handle rsui-card-group__drag-handle--${props.handleAlignment}`
|
|
236
|
+
handle.setAttribute('role', 'button')
|
|
237
|
+
handle.setAttribute('tabindex', '0')
|
|
238
|
+
handle.setAttribute('aria-label', 'Reorder')
|
|
199
239
|
handle.addEventListener('mousedown', onHandleMouseDown)
|
|
200
240
|
handle.addEventListener('mouseup', onHandleMouseUp)
|
|
241
|
+
handle.addEventListener('keydown', onHandleKeyDown)
|
|
201
242
|
handle.appendChild(createHandleSvg())
|
|
202
243
|
child.style.position = 'relative'
|
|
203
244
|
child.appendChild(handle)
|
|
204
245
|
} else if (!props.reorderable && hasHandle) {
|
|
205
246
|
hasHandle.removeEventListener('mousedown', onHandleMouseDown)
|
|
206
247
|
hasHandle.removeEventListener('mouseup', onHandleMouseUp)
|
|
248
|
+
hasHandle.removeEventListener('keydown', onHandleKeyDown)
|
|
207
249
|
hasHandle.remove()
|
|
208
250
|
}
|
|
209
251
|
})
|
|
@@ -217,6 +259,7 @@ function cleanupHandles() {
|
|
|
217
259
|
if (handle) {
|
|
218
260
|
handle.removeEventListener('mousedown', onHandleMouseDown)
|
|
219
261
|
handle.removeEventListener('mouseup', onHandleMouseUp)
|
|
262
|
+
handle.removeEventListener('keydown', onHandleKeyDown)
|
|
220
263
|
handle.remove()
|
|
221
264
|
}
|
|
222
265
|
child.removeAttribute('draggable')
|
|
@@ -116,6 +116,8 @@ onMounted(() => {
|
|
|
116
116
|
>
|
|
117
117
|
<slot name="trigger"
|
|
118
118
|
:handleTrigger="handleTrigger"
|
|
119
|
+
:isOpen="isOpen"
|
|
120
|
+
:contentId="contentId"
|
|
119
121
|
>
|
|
120
122
|
<ButtonTertiary
|
|
121
123
|
:sm="$attrs.sm"
|
|
@@ -124,6 +126,8 @@ onMounted(() => {
|
|
|
124
126
|
:xl="$attrs.xl"
|
|
125
127
|
:2xl="$attrs['2xl']"
|
|
126
128
|
:full="$attrs.full"
|
|
129
|
+
:aria-expanded="isOpen"
|
|
130
|
+
:aria-controls="contentId"
|
|
127
131
|
@click.stop="handleTrigger"
|
|
128
132
|
>
|
|
129
133
|
<Icon v-bind="iconProps">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
2
|
+
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
3
3
|
import ButtonTertiary from '../Button/ButtonTertiary.vue'
|
|
4
4
|
|
|
5
5
|
const props = defineProps({
|
|
@@ -26,27 +26,67 @@ const containerClass = computed(() => [
|
|
|
26
26
|
},
|
|
27
27
|
])
|
|
28
28
|
|
|
29
|
+
const containerRef = ref(null)
|
|
30
|
+
|
|
29
31
|
function open() {
|
|
30
32
|
isOpen.value = true
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
// Focus first menuitem when menu opens
|
|
36
|
+
watch(isOpen, (value) => {
|
|
37
|
+
if (value) {
|
|
38
|
+
nextTick(() => {
|
|
39
|
+
if (!containerRef.value) return
|
|
40
|
+
const firstItem = containerRef.value.querySelector('[role="menuitem"]')
|
|
41
|
+
if (firstItem) firstItem.focus()
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
33
46
|
function close() {
|
|
34
47
|
isOpen.value = false
|
|
35
48
|
}
|
|
36
49
|
|
|
37
50
|
function closeOnEscape(e) {
|
|
38
|
-
if (isOpen.value && e.key === 'Escape') {
|
|
51
|
+
if (isOpen.value && e.key === 'Escape' && !e.repeat) {
|
|
39
52
|
isOpen.value = false
|
|
40
53
|
}
|
|
41
54
|
}
|
|
42
55
|
|
|
56
|
+
// Arrow key navigation between menu items (WAI-ARIA menu pattern)
|
|
57
|
+
function handleMenuKeydown(event) {
|
|
58
|
+
const items = containerRef.value?.querySelectorAll('[role="menuitem"]')
|
|
59
|
+
if (!items?.length) return
|
|
60
|
+
|
|
61
|
+
const currentIndex = Array.from(items).indexOf(document.activeElement)
|
|
62
|
+
let nextIndex = -1
|
|
63
|
+
|
|
64
|
+
if (event.key === 'ArrowDown') {
|
|
65
|
+
nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0
|
|
66
|
+
}
|
|
67
|
+
if (event.key === 'ArrowUp') {
|
|
68
|
+
nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1
|
|
69
|
+
}
|
|
70
|
+
if (event.key === 'Home') {
|
|
71
|
+
nextIndex = 0
|
|
72
|
+
}
|
|
73
|
+
if (event.key === 'End') {
|
|
74
|
+
nextIndex = items.length - 1
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (nextIndex >= 0) {
|
|
78
|
+
event.preventDefault()
|
|
79
|
+
items[nextIndex].focus()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
43
83
|
onMounted(() => document.addEventListener('keydown', closeOnEscape))
|
|
44
84
|
onUnmounted(() => document.removeEventListener('keydown', closeOnEscape))
|
|
45
85
|
</script>
|
|
46
86
|
<template>
|
|
47
87
|
<div class="rsui-dropdown-menu">
|
|
48
|
-
<slot name="trigger" :open="open">
|
|
49
|
-
<ButtonTertiary @click="open">
|
|
88
|
+
<slot name="trigger" :open="open" :isOpen="isOpen">
|
|
89
|
+
<ButtonTertiary :aria-expanded="isOpen" @click="open">
|
|
50
90
|
<slot name="trigger-label"></slot>
|
|
51
91
|
</ButtonTertiary>
|
|
52
92
|
</slot>
|
|
@@ -67,7 +107,10 @@ onUnmounted(() => document.removeEventListener('keydown', closeOnEscape))
|
|
|
67
107
|
>
|
|
68
108
|
<div
|
|
69
109
|
v-show="isOpen"
|
|
110
|
+
ref="containerRef"
|
|
70
111
|
:class="containerClass"
|
|
112
|
+
role="menu"
|
|
113
|
+
@keydown="handleMenuKeydown"
|
|
71
114
|
@click="close"
|
|
72
115
|
>
|
|
73
116
|
<slot></slot>
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
const emit = defineEmits(['click'])
|
|
3
|
+
|
|
4
|
+
function handleClick() {
|
|
5
|
+
emit('click')
|
|
6
|
+
}
|
|
3
7
|
</script>
|
|
4
8
|
<template>
|
|
5
9
|
<div class="rsui-dropdown-option"
|
|
6
|
-
|
|
10
|
+
role="menuitem"
|
|
11
|
+
tabindex="0"
|
|
12
|
+
@click="handleClick"
|
|
13
|
+
@keydown.enter.prevent="!$event.repeat && handleClick()"
|
|
14
|
+
@keydown.space.prevent="!$event.repeat && handleClick()"
|
|
7
15
|
>
|
|
8
16
|
<slot></slot>
|
|
9
17
|
</div>
|
|
@@ -37,7 +37,7 @@ function check(event) {
|
|
|
37
37
|
<div class="rsui-form-field-checkbox__check">
|
|
38
38
|
<CheckIcon v-if="checked" aria-hidden="true"></CheckIcon>
|
|
39
39
|
<input
|
|
40
|
-
|
|
40
|
+
:checked="checked"
|
|
41
41
|
type="checkbox"
|
|
42
42
|
:aria-describedby="ariaDescribedby"
|
|
43
43
|
:aria-invalid="ariaInvalid"
|
|
@@ -47,7 +47,7 @@ function check(event) {
|
|
|
47
47
|
:id="inputId || $attrs.id"
|
|
48
48
|
:name="$attrs.name"
|
|
49
49
|
:required="$attrs.required"
|
|
50
|
-
@
|
|
50
|
+
@change="check"
|
|
51
51
|
>
|
|
52
52
|
</div>
|
|
53
53
|
<div v-if="$slots.label"
|
|
@@ -449,9 +449,12 @@ defineExpose({
|
|
|
449
449
|
]"
|
|
450
450
|
>
|
|
451
451
|
<!-- Loading state -->
|
|
452
|
-
<div
|
|
452
|
+
<div
|
|
453
453
|
v-if="dropdownContent === 'loading'"
|
|
454
454
|
class="rsui-form-field-combobox__option rsui-form-field-combobox__option--disabled"
|
|
455
|
+
role="option"
|
|
456
|
+
aria-disabled="true"
|
|
457
|
+
aria-selected="false"
|
|
455
458
|
>
|
|
456
459
|
<div class="rsui-form-field-combobox__option-label">
|
|
457
460
|
<slot name="loading-text">Searching...</slot>
|
|
@@ -462,9 +465,12 @@ defineExpose({
|
|
|
462
465
|
</div>
|
|
463
466
|
|
|
464
467
|
<!-- Error state -->
|
|
465
|
-
<div
|
|
466
|
-
v-
|
|
468
|
+
<div
|
|
469
|
+
v-if="dropdownContent === 'error'"
|
|
467
470
|
class="rsui-form-field-combobox__option rsui-form-field-combobox__option--disabled rsui-form-field-combobox__option--error"
|
|
471
|
+
role="option"
|
|
472
|
+
aria-disabled="true"
|
|
473
|
+
aria-selected="false"
|
|
468
474
|
>
|
|
469
475
|
<div class="rsui-form-field-combobox__option-label">
|
|
470
476
|
<slot name="error-text">Search failed</slot>
|
|
@@ -472,9 +478,12 @@ defineExpose({
|
|
|
472
478
|
</div>
|
|
473
479
|
|
|
474
480
|
<!-- Start typing message -->
|
|
475
|
-
<div
|
|
476
|
-
v-
|
|
481
|
+
<div
|
|
482
|
+
v-if="dropdownContent === 'start-typing'"
|
|
477
483
|
class="rsui-form-field-combobox__option rsui-form-field-combobox__option--disabled rsui-form-field-combobox__option--hint"
|
|
484
|
+
role="option"
|
|
485
|
+
aria-disabled="true"
|
|
486
|
+
aria-selected="false"
|
|
478
487
|
>
|
|
479
488
|
<div class="rsui-form-field-combobox__option-label">
|
|
480
489
|
<slot name="start-typing-text">Start typing to search</slot>
|
|
@@ -482,9 +491,12 @@ defineExpose({
|
|
|
482
491
|
</div>
|
|
483
492
|
|
|
484
493
|
<!-- Minimum length message -->
|
|
485
|
-
<div
|
|
486
|
-
v-
|
|
494
|
+
<div
|
|
495
|
+
v-if="dropdownContent === 'min-length'"
|
|
487
496
|
class="rsui-form-field-combobox__option rsui-form-field-combobox__option--disabled"
|
|
497
|
+
role="option"
|
|
498
|
+
aria-disabled="true"
|
|
499
|
+
aria-selected="false"
|
|
488
500
|
>
|
|
489
501
|
<div class="rsui-form-field-combobox__option-label">
|
|
490
502
|
Type at least {{ minSearchLength }} characters to search
|
|
@@ -492,9 +504,12 @@ defineExpose({
|
|
|
492
504
|
</div>
|
|
493
505
|
|
|
494
506
|
<!-- No results -->
|
|
495
|
-
<div
|
|
496
|
-
v-
|
|
507
|
+
<div
|
|
508
|
+
v-if="dropdownContent === 'no-results'"
|
|
497
509
|
class="rsui-form-field-combobox__option rsui-form-field-combobox__option--disabled"
|
|
510
|
+
role="option"
|
|
511
|
+
aria-disabled="true"
|
|
512
|
+
aria-selected="false"
|
|
498
513
|
>
|
|
499
514
|
<div class="rsui-form-field-combobox__option-label">
|
|
500
515
|
<slot name="no-results-text">No results found</slot>
|
|
@@ -502,9 +517,14 @@ defineExpose({
|
|
|
502
517
|
</div>
|
|
503
518
|
|
|
504
519
|
<!-- Grouped options -->
|
|
505
|
-
<template v-
|
|
506
|
-
<
|
|
507
|
-
|
|
520
|
+
<template v-if="dropdownContent === 'options' && groupedOptions">
|
|
521
|
+
<div
|
|
522
|
+
v-for="group in groupedOptions"
|
|
523
|
+
:key="group.name"
|
|
524
|
+
role="group"
|
|
525
|
+
:aria-label="group.name || undefined"
|
|
526
|
+
>
|
|
527
|
+
<div v-if="group.name" class="rsui-form-field-combobox__group-header" aria-hidden="true">
|
|
508
528
|
{{ group.name }}
|
|
509
529
|
</div>
|
|
510
530
|
<div
|
|
@@ -529,11 +549,11 @@ defineExpose({
|
|
|
529
549
|
<CheckIcon v-if="!navigable && option.value === model"></CheckIcon>
|
|
530
550
|
</div>
|
|
531
551
|
</div>
|
|
532
|
-
</
|
|
552
|
+
</div>
|
|
533
553
|
</template>
|
|
534
554
|
|
|
535
555
|
<!-- Flat options -->
|
|
536
|
-
<template v-
|
|
556
|
+
<template v-if="dropdownContent === 'options' && !groupedOptions">
|
|
537
557
|
<div
|
|
538
558
|
v-for="(option, index) in filteredOptions"
|
|
539
559
|
:key="option.value"
|
|
@@ -582,7 +602,7 @@ defineExpose({
|
|
|
582
602
|
<div
|
|
583
603
|
aria-live="polite"
|
|
584
604
|
aria-atomic="true"
|
|
585
|
-
class="
|
|
605
|
+
class="rsui-form-field-combobox__live-region"
|
|
586
606
|
>
|
|
587
607
|
{{ liveAnnouncement }}
|
|
588
608
|
</div>
|
|
@@ -65,7 +65,8 @@ function setValue(value) {
|
|
|
65
65
|
:value="option.value"
|
|
66
66
|
v-model="model"
|
|
67
67
|
:id="`${effectiveId}-option-${index}`"
|
|
68
|
-
:name="$attrs.name"
|
|
68
|
+
:name="$attrs.name || effectiveId"
|
|
69
|
+
:aria-describedby="ariaDescribedby"
|
|
69
70
|
>
|
|
70
71
|
<label
|
|
71
72
|
:for="`${effectiveId}-option-${index}`"
|