@aleph-alpha/ui-library 1.18.0 → 1.20.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/config.d.ts +7 -0
- package/config.js +100 -0
- package/dist/system/index.d.ts +15 -1
- package/dist/system/lib.js +11471 -11418
- package/docs/public-docs/getting-started-designers.md +19 -6
- package/package.json +1 -1
- package/src/components/UiDropdownMenu/UiDropdownMenu.stories.ts +110 -0
- package/src/components/UiField/UiField.vue +2 -2
- package/src/components/UiField/UiFieldError.vue +1 -1
- package/src/components/core/badge/Badge.vue +25 -2
- package/src/components/core/badge/index.ts +4 -5
- package/src/components/core/button/Button.vue +30 -2
- package/src/components/core/button/index.ts +7 -7
- package/src/directives/index.ts +1 -0
- package/src/directives/vHover.stories.ts +129 -0
- package/src/directives/vHover.ts +70 -0
- package/src/index.ts +4 -0
- package/src/patterns/UiDataTable/UiDataTable.stories.ts +98 -1
- package/src/patterns/UiDataTable/UiDataTable.vue +15 -0
- package/src/patterns/UiDataTable/UiDataTableColumnHeader.vue +29 -55
- package/src/patterns/UiDataTable/__tests__/UiDataTable.test.ts +90 -8
- package/src/patterns/UiDataTable/__tests__/UiDataTableColumnHeader.test.ts +27 -81
- package/src/patterns/UiDataTable/types.ts +5 -0
- package/src/patterns/UiDatePicker/__tests__/UiDatePicker.test.ts +8 -6
- package/src/theme/Overlay.stories.ts +12 -12
- package/tokens.json +4048 -1388
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
import type { UiDataTableProps } from './types';
|
|
23
23
|
import { DATA_TABLE_LABELS, DEFAULT_COLUMN_SIZE } from './constants';
|
|
24
24
|
|
|
25
|
+
const INTERACTIVE_SELECTOR =
|
|
26
|
+
'button, a, input, textarea, select, [role="button"], [role="link"], [role="menuitem"], [role="checkbox"], [role="switch"], [contenteditable="true"]';
|
|
27
|
+
|
|
25
28
|
defineOptions({
|
|
26
29
|
name: 'UiDataTable',
|
|
27
30
|
});
|
|
@@ -68,6 +71,14 @@
|
|
|
68
71
|
},
|
|
69
72
|
});
|
|
70
73
|
|
|
74
|
+
function handleRowClick(event: MouseEvent | KeyboardEvent, original: TData) {
|
|
75
|
+
if (!props.onRowClick) return;
|
|
76
|
+
const target = event.target as HTMLElement;
|
|
77
|
+
if (target.closest?.(INTERACTIVE_SELECTOR)) return;
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
props.onRowClick(original);
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
defineExpose({
|
|
72
83
|
table,
|
|
73
84
|
});
|
|
@@ -102,6 +113,7 @@
|
|
|
102
113
|
v-for="row in table.getRowModel().rows"
|
|
103
114
|
:key="row.id"
|
|
104
115
|
:data-state="row.getIsSelected() ? 'selected' : undefined"
|
|
116
|
+
:class="props.onRowClick ? 'cursor-pointer' : undefined"
|
|
105
117
|
>
|
|
106
118
|
<UiTableCell
|
|
107
119
|
v-for="cell in row.getVisibleCells()"
|
|
@@ -111,6 +123,9 @@
|
|
|
111
123
|
? { width: `${cell.column.getSize()}px` }
|
|
112
124
|
: undefined
|
|
113
125
|
"
|
|
126
|
+
@click="handleRowClick($event, row.original)"
|
|
127
|
+
@keydown.enter="handleRowClick($event, row.original)"
|
|
128
|
+
@keydown.space="handleRowClick($event, row.original)"
|
|
114
129
|
>
|
|
115
130
|
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
|
116
131
|
</UiTableCell>
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts" generic="TData, TValue">
|
|
2
2
|
import { UiButton } from '@/components/UiButton';
|
|
3
3
|
import { UiIcon } from '@/components/UiIcon';
|
|
4
|
-
import {
|
|
5
|
-
UiDropdownMenu,
|
|
6
|
-
UiDropdownMenuContent,
|
|
7
|
-
UiDropdownMenuItem,
|
|
8
|
-
UiDropdownMenuTrigger,
|
|
9
|
-
} from '@/components/UiDropdownMenu';
|
|
10
4
|
import { cn } from '@/lib/utils';
|
|
11
5
|
import { computed } from 'vue';
|
|
12
6
|
import { DATA_TABLE_LABELS } from './constants';
|
|
@@ -24,8 +18,6 @@
|
|
|
24
18
|
const props = defineProps<UiDataTableColumnHeaderProps<TData, TValue>>();
|
|
25
19
|
|
|
26
20
|
const labels = computed(() => ({
|
|
27
|
-
sortAscending: props.labels?.sortAscending ?? DATA_TABLE_LABELS.sortAscending,
|
|
28
|
-
sortDescending: props.labels?.sortDescending ?? DATA_TABLE_LABELS.sortDescending,
|
|
29
21
|
sortedAscending: props.labels?.sortedAscending ?? DATA_TABLE_LABELS.sortedAscending,
|
|
30
22
|
sortedDescending: props.labels?.sortedDescending ?? DATA_TABLE_LABELS.sortedDescending,
|
|
31
23
|
clickToSort: props.labels?.clickToSort ?? DATA_TABLE_LABELS.clickToSort,
|
|
@@ -37,57 +29,39 @@
|
|
|
37
29
|
if (sortState === 'desc') return `${props.title}, ${labels.value.sortedDescending}`;
|
|
38
30
|
return `${props.title}, ${labels.value.clickToSort}`;
|
|
39
31
|
});
|
|
32
|
+
|
|
33
|
+
function handleSort() {
|
|
34
|
+
const current = props.column.getIsSorted();
|
|
35
|
+
if (current === false) {
|
|
36
|
+
props.column.toggleSorting(false); // -> asc
|
|
37
|
+
} else if (current === 'asc') {
|
|
38
|
+
props.column.toggleSorting(true); // -> desc
|
|
39
|
+
} else {
|
|
40
|
+
props.column.clearSorting(); // -> unsorted
|
|
41
|
+
}
|
|
42
|
+
}
|
|
40
43
|
</script>
|
|
41
44
|
|
|
42
45
|
<template>
|
|
43
46
|
<div v-if="column.getCanSort()" :class="cn('flex items-center space-x-2', $attrs.class ?? '')">
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
name="arrow-up"
|
|
63
|
-
:size="16"
|
|
64
|
-
class="ml-2"
|
|
65
|
-
aria-hidden="true"
|
|
66
|
-
/>
|
|
67
|
-
<UiIcon v-else name="chevrons-up-down" :size="16" class="ml-2" aria-hidden="true" />
|
|
68
|
-
</UiButton>
|
|
69
|
-
</UiDropdownMenuTrigger>
|
|
70
|
-
<UiDropdownMenuContent align="start">
|
|
71
|
-
<UiDropdownMenuItem @click="column.toggleSorting(false)">
|
|
72
|
-
<UiIcon
|
|
73
|
-
name="arrow-up"
|
|
74
|
-
:size="14"
|
|
75
|
-
class="mr-2 text-muted-foreground/70"
|
|
76
|
-
aria-hidden="true"
|
|
77
|
-
/>
|
|
78
|
-
{{ labels.sortAscending }}
|
|
79
|
-
</UiDropdownMenuItem>
|
|
80
|
-
<UiDropdownMenuItem @click="column.toggleSorting(true)">
|
|
81
|
-
<UiIcon
|
|
82
|
-
name="arrow-down"
|
|
83
|
-
:size="14"
|
|
84
|
-
class="mr-2 text-muted-foreground/70"
|
|
85
|
-
aria-hidden="true"
|
|
86
|
-
/>
|
|
87
|
-
{{ labels.sortDescending }}
|
|
88
|
-
</UiDropdownMenuItem>
|
|
89
|
-
</UiDropdownMenuContent>
|
|
90
|
-
</UiDropdownMenu>
|
|
47
|
+
<UiButton variant="ghost" size="sm" class="h-8" :aria-label="sortLabel" @click="handleSort">
|
|
48
|
+
<span>{{ title }}</span>
|
|
49
|
+
<UiIcon
|
|
50
|
+
v-if="column.getIsSorted() === 'desc'"
|
|
51
|
+
name="arrow-down"
|
|
52
|
+
:size="16"
|
|
53
|
+
class="ml-2"
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
/>
|
|
56
|
+
<UiIcon
|
|
57
|
+
v-else-if="column.getIsSorted() === 'asc'"
|
|
58
|
+
name="arrow-up"
|
|
59
|
+
:size="16"
|
|
60
|
+
class="ml-2"
|
|
61
|
+
aria-hidden="true"
|
|
62
|
+
/>
|
|
63
|
+
<UiIcon v-else name="chevrons-up-down" :size="16" class="ml-2" aria-hidden="true" />
|
|
64
|
+
</UiButton>
|
|
91
65
|
</div>
|
|
92
66
|
|
|
93
67
|
<div v-else :class="$attrs.class">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { render, within } from '@testing-library/vue';
|
|
1
|
+
import { fireEvent, render, within } from '@testing-library/vue';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
|
-
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
4
4
|
import { h, ref, nextTick } from 'vue';
|
|
5
5
|
import type { ColumnDef } from '@tanstack/vue-table';
|
|
6
6
|
import UiDataTable from '../UiDataTable.vue';
|
|
@@ -137,35 +137,117 @@ describe('UiDataTable', () => {
|
|
|
137
137
|
});
|
|
138
138
|
|
|
139
139
|
describe('Sorting', () => {
|
|
140
|
-
test('sorts
|
|
140
|
+
test('sorts ascending on first click', async () => {
|
|
141
141
|
const user = userEvent.setup();
|
|
142
142
|
const { getByRole, getAllByRole } = render(UiDataTable, {
|
|
143
143
|
props: { columns: sortableColumns, data: testData },
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
-
// Click the Name column header button to
|
|
146
|
+
// Click the Name column header button to sort ascending
|
|
147
147
|
await user.click(getByRole('button', { name: /Name/i }));
|
|
148
|
-
// Click ascending option
|
|
149
|
-
await user.click(getByRole('menuitem', { name: /Asc/i }));
|
|
150
148
|
|
|
151
149
|
const cells = getAllByRole('cell');
|
|
152
150
|
// First data row should be Alice (alphabetically first)
|
|
153
151
|
expect(cells[0]).toHaveTextContent('Alice');
|
|
154
152
|
});
|
|
155
153
|
|
|
156
|
-
test('sorts descending
|
|
154
|
+
test('sorts descending on second click', async () => {
|
|
157
155
|
const user = userEvent.setup();
|
|
158
156
|
const { getByRole, getAllByRole } = render(UiDataTable, {
|
|
159
157
|
props: { columns: sortableColumns, data: testData },
|
|
160
158
|
});
|
|
161
159
|
|
|
160
|
+
// First click: ascending
|
|
161
|
+
await user.click(getByRole('button', { name: /Name/i }));
|
|
162
|
+
// Second click: descending
|
|
162
163
|
await user.click(getByRole('button', { name: /Name/i }));
|
|
163
|
-
await user.click(getByRole('menuitem', { name: /Desc/i }));
|
|
164
164
|
|
|
165
165
|
const cells = getAllByRole('cell');
|
|
166
166
|
// First data row should be Charlie (alphabetically last)
|
|
167
167
|
expect(cells[0]).toHaveTextContent('Charlie');
|
|
168
168
|
});
|
|
169
|
+
|
|
170
|
+
test('clears sorting on third click', async () => {
|
|
171
|
+
const user = userEvent.setup();
|
|
172
|
+
const { getByRole, getAllByRole } = render(UiDataTable, {
|
|
173
|
+
props: { columns: sortableColumns, data: testData },
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const initialCells = getAllByRole('cell');
|
|
177
|
+
const initialFirst = initialCells[0].textContent;
|
|
178
|
+
|
|
179
|
+
const headerButton = getByRole('button', { name: /Name/i });
|
|
180
|
+
// First click: ascending
|
|
181
|
+
await user.click(headerButton);
|
|
182
|
+
// Second click: descending
|
|
183
|
+
await user.click(headerButton);
|
|
184
|
+
// Third click: clear sorting (back to original order)
|
|
185
|
+
await user.click(headerButton);
|
|
186
|
+
|
|
187
|
+
const finalCells = getAllByRole('cell');
|
|
188
|
+
expect(finalCells[0]).toHaveTextContent(initialFirst ?? '');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('Row Click', () => {
|
|
193
|
+
test('calls onRowClick with row data when a row is clicked', async () => {
|
|
194
|
+
const onRowClick = vi.fn();
|
|
195
|
+
const { getByText } = render(UiDataTable, {
|
|
196
|
+
props: { columns: basicColumns, data: testData, onRowClick },
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Click cell content - handler is on <td>, event bubbles from text
|
|
200
|
+
await fireEvent.click(getByText('Alice'));
|
|
201
|
+
|
|
202
|
+
expect(onRowClick).toHaveBeenCalledWith(testData[0]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('adds cursor-pointer class when onRowClick is provided', () => {
|
|
206
|
+
const { container } = render(UiDataTable, {
|
|
207
|
+
props: { columns: basicColumns, data: testData, onRowClick: () => {} },
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const dataRows = container.querySelectorAll('tbody tr');
|
|
211
|
+
expect(dataRows[0]).toHaveClass('cursor-pointer');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('does not add cursor-pointer when onRowClick is not provided', () => {
|
|
215
|
+
const { container } = render(UiDataTable, {
|
|
216
|
+
props: { columns: basicColumns, data: testData },
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const dataRows = container.querySelectorAll('tbody tr');
|
|
220
|
+
expect(dataRows[0]).not.toHaveClass('cursor-pointer');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('does not fire onRowClick when clicking interactive elements inside a row', async () => {
|
|
224
|
+
const user = userEvent.setup();
|
|
225
|
+
const onRowClick = vi.fn();
|
|
226
|
+
const { getByLabelText } = render({
|
|
227
|
+
components: { UiDataTable },
|
|
228
|
+
template: '<UiDataTable :columns="columns" :data="data" :on-row-click="onRowClick" />',
|
|
229
|
+
setup() {
|
|
230
|
+
return { columns: selectableColumns, data: testData, onRowClick };
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await nextTick();
|
|
235
|
+
|
|
236
|
+
// Click the checkbox (interactive element) - should NOT trigger onRowClick
|
|
237
|
+
await user.click(getByLabelText('Select row Alice'));
|
|
238
|
+
expect(onRowClick).not.toHaveBeenCalled();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('activates onRowClick via keyboard on cell', async () => {
|
|
242
|
+
const onRowClick = vi.fn();
|
|
243
|
+
const { getByText } = render(UiDataTable, {
|
|
244
|
+
props: { columns: basicColumns, data: testData, onRowClick },
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await fireEvent.keyDown(getByText('Alice').closest('td')!, { key: 'Enter' });
|
|
248
|
+
|
|
249
|
+
expect(onRowClick).toHaveBeenCalledWith(testData[0]);
|
|
250
|
+
});
|
|
169
251
|
});
|
|
170
252
|
|
|
171
253
|
describe('Filtering', () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render
|
|
1
|
+
import { render } from '@testing-library/vue';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { describe, expect, test, vi } from 'vitest';
|
|
4
4
|
import UiDataTableColumnHeader from '../UiDataTableColumnHeader.vue';
|
|
@@ -10,6 +10,7 @@ function createMockColumn(options: { canSort?: boolean; isSorted?: false | 'asc'
|
|
|
10
10
|
getCanSort: vi.fn(() => canSort),
|
|
11
11
|
getIsSorted: vi.fn(() => isSorted),
|
|
12
12
|
toggleSorting: vi.fn(),
|
|
13
|
+
clearSorting: vi.fn(),
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -34,13 +35,12 @@ describe('UiDataTableColumnHeader', () => {
|
|
|
34
35
|
expect(queryByRole('button')).not.toBeInTheDocument();
|
|
35
36
|
});
|
|
36
37
|
|
|
37
|
-
test('renders as button
|
|
38
|
+
test('renders as button when column is sortable', () => {
|
|
38
39
|
const column = createMockColumn({ canSort: true });
|
|
39
40
|
const { getByRole } = render(UiDataTableColumnHeader, {
|
|
40
41
|
props: { column, title: 'Name' },
|
|
41
42
|
});
|
|
42
43
|
|
|
43
|
-
// Button should be accessible by role and name
|
|
44
44
|
expect(getByRole('button', { name: /Name/i })).toBeInTheDocument();
|
|
45
45
|
});
|
|
46
46
|
|
|
@@ -55,36 +55,42 @@ describe('UiDataTableColumnHeader', () => {
|
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
describe('
|
|
59
|
-
test('
|
|
58
|
+
describe('Click-to-Sort', () => {
|
|
59
|
+
test('sorts ascending when unsorted column is clicked', async () => {
|
|
60
60
|
const user = userEvent.setup();
|
|
61
|
-
const column = createMockColumn();
|
|
61
|
+
const column = createMockColumn({ isSorted: false });
|
|
62
62
|
const { getByRole } = render(UiDataTableColumnHeader, {
|
|
63
63
|
props: { column, title: 'Name' },
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
await user.click(getByRole('button'));
|
|
67
|
-
await waitFor(() => expect(getByRole('menu')).toBeInTheDocument());
|
|
68
|
-
|
|
69
|
-
await user.click(getByRole('menuitem', { name: /Asc/i }));
|
|
70
67
|
|
|
71
68
|
expect(column.toggleSorting).toHaveBeenCalledWith(false);
|
|
72
69
|
});
|
|
73
70
|
|
|
74
|
-
test('
|
|
71
|
+
test('sorts descending when ascending column is clicked', async () => {
|
|
75
72
|
const user = userEvent.setup();
|
|
76
|
-
const column = createMockColumn();
|
|
73
|
+
const column = createMockColumn({ isSorted: 'asc' });
|
|
77
74
|
const { getByRole } = render(UiDataTableColumnHeader, {
|
|
78
75
|
props: { column, title: 'Name' },
|
|
79
76
|
});
|
|
80
77
|
|
|
81
78
|
await user.click(getByRole('button'));
|
|
82
|
-
await waitFor(() => expect(getByRole('menu')).toBeInTheDocument());
|
|
83
|
-
|
|
84
|
-
await user.click(getByRole('menuitem', { name: /Desc/i }));
|
|
85
79
|
|
|
86
80
|
expect(column.toggleSorting).toHaveBeenCalledWith(true);
|
|
87
81
|
});
|
|
82
|
+
|
|
83
|
+
test('clears sorting when descending column is clicked', async () => {
|
|
84
|
+
const user = userEvent.setup();
|
|
85
|
+
const column = createMockColumn({ isSorted: 'desc' });
|
|
86
|
+
const { getByRole } = render(UiDataTableColumnHeader, {
|
|
87
|
+
props: { column, title: 'Name' },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await user.click(getByRole('button'));
|
|
91
|
+
|
|
92
|
+
expect(column.clearSorting).toHaveBeenCalled();
|
|
93
|
+
});
|
|
88
94
|
});
|
|
89
95
|
|
|
90
96
|
describe('Accessibility', () => {
|
|
@@ -115,73 +121,30 @@ describe('UiDataTableColumnHeader', () => {
|
|
|
115
121
|
});
|
|
116
122
|
|
|
117
123
|
describe('i18n Support', () => {
|
|
118
|
-
test('uses
|
|
119
|
-
const user = userEvent.setup();
|
|
120
|
-
const column = createMockColumn();
|
|
121
|
-
const { getByRole } = render(UiDataTableColumnHeader, {
|
|
122
|
-
props: { column, title: 'Name' },
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
await user.click(getByRole('button'));
|
|
126
|
-
|
|
127
|
-
await waitFor(() => {
|
|
128
|
-
expect(getByRole('menuitem', { name: 'Asc' })).toBeInTheDocument();
|
|
129
|
-
expect(getByRole('menuitem', { name: 'Desc' })).toBeInTheDocument();
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('applies custom labels when provided', async () => {
|
|
134
|
-
const user = userEvent.setup();
|
|
124
|
+
test('uses custom aria labels when provided', () => {
|
|
135
125
|
const column = createMockColumn({ isSorted: 'asc' });
|
|
136
126
|
const { getByRole } = render(UiDataTableColumnHeader, {
|
|
137
127
|
props: {
|
|
138
128
|
column,
|
|
139
129
|
title: 'Name',
|
|
140
|
-
labels: {
|
|
141
|
-
sortAscending: 'Aufsteigend',
|
|
142
|
-
sortDescending: 'Absteigend',
|
|
143
|
-
sortedAscending: 'aufsteigend sortiert',
|
|
144
|
-
},
|
|
130
|
+
labels: { sortedAscending: 'aufsteigend sortiert' },
|
|
145
131
|
},
|
|
146
132
|
});
|
|
147
133
|
|
|
148
|
-
// Check custom aria-label
|
|
149
134
|
expect(getByRole('button')).toHaveAttribute('aria-label', 'Name, aufsteigend sortiert');
|
|
150
|
-
|
|
151
|
-
// Check custom menu labels
|
|
152
|
-
await user.click(getByRole('button'));
|
|
153
|
-
|
|
154
|
-
await waitFor(() => {
|
|
155
|
-
expect(getByRole('menuitem', { name: 'Aufsteigend' })).toBeInTheDocument();
|
|
156
|
-
expect(getByRole('menuitem', { name: 'Absteigend' })).toBeInTheDocument();
|
|
157
|
-
});
|
|
158
135
|
});
|
|
159
136
|
|
|
160
|
-
test('
|
|
161
|
-
const user = userEvent.setup();
|
|
137
|
+
test('uses custom clickToSort label in aria-label', () => {
|
|
162
138
|
const column = createMockColumn({ isSorted: false });
|
|
163
139
|
const { getByRole } = render(UiDataTableColumnHeader, {
|
|
164
140
|
props: {
|
|
165
141
|
column,
|
|
166
|
-
title: '
|
|
167
|
-
labels: {
|
|
168
|
-
sortAscending: 'Sort A-Z',
|
|
169
|
-
// Other labels should fall back to defaults
|
|
170
|
-
},
|
|
142
|
+
title: 'Email',
|
|
143
|
+
labels: { clickToSort: 'zum Sortieren klicken' },
|
|
171
144
|
},
|
|
172
145
|
});
|
|
173
146
|
|
|
174
|
-
|
|
175
|
-
expect(getByRole('button')).toHaveAttribute('aria-label', 'Status, click to sort');
|
|
176
|
-
|
|
177
|
-
await user.click(getByRole('button'));
|
|
178
|
-
|
|
179
|
-
await waitFor(() => {
|
|
180
|
-
// Custom label
|
|
181
|
-
expect(getByRole('menuitem', { name: 'Sort A-Z' })).toBeInTheDocument();
|
|
182
|
-
// Default label (not overridden)
|
|
183
|
-
expect(getByRole('menuitem', { name: 'Desc' })).toBeInTheDocument();
|
|
184
|
-
});
|
|
147
|
+
expect(getByRole('button')).toHaveAttribute('aria-label', 'Email, zum Sortieren klicken');
|
|
185
148
|
});
|
|
186
149
|
|
|
187
150
|
test('uses custom sortedDescending label in aria-label', () => {
|
|
@@ -190,28 +153,11 @@ describe('UiDataTableColumnHeader', () => {
|
|
|
190
153
|
props: {
|
|
191
154
|
column,
|
|
192
155
|
title: 'Amount',
|
|
193
|
-
labels: {
|
|
194
|
-
sortedDescending: 'absteigend sortiert',
|
|
195
|
-
},
|
|
156
|
+
labels: { sortedDescending: 'absteigend sortiert' },
|
|
196
157
|
},
|
|
197
158
|
});
|
|
198
159
|
|
|
199
160
|
expect(getByRole('button')).toHaveAttribute('aria-label', 'Amount, absteigend sortiert');
|
|
200
161
|
});
|
|
201
|
-
|
|
202
|
-
test('uses custom clickToSort label in aria-label', () => {
|
|
203
|
-
const column = createMockColumn({ isSorted: false });
|
|
204
|
-
const { getByRole } = render(UiDataTableColumnHeader, {
|
|
205
|
-
props: {
|
|
206
|
-
column,
|
|
207
|
-
title: 'Email',
|
|
208
|
-
labels: {
|
|
209
|
-
clickToSort: 'zum Sortieren klicken',
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
expect(getByRole('button')).toHaveAttribute('aria-label', 'Email, zum Sortieren klicken');
|
|
215
|
-
});
|
|
216
162
|
});
|
|
217
163
|
});
|
|
@@ -36,6 +36,11 @@ export type UiDataTableProps<TData, TValue> = {
|
|
|
36
36
|
* @example "400px"
|
|
37
37
|
*/
|
|
38
38
|
tableMinHeight?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Callback when a data row is clicked. Receives the row's original data item.
|
|
41
|
+
* @example (item) => router.push(`/details/${item.id}`)
|
|
42
|
+
*/
|
|
43
|
+
onRowClick?: (item: TData) => void;
|
|
39
44
|
};
|
|
40
45
|
|
|
41
46
|
/**
|
|
@@ -245,7 +245,7 @@ describe('UiDatePicker', () => {
|
|
|
245
245
|
|
|
246
246
|
describe('lazy loading optimization', () => {
|
|
247
247
|
test('lazily loads UiCalendar in single mode when popover opens', async () => {
|
|
248
|
-
const { getByRole, queryByRole, findByRole } = render(UiDatePicker, {
|
|
248
|
+
const { getByRole, queryByRole, findByRole, findAllByRole } = render(UiDatePicker, {
|
|
249
249
|
props: { mode: 'single', modelValue: new CalendarDate(2025, 1, 15) },
|
|
250
250
|
});
|
|
251
251
|
|
|
@@ -255,13 +255,14 @@ describe('UiDatePicker', () => {
|
|
|
255
255
|
// Open the popover
|
|
256
256
|
await userEvent.click(getByRole('button'));
|
|
257
257
|
|
|
258
|
-
// Calendar should be rendered after async load (
|
|
258
|
+
// Calendar should be rendered after async load (find* waits for element)
|
|
259
259
|
await findByRole('dialog');
|
|
260
|
-
await
|
|
260
|
+
const cells = await findAllByRole('gridcell');
|
|
261
|
+
expect(cells.length).toBeGreaterThan(0);
|
|
261
262
|
});
|
|
262
263
|
|
|
263
264
|
test('lazily loads UiRangeCalendar in range mode when popover opens', async () => {
|
|
264
|
-
const { getByRole, queryByRole, findByRole } = render(UiDatePicker, {
|
|
265
|
+
const { getByRole, queryByRole, findByRole, findAllByRole } = render(UiDatePicker, {
|
|
265
266
|
props: { mode: 'range' },
|
|
266
267
|
});
|
|
267
268
|
|
|
@@ -271,9 +272,10 @@ describe('UiDatePicker', () => {
|
|
|
271
272
|
// Open the popover
|
|
272
273
|
await userEvent.click(getByRole('button'));
|
|
273
274
|
|
|
274
|
-
// RangeCalendar should be rendered after async load (
|
|
275
|
+
// RangeCalendar should be rendered after async load (find* waits for element)
|
|
275
276
|
await findByRole('dialog');
|
|
276
|
-
await
|
|
277
|
+
const cells = await findAllByRole('gridcell');
|
|
278
|
+
expect(cells.length).toBeGreaterThan(0);
|
|
277
279
|
});
|
|
278
280
|
|
|
279
281
|
test('single mode calendar is fully functional after lazy load', async () => {
|
|
@@ -73,8 +73,8 @@ export const Surface: Story = {
|
|
|
73
73
|
};
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* Hover overlay
|
|
77
|
-
*
|
|
76
|
+
* Hover overlay tokens used by `v-hover` (`subtle` / `default` / `strong`).
|
|
77
|
+
* Same three tiers as interactive overlay layers; use these utilities when you need a solid swatch.
|
|
78
78
|
*/
|
|
79
79
|
export const Hover: Story = {
|
|
80
80
|
render: () => ({
|
|
@@ -90,31 +90,31 @@ export const Hover: Story = {
|
|
|
90
90
|
|
|
91
91
|
<div class="mb-6">
|
|
92
92
|
<h2 class="text-xl font-bold text-content-on-surface-primary mb-1">hover</h2>
|
|
93
|
-
<p class="text-sm text-content-on-surface-muted">Overlay
|
|
93
|
+
<p class="text-sm text-content-on-surface-muted">Overlay hover tiers (<code class="text-xs">subtle</code>, <code class="text-xs">default</code>, <code class="text-xs">strong</code>). Matches <code class="text-xs">v-hover</code>. Click class name to copy.</p>
|
|
94
94
|
</div>
|
|
95
95
|
|
|
96
96
|
<div class="flex flex-wrap gap-6">
|
|
97
97
|
<div class="flex flex-col items-center gap-1.5">
|
|
98
|
-
<div class="w-16 h-16 rounded-md bg-overlay-hover-
|
|
98
|
+
<div class="w-16 h-16 rounded-md bg-overlay-hover-subtle border border-border-surface-default"></div>
|
|
99
99
|
<div class="text-center">
|
|
100
|
-
<span class="text-xs text-content-on-surface-primary block font-medium">
|
|
101
|
-
<code class="copy-btn text-[10px] text-content-on-surface-muted block mt-0.5 px-1 py-0.5 rounded" onclick="copyToClipboard('bg-overlay-hover-
|
|
100
|
+
<span class="text-xs text-content-on-surface-primary block font-medium">subtle</span>
|
|
101
|
+
<code class="copy-btn text-[10px] text-content-on-surface-muted block mt-0.5 px-1 py-0.5 rounded" onclick="copyToClipboard('bg-overlay-hover-subtle', this)">bg-overlay-hover-subtle</code>
|
|
102
102
|
</div>
|
|
103
103
|
</div>
|
|
104
104
|
|
|
105
105
|
<div class="flex flex-col items-center gap-1.5">
|
|
106
|
-
<div class="w-16 h-16 rounded-md bg-overlay-hover-
|
|
106
|
+
<div class="w-16 h-16 rounded-md bg-overlay-hover-default border border-border-surface-default"></div>
|
|
107
107
|
<div class="text-center">
|
|
108
|
-
<span class="text-xs text-content-on-surface-primary block font-medium">
|
|
109
|
-
<code class="copy-btn text-[10px] text-content-on-surface-muted block mt-0.5 px-1 py-0.5 rounded" onclick="copyToClipboard('bg-overlay-hover-
|
|
108
|
+
<span class="text-xs text-content-on-surface-primary block font-medium">default</span>
|
|
109
|
+
<code class="copy-btn text-[10px] text-content-on-surface-muted block mt-0.5 px-1 py-0.5 rounded" onclick="copyToClipboard('bg-overlay-hover-default', this)">bg-overlay-hover-default</code>
|
|
110
110
|
</div>
|
|
111
111
|
</div>
|
|
112
112
|
|
|
113
113
|
<div class="flex flex-col items-center gap-1.5">
|
|
114
|
-
<div class="w-16 h-16 rounded-md bg-overlay-hover-
|
|
114
|
+
<div class="w-16 h-16 rounded-md bg-overlay-hover-strong border border-border-surface-default"></div>
|
|
115
115
|
<div class="text-center">
|
|
116
|
-
<span class="text-xs text-content-on-surface-primary block font-medium">
|
|
117
|
-
<code class="copy-btn text-[10px] text-content-on-surface-muted block mt-0.5 px-1 py-0.5 rounded" onclick="copyToClipboard('bg-overlay-hover-
|
|
116
|
+
<span class="text-xs text-content-on-surface-primary block font-medium">strong</span>
|
|
117
|
+
<code class="copy-btn text-[10px] text-content-on-surface-muted block mt-0.5 px-1 py-0.5 rounded" onclick="copyToClipboard('bg-overlay-hover-strong', this)">bg-overlay-hover-strong</code>
|
|
118
118
|
</div>
|
|
119
119
|
</div>
|
|
120
120
|
</div>
|