@nexxtmove/ui 0.1.28 → 0.1.30
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 +50 -8
- package/dist/index.js +1256 -951
- package/dist/nuxt.js +22 -19
- package/package.json +1 -1
- package/src/components/Button/Button.vue +4 -4
- 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/DropdownButton/DropdownButton.stories.ts +68 -0
- package/src/components/DropdownButton/DropdownButton.test.ts +124 -0
- package/src/components/DropdownButton/DropdownButton.vue +159 -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,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
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
"NexxtAnimatedNumber": "components/AnimatedNumber/AnimatedNumber.vue",
|
|
3
3
|
"NexxtButton": "components/Button/Button.vue",
|
|
4
4
|
"NexxtCalendar": "components/Calendar/Calendar.vue",
|
|
5
|
+
"NexxtCheckbox": "components/Checkbox/Checkbox.vue",
|
|
5
6
|
"NexxtChip": "components/Chip/Chip.vue",
|
|
6
7
|
"NexxtCustomerJourneyTile": "components/CustomerJourneyTile/CustomerJourneyTile.vue",
|
|
7
8
|
"NexxtDatePicker": "components/DatePicker/DatePicker.vue",
|
|
9
|
+
"NexxtDropdownButton": "components/DropdownButton/DropdownButton.vue",
|
|
8
10
|
"NexxtHeader": "components/Header/Header.vue",
|
|
9
11
|
"NexxtIcon": "components/Icon/Icon.vue",
|
|
10
12
|
"NexxtInfoBlock": "components/InfoBlock/InfoBlock.vue",
|
|
@@ -14,6 +16,7 @@
|
|
|
14
16
|
"NexxtSocialMediaTemplate": "components/SocialMediaTemplate/SocialMediaTemplate.vue",
|
|
15
17
|
"NexxtSocialMediaType": "components/SocialMediaType/SocialMediaType.vue",
|
|
16
18
|
"NexxtStepperHeader": "components/StepperHeader/StepperHeader.vue",
|
|
19
|
+
"NexxtTable": "components/Table/Table.vue",
|
|
17
20
|
"NexxtTimelineEvent": "components/TimelineEvent/TimelineEvent.vue",
|
|
18
21
|
"NexxtTimelinePhaseblock": "components/TimelinePhaseblock/TimelinePhaseblock.vue",
|
|
19
22
|
"NexxtTooltip": "components/Tooltip/Tooltip.vue"
|
package/src/index.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
export { default as NexxtAnimatedNumber } from './components/AnimatedNumber/AnimatedNumber.vue'
|
|
3
3
|
export { default as NexxtButton } from './components/Button/Button.vue'
|
|
4
4
|
export { default as NexxtCalendar } from './components/Calendar/Calendar.vue'
|
|
5
|
+
export { default as NexxtCheckbox } from './components/Checkbox/Checkbox.vue'
|
|
5
6
|
export { default as NexxtChip } from './components/Chip/Chip.vue'
|
|
6
7
|
export { default as NexxtCustomerJourneyTile } from './components/CustomerJourneyTile/CustomerJourneyTile.vue'
|
|
7
8
|
export { default as NexxtDatePicker } from './components/DatePicker/DatePicker.vue'
|
|
9
|
+
export { default as NexxtDropdownButton } from './components/DropdownButton/DropdownButton.vue'
|
|
8
10
|
export { default as NexxtHeader } from './components/Header/Header.vue'
|
|
9
11
|
export { default as NexxtIcon } from './components/Icon/Icon.vue'
|
|
10
12
|
export { default as NexxtInfoBlock } from './components/InfoBlock/InfoBlock.vue'
|
|
@@ -14,6 +16,7 @@ export { default as NexxtSocialMediaCustomTemplate } from './components/SocialMe
|
|
|
14
16
|
export { default as NexxtSocialMediaTemplate } from './components/SocialMediaTemplate/SocialMediaTemplate.vue'
|
|
15
17
|
export { default as NexxtSocialMediaType } from './components/SocialMediaType/SocialMediaType.vue'
|
|
16
18
|
export { default as NexxtStepperHeader } from './components/StepperHeader/StepperHeader.vue'
|
|
19
|
+
export { default as NexxtTable } from './components/Table/Table.vue'
|
|
17
20
|
export { default as NexxtTimelineEvent } from './components/TimelineEvent/TimelineEvent.vue'
|
|
18
21
|
export { default as NexxtTimelinePhaseblock } from './components/TimelinePhaseblock/TimelinePhaseblock.vue'
|
|
19
22
|
export { default as NexxtTooltip } from './components/Tooltip/Tooltip.vue'
|