@nexxtmove/ui 0.1.29 → 0.1.31
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/dist/index.d.ts +39 -8
- package/dist/index.js +1193 -958
- package/dist/nuxt.js +28 -25
- package/package.json +1 -1
- package/src/components/Avatar/Avatar.stories.ts +28 -0
- package/src/components/Avatar/Avatar.test.ts +55 -0
- package/src/components/Avatar/Avatar.vue +49 -0
- package/src/components/Checkbox/Checkbox.stories.ts +35 -0
- package/src/components/Checkbox/Checkbox.test.ts +55 -0
- package/src/components/Checkbox/Checkbox.vue +46 -0
- package/src/components/Table/Table.stories.ts +283 -0
- package/src/components/Table/Table.test.ts +220 -0
- package/src/components/Table/Table.vue +227 -0
- package/src/components.json +3 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import Table from './Table.vue'
|
|
4
|
+
|
|
5
|
+
const columns = [
|
|
6
|
+
{ key: 'name', label: 'Name', sortable: true },
|
|
7
|
+
{ key: 'email', label: 'Email' },
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
const rows = [
|
|
11
|
+
{ name: 'Alice', email: 'alice@example.com' },
|
|
12
|
+
{ name: 'Bob', email: 'bob@example.com' },
|
|
13
|
+
{ name: 'Carol', email: 'carol@example.com' },
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
describe('Table', () => {
|
|
17
|
+
describe('rendering', () => {
|
|
18
|
+
it('renders column headers', () => {
|
|
19
|
+
const wrapper = mount(Table, { props: { columns, rows } })
|
|
20
|
+
expect(wrapper.text()).toContain('Name')
|
|
21
|
+
expect(wrapper.text()).toContain('Email')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('renders row data', () => {
|
|
25
|
+
const wrapper = mount(Table, { props: { columns, rows } })
|
|
26
|
+
expect(wrapper.text()).toContain('Alice')
|
|
27
|
+
expect(wrapper.text()).toContain('bob@example.com')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('renders empty slot when rows is empty', () => {
|
|
31
|
+
const wrapper = mount(Table, { props: { columns, rows: [] } })
|
|
32
|
+
expect(wrapper.text()).toContain('Geen data beschikbaar')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('renders custom empty slot content', () => {
|
|
36
|
+
const wrapper = mount(Table, {
|
|
37
|
+
props: { columns, rows: [] },
|
|
38
|
+
slots: { empty: 'Nothing here' },
|
|
39
|
+
})
|
|
40
|
+
expect(wrapper.text()).toContain('Nothing here')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('renders cell slot content', () => {
|
|
44
|
+
const wrapper = mount(Table, {
|
|
45
|
+
props: { columns, rows },
|
|
46
|
+
slots: { 'cell-name': '<strong>custom</strong>' },
|
|
47
|
+
})
|
|
48
|
+
expect(wrapper.html()).toContain('<strong>custom</strong>')
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('sorting', () => {
|
|
53
|
+
it('renders sort icon on sortable columns', () => {
|
|
54
|
+
const wrapper = mount(Table, { props: { columns, rows } })
|
|
55
|
+
const headers = wrapper.findAll('[role="columnheader"]')
|
|
56
|
+
const nameHeader = headers[0]
|
|
57
|
+
expect(nameHeader.find('i').exists()).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('does not render sort icon on non-sortable columns', () => {
|
|
61
|
+
const wrapper = mount(Table, { props: { columns, rows } })
|
|
62
|
+
const headers = wrapper.findAll('[role="columnheader"]')
|
|
63
|
+
const emailHeader = headers[1]
|
|
64
|
+
expect(emailHeader.find('i').exists()).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('emits sort event when clicking a sortable header', async () => {
|
|
68
|
+
const wrapper = mount(Table, { props: { columns, rows } })
|
|
69
|
+
const nameHeader = wrapper.findAll('[role="columnheader"]')[0]
|
|
70
|
+
await nameHeader.trigger('click')
|
|
71
|
+
expect(wrapper.emitted('sort')).toBeTruthy()
|
|
72
|
+
expect(wrapper.emitted('sort')![0]).toEqual(['name', 'asc'])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('emits desc direction when clicking an already sorted column', async () => {
|
|
76
|
+
const wrapper = mount(Table, {
|
|
77
|
+
props: { columns, rows, sortKey: 'name', sortDirection: 'asc' },
|
|
78
|
+
})
|
|
79
|
+
const nameHeader = wrapper.findAll('[role="columnheader"]')[0]
|
|
80
|
+
await nameHeader.trigger('click')
|
|
81
|
+
expect(wrapper.emitted('sort')![0]).toEqual(['name', 'desc'])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('does not emit sort when clicking a non-sortable header', async () => {
|
|
85
|
+
const wrapper = mount(Table, { props: { columns, rows } })
|
|
86
|
+
const emailHeader = wrapper.findAll('[role="columnheader"]')[1]
|
|
87
|
+
await emailHeader.trigger('click')
|
|
88
|
+
expect(wrapper.emitted('sort')).toBeFalsy()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('sets aria-sort to ascending on the active sort column', () => {
|
|
92
|
+
const wrapper = mount(Table, {
|
|
93
|
+
props: { columns, rows, sortKey: 'name', sortDirection: 'asc' },
|
|
94
|
+
})
|
|
95
|
+
const nameHeader = wrapper.findAll('[role="columnheader"]')[0]
|
|
96
|
+
expect(nameHeader.attributes('aria-sort')).toBe('ascending')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('sets aria-sort to descending on the active sort column', () => {
|
|
100
|
+
const wrapper = mount(Table, {
|
|
101
|
+
props: { columns, rows, sortKey: 'name', sortDirection: 'desc' },
|
|
102
|
+
})
|
|
103
|
+
const nameHeader = wrapper.findAll('[role="columnheader"]')[0]
|
|
104
|
+
expect(nameHeader.attributes('aria-sort')).toBe('descending')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('sets aria-sort to none on an inactive sortable column', () => {
|
|
108
|
+
const wrapper = mount(Table, { props: { columns, rows } })
|
|
109
|
+
const nameHeader = wrapper.findAll('[role="columnheader"]')[0]
|
|
110
|
+
expect(nameHeader.attributes('aria-sort')).toBe('none')
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('selection', () => {
|
|
115
|
+
it('does not render checkboxes when selectable is false', () => {
|
|
116
|
+
const wrapper = mount(Table, { props: { columns, rows } })
|
|
117
|
+
expect(wrapper.find('input[type="checkbox"]').exists()).toBe(false)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('renders a checkbox per row and one in the header when selectable', () => {
|
|
121
|
+
const wrapper = mount(Table, {
|
|
122
|
+
props: { columns, rows, selectable: true, modelValue: [] },
|
|
123
|
+
})
|
|
124
|
+
const checkboxes = wrapper.findAll('input[type="checkbox"]')
|
|
125
|
+
expect(checkboxes).toHaveLength(rows.length + 1)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('emits update:modelValue with all indices when select-all is clicked', async () => {
|
|
129
|
+
const wrapper = mount(Table, {
|
|
130
|
+
props: { columns, rows, selectable: true, modelValue: [] },
|
|
131
|
+
})
|
|
132
|
+
const headerCheckbox = wrapper.find('input[type="checkbox"]')
|
|
133
|
+
await headerCheckbox.setValue(true)
|
|
134
|
+
expect(wrapper.emitted('update:modelValue')![0]).toEqual([[0, 1, 2]])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('emits update:modelValue with empty array when deselecting all', async () => {
|
|
138
|
+
const wrapper = mount(Table, {
|
|
139
|
+
props: { columns, rows, selectable: true, modelValue: [0, 1, 2] },
|
|
140
|
+
})
|
|
141
|
+
const headerCheckbox = wrapper.find('input[type="checkbox"]')
|
|
142
|
+
await headerCheckbox.setValue(false)
|
|
143
|
+
expect(wrapper.emitted('update:modelValue')![0]).toEqual([[]])
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('emits update:modelValue when a row checkbox is changed', async () => {
|
|
147
|
+
const wrapper = mount(Table, {
|
|
148
|
+
props: { columns, rows, selectable: true, modelValue: [] },
|
|
149
|
+
})
|
|
150
|
+
const rowCheckboxes = wrapper.findAll('input[type="checkbox"]').slice(1)
|
|
151
|
+
await rowCheckboxes[0].setValue(true)
|
|
152
|
+
expect(wrapper.emitted('update:modelValue')![0]).toEqual([[0]])
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('removes index from selection when an already-selected row checkbox is changed', async () => {
|
|
156
|
+
const wrapper = mount(Table, {
|
|
157
|
+
props: { columns, rows, selectable: true, modelValue: [0, 1] },
|
|
158
|
+
})
|
|
159
|
+
const rowCheckboxes = wrapper.findAll('input[type="checkbox"]').slice(1)
|
|
160
|
+
await rowCheckboxes[0].setValue(false)
|
|
161
|
+
expect(wrapper.emitted('update:modelValue')![0]).toEqual([[1]])
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('row click selection', () => {
|
|
166
|
+
it('selects a row when clicking on it', async () => {
|
|
167
|
+
const wrapper = mount(Table, {
|
|
168
|
+
props: { columns, rows, selectable: true, modelValue: [] },
|
|
169
|
+
})
|
|
170
|
+
const dataRows = wrapper.findAll('[role="row"]').slice(1)
|
|
171
|
+
await dataRows[0].trigger('click')
|
|
172
|
+
expect(wrapper.emitted('update:modelValue')![0]).toEqual([[0]])
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('does not emit selection when clicking a button inside a row', async () => {
|
|
176
|
+
const wrapper = mount(Table, {
|
|
177
|
+
props: { columns, rows, selectable: true, modelValue: [] },
|
|
178
|
+
slots: { 'cell-name': '<button>action</button>' },
|
|
179
|
+
})
|
|
180
|
+
const button = wrapper.find('button')
|
|
181
|
+
await button.trigger('click')
|
|
182
|
+
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('does not emit selection when clicking a link inside a row', async () => {
|
|
186
|
+
const wrapper = mount(Table, {
|
|
187
|
+
props: { columns, rows, selectable: true, modelValue: [] },
|
|
188
|
+
slots: { 'cell-name': '<a href="#">link</a>' },
|
|
189
|
+
})
|
|
190
|
+
const link = wrapper.find('a')
|
|
191
|
+
await link.trigger('click')
|
|
192
|
+
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('does not select when selectable is false', async () => {
|
|
196
|
+
const wrapper = mount(Table, { props: { columns, rows } })
|
|
197
|
+
const dataRows = wrapper.findAll('[role="row"]').slice(1)
|
|
198
|
+
await dataRows[0].trigger('click')
|
|
199
|
+
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
describe('keyboard navigation', () => {
|
|
204
|
+
it('moves focus to next row on ArrowDown', async () => {
|
|
205
|
+
const wrapper = mount(Table, { props: { columns, rows }, attachTo: document.body })
|
|
206
|
+
const dataRows = wrapper.findAll('[role="row"]').slice(1)
|
|
207
|
+
await dataRows[0].trigger('keydown', { key: 'ArrowDown' })
|
|
208
|
+
expect(document.activeElement).toBe(dataRows[1].element)
|
|
209
|
+
wrapper.unmount()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('moves focus to previous row on ArrowUp', async () => {
|
|
213
|
+
const wrapper = mount(Table, { props: { columns, rows }, attachTo: document.body })
|
|
214
|
+
const dataRows = wrapper.findAll('[role="row"]').slice(1)
|
|
215
|
+
await dataRows[1].trigger('keydown', { key: 'ArrowUp' })
|
|
216
|
+
expect(document.activeElement).toBe(dataRows[0].element)
|
|
217
|
+
wrapper.unmount()
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
})
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
<script lang="ts" setup generic="TKey extends string">
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import Icon from '../Icon/Icon.vue'
|
|
4
|
+
import Checkbox from '../Checkbox/Checkbox.vue'
|
|
5
|
+
|
|
6
|
+
export interface TableColumn<K extends string = string> {
|
|
7
|
+
key: K
|
|
8
|
+
label: string
|
|
9
|
+
sortable?: boolean
|
|
10
|
+
width?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface NexxtTableProps {
|
|
14
|
+
columns: TableColumn<TKey>[]
|
|
15
|
+
rows: Record<TKey, unknown>[]
|
|
16
|
+
sortKey?: TKey
|
|
17
|
+
sortDirection?: 'asc' | 'desc'
|
|
18
|
+
selectable?: boolean
|
|
19
|
+
modelValue?: number[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const INTERACTIVE = ['a', 'button', 'input', 'select', 'textarea', 'label']
|
|
23
|
+
|
|
24
|
+
defineOptions({ name: 'NexxtTable' })
|
|
25
|
+
|
|
26
|
+
const props = withDefaults(defineProps<NexxtTableProps>(), {
|
|
27
|
+
sortDirection: 'asc',
|
|
28
|
+
})
|
|
29
|
+
const emit = defineEmits<{
|
|
30
|
+
sort: [key: string, direction: 'asc' | 'desc']
|
|
31
|
+
'update:modelValue': [selected: number[]]
|
|
32
|
+
}>()
|
|
33
|
+
|
|
34
|
+
const selected = computed(() => props.modelValue ?? [])
|
|
35
|
+
|
|
36
|
+
const allSelected = computed(
|
|
37
|
+
() => props.rows.length > 0 && selected.value.length === props.rows.length,
|
|
38
|
+
)
|
|
39
|
+
const someSelected = computed(
|
|
40
|
+
() => selected.value.length > 0 && selected.value.length < props.rows.length,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const toggleAll = () => {
|
|
44
|
+
emit('update:modelValue', allSelected.value ? [] : props.rows.map((_, i) => i))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const toggleRow = (index: number) => {
|
|
48
|
+
const next = selected.value.includes(index)
|
|
49
|
+
? selected.value.filter((i) => i !== index)
|
|
50
|
+
: [...selected.value, index]
|
|
51
|
+
emit('update:modelValue', next)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const columnTemplate = computed(() =>
|
|
55
|
+
props.columns.map((col) => `minmax(100px, ${col.width ?? '1fr'})`).join(' '),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const gridTemplate = computed(() =>
|
|
59
|
+
props.selectable ? `40px ${columnTemplate.value}` : columnTemplate.value,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const handleSort = (column: TableColumn) => {
|
|
63
|
+
if (!column.sortable) return
|
|
64
|
+
const newDirection =
|
|
65
|
+
props.sortKey === column.key && props.sortDirection === 'asc' ? 'desc' : 'asc'
|
|
66
|
+
emit('sort', column.key, newDirection)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const handleRowClick = (event: MouseEvent, rowIndex: number) => {
|
|
70
|
+
if (!props.selectable) return
|
|
71
|
+
const path = event.composedPath() as Element[]
|
|
72
|
+
const isInteractive = path.some(
|
|
73
|
+
(el) => el.tagName && INTERACTIVE.includes(el.tagName.toLowerCase()),
|
|
74
|
+
)
|
|
75
|
+
if (!isInteractive) toggleRow(rowIndex)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const getRowRoundedClass = (rowIndex: number) => {
|
|
79
|
+
if (!props.selectable || !selected.value.includes(rowIndex)) return ''
|
|
80
|
+
const prevSelected = selected.value.includes(rowIndex - 1)
|
|
81
|
+
const nextSelected = selected.value.includes(rowIndex + 1)
|
|
82
|
+
if (!prevSelected && !nextSelected) return 'rounded-md'
|
|
83
|
+
if (!prevSelected) return 'rounded-t-md'
|
|
84
|
+
if (!nextSelected) return 'rounded-b-md'
|
|
85
|
+
return ''
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const rowRefs = ref<HTMLElement[]>([])
|
|
89
|
+
|
|
90
|
+
const handleRowKeydown = (event: KeyboardEvent, rowIndex: number) => {
|
|
91
|
+
if (event.key === 'ArrowDown') {
|
|
92
|
+
event.preventDefault()
|
|
93
|
+
rowRefs.value[Math.min(rowIndex + 1, props.rows.length - 1)]?.focus()
|
|
94
|
+
} else if (event.key === 'ArrowUp') {
|
|
95
|
+
event.preventDefault()
|
|
96
|
+
rowRefs.value[Math.max(rowIndex - 1, 0)]?.focus()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<template>
|
|
102
|
+
<div class="flex flex-col gap-3">
|
|
103
|
+
<div class="overflow-auto">
|
|
104
|
+
<div
|
|
105
|
+
role="grid"
|
|
106
|
+
:aria-colcount="columns.length + (selectable ? 1 : 0)"
|
|
107
|
+
:aria-rowcount="rows.length + 1"
|
|
108
|
+
class="w-full min-w-max"
|
|
109
|
+
>
|
|
110
|
+
<div class="sticky top-0 z-10 mb-0.5">
|
|
111
|
+
<div v-if="$slots.toolbar">
|
|
112
|
+
<slot name="toolbar" />
|
|
113
|
+
</div>
|
|
114
|
+
<div
|
|
115
|
+
class="grid rounded-md bg-gray-50"
|
|
116
|
+
:style="{ gridTemplateColumns: gridTemplate }"
|
|
117
|
+
role="row"
|
|
118
|
+
aria-rowindex="1"
|
|
119
|
+
>
|
|
120
|
+
<div
|
|
121
|
+
v-if="selectable"
|
|
122
|
+
class="flex items-center justify-center px-2 py-3"
|
|
123
|
+
role="columnheader"
|
|
124
|
+
aria-colindex="1"
|
|
125
|
+
>
|
|
126
|
+
<Checkbox
|
|
127
|
+
:model-value="allSelected"
|
|
128
|
+
:indeterminate="someSelected"
|
|
129
|
+
aria-label="Select all rows"
|
|
130
|
+
@update:model-value="toggleAll"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
<div
|
|
134
|
+
v-for="(column, colIndex) in columns"
|
|
135
|
+
:key="column.key"
|
|
136
|
+
class="flex items-center gap-1.5 px-4 py-3 heading-4 text-slate-700"
|
|
137
|
+
:class="{
|
|
138
|
+
'cursor-pointer select-none hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-cornflower-blue-500 focus-visible:outline-none focus-visible:ring-inset':
|
|
139
|
+
column.sortable,
|
|
140
|
+
}"
|
|
141
|
+
role="columnheader"
|
|
142
|
+
:aria-colindex="colIndex + (selectable ? 2 : 1)"
|
|
143
|
+
:aria-sort="
|
|
144
|
+
column.sortable
|
|
145
|
+
? sortKey === column.key
|
|
146
|
+
? sortDirection === 'asc'
|
|
147
|
+
? 'ascending'
|
|
148
|
+
: 'descending'
|
|
149
|
+
: 'none'
|
|
150
|
+
: undefined
|
|
151
|
+
"
|
|
152
|
+
:tabindex="column.sortable ? 0 : undefined"
|
|
153
|
+
@click="handleSort(column)"
|
|
154
|
+
@keydown.enter.prevent="handleSort(column)"
|
|
155
|
+
@keydown.space.prevent="handleSort(column)"
|
|
156
|
+
>
|
|
157
|
+
{{ column.label }}
|
|
158
|
+
<Icon
|
|
159
|
+
v-if="column.sortable"
|
|
160
|
+
:name="
|
|
161
|
+
sortKey === column.key
|
|
162
|
+
? sortDirection === 'asc'
|
|
163
|
+
? 'angle-down'
|
|
164
|
+
: 'angle-up'
|
|
165
|
+
: 'angles-up-down'
|
|
166
|
+
"
|
|
167
|
+
class="shrink-0 text-xs"
|
|
168
|
+
:class="sortKey === column.key ? 'text-cornflower-blue-600' : 'text-slate-400'"
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<div role="rowgroup">
|
|
175
|
+
<div
|
|
176
|
+
v-for="(row, rowIndex) in rows"
|
|
177
|
+
:key="rowIndex"
|
|
178
|
+
ref="rowRefs"
|
|
179
|
+
class="grid border-b border-gray-100 transition-colors duration-150 focus-visible:bg-slate-50 focus-visible:ring-2 focus-visible:ring-cornflower-blue-500 focus-visible:outline-none focus-visible:ring-inset"
|
|
180
|
+
:class="[
|
|
181
|
+
{
|
|
182
|
+
'hover:bg-slate-50': selectable,
|
|
183
|
+
'border-white/80 bg-cornflower-blue-50': selectable && selected.includes(rowIndex),
|
|
184
|
+
},
|
|
185
|
+
getRowRoundedClass(rowIndex),
|
|
186
|
+
]"
|
|
187
|
+
:style="{ gridTemplateColumns: gridTemplate }"
|
|
188
|
+
role="row"
|
|
189
|
+
:aria-rowindex="rowIndex + 2"
|
|
190
|
+
:aria-selected="selectable ? selected.includes(rowIndex) : undefined"
|
|
191
|
+
tabindex="0"
|
|
192
|
+
@click="handleRowClick($event, rowIndex)"
|
|
193
|
+
@keydown="handleRowKeydown($event, rowIndex)"
|
|
194
|
+
>
|
|
195
|
+
<div
|
|
196
|
+
v-if="selectable"
|
|
197
|
+
class="flex items-center justify-center px-2 py-4"
|
|
198
|
+
role="gridcell"
|
|
199
|
+
:aria-colindex="1"
|
|
200
|
+
>
|
|
201
|
+
<Checkbox
|
|
202
|
+
:model-value="selected.includes(rowIndex)"
|
|
203
|
+
:aria-label="`Select row ${rowIndex + 1}`"
|
|
204
|
+
@update:model-value="toggleRow(rowIndex)"
|
|
205
|
+
/>
|
|
206
|
+
</div>
|
|
207
|
+
<div
|
|
208
|
+
v-for="(column, colIndex) in columns"
|
|
209
|
+
:key="column.key"
|
|
210
|
+
class="flex items-center truncate px-4 py-4 extra-small-normal text-slate-700"
|
|
211
|
+
role="gridcell"
|
|
212
|
+
:aria-colindex="colIndex + (selectable ? 2 : 1)"
|
|
213
|
+
>
|
|
214
|
+
<slot :name="`cell-${column.key}`" :row="row" :value="row[column.key]">
|
|
215
|
+
{{ row[column.key] }}
|
|
216
|
+
</slot>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<div v-if="!rows.length" class="px-4 py-8 text-center text-sm text-slate-400" role="row">
|
|
221
|
+
<slot name="empty">Geen data beschikbaar</slot>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</template>
|
package/src/components.json
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"NexxtAnimatedNumber": "components/AnimatedNumber/AnimatedNumber.vue",
|
|
3
|
+
"NexxtAvatar": "components/Avatar/Avatar.vue",
|
|
3
4
|
"NexxtButton": "components/Button/Button.vue",
|
|
4
5
|
"NexxtCalendar": "components/Calendar/Calendar.vue",
|
|
6
|
+
"NexxtCheckbox": "components/Checkbox/Checkbox.vue",
|
|
5
7
|
"NexxtChip": "components/Chip/Chip.vue",
|
|
6
8
|
"NexxtCustomerJourneyTile": "components/CustomerJourneyTile/CustomerJourneyTile.vue",
|
|
7
9
|
"NexxtDatePicker": "components/DatePicker/DatePicker.vue",
|
|
@@ -15,6 +17,7 @@
|
|
|
15
17
|
"NexxtSocialMediaTemplate": "components/SocialMediaTemplate/SocialMediaTemplate.vue",
|
|
16
18
|
"NexxtSocialMediaType": "components/SocialMediaType/SocialMediaType.vue",
|
|
17
19
|
"NexxtStepperHeader": "components/StepperHeader/StepperHeader.vue",
|
|
20
|
+
"NexxtTable": "components/Table/Table.vue",
|
|
18
21
|
"NexxtTimelineEvent": "components/TimelineEvent/TimelineEvent.vue",
|
|
19
22
|
"NexxtTimelinePhaseblock": "components/TimelinePhaseblock/TimelinePhaseblock.vue",
|
|
20
23
|
"NexxtTooltip": "components/Tooltip/Tooltip.vue"
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// This file is auto-generated by scripts/generate-index.ts
|
|
2
2
|
export { default as NexxtAnimatedNumber } from './components/AnimatedNumber/AnimatedNumber.vue'
|
|
3
|
+
export { default as NexxtAvatar } from './components/Avatar/Avatar.vue'
|
|
3
4
|
export { default as NexxtButton } from './components/Button/Button.vue'
|
|
4
5
|
export { default as NexxtCalendar } from './components/Calendar/Calendar.vue'
|
|
6
|
+
export { default as NexxtCheckbox } from './components/Checkbox/Checkbox.vue'
|
|
5
7
|
export { default as NexxtChip } from './components/Chip/Chip.vue'
|
|
6
8
|
export { default as NexxtCustomerJourneyTile } from './components/CustomerJourneyTile/CustomerJourneyTile.vue'
|
|
7
9
|
export { default as NexxtDatePicker } from './components/DatePicker/DatePicker.vue'
|
|
@@ -15,6 +17,7 @@ export { default as NexxtSocialMediaCustomTemplate } from './components/SocialMe
|
|
|
15
17
|
export { default as NexxtSocialMediaTemplate } from './components/SocialMediaTemplate/SocialMediaTemplate.vue'
|
|
16
18
|
export { default as NexxtSocialMediaType } from './components/SocialMediaType/SocialMediaType.vue'
|
|
17
19
|
export { default as NexxtStepperHeader } from './components/StepperHeader/StepperHeader.vue'
|
|
20
|
+
export { default as NexxtTable } from './components/Table/Table.vue'
|
|
18
21
|
export { default as NexxtTimelineEvent } from './components/TimelineEvent/TimelineEvent.vue'
|
|
19
22
|
export { default as NexxtTimelinePhaseblock } from './components/TimelinePhaseblock/TimelinePhaseblock.vue'
|
|
20
23
|
export { default as NexxtTooltip } from './components/Tooltip/Tooltip.vue'
|