@furystack/shades-common-components 12.5.0 → 12.6.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/CHANGELOG.md +63 -0
- package/esm/components/data-grid/data-grid.d.ts +7 -1
- package/esm/components/data-grid/data-grid.d.ts.map +1 -1
- package/esm/components/data-grid/data-grid.js +1 -1
- package/esm/components/data-grid/data-grid.js.map +1 -1
- package/esm/components/data-grid/footer.d.ts +1 -0
- package/esm/components/data-grid/footer.d.ts.map +1 -1
- package/esm/components/data-grid/footer.js +8 -15
- package/esm/components/data-grid/footer.js.map +1 -1
- package/esm/components/data-grid/footer.spec.js +85 -47
- package/esm/components/data-grid/footer.spec.js.map +1 -1
- package/esm/components/grid.d.ts +3 -0
- package/esm/components/grid.d.ts.map +1 -1
- package/esm/components/grid.js +3 -0
- package/esm/components/grid.js.map +1 -1
- package/esm/components/inputs/autocomplete.d.ts +3 -0
- package/esm/components/inputs/autocomplete.d.ts.map +1 -1
- package/esm/components/inputs/autocomplete.js +3 -0
- package/esm/components/inputs/autocomplete.js.map +1 -1
- package/esm/components/list/list.d.ts +10 -0
- package/esm/components/list/list.d.ts.map +1 -1
- package/esm/components/list/list.js +23 -2
- package/esm/components/list/list.js.map +1 -1
- package/esm/components/list/list.spec.js +101 -0
- package/esm/components/list/list.spec.js.map +1 -1
- package/esm/components/markdown/markdown-input.d.ts +14 -0
- package/esm/components/markdown/markdown-input.d.ts.map +1 -1
- package/esm/components/markdown/markdown-input.js +48 -2
- package/esm/components/markdown/markdown-input.js.map +1 -1
- package/esm/components/markdown/markdown-input.spec.js +97 -0
- package/esm/components/markdown/markdown-input.spec.js.map +1 -1
- package/esm/components/suggest/index.d.ts +10 -2
- package/esm/components/suggest/index.d.ts.map +1 -1
- package/esm/components/suggest/index.js +21 -1
- package/esm/components/suggest/index.js.map +1 -1
- package/esm/components/suggest/index.spec.js +50 -0
- package/esm/components/suggest/index.spec.js.map +1 -1
- package/esm/components/wizard/index.d.ts +8 -0
- package/esm/components/wizard/index.d.ts.map +1 -1
- package/esm/components/wizard/index.js +90 -0
- package/esm/components/wizard/index.js.map +1 -1
- package/esm/components/wizard/index.spec.js +79 -2
- package/esm/components/wizard/index.spec.js.map +1 -1
- package/package.json +3 -3
- package/src/components/data-grid/data-grid.tsx +13 -2
- package/src/components/data-grid/footer.spec.tsx +104 -50
- package/src/components/data-grid/footer.tsx +25 -31
- package/src/components/grid.tsx +3 -0
- package/src/components/inputs/autocomplete.tsx +3 -0
- package/src/components/list/list.spec.tsx +173 -0
- package/src/components/list/list.tsx +56 -19
- package/src/components/markdown/markdown-input.spec.tsx +142 -0
- package/src/components/markdown/markdown-input.tsx +65 -1
- package/src/components/suggest/index.spec.tsx +83 -0
- package/src/components/suggest/index.tsx +36 -3
- package/src/components/wizard/index.spec.tsx +118 -1
- package/src/components/wizard/index.tsx +125 -0
|
@@ -64,9 +64,9 @@ describe('DataGridFooter', () => {
|
|
|
64
64
|
await flushUpdates()
|
|
65
65
|
|
|
66
66
|
const footer = document.querySelector('shade-data-grid-footer')
|
|
67
|
-
const
|
|
67
|
+
const rowsPerPageSelect = footer?.querySelector('.pager-section select')
|
|
68
68
|
|
|
69
|
-
expect(
|
|
69
|
+
expect(rowsPerPageSelect).not.toBeNull()
|
|
70
70
|
})
|
|
71
71
|
})
|
|
72
72
|
|
|
@@ -97,7 +97,7 @@ describe('DataGridFooter', () => {
|
|
|
97
97
|
})
|
|
98
98
|
})
|
|
99
99
|
|
|
100
|
-
it('should show
|
|
100
|
+
it('should show Pagination component when pagination is enabled', async () => {
|
|
101
101
|
await usingAsync(new Injector(), async (injector) => {
|
|
102
102
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
103
103
|
const service = createService([], 100)
|
|
@@ -112,12 +112,12 @@ describe('DataGridFooter', () => {
|
|
|
112
112
|
await flushUpdates()
|
|
113
113
|
|
|
114
114
|
const footer = document.querySelector('shade-data-grid-footer')
|
|
115
|
-
const
|
|
116
|
-
expect(
|
|
115
|
+
const pagination = footer?.querySelector('shade-pagination')
|
|
116
|
+
expect(pagination).not.toBeNull()
|
|
117
117
|
})
|
|
118
118
|
})
|
|
119
119
|
|
|
120
|
-
it('should hide
|
|
120
|
+
it('should hide Pagination when showing all items (Infinity)', async () => {
|
|
121
121
|
await usingAsync(new Injector(), async (injector) => {
|
|
122
122
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
123
123
|
const service = createService([], 50)
|
|
@@ -132,14 +132,12 @@ describe('DataGridFooter', () => {
|
|
|
132
132
|
await flushUpdates()
|
|
133
133
|
|
|
134
134
|
const footer = document.querySelector('shade-data-grid-footer')
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
const pageSection = sections.find((s) => s.textContent?.includes('Page'))
|
|
138
|
-
expect(pageSection).toBeUndefined()
|
|
135
|
+
const pagination = footer?.querySelector('shade-pagination')
|
|
136
|
+
expect(pagination).toBeNull()
|
|
139
137
|
})
|
|
140
138
|
})
|
|
141
139
|
|
|
142
|
-
it('should render
|
|
140
|
+
it('should render page buttons in Pagination based on data count and items per page', async () => {
|
|
143
141
|
await usingAsync(new Injector(), async (injector) => {
|
|
144
142
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
145
143
|
const service = createService([], 100)
|
|
@@ -154,19 +152,17 @@ describe('DataGridFooter', () => {
|
|
|
154
152
|
await flushUpdates()
|
|
155
153
|
|
|
156
154
|
const footer = document.querySelector('shade-data-grid-footer')
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
const parent = s.closest('.pager-section')
|
|
160
|
-
return parent?.textContent?.includes('Page')
|
|
161
|
-
})
|
|
155
|
+
const pagination = footer?.querySelector('shade-pagination')
|
|
156
|
+
expect(pagination).not.toBeNull()
|
|
162
157
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
158
|
+
const pageButtons = Array.from(pagination?.querySelectorAll('.pagination-item') ?? []).filter((btn) =>
|
|
159
|
+
btn.getAttribute('aria-label')?.startsWith('Go to page'),
|
|
160
|
+
)
|
|
161
|
+
expect(pageButtons.length).toBe(4)
|
|
166
162
|
})
|
|
167
163
|
})
|
|
168
164
|
|
|
169
|
-
it('should update findOptions when page is changed', async () => {
|
|
165
|
+
it('should update findOptions when page is changed via Pagination', async () => {
|
|
170
166
|
await usingAsync(new Injector(), async (injector) => {
|
|
171
167
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
172
168
|
const service = createService([], 100)
|
|
@@ -181,21 +177,16 @@ describe('DataGridFooter', () => {
|
|
|
181
177
|
await flushUpdates()
|
|
182
178
|
|
|
183
179
|
const footer = document.querySelector('shade-data-grid-footer')
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const parent = s.closest('.pager-section')
|
|
187
|
-
return parent?.textContent?.includes('Page')
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
expect(pageSelect).toBeDefined()
|
|
180
|
+
const pagination = footer?.querySelector('shade-pagination')
|
|
181
|
+
const nextButton = pagination?.querySelector('[aria-label="Go to next page"]') as HTMLButtonElement | null
|
|
191
182
|
|
|
192
|
-
|
|
193
|
-
|
|
183
|
+
expect(nextButton).not.toBeNull()
|
|
184
|
+
nextButton!.click()
|
|
194
185
|
|
|
195
186
|
await flushUpdates()
|
|
196
187
|
|
|
197
188
|
const updatedOptions = findOptions.getValue()
|
|
198
|
-
expect(updatedOptions.skip).toBe(
|
|
189
|
+
expect(updatedOptions.skip).toBe(10)
|
|
199
190
|
})
|
|
200
191
|
})
|
|
201
192
|
|
|
@@ -266,7 +257,7 @@ describe('DataGridFooter', () => {
|
|
|
266
257
|
})
|
|
267
258
|
})
|
|
268
259
|
|
|
269
|
-
it('should
|
|
260
|
+
it('should highlight the correct current page in Pagination', async () => {
|
|
270
261
|
await usingAsync(new Injector(), async (injector) => {
|
|
271
262
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
272
263
|
const service = createService([], 100)
|
|
@@ -281,14 +272,11 @@ describe('DataGridFooter', () => {
|
|
|
281
272
|
await flushUpdates()
|
|
282
273
|
|
|
283
274
|
const footer = document.querySelector('shade-data-grid-footer')
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
const parent = s.closest('.pager-section')
|
|
287
|
-
return parent?.textContent?.includes('Page')
|
|
288
|
-
})
|
|
275
|
+
const pagination = footer?.querySelector('shade-pagination')
|
|
276
|
+
const selectedButton = pagination?.querySelector('.pagination-item[data-selected]')
|
|
289
277
|
|
|
290
|
-
expect(
|
|
291
|
-
expect(
|
|
278
|
+
expect(selectedButton).not.toBeNull()
|
|
279
|
+
expect(selectedButton?.textContent?.trim()).toBe('4')
|
|
292
280
|
})
|
|
293
281
|
})
|
|
294
282
|
|
|
@@ -321,7 +309,7 @@ describe('DataGridFooter', () => {
|
|
|
321
309
|
it('should react to data count changes', async () => {
|
|
322
310
|
await usingAsync(new Injector(), async (injector) => {
|
|
323
311
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
324
|
-
const service = createService([],
|
|
312
|
+
const service = createService([], 30)
|
|
325
313
|
const findOptions = createFindOptions(10, 0)
|
|
326
314
|
|
|
327
315
|
initializeShadeRoot({
|
|
@@ -333,26 +321,92 @@ describe('DataGridFooter', () => {
|
|
|
333
321
|
await flushUpdates()
|
|
334
322
|
|
|
335
323
|
let footer = document.querySelector('shade-data-grid-footer')
|
|
336
|
-
let
|
|
337
|
-
let
|
|
338
|
-
|
|
324
|
+
let pagination = footer?.querySelector('shade-pagination')
|
|
325
|
+
let pageButtons = Array.from(pagination?.querySelectorAll('.pagination-item') ?? []).filter((btn) =>
|
|
326
|
+
btn.getAttribute('aria-label')?.startsWith('Go to page'),
|
|
327
|
+
)
|
|
328
|
+
expect(pageButtons.length).toBe(3)
|
|
339
329
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
service.data.setValue({ entries: [], count: 100 })
|
|
330
|
+
service.data.setValue({ entries: [], count: 50 })
|
|
343
331
|
|
|
344
332
|
await flushUpdates()
|
|
345
333
|
|
|
346
334
|
footer = document.querySelector('shade-data-grid-footer')
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
expect(
|
|
335
|
+
pagination = footer?.querySelector('shade-pagination')
|
|
336
|
+
pageButtons = Array.from(pagination?.querySelectorAll('.pagination-item') ?? []).filter((btn) =>
|
|
337
|
+
btn.getAttribute('aria-label')?.startsWith('Go to page'),
|
|
338
|
+
)
|
|
339
|
+
expect(pageButtons.length).toBe(5)
|
|
352
340
|
})
|
|
353
341
|
})
|
|
354
342
|
|
|
355
343
|
it('should export dataGridItemsPerPage constant', () => {
|
|
356
344
|
expect(dataGridItemsPerPage).toEqual([10, 20, 25, 50, 100, Infinity])
|
|
357
345
|
})
|
|
346
|
+
|
|
347
|
+
it('should render custom paginationOptions', async () => {
|
|
348
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
349
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
350
|
+
const service = createService()
|
|
351
|
+
const findOptions = createFindOptions()
|
|
352
|
+
|
|
353
|
+
initializeShadeRoot({
|
|
354
|
+
injector,
|
|
355
|
+
rootElement,
|
|
356
|
+
jsxElement: <DataGridFooter service={service} findOptions={findOptions} paginationOptions={[5, 15, 30]} />,
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
await flushUpdates()
|
|
360
|
+
|
|
361
|
+
const footer = document.querySelector('shade-data-grid-footer')
|
|
362
|
+
const itemsPerPageSelect = footer?.querySelector('.pager-section select')
|
|
363
|
+
|
|
364
|
+
expect(itemsPerPageSelect).not.toBeNull()
|
|
365
|
+
const options = itemsPerPageSelect?.querySelectorAll('option')
|
|
366
|
+
expect(options?.length).toBe(3)
|
|
367
|
+
expect(options?.[0]?.textContent).toBe('5')
|
|
368
|
+
expect(options?.[1]?.textContent).toBe('15')
|
|
369
|
+
expect(options?.[2]?.textContent).toBe('30')
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('should hide the rows-per-page selector when only one paginationOption is provided', async () => {
|
|
374
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
375
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
376
|
+
const service = createService([], 50)
|
|
377
|
+
const findOptions = createFindOptions(10, 0)
|
|
378
|
+
|
|
379
|
+
initializeShadeRoot({
|
|
380
|
+
injector,
|
|
381
|
+
rootElement,
|
|
382
|
+
jsxElement: <DataGridFooter service={service} findOptions={findOptions} paginationOptions={[10]} />,
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
await flushUpdates()
|
|
386
|
+
|
|
387
|
+
const footer = document.querySelector('shade-data-grid-footer')
|
|
388
|
+
const pagerSection = footer?.querySelector('.pager-section')
|
|
389
|
+
expect(pagerSection).toBeNull()
|
|
390
|
+
})
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('should use default dataGridItemsPerPage when paginationOptions is not provided', async () => {
|
|
394
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
395
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
396
|
+
const service = createService()
|
|
397
|
+
const findOptions = createFindOptions()
|
|
398
|
+
|
|
399
|
+
initializeShadeRoot({
|
|
400
|
+
injector,
|
|
401
|
+
rootElement,
|
|
402
|
+
jsxElement: <DataGridFooter service={service} findOptions={findOptions} />,
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
await flushUpdates()
|
|
406
|
+
|
|
407
|
+
const footer = document.querySelector('shade-data-grid-footer')
|
|
408
|
+
const options = footer?.querySelectorAll('.pager-section select option')
|
|
409
|
+
expect(options?.length).toBe(dataGridItemsPerPage.length)
|
|
410
|
+
})
|
|
411
|
+
})
|
|
358
412
|
})
|
|
@@ -3,12 +3,14 @@ import { Shade, createComponent } from '@furystack/shades'
|
|
|
3
3
|
import type { ObservableValue } from '@furystack/utils'
|
|
4
4
|
import type { CollectionService } from '../../services/collection-service.js'
|
|
5
5
|
import { cssVariableTheme } from '../../services/css-variable-theme.js'
|
|
6
|
+
import { Pagination } from '../pagination.js'
|
|
6
7
|
|
|
7
8
|
export const dataGridItemsPerPage = [10, 20, 25, 50, 100, Infinity]
|
|
8
9
|
|
|
9
10
|
export const DataGridFooter: <T>(props: {
|
|
10
11
|
service: CollectionService<T>
|
|
11
12
|
findOptions: ObservableValue<FindOptions<T, Array<keyof T>>>
|
|
13
|
+
paginationOptions?: number[]
|
|
12
14
|
}) => JSX.Element = Shade({
|
|
13
15
|
shadowDomName: 'shade-data-grid-footer',
|
|
14
16
|
css: {
|
|
@@ -41,7 +43,7 @@ export const DataGridFooter: <T>(props: {
|
|
|
41
43
|
},
|
|
42
44
|
},
|
|
43
45
|
render: ({ props, useObservable }) => {
|
|
44
|
-
const { service, findOptions } = props
|
|
46
|
+
const { service, findOptions, paginationOptions = dataGridItemsPerPage } = props
|
|
45
47
|
const [currentData] = useObservable('dataUpdater', service.data)
|
|
46
48
|
const [currentOptions, setCurrentOptions] = useObservable('optionsUpdater', findOptions, {
|
|
47
49
|
filter: (newValue, oldValue) => {
|
|
@@ -54,49 +56,41 @@ export const DataGridFooter: <T>(props: {
|
|
|
54
56
|
const currentPage = Math.ceil(skip) / (top || 1)
|
|
55
57
|
const currentEntriesPerPage = top
|
|
56
58
|
|
|
57
|
-
const
|
|
58
|
-
.fill(0)
|
|
59
|
-
.map((_, index) => index)
|
|
59
|
+
const pageCount = Math.ceil(currentData.count / (currentOptions.top || Infinity))
|
|
60
60
|
|
|
61
61
|
return (
|
|
62
62
|
<div className="pager">
|
|
63
|
-
{currentEntriesPerPage !== Infinity && (
|
|
63
|
+
{currentEntriesPerPage !== Infinity && pageCount > 1 && (
|
|
64
|
+
<Pagination
|
|
65
|
+
count={pageCount}
|
|
66
|
+
page={currentPage + 1}
|
|
67
|
+
size="small"
|
|
68
|
+
onPageChange={(newPage) => {
|
|
69
|
+
setCurrentOptions({ ...currentOptions, skip: (currentOptions.top || 0) * (newPage - 1) })
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
{paginationOptions.length > 1 && (
|
|
64
74
|
<div className="pager-section">
|
|
65
|
-
<span>
|
|
75
|
+
<span>Rows per page</span>
|
|
66
76
|
<select
|
|
67
77
|
onchange={(ev) => {
|
|
68
|
-
const value = parseInt((ev.
|
|
69
|
-
setCurrentOptions({
|
|
78
|
+
const value = parseInt((ev.currentTarget as HTMLInputElement).value, 10)
|
|
79
|
+
setCurrentOptions({
|
|
80
|
+
...currentOptions,
|
|
81
|
+
top: value,
|
|
82
|
+
skip: currentPage * value,
|
|
83
|
+
})
|
|
70
84
|
}}
|
|
71
85
|
>
|
|
72
|
-
{
|
|
73
|
-
<option value={
|
|
74
|
-
{
|
|
86
|
+
{paginationOptions.map((no) => (
|
|
87
|
+
<option value={no.toString()} selected={no === currentEntriesPerPage}>
|
|
88
|
+
{no === Infinity ? 'All' : no.toString()}
|
|
75
89
|
</option>
|
|
76
90
|
))}
|
|
77
91
|
</select>
|
|
78
|
-
<span>of {pages.length}</span>
|
|
79
92
|
</div>
|
|
80
93
|
)}
|
|
81
|
-
<div className="pager-section">
|
|
82
|
-
<span>Rows per page</span>
|
|
83
|
-
<select
|
|
84
|
-
onchange={(ev) => {
|
|
85
|
-
const value = parseInt((ev.currentTarget as HTMLInputElement).value, 10)
|
|
86
|
-
setCurrentOptions({
|
|
87
|
-
...currentOptions,
|
|
88
|
-
top: value,
|
|
89
|
-
skip: currentPage * value,
|
|
90
|
-
})
|
|
91
|
-
}}
|
|
92
|
-
>
|
|
93
|
-
{dataGridItemsPerPage.map((no) => (
|
|
94
|
-
<option value={no.toString()} selected={no === currentEntriesPerPage}>
|
|
95
|
-
{no === Infinity ? 'All' : no.toString()}
|
|
96
|
-
</option>
|
|
97
|
-
))}
|
|
98
|
-
</select>
|
|
99
|
-
</div>
|
|
100
94
|
</div>
|
|
101
95
|
)
|
|
102
96
|
},
|
package/src/components/grid.tsx
CHANGED
|
@@ -23,6 +23,9 @@ export type RowCells<T, Columns extends string> = {
|
|
|
23
23
|
[TKey in Columns | 'default']?: (element: T, column: Columns) => JSX.Element
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* @deprecated Use `DataGrid` instead. This component will be removed in a future version.
|
|
28
|
+
*/
|
|
26
29
|
export const Grid: <T, Column extends string>(props: GridProps<T, Column>, children: ChildrenList) => JSX.Element<any> =
|
|
27
30
|
Shade({
|
|
28
31
|
shadowDomName: 'shade-grid',
|
|
@@ -2,6 +2,9 @@ import { Shade, createComponent } from '@furystack/shades'
|
|
|
2
2
|
import type { TextInputProps } from './input.js'
|
|
3
3
|
import { Input } from './input.js'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @deprecated Use `Suggest` with the `suggestions` prop instead. This component will be removed in a future version.
|
|
7
|
+
*/
|
|
5
8
|
export const Autocomplete = Shade<{
|
|
6
9
|
inputProps?: TextInputProps
|
|
7
10
|
suggestions: string[]
|
|
@@ -783,6 +783,179 @@ describe('List', () => {
|
|
|
783
783
|
})
|
|
784
784
|
})
|
|
785
785
|
|
|
786
|
+
describe('pagination', () => {
|
|
787
|
+
const manyItems: TestItem[] = Array.from({ length: 25 }, (_, i) => ({ id: i + 1, name: `Item ${i + 1}` }))
|
|
788
|
+
|
|
789
|
+
it('should render only current page items when pagination is provided', async () => {
|
|
790
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
791
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
792
|
+
const service = createTestService()
|
|
793
|
+
|
|
794
|
+
initializeShadeRoot({
|
|
795
|
+
injector,
|
|
796
|
+
rootElement,
|
|
797
|
+
jsxElement: (
|
|
798
|
+
<List<TestItem>
|
|
799
|
+
items={manyItems}
|
|
800
|
+
listService={service}
|
|
801
|
+
renderItem={(item) => <span>{item.name}</span>}
|
|
802
|
+
pagination={{ itemsPerPage: 10, page: 1, onPageChange: () => {} }}
|
|
803
|
+
/>
|
|
804
|
+
),
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
await flushUpdates()
|
|
808
|
+
|
|
809
|
+
const list = document.querySelector('shade-list')
|
|
810
|
+
const listItems = list?.querySelectorAll('shade-list-item')
|
|
811
|
+
expect(listItems?.length).toBe(10)
|
|
812
|
+
|
|
813
|
+
service[Symbol.dispose]()
|
|
814
|
+
})
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
it('should render the Pagination component', async () => {
|
|
818
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
819
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
820
|
+
const service = createTestService()
|
|
821
|
+
|
|
822
|
+
initializeShadeRoot({
|
|
823
|
+
injector,
|
|
824
|
+
rootElement,
|
|
825
|
+
jsxElement: (
|
|
826
|
+
<List<TestItem>
|
|
827
|
+
items={manyItems}
|
|
828
|
+
listService={service}
|
|
829
|
+
renderItem={(item) => <span>{item.name}</span>}
|
|
830
|
+
pagination={{ itemsPerPage: 10, page: 1, onPageChange: () => {} }}
|
|
831
|
+
/>
|
|
832
|
+
),
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
await flushUpdates()
|
|
836
|
+
|
|
837
|
+
const pagination = document.querySelector('shade-list shade-pagination')
|
|
838
|
+
expect(pagination).not.toBeNull()
|
|
839
|
+
|
|
840
|
+
service[Symbol.dispose]()
|
|
841
|
+
})
|
|
842
|
+
})
|
|
843
|
+
|
|
844
|
+
it('should show last page items correctly', async () => {
|
|
845
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
846
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
847
|
+
const service = createTestService()
|
|
848
|
+
|
|
849
|
+
initializeShadeRoot({
|
|
850
|
+
injector,
|
|
851
|
+
rootElement,
|
|
852
|
+
jsxElement: (
|
|
853
|
+
<List<TestItem>
|
|
854
|
+
items={manyItems}
|
|
855
|
+
listService={service}
|
|
856
|
+
renderItem={(item) => <span>{item.name}</span>}
|
|
857
|
+
pagination={{ itemsPerPage: 10, page: 3, onPageChange: () => {} }}
|
|
858
|
+
/>
|
|
859
|
+
),
|
|
860
|
+
})
|
|
861
|
+
|
|
862
|
+
await flushUpdates()
|
|
863
|
+
|
|
864
|
+
const list = document.querySelector('shade-list')
|
|
865
|
+
const listItems = list?.querySelectorAll('shade-list-item')
|
|
866
|
+
expect(listItems?.length).toBe(5)
|
|
867
|
+
|
|
868
|
+
service[Symbol.dispose]()
|
|
869
|
+
})
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
it('should not render Pagination when all items fit on one page', async () => {
|
|
873
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
874
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
875
|
+
const service = createTestService()
|
|
876
|
+
|
|
877
|
+
initializeShadeRoot({
|
|
878
|
+
injector,
|
|
879
|
+
rootElement,
|
|
880
|
+
jsxElement: (
|
|
881
|
+
<List<TestItem>
|
|
882
|
+
items={testItems}
|
|
883
|
+
listService={service}
|
|
884
|
+
renderItem={(item) => <span>{item.name}</span>}
|
|
885
|
+
pagination={{ itemsPerPage: 10, page: 1, onPageChange: () => {} }}
|
|
886
|
+
/>
|
|
887
|
+
),
|
|
888
|
+
})
|
|
889
|
+
|
|
890
|
+
await flushUpdates()
|
|
891
|
+
|
|
892
|
+
const pagination = document.querySelector('shade-list shade-pagination')
|
|
893
|
+
expect(pagination).toBeNull()
|
|
894
|
+
|
|
895
|
+
service[Symbol.dispose]()
|
|
896
|
+
})
|
|
897
|
+
})
|
|
898
|
+
|
|
899
|
+
it('should call onPageChange when a pagination button is clicked', async () => {
|
|
900
|
+
const onPageChange = vi.fn()
|
|
901
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
902
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
903
|
+
const service = createTestService()
|
|
904
|
+
|
|
905
|
+
initializeShadeRoot({
|
|
906
|
+
injector,
|
|
907
|
+
rootElement,
|
|
908
|
+
jsxElement: (
|
|
909
|
+
<List<TestItem>
|
|
910
|
+
items={manyItems}
|
|
911
|
+
listService={service}
|
|
912
|
+
renderItem={(item) => <span>{item.name}</span>}
|
|
913
|
+
pagination={{ itemsPerPage: 10, page: 1, onPageChange }}
|
|
914
|
+
/>
|
|
915
|
+
),
|
|
916
|
+
})
|
|
917
|
+
|
|
918
|
+
await flushUpdates()
|
|
919
|
+
|
|
920
|
+
const nextButton = document.querySelector(
|
|
921
|
+
'shade-list shade-pagination [aria-label="Go to next page"]',
|
|
922
|
+
) as HTMLButtonElement
|
|
923
|
+
expect(nextButton).not.toBeNull()
|
|
924
|
+
nextButton.click()
|
|
925
|
+
|
|
926
|
+
expect(onPageChange).toHaveBeenCalledWith(2)
|
|
927
|
+
|
|
928
|
+
service[Symbol.dispose]()
|
|
929
|
+
})
|
|
930
|
+
})
|
|
931
|
+
|
|
932
|
+
it('should render all items when pagination is not provided', async () => {
|
|
933
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
934
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
935
|
+
const service = createTestService()
|
|
936
|
+
|
|
937
|
+
initializeShadeRoot({
|
|
938
|
+
injector,
|
|
939
|
+
rootElement,
|
|
940
|
+
jsxElement: (
|
|
941
|
+
<List<TestItem> items={manyItems} listService={service} renderItem={(item) => <span>{item.name}</span>} />
|
|
942
|
+
),
|
|
943
|
+
})
|
|
944
|
+
|
|
945
|
+
await flushUpdates()
|
|
946
|
+
|
|
947
|
+
const list = document.querySelector('shade-list')
|
|
948
|
+
const listItems = list?.querySelectorAll('shade-list-item')
|
|
949
|
+
expect(listItems?.length).toBe(25)
|
|
950
|
+
|
|
951
|
+
const pagination = document.querySelector('shade-list shade-pagination')
|
|
952
|
+
expect(pagination).toBeNull()
|
|
953
|
+
|
|
954
|
+
service[Symbol.dispose]()
|
|
955
|
+
})
|
|
956
|
+
})
|
|
957
|
+
})
|
|
958
|
+
|
|
786
959
|
describe('keyboard listener cleanup', () => {
|
|
787
960
|
it('should remove keyboard listener when component is disconnected', async () => {
|
|
788
961
|
await usingAsync(new Injector(), async (injector) => {
|
|
@@ -2,6 +2,7 @@ import type { ChildrenList, PartialElement } from '@furystack/shades'
|
|
|
2
2
|
import { createComponent, Shade } from '@furystack/shades'
|
|
3
3
|
import { ClickAwayService } from '../../services/click-away-service.js'
|
|
4
4
|
import type { ListService } from '../../services/list-service.js'
|
|
5
|
+
import { Pagination } from '../pagination.js'
|
|
5
6
|
import { ListItem } from './list-item.js'
|
|
6
7
|
|
|
7
8
|
export type ListItemState = {
|
|
@@ -9,6 +10,15 @@ export type ListItemState = {
|
|
|
9
10
|
isSelected: boolean
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
export type ListPaginationProps = {
|
|
14
|
+
/** Number of items to display per page */
|
|
15
|
+
itemsPerPage: number
|
|
16
|
+
/** Current page (1-indexed) */
|
|
17
|
+
page: number
|
|
18
|
+
/** Callback fired when the page changes */
|
|
19
|
+
onPageChange: (page: number) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
export type ListProps<T> = {
|
|
13
23
|
items: T[]
|
|
14
24
|
listService: ListService<T>
|
|
@@ -18,6 +28,8 @@ export type ListProps<T> = {
|
|
|
18
28
|
variant?: 'contained' | 'outlined'
|
|
19
29
|
onItemActivate?: (item: T) => void
|
|
20
30
|
onSelectionChange?: (selected: T[]) => void
|
|
31
|
+
/** Optional pagination configuration. When provided, items are sliced and a Pagination control is rendered. */
|
|
32
|
+
pagination?: ListPaginationProps
|
|
21
33
|
} & PartialElement<HTMLDivElement>
|
|
22
34
|
|
|
23
35
|
export const List: <T>(props: ListProps<T>, children: ChildrenList) => JSX.Element<any> = Shade({
|
|
@@ -26,6 +38,11 @@ export const List: <T>(props: ListProps<T>, children: ChildrenList) => JSX.Eleme
|
|
|
26
38
|
display: 'block',
|
|
27
39
|
width: '100%',
|
|
28
40
|
overflow: 'auto',
|
|
41
|
+
'& .shade-list-pagination': {
|
|
42
|
+
display: 'flex',
|
|
43
|
+
justifyContent: 'center',
|
|
44
|
+
padding: '8px 0',
|
|
45
|
+
},
|
|
29
46
|
},
|
|
30
47
|
render: ({ props, useDisposable, useHostProps, useRef }) => {
|
|
31
48
|
const wrapperRef = useRef<HTMLDivElement>('listWrapper')
|
|
@@ -45,7 +62,20 @@ export const List: <T>(props: ListProps<T>, children: ChildrenList) => JSX.Eleme
|
|
|
45
62
|
return { [Symbol.dispose]: () => window.removeEventListener('keydown', listener) }
|
|
46
63
|
})
|
|
47
64
|
|
|
48
|
-
props
|
|
65
|
+
const { pagination } = props
|
|
66
|
+
let visibleItems: typeof props.items
|
|
67
|
+
let pageCount = 1
|
|
68
|
+
|
|
69
|
+
if (pagination) {
|
|
70
|
+
const { itemsPerPage, page } = pagination
|
|
71
|
+
pageCount = Math.ceil(props.items.length / itemsPerPage)
|
|
72
|
+
const startIndex = (page - 1) * itemsPerPage
|
|
73
|
+
visibleItems = props.items.slice(startIndex, startIndex + itemsPerPage)
|
|
74
|
+
} else {
|
|
75
|
+
visibleItems = props.items
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
props.listService.items.setValue(visibleItems)
|
|
49
79
|
|
|
50
80
|
useDisposable(
|
|
51
81
|
'clickAway',
|
|
@@ -69,24 +99,31 @@ export const List: <T>(props: ListProps<T>, children: ChildrenList) => JSX.Eleme
|
|
|
69
99
|
})
|
|
70
100
|
|
|
71
101
|
return (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
<>
|
|
103
|
+
<div
|
|
104
|
+
ref={wrapperRef}
|
|
105
|
+
role="listbox"
|
|
106
|
+
ariaMultiSelectable="true"
|
|
107
|
+
className="shade-list-wrapper"
|
|
108
|
+
onclick={() => props.listService.hasFocus.setValue(true)}
|
|
109
|
+
>
|
|
110
|
+
{visibleItems.map((item) => (
|
|
111
|
+
<ListItem
|
|
112
|
+
item={item}
|
|
113
|
+
listService={props.listService}
|
|
114
|
+
renderItem={props.renderItem}
|
|
115
|
+
renderIcon={props.renderIcon}
|
|
116
|
+
renderSecondaryActions={props.renderSecondaryActions}
|
|
117
|
+
onActivate={props.onItemActivate}
|
|
118
|
+
/>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
{pagination && pageCount > 1 && (
|
|
122
|
+
<div className="shade-list-pagination">
|
|
123
|
+
<Pagination count={pageCount} page={pagination.page} onPageChange={pagination.onPageChange} size="small" />
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</>
|
|
90
127
|
)
|
|
91
128
|
},
|
|
92
129
|
})
|