@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,308 @@
|
|
|
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
|
+
Sidebar,
|
|
6
|
+
SidebarCard,
|
|
7
|
+
SidebarHeader,
|
|
8
|
+
SidebarItem,
|
|
9
|
+
SidebarLabel,
|
|
10
|
+
SidebarSearch,
|
|
11
|
+
SidebarSection,
|
|
12
|
+
SidebarTrigger,
|
|
13
|
+
useSidebar,
|
|
14
|
+
} from './Sidebar'
|
|
15
|
+
|
|
16
|
+
function Toggler() {
|
|
17
|
+
const { toggle } = useSidebar()
|
|
18
|
+
return (
|
|
19
|
+
<button type="button" onClick={toggle}>
|
|
20
|
+
Toggle
|
|
21
|
+
</button>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('Sidebar', () => {
|
|
26
|
+
it('is a named navigation landmark', () => {
|
|
27
|
+
const { getByRole } = render(() => <Sidebar />)
|
|
28
|
+
const nav = getByRole('navigation')
|
|
29
|
+
expect(nav.getAttribute('aria-label')).toBe('Main')
|
|
30
|
+
expect(nav.getAttribute('data-state')).toBe('expanded')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('honours a controlled collapsed state', () => {
|
|
34
|
+
const { container } = render(() => <Sidebar collapsed />)
|
|
35
|
+
const nav = container.querySelector('[data-slot="sidebar"]') as HTMLElement
|
|
36
|
+
expect(nav.getAttribute('data-state')).toBe('collapsed')
|
|
37
|
+
expect(nav.style.width).toBe('3rem')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('reports a toggle through onCollapsedChange', async () => {
|
|
41
|
+
const onCollapsedChange = vi.fn()
|
|
42
|
+
const { getByRole } = render(() => (
|
|
43
|
+
<Sidebar onCollapsedChange={onCollapsedChange}>
|
|
44
|
+
<Toggler />
|
|
45
|
+
</Sidebar>
|
|
46
|
+
))
|
|
47
|
+
await userEvent.click(getByRole('button', { name: 'Toggle' }))
|
|
48
|
+
expect(onCollapsedChange).toHaveBeenCalledWith(true)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('hides the title block and shows a divider label when collapsed', () => {
|
|
52
|
+
const { container, queryByText } = render(() => (
|
|
53
|
+
<Sidebar collapsed>
|
|
54
|
+
<SidebarHeader title="Workspace" />
|
|
55
|
+
<SidebarLabel divider>General</SidebarLabel>
|
|
56
|
+
</Sidebar>
|
|
57
|
+
))
|
|
58
|
+
expect(queryByText('Workspace')).toBeNull()
|
|
59
|
+
expect(
|
|
60
|
+
container.querySelector('[data-slot="sidebar-label"] hr'),
|
|
61
|
+
).toBeTruthy()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('renders the header logo box', () => {
|
|
65
|
+
const { container } = render(() => (
|
|
66
|
+
<Sidebar>
|
|
67
|
+
<SidebarHeader title="Workspace" logo="https://example.com/logo.png" />
|
|
68
|
+
</Sidebar>
|
|
69
|
+
))
|
|
70
|
+
const img = container.querySelector('[data-slot="sidebar-header"] img')
|
|
71
|
+
expect(img?.getAttribute('src')).toBe('https://example.com/logo.png')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('falls back to the title initial when no logo is given', () => {
|
|
75
|
+
const { container } = render(() => (
|
|
76
|
+
<Sidebar>
|
|
77
|
+
<SidebarHeader title="workspace" />
|
|
78
|
+
</Sidebar>
|
|
79
|
+
))
|
|
80
|
+
expect(
|
|
81
|
+
container.querySelector('[data-slot="sidebar-header"]')?.textContent,
|
|
82
|
+
).toContain('W')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('collapses the item label to an accessible-only name', () => {
|
|
86
|
+
const { container } = render(() => (
|
|
87
|
+
<Sidebar collapsed>
|
|
88
|
+
<SidebarItem label="Home">Home</SidebarItem>
|
|
89
|
+
</Sidebar>
|
|
90
|
+
))
|
|
91
|
+
const label = container.querySelector(
|
|
92
|
+
'[data-slot="sidebar-item"] span',
|
|
93
|
+
) as HTMLElement
|
|
94
|
+
expect(label.className).toContain('sr-only')
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('SidebarSearch', () => {
|
|
99
|
+
it('is a labelled filter that reports what is typed', async () => {
|
|
100
|
+
const onInput = vi.fn()
|
|
101
|
+
render(() => (
|
|
102
|
+
<Sidebar>
|
|
103
|
+
<SidebarSearch placeholder="Filter pages" onInput={onInput} />
|
|
104
|
+
</Sidebar>
|
|
105
|
+
))
|
|
106
|
+
|
|
107
|
+
const field = screen.getByLabelText('Filter pages')
|
|
108
|
+
await userEvent.type(field, 'acc')
|
|
109
|
+
expect(onInput).toHaveBeenLastCalledWith('acc')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('hides itself when the sidebar is collapsed', () => {
|
|
113
|
+
render(() => (
|
|
114
|
+
<Sidebar collapsed>
|
|
115
|
+
<SidebarSearch placeholder="Filter pages" />
|
|
116
|
+
</Sidebar>
|
|
117
|
+
))
|
|
118
|
+
expect(screen.queryByLabelText('Filter pages')).toBeNull()
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('SidebarItem while collapsed', () => {
|
|
123
|
+
it('names itself and offers a real tooltip, not a bare title', async () => {
|
|
124
|
+
render(() => (
|
|
125
|
+
<Sidebar collapsed>
|
|
126
|
+
<SidebarItem label="Accordion" prefix={<span>icon</span>}>
|
|
127
|
+
Accordion
|
|
128
|
+
</SidebarItem>
|
|
129
|
+
</Sidebar>
|
|
130
|
+
))
|
|
131
|
+
|
|
132
|
+
const item = screen.getByRole('button', { name: 'Accordion' })
|
|
133
|
+
expect(item.getAttribute('title')).toBeNull()
|
|
134
|
+
|
|
135
|
+
await userEvent.hover(item)
|
|
136
|
+
expect(await screen.findByRole('tooltip')).toHaveTextContent('Accordion')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('keeps the label visible when it is not a rail', () => {
|
|
140
|
+
render(() => (
|
|
141
|
+
<Sidebar>
|
|
142
|
+
<SidebarItem label="Accordion">Accordion</SidebarItem>
|
|
143
|
+
</Sidebar>
|
|
144
|
+
))
|
|
145
|
+
expect(screen.getByText('Accordion')).toBeTruthy()
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('SidebarItem links (frappe-ui parity)', () => {
|
|
150
|
+
it('renders a route as a same-tab link and marks the active row', () => {
|
|
151
|
+
render(() => (
|
|
152
|
+
<Sidebar>
|
|
153
|
+
<SidebarItem route="/inbox" active>
|
|
154
|
+
Inbox
|
|
155
|
+
</SidebarItem>
|
|
156
|
+
<SidebarItem href="https://example.com">External</SidebarItem>
|
|
157
|
+
</Sidebar>
|
|
158
|
+
))
|
|
159
|
+
const inbox = screen.getByRole('link', { name: 'Inbox' })
|
|
160
|
+
expect(inbox.getAttribute('href')).toBe('/inbox')
|
|
161
|
+
expect(inbox.getAttribute('aria-current')).toBe('page')
|
|
162
|
+
expect(inbox.getAttribute('target')).toBeNull()
|
|
163
|
+
const external = screen.getByRole('link', { name: 'External' })
|
|
164
|
+
expect(external.getAttribute('target')).toBe('_blank')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('keeps the suffix beside the link so it may hold a button', () => {
|
|
168
|
+
const { container } = render(() => (
|
|
169
|
+
<Sidebar>
|
|
170
|
+
<SidebarItem
|
|
171
|
+
route="/inbox"
|
|
172
|
+
icon="lucide-inbox"
|
|
173
|
+
suffix={<button type="button">Options</button>}
|
|
174
|
+
>
|
|
175
|
+
Inbox
|
|
176
|
+
</SidebarItem>
|
|
177
|
+
</Sidebar>
|
|
178
|
+
))
|
|
179
|
+
const link = screen.getByRole('link', { name: 'Inbox' })
|
|
180
|
+
const options = screen.getByRole('button', { name: 'Options' })
|
|
181
|
+
expect(link.contains(options)).toBe(false)
|
|
182
|
+
expect(
|
|
183
|
+
container.querySelector('[data-slot="sidebar-item"] .lucide-inbox'),
|
|
184
|
+
).toBeTruthy()
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('SidebarLabel action', () => {
|
|
189
|
+
it('carries a trailing control with the label, and drops it while collapsed', () => {
|
|
190
|
+
const { unmount } = render(() => (
|
|
191
|
+
<Sidebar>
|
|
192
|
+
<SidebarLabel
|
|
193
|
+
action={
|
|
194
|
+
<button type="button" aria-label="Sort spaces">
|
|
195
|
+
Sort
|
|
196
|
+
</button>
|
|
197
|
+
}
|
|
198
|
+
>
|
|
199
|
+
Spaces
|
|
200
|
+
</SidebarLabel>
|
|
201
|
+
</Sidebar>
|
|
202
|
+
))
|
|
203
|
+
expect(screen.getByText('Spaces')).toBeTruthy()
|
|
204
|
+
expect(screen.getByRole('button', { name: 'Sort spaces' })).toBeVisible()
|
|
205
|
+
unmount()
|
|
206
|
+
|
|
207
|
+
// A collapsed sidebar has no room for the label, so the control that acts on it goes
|
|
208
|
+
// with it — an icon button beside nothing is a mystery.
|
|
209
|
+
render(() => (
|
|
210
|
+
<Sidebar collapsed>
|
|
211
|
+
<SidebarLabel action={<button type="button">Sort</button>}>
|
|
212
|
+
Spaces
|
|
213
|
+
</SidebarLabel>
|
|
214
|
+
</Sidebar>
|
|
215
|
+
))
|
|
216
|
+
expect(screen.queryByRole('button', { name: 'Sort' })).toBeNull()
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
describe('SidebarSection controlled state', () => {
|
|
221
|
+
it('honours controlled collapsed and reports toggles', async () => {
|
|
222
|
+
const onCollapsedChange = vi.fn()
|
|
223
|
+
render(() => (
|
|
224
|
+
<Sidebar>
|
|
225
|
+
<SidebarSection
|
|
226
|
+
label="Projects"
|
|
227
|
+
collapsible
|
|
228
|
+
collapsed
|
|
229
|
+
onCollapsedChange={onCollapsedChange}
|
|
230
|
+
>
|
|
231
|
+
<SidebarItem>Website</SidebarItem>
|
|
232
|
+
</SidebarSection>
|
|
233
|
+
</Sidebar>
|
|
234
|
+
))
|
|
235
|
+
// The body stays mounted and hidden, so the heading's `aria-controls`
|
|
236
|
+
// resolves in both states; hidden is the assertion, not absent.
|
|
237
|
+
expect(screen.getByText('Website')).not.toBeVisible()
|
|
238
|
+
await userEvent.click(screen.getByRole('button', { name: /Projects/ }))
|
|
239
|
+
expect(onCollapsedChange).toHaveBeenCalledWith(false)
|
|
240
|
+
// Controlled: the parent did not flip the prop, so the body stays shut.
|
|
241
|
+
expect(screen.getByText('Website')).not.toBeVisible()
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
describe('SidebarCard', () => {
|
|
246
|
+
it('hides itself when the sidebar is collapsed', () => {
|
|
247
|
+
render(() => (
|
|
248
|
+
<Sidebar collapsed>
|
|
249
|
+
<SidebarCard title="Trial ends soon" />
|
|
250
|
+
</Sidebar>
|
|
251
|
+
))
|
|
252
|
+
expect(screen.queryByText('Trial ends soon')).toBeNull()
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('hands the action a dismiss that fires onDismiss', async () => {
|
|
256
|
+
const onDismiss = vi.fn()
|
|
257
|
+
render(() => (
|
|
258
|
+
<SidebarCard
|
|
259
|
+
title="Trial ends soon"
|
|
260
|
+
onDismiss={onDismiss}
|
|
261
|
+
action={{
|
|
262
|
+
label: 'Upgrade',
|
|
263
|
+
onClick: ({ dismiss }) => dismiss(),
|
|
264
|
+
}}
|
|
265
|
+
/>
|
|
266
|
+
))
|
|
267
|
+
await userEvent.click(screen.getByRole('button', { name: 'Upgrade' }))
|
|
268
|
+
expect(onDismiss).toHaveBeenCalledOnce()
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
describe('SidebarTrigger', () => {
|
|
273
|
+
it('folds the sidebar and says so', async () => {
|
|
274
|
+
const onCollapsedChange = vi.fn()
|
|
275
|
+
render(() => (
|
|
276
|
+
<Sidebar onCollapsedChange={onCollapsedChange}>
|
|
277
|
+
<SidebarTrigger />
|
|
278
|
+
</Sidebar>
|
|
279
|
+
))
|
|
280
|
+
|
|
281
|
+
const trigger = screen.getByRole('button', { name: 'Collapse sidebar' })
|
|
282
|
+
expect(trigger.getAttribute('aria-expanded')).toBe('true')
|
|
283
|
+
|
|
284
|
+
await userEvent.click(trigger)
|
|
285
|
+
expect(onCollapsedChange).toHaveBeenCalledWith(true)
|
|
286
|
+
expect(screen.getByRole('button', { name: 'Expand sidebar' })).toBeTruthy()
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('is bound to ⌘B and Ctrl+B', async () => {
|
|
290
|
+
const onCollapsedChange = vi.fn()
|
|
291
|
+
render(() => <Sidebar onCollapsedChange={onCollapsedChange} />)
|
|
292
|
+
|
|
293
|
+
await userEvent.keyboard('{Meta>}b{/Meta}')
|
|
294
|
+
expect(onCollapsedChange).toHaveBeenLastCalledWith(true)
|
|
295
|
+
await userEvent.keyboard('{Control>}b{/Control}')
|
|
296
|
+
expect(onCollapsedChange).toHaveBeenLastCalledWith(false)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('leaves the key alone when the shortcut is off', async () => {
|
|
300
|
+
const onCollapsedChange = vi.fn()
|
|
301
|
+
render(() => (
|
|
302
|
+
<Sidebar shortcut={false} onCollapsedChange={onCollapsedChange} />
|
|
303
|
+
))
|
|
304
|
+
|
|
305
|
+
await userEvent.keyboard('{Meta>}b{/Meta}')
|
|
306
|
+
expect(onCollapsedChange).not.toHaveBeenCalled()
|
|
307
|
+
})
|
|
308
|
+
})
|