picocrank 1.33.0 → 1.34.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/docs/filter-query-v1.md +5 -1
- package/package.json +1 -1
- package/router.js +33 -0
- package/styles/picocrank-extensions.css +23 -2
- package/vue/components/Accordion.spec.js +48 -0
- package/vue/components/Accordion.vue +46 -0
- package/vue/components/AccordionItem.vue +103 -0
- package/vue/components/FieldValue.spec.js +88 -0
- package/vue/components/FieldValue.vue +222 -0
- package/vue/components/FormField.spec.js +1 -0
- package/vue/components/FormField.vue +38 -10
- package/vue/components/Header.vue +24 -2
- package/vue/components/NotificationBlock.vue +25 -0
- package/vue/components/ReorderList.spec.js +318 -0
- package/vue/components/ReorderList.vue +614 -0
- package/vue/components/Table.spec.js +27 -0
- package/vue/components/Table.vue +458 -112
- package/vue/components/TableColumnFilterPopover.vue +63 -19
- package/vue/components/TableColumnOptionsPopover.vue +248 -273
- package/vue/components/TopBar.vue +6 -2
- package/vue/components/accordionContext.js +1 -0
- package/vue/composables/tableColumnPresets.js +33 -0
- package/vue/composables/tableGroupBy.js +173 -0
- package/vue/composables/tableGroupBy.spec.js +69 -0
- package/vue/composables/tableViewSettings.js +34 -0
- package/vue/composables/tableViewSettings.spec.js +21 -0
- package/vue/composables/useHeaderTopBarFit.js +91 -0
- package/vue/composables/useResponsiveNav.js +5 -3
- package/vue/data/navSectionHubs.js +16 -1
- package/vue/examples/AccordionExample.vue +33 -0
- package/vue/examples/FieldValueExample.vue +143 -0
- package/vue/examples/FormExample.vue +11 -6
- package/vue/examples/ReorderListExample.vue +236 -0
- package/vue/examples/TableExample.vue +5 -1
- package/vue/examples/TableRemoteExample.vue +28 -4
|
@@ -20,16 +20,19 @@
|
|
|
20
20
|
rel="noopener noreferrer"
|
|
21
21
|
>{{ docsLinkTitle }}</a>
|
|
22
22
|
</div>
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
<div class="form-field-value-stack">
|
|
24
|
+
<slot />
|
|
25
|
+
<NotificationBlock
|
|
26
|
+
v-if="fieldError"
|
|
27
|
+
:id="errorId"
|
|
28
|
+
class="form-field-error"
|
|
29
|
+
type="bad"
|
|
30
|
+
label="Error"
|
|
31
|
+
:message="fieldError"
|
|
32
|
+
attached-below
|
|
33
|
+
role="alert"
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
33
36
|
<div v-if="!descriptionAbove && hasFieldMeta" class="form-field-meta">
|
|
34
37
|
<p v-if="description" class="subtle form-field-description">{{ description }}</p>
|
|
35
38
|
<a
|
|
@@ -303,10 +306,35 @@ label.disabled,
|
|
|
303
306
|
align-self: flex-start;
|
|
304
307
|
}
|
|
305
308
|
|
|
309
|
+
.form-field-value-stack {
|
|
310
|
+
display: flex;
|
|
311
|
+
flex-direction: column;
|
|
312
|
+
gap: 0;
|
|
313
|
+
width: 100%;
|
|
314
|
+
min-width: 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
306
317
|
.form-field-control :deep(.form-field-error) {
|
|
307
318
|
font-size: 0.9em;
|
|
308
319
|
}
|
|
309
320
|
|
|
321
|
+
.form-field-value-stack:has(> :is(input, textarea, select).field-invalid + :deep(.form-field-error.attached-below)) {
|
|
322
|
+
box-shadow: var(--field-invalid-outline);
|
|
323
|
+
border-radius: 0.4em;
|
|
324
|
+
overflow: hidden;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.form-field-value-stack:has(> :is(input, textarea, select).field-invalid + :deep(.form-field-error.attached-below)) > :is(input, textarea, select).field-invalid {
|
|
328
|
+
border-bottom-left-radius: 0;
|
|
329
|
+
border-bottom-right-radius: 0;
|
|
330
|
+
box-shadow: none;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.form-field-value-stack:has(> :is(input, textarea, select).field-invalid + :deep(.form-field-error.attached-below)) :deep(.form-field-error.attached-below) {
|
|
334
|
+
margin: 0;
|
|
335
|
+
box-shadow: none;
|
|
336
|
+
}
|
|
337
|
+
|
|
310
338
|
.form-field-required {
|
|
311
339
|
color: var(--form-field-required-fg);
|
|
312
340
|
margin-left: 0.15em;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<header :class="{ fixed }">
|
|
2
|
+
<header ref="headerRoot" :class="{ fixed }">
|
|
3
3
|
<div
|
|
4
4
|
:id = "showSidebarChrome ? 'sidebar-button' : null"
|
|
5
5
|
:class="['image-and-title', 'flex-row', { 'disabled-branding': !showBranding, 'logo-home-link': !showSidebarChrome && showBranding }]"
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
</template>
|
|
72
72
|
|
|
73
73
|
<script setup>
|
|
74
|
-
import { computed, ref } from "vue";
|
|
74
|
+
import { computed, nextTick, ref, watch } from "vue";
|
|
75
75
|
import { RouterLink } from "vue-router";
|
|
76
76
|
import { HugeiconsIcon } from "@hugeicons/vue";
|
|
77
77
|
import { BulbIcon, Menu01Icon, Moon02Icon, Sun01Icon, UserIcon } from "@hugeicons/core-free-icons";
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
import TopBar from "./TopBar.vue";
|
|
81
81
|
import Sidebar from "./Sidebar.vue";
|
|
82
82
|
import { useTheme } from '../composables/useTheme.js';
|
|
83
|
+
import { useHeaderTopBarFit } from '../composables/useHeaderTopBarFit.js';
|
|
83
84
|
import { useResponsiveNav } from '../composables/useResponsiveNav.js';
|
|
84
85
|
|
|
85
86
|
const { theme, nextTheme, toggleTheme } = useTheme();
|
|
@@ -161,10 +162,31 @@
|
|
|
161
162
|
});
|
|
162
163
|
|
|
163
164
|
const fallbackSidebar = ref(null);
|
|
165
|
+
const headerRoot = ref(null);
|
|
166
|
+
const topBarSpaceLimited = ref(false);
|
|
164
167
|
|
|
165
168
|
const { showTopBar, showSidebarChrome, needsFallbackSidebar, isMobile } = useResponsiveNav(
|
|
166
169
|
() => props.sidebarEnabled,
|
|
167
170
|
() => props.topBarEnabled,
|
|
171
|
+
topBarSpaceLimited,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const topBarMeasureActive = computed(
|
|
175
|
+
() => props.topBarEnabled && !isMobile.value,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const { remeasure: remeasureTopBar } = useHeaderTopBarFit(
|
|
179
|
+
headerRoot,
|
|
180
|
+
topBarMeasureActive,
|
|
181
|
+
topBarSpaceLimited,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
watch(showTopBar, () => nextTick(() => remeasureTopBar()));
|
|
185
|
+
|
|
186
|
+
watch(
|
|
187
|
+
() => props.topBarNavigation,
|
|
188
|
+
() => remeasureTopBar(),
|
|
189
|
+
{ deep: true },
|
|
168
190
|
);
|
|
169
191
|
|
|
170
192
|
const showBreadcrumbsBar = computed(
|
|
@@ -89,6 +89,14 @@ const props = defineProps({
|
|
|
89
89
|
type: String,
|
|
90
90
|
default: 'status',
|
|
91
91
|
},
|
|
92
|
+
/**
|
|
93
|
+
* Nest the block directly under a control (flat top edge, shared width).
|
|
94
|
+
* Pair with an input, select, or textarea immediately above.
|
|
95
|
+
*/
|
|
96
|
+
attachedBelow: {
|
|
97
|
+
type: Boolean,
|
|
98
|
+
default: false,
|
|
99
|
+
},
|
|
92
100
|
})
|
|
93
101
|
|
|
94
102
|
const emit = defineEmits(['click', 'dismiss'])
|
|
@@ -108,6 +116,7 @@ const hasLink = computed(() => Boolean(
|
|
|
108
116
|
const rootClasses = computed(() => ({
|
|
109
117
|
notification: true,
|
|
110
118
|
[props.type]: true,
|
|
119
|
+
'attached-below': props.attachedBelow,
|
|
111
120
|
clickable: props.clickable || props.dismissible,
|
|
112
121
|
}))
|
|
113
122
|
|
|
@@ -118,3 +127,19 @@ function handleClick(event) {
|
|
|
118
127
|
}
|
|
119
128
|
}
|
|
120
129
|
</script>
|
|
130
|
+
|
|
131
|
+
<style scoped>
|
|
132
|
+
.notification.attached-below {
|
|
133
|
+
box-sizing: border-box;
|
|
134
|
+
width: 100%;
|
|
135
|
+
margin: 0;
|
|
136
|
+
border-top: none;
|
|
137
|
+
border-radius: 0 0 0.4em 0.4em;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.notification.attached-below.bad,
|
|
141
|
+
.notification.attached-below.error,
|
|
142
|
+
.notification.attached-below.critical {
|
|
143
|
+
border-color: var(--karma-bad-border);
|
|
144
|
+
}
|
|
145
|
+
</style>
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import { h, nextTick, ref } from 'vue'
|
|
4
|
+
import ReorderList from './ReorderList.vue'
|
|
5
|
+
import ReorderListExample from '../examples/ReorderListExample.vue'
|
|
6
|
+
|
|
7
|
+
const globalStubs = {
|
|
8
|
+
HugeiconsIcon: true,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const rows = [
|
|
12
|
+
{ id: 'a', title: 'Alpha' },
|
|
13
|
+
{ id: 'b', title: 'Beta' },
|
|
14
|
+
{ id: 'c', title: 'Gamma' },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
function mountList(props = {}, slots = {}) {
|
|
18
|
+
const wrapper = mount(ReorderList, {
|
|
19
|
+
props: {
|
|
20
|
+
modelValue: rows.map((row) => ({ ...row })),
|
|
21
|
+
ariaLabel: 'Queue',
|
|
22
|
+
...props,
|
|
23
|
+
},
|
|
24
|
+
slots: {
|
|
25
|
+
item: ({ item }) => h('span', { class: 'row-label' }, item.title),
|
|
26
|
+
...slots,
|
|
27
|
+
},
|
|
28
|
+
global: { stubs: globalStubs },
|
|
29
|
+
})
|
|
30
|
+
return wrapper
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function dataTransfer() {
|
|
34
|
+
return {
|
|
35
|
+
effectAllowed: '',
|
|
36
|
+
dropEffect: '',
|
|
37
|
+
setData: () => {},
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('ReorderList', () => {
|
|
42
|
+
it('renders row slot content in model order', () => {
|
|
43
|
+
const wrapper = mountList()
|
|
44
|
+
const labels = wrapper.findAll('.row-label').map((node) => node.text())
|
|
45
|
+
|
|
46
|
+
expect(labels).toEqual(['Alpha', 'Beta', 'Gamma'])
|
|
47
|
+
expect(wrapper.attributes('aria-label')).toBe('Queue')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('moves a row down and up with the buttons', async () => {
|
|
51
|
+
const wrapper = mountList({ showMoveButtons: true })
|
|
52
|
+
|
|
53
|
+
await wrapper.find('[aria-label="Move Alpha down"]').trigger('click')
|
|
54
|
+
expect(wrapper.emitted('update:modelValue')[0][0].map((item) => item.id)).toEqual(['b', 'a', 'c'])
|
|
55
|
+
expect(wrapper.emitted('reorder')[0][0]).toMatchObject({ fromIndex: 0, toIndex: 1 })
|
|
56
|
+
|
|
57
|
+
await wrapper.setProps({ modelValue: wrapper.emitted('update:modelValue')[0][0] })
|
|
58
|
+
await wrapper.find('[aria-label="Move Alpha up"]').trigger('click')
|
|
59
|
+
const emitted = wrapper.emitted('update:modelValue')
|
|
60
|
+
expect(emitted[emitted.length - 1][0].map((item) => item.id)).toEqual(['a', 'b', 'c'])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('disables moves past the ends of the list', () => {
|
|
64
|
+
const wrapper = mountList({ showMoveButtons: true })
|
|
65
|
+
|
|
66
|
+
expect(wrapper.find('[aria-label="Move Alpha up"]').attributes('disabled')).toBeDefined()
|
|
67
|
+
expect(wrapper.find('[aria-label="Move Gamma down"]').attributes('disabled')).toBeDefined()
|
|
68
|
+
expect(wrapper.find('[aria-label="Move Beta up"]').attributes('disabled')).toBeUndefined()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
function layoutRows(wrapper) {
|
|
72
|
+
const list = wrapper.element?.classList?.contains('reorder-list')
|
|
73
|
+
? wrapper
|
|
74
|
+
: wrapper.get('.reorder-list')
|
|
75
|
+
const rowEls = [...list.element.querySelectorAll(':scope > li.reorder-list-row')]
|
|
76
|
+
rowEls.forEach((rowEl, index) => {
|
|
77
|
+
const top = index * 50
|
|
78
|
+
rowEl.getBoundingClientRect = () => ({
|
|
79
|
+
top,
|
|
80
|
+
bottom: top + 40,
|
|
81
|
+
height: 40,
|
|
82
|
+
left: 0,
|
|
83
|
+
right: 200,
|
|
84
|
+
width: 200,
|
|
85
|
+
x: 0,
|
|
86
|
+
y: top,
|
|
87
|
+
toJSON() { return {} },
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
list.element.getBoundingClientRect = () => ({
|
|
91
|
+
top: 0,
|
|
92
|
+
bottom: rowEls.length * 50,
|
|
93
|
+
height: rowEls.length * 50,
|
|
94
|
+
left: 0,
|
|
95
|
+
right: 200,
|
|
96
|
+
width: 200,
|
|
97
|
+
x: 0,
|
|
98
|
+
y: 0,
|
|
99
|
+
toJSON() { return {} },
|
|
100
|
+
})
|
|
101
|
+
return list
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
it('shows a separator in the gap between rows and inserts there', async () => {
|
|
105
|
+
const wrapper = mountList()
|
|
106
|
+
const list = layoutRows(wrapper)
|
|
107
|
+
const handles = wrapper.findAll('.reorder-list-handle')
|
|
108
|
+
|
|
109
|
+
await handles[0].trigger('dragstart', { dataTransfer: dataTransfer() })
|
|
110
|
+
expect(wrapper.find('.reorder-list-row').classes()).toContain('is-dragging')
|
|
111
|
+
|
|
112
|
+
// Lower half of Beta: the gap between Beta and Gamma.
|
|
113
|
+
await list.trigger('dragover', { clientY: 80, dataTransfer: dataTransfer() })
|
|
114
|
+
const separator = wrapper.get('.reorder-list-separator')
|
|
115
|
+
expect(separator.attributes('data-drop-gap')).toBe('2')
|
|
116
|
+
expect(wrapper.find('.is-drag-over').exists()).toBe(false)
|
|
117
|
+
|
|
118
|
+
await list.trigger('drop', { clientY: 80 })
|
|
119
|
+
expect(wrapper.emitted('update:modelValue')[0][0].map((item) => item.id)).toEqual(['b', 'a', 'c'])
|
|
120
|
+
expect(wrapper.emitted('reorder')[0][0]).toMatchObject({ fromIndex: 0, toIndex: 1 })
|
|
121
|
+
expect(wrapper.find('.reorder-list-separator').exists()).toBe(false)
|
|
122
|
+
expect(wrapper.find('.reorder-list-row.is-dragging').exists()).toBe(false)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('places the separator after the last row when the pointer is below it', async () => {
|
|
126
|
+
const wrapper = mountList()
|
|
127
|
+
const list = layoutRows(wrapper)
|
|
128
|
+
|
|
129
|
+
await wrapper.findAll('.reorder-list-handle')[0].trigger('dragstart', { dataTransfer: dataTransfer() })
|
|
130
|
+
await list.trigger('dragover', { clientY: 140, dataTransfer: dataTransfer() })
|
|
131
|
+
|
|
132
|
+
expect(wrapper.get('.reorder-list-separator').attributes('data-drop-gap')).toBe('3')
|
|
133
|
+
await list.trigger('drop', { clientY: 140 })
|
|
134
|
+
expect(wrapper.emitted('update:modelValue')[0][0].map((item) => item.id)).toEqual(['b', 'c', 'a'])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('places the separator before the first row when dragging upward', async () => {
|
|
138
|
+
const wrapper = mountList()
|
|
139
|
+
const list = layoutRows(wrapper)
|
|
140
|
+
|
|
141
|
+
await wrapper.findAll('.reorder-list-handle')[2].trigger('dragstart', { dataTransfer: dataTransfer() })
|
|
142
|
+
await list.trigger('dragover', { clientY: 10, dataTransfer: dataTransfer() })
|
|
143
|
+
|
|
144
|
+
expect(wrapper.get('.reorder-list-separator').attributes('data-drop-gap')).toBe('0')
|
|
145
|
+
await list.trigger('drop', { clientY: 10 })
|
|
146
|
+
expect(wrapper.emitted('update:modelValue')[0][0].map((item) => item.id)).toEqual(['c', 'a', 'b'])
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('hides the separator when the gap would not move the row', async () => {
|
|
150
|
+
const wrapper = mountList()
|
|
151
|
+
const list = layoutRows(wrapper)
|
|
152
|
+
|
|
153
|
+
await wrapper.findAll('.reorder-list-handle')[0].trigger('dragstart', { dataTransfer: dataTransfer() })
|
|
154
|
+
await list.trigger('dragover', { clientY: 30, dataTransfer: dataTransfer() })
|
|
155
|
+
|
|
156
|
+
expect(wrapper.find('.reorder-list-separator').exists()).toBe(false)
|
|
157
|
+
await list.trigger('drop', { clientY: 30 })
|
|
158
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('does not reorder when disabled', async () => {
|
|
162
|
+
const wrapper = mountList({ disabled: true })
|
|
163
|
+
|
|
164
|
+
await wrapper.findAll('.reorder-list-handle')[0].trigger('dragstart', { dataTransfer: dataTransfer() })
|
|
165
|
+
await wrapper.findAll('.reorder-list-row')[2].trigger('drop')
|
|
166
|
+
|
|
167
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
168
|
+
expect(wrapper.classes()).toContain('is-disabled')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('moves with the arrow keys when move buttons are hidden', async () => {
|
|
172
|
+
const wrapper = mountList()
|
|
173
|
+
const handle = wrapper.findAll('.reorder-list-handle')[0]
|
|
174
|
+
|
|
175
|
+
expect(handle.attributes('tabindex')).toBe('0')
|
|
176
|
+
await handle.trigger('keydown', { key: 'ArrowDown' })
|
|
177
|
+
await nextTick()
|
|
178
|
+
|
|
179
|
+
expect(wrapper.emitted('update:modelValue')[0][0].map((item) => item.id)).toEqual(['b', 'a', 'c'])
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('renders the empty slot', () => {
|
|
183
|
+
const wrapper = mountList(
|
|
184
|
+
{ modelValue: [] },
|
|
185
|
+
{ empty: () => h('span', { class: 'empty' }, 'No rows') },
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
expect(wrapper.find('.empty').text()).toBe('No rows')
|
|
189
|
+
expect(wrapper.find('.reorder-list-row').exists()).toBe(false)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('hides move buttons and drag handles independently', () => {
|
|
193
|
+
const buttonsOnly = mountList({ showMoveButtons: true, showDragHandles: false })
|
|
194
|
+
expect(buttonsOnly.find('.reorder-list-handle').exists()).toBe(false)
|
|
195
|
+
expect(buttonsOnly.find('.reorder-list-move').exists()).toBe(true)
|
|
196
|
+
|
|
197
|
+
const handlesOnly = mountList()
|
|
198
|
+
expect(handlesOnly.find('.reorder-list-move').exists()).toBe(false)
|
|
199
|
+
expect(handlesOnly.find('.reorder-list-handle').exists()).toBe(true)
|
|
200
|
+
|
|
201
|
+
const neither = mountList({ showDragHandles: false })
|
|
202
|
+
expect(neither.find('.reorder-list-controls').exists()).toBe(false)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('drags from the whole row when dragWholeRow is enabled', async () => {
|
|
206
|
+
const wrapper = mountList({ dragWholeRow: true, showDragHandles: true })
|
|
207
|
+
const list = layoutRows(wrapper)
|
|
208
|
+
|
|
209
|
+
expect(wrapper.find('.reorder-list-handle').exists()).toBe(false)
|
|
210
|
+
expect(wrapper.find('.reorder-list-row.is-drag-whole-row').exists()).toBe(true)
|
|
211
|
+
|
|
212
|
+
await wrapper.findAll('.reorder-list-row')[0].trigger('dragstart', { dataTransfer: dataTransfer() })
|
|
213
|
+
await list.trigger('dragover', { clientY: 80, dataTransfer: dataTransfer() })
|
|
214
|
+
await list.trigger('drop', { clientY: 80 })
|
|
215
|
+
|
|
216
|
+
expect(wrapper.emitted('update:modelValue')[0][0].map((item) => item.id)).toEqual(['b', 'a', 'c'])
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('does not start a whole-row drag from move buttons', async () => {
|
|
220
|
+
const wrapper = mountList({ dragWholeRow: true, showMoveButtons: true })
|
|
221
|
+
await wrapper.find('[aria-label="Move Alpha down"]').trigger('dragstart', { dataTransfer: dataTransfer() })
|
|
222
|
+
expect(wrapper.find('.reorder-list-row.is-dragging').exists()).toBe(false)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('does not show drop indicators on nested lists while dragging the parent', async () => {
|
|
226
|
+
const groups = ref([
|
|
227
|
+
{
|
|
228
|
+
id: 'g1',
|
|
229
|
+
title: 'Group 1',
|
|
230
|
+
children: [{ id: 'c1', label: 'Step 1' }, { id: 'c2', label: 'Step 2' }],
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: 'g2',
|
|
234
|
+
title: 'Group 2',
|
|
235
|
+
children: [{ id: 'c3', label: 'Step 3' }],
|
|
236
|
+
},
|
|
237
|
+
])
|
|
238
|
+
const wrapper = mount({
|
|
239
|
+
components: { ReorderList },
|
|
240
|
+
setup() {
|
|
241
|
+
return { groups }
|
|
242
|
+
},
|
|
243
|
+
template: `
|
|
244
|
+
<ReorderList v-model="groups" item-key="id" aria-label="Outer">
|
|
245
|
+
<template #item="{ item }">
|
|
246
|
+
<div class="nested-slot">
|
|
247
|
+
<strong>{{ item.title }}</strong>
|
|
248
|
+
<ReorderList
|
|
249
|
+
v-model="item.children"
|
|
250
|
+
item-key="id"
|
|
251
|
+
aria-label="Inner steps"
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
254
|
+
</template>
|
|
255
|
+
</ReorderList>
|
|
256
|
+
`,
|
|
257
|
+
global: { stubs: globalStubs },
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const lists = wrapper.findAll('.reorder-list')
|
|
261
|
+
const outer = lists[0]
|
|
262
|
+
const innerLists = lists.slice(1)
|
|
263
|
+
|
|
264
|
+
layoutRows(outer)
|
|
265
|
+
await outer.find('.reorder-list-handle').trigger('dragstart', { dataTransfer: dataTransfer() })
|
|
266
|
+
await innerLists[0].trigger('dragover', { clientY: 120, dataTransfer: dataTransfer() })
|
|
267
|
+
|
|
268
|
+
expect(innerLists[0].find('.reorder-list-separator').exists()).toBe(false)
|
|
269
|
+
expect(innerLists[1].find('.reorder-list-separator').exists()).toBe(false)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('uses a custom item key function', async () => {
|
|
273
|
+
const wrapper = mountList({
|
|
274
|
+
modelValue: [{ slug: 'one' }, { slug: 'two' }],
|
|
275
|
+
itemKey: (item) => item.slug,
|
|
276
|
+
itemLabel: (item) => item.slug,
|
|
277
|
+
showMoveButtons: true,
|
|
278
|
+
}, {
|
|
279
|
+
item: ({ item }) => h('span', { class: 'row-label' }, item.slug),
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
await wrapper.find('[aria-label="Move one down"]').trigger('click')
|
|
283
|
+
expect(wrapper.emitted('update:modelValue')[0][0].map((item) => item.slug)).toEqual(['two', 'one'])
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
describe('ReorderList example', () => {
|
|
288
|
+
it('toggles move buttons and drag handles from the section toolbar', async () => {
|
|
289
|
+
const wrapper = mount(ReorderListExample, {
|
|
290
|
+
global: { stubs: globalStubs },
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
const checklist = wrapper.findAll('section').find((section) => (
|
|
294
|
+
section.find('.reorder-example-toggle').exists()
|
|
295
|
+
))
|
|
296
|
+
const moveButtons = checklist.get('input[type="checkbox"]')
|
|
297
|
+
const dragHandles = checklist.findAll('input[type="checkbox"]')[1]
|
|
298
|
+
|
|
299
|
+
expect(checklist.find('.reorder-list-move').exists()).toBe(false)
|
|
300
|
+
expect(checklist.find('.reorder-list-handle').exists()).toBe(true)
|
|
301
|
+
|
|
302
|
+
await moveButtons.setValue(true)
|
|
303
|
+
expect(checklist.find('.reorder-list-move').exists()).toBe(true)
|
|
304
|
+
expect(checklist.find('.reorder-list-handle').exists()).toBe(true)
|
|
305
|
+
|
|
306
|
+
await dragHandles.setValue(false)
|
|
307
|
+
expect(checklist.find('.reorder-list-handle').exists()).toBe(false)
|
|
308
|
+
expect(checklist.find('.reorder-list-move').exists()).toBe(true)
|
|
309
|
+
|
|
310
|
+
await moveButtons.setValue(false)
|
|
311
|
+
expect(checklist.find('.reorder-list-controls').exists()).toBe(false)
|
|
312
|
+
|
|
313
|
+
await dragHandles.setValue(true)
|
|
314
|
+
await moveButtons.setValue(false)
|
|
315
|
+
expect(checklist.find('.reorder-list-move').exists()).toBe(false)
|
|
316
|
+
expect(checklist.find('.reorder-list-handle').exists()).toBe(true)
|
|
317
|
+
})
|
|
318
|
+
})
|