ember-primitives 0.57.0 → 0.59.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.
@@ -0,0 +1,162 @@
1
+ import Component from "@glimmer/component";
2
+ import type Owner from "@ember/owner";
3
+ export interface Signature<T = unknown> {
4
+ Args: {
5
+ /**
6
+ * The collection of items to render.
7
+ *
8
+ * Replacing the array (new identity) restarts rendering from the
9
+ * first batch.
10
+ *
11
+ * ```gjs
12
+ * import { IncrementalEach } from 'ember-primitives';
13
+ *
14
+ * <template>
15
+ * <IncrementalEach @items={{this.rows}} as |row|>
16
+ * <my-row @row={{row}} />
17
+ * </IncrementalEach>
18
+ * </template>
19
+ * ```
20
+ */
21
+ items: readonly T[];
22
+ /**
23
+ * How many items to add per animation frame.
24
+ *
25
+ * Larger batches add more items per chunk; smaller batches yield to
26
+ * the browser more often.
27
+ *
28
+ * Default: 50. Must be positive; `0` or less asserts in development.
29
+ *
30
+ * ```gjs
31
+ * import { IncrementalEach } from 'ember-primitives';
32
+ *
33
+ * <template>
34
+ * <IncrementalEach @items={{this.rows}} @batchSize={{100}} as |row|>
35
+ * <my-row @row={{row}} />
36
+ * </IncrementalEach>
37
+ * </template>
38
+ * ```
39
+ */
40
+ batchSize?: number;
41
+ /**
42
+ * Controls how the initial batch is committed.
43
+ *
44
+ * - `"sync"` (default): the first `@batchSize` items render in the
45
+ * same render pass as mount / `@items` change. The user sees
46
+ * content on the very first paint, and the rest of the list
47
+ * fills in one batch per animation frame. This is the right
48
+ * default for most lists — even a perceived "empty for one
49
+ * frame" is worse than rendering a few extra items synchronously.
50
+ * - `"lazy"`: even the first batch waits for an animation frame, so
51
+ * the initial paint is empty and content arrives one batch per
52
+ * frame. Use this when the first batch itself would be expensive
53
+ * enough to block the first paint, and you'd rather show an
54
+ * empty container than delay it.
55
+ *
56
+ * Default: `"sync"`.
57
+ *
58
+ * ```gjs
59
+ * import { IncrementalEach } from 'ember-primitives';
60
+ *
61
+ * <template>
62
+ * <IncrementalEach @items={{this.rows}} @initial="lazy" as |row|>
63
+ * <my-row @row={{row}} />
64
+ * </IncrementalEach>
65
+ * </template>
66
+ * ```
67
+ */
68
+ initial?: "sync" | "lazy";
69
+ /**
70
+ * Called once with no arguments when every item in `@items` has
71
+ * been committed to the DOM. Fires after the final batch lands;
72
+ * does not fire on intermediate batches.
73
+ *
74
+ * Fires again on a fresh swap (new `@items` identity) once that
75
+ * new collection finishes rendering. An empty `@items` array
76
+ * does not fire the callback.
77
+ *
78
+ * Useful for marking the list as ready for screenshot tests,
79
+ * dismissing a loading indicator, or measuring how long the
80
+ * whole render took.
81
+ *
82
+ * ```gjs
83
+ * import { IncrementalEach } from 'ember-primitives';
84
+ *
85
+ * <template>
86
+ * <IncrementalEach @items={{this.rows}} @onDone={{this.handleDone}} as |row|>
87
+ * <my-row @row={{row}} />
88
+ * </IncrementalEach>
89
+ * </template>
90
+ * ```
91
+ */
92
+ onDone?: () => void;
93
+ };
94
+ Blocks: {
95
+ /**
96
+ * Yielded for each rendered item, with the index in the original
97
+ * `@items` array.
98
+ *
99
+ * ```gjs
100
+ * import { IncrementalEach } from 'ember-primitives';
101
+ *
102
+ * <template>
103
+ * <IncrementalEach @items={{this.rows}} as |row index|>
104
+ * {{index}}: {{row.label}}
105
+ * </IncrementalEach>
106
+ * </template>
107
+ * ```
108
+ */
109
+ default: [item: T, index: number];
110
+ };
111
+ }
112
+ /**
113
+ * A drop-in replacement for `{{#each}}` that renders a large collection
114
+ * a batch at a time on each animation frame, instead of all at once.
115
+ *
116
+ * Every item ends up in the DOM, so browser find (Ctrl+F / Cmd+F), anchor
117
+ * links, screen readers, print, and SEO all work against the full list.
118
+ * Yielding the main thread between batches keeps the page responsive while
119
+ * the rest of the list is filling in.
120
+ *
121
+ * By default the first batch lands synchronously, so the user sees content
122
+ * on the very first paint. Pass `@initial="lazy"` to defer the first batch
123
+ * to an animation frame as well.
124
+ *
125
+ * Intended for non-scrollable containers, or anywhere a virtual/windowed
126
+ * list does not apply (variable item heights, lists that grow the page,
127
+ * surfaces that need every row indexable).
128
+ *
129
+ * Do not nest one `<IncrementalEach>` inside another. Each level adds an
130
+ * animation-frame delay before its content paints; nesting compounds those
131
+ * delays, so inner rows appear to flicker in with missing sub-content.
132
+ * If you have nested loops, only the outermost one should be
133
+ * `<IncrementalEach>`; leave deeper loops as plain `{{#each}}`.
134
+ *
135
+ * @example
136
+ * ```gjs
137
+ * import { IncrementalEach } from 'ember-primitives';
138
+ *
139
+ * <template>
140
+ * <ul>
141
+ * <IncrementalEach @items={{this.rows}} @batchSize={{100}} as |row index|>
142
+ * <li>{{index}}: {{row.label}}</li>
143
+ * </IncrementalEach>
144
+ * </ul>
145
+ * </template>
146
+ * ```
147
+ */
148
+ export declare class IncrementalEach<T = unknown> extends Component<Signature<T>> {
149
+ #private;
150
+ constructor(owner: Owner, args: Signature<T>["Args"]);
151
+ get i(): number;
152
+ get bucketed(): {
153
+ isReady: () => boolean;
154
+ items: {
155
+ value: T;
156
+ index: number;
157
+ }[];
158
+ }[];
159
+ tick: () => void;
160
+ checkDone: () => void;
161
+ }
162
+ //# sourceMappingURL=incremental-each.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"incremental-each.d.ts","sourceRoot":"","sources":["../../src/components/incremental-each.gts"],"names":[],"mappings":"AAoSA,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAQ3C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAiBtC,MAAM,WAAW,SAAS,CAAC,CAAC,GAAG,OAAO;IACpC,IAAI,EAAE;QACJ;;;;;;;;;;;;;;;WAeG;QACH,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;QAEpB;;;;;;;;;;;;;;;;;WAiBG;QACH,SAAS,CAAC,EAAE,MAAM,CAAC;QAEnB;;;;;;;;;;;;;;;;;;;;;;;;;;WA0BG;QACH,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAE1B;;;;;;;;;;;;;;;;;;;;;;WAsBG;QACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;KACrB,CAAC;IACF,MAAM,EAAE;QACN;;;;;;;;;;;;;WAaG;QACH,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;KACnC,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,qBAAa,eAAe,CAAC,CAAC,GAAG,OAAO,CAAE,SAAQ,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;;gBAM3D,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAsCpD,IAAI,CAAC,WAEJ;IAED,IACI,QAAQ;;;;;;QAWX;IA2BD,IAAI,aAIF;IAEF,SAAS,aAYP;CA4BH"}
@@ -1,4 +1,4 @@
1
- import type { Signature as FloatingUiComponentSignature } from "../floating-ui/component.ts";
1
+ import type { Signature as FloatingUiComponentSignature } from '../floating-ui/component';
2
2
  import type { Signature as HookSignature } from "../floating-ui/modifier.ts";
3
3
  import type { TOC } from "@ember/component/template-only";
4
4
  import type { ModifierLike, WithBoundArgs } from "@glint/template";
@@ -1 +1 @@
1
- {"version":3,"file":"popover.d.ts","sourceRoot":"","sources":["../../src/components/popover.gts"],"names":[],"mappings":"AAyQA,OAAO,KAAK,EAAE,SAAS,IAAI,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AAC7F,OAAO,KAAK,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC7E,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gCAAgC,CAAC;AAE1D,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEnE,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE;QACJ;;;;WAIG;QACH,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,aAAa,CAAC,CAAC;QAC5D;;;;WAIG;QACH,UAAU,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;QAC1D;;;;WAIG;QACH,aAAa,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,eAAe,CAAC,CAAC;QAChE;;;;;;;;;;;;WAYG;QACH,SAAS,CAAC,EAAE,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,EAAE,GAAG,QAAQ,GAAG,MAAM,EAAE,CAAC;QAC9E;;;;WAIG;QACH,YAAY,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,cAAc,CAAC,CAAC;QAC9D;;;;;;WAMG;QACH,QAAQ,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC;KACvD,CAAC;IACF,MAAM,EAAE;QACN,OAAO,EAAE;YACP;gBACE,SAAS,EAAE,4BAA4B,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;gBAChE,YAAY,EAAE,4BAA4B,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC;gBACnF,OAAO,EAAE,aAAa,CAAC,OAAO,OAAO,EAAE,UAAU,CAAC,CAAC;gBACnD,IAAI,EAAE,4BAA4B,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;gBACnE,KAAK,EAAE,YAAY,CAAC;oBAAE,OAAO,EAAE,WAAW,CAAA;iBAAE,CAAC,CAAC;aAC/C;SACF,CAAC;KACH,CAAC;CACH;AAoCD;;;;;GAKG;AACH,QAAA,MAAM,OAAO,EAAE,GAAG,CAAC;IACjB,OAAO,EAAE,cAAc,CAAC;IACxB,IAAI,EAAE;QACJ,QAAQ,EAAE,YAAY,CAAC;YAAE,OAAO,EAAE,WAAW,CAAA;SAAE,CAAC,CAAC;QACjD;;;;;;;;;;;;WAYG;QACH,EAAE,CAAC,EAAE,MAAM,CAAC;KACb,CAAC;IACF,MAAM,EAAE;QAAE,OAAO,EAAE,EAAE,CAAA;KAAE,CAAC;CACzB,CA0BC,CAAC;AAkFH,eAAO,MAAM,OAAO,EAAE,GAAG,CAAC,SAAS,CAmCjC,CAAC;AAEH,eAAe,OAAO,CAAC"}
1
+ {"version":3,"file":"popover.d.ts","sourceRoot":"","sources":["../../src/components/popover.gts"],"names":[],"mappings":"AAyQA,OAAO,KAAK,EAAE,SAAS,IAAI,4BAA4B,EAAE,MAAM,8BAA8B,CAAC;AAC9F,OAAO,KAAK,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC7E,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gCAAgC,CAAC;AAE1D,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEnE,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE;QACJ;;;;WAIG;QACH,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,aAAa,CAAC,CAAC;QAC5D;;;;WAIG;QACH,UAAU,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;QAC1D;;;;WAIG;QACH,aAAa,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,eAAe,CAAC,CAAC;QAChE;;;;;;;;;;;;WAYG;QACH,SAAS,CAAC,EAAE,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,EAAE,GAAG,QAAQ,GAAG,MAAM,EAAE,CAAC;QAC9E;;;;WAIG;QACH,YAAY,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,cAAc,CAAC,CAAC;QAC9D;;;;;;WAMG;QACH,QAAQ,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC;KACvD,CAAC;IACF,MAAM,EAAE;QACN,OAAO,EAAE;YACP;gBACE,SAAS,EAAE,4BAA4B,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;gBAChE,YAAY,EAAE,4BAA4B,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC;gBACnF,OAAO,EAAE,aAAa,CAAC,OAAO,OAAO,EAAE,UAAU,CAAC,CAAC;gBACnD,IAAI,EAAE,4BAA4B,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;gBACnE,KAAK,EAAE,YAAY,CAAC;oBAAE,OAAO,EAAE,WAAW,CAAA;iBAAE,CAAC,CAAC;aAC/C;SACF,CAAC;KACH,CAAC;CACH;AAoCD;;;;;GAKG;AACH,QAAA,MAAM,OAAO,EAAE,GAAG,CAAC;IACjB,OAAO,EAAE,cAAc,CAAC;IACxB,IAAI,EAAE;QACJ,QAAQ,EAAE,YAAY,CAAC;YAAE,OAAO,EAAE,WAAW,CAAA;SAAE,CAAC,CAAC;QACjD;;;;;;;;;;;;WAYG;QACH,EAAE,CAAC,EAAE,MAAM,CAAC;KACb,CAAC;IACF,MAAM,EAAE;QAAE,OAAO,EAAE,EAAE,CAAA;KAAE,CAAC;CACzB,CA0BC,CAAC;AAkFH,eAAO,MAAM,OAAO,EAAE,GAAG,CAAC,SAAS,CAmCjC,CAAC;AAEH,eAAe,OAAO,CAAC"}
@@ -6,6 +6,7 @@ export { Dialog, Dialog as Modal } from './components/dialog';
6
6
  export { Drawer } from './components/drawer';
7
7
  export { ExternalLink } from './components/external-link';
8
8
  export { Form } from './components/form';
9
+ export { IncrementalEach } from './components/incremental-each';
9
10
  export { Key, KeyCombo } from './components/keys';
10
11
  export { StickyFooter } from './components/layout/sticky-footer';
11
12
  export { Link } from './components/link';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,YAAY,EACV,iCAAiC,EACjC,gCAAgC,EAChC,8BAA8B,EAC9B,iCAAiC,GAClC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,IAAI,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AACrE,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,YAAY,EACV,iCAAiC,EACjC,gCAAgC,EAChC,8BAA8B,EAC9B,iCAAiC,GAClC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,IAAI,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACpE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AACrE,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,cAAc,cAAc,CAAC"}
@@ -12,7 +12,7 @@ import type { PortalTargets } from './components/portal-targets';
12
12
  import type { Shadowed } from './components/shadowed';
13
13
  import type { Switch } from './components/switch';
14
14
  import type { Toggle } from './components/toggle';
15
- import type { service } from './helpers/service';
15
+ import type { service } from './helpers/service.ts';
16
16
  export default interface Registry {
17
17
  Accordion: typeof Accordion;
18
18
  AccordionItem: typeof AccordionItem;
@@ -1 +1 @@
1
- {"version":3,"file":"template-registry.d.ts","sourceRoot":"","sources":["../src/template-registry.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAMjD,MAAM,CAAC,OAAO,WAAW,QAAQ;IAE/B,SAAS,EAAE,OAAO,SAAS,CAAC;IAC5B,aAAa,EAAE,OAAO,aAAa,CAAC;IACpC,eAAe,EAAE,OAAO,eAAe,CAAC;IACxC,gBAAgB,EAAE,OAAO,gBAAgB,CAAC;IAC1C,gBAAgB,EAAE,OAAO,gBAAgB,CAAC;IAC1C,MAAM,EAAE,OAAO,MAAM,CAAC;IACtB,YAAY,EAAE,OAAO,YAAY,CAAC;IAClC,IAAI,EAAE,OAAO,IAAI,CAAC;IAClB,OAAO,EAAE,OAAO,OAAO,CAAC;IACxB,aAAa,EAAE,OAAO,aAAa,CAAC;IACpC,MAAM,EAAE,OAAO,MAAM,CAAC;IACtB,QAAQ,EAAE,OAAO,QAAQ,CAAC;IAC1B,MAAM,EAAE,OAAO,MAAM,CAAC;IACtB,MAAM,EAAE,OAAO,MAAM,CAAC;IAGtB,OAAO,EAAE,OAAO,OAAO,CAAC;CACzB"}
1
+ {"version":3,"file":"template-registry.d.ts","sourceRoot":"","sources":["../src/template-registry.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAMpD,MAAM,CAAC,OAAO,WAAW,QAAQ;IAE/B,SAAS,EAAE,OAAO,SAAS,CAAC;IAC5B,aAAa,EAAE,OAAO,aAAa,CAAC;IACpC,eAAe,EAAE,OAAO,eAAe,CAAC;IACxC,gBAAgB,EAAE,OAAO,gBAAgB,CAAC;IAC1C,gBAAgB,EAAE,OAAO,gBAAgB,CAAC;IAC1C,MAAM,EAAE,OAAO,MAAM,CAAC;IACtB,YAAY,EAAE,OAAO,YAAY,CAAC;IAClC,IAAI,EAAE,OAAO,IAAI,CAAC;IAClB,OAAO,EAAE,OAAO,OAAO,CAAC;IACxB,aAAa,EAAE,OAAO,aAAa,CAAC;IACpC,MAAM,EAAE,OAAO,MAAM,CAAC;IACtB,QAAQ,EAAE,OAAO,QAAQ,CAAC;IAC1B,MAAM,EAAE,OAAO,MAAM,CAAC;IACtB,MAAM,EAAE,OAAO,MAAM,CAAC;IAGtB,OAAO,EAAE,OAAO,OAAO,CAAC;CACzB"}
@@ -0,0 +1,152 @@
1
+ import Component from '@glimmer/component';
2
+ import { cached } from '@glimmer/tracking';
3
+ import { assert } from '@ember/debug';
4
+ import { registerDestructor, isDestroyed, isDestroying } from '@ember/destroyable';
5
+ import { buildWaiter } from '@ember/test-waiters';
6
+ import { cell } from 'ember-resources';
7
+ import { precompileTemplate } from '@ember/template-compilation';
8
+ import { setComponentTemplate } from '@ember/component';
9
+ import { n } from 'decorator-transforms/runtime';
10
+
11
+ const DEFAULT_BATCH_SIZE = 50;
12
+ const DEFAULT_INITIAL = "sync";
13
+ const waiter = buildWaiter("ember-primitives:incremental-each");
14
+ function chunk(arr, size) {
15
+ const out = [];
16
+ for (let i = 0; i < arr.length; i += size) {
17
+ out.push(arr.slice(i, i + size));
18
+ }
19
+ return out;
20
+ }
21
+ /**
22
+ * A drop-in replacement for `{{#each}}` that renders a large collection
23
+ * a batch at a time on each animation frame, instead of all at once.
24
+ *
25
+ * Every item ends up in the DOM, so browser find (Ctrl+F / Cmd+F), anchor
26
+ * links, screen readers, print, and SEO all work against the full list.
27
+ * Yielding the main thread between batches keeps the page responsive while
28
+ * the rest of the list is filling in.
29
+ *
30
+ * By default the first batch lands synchronously, so the user sees content
31
+ * on the very first paint. Pass `@initial="lazy"` to defer the first batch
32
+ * to an animation frame as well.
33
+ *
34
+ * Intended for non-scrollable containers, or anywhere a virtual/windowed
35
+ * list does not apply (variable item heights, lists that grow the page,
36
+ * surfaces that need every row indexable).
37
+ *
38
+ * Do not nest one `<IncrementalEach>` inside another. Each level adds an
39
+ * animation-frame delay before its content paints; nesting compounds those
40
+ * delays, so inner rows appear to flicker in with missing sub-content.
41
+ * If you have nested loops, only the outermost one should be
42
+ * `<IncrementalEach>`; leave deeper loops as plain `{{#each}}`.
43
+ *
44
+ * @example
45
+ * ```gjs
46
+ * import { IncrementalEach } from 'ember-primitives';
47
+ *
48
+ * <template>
49
+ * <ul>
50
+ * <IncrementalEach @items={{this.rows}} @batchSize={{100}} as |row index|>
51
+ * <li>{{index}}: {{row.label}}</li>
52
+ * </IncrementalEach>
53
+ * </ul>
54
+ * </template>
55
+ * ```
56
+ */
57
+ class IncrementalEach extends Component {
58
+ #count = cell(0);
59
+ #itemsRef = null;
60
+ #waiterToken = null;
61
+ #doneFor = null;
62
+ constructor(owner, args) {
63
+ super(owner, args);
64
+ registerDestructor(this, () => this.#endWaiter());
65
+ }
66
+ // Reset progress and (re)open the test-waiter when `@items` identity
67
+ // changes, so a swap restarts at the first batch, `@onDone` can fire
68
+ // again for the new collection, and `await settled()` knows to wait
69
+ // until `checkDone` closes the waiter. Mutating from a getter is safe
70
+ // here because the writes happen before any consumer reads them in
71
+ // the same render pass.
72
+ /* eslint-disable ember/no-side-effects */
73
+ get #items() {
74
+ const items = this.args.items;
75
+ assert(`@items must be an array`, items);
76
+ if (items !== this.#itemsRef) {
77
+ this.#itemsRef = items;
78
+ this.#count.current = 0;
79
+ this.#endWaiter();
80
+ if (items.length > 0) {
81
+ this.#waiterToken = waiter.beginAsync();
82
+ }
83
+ }
84
+ return items;
85
+ }
86
+ /* eslint-enable ember/no-side-effects */ // `"sync"` keeps bucket 0 visible at count=0 (`i = 0 >= 0`); `"lazy"`
87
+ // starts one step behind so even bucket 0 needs a tick.
88
+ get #start() {
89
+ return this.#initial === "sync" ? 0 : -1;
90
+ }
91
+ get i() {
92
+ return this.#start + this.#count.current;
93
+ }
94
+ get bucketed() {
95
+ const size = this.#batchSize;
96
+ return chunk(this.#items, size).map((items, b) => {
97
+ const start = b * size;
98
+ return {
99
+ isReady: () => this.i >= b,
100
+ items: items.map((value, j) => ({
101
+ value,
102
+ index: start + j
103
+ }))
104
+ };
105
+ });
106
+ }
107
+ static {
108
+ n(this.prototype, "bucketed", [cached]);
109
+ }
110
+ get #batchSize() {
111
+ const requested = this.args.batchSize ?? DEFAULT_BATCH_SIZE;
112
+ assert(`<IncrementalEach> @batchSize must be a positive number, got ${requested}`, requested > 0);
113
+ return requested;
114
+ }
115
+ get #initial() {
116
+ const requested = this.args.initial ?? DEFAULT_INITIAL;
117
+ assert(`<IncrementalEach> @initial must be "sync" or "lazy", got ${requested}`, requested === "sync" || requested === "lazy");
118
+ return requested;
119
+ }
120
+ // `#items` is read before `#count` so the count-reset inside `#items`
121
+ // (on `@items` swap) lands before this read of count this render —
122
+ // otherwise tracked-value backtracking asserts.
123
+ tick = () => {
124
+ if (this.#items.length > this.#count.current) {
125
+ requestIdleCallback(() => this.#count.current++, {
126
+ timeout: 10
127
+ });
128
+ }
129
+ };
130
+ checkDone = () => {
131
+ const bucketed = this.bucketed;
132
+ if (this.#doneFor === bucketed) return;
133
+ if (this.i < bucketed.length - 1) return;
134
+ this.#doneFor = bucketed;
135
+ queueMicrotask(() => {
136
+ if (isDestroyed(this) || isDestroying(this)) return;
137
+ this.args.onDone?.();
138
+ this.#endWaiter();
139
+ });
140
+ };
141
+ #endWaiter() {
142
+ if (this.#waiterToken) waiter.endAsync(this.#waiterToken);
143
+ }
144
+ static {
145
+ setComponentTemplate(precompileTemplate("{{(this.tick)}}{{#each this.bucketed as |bucket|}}{{#if (bucket.isReady)}}{{#each bucket.items as |entry|}}{{yield entry.value entry.index}}{{/each}}{{(this.checkDone)}}{{/if}}{{/each}}", {
146
+ strictMode: true
147
+ }), this);
148
+ }
149
+ }
150
+
151
+ export { IncrementalEach };
152
+ //# sourceMappingURL=incremental-each.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"incremental-each.js","sources":["../../src/components/incremental-each.gts"],"sourcesContent":["import Component from \"@glimmer/component\";\nimport { cached } from \"@glimmer/tracking\";\nimport { assert } from \"@ember/debug\";\nimport { isDestroyed, isDestroying, registerDestructor } from \"@ember/destroyable\";\nimport { buildWaiter } from \"@ember/test-waiters\";\n\nimport { cell } from \"ember-resources\";\n\nimport type Owner from \"@ember/owner\";\n\nconst DEFAULT_BATCH_SIZE = 50;\nconst DEFAULT_INITIAL = \"sync\";\n\nconst waiter = buildWaiter(\"ember-primitives:incremental-each\");\n\nfunction chunk<T>(arr: readonly T[], size: number): T[][] {\n const out: T[][] = [];\n\n for (let i = 0; i < arr.length; i += size) {\n out.push(arr.slice(i, i + size));\n }\n\n return out;\n}\n\nexport interface Signature<T = unknown> {\n Args: {\n /**\n * The collection of items to render.\n *\n * Replacing the array (new identity) restarts rendering from the\n * first batch.\n *\n * ```gjs\n * import { IncrementalEach } from 'ember-primitives';\n *\n * <template>\n * <IncrementalEach @items={{this.rows}} as |row|>\n * <my-row @row={{row}} />\n * </IncrementalEach>\n * </template>\n * ```\n */\n items: readonly T[];\n\n /**\n * How many items to add per animation frame.\n *\n * Larger batches add more items per chunk; smaller batches yield to\n * the browser more often.\n *\n * Default: 50. Must be positive; `0` or less asserts in development.\n *\n * ```gjs\n * import { IncrementalEach } from 'ember-primitives';\n *\n * <template>\n * <IncrementalEach @items={{this.rows}} @batchSize={{100}} as |row|>\n * <my-row @row={{row}} />\n * </IncrementalEach>\n * </template>\n * ```\n */\n batchSize?: number;\n\n /**\n * Controls how the initial batch is committed.\n *\n * - `\"sync\"` (default): the first `@batchSize` items render in the\n * same render pass as mount / `@items` change. The user sees\n * content on the very first paint, and the rest of the list\n * fills in one batch per animation frame. This is the right\n * default for most lists — even a perceived \"empty for one\n * frame\" is worse than rendering a few extra items synchronously.\n * - `\"lazy\"`: even the first batch waits for an animation frame, so\n * the initial paint is empty and content arrives one batch per\n * frame. Use this when the first batch itself would be expensive\n * enough to block the first paint, and you'd rather show an\n * empty container than delay it.\n *\n * Default: `\"sync\"`.\n *\n * ```gjs\n * import { IncrementalEach } from 'ember-primitives';\n *\n * <template>\n * <IncrementalEach @items={{this.rows}} @initial=\"lazy\" as |row|>\n * <my-row @row={{row}} />\n * </IncrementalEach>\n * </template>\n * ```\n */\n initial?: \"sync\" | \"lazy\";\n\n /**\n * Called once with no arguments when every item in `@items` has\n * been committed to the DOM. Fires after the final batch lands;\n * does not fire on intermediate batches.\n *\n * Fires again on a fresh swap (new `@items` identity) once that\n * new collection finishes rendering. An empty `@items` array\n * does not fire the callback.\n *\n * Useful for marking the list as ready for screenshot tests,\n * dismissing a loading indicator, or measuring how long the\n * whole render took.\n *\n * ```gjs\n * import { IncrementalEach } from 'ember-primitives';\n *\n * <template>\n * <IncrementalEach @items={{this.rows}} @onDone={{this.handleDone}} as |row|>\n * <my-row @row={{row}} />\n * </IncrementalEach>\n * </template>\n * ```\n */\n onDone?: () => void;\n };\n Blocks: {\n /**\n * Yielded for each rendered item, with the index in the original\n * `@items` array.\n *\n * ```gjs\n * import { IncrementalEach } from 'ember-primitives';\n *\n * <template>\n * <IncrementalEach @items={{this.rows}} as |row index|>\n * {{index}}: {{row.label}}\n * </IncrementalEach>\n * </template>\n * ```\n */\n default: [item: T, index: number];\n };\n}\n\n/**\n * A drop-in replacement for `{{#each}}` that renders a large collection\n * a batch at a time on each animation frame, instead of all at once.\n *\n * Every item ends up in the DOM, so browser find (Ctrl+F / Cmd+F), anchor\n * links, screen readers, print, and SEO all work against the full list.\n * Yielding the main thread between batches keeps the page responsive while\n * the rest of the list is filling in.\n *\n * By default the first batch lands synchronously, so the user sees content\n * on the very first paint. Pass `@initial=\"lazy\"` to defer the first batch\n * to an animation frame as well.\n *\n * Intended for non-scrollable containers, or anywhere a virtual/windowed\n * list does not apply (variable item heights, lists that grow the page,\n * surfaces that need every row indexable).\n *\n * Do not nest one `<IncrementalEach>` inside another. Each level adds an\n * animation-frame delay before its content paints; nesting compounds those\n * delays, so inner rows appear to flicker in with missing sub-content.\n * If you have nested loops, only the outermost one should be\n * `<IncrementalEach>`; leave deeper loops as plain `{{#each}}`.\n *\n * @example\n * ```gjs\n * import { IncrementalEach } from 'ember-primitives';\n *\n * <template>\n * <ul>\n * <IncrementalEach @items={{this.rows}} @batchSize={{100}} as |row index|>\n * <li>{{index}}: {{row.label}}</li>\n * </IncrementalEach>\n * </ul>\n * </template>\n * ```\n */\nexport class IncrementalEach<T = unknown> extends Component<Signature<T>> {\n #count = cell(0);\n #itemsRef: readonly T[] | null = null;\n #waiterToken: unknown = null;\n #doneFor: object | null = null;\n\n constructor(owner: Owner, args: Signature<T>[\"Args\"]) {\n super(owner, args);\n\n registerDestructor(this, () => this.#endWaiter());\n }\n\n // Reset progress and (re)open the test-waiter when `@items` identity\n // changes, so a swap restarts at the first batch, `@onDone` can fire\n // again for the new collection, and `await settled()` knows to wait\n // until `checkDone` closes the waiter. Mutating from a getter is safe\n // here because the writes happen before any consumer reads them in\n // the same render pass.\n /* eslint-disable ember/no-side-effects */\n get #items(): readonly T[] {\n const items = this.args.items;\n\n assert(`@items must be an array`, items);\n\n if (items !== this.#itemsRef) {\n this.#itemsRef = items;\n this.#count.current = 0;\n this.#endWaiter();\n\n if (items.length > 0) {\n this.#waiterToken = waiter.beginAsync();\n }\n }\n\n return items;\n }\n /* eslint-enable ember/no-side-effects */\n\n // `\"sync\"` keeps bucket 0 visible at count=0 (`i = 0 >= 0`); `\"lazy\"`\n // starts one step behind so even bucket 0 needs a tick.\n get #start() {\n return this.#initial === \"sync\" ? 0 : -1;\n }\n\n get i() {\n return this.#start + this.#count.current;\n }\n\n @cached\n get bucketed() {\n const size = this.#batchSize;\n\n return chunk(this.#items, size).map((items, b) => {\n const start = b * size;\n\n return {\n isReady: () => this.i >= b,\n items: items.map((value, j) => ({ value, index: start + j })),\n };\n });\n }\n\n get #batchSize(): number {\n const requested = this.args.batchSize ?? DEFAULT_BATCH_SIZE;\n\n assert(\n `<IncrementalEach> @batchSize must be a positive number, got ${requested}`,\n requested > 0,\n );\n\n return requested;\n }\n\n get #initial(): \"sync\" | \"lazy\" {\n const requested = this.args.initial ?? DEFAULT_INITIAL;\n\n assert(\n `<IncrementalEach> @initial must be \"sync\" or \"lazy\", got ${requested}`,\n requested === \"sync\" || requested === \"lazy\",\n );\n\n return requested;\n }\n\n // `#items` is read before `#count` so the count-reset inside `#items`\n // (on `@items` swap) lands before this read of count this render —\n // otherwise tracked-value backtracking asserts.\n tick = () => {\n if (this.#items.length > this.#count.current) {\n requestIdleCallback(() => this.#count.current++, { timeout: 10 });\n }\n };\n\n checkDone = () => {\n const bucketed = this.bucketed;\n\n if (this.#doneFor === bucketed) return;\n if (this.i < bucketed.length - 1) return;\n\n this.#doneFor = bucketed;\n queueMicrotask(() => {\n if (isDestroyed(this) || isDestroying(this)) return;\n this.args.onDone?.();\n this.#endWaiter();\n });\n };\n\n #endWaiter() {\n if (this.#waiterToken) waiter.endAsync(this.#waiterToken);\n }\n\n <template>\n {{(this.tick)}}{{#each this.bucketed as |bucket|}}{{#if (bucket.isReady)}}{{#each\n bucket.items\n as |entry|\n }}{{yield entry.value entry.index}}{{/each}}{{(this.checkDone)}}{{/if}}{{/each}}\n </template>\n}\n"],"names":["DEFAULT_BATCH_SIZE","DEFAULT_INITIAL","waiter","buildWaiter","chunk","arr","size","out","i","length","push","slice","IncrementalEach","Component","cell","constructor","owner","args","registerDestructor","#items","items","assert","current","beginAsync","#start","bucketed","map","b","start","isReady","value","j","index","n","prototype","cached","#batchSize","requested","batchSize","#initial","initial","tick","requestIdleCallback","timeout","checkDone","queueMicrotask","isDestroyed","isDestroying","onDone","#endWaiter","endAsync","setComponentTemplate","precompileTemplate","strictMode"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"popover.js","sources":["../../src/components/popover.gts"],"sourcesContent":["import { hash } from \"@ember/helper\";\n\nimport { arrow } from \"@floating-ui/dom\";\nimport { element } from \"ember-element-helper\";\nimport { modifier as eModifier } from \"ember-modifier\";\nimport { cell } from \"ember-resources\";\n\nimport { FloatingUI } from \"../floating-ui.ts\";\n\nimport type { Signature as FloatingUiComponentSignature } from \"../floating-ui/component.ts\";\nimport type { Signature as HookSignature } from \"../floating-ui/modifier.ts\";\nimport type { TOC } from \"@ember/component/template-only\";\nimport type { ElementContext, Middleware } from \"@floating-ui/dom\";\nimport type { ModifierLike, WithBoundArgs } from \"@glint/template\";\n\nexport interface Signature {\n Args: {\n /**\n * See the Floating UI's [flip docs](https://floating-ui.com/docs/flip) for possible values.\n *\n * This argument is forwarded to the `<FloatingUI>` component.\n */\n flipOptions?: HookSignature[\"Args\"][\"Named\"][\"flipOptions\"];\n /**\n * Array of one or more objects to add to Floating UI's list of [middleware](https://floating-ui.com/docs/middleware)\n *\n * This argument is forwarded to the `<FloatingUI>` component.\n */\n middleware?: HookSignature[\"Args\"][\"Named\"][\"middleware\"];\n /**\n * See the Floating UI's [offset docs](https://floating-ui.com/docs/offset) for possible values.\n *\n * This argument is forwarded to the `<FloatingUI>` component.\n */\n offsetOptions?: HookSignature[\"Args\"][\"Named\"][\"offsetOptions\"];\n /**\n * One of the possible [`placements`](https://floating-ui.com/docs/computeposition#placement). The default is 'bottom'.\n *\n * Possible values are\n * - top\n * - bottom\n * - right\n * - left\n *\n * And may optionally have `-start` or `-end` added to adjust position along the side.\n *\n * This argument is forwarded to the `<FloatingUI>` component.\n */\n placement?: `${\"top\" | \"bottom\" | \"left\" | \"right\"}${\"\" | \"-start\" | \"-end\"}`;\n /**\n * See the Floating UI's [shift docs](https://floating-ui.com/docs/shift) for possible values.\n *\n * This argument is forwarded to the `<FloatingUI>` component.\n */\n shiftOptions?: HookSignature[\"Args\"][\"Named\"][\"shiftOptions\"];\n /**\n * CSS position property, either `fixed` or `absolute`.\n *\n * Pros and cons of each strategy are explained on [Floating UI's Docs](https://floating-ui.com/docs/computePosition#strategy)\n *\n * This argument is forwarded to the `<FloatingUI>` component.\n */\n strategy?: HookSignature[\"Args\"][\"Named\"][\"strategy\"];\n };\n Blocks: {\n default: [\n {\n reference: FloatingUiComponentSignature[\"Blocks\"][\"default\"][0];\n setReference: FloatingUiComponentSignature[\"Blocks\"][\"default\"][2][\"setReference\"];\n Content: WithBoundArgs<typeof Content, \"floating\">;\n data: FloatingUiComponentSignature[\"Blocks\"][\"default\"][2][\"data\"];\n arrow: ModifierLike<{ Element: HTMLElement }>;\n },\n ];\n };\n}\n\nconst showPopover = eModifier<{ Element: Element }>((element) => {\n const el = element as HTMLElement;\n\n // Reset [popover] UA overflow default that clips arrows positioned outside\n el.style.setProperty(\"overflow\", \"visible\");\n\n // Don't promote to top layer if already inside a popover — the parent\n // popover already handles layering. Adding both to the top layer causes\n // stacking issues where the parent renders on top of the child.\n if (el.parentElement?.closest(\"[popover]\")) {\n el.removeAttribute(\"popover\");\n\n // <dialog> elements are hidden by default — ensure they're visible\n // when opting out of the top layer.\n if (el instanceof HTMLDialogElement) {\n el.setAttribute(\"open\", \"\");\n }\n } else {\n el.showPopover();\n }\n\n return () => {\n try {\n el.hidePopover();\n } catch {\n /* already hidden */\n }\n };\n});\n\nfunction getElementTag(tagName: undefined | string) {\n return tagName || \"div\";\n}\n\n/**\n * Content uses `popover=\"manual\"` + `showPopover()` to promote\n * the element to the browser's top layer. This escapes all ancestor\n * overflow clipping and stacking contexts — the same guarantee that\n * portalling provided, but using the browser's native mechanism.\n */\nconst Content: TOC<{\n Element: HTMLDivElement;\n Args: {\n floating: ModifierLike<{ Element: HTMLElement }>;\n /**\n * By default the popover content is wrapped in a div.\n * You may change this by supplying the name of an element here.\n *\n * For example:\n * ```gjs\n * <Popover as |p|>\n * <p.Content @as=\"dialog\">\n * this is now focus trapped\n * </p.Content>\n * </Popover>\n * ```\n */\n as?: string;\n };\n Blocks: { default: [] };\n}> = <template>\n {{#let (element (getElementTag @as)) as |El|}}\n {{! @glint-ignore\n https://github.com/tildeio/ember-element-helper/issues/91\n https://github.com/typed-ember/glint/issues/610\n }}\n <El popover=\"manual\" {{showPopover}} {{@floating}} ...attributes>\n {{yield}}\n </El>\n {{/let}}\n</template>;\n\ninterface AttachArrowSignature {\n Element: HTMLElement;\n Args: {\n Named: {\n arrowElement: ReturnType<typeof ArrowElement>;\n data:\n | undefined\n | {\n placement: string;\n middlewareData?: {\n arrow?: { x?: number; y?: number };\n };\n };\n };\n };\n}\n\nconst arrowSides = {\n top: \"bottom\",\n right: \"left\",\n bottom: \"top\",\n left: \"right\",\n};\n\ntype Direction = \"top\" | \"bottom\" | \"left\" | \"right\";\ntype Placement = `${Direction}${\"\" | \"-start\" | \"-end\"}`;\n\nconst attachArrow: ModifierLike<AttachArrowSignature> = eModifier<AttachArrowSignature>(\n (element, _: [], named) => {\n if (element === named.arrowElement.current) {\n if (!named.data) return;\n if (!named.data.middlewareData) return;\n\n const { arrow } = named.data.middlewareData;\n const { placement } = named.data;\n\n if (!arrow) return;\n if (!placement) return;\n\n const { x: arrowX, y: arrowY } = arrow;\n const otherSide = (placement as Placement).split(\"-\")[0] as Direction;\n const staticSide = arrowSides[otherSide];\n\n Object.assign(named.arrowElement.current.style, {\n left: arrowX != null ? `${arrowX}px` : \"\",\n top: arrowY != null ? `${arrowY}px` : \"\",\n right: \"\",\n bottom: \"\",\n [staticSide]: \"-4px\",\n });\n\n return;\n }\n\n void (async () => {\n await Promise.resolve();\n named.arrowElement.set(element);\n })();\n },\n);\n\nconst ArrowElement: () => ReturnType<typeof cell<HTMLElement>> = () => cell<HTMLElement>();\n\nfunction maybeAddArrow(middleware: Middleware[] | undefined, element: Element | undefined) {\n const result = [...(middleware || [])];\n\n if (element) {\n result.push(arrow({ element }));\n }\n\n return result;\n}\n\nfunction flipOptions(options: HookSignature[\"Args\"][\"Named\"][\"flipOptions\"]) {\n return {\n elementContext: \"reference\" as ElementContext,\n ...options,\n };\n}\n\nexport const Popover: TOC<Signature> = <template>\n {{#let (ArrowElement) as |arrowElement|}}\n <FloatingUI\n @placement={{@placement}}\n @strategy={{@strategy}}\n @middleware={{maybeAddArrow @middleware arrowElement.current}}\n @flipOptions={{flipOptions @flipOptions}}\n @shiftOptions={{@shiftOptions}}\n @offsetOptions={{@offsetOptions}}\n as |reference floating extra|\n >\n {{#let (modifier attachArrow arrowElement=arrowElement data=extra.data) as |arrow|}}\n {{yield\n (hash\n reference=reference\n setReference=extra.setReference\n Content=(component Content floating=floating)\n data=extra.data\n arrow=arrow\n )\n }}\n {{/let}}\n </FloatingUI>\n {{/let}}\n</template>;\n\nexport default Popover;\n"],"names":["showPopover","eModifier","element","el","style","setProperty","parentElement","closest","removeAttribute","HTMLDialogElement","setAttribute","hidePopover","getElementTag","tagName","Content","setComponentTemplate","precompileTemplate","strictMode","scope","templateOnly","arrowSides","top","right","bottom","left","attachArrow","_","named","arrowElement","current","data","middlewareData","arrow","placement","x","arrowX","y","arrowY","otherSide","split","staticSide","Object","assign","Promise","resolve","set","ArrowElement","cell","maybeAddArrow","middleware","result","push","flipOptions","options","elementContext","Popover","FloatingUI","hash"],"mappings":"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"}
1
+ {"version":3,"file":"popover.js","sources":["../../src/components/popover.gts"],"sourcesContent":["import { hash } from \"@ember/helper\";\n\nimport { arrow } from \"@floating-ui/dom\";\nimport { element } from \"ember-element-helper\";\nimport { modifier as eModifier } from \"ember-modifier\";\nimport { cell } from \"ember-resources\";\n\nimport { FloatingUI } from \"../floating-ui.ts\";\n\nimport type { Signature as FloatingUiComponentSignature } from \"../floating-ui/component.gts\";\nimport type { Signature as HookSignature } from \"../floating-ui/modifier.ts\";\nimport type { TOC } from \"@ember/component/template-only\";\nimport type { ElementContext, Middleware } from \"@floating-ui/dom\";\nimport type { ModifierLike, WithBoundArgs } from \"@glint/template\";\n\nexport interface Signature {\n Args: {\n /**\n * See the Floating UI's [flip docs](https://floating-ui.com/docs/flip) for possible values.\n *\n * This argument is forwarded to the `<FloatingUI>` component.\n */\n flipOptions?: HookSignature[\"Args\"][\"Named\"][\"flipOptions\"];\n /**\n * Array of one or more objects to add to Floating UI's list of [middleware](https://floating-ui.com/docs/middleware)\n *\n * This argument is forwarded to the `<FloatingUI>` component.\n */\n middleware?: HookSignature[\"Args\"][\"Named\"][\"middleware\"];\n /**\n * See the Floating UI's [offset docs](https://floating-ui.com/docs/offset) for possible values.\n *\n * This argument is forwarded to the `<FloatingUI>` component.\n */\n offsetOptions?: HookSignature[\"Args\"][\"Named\"][\"offsetOptions\"];\n /**\n * One of the possible [`placements`](https://floating-ui.com/docs/computeposition#placement). The default is 'bottom'.\n *\n * Possible values are\n * - top\n * - bottom\n * - right\n * - left\n *\n * And may optionally have `-start` or `-end` added to adjust position along the side.\n *\n * This argument is forwarded to the `<FloatingUI>` component.\n */\n placement?: `${\"top\" | \"bottom\" | \"left\" | \"right\"}${\"\" | \"-start\" | \"-end\"}`;\n /**\n * See the Floating UI's [shift docs](https://floating-ui.com/docs/shift) for possible values.\n *\n * This argument is forwarded to the `<FloatingUI>` component.\n */\n shiftOptions?: HookSignature[\"Args\"][\"Named\"][\"shiftOptions\"];\n /**\n * CSS position property, either `fixed` or `absolute`.\n *\n * Pros and cons of each strategy are explained on [Floating UI's Docs](https://floating-ui.com/docs/computePosition#strategy)\n *\n * This argument is forwarded to the `<FloatingUI>` component.\n */\n strategy?: HookSignature[\"Args\"][\"Named\"][\"strategy\"];\n };\n Blocks: {\n default: [\n {\n reference: FloatingUiComponentSignature[\"Blocks\"][\"default\"][0];\n setReference: FloatingUiComponentSignature[\"Blocks\"][\"default\"][2][\"setReference\"];\n Content: WithBoundArgs<typeof Content, \"floating\">;\n data: FloatingUiComponentSignature[\"Blocks\"][\"default\"][2][\"data\"];\n arrow: ModifierLike<{ Element: HTMLElement }>;\n },\n ];\n };\n}\n\nconst showPopover = eModifier<{ Element: Element }>((element) => {\n const el = element as HTMLElement;\n\n // Reset [popover] UA overflow default that clips arrows positioned outside\n el.style.setProperty(\"overflow\", \"visible\");\n\n // Don't promote to top layer if already inside a popover — the parent\n // popover already handles layering. Adding both to the top layer causes\n // stacking issues where the parent renders on top of the child.\n if (el.parentElement?.closest(\"[popover]\")) {\n el.removeAttribute(\"popover\");\n\n // <dialog> elements are hidden by default — ensure they're visible\n // when opting out of the top layer.\n if (el instanceof HTMLDialogElement) {\n el.setAttribute(\"open\", \"\");\n }\n } else {\n el.showPopover();\n }\n\n return () => {\n try {\n el.hidePopover();\n } catch {\n /* already hidden */\n }\n };\n});\n\nfunction getElementTag(tagName: undefined | string) {\n return tagName || \"div\";\n}\n\n/**\n * Content uses `popover=\"manual\"` + `showPopover()` to promote\n * the element to the browser's top layer. This escapes all ancestor\n * overflow clipping and stacking contexts — the same guarantee that\n * portalling provided, but using the browser's native mechanism.\n */\nconst Content: TOC<{\n Element: HTMLDivElement;\n Args: {\n floating: ModifierLike<{ Element: HTMLElement }>;\n /**\n * By default the popover content is wrapped in a div.\n * You may change this by supplying the name of an element here.\n *\n * For example:\n * ```gjs\n * <Popover as |p|>\n * <p.Content @as=\"dialog\">\n * this is now focus trapped\n * </p.Content>\n * </Popover>\n * ```\n */\n as?: string;\n };\n Blocks: { default: [] };\n}> = <template>\n {{#let (element (getElementTag @as)) as |El|}}\n {{! @glint-ignore\n https://github.com/tildeio/ember-element-helper/issues/91\n https://github.com/typed-ember/glint/issues/610\n }}\n <El popover=\"manual\" {{showPopover}} {{@floating}} ...attributes>\n {{yield}}\n </El>\n {{/let}}\n</template>;\n\ninterface AttachArrowSignature {\n Element: HTMLElement;\n Args: {\n Named: {\n arrowElement: ReturnType<typeof ArrowElement>;\n data:\n | undefined\n | {\n placement: string;\n middlewareData?: {\n arrow?: { x?: number; y?: number };\n };\n };\n };\n };\n}\n\nconst arrowSides = {\n top: \"bottom\",\n right: \"left\",\n bottom: \"top\",\n left: \"right\",\n};\n\ntype Direction = \"top\" | \"bottom\" | \"left\" | \"right\";\ntype Placement = `${Direction}${\"\" | \"-start\" | \"-end\"}`;\n\nconst attachArrow: ModifierLike<AttachArrowSignature> = eModifier<AttachArrowSignature>(\n (element, _: [], named) => {\n if (element === named.arrowElement.current) {\n if (!named.data) return;\n if (!named.data.middlewareData) return;\n\n const { arrow } = named.data.middlewareData;\n const { placement } = named.data;\n\n if (!arrow) return;\n if (!placement) return;\n\n const { x: arrowX, y: arrowY } = arrow;\n const otherSide = (placement as Placement).split(\"-\")[0] as Direction;\n const staticSide = arrowSides[otherSide];\n\n Object.assign(named.arrowElement.current.style, {\n left: arrowX != null ? `${arrowX}px` : \"\",\n top: arrowY != null ? `${arrowY}px` : \"\",\n right: \"\",\n bottom: \"\",\n [staticSide]: \"-4px\",\n });\n\n return;\n }\n\n void (async () => {\n await Promise.resolve();\n named.arrowElement.set(element);\n })();\n },\n);\n\nconst ArrowElement: () => ReturnType<typeof cell<HTMLElement>> = () => cell<HTMLElement>();\n\nfunction maybeAddArrow(middleware: Middleware[] | undefined, element: Element | undefined) {\n const result = [...(middleware || [])];\n\n if (element) {\n result.push(arrow({ element }));\n }\n\n return result;\n}\n\nfunction flipOptions(options: HookSignature[\"Args\"][\"Named\"][\"flipOptions\"]) {\n return {\n elementContext: \"reference\" as ElementContext,\n ...options,\n };\n}\n\nexport const Popover: TOC<Signature> = <template>\n {{#let (ArrowElement) as |arrowElement|}}\n <FloatingUI\n @placement={{@placement}}\n @strategy={{@strategy}}\n @middleware={{maybeAddArrow @middleware arrowElement.current}}\n @flipOptions={{flipOptions @flipOptions}}\n @shiftOptions={{@shiftOptions}}\n @offsetOptions={{@offsetOptions}}\n as |reference floating extra|\n >\n {{#let (modifier attachArrow arrowElement=arrowElement data=extra.data) as |arrow|}}\n {{yield\n (hash\n reference=reference\n setReference=extra.setReference\n Content=(component Content floating=floating)\n data=extra.data\n arrow=arrow\n )\n }}\n {{/let}}\n </FloatingUI>\n {{/let}}\n</template>;\n\nexport default Popover;\n"],"names":["showPopover","eModifier","element","el","style","setProperty","parentElement","closest","removeAttribute","HTMLDialogElement","setAttribute","hidePopover","getElementTag","tagName","Content","setComponentTemplate","precompileTemplate","strictMode","scope","templateOnly","arrowSides","top","right","bottom","left","attachArrow","_","named","arrowElement","current","data","middlewareData","arrow","placement","x","arrowX","y","arrowY","otherSide","split","staticSide","Object","assign","Promise","resolve","set","ArrowElement","cell","maybeAddArrow","middleware","result","push","flipOptions","options","elementContext","Popover","FloatingUI","hash"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ export { Dialog, Dialog as Modal } from './components/dialog.js';
6
6
  export { Drawer } from './components/drawer.js';
7
7
  export { ExternalLink } from './components/external-link.js';
8
8
  export { Form } from './components/form.js';
9
+ export { IncrementalEach } from './components/incremental-each.js';
9
10
  export { Key, KeyCombo } from './components/keys.js';
10
11
  export { StickyFooter } from './components/layout/sticky-footer.js';
11
12
  export { Link } from './components/link.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * DANGER: this is a *barrel file*\n *\n * It forces the whole library to be loaded and all dependencies.\n *\n * If you have a small app, you probably don't want to import from here -- instead import from each sub-path.\n */\nimport { importSync, isDevelopingApp, macroCondition } from '@embroider/macros';\n\nif (macroCondition(isDevelopingApp())) {\n importSync('./components/violations.css');\n}\n\nexport { Accordion } from './components/accordion.gts';\nexport type {\n AccordionContentExternalSignature,\n AccordionHeaderExternalSignature,\n AccordionItemExternalSignature,\n AccordionTriggerExternalSignature,\n} from './components/accordion/public.ts';\nexport { Avatar } from './components/avatar.gts';\nexport { Breadcrumb } from './components/breadcrumb.gts';\nexport { Dialog, Dialog as Modal } from './components/dialog.gts';\nexport { Drawer } from './components/drawer.gts';\nexport { ExternalLink } from './components/external-link.gts';\nexport { Form } from './components/form.gts';\nexport { Key, KeyCombo } from './components/keys.gts';\nexport { StickyFooter } from './components/layout/sticky-footer.gts';\nexport { Link } from './components/link.gts';\nexport { Menu } from './components/menu.gts';\nexport { OTP, OTPInput } from './components/one-time-password.gts';\nexport { Popover } from './components/popover.gts';\nexport { Portal } from './components/portal.gts';\nexport { PortalTargets } from './components/portal-targets.gts';\nexport { TARGETS as PORTALS } from './components/portal-targets.gts';\nexport { Progress } from './components/progress.gts';\nexport { Rating } from './components/rating.gts';\nexport { Scroller } from './components/scroller.gts';\nexport { Separator } from './components/separator.gts';\nexport { Shadowed } from './components/shadowed.gts';\nexport { Switch } from './components/switch.gts';\nexport { Toggle } from './components/toggle.gts';\nexport { ToggleGroup } from './components/toggle-group.gts';\nexport { VisuallyHidden } from './components/visually-hidden.gts';\nexport { Zoetrope } from './components/zoetrope.ts';\nexport * from './helpers.ts';\n"],"names":["macroCondition","isDevelopingApp","importSync"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AAGA,IAAIA,cAAc,CAACC,eAAe,EAAE,CAAC,EAAE;EACrCC,UAAU,CAAC,6BAA6B,CAAC;AAC3C"}
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * DANGER: this is a *barrel file*\n *\n * It forces the whole library to be loaded and all dependencies.\n *\n * If you have a small app, you probably don't want to import from here -- instead import from each sub-path.\n */\nimport { importSync, isDevelopingApp, macroCondition } from '@embroider/macros';\n\nif (macroCondition(isDevelopingApp())) {\n importSync('./components/violations.css');\n}\n\nexport { Accordion } from './components/accordion.gts';\nexport type {\n AccordionContentExternalSignature,\n AccordionHeaderExternalSignature,\n AccordionItemExternalSignature,\n AccordionTriggerExternalSignature,\n} from './components/accordion/public.ts';\nexport { Avatar } from './components/avatar.gts';\nexport { Breadcrumb } from './components/breadcrumb.gts';\nexport { Dialog, Dialog as Modal } from './components/dialog.gts';\nexport { Drawer } from './components/drawer.gts';\nexport { ExternalLink } from './components/external-link.gts';\nexport { Form } from './components/form.gts';\nexport { IncrementalEach } from './components/incremental-each.gts';\nexport { Key, KeyCombo } from './components/keys.gts';\nexport { StickyFooter } from './components/layout/sticky-footer.gts';\nexport { Link } from './components/link.gts';\nexport { Menu } from './components/menu.gts';\nexport { OTP, OTPInput } from './components/one-time-password.gts';\nexport { Popover } from './components/popover.gts';\nexport { Portal } from './components/portal.gts';\nexport { PortalTargets } from './components/portal-targets.gts';\nexport { TARGETS as PORTALS } from './components/portal-targets.gts';\nexport { Progress } from './components/progress.gts';\nexport { Rating } from './components/rating.gts';\nexport { Scroller } from './components/scroller.gts';\nexport { Separator } from './components/separator.gts';\nexport { Shadowed } from './components/shadowed.gts';\nexport { Switch } from './components/switch.gts';\nexport { Toggle } from './components/toggle.gts';\nexport { ToggleGroup } from './components/toggle-group.gts';\nexport { VisuallyHidden } from './components/visually-hidden.gts';\nexport { Zoetrope } from './components/zoetrope.ts';\nexport * from './helpers.ts';\n"],"names":["macroCondition","isDevelopingApp","importSync"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AAGA,IAAIA,cAAc,CAACC,eAAe,EAAE,CAAC,EAAE;EACrCC,UAAU,CAAC,6BAA6B,CAAC;AAC3C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember-primitives",
3
- "version": "0.57.0",
3
+ "version": "0.59.0",
4
4
  "description": "Making apps easier to build",
5
5
  "keywords": [
6
6
  "ember-addon"
@@ -0,0 +1,292 @@
1
+ import Component from "@glimmer/component";
2
+ import { cached } from "@glimmer/tracking";
3
+ import { assert } from "@ember/debug";
4
+ import { isDestroyed, isDestroying, registerDestructor } from "@ember/destroyable";
5
+ import { buildWaiter } from "@ember/test-waiters";
6
+
7
+ import { cell } from "ember-resources";
8
+
9
+ import type Owner from "@ember/owner";
10
+
11
+ const DEFAULT_BATCH_SIZE = 50;
12
+ const DEFAULT_INITIAL = "sync";
13
+
14
+ const waiter = buildWaiter("ember-primitives:incremental-each");
15
+
16
+ function chunk<T>(arr: readonly T[], size: number): T[][] {
17
+ const out: T[][] = [];
18
+
19
+ for (let i = 0; i < arr.length; i += size) {
20
+ out.push(arr.slice(i, i + size));
21
+ }
22
+
23
+ return out;
24
+ }
25
+
26
+ export interface Signature<T = unknown> {
27
+ Args: {
28
+ /**
29
+ * The collection of items to render.
30
+ *
31
+ * Replacing the array (new identity) restarts rendering from the
32
+ * first batch.
33
+ *
34
+ * ```gjs
35
+ * import { IncrementalEach } from 'ember-primitives';
36
+ *
37
+ * <template>
38
+ * <IncrementalEach @items={{this.rows}} as |row|>
39
+ * <my-row @row={{row}} />
40
+ * </IncrementalEach>
41
+ * </template>
42
+ * ```
43
+ */
44
+ items: readonly T[];
45
+
46
+ /**
47
+ * How many items to add per animation frame.
48
+ *
49
+ * Larger batches add more items per chunk; smaller batches yield to
50
+ * the browser more often.
51
+ *
52
+ * Default: 50. Must be positive; `0` or less asserts in development.
53
+ *
54
+ * ```gjs
55
+ * import { IncrementalEach } from 'ember-primitives';
56
+ *
57
+ * <template>
58
+ * <IncrementalEach @items={{this.rows}} @batchSize={{100}} as |row|>
59
+ * <my-row @row={{row}} />
60
+ * </IncrementalEach>
61
+ * </template>
62
+ * ```
63
+ */
64
+ batchSize?: number;
65
+
66
+ /**
67
+ * Controls how the initial batch is committed.
68
+ *
69
+ * - `"sync"` (default): the first `@batchSize` items render in the
70
+ * same render pass as mount / `@items` change. The user sees
71
+ * content on the very first paint, and the rest of the list
72
+ * fills in one batch per animation frame. This is the right
73
+ * default for most lists — even a perceived "empty for one
74
+ * frame" is worse than rendering a few extra items synchronously.
75
+ * - `"lazy"`: even the first batch waits for an animation frame, so
76
+ * the initial paint is empty and content arrives one batch per
77
+ * frame. Use this when the first batch itself would be expensive
78
+ * enough to block the first paint, and you'd rather show an
79
+ * empty container than delay it.
80
+ *
81
+ * Default: `"sync"`.
82
+ *
83
+ * ```gjs
84
+ * import { IncrementalEach } from 'ember-primitives';
85
+ *
86
+ * <template>
87
+ * <IncrementalEach @items={{this.rows}} @initial="lazy" as |row|>
88
+ * <my-row @row={{row}} />
89
+ * </IncrementalEach>
90
+ * </template>
91
+ * ```
92
+ */
93
+ initial?: "sync" | "lazy";
94
+
95
+ /**
96
+ * Called once with no arguments when every item in `@items` has
97
+ * been committed to the DOM. Fires after the final batch lands;
98
+ * does not fire on intermediate batches.
99
+ *
100
+ * Fires again on a fresh swap (new `@items` identity) once that
101
+ * new collection finishes rendering. An empty `@items` array
102
+ * does not fire the callback.
103
+ *
104
+ * Useful for marking the list as ready for screenshot tests,
105
+ * dismissing a loading indicator, or measuring how long the
106
+ * whole render took.
107
+ *
108
+ * ```gjs
109
+ * import { IncrementalEach } from 'ember-primitives';
110
+ *
111
+ * <template>
112
+ * <IncrementalEach @items={{this.rows}} @onDone={{this.handleDone}} as |row|>
113
+ * <my-row @row={{row}} />
114
+ * </IncrementalEach>
115
+ * </template>
116
+ * ```
117
+ */
118
+ onDone?: () => void;
119
+ };
120
+ Blocks: {
121
+ /**
122
+ * Yielded for each rendered item, with the index in the original
123
+ * `@items` array.
124
+ *
125
+ * ```gjs
126
+ * import { IncrementalEach } from 'ember-primitives';
127
+ *
128
+ * <template>
129
+ * <IncrementalEach @items={{this.rows}} as |row index|>
130
+ * {{index}}: {{row.label}}
131
+ * </IncrementalEach>
132
+ * </template>
133
+ * ```
134
+ */
135
+ default: [item: T, index: number];
136
+ };
137
+ }
138
+
139
+ /**
140
+ * A drop-in replacement for `{{#each}}` that renders a large collection
141
+ * a batch at a time on each animation frame, instead of all at once.
142
+ *
143
+ * Every item ends up in the DOM, so browser find (Ctrl+F / Cmd+F), anchor
144
+ * links, screen readers, print, and SEO all work against the full list.
145
+ * Yielding the main thread between batches keeps the page responsive while
146
+ * the rest of the list is filling in.
147
+ *
148
+ * By default the first batch lands synchronously, so the user sees content
149
+ * on the very first paint. Pass `@initial="lazy"` to defer the first batch
150
+ * to an animation frame as well.
151
+ *
152
+ * Intended for non-scrollable containers, or anywhere a virtual/windowed
153
+ * list does not apply (variable item heights, lists that grow the page,
154
+ * surfaces that need every row indexable).
155
+ *
156
+ * Do not nest one `<IncrementalEach>` inside another. Each level adds an
157
+ * animation-frame delay before its content paints; nesting compounds those
158
+ * delays, so inner rows appear to flicker in with missing sub-content.
159
+ * If you have nested loops, only the outermost one should be
160
+ * `<IncrementalEach>`; leave deeper loops as plain `{{#each}}`.
161
+ *
162
+ * @example
163
+ * ```gjs
164
+ * import { IncrementalEach } from 'ember-primitives';
165
+ *
166
+ * <template>
167
+ * <ul>
168
+ * <IncrementalEach @items={{this.rows}} @batchSize={{100}} as |row index|>
169
+ * <li>{{index}}: {{row.label}}</li>
170
+ * </IncrementalEach>
171
+ * </ul>
172
+ * </template>
173
+ * ```
174
+ */
175
+ export class IncrementalEach<T = unknown> extends Component<Signature<T>> {
176
+ #count = cell(0);
177
+ #itemsRef: readonly T[] | null = null;
178
+ #waiterToken: unknown = null;
179
+ #doneFor: object | null = null;
180
+
181
+ constructor(owner: Owner, args: Signature<T>["Args"]) {
182
+ super(owner, args);
183
+
184
+ registerDestructor(this, () => this.#endWaiter());
185
+ }
186
+
187
+ // Reset progress and (re)open the test-waiter when `@items` identity
188
+ // changes, so a swap restarts at the first batch, `@onDone` can fire
189
+ // again for the new collection, and `await settled()` knows to wait
190
+ // until `checkDone` closes the waiter. Mutating from a getter is safe
191
+ // here because the writes happen before any consumer reads them in
192
+ // the same render pass.
193
+ /* eslint-disable ember/no-side-effects */
194
+ get #items(): readonly T[] {
195
+ const items = this.args.items;
196
+
197
+ assert(`@items must be an array`, items);
198
+
199
+ if (items !== this.#itemsRef) {
200
+ this.#itemsRef = items;
201
+ this.#count.current = 0;
202
+ this.#endWaiter();
203
+
204
+ if (items.length > 0) {
205
+ this.#waiterToken = waiter.beginAsync();
206
+ }
207
+ }
208
+
209
+ return items;
210
+ }
211
+ /* eslint-enable ember/no-side-effects */
212
+
213
+ // `"sync"` keeps bucket 0 visible at count=0 (`i = 0 >= 0`); `"lazy"`
214
+ // starts one step behind so even bucket 0 needs a tick.
215
+ get #start() {
216
+ return this.#initial === "sync" ? 0 : -1;
217
+ }
218
+
219
+ get i() {
220
+ return this.#start + this.#count.current;
221
+ }
222
+
223
+ @cached
224
+ get bucketed() {
225
+ const size = this.#batchSize;
226
+
227
+ return chunk(this.#items, size).map((items, b) => {
228
+ const start = b * size;
229
+
230
+ return {
231
+ isReady: () => this.i >= b,
232
+ items: items.map((value, j) => ({ value, index: start + j })),
233
+ };
234
+ });
235
+ }
236
+
237
+ get #batchSize(): number {
238
+ const requested = this.args.batchSize ?? DEFAULT_BATCH_SIZE;
239
+
240
+ assert(
241
+ `<IncrementalEach> @batchSize must be a positive number, got ${requested}`,
242
+ requested > 0,
243
+ );
244
+
245
+ return requested;
246
+ }
247
+
248
+ get #initial(): "sync" | "lazy" {
249
+ const requested = this.args.initial ?? DEFAULT_INITIAL;
250
+
251
+ assert(
252
+ `<IncrementalEach> @initial must be "sync" or "lazy", got ${requested}`,
253
+ requested === "sync" || requested === "lazy",
254
+ );
255
+
256
+ return requested;
257
+ }
258
+
259
+ // `#items` is read before `#count` so the count-reset inside `#items`
260
+ // (on `@items` swap) lands before this read of count this render —
261
+ // otherwise tracked-value backtracking asserts.
262
+ tick = () => {
263
+ if (this.#items.length > this.#count.current) {
264
+ requestIdleCallback(() => this.#count.current++, { timeout: 10 });
265
+ }
266
+ };
267
+
268
+ checkDone = () => {
269
+ const bucketed = this.bucketed;
270
+
271
+ if (this.#doneFor === bucketed) return;
272
+ if (this.i < bucketed.length - 1) return;
273
+
274
+ this.#doneFor = bucketed;
275
+ queueMicrotask(() => {
276
+ if (isDestroyed(this) || isDestroying(this)) return;
277
+ this.args.onDone?.();
278
+ this.#endWaiter();
279
+ });
280
+ };
281
+
282
+ #endWaiter() {
283
+ if (this.#waiterToken) waiter.endAsync(this.#waiterToken);
284
+ }
285
+
286
+ <template>
287
+ {{(this.tick)}}{{#each this.bucketed as |bucket|}}{{#if (bucket.isReady)}}{{#each
288
+ bucket.items
289
+ as |entry|
290
+ }}{{yield entry.value entry.index}}{{/each}}{{(this.checkDone)}}{{/if}}{{/each}}
291
+ </template>
292
+ }
@@ -7,7 +7,7 @@ import { cell } from "ember-resources";
7
7
 
8
8
  import { FloatingUI } from "../floating-ui.ts";
9
9
 
10
- import type { Signature as FloatingUiComponentSignature } from "../floating-ui/component.ts";
10
+ import type { Signature as FloatingUiComponentSignature } from "../floating-ui/component.gts";
11
11
  import type { Signature as HookSignature } from "../floating-ui/modifier.ts";
12
12
  import type { TOC } from "@ember/component/template-only";
13
13
  import type { ElementContext, Middleware } from "@floating-ui/dom";
package/src/index.ts CHANGED
@@ -24,6 +24,7 @@ export { Dialog, Dialog as Modal } from './components/dialog.gts';
24
24
  export { Drawer } from './components/drawer.gts';
25
25
  export { ExternalLink } from './components/external-link.gts';
26
26
  export { Form } from './components/form.gts';
27
+ export { IncrementalEach } from './components/incremental-each.gts';
27
28
  export { Key, KeyCombo } from './components/keys.gts';
28
29
  export { StickyFooter } from './components/layout/sticky-footer.gts';
29
30
  export { Link } from './components/link.gts';
@@ -2,21 +2,21 @@
2
2
  // Add all your components, helpers and modifiers to the template registry here, so apps don't have to do this.
3
3
  // See https://typed-ember.gitbook.io/glint/using-glint/ember/authoring-addons
4
4
 
5
- import type { Accordion } from './components/accordion';
6
- import type { AccordionContent } from './components/accordion/content';
7
- import type { AccordionHeader } from './components/accordion/header';
8
- import type { AccordionItem } from './components/accordion/item';
9
- import type { AccordionTrigger } from './components/accordion/trigger';
10
- import type { Dialog } from './components/dialog';
11
- import type { ExternalLink } from './components/external-link';
12
- import type { Link } from './components/link';
13
- import type { Popover } from './components/popover';
14
- import type { Portal } from './components/portal';
15
- import type { PortalTargets } from './components/portal-targets';
16
- import type { Shadowed } from './components/shadowed';
17
- import type { Switch } from './components/switch';
18
- import type { Toggle } from './components/toggle';
19
- import type { service } from './helpers/service';
5
+ import type { Accordion } from './components/accordion.gts';
6
+ import type { AccordionContent } from './components/accordion/content.gts';
7
+ import type { AccordionHeader } from './components/accordion/header.gts';
8
+ import type { AccordionItem } from './components/accordion/item.gts';
9
+ import type { AccordionTrigger } from './components/accordion/trigger.gts';
10
+ import type { Dialog } from './components/dialog.gts';
11
+ import type { ExternalLink } from './components/external-link.gts';
12
+ import type { Link } from './components/link.gts';
13
+ import type { Popover } from './components/popover.gts';
14
+ import type { Portal } from './components/portal.gts';
15
+ import type { PortalTargets } from './components/portal-targets.gts';
16
+ import type { Shadowed } from './components/shadowed.gts';
17
+ import type { Switch } from './components/switch.gts';
18
+ import type { Toggle } from './components/toggle.gts';
19
+ import type { service } from './helpers/service.ts';
20
20
 
21
21
  // import type MyComponent from './components/my-component';
22
22