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,519 @@
1
+ /**
2
+ * References:
3
+ * - https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/tablist_role
4
+ * - https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
5
+ *
6
+ *
7
+ * Keyboard behaviors (optionally) provided by tabster
8
+ */
9
+
10
+ import Component from "@glimmer/component";
11
+ import { tracked } from "@glimmer/tracking";
12
+ import { isDestroyed, isDestroying } from "@ember/destroyable";
13
+ import { fn } from "@ember/helper";
14
+ import { on } from "@ember/modifier";
15
+ import { next } from "@ember/runloop";
16
+
17
+ import { getTabsterAttribute, MoverDirections } from "tabster";
18
+
19
+ import { uniqueId } from "../utils.ts";
20
+ import Portal from "./portal.gts";
21
+
22
+ import type { TOC } from "@ember/component/template-only";
23
+ import type Owner from "@ember/owner";
24
+ import type { ComponentLike, WithBoundArgs } from "@glint/template";
25
+
26
+ const UNSET = Symbol.for("ember-primitives:tabs:unset");
27
+
28
+ const TABSTER_CONFIG = getTabsterAttribute(
29
+ {
30
+ mover: {
31
+ direction: MoverDirections.Both,
32
+ cyclic: true,
33
+ memorizeCurrent: true,
34
+ },
35
+ deloser: {},
36
+ },
37
+ true,
38
+ );
39
+
40
+ const TabLink: TOC<{
41
+ Element: HTMLAnchorElement;
42
+ Args: {
43
+ /**
44
+ * @internal
45
+ * for linking of aria
46
+ */
47
+ id: string;
48
+ /**
49
+ * @internal
50
+ * for linking of aria
51
+ */
52
+ panelId: string;
53
+ };
54
+ Blocks: { default: [] };
55
+ }> = <template>
56
+ <a href="##missing##" ...attributes role="tab" aria-controls={{@panelId}} id={{@id}}>
57
+ {{yield}}
58
+ </a>
59
+ </template>;
60
+
61
+ export type ButtonType = ComponentLike<ButtonSignature>;
62
+ export interface ButtonSignature {
63
+ Element: HTMLButtonElement;
64
+ Blocks: {
65
+ default: [];
66
+ };
67
+ }
68
+
69
+ const TabButton: TOC<{
70
+ Args: {
71
+ /**
72
+ * @internal
73
+ * for linking of aria
74
+ */
75
+ id: string;
76
+ /**
77
+ * @internal
78
+ * for linking of aria
79
+ */
80
+ panelId: string;
81
+
82
+ /**
83
+ * @internal
84
+ * for managing state
85
+ */
86
+ handleClick: () => void;
87
+
88
+ /**
89
+ * @internal
90
+ * for managing state
91
+ */
92
+ value: string | undefined;
93
+
94
+ /**
95
+ * @internal
96
+ */
97
+ state: TabState;
98
+ };
99
+ Blocks: {
100
+ default: [];
101
+ };
102
+ }> = <template>
103
+ <button
104
+ ...attributes
105
+ role="tab"
106
+ type="button"
107
+ aria-controls={{@panelId}}
108
+ aria-selected={{String (@state.isActive @id @value)}}
109
+ id={{@id}}
110
+ {{on "click" @handleClick}}
111
+ {{! The Types for modifier are wrong }}
112
+ {{! @glint-expect-error}}
113
+ {{(if @state.isAutomatic (modifier on "focus" @handleClick))}}
114
+ >
115
+ {{yield}}
116
+ </button>
117
+ </template>;
118
+
119
+ export type ContentType = ComponentLike<ContentSignature>;
120
+ export interface ContentSignature {
121
+ /**
122
+ * the [role=tabpanel] element
123
+ */
124
+ Element: HTMLDivElement;
125
+ Blocks: {
126
+ default: [];
127
+ };
128
+ }
129
+
130
+ const TabContent: TOC<{
131
+ Element: HTMLDivElement;
132
+ Args: {
133
+ /**
134
+ * @internal
135
+ * for linking of aria
136
+ */
137
+ id: string;
138
+ /**
139
+ * @internal
140
+ * for linking of aria
141
+ */
142
+ tabId: string;
143
+ /**
144
+ * @internal
145
+ */
146
+ state: TabState;
147
+ };
148
+ Blocks: {
149
+ default: [];
150
+ };
151
+ }> = <template>
152
+ <Portal @to="#{{@state.tabpanelId}}" @append={{true}}>
153
+ {{#if (@state.isActive @tabId)}}
154
+ <div ...attributes role="tabpanel" aria-labelledby={{@tabId}} id={{@id}}>
155
+ {{yield}}
156
+ </div>
157
+ {{/if}}
158
+ </Portal>
159
+ </template>;
160
+
161
+ function isString(x: unknown): x is string {
162
+ return typeof x === "string";
163
+ }
164
+
165
+ function makeTab(tabButton: any, tabLink: any): any {
166
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
167
+ tabButton.Link = tabLink;
168
+
169
+ return tabButton;
170
+ }
171
+
172
+ export type ContainerType = ComponentLike<ContainerSignature>;
173
+ export type ContainerSignature =
174
+ | {
175
+ Blocks: {
176
+ default: [];
177
+ };
178
+ }
179
+ | {
180
+ Args: {
181
+ label: string | ComponentLike;
182
+ content: string | ComponentLike;
183
+ };
184
+ }
185
+ | {
186
+ Args: {
187
+ label: string | ComponentLike;
188
+ };
189
+ Blocks: {
190
+ /**
191
+ * The content for the tab
192
+ */
193
+ default: [];
194
+ };
195
+ };
196
+
197
+ class TabContainer extends Component<{
198
+ Args: {
199
+ /**
200
+ * @internal
201
+ */
202
+ state: TabState;
203
+
204
+ /**
205
+ * When opting for a "controlled component",
206
+ * the value will be needed to make sense of the selected tab.
207
+ *
208
+ * The default value used for communication within the Tabs component (and eventually emitted via the @onChange argument) is a unique random id.
209
+ * So while that could still be used for controlling the tabs component, it may be more easy to grok with user-managed values.
210
+ */
211
+ value?: string;
212
+
213
+ /**
214
+ * optional user-passable label
215
+ */
216
+ label?: string | ComponentLike;
217
+
218
+ /**
219
+ * optional user-passable content.
220
+ */
221
+ content?: string | ComponentLike;
222
+ };
223
+ Blocks: {
224
+ default: [
225
+ Label: WithBoundArgs<typeof TabButton, "state" | "id" | "panelId" | "handleClick" | "value">,
226
+ Content: WithBoundArgs<typeof TabContent, "state" | "id" | "tabId">,
227
+ ];
228
+ };
229
+ }> {
230
+ id = `ember-primitives__tab-${uniqueId()}`;
231
+
232
+ get tabId() {
233
+ return `${this.id}__tab`;
234
+ }
235
+
236
+ get panelId() {
237
+ return `${this.id}__panel`;
238
+ }
239
+
240
+ get label() {
241
+ return this.args.label ?? this.tabId;
242
+ }
243
+
244
+ <template>
245
+ {{#if @label}}
246
+ <TabButton
247
+ @state={{@state}}
248
+ @id={{this.tabId}}
249
+ @value={{@value}}
250
+ @panelId={{this.panelId}}
251
+ @handleClick={{fn @state.handleChange this.tabId @value}}
252
+ >
253
+ {{#if (isString @label)}}
254
+ {{@label}}
255
+ {{else}}
256
+ <@label />
257
+ {{/if}}
258
+ </TabButton>
259
+
260
+ <TabContent @state={{@state}} @id={{this.panelId}} @tabId={{this.tabId}}>
261
+ {{#if @content}}
262
+ {{#if (isString @content)}}
263
+ {{@content}}
264
+ {{else}}
265
+ <@content />
266
+ {{/if}}
267
+ {{else}}
268
+ {{yield}}
269
+ {{/if}}
270
+ </TabContent>
271
+ {{else}}
272
+ {{yield
273
+ (makeTab
274
+ (component
275
+ TabButton
276
+ state=@state
277
+ value=@value
278
+ id=this.tabId
279
+ panelId=this.panelId
280
+ handleClick=(fn @state.handleChange this.tabId @value)
281
+ )
282
+ (component TabLink state=@state id=this.tabId panelId=this.panelId)
283
+ )
284
+ (component TabContent state=@state id=this.panelId tabId=this.tabId)
285
+ }}
286
+ {{/if}}
287
+ </template>
288
+ }
289
+
290
+ const Label: TOC<{
291
+ /**
292
+ * The label wiring (id, aria, etc) are handled for you.
293
+ * If you'd like to use a heading element (h3, etc), place that in the block content
294
+ * when invoking this Label component.
295
+ */
296
+ Element: null;
297
+ Args: {
298
+ /**
299
+ * @internal
300
+ */
301
+ state: TabState;
302
+ };
303
+ Blocks: { default: [] };
304
+ }> = <template>
305
+ <Portal @to="#{{@state.labelId}}">
306
+ {{yield}}
307
+ </Portal>
308
+ </template>;
309
+
310
+ export interface Signature {
311
+ /**
312
+ * The wrapping element for the overall Tabs component.
313
+ * This should be used for styling the layout of the tabs.
314
+ */
315
+ Element: HTMLDivElement;
316
+ Args: {
317
+ /**
318
+ * Sets the active tab.
319
+ * If not passed, the first tab will be selected
320
+ */
321
+ activeTab?: string;
322
+
323
+ /**
324
+ * Optional label for the overall TabList
325
+ */
326
+ label?: string | ComponentLike;
327
+
328
+ /**
329
+ * When the tab changes, this function will be called.
330
+ * The function receives both the newly selected tab as well as the previous tab.
331
+ *
332
+ * However, if the tabs are not configured with names, these values will be null.
333
+ */
334
+ onChange?: (selectedTab: string, previousTab: string | null) => void;
335
+
336
+ /**
337
+ * When activationMode is set to "automatic", tabs are activated when receiving focus. When set to "manual", tabs are activated when clicked (or when "enter" is pressed via the keyboard).
338
+ */
339
+ activationMode?: "automatic" | "manual";
340
+ };
341
+ Blocks: {
342
+ default: [
343
+ Tab: WithBoundArgs<typeof TabContainer, "state"> & {
344
+ Label: WithBoundArgs<typeof Label, "state">;
345
+ },
346
+ ];
347
+ };
348
+ }
349
+
350
+ /**
351
+ * We're doing old skool hax with this, so we don't need to care about what the types think, really
352
+ */
353
+ function makeAPI(tabContainer: any, labelComponent: any): any {
354
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
355
+ tabContainer.Label = labelComponent;
356
+
357
+ return tabContainer;
358
+ }
359
+
360
+ import { buildWaiter } from "@ember/test-waiters";
361
+
362
+ const stateWaiter = buildWaiter("ember-primitives:tabs");
363
+
364
+ /**
365
+ * State bucket passed around to all the sub-components.
366
+ *
367
+ * Sort of a "Context", but with a bit of prop-drilling (which is more efficient than dom-context)
368
+ */
369
+ class TabState {
370
+ declare args: {
371
+ activeTab?: string;
372
+ activationMode?: "automatic" | "manual";
373
+ onChange?: (selected: string, previous: string | null) => void;
374
+ };
375
+
376
+ @tracked _active: string | null = null;
377
+
378
+ @tracked _label: string | undefined;
379
+
380
+ #first: string | null = null;
381
+ id: string;
382
+ labelId: string;
383
+ tabpanelId: string;
384
+ #token: unknown;
385
+
386
+ constructor(args: { activeTab?: string; onChange?: () => void }) {
387
+ this.args = args;
388
+
389
+ this.id = `ember-primitives-${uniqueId()}`;
390
+ this.labelId = `${this.id}__label`;
391
+ this.tabpanelId = `${this.id}__tabpanel`;
392
+ }
393
+
394
+ get activationMode() {
395
+ return this.args.activationMode ?? "automatic";
396
+ }
397
+
398
+ get isAutomatic() {
399
+ return this.activationMode === "automatic";
400
+ }
401
+
402
+ /**
403
+ * This function relies on the fact that during rendering,
404
+ * the first component to be rendered will be first,
405
+ * and it will be the one to set the secret first value,
406
+ * which means all other tabs will not be first.
407
+ *
408
+ */
409
+ isActive = (tabId: string, tabValue: undefined | string) => {
410
+ /**
411
+ * When users pass the @value to a tab, we use that for managing
412
+ * the "active state" instead of the DOM ID.
413
+ *
414
+ * NOTE: DOM IDs must be unique across the whole document, but @value
415
+ * does not need to be unqiue.
416
+ * `@value` *should* be unique for the Tabs component though
417
+ */
418
+ const isSelected = (x: string) => {
419
+ if (tabValue) return x === tabValue;
420
+
421
+ return x === tabId;
422
+ };
423
+
424
+ if (this.active === UNSET) {
425
+ if (this.#first) return isSelected(this.#first);
426
+
427
+ this.#first = tabValue ?? tabId;
428
+ this.#token = stateWaiter.beginAsync();
429
+
430
+ // eslint-disable-next-line ember/no-runloop
431
+ next(() => {
432
+ if (!this.#token) return;
433
+ stateWaiter.endAsync(this.#token);
434
+ if (this._active) return;
435
+ if (isDestroyed(this) || isDestroying(this)) return;
436
+
437
+ this._label = tabValue ?? tabId;
438
+ });
439
+
440
+ return true;
441
+ }
442
+
443
+ return isSelected(this.active);
444
+ };
445
+
446
+ get active() {
447
+ return this._active ?? this.args.activeTab ?? UNSET;
448
+ }
449
+
450
+ get activeLabel() {
451
+ /**
452
+ * This is only needed during the first set
453
+ * because we prioritize rendering first, and then updating metadata later
454
+ * (next render)
455
+ *
456
+ * NOTE: this does not mean that the a11y tree is updated later.
457
+ * it is correct on initial render
458
+ */
459
+ if (this._label) {
460
+ return this._label;
461
+ }
462
+
463
+ if (this.active === UNSET) {
464
+ return "Pending";
465
+ }
466
+
467
+ return this.active;
468
+ }
469
+
470
+ handleChange = (tabId: string, tabValue: string | undefined) => {
471
+ const previous = this.active;
472
+ const next = tabValue ?? tabId;
473
+
474
+ // No change, no need to be noisy
475
+ if (next === previous) return;
476
+
477
+ this._active = this._label = next;
478
+
479
+ this.args.onChange?.(next, previous === UNSET ? null : previous);
480
+ };
481
+ }
482
+
483
+ export class Tabs extends Component<Signature> {
484
+ state: TabState;
485
+
486
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
487
+ constructor(owner: Owner, args: {}) {
488
+ super(owner, args);
489
+
490
+ this.state = new TabState(args);
491
+ }
492
+
493
+ <template>
494
+ <div class="ember-primitives__tabs" ...attributes data-active={{this.state.activeLabel}}>
495
+ {{! This element will be portaled in to and replaced if tabs.Label is invoked }}
496
+ <div class="ember-primitives__tabs__label" id={{this.state.labelId}}>
497
+ {{#if (isString @label)}}
498
+ {{@label}}
499
+ {{else}}
500
+ <@label />
501
+ {{/if}}
502
+ </div>
503
+ <div
504
+ class="ember-primitives__tabs__tablist"
505
+ role="tablist"
506
+ aria-labelledby={{this.state.labelId}}
507
+ data-tabster={{TABSTER_CONFIG}}
508
+ >
509
+ {{yield
510
+ (makeAPI (component TabContainer state=this.state) (component Label state=this.state))
511
+ }}
512
+ </div>
513
+ {{!
514
+ Tab's contents are portaled in to this element
515
+ }}
516
+ <div class="ember-primitives__tabs__tabpanel" id={{this.state.tabpanelId}}></div>
517
+ </div>
518
+ </template>
519
+ }