ember-primitives 0.48.2 → 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 (218) hide show
  1. package/bin/index.mjs +271 -0
  2. package/declarations/components/portal.d.ts.map +1 -1
  3. package/declarations/components/rating/public-types.d.ts +0 -4
  4. package/declarations/components/rating/public-types.d.ts.map +1 -1
  5. package/declarations/components/rating/rating.d.ts +9 -1
  6. package/declarations/components/rating/rating.d.ts.map +1 -1
  7. package/declarations/components/rating/stars.d.ts.map +1 -1
  8. package/declarations/components/rating/state.d.ts +4 -0
  9. package/declarations/components/rating/state.d.ts.map +1 -1
  10. package/declarations/components/rating/utils.d.ts +0 -1
  11. package/declarations/components/rating/utils.d.ts.map +1 -1
  12. package/declarations/tabster.d.ts.map +1 -1
  13. package/declarations/utils.d.ts.map +1 -1
  14. package/declarations/viewport/in-viewport.d.ts +70 -0
  15. package/declarations/viewport/in-viewport.d.ts.map +1 -0
  16. package/declarations/viewport/viewport.d.ts +59 -0
  17. package/declarations/viewport/viewport.d.ts.map +1 -0
  18. package/declarations/viewport.d.ts +3 -0
  19. package/declarations/viewport.d.ts.map +1 -0
  20. package/dist/-private.js +0 -1
  21. package/dist/-private.js.map +1 -1
  22. package/dist/color-scheme.js +0 -1
  23. package/dist/color-scheme.js.map +1 -1
  24. package/dist/{component-Bs3N-G9z.js → component-BXy_iafw.js} +2 -3
  25. package/dist/component-BXy_iafw.js.map +1 -0
  26. package/dist/components/accordion.js +5 -6
  27. package/dist/components/accordion.js.map +1 -1
  28. package/dist/components/avatar.js +3 -4
  29. package/dist/components/avatar.js.map +1 -1
  30. package/dist/components/dialog.js +2 -3
  31. package/dist/components/dialog.js.map +1 -1
  32. package/dist/components/external-link.js +1 -2
  33. package/dist/components/external-link.js.map +1 -1
  34. package/dist/components/form.js +1 -2
  35. package/dist/components/form.js.map +1 -1
  36. package/dist/components/heading.js +1 -2
  37. package/dist/components/heading.js.map +1 -1
  38. package/dist/components/keys.js +2 -3
  39. package/dist/components/keys.js.map +1 -1
  40. package/dist/components/layout/hero.js +1 -1
  41. package/dist/components/layout/sticky-footer.js +1 -1
  42. package/dist/components/link.js +1 -2
  43. package/dist/components/link.js.map +1 -1
  44. package/dist/components/menu.js +6 -8
  45. package/dist/components/menu.js.map +1 -1
  46. package/dist/components/one-time-password.js +1 -2
  47. package/dist/components/popover.js +3 -4
  48. package/dist/components/popover.js.map +1 -1
  49. package/dist/components/portal-targets.js +2 -3
  50. package/dist/components/portal-targets.js.map +1 -1
  51. package/dist/components/portal.js +3 -7
  52. package/dist/components/portal.js.map +1 -1
  53. package/dist/components/progress.js +2 -3
  54. package/dist/components/progress.js.map +1 -1
  55. package/dist/components/rating.js +1 -2
  56. package/dist/components/scroller.js +1 -2
  57. package/dist/components/scroller.js.map +1 -1
  58. package/dist/components/shadowed.js +2 -3
  59. package/dist/components/shadowed.js.map +1 -1
  60. package/dist/components/switch.js +5 -6
  61. package/dist/components/switch.js.map +1 -1
  62. package/dist/components/tabs.js +6 -7
  63. package/dist/components/tabs.js.map +1 -1
  64. package/dist/components/toggle-group.js +3 -4
  65. package/dist/components/toggle-group.js.map +1 -1
  66. package/dist/components/toggle.js +2 -3
  67. package/dist/components/toggle.js.map +1 -1
  68. package/dist/components/visually-hidden.js +1 -2
  69. package/dist/components/visually-hidden.js.map +1 -1
  70. package/dist/components/zoetrope.js +1 -2
  71. package/dist/dom-context.js +2 -3
  72. package/dist/dom-context.js.map +1 -1
  73. package/dist/floating-ui.js +1 -2
  74. package/dist/head.js +1 -2
  75. package/dist/head.js.map +1 -1
  76. package/dist/helpers/body-class.js +0 -1
  77. package/dist/helpers/body-class.js.map +1 -1
  78. package/dist/helpers/link.js +0 -1
  79. package/dist/helpers/link.js.map +1 -1
  80. package/dist/helpers/service.js +0 -1
  81. package/dist/helpers/service.js.map +1 -1
  82. package/dist/helpers.js +0 -1
  83. package/dist/helpers.js.map +1 -1
  84. package/dist/iframe.js +0 -1
  85. package/dist/iframe.js.map +1 -1
  86. package/dist/{index-DKE67I8L.js → index-gRO4Cvlf.js} +2 -2
  87. package/dist/index-gRO4Cvlf.js.map +1 -0
  88. package/dist/index.js +3 -4
  89. package/dist/index.js.map +1 -1
  90. package/dist/load.js +0 -1
  91. package/dist/load.js.map +1 -1
  92. package/dist/narrowing.js +0 -1
  93. package/dist/narrowing.js.map +1 -1
  94. package/dist/on-resize.js +0 -1
  95. package/dist/on-resize.js.map +1 -1
  96. package/dist/{otp-C6hCCXKx.js → otp-7rz1PWP0.js} +6 -7
  97. package/dist/otp-7rz1PWP0.js.map +1 -0
  98. package/dist/proper-links.js +0 -1
  99. package/dist/proper-links.js.map +1 -1
  100. package/dist/qp.js +0 -1
  101. package/dist/qp.js.map +1 -1
  102. package/dist/rating-BrIiwDLw.js +152 -0
  103. package/dist/rating-BrIiwDLw.js.map +1 -0
  104. package/dist/resize-observer.js +0 -1
  105. package/dist/resize-observer.js.map +1 -1
  106. package/dist/service.js +0 -1
  107. package/dist/service.js.map +1 -1
  108. package/dist/store.js +0 -1
  109. package/dist/store.js.map +1 -1
  110. package/dist/styles.css.js +0 -1
  111. package/dist/tabster.js +0 -1
  112. package/dist/tabster.js.map +1 -1
  113. package/dist/test-support.js +0 -1
  114. package/dist/test-support.js.map +1 -1
  115. package/dist/{utils-C5796IKA.js → utils-D0v9WKmV.js} +1 -2
  116. package/dist/utils-D0v9WKmV.js.map +1 -0
  117. package/dist/utils.js +4 -1
  118. package/dist/utils.js.map +1 -1
  119. package/dist/viewport/in-viewport.js +82 -0
  120. package/dist/viewport/in-viewport.js.map +1 -0
  121. package/dist/viewport/viewport.js +92 -0
  122. package/dist/viewport/viewport.js.map +1 -0
  123. package/dist/viewport.js +3 -0
  124. package/dist/viewport.js.map +1 -0
  125. package/package.json +24 -20
  126. package/src/-private.ts +4 -0
  127. package/src/color-scheme.ts +165 -0
  128. package/src/components/-private/typed-elements.gts +13 -0
  129. package/src/components/-private/utils.ts +16 -0
  130. package/src/components/accordion/content.gts +34 -0
  131. package/src/components/accordion/header.gts +36 -0
  132. package/src/components/accordion/item.gts +55 -0
  133. package/src/components/accordion/public.ts +64 -0
  134. package/src/components/accordion/trigger.gts +32 -0
  135. package/src/components/accordion.gts +195 -0
  136. package/src/components/avatar.gts +108 -0
  137. package/src/components/dialog.gts +234 -0
  138. package/src/components/external-link.gts +14 -0
  139. package/src/components/form.gts +75 -0
  140. package/src/components/heading.gts +36 -0
  141. package/src/components/keys.gts +53 -0
  142. package/src/components/layout/hero.css +5 -0
  143. package/src/components/layout/hero.gts +17 -0
  144. package/src/components/layout/sticky-footer.css +9 -0
  145. package/src/components/layout/sticky-footer.gts +40 -0
  146. package/src/components/link.gts +172 -0
  147. package/src/components/menu.gts +373 -0
  148. package/src/components/one-time-password/buttons.gts +31 -0
  149. package/src/components/one-time-password/input.gts +198 -0
  150. package/src/components/one-time-password/otp.gts +130 -0
  151. package/src/components/one-time-password/utils.ts +201 -0
  152. package/src/components/one-time-password.gts +2 -0
  153. package/src/components/popover.gts +248 -0
  154. package/src/components/portal-targets.gts +136 -0
  155. package/src/components/portal.gts +194 -0
  156. package/src/components/progress.gts +154 -0
  157. package/src/components/rating/public-types.ts +44 -0
  158. package/src/components/rating/range.gts +22 -0
  159. package/src/components/rating/rating.gts +228 -0
  160. package/src/components/rating/stars.gts +60 -0
  161. package/src/components/rating/state.gts +144 -0
  162. package/src/components/rating/utils.ts +7 -0
  163. package/src/components/rating.gts +5 -0
  164. package/src/components/scroller.gts +179 -0
  165. package/src/components/shadowed.gts +110 -0
  166. package/src/components/switch.gts +103 -0
  167. package/src/components/tabs.gts +519 -0
  168. package/src/components/toggle-group.gts +265 -0
  169. package/src/components/toggle.gts +81 -0
  170. package/src/components/violations.css +105 -0
  171. package/src/components/violations.css.ts +1 -0
  172. package/src/components/visually-hidden.css +14 -0
  173. package/src/components/visually-hidden.gts +15 -0
  174. package/src/components/zoetrope/index.gts +358 -0
  175. package/src/components/zoetrope/styles.css +40 -0
  176. package/src/components/zoetrope/types.ts +65 -0
  177. package/src/components/zoetrope.ts +3 -0
  178. package/src/dom-context.gts +245 -0
  179. package/src/floating-ui/component.gts +186 -0
  180. package/src/floating-ui/middleware.ts +13 -0
  181. package/src/floating-ui/modifier.ts +183 -0
  182. package/src/floating-ui.ts +2 -0
  183. package/src/head.gts +37 -0
  184. package/src/helpers/body-class.ts +94 -0
  185. package/src/helpers/link.ts +125 -0
  186. package/src/helpers/service.ts +25 -0
  187. package/src/helpers.ts +2 -0
  188. package/src/iframe.ts +31 -0
  189. package/src/index.ts +43 -0
  190. package/src/load.gts +77 -0
  191. package/src/narrowing.ts +7 -0
  192. package/src/on-resize.ts +64 -0
  193. package/src/proper-links.ts +140 -0
  194. package/src/qp.ts +107 -0
  195. package/src/resize-observer.ts +132 -0
  196. package/src/service.ts +103 -0
  197. package/src/store.ts +72 -0
  198. package/src/styles.css.ts +5 -0
  199. package/src/tabster.ts +54 -0
  200. package/src/template-registry.ts +44 -0
  201. package/src/test-support/a11y.ts +50 -0
  202. package/src/test-support/dom.ts +112 -0
  203. package/src/test-support/otp.ts +64 -0
  204. package/src/test-support/rating.ts +144 -0
  205. package/src/test-support/routing.ts +62 -0
  206. package/src/test-support/zoetrope.ts +51 -0
  207. package/src/test-support.gts +6 -0
  208. package/src/type-utils.ts +1 -0
  209. package/src/utils.ts +75 -0
  210. package/src/viewport/in-viewport.gts +128 -0
  211. package/src/viewport/viewport.ts +122 -0
  212. package/src/viewport.ts +2 -0
  213. package/dist/component-Bs3N-G9z.js.map +0 -1
  214. package/dist/index-DKE67I8L.js.map +0 -1
  215. package/dist/otp-C6hCCXKx.js.map +0 -1
  216. package/dist/rating-D052JWRa.js +0 -149
  217. package/dist/rating-D052JWRa.js.map +0 -1
  218. package/dist/utils-C5796IKA.js.map +0 -1
package/src/store.ts ADDED
@@ -0,0 +1,72 @@
1
+ import { link } from 'reactiveweb/link';
2
+
3
+ import { isNewable } from './utils.ts';
4
+
5
+ import type { Newable } from './type-utils.ts';
6
+
7
+ /**
8
+ * context => { class => instance }
9
+ */
10
+ const contextCache = new WeakMap<object, Map<object, object>>();
11
+
12
+ /**
13
+ * Creates a singleton for the given context and links the lifetime of the created class to the passed context
14
+ *
15
+ * Note that this function is _not_ lazy. Calling `createStore` will create an instance of the passed class.
16
+ * When combined with a getter though, creation becomes lazy.
17
+ *
18
+ * In this example, `MyState` is created once per instance of the component.
19
+ * repeat accesses to `this.foo` return a stable reference _as if_ `@cached` were used.
20
+ * ```js
21
+ * class MyState {}
22
+ *
23
+ * class Demo extends Component {
24
+ * // this is a stable reference
25
+ * get foo() {
26
+ * return createStore(this, MyState);
27
+ * }
28
+ *
29
+ * // or
30
+ * bar = createStore(this, MyState);
31
+ *
32
+ * // or
33
+ * three = createStore(this, () => new MyState(1, 2));
34
+ * }
35
+ * ```
36
+ *
37
+ * If arguments need to be configured during construction, the second argument may also be a function
38
+ * ```js
39
+ * class MyState {}
40
+ *
41
+ * class Demo extends Component {
42
+ * // this is a stable reference
43
+ * get foo() {
44
+ * return createStore(this, MyState);
45
+ * }
46
+ * }
47
+ * ```
48
+ */
49
+ export function createStore<Instance extends object>(
50
+ context: object,
51
+ theClass: Newable<Instance> | (() => Instance)
52
+ ): Instance {
53
+ let cache = contextCache.get(context);
54
+
55
+ if (!cache) {
56
+ cache = new Map();
57
+ contextCache.set(context, cache);
58
+ }
59
+
60
+ let existing = cache.get(theClass);
61
+
62
+ if (!existing) {
63
+ const instance = isNewable(theClass) ? new theClass() : theClass();
64
+
65
+ link(instance, context);
66
+
67
+ cache.set(theClass, instance);
68
+ existing = instance;
69
+ }
70
+
71
+ return existing as Instance;
72
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Styles that are always needed, but their components
3
+ * may not be are included here.
4
+ */
5
+ import './components/visually-hidden.css';
package/src/tabster.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { registerDestructor } from '@ember/destroyable';
2
+
3
+ export async function setupTabster(
4
+ /**
5
+ * A destroyable object.
6
+ * This is needed so that when the app (or tests) or unmounted or ending,
7
+ * the tabster instance can be disposed of.
8
+ */
9
+ context: object,
10
+ {
11
+ tabster,
12
+ setTabsterRoot,
13
+ }: {
14
+ /**
15
+ * Let this setup function initalize tabster.
16
+ * https://tabster.io/docs/core
17
+ *
18
+ * This should be done only once per application as we don't want
19
+ * focus managers fighting with each other.
20
+ *
21
+ * Defaults to `true`,
22
+ *
23
+ * Will fallback to an existing tabster instance automatically if `getTabster` returns a value.
24
+ *
25
+ * If `false` is explicitly passed here, you'll also be in charge of teardown.
26
+ */
27
+ tabster?: boolean;
28
+ setTabsterRoot?: boolean;
29
+ } = {}
30
+ ) {
31
+ const { createTabster, getDeloser, getMover, getTabster, disposeTabster } =
32
+ await import('tabster');
33
+
34
+ tabster ??= true;
35
+ setTabsterRoot ??= true;
36
+
37
+ if (!tabster) {
38
+ return;
39
+ }
40
+
41
+ const existing = getTabster(window);
42
+ const primitivesTabster = existing ?? createTabster(window);
43
+
44
+ getMover(primitivesTabster);
45
+ getDeloser(primitivesTabster);
46
+
47
+ if (setTabsterRoot) {
48
+ document.body.setAttribute('data-tabster', '{ "root": {} }');
49
+ }
50
+
51
+ registerDestructor(context, () => {
52
+ disposeTabster(primitivesTabster);
53
+ });
54
+ }
@@ -0,0 +1,44 @@
1
+ // Easily allow apps, which are not yet using strict mode templates, to consume your Glint types, by importing this file.
2
+ // Add all your components, helpers and modifiers to the template registry here, so apps don't have to do this.
3
+ // See https://typed-ember.gitbook.io/glint/using-glint/ember/authoring-addons
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';
20
+
21
+ // import type MyComponent from './components/my-component';
22
+
23
+ // Remove this once entries have been added! 👇
24
+
25
+ export default interface Registry {
26
+ // components
27
+ Accordion: typeof Accordion;
28
+ AccordionItem: typeof AccordionItem;
29
+ AccordionHeader: typeof AccordionHeader;
30
+ AccordionContent: typeof AccordionContent;
31
+ AccordionTrigger: typeof AccordionTrigger;
32
+ Dialog: typeof Dialog;
33
+ ExternalLink: typeof ExternalLink;
34
+ Link: typeof Link;
35
+ Popover: typeof Popover;
36
+ PortalTargets: typeof PortalTargets;
37
+ Portal: typeof Portal;
38
+ Shadowed: typeof Shadowed;
39
+ Switch: typeof Switch;
40
+ Toggle: typeof Toggle;
41
+
42
+ // helpers
43
+ service: typeof service;
44
+ }
@@ -0,0 +1,50 @@
1
+ import { assert } from '@ember/debug';
2
+
3
+ import { setupTabster as _setupTabster } from '../tabster.ts';
4
+
5
+ import type Owner from '@ember/owner';
6
+
7
+ /**
8
+ * Sets up all support utilities for primitive components.
9
+ * Including the tabster root.
10
+ */
11
+ async function setup(owner: Owner) {
12
+ await _setupTabster(owner, { setTabsterRoot: false });
13
+
14
+ document.querySelector('#ember-testing')?.setAttribute('data-tabster', '{ "root": {} }');
15
+ }
16
+
17
+ /**
18
+ * A QUnit test utility for setting up the tabbing utility that a few of the components in ember-primitive use for providing enhanced keyboard support.
19
+ *
20
+ * ```gjs
21
+ * import { module, test } from 'qunit';
22
+ * import { setupRenderingTest } from 'ember-qunit';
23
+ * import { setupTabster } from 'ember-primitives/test-support';
24
+ *
25
+ * module('your suite', function (hooks) {
26
+ * setupRenderingTest(hooks);
27
+ * setupTabster(hooks);
28
+ *
29
+ * test('your test', async function (assert) {
30
+ * // ...
31
+ * });
32
+ * });
33
+ * ```
34
+ *
35
+ * This utility takes no options.
36
+ */
37
+ export function setupTabster(hooks: {
38
+ beforeEach: (callback: () => void | Promise<void>) => unknown;
39
+ }) {
40
+ hooks.beforeEach(async function (this: { owner: object }) {
41
+ const owner = this.owner;
42
+
43
+ assert(
44
+ `Test does not have an owner, be sure to use setupRenderingTest, setupTest, or setupApplicationTest (from ember-qunit (or similar))`,
45
+ owner
46
+ );
47
+
48
+ await setup(this.owner as Owner);
49
+ });
50
+ }
@@ -0,0 +1,112 @@
1
+ import { assert } from '@ember/debug';
2
+ import { find } from '@ember/test-helpers';
3
+
4
+ type Findable = Parameters<typeof find>[0] | Element;
5
+
6
+ /**
7
+ * Find an element within a given element that has a shadow-root.
8
+ *
9
+ * If the `root` can't be found, or if there actually is no shadow root,
10
+ * nothing will be returned.
11
+ *
12
+ * ```gjs
13
+ * import { findInShadow } from 'ember-primitives/test-support';
14
+ *
15
+ * // ...
16
+ *
17
+ * test('...', async function (assert) {
18
+ * // ...
19
+ * const root = find('div.with-shadowdom');
20
+ * assert.dom(findInShadow(root, 'h1')).containsText('welcome');
21
+ * });
22
+ * ```
23
+ */
24
+ export function findInShadow(root: Findable, query: string) {
25
+ const rootElement = root instanceof Element ? root : find(root);
26
+
27
+ return rootElement?.shadowRoot?.querySelector(query);
28
+ }
29
+
30
+ /**
31
+ * Does the element have a shadow root?
32
+ *
33
+ * Using this utility function will only save a few characters over using its implementation directly.
34
+ *
35
+ * ```gjs
36
+ * import { hasShadowRoot } from 'ember-primitives/test-support';
37
+ *
38
+ * // ...
39
+ *
40
+ * test('...', async function (assert) {
41
+ * // ...
42
+ * const el = find('div.with-shadowdom');
43
+ * assert.ok(hasShadowRoot(el), 'expecting el to have a shadow root');
44
+ * });
45
+ * ```
46
+ */
47
+ export function hasShadowRoot(el: Element) {
48
+ return Boolean(el.shadowRoot);
49
+ }
50
+
51
+ /**
52
+ * Find an element within `root`, that has a shadow root.
53
+ * The `root` param is optional, and if not provided, all of `#ember-testing` will be searched.
54
+ *
55
+ * This only returns the first-found shadow, so if you want a specifc shadow root,
56
+ * you'll need to narrow down the search by specifying a `root`.
57
+ *
58
+ * ```gjs
59
+ * import { findShadow } from 'ember-primitives/test-support';
60
+ *
61
+ * // ...
62
+ *
63
+ * test('...', async function (assert) {
64
+ * // ...
65
+ * const el = findShadow('div.with-shadowdom');
66
+ * // ...
67
+ * });
68
+ * ```
69
+ */
70
+ export function findShadow(root?: Findable) {
71
+ const rootElement = root
72
+ ? root instanceof Element
73
+ ? root
74
+ : find(root)
75
+ : document.getElementById('ember-testing');
76
+
77
+ if (!rootElement) return;
78
+
79
+ for (const element of rootElement.querySelectorAll('*')) {
80
+ if (element.shadowRoot) {
81
+ return element;
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * For the first available shadow root on the page, query in to it, like you would with `querySelector`.
88
+ *
89
+ *
90
+ * ```gjs
91
+ * import { findInFirstShadow } from 'ember-primitives/test-support';
92
+ *
93
+ * // ...
94
+ *
95
+ * test('...', async function (assert) {
96
+ * // ...
97
+ * assert.dom(findInFirstShadow('h1')).containsText('welcome');
98
+ * });
99
+ * ```
100
+ *
101
+ * If there are multiple shadow roots on the page / test-render,
102
+ * this is not the utility for you.
103
+ *
104
+ * For querying in specific shadow roots, you'll want to use `findInShadow`
105
+ */
106
+ export function findInFirstShadow(query: string) {
107
+ const host = findShadow();
108
+
109
+ assert(`No element with a shadow root could be found`, host);
110
+
111
+ return findInShadow(host, query);
112
+ }
@@ -0,0 +1,64 @@
1
+ import { assert } from '@ember/debug';
2
+ import { fillIn, find, settled } from '@ember/test-helpers';
3
+
4
+ /**
5
+ * Fill the OTP input
6
+ *
7
+ * ```gjs
8
+ * import { fillOTP } from 'ember-primitives/test-support';
9
+ *
10
+ * test('...', async function(assert) {
11
+ * // ...
12
+ * await fillOTP('123456');
13
+ * // ...
14
+ * })
15
+ *
16
+ * ```
17
+ *
18
+ * @param {string} code the code to fill the input(s) with.
19
+ * @param {string} [ selector ] if there are multiple OTP components on a page, this can be used to select one of them.
20
+ */
21
+ export async function fillOTP(code: string, selector?: string) {
22
+ const ancestor = selector ? find(selector) : document;
23
+
24
+ assert(
25
+ `Could not find ancestor element, does your selector match an existing element?`,
26
+ ancestor
27
+ );
28
+
29
+ const fieldset =
30
+ ancestor instanceof HTMLFieldSetElement ? ancestor : ancestor.querySelector('fieldset');
31
+
32
+ assert(
33
+ `Could not find containing fieldset element (this holds the OTP Input fields). Was the OTP component rendered?`,
34
+ fieldset
35
+ );
36
+
37
+ const inputs = fieldset.querySelectorAll('input');
38
+
39
+ assert(
40
+ `code cannot be longer than the available inputs. code is of length ${code.length} but there are ${inputs.length}`,
41
+ code.length <= inputs.length
42
+ );
43
+
44
+ const chars = code.split('');
45
+
46
+ assert(`OTP Input for index 0 is missing!`, inputs[0]);
47
+ assert(`Character at index 0 is missing`, chars[0]);
48
+
49
+ for (let i = 0; i < chars.length; i++) {
50
+ const input = inputs[i];
51
+ const char = chars[i];
52
+
53
+ assert(`Input at index ${i} is missing`, input);
54
+ assert(`Character at index ${i} is missing`, char);
55
+
56
+ input.value = char;
57
+ }
58
+
59
+ await fillIn(inputs[0], chars[0]);
60
+
61
+ // Account for out-of-settled-system delay due to RAF debounce.
62
+ await new Promise((resolve) => requestAnimationFrame(resolve));
63
+ await settled();
64
+ }
@@ -0,0 +1,144 @@
1
+ import { assert } from '@ember/debug';
2
+ import { click, fillIn, find, findAll } from '@ember/test-helpers';
3
+
4
+ const selectors = {
5
+ root: '.ember-primitives__rating',
6
+ item: '.ember-primitives__rating__item',
7
+ label: '.ember-primitives__rating__label',
8
+
9
+ rootData: {
10
+ total: '[data-total]',
11
+ value: '[data-value]',
12
+ },
13
+
14
+ itemData: {
15
+ number: '[data-number]',
16
+ readonly: '[data-readonly]',
17
+ selected: '[data-selected]',
18
+ itemPercent: '[data-percent-selected]',
19
+ },
20
+ };
21
+
22
+ const stars = {
23
+ selected: '★',
24
+ unselected: '☆',
25
+ };
26
+
27
+ /**
28
+ * Test utility for interacting with the
29
+ * Rating component.
30
+ *
31
+ * Simulates user behavior and provides high level functions so you don't need to worry about the DOM.
32
+ *
33
+ * Actual elements are not exposed, as the elements are private API.
34
+ * Even as you build a design system, the DOM should not be exposed to your consumers.
35
+ */
36
+ export function rating(selector?: string) {
37
+ const root = `${selector ?? ''}${selectors.root}`;
38
+
39
+ return new RatingPageObject(root);
40
+ }
41
+
42
+ class RatingPageObject {
43
+ #root: string;
44
+
45
+ constructor(root: string) {
46
+ this.#root = root;
47
+ }
48
+
49
+ get #rootElement() {
50
+ const element = find(this.#root);
51
+
52
+ assert(
53
+ `Could not find the root element for the <Rating> component. Used the selector \`${this.#root}\`. Was it rendered?`,
54
+ element
55
+ );
56
+
57
+ return element;
58
+ }
59
+
60
+ get #labelElement() {
61
+ const element = find(`${this.#root} ${selectors.label}`);
62
+
63
+ assert(`Could not find the label for the <Rating> component. Was it rendered?`, element);
64
+
65
+ return element;
66
+ }
67
+
68
+ get label() {
69
+ return this.#labelElement.textContent?.replaceAll(/\s+/g, ' ').trim();
70
+ }
71
+
72
+ get #starElements() {
73
+ const elements = findAll(`${this.#root} ${selectors.item}`);
74
+
75
+ assert(
76
+ `There are no stars/items. Is the <Rating> component misconfigured?`,
77
+ elements.length > 0
78
+ );
79
+
80
+ return elements as HTMLElement[];
81
+ }
82
+
83
+ get stars() {
84
+ const elements = this.#starElements;
85
+
86
+ return elements
87
+ .map((x) => (x.hasAttribute('data-selected') ? stars.selected : stars.unselected))
88
+ .join(' ');
89
+ }
90
+
91
+ get starTexts() {
92
+ const elements = this.#starElements;
93
+
94
+ return elements.map((x) => x.querySelector('[aria-hidden]')?.textContent?.trim()).join(' ');
95
+ }
96
+
97
+ get value() {
98
+ const value = this.#rootElement.getAttribute(`data-value`);
99
+
100
+ assert(`data-value attribute is missing on element '${this.#root}'`, value);
101
+
102
+ const number = parseFloat(value);
103
+
104
+ return number;
105
+ }
106
+
107
+ get isReadonly() {
108
+ return this.#starElements.every((x) => x.hasAttribute('data-readonly'));
109
+ }
110
+
111
+ async select(stars: number) {
112
+ const root = this.#rootElement;
113
+
114
+ const star = root.querySelector(`[data-number="${stars}"] input`);
115
+
116
+ if (star) {
117
+ await click(star);
118
+
119
+ return;
120
+ }
121
+
122
+ /**
123
+ * When we don't have an input, we require an input --
124
+ * which is also the only way we can choose non-integer values.
125
+ *
126
+ * Should be able to be a number input or range input.
127
+ */
128
+ const input = root.querySelector('input[type="number"], input[type="range"]');
129
+
130
+ if (input) {
131
+ await fillIn(input, `${stars}`);
132
+
133
+ return;
134
+ }
135
+
136
+ const available = [...root.querySelectorAll('[data-number]')].map((x) =>
137
+ x.getAttribute('data-number')
138
+ );
139
+
140
+ assert(
141
+ `Could not find item/star in <Rating> with value '${stars}' (or a number or range input with the same "name" value). Is the number (${stars}) correct and in-range for this component? The found available values are ${available.join(', ')}.`
142
+ );
143
+ }
144
+ }
@@ -0,0 +1,62 @@
1
+ import { assert } from '@ember/debug';
2
+ import Router from '@ember/routing/router';
3
+
4
+ import { properLinks } from '../proper-links.ts';
5
+
6
+ import type Owner from '@ember/owner';
7
+ import type { DSLCallback } from '@ember/routing/lib/dsl';
8
+ import type RouterService from '@ember/routing/router-service';
9
+
10
+ /**
11
+ * Allows setting up routes in tests without the need to scaffold routes in the actual app,
12
+ * allowing for iterating on many different routing scenario / configurations rapidly.
13
+ *
14
+ * Example:
15
+ * ```js
16
+ * import { setupRouting } from 'ember-primitives/test-support';
17
+ *
18
+ * ...
19
+ *
20
+ * test('my test', async function (assert) {
21
+ * setupRouting(this.owner, function () {
22
+ * this.route('foo');
23
+ * this.route('bar', function () {
24
+ * this.route('a');
25
+ * this.route('b');
26
+ * })
27
+ * });
28
+ *
29
+ * await visit('/bar/b');
30
+ * });
31
+ * ```
32
+ *
33
+ */
34
+ export function setupRouting(owner: Owner, map: DSLCallback, options?: { rootURL: string }) {
35
+ if (options?.rootURL) {
36
+ assert('rootURL must begin with a forward slash ("/")', options?.rootURL?.startsWith('/'));
37
+ }
38
+
39
+ @properLinks
40
+ class TestRouter extends Router {
41
+ rootURL = options?.rootURL ?? '/';
42
+ }
43
+
44
+ TestRouter.map(map);
45
+
46
+ owner.register('router:main', TestRouter);
47
+
48
+ // eslint-disable-next-line ember/no-private-routing-service
49
+ const iKnowWhatIMDoing = owner.lookup('router:main');
50
+
51
+ // We need a public testing API for this sort of stuff
52
+
53
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
54
+ (iKnowWhatIMDoing as any).setupRouter();
55
+ }
56
+
57
+ /**
58
+ * A small utility that only gives you a _typed_ router service.
59
+ */
60
+ export function getRouter(owner: Owner): RouterService {
61
+ return owner.lookup('service:router');
62
+ }
@@ -0,0 +1,51 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+ import { click } from '@ember/test-helpers';
3
+
4
+ export class ZoetropeHelper {
5
+ parentSelector = '.ember-primitives__zoetrope';
6
+
7
+ constructor(parentSelector?: string) {
8
+ if (parentSelector) {
9
+ this.parentSelector = parentSelector;
10
+ }
11
+ }
12
+
13
+ async scrollLeft() {
14
+ await click(`${this.parentSelector} .ember-primitives__zoetrope__controls button:first-child`);
15
+ }
16
+
17
+ async scrollRight() {
18
+ await click(`${this.parentSelector} .ember-primitives__zoetrope__controls button:last-child`);
19
+ }
20
+
21
+ visibleItems() {
22
+ const zoetropeContent = document.querySelectorAll(
23
+ `${this.parentSelector} .ember-primitives__zoetrope__scroller > *`
24
+ );
25
+
26
+ let firstVisibleItemIndex = -1;
27
+ let lastVisibleItemIndex = -1;
28
+
29
+ for (let i = 0; i < zoetropeContent.length; i++) {
30
+ const item = zoetropeContent[i]!;
31
+ const rect = item.getBoundingClientRect();
32
+ const parentRect = item.parentElement!.getBoundingClientRect();
33
+
34
+ if (rect.right >= parentRect?.left && rect.left <= parentRect?.right) {
35
+ if (firstVisibleItemIndex === -1) {
36
+ firstVisibleItemIndex = i;
37
+ }
38
+
39
+ lastVisibleItemIndex = i;
40
+ } else if (firstVisibleItemIndex !== -1) {
41
+ break;
42
+ }
43
+ }
44
+
45
+ return Array.from(zoetropeContent).slice(firstVisibleItemIndex, lastVisibleItemIndex + 1);
46
+ }
47
+
48
+ visibleItemCount() {
49
+ return this.visibleItems().length;
50
+ }
51
+ }
@@ -0,0 +1,6 @@
1
+ export { setupTabster } from "./test-support/a11y.ts";
2
+ export { findInFirstShadow, findInShadow, findShadow, hasShadowRoot } from "./test-support/dom.ts";
3
+ export { fillOTP } from "./test-support/otp.ts";
4
+ export { rating } from "./test-support/rating.ts";
5
+ export { getRouter, setupRouting } from "./test-support/routing.ts";
6
+ export { ZoetropeHelper } from "./test-support/zoetrope.ts";
@@ -0,0 +1 @@
1
+ export type Newable<T extends object = object> = { new (...args: any[]): T };