jattac.libs.web.responsive-table 0.16.0 → 0.17.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.
@@ -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):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jattac.libs.web.responsive-table",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "description": "A fully responsive, customizable, and lightweight React table component with a modern, mobile-first design and a powerful plugin system.",
5
5
  "author": {
6
6
  "name": "Nyingi Maina",