@gtkx/components 1.0.0-rc.4 → 1.0.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.
Files changed (86) hide show
  1. package/README.md +5 -5
  2. package/dist/column-view.d.ts +2 -2
  3. package/dist/column-view.d.ts.map +1 -1
  4. package/dist/column-view.js +14 -12
  5. package/dist/column-view.js.map +1 -1
  6. package/dist/grid-view.js +1 -1
  7. package/dist/grid-view.js.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/internal/cells.d.ts +31 -23
  12. package/dist/internal/cells.d.ts.map +1 -1
  13. package/dist/internal/cells.js +168 -77
  14. package/dist/internal/cells.js.map +1 -1
  15. package/dist/internal/collection-index.d.ts +6 -10
  16. package/dist/internal/collection-index.d.ts.map +1 -1
  17. package/dist/internal/collection-index.js +32 -69
  18. package/dist/internal/collection-index.js.map +1 -1
  19. package/dist/internal/collection-model.d.ts +23 -2
  20. package/dist/internal/collection-model.d.ts.map +1 -1
  21. package/dist/internal/collection-model.js +210 -95
  22. package/dist/internal/collection-model.js.map +1 -1
  23. package/dist/internal/collection.d.ts +8 -11
  24. package/dist/internal/collection.d.ts.map +1 -1
  25. package/dist/internal/collection.js +38 -49
  26. package/dist/internal/collection.js.map +1 -1
  27. package/dist/internal/controlled-sync.d.ts +11 -0
  28. package/dist/internal/controlled-sync.d.ts.map +1 -0
  29. package/dist/internal/controlled-sync.js +19 -0
  30. package/dist/internal/controlled-sync.js.map +1 -0
  31. package/dist/internal/drop-down.d.ts.map +1 -1
  32. package/dist/internal/drop-down.js +27 -15
  33. package/dist/internal/drop-down.js.map +1 -1
  34. package/dist/internal/expansion.d.ts +3 -2
  35. package/dist/internal/expansion.d.ts.map +1 -1
  36. package/dist/internal/expansion.js +80 -47
  37. package/dist/internal/expansion.js.map +1 -1
  38. package/dist/internal/keys.d.ts +5 -0
  39. package/dist/internal/keys.d.ts.map +1 -0
  40. package/dist/internal/keys.js +13 -0
  41. package/dist/internal/keys.js.map +1 -0
  42. package/dist/internal/selection.d.ts +3 -2
  43. package/dist/internal/selection.d.ts.map +1 -1
  44. package/dist/internal/selection.js +68 -35
  45. package/dist/internal/selection.js.map +1 -1
  46. package/dist/internal/slots.d.ts +10 -0
  47. package/dist/internal/slots.d.ts.map +1 -0
  48. package/dist/internal/slots.js +43 -0
  49. package/dist/internal/slots.js.map +1 -0
  50. package/dist/internal/tree-expansion.d.ts +20 -0
  51. package/dist/internal/tree-expansion.d.ts.map +1 -0
  52. package/dist/internal/tree-expansion.js +68 -0
  53. package/dist/internal/tree-expansion.js.map +1 -0
  54. package/dist/internal/tree-order.d.ts +27 -0
  55. package/dist/internal/tree-order.d.ts.map +1 -0
  56. package/dist/internal/tree-order.js +71 -0
  57. package/dist/internal/tree-order.js.map +1 -0
  58. package/dist/internal/use-collection.d.ts +1 -1
  59. package/dist/internal/use-collection.d.ts.map +1 -1
  60. package/dist/internal/use-collection.js +4 -4
  61. package/dist/internal/use-collection.js.map +1 -1
  62. package/dist/list-view.d.ts.map +1 -1
  63. package/dist/list-view.js +6 -6
  64. package/dist/list-view.js.map +1 -1
  65. package/dist/types.d.ts +70 -21
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/types.js.map +1 -1
  68. package/package.json +7 -7
  69. package/src/column-view.tsx +28 -25
  70. package/src/grid-view.tsx +1 -1
  71. package/src/index.ts +3 -0
  72. package/src/internal/cells.tsx +339 -117
  73. package/src/internal/collection-index.ts +44 -92
  74. package/src/internal/collection-model.ts +259 -107
  75. package/src/internal/collection.ts +45 -67
  76. package/src/internal/controlled-sync.ts +34 -0
  77. package/src/internal/drop-down.tsx +41 -20
  78. package/src/internal/expansion.ts +104 -67
  79. package/src/internal/keys.ts +18 -0
  80. package/src/internal/selection.tsx +100 -51
  81. package/src/internal/slots.ts +67 -0
  82. package/src/internal/tree-expansion.ts +110 -0
  83. package/src/internal/tree-order.ts +138 -0
  84. package/src/internal/use-collection.tsx +5 -5
  85. package/src/list-view.tsx +14 -10
  86. package/src/types.ts +76 -19
@@ -0,0 +1,67 @@
1
+ import { decodePartAt, encodePart } from "./keys.js";
2
+
3
+ type SlotKey = {
4
+ levelPath: string;
5
+ slot: number;
6
+ };
7
+
8
+ type SlotMap = Map<string, Set<number>>;
9
+
10
+ function getLevelBoundary(path: string): number {
11
+ let offset = 0;
12
+ let boundary = 0;
13
+
14
+ while (offset < path.length) {
15
+ const part = decodePartAt(path, offset);
16
+
17
+ if (part === null) {
18
+ return boundary;
19
+ }
20
+
21
+ boundary = offset;
22
+ offset += encodePart(part).length;
23
+ }
24
+
25
+ return boundary;
26
+ }
27
+
28
+ function getSlotKey(path: string): SlotKey | null {
29
+ const boundary = getLevelBoundary(path);
30
+ const part = decodePartAt(path, boundary);
31
+
32
+ if (part === null) {
33
+ return null;
34
+ }
35
+
36
+ return { levelPath: path.slice(0, boundary), slot: Number(part) };
37
+ }
38
+
39
+ function trackPath(slots: SlotMap, path: string): void {
40
+ const key = getSlotKey(path);
41
+
42
+ if (key === null) {
43
+ return;
44
+ }
45
+
46
+ const level = slots.get(key.levelPath);
47
+
48
+ if (level === undefined) {
49
+ slots.set(key.levelPath, new Set([key.slot]));
50
+
51
+ return;
52
+ }
53
+
54
+ level.add(key.slot);
55
+ }
56
+
57
+ function trackPaths(paths: Iterable<string>): SlotMap {
58
+ const slots: SlotMap = new Map();
59
+
60
+ for (const path of paths) {
61
+ trackPath(slots, path);
62
+ }
63
+
64
+ return slots;
65
+ }
66
+
67
+ export { getSlotKey, trackPath, trackPaths, type SlotMap };
@@ -0,0 +1,110 @@
1
+ import type { CollectionIndex } from "./collection-index.js";
2
+ import type { SlotMap } from "./slots.js";
3
+ import type { VisibleOrder } from "./tree-order.js";
4
+ import { encodePart } from "./keys.js";
5
+ import { getSlotKey, trackPath, trackPaths } from "./slots.js";
6
+ import { buildVisibleOrder } from "./tree-order.js";
7
+
8
+ type TreeExpansion = {
9
+ index: CollectionIndex;
10
+ expanded: Set<string>;
11
+ slots: SlotMap;
12
+ order: VisibleOrder | null;
13
+ isApplying: boolean;
14
+ isSyncing: boolean;
15
+ };
16
+
17
+ function dropSubtree(expansion: TreeExpansion, path: string): void {
18
+ expansion.expanded.delete(path);
19
+ const level = expansion.slots.get(path);
20
+
21
+ if (level === undefined) {
22
+ return;
23
+ }
24
+
25
+ expansion.slots.delete(path);
26
+
27
+ for (const slot of level) {
28
+ dropSubtree(expansion, path + encodePart(String(slot)));
29
+ }
30
+ }
31
+
32
+ function createTreeExpansion(index: CollectionIndex): TreeExpansion {
33
+ return { index, expanded: new Set(), slots: new Map(), order: null, isApplying: false, isSyncing: false };
34
+ }
35
+
36
+ function orderFor(expansion: TreeExpansion): VisibleOrder {
37
+ expansion.order ??= buildVisibleOrder(expansion.index, expansion.slots);
38
+
39
+ return expansion.order;
40
+ }
41
+
42
+ function adoptOrder(expansion: TreeExpansion, order: VisibleOrder): void {
43
+ expansion.expanded = new Set(order.expandedPaths);
44
+ expansion.slots = trackPaths(order.expandedPaths);
45
+ expansion.order = order;
46
+ }
47
+
48
+ function prunePath(expansion: TreeExpansion, path: string): void {
49
+ const key = getSlotKey(path);
50
+
51
+ if (key !== null) {
52
+ expansion.slots.get(key.levelPath)?.delete(key.slot);
53
+ }
54
+
55
+ dropSubtree(expansion, path);
56
+ expansion.order = null;
57
+ }
58
+
59
+ function pruneSlots(expansion: TreeExpansion, levelPath: string, isPruned: (slot: number) => boolean): void {
60
+ const level = expansion.slots.get(levelPath);
61
+
62
+ if (level === undefined) {
63
+ return;
64
+ }
65
+
66
+ for (const slot of level) {
67
+ if (!isPruned(slot)) {
68
+ continue;
69
+ }
70
+
71
+ level.delete(slot);
72
+ dropSubtree(expansion, levelPath + encodePart(String(slot)));
73
+ }
74
+
75
+ expansion.order = null;
76
+ }
77
+
78
+ function markExpanded(expansion: TreeExpansion, path: string, isExpanded: boolean): void {
79
+ if (isExpanded) {
80
+ expansion.expanded.add(path);
81
+ trackPath(expansion.slots, path);
82
+ expansion.order = null;
83
+
84
+ return;
85
+ }
86
+
87
+ prunePath(expansion, path);
88
+ }
89
+
90
+ function resetExpansion(expansion: TreeExpansion): void {
91
+ expansion.expanded = new Set();
92
+ expansion.slots = new Map();
93
+ expansion.order = null;
94
+ }
95
+
96
+ function adoptIndex(expansion: TreeExpansion, index: CollectionIndex): void {
97
+ expansion.index = index;
98
+ expansion.order = null;
99
+ }
100
+
101
+ export {
102
+ adoptIndex,
103
+ adoptOrder,
104
+ createTreeExpansion,
105
+ markExpanded,
106
+ orderFor,
107
+ pruneSlots,
108
+ resetExpansion,
109
+ type TreeExpansion,
110
+ };
@@ -0,0 +1,138 @@
1
+ import type { ListItem } from "../types.js";
2
+ import type { CollectionIndex, Level } from "./collection-index.js";
3
+ import type { SlotMap } from "./slots.js";
4
+ import { encodePart } from "./keys.js";
5
+
6
+ type VisibleRow = {
7
+ level: Level;
8
+ slot: number;
9
+ item: ListItem;
10
+ position: number;
11
+ isOpen: boolean;
12
+ isMarked: boolean;
13
+ };
14
+
15
+ type VisibleOrder = {
16
+ expandedPaths: string[];
17
+ expandedIds: string[];
18
+ };
19
+
20
+ type RowVisitor = (row: VisibleRow) => void;
21
+
22
+ type WalkOptions = {
23
+ index: CollectionIndex;
24
+ slots: SlotMap;
25
+ marks?: SlotMap | undefined;
26
+ visit?: RowVisitor | undefined;
27
+ };
28
+
29
+ type WalkFrame = {
30
+ level: Level;
31
+ cursor: number;
32
+ open: Set<number> | undefined;
33
+ marked: Set<number> | undefined;
34
+ };
35
+
36
+ type WalkState = {
37
+ options: WalkOptions;
38
+ stack: WalkFrame[];
39
+ row: VisibleRow;
40
+ order: VisibleOrder;
41
+ };
42
+
43
+ const NO_LEVEL: Level = { path: "", items: [], expandableFlags: [] };
44
+ const NO_ITEM: ListItem = { id: "", value: undefined };
45
+
46
+ const getRowPath = (row: VisibleRow): string => row.level.path + encodePart(String(row.slot));
47
+
48
+ function frameFor(options: WalkOptions, level: Level): WalkFrame {
49
+ return { level, cursor: 0, open: options.slots.get(level.path), marked: options.marks?.get(level.path) };
50
+ }
51
+
52
+ function descend(state: WalkState, path: string): void {
53
+ const child = state.options.index.children.get(path);
54
+
55
+ if (child === undefined || child.items.length === 0) {
56
+ return;
57
+ }
58
+
59
+ state.order.expandedPaths.push(path);
60
+ state.order.expandedIds.push(state.row.item.id);
61
+ state.stack.push(frameFor(state.options, child));
62
+ }
63
+
64
+ function visitSlot(state: WalkState, frame: WalkFrame, slot: number): void {
65
+ const item = frame.level.items[slot];
66
+
67
+ if (item === undefined) {
68
+ return;
69
+ }
70
+
71
+ const { row } = state;
72
+ row.level = frame.level;
73
+ row.slot = slot;
74
+ row.item = item;
75
+ row.isOpen = frame.open?.has(slot) ?? false;
76
+ row.isMarked = frame.marked?.has(slot) ?? false;
77
+ state.options.visit?.(row);
78
+ row.position += 1;
79
+
80
+ if (row.isOpen) {
81
+ descend(state, getRowPath(row));
82
+ }
83
+ }
84
+
85
+ function advanceFrame(state: WalkState): void {
86
+ const frame = state.stack.at(-1);
87
+
88
+ if (frame === undefined || frame.cursor >= frame.level.items.length) {
89
+ state.stack.pop();
90
+
91
+ return;
92
+ }
93
+
94
+ const slot = frame.cursor;
95
+ frame.cursor += 1;
96
+ visitSlot(state, frame, slot);
97
+ }
98
+
99
+ function walkVisible(options: WalkOptions): VisibleOrder {
100
+ const order: VisibleOrder = { expandedPaths: [], expandedIds: [] };
101
+ const row: VisibleRow = { level: NO_LEVEL, slot: 0, item: NO_ITEM, position: 0, isOpen: false, isMarked: false };
102
+ const frames = options.index.groups.map((level) => frameFor(options, level));
103
+ const state: WalkState = { options, stack: frames.toReversed(), row, order };
104
+
105
+ while (state.stack.length > 0) {
106
+ advanceFrame(state);
107
+ }
108
+
109
+ return order;
110
+ }
111
+
112
+ function buildVisibleOrder(index: CollectionIndex, slots: SlotMap): VisibleOrder {
113
+ return walkVisible({ index, slots });
114
+ }
115
+
116
+ function findPositions(index: CollectionIndex, slots: SlotMap, ids: Set<string>): number[] {
117
+ const found: number[] = [];
118
+
119
+ walkVisible({
120
+ index,
121
+ slots,
122
+ visit: (row) => {
123
+ if (ids.has(row.item.id)) {
124
+ found.push(row.position);
125
+ }
126
+ },
127
+ });
128
+
129
+ return found;
130
+ }
131
+
132
+ export {
133
+ buildVisibleOrder,
134
+ findPositions,
135
+ walkVisible,
136
+ type VisibleOrder,
137
+ type WalkOptions,
138
+ };
@@ -23,13 +23,13 @@ type CollectionResult = {
23
23
 
24
24
  function useCollectionData(options: CollectionDataOptions): Collection {
25
25
  const { items, sections, isFlat } = options;
26
- const [gtk] = useState(createCollectionModel);
26
+ const [collectionModel] = useState(createCollectionModel);
27
27
  const index = useMemo(() => createCollectionIndex(items, sections, isFlat === true), [items, sections, isFlat]);
28
- const collection = useMemo(() => createCollection(gtk, index), [gtk, index]);
28
+ const collection = useMemo(() => createCollection(collectionModel, index), [collectionModel, index]);
29
29
 
30
30
  useLayoutEffect(() => {
31
- gtk.sync(index);
32
- }, [gtk, index]);
31
+ collectionModel.sync(index);
32
+ }, [collectionModel, index]);
33
33
 
34
34
  return collection;
35
35
  }
@@ -54,4 +54,4 @@ function useCollection(options: CollectionOptions): CollectionResult {
54
54
  return { collection, selection };
55
55
  }
56
56
 
57
- export { useCollection, useCollectionData, type CollectionDataOptions, type CollectionOptions };
57
+ export { useCollection, useCollectionData };
package/src/list-view.tsx CHANGED
@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
2
2
  import { GtkListView, GtkSignalListItemFactory } from "@gtkx/jsx/gtk";
3
3
  import { omit } from "@gtkx/utils";
4
4
  import type { ListViewProps } from "./types.js";
5
- import { HeaderPortals, ItemPortals, useHeaderCells, useItemCells } from "./internal/cells.js";
5
+ import { ItemPortals, useItemCells, useSectionHeader } from "./internal/cells.js";
6
6
  import { useCollection } from "./internal/use-collection.js";
7
7
 
8
8
  const LIST_VIEW_PROPS = [
@@ -15,6 +15,7 @@ const LIST_VIEW_PROPS = [
15
15
  "selectionMode",
16
16
  "expandedIds",
17
17
  "onExpandedChange",
18
+ "expanderDescriptions",
18
19
  "estimatedItemHeight",
19
20
  "estimatedItemWidth",
20
21
  ] as const satisfies (keyof ListViewProps)[];
@@ -24,27 +25,30 @@ const LIST_VIEW_PROPS = [
24
25
  * controlled selection, controlled tree expansion, and estimated item sizing.
25
26
  */
26
27
  function ListView<T = unknown, S = unknown>(props: ListViewProps<T, S>): ReactNode {
27
- const { renderItem, renderHeader, expandedIds, estimatedItemHeight, estimatedItemWidth } = props;
28
+ const { renderItem, renderHeader, expandedIds, expanderDescriptions } = props;
29
+ const { estimatedItemHeight, estimatedItemWidth } = props;
28
30
  const rest = omit(props, LIST_VIEW_PROPS);
29
31
  const size = { width: estimatedItemWidth ?? -1, height: estimatedItemHeight ?? -1 };
30
32
  const { collection, selection } = useCollection(props);
31
33
  const itemCells = useItemCells(size);
32
- const headerCells = useHeaderCells(size);
34
+ const header = useSectionHeader(renderHeader, collection, size);
33
35
 
34
36
  return (
35
37
  <>
36
38
  <GtkListView
37
39
  model={selection}
38
40
  factory={<GtkSignalListItemFactory {...itemCells.handlers} />}
39
- {...(renderHeader != null && {
40
- headerFactory: <GtkSignalListItemFactory {...headerCells.handlers} />,
41
- })}
41
+ {...header.factoryProps}
42
42
  {...rest}
43
43
  />
44
- <ItemPortals store={itemCells} render={renderItem} collection={collection} expandedIds={expandedIds} />
45
- {renderHeader != null && (
46
- <HeaderPortals store={headerCells} render={renderHeader} collection={collection} />
47
- )}
44
+ <ItemPortals
45
+ registry={itemCells}
46
+ render={renderItem}
47
+ collection={collection}
48
+ expandedIds={expandedIds}
49
+ expanderDescriptions={expanderDescriptions}
50
+ />
51
+ {header.portals}
48
52
  </>
49
53
  );
50
54
  }
package/src/types.ts CHANGED
@@ -13,18 +13,18 @@ import type { ReactNode } from "react";
13
13
  * arbitrary value. Nested items form a tree.
14
14
  */
15
15
  type ListItem<T = unknown> = {
16
- /** Stable identifier used to track the item across updates and selection. */
16
+ /** Stable identifier used to track the item across updates and selection, naming every row that carries it. */
17
17
  id: string;
18
18
  /** Payload handed to the cell renderer as `ListItemRenderArgs.item`. */
19
19
  value: T;
20
20
  /** Child items nested under this one, which turn a plain item list into a tree. */
21
21
  children?: ListItem<T>[] | undefined;
22
- /** Hides the tree expander arrow even when the item has children. */
23
- hideExpander?: boolean | undefined;
24
- /** Adds indentation matching the item's depth in the tree. */
25
- indentForDepth?: boolean | undefined;
26
- /** Reserves indentation space for an expander icon. */
27
- indentForIcon?: boolean | undefined;
22
+ /** Hides the tree expander arrow even when the item has children, through `hide-expander`. */
23
+ shouldHideExpander?: boolean | undefined;
24
+ /** Adds indentation matching the item's depth in the tree, through `indent-for-depth`. */
25
+ shouldIndentForDepth?: boolean | undefined;
26
+ /** Reserves indentation space for an expander icon, through `indent-for-icon`. */
27
+ shouldIndentForIcon?: boolean | undefined;
28
28
  };
29
29
 
30
30
  /** A group of items rendered under a shared section header. */
@@ -49,6 +49,24 @@ type ListItemRenderArgs<T> = {
49
49
  isExpanded?: boolean | undefined;
50
50
  };
51
51
 
52
+ /**
53
+ * Properties applied to the row that carries one item's cells. A Gtk.ColumnViewRow is not a widget, so
54
+ * these are the only handles it offers; anything omitted falls back to the row's own default, which
55
+ * leaves the accessibility strings unset rather than setting them to an empty string.
56
+ */
57
+ type ListRowProps = {
58
+ /** Name a screen reader announces for the row, through `accessible-label`. */
59
+ accessibleLabel?: string | undefined;
60
+ /** Longer description a screen reader announces for the row, through `accessible-description`. */
61
+ accessibleDescription?: string | undefined;
62
+ /** Whether activating the row emits the view's `activate` signal; rows are activatable by default. */
63
+ isActivatable?: boolean | undefined;
64
+ /** Whether the row takes keyboard focus; rows are focusable by default. */
65
+ isFocusable?: boolean | undefined;
66
+ /** Whether clicking or keying the row tries to select it; rows are selectable by default. */
67
+ isSelectable?: boolean | undefined;
68
+ };
69
+
52
70
  /** Arguments passed to a {@link ListSectionRenderer} when rendering one section header. */
53
71
  type ListSectionRenderArgs<S> = {
54
72
  /** Value of the `ListSection` whose header is being rendered. */
@@ -59,6 +77,8 @@ type ListSectionRenderArgs<S> = {
59
77
  type ListItemRenderer<T> = (args: ListItemRenderArgs<T>) => ReactNode;
60
78
  /** Renders the contents of one section header of a collection view. */
61
79
  type ListSectionRenderer<S> = (args: ListSectionRenderArgs<S>) => ReactNode;
80
+ /** Returns the {@link ListRowProps} to apply to the row displaying one item, re-run whenever the item renders. */
81
+ type ListRowPropsResolver<T> = (args: ListItemRenderArgs<T>) => ListRowProps;
62
82
 
63
83
  /** Size a collection view requests for each cell before its contents render, keeping scroll estimates steady. */
64
84
  type ItemSizeProps = {
@@ -70,20 +90,56 @@ type ItemSizeProps = {
70
90
 
71
91
  /** Controlled selection shared by the multi-item collection views. */
72
92
  type SelectionProps = {
73
- /** Ids of the items to keep selected; omitting it leaves the view's own selection alone. */
93
+ /**
94
+ * Ids of the items to keep selected; omitting it keeps nothing selected, and `onSelectionChanged` is how a
95
+ * user's selection is adopted into it. An id repeated in several branches of a tree names every matching row,
96
+ * and a single-selection view takes the first of them.
97
+ */
74
98
  selectedIds?: string[] | null | undefined;
75
- /** Called with the ids of every selected item whenever the selection changes, and once on mount. */
99
+ /** Called with one id per selected row whenever the selection changes, and once on mount. */
76
100
  onSelectionChanged?: ((ids: string[]) => void) | null | undefined;
77
101
  /** How much the user may select, one item at a time unless `MULTIPLE` or `NONE` is given. */
78
102
  selectionMode?: Gtk.SelectionMode | null | undefined;
79
103
  };
80
104
 
105
+ /**
106
+ * Wording a screen reader announces for the control that expands and collapses a row. GTK names that control
107
+ * after the row's own content and reports whether it is expanded, but says nothing about what activating it
108
+ * does, so these strings supply that and belong in the application's own language.
109
+ */
110
+ type ExpanderDescriptions = {
111
+ /** Announced while the row is collapsed, such as "Expand". */
112
+ expand: string;
113
+ /** Announced while the row is expanded, such as "Collapse". */
114
+ collapse: string;
115
+ };
116
+
81
117
  /** Controlled expansion for the views that turn nested `ListItem.children` into a tree. */
82
118
  type ExpansionProps = {
83
- /** Ids of the items to keep expanded; omitting it leaves the rows' own expanded state alone. */
119
+ /**
120
+ * Ids of the items to keep expanded; omitting it keeps every row collapsed, and `onExpandedChange` is how a
121
+ * user's expansion is adopted into it. An id repeated in several branches names every matching row, so all of
122
+ * them expand together.
123
+ */
84
124
  expandedIds?: string[] | null | undefined;
85
- /** Called with the ids of every expanded row whenever expansion changes. */
125
+ /** Called with one id per expanded row, in visible order, whenever expansion changes. */
86
126
  onExpandedChange?: ((ids: string[]) => void) | null | undefined;
127
+ /**
128
+ * Wording announced for the expander of every row that can expand, describing what activating it does.
129
+ * Omitting it leaves those expanders described only by the row they carry, and rows that cannot expand or
130
+ * that hide their expander are never described.
131
+ */
132
+ expanderDescriptions?: ExpanderDescriptions | null | undefined;
133
+ };
134
+
135
+ /** Controlled sorting for the views whose headers can sort the collection. */
136
+ type SortProps = {
137
+ /** Id of the column the view is sorted by, making sorting controlled. */
138
+ sortColumn?: string | null | undefined;
139
+ /** Direction `sortColumn` is sorted in, defaulting to ascending. */
140
+ sortOrder?: Gtk.SortType | null | undefined;
141
+ /** Called when the user sorts from a header, with the primary column's id, or null, and its order. */
142
+ onSortChanged?: ((column: string | null, order: Gtk.SortType) => void) | null | undefined;
87
143
  };
88
144
 
89
145
  /** The data a collection view renders, either as a plain item list or grouped into sections. */
@@ -111,29 +167,26 @@ type ColumnViewColumn<T = unknown> = Omit<GtkColumnViewColumnProps, "factory" |
111
167
  /** The declarative collection props {@link ColumnView} adds on top of Gtk.ColumnView's own. */
112
168
  type ColumnViewOwnProps<T, S> = SelectionProps &
113
169
  ExpansionProps &
170
+ SortProps &
114
171
  SourceProps<T, S> &
115
172
  Omit<ItemSizeProps, "estimatedItemWidth"> & {
116
173
  /** Columns to render, in order; each carries its own cell renderer. */
117
174
  columns: ColumnViewColumn<T>[];
118
175
  /** Renders the header shown above each section. */
119
176
  renderHeader?: ListSectionRenderer<S> | null | undefined;
120
- /** Id of the column the view is sorted by, making sorting controlled. */
121
- sortColumn?: string | null | undefined;
122
- /** Direction `sortColumn` is sorted in, defaulting to ascending. */
123
- sortOrder?: Gtk.SortType | null | undefined;
124
- /** Called when the user sorts from a header, with the primary column's id, or null, and its order. */
125
- onSortChanged?: ((column: string | null, order: Gtk.SortType) => void) | null | undefined;
177
+ /** Resolves the props of the row carrying one item's cells, such as its screen-reader label. */
178
+ rowProps?: ListRowPropsResolver<T> | null | undefined;
126
179
  };
127
180
 
128
181
  /**
129
182
  * Props for {@link ColumnView}. Combines the underlying Gtk.ColumnView props with
130
183
  * declarative collection props: flat items or grouped sections, controlled selection
131
184
  * and expansion, sorting (sortColumn, sortOrder, onSortChanged), an optional section
132
- * header renderer, and the columns to render.
185
+ * header renderer, per-row props, and the columns to render.
133
186
  */
134
187
  type ColumnViewProps<T = unknown, S = unknown> = Omit<
135
188
  GtkColumnViewProps,
136
- "columns" | "model" | "headerFactory" | keyof ColumnViewOwnProps<T, S>
189
+ "columns" | "model" | "headerFactory" | "rowFactory" | keyof ColumnViewOwnProps<T, S>
137
190
  > &
138
191
  ColumnViewOwnProps<T, S>;
139
192
 
@@ -207,14 +260,18 @@ ListViewOwnProps<T, S>;
207
260
 
208
261
  export {
209
262
  type SelectionProps,
263
+ type ExpanderDescriptions,
210
264
  type ExpansionProps,
265
+ type SortProps,
211
266
  type DropDownOwnProps,
212
267
  type DropDownWidgetProps,
213
268
  type ListItem,
214
269
  type ListSection,
215
270
  type ListItemRenderArgs,
271
+ type ListRowProps,
216
272
  type ListSectionRenderArgs,
217
273
  type ListItemRenderer,
274
+ type ListRowPropsResolver,
218
275
  type ListSectionRenderer,
219
276
  type ColumnViewColumn,
220
277
  type ColumnViewProps,