@redseed/redseed-ui-vue3 8.35.0 → 8.36.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/Card/Card.vue +35 -1
- package/src/components/Card/MetricCard.vue +10 -0
- package/src/components/Disclosure/Disclosure.vue +11 -0
- package/src/components/FormField/FormFieldSelect.vue +10 -3
- package/src/components/Image/Image.vue +1 -0
- package/src/components/Loader/Loader.vue +9 -1
- package/src/components/MessageBox/MessageBox.vue +34 -7
- package/src/components/MetaInfo/MetaInfo.vue +9 -3
- package/src/components/Section/SectionHeader.vue +20 -1
- package/src/components/Section/SectionSlider.vue +16 -12
- package/src/components/Section/SectionSliderItem.vue +1 -8
- package/src/components/Skeleton/Skeleton.vue +10 -1
- package/src/components/Table/Table.vue +1 -0
- package/src/components/Table/Th.vue +14 -8
- package/src/components/Table/Tr.vue +9 -0
package/package.json
CHANGED
|
@@ -59,6 +59,22 @@ const props = defineProps({
|
|
|
59
59
|
type: String,
|
|
60
60
|
default: undefined,
|
|
61
61
|
},
|
|
62
|
+
// Tri-state toggle state for the action layer. `null` (default) leaves the
|
|
63
|
+
// card a plain button; `true`/`false` turn it into a toggle button that
|
|
64
|
+
// exposes its state via `aria-pressed` (WCAG 4.1.2).
|
|
65
|
+
pressed: {
|
|
66
|
+
type: Boolean,
|
|
67
|
+
default: null,
|
|
68
|
+
},
|
|
69
|
+
// Marks the card as the current item within a set via `aria-current`.
|
|
70
|
+
// `false` (default) omits the attribute; `true` renders `aria-current="true"`;
|
|
71
|
+
// a string passes an aria-current token through (e.g. 'page', 'step').
|
|
72
|
+
current: {
|
|
73
|
+
type: [Boolean, String],
|
|
74
|
+
default: false,
|
|
75
|
+
},
|
|
76
|
+
// PostHog autocapture component label — emitted as data-ph-capture-attribute-
|
|
77
|
+
// component on the action layer so events can be grouped by component type.
|
|
62
78
|
phComponent: {
|
|
63
79
|
type: String,
|
|
64
80
|
default: 'Card',
|
|
@@ -67,6 +83,22 @@ const props = defineProps({
|
|
|
67
83
|
|
|
68
84
|
const emit = defineEmits(['click'])
|
|
69
85
|
|
|
86
|
+
// `aria-pressed` is only emitted when the consumer opts in (pressed !== null),
|
|
87
|
+
// so non-toggle clickable cards are unaffected.
|
|
88
|
+
const ariaPressed = computed(() => {
|
|
89
|
+
if (props.pressed === null || props.pressed === undefined) return undefined
|
|
90
|
+
return props.pressed ? 'true' : 'false'
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const ariaCurrent = computed(() => {
|
|
94
|
+
if (!props.current) return undefined
|
|
95
|
+
return props.current === true ? 'true' : props.current
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Drives a non-colour selection cue (a ring) so the active state is not
|
|
99
|
+
// conveyed by colour alone (WCAG 1.4.1).
|
|
100
|
+
const isActive = computed(() => props.pressed === true || Boolean(props.current))
|
|
101
|
+
|
|
70
102
|
const slots = useSlots()
|
|
71
103
|
|
|
72
104
|
const cardElement = ref(null)
|
|
@@ -149,12 +181,14 @@ function onClick() {
|
|
|
149
181
|
</svg>
|
|
150
182
|
<div
|
|
151
183
|
v-if="clickable"
|
|
152
|
-
class="rsui-card__action-layer"
|
|
184
|
+
:class="['rsui-card__action-layer', { 'rsui-card__action-layer--active': isActive }]"
|
|
153
185
|
role="button"
|
|
154
186
|
:tabindex="disabled ? undefined : 0"
|
|
155
187
|
:title="$attrs.title"
|
|
156
188
|
:aria-label="!$slots['aria-label'] ? ariaLabel : undefined"
|
|
157
189
|
:aria-disabled="disabled ? true : undefined"
|
|
190
|
+
:aria-pressed="ariaPressed"
|
|
191
|
+
:aria-current="ariaCurrent"
|
|
158
192
|
:data-ph-capture-attribute-component="phComponent"
|
|
159
193
|
:data-ph-capture-attribute-label="captureLabel"
|
|
160
194
|
@click="onClick"
|
|
@@ -20,13 +20,23 @@ const props = defineProps({
|
|
|
20
20
|
type: Boolean,
|
|
21
21
|
default: false,
|
|
22
22
|
},
|
|
23
|
+
// Active/selected state for toggle use (e.g. a metric-filter card). Maps to
|
|
24
|
+
// the underlying Card's `pressed` so a clickable card exposes `aria-pressed`
|
|
25
|
+
// (WCAG 4.1.2) and shows a non-colour selection ring (WCAG 1.4.1). `null`
|
|
26
|
+
// (default) leaves the card a plain card; pair with `clickable` to toggle.
|
|
27
|
+
active: {
|
|
28
|
+
type: Boolean,
|
|
29
|
+
default: null,
|
|
30
|
+
},
|
|
23
31
|
})
|
|
24
32
|
</script>
|
|
25
33
|
|
|
26
34
|
<template>
|
|
27
35
|
<Card class="rsui-metric-card"
|
|
36
|
+
:class="{ 'rsui-metric-card--active': active === true }"
|
|
28
37
|
:clickable="props.clickable"
|
|
29
38
|
:hoverable="props.hoverable"
|
|
39
|
+
:pressed="active"
|
|
30
40
|
ph-component="MetricCard"
|
|
31
41
|
>
|
|
32
42
|
<template v-if="$slots['aria-label']" #aria-label>
|
|
@@ -17,6 +17,16 @@ const props = defineProps({
|
|
|
17
17
|
type: Object,
|
|
18
18
|
default: () => {},
|
|
19
19
|
},
|
|
20
|
+
/**
|
|
21
|
+
* Accessible name for the default icon-only toggle button. The chevron has
|
|
22
|
+
* no visible text, so set this (e.g. to the section/disclosure title) to
|
|
23
|
+
* give the trigger a name (WCAG 4.1.2). Ignored when a custom #trigger slot
|
|
24
|
+
* is supplied — name that button yourself.
|
|
25
|
+
*/
|
|
26
|
+
triggerLabel: {
|
|
27
|
+
type: String,
|
|
28
|
+
default: '',
|
|
29
|
+
},
|
|
20
30
|
})
|
|
21
31
|
|
|
22
32
|
const emit = defineEmits(['click'])
|
|
@@ -126,6 +136,7 @@ onMounted(() => {
|
|
|
126
136
|
:xl="$attrs.xl"
|
|
127
137
|
:2xl="$attrs['2xl']"
|
|
128
138
|
:full="$attrs.full"
|
|
139
|
+
:aria-label="triggerLabel || undefined"
|
|
129
140
|
:aria-expanded="isOpen"
|
|
130
141
|
:aria-controls="contentId"
|
|
131
142
|
@click.stop="handleTrigger"
|
|
@@ -27,7 +27,14 @@ const isOpen = ref(false)
|
|
|
27
27
|
const isMobileDevice = ref(false)
|
|
28
28
|
const highlightedIndex = ref(-1)
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// Fall back to a locally-generated unique id when this Select isn't wrapped by
|
|
31
|
+
// a FormFieldSlot (no injected inputId) and no id attr is supplied. Without it,
|
|
32
|
+
// effectiveId is undefined and the listbox/option ids — and the trigger's
|
|
33
|
+
// aria-controls — interpolate the literal string "undefined" (e.g.
|
|
34
|
+
// "undefined-listbox"), a dangling reference and a duplicate id across
|
|
35
|
+
// instances. Mirrors FormFieldSlot's _.uniqueId('form-field-') mechanism.
|
|
36
|
+
const autoId = _.uniqueId('form-field-')
|
|
37
|
+
const effectiveId = computed(() => inputId.value || attrs.id || autoId)
|
|
31
38
|
|
|
32
39
|
function toggleOptions() {
|
|
33
40
|
isOpen.value = !isOpen.value
|
|
@@ -228,7 +235,7 @@ defineExpose({
|
|
|
228
235
|
<template>
|
|
229
236
|
<FormFieldSlot
|
|
230
237
|
ref="formFieldSelectElement"
|
|
231
|
-
:id="
|
|
238
|
+
:id="effectiveId"
|
|
232
239
|
:class="[
|
|
233
240
|
$attrs.class,
|
|
234
241
|
'rsui-form-field-select',
|
|
@@ -254,7 +261,7 @@ defineExpose({
|
|
|
254
261
|
]"
|
|
255
262
|
role="combobox"
|
|
256
263
|
:aria-activedescendant="highlightedIndex >= 0 ? `${effectiveId}-option-${highlightedIndex}` : undefined"
|
|
257
|
-
:aria-controls="`${effectiveId}-listbox`"
|
|
264
|
+
:aria-controls="isOpen ? `${effectiveId}-listbox` : undefined"
|
|
258
265
|
:aria-describedby="ariaDescribedby"
|
|
259
266
|
:aria-expanded="isOpen"
|
|
260
267
|
:aria-haspopup="'listbox'"
|
|
@@ -16,6 +16,10 @@ const props = defineProps({
|
|
|
16
16
|
type: Boolean,
|
|
17
17
|
default: false,
|
|
18
18
|
},
|
|
19
|
+
label: {
|
|
20
|
+
type: String,
|
|
21
|
+
default: 'Loading',
|
|
22
|
+
},
|
|
19
23
|
})
|
|
20
24
|
|
|
21
25
|
const defaultColor = computed(() => !props.primary && !props.secondary && !props.white)
|
|
@@ -36,5 +40,9 @@ useLottie(loaderElement, LottieCubes)
|
|
|
36
40
|
<template>
|
|
37
41
|
<div ref="loaderElement"
|
|
38
42
|
:class="loaderClass"
|
|
39
|
-
|
|
43
|
+
role="status"
|
|
44
|
+
aria-busy="true"
|
|
45
|
+
>
|
|
46
|
+
<span v-if="label" class="rsui-loader__label">{{ label }}</span>
|
|
47
|
+
</div>
|
|
40
48
|
</template>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { ref } from 'vue'
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
3
|
import Icon from '../Icon/Icon.vue'
|
|
4
4
|
import { XMarkIcon } from '@heroicons/vue/24/outline'
|
|
5
5
|
|
|
@@ -12,6 +12,10 @@ const props = defineProps({
|
|
|
12
12
|
type: Boolean,
|
|
13
13
|
default: false,
|
|
14
14
|
},
|
|
15
|
+
closeLabel: {
|
|
16
|
+
type: String,
|
|
17
|
+
default: 'Close',
|
|
18
|
+
},
|
|
15
19
|
variant: {
|
|
16
20
|
type: String,
|
|
17
21
|
default: 'default',
|
|
@@ -23,6 +27,20 @@ const emit = defineEmits(['close'])
|
|
|
23
27
|
|
|
24
28
|
const isClosed = ref(props.closed)
|
|
25
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Announce the box as a live region so flash / MOTD content reaches assistive
|
|
32
|
+
* tech (WCAG 4.1.3 Status Messages). `alert` is assertive — reserved for errors
|
|
33
|
+
* that must interrupt; advisory variants use polite `status`. The neutral
|
|
34
|
+
* `default` variant is a plain container, not a status message, so it gets no
|
|
35
|
+
* live role (avoids announcing arbitrary static content). Both roles carry an
|
|
36
|
+
* implicit aria-live, so no separate attribute is needed.
|
|
37
|
+
*/
|
|
38
|
+
const liveRole = computed(() => {
|
|
39
|
+
if (props.variant === 'error') return 'alert'
|
|
40
|
+
if (['info', 'success', 'warning'].includes(props.variant)) return 'status'
|
|
41
|
+
return undefined
|
|
42
|
+
})
|
|
43
|
+
|
|
26
44
|
function close() {
|
|
27
45
|
isClosed.value = true
|
|
28
46
|
emit('close')
|
|
@@ -34,17 +52,22 @@ function close() {
|
|
|
34
52
|
'rsui-message-box',
|
|
35
53
|
`rsui-message-box--${variant}`
|
|
36
54
|
]"
|
|
55
|
+
:role="liveRole"
|
|
37
56
|
>
|
|
38
57
|
<div v-if="$slots.title" class="rsui-message-box__head">
|
|
39
58
|
<div class="rsui-message-box__title">
|
|
40
59
|
<slot name="title"></slot>
|
|
41
60
|
</div>
|
|
42
61
|
<div v-if="closeable" class="rsui-message-box__close">
|
|
43
|
-
<
|
|
62
|
+
<button type="button"
|
|
63
|
+
class="rsui-message-box__close-icon"
|
|
64
|
+
:aria-label="closeLabel"
|
|
65
|
+
@click="close"
|
|
66
|
+
>
|
|
44
67
|
<Icon disabled>
|
|
45
|
-
<XMarkIcon
|
|
68
|
+
<XMarkIcon aria-hidden="true"></XMarkIcon>
|
|
46
69
|
</Icon>
|
|
47
|
-
</
|
|
70
|
+
</button>
|
|
48
71
|
</div>
|
|
49
72
|
</div>
|
|
50
73
|
<div class="rsui-message-box__content">
|
|
@@ -52,11 +75,15 @@ function close() {
|
|
|
52
75
|
<slot></slot>
|
|
53
76
|
</div>
|
|
54
77
|
<div v-if="closeable && !$slots.title" class="rsui-message-box__close">
|
|
55
|
-
<
|
|
78
|
+
<button type="button"
|
|
79
|
+
class="rsui-message-box__close-icon"
|
|
80
|
+
:aria-label="closeLabel"
|
|
81
|
+
@click="close"
|
|
82
|
+
>
|
|
56
83
|
<Icon disabled>
|
|
57
|
-
<XMarkIcon
|
|
84
|
+
<XMarkIcon aria-hidden="true"></XMarkIcon>
|
|
58
85
|
</Icon>
|
|
59
|
-
</
|
|
86
|
+
</button>
|
|
60
87
|
</div>
|
|
61
88
|
</div>
|
|
62
89
|
</div>
|
|
@@ -8,6 +8,10 @@ const props = defineProps({
|
|
|
8
8
|
type: Boolean,
|
|
9
9
|
default: false
|
|
10
10
|
},
|
|
11
|
+
helpLabel: {
|
|
12
|
+
type: String,
|
|
13
|
+
default: 'More information'
|
|
14
|
+
},
|
|
11
15
|
inline: {
|
|
12
16
|
type: Boolean,
|
|
13
17
|
default: false
|
|
@@ -42,16 +46,18 @@ const emit = defineEmits(['click'])
|
|
|
42
46
|
<div class="rsui-meta-info__label">
|
|
43
47
|
<slot name="label"></slot>
|
|
44
48
|
</div>
|
|
45
|
-
<
|
|
49
|
+
<button v-if="help"
|
|
50
|
+
type="button"
|
|
46
51
|
class="rsui-meta-info__help"
|
|
52
|
+
:aria-label="helpLabel"
|
|
47
53
|
@click="$emit('click')"
|
|
48
54
|
>
|
|
49
55
|
<slot name="help-icon">
|
|
50
56
|
<Icon sm secondary>
|
|
51
|
-
<QuestionMarkCircleIcon />
|
|
57
|
+
<QuestionMarkCircleIcon aria-hidden="true" />
|
|
52
58
|
</Icon>
|
|
53
59
|
</slot>
|
|
54
|
-
</
|
|
60
|
+
</button>
|
|
55
61
|
</div>
|
|
56
62
|
<div class="rsui-meta-info__value">
|
|
57
63
|
<slot></slot>
|
|
@@ -26,6 +26,17 @@ const props = defineProps({
|
|
|
26
26
|
type: Boolean,
|
|
27
27
|
default: true,
|
|
28
28
|
},
|
|
29
|
+
/**
|
|
30
|
+
* Heading level (1-6) for the title. When set, the title renders as a real
|
|
31
|
+
* <h1>-<h6> so consuming pages get a correct document outline (WCAG 1.3.1,
|
|
32
|
+
* 2.4.6). Left unset it stays a non-heading <span>, keeping existing
|
|
33
|
+
* consumers unaffected — pick the level that fits the page's heading order.
|
|
34
|
+
*/
|
|
35
|
+
headingLevel: {
|
|
36
|
+
type: [Number, String],
|
|
37
|
+
default: null,
|
|
38
|
+
validator: (value) => value === null || ['1', '2', '3', '4', '5', '6'].includes(String(value)),
|
|
39
|
+
},
|
|
29
40
|
})
|
|
30
41
|
|
|
31
42
|
const sectionHeaderElement = ref(null)
|
|
@@ -50,6 +61,12 @@ const showToolbar = computed(() => {
|
|
|
50
61
|
return props.showActions
|
|
51
62
|
&& (slots.actions || showMoreActions)
|
|
52
63
|
})
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Element used to render the title — a real heading when a level is supplied,
|
|
67
|
+
* otherwise a non-heading <span> (preserves the previous, level-less markup).
|
|
68
|
+
*/
|
|
69
|
+
const titleTag = computed(() => props.headingLevel ? `h${props.headingLevel}` : 'span')
|
|
53
70
|
</script>
|
|
54
71
|
<template>
|
|
55
72
|
<div ref="sectionHeaderElement"
|
|
@@ -77,7 +94,9 @@ const showToolbar = computed(() => {
|
|
|
77
94
|
|
|
78
95
|
<!-- Title slot, default slot -->
|
|
79
96
|
<div class="rsui-section-header__title">
|
|
80
|
-
<
|
|
97
|
+
<component :is="titleTag" class="rsui-section-header__title-text">
|
|
98
|
+
<slot></slot>
|
|
99
|
+
</component>
|
|
81
100
|
<div v-if="$slots.badge" class="rsui-section-header__badge">
|
|
82
101
|
<slot name="badge"></slot>
|
|
83
102
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref, computed, useSlots } from 'vue'
|
|
3
|
-
import { useScroll
|
|
3
|
+
import { useScroll } from '@vueuse/core'
|
|
4
4
|
import { Icon } from '../Icon'
|
|
5
5
|
import Section from './Section.vue'
|
|
6
6
|
import SectionHeader from './SectionHeader.vue'
|
|
@@ -20,6 +20,16 @@ const props = defineProps({
|
|
|
20
20
|
type: Boolean,
|
|
21
21
|
default: false,
|
|
22
22
|
},
|
|
23
|
+
/**
|
|
24
|
+
* Heading level (1-6) for the slider title. When set, the title renders
|
|
25
|
+
* as a real <h1>-<h6> for a correct document outline; left unset it stays
|
|
26
|
+
* a non-heading <span> so existing consumers are unaffected.
|
|
27
|
+
*/
|
|
28
|
+
headingLevel: {
|
|
29
|
+
type: [Number, String],
|
|
30
|
+
default: null,
|
|
31
|
+
validator: (value) => value === null || ['1', '2', '3', '4', '5', '6'].includes(String(value)),
|
|
32
|
+
},
|
|
23
33
|
})
|
|
24
34
|
|
|
25
35
|
const sectionVariant = computed(() => {
|
|
@@ -56,14 +66,6 @@ function generateItemId(index) {
|
|
|
56
66
|
*/
|
|
57
67
|
const sliderContainerRef = ref(null)
|
|
58
68
|
|
|
59
|
-
/**
|
|
60
|
-
* Listener for keyboard events on the document
|
|
61
|
-
*/
|
|
62
|
-
useEventListener(document, 'keydown', (e) => {
|
|
63
|
-
if (e.key === 'ArrowRight') e.preventDefault()
|
|
64
|
-
if (e.key === 'ArrowLeft') e.preventDefault()
|
|
65
|
-
})
|
|
66
|
-
|
|
67
69
|
/**
|
|
68
70
|
* Whether the slider container is scrolling
|
|
69
71
|
*/
|
|
@@ -170,7 +172,7 @@ function showPreviousSlide() {
|
|
|
170
172
|
v-bind="$attrs"
|
|
171
173
|
>
|
|
172
174
|
<template #header>
|
|
173
|
-
<SectionHeader :showDivider="false" :showMoreActions="false">
|
|
175
|
+
<SectionHeader :showDivider="false" :showMoreActions="false" :headingLevel="headingLevel">
|
|
174
176
|
<template v-if="slots.icon" #icon>
|
|
175
177
|
<slot name="icon"></slot>
|
|
176
178
|
</template>
|
|
@@ -193,8 +195,9 @@ function showPreviousSlide() {
|
|
|
193
195
|
:disabledNextButton="disabledNextButton"
|
|
194
196
|
>
|
|
195
197
|
<button class="rsui-section-slider__action"
|
|
196
|
-
|
|
198
|
+
type="button"
|
|
197
199
|
aria-label="Previous"
|
|
200
|
+
:disabled="disabledPrevButton"
|
|
198
201
|
data-ph-capture-attribute-component="SectionSliderAction"
|
|
199
202
|
data-ph-capture-attribute-label="Previous"
|
|
200
203
|
@click="showPreviousSlide"
|
|
@@ -205,8 +208,9 @@ function showPreviousSlide() {
|
|
|
205
208
|
</button>
|
|
206
209
|
|
|
207
210
|
<button class="rsui-section-slider__action"
|
|
208
|
-
|
|
211
|
+
type="button"
|
|
209
212
|
aria-label="Next"
|
|
213
|
+
:disabled="disabledNextButton"
|
|
210
214
|
data-ph-capture-attribute-component="SectionSliderAction"
|
|
211
215
|
data-ph-capture-attribute-label="Next"
|
|
212
216
|
@click="showNextSlide"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref, watch, onBeforeUnmount } from 'vue'
|
|
3
|
-
import {
|
|
3
|
+
import { useElementVisibility } from '@vueuse/core'
|
|
4
4
|
|
|
5
5
|
const props = defineProps({
|
|
6
6
|
id: {
|
|
@@ -31,13 +31,6 @@ watch(isVisible, (newVal) => {
|
|
|
31
31
|
onBeforeUnmount(() => {
|
|
32
32
|
emit('hidden', props.id)
|
|
33
33
|
})
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Listener for keyboard events on the item
|
|
37
|
-
*/
|
|
38
|
-
useEventListener(itemRef, 'keydown', (e) => {
|
|
39
|
-
if (e.key === 'Tab') e.preventDefault()
|
|
40
|
-
})
|
|
41
34
|
</script>
|
|
42
35
|
|
|
43
36
|
<template>
|
|
@@ -66,6 +66,10 @@ const props = defineProps({
|
|
|
66
66
|
type: Boolean,
|
|
67
67
|
default: false,
|
|
68
68
|
},
|
|
69
|
+
label: {
|
|
70
|
+
type: String,
|
|
71
|
+
default: '',
|
|
72
|
+
},
|
|
69
73
|
})
|
|
70
74
|
|
|
71
75
|
const shape = computed(() => {
|
|
@@ -99,7 +103,12 @@ const positionClass = computed(() => {
|
|
|
99
103
|
</script>
|
|
100
104
|
|
|
101
105
|
<template>
|
|
102
|
-
<div
|
|
106
|
+
<div
|
|
107
|
+
:class="['rsui-skeleton', widthClass, positionClass]"
|
|
108
|
+
:role="label ? 'status' : undefined"
|
|
109
|
+
:aria-busy="label ? 'true' : undefined"
|
|
110
|
+
>
|
|
111
|
+
<span v-if="label" class="rsui-skeleton__label">{{ label }}</span>
|
|
103
112
|
<div
|
|
104
113
|
:class="[
|
|
105
114
|
shape === 'line' && 'rsui-skeleton__line',
|
|
@@ -88,16 +88,18 @@ function handleSort(event) {
|
|
|
88
88
|
]"
|
|
89
89
|
:scope="scope"
|
|
90
90
|
:aria-sort="ariaSort"
|
|
91
|
-
:tabindex="sortable ? 0 : undefined"
|
|
92
|
-
@click="handleSort($event)"
|
|
93
|
-
@keydown.enter.self.prevent="handleSort($event)"
|
|
94
|
-
@keydown.space.self.prevent="handleSort($event)"
|
|
95
91
|
>
|
|
96
|
-
<
|
|
92
|
+
<!-- Sortable headers wrap their content in a real <button> so AT announces
|
|
93
|
+
them as activatable (4.1.2) and Enter/Space work natively. The <th>
|
|
94
|
+
keeps its columnheader role + aria-sort (valid only on that role). -->
|
|
95
|
+
<button v-if="sortable"
|
|
96
|
+
type="button"
|
|
97
|
+
class="rsui-th__button"
|
|
98
|
+
@click="handleSort($event)"
|
|
99
|
+
>
|
|
97
100
|
<slot></slot>
|
|
98
101
|
|
|
99
|
-
<
|
|
100
|
-
class="rsui-th__sort"
|
|
102
|
+
<span class="rsui-th__sort"
|
|
101
103
|
aria-hidden="true"
|
|
102
104
|
>
|
|
103
105
|
<Icon v-if="!isAsc && !isDesc"
|
|
@@ -118,7 +120,11 @@ function handleSort(event) {
|
|
|
118
120
|
>
|
|
119
121
|
<ArrowUpIcon />
|
|
120
122
|
</Icon>
|
|
121
|
-
</
|
|
123
|
+
</span>
|
|
124
|
+
</button>
|
|
125
|
+
|
|
126
|
+
<div v-else class="rsui-th__content">
|
|
127
|
+
<slot></slot>
|
|
122
128
|
</div>
|
|
123
129
|
</th>
|
|
124
130
|
</template>
|
|
@@ -4,6 +4,14 @@ const props = defineProps({
|
|
|
4
4
|
type: Boolean,
|
|
5
5
|
default: false,
|
|
6
6
|
},
|
|
7
|
+
// Accessible name for the clickable row. The row keeps its native table-row
|
|
8
|
+
// role; without a label, assistive tech falls back to the row's cell text,
|
|
9
|
+
// so set this to a concise action label (e.g. "View John Doe") for clearer
|
|
10
|
+
// AT output.
|
|
11
|
+
ariaLabel: {
|
|
12
|
+
type: String,
|
|
13
|
+
default: undefined,
|
|
14
|
+
},
|
|
7
15
|
})
|
|
8
16
|
|
|
9
17
|
const emit = defineEmits(['click'])
|
|
@@ -23,6 +31,7 @@ function handleClick(event) {
|
|
|
23
31
|
{ 'rsui-tr--clickable': clickable },
|
|
24
32
|
]"
|
|
25
33
|
:tabindex="clickable ? 0 : undefined"
|
|
34
|
+
:aria-label="clickable ? ariaLabel : undefined"
|
|
26
35
|
@click="handleClick($event)"
|
|
27
36
|
@keydown.enter.self.prevent="handleClick($event)"
|
|
28
37
|
@keydown.space.self.prevent="handleClick($event)"
|