@talelk/newui-organisms 0.0.0-stage → 2.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/LICENSE +202 -0
- package/README.md +12 -2
- package/package.json +38 -3
- package/src/exports.test.ts +27 -0
- package/src/index.ts +1 -0
- package/src/organisms/AppSidebar/AppSidebar.test.tsx +136 -0
- package/src/organisms/AppSidebar/AppSidebar.tsx +259 -0
- package/src/organisms/AppSidebar/index.ts +1 -0
- package/src/organisms/AuthCard/AuthCard.a11y.test.tsx +36 -0
- package/src/organisms/AuthCard/AuthCard.test.tsx +56 -0
- package/src/organisms/AuthCard/AuthCard.tsx +89 -0
- package/src/organisms/AuthCard/index.ts +1 -0
- package/src/organisms/CommandPalette/CommandPalette.test.tsx +196 -0
- package/src/organisms/CommandPalette/CommandPalette.tsx +468 -0
- package/src/organisms/CommandPalette/index.ts +1 -0
- package/src/organisms/DataState/DataState.test.tsx +45 -0
- package/src/organisms/DataState/DataState.tsx +111 -0
- package/src/organisms/DataState/index.ts +6 -0
- package/src/organisms/DataTable/DataTable.test.tsx +161 -0
- package/src/organisms/DataTable/DataTable.tsx +612 -0
- package/src/organisms/DataTable/index.ts +1 -0
- package/src/organisms/DetailHeader/DetailHeader.test.tsx +55 -0
- package/src/organisms/DetailHeader/DetailHeader.tsx +95 -0
- package/src/organisms/DetailHeader/index.ts +1 -0
- package/src/organisms/FilterBar/FilterBar.test.tsx +152 -0
- package/src/organisms/FilterBar/FilterBar.tsx +251 -0
- package/src/organisms/FilterBar/index.ts +1 -0
- package/src/organisms/KanbanBoard/KanbanBoard.test.tsx +255 -0
- package/src/organisms/KanbanBoard/KanbanBoard.tsx +605 -0
- package/src/organisms/KanbanBoard/drag.test.ts +448 -0
- package/src/organisms/KanbanBoard/drag.ts +407 -0
- package/src/organisms/KanbanBoard/index.ts +17 -0
- package/src/organisms/MobileNav/MobileNav.browser.test.tsx +39 -0
- package/src/organisms/MobileNav/MobileNav.test.tsx +108 -0
- package/src/organisms/MobileNav/MobileNav.tsx +120 -0
- package/src/organisms/MobileNav/index.ts +6 -0
- package/src/organisms/PageToolbar/PageToolbar.a11y.test.tsx +35 -0
- package/src/organisms/PageToolbar/PageToolbar.test.tsx +42 -0
- package/src/organisms/PageToolbar/PageToolbar.tsx +126 -0
- package/src/organisms/PageToolbar/index.ts +1 -0
- package/src/organisms/Section/Section.test.tsx +41 -0
- package/src/organisms/Section/Section.tsx +108 -0
- package/src/organisms/Section/index.ts +1 -0
- package/src/organisms/SelectionBar/SelectionBar.test.tsx +35 -0
- package/src/organisms/SelectionBar/SelectionBar.tsx +84 -0
- package/src/organisms/SelectionBar/index.ts +5 -0
- package/src/organisms/SettingsDialog/SettingsDialog.a11y.test.tsx +58 -0
- package/src/organisms/SettingsDialog/SettingsDialog.browser.test.tsx +43 -0
- package/src/organisms/SettingsDialog/SettingsDialog.ssr.test.tsx +27 -0
- package/src/organisms/SettingsDialog/SettingsDialog.test.tsx +202 -0
- package/src/organisms/SettingsDialog/SettingsDialog.tsx +548 -0
- package/src/organisms/SettingsDialog/index.ts +20 -0
- package/src/organisms/Sidebar/Sidebar.browser.test.tsx +52 -0
- package/src/organisms/Sidebar/Sidebar.test.tsx +308 -0
- package/src/organisms/Sidebar/Sidebar.tsx +732 -0
- package/src/organisms/Sidebar/index.ts +22 -0
- package/src/organisms/StatGrid/StatGrid.a11y.test.tsx +40 -0
- package/src/organisms/StatGrid/StatGrid.test.tsx +95 -0
- package/src/organisms/StatGrid/StatGrid.tsx +151 -0
- package/src/organisms/StatGrid/index.ts +1 -0
- package/src/organisms/VirtualList/VirtualList.test.tsx +80 -0
- package/src/organisms/VirtualList/VirtualList.tsx +113 -0
- package/src/organisms/VirtualList/index.ts +1 -0
- package/src/organisms/index.ts +16 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { render, screen } from '@solidjs/testing-library'
|
|
2
|
+
import userEvent from '@testing-library/user-event'
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import {
|
|
5
|
+
KanbanBoard,
|
|
6
|
+
type KanbanColumn,
|
|
7
|
+
type KanbanMove,
|
|
8
|
+
placeKanbanItem,
|
|
9
|
+
} from './KanbanBoard'
|
|
10
|
+
|
|
11
|
+
interface Deal {
|
|
12
|
+
id: string
|
|
13
|
+
name: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const deals: Deal[] = [
|
|
17
|
+
{ id: 'D-1', name: 'Globex' },
|
|
18
|
+
{ id: 'D-2', name: 'Stark' },
|
|
19
|
+
{ id: 'D-3', name: 'Wayne' },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
function stages(
|
|
23
|
+
overrides: Partial<Record<string, Deal[]>> = {},
|
|
24
|
+
): KanbanColumn<Deal>[] {
|
|
25
|
+
return [
|
|
26
|
+
{
|
|
27
|
+
id: 'open',
|
|
28
|
+
name: 'Open',
|
|
29
|
+
theme: 'gray',
|
|
30
|
+
items: overrides['open'] ?? deals,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'won',
|
|
34
|
+
name: 'Won',
|
|
35
|
+
theme: 'green',
|
|
36
|
+
items: overrides['won'] ?? [],
|
|
37
|
+
},
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const card = (deal: Deal) => <span>{deal.name}</span>
|
|
42
|
+
|
|
43
|
+
const idsIn = (columnId: string) =>
|
|
44
|
+
[
|
|
45
|
+
...document.querySelectorAll(
|
|
46
|
+
`[data-kanban-column="${columnId}"] [data-kanban-id]`,
|
|
47
|
+
),
|
|
48
|
+
].map((node) => node.getAttribute('data-kanban-id'))
|
|
49
|
+
|
|
50
|
+
/** Open a card's menu and choose a column. */
|
|
51
|
+
async function moveViaMenu(name: string, column: string) {
|
|
52
|
+
await userEvent.click(
|
|
53
|
+
screen.getByRole('button', { name: `Actions for ${name}` }),
|
|
54
|
+
)
|
|
55
|
+
await userEvent.click(await screen.findByRole('menuitem', { name: column }))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('placeKanbanItem', () => {
|
|
59
|
+
const columns = stages()
|
|
60
|
+
|
|
61
|
+
it('moves an item to the end of another column by index', () => {
|
|
62
|
+
const next = placeKanbanItem(columns, 'D-1', { column: 'won', index: 0 })
|
|
63
|
+
expect(next[0]?.items.map((item) => item.id)).toEqual(['D-2', 'D-3'])
|
|
64
|
+
expect(next[1]?.items.map((item) => item.id)).toEqual(['D-1'])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('counts the index over the column *without* the item being moved', () => {
|
|
68
|
+
// Dragging D-1 down one place: the index is measured after D-1 is taken out,
|
|
69
|
+
// so 1 lands it between D-2 and D-3 rather than back at the top.
|
|
70
|
+
const next = placeKanbanItem(columns, 'D-1', { column: 'open', index: 1 })
|
|
71
|
+
expect(next[0]?.items.map((item) => item.id)).toEqual(['D-2', 'D-1', 'D-3'])
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('clamps an index past the end of the column', () => {
|
|
75
|
+
const next = placeKanbanItem(columns, 'D-1', { column: 'won', index: 9 })
|
|
76
|
+
expect(next[1]?.items.map((item) => item.id)).toEqual(['D-1'])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('returns what came in when the move changes nothing', () => {
|
|
80
|
+
expect(placeKanbanItem(columns, 'D-1', { column: 'open', index: 0 })).toBe(
|
|
81
|
+
columns,
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('keeps the columns it did not touch, by reference', () => {
|
|
86
|
+
const next = placeKanbanItem(columns, 'D-1', { column: 'won', index: 0 })
|
|
87
|
+
// The untouched column is the same object: keying rows on it would throw
|
|
88
|
+
// that column's DOM away, along with its scroll position.
|
|
89
|
+
expect(next[0]).not.toBe(columns[0])
|
|
90
|
+
expect(next[1]).not.toBe(columns[1])
|
|
91
|
+
expect(next[0]?.name).toBe('Open')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('ignores an item the board does not have', () => {
|
|
95
|
+
expect(placeKanbanItem(columns, 'D-9', { column: 'won', index: 0 })).toBe(
|
|
96
|
+
columns,
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('KanbanBoard', () => {
|
|
102
|
+
it('renders the columns, their counts and the cards', () => {
|
|
103
|
+
render(() => (
|
|
104
|
+
<KanbanBoard columns={stages()} label="Deals">
|
|
105
|
+
{card}
|
|
106
|
+
</KanbanBoard>
|
|
107
|
+
))
|
|
108
|
+
|
|
109
|
+
expect(screen.getByRole('list', { name: 'Deals' })).toBeTruthy()
|
|
110
|
+
expect(screen.getByRole('heading', { name: 'Open', level: 2 })).toBeTruthy()
|
|
111
|
+
expect(screen.getByText('Globex')).toBeTruthy()
|
|
112
|
+
// The count is the column's, not the card's.
|
|
113
|
+
expect(screen.getByText('3')).toBeTruthy()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('renders a column action, a summary and its empty state', async () => {
|
|
117
|
+
const onAdd = vi.fn()
|
|
118
|
+
render(() => (
|
|
119
|
+
<KanbanBoard
|
|
120
|
+
columns={[
|
|
121
|
+
{
|
|
122
|
+
id: 'open',
|
|
123
|
+
name: 'Open',
|
|
124
|
+
items: [],
|
|
125
|
+
summary: <span>$210,000</span>,
|
|
126
|
+
action: { label: 'Add deal', onClick: onAdd },
|
|
127
|
+
},
|
|
128
|
+
]}
|
|
129
|
+
emptyColumn={() => <span>Nothing open.</span>}
|
|
130
|
+
>
|
|
131
|
+
{card}
|
|
132
|
+
</KanbanBoard>
|
|
133
|
+
))
|
|
134
|
+
|
|
135
|
+
expect(screen.getByText('$210,000')).toBeTruthy()
|
|
136
|
+
expect(screen.getByText('Nothing open.')).toBeTruthy()
|
|
137
|
+
await userEvent.click(screen.getByRole('button', { name: 'Add deal' }))
|
|
138
|
+
expect(onAdd).toHaveBeenCalled()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('moves a card with the menu, and reports a single move', async () => {
|
|
142
|
+
const moves: KanbanMove<Deal>[] = []
|
|
143
|
+
render(() => (
|
|
144
|
+
<KanbanBoard
|
|
145
|
+
defaultColumns={stages()}
|
|
146
|
+
onMove={(move) => moves.push(move)}
|
|
147
|
+
itemLabel={(deal) => deal.name}
|
|
148
|
+
>
|
|
149
|
+
{card}
|
|
150
|
+
</KanbanBoard>
|
|
151
|
+
))
|
|
152
|
+
|
|
153
|
+
await moveViaMenu('Globex', 'Won')
|
|
154
|
+
|
|
155
|
+
expect(moves).toHaveLength(1)
|
|
156
|
+
expect(moves[0]?.itemId).toBe('D-1')
|
|
157
|
+
expect(moves[0]?.from).toEqual({ column: 'open', index: 0 })
|
|
158
|
+
expect(moves[0]?.to).toEqual({ column: 'won', index: 0 })
|
|
159
|
+
expect(moves[0]?.columns[1]?.items.map((item) => item.id)).toEqual(['D-1'])
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('only offers the columns the card is not already in', async () => {
|
|
163
|
+
render(() => (
|
|
164
|
+
<KanbanBoard defaultColumns={stages()} itemLabel={(deal) => deal.name}>
|
|
165
|
+
{card}
|
|
166
|
+
</KanbanBoard>
|
|
167
|
+
))
|
|
168
|
+
|
|
169
|
+
await userEvent.click(
|
|
170
|
+
screen.getByRole('button', { name: 'Actions for Globex' }),
|
|
171
|
+
)
|
|
172
|
+
const entries = await screen.findAllByRole('menuitem')
|
|
173
|
+
expect(entries.map((entry) => entry.textContent)).toEqual(['Won'])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('owns the order when uncontrolled, and leaves it alone when controlled', async () => {
|
|
177
|
+
const uncontrolled = render(() => (
|
|
178
|
+
<KanbanBoard defaultColumns={stages()} itemLabel={(deal) => deal.name}>
|
|
179
|
+
{card}
|
|
180
|
+
</KanbanBoard>
|
|
181
|
+
))
|
|
182
|
+
expect(idsIn('won')).toEqual([])
|
|
183
|
+
await moveViaMenu('Globex', 'Won')
|
|
184
|
+
expect(idsIn('won')).toEqual(['D-1'])
|
|
185
|
+
expect(idsIn('open')).toEqual(['D-2', 'D-3'])
|
|
186
|
+
uncontrolled.unmount()
|
|
187
|
+
|
|
188
|
+
const columns = stages()
|
|
189
|
+
render(() => (
|
|
190
|
+
<KanbanBoard columns={columns} itemLabel={(deal) => deal.name}>
|
|
191
|
+
{card}
|
|
192
|
+
</KanbanBoard>
|
|
193
|
+
))
|
|
194
|
+
await moveViaMenu('Globex', 'Won')
|
|
195
|
+
// The board reported the move but did not take it: the owner decides.
|
|
196
|
+
expect(idsIn('won')).toEqual([])
|
|
197
|
+
expect(idsIn('open')).toEqual(['D-1', 'D-2', 'D-3'])
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('lets a card bring its own actions instead', async () => {
|
|
201
|
+
const onArchive = vi.fn()
|
|
202
|
+
render(() => (
|
|
203
|
+
<KanbanBoard
|
|
204
|
+
defaultColumns={stages()}
|
|
205
|
+
itemLabel={(deal) => deal.name}
|
|
206
|
+
renderActions={() => (
|
|
207
|
+
<button type="button" onClick={onArchive}>
|
|
208
|
+
Archive
|
|
209
|
+
</button>
|
|
210
|
+
)}
|
|
211
|
+
>
|
|
212
|
+
{card}
|
|
213
|
+
</KanbanBoard>
|
|
214
|
+
))
|
|
215
|
+
|
|
216
|
+
await userEvent.click(
|
|
217
|
+
screen.getAllByRole('button', { name: 'Archive' })[0] as HTMLElement,
|
|
218
|
+
)
|
|
219
|
+
expect(onArchive).toHaveBeenCalled()
|
|
220
|
+
expect(screen.queryByRole('button', { name: /Actions for/ })).toBeNull()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('snaps the columns only where the board is a phone-width panner', () => {
|
|
224
|
+
render(() => (
|
|
225
|
+
<KanbanBoard defaultColumns={stages()} itemLabel={(deal) => deal.name}>
|
|
226
|
+
{card}
|
|
227
|
+
</KanbanBoard>
|
|
228
|
+
))
|
|
229
|
+
|
|
230
|
+
const row = screen.getByRole('list', { name: 'Board' })
|
|
231
|
+
const classes = row.className.split(/\s+/)
|
|
232
|
+
// Every snap utility is behind `max-lg:`. An unprefixed one would snap the
|
|
233
|
+
// board on a wide screen too, under the pointer, mid-drop.
|
|
234
|
+
expect(classes.filter((name) => name.includes('snap'))).toEqual([
|
|
235
|
+
'max-lg:snap-x',
|
|
236
|
+
'max-lg:snap-mandatory',
|
|
237
|
+
])
|
|
238
|
+
const column = document.querySelector('[data-kanban-column="open"]')
|
|
239
|
+
expect(column?.className).toContain('max-lg:snap-start')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('names the board and each card for a screen reader', () => {
|
|
243
|
+
render(() => (
|
|
244
|
+
<KanbanBoard defaultColumns={stages()} itemLabel={(deal) => deal.name}>
|
|
245
|
+
{card}
|
|
246
|
+
</KanbanBoard>
|
|
247
|
+
))
|
|
248
|
+
|
|
249
|
+
expect(screen.getByRole('list', { name: 'Board' })).toBeTruthy()
|
|
250
|
+
expect(
|
|
251
|
+
screen.getByRole('button', { name: 'Actions for Globex' }),
|
|
252
|
+
).toBeTruthy()
|
|
253
|
+
expect(screen.getByRole('heading', { name: 'Won' })).toBeTruthy()
|
|
254
|
+
})
|
|
255
|
+
})
|