ember-primitives 0.49.0 → 0.50.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 (103) hide show
  1. package/bin/index.mjs +271 -0
  2. package/declarations/components/rating/public-types.d.ts +0 -4
  3. package/declarations/components/rating/public-types.d.ts.map +1 -1
  4. package/declarations/components/rating/rating.d.ts +9 -1
  5. package/declarations/components/rating/rating.d.ts.map +1 -1
  6. package/declarations/components/rating/stars.d.ts.map +1 -1
  7. package/declarations/components/rating/state.d.ts +4 -0
  8. package/declarations/components/rating/state.d.ts.map +1 -1
  9. package/declarations/components/rating/utils.d.ts +0 -1
  10. package/declarations/components/rating/utils.d.ts.map +1 -1
  11. package/dist/components/rating.js +1 -1
  12. package/dist/index.js +1 -1
  13. package/dist/{rating-CjBVsX6q.js → rating-BrIiwDLw.js} +21 -17
  14. package/dist/rating-BrIiwDLw.js.map +1 -0
  15. package/package.json +6 -2
  16. package/src/-private.ts +4 -0
  17. package/src/color-scheme.ts +165 -0
  18. package/src/components/-private/typed-elements.gts +13 -0
  19. package/src/components/-private/utils.ts +16 -0
  20. package/src/components/accordion/content.gts +34 -0
  21. package/src/components/accordion/header.gts +36 -0
  22. package/src/components/accordion/item.gts +55 -0
  23. package/src/components/accordion/public.ts +64 -0
  24. package/src/components/accordion/trigger.gts +32 -0
  25. package/src/components/accordion.gts +195 -0
  26. package/src/components/avatar.gts +108 -0
  27. package/src/components/dialog.gts +234 -0
  28. package/src/components/external-link.gts +14 -0
  29. package/src/components/form.gts +75 -0
  30. package/src/components/heading.gts +36 -0
  31. package/src/components/keys.gts +53 -0
  32. package/src/components/layout/hero.css +5 -0
  33. package/src/components/layout/hero.gts +17 -0
  34. package/src/components/layout/sticky-footer.css +9 -0
  35. package/src/components/layout/sticky-footer.gts +40 -0
  36. package/src/components/link.gts +172 -0
  37. package/src/components/menu.gts +373 -0
  38. package/src/components/one-time-password/buttons.gts +31 -0
  39. package/src/components/one-time-password/input.gts +198 -0
  40. package/src/components/one-time-password/otp.gts +130 -0
  41. package/src/components/one-time-password/utils.ts +201 -0
  42. package/src/components/one-time-password.gts +2 -0
  43. package/src/components/popover.gts +248 -0
  44. package/src/components/portal-targets.gts +136 -0
  45. package/src/components/portal.gts +194 -0
  46. package/src/components/progress.gts +154 -0
  47. package/src/components/rating/public-types.ts +44 -0
  48. package/src/components/rating/range.gts +22 -0
  49. package/src/components/rating/rating.gts +228 -0
  50. package/src/components/rating/stars.gts +60 -0
  51. package/src/components/rating/state.gts +144 -0
  52. package/src/components/rating/utils.ts +7 -0
  53. package/src/components/rating.gts +5 -0
  54. package/src/components/scroller.gts +179 -0
  55. package/src/components/shadowed.gts +110 -0
  56. package/src/components/switch.gts +103 -0
  57. package/src/components/tabs.gts +519 -0
  58. package/src/components/toggle-group.gts +265 -0
  59. package/src/components/toggle.gts +81 -0
  60. package/src/components/violations.css +105 -0
  61. package/src/components/violations.css.ts +1 -0
  62. package/src/components/visually-hidden.css +14 -0
  63. package/src/components/visually-hidden.gts +15 -0
  64. package/src/components/zoetrope/index.gts +358 -0
  65. package/src/components/zoetrope/styles.css +40 -0
  66. package/src/components/zoetrope/types.ts +65 -0
  67. package/src/components/zoetrope.ts +3 -0
  68. package/src/dom-context.gts +245 -0
  69. package/src/floating-ui/component.gts +186 -0
  70. package/src/floating-ui/middleware.ts +13 -0
  71. package/src/floating-ui/modifier.ts +183 -0
  72. package/src/floating-ui.ts +2 -0
  73. package/src/head.gts +37 -0
  74. package/src/helpers/body-class.ts +94 -0
  75. package/src/helpers/link.ts +125 -0
  76. package/src/helpers/service.ts +25 -0
  77. package/src/helpers.ts +2 -0
  78. package/src/iframe.ts +31 -0
  79. package/src/index.ts +43 -0
  80. package/src/load.gts +77 -0
  81. package/src/narrowing.ts +7 -0
  82. package/src/on-resize.ts +64 -0
  83. package/src/proper-links.ts +140 -0
  84. package/src/qp.ts +107 -0
  85. package/src/resize-observer.ts +132 -0
  86. package/src/service.ts +103 -0
  87. package/src/store.ts +72 -0
  88. package/src/styles.css.ts +5 -0
  89. package/src/tabster.ts +54 -0
  90. package/src/template-registry.ts +44 -0
  91. package/src/test-support/a11y.ts +50 -0
  92. package/src/test-support/dom.ts +112 -0
  93. package/src/test-support/otp.ts +64 -0
  94. package/src/test-support/rating.ts +144 -0
  95. package/src/test-support/routing.ts +62 -0
  96. package/src/test-support/zoetrope.ts +51 -0
  97. package/src/test-support.gts +6 -0
  98. package/src/type-utils.ts +1 -0
  99. package/src/utils.ts +75 -0
  100. package/src/viewport/in-viewport.gts +128 -0
  101. package/src/viewport/viewport.ts +122 -0
  102. package/src/viewport.ts +2 -0
  103. package/dist/rating-CjBVsX6q.js.map +0 -1
@@ -0,0 +1,358 @@
1
+ import "./styles.css";
2
+
3
+ import Component from "@glimmer/component";
4
+ import { tracked } from "@glimmer/tracking";
5
+ import { hash } from "@ember/helper";
6
+ import { on } from "@ember/modifier";
7
+ import { buildWaiter, waitForPromise } from "@ember/test-waiters";
8
+ import { isTesting, macroCondition } from "@embroider/macros";
9
+
10
+ import { modifier } from "ember-modifier";
11
+
12
+ import type { ScrollBehavior, Signature } from "./types.ts";
13
+
14
+ const testWaiter = buildWaiter("ember-primitive:zoetrope-waiter");
15
+ const DEFAULT_GAP = 8;
16
+ const DEFAULT_OFFSET = 0;
17
+
18
+ export class Zoetrope extends Component<Signature> {
19
+ @tracked scrollerElement: HTMLElement | null = null;
20
+ @tracked currentlyScrolled = 0;
21
+ @tracked scrollWidth = 0;
22
+ @tracked offsetWidth = 0;
23
+
24
+ private setCSSVariables = modifier(
25
+ (element: HTMLElement, _: unknown, { gap, offset }: { gap: number; offset: number }) => {
26
+ if (gap) element.style.setProperty("--zoetrope-gap", `${gap}px`);
27
+ if (offset) element.style.setProperty("--zoetrope-offset", `${offset}px`);
28
+ },
29
+ );
30
+
31
+ scrollerWaiter = testWaiter.beginAsync();
32
+ noScrollWaiter = () => {
33
+ testWaiter.endAsync(this.scrollerWaiter);
34
+ };
35
+
36
+ private configureScroller = modifier((element: HTMLElement) => {
37
+ this.scrollerElement = element;
38
+ this.currentlyScrolled = element.scrollLeft;
39
+
40
+ const zoetropeResizeObserver = new ResizeObserver(() => {
41
+ this.scrollWidth = element.scrollWidth;
42
+ this.offsetWidth = element.offsetWidth;
43
+ });
44
+
45
+ zoetropeResizeObserver.observe(element);
46
+
47
+ element.addEventListener("scroll", this.scrollListener, { passive: true });
48
+ element.addEventListener("keydown", this.tabListener);
49
+
50
+ requestAnimationFrame(() => {
51
+ testWaiter.endAsync(this.scrollerWaiter);
52
+ });
53
+
54
+ return () => {
55
+ element.removeEventListener("scroll", this.scrollListener);
56
+ element.removeEventListener("keydown", this.tabListener);
57
+
58
+ zoetropeResizeObserver.unobserve(element);
59
+ };
60
+ });
61
+
62
+ private tabListener = (event: KeyboardEvent) => {
63
+ const target = event.target as HTMLElement;
64
+ const { key, shiftKey } = event;
65
+
66
+ if (!this.scrollerElement || this.scrollerElement === target) {
67
+ return;
68
+ }
69
+
70
+ if (key !== "Tab") {
71
+ return;
72
+ }
73
+
74
+ const nextElement = target.nextElementSibling;
75
+ const previousElement = target.previousElementSibling;
76
+
77
+ if ((!shiftKey && !nextElement) || (shiftKey && !previousElement)) {
78
+ return;
79
+ }
80
+
81
+ event.preventDefault();
82
+
83
+ let newTarget: HTMLElement | null = null;
84
+
85
+ if (shiftKey) {
86
+ newTarget = previousElement as HTMLElement;
87
+ } else {
88
+ newTarget = nextElement as HTMLElement;
89
+ }
90
+
91
+ if (!newTarget) {
92
+ return;
93
+ }
94
+
95
+ newTarget?.focus({ preventScroll: true });
96
+
97
+ const rect = getRelativeBoundingClientRect(newTarget, this.scrollerElement);
98
+
99
+ this.scrollerElement?.scrollBy({
100
+ left: rect.left,
101
+ behavior: this.scrollBehavior,
102
+ });
103
+ };
104
+
105
+ private scrollListener = () => {
106
+ this.currentlyScrolled = this.scrollerElement?.scrollLeft || 0;
107
+ };
108
+
109
+ get offset() {
110
+ return this.args.offset ?? DEFAULT_OFFSET;
111
+ }
112
+
113
+ get gap() {
114
+ return this.args.gap ?? DEFAULT_GAP;
115
+ }
116
+
117
+ get canScroll() {
118
+ return this.scrollWidth > this.offsetWidth + this.offset;
119
+ }
120
+
121
+ get cannotScrollLeft() {
122
+ return this.currentlyScrolled <= this.offset;
123
+ }
124
+
125
+ get cannotScrollRight() {
126
+ return this.scrollWidth - this.offsetWidth - this.offset < this.currentlyScrolled;
127
+ }
128
+
129
+ get scrollBehavior(): ScrollBehavior {
130
+ if (macroCondition(isTesting())) {
131
+ return "instant";
132
+ }
133
+
134
+ return this.args.scrollBehavior || "smooth";
135
+ }
136
+
137
+ scrollLeft = () => {
138
+ if (!(this.scrollerElement instanceof HTMLElement)) {
139
+ return;
140
+ }
141
+
142
+ const { firstChild } = this.findOverflowingElement();
143
+
144
+ if (!firstChild) {
145
+ return;
146
+ }
147
+
148
+ const children = [...this.scrollerElement.children];
149
+
150
+ const firstChildIndex = children.indexOf(firstChild);
151
+
152
+ let targetElement = firstChild;
153
+ let accumalatedWidth = 0;
154
+
155
+ for (let i = firstChildIndex; i >= 0; i--) {
156
+ const child = children[i];
157
+
158
+ if (!(child instanceof HTMLElement)) {
159
+ continue;
160
+ }
161
+
162
+ accumalatedWidth += child.offsetWidth + this.gap;
163
+
164
+ if (accumalatedWidth >= this.offsetWidth) {
165
+ break;
166
+ }
167
+
168
+ targetElement = child;
169
+ }
170
+
171
+ const rect = getRelativeBoundingClientRect(targetElement, this.scrollerElement);
172
+
173
+ this.scrollerElement.scrollBy({
174
+ left: rect.left,
175
+ behavior: this.scrollBehavior,
176
+ });
177
+
178
+ void waitForPromise(new Promise(requestAnimationFrame));
179
+ };
180
+
181
+ scrollRight = () => {
182
+ if (!(this.scrollerElement instanceof HTMLElement)) {
183
+ return;
184
+ }
185
+
186
+ const { activeSlide, lastChild } = this.findOverflowingElement();
187
+
188
+ if (!lastChild) {
189
+ return;
190
+ }
191
+
192
+ let rect = getRelativeBoundingClientRect(lastChild, this.scrollerElement);
193
+
194
+ // If the card is larger than the container then skip to the next card
195
+ if (rect.width > this.offsetWidth && activeSlide === lastChild) {
196
+ const children = [...this.scrollerElement.children];
197
+ const lastChildIndex = children.indexOf(lastChild);
198
+ const targetElement = children[lastChildIndex + 1];
199
+
200
+ if (!targetElement) {
201
+ return;
202
+ }
203
+
204
+ rect = getRelativeBoundingClientRect(targetElement, this.scrollerElement);
205
+ }
206
+
207
+ this.scrollerElement?.scrollBy({
208
+ left: rect.left,
209
+ behavior: this.scrollBehavior,
210
+ });
211
+
212
+ void waitForPromise(new Promise(requestAnimationFrame));
213
+ };
214
+
215
+ private findOverflowingElement() {
216
+ const returnObj: {
217
+ activeSlide?: Element;
218
+ firstChild?: Element;
219
+ lastChild?: Element;
220
+ } = {
221
+ firstChild: undefined,
222
+ lastChild: undefined,
223
+ activeSlide: undefined,
224
+ };
225
+
226
+ if (!this.scrollerElement) {
227
+ return returnObj;
228
+ }
229
+
230
+ const parentElement = this.scrollerElement.parentElement;
231
+
232
+ if (!parentElement) {
233
+ return returnObj;
234
+ }
235
+
236
+ const containerRect = getRelativeBoundingClientRect(this.scrollerElement, parentElement);
237
+
238
+ const children = [...this.scrollerElement.children];
239
+
240
+ // Find the first child that is overflowing the left edge of the container
241
+ // and the last child that is overflowing the right edge of the container
242
+ for (const child of children) {
243
+ const rect = getRelativeBoundingClientRect(child, this.scrollerElement);
244
+
245
+ if (rect.right + this.gap >= containerRect.left && !returnObj.firstChild) {
246
+ returnObj.firstChild = child;
247
+ }
248
+
249
+ if (rect.left >= this.offset && !returnObj.activeSlide) {
250
+ returnObj.activeSlide = child;
251
+ }
252
+
253
+ if (rect.right >= containerRect.width && !returnObj.lastChild) {
254
+ returnObj.lastChild = child;
255
+
256
+ break;
257
+ }
258
+ }
259
+
260
+ if (!returnObj.firstChild) {
261
+ returnObj.firstChild = children[0];
262
+ }
263
+
264
+ if (!returnObj.lastChild) {
265
+ returnObj.lastChild = children[children.length - 1];
266
+ }
267
+
268
+ return returnObj;
269
+ }
270
+
271
+ <template>
272
+ <section
273
+ class="ember-primitives__zoetrope"
274
+ {{this.setCSSVariables gap=this.gap offset=this.offset}}
275
+ ...attributes
276
+ >
277
+ {{#if (has-block "header")}}
278
+ <div class="ember-primitives__zoetrope__header">
279
+ {{yield to="header"}}
280
+ </div>
281
+ {{/if}}
282
+
283
+ {{#if (has-block "controls")}}
284
+ {{yield
285
+ (hash
286
+ cannotScrollLeft=this.cannotScrollLeft
287
+ cannotScrollRight=this.cannotScrollRight
288
+ canScroll=this.canScroll
289
+ scrollLeft=this.scrollLeft
290
+ scrollRight=this.scrollRight
291
+ )
292
+ to="controls"
293
+ }}
294
+ {{else}}
295
+ {{#if this.canScroll}}
296
+ <div class="ember-primitives__zoetrope__controls">
297
+ <button
298
+ type="button"
299
+ {{on "click" this.scrollLeft}}
300
+ disabled={{this.cannotScrollLeft}}
301
+ >Left</button>
302
+
303
+ <button
304
+ type="button"
305
+ {{on "click" this.scrollRight}}
306
+ disabled={{this.cannotScrollRight}}
307
+ >Right</button>
308
+ </div>
309
+ {{/if}}
310
+ {{/if}}
311
+ {{#if (has-block "content")}}
312
+ <div class="ember-primitives__zoetrope__scroller" {{this.configureScroller}}>
313
+ {{yield to="content"}}
314
+ </div>
315
+ {{else}}
316
+ {{(this.noScrollWaiter)}}
317
+ {{/if}}
318
+ </section>
319
+ </template>
320
+ }
321
+
322
+ export default Zoetrope;
323
+
324
+ function getRelativeBoundingClientRect(childElement: Element, parentElement: Element) {
325
+ if (!childElement || !parentElement) {
326
+ throw new Error("Both childElement and parentElement must be provided");
327
+ }
328
+
329
+ // Get the bounding rect of the child and parent elements
330
+ const childRect = childElement.getBoundingClientRect();
331
+ const parentRect = parentElement.getBoundingClientRect();
332
+
333
+ // Get computed styles of the parent element
334
+ const parentStyles = window.getComputedStyle(parentElement);
335
+
336
+ // Extract and parse parent's padding, and border, for all sides
337
+ const parentPaddingTop = parseFloat(parentStyles.paddingTop);
338
+ const parentPaddingLeft = parseFloat(parentStyles.paddingLeft);
339
+
340
+ const parentBorderTopWidth = parseFloat(parentStyles.borderTopWidth);
341
+ const parentBorderLeftWidth = parseFloat(parentStyles.borderLeftWidth);
342
+
343
+ // Calculate child's position relative to parent's content area (including padding and borders)
344
+ return {
345
+ width: childRect.width,
346
+ height: childRect.height,
347
+ top: childRect.top - parentRect.top - parentBorderTopWidth - parentPaddingTop,
348
+ left: childRect.left - parentRect.left - parentBorderLeftWidth - parentPaddingLeft,
349
+ bottom:
350
+ childRect.top - parentRect.top - parentBorderTopWidth - parentPaddingTop + childRect.height,
351
+ right:
352
+ childRect.left -
353
+ parentRect.left -
354
+ parentBorderLeftWidth -
355
+ parentPaddingLeft +
356
+ childRect.width,
357
+ };
358
+ }
@@ -0,0 +1,40 @@
1
+ .ember-primitives__zoetrope {
2
+ display: flex;
3
+ flex-wrap: wrap;
4
+ position: relative;
5
+ width: 100%;
6
+ }
7
+
8
+ .ember-primitives__zoetrope__header {
9
+ align-items: center;
10
+ display: flex;
11
+ flex: 1;
12
+ justify-content: space-between;
13
+ padding-left: var(--zoetrope-offset, 0);
14
+ }
15
+
16
+ .ember-primitives__zoetrope__controls {
17
+ align-items: center;
18
+ display: flex;
19
+ padding-right: var(--zoetrope-offset, 0);
20
+ gap: 4px;
21
+ }
22
+
23
+ .ember-primitives__zoetrope__scroller {
24
+ display: flex;
25
+ flex-flow: row nowrap;
26
+ gap: var(--zoetrope-gap, 8px);
27
+ overflow: scroll visible;
28
+ padding: 8px var(--zoetrope-offset, 0);
29
+ scroll-behavior: smooth;
30
+ scroll-padding-left: var(--zoetrope-offset, 0);
31
+ scroll-snap-type: x mandatory;
32
+ scrollbar-color: transparent transparent;
33
+ scrollbar-width: none;
34
+ width: 100%;
35
+
36
+ & > * {
37
+ flex-shrink: 0;
38
+ scroll-snap-align: start;
39
+ }
40
+ }
@@ -0,0 +1,65 @@
1
+ export type ScrollBehavior = 'auto' | 'smooth' | 'instant';
2
+
3
+ export interface Signature {
4
+ Args: {
5
+ /**
6
+ * The distance in pixels between each item in the slider.
7
+ */
8
+ gap?: number;
9
+
10
+ /**
11
+ * The distance from the edge of the container to the first and last item, this allows
12
+ * the contents to visually overflow the container
13
+ */
14
+ offset?: number;
15
+
16
+ /**
17
+ * The scroll behavior to use when scrolling the slider. Defaults to smooth.
18
+ */
19
+ scrollBehavior?: ScrollBehavior;
20
+ };
21
+ Blocks: {
22
+ /**
23
+ * The header block is where the header content is placed.
24
+ */
25
+ header: [];
26
+
27
+ /**
28
+ * The content block is where the items that will be scrolled are placed.
29
+ */
30
+ content: [];
31
+
32
+ /**
33
+ * The controls block is where the left and right buttons are placed.
34
+ */
35
+ controls: [
36
+ {
37
+ /**
38
+ * Whether the slider can scroll.
39
+ */
40
+ canScroll: boolean;
41
+
42
+ /**
43
+ * Whether the slider cannot scroll left.
44
+ */
45
+ cannotScrollLeft: boolean;
46
+
47
+ /**
48
+ * Whether the slider cannot scroll right.
49
+ */
50
+ cannotScrollRight: boolean;
51
+
52
+ /**
53
+ * The function to scroll the slider left.
54
+ */
55
+ scrollLeft: () => void;
56
+
57
+ /**
58
+ * The function to scroll the slider right.
59
+ */
60
+ scrollRight: () => void;
61
+ },
62
+ ];
63
+ };
64
+ Element: HTMLElement;
65
+ }
@@ -0,0 +1,3 @@
1
+ export { Zoetrope } from './zoetrope/index.gts';
2
+ export { default } from './zoetrope/index.gts';
3
+ export type { Signature } from './zoetrope/types.ts';
@@ -0,0 +1,245 @@
1
+ import Component from "@glimmer/component";
2
+ import { cached, tracked } from "@glimmer/tracking";
3
+ import { assert } from "@ember/debug";
4
+
5
+ import { isElement } from "./narrowing.ts";
6
+ import { createStore } from "./store.ts";
7
+
8
+ import type { Newable } from "./type-utils";
9
+ import type Owner from "@ember/owner";
10
+
11
+ /**
12
+ * IMPLEMENTATION NOTE:
13
+ * we don't use https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md
14
+ * because it is not inherently reactive.
15
+ *
16
+ * Its *event* based, which opts you out of fine-grained reactivity.
17
+ * We want minimal effort fine-grained reactivity.
18
+ *
19
+ * This Technique follows the DOM tree, and is synchronous,
20
+ * allowing correct fine-grained signals-based reactivity.
21
+ *
22
+ * We *could* do less work to find Providers,
23
+ * but only if we forgoe DOM-tree scoping.
24
+ * We must traverse the DOM hierarchy to validate that we aren't accessing providers from different subtrees.
25
+ */
26
+ const LOOKUP = new WeakMap<Text | Element, [unknown, () => unknown]>();
27
+
28
+ export class Provide<Data extends object> extends Component<{
29
+ /**
30
+ * The Element is not customizable
31
+ * (and also sometimes doesn't exist (depending on the `@element` value))
32
+ */
33
+ Element: null;
34
+ Args: {
35
+ /**
36
+ * What data do you want to provide to the DOM subtree?
37
+ *
38
+ * If this is a function or class, it will be instantiated and given an
39
+ * owner + destroyable linkage via `createStore`
40
+ */
41
+ data: Data | (() => Data) | Newable<Data>;
42
+
43
+ /**
44
+ * Optionally, you may use string-based keys to reference the data in the Provide.
45
+ *
46
+ * This is not recommended though, because when using a class or other object-like structure,
47
+ * the type in the `<Consume>` component can be derived from that class or object-like structure.
48
+ * With string keys, the `<Consume>` type will be unknown.
49
+ */
50
+ key?: string;
51
+
52
+ /**
53
+ * Can be used to either customize the element tag ( defaults to div )
54
+ * If set to `false`, we won't use an element for the Provider boundary.
55
+ *
56
+ * Setting this to `false` changes the DOM Node containing the Provider's data to be a text node -- which can be useful when certain CSS situations are needed.
57
+ *
58
+ * But setting to `false` has a hazard: it allows subsequent sibling subtrees to access adjacent providers.
59
+ *
60
+ * There is no way around caveat in library land, and in a framework implementation of context,
61
+ * it can only be solved by having render-tree context implemented, and ignoring DOM
62
+ * (which then makes the only difference between DOM-Context and Context be whether or not
63
+ * the context punches through Portals)
64
+ */
65
+ element?: keyof HTMLElementTagNameMap | false | undefined;
66
+ };
67
+ Blocks: {
68
+ /**
69
+ * The content that this component will _provide_ data to the entire hierarchy.
70
+ */
71
+ default: [];
72
+ };
73
+ }> {
74
+ get data() {
75
+ assert(`@data is missing in <Provide>. Please pass @data.`, "data" in this.args);
76
+
77
+ /**
78
+ * This covers both classes and functions
79
+ */
80
+ if (typeof this.args.data === "function") {
81
+ return createStore<Data>(this, this.args.data);
82
+ }
83
+
84
+ /**
85
+ * Non-instantiable value
86
+ */
87
+ return this.args.data;
88
+ }
89
+
90
+ element: Text | HTMLElement;
91
+
92
+ constructor(
93
+ owner: Owner,
94
+ args: {
95
+ data: Data | (() => Data) | Newable<Data>;
96
+ key?: string;
97
+ },
98
+ ) {
99
+ super(owner, args);
100
+
101
+ assert(
102
+ `@element may only be \`false\` or a string (or undefined (default when not set))`,
103
+ this.args.element === undefined ||
104
+ this.args.element === false ||
105
+ typeof this.args.element === "string",
106
+ );
107
+
108
+ if (this.useElementProvider) {
109
+ this.element = document.createElement(this.args.element || "div");
110
+
111
+ // This tells the browser to ignore everything about this element when it comes to styling
112
+ this.element.style.display = "contents";
113
+ } else {
114
+ this.element = document.createTextNode("");
115
+ }
116
+
117
+ const key = this.args.key ?? this.args.data;
118
+
119
+ LOOKUP.set(this.element, [key, () => this.data]);
120
+ }
121
+
122
+ get useElementProvider() {
123
+ return this.args.element !== false;
124
+ }
125
+
126
+ <template>
127
+ {{#if (isElement this.element)}}
128
+ {{this.element}}
129
+
130
+ {{#in-element this.element}}
131
+ {{yield}}
132
+ {{/in-element}}
133
+
134
+ {{else}}
135
+ {{! NOTE! This type of provider will _allow_ non-descendents using the same key to find the provider and use it.
136
+
137
+ For example:
138
+ Provider
139
+ Consumer
140
+
141
+ Consumer (finds Provider)
142
+ }}
143
+
144
+ {{this.element}}
145
+ {{yield}}
146
+
147
+ {{/if}}
148
+ </template>
149
+ }
150
+
151
+ /**
152
+ * How this works:
153
+ * - starting at some deep node (Text, Element, whatever),
154
+ * start crawling up the ancenstry graph (of DOM Nodes).
155
+ *
156
+ * - This algo "tops out" (since we traverse upwards (otherwise this would be "bottoming out")) at the HTMLDocument (parent of the HTML Tag)
157
+ *
158
+ */
159
+ function findForKey<Data>(startElement: Text, key: string | object): undefined | (() => Data) {
160
+ let parent: ParentNode | Text | null | undefined = startElement;
161
+
162
+ while (parent) {
163
+ let target: ParentNode | ChildNode | Text | null | undefined = parent;
164
+
165
+ while (target) {
166
+ if (!(target instanceof Element) && !(target instanceof Text)) {
167
+ target = target?.previousSibling;
168
+ continue;
169
+ }
170
+
171
+ const maybe = LOOKUP.get(target);
172
+
173
+ target = target?.previousSibling;
174
+
175
+ if (!maybe) {
176
+ continue;
177
+ }
178
+
179
+ if (maybe[0] === key) {
180
+ return maybe[1] as () => Data;
181
+ }
182
+ }
183
+
184
+ parent = parent.parentElement;
185
+ }
186
+ }
187
+
188
+ type DataForKey<Key> = Key extends string
189
+ ? unknown
190
+ : Key extends Newable<infer T>
191
+ ? T
192
+ : Key extends () => infer T
193
+ ? T
194
+ : Key;
195
+
196
+ export class Consume<Key extends object | string> extends Component<{
197
+ Args: {
198
+ key: Key;
199
+ };
200
+ Blocks: {
201
+ default: [
202
+ context: {
203
+ data: DataForKey<Key>;
204
+ },
205
+ ];
206
+ };
207
+ }> {
208
+ // SAFETY: We do a runtime assert in the getter below.
209
+ @tracked getData!: () => DataForKey<Key>;
210
+
211
+ element: Text;
212
+
213
+ constructor(owner: Owner, args: { key: Key }) {
214
+ super(owner, args);
215
+
216
+ this.element = document.createTextNode("");
217
+ }
218
+
219
+ @cached
220
+ get context() {
221
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
222
+ const self = this;
223
+
224
+ return {
225
+ get data(): DataForKey<Key> {
226
+ const getData = findForKey<Key>(self.element, self.args.key);
227
+
228
+ assert(
229
+ `Could not find provided context in <Consume>. Please assure that there is a corresponding <Provide> component before using this <Consume> component`,
230
+ getData,
231
+ );
232
+
233
+ // SAFETY: return type handled by getter's signature
234
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
235
+ return getData() as any;
236
+ },
237
+ };
238
+ }
239
+
240
+ <template>
241
+ {{this.element}}
242
+
243
+ {{yield this.context}}
244
+ </template>
245
+ }