Skip to content

Commit 7a79d48

Browse files
authored
Merge pull request #1477 from privy-open-source/feat/table-sortable-field
Feat/table sortable field
2 parents 03dd93d + 350424a commit 7a79d48

File tree

12 files changed

+874
-29
lines changed

12 files changed

+874
-29
lines changed

src/components/dropzone/Dropzone.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,10 @@ defineSlots<{
213213

214214
<style lang="postcss">
215215
.dropzone {
216-
@apply cursor-pointer block w-full;
216+
@apply cursor-pointer block w-full relative;
217217
218218
&__input {
219-
@apply hidden;
219+
@apply absolute w-[.1px] h-[.1px] opacity-0 overflow-hidden -z-1 bottom-0 right-0;
220220
}
221221
222222
* {

src/components/table-static/TableStatic.spec.ts

Lines changed: 171 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import Draggable from '../table/__mocks__/vuedraggable'
22
import { vi } from 'vitest'
3-
import { fireEvent, render } from '@testing-library/vue'
4-
import { ref } from 'vue-demi'
3+
import {
4+
render,
5+
fireEvent,
6+
queryByTestId,
7+
} from '@testing-library/vue'
8+
import { nextTick, ref } from 'vue-demi'
59
import { defineTable } from '../table'
610
import Table from './TableStatic.vue'
711

@@ -37,6 +41,54 @@ const items = ref([
3741
},
3842
])
3943

44+
const sortableFields = defineTable([
45+
{ key: 'id' },
46+
{ key: 'name' },
47+
{
48+
key : 'gender',
49+
sortable: false,
50+
},
51+
{
52+
key : 'age',
53+
sortable: true,
54+
},
55+
])
56+
57+
function generateItems () {
58+
return [
59+
{
60+
id : 1,
61+
name : 'Dora',
62+
gender: 'male',
63+
age : 27,
64+
},
65+
{
66+
id : 2,
67+
name : 'Emilly',
68+
gender: 'male',
69+
age : 20,
70+
},
71+
{
72+
id : 3,
73+
name : 'Jane',
74+
gender: 'female',
75+
age : 30,
76+
},
77+
{
78+
id : 4,
79+
name : 'Andi',
80+
gender: 'male',
81+
age : 21,
82+
},
83+
{
84+
id : 5,
85+
name : 'Bella',
86+
gender: 'female',
87+
age : 24,
88+
},
89+
]
90+
}
91+
4092
it('should render properly', () => {
4193
const screen = render({
4294
components: { Table },
@@ -305,3 +357,120 @@ it('should able to select all items (except disable one) in variant static', asy
305357

306358
expect(selected.value).toHaveLength(0)
307359
})
360+
361+
it('should X field header have sortable class if have `sortable` property with `true` value & `sortable` prop is provided', () => {
362+
const items = ref(generateItems())
363+
const screen = render({
364+
components: { Table },
365+
template : `
366+
<Table
367+
:fields="sortableFields"
368+
:items="items"
369+
sortable
370+
/>`,
371+
setup () {
372+
return {
373+
sortableFields,
374+
items,
375+
}
376+
},
377+
})
378+
379+
const heads = screen.queryAllByTestId('table-static-header')
380+
381+
expect(heads.at(0)).toHaveClass('table-static__header--sortable')
382+
expect(heads.at(2)).not.toHaveClass('table-static__header--sortable')
383+
})
384+
385+
it('should able modify sort by header using v-model:sort-by', async () => {
386+
const items = ref(generateItems())
387+
const sortBy = ref({})
388+
const screen = render({
389+
components: { Table },
390+
template : `
391+
<Table
392+
v-model:sort-by="sortBy"
393+
:fields="sortableFields"
394+
:items="items"
395+
sortable
396+
/>`,
397+
setup () {
398+
return {
399+
sortableFields,
400+
items,
401+
sortBy,
402+
}
403+
},
404+
})
405+
406+
const heads = screen.queryAllByTestId('table-static-header')
407+
408+
expect(heads.at(0)).toHaveClass('table-static__header--sortable')
409+
expect(heads.at(2)).not.toHaveClass('table-static__header--sortable')
410+
411+
await fireEvent.click(heads.at(0))
412+
413+
const icon = queryByTestId(heads[0], 'table-static-header-sort')
414+
415+
expect(icon).toHaveAttribute('active', 'asc')
416+
expect(sortBy.value).toStrictEqual({ id: 'asc' })
417+
418+
await fireEvent.click(heads.at(0))
419+
420+
expect(icon).toHaveAttribute('active', 'desc')
421+
expect(sortBy.value).toStrictEqual({ id: 'desc' })
422+
423+
await fireEvent.click(heads.at(0))
424+
425+
expect(icon).not.toHaveAttribute('active', 'desc')
426+
expect(sortBy.value).toStrictEqual({ id: undefined })
427+
428+
sortBy.value = { name: 'desc' }
429+
await nextTick()
430+
431+
const icon2 = queryByTestId(heads[1], 'table-static-header-sort')
432+
433+
expect(icon).not.toHaveAttribute('active', 'asc')
434+
expect(icon2).toHaveAttribute('active', 'desc')
435+
})
436+
437+
it('should have multiple value if sortable set to `multiple`', async () => {
438+
const items = ref(generateItems())
439+
const sortBy = ref({})
440+
const screen = render({
441+
components: { Table },
442+
template : `
443+
<Table
444+
v-model:sort-by="sortBy"
445+
:fields="sortableFields"
446+
:items="items"
447+
sortable="multiple"
448+
/>`,
449+
setup () {
450+
return {
451+
sortableFields,
452+
items,
453+
sortBy,
454+
}
455+
},
456+
})
457+
458+
const heads = screen.queryAllByTestId('table-static-header')
459+
460+
expect(heads.at(0)).toHaveClass('table-static__header--sortable')
461+
expect(heads.at(2)).not.toHaveClass('table-static__header--sortable')
462+
463+
await fireEvent.click(heads.at(0))
464+
465+
const icon = queryByTestId(heads[0], 'table-static-header-sort')
466+
const icon2 = queryByTestId(heads[1], 'table-static-header-sort')
467+
468+
expect(icon).toHaveAttribute('active', 'asc')
469+
expect(sortBy.value).toStrictEqual({ id: 'asc' })
470+
471+
await fireEvent.click(heads.at(1))
472+
473+
expect(icon).toHaveAttribute('active', 'asc')
474+
expect(icon2).toHaveAttribute('active', 'asc')
475+
expect(sortBy.value).toStrictEqual({ id: 'asc', name: 'asc' })
476+
})

src/components/table-static/TableStatic.vue

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,20 @@
2525
class="table-static__header"
2626
data-testid="table-static-header"
2727
:style="field.width ? { width: withUnit(field.width) } : {}"
28-
:class="field.thClass"
29-
:data-header="field.key">
28+
:class="[field.thClass, { 'table-static__header--sortable': isSortable(field) }]"
29+
:data-header="field.key"
30+
@click="setSortField(field)">
3031
<slot
3132
:name="`head(${field.key})`"
3233
:label="field.label"
33-
:field="field">
34-
{{ field.label }}
34+
:field="field"
35+
:sort="getSortField(field)">
36+
<span class="table-static__header-label">
37+
{{ field.label }}
38+
<TableStaticSort
39+
v-if="isSortable(field)"
40+
:active="getSortField(field)" />
41+
</span>
3542
</slot>
3643
</th>
3744
</tr>
@@ -113,6 +120,8 @@ import type {
113120
VNode,
114121
} from 'vue-demi'
115122
import {
123+
ref,
124+
watch,
116125
computed,
117126
} from 'vue-demi'
118127
import type {
@@ -130,6 +139,8 @@ import TableStaticRoot from './TableStaticRoot.vue'
130139
import { useVModel } from '../input'
131140
import IconDrag from '@privyid/persona-icon/vue/draggable/20.vue'
132141
import Draggable from 'vuedraggable'
142+
import type { SortValue } from '.'
143+
import TableStaticSort from './TableStaticSort.vue'
133144
134145
const props = defineProps({
135146
apperance: {
@@ -184,12 +195,23 @@ const props = defineProps({
184195
type : Boolean,
185196
default: true,
186197
},
198+
sortable: {
199+
type : [Boolean, String] as PropType<boolean | 'single' | 'multiple'>,
200+
default: false,
201+
},
202+
sortBy: {
203+
type : Object as PropType<Record<string, any>>,
204+
default: () => ({}),
205+
},
187206
})
188207
189-
const model = useVModel(props)
190-
const emit = defineEmits<{
208+
const model = useVModel(props)
209+
const localSortBy = ref(props.sortBy)
210+
const emit = defineEmits<{
191211
'update:modelValue': [T[]],
192212
'update:items': [T[]],
213+
'update:sortBy': [Record<string, SortValue>],
214+
'sort': [Record<string, SortValue>],
193215
}>()
194216
195217
const rows = computed<T[]>({
@@ -249,15 +271,43 @@ const indeterminate = computed(() => {
249271
&& model.value.length < selectableRows.value.length
250272
})
251273
274+
watch(() => props.sortBy, (value) => {
275+
localSortBy.value = value
276+
})
277+
278+
function isSortable (field: TableField<T>) {
279+
return props.sortable && field.sortable !== false
280+
}
281+
282+
function getSortField (field: TableField<T>): SortValue {
283+
return localSortBy.value[field.key as string]
284+
}
285+
286+
function setSortField (field: TableField<T>) {
287+
if (isSortable(field)) {
288+
const oldValue: SortValue = getSortField(field)
289+
const newValue: SortValue = oldValue === 'asc' ? 'desc' : (oldValue === 'desc' ? undefined : 'asc')
290+
291+
const value = props.sortable === 'multiple'
292+
? { ...localSortBy.value, [field.key]: newValue }
293+
: { [field.key]: newValue }
294+
295+
localSortBy.value = value
296+
297+
emit('update:sortBy', value)
298+
emit('sort', value)
299+
}
300+
}
301+
252302
defineSlots<{
253303
'empty'(): VNode[],
254304
'row'(props: { index: number, item: T }): VNode[],
255305
[K: `cell(${string})`]:(props: { index: number }) => VNode[],
256-
[K: `head(${string})`]:(props: { field: TableField<T>, label: string }) => VNode[],
306+
[K: `head(${string})`]:(props: { field: TableField<T>, label: string, sort?: 'asc' | 'desc' }) => VNode[],
257307
} & {
258308
[K in KeyType<T> as `cell(${K})`]:(props: { item: T, index: number }) => VNode[]
259309
} & {
260-
[K in KeyType<T> as `head(${K})`]:(props: { field: TableField<T>, label: string }) => VNode[]
310+
[K in KeyType<T> as `head(${K})`]:(props: { field: TableField<T>, label: string, sort?: 'asc' | 'desc' }) => VNode[]
261311
}>()
262312
</script>
263313

@@ -303,10 +353,42 @@ defineSlots<{
303353
.table-static__header {
304354
@apply px-3 text-xs border-x-0;
305355
356+
&--sortable {
357+
@apply cursor-pointer select-none;
358+
}
359+
306360
&.table-static__drag,
307361
&.table-static__checkbox {
308362
@apply w-[1%];
309363
}
364+
365+
&-label {
366+
@apply inline-flex items-center;
367+
}
368+
369+
&-sort {
370+
@apply flex flex-col flex-shrink-0 items-center justify-center ml-1;
371+
@apply text-muted;
372+
@apply dark:text-dark-muted;
373+
374+
&[active="asc"] {
375+
> .table-static__header-sort-down {
376+
@apply text-default;
377+
@apply dark:text-dark-default;
378+
}
379+
}
380+
381+
&[active="desc"] {
382+
> .table-static__header-sort-up {
383+
@apply text-default;
384+
@apply dark:text-dark-default;
385+
}
386+
}
387+
}
388+
389+
&-sort-down {
390+
@apply -mt-2;
391+
}
310392
}
311393
312394
+ .table-static__body {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<template>
2+
<div
3+
:active="active"
4+
class="table-static__header-sort"
5+
data-testid="table-static-header-sort">
6+
<IconUp
7+
class="table-static__header-sort-up" />
8+
<IconDown
9+
class="table-static__header-sort-down" />
10+
</div>
11+
</template>
12+
13+
<script lang="ts" setup>
14+
import IconUp from '@privyid/persona-icon/vue/caret-up/16.vue'
15+
import IconDown from '@privyid/persona-icon/vue/caret-down/16.vue'
16+
17+
defineProps({
18+
active: {
19+
type : String,
20+
default: undefined,
21+
},
22+
})
23+
</script>

0 commit comments

Comments
 (0)