@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.
@@ -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>
@@ -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'