@xsolla/xui-table 0.207.0 → 0.208.1
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/README.md +155 -17
- package/native/index.d.mts +84 -5
- package/native/index.d.ts +84 -5
- package/native/index.js +111 -7
- package/native/index.js.map +1 -1
- package/native/index.mjs +111 -7
- package/native/index.mjs.map +1 -1
- package/package.json +4 -4
- package/web/index.d.mts +84 -5
- package/web/index.d.ts +84 -5
- package/web/index.js +111 -7
- package/web/index.js.map +1 -1
- package/web/index.mjs +111 -7
- package/web/index.mjs.map +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Designed against the [Xsolla UI Kit Table](https://www.figma.com/design/m0qqLlCv
|
|
|
34
34
|
|
|
35
35
|
| Sub-component | Role | Notes |
|
|
36
36
|
|---|---|---|
|
|
37
|
-
| `Table` | `role="table"` container | Padding, radius, gap, theme |
|
|
37
|
+
| `Table` | `role="table"` container | Padding, radius, gap, theme, and the layout switches below |
|
|
38
38
|
| `Table.Caption` | Caption / description | Goes anywhere inside `<Table>`; muted text by default |
|
|
39
39
|
| `Table.Header` | `role="rowgroup"` (sticky) | Wraps a single `<Table.Row>` containing `<Table.Head>` cells |
|
|
40
40
|
| `Table.Body` | `role="rowgroup"` | Wraps body `<Table.Row>` nodes; optional `minRows` prop reserves a stable height for paginated tables (see "Stable height across pages") |
|
|
@@ -49,6 +49,20 @@ token. Figma node `22969:49282` only ships one canonical density (`56 px`
|
|
|
49
49
|
rows, `24 px` horizontal padding, `14 px` body font); if denser variants
|
|
50
50
|
are added later, this will become a `size` prop.
|
|
51
51
|
|
|
52
|
+
### Layout switches on `<Table>`
|
|
53
|
+
|
|
54
|
+
Four props control the responsive layout and the horizontal scroll
|
|
55
|
+
viewport. They're the only non-`children` surface on `<Table>` itself.
|
|
56
|
+
|
|
57
|
+
| Prop | Default | What it does |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `device="desktop" \| "mobile"` | `"desktop"` | Layout variant. Pick it from the viewport, not from a user preference. |
|
|
60
|
+
| `scroll` | `true` on mobile or with `floatFirstColumn`, else `false` | Wraps the header + body rowgroups in one horizontal scroll viewport. |
|
|
61
|
+
| `scrollBar` | `false` | Renders a visible scrollbar for that viewport. |
|
|
62
|
+
| `floatFirstColumn` | `false` | Pins the first column to the left edge while the rest scrolls. Desktop only. |
|
|
63
|
+
|
|
64
|
+
See "Scrolling" and "Responsive layout" below.
|
|
65
|
+
|
|
52
66
|
## What's NOT in the toolkit (and why)
|
|
53
67
|
|
|
54
68
|
| Pattern | Where it lives | Why |
|
|
@@ -285,6 +299,10 @@ the same reason at the bottom.
|
|
|
285
299
|
filter row above is visually separated from the column-header row
|
|
286
300
|
without the consumer needing to add their own divider.
|
|
287
301
|
|
|
302
|
+
These blocks stay **outside** the horizontal scroll viewport when
|
|
303
|
+
`scroll` is on — only `<Table.Header>` and `<Table.Body>` scroll. See
|
|
304
|
+
"Scrolling" below.
|
|
305
|
+
|
|
288
306
|
### B. Outside `<Table>`, above the card
|
|
289
307
|
|
|
290
308
|
Useful when the bulk-selection bar should appear *above* the rounded card
|
|
@@ -387,8 +405,9 @@ height jump.
|
|
|
387
405
|
The cell is hidden via `opacity: 0` + `pointer-events: none` (not
|
|
388
406
|
`visibility: hidden`), so keyboard Tab still focuses descendants. Focusing
|
|
389
407
|
a descendant flips the row's `focus-within` state and reveals the cell.
|
|
390
|
-
On touch-only devices
|
|
391
|
-
"
|
|
408
|
+
On touch-only devices — and whenever the table is rendered with
|
|
409
|
+
`device="mobile"` — the cell is always visible, matching Figma's "Right
|
|
410
|
+
cell actions are always visible on touch" spec.
|
|
392
411
|
|
|
393
412
|
## Truncation + tooltip on long values
|
|
394
413
|
|
|
@@ -418,10 +437,49 @@ when real data arrives. See the `LoadingState` story.
|
|
|
418
437
|
|
|
419
438
|
Keeps screen reader output meaningful.
|
|
420
439
|
|
|
421
|
-
##
|
|
440
|
+
## Responsive layout (`device`)
|
|
441
|
+
|
|
442
|
+
`device` is a layout variant, not a runtime toggle for the user. Select it
|
|
443
|
+
from the viewport — render `device="mobile"` below the mobile breakpoint
|
|
444
|
+
and `device="desktop"` above it:
|
|
445
|
+
|
|
446
|
+
```tsx
|
|
447
|
+
<Table device={isNarrowViewport ? "mobile" : "desktop"}>...</Table>
|
|
448
|
+
```
|
|
422
449
|
|
|
423
|
-
|
|
424
|
-
|
|
450
|
+
Because the toolkit is headless, the data, filters, sort order, and
|
|
451
|
+
selection all live in consumer state — so they survive the switch for
|
|
452
|
+
free. A user who selects three rows on desktop and rotates to a narrow
|
|
453
|
+
viewport keeps that selection.
|
|
454
|
+
|
|
455
|
+
The Mobile variant keeps the **same columnar layout** — it does not
|
|
456
|
+
collapse rows into stacked cards. What changes:
|
|
457
|
+
|
|
458
|
+
| | `desktop` | `mobile` |
|
|
459
|
+
|---|---|---|
|
|
460
|
+
| Horizontal scroll | Off unless you opt in | **On by default** |
|
|
461
|
+
| `revealOnHover` cells | Hidden until hover / focus | **Always visible** (no hover on touch) |
|
|
462
|
+
| `floatFirstColumn` | Honored | Ignored |
|
|
463
|
+
|
|
464
|
+
What adapts *around* the table is consumer-side: the title block stacks,
|
|
465
|
+
and the filter panel collapses to a search field plus a filter button that
|
|
466
|
+
opens the remaining filters in a `<Modal type="bottom sheet">`. Keep a
|
|
467
|
+
`<Badge>` on that button so active filters are visible from the collapsed
|
|
468
|
+
state.
|
|
469
|
+
|
|
470
|
+
**Mobile column order matters.** The leftmost columns are the ones read
|
|
471
|
+
without scrolling, so put the row's identifying field first, then the
|
|
472
|
+
values users compare most often. Push rarely-read columns to the right
|
|
473
|
+
rather than removing them. This — not `floatFirstColumn` — is how
|
|
474
|
+
readability is handled on mobile.
|
|
475
|
+
|
|
476
|
+
**Touch targets.** Hover doesn't exist on touch, so row-level actions
|
|
477
|
+
can't be revealed on hover. Keep the overflow (`⋯`) button permanently
|
|
478
|
+
visible in each row and size it to meet the 44 × 44 px touch target.
|
|
479
|
+
|
|
480
|
+
See the `MobileDevice` story.
|
|
481
|
+
|
|
482
|
+
## Scrolling
|
|
425
483
|
|
|
426
484
|
### Vertical scroll with sticky header
|
|
427
485
|
|
|
@@ -429,13 +487,47 @@ scroll) in a sized container.
|
|
|
429
487
|
`<Table.Body>` in a fixed-height `overflow-y: auto` container and the
|
|
430
488
|
header stays pinned while rows scroll. See the `Scrollable` story.
|
|
431
489
|
|
|
432
|
-
### Horizontal scroll
|
|
490
|
+
### Horizontal scroll (`scroll`, `scrollBar`)
|
|
491
|
+
|
|
492
|
+
When the columns exceed the available width, the table body scrolls
|
|
493
|
+
horizontally. Set `scroll` and `<Table>` wraps the contiguous run of
|
|
494
|
+
`<Table.Header>` / `<Table.Body>` children in a single scroll viewport, so
|
|
495
|
+
the header row and the body scroll **in lockstep** and column labels stay
|
|
496
|
+
aligned with their values.
|
|
497
|
+
|
|
498
|
+
Everything else stays fixed to the container width: `<Table.Caption>`,
|
|
499
|
+
`<Table.Footer>`, and any consumer-composed filter panel or item counter
|
|
500
|
+
dropped in as a direct child (position A above).
|
|
433
501
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
502
|
+
```tsx
|
|
503
|
+
<Table scroll scrollBar>
|
|
504
|
+
<Table.Caption>Stays fixed while the rows scroll</Table.Caption>
|
|
505
|
+
<Table.Header>...</Table.Header>
|
|
506
|
+
<Table.Body>...</Table.Body>
|
|
507
|
+
<Table.Footer>...</Table.Footer> {/* also stays fixed */}
|
|
508
|
+
</Table>
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
`scroll` defaults to `true` on `device="mobile"` and whenever
|
|
512
|
+
`floatFirstColumn` is on, and `false` otherwise — so desktop tables that
|
|
513
|
+
already supply their own outer scroll container (below) are unaffected.
|
|
514
|
+
|
|
515
|
+
`scrollBar` controls whether a **visible** scrollbar is rendered for that
|
|
516
|
+
viewport, and is `false` by default (matching the Figma `ScrollBar` prop).
|
|
517
|
+
Because it's off by default, an overflowing table gives no persistent
|
|
518
|
+
signal that content continues to the right — decide deliberately rather
|
|
519
|
+
than leaving the default in place. On desktop there is no swipe
|
|
520
|
+
affordance, so without a visible scrollbar the overflowing columns can go
|
|
521
|
+
unnoticed entirely. The scrollbar reflects the table body only.
|
|
522
|
+
|
|
523
|
+
See the `BuiltInHorizontalScroll` story.
|
|
524
|
+
|
|
525
|
+
#### Composing your own scroll viewport instead
|
|
526
|
+
|
|
527
|
+
`scroll` is opt-in on desktop precisely so the pre-existing two-Box
|
|
528
|
+
wrapper keeps working. The outer Box scrolls horizontally and the inner
|
|
529
|
+
uses `min-width: max-content` so the table card grows to the natural width
|
|
530
|
+
of its widest row:
|
|
439
531
|
|
|
440
532
|
```tsx
|
|
441
533
|
<Box style={{ width: "100%", overflowX: "auto" }}>
|
|
@@ -452,12 +544,55 @@ cells spilling outside the card's white background when you scroll.
|
|
|
452
544
|
`max-content` lets the browser compute the right value so the card and
|
|
453
545
|
the cells stay the same width. See the `HorizontalScroll` story.
|
|
454
546
|
|
|
455
|
-
|
|
547
|
+
Don't combine the two: if you supply your own viewport, leave `scroll`
|
|
548
|
+
off (or pass `scroll={false}` on mobile) so you don't nest one scroll
|
|
549
|
+
container inside another.
|
|
550
|
+
|
|
551
|
+
### Floating first column (`floatFirstColumn`, desktop only)
|
|
552
|
+
|
|
553
|
+
When the columns overflow and the body scrolls horizontally, set
|
|
554
|
+
`floatFirstColumn` and the first column holds its position at the left
|
|
555
|
+
edge, keeping a shadow — the only cue that content continues underneath
|
|
556
|
+
it. It implies the scroll viewport, so `scroll` is optional alongside it.
|
|
557
|
+
|
|
558
|
+
```tsx
|
|
559
|
+
<Table floatFirstColumn scrollBar>
|
|
560
|
+
<Table.Header>
|
|
561
|
+
<Table.Row>
|
|
562
|
+
<Table.Head width={200}>Name</Table.Head>
|
|
563
|
+
<Table.Head width={120}>Status</Table.Head>
|
|
564
|
+
{/* …more columns than fit… */}
|
|
565
|
+
</Table.Row>
|
|
566
|
+
</Table.Header>
|
|
567
|
+
<Table.Body>...</Table.Body>
|
|
568
|
+
</Table>
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
Use it when the table has more columns than fit the viewport and the first
|
|
572
|
+
column carries the row's identity (a name, an ID, an avatar) the user
|
|
573
|
+
needs as a reference while reading columns to the right. Keep the pinned
|
|
574
|
+
column narrow — only the first column can be pinned, and pinning a wide
|
|
575
|
+
one leaves too little room for the scrolling area.
|
|
576
|
+
|
|
577
|
+
Implementation notes:
|
|
578
|
+
|
|
579
|
+
- It is the **real first cell** held in place inside the scrolling
|
|
580
|
+
container, not a duplicate overlay kept in step with the real column.
|
|
581
|
+
(Figma builds it as a separate absolutely-positioned layer only because
|
|
582
|
+
Figma can't express a sticky column any other way.)
|
|
583
|
+
- The row's left gutter moves onto the pinned cell so it stays flush with
|
|
584
|
+
the container edge instead of leaving a transparent strip when scrolled.
|
|
585
|
+
- The pinned cell paints the row's current background — including the
|
|
586
|
+
hover and `selected` states — so scrolled-under columns can't show
|
|
587
|
+
through it.
|
|
588
|
+
- Only `<Table.Cell>` and `<Table.Head>` are pinned. If the first child of
|
|
589
|
+
a row is a custom wrapper, nothing is pinned for that row.
|
|
590
|
+
|
|
591
|
+
**Desktop only.** On `device="mobile"` the prop is ignored: horizontal
|
|
592
|
+
scrolling is already the reading mode there, so handle readability through
|
|
593
|
+
column order instead (see "Responsive layout").
|
|
456
594
|
|
|
457
|
-
|
|
458
|
-
background color and z-index. The toolkit doesn't ship a `freezeFirst`
|
|
459
|
-
prop because the visual treatment (border, shadow, background) varies per
|
|
460
|
-
team.
|
|
595
|
+
See the `FloatingFirstColumn` story.
|
|
461
596
|
|
|
462
597
|
## Virtualization
|
|
463
598
|
|
|
@@ -488,6 +623,9 @@ For native screens, pair `FlatList` with your own row + cell components.
|
|
|
488
623
|
| Sort indicator | Built into `Table.Head` (symmetric dual-chevron + `aria-sort` for direction) |
|
|
489
624
|
| Hover-reveal cells | `revealOnHover` prop on `Table.Cell` |
|
|
490
625
|
| Sticky header | Built into `Table.Header` |
|
|
626
|
+
| Responsive variant | `device="desktop" \| "mobile"` on `Table` |
|
|
627
|
+
| Horizontal scroll | `scroll` + `scrollBar` on `Table` (or compose your own viewport) |
|
|
628
|
+
| Frozen first column | `floatFirstColumn` on `Table` (desktop only) |
|
|
491
629
|
| Theming | Toolkit theme tokens (`theme.sizing.table`) |
|
|
492
630
|
| Cross-platform | Web only |
|
|
493
631
|
| Bundles a row data engine | No |
|
package/native/index.d.mts
CHANGED
|
@@ -5,6 +5,13 @@ import { ThemeOverrideProps } from '@xsolla/xui-core';
|
|
|
5
5
|
|
|
6
6
|
type TableCellAlign = "left" | "right" | "center";
|
|
7
7
|
type TableCellPosition = "default" | "left" | "right";
|
|
8
|
+
/**
|
|
9
|
+
* Layout variant of the table. Not a runtime user preference — pick it
|
|
10
|
+
* from the viewport (render `"mobile"` below the mobile breakpoint,
|
|
11
|
+
* `"desktop"` above it) per the Figma spec. Data, filters, sort order,
|
|
12
|
+
* and selection are owned by the consumer, so they survive the switch.
|
|
13
|
+
*/
|
|
14
|
+
type TableDevice = "desktop" | "mobile";
|
|
8
15
|
/**
|
|
9
16
|
* Structural primitives only:
|
|
10
17
|
*
|
|
@@ -33,12 +40,63 @@ type TableCellPosition = "default" | "left" | "right";
|
|
|
33
40
|
*/
|
|
34
41
|
/**
|
|
35
42
|
* `<Table>` is intentionally opaque: no `style`, no Box-level props. The
|
|
36
|
-
* surface is `children`
|
|
37
|
-
*
|
|
38
|
-
*
|
|
43
|
+
* surface is `children` plus the layout switches below (`device`,
|
|
44
|
+
* `scroll`, `scrollBar`, `floatFirstColumn`) and theme overrides.
|
|
45
|
+
*
|
|
46
|
+
* The scroll switches wrap the contiguous run of `<Table.Header>` /
|
|
47
|
+
* `<Table.Body>` children in a horizontal scroll viewport, so the header
|
|
48
|
+
* row and the body scroll in lockstep while `<Table.Caption>`,
|
|
49
|
+
* `<Table.Footer>`, and any consumer-composed filter panel / item counter
|
|
50
|
+
* stay fixed to the container width.
|
|
39
51
|
*/
|
|
40
52
|
interface TableProps extends ThemeOverrideProps {
|
|
41
53
|
children?: ReactNode;
|
|
54
|
+
/**
|
|
55
|
+
* Layout variant. Defaults to `"desktop"`.
|
|
56
|
+
*
|
|
57
|
+
* `"mobile"` keeps the same columnar layout — it does not collapse rows
|
|
58
|
+
* into stacked cards. What changes:
|
|
59
|
+
*
|
|
60
|
+
* - horizontal scrolling is on by default (this is how a wide table is
|
|
61
|
+
* read on a narrow screen),
|
|
62
|
+
* - `<Table.Cell revealOnHover>` cells are always visible, because hover
|
|
63
|
+
* does not exist on touch,
|
|
64
|
+
* - `floatFirstColumn` is ignored — order columns by importance instead
|
|
65
|
+
* and put the row's identifying field first.
|
|
66
|
+
*/
|
|
67
|
+
device?: TableDevice;
|
|
68
|
+
/**
|
|
69
|
+
* Wrap the header + body rowgroups in a horizontal scroll viewport.
|
|
70
|
+
*
|
|
71
|
+
* Defaults to `true` when `device="mobile"` or `floatFirstColumn` is on,
|
|
72
|
+
* and `false` otherwise — so existing desktop tables that supply their
|
|
73
|
+
* own outer scroll container (see the `HorizontalScroll` story) are
|
|
74
|
+
* unaffected. Set it explicitly to override.
|
|
75
|
+
*/
|
|
76
|
+
scroll?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Render a visible scrollbar for the horizontal scroll viewport.
|
|
79
|
+
* Defaults to `false`, matching the Figma `ScrollBar` prop.
|
|
80
|
+
*
|
|
81
|
+
* Because it is off by default, an overflowing table gives no persistent
|
|
82
|
+
* signal that content continues to the right. On desktop there is no
|
|
83
|
+
* swipe affordance, so turn it on whenever columns are expected to
|
|
84
|
+
* overflow.
|
|
85
|
+
*/
|
|
86
|
+
scrollBar?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Pin the first column to the left edge while the rest of the row
|
|
89
|
+
* scrolls horizontally. Defaults to `false`. **Desktop only** — ignored
|
|
90
|
+
* when `device="mobile"`.
|
|
91
|
+
*
|
|
92
|
+
* This is the real first cell held in place inside the scrolling
|
|
93
|
+
* container, not a duplicate overlay. Use it when the table has more
|
|
94
|
+
* columns than fit and the first column carries the row's identity (a
|
|
95
|
+
* name, an ID, an avatar) the user needs as a reference while reading
|
|
96
|
+
* columns to the right. Keep the pinned column narrow; only the first
|
|
97
|
+
* column can be pinned.
|
|
98
|
+
*/
|
|
99
|
+
floatFirstColumn?: boolean;
|
|
42
100
|
testID?: string;
|
|
43
101
|
}
|
|
44
102
|
interface TableCaptionProps extends BoxProps, ThemeOverrideProps {
|
|
@@ -102,6 +160,20 @@ interface TableRowProps extends BoxProps, ThemeOverrideProps {
|
|
|
102
160
|
*/
|
|
103
161
|
onKeyDown?: (e: React.KeyboardEvent<Element>) => void;
|
|
104
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Resolved sticky-column styling handed down from `<Table.Row>` to the
|
|
165
|
+
* first cell in the row when `floatFirstColumn` is on.
|
|
166
|
+
*
|
|
167
|
+
* @internal Not part of the public API.
|
|
168
|
+
*/
|
|
169
|
+
interface TableFloatingCellConfig {
|
|
170
|
+
/** Row background at its current state, so scrolled cells can't show through. */
|
|
171
|
+
background: string;
|
|
172
|
+
/** Row's horizontal gutter, moved onto the pinned cell so it stays flush. */
|
|
173
|
+
paddingLeft: number;
|
|
174
|
+
/** Elevation cue that content continues underneath the pinned column. */
|
|
175
|
+
shadow: string;
|
|
176
|
+
}
|
|
105
177
|
interface TableCellProps extends Omit<BoxProps, "position">, ThemeOverrideProps {
|
|
106
178
|
children?: ReactNode;
|
|
107
179
|
/** Cell text alignment. Defaults to "left". */
|
|
@@ -113,9 +185,16 @@ interface TableCellProps extends Omit<BoxProps, "position">, ThemeOverrideProps
|
|
|
113
185
|
/**
|
|
114
186
|
* Hide the cell until the parent row is hovered or focused (via `:focus-within`).
|
|
115
187
|
* Typically used for `position="right"` action cells per Figma spec.
|
|
116
|
-
* Falls back to always-visible on touch devices
|
|
188
|
+
* Falls back to always-visible on touch devices and when the enclosing
|
|
189
|
+
* `<Table>` is rendered with `device="mobile"`.
|
|
117
190
|
*/
|
|
118
191
|
revealOnHover?: boolean;
|
|
192
|
+
/**
|
|
193
|
+
* @internal Injected by `<Table.Row>` on the first cell when the
|
|
194
|
+
* enclosing `<Table>` has `floatFirstColumn`. Do not set this manually —
|
|
195
|
+
* it is destructured away and never reaches the DOM.
|
|
196
|
+
*/
|
|
197
|
+
__floating?: TableFloatingCellConfig;
|
|
119
198
|
testID?: string;
|
|
120
199
|
}
|
|
121
200
|
interface TableHeadProps extends TableCellProps {
|
|
@@ -136,4 +215,4 @@ declare const Table: React__default.ForwardRefExoticComponent<TableProps & React
|
|
|
136
215
|
Cell: React__default.ForwardRefExoticComponent<TableCellProps & React__default.RefAttributes<any>>;
|
|
137
216
|
};
|
|
138
217
|
|
|
139
|
-
export { Table, type TableBodyProps, type TableCaptionProps, type TableCellAlign, type TableCellPosition, type TableCellProps, type TableFooterProps, type TableHeadProps, type TableHeaderProps, type TableProps, type TableRowProps };
|
|
218
|
+
export { Table, type TableBodyProps, type TableCaptionProps, type TableCellAlign, type TableCellPosition, type TableCellProps, type TableDevice, type TableFloatingCellConfig, type TableFooterProps, type TableHeadProps, type TableHeaderProps, type TableProps, type TableRowProps };
|
package/native/index.d.ts
CHANGED
|
@@ -5,6 +5,13 @@ import { ThemeOverrideProps } from '@xsolla/xui-core';
|
|
|
5
5
|
|
|
6
6
|
type TableCellAlign = "left" | "right" | "center";
|
|
7
7
|
type TableCellPosition = "default" | "left" | "right";
|
|
8
|
+
/**
|
|
9
|
+
* Layout variant of the table. Not a runtime user preference — pick it
|
|
10
|
+
* from the viewport (render `"mobile"` below the mobile breakpoint,
|
|
11
|
+
* `"desktop"` above it) per the Figma spec. Data, filters, sort order,
|
|
12
|
+
* and selection are owned by the consumer, so they survive the switch.
|
|
13
|
+
*/
|
|
14
|
+
type TableDevice = "desktop" | "mobile";
|
|
8
15
|
/**
|
|
9
16
|
* Structural primitives only:
|
|
10
17
|
*
|
|
@@ -33,12 +40,63 @@ type TableCellPosition = "default" | "left" | "right";
|
|
|
33
40
|
*/
|
|
34
41
|
/**
|
|
35
42
|
* `<Table>` is intentionally opaque: no `style`, no Box-level props. The
|
|
36
|
-
* surface is `children`
|
|
37
|
-
*
|
|
38
|
-
*
|
|
43
|
+
* surface is `children` plus the layout switches below (`device`,
|
|
44
|
+
* `scroll`, `scrollBar`, `floatFirstColumn`) and theme overrides.
|
|
45
|
+
*
|
|
46
|
+
* The scroll switches wrap the contiguous run of `<Table.Header>` /
|
|
47
|
+
* `<Table.Body>` children in a horizontal scroll viewport, so the header
|
|
48
|
+
* row and the body scroll in lockstep while `<Table.Caption>`,
|
|
49
|
+
* `<Table.Footer>`, and any consumer-composed filter panel / item counter
|
|
50
|
+
* stay fixed to the container width.
|
|
39
51
|
*/
|
|
40
52
|
interface TableProps extends ThemeOverrideProps {
|
|
41
53
|
children?: ReactNode;
|
|
54
|
+
/**
|
|
55
|
+
* Layout variant. Defaults to `"desktop"`.
|
|
56
|
+
*
|
|
57
|
+
* `"mobile"` keeps the same columnar layout — it does not collapse rows
|
|
58
|
+
* into stacked cards. What changes:
|
|
59
|
+
*
|
|
60
|
+
* - horizontal scrolling is on by default (this is how a wide table is
|
|
61
|
+
* read on a narrow screen),
|
|
62
|
+
* - `<Table.Cell revealOnHover>` cells are always visible, because hover
|
|
63
|
+
* does not exist on touch,
|
|
64
|
+
* - `floatFirstColumn` is ignored — order columns by importance instead
|
|
65
|
+
* and put the row's identifying field first.
|
|
66
|
+
*/
|
|
67
|
+
device?: TableDevice;
|
|
68
|
+
/**
|
|
69
|
+
* Wrap the header + body rowgroups in a horizontal scroll viewport.
|
|
70
|
+
*
|
|
71
|
+
* Defaults to `true` when `device="mobile"` or `floatFirstColumn` is on,
|
|
72
|
+
* and `false` otherwise — so existing desktop tables that supply their
|
|
73
|
+
* own outer scroll container (see the `HorizontalScroll` story) are
|
|
74
|
+
* unaffected. Set it explicitly to override.
|
|
75
|
+
*/
|
|
76
|
+
scroll?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Render a visible scrollbar for the horizontal scroll viewport.
|
|
79
|
+
* Defaults to `false`, matching the Figma `ScrollBar` prop.
|
|
80
|
+
*
|
|
81
|
+
* Because it is off by default, an overflowing table gives no persistent
|
|
82
|
+
* signal that content continues to the right. On desktop there is no
|
|
83
|
+
* swipe affordance, so turn it on whenever columns are expected to
|
|
84
|
+
* overflow.
|
|
85
|
+
*/
|
|
86
|
+
scrollBar?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Pin the first column to the left edge while the rest of the row
|
|
89
|
+
* scrolls horizontally. Defaults to `false`. **Desktop only** — ignored
|
|
90
|
+
* when `device="mobile"`.
|
|
91
|
+
*
|
|
92
|
+
* This is the real first cell held in place inside the scrolling
|
|
93
|
+
* container, not a duplicate overlay. Use it when the table has more
|
|
94
|
+
* columns than fit and the first column carries the row's identity (a
|
|
95
|
+
* name, an ID, an avatar) the user needs as a reference while reading
|
|
96
|
+
* columns to the right. Keep the pinned column narrow; only the first
|
|
97
|
+
* column can be pinned.
|
|
98
|
+
*/
|
|
99
|
+
floatFirstColumn?: boolean;
|
|
42
100
|
testID?: string;
|
|
43
101
|
}
|
|
44
102
|
interface TableCaptionProps extends BoxProps, ThemeOverrideProps {
|
|
@@ -102,6 +160,20 @@ interface TableRowProps extends BoxProps, ThemeOverrideProps {
|
|
|
102
160
|
*/
|
|
103
161
|
onKeyDown?: (e: React.KeyboardEvent<Element>) => void;
|
|
104
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Resolved sticky-column styling handed down from `<Table.Row>` to the
|
|
165
|
+
* first cell in the row when `floatFirstColumn` is on.
|
|
166
|
+
*
|
|
167
|
+
* @internal Not part of the public API.
|
|
168
|
+
*/
|
|
169
|
+
interface TableFloatingCellConfig {
|
|
170
|
+
/** Row background at its current state, so scrolled cells can't show through. */
|
|
171
|
+
background: string;
|
|
172
|
+
/** Row's horizontal gutter, moved onto the pinned cell so it stays flush. */
|
|
173
|
+
paddingLeft: number;
|
|
174
|
+
/** Elevation cue that content continues underneath the pinned column. */
|
|
175
|
+
shadow: string;
|
|
176
|
+
}
|
|
105
177
|
interface TableCellProps extends Omit<BoxProps, "position">, ThemeOverrideProps {
|
|
106
178
|
children?: ReactNode;
|
|
107
179
|
/** Cell text alignment. Defaults to "left". */
|
|
@@ -113,9 +185,16 @@ interface TableCellProps extends Omit<BoxProps, "position">, ThemeOverrideProps
|
|
|
113
185
|
/**
|
|
114
186
|
* Hide the cell until the parent row is hovered or focused (via `:focus-within`).
|
|
115
187
|
* Typically used for `position="right"` action cells per Figma spec.
|
|
116
|
-
* Falls back to always-visible on touch devices
|
|
188
|
+
* Falls back to always-visible on touch devices and when the enclosing
|
|
189
|
+
* `<Table>` is rendered with `device="mobile"`.
|
|
117
190
|
*/
|
|
118
191
|
revealOnHover?: boolean;
|
|
192
|
+
/**
|
|
193
|
+
* @internal Injected by `<Table.Row>` on the first cell when the
|
|
194
|
+
* enclosing `<Table>` has `floatFirstColumn`. Do not set this manually —
|
|
195
|
+
* it is destructured away and never reaches the DOM.
|
|
196
|
+
*/
|
|
197
|
+
__floating?: TableFloatingCellConfig;
|
|
119
198
|
testID?: string;
|
|
120
199
|
}
|
|
121
200
|
interface TableHeadProps extends TableCellProps {
|
|
@@ -136,4 +215,4 @@ declare const Table: React__default.ForwardRefExoticComponent<TableProps & React
|
|
|
136
215
|
Cell: React__default.ForwardRefExoticComponent<TableCellProps & React__default.RefAttributes<any>>;
|
|
137
216
|
};
|
|
138
217
|
|
|
139
|
-
export { Table, type TableBodyProps, type TableCaptionProps, type TableCellAlign, type TableCellPosition, type TableCellProps, type TableFooterProps, type TableHeadProps, type TableHeaderProps, type TableProps, type TableRowProps };
|
|
218
|
+
export { Table, type TableBodyProps, type TableCaptionProps, type TableCellAlign, type TableCellPosition, type TableCellProps, type TableDevice, type TableFloatingCellConfig, type TableFooterProps, type TableHeadProps, type TableHeaderProps, type TableProps, type TableRowProps };
|
package/native/index.js
CHANGED
|
@@ -344,6 +344,10 @@ var import_xui_core = require("@xsolla/xui-core");
|
|
|
344
344
|
var import_xui_icons_base = require("@xsolla/xui-icons-base");
|
|
345
345
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
346
346
|
var TableRowGroupContext = (0, import_react.createContext)("body");
|
|
347
|
+
var TableConfigContext = (0, import_react.createContext)({
|
|
348
|
+
device: "desktop",
|
|
349
|
+
floatFirstColumn: false
|
|
350
|
+
});
|
|
347
351
|
var RowRevealContext = (0, import_react.createContext)({
|
|
348
352
|
revealed: true
|
|
349
353
|
});
|
|
@@ -369,11 +373,35 @@ var Divider = ({ color }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
|
369
373
|
style: { flexShrink: 0 }
|
|
370
374
|
}
|
|
371
375
|
);
|
|
376
|
+
var floatingCellStyle = (floating) => ({
|
|
377
|
+
position: "sticky",
|
|
378
|
+
left: 0,
|
|
379
|
+
zIndex: 2,
|
|
380
|
+
backgroundColor: floating.background,
|
|
381
|
+
paddingLeft: floating.paddingLeft,
|
|
382
|
+
boxShadow: floating.shadow
|
|
383
|
+
});
|
|
372
384
|
var TableRoot = (0, import_react.forwardRef)(
|
|
373
|
-
({
|
|
385
|
+
({
|
|
386
|
+
children,
|
|
387
|
+
device = "desktop",
|
|
388
|
+
scroll,
|
|
389
|
+
scrollBar = false,
|
|
390
|
+
floatFirstColumn = false,
|
|
391
|
+
testID,
|
|
392
|
+
themeMode,
|
|
393
|
+
themeProductContext
|
|
394
|
+
}, ref) => {
|
|
374
395
|
const { theme } = (0, import_xui_core.useResolvedTheme)({ themeMode, themeProductContext });
|
|
375
396
|
const sizing = theme.sizing.table;
|
|
376
|
-
|
|
397
|
+
const floatEnabled = floatFirstColumn && device !== "mobile";
|
|
398
|
+
const scrollEnabled = scroll ?? (device === "mobile" || floatEnabled);
|
|
399
|
+
const config = (0, import_react.useMemo)(
|
|
400
|
+
() => ({ device, floatFirstColumn: floatEnabled }),
|
|
401
|
+
[device, floatEnabled]
|
|
402
|
+
);
|
|
403
|
+
const content = scrollEnabled ? wrapRowGroupsInScrollViewport(children, scrollBar) : children;
|
|
404
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TableConfigContext.Provider, { value: config, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
377
405
|
Box,
|
|
378
406
|
{
|
|
379
407
|
ref,
|
|
@@ -390,9 +418,9 @@ var TableRoot = (0, import_react.forwardRef)(
|
|
|
390
418
|
color: theme.colors.content.primary,
|
|
391
419
|
overflow: "clip"
|
|
392
420
|
},
|
|
393
|
-
children
|
|
421
|
+
children: content
|
|
394
422
|
}
|
|
395
|
-
);
|
|
423
|
+
) });
|
|
396
424
|
}
|
|
397
425
|
);
|
|
398
426
|
TableRoot.displayName = "Table";
|
|
@@ -536,13 +564,14 @@ var TableRow = (0, import_react.forwardRef)(
|
|
|
536
564
|
const { theme } = (0, import_xui_core.useResolvedTheme)({ themeMode, themeProductContext });
|
|
537
565
|
const sizing = theme.sizing.table;
|
|
538
566
|
const rowGroup = (0, import_react.useContext)(TableRowGroupContext);
|
|
567
|
+
const { device, floatFirstColumn } = (0, import_react.useContext)(TableConfigContext);
|
|
539
568
|
const hoverCapable = useHoverCapable();
|
|
540
569
|
const isHeaderRow = rowGroup === "header";
|
|
541
570
|
const hoverable = hoverableProp ?? !isHeaderRow;
|
|
542
571
|
const hideDivider = hideDividerProp ?? isHeaderRow;
|
|
543
572
|
const [hovered, setHovered] = (0, import_react.useState)(false);
|
|
544
573
|
const [focusedWithin, setFocusedWithin] = (0, import_react.useState)(false);
|
|
545
|
-
const revealed = hoverCapable ? hovered || focusedWithin
|
|
574
|
+
const revealed = device === "mobile" || !hoverCapable ? true : hovered || focusedWithin;
|
|
546
575
|
const handleMouseEnter = (0, import_react.useCallback)(
|
|
547
576
|
(e) => {
|
|
548
577
|
setHovered(true);
|
|
@@ -590,6 +619,26 @@ var TableRow = (0, import_react.forwardRef)(
|
|
|
590
619
|
const rowHeight = isHeaderRow ? sizing.headerRowHeight : sizing.rowHeight;
|
|
591
620
|
const rowPaddingHorizontal = isHeaderRow ? sizing.headerRowPaddingHorizontal : sizing.rowPaddingHorizontal;
|
|
592
621
|
const ctxValue = (0, import_react.useMemo)(() => ({ revealed }), [revealed]);
|
|
622
|
+
const resolvedBg = hoverable && hovered ? theme.colors.background.secondary : baseBg;
|
|
623
|
+
let renderedChildren = children;
|
|
624
|
+
let didFloat = false;
|
|
625
|
+
if (floatFirstColumn) {
|
|
626
|
+
const childArray = import_react.default.Children.toArray(children);
|
|
627
|
+
const firstCellIndex = childArray.findIndex(
|
|
628
|
+
(c) => import_react.default.isValidElement(c) && (c.type === TableCell || c.type === TableHead)
|
|
629
|
+
);
|
|
630
|
+
if (firstCellIndex !== -1) {
|
|
631
|
+
didFloat = true;
|
|
632
|
+
const floating = {
|
|
633
|
+
background: resolvedBg,
|
|
634
|
+
paddingLeft: rowPaddingHorizontal,
|
|
635
|
+
shadow: theme.shadow.surface
|
|
636
|
+
};
|
|
637
|
+
renderedChildren = childArray.map(
|
|
638
|
+
(child, i) => i === firstCellIndex && import_react.default.isValidElement(child) ? import_react.default.cloneElement(child, { __floating: floating }) : child
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
593
642
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(RowRevealContext.Provider, { value: ctxValue, children: [
|
|
594
643
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
595
644
|
Box,
|
|
@@ -614,8 +663,14 @@ var TableRow = (0, import_react.forwardRef)(
|
|
|
614
663
|
onBlur: handleBlur,
|
|
615
664
|
...props,
|
|
616
665
|
onKeyDown: handleKeyDown,
|
|
617
|
-
style
|
|
618
|
-
|
|
666
|
+
style: {
|
|
667
|
+
// The pinned cell takes over the row's left gutter so it stays
|
|
668
|
+
// flush with the scroll container edge — without this the row
|
|
669
|
+
// padding scrolls away and leaves a transparent strip.
|
|
670
|
+
...didFloat && { paddingLeft: 0 },
|
|
671
|
+
...style
|
|
672
|
+
},
|
|
673
|
+
children: renderedChildren
|
|
619
674
|
}
|
|
620
675
|
),
|
|
621
676
|
!hideDivider && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Divider, { color: theme.colors.border.secondary })
|
|
@@ -651,6 +706,7 @@ var TableCell = (0, import_react.forwardRef)(
|
|
|
651
706
|
width,
|
|
652
707
|
grow,
|
|
653
708
|
revealOnHover,
|
|
709
|
+
__floating,
|
|
654
710
|
testID,
|
|
655
711
|
themeMode,
|
|
656
712
|
themeProductContext,
|
|
@@ -679,6 +735,7 @@ var TableCell = (0, import_react.forwardRef)(
|
|
|
679
735
|
minWidth: 0,
|
|
680
736
|
...positionStyle,
|
|
681
737
|
...revealStyle,
|
|
738
|
+
...__floating && floatingCellStyle(__floating),
|
|
682
739
|
...style
|
|
683
740
|
},
|
|
684
741
|
children: typeof children === "string" || typeof children === "number" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
@@ -721,6 +778,7 @@ var TableHead = (0, import_react.forwardRef)(
|
|
|
721
778
|
grow,
|
|
722
779
|
sort,
|
|
723
780
|
onSortToggle,
|
|
781
|
+
__floating,
|
|
724
782
|
testID,
|
|
725
783
|
themeMode,
|
|
726
784
|
themeProductContext,
|
|
@@ -751,6 +809,7 @@ var TableHead = (0, import_react.forwardRef)(
|
|
|
751
809
|
flex: width != null ? "0 0 auto" : grow ?? 1,
|
|
752
810
|
minWidth: 0,
|
|
753
811
|
...positionStyle,
|
|
812
|
+
...__floating && floatingCellStyle(__floating),
|
|
754
813
|
...style
|
|
755
814
|
},
|
|
756
815
|
children: [
|
|
@@ -805,6 +864,51 @@ var TableFooter = (0, import_react.forwardRef)(
|
|
|
805
864
|
}
|
|
806
865
|
);
|
|
807
866
|
TableFooter.displayName = "Table.Footer";
|
|
867
|
+
function wrapRowGroupsInScrollViewport(children, scrollBar) {
|
|
868
|
+
const viewportStyle = {
|
|
869
|
+
overflowX: "auto",
|
|
870
|
+
WebkitOverflowScrolling: "touch",
|
|
871
|
+
// `false` hides the scrollbar without disabling the scrolling itself.
|
|
872
|
+
...scrollBar ? { scrollbarWidth: "thin" } : { scrollbarWidth: "none", msOverflowStyle: "none" }
|
|
873
|
+
};
|
|
874
|
+
const out = [];
|
|
875
|
+
let run = [];
|
|
876
|
+
const flush = () => {
|
|
877
|
+
if (run.length === 0) return;
|
|
878
|
+
const group = run;
|
|
879
|
+
run = [];
|
|
880
|
+
out.push(
|
|
881
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
882
|
+
Box,
|
|
883
|
+
{
|
|
884
|
+
width: "100%",
|
|
885
|
+
style: viewportStyle,
|
|
886
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
887
|
+
Box,
|
|
888
|
+
{
|
|
889
|
+
flexDirection: "column",
|
|
890
|
+
alignItems: "stretch",
|
|
891
|
+
style: { minWidth: "max-content" },
|
|
892
|
+
children: group
|
|
893
|
+
}
|
|
894
|
+
)
|
|
895
|
+
},
|
|
896
|
+
`__table-scroll-${out.length}`
|
|
897
|
+
)
|
|
898
|
+
);
|
|
899
|
+
};
|
|
900
|
+
import_react.default.Children.toArray(children).forEach((child) => {
|
|
901
|
+
const isRowGroup = import_react.default.isValidElement(child) && (child.type === TableHeader || child.type === TableBody);
|
|
902
|
+
if (isRowGroup) {
|
|
903
|
+
run.push(child);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
flush();
|
|
907
|
+
out.push(child);
|
|
908
|
+
});
|
|
909
|
+
flush();
|
|
910
|
+
return out;
|
|
911
|
+
}
|
|
808
912
|
var Table = Object.assign(TableRoot, {
|
|
809
913
|
Caption: TableCaption,
|
|
810
914
|
Header: TableHeader,
|