@redseed/redseed-ui-vue3 8.33.3 → 8.35.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
  {
2
2
  "name": "@redseed/redseed-ui-vue3",
3
- "version": "8.33.3",
3
+ "version": "8.35.0",
4
4
  "description": "RedSeed UI Vue 3 components",
5
5
  "main": "index.js",
6
6
  "repository": "https://github.com/redseedtraining/redseed-ui",
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { computed, ref } from 'vue'
2
+ import { computed, ref, useSlots } from 'vue'
3
3
  import { useResponsiveWidth } from '../../helpers'
4
4
 
5
5
  const props = defineProps({
@@ -59,10 +59,16 @@ const props = defineProps({
59
59
  type: String,
60
60
  default: undefined,
61
61
  },
62
+ phComponent: {
63
+ type: String,
64
+ default: 'Card',
65
+ },
62
66
  })
63
67
 
64
68
  const emit = defineEmits(['click'])
65
69
 
70
+ const slots = useSlots()
71
+
66
72
  const cardElement = ref(null)
67
73
  const { responsiveWidth } = useResponsiveWidth(cardElement)
68
74
 
@@ -91,6 +97,28 @@ const cardClass = computed(() => [
91
97
  }
92
98
  ])
93
99
 
100
+ // PostHog autocapture reads the accessible name off the action layer. The
101
+ // ariaLabel prop covers the prop path, but cards commonly supply their label
102
+ // via the #aria-label slot (see ButtonCard / MetricCard), where the prop is
103
+ // undefined. Mirror the slot's text so slot-labelled cards still emit a named
104
+ // capture event instead of an unlabeled one.
105
+ function slotText(nodes) {
106
+ if (!Array.isArray(nodes)) return ''
107
+ return nodes
108
+ .map((node) => {
109
+ if (typeof node.children === 'string') return node.children
110
+ if (Array.isArray(node.children)) return slotText(node.children)
111
+ return ''
112
+ })
113
+ .join('')
114
+ }
115
+
116
+ const captureLabel = computed(() => {
117
+ if (props.ariaLabel) return props.ariaLabel
118
+ if (slots['aria-label']) return slotText(slots['aria-label']()).trim() || undefined
119
+ return undefined
120
+ })
121
+
94
122
  function onClick() {
95
123
  if (props.disabled) return
96
124
 
@@ -127,6 +155,8 @@ function onClick() {
127
155
  :title="$attrs.title"
128
156
  :aria-label="!$slots['aria-label'] ? ariaLabel : undefined"
129
157
  :aria-disabled="disabled ? true : undefined"
158
+ :data-ph-capture-attribute-component="phComponent"
159
+ :data-ph-capture-attribute-label="captureLabel"
130
160
  @click="onClick"
131
161
  @keydown.enter.prevent="!$event.repeat && onClick()"
132
162
  @keydown.space.prevent="!$event.repeat && onClick()"
@@ -27,6 +27,7 @@ const props = defineProps({
27
27
  <Card class="rsui-metric-card"
28
28
  :clickable="props.clickable"
29
29
  :hoverable="props.hoverable"
30
+ ph-component="MetricCard"
30
31
  >
31
32
  <template v-if="$slots['aria-label']" #aria-label>
32
33
  <slot name="aria-label"></slot>
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { ref, watch } from 'vue'
2
+ import { ref, watch, computed } from 'vue'
3
3
  import FormFieldSlot from './FormFieldSlot.vue'
4
4
  import { CheckIcon } from '@heroicons/vue/24/outline'
5
5
  import { useFormFieldA11y } from '../../composables/useFormFieldA11y.js'
@@ -8,6 +8,22 @@ defineOptions({
8
8
  inheritAttrs: false,
9
9
  })
10
10
 
11
+ const props = defineProps({
12
+ // Smaller variants for dense surfaces. Default (no flag) is the 24px size;
13
+ // only the smallest enabled flag applies (xs wins over sm).
14
+ sm: {
15
+ type: Boolean,
16
+ default: false,
17
+ },
18
+ xs: {
19
+ type: Boolean,
20
+ default: false,
21
+ },
22
+ })
23
+
24
+ // Resolve to a single size modifier so only one applies at a time.
25
+ const size = computed(() => props.xs ? 'xs' : props.sm ? 'sm' : null)
26
+
11
27
  const model = defineModel()
12
28
 
13
29
  const checked = ref(false)
@@ -35,7 +51,7 @@ function check(event) {
35
51
  >
36
52
  <template #label>
37
53
  <div class="rsui-form-field-checkbox__checkbox">
38
- <div class="rsui-form-field-checkbox__check">
54
+ <div :class="['rsui-form-field-checkbox__check', size && `rsui-form-field-checkbox__check--${size}`]">
39
55
  <CheckIcon v-if="checked" aria-hidden="true"></CheckIcon>
40
56
  <input
41
57
  :checked="checked"
@@ -52,7 +68,7 @@ function check(event) {
52
68
  >
53
69
  </div>
54
70
  <div v-if="$slots.label"
55
- class="rsui-form-field-checkbox__label"
71
+ :class="['rsui-form-field-checkbox__label', size && `rsui-form-field-checkbox__label--${size}`]"
56
72
  >
57
73
  <slot name="label"></slot>
58
74
  <span
@@ -194,6 +194,9 @@ function showPreviousSlide() {
194
194
  >
195
195
  <button class="rsui-section-slider__action"
196
196
  :disabled="disabledPrevButton"
197
+ aria-label="Previous"
198
+ data-ph-capture-attribute-component="SectionSliderAction"
199
+ data-ph-capture-attribute-label="Previous"
197
200
  @click="showPreviousSlide"
198
201
  >
199
202
  <Icon :invert="featured">
@@ -203,6 +206,9 @@ function showPreviousSlide() {
203
206
 
204
207
  <button class="rsui-section-slider__action"
205
208
  :disabled="disabledNextButton"
209
+ aria-label="Next"
210
+ data-ph-capture-attribute-component="SectionSliderAction"
211
+ data-ph-capture-attribute-label="Next"
206
212
  @click="showNextSlide"
207
213
  >
208
214
  <Icon :invert="featured">
@@ -1,9 +1,11 @@
1
1
  <script setup>
2
2
  import { computed } from 'vue'
3
3
  import DropdownMenu from '../DropdownMenu/DropdownMenu.vue'
4
+ import ButtonSecondary from '../Button/ButtonSecondary.vue'
4
5
  import ButtonTertiary from '../Button/ButtonTertiary.vue'
5
6
  import Icon from '../Icon/Icon.vue'
6
- import { AdjustmentsHorizontalIcon, CheckIcon } from '@heroicons/vue/24/outline'
7
+ import FormFieldCheckbox from '../FormField/FormFieldCheckbox.vue'
8
+ import { AdjustmentsHorizontalIcon } from '@heroicons/vue/24/outline'
7
9
 
8
10
  const props = defineProps({
9
11
  columns: {
@@ -19,9 +21,22 @@ const props = defineProps({
19
21
 
20
22
  const visibleKeys = defineModel('visibleKeys', { type: Array, required: true })
21
23
 
22
- const isVisible = (key) => visibleKeys.value.includes(key)
24
+ // Columns shown in the picker — `pickerHidden` columns are omitted entirely.
25
+ const pickerColumns = computed(() => props.columns.filter(c => !c.pickerHidden))
26
+
27
+ // Required columns are always rendered by the table (Table.vue), so the picker
28
+ // treats them as visible regardless of `visibleKeys` — keeps the locked-on
29
+ // checkbox checked and the count consistent even when a controlled
30
+ // `visibleKeys` omits the required key.
31
+ const isVisible = (key) => {
32
+ const column = props.columns.find(c => c.key === key)
33
+ if (column?.required) return true
34
+ return visibleKeys.value.includes(key)
35
+ }
23
36
 
24
37
  function toggle(key) {
38
+ const column = props.columns.find(c => c.key === key)
39
+ if (column?.required) return
25
40
  if (visibleKeys.value.includes(key)) {
26
41
  visibleKeys.value = visibleKeys.value.filter(k => k !== key)
27
42
  } else {
@@ -29,37 +44,53 @@ function toggle(key) {
29
44
  }
30
45
  }
31
46
 
32
- const visibleCount = computed(() => visibleKeys.value.length)
33
- const totalCount = computed(() => props.columns.length)
47
+ const visibleCount = computed(() => pickerColumns.value.filter(c => isVisible(c.key)).length)
48
+ const totalCount = computed(() => pickerColumns.value.length)
34
49
  </script>
35
50
 
36
51
  <template>
37
52
  <DropdownMenu right class="rsui-column-picker">
38
53
  <template #trigger="{ open, isOpen }">
39
- <ButtonTertiary :aria-expanded="isOpen" @click="open">
54
+ <ButtonTertiary :aria-expanded="isOpen"
55
+ class="rsui-column-picker__trigger--desktop"
56
+ @click="open"
57
+ >
40
58
  <Icon sm><AdjustmentsHorizontalIcon /></Icon>
41
59
  {{ label }}
42
60
  <span class="rsui-column-picker__count">{{ visibleCount }}/{{ totalCount }}</span>
43
61
  </ButtonTertiary>
62
+ <ButtonSecondary :aria-expanded="isOpen"
63
+ class="rsui-column-picker__trigger--mobile"
64
+ @click="open"
65
+ >
66
+ <Icon sm><AdjustmentsHorizontalIcon /></Icon>
67
+ {{ label }}
68
+ <span class="rsui-column-picker__count">{{ visibleCount }}/{{ totalCount }}</span>
69
+ </ButtonSecondary>
44
70
  </template>
45
71
 
46
72
  <ul class="rsui-column-picker__list" role="none">
47
- <li v-for="column in columns"
73
+ <li v-for="column in pickerColumns"
48
74
  :key="column.key"
49
- class="rsui-column-picker__item"
75
+ :class="['rsui-column-picker__item', { 'rsui-column-picker__item--disabled': column.required }]"
50
76
  role="menuitemcheckbox"
51
77
  :aria-checked="isVisible(column.key)"
52
- tabindex="0"
78
+ :aria-disabled="column.required ? 'true' : undefined"
79
+ :aria-label="column.name || column.key"
80
+ :tabindex="column.required ? -1 : 0"
53
81
  @click.stop="toggle(column.key)"
54
82
  @keydown.enter.self.prevent="toggle(column.key)"
55
83
  @keydown.space.self.prevent="toggle(column.key)"
56
84
  >
57
- <span :class="['rsui-column-picker__check', { 'rsui-column-picker__check--on': isVisible(column.key) }]"
58
- aria-hidden="true"
59
- >
60
- <CheckIcon v-if="isVisible(column.key)" />
85
+ <!-- The row owns the click, keyboard and aria state; this rsui
86
+ checkbox is purely presentational, controlled by the row's
87
+ visible state. `inert` keeps its input out of the tab order
88
+ and a11y tree (the row carries the label via aria-label). -->
89
+ <span class="rsui-column-picker__checkbox" inert>
90
+ <FormFieldCheckbox sm :disabled="column.required" :model-value="isVisible(column.key)">
91
+ <template #label>{{ column.name || column.key }}</template>
92
+ </FormFieldCheckbox>
61
93
  </span>
62
- <span class="rsui-column-picker__label">{{ column.name || column.key }}</span>
63
94
  </li>
64
95
  </ul>
65
96
  </DropdownMenu>
@@ -81,7 +81,11 @@ const effectiveVisibleKeys = computed({
81
81
  })
82
82
 
83
83
  const visibleColumns = computed(() => {
84
- return props.columns.filter(column => effectiveVisibleKeys.value.includes(column.key))
84
+ return props.columns.filter(column => {
85
+ // `required` and `pickerHidden` columns are always rendered, regardless of visibleKeys.
86
+ if (column.required || column.pickerHidden) return true
87
+ return effectiveVisibleKeys.value.includes(column.key)
88
+ })
85
89
  })
86
90
  </script>
87
91