jattac.libs.web.responsive-table 0.17.0 → 0.18.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/UI/ResponsiveTable.d.ts +8 -0
- package/dist/UI/ScrollPosition.test.d.ts +1 -0
- package/dist/index.es.js +23 -3
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/docs/api.md +53 -0
- package/docs/expand-collapse.md +306 -0
- package/docs/scroll-position.md +449 -0
- package/package.json +1 -1
package/docs/expand-collapse.md
CHANGED
|
@@ -33,6 +33,14 @@ Expandable rows reveal arbitrary content below any row on demand. A chevron in a
|
|
|
33
33
|
- [Inline Editing Panel](#inline-editing-panel)
|
|
34
34
|
- [Nested Table](#nested-table)
|
|
35
35
|
- [Charts and Rich Media](#charts-and-rich-media)
|
|
36
|
+
- [Programmatic Control (Ref API)](#programmatic-control-ref-api)
|
|
37
|
+
- [Rationale](#rationale)
|
|
38
|
+
- [Setting up the Ref](#setting-up-the-ref)
|
|
39
|
+
- [expandRows / collapseRows / toggleRows](#expandrows--collapserows--togglerows)
|
|
40
|
+
- [defaultExpandedIds](#defaultexpandedids)
|
|
41
|
+
- [Common Patterns](#common-ref-patterns)
|
|
42
|
+
- [Best Practices](#best-practices-ref-api)
|
|
43
|
+
- [Pitfalls and Edge Cases](#pitfalls-and-edge-cases)
|
|
36
44
|
- [Visual Anatomy](#visual-anatomy)
|
|
37
45
|
- [Animation System](#animation-system)
|
|
38
46
|
- [CSS Customization Reference](#css-customization-reference)
|
|
@@ -45,6 +53,15 @@ Expandable rows reveal arbitrary content below any row on demand. A chevron in a
|
|
|
45
53
|
| :--- | :--- | :--- | :--- |
|
|
46
54
|
| `expandRowRenderer` | `(row: TData, rowIndex: number) => ReactNode` | — | Renderer for the detail panel. Return `null` or `undefined` to suppress the toggle on that row. |
|
|
47
55
|
| `expandChevronClassName` | `string` | — | CSS class applied to the chevron `<span>`. Use to override color, size, or other styles. Do not override `transform` or `transition`. |
|
|
56
|
+
| `defaultExpandedIds` | `(string \| number)[]` | — | Row IDs to expand on initial render. Read once at mount; changes after mount have no effect. |
|
|
57
|
+
|
|
58
|
+
**Imperative handle methods** (via `ref` — see [Programmatic Control](#programmatic-control-ref-api)):
|
|
59
|
+
|
|
60
|
+
| Method | Description |
|
|
61
|
+
| :--- | :--- |
|
|
62
|
+
| `expandRows(...ids)` | Expands one or more rows by ID. |
|
|
63
|
+
| `collapseRows(...ids)` | Collapses one or more rows by ID. |
|
|
64
|
+
| `toggleRows(...ids)` | Toggles one or more rows by ID — mirrors a chevron click. |
|
|
48
65
|
|
|
49
66
|
---
|
|
50
67
|
|
|
@@ -533,6 +550,295 @@ import { LineChart, Line, XAxis, YAxis, Tooltip } from 'recharts';
|
|
|
533
550
|
|
|
534
551
|
---
|
|
535
552
|
|
|
553
|
+
## Programmatic Control (Ref API)
|
|
554
|
+
|
|
555
|
+
### Rationale
|
|
556
|
+
|
|
557
|
+
By default, expansion state is entirely user-driven — a user clicks the chevron, a panel opens. That covers most cases. But some UX flows need to drive expansion from outside the table:
|
|
558
|
+
|
|
559
|
+
- An **onboarding tour** that walks the user to a specific row and opens its panel automatically
|
|
560
|
+
- A **"Expand all / Collapse all"** button in a toolbar
|
|
561
|
+
- A **wizard or multi-step flow** that pre-opens the row the user is expected to act on next
|
|
562
|
+
- **Deep-linked navigation** — a URL like `/orders?expanded=1042` should open order 1042's detail pane on load
|
|
563
|
+
- **Programmatic workflows** — a side panel or search result that says "show me this row's details"
|
|
564
|
+
|
|
565
|
+
The ref API provides this without requiring the consumer to take ownership of expansion state. The table still manages its own `Set` internally; the ref just gives you a handle into it. A ref call acts exactly as if the user had clicked the chevron — same state path, same CSS animation, same lazy-mount behaviour.
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
### Setting up the Ref
|
|
570
|
+
|
|
571
|
+
Import the handle type and create a typed ref:
|
|
572
|
+
|
|
573
|
+
```tsx
|
|
574
|
+
import ResponsiveTable, { ResponsiveTableHandle } from 'jattac.libs.web.responsive-table';
|
|
575
|
+
import { useRef } from 'react';
|
|
576
|
+
|
|
577
|
+
type Order = { id: string; reference: string; customer: string; total: number };
|
|
578
|
+
|
|
579
|
+
function OrdersPage() {
|
|
580
|
+
const tableRef = useRef<ResponsiveTableHandle<Order>>(null);
|
|
581
|
+
|
|
582
|
+
return (
|
|
583
|
+
<ResponsiveTable<Order>
|
|
584
|
+
ref={tableRef}
|
|
585
|
+
data={orders}
|
|
586
|
+
columnDefinitions={columns}
|
|
587
|
+
selectionProps={{ rowIdKey: 'id', onSelectionChange: () => {} }}
|
|
588
|
+
expandRowRenderer={(order) => <OrderDetail order={order} />}
|
|
589
|
+
/>
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
> **Always pair `ref` with `selectionProps.rowIdKey`** when using the programmatic API. Without a stable key, the IDs in the ref calls are array indices — they shift when the user sorts or filters, causing the wrong rows to expand. See [Expansion State and Row Identity](#expansion-state-and-row-identity).
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
### `expandRows` / `collapseRows` / `toggleRows`
|
|
599
|
+
|
|
600
|
+
All three methods accept **rest parameters** — one call handles any number of IDs.
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
tableRef.current?.expandRows(...ids: (string | number)[]): void
|
|
604
|
+
tableRef.current?.collapseRows(...ids: (string | number)[]): void
|
|
605
|
+
tableRef.current?.toggleRows(...ids: (string | number)[]): void
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
```tsx
|
|
609
|
+
// Expand a single row
|
|
610
|
+
tableRef.current?.expandRows('order-42');
|
|
611
|
+
|
|
612
|
+
// Expand multiple rows in one call
|
|
613
|
+
tableRef.current?.expandRows('order-1', 'order-2', 'order-3');
|
|
614
|
+
|
|
615
|
+
// Spread an array
|
|
616
|
+
tableRef.current?.expandRows(...selectedOrderIds);
|
|
617
|
+
|
|
618
|
+
// Collapse specific rows
|
|
619
|
+
tableRef.current?.collapseRows('order-1', 'order-2');
|
|
620
|
+
|
|
621
|
+
// Toggle — mirrors clicking the chevron: open→close, closed→open
|
|
622
|
+
tableRef.current?.toggleRows('order-42');
|
|
623
|
+
|
|
624
|
+
// Toggle multiple rows simultaneously
|
|
625
|
+
tableRef.current?.toggleRows('order-1', 'order-2', 'order-3');
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
**Behaviour contract:**
|
|
629
|
+
|
|
630
|
+
| Method | Already expanded | Already collapsed |
|
|
631
|
+
| :--- | :--- | :--- |
|
|
632
|
+
| `expandRows` | No-op (safe to call) | Opens the panel |
|
|
633
|
+
| `collapseRows` | Closes the panel | No-op (safe to call) |
|
|
634
|
+
| `toggleRows` | Closes the panel | Opens the panel |
|
|
635
|
+
|
|
636
|
+
All methods are synchronous state updates — the React re-render and CSS animation begin immediately after the call.
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
### `defaultExpandedIds`
|
|
641
|
+
|
|
642
|
+
Pre-open specific rows on initial render without any runtime ref calls:
|
|
643
|
+
|
|
644
|
+
```tsx
|
|
645
|
+
<ResponsiveTable
|
|
646
|
+
data={orders}
|
|
647
|
+
columnDefinitions={columns}
|
|
648
|
+
selectionProps={{ rowIdKey: 'id', onSelectionChange: () => {} }}
|
|
649
|
+
expandRowRenderer={(order) => <OrderDetail order={order} />}
|
|
650
|
+
defaultExpandedIds={['order-1042']}
|
|
651
|
+
/>
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
The value is read **once at mount** and used to initialise the internal `Set`. Changes to `defaultExpandedIds` after mount are ignored — it is not a controlled prop.
|
|
655
|
+
|
|
656
|
+
For dynamic initial expansion (e.g., from a URL parameter), derive the value before rendering:
|
|
657
|
+
|
|
658
|
+
```tsx
|
|
659
|
+
const initialExpanded = useMemo(() => {
|
|
660
|
+
const param = new URLSearchParams(window.location.search).get('expanded');
|
|
661
|
+
return param ? [param] : [];
|
|
662
|
+
}, []); // empty deps — only read once
|
|
663
|
+
|
|
664
|
+
<ResponsiveTable
|
|
665
|
+
defaultExpandedIds={initialExpanded}
|
|
666
|
+
...
|
|
667
|
+
/>
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
### Common Ref Patterns
|
|
673
|
+
|
|
674
|
+
#### Expand All / Collapse All
|
|
675
|
+
|
|
676
|
+
```tsx
|
|
677
|
+
function OrdersPage() {
|
|
678
|
+
const tableRef = useRef<ResponsiveTableHandle<Order>>(null);
|
|
679
|
+
const [orders, setOrders] = useState<Order[]>([]);
|
|
680
|
+
|
|
681
|
+
const expandAll = () => {
|
|
682
|
+
const ids = orders.map((o) => o.id);
|
|
683
|
+
tableRef.current?.expandRows(...ids);
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const collapseAll = () => {
|
|
687
|
+
const ids = orders.map((o) => o.id);
|
|
688
|
+
tableRef.current?.collapseRows(...ids);
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
return (
|
|
692
|
+
<>
|
|
693
|
+
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.5rem' }}>
|
|
694
|
+
<button onClick={expandAll}>Expand all</button>
|
|
695
|
+
<button onClick={collapseAll}>Collapse all</button>
|
|
696
|
+
</div>
|
|
697
|
+
<ResponsiveTable
|
|
698
|
+
ref={tableRef}
|
|
699
|
+
data={orders}
|
|
700
|
+
columnDefinitions={columns}
|
|
701
|
+
selectionProps={{ rowIdKey: 'id', onSelectionChange: () => {} }}
|
|
702
|
+
expandRowRenderer={(o) => <OrderDetail order={o} />}
|
|
703
|
+
/>
|
|
704
|
+
</>
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
#### Deep-Linked Expansion (URL Param)
|
|
710
|
+
|
|
711
|
+
```tsx
|
|
712
|
+
function OrdersPage() {
|
|
713
|
+
const tableRef = useRef<ResponsiveTableHandle<Order>>(null);
|
|
714
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
715
|
+
const targetId = searchParams.get('expanded');
|
|
716
|
+
|
|
717
|
+
return (
|
|
718
|
+
<ResponsiveTable
|
|
719
|
+
ref={tableRef}
|
|
720
|
+
data={orders}
|
|
721
|
+
columnDefinitions={columns}
|
|
722
|
+
selectionProps={{ rowIdKey: 'id', onSelectionChange: () => {} }}
|
|
723
|
+
expandRowRenderer={(o) => <OrderDetail order={o} />}
|
|
724
|
+
defaultExpandedIds={targetId ? [targetId] : []}
|
|
725
|
+
/>
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
#### Expand on External Search Result Click
|
|
731
|
+
|
|
732
|
+
```tsx
|
|
733
|
+
function OrdersPage() {
|
|
734
|
+
const tableRef = useRef<ResponsiveTableHandle<Order>>(null);
|
|
735
|
+
|
|
736
|
+
const handleSearchResultClick = (orderId: string) => {
|
|
737
|
+
tableRef.current?.expandRows(orderId);
|
|
738
|
+
// optionally scroll the row into view separately
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
return (
|
|
742
|
+
<>
|
|
743
|
+
<SearchBar onResultClick={handleSearchResultClick} />
|
|
744
|
+
<ResponsiveTable
|
|
745
|
+
ref={tableRef}
|
|
746
|
+
data={orders}
|
|
747
|
+
columnDefinitions={columns}
|
|
748
|
+
selectionProps={{ rowIdKey: 'id', onSelectionChange: () => {} }}
|
|
749
|
+
expandRowRenderer={(o) => <OrderDetail order={o} />}
|
|
750
|
+
/>
|
|
751
|
+
</>
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
#### Onboarding Tour (auto-open a specific row)
|
|
757
|
+
|
|
758
|
+
```tsx
|
|
759
|
+
useEffect(() => {
|
|
760
|
+
if (tourStep === 'show-order-detail') {
|
|
761
|
+
tableRef.current?.expandRows(featuredOrderId);
|
|
762
|
+
}
|
|
763
|
+
}, [tourStep]);
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
### Best Practices (Ref API)
|
|
769
|
+
|
|
770
|
+
**Always provide `selectionProps.rowIdKey`.**
|
|
771
|
+
The ref methods target rows by the same ID the table uses internally. Without `rowIdKey`, that ID is the array index — which shifts whenever the user sorts or filters. Any ref call after a sort will expand the wrong row. If you don't need selection UI, pass a no-op `onSelectionChange`:
|
|
772
|
+
|
|
773
|
+
```tsx
|
|
774
|
+
selectionProps={{ rowIdKey: 'id', onSelectionChange: () => {} }}
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
**Guard against null.**
|
|
778
|
+
The ref is `null` until the component mounts. Always use optional chaining:
|
|
779
|
+
|
|
780
|
+
```tsx
|
|
781
|
+
tableRef.current?.expandRows('id-1'); // safe
|
|
782
|
+
tableRef.current.expandRows('id-1'); // throws if called before mount
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
**Prefer `defaultExpandedIds` over a `useEffect` + ref for initial state.**
|
|
786
|
+
A `useEffect` that calls `expandRows` on mount introduces a flash: the table renders collapsed, then the effect fires and rows pop open. `defaultExpandedIds` initialises the internal Set before the first render — no flash.
|
|
787
|
+
|
|
788
|
+
```tsx
|
|
789
|
+
// Preferred — no flash
|
|
790
|
+
<ResponsiveTable defaultExpandedIds={['order-42']} ... />
|
|
791
|
+
|
|
792
|
+
// Avoid — causes a visible collapsed→expanded flash
|
|
793
|
+
useEffect(() => { tableRef.current?.expandRows('order-42'); }, []);
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
**All methods are idempotent.**
|
|
797
|
+
`expandRows` on an already-expanded row is a no-op. `collapseRows` on an already-collapsed row is a no-op. Safe to call without pre-checking state.
|
|
798
|
+
|
|
799
|
+
**The ref API and chevron clicks share the same state.**
|
|
800
|
+
They are not separate channels — both write to the same internal `Set`. A user can click the chevron on a row you expanded programmatically, and it will collapse normally.
|
|
801
|
+
|
|
802
|
+
---
|
|
803
|
+
|
|
804
|
+
### Pitfalls and Edge Cases
|
|
805
|
+
|
|
806
|
+
**Passing an ID that doesn't correspond to any visible row.**
|
|
807
|
+
The ID is added to the `Set` but nothing is visually expanded because no row matches. This is silent — no error, no warning. If you later load data that includes a row with that ID (e.g., via `dataSource` pagination), it will expand automatically when it arrives.
|
|
808
|
+
|
|
809
|
+
**`defaultExpandedIds` is not reactive.**
|
|
810
|
+
It is passed to `useState(() => new Set(...))` — the initialiser runs once. If `defaultExpandedIds` changes after mount (e.g., derived from prop that updates), the change is ignored. For post-mount control, use the ref API.
|
|
811
|
+
|
|
812
|
+
**Passing a row ID when `expandRowRenderer` returns `null` for that row.**
|
|
813
|
+
The row is added to the expanded Set, but since there is no detail content, nothing is visually displayed. If `expandRowRenderer` later returns content for that row (after a state change in the parent), the panel will be visible immediately on the next render — because the row is already in the expanded Set. This can be surprising: a row that had no toggle is suddenly expanded. Avoid keeping rows without content in the expanded Set.
|
|
814
|
+
|
|
815
|
+
**Index-based IDs with sort or filter.**
|
|
816
|
+
Without `selectionProps.rowIdKey`, row keys are array indices (`0`, `1`, `2`, …). After the user sorts the table, `expandRows(0)` expands whatever row is now first — not necessarily the row you intended. Always use data-derived stable IDs with the ref API.
|
|
817
|
+
|
|
818
|
+
**The ref is typed — mismatched generic.**
|
|
819
|
+
`ResponsiveTableHandle<Order>` is generic. If you use `ResponsiveTableHandle` without a type argument, TypeScript infers `unknown` and the ref will typecheck but may not match the table instance. Always provide the same generic as the table component:
|
|
820
|
+
|
|
821
|
+
```tsx
|
|
822
|
+
// Correct — types match
|
|
823
|
+
const tableRef = useRef<ResponsiveTableHandle<Order>>(null);
|
|
824
|
+
<ResponsiveTable<Order> ref={tableRef} ... />
|
|
825
|
+
|
|
826
|
+
// Risky — mismatched generic silences type errors
|
|
827
|
+
const tableRef = useRef<ResponsiveTableHandle<unknown>>(null);
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
**Do not rely on `expandRows` returning a promise.**
|
|
831
|
+
The methods return `void` and update state synchronously. If you need to act after the panel is visually open (e.g., focus an element inside), use a `setTimeout` or `requestAnimationFrame` to yield after the CSS transition:
|
|
832
|
+
|
|
833
|
+
```tsx
|
|
834
|
+
tableRef.current?.expandRows('order-42');
|
|
835
|
+
setTimeout(() => {
|
|
836
|
+
document.querySelector('#order-42-form input')?.focus();
|
|
837
|
+
}, 350); // matches the 300ms panel open transition + margin
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
---
|
|
841
|
+
|
|
536
842
|
## Visual Anatomy
|
|
537
843
|
|
|
538
844
|
When `expandRowRenderer` returns content for a row, the following structure is rendered (desktop shown; mobile uses the same visual model in card layout):
|