@gtkx/components 2.0.0-beta.11 → 2.0.0-beta.13

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.
Files changed (92) hide show
  1. package/README.md +4 -4
  2. package/dist/column-view.d.ts.map +1 -1
  3. package/dist/column-view.js +8 -4
  4. package/dist/column-view.js.map +1 -1
  5. package/dist/combo-row.d.ts.map +1 -1
  6. package/dist/combo-row.js +3 -1
  7. package/dist/combo-row.js.map +1 -1
  8. package/dist/grid-view.d.ts.map +1 -1
  9. package/dist/grid-view.js +4 -3
  10. package/dist/grid-view.js.map +1 -1
  11. package/dist/internal/cells.d.ts +8 -9
  12. package/dist/internal/cells.d.ts.map +1 -1
  13. package/dist/internal/cells.js +98 -83
  14. package/dist/internal/cells.js.map +1 -1
  15. package/dist/internal/collection-index.d.ts +3 -5
  16. package/dist/internal/collection-index.d.ts.map +1 -1
  17. package/dist/internal/collection-index.js +3 -7
  18. package/dist/internal/collection-index.js.map +1 -1
  19. package/dist/internal/collection-model.d.ts +15 -4
  20. package/dist/internal/collection-model.d.ts.map +1 -1
  21. package/dist/internal/collection-model.js +154 -73
  22. package/dist/internal/collection-model.js.map +1 -1
  23. package/dist/internal/collection.d.ts +3 -3
  24. package/dist/internal/collection.d.ts.map +1 -1
  25. package/dist/internal/collection.js +11 -9
  26. package/dist/internal/collection.js.map +1 -1
  27. package/dist/internal/controlled-sync.d.ts +2 -1
  28. package/dist/internal/controlled-sync.d.ts.map +1 -1
  29. package/dist/internal/controlled-sync.js +6 -1
  30. package/dist/internal/controlled-sync.js.map +1 -1
  31. package/dist/internal/drop-down.d.ts +10 -4
  32. package/dist/internal/drop-down.d.ts.map +1 -1
  33. package/dist/internal/drop-down.js +61 -9
  34. package/dist/internal/drop-down.js.map +1 -1
  35. package/dist/internal/expansion.d.ts.map +1 -1
  36. package/dist/internal/expansion.js +46 -19
  37. package/dist/internal/expansion.js.map +1 -1
  38. package/dist/internal/first-header.d.ts +11 -0
  39. package/dist/internal/first-header.d.ts.map +1 -0
  40. package/dist/internal/first-header.js +21 -0
  41. package/dist/internal/first-header.js.map +1 -0
  42. package/dist/internal/keys.d.ts.map +1 -1
  43. package/dist/internal/level-splice.d.ts +27 -0
  44. package/dist/internal/level-splice.d.ts.map +1 -0
  45. package/dist/internal/level-splice.js +98 -0
  46. package/dist/internal/level-splice.js.map +1 -0
  47. package/dist/internal/selection.d.ts.map +1 -1
  48. package/dist/internal/selection.js +23 -16
  49. package/dist/internal/selection.js.map +1 -1
  50. package/dist/internal/tree-expansion.d.ts +3 -1
  51. package/dist/internal/tree-expansion.d.ts.map +1 -1
  52. package/dist/internal/tree-expansion.js +31 -2
  53. package/dist/internal/tree-expansion.js.map +1 -1
  54. package/dist/internal/tree-order.d.ts +2 -1
  55. package/dist/internal/tree-order.d.ts.map +1 -1
  56. package/dist/internal/tree-order.js +3 -2
  57. package/dist/internal/tree-order.js.map +1 -1
  58. package/dist/internal/use-collection.d.ts +1 -0
  59. package/dist/internal/use-collection.d.ts.map +1 -1
  60. package/dist/internal/use-collection.js +6 -2
  61. package/dist/internal/use-collection.js.map +1 -1
  62. package/dist/internal/use-widget-ref.d.ts.map +1 -1
  63. package/dist/list-item-factory.d.ts +1 -1
  64. package/dist/list-item-factory.d.ts.map +1 -1
  65. package/dist/list-item-factory.js +3 -3
  66. package/dist/list-item-factory.js.map +1 -1
  67. package/dist/list-view.d.ts.map +1 -1
  68. package/dist/list-view.js +9 -4
  69. package/dist/list-view.js.map +1 -1
  70. package/dist/types.d.ts +35 -12
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/types.js.map +1 -1
  73. package/package.json +10 -20
  74. package/src/column-view.tsx +8 -4
  75. package/src/combo-row.tsx +4 -1
  76. package/src/grid-view.tsx +3 -2
  77. package/src/internal/cells.tsx +174 -151
  78. package/src/internal/collection-index.ts +7 -14
  79. package/src/internal/collection-model.ts +211 -76
  80. package/src/internal/collection.ts +14 -13
  81. package/src/internal/controlled-sync.ts +8 -1
  82. package/src/internal/drop-down.tsx +112 -19
  83. package/src/internal/expansion.ts +52 -20
  84. package/src/internal/first-header.ts +23 -0
  85. package/src/internal/level-splice.ts +157 -0
  86. package/src/internal/selection.tsx +24 -19
  87. package/src/internal/tree-expansion.ts +43 -1
  88. package/src/internal/tree-order.ts +4 -10
  89. package/src/internal/use-collection.tsx +8 -2
  90. package/src/list-item-factory.tsx +2 -1
  91. package/src/list-view.tsx +10 -2
  92. package/src/types.ts +54 -24
@@ -1,9 +1,10 @@
1
1
  import type * as GObject from "@gtkx/gi/gobject";
2
- import type { ReactElement, ReactNode } from "react";
2
+ import type { ReactElement, ReactNode, RefObject } from "react";
3
3
  import * as Gtk from "@gtkx/gi/gtk";
4
4
  import { GtkBox, GtkSignalListItemFactory, GtkTreeExpander } from "@gtkx/jsx/gtk";
5
5
  import { createPortal, useProperty } from "@gtkx/react";
6
- import { Fragment, memo, useInsertionEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
6
+ import { useLatestRef } from "@gtkx/react/internal";
7
+ import { createRef, Fragment, memo, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
7
8
  import type {
8
9
  ExpanderDescriptions,
9
10
  ListItem,
@@ -14,7 +15,7 @@ import type {
14
15
  ListSectionRenderer,
15
16
  } from "../types.js";
16
17
  import type { Collection } from "./collection.js";
17
- import { slotPathAt, type SlotRef, slotRefFor } from "./collection-model.js";
18
+ import { type LazyLevelStore, levelStoreFor, slotRefFor } from "./collection-model.js";
18
19
  import { joinParts } from "./keys.js";
19
20
 
20
21
  type CellSize = {
@@ -34,7 +35,7 @@ type CellHost = FactoryHost & {
34
35
  type CellEntry<H extends FactoryHost> = {
35
36
  key: string;
36
37
  host: H;
37
- identity: ItemIdentityStore;
38
+ identity: RefObject<ItemIdentity | null>;
38
39
  };
39
40
 
40
41
  type FactoryHandlers = {
@@ -86,6 +87,7 @@ type PositionedHost = Gtk.ListItem | Gtk.ColumnViewRow;
86
87
  type ItemSlot = {
87
88
  itemKey: string;
88
89
  item: ListItem;
90
+ position: number | undefined;
89
91
  row: Gtk.TreeListRow | null;
90
92
  args: ListItemRenderArgs<unknown>;
91
93
  };
@@ -95,27 +97,19 @@ type CollectionState = {
95
97
  expandedIds: string[] | null | undefined;
96
98
  };
97
99
 
98
- type CollectionStateStore = {
99
- read: () => CollectionState;
100
- write: (state: CollectionState) => void;
101
- };
102
-
103
100
  type ItemIdentity = {
104
101
  itemKey: string;
105
102
  item: ListItem | undefined;
106
103
  position: number;
104
+ index: number;
107
105
  isExpanded: boolean;
108
106
  };
109
107
 
110
- type ItemIdentityStore = {
111
- read: () => ItemIdentity | null;
112
- write: (identity: ItemIdentity) => void;
113
- };
114
-
115
108
  type ItemCellProps = {
116
109
  entry: CellEntry<Gtk.ListItem>;
117
110
  render: ListItemRenderer<never>;
118
- state: CollectionStateStore;
111
+ rowProps: ListRowPropsResolver<never> | null | undefined;
112
+ state: RefObject<CollectionState>;
119
113
  hasExpander: boolean;
120
114
  expanderDescriptions: ExpanderDescriptions | null | undefined;
121
115
  } & ItemIdentity;
@@ -123,16 +117,26 @@ type ItemCellProps = {
123
117
  type ItemPortalsProps = {
124
118
  registry: CellRegistry<Gtk.ListItem>;
125
119
  render: ListItemRenderer<never>;
120
+ rowProps?: ListRowPropsResolver<never> | null | undefined;
126
121
  collection: Collection;
127
122
  hasExpander?: boolean | undefined;
128
123
  expandedIds?: string[] | null | undefined;
129
124
  expanderDescriptions?: ExpanderDescriptions | null | undefined;
130
125
  };
131
126
 
127
+ type PendingList<H extends FactoryHost> = {
128
+ entries: CellEntry<H>[];
129
+ isPending: boolean;
130
+ };
131
+
132
+ type ItemCellListProps = Omit<ItemPortalsProps, "registry"> & PendingList<Gtk.ListItem>;
133
+
134
+ type ItemRowListProps = Omit<RowPortalsProps, "registry"> & PendingList<Gtk.ColumnViewRow>;
135
+
132
136
  type ItemRowProps = {
133
137
  entry: CellEntry<Gtk.ColumnViewRow>;
134
138
  rowProps: ListRowPropsResolver<never>;
135
- state: CollectionStateStore;
139
+ state: RefObject<CollectionState>;
136
140
  } & ItemIdentity;
137
141
 
138
142
  type RowPortalsProps = {
@@ -145,7 +149,7 @@ type RowPortalsProps = {
145
149
  type HeaderCellProps = {
146
150
  entry: CellEntry<Gtk.ListHeader>;
147
151
  render: ListSectionRenderer<never>;
148
- collection: Collection;
152
+ version: number;
149
153
  };
150
154
 
151
155
  type HeaderPortalsProps = {
@@ -161,6 +165,7 @@ type NativeItemKeyStore = {
161
165
  type NativeItemCellProps<T extends GObject.Object> = {
162
166
  entry: CellEntry<Gtk.ListItem>;
163
167
  render: ListItemRenderer<T>;
168
+ rowProps: ListRowPropsResolver<T> | null | undefined;
164
169
  keys: NativeItemKeyStore;
165
170
  size: CellSize;
166
171
  };
@@ -168,6 +173,7 @@ type NativeItemCellProps<T extends GObject.Object> = {
168
173
  type NativeItemPortalsProps<T extends GObject.Object> = {
169
174
  registry: CellRegistry<Gtk.ListItem>;
170
175
  render: ListItemRenderer<T>;
176
+ rowProps: ListRowPropsResolver<T> | null | undefined;
171
177
  size: CellSize;
172
178
  };
173
179
 
@@ -184,11 +190,14 @@ type RowSlot = {
184
190
  const ItemCell = memo(ItemCellImpl, isSameItemCell);
185
191
  const ItemRow = memo(ItemRowImpl, isSameItemRow);
186
192
  const HeaderCell = memo(HeaderCellImpl);
193
+ const ItemCellList = memo(ItemCellListImpl, isWaitingForSync);
194
+ const ItemRowList = memo(ItemRowListImpl, isWaitingForSync);
187
195
  const CELL_OPTIONS: CellRegistryOptions<Gtk.ListItem> = { isHost: isListItem, prepare: prepareCell };
188
196
  const HEADER_OPTIONS: CellRegistryOptions<Gtk.ListHeader> = { isHost: isListHeader, prepare: prepareCell };
189
197
  const ROW_OPTIONS: CellRegistryOptions<Gtk.ColumnViewRow> = { isHost: isColumnViewRow };
190
198
  const NO_SIZE: CellSize = { width: -1, height: -1 };
191
199
  const NO_ROW_PROPS: ListRowProps = {};
200
+ const DEFAULT_ROW_PROPS = resolveRowProps(NO_ROW_PROPS);
192
201
  const placeholders: WeakSet<Gtk.Widget> = new WeakSet();
193
202
 
194
203
  const placeholder = (size: CellSize): Gtk.Widget => {
@@ -231,7 +240,7 @@ function notifyRegistry<H extends FactoryHost>(state: CellRegistryState<H>): voi
231
240
  function createEntry<H extends FactoryHost>(state: CellRegistryState<H>, host: H): CellEntry<H> {
232
241
  state.serial += 1;
233
242
 
234
- return { key: `gtkx-cell-${String(state.serial)}`, host, identity: createItemIdentityStore() };
243
+ return { key: `gtkx-cell-${String(state.serial)}`, host, identity: createRef<ItemIdentity>() };
235
244
  }
236
245
 
237
246
  function setupHost<H extends FactoryHost>(state: CellRegistryState<H>, host: H): void {
@@ -338,13 +347,27 @@ function isRowWanted(options: ItemSlotOptions, item: ListItem): boolean {
338
347
  return options.expandedIds?.includes(item.id) ?? false;
339
348
  }
340
349
 
350
+ function collectionIndex(collection: Collection, item: ListItem, position: number | undefined): number {
351
+ if (position === undefined) {
352
+ return 0;
353
+ }
354
+
355
+ if (collection.idAt(position) === item.id) {
356
+ return position;
357
+ }
358
+
359
+ const index = collection.positionFor(item.id);
360
+
361
+ return index === -1 ? position : index;
362
+ }
363
+
341
364
  function itemArgs(
342
365
  item: ListItem,
343
- position: number | undefined,
366
+ index: number,
344
367
  row: Gtk.TreeListRow | null,
345
368
  isExpanded: boolean,
346
369
  ): ListItemRenderArgs<unknown> {
347
- const args: ListItemRenderArgs<unknown> = { item: item.value, index: position ?? 0 };
370
+ const args: ListItemRenderArgs<unknown> = { item: item.value, index };
348
371
 
349
372
  if (row === null) {
350
373
  return args;
@@ -359,10 +382,6 @@ function itemArgs(
359
382
  return args;
360
383
  }
361
384
 
362
- function keyForItem(ref: SlotRef, item: ListItem): string {
363
- return joinParts([slotPathAt(ref.store, ref.slot), item.id]);
364
- }
365
-
366
385
  function slotFor(options: ItemSlotOptions): ItemSlot | null {
367
386
  const ref = slotRefFor(options.item);
368
387
 
@@ -377,35 +396,41 @@ function slotFor(options: ItemSlotOptions): ItemSlot | null {
377
396
  }
378
397
 
379
398
  return {
380
- itemKey: keyForItem(ref, item),
399
+ itemKey: ref.key,
381
400
  item,
401
+ position: options.position,
382
402
  row: options.row,
383
- args: itemArgs(item, options.position, options.row, isRowWanted(options, item)),
403
+ args: itemArgs(
404
+ item,
405
+ collectionIndex(options.collection, item, options.position),
406
+ options.row,
407
+ isRowWanted(options, item),
408
+ ),
384
409
  };
385
410
  }
386
411
 
387
412
  function useItemSlot(
388
413
  entry: CellEntry<PositionedHost>,
389
- state: CollectionStateStore,
414
+ state: RefObject<CollectionState>,
390
415
  identity: ItemIdentity,
391
416
  ): ItemSlot | null {
392
417
  const position = useProperty(entry.host, "position");
393
418
  const item = useProperty(entry.host, "item") ?? null;
394
419
  const row = item instanceof Gtk.TreeListRow ? item : null;
420
+ const { collection, expandedIds } = state.current;
395
421
 
396
422
  if (position === identity.position) {
397
423
  return identity.item === undefined
398
424
  ? null
399
425
  : {
400
- itemKey: identity.itemKey,
401
- item: identity.item,
402
- row,
403
- args: itemArgs(identity.item, position, row, identity.isExpanded),
404
- };
426
+ itemKey: identity.itemKey,
427
+ item: identity.item,
428
+ position,
429
+ row,
430
+ args: itemArgs(identity.item, identity.index, row, identity.isExpanded),
431
+ };
405
432
  }
406
433
 
407
- const { collection, expandedIds } = state.read();
408
-
409
434
  return slotFor({ item, position, row, collection, expandedIds });
410
435
  }
411
436
 
@@ -416,11 +441,13 @@ function itemIdentity(
416
441
  ): ItemIdentity {
417
442
  const ref = slotRefFor(entry.host.getItem());
418
443
  const item = ref === null ? undefined : collection.itemAt(ref);
444
+ const position = entry.host.getPosition();
419
445
 
420
446
  return {
421
- itemKey: ref === null || item === undefined ? "" : keyForItem(ref, item),
447
+ itemKey: ref === null || item === undefined ? "" : ref.key,
422
448
  item,
423
- position: entry.host.getPosition(),
449
+ position,
450
+ index: item === undefined ? 0 : collectionIndex(collection, item, position),
424
451
  isExpanded: item !== undefined && (expandedIds?.includes(item.id) ?? false),
425
452
  };
426
453
  }
@@ -430,37 +457,32 @@ function isSameItemIdentity(previous: ItemIdentity, next: ItemIdentity): boolean
430
457
  previous.item === next.item &&
431
458
  previous.itemKey === next.itemKey &&
432
459
  previous.position === next.position &&
460
+ previous.index === next.index &&
433
461
  previous.isExpanded === next.isExpanded
434
462
  );
435
463
  }
436
464
 
437
465
  function isSameItemCell(previous: ItemCellProps, next: ItemCellProps): boolean {
438
- const identity = previous.entry.identity.read() ?? previous;
466
+ const identity = previous.entry.identity.current ?? previous;
439
467
 
440
468
  return (
441
469
  previous.entry === next.entry &&
442
470
  previous.render === next.render &&
471
+ previous.rowProps === next.rowProps &&
443
472
  previous.hasExpander === next.hasExpander &&
444
473
  previous.expanderDescriptions === next.expanderDescriptions &&
445
474
  isSameItemIdentity(identity, next)
446
475
  );
447
476
  }
448
477
 
449
- function isSameItemRow(previous: ItemRowProps, next: ItemRowProps): boolean {
450
- const identity = previous.entry.identity.read() ?? previous;
451
-
452
- return previous.entry === next.entry && previous.rowProps === next.rowProps && isSameItemIdentity(identity, next);
478
+ function isWaitingForSync<H extends FactoryHost>(_previous: PendingList<H>, next: PendingList<H>): boolean {
479
+ return next.isPending;
453
480
  }
454
481
 
455
- function createItemIdentityStore(): ItemIdentityStore {
456
- let current: ItemIdentity | null = null;
482
+ function isSameItemRow(previous: ItemRowProps, next: ItemRowProps): boolean {
483
+ const identity = previous.entry.identity.current ?? previous;
457
484
 
458
- return {
459
- read: () => current,
460
- write: (identity) => {
461
- current = identity;
462
- },
463
- };
485
+ return previous.entry === next.entry && previous.rowProps === next.rowProps && isSameItemIdentity(identity, next);
464
486
  }
465
487
 
466
488
  function createNativeItemKeyStore(): NativeItemKeyStore {
@@ -488,41 +510,19 @@ function renderedIdentity(entry: CellEntry<PositionedHost>, slot: ItemSlot | nul
488
510
  return {
489
511
  itemKey: slot?.itemKey ?? "",
490
512
  item: slot?.item,
491
- position: slot?.args.index ?? entry.host.getPosition(),
513
+ position: slot?.position ?? entry.host.getPosition(),
514
+ index: slot?.args.index ?? 0,
492
515
  isExpanded: slot?.args.isExpanded ?? false,
493
516
  };
494
517
  }
495
518
 
496
519
  function useRememberIdentity(entry: CellEntry<PositionedHost>, slot: ItemSlot | null): void {
520
+ const identityRef = entry.identity;
497
521
  useLayoutEffect(() => {
498
- entry.identity.write(renderedIdentity(entry, slot));
522
+ identityRef.current = renderedIdentity(entry, slot);
499
523
  });
500
524
  }
501
525
 
502
- function createCollectionStateStore(initial: CollectionState): CollectionStateStore {
503
- let current = initial;
504
-
505
- return {
506
- read: () => current,
507
- write: (state) => {
508
- current = state;
509
- },
510
- };
511
- }
512
-
513
- function useCollectionState(
514
- collection: Collection,
515
- expandedIds: string[] | null | undefined,
516
- ): CollectionStateStore {
517
- const [state] = useState(() => createCollectionStateStore({ collection, expandedIds }));
518
-
519
- useInsertionEffect(() => {
520
- state.write({ collection, expandedIds });
521
- }, [state, collection, expandedIds]);
522
-
523
- return state;
524
- }
525
-
526
526
  function expanderDescriptionFor(
527
527
  slot: ItemSlot,
528
528
  descriptions: ExpanderDescriptions | null | undefined,
@@ -544,21 +544,19 @@ function wrapExpander(
544
544
  const { item, row } = slot;
545
545
 
546
546
  const expander: ReactNode =
547
- row === null
548
- ? (
549
- content
550
- )
551
- : (
552
- <GtkTreeExpander
553
- listRow={row}
554
- hideExpander={item.shouldHideExpander ?? false}
555
- indentForDepth={item.shouldIndentForDepth ?? true}
556
- indentForIcon={item.shouldIndentForIcon ?? true}
557
- accessibleDescription={expanderDescriptionFor(slot, descriptions)}
558
- >
559
- {content}
560
- </GtkTreeExpander>
561
- );
547
+ row === null ? (
548
+ content
549
+ ) : (
550
+ <GtkTreeExpander
551
+ listRow={row}
552
+ hideExpander={item.shouldHideExpander ?? false}
553
+ indentForDepth={item.shouldIndentForDepth ?? true}
554
+ indentForIcon={item.shouldIndentForIcon ?? true}
555
+ accessibleDescription={expanderDescriptionFor(slot, descriptions)}
556
+ >
557
+ {content}
558
+ </GtkTreeExpander>
559
+ );
562
560
 
563
561
  return expander;
564
562
  }
@@ -576,24 +574,18 @@ function itemBody(
576
574
  const renderItem = render as ListItemRenderer<unknown>;
577
575
  const content = renderItem(slot.args);
578
576
 
579
- return (
580
- <Fragment key={slot.itemKey}>{hasExpander ? wrapExpander(slot, content, descriptions) : content}</Fragment>
581
- );
577
+ return <Fragment key={slot.itemKey}>{hasExpander ? wrapExpander(slot, content, descriptions) : content}</Fragment>;
582
578
  }
583
579
 
584
580
  function rowText(value: string | undefined): string | null {
585
581
  return value ?? null;
586
582
  }
587
583
 
588
- /* eslint-disable-next-line unicorn/consistent-boolean-name -- reads the flag off a row, paired with rowText */
589
584
  function rowFlag(value: boolean | undefined): boolean {
590
585
  return value ?? true;
591
586
  }
592
587
 
593
- function rowPropsFor(slot: ItemSlot | null, rowProps: ListRowPropsResolver<never>): ResolvedRowProps {
594
- const resolve = rowProps as ListRowPropsResolver<unknown>;
595
- const props = slot === null ? NO_ROW_PROPS : resolve(slot.args);
596
-
588
+ function resolveRowProps(props: ListRowProps): ResolvedRowProps {
597
589
  return {
598
590
  accessibleLabel: rowText(props.accessibleLabel),
599
591
  accessibleDescription: rowText(props.accessibleDescription),
@@ -603,11 +595,13 @@ function rowPropsFor(slot: ItemSlot | null, rowProps: ListRowPropsResolver<never
603
595
  };
604
596
  }
605
597
 
606
- function applyRowProps(
607
- host: Gtk.ColumnViewRow,
608
- props: ResolvedRowProps,
609
- previous: ResolvedRowProps | null,
610
- ): void {
598
+ function rowPropsFor(slot: ItemSlot | null, rowProps: ListRowPropsResolver<never>): ResolvedRowProps {
599
+ const resolve = rowProps as ListRowPropsResolver<unknown>;
600
+
601
+ return resolveRowProps(slot === null ? NO_ROW_PROPS : resolve(slot.args));
602
+ }
603
+
604
+ function applyRowProps(host: PositionedHost, props: ResolvedRowProps, previous: ResolvedRowProps | null): void {
611
605
  if (previous?.accessibleLabel !== props.accessibleLabel) {
612
606
  host.accessibleLabel = props.accessibleLabel;
613
607
  }
@@ -629,9 +623,24 @@ function applyRowProps(
629
623
  }
630
624
  }
631
625
 
626
+ function useAppliedRowProps(host: PositionedHost, props: ResolvedRowProps | null): void {
627
+ const previous = useRef<ResolvedRowProps | null>(null);
628
+
629
+ useLayoutEffect(() => {
630
+ if (props === null && previous.current === null) {
631
+ return;
632
+ }
633
+
634
+ const next = props ?? DEFAULT_ROW_PROPS;
635
+ applyRowProps(host, next, previous.current);
636
+ previous.current = props === null ? null : next;
637
+ }, [host, props]);
638
+ }
639
+
632
640
  function ItemCellImpl({
633
641
  entry,
634
642
  render,
643
+ rowProps,
635
644
  state,
636
645
  hasExpander,
637
646
  expanderDescriptions,
@@ -639,6 +648,7 @@ function ItemCellImpl({
639
648
  }: ItemCellProps): ReactNode {
640
649
  const slot = useItemSlot(entry, state, identity);
641
650
  useRememberIdentity(entry, slot);
651
+ useAppliedRowProps(entry.host, rowProps == null ? null : rowPropsFor(slot, rowProps));
642
652
  const body = itemBody(slot, render, hasExpander, expanderDescriptions);
643
653
 
644
654
  return createPortal(body, entry.host, entry.key);
@@ -647,61 +657,56 @@ function ItemCellImpl({
647
657
  function ItemRowImpl({ entry, rowProps, state, ...identity }: ItemRowProps): ReactNode {
648
658
  const slot = useItemSlot(entry, state, identity);
649
659
  useRememberIdentity(entry, slot);
650
- const props = rowPropsFor(slot, rowProps);
651
- const previous = useRef<ResolvedRowProps | null>(null);
652
-
653
- useLayoutEffect(() => {
654
- applyRowProps(entry.host, props, previous.current);
655
- previous.current = props;
656
- }, [entry.host, props]);
660
+ useAppliedRowProps(entry.host, rowPropsFor(slot, rowProps));
657
661
 
658
662
  return null;
659
663
  }
660
664
 
661
- function HeaderCellImpl({ entry, render, collection }: HeaderCellProps): ReactNode {
665
+ function HeaderCellImpl({ entry, render }: HeaderCellProps): ReactNode {
662
666
  const item = useProperty(entry.host, "item") ?? null;
663
667
 
664
- return createPortal(headerBody(slotRefFor(item), render, collection), entry.host, entry.key);
668
+ return createPortal(headerBody(levelStoreFor(item), render), entry.host, entry.key);
665
669
  }
666
670
 
667
671
  function NativeItemCell<T extends GObject.Object>({
668
672
  entry,
669
673
  render,
674
+ rowProps,
670
675
  keys,
671
676
  size,
672
677
  }: NativeItemCellProps<T>): ReactNode {
673
678
  const position = useProperty(entry.host, "position");
674
679
  const item = useProperty(entry.host, "item") ?? null;
680
+ const args = item !== null && position !== undefined ? { item: item as T, index: position } : null;
681
+ useAppliedRowProps(
682
+ entry.host,
683
+ rowProps == null ? null : resolveRowProps(args === null ? NO_ROW_PROPS : rowProps(args)),
684
+ );
675
685
  let body: ReactNode = null;
676
686
 
677
- if (item !== null && position !== undefined) {
678
- const content = render({ item: item as T, index: position });
679
- const child = content == null || typeof content === "boolean"
680
- ? <GtkBox widthRequest={size.width} heightRequest={size.height} />
681
- : content;
687
+ if (item !== null && args !== null) {
688
+ const content = render(args);
689
+ const child =
690
+ content == null || typeof content === "boolean" ? (
691
+ <GtkBox widthRequest={size.width} heightRequest={size.height} />
692
+ ) : (
693
+ content
694
+ );
682
695
  body = <Fragment key={keys.keyFor(item)}>{child}</Fragment>;
683
696
  }
684
697
 
685
698
  return createPortal(body, entry.host, entry.key);
686
699
  }
687
700
 
688
- function headerBody(
689
- ref: SlotRef | null,
690
- render: ListSectionRenderer<never>,
691
- collection: Collection,
692
- ): ReactNode {
693
- if (ref === null) {
694
- return null;
695
- }
696
-
697
- const section = collection.sectionFor(ref.store.level.path);
701
+ function headerBody(store: LazyLevelStore | null, render: ListSectionRenderer<never>): ReactNode {
702
+ const section = store?.level.section;
698
703
 
699
- if (section === undefined) {
704
+ if (store === null || section === undefined) {
700
705
  return null;
701
706
  }
702
707
 
703
708
  const renderHeader = render as ListSectionRenderer<unknown>;
704
- const key = joinParts([ref.store.level.path, section.id]);
709
+ const key = joinParts([store.key, section.id]);
705
710
 
706
711
  return <Fragment key={key}>{renderHeader({ section: section.value })}</Fragment>;
707
712
  }
@@ -710,33 +715,44 @@ function usePortalEntries<H extends FactoryHost>(registry: CellRegistry<H>): Cel
710
715
  return useSyncExternalStore(registry.subscribe, registry.getEntries);
711
716
  }
712
717
 
713
- const ItemPortals = ({
714
- registry,
718
+ function useCollectionVersion(collection: Collection): number {
719
+ return useSyncExternalStore(collection.subscribe, collection.getVersion);
720
+ }
721
+
722
+ function ItemCellListImpl({
723
+ entries,
715
724
  render,
725
+ rowProps,
716
726
  collection,
717
727
  hasExpander,
718
728
  expandedIds,
719
729
  expanderDescriptions,
720
- }: ItemPortalsProps): ReactNode => {
721
- const entries = usePortalEntries(registry);
722
- const state = useCollectionState(collection, expandedIds);
730
+ }: ItemCellListProps): ReactNode {
731
+ const state = useLatestRef({ collection, expandedIds });
723
732
 
724
733
  return entries.map((entry) => (
725
734
  <ItemCell
726
735
  key={entry.key}
727
736
  entry={entry}
728
737
  render={render}
738
+ rowProps={rowProps}
729
739
  state={state}
730
740
  hasExpander={hasExpander ?? true}
731
741
  expanderDescriptions={expanderDescriptions}
732
742
  {...itemIdentity(entry, collection, expandedIds)}
733
743
  />
734
744
  ));
735
- };
745
+ }
736
746
 
737
- const RowPortals = ({ registry, rowProps, collection, expandedIds }: RowPortalsProps): ReactNode => {
747
+ const ItemPortals = ({ registry, ...props }: ItemPortalsProps): ReactNode => {
738
748
  const entries = usePortalEntries(registry);
739
- const state = useCollectionState(collection, expandedIds);
749
+ useCollectionVersion(props.collection);
750
+
751
+ return <ItemCellList {...props} entries={entries} isPending={props.collection.isPending()} />;
752
+ };
753
+
754
+ function ItemRowListImpl({ entries, rowProps, collection, expandedIds }: ItemRowListProps): ReactNode {
755
+ const state = useLatestRef({ collection, expandedIds });
740
756
 
741
757
  return entries.map((entry) => (
742
758
  <ItemRow
@@ -747,19 +763,33 @@ const RowPortals = ({ registry, rowProps, collection, expandedIds }: RowPortalsP
747
763
  {...itemIdentity(entry, collection, expandedIds)}
748
764
  />
749
765
  ));
766
+ }
767
+
768
+ const RowPortals = ({ registry, ...props }: RowPortalsProps): ReactNode => {
769
+ const entries = usePortalEntries(registry);
770
+ useCollectionVersion(props.collection);
771
+
772
+ return <ItemRowList {...props} entries={entries} isPending={props.collection.isPending()} />;
750
773
  };
751
774
 
752
- const HeaderPortals = ({ registry, render, collection }: HeaderPortalsProps): ReactNode =>
753
- usePortalEntries(registry).map((entry) => (
754
- <HeaderCell key={entry.key} entry={entry} render={render} collection={collection} />
755
- ));
775
+ const HeaderPortals = ({ registry, render, collection }: HeaderPortalsProps): ReactNode => {
776
+ const entries = usePortalEntries(registry);
777
+ const version = useCollectionVersion(collection);
756
778
 
757
- function NativeItemPortals<T extends GObject.Object>({ registry, render, size }: NativeItemPortalsProps<T>): ReactNode {
779
+ return entries.map((entry) => <HeaderCell key={entry.key} entry={entry} render={render} version={version} />);
780
+ };
781
+
782
+ function NativeItemPortals<T extends GObject.Object>({
783
+ registry,
784
+ render,
785
+ rowProps,
786
+ size,
787
+ }: NativeItemPortalsProps<T>): ReactNode {
758
788
  const entries = usePortalEntries(registry);
759
789
  const [keys] = useState(createNativeItemKeyStore);
760
790
 
761
791
  return entries.map((entry) => (
762
- <NativeItemCell key={entry.key} entry={entry} render={render} keys={keys} size={size} />
792
+ <NativeItemCell key={entry.key} entry={entry} render={render} rowProps={rowProps} keys={keys} size={size} />
763
793
  ));
764
794
  }
765
795
 
@@ -799,11 +829,4 @@ const useRowProps = (
799
829
  };
800
830
  };
801
831
 
802
- export {
803
- useItemCells,
804
- useRowProps,
805
- useSectionHeader,
806
- ItemPortals,
807
- NativeItemPortals,
808
- type CellSize,
809
- };
832
+ export { useItemCells, useRowProps, useSectionHeader, ItemPortals, NativeItemPortals, type CellSize };