@object-ui/plugin-kanban 0.5.0 → 3.0.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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +34 -0
- package/dist/{KanbanEnhanced-BqDEu7Z6.js → KanbanEnhanced-BPIKjTDv.js} +7 -7
- package/dist/KanbanImpl-BfOKAnJS.js +194 -0
- package/dist/{index-CrR06na7.js → index-CWGTi2xn.js} +253 -215
- package/dist/index.js +1 -1
- package/dist/index.umd.cjs +4 -4
- package/dist/{sortable.esm-ZHwgFQIO.js → sortable.esm-CNNHgHk5.js} +1 -0
- package/dist/src/KanbanImpl.d.ts +2 -1
- package/dist/src/KanbanImpl.d.ts.map +1 -1
- package/dist/src/ObjectKanban.EdgeCases.stories.d.ts +26 -0
- package/dist/src/ObjectKanban.EdgeCases.stories.d.ts.map +1 -0
- package/dist/src/ObjectKanban.d.ts +2 -0
- package/dist/src/ObjectKanban.d.ts.map +1 -1
- package/dist/src/ObjectKanban.stories.d.ts +24 -0
- package/dist/src/ObjectKanban.stories.d.ts.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +10 -10
- package/src/KanbanImpl.tsx +82 -20
- package/src/ObjectKanban.EdgeCases.stories.tsx +168 -0
- package/src/ObjectKanban.stories.tsx +152 -0
- package/src/ObjectKanban.tsx +43 -2
- package/src/__tests__/KanbanEnhanced.test.tsx +1 -0
- package/src/__tests__/accessibility.test.tsx +296 -0
- package/src/__tests__/dnd-undo-integration.test.tsx +525 -0
- package/src/__tests__/performance-benchmark.test.tsx +306 -0
- package/src/__tests__/view-states.test.tsx +403 -0
- package/src/index.test.ts +8 -8
- package/src/index.tsx +29 -4
- package/dist/KanbanImpl-B8nu2BvG.js +0 -144
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Screen reader experience tests for KanbanEnhanced.
|
|
11
|
+
*
|
|
12
|
+
* Tests ARIA attributes, roles, landmarks, keyboard navigation,
|
|
13
|
+
* and screen reader announcements for the kanban board plugin.
|
|
14
|
+
* Part of P2.3 Accessibility & Inclusive Design roadmap.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
18
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
19
|
+
import '@testing-library/jest-dom';
|
|
20
|
+
import React from 'react';
|
|
21
|
+
import { KanbanEnhanced, type KanbanColumn } from '../KanbanEnhanced';
|
|
22
|
+
|
|
23
|
+
// Mock @tanstack/react-virtual
|
|
24
|
+
vi.mock('@tanstack/react-virtual', () => ({
|
|
25
|
+
useVirtualizer: () => ({
|
|
26
|
+
getTotalSize: () => 1000,
|
|
27
|
+
getVirtualItems: () => [],
|
|
28
|
+
measureElement: vi.fn(),
|
|
29
|
+
}),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
// Mock @dnd-kit/core and utilities
|
|
33
|
+
vi.mock('@dnd-kit/core', () => ({
|
|
34
|
+
DndContext: ({ children }: any) => <div data-testid="dnd-context">{children}</div>,
|
|
35
|
+
DragOverlay: ({ children }: any) => <div data-testid="drag-overlay">{children}</div>,
|
|
36
|
+
PointerSensor: vi.fn(),
|
|
37
|
+
TouchSensor: vi.fn(),
|
|
38
|
+
useSensor: vi.fn(),
|
|
39
|
+
useSensors: () => [],
|
|
40
|
+
closestCorners: vi.fn(),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
vi.mock('@dnd-kit/sortable', () => ({
|
|
44
|
+
SortableContext: ({ children }: any) => <div data-testid="sortable-context">{children}</div>,
|
|
45
|
+
useSortable: () => ({
|
|
46
|
+
attributes: {},
|
|
47
|
+
listeners: {},
|
|
48
|
+
setNodeRef: vi.fn(),
|
|
49
|
+
transform: null,
|
|
50
|
+
transition: null,
|
|
51
|
+
isDragging: false,
|
|
52
|
+
}),
|
|
53
|
+
arrayMove: (array: any[], from: number, to: number) => {
|
|
54
|
+
const newArray = [...array];
|
|
55
|
+
newArray.splice(to, 0, newArray.splice(from, 1)[0]);
|
|
56
|
+
return newArray;
|
|
57
|
+
},
|
|
58
|
+
verticalListSortingStrategy: vi.fn(),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
vi.mock('@dnd-kit/utilities', () => ({
|
|
62
|
+
CSS: {
|
|
63
|
+
Transform: {
|
|
64
|
+
toString: () => '',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
const mockColumns: KanbanColumn[] = [
|
|
70
|
+
{
|
|
71
|
+
id: 'todo',
|
|
72
|
+
title: 'To Do',
|
|
73
|
+
cards: [
|
|
74
|
+
{
|
|
75
|
+
id: 'card-1',
|
|
76
|
+
title: 'Design Landing Page',
|
|
77
|
+
description: 'Create wireframes and mockups',
|
|
78
|
+
badges: [{ label: 'High', variant: 'destructive' as const }],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'card-2',
|
|
82
|
+
title: 'Write Documentation',
|
|
83
|
+
description: 'API reference and guides',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'in-progress',
|
|
89
|
+
title: 'In Progress',
|
|
90
|
+
limit: 3,
|
|
91
|
+
cards: [
|
|
92
|
+
{
|
|
93
|
+
id: 'card-3',
|
|
94
|
+
title: 'Build Components',
|
|
95
|
+
description: 'Implement the design system',
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'done',
|
|
101
|
+
title: 'Done',
|
|
102
|
+
cards: [],
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
describe('KanbanEnhanced: Screen Reader & Accessibility', () => {
|
|
107
|
+
describe('board structure and landmarks', () => {
|
|
108
|
+
it('renders all column titles visible for screen readers', () => {
|
|
109
|
+
render(<KanbanEnhanced columns={mockColumns} />);
|
|
110
|
+
|
|
111
|
+
expect(screen.getByText('To Do')).toBeInTheDocument();
|
|
112
|
+
expect(screen.getByText('In Progress')).toBeInTheDocument();
|
|
113
|
+
expect(screen.getByText('Done')).toBeInTheDocument();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('column titles use heading elements for hierarchy', () => {
|
|
117
|
+
render(<KanbanEnhanced columns={mockColumns} />);
|
|
118
|
+
|
|
119
|
+
const todoTitle = screen.getByText('To Do');
|
|
120
|
+
expect(todoTitle.tagName).toBe('H3');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('renders within a dnd-context container', () => {
|
|
124
|
+
const { container } = render(<KanbanEnhanced columns={mockColumns} />);
|
|
125
|
+
|
|
126
|
+
const dndContext = container.querySelector('[data-testid="dnd-context"]');
|
|
127
|
+
expect(dndContext).toBeInTheDocument();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('card content accessibility', () => {
|
|
132
|
+
it('card titles are visible to screen readers', () => {
|
|
133
|
+
render(<KanbanEnhanced columns={mockColumns} />);
|
|
134
|
+
|
|
135
|
+
expect(screen.getByText('Design Landing Page')).toBeInTheDocument();
|
|
136
|
+
expect(screen.getByText('Write Documentation')).toBeInTheDocument();
|
|
137
|
+
expect(screen.getByText('Build Components')).toBeInTheDocument();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('card descriptions are accessible', () => {
|
|
141
|
+
render(<KanbanEnhanced columns={mockColumns} />);
|
|
142
|
+
|
|
143
|
+
expect(screen.getByText('Create wireframes and mockups')).toBeInTheDocument();
|
|
144
|
+
expect(screen.getByText('API reference and guides')).toBeInTheDocument();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('card badges convey semantic information', () => {
|
|
148
|
+
render(<KanbanEnhanced columns={mockColumns} />);
|
|
149
|
+
|
|
150
|
+
const badge = screen.getByText('High');
|
|
151
|
+
expect(badge).toBeInTheDocument();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('column count and limit indicators', () => {
|
|
156
|
+
it('displays card count per column for progress tracking', () => {
|
|
157
|
+
render(<KanbanEnhanced columns={mockColumns} />);
|
|
158
|
+
|
|
159
|
+
// "In Progress" has limit = 3, cards = 1 → shows "1 / 3"
|
|
160
|
+
expect(screen.getByText('1 / 3')).toBeInTheDocument();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('shows warning for columns near capacity', () => {
|
|
164
|
+
const nearLimitColumns: KanbanColumn[] = [
|
|
165
|
+
{
|
|
166
|
+
id: 'limited',
|
|
167
|
+
title: 'Limited Column',
|
|
168
|
+
limit: 5,
|
|
169
|
+
cards: Array(4)
|
|
170
|
+
.fill(null)
|
|
171
|
+
.map((_, i) => ({
|
|
172
|
+
id: `card-${i}`,
|
|
173
|
+
title: `Task ${i}`,
|
|
174
|
+
})),
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
const { container } = render(<KanbanEnhanced columns={nearLimitColumns} />);
|
|
179
|
+
|
|
180
|
+
// Near-limit indicator uses yellow color
|
|
181
|
+
expect(container.querySelector('.text-yellow-500')).toBeTruthy();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('shows error indicator for columns over limit', () => {
|
|
185
|
+
const overLimitColumns: KanbanColumn[] = [
|
|
186
|
+
{
|
|
187
|
+
id: 'full',
|
|
188
|
+
title: 'Full Column',
|
|
189
|
+
limit: 2,
|
|
190
|
+
cards: [
|
|
191
|
+
{ id: 'card-1', title: 'Task 1' },
|
|
192
|
+
{ id: 'card-2', title: 'Task 2' },
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
const { container } = render(<KanbanEnhanced columns={overLimitColumns} />);
|
|
198
|
+
|
|
199
|
+
// Over-limit shows destructive badge "Full"
|
|
200
|
+
const fullBadge = container.querySelector('[class*="destructive"]');
|
|
201
|
+
expect(fullBadge).toBeTruthy();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('collapse/expand behavior', () => {
|
|
206
|
+
it('columns have toggle buttons for collapse/expand', () => {
|
|
207
|
+
render(<KanbanEnhanced columns={mockColumns} />);
|
|
208
|
+
|
|
209
|
+
const buttons = screen.getAllByRole('button');
|
|
210
|
+
// Each column has at least one toggle button
|
|
211
|
+
expect(buttons.length).toBeGreaterThanOrEqual(3);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('collapsed column shows title in vertical orientation', () => {
|
|
215
|
+
const collapsedColumns: KanbanColumn[] = [
|
|
216
|
+
{
|
|
217
|
+
id: 'collapsed',
|
|
218
|
+
title: 'Collapsed Column',
|
|
219
|
+
collapsed: true,
|
|
220
|
+
cards: [{ id: 'card-1', title: 'Task 1' }],
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
render(<KanbanEnhanced columns={collapsedColumns} />);
|
|
225
|
+
|
|
226
|
+
// Collapsed column still shows title (vertically)
|
|
227
|
+
expect(screen.getByText('Collapsed Column')).toBeInTheDocument();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('toggle button click changes column state', () => {
|
|
231
|
+
render(<KanbanEnhanced columns={mockColumns} />);
|
|
232
|
+
|
|
233
|
+
// Find the toggle buttons (ghost variant, small)
|
|
234
|
+
const buttons = screen.getAllByRole('button');
|
|
235
|
+
const toggleButton = buttons[0];
|
|
236
|
+
|
|
237
|
+
// Click to collapse
|
|
238
|
+
fireEvent.click(toggleButton);
|
|
239
|
+
|
|
240
|
+
// The component should still render (no crash)
|
|
241
|
+
expect(toggleButton).toBeInTheDocument();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('drag and drop accessibility', () => {
|
|
246
|
+
it('drag overlay container exists for visual feedback', () => {
|
|
247
|
+
const { container } = render(<KanbanEnhanced columns={mockColumns} />);
|
|
248
|
+
|
|
249
|
+
const overlay = container.querySelector('[data-testid="drag-overlay"]');
|
|
250
|
+
expect(overlay).toBeTruthy();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('sortable context wraps card list for DnD', () => {
|
|
254
|
+
const { container } = render(<KanbanEnhanced columns={mockColumns} />);
|
|
255
|
+
|
|
256
|
+
const sortableContexts = container.querySelectorAll('[data-testid="sortable-context"]');
|
|
257
|
+
// Each non-collapsed column has a sortable context
|
|
258
|
+
expect(sortableContexts.length).toBe(3);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('empty state handling', () => {
|
|
263
|
+
it('handles empty columns array without errors', () => {
|
|
264
|
+
const { container } = render(<KanbanEnhanced columns={[]} />);
|
|
265
|
+
expect(container).toBeTruthy();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('renders columns with no cards correctly', () => {
|
|
269
|
+
const emptyColumns: KanbanColumn[] = [
|
|
270
|
+
{ id: 'empty', title: 'Empty Column', cards: [] },
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
render(<KanbanEnhanced columns={emptyColumns} />);
|
|
274
|
+
|
|
275
|
+
expect(screen.getByText('Empty Column')).toBeInTheDocument();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('visual hierarchy and semantics', () => {
|
|
280
|
+
it('card uses Card component with proper structure', () => {
|
|
281
|
+
render(<KanbanEnhanced columns={mockColumns} />);
|
|
282
|
+
|
|
283
|
+
// Card titles use CardTitle which renders with proper styling
|
|
284
|
+
const cardTitle = screen.getByText('Design Landing Page');
|
|
285
|
+
expect(cardTitle).toBeInTheDocument();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('custom className is applied to root container', () => {
|
|
289
|
+
const { container } = render(
|
|
290
|
+
<KanbanEnhanced columns={mockColumns} className="custom-board" />
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
expect(container.querySelector('.custom-board')).toBeTruthy();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|