@lumiose/ui 0.1.3 → 0.2.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/index.ts +17 -0
- package/dist/virtualizer/virtual.tsx +398 -0
- package/dist/virtualizer/virtualizer.ts +354 -0
- package/docs/README.md +4 -0
- package/docs/llms-full.txt +721 -0
- package/docs/llms.txt +4 -0
- package/docs/virtual-grid.md +136 -0
- package/docs/virtual-list.md +121 -0
- package/docs/virtual-table.md +142 -0
- package/docs/virtualizer.md +306 -0
- package/package.json +2 -1
package/docs/llms.txt
CHANGED
|
@@ -12,6 +12,7 @@ props, events and styling hooks. The Lumiose docs: `node_modules/lumiose/docs/ll
|
|
|
12
12
|
- [UI components](README.md): `@lumiose/ui`: accessible, unstyled UI primitives built on native elements (`<dialog>`, popover, invoker commands, CSS anchor positioning), server-rendered without hydration. Style them with your own CSS or the optional default theme.
|
|
13
13
|
- [Installation](installation.md): Add `@lumiose/ui` to a Lumiose app and import its stylesheets, and the default theme if you want its look. The components then render in server HTML, without an island, and a page loads only the small scripts of the components it renders.
|
|
14
14
|
- [Theming](theming.md): `@lumiose/ui` is unstyled: its stylesheets hold only what the components need to work. The optional default theme gives them a look, light and dark, through `--lu-` custom properties you can change; or style the parts with your own CSS.
|
|
15
|
+
- [Virtualizer](virtualizer.md): A headless virtualizer for long lists, tables and grids in islands: `createVirtualizer()` computes which rows are near the viewport and where they go, and your component renders, places and styles them. The window or an element scrolls; rows have a fixed or an estimated size, or are measured as they render (one ResizeObserver for all of them); grids fit their columns to the container's width. The server renders the first items, and the island takes those same keyed rows over. 1.6 kB brotli, no script and no CSS of its own. For a list, a grid of cells or a table, start with Virtual List, Virtual Grid or Virtual Table, which run on it.
|
|
15
16
|
|
|
16
17
|
## Components
|
|
17
18
|
|
|
@@ -51,3 +52,6 @@ props, events and styling hooks. The Lumiose docs: `node_modules/lumiose/docs/ll
|
|
|
51
52
|
- [Toggle Group](toggle-group.md): A set of Toggles with one value, after Base UI's ToggleGroup: text alignment (one pressed at a time) or text style (several). `<lu-toggle-group role="group">` presses its toggles and gives the group one tab stop that the arrow keys, Home and End move, following the orientation and the direction, on server pages and in islands. `bind:value` keeps a signal in sync.
|
|
52
53
|
- [Toolbar](toolbar.md): A set of controls with one tab stop, after Base UI's Toolbar and the APG toolbar pattern: `<lu-toolbar role="toolbar">` moves focus between its items with the arrow keys, Home and End, following the orientation and the text direction. Its items are native: buttons that stay focusable when disabled, links, inputs, and the toggles of a Toggle Group inside it. In the server HTML every item is a tab stop, so each is reachable without JavaScript; a 1.2 kB script makes them one tab stop and moves it.
|
|
53
54
|
- [Tooltip](tooltip.md): A short tip next to its trigger, on hover or keyboard focus: a native `popover="hint"` with `role="tooltip"`, placed with CSS anchor positioning. The trigger is described by the tip in server HTML, so screen readers announce it without hovering; a 2.1 kB script opens and closes it as Base UI's tooltip does. `bind:open` (or `open` and `onOpenChange`) keeps a signal in sync in islands.
|
|
55
|
+
- [Virtual Grid](virtual-grid.md): A long grid that renders only the rows near the viewport, in islands: a `<ul>` of `<li>` cells placed in rows of as many columns as the width fits (`minItemWidth`, like CSS's `auto-fill`) or of `columns`, by createVirtualizer. Rows have a fixed height, or each is as tall as its tallest measured cell. The server renders the first `initial` cells in flow for your CSS grid to lay out, and the island takes those same keyed cells over. 2.3 kB brotli with the virtualizer.
|
|
56
|
+
- [Virtual List](virtual-list.md): A long list that renders only the rows near the viewport, in islands: a `<ul>` whose rows are `<li>`s placed by createVirtualizer, each keyed by `itemKey` and carrying `aria-posinset` and `aria-setsize`. The children function renders a row's content once; a row kept under its key keeps its DOM and state while its item updates in place. The server renders the first `initial` rows in flow. 2.2 kB brotli with the virtualizer.
|
|
57
|
+
- [Virtual Table](virtual-table.md): A long table that renders only the rows near its scroller's viewport, in islands: a `<table>` with `aria-rowcount`, your head, and a body whose `<tr aria-rowindex>` rows sit between two spacer rows, by createVirtualizer. The children function renders a row's cells once; a row kept under its key updates in place. The server renders the first `initial` rows and a spacer for the rest. 2.3 kB brotli with the virtualizer.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Virtual Grid
|
|
2
|
+
|
|
3
|
+
> A long grid that renders only the rows near the viewport, in islands: a `<ul>` of `<li>` cells placed in rows of as many columns as the width fits (`minItemWidth`, like CSS's `auto-fill`) or of `columns`, by [createVirtualizer](virtualizer.md). Rows have a fixed height, or each is as tall as its tallest measured cell. The server renders the first `initial` cells in flow for your CSS grid to lay out, and the island takes those same keyed cells over. 2.3 kB brotli with the virtualizer.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { VirtualGrid } from "@lumiose/ui"; // or "@lumiose/ui/virtualizer"
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Anatomy
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
<VirtualGrid items={cards()} itemKey={(card) => card.id} minItemWidth={192} gap={16} size={160}>
|
|
13
|
+
{(cell) => <Card card={cell.item} />}
|
|
14
|
+
</VirtualGrid>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
| Part | Renders |
|
|
18
|
+
| ------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
19
|
+
| `VirtualGrid` | `<ul>`, `total()` tall once mounted; an `<li data-row aria-posinset aria-setsize>` per cell, a column wide |
|
|
20
|
+
|
|
21
|
+
## API
|
|
22
|
+
|
|
23
|
+
### VirtualGrid
|
|
24
|
+
|
|
25
|
+
| Prop | Type | Default | |
|
|
26
|
+
| -------------- | --------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
27
|
+
| `items` | `readonly T[]` | | The items. |
|
|
28
|
+
| `itemKey` | `(item: T) => string \| number` | | The item's identity: a cell stays while its key does. |
|
|
29
|
+
| `children` | `(cell: VirtualItem<T>) => JSX.Element` | | A cell's content, rendered once per cell; `cell.item` and `cell.index` are getters. |
|
|
30
|
+
| `size` | `number \| (row: number) => number` | | A row's height in px: fixed (each cell gets it), or with `measure` the estimate. A function may read the virtualizer's `itemWidth()`. |
|
|
31
|
+
| `measure` | `boolean` | `false` | Measures the cells: each row is as tall as its tallest. |
|
|
32
|
+
| `minItemWidth` | `number` | | As many columns of at least this many px as the container's width fits. |
|
|
33
|
+
| `minColumns` | `number` | `1` | The fewest columns with `minItemWidth`, before mount too. |
|
|
34
|
+
| `columns` | `number` | `1` | A fixed number of columns, without `minItemWidth`. |
|
|
35
|
+
| `gap` | `number` | `0` | The px between rows. |
|
|
36
|
+
| `columnGap` | `number` | `gap` | The px between columns. |
|
|
37
|
+
| `overscan` | `number` | `4` | Rows rendered past each edge of the viewport. |
|
|
38
|
+
| `scroller` | `() => Element \| null \| undefined` | window | The element that scrolls. |
|
|
39
|
+
| `initial` | `number` | `0` | The cells rendered before mount (in whole rows of `minColumns`): the server's HTML. |
|
|
40
|
+
| `keepFocused` | `boolean` | `true` | Keeps the row of the focused cell rendered while the grid scrolls away from it. |
|
|
41
|
+
| `itemProps` | `(item: T, index: number) => li props` | | Attributes of each `<li>`. Not `style`, which places the cell. |
|
|
42
|
+
| `virtualizer` | `(virtualizer: Virtualizer) => void` | | Receives the [virtualizer](virtualizer.md): `scrollToIndex()`, `itemWidth()`, `columns()`. |
|
|
43
|
+
|
|
44
|
+
Every other `<ul>` prop passes through. A `style` lays out the server's cells before mount (a
|
|
45
|
+
CSS grid); once mounted, a `style` object stays under the grid's own position and height.
|
|
46
|
+
|
|
47
|
+
## Example
|
|
48
|
+
|
|
49
|
+
1,200 entries in a scroll box. The server renders the first 48 in a CSS grid of the same columns
|
|
50
|
+
(the `style`, kept until the grid mounts), and the island places the same cells:
|
|
51
|
+
|
|
52
|
+
```tsx check
|
|
53
|
+
import { VirtualGrid } from "@lumiose/ui";
|
|
54
|
+
|
|
55
|
+
const ENTRIES = Array.from({ length: 1200 }, (_, i) => ({ id: `e${i + 1}`, label: `#${i + 1}` }));
|
|
56
|
+
|
|
57
|
+
export function EntryGrid() {
|
|
58
|
+
let box: HTMLDivElement | undefined;
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
ref={(el) => (box = el)}
|
|
62
|
+
role="region"
|
|
63
|
+
aria-label="Entries"
|
|
64
|
+
tabindex={0}
|
|
65
|
+
style={{ "inline-size": "100%", "max-block-size": "18rem", overflow: "auto" }}
|
|
66
|
+
>
|
|
67
|
+
<VirtualGrid
|
|
68
|
+
aria-label="Entries"
|
|
69
|
+
style={{
|
|
70
|
+
margin: "0",
|
|
71
|
+
padding: "0",
|
|
72
|
+
"list-style": "none",
|
|
73
|
+
display: "grid",
|
|
74
|
+
"grid-template-columns": "repeat(auto-fill, minmax(96px, 1fr))",
|
|
75
|
+
gap: "8px",
|
|
76
|
+
}}
|
|
77
|
+
items={ENTRIES}
|
|
78
|
+
itemKey={(entry) => entry.id}
|
|
79
|
+
minItemWidth={96}
|
|
80
|
+
gap={8}
|
|
81
|
+
size={64}
|
|
82
|
+
initial={48}
|
|
83
|
+
scroller={() => box}
|
|
84
|
+
>
|
|
85
|
+
{(cell) => (
|
|
86
|
+
<button type="button" style={{ "inline-size": "100%", "block-size": "64px" }}>
|
|
87
|
+
{cell.item.label}
|
|
88
|
+
</button>
|
|
89
|
+
)}
|
|
90
|
+
</VirtualGrid>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The same server layout in CSS, for a grid whose columns change at breakpoints:
|
|
97
|
+
|
|
98
|
+
```css
|
|
99
|
+
.entries:not([data-mounted]) {
|
|
100
|
+
display: grid;
|
|
101
|
+
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
|
102
|
+
gap: 8px;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Styling
|
|
107
|
+
|
|
108
|
+
Unstyled. Before mount the cells are in flow: lay them out with a CSS grid, in the grid's `style`
|
|
109
|
+
or under `:not([data-mounted])`, with the columns and gaps the grid computes (`repeat(auto-fill,
|
|
110
|
+
minmax(<minItemWidth>px, 1fr))` and `gap`), and the cells take the same places once mounted.
|
|
111
|
+
Mounted, the `<ul>` gets `data-mounted`, `position: relative` and its total height, and each `<li>`
|
|
112
|
+
`position: absolute`, its column as `left`, the column's width, its row as a `translateY`, and with
|
|
113
|
+
a fixed `size` the row's height. Give the content `block-size: 100%` to fill a fixed cell. The cells
|
|
114
|
+
are the component's elements: reach them from the grid's class with `:deep()`
|
|
115
|
+
(`.entries :deep(li)`); `[data-row]` is each cell's row.
|
|
116
|
+
|
|
117
|
+
## Keyboard and focus
|
|
118
|
+
|
|
119
|
+
Cells are in DOM order, row by row, so Tab moves through them and the window follows. The row of
|
|
120
|
+
the focused cell stays rendered while the grid scrolls away (`keepFocused`, on by default, decision-84). Cells are keyed by
|
|
121
|
+
the record, so focus and state stay with their record through a filter, a sort or a refetch.
|
|
122
|
+
|
|
123
|
+
## Without JavaScript
|
|
124
|
+
|
|
125
|
+
The server renders the first `initial` cells, which your CSS lays out. See
|
|
126
|
+
[Virtualizer](virtualizer.md#server-rendering) for grids that must be complete in the HTML.
|
|
127
|
+
|
|
128
|
+
## Browser support
|
|
129
|
+
|
|
130
|
+
ResizeObserver (for `measure` and the width): every supported browser. Cells are placed left to
|
|
131
|
+
right.
|
|
132
|
+
|
|
133
|
+
## Related
|
|
134
|
+
|
|
135
|
+
- [Virtualizer](virtualizer.md): the primitive the grid runs on.
|
|
136
|
+
- [Virtual List](virtual-list.md) and [Virtual Table](virtual-table.md).
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Virtual List
|
|
2
|
+
|
|
3
|
+
> A long list that renders only the rows near the viewport, in islands: a `<ul>` whose rows are `<li>`s placed by [createVirtualizer](virtualizer.md), each keyed by `itemKey` and carrying `aria-posinset` and `aria-setsize`. The children function renders a row's content once; a row kept under its key keeps its DOM and state while its item updates in place. The server renders the first `initial` rows in flow. 2.2 kB brotli with the virtualizer.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { VirtualList } from "@lumiose/ui"; // or "@lumiose/ui/virtualizer"
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Anatomy
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
<VirtualList items={people()} itemKey={(person) => person.id} size={40}>
|
|
13
|
+
{(row) => <a href={`/people/${row.item.id}`}>{row.item.name}</a>}
|
|
14
|
+
</VirtualList>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
| Part | Renders |
|
|
18
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
19
|
+
| `VirtualList` | `<ul>`, `total()` tall and `position: relative` once mounted; an `<li data-row aria-posinset aria-setsize>` per row |
|
|
20
|
+
|
|
21
|
+
## API
|
|
22
|
+
|
|
23
|
+
### VirtualList
|
|
24
|
+
|
|
25
|
+
| Prop | Type | Default | |
|
|
26
|
+
| ------------- | -------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------- |
|
|
27
|
+
| `items` | `readonly T[]` | | The items. |
|
|
28
|
+
| `itemKey` | `(item: T) => string \| number` | | The item's identity, such as its record's id: a row stays while its key does. |
|
|
29
|
+
| `children` | `(row: VirtualItem<T>) => JSX.Element` | | A row's content, rendered once per row. `row.item` and `row.index` are getters: read them where they are used. |
|
|
30
|
+
| `size` | `number \| (row: number) => number` | | A row's height in px: fixed (the `<li>` gets it), or with `measure` the estimate. |
|
|
31
|
+
| `measure` | `boolean` | `false` | Measures the rendered rows (one ResizeObserver): their heights replace `size`. |
|
|
32
|
+
| `gap` | `number` | `0` | The px between rows. |
|
|
33
|
+
| `overscan` | `number` | `4` | Rows rendered past each edge of the viewport. |
|
|
34
|
+
| `scroller` | `() => Element \| null \| undefined` | window | The element that scrolls. |
|
|
35
|
+
| `initial` | `number` | `0` | The rows rendered before mount: the server's HTML. |
|
|
36
|
+
| `keepFocused` | `boolean` | `true` | Keeps the row that holds focus rendered while the list scrolls away from it. |
|
|
37
|
+
| `itemProps` | `(item: T, index: number) => li props` | | Attributes of each `<li>`: data attributes, a title. Not `style`, which places the row. |
|
|
38
|
+
| `virtualizer` | `(virtualizer: Virtualizer) => void` | | Receives the [virtualizer](virtualizer.md), for `scrollToIndex()` and `range()`. |
|
|
39
|
+
|
|
40
|
+
Every other `<ul>` prop passes through: `class`, `aria-label`, `aria-labelledby`, `id`. A `style`
|
|
41
|
+
object applies too, with the list's own properties (its padding, position and height) over it.
|
|
42
|
+
|
|
43
|
+
## Example
|
|
44
|
+
|
|
45
|
+
A thousand people in a scroll box, the first 20 in the server's HTML:
|
|
46
|
+
|
|
47
|
+
```tsx check
|
|
48
|
+
import { VirtualList, type Virtualizer } from "@lumiose/ui";
|
|
49
|
+
|
|
50
|
+
const PEOPLE = Array.from({ length: 1000 }, (_, i) => ({
|
|
51
|
+
id: `p${i + 1}`,
|
|
52
|
+
name: `Person ${i + 1}`,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
export function PeopleList() {
|
|
56
|
+
let box: HTMLDivElement | undefined;
|
|
57
|
+
let list: Virtualizer | undefined;
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
<button type="button" onClick={() => list?.scrollToIndex(499, { align: "start" })}>
|
|
61
|
+
Go to person 500
|
|
62
|
+
</button>
|
|
63
|
+
<div
|
|
64
|
+
ref={(el) => (box = el)}
|
|
65
|
+
role="region"
|
|
66
|
+
aria-label="People"
|
|
67
|
+
tabindex={0}
|
|
68
|
+
style={{ "inline-size": "100%", "max-block-size": "16rem", overflow: "auto" }}
|
|
69
|
+
>
|
|
70
|
+
<VirtualList
|
|
71
|
+
aria-label="People"
|
|
72
|
+
style={{ margin: "0", padding: "0", "list-style": "none" }}
|
|
73
|
+
items={PEOPLE}
|
|
74
|
+
itemKey={(person) => person.id}
|
|
75
|
+
size={40}
|
|
76
|
+
initial={20}
|
|
77
|
+
scroller={() => box}
|
|
78
|
+
virtualizer={(v) => (list = v)}
|
|
79
|
+
>
|
|
80
|
+
{(row) => (
|
|
81
|
+
<span>
|
|
82
|
+
{row.index + 1}. {row.item.name}
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
</VirtualList>
|
|
86
|
+
</div>
|
|
87
|
+
</>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Styling
|
|
93
|
+
|
|
94
|
+
Unstyled. Before mount the rows are in flow (each `size` tall, the rest of the list's height a
|
|
95
|
+
`padding-block-end`), so the server's list already has its full height. Once mounted the `<ul>`
|
|
96
|
+
gets `data-mounted`, `position: relative` and its total height, and each `<li>` `position:
|
|
97
|
+
absolute`, `inset: 0 0 auto` and a `transform`. Style with `class` on the list, `[data-mounted]`,
|
|
98
|
+
and the rows through the list's class: they are the component's elements, so a scoped selector
|
|
99
|
+
reaches them with `:deep()`, as in `.people :deep(li)`. The content you render is yours to style.
|
|
100
|
+
|
|
101
|
+
## Keyboard and focus
|
|
102
|
+
|
|
103
|
+
The rows are in DOM order, so Tab moves through them, and the browser scrolls the next one into
|
|
104
|
+
view. A focused row stays rendered while the list scrolls away from it (`keepFocused`, on by default for placed rows, decision-84), so focus
|
|
105
|
+
never falls back to the page. Rows are keyed by the record: a row's focus, typed text or toggled
|
|
106
|
+
state stays with its record when items are added, removed, reordered or fetched again.
|
|
107
|
+
|
|
108
|
+
## Without JavaScript
|
|
109
|
+
|
|
110
|
+
The server renders the first `initial` rows, in flow, with the list's full height reserved. See
|
|
111
|
+
[Virtualizer](virtualizer.md#server-rendering) for the takeover and for lists that must be
|
|
112
|
+
complete in the HTML.
|
|
113
|
+
|
|
114
|
+
## Browser support
|
|
115
|
+
|
|
116
|
+
ResizeObserver (for `measure`): every supported browser.
|
|
117
|
+
|
|
118
|
+
## Related
|
|
119
|
+
|
|
120
|
+
- [Virtualizer](virtualizer.md): the primitive the list runs on, for other layouts.
|
|
121
|
+
- [Virtual Grid](virtual-grid.md) and [Virtual Table](virtual-table.md).
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Virtual Table
|
|
2
|
+
|
|
3
|
+
> A long table that renders only the rows near its scroller's viewport, in islands: a `<table>` with `aria-rowcount`, your head, and a body whose `<tr aria-rowindex>` rows sit between two spacer rows, by [createVirtualizer](virtualizer.md). The children function renders a row's cells once; a row kept under its key updates in place. The server renders the first `initial` rows and a spacer for the rest. 2.3 kB brotli with the virtualizer.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { VirtualTable } from "@lumiose/ui"; // or "@lumiose/ui/virtualizer"
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Anatomy
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
<VirtualTable
|
|
13
|
+
items={releases()}
|
|
14
|
+
itemKey={(release) => release.id}
|
|
15
|
+
size={32}
|
|
16
|
+
scroller={() => box}
|
|
17
|
+
head={
|
|
18
|
+
<thead>
|
|
19
|
+
<tr aria-rowindex={1}>
|
|
20
|
+
<th scope="col">Version</th>
|
|
21
|
+
</tr>
|
|
22
|
+
</thead>
|
|
23
|
+
}
|
|
24
|
+
>
|
|
25
|
+
{(row) => <td>{row.item.version}</td>}
|
|
26
|
+
</VirtualTable>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| Part | Renders |
|
|
30
|
+
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
31
|
+
| `VirtualTable` | `<table aria-rowcount>`, `head`, and a `<tbody>` of `<tr data-row aria-rowindex>` rows between `<tr aria-hidden data-spacer>` rows |
|
|
32
|
+
|
|
33
|
+
## API
|
|
34
|
+
|
|
35
|
+
### VirtualTable
|
|
36
|
+
|
|
37
|
+
| Prop | Type | Default | |
|
|
38
|
+
| ------------- | -------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------- |
|
|
39
|
+
| `items` | `readonly T[]` | | The items. |
|
|
40
|
+
| `itemKey` | `(item: T) => string \| number` | | The item's identity: a row stays while its key does. |
|
|
41
|
+
| `children` | `(row: VirtualItem<T>) => JSX.Element` | | A row's cells, rendered once per row; `row.item` and `row.index` are getters. |
|
|
42
|
+
| `size` | `number \| (row: number) => number` | | A row's height in px: fixed, or with `measure` the estimate. It must match the CSS's row height. |
|
|
43
|
+
| `measure` | `boolean` | `false` | Measures the rendered rows: their heights replace `size`. |
|
|
44
|
+
| `scroller` | `() => Element \| null \| undefined` | window | The element that scrolls, usually a `role="region"` box around the table. |
|
|
45
|
+
| `head` | `JSX.Element` | | What comes before the body: `<caption>`, `<colgroup>`, `<thead>`. Give its rows `aria-rowindex` from 1. |
|
|
46
|
+
| `headerRows` | `number` | `1` | How many rows `head` has: `aria-rowcount` is the items plus these, and the body's indexes follow. |
|
|
47
|
+
| `colSpan` | `number` | `1` | The columns a spacer row spans. |
|
|
48
|
+
| `empty` | `JSX.Element` | | A row shown in the body while there are no items. |
|
|
49
|
+
| `overscan` | `number` | `4` | Rows rendered past each edge of the viewport. |
|
|
50
|
+
| `initial` | `number` | `0` | The rows rendered before mount: the server's HTML. |
|
|
51
|
+
| `rowProps` | `(item: T, index: number) => tr props` | | Attributes of each row: data attributes, a title. |
|
|
52
|
+
| `virtualizer` | `(virtualizer: Virtualizer) => void` | | Receives the [virtualizer](virtualizer.md), for `scrollToIndex()`. |
|
|
53
|
+
|
|
54
|
+
Every other `<table>` prop passes through: `class`, `aria-label`, `id`.
|
|
55
|
+
|
|
56
|
+
## Example
|
|
57
|
+
|
|
58
|
+
```tsx check
|
|
59
|
+
import { VirtualTable } from "@lumiose/ui";
|
|
60
|
+
|
|
61
|
+
const RELEASES = Array.from({ length: 2000 }, (_, i) => ({
|
|
62
|
+
id: `r${i + 1}`,
|
|
63
|
+
version: `1.${Math.floor(i / 10)}.${i % 10}`,
|
|
64
|
+
downloads: (i * 7919) % 10007,
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
export function ReleaseTable() {
|
|
68
|
+
let box: HTMLDivElement | undefined;
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
ref={(el) => (box = el)}
|
|
72
|
+
role="region"
|
|
73
|
+
aria-label="Releases"
|
|
74
|
+
tabindex={0}
|
|
75
|
+
style={{ "inline-size": "100%", "max-block-size": "20rem", overflow: "auto" }}
|
|
76
|
+
>
|
|
77
|
+
<VirtualTable
|
|
78
|
+
style={{ "border-collapse": "collapse" }}
|
|
79
|
+
items={RELEASES}
|
|
80
|
+
itemKey={(release) => release.id}
|
|
81
|
+
size={32}
|
|
82
|
+
scroller={() => box}
|
|
83
|
+
initial={15}
|
|
84
|
+
colSpan={2}
|
|
85
|
+
head={
|
|
86
|
+
<thead>
|
|
87
|
+
<tr aria-rowindex={1}>
|
|
88
|
+
<th scope="col">Version</th>
|
|
89
|
+
<th scope="col">Downloads</th>
|
|
90
|
+
</tr>
|
|
91
|
+
</thead>
|
|
92
|
+
}
|
|
93
|
+
rowProps={() => ({ style: "block-size: 32px" })}
|
|
94
|
+
empty={
|
|
95
|
+
<tr>
|
|
96
|
+
<td colspan={2}>No releases yet.</td>
|
|
97
|
+
</tr>
|
|
98
|
+
}
|
|
99
|
+
>
|
|
100
|
+
{(row) => (
|
|
101
|
+
<>
|
|
102
|
+
<td>{row.item.version}</td>
|
|
103
|
+
<td>{row.item.downloads}</td>
|
|
104
|
+
</>
|
|
105
|
+
)}
|
|
106
|
+
</VirtualTable>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Styling
|
|
113
|
+
|
|
114
|
+
Unstyled. Rows stay in the table's flow, so the table lays out its columns as usual; set the rows'
|
|
115
|
+
height in CSS to match `size` (`block-size` on the rows, or a custom property that `size` reads),
|
|
116
|
+
and collapse the borders (`border-collapse: collapse`), or the default `border-spacing` puts 2px
|
|
117
|
+
between rows that `size` does not count.
|
|
118
|
+
The spacer rows (`[data-spacer]`) have no padding or border. The rows are the component's `<tr>`s:
|
|
119
|
+
reach them from the table's class with `:deep()`, as in `.releases :deep(tr[data-row])`. The cells
|
|
120
|
+
you render are yours to style; a sticky `<thead>` works as in any scroller.
|
|
121
|
+
|
|
122
|
+
## Keyboard and focus
|
|
123
|
+
|
|
124
|
+
Focus the scroller (`tabindex="0"`) and the arrow keys, Page Up and Down, Home and End scroll it; the
|
|
125
|
+
rows follow. Rows are keyed by the record, so a link focused in a row keeps focus through a sort or
|
|
126
|
+
a refetch while it stays in the window. The table has no `keepFocused` (decision-84): a row outside the window
|
|
127
|
+
cannot sit between the spacers.
|
|
128
|
+
|
|
129
|
+
## Without JavaScript
|
|
130
|
+
|
|
131
|
+
The server renders the head, the first `initial` rows and a spacer for the rest, so the scroller has
|
|
132
|
+
its full height. See [Virtualizer](virtualizer.md#server-rendering) for tables that must be complete
|
|
133
|
+
in the HTML.
|
|
134
|
+
|
|
135
|
+
## Browser support
|
|
136
|
+
|
|
137
|
+
ResizeObserver: every supported browser.
|
|
138
|
+
|
|
139
|
+
## Related
|
|
140
|
+
|
|
141
|
+
- [Virtualizer](virtualizer.md): the primitive the table runs on.
|
|
142
|
+
- [Virtual List](virtual-list.md) and [Virtual Grid](virtual-grid.md).
|