@gtkx/components 2.0.0-beta.10 → 2.0.0-beta.12

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 (86) hide show
  1. package/README.md +8 -168
  2. package/dist/column-view.d.ts.map +1 -1
  3. package/dist/column-view.js +20 -10
  4. package/dist/column-view.js.map +1 -1
  5. package/dist/grid-view.js +1 -1
  6. package/dist/index.d.ts +4 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/internal/cells.d.ts +10 -7
  11. package/dist/internal/cells.d.ts.map +1 -1
  12. package/dist/internal/cells.js +88 -51
  13. package/dist/internal/cells.js.map +1 -1
  14. package/dist/internal/collection-index.d.ts +3 -2
  15. package/dist/internal/collection-index.d.ts.map +1 -1
  16. package/dist/internal/collection-index.js +3 -3
  17. package/dist/internal/collection-index.js.map +1 -1
  18. package/dist/internal/collection-model.d.ts +23 -2
  19. package/dist/internal/collection-model.d.ts.map +1 -1
  20. package/dist/internal/collection-model.js +118 -98
  21. package/dist/internal/collection-model.js.map +1 -1
  22. package/dist/internal/collection.d.ts +5 -4
  23. package/dist/internal/collection.d.ts.map +1 -1
  24. package/dist/internal/collection.js +20 -21
  25. package/dist/internal/collection.js.map +1 -1
  26. package/dist/internal/controlled-sync.d.ts +6 -8
  27. package/dist/internal/controlled-sync.d.ts.map +1 -1
  28. package/dist/internal/controlled-sync.js +3 -3
  29. package/dist/internal/controlled-sync.js.map +1 -1
  30. package/dist/internal/drop-down.d.ts.map +1 -1
  31. package/dist/internal/drop-down.js +44 -45
  32. package/dist/internal/drop-down.js.map +1 -1
  33. package/dist/internal/expansion.js +4 -4
  34. package/dist/internal/expansion.js.map +1 -1
  35. package/dist/internal/keys.d.ts.map +1 -1
  36. package/dist/internal/selection.d.ts +1 -0
  37. package/dist/internal/selection.d.ts.map +1 -1
  38. package/dist/internal/selection.js +6 -6
  39. package/dist/internal/selection.js.map +1 -1
  40. package/dist/internal/tree-expansion.d.ts +1 -1
  41. package/dist/internal/tree-expansion.d.ts.map +1 -1
  42. package/dist/internal/tree-expansion.js +3 -12
  43. package/dist/internal/tree-expansion.js.map +1 -1
  44. package/dist/internal/tree-order.d.ts +1 -1
  45. package/dist/internal/tree-order.d.ts.map +1 -1
  46. package/dist/internal/tree-order.js +33 -77
  47. package/dist/internal/tree-order.js.map +1 -1
  48. package/dist/internal/use-collection.d.ts +5 -1
  49. package/dist/internal/use-collection.d.ts.map +1 -1
  50. package/dist/internal/use-collection.js +21 -7
  51. package/dist/internal/use-collection.js.map +1 -1
  52. package/dist/internal/use-widget-ref.d.ts.map +1 -1
  53. package/dist/list-item-factory.d.ts +10 -0
  54. package/dist/list-item-factory.d.ts.map +1 -0
  55. package/dist/list-item-factory.js +14 -0
  56. package/dist/list-item-factory.js.map +1 -0
  57. package/dist/list-view.js +1 -1
  58. package/dist/tab-collection.d.ts +9 -0
  59. package/dist/tab-collection.d.ts.map +1 -0
  60. package/dist/tab-collection.js +114 -0
  61. package/dist/tab-collection.js.map +1 -0
  62. package/dist/toast.d.ts +3 -5
  63. package/dist/toast.d.ts.map +1 -1
  64. package/dist/toast.js +3 -12
  65. package/dist/toast.js.map +1 -1
  66. package/dist/types.d.ts +67 -51
  67. package/dist/types.d.ts.map +1 -1
  68. package/dist/types.js.map +1 -1
  69. package/package.json +13 -21
  70. package/src/column-view.tsx +23 -12
  71. package/src/index.ts +7 -2
  72. package/src/internal/cells.tsx +155 -103
  73. package/src/internal/collection-index.ts +8 -6
  74. package/src/internal/collection-model.ts +156 -134
  75. package/src/internal/collection.ts +28 -24
  76. package/src/internal/controlled-sync.ts +9 -12
  77. package/src/internal/drop-down.tsx +63 -63
  78. package/src/internal/expansion.ts +4 -4
  79. package/src/internal/selection.tsx +8 -8
  80. package/src/internal/tree-expansion.ts +3 -23
  81. package/src/internal/tree-order.ts +34 -130
  82. package/src/internal/use-collection.tsx +29 -8
  83. package/src/list-item-factory.tsx +27 -0
  84. package/src/tab-collection.tsx +124 -0
  85. package/src/toast.tsx +4 -18
  86. package/src/types.ts +81 -55
@@ -1,10 +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
- import { GtkSignalListItemFactory, GtkTreeExpander } from "@gtkx/jsx/gtk";
4
+ import { GtkBox, GtkSignalListItemFactory, GtkTreeExpander } from "@gtkx/jsx/gtk";
5
5
  import { createPortal, useProperty } from "@gtkx/react";
6
- import { setProperty, t } from "@gtkx/runtime";
7
- import { 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";
8
8
  import type {
9
9
  ExpanderDescriptions,
10
10
  ListItem,
@@ -15,7 +15,8 @@ import type {
15
15
  ListSectionRenderer,
16
16
  } from "../types.js";
17
17
  import type { Collection } from "./collection.js";
18
- import { slotRefFor } from "./collection-model.js";
18
+ import { slotPathAt, type SlotRef, slotRefFor } from "./collection-model.js";
19
+ import { joinParts } from "./keys.js";
19
20
 
20
21
  type CellSize = {
21
22
  width: number;
@@ -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 = {
@@ -84,6 +85,7 @@ type ItemSlotOptions = {
84
85
  type PositionedHost = Gtk.ListItem | Gtk.ColumnViewRow;
85
86
 
86
87
  type ItemSlot = {
88
+ itemKey: string;
87
89
  item: ListItem;
88
90
  row: Gtk.TreeListRow | null;
89
91
  args: ListItemRenderArgs<unknown>;
@@ -94,26 +96,17 @@ type CollectionState = {
94
96
  expandedIds: string[] | null | undefined;
95
97
  };
96
98
 
97
- type CollectionStateStore = {
98
- read: () => CollectionState;
99
- write: (state: CollectionState) => void;
100
- };
101
-
102
99
  type ItemIdentity = {
100
+ itemKey: string;
103
101
  item: ListItem | undefined;
104
102
  position: number;
105
103
  isExpanded: boolean;
106
104
  };
107
105
 
108
- type ItemIdentityStore = {
109
- read: () => ItemIdentity | null;
110
- write: (identity: ItemIdentity) => void;
111
- };
112
-
113
106
  type ItemCellProps = {
114
107
  entry: CellEntry<Gtk.ListItem>;
115
108
  render: ListItemRenderer<never>;
116
- state: CollectionStateStore;
109
+ state: RefObject<CollectionState>;
117
110
  hasExpander: boolean;
118
111
  expanderDescriptions: ExpanderDescriptions | null | undefined;
119
112
  } & ItemIdentity;
@@ -130,7 +123,7 @@ type ItemPortalsProps = {
130
123
  type ItemRowProps = {
131
124
  entry: CellEntry<Gtk.ColumnViewRow>;
132
125
  rowProps: ListRowPropsResolver<never>;
133
- state: CollectionStateStore;
126
+ state: RefObject<CollectionState>;
134
127
  } & ItemIdentity;
135
128
 
136
129
  type RowPortalsProps = {
@@ -152,6 +145,23 @@ type HeaderPortalsProps = {
152
145
  collection: Collection;
153
146
  };
154
147
 
148
+ type NativeItemKeyStore = {
149
+ keyFor: (item: GObject.Object) => string;
150
+ };
151
+
152
+ type NativeItemCellProps<T extends GObject.Object> = {
153
+ entry: CellEntry<Gtk.ListItem>;
154
+ render: ListItemRenderer<T>;
155
+ keys: NativeItemKeyStore;
156
+ size: CellSize;
157
+ };
158
+
159
+ type NativeItemPortalsProps<T extends GObject.Object> = {
160
+ registry: CellRegistry<Gtk.ListItem>;
161
+ render: ListItemRenderer<T>;
162
+ size: CellSize;
163
+ };
164
+
155
165
  type SectionHeaderSlot = {
156
166
  factoryProps: { headerFactory?: ReactElement };
157
167
  portals: ReactNode;
@@ -170,10 +180,14 @@ const HEADER_OPTIONS: CellRegistryOptions<Gtk.ListHeader> = { isHost: isListHead
170
180
  const ROW_OPTIONS: CellRegistryOptions<Gtk.ColumnViewRow> = { isHost: isColumnViewRow };
171
181
  const NO_SIZE: CellSize = { width: -1, height: -1 };
172
182
  const NO_ROW_PROPS: ListRowProps = {};
173
- const ROW_TEXT_DESCRIPTOR = t.string("borrowed");
183
+ const placeholders: WeakSet<Gtk.Widget> = new WeakSet();
184
+
185
+ const placeholder = (size: CellSize): Gtk.Widget => {
186
+ const widget = new Gtk.Box({ widthRequest: size.width, heightRequest: size.height });
187
+ placeholders.add(widget);
174
188
 
175
- const placeholder = (size: CellSize): Gtk.Widget =>
176
- new Gtk.Box({ widthRequest: size.width, heightRequest: size.height });
189
+ return widget;
190
+ };
177
191
 
178
192
  function isListItem(value: GObject.Object): value is Gtk.ListItem {
179
193
  return value instanceof Gtk.ListItem;
@@ -188,8 +202,12 @@ function isColumnViewRow(value: GObject.Object): value is Gtk.ColumnViewRow {
188
202
  }
189
203
 
190
204
  function prepareCell(host: CellHost, size: CellSize): void {
191
- if (host.getChild() === null) {
205
+ const child = host.getChild();
206
+
207
+ if (child === null) {
192
208
  host.setChild(placeholder(size));
209
+ } else if (placeholders.has(child)) {
210
+ child.setSizeRequest(size.width, size.height);
193
211
  }
194
212
  }
195
213
 
@@ -204,7 +222,7 @@ function notifyRegistry<H extends FactoryHost>(state: CellRegistryState<H>): voi
204
222
  function createEntry<H extends FactoryHost>(state: CellRegistryState<H>, host: H): CellEntry<H> {
205
223
  state.serial += 1;
206
224
 
207
- return { key: `gtkx-cell-${String(state.serial)}`, host, identity: createItemIdentityStore() };
225
+ return { key: `gtkx-cell-${String(state.serial)}`, host, identity: createRef<ItemIdentity>() };
208
226
  }
209
227
 
210
228
  function setupHost<H extends FactoryHost>(state: CellRegistryState<H>, host: H): void {
@@ -274,7 +292,15 @@ function createCellRegistry<H extends FactoryHost>(options: CellRegistryOptions<
274
292
  return {
275
293
  handlers: createHandlers(state),
276
294
  setSize: (next) => {
295
+ if (state.size.width === next.width && state.size.height === next.height) {
296
+ return;
297
+ }
298
+
277
299
  state.size = next;
300
+
301
+ for (const host of state.entries.keys()) {
302
+ state.prepare?.(host, next);
303
+ }
278
304
  },
279
305
  subscribe: (onChange) => subscribeRegistry(state, onChange),
280
306
  getEntries: () => getRegistryEntries(state),
@@ -324,15 +350,25 @@ function itemArgs(
324
350
  return args;
325
351
  }
326
352
 
353
+ function keyForItem(ref: SlotRef, item: ListItem): string {
354
+ return joinParts([slotPathAt(ref.store, ref.slot), item.id]);
355
+ }
356
+
327
357
  function slotFor(options: ItemSlotOptions): ItemSlot | null {
328
358
  const ref = slotRefFor(options.item);
329
- const item = ref === null ? undefined : options.collection.itemAt(ref);
359
+
360
+ if (ref === null) {
361
+ return null;
362
+ }
363
+
364
+ const item = options.collection.itemAt(ref);
330
365
 
331
366
  if (item === undefined) {
332
367
  return null;
333
368
  }
334
369
 
335
370
  return {
371
+ itemKey: keyForItem(ref, item),
336
372
  item,
337
373
  row: options.row,
338
374
  args: itemArgs(item, options.position, options.row, isRowWanted(options, item)),
@@ -341,7 +377,7 @@ function slotFor(options: ItemSlotOptions): ItemSlot | null {
341
377
 
342
378
  function useItemSlot(
343
379
  entry: CellEntry<PositionedHost>,
344
- state: CollectionStateStore,
380
+ state: RefObject<CollectionState>,
345
381
  identity: ItemIdentity,
346
382
  ): ItemSlot | null {
347
383
  const position = useProperty(entry.host, "position");
@@ -352,13 +388,14 @@ function useItemSlot(
352
388
  return identity.item === undefined
353
389
  ? null
354
390
  : {
355
- item: identity.item,
356
- row,
357
- args: itemArgs(identity.item, position, row, identity.isExpanded),
358
- };
391
+ itemKey: identity.itemKey,
392
+ item: identity.item,
393
+ row,
394
+ args: itemArgs(identity.item, position, row, identity.isExpanded),
395
+ };
359
396
  }
360
397
 
361
- const { collection, expandedIds } = state.read();
398
+ const { collection, expandedIds } = state.current;
362
399
 
363
400
  return slotFor({ item, position, row, collection, expandedIds });
364
401
  }
@@ -372,22 +409,24 @@ function itemIdentity(
372
409
  const item = ref === null ? undefined : collection.itemAt(ref);
373
410
 
374
411
  return {
412
+ itemKey: ref === null || item === undefined ? "" : keyForItem(ref, item),
375
413
  item,
376
414
  position: entry.host.getPosition(),
377
- isExpanded: item === undefined ? false : (expandedIds?.includes(item.id) ?? false),
415
+ isExpanded: item !== undefined && (expandedIds?.includes(item.id) ?? false),
378
416
  };
379
417
  }
380
418
 
381
419
  function isSameItemIdentity(previous: ItemIdentity, next: ItemIdentity): boolean {
382
420
  return (
383
421
  previous.item === next.item &&
422
+ previous.itemKey === next.itemKey &&
384
423
  previous.position === next.position &&
385
424
  previous.isExpanded === next.isExpanded
386
425
  );
387
426
  }
388
427
 
389
428
  function isSameItemCell(previous: ItemCellProps, next: ItemCellProps): boolean {
390
- const identity = previous.entry.identity.read() ?? previous;
429
+ const identity = previous.entry.identity.current ?? previous;
391
430
 
392
431
  return (
393
432
  previous.entry === next.entry &&
@@ -399,24 +438,35 @@ function isSameItemCell(previous: ItemCellProps, next: ItemCellProps): boolean {
399
438
  }
400
439
 
401
440
  function isSameItemRow(previous: ItemRowProps, next: ItemRowProps): boolean {
402
- const identity = previous.entry.identity.read() ?? previous;
441
+ const identity = previous.entry.identity.current ?? previous;
403
442
 
404
443
  return previous.entry === next.entry && previous.rowProps === next.rowProps && isSameItemIdentity(identity, next);
405
444
  }
406
445
 
407
- function createItemIdentityStore(): ItemIdentityStore {
408
- let current: ItemIdentity | null = null;
446
+ function createNativeItemKeyStore(): NativeItemKeyStore {
447
+ const keys: WeakMap<GObject.Object, string> = new WeakMap();
448
+ let serial = 0;
409
449
 
410
450
  return {
411
- read: () => current,
412
- write: (identity) => {
413
- current = identity;
451
+ keyFor: (item) => {
452
+ const current = keys.get(item);
453
+
454
+ if (current !== undefined) {
455
+ return current;
456
+ }
457
+
458
+ serial += 1;
459
+ const key = `gtkx-native-item-${String(serial)}`;
460
+ keys.set(item, key);
461
+
462
+ return key;
414
463
  },
415
464
  };
416
465
  }
417
466
 
418
467
  function renderedIdentity(entry: CellEntry<PositionedHost>, slot: ItemSlot | null): ItemIdentity {
419
468
  return {
469
+ itemKey: slot?.itemKey ?? "",
420
470
  item: slot?.item,
421
471
  position: slot?.args.index ?? entry.host.getPosition(),
422
472
  isExpanded: slot?.args.isExpanded ?? false,
@@ -424,35 +474,12 @@ function renderedIdentity(entry: CellEntry<PositionedHost>, slot: ItemSlot | nul
424
474
  }
425
475
 
426
476
  function useRememberIdentity(entry: CellEntry<PositionedHost>, slot: ItemSlot | null): void {
477
+ const identityRef = entry.identity;
427
478
  useLayoutEffect(() => {
428
- entry.identity.write(renderedIdentity(entry, slot));
479
+ identityRef.current = renderedIdentity(entry, slot);
429
480
  });
430
481
  }
431
482
 
432
- function createCollectionStateStore(initial: CollectionState): CollectionStateStore {
433
- let current = initial;
434
-
435
- return {
436
- read: () => current,
437
- write: (state) => {
438
- current = state;
439
- },
440
- };
441
- }
442
-
443
- function useCollectionState(
444
- collection: Collection,
445
- expandedIds: string[] | null | undefined,
446
- ): CollectionStateStore {
447
- const [state] = useState(() => createCollectionStateStore({ collection, expandedIds }));
448
-
449
- useInsertionEffect(() => {
450
- state.write({ collection, expandedIds });
451
- }, [state, collection, expandedIds]);
452
-
453
- return state;
454
- }
455
-
456
483
  function expanderDescriptionFor(
457
484
  slot: ItemSlot,
458
485
  descriptions: ExpanderDescriptions | null | undefined,
@@ -474,21 +501,19 @@ function wrapExpander(
474
501
  const { item, row } = slot;
475
502
 
476
503
  const expander: ReactNode =
477
- row === null
478
- ? (
479
- content
480
- )
481
- : (
482
- <GtkTreeExpander
483
- listRow={row}
484
- hideExpander={item.shouldHideExpander ?? false}
485
- indentForDepth={item.shouldIndentForDepth ?? true}
486
- indentForIcon={item.shouldIndentForIcon ?? true}
487
- accessibleDescription={expanderDescriptionFor(slot, descriptions)}
488
- >
489
- {content}
490
- </GtkTreeExpander>
491
- );
504
+ row === null ? (
505
+ content
506
+ ) : (
507
+ <GtkTreeExpander
508
+ listRow={row}
509
+ hideExpander={item.shouldHideExpander ?? false}
510
+ indentForDepth={item.shouldIndentForDepth ?? true}
511
+ indentForIcon={item.shouldIndentForIcon ?? true}
512
+ accessibleDescription={expanderDescriptionFor(slot, descriptions)}
513
+ >
514
+ {content}
515
+ </GtkTreeExpander>
516
+ );
492
517
 
493
518
  return expander;
494
519
  }
@@ -506,14 +531,13 @@ function itemBody(
506
531
  const renderItem = render as ListItemRenderer<unknown>;
507
532
  const content = renderItem(slot.args);
508
533
 
509
- return hasExpander ? wrapExpander(slot, content, descriptions) : content;
534
+ return <Fragment key={slot.itemKey}>{hasExpander ? wrapExpander(slot, content, descriptions) : content}</Fragment>;
510
535
  }
511
536
 
512
537
  function rowText(value: string | undefined): string | null {
513
538
  return value ?? null;
514
539
  }
515
540
 
516
- /* eslint-disable-next-line unicorn/consistent-boolean-name -- reads the flag off a row, paired with rowText */
517
541
  function rowFlag(value: boolean | undefined): boolean {
518
542
  return value ?? true;
519
543
  }
@@ -531,21 +555,13 @@ function rowPropsFor(slot: ItemSlot | null, rowProps: ListRowPropsResolver<never
531
555
  };
532
556
  }
533
557
 
534
- function setRowText(host: Gtk.ColumnViewRow, name: string, value: string | null): void {
535
- setProperty(host, name, ROW_TEXT_DESCRIPTOR, value);
536
- }
537
-
538
- function applyRowProps(
539
- host: Gtk.ColumnViewRow,
540
- props: ResolvedRowProps,
541
- previous: ResolvedRowProps | null,
542
- ): void {
558
+ function applyRowProps(host: Gtk.ColumnViewRow, props: ResolvedRowProps, previous: ResolvedRowProps | null): void {
543
559
  if (previous?.accessibleLabel !== props.accessibleLabel) {
544
- setRowText(host, "accessible-label", props.accessibleLabel);
560
+ host.accessibleLabel = props.accessibleLabel;
545
561
  }
546
562
 
547
563
  if (previous?.accessibleDescription !== props.accessibleDescription) {
548
- setRowText(host, "accessible-description", props.accessibleDescription);
564
+ host.accessibleDescription = props.accessibleDescription;
549
565
  }
550
566
 
551
567
  if (previous?.isActivatable !== props.isActivatable) {
@@ -592,13 +608,46 @@ function ItemRowImpl({ entry, rowProps, state, ...identity }: ItemRowProps): Rea
592
608
 
593
609
  function HeaderCellImpl({ entry, render, collection }: HeaderCellProps): ReactNode {
594
610
  const item = useProperty(entry.host, "item") ?? null;
595
- const ref = slotRefFor(item);
596
- const renderHeader = render as ListSectionRenderer<unknown>;
597
- const body = ref === null ? null : renderHeader({ section: collection.sectionFor(ref.store.level.path) });
611
+
612
+ return createPortal(headerBody(slotRefFor(item), render, collection), entry.host, entry.key);
613
+ }
614
+
615
+ function NativeItemCell<T extends GObject.Object>({ entry, render, keys, size }: NativeItemCellProps<T>): ReactNode {
616
+ const position = useProperty(entry.host, "position");
617
+ const item = useProperty(entry.host, "item") ?? null;
618
+ let body: ReactNode = null;
619
+
620
+ if (item !== null && position !== undefined) {
621
+ const content = render({ item: item as T, index: position });
622
+ const child =
623
+ content == null || typeof content === "boolean" ? (
624
+ <GtkBox widthRequest={size.width} heightRequest={size.height} />
625
+ ) : (
626
+ content
627
+ );
628
+ body = <Fragment key={keys.keyFor(item)}>{child}</Fragment>;
629
+ }
598
630
 
599
631
  return createPortal(body, entry.host, entry.key);
600
632
  }
601
633
 
634
+ function headerBody(ref: SlotRef | null, render: ListSectionRenderer<never>, collection: Collection): ReactNode {
635
+ if (ref === null) {
636
+ return null;
637
+ }
638
+
639
+ const section = collection.sectionFor(ref.store.level.path);
640
+
641
+ if (section === undefined) {
642
+ return null;
643
+ }
644
+
645
+ const renderHeader = render as ListSectionRenderer<unknown>;
646
+ const key = joinParts([ref.store.level.path, section.id]);
647
+
648
+ return <Fragment key={key}>{renderHeader({ section: section.value })}</Fragment>;
649
+ }
650
+
602
651
  function usePortalEntries<H extends FactoryHost>(registry: CellRegistry<H>): CellEntry<H>[] {
603
652
  return useSyncExternalStore(registry.subscribe, registry.getEntries);
604
653
  }
@@ -612,7 +661,7 @@ const ItemPortals = ({
612
661
  expanderDescriptions,
613
662
  }: ItemPortalsProps): ReactNode => {
614
663
  const entries = usePortalEntries(registry);
615
- const state = useCollectionState(collection, expandedIds);
664
+ const state = useLatestRef({ collection, expandedIds });
616
665
 
617
666
  return entries.map((entry) => (
618
667
  <ItemCell
@@ -629,7 +678,7 @@ const ItemPortals = ({
629
678
 
630
679
  const RowPortals = ({ registry, rowProps, collection, expandedIds }: RowPortalsProps): ReactNode => {
631
680
  const entries = usePortalEntries(registry);
632
- const state = useCollectionState(collection, expandedIds);
681
+ const state = useLatestRef({ collection, expandedIds });
633
682
 
634
683
  return entries.map((entry) => (
635
684
  <ItemRow
@@ -647,6 +696,15 @@ const HeaderPortals = ({ registry, render, collection }: HeaderPortalsProps): Re
647
696
  <HeaderCell key={entry.key} entry={entry} render={render} collection={collection} />
648
697
  ));
649
698
 
699
+ function NativeItemPortals<T extends GObject.Object>({ registry, render, size }: NativeItemPortalsProps<T>): ReactNode {
700
+ const entries = usePortalEntries(registry);
701
+ const [keys] = useState(createNativeItemKeyStore);
702
+
703
+ return entries.map((entry) => (
704
+ <NativeItemCell key={entry.key} entry={entry} render={render} keys={keys} size={size} />
705
+ ));
706
+ }
707
+
650
708
  const useSectionHeader = (
651
709
  render: ListSectionRenderer<never> | null | undefined,
652
710
  collection: Collection,
@@ -683,10 +741,4 @@ const useRowProps = (
683
741
  };
684
742
  };
685
743
 
686
- export {
687
- useItemCells,
688
- useRowProps,
689
- useSectionHeader,
690
- ItemPortals,
691
- type CellSize,
692
- };
744
+ export { useItemCells, useRowProps, useSectionHeader, ItemPortals, NativeItemPortals, type CellSize };
@@ -7,20 +7,22 @@ type Level = {
7
7
  expandableFlags: boolean[];
8
8
  };
9
9
 
10
+ type SectionIdentity = Pick<ListSection, "id" | "value">;
11
+
10
12
  type CollectionIndex = {
11
13
  isTree: boolean;
12
14
  groups: Level[];
13
15
  childLevel: (level: Level, slot: number) => Level | undefined;
14
16
  levelFor: (levelPath: string) => Level | undefined;
15
17
  itemAt: (levelPath: string, slot: number) => ListItem | undefined;
16
- sectionFor: (levelPath: string) => unknown;
18
+ sectionFor: (levelPath: string) => SectionIdentity | undefined;
17
19
  };
18
20
 
19
21
  type IndexState = {
20
22
  isTree: boolean;
21
23
  groups: Level[];
22
24
  levels: Map<string, Level>;
23
- sectionValues: Map<string, unknown>;
25
+ sections: Map<string, SectionIdentity>;
24
26
  };
25
27
 
26
28
  type ParentItem = ListItem & { children: ListItem[] };
@@ -96,7 +98,7 @@ function buildGroups(state: IndexState, source: ListItem[], sections: ListSectio
96
98
 
97
99
  return sections.map((section, group) => {
98
100
  const path = encodePart(String(group));
99
- state.sectionValues.set(path, section.value);
101
+ state.sections.set(path, { id: section.id, value: section.value });
100
102
 
101
103
  return newLevel(state, path, section.data);
102
104
  });
@@ -125,7 +127,7 @@ function createCollectionIndex(
125
127
  isTree: isTreeSource(source, sections, isFlat),
126
128
  groups: [],
127
129
  levels: new Map(),
128
- sectionValues: new Map(),
130
+ sections: new Map(),
129
131
  };
130
132
 
131
133
  state.groups = buildGroups(state, source, sections);
@@ -136,8 +138,8 @@ function createCollectionIndex(
136
138
  childLevel: (level, slot) => childLevel(state, level, slot),
137
139
  levelFor: (levelPath) => levelFor(state, levelPath),
138
140
  itemAt: (levelPath, slot) => levelFor(state, levelPath)?.items[slot],
139
- sectionFor: (levelPath) => state.sectionValues.get(levelPath),
141
+ sectionFor: (levelPath) => state.sections.get(levelPath),
140
142
  };
141
143
  }
142
144
 
143
- export { createCollectionIndex, type CollectionIndex, type Level };
145
+ export { createCollectionIndex, type CollectionIndex, type Level, type SectionIdentity };