picocrank 1.25.2 → 1.27.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/eslint.config.js +75 -0
- package/package.json +17 -4
- package/router.js +74 -0
- package/styles/dark-theme.css +20 -11
- package/styles/picocrank-extensions.css +123 -30
- package/supplemental-themes/catppuccin-latte-frappe/theme.css +10 -24
- package/supplemental-themes/dracula-alucard/theme.css +10 -24
- package/supplemental-themes/gruvbox-dark-light/theme.css +10 -24
- package/supplemental-themes/waffles/theme.css +2 -16
- package/vite.config.js +5 -0
- package/vue/components/Breadcrumbs.vue +192 -45
- package/vue/components/Calendar.vue +162 -145
- package/vue/components/DangerZone.vue +186 -0
- package/vue/components/FormField.spec.js +350 -0
- package/vue/components/FormField.vue +236 -32
- package/vue/components/FormLayout.spec.js +56 -0
- package/vue/components/Header.vue +22 -6
- package/vue/components/Login.vue +152 -41
- package/vue/components/NavigationGrid.vue +26 -180
- package/vue/components/NotificationBlock.vue +120 -0
- package/vue/components/NotificationPopup.vue +44 -41
- package/vue/components/Pagination.vue +5 -4
- package/vue/components/ReadOnlyTextArea.vue +1 -1
- package/vue/components/SidebarNavLink.vue +3 -3
- package/vue/components/Table.vue +107 -79
- package/vue/components/TableColumnOptionsPopover.vue +9 -0
- package/vue/components/Tabs.spec.js +101 -0
- package/vue/components/Tabs.vue +455 -81
- package/vue/components/TopBar.vue +2 -2
- package/vue/components/form-validation.spec.js +126 -0
- package/vue/composables/tableColumnPriorities.js +33 -0
- package/vue/composables/tableFilters.js +1 -1
- package/vue/data/navSectionHubs.js +196 -0
- package/vue/data/routeBreadcrumbs.js +90 -0
- package/vue/examples/Admin.vue +12 -12
- package/vue/examples/App.vue +27 -36
- package/vue/examples/CalendarExample.vue +32 -22
- package/vue/examples/ExampleHeaderThemeSwitcher.vue +1 -1
- package/vue/examples/FormExample.vue +76 -48
- package/vue/examples/Login.vue +5 -3
- package/vue/examples/LoginExample.vue +99 -15
- package/vue/examples/NavSectionHub.vue +36 -0
- package/vue/examples/PageStructure.vue +15 -12
- package/vue/examples/StatusExample.vue +248 -129
- package/vue/examples/TableExample.vue +72 -71
- package/vue/examples/TabsExample.vue +236 -257
- package/vue/examples/ThemePreview.vue +21 -23
- package/vue/examples/ViewItem.vue +32 -5
- package/vue/examples/Welcome.vue +3 -40
- package/vue/test/formTestUtils.js +279 -0
- package/vue/test/setup.js +5 -0
|
@@ -1,9 +1,34 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
<div class="notification-popup" role="status">
|
|
3
|
+
<NotificationBlock
|
|
4
|
+
:type="notificationType"
|
|
5
|
+
:label="popup.label || ''"
|
|
6
|
+
>
|
|
7
|
+
<span class="notification-popup-content">
|
|
8
|
+
<span class="notification-popup-message">{{ popup.message }}</span>
|
|
9
|
+
|
|
10
|
+
<RouterLink
|
|
11
|
+
v-if="popup.linkTo && !isExternalLink"
|
|
12
|
+
:to="popup.linkTo"
|
|
13
|
+
class="notification-popup-link"
|
|
14
|
+
@click="emit('dismiss', popup.id)"
|
|
15
|
+
>
|
|
16
|
+
{{ popup.linkLabel || 'View' }}
|
|
17
|
+
</RouterLink>
|
|
18
|
+
|
|
19
|
+
<a
|
|
20
|
+
v-else-if="popup.linkTo && isExternalLink"
|
|
21
|
+
:href="popup.linkTo"
|
|
22
|
+
class="notification-popup-link"
|
|
23
|
+
target="_blank"
|
|
24
|
+
rel="noopener noreferrer"
|
|
25
|
+
@click="emit('dismiss', popup.id)"
|
|
26
|
+
>
|
|
27
|
+
{{ popup.linkLabel || 'View' }}
|
|
28
|
+
</a>
|
|
29
|
+
</span>
|
|
30
|
+
</NotificationBlock>
|
|
31
|
+
|
|
7
32
|
<button
|
|
8
33
|
type="button"
|
|
9
34
|
class="notification-popup-dismiss"
|
|
@@ -12,33 +37,6 @@
|
|
|
12
37
|
>
|
|
13
38
|
<HugeiconsIcon :icon="Cancel01Icon" width="1em" height="1em" />
|
|
14
39
|
</button>
|
|
15
|
-
|
|
16
|
-
<div class="notification-popup-body">
|
|
17
|
-
<p class="notification-popup-message">
|
|
18
|
-
<strong v-if="popup.label">{{ popup.label }}:</strong>
|
|
19
|
-
{{ popup.message }}
|
|
20
|
-
</p>
|
|
21
|
-
|
|
22
|
-
<RouterLink
|
|
23
|
-
v-if="popup.linkTo && !isExternalLink"
|
|
24
|
-
:to="popup.linkTo"
|
|
25
|
-
class="notification-popup-link"
|
|
26
|
-
@click="emit('dismiss', popup.id)"
|
|
27
|
-
>
|
|
28
|
-
{{ popup.linkLabel || 'View' }}
|
|
29
|
-
</RouterLink>
|
|
30
|
-
|
|
31
|
-
<a
|
|
32
|
-
v-else-if="popup.linkTo && isExternalLink"
|
|
33
|
-
:href="popup.linkTo"
|
|
34
|
-
class="notification-popup-link"
|
|
35
|
-
target="_blank"
|
|
36
|
-
rel="noopener noreferrer"
|
|
37
|
-
@click="emit('dismiss', popup.id)"
|
|
38
|
-
>
|
|
39
|
-
{{ popup.linkLabel || 'View' }}
|
|
40
|
-
</a>
|
|
41
|
-
</div>
|
|
42
40
|
</div>
|
|
43
41
|
</template>
|
|
44
42
|
|
|
@@ -47,6 +45,7 @@ import { computed } from 'vue'
|
|
|
47
45
|
import { RouterLink } from 'vue-router'
|
|
48
46
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
49
47
|
import { Cancel01Icon } from '@hugeicons/core-free-icons'
|
|
48
|
+
import NotificationBlock from './NotificationBlock.vue'
|
|
50
49
|
|
|
51
50
|
const props = defineProps({
|
|
52
51
|
popup: {
|
|
@@ -57,6 +56,8 @@ const props = defineProps({
|
|
|
57
56
|
|
|
58
57
|
const emit = defineEmits(['dismiss'])
|
|
59
58
|
|
|
59
|
+
const notificationType = computed(() => props.popup.class || 'info')
|
|
60
|
+
|
|
60
61
|
const isExternalLink = computed(() =>
|
|
61
62
|
typeof props.popup.linkTo === 'string' && /^https?:\/\//.test(props.popup.linkTo)
|
|
62
63
|
)
|
|
@@ -67,30 +68,32 @@ const isExternalLink = computed(() =>
|
|
|
67
68
|
position: relative;
|
|
68
69
|
width: min(22rem, calc(100vw - 2rem));
|
|
69
70
|
margin: 0;
|
|
70
|
-
|
|
71
|
-
cursor: default;
|
|
72
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
71
|
+
box-shadow: 0 4px 12px color-mix(in srgb, var(--shadow-color) 60%, transparent);
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
.notification-popup
|
|
74
|
+
.notification-popup :deep(.notification) {
|
|
76
75
|
margin: 0;
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
.notification-popup-
|
|
78
|
+
.notification-popup-content {
|
|
80
79
|
display: flex;
|
|
81
80
|
flex-direction: column;
|
|
82
81
|
gap: 0.5rem;
|
|
83
82
|
}
|
|
84
83
|
|
|
84
|
+
.notification-popup-message {
|
|
85
|
+
display: block;
|
|
86
|
+
}
|
|
87
|
+
|
|
85
88
|
.notification-popup-link {
|
|
86
|
-
color:
|
|
89
|
+
color: var(--link-color);
|
|
87
90
|
font-weight: bold;
|
|
88
91
|
text-decoration: underline;
|
|
89
92
|
align-self: flex-start;
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
.notification-popup-link:hover {
|
|
93
|
-
|
|
96
|
+
text-decoration-thickness: 2px;
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
.notification-popup-dismiss {
|
|
@@ -106,13 +109,13 @@ const isExternalLink = computed(() =>
|
|
|
106
109
|
border: 0;
|
|
107
110
|
border-radius: 0.35rem;
|
|
108
111
|
background: transparent;
|
|
109
|
-
color:
|
|
112
|
+
color: var(--text-color);
|
|
110
113
|
cursor: pointer;
|
|
111
114
|
opacity: 0.75;
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
.notification-popup-dismiss:hover {
|
|
115
118
|
opacity: 1;
|
|
116
|
-
background:
|
|
119
|
+
background: var(--hover-background-color);
|
|
117
120
|
}
|
|
118
121
|
</style>
|
|
@@ -185,19 +185,20 @@ watch(() => props.pageSize, (newSize) => {
|
|
|
185
185
|
|
|
186
186
|
.button:disabled {
|
|
187
187
|
background: transparent;
|
|
188
|
+
color: var(--button-transparent-disabled-fg);
|
|
188
189
|
cursor: not-allowed;
|
|
189
190
|
}
|
|
190
191
|
|
|
192
|
+
.button.active:disabled {
|
|
193
|
+
color: var(--text-color);
|
|
194
|
+
}
|
|
195
|
+
|
|
191
196
|
#page-size {
|
|
192
197
|
background: transparent;
|
|
193
198
|
margin-left: 0.5rem;
|
|
194
199
|
margin-right: 0.5rem;
|
|
195
200
|
}
|
|
196
201
|
|
|
197
|
-
option {
|
|
198
|
-
background: #545f69;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
202
|
/* Responsive design */
|
|
202
203
|
@media (max-width: 768px) {
|
|
203
204
|
.pagination {
|
|
@@ -121,7 +121,7 @@ a.active {
|
|
|
121
121
|
width: 0.5rem;
|
|
122
122
|
height: 0.5rem;
|
|
123
123
|
border-radius: 50%;
|
|
124
|
-
background: var(--
|
|
124
|
+
background: var(--karma-bad-tint);
|
|
125
125
|
flex-shrink: 0;
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -129,8 +129,8 @@ a.active {
|
|
|
129
129
|
min-width: 1.25rem;
|
|
130
130
|
padding: 0.1rem 0.4rem;
|
|
131
131
|
border-radius: 999px;
|
|
132
|
-
background: var(--
|
|
133
|
-
color:
|
|
132
|
+
background: var(--karma-bad-tint);
|
|
133
|
+
color: var(--text-color);
|
|
134
134
|
font-size: 0.75em;
|
|
135
135
|
font-weight: 600;
|
|
136
136
|
line-height: 1.2;
|
package/vue/components/Table.vue
CHANGED
|
@@ -37,27 +37,24 @@
|
|
|
37
37
|
v-if="showColumnOptionsButton"
|
|
38
38
|
class="actions table-column-options-header"
|
|
39
39
|
>
|
|
40
|
-
<
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
</button>
|
|
59
|
-
</span>
|
|
60
|
-
</div>
|
|
40
|
+
<span class="actions-menu-trigger">
|
|
41
|
+
<button
|
|
42
|
+
ref="columnOptionsButtonRef"
|
|
43
|
+
type="button"
|
|
44
|
+
aria-haspopup="dialog"
|
|
45
|
+
aria-label="Column options"
|
|
46
|
+
:aria-expanded="columnOptionsOpen ? 'true' : 'false'"
|
|
47
|
+
@click.stop="openColumnOptions"
|
|
48
|
+
>
|
|
49
|
+
<HugeiconsIcon
|
|
50
|
+
:icon="LayoutGridIcon"
|
|
51
|
+
width="0.95em"
|
|
52
|
+
height="0.95em"
|
|
53
|
+
:strokeWidth="2"
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
/>
|
|
56
|
+
</button>
|
|
57
|
+
</span>
|
|
61
58
|
</th>
|
|
62
59
|
</tr>
|
|
63
60
|
</thead>
|
|
@@ -71,7 +68,12 @@
|
|
|
71
68
|
<td
|
|
72
69
|
v-for="(header, cellIndex) in visibleHeaders"
|
|
73
70
|
:key="header.key || cellIndex"
|
|
74
|
-
:
|
|
71
|
+
:colspan="bodyColumnColspan(cellIndex) || undefined"
|
|
72
|
+
:class="[
|
|
73
|
+
cellClasses(header),
|
|
74
|
+
header.class,
|
|
75
|
+
isLastColumnWithOptions(cellIndex) ? 'table-last-column-with-options' : null,
|
|
76
|
+
]"
|
|
75
77
|
>
|
|
76
78
|
<slot
|
|
77
79
|
v-if="slots[`cell-${header.key}`]"
|
|
@@ -89,11 +91,6 @@
|
|
|
89
91
|
{{ row[header.key] }}
|
|
90
92
|
</span>
|
|
91
93
|
</td>
|
|
92
|
-
<td
|
|
93
|
-
v-if="showColumnOptionsButton"
|
|
94
|
-
class="actions table-column-options-cell"
|
|
95
|
-
aria-hidden="true"
|
|
96
|
-
/>
|
|
97
94
|
</tr>
|
|
98
95
|
</tbody>
|
|
99
96
|
</table>
|
|
@@ -135,6 +132,7 @@
|
|
|
135
132
|
:column-keys="resolvedColumnOrder"
|
|
136
133
|
:visible-keys="visibleColumnKeys"
|
|
137
134
|
:column-priorities="resolvedColumnPriorities"
|
|
135
|
+
:default-column-priorities="defaultColumnPriorities"
|
|
138
136
|
:filters="activeFilters"
|
|
139
137
|
:rows="props.data"
|
|
140
138
|
:table-id="props.tableId"
|
|
@@ -161,11 +159,9 @@ import TableColumnFilterPopover from './TableColumnFilterPopover.vue'
|
|
|
161
159
|
import TableColumnOptionsPopover from './TableColumnOptionsPopover.vue'
|
|
162
160
|
import { useLongPress } from '../composables/useLongPress.js'
|
|
163
161
|
import {
|
|
164
|
-
applyFilters,
|
|
165
162
|
buildSelectOptions,
|
|
166
163
|
cloneColumnFilterEntries,
|
|
167
164
|
cloneFilters,
|
|
168
|
-
countActiveFilters,
|
|
169
165
|
isColumnFiltered,
|
|
170
166
|
isFilterEntryActive,
|
|
171
167
|
normalizeColumnFilterEntries,
|
|
@@ -186,6 +182,10 @@ import {
|
|
|
186
182
|
sanitizePresetState,
|
|
187
183
|
layoutStatesEqual,
|
|
188
184
|
} from '../composables/tableColumnPresets.js'
|
|
185
|
+
import {
|
|
186
|
+
defaultColumnPriorityForIndex,
|
|
187
|
+
isValidColPriority,
|
|
188
|
+
} from '../composables/tableColumnPriorities.js'
|
|
189
189
|
|
|
190
190
|
const DEFAULT_PAGE_SIZE = 10
|
|
191
191
|
|
|
@@ -231,6 +231,10 @@ const props = defineProps({
|
|
|
231
231
|
type: Boolean,
|
|
232
232
|
default: false,
|
|
233
233
|
},
|
|
234
|
+
responsiveColumns: {
|
|
235
|
+
type: Boolean,
|
|
236
|
+
default: true,
|
|
237
|
+
},
|
|
234
238
|
stickyCols: {
|
|
235
239
|
type: Number,
|
|
236
240
|
default: null,
|
|
@@ -286,6 +290,8 @@ const emit = defineEmits([
|
|
|
286
290
|
'column-order-change',
|
|
287
291
|
'update:columnPriorities',
|
|
288
292
|
'column-priorities-change',
|
|
293
|
+
'update:layoutLabel',
|
|
294
|
+
'layout-label-change',
|
|
289
295
|
])
|
|
290
296
|
|
|
291
297
|
const attrs = useAttrs()
|
|
@@ -368,6 +374,18 @@ const showColumnOptionsButton = computed(() =>
|
|
|
368
374
|
props.columnOptions && manageableColumnKeys.value.length > 0,
|
|
369
375
|
)
|
|
370
376
|
|
|
377
|
+
function isLastVisibleColumn(index) {
|
|
378
|
+
return index === visibleHeaders.value.length - 1
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function isLastColumnWithOptions(index) {
|
|
382
|
+
return showColumnOptionsButton.value && isLastVisibleColumn(index)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function bodyColumnColspan(index) {
|
|
386
|
+
return isLastColumnWithOptions(index) ? 2 : null
|
|
387
|
+
}
|
|
388
|
+
|
|
371
389
|
function isHeaderHidden(header) {
|
|
372
390
|
if (header.hidden) {
|
|
373
391
|
return true
|
|
@@ -386,9 +404,13 @@ const visibleColumnKeys = computed(() =>
|
|
|
386
404
|
.map((header) => header.key),
|
|
387
405
|
)
|
|
388
406
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
407
|
+
const hasHorizontalScroll = computed(() =>
|
|
408
|
+
props.horizontalScroll || (props.stickyCols >= 1 && props.stickyCols <= 3),
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
const useResponsiveColumns = computed(() =>
|
|
412
|
+
props.responsiveColumns && !hasHorizontalScroll.value,
|
|
413
|
+
)
|
|
392
414
|
|
|
393
415
|
function resolvedColumnPriority(header) {
|
|
394
416
|
if (!header?.key) {
|
|
@@ -404,9 +426,35 @@ function resolvedColumnPriority(header) {
|
|
|
404
426
|
return isValidColPriority(override) ? override : null
|
|
405
427
|
}
|
|
406
428
|
|
|
407
|
-
|
|
429
|
+
if (!useResponsiveColumns.value) {
|
|
430
|
+
return null
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const index = visibleHeaders.value.findIndex((item) => item.key === header.key)
|
|
434
|
+
if (index < 0) {
|
|
435
|
+
return null
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return defaultColumnPriorityForIndex(header, index, useResponsiveColumns.value)
|
|
408
439
|
}
|
|
409
440
|
|
|
441
|
+
const defaultColumnPriorities = computed(() => {
|
|
442
|
+
if (!useResponsiveColumns.value) {
|
|
443
|
+
return Object.fromEntries(
|
|
444
|
+
visibleHeaders.value
|
|
445
|
+
.filter((header) => isValidColPriority(header?.colPriority))
|
|
446
|
+
.map((header) => [header.key, header.colPriority]),
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return Object.fromEntries(
|
|
451
|
+
visibleHeaders.value.flatMap((header, index) => {
|
|
452
|
+
const priority = defaultColumnPriorityForIndex(header, index, true)
|
|
453
|
+
return isValidColPriority(priority) ? [[header.key, priority]] : []
|
|
454
|
+
}),
|
|
455
|
+
)
|
|
456
|
+
})
|
|
457
|
+
|
|
410
458
|
const resolvedColumnPriorities = computed(() =>
|
|
411
459
|
Object.fromEntries(
|
|
412
460
|
manageableColumnKeys.value.map((key) => {
|
|
@@ -420,10 +468,6 @@ const hasColPriorities = computed(() =>
|
|
|
420
468
|
orderedHeaders.value.some((header) => isValidColPriority(resolvedColumnPriority(header))),
|
|
421
469
|
)
|
|
422
470
|
|
|
423
|
-
const hasHorizontalScroll = computed(() =>
|
|
424
|
-
props.horizontalScroll || (props.stickyCols >= 1 && props.stickyCols <= 3),
|
|
425
|
-
)
|
|
426
|
-
|
|
427
471
|
const needsTableWrapper = computed(() => hasColPriorities.value || hasHorizontalScroll.value)
|
|
428
472
|
|
|
429
473
|
const tableWrapperClasses = computed(() => {
|
|
@@ -528,10 +572,6 @@ const queryParams = computed(() => {
|
|
|
528
572
|
return params
|
|
529
573
|
})
|
|
530
574
|
|
|
531
|
-
function isValidColPriority(priority) {
|
|
532
|
-
return Number.isInteger(priority) && priority >= 1 && priority <= 5
|
|
533
|
-
}
|
|
534
|
-
|
|
535
575
|
function colPriorityClass(priority) {
|
|
536
576
|
return isValidColPriority(priority) ? `col-priority-${priority}` : null
|
|
537
577
|
}
|
|
@@ -1105,6 +1145,11 @@ const activeLayoutLabel = computed(() => {
|
|
|
1105
1145
|
return 'Custom'
|
|
1106
1146
|
})
|
|
1107
1147
|
|
|
1148
|
+
function emitLayoutLabelChange(label) {
|
|
1149
|
+
emit('update:layoutLabel', label)
|
|
1150
|
+
emit('layout-label-change', label)
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1108
1153
|
function loadDeveloperDefaults() {
|
|
1109
1154
|
setFilters({})
|
|
1110
1155
|
sortBy.value = null
|
|
@@ -1313,6 +1358,10 @@ watch(
|
|
|
1313
1358
|
{ deep: true },
|
|
1314
1359
|
)
|
|
1315
1360
|
|
|
1361
|
+
watch(activeLayoutLabel, (label) => {
|
|
1362
|
+
emitLayoutLabelChange(label)
|
|
1363
|
+
}, { immediate: true })
|
|
1364
|
+
|
|
1316
1365
|
watch(sortedItems, () => {
|
|
1317
1366
|
if (!isRemote.value) {
|
|
1318
1367
|
page.value = 1
|
|
@@ -1356,6 +1405,11 @@ onBeforeUnmount(() => {
|
|
|
1356
1405
|
clearTimeout(fetchDebounceTimer.value)
|
|
1357
1406
|
}
|
|
1358
1407
|
})
|
|
1408
|
+
|
|
1409
|
+
defineExpose({
|
|
1410
|
+
layoutLabel: activeLayoutLabel,
|
|
1411
|
+
loadDeveloperDefaults,
|
|
1412
|
+
})
|
|
1359
1413
|
</script>
|
|
1360
1414
|
|
|
1361
1415
|
<style scoped>
|
|
@@ -1443,41 +1497,19 @@ tbody tr.row-clickable {
|
|
|
1443
1497
|
margin: 0;
|
|
1444
1498
|
}
|
|
1445
1499
|
|
|
1446
|
-
th.table-column-options-header
|
|
1447
|
-
td.table-column-options-cell {
|
|
1500
|
+
th.table-column-options-header {
|
|
1448
1501
|
padding-right: 1rem;
|
|
1449
1502
|
text-align: right;
|
|
1450
1503
|
vertical-align: middle;
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
th.table-column-options-header {
|
|
1454
1504
|
width: auto;
|
|
1455
1505
|
min-width: 2.5rem;
|
|
1456
1506
|
max-width: none;
|
|
1457
1507
|
white-space: nowrap;
|
|
1458
1508
|
}
|
|
1459
1509
|
|
|
1460
|
-
td.table-column-options
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
max-width: 2.5rem;
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
.table-column-options-header-content {
|
|
1467
|
-
display: inline-flex;
|
|
1468
|
-
align-items: center;
|
|
1469
|
-
justify-content: flex-end;
|
|
1470
|
-
gap: 0.5rem;
|
|
1471
|
-
width: 100%;
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
.table-layout-label {
|
|
1475
|
-
font-size: 0.85rem;
|
|
1476
|
-
color: var(--table-muted-fg);
|
|
1477
|
-
max-width: 10rem;
|
|
1478
|
-
overflow: hidden;
|
|
1479
|
-
text-overflow: ellipsis;
|
|
1480
|
-
white-space: nowrap;
|
|
1510
|
+
td.table-last-column-with-options {
|
|
1511
|
+
padding-right: 1rem;
|
|
1512
|
+
vertical-align: middle;
|
|
1481
1513
|
}
|
|
1482
1514
|
|
|
1483
1515
|
th.table-column-options-header .actions-menu-trigger {
|
|
@@ -1486,24 +1518,20 @@ th.table-column-options-header .actions-menu-trigger {
|
|
|
1486
1518
|
flex: 0 0 auto;
|
|
1487
1519
|
}
|
|
1488
1520
|
|
|
1489
|
-
.table-scroll
|
|
1490
|
-
|
|
1521
|
+
.table-scroll {
|
|
1522
|
+
min-width: 0;
|
|
1523
|
+
max-width: 100%;
|
|
1524
|
+
contain: inline-size;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
.table-scroll th.table-column-options-header {
|
|
1491
1528
|
position: sticky;
|
|
1492
1529
|
right: 0;
|
|
1493
|
-
z-index:
|
|
1530
|
+
z-index: 2;
|
|
1494
1531
|
background-color: var(--table-sticky-bg);
|
|
1495
1532
|
box-shadow: -1px 0 0 var(--border-color);
|
|
1496
1533
|
}
|
|
1497
1534
|
|
|
1498
|
-
.table-scroll thead th.table-column-options-header {
|
|
1499
|
-
z-index: 2;
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
.table-scroll table.row-hover tbody tr:hover td.table-column-options-cell,
|
|
1503
|
-
.table-scroll table.row-hover tbody tr:focus-within td.table-column-options-cell {
|
|
1504
|
-
background-color: var(--table-sticky-hover-bg);
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
1535
|
th.actions .actions-menu-trigger button {
|
|
1508
1536
|
display: inline-flex;
|
|
1509
1537
|
align-items: center;
|
|
@@ -264,6 +264,10 @@ const props = defineProps({
|
|
|
264
264
|
type: Object,
|
|
265
265
|
default: () => ({}),
|
|
266
266
|
},
|
|
267
|
+
defaultColumnPriorities: {
|
|
268
|
+
type: Object,
|
|
269
|
+
default: () => ({}),
|
|
270
|
+
},
|
|
267
271
|
anchorEl: {
|
|
268
272
|
type: Object,
|
|
269
273
|
default: null,
|
|
@@ -389,6 +393,11 @@ function isValidColPriority(priority) {
|
|
|
389
393
|
}
|
|
390
394
|
|
|
391
395
|
function defaultColumnPriority(key) {
|
|
396
|
+
if (Object.prototype.hasOwnProperty.call(props.defaultColumnPriorities, key)) {
|
|
397
|
+
const value = props.defaultColumnPriorities[key]
|
|
398
|
+
return isValidColPriority(value) ? value : null
|
|
399
|
+
}
|
|
400
|
+
|
|
392
401
|
const header = headerByKey.value.get(key)
|
|
393
402
|
return isValidColPriority(header?.colPriority) ? header.colPriority : null
|
|
394
403
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import { nextTick } from 'vue'
|
|
4
|
+
import Tabs from './Tabs.vue'
|
|
5
|
+
|
|
6
|
+
const globalStubs = {
|
|
7
|
+
HugeiconsIcon: true,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('Tabs disabled support', () => {
|
|
11
|
+
const tabs = [
|
|
12
|
+
{ id: 'first', label: 'First' },
|
|
13
|
+
{ id: 'second', label: 'Second', disabled: true },
|
|
14
|
+
{ id: 'third', label: 'Third' },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
function mountTabs(overrides = {}) {
|
|
18
|
+
return mount(Tabs, {
|
|
19
|
+
attachTo: document.body,
|
|
20
|
+
props: {
|
|
21
|
+
tabs,
|
|
22
|
+
defaultTab: 'first',
|
|
23
|
+
...overrides.props,
|
|
24
|
+
},
|
|
25
|
+
slots: {
|
|
26
|
+
'tab-first': '<p>First panel</p>',
|
|
27
|
+
'tab-second': '<p>Second panel</p>',
|
|
28
|
+
'tab-third': '<p>Third panel</p>',
|
|
29
|
+
},
|
|
30
|
+
global: {
|
|
31
|
+
stubs: globalStubs,
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
it('marks disabled tabs with disabled and aria-disabled', () => {
|
|
37
|
+
const wrapper = mountTabs()
|
|
38
|
+
|
|
39
|
+
const disabledTab = wrapper.find('#tab-second')
|
|
40
|
+
expect(disabledTab.attributes('disabled')).toBeDefined()
|
|
41
|
+
expect(disabledTab.attributes('aria-disabled')).toBe('true')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('does not select a disabled tab on click', async () => {
|
|
45
|
+
const wrapper = mountTabs()
|
|
46
|
+
|
|
47
|
+
await wrapper.find('#tab-second').trigger('click')
|
|
48
|
+
await nextTick()
|
|
49
|
+
|
|
50
|
+
expect(wrapper.find('#tab-first').attributes('aria-selected')).toBe('true')
|
|
51
|
+
expect(wrapper.emitted('tab-change')).toBeUndefined()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('skips disabled tabs when moving focus with arrow keys', async () => {
|
|
55
|
+
const wrapper = mountTabs()
|
|
56
|
+
|
|
57
|
+
await wrapper.find('#tab-first').trigger('keydown', { key: 'ArrowRight' })
|
|
58
|
+
await nextTick()
|
|
59
|
+
|
|
60
|
+
expect(wrapper.find('#tab-third').attributes('tabindex')).toBe('0')
|
|
61
|
+
expect(wrapper.find('#tab-third').attributes('aria-selected')).toBe('true')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('selects the first enabled tab when defaultTab is disabled', async () => {
|
|
65
|
+
const wrapper = mountTabs({
|
|
66
|
+
props: {
|
|
67
|
+
defaultTab: 'second',
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
await nextTick()
|
|
72
|
+
|
|
73
|
+
expect(wrapper.find('#tab-first').attributes('aria-selected')).toBe('true')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('moves off a tab when it becomes disabled', async () => {
|
|
77
|
+
const wrapper = mountTabs({
|
|
78
|
+
props: {
|
|
79
|
+
tabs: [
|
|
80
|
+
{ id: 'first', label: 'First' },
|
|
81
|
+
{ id: 'second', label: 'Second' },
|
|
82
|
+
],
|
|
83
|
+
defaultTab: 'second',
|
|
84
|
+
},
|
|
85
|
+
slots: {
|
|
86
|
+
'tab-first': '<p>First panel</p>',
|
|
87
|
+
'tab-second': '<p>Second panel</p>',
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
await wrapper.setProps({
|
|
92
|
+
tabs: [
|
|
93
|
+
{ id: 'first', label: 'First' },
|
|
94
|
+
{ id: 'second', label: 'Second', disabled: true },
|
|
95
|
+
],
|
|
96
|
+
})
|
|
97
|
+
await nextTick()
|
|
98
|
+
|
|
99
|
+
expect(wrapper.find('#tab-first').attributes('aria-selected')).toBe('true')
|
|
100
|
+
})
|
|
101
|
+
})
|