@redseed/redseed-ui-vue3 8.31.1 → 8.32.1

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/index.js CHANGED
@@ -14,6 +14,7 @@ export * from './src/components/Card'
14
14
  export * from './src/components/CardGroup'
15
15
  export * from './src/components/Comment'
16
16
  export * from './src/components/Disclosure'
17
+ export * from './src/components/Drawer'
17
18
  export * from './src/components/DropdownMenu'
18
19
  export * from './src/components/Empty'
19
20
  export * from './src/components/FlexContainer'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redseed/redseed-ui-vue3",
3
- "version": "8.31.1",
3
+ "version": "8.32.1",
4
4
  "description": "RedSeed UI Vue 3 components",
5
5
  "main": "index.js",
6
6
  "repository": "https://github.com/redseedtraining/redseed-ui",
@@ -0,0 +1,208 @@
1
+ <script setup>
2
+ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
3
+ import Icon from '../Icon/Icon.vue'
4
+ import { XMarkIcon } from '@heroicons/vue/24/outline'
5
+
6
+ defineOptions({
7
+ inheritAttrs: false,
8
+ })
9
+
10
+ const drawerHeaderId = _.uniqueId('drawer-header-')
11
+ const drawerPanelRef = ref(null)
12
+ let previousActiveElement = null
13
+
14
+ const props = defineProps({
15
+ show: {
16
+ type: Boolean,
17
+ default: false,
18
+ },
19
+ closeable: {
20
+ type: Boolean,
21
+ default: true,
22
+ },
23
+ sm: {
24
+ type: Boolean,
25
+ default: false,
26
+ },
27
+ md: {
28
+ type: Boolean,
29
+ default: false,
30
+ },
31
+ lg: {
32
+ type: Boolean,
33
+ default: false,
34
+ },
35
+ left: {
36
+ type: Boolean,
37
+ default: false,
38
+ },
39
+ headerPadded: {
40
+ type: Boolean,
41
+ default: true,
42
+ },
43
+ bodyPadded: {
44
+ type: Boolean,
45
+ default: true,
46
+ },
47
+ footerPadded: {
48
+ type: Boolean,
49
+ default: true,
50
+ },
51
+ closeIcon: {
52
+ type: Boolean,
53
+ default: false,
54
+ },
55
+ })
56
+
57
+ const defaultWidth = computed(() => !props.sm && !props.md && !props.lg)
58
+
59
+ const drawerPanelClass = computed(() => [
60
+ 'rsui-drawer__panel',
61
+ {
62
+ 'rsui-drawer__panel--sm': props.sm,
63
+ 'rsui-drawer__panel--md': props.md || defaultWidth.value,
64
+ 'rsui-drawer__panel--lg': props.lg,
65
+ 'rsui-drawer__panel--left': props.left,
66
+ },
67
+ ])
68
+
69
+ const offScreenClass = computed(() => props.left ? 'rsui-drawer__panel--off-screen-left' : 'rsui-drawer__panel--off-screen-right')
70
+
71
+ const emit = defineEmits(['close'])
72
+
73
+ const isMounted = ref(props.show)
74
+ const isOverlayVisible = ref(props.show)
75
+ const isPanelVisible = ref(props.show)
76
+
77
+ watch(() => props.show, (value) => {
78
+ if (value) {
79
+ previousActiveElement = document.activeElement
80
+ isMounted.value = true
81
+ isOverlayVisible.value = true
82
+ isPanelVisible.value = true
83
+ document.body.style.overflow = 'hidden'
84
+
85
+ nextTick(() => {
86
+ const panel = drawerPanelRef.value
87
+ if (!panel) return
88
+ const focusable = panel.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
89
+ if (focusable) {
90
+ focusable.focus()
91
+ } else {
92
+ panel.focus()
93
+ }
94
+ })
95
+ } else {
96
+ isPanelVisible.value = false
97
+ document.body.style.overflow = null
98
+
99
+ if (previousActiveElement && typeof previousActiveElement.focus === 'function') {
100
+ nextTick(() => previousActiveElement.focus())
101
+ }
102
+ }
103
+ })
104
+
105
+ function close() {
106
+ if (props.closeable) {
107
+ emit('close')
108
+ }
109
+ }
110
+
111
+ function closeOnEscape(e) {
112
+ if (e.key === 'Escape' && props.show && !e.repeat) {
113
+ close()
114
+ }
115
+ }
116
+
117
+ onMounted(() => document.addEventListener('keydown', closeOnEscape))
118
+
119
+ onUnmounted(() => {
120
+ document.removeEventListener('keydown', closeOnEscape)
121
+ document.body.style.overflow = null
122
+ })
123
+
124
+ function handlePanelAfterLeave() {
125
+ if (!props.show) {
126
+ isOverlayVisible.value = false
127
+ isMounted.value = false
128
+ }
129
+ }
130
+ </script>
131
+ <template>
132
+ <teleport to="body">
133
+ <div v-if="isMounted" class="rsui-drawer" v-bind="$attrs">
134
+ <div v-if="isOverlayVisible"
135
+ class="rsui-drawer__background-wrapper"
136
+ aria-hidden="true"
137
+ @click="close"
138
+ >
139
+ <div class="rsui-drawer__background" />
140
+ </div>
141
+
142
+ <Transition
143
+ appear
144
+ enter-active-class="rsui-drawer__panel--entering"
145
+ :enter-from-class="offScreenClass"
146
+ leave-active-class="rsui-drawer__panel--leaving"
147
+ :leave-to-class="offScreenClass"
148
+ @after-leave="handlePanelAfterLeave"
149
+ >
150
+ <div v-if="isPanelVisible"
151
+ ref="drawerPanelRef"
152
+ :class="drawerPanelClass"
153
+ role="dialog"
154
+ aria-modal="true"
155
+ :aria-labelledby="$slots.header ? drawerHeaderId : undefined"
156
+ tabindex="-1"
157
+ >
158
+ <button v-if="closeIcon"
159
+ type="button"
160
+ class="rsui-drawer__close-icon"
161
+ aria-label="Close"
162
+ @click="close"
163
+ >
164
+ <Icon disabled>
165
+ <XMarkIcon />
166
+ </Icon>
167
+ </button>
168
+
169
+ <div v-if="$slots.header"
170
+ :id="drawerHeaderId"
171
+ :class="{
172
+ 'rsui-drawer__header': true,
173
+ 'rsui-drawer__header--padded': headerPadded,
174
+ }"
175
+ >
176
+ <slot name="header"
177
+ :close="close"
178
+ ></slot>
179
+ </div>
180
+
181
+ <div
182
+ :class="{
183
+ 'rsui-drawer__body': true,
184
+ 'rsui-drawer__body--padded': bodyPadded,
185
+ }"
186
+ >
187
+ <slot>
188
+ <slot name="body"
189
+ :close="close"
190
+ ></slot>
191
+ </slot>
192
+ </div>
193
+
194
+ <div v-if="$slots.footer"
195
+ :class="{
196
+ 'rsui-drawer__footer': true,
197
+ 'rsui-drawer__footer--padded': footerPadded,
198
+ }"
199
+ >
200
+ <slot name="footer"
201
+ :close="close"
202
+ ></slot>
203
+ </div>
204
+ </div>
205
+ </Transition>
206
+ </div>
207
+ </teleport>
208
+ </template>
@@ -0,0 +1,5 @@
1
+ import Drawer from './Drawer.vue'
2
+
3
+ export {
4
+ Drawer,
5
+ }
@@ -0,0 +1,66 @@
1
+ <script setup>
2
+ import { computed } from 'vue'
3
+ import DropdownMenu from '../DropdownMenu/DropdownMenu.vue'
4
+ import ButtonTertiary from '../Button/ButtonTertiary.vue'
5
+ import Icon from '../Icon/Icon.vue'
6
+ import { AdjustmentsHorizontalIcon, CheckIcon } from '@heroicons/vue/24/outline'
7
+
8
+ const props = defineProps({
9
+ columns: {
10
+ type: Array,
11
+ required: true,
12
+ validator: value => value.every(c => typeof c === 'object' && c.key && c.name !== undefined),
13
+ },
14
+ label: {
15
+ type: String,
16
+ default: 'Columns',
17
+ },
18
+ })
19
+
20
+ const visibleKeys = defineModel('visibleKeys', { type: Array, required: true })
21
+
22
+ const isVisible = (key) => visibleKeys.value.includes(key)
23
+
24
+ function toggle(key) {
25
+ if (visibleKeys.value.includes(key)) {
26
+ visibleKeys.value = visibleKeys.value.filter(k => k !== key)
27
+ } else {
28
+ visibleKeys.value = [...visibleKeys.value, key]
29
+ }
30
+ }
31
+
32
+ const visibleCount = computed(() => visibleKeys.value.length)
33
+ const totalCount = computed(() => props.columns.length)
34
+ </script>
35
+
36
+ <template>
37
+ <DropdownMenu right class="rsui-column-picker">
38
+ <template #trigger="{ open, isOpen }">
39
+ <ButtonTertiary :aria-expanded="isOpen" @click="open">
40
+ <Icon sm><AdjustmentsHorizontalIcon /></Icon>
41
+ {{ label }}
42
+ <span class="rsui-column-picker__count">{{ visibleCount }}/{{ totalCount }}</span>
43
+ </ButtonTertiary>
44
+ </template>
45
+
46
+ <ul class="rsui-column-picker__list" role="none">
47
+ <li v-for="column in columns"
48
+ :key="column.key"
49
+ class="rsui-column-picker__item"
50
+ role="menuitemcheckbox"
51
+ :aria-checked="isVisible(column.key)"
52
+ tabindex="0"
53
+ @click.stop="toggle(column.key)"
54
+ @keydown.enter.self.prevent="toggle(column.key)"
55
+ @keydown.space.self.prevent="toggle(column.key)"
56
+ >
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)" />
61
+ </span>
62
+ <span class="rsui-column-picker__label">{{ column.name || column.key }}</span>
63
+ </li>
64
+ </ul>
65
+ </DropdownMenu>
66
+ </template>
@@ -1,8 +1,10 @@
1
1
  <script setup>
2
+ import { computed, ref, watch } from 'vue'
2
3
  import { Card, CardHeader } from '../Card'
3
4
  import Tr from './Tr.vue'
4
5
  import Th from './Th.vue'
5
6
  import Td from './Td.vue'
7
+ import ColumnPicker from './ColumnPicker.vue'
6
8
 
7
9
  const titleId = _.uniqueId('table-title-')
8
10
 
@@ -39,6 +41,47 @@ const props = defineProps({
39
41
  type: String,
40
42
  default: undefined,
41
43
  },
44
+ // When true, Table renders an internal ColumnPicker in the header actions and
45
+ // manages column visibility itself. Use `v-model:visibleKeys` to persist or
46
+ // override the selection externally; without v-model, all columns are visible
47
+ // by default and toggling is purely local.
48
+ columnPicker: {
49
+ type: Boolean,
50
+ default: false,
51
+ },
52
+ })
53
+
54
+ // v-model:visibleKeys — undefined means "uncontrolled" / use internal default (all visible).
55
+ const visibleKeys = defineModel('visibleKeys', {
56
+ type: Array,
57
+ default: undefined,
58
+ })
59
+
60
+ // Internal fallback when no v-model is bound but columnPicker is enabled.
61
+ const internalVisibleKeys = ref(props.columns.map(c => c.key))
62
+
63
+ watch(() => props.columns, (next) => {
64
+ // Keep internal state in sync if the columns array changes (e.g. consumer adds/removes a column).
65
+ const knownKeys = next.map(c => c.key)
66
+ internalVisibleKeys.value = internalVisibleKeys.value.filter(k => knownKeys.includes(k))
67
+ for (const key of knownKeys) {
68
+ if (!internalVisibleKeys.value.includes(key)) internalVisibleKeys.value.push(key)
69
+ }
70
+ }, { deep: true })
71
+
72
+ const effectiveVisibleKeys = computed({
73
+ get: () => visibleKeys.value ?? internalVisibleKeys.value,
74
+ set: (next) => {
75
+ if (visibleKeys.value !== undefined) {
76
+ visibleKeys.value = next
77
+ } else {
78
+ internalVisibleKeys.value = next
79
+ }
80
+ },
81
+ })
82
+
83
+ const visibleColumns = computed(() => {
84
+ return props.columns.filter(column => effectiveVisibleKeys.value.includes(column.key))
42
85
  })
43
86
  </script>
44
87
 
@@ -67,8 +110,12 @@ const props = defineProps({
67
110
  <slot name="subtitle"></slot>
68
111
  </template>
69
112
 
70
- <template #actions v-if="$slots.actions">
113
+ <template #actions v-if="$slots.actions || columnPicker">
71
114
  <slot name="actions"></slot>
115
+ <ColumnPicker v-if="columnPicker"
116
+ :columns="columns"
117
+ v-model:visibleKeys="effectiveVisibleKeys"
118
+ />
72
119
  </template>
73
120
 
74
121
  <template #more-actions v-if="$slots['more-actions']">
@@ -93,9 +140,9 @@ const props = defineProps({
93
140
  :aria-label="!showHeader || !$slots.title ? ariaLabel : undefined"
94
141
  >
95
142
  <caption v-if="!showHeader && $slots.title"><slot name="title"></slot></caption>
96
- <thead v-if="columns">
143
+ <thead v-if="visibleColumns.length">
97
144
  <Tr>
98
- <Th v-for="column in columns"
145
+ <Th v-for="column in visibleColumns"
99
146
  :key="column.key"
100
147
  scope="col"
101
148
  :alignment="column?.alignment"
@@ -119,7 +166,7 @@ const props = defineProps({
119
166
  :clickable="row.clickable ?? clickableRows"
120
167
  @click="$emit('click:row', row)"
121
168
  >
122
- <Td v-for="column in columns"
169
+ <Td v-for="column in visibleColumns"
123
170
  :key="column.key"
124
171
  :alignment="column?.alignment"
125
172
  :fixed="fixedColumns"