@charcuterie/ui 3.28.1 → 3.29.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/dist/SearchInput/SearchInput.d.ts +22 -0
- package/dist/SearchInput/SearchInput.d.ts.map +1 -0
- package/dist/SearchInput/SearchInput.js +36 -0
- package/dist/SearchInput/SearchInput.js.map +1 -0
- package/dist/VirtualizedGrid/ElementGrid.d.ts +7 -0
- package/dist/VirtualizedGrid/ElementGrid.d.ts.map +1 -0
- package/dist/VirtualizedGrid/ElementGrid.js +19 -0
- package/dist/VirtualizedGrid/ElementGrid.js.map +1 -0
- package/dist/VirtualizedGrid/GridRows.d.ts +4 -0
- package/dist/VirtualizedGrid/GridRows.d.ts.map +1 -0
- package/dist/VirtualizedGrid/GridRows.js +40 -0
- package/dist/VirtualizedGrid/GridRows.js.map +1 -0
- package/dist/VirtualizedGrid/VirtualizedGrid.d.ts +6 -8
- package/dist/VirtualizedGrid/VirtualizedGrid.d.ts.map +1 -1
- package/dist/VirtualizedGrid/VirtualizedGrid.js +30 -87
- package/dist/VirtualizedGrid/VirtualizedGrid.js.map +1 -1
- package/dist/VirtualizedGrid/WindowGrid.d.ts +3 -0
- package/dist/VirtualizedGrid/WindowGrid.d.ts.map +1 -0
- package/dist/VirtualizedGrid/WindowGrid.js +20 -0
- package/dist/VirtualizedGrid/WindowGrid.js.map +1 -0
- package/dist/VirtualizedGrid/virtualizedGridScroll.d.ts +25 -0
- package/dist/VirtualizedGrid/virtualizedGridScroll.d.ts.map +1 -0
- package/dist/VirtualizedGrid/virtualizedGridScroll.js +50 -0
- package/dist/VirtualizedGrid/virtualizedGridScroll.js.map +1 -0
- package/dist/VirtualizedGrid/virtualizedGridTypes.d.ts +27 -0
- package/dist/VirtualizedGrid/virtualizedGridTypes.d.ts.map +1 -0
- package/dist/VirtualizedGrid/virtualizedGridTypes.js +2 -0
- package/dist/VirtualizedGrid/virtualizedGridTypes.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/SearchInput/SearchInput.mdx +15 -0
- package/src/SearchInput/SearchInput.stories.tsx +136 -0
- package/src/SearchInput/SearchInput.test.tsx +28 -0
- package/src/SearchInput/SearchInput.tsx +105 -0
- package/src/VirtualizedGrid/ElementGrid.tsx +45 -0
- package/src/VirtualizedGrid/GridRows.tsx +90 -0
- package/src/VirtualizedGrid/VirtualizedGrid.mdx +9 -8
- package/src/VirtualizedGrid/VirtualizedGrid.stories.tsx +26 -0
- package/src/VirtualizedGrid/VirtualizedGrid.test.tsx +60 -1
- package/src/VirtualizedGrid/VirtualizedGrid.tsx +42 -142
- package/src/VirtualizedGrid/WindowGrid.tsx +39 -0
- package/src/VirtualizedGrid/virtualizedGridScroll.ts +69 -0
- package/src/VirtualizedGrid/virtualizedGridTypes.ts +32 -0
- package/src/index.ts +2 -0
- package/src/sourceRules.test.ts +1 -1
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { useWindowVirtualizer } from "@tanstack/react-virtual"
|
|
2
1
|
import type { ReactNode } from "react"
|
|
3
|
-
import {
|
|
4
|
-
useCallback,
|
|
5
|
-
useLayoutEffect,
|
|
6
|
-
useRef,
|
|
7
|
-
useState,
|
|
8
|
-
} from "react"
|
|
2
|
+
import { useLayoutEffect, useState } from "react"
|
|
9
3
|
|
|
10
|
-
import { getContentMaxInlineSize } from "../AdaptiveGrid/chooseColumns.ts"
|
|
11
4
|
import type { BlockSizeResolver } from "../AdaptiveGrid/useAdaptiveColumns.ts"
|
|
12
5
|
import { useAdaptiveColumns } from "../AdaptiveGrid/useAdaptiveColumns.ts"
|
|
13
6
|
import { toClassName } from "../toClassName.ts"
|
|
7
|
+
import { ElementGrid } from "./ElementGrid.tsx"
|
|
14
8
|
import { DEFAULT_GRID_GAP_PX } from "./gridGap.ts"
|
|
9
|
+
import { findScrollOwner } from "./virtualizedGridScroll.ts"
|
|
10
|
+
import type {
|
|
11
|
+
GridWindowProps,
|
|
12
|
+
ScrollOwner,
|
|
13
|
+
} from "./virtualizedGridTypes.ts"
|
|
14
|
+
import { WindowGrid } from "./WindowGrid.tsx"
|
|
15
15
|
|
|
16
16
|
export type VirtualizedGridProps<Item> = {
|
|
17
17
|
/**
|
|
@@ -140,15 +140,13 @@ export type VirtualizedGridProps<Item> = {
|
|
|
140
140
|
* idea of the distance from one row to the next and the browser's
|
|
141
141
|
* the same number.
|
|
142
142
|
*
|
|
143
|
-
* ###
|
|
143
|
+
* ### It follows the page's existing scroll owner
|
|
144
144
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* `
|
|
149
|
-
*
|
|
150
|
-
* to be told how far down the document the grid starts, which is
|
|
151
|
-
* measured below.
|
|
145
|
+
* A standalone grid follows the browser window. Inside `Shell`, it
|
|
146
|
+
* follows `Main`, which is the shell's one vertical scroll region.
|
|
147
|
+
* It never creates a scroll box of its own. The cost is
|
|
148
|
+
* `scrollMargin`: the virtualizer has to be told how far down its
|
|
149
|
+
* scroll owner the grid starts, which is measured below.
|
|
152
150
|
*
|
|
153
151
|
* ### What windowing costs, and what is done about it
|
|
154
152
|
*
|
|
@@ -188,85 +186,38 @@ export const VirtualizedGrid = <Item,>({
|
|
|
188
186
|
|
|
189
187
|
const drawnColumns = columns ?? layout.columns
|
|
190
188
|
|
|
191
|
-
// Folded from what is DRAWN rather than read off the hook, so an
|
|
192
|
-
// override widens the page cap with it — the same rule
|
|
193
|
-
// `AdaptiveGrid` follows, and wrong in the same way if it does
|
|
194
|
-
// not.
|
|
195
|
-
const contentMaxInlineSize = getContentMaxInlineSize({
|
|
196
|
-
columns: drawnColumns,
|
|
197
|
-
})
|
|
198
|
-
|
|
199
189
|
const rowCount = Math.ceil(items.length / drawnColumns)
|
|
200
190
|
|
|
201
191
|
/**
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* the top of the page, and the whole list sits shifted by the
|
|
207
|
-
* height of the header above it.
|
|
208
|
-
*
|
|
209
|
-
* State and not a ref, because it feeds `scrollMargin` and a
|
|
210
|
-
* change has to re-render. Measured on every commit rather than
|
|
211
|
-
* on a dependency list: a filter row that grows a line moves the
|
|
212
|
-
* grid down the page, and nothing in this component's own props
|
|
213
|
-
* would report that.
|
|
192
|
+
* Main owns vertical scrolling inside an app Shell. Older pages
|
|
193
|
+
* and a standalone grid still use the window. Resolve the owner
|
|
194
|
+
* from the actual tree rather than making every app wire a ref
|
|
195
|
+
* between two Charcuterie components.
|
|
214
196
|
*/
|
|
215
|
-
const
|
|
216
|
-
|
|
197
|
+
const [scrollOwner, setScrollOwner] =
|
|
198
|
+
useState<ScrollOwner | null>(null)
|
|
217
199
|
|
|
218
200
|
useLayoutEffect(() => {
|
|
219
|
-
const element =
|
|
201
|
+
const element = layout.containerRef.current
|
|
220
202
|
|
|
221
203
|
if (!element) {
|
|
222
204
|
return
|
|
223
205
|
}
|
|
224
206
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
setScrollMargin((current) =>
|
|
229
|
-
current === next ? current : next,
|
|
230
|
-
)
|
|
231
|
-
})
|
|
207
|
+
setScrollOwner(findScrollOwner(element))
|
|
208
|
+
}, [layout.containerRef])
|
|
232
209
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
[gap],
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
const rowVirtualizer = useWindowVirtualizer({
|
|
248
|
-
count: rowCount,
|
|
249
|
-
estimateSize: () => itemBlockSize + gap,
|
|
250
|
-
measureElement: measureRow,
|
|
251
|
-
overscan: overscanRows,
|
|
252
|
-
scrollMargin,
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
const virtualRows = rowVirtualizer.getVirtualItems()
|
|
256
|
-
|
|
257
|
-
const firstRow = virtualRows[0]
|
|
258
|
-
const lastRow = virtualRows[virtualRows.length - 1]
|
|
259
|
-
|
|
260
|
-
// Everything scrolled past, and everything still to come. The two
|
|
261
|
-
// numbers that stand in for the rows that do not exist.
|
|
262
|
-
const paddingBlockStart = firstRow?.start ?? 0
|
|
263
|
-
|
|
264
|
-
const paddingBlockEnd = lastRow
|
|
265
|
-
? Math.max(
|
|
266
|
-
0,
|
|
267
|
-
rowVirtualizer.getTotalSize() - lastRow.end,
|
|
268
|
-
)
|
|
269
|
-
: 0
|
|
210
|
+
const gridProps: GridWindowProps<Item> = {
|
|
211
|
+
drawnColumns,
|
|
212
|
+
gap,
|
|
213
|
+
getItemKey,
|
|
214
|
+
itemBlockSize,
|
|
215
|
+
items,
|
|
216
|
+
label,
|
|
217
|
+
overscanRows,
|
|
218
|
+
renderItem,
|
|
219
|
+
rowCount,
|
|
220
|
+
}
|
|
270
221
|
|
|
271
222
|
return (
|
|
272
223
|
<div
|
|
@@ -276,65 +227,14 @@ export const VirtualizedGrid = <Item,>({
|
|
|
276
227
|
)}
|
|
277
228
|
ref={layout.containerRef}
|
|
278
229
|
>
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
// Offset by `scrollMargin` because `start` is measured
|
|
288
|
-
// from the top of the document, and this padding is
|
|
289
|
-
// measured from the top of the grid.
|
|
290
|
-
paddingBlockEnd: `${paddingBlockEnd}px`,
|
|
291
|
-
paddingBlockStart: `${Math.max(0, paddingBlockStart - scrollMargin)}px`,
|
|
292
|
-
}}
|
|
293
|
-
>
|
|
294
|
-
{virtualRows.flatMap((row) => {
|
|
295
|
-
const firstIndex = row.index * drawnColumns
|
|
296
|
-
|
|
297
|
-
return items
|
|
298
|
-
.slice(firstIndex, firstIndex + drawnColumns)
|
|
299
|
-
.map((item, offset) => {
|
|
300
|
-
const index = firstIndex + offset
|
|
301
|
-
|
|
302
|
-
return (
|
|
303
|
-
<li
|
|
304
|
-
aria-posinset={index + 1}
|
|
305
|
-
aria-setsize={items.length}
|
|
306
|
-
// `data-index` names the ROW, so it goes on
|
|
307
|
-
// every cell in it — they all sit in that row
|
|
308
|
-
// and the attribute is true of all of them.
|
|
309
|
-
//
|
|
310
|
-
// Putting it only on the measured cell is the
|
|
311
|
-
// version that looks tidier and warns on every
|
|
312
|
-
// resize: the virtualizer keeps a
|
|
313
|
-
// `ResizeObserver` on whatever it measured and
|
|
314
|
-
// re-reads the attribute whenever that node
|
|
315
|
-
// changes size. Let the node stop being the
|
|
316
|
-
// leading cell — a column count change is all it
|
|
317
|
-
// takes — and it is still observed, now with no
|
|
318
|
-
// attribute to resolve.
|
|
319
|
-
data-index={row.index}
|
|
320
|
-
key={getItemKey?.(item, index) ?? index}
|
|
321
|
-
// Measured on the leading cell alone. A CSS grid
|
|
322
|
-
// stretches every cell to its row's height, so
|
|
323
|
-
// one is enough — and measuring all of them
|
|
324
|
-
// would have the virtualizer record the same row
|
|
325
|
-
// once per column.
|
|
326
|
-
ref={
|
|
327
|
-
offset === 0
|
|
328
|
-
? rowVirtualizer.measureElement
|
|
329
|
-
: undefined
|
|
330
|
-
}
|
|
331
|
-
>
|
|
332
|
-
{renderItem(item, index)}
|
|
333
|
-
</li>
|
|
334
|
-
)
|
|
335
|
-
})
|
|
336
|
-
})}
|
|
337
|
-
</ul>
|
|
230
|
+
{scrollOwner?.kind === "element" ? (
|
|
231
|
+
<ElementGrid
|
|
232
|
+
{...gridProps}
|
|
233
|
+
scrollOwner={scrollOwner}
|
|
234
|
+
/>
|
|
235
|
+
) : scrollOwner?.kind === "window" ? (
|
|
236
|
+
<WindowGrid {...gridProps} />
|
|
237
|
+
) : null}
|
|
338
238
|
</div>
|
|
339
239
|
)
|
|
340
240
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useWindowVirtualizer } from "@tanstack/react-virtual"
|
|
2
|
+
import { useCallback } from "react"
|
|
3
|
+
|
|
4
|
+
import { GridRows } from "./GridRows.tsx"
|
|
5
|
+
import { useScrollMargin } from "./virtualizedGridScroll.ts"
|
|
6
|
+
import type { GridWindowProps } from "./virtualizedGridTypes.ts"
|
|
7
|
+
|
|
8
|
+
export const WindowGrid = <Item,>(
|
|
9
|
+
props: GridWindowProps<Item>,
|
|
10
|
+
) => {
|
|
11
|
+
const { gridElement, scrollMargin } = useScrollMargin({
|
|
12
|
+
kind: "window",
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const measureRow = useCallback(
|
|
16
|
+
(element: Element) =>
|
|
17
|
+
element.getBoundingClientRect().height + props.gap,
|
|
18
|
+
[props.gap],
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const rowVirtualizer = useWindowVirtualizer({
|
|
22
|
+
count: props.rowCount,
|
|
23
|
+
estimateSize: () => props.itemBlockSize + props.gap,
|
|
24
|
+
measureElement: measureRow,
|
|
25
|
+
overscan: props.overscanRows,
|
|
26
|
+
scrollMargin,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<GridRows
|
|
31
|
+
{...props}
|
|
32
|
+
gridElement={gridElement}
|
|
33
|
+
measureElement={rowVirtualizer.measureElement}
|
|
34
|
+
scrollMargin={scrollMargin}
|
|
35
|
+
totalSize={rowVirtualizer.getTotalSize()}
|
|
36
|
+
virtualRows={rowVirtualizer.getVirtualItems()}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from "react"
|
|
2
|
+
|
|
3
|
+
import type { ScrollOwner } from "./virtualizedGridTypes.ts"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The closest ancestor that owns vertical scrolling.
|
|
7
|
+
*
|
|
8
|
+
* The computed value is enough. An `overflow-y:auto` ancestor can
|
|
9
|
+
* be shorter than its contents only AFTER this grid installs its
|
|
10
|
+
* virtual spacer, so requiring `scrollHeight > clientHeight` here
|
|
11
|
+
* would miss the owner on the first commit and create the exact
|
|
12
|
+
* document scrollbar this lookup prevents.
|
|
13
|
+
*/
|
|
14
|
+
export const findScrollOwner = (
|
|
15
|
+
element: HTMLElement,
|
|
16
|
+
): ScrollOwner => {
|
|
17
|
+
let ancestor = element.parentElement
|
|
18
|
+
|
|
19
|
+
while (ancestor) {
|
|
20
|
+
const overflow = getComputedStyle(ancestor).overflowY
|
|
21
|
+
|
|
22
|
+
if (overflow === "auto" || overflow === "scroll") {
|
|
23
|
+
return { element: ancestor, kind: "element" }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ancestor = ancestor.parentElement
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { kind: "window" }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The offset from the scroll owner's content origin to the grid.
|
|
34
|
+
*
|
|
35
|
+
* Both formulas stay stable while their owner scrolls: the grid's
|
|
36
|
+
* viewport-relative top falls by the same amount that `scrollY` or
|
|
37
|
+
* `scrollTop` rises. The element path subtracts its border because
|
|
38
|
+
* a scroll offset starts at the padding edge, not at the outer
|
|
39
|
+
* border returned by `getBoundingClientRect()`.
|
|
40
|
+
*/
|
|
41
|
+
export const useScrollMargin = (
|
|
42
|
+
scrollOwner: ScrollOwner,
|
|
43
|
+
) => {
|
|
44
|
+
const gridElement = useRef<HTMLUListElement>(null)
|
|
45
|
+
const [scrollMargin, setScrollMargin] = useState(0)
|
|
46
|
+
|
|
47
|
+
useLayoutEffect(() => {
|
|
48
|
+
const element = gridElement.current
|
|
49
|
+
|
|
50
|
+
if (!element) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const gridTop = element.getBoundingClientRect().top
|
|
55
|
+
const next =
|
|
56
|
+
scrollOwner.kind === "window"
|
|
57
|
+
? gridTop + window.scrollY
|
|
58
|
+
: gridTop -
|
|
59
|
+
scrollOwner.element.getBoundingClientRect().top -
|
|
60
|
+
scrollOwner.element.clientTop +
|
|
61
|
+
scrollOwner.element.scrollTop
|
|
62
|
+
|
|
63
|
+
setScrollMargin((current) =>
|
|
64
|
+
current === next ? current : next,
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return { gridElement, scrollMargin }
|
|
69
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { VirtualItem } from "@tanstack/react-virtual"
|
|
2
|
+
import type { ReactNode, RefObject } from "react"
|
|
3
|
+
|
|
4
|
+
export type ScrollOwner =
|
|
5
|
+
| { kind: "element"; element: HTMLElement }
|
|
6
|
+
| { kind: "window" }
|
|
7
|
+
|
|
8
|
+
export type GridWindowProps<Item> = {
|
|
9
|
+
drawnColumns: number
|
|
10
|
+
gap: number
|
|
11
|
+
getItemKey?: (
|
|
12
|
+
item: Item,
|
|
13
|
+
index: number,
|
|
14
|
+
) => string | number
|
|
15
|
+
itemBlockSize: number
|
|
16
|
+
items: readonly Item[]
|
|
17
|
+
label?: string
|
|
18
|
+
overscanRows: number
|
|
19
|
+
renderItem: (item: Item, index: number) => ReactNode
|
|
20
|
+
rowCount: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type GridRowsProps<Item> = Omit<
|
|
24
|
+
GridWindowProps<Item>,
|
|
25
|
+
"itemBlockSize" | "overscanRows" | "rowCount"
|
|
26
|
+
> & {
|
|
27
|
+
gridElement: RefObject<HTMLUListElement | null>
|
|
28
|
+
measureElement: (element: Element | null) => void
|
|
29
|
+
scrollMargin: number
|
|
30
|
+
totalSize: number
|
|
31
|
+
virtualRows: VirtualItem[]
|
|
32
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -360,6 +360,8 @@ export type {
|
|
|
360
360
|
RouterLinkProps,
|
|
361
361
|
} from "./RouterLink/routerLink.ts"
|
|
362
362
|
export { getIsRoutedHref } from "./RouterLink/routerLink.ts"
|
|
363
|
+
export type { SearchInputProps } from "./SearchInput/SearchInput.tsx"
|
|
364
|
+
export { SearchInput } from "./SearchInput/SearchInput.tsx"
|
|
363
365
|
export type {
|
|
364
366
|
SegmentedControlProps,
|
|
365
367
|
SegmentedItem,
|
package/src/sourceRules.test.ts
CHANGED
|
@@ -537,7 +537,7 @@ test("the barrel is the only place components are re-exported", async () => {
|
|
|
537
537
|
// active-path rule an app's tests want to assert directly, and
|
|
538
538
|
// `useNavLayout.ts`, because the app has to read the width rule to
|
|
539
539
|
// know which corner to render into.
|
|
540
|
-
expect(componentNames.length).toBe(
|
|
540
|
+
expect(componentNames.length).toBe(61)
|
|
541
541
|
|
|
542
542
|
for (const name of componentNames) {
|
|
543
543
|
expect(barrel).toContain(`export { ${name} }`)
|