picocrank 1.32.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/docs/table.md +99 -0
- 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 +490 -1
- package/vue/components/Table.vue +1038 -111
- 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/tableFilters.js +6 -1
- 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
|
@@ -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
|
+
})
|