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.
- package/bin/index.mjs +271 -0
- package/declarations/components/rating/public-types.d.ts +0 -4
- package/declarations/components/rating/public-types.d.ts.map +1 -1
- package/declarations/components/rating/rating.d.ts +9 -1
- package/declarations/components/rating/rating.d.ts.map +1 -1
- package/declarations/components/rating/stars.d.ts.map +1 -1
- package/declarations/components/rating/state.d.ts +4 -0
- package/declarations/components/rating/state.d.ts.map +1 -1
- package/declarations/components/rating/utils.d.ts +0 -1
- package/declarations/components/rating/utils.d.ts.map +1 -1
- package/dist/components/rating.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{rating-CjBVsX6q.js → rating-BrIiwDLw.js} +21 -17
- package/dist/rating-BrIiwDLw.js.map +1 -0
- package/package.json +6 -2
- package/src/-private.ts +4 -0
- package/src/color-scheme.ts +165 -0
- package/src/components/-private/typed-elements.gts +13 -0
- package/src/components/-private/utils.ts +16 -0
- package/src/components/accordion/content.gts +34 -0
- package/src/components/accordion/header.gts +36 -0
- package/src/components/accordion/item.gts +55 -0
- package/src/components/accordion/public.ts +64 -0
- package/src/components/accordion/trigger.gts +32 -0
- package/src/components/accordion.gts +195 -0
- package/src/components/avatar.gts +108 -0
- package/src/components/dialog.gts +234 -0
- package/src/components/external-link.gts +14 -0
- package/src/components/form.gts +75 -0
- package/src/components/heading.gts +36 -0
- package/src/components/keys.gts +53 -0
- package/src/components/layout/hero.css +5 -0
- package/src/components/layout/hero.gts +17 -0
- package/src/components/layout/sticky-footer.css +9 -0
- package/src/components/layout/sticky-footer.gts +40 -0
- package/src/components/link.gts +172 -0
- package/src/components/menu.gts +373 -0
- package/src/components/one-time-password/buttons.gts +31 -0
- package/src/components/one-time-password/input.gts +198 -0
- package/src/components/one-time-password/otp.gts +130 -0
- package/src/components/one-time-password/utils.ts +201 -0
- package/src/components/one-time-password.gts +2 -0
- package/src/components/popover.gts +248 -0
- package/src/components/portal-targets.gts +136 -0
- package/src/components/portal.gts +194 -0
- package/src/components/progress.gts +154 -0
- package/src/components/rating/public-types.ts +44 -0
- package/src/components/rating/range.gts +22 -0
- package/src/components/rating/rating.gts +228 -0
- package/src/components/rating/stars.gts +60 -0
- package/src/components/rating/state.gts +144 -0
- package/src/components/rating/utils.ts +7 -0
- package/src/components/rating.gts +5 -0
- package/src/components/scroller.gts +179 -0
- package/src/components/shadowed.gts +110 -0
- package/src/components/switch.gts +103 -0
- package/src/components/tabs.gts +519 -0
- package/src/components/toggle-group.gts +265 -0
- package/src/components/toggle.gts +81 -0
- package/src/components/violations.css +105 -0
- package/src/components/violations.css.ts +1 -0
- package/src/components/visually-hidden.css +14 -0
- package/src/components/visually-hidden.gts +15 -0
- package/src/components/zoetrope/index.gts +358 -0
- package/src/components/zoetrope/styles.css +40 -0
- package/src/components/zoetrope/types.ts +65 -0
- package/src/components/zoetrope.ts +3 -0
- package/src/dom-context.gts +245 -0
- package/src/floating-ui/component.gts +186 -0
- package/src/floating-ui/middleware.ts +13 -0
- package/src/floating-ui/modifier.ts +183 -0
- package/src/floating-ui.ts +2 -0
- package/src/head.gts +37 -0
- package/src/helpers/body-class.ts +94 -0
- package/src/helpers/link.ts +125 -0
- package/src/helpers/service.ts +25 -0
- package/src/helpers.ts +2 -0
- package/src/iframe.ts +31 -0
- package/src/index.ts +43 -0
- package/src/load.gts +77 -0
- package/src/narrowing.ts +7 -0
- package/src/on-resize.ts +64 -0
- package/src/proper-links.ts +140 -0
- package/src/qp.ts +107 -0
- package/src/resize-observer.ts +132 -0
- package/src/service.ts +103 -0
- package/src/store.ts +72 -0
- package/src/styles.css.ts +5 -0
- package/src/tabster.ts +54 -0
- package/src/template-registry.ts +44 -0
- package/src/test-support/a11y.ts +50 -0
- package/src/test-support/dom.ts +112 -0
- package/src/test-support/otp.ts +64 -0
- package/src/test-support/rating.ts +144 -0
- package/src/test-support/routing.ts +62 -0
- package/src/test-support/zoetrope.ts +51 -0
- package/src/test-support.gts +6 -0
- package/src/type-utils.ts +1 -0
- package/src/utils.ts +75 -0
- package/src/viewport/in-viewport.gts +128 -0
- package/src/viewport/viewport.ts +122 -0
- package/src/viewport.ts +2 -0
- 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,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
|
+
}
|