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.
- package/bin/index.mjs +271 -0
- package/declarations/components/portal.d.ts.map +1 -1
- 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/declarations/tabster.d.ts.map +1 -1
- package/declarations/utils.d.ts.map +1 -1
- package/declarations/viewport/in-viewport.d.ts +70 -0
- package/declarations/viewport/in-viewport.d.ts.map +1 -0
- package/declarations/viewport/viewport.d.ts +59 -0
- package/declarations/viewport/viewport.d.ts.map +1 -0
- package/declarations/viewport.d.ts +3 -0
- package/declarations/viewport.d.ts.map +1 -0
- package/dist/-private.js +0 -1
- package/dist/-private.js.map +1 -1
- package/dist/color-scheme.js +0 -1
- package/dist/color-scheme.js.map +1 -1
- package/dist/{component-Bs3N-G9z.js → component-BXy_iafw.js} +2 -3
- package/dist/component-BXy_iafw.js.map +1 -0
- package/dist/components/accordion.js +5 -6
- package/dist/components/accordion.js.map +1 -1
- package/dist/components/avatar.js +3 -4
- package/dist/components/avatar.js.map +1 -1
- package/dist/components/dialog.js +2 -3
- package/dist/components/dialog.js.map +1 -1
- package/dist/components/external-link.js +1 -2
- package/dist/components/external-link.js.map +1 -1
- package/dist/components/form.js +1 -2
- package/dist/components/form.js.map +1 -1
- package/dist/components/heading.js +1 -2
- package/dist/components/heading.js.map +1 -1
- package/dist/components/keys.js +2 -3
- package/dist/components/keys.js.map +1 -1
- package/dist/components/layout/hero.js +1 -1
- package/dist/components/layout/sticky-footer.js +1 -1
- package/dist/components/link.js +1 -2
- package/dist/components/link.js.map +1 -1
- package/dist/components/menu.js +6 -8
- package/dist/components/menu.js.map +1 -1
- package/dist/components/one-time-password.js +1 -2
- package/dist/components/popover.js +3 -4
- package/dist/components/popover.js.map +1 -1
- package/dist/components/portal-targets.js +2 -3
- package/dist/components/portal-targets.js.map +1 -1
- package/dist/components/portal.js +3 -7
- package/dist/components/portal.js.map +1 -1
- package/dist/components/progress.js +2 -3
- package/dist/components/progress.js.map +1 -1
- package/dist/components/rating.js +1 -2
- package/dist/components/scroller.js +1 -2
- package/dist/components/scroller.js.map +1 -1
- package/dist/components/shadowed.js +2 -3
- package/dist/components/shadowed.js.map +1 -1
- package/dist/components/switch.js +5 -6
- package/dist/components/switch.js.map +1 -1
- package/dist/components/tabs.js +6 -7
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/toggle-group.js +3 -4
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle.js +2 -3
- package/dist/components/toggle.js.map +1 -1
- package/dist/components/visually-hidden.js +1 -2
- package/dist/components/visually-hidden.js.map +1 -1
- package/dist/components/zoetrope.js +1 -2
- package/dist/dom-context.js +2 -3
- package/dist/dom-context.js.map +1 -1
- package/dist/floating-ui.js +1 -2
- package/dist/head.js +1 -2
- package/dist/head.js.map +1 -1
- package/dist/helpers/body-class.js +0 -1
- package/dist/helpers/body-class.js.map +1 -1
- package/dist/helpers/link.js +0 -1
- package/dist/helpers/link.js.map +1 -1
- package/dist/helpers/service.js +0 -1
- package/dist/helpers/service.js.map +1 -1
- package/dist/helpers.js +0 -1
- package/dist/helpers.js.map +1 -1
- package/dist/iframe.js +0 -1
- package/dist/iframe.js.map +1 -1
- package/dist/{index-DKE67I8L.js → index-gRO4Cvlf.js} +2 -2
- package/dist/index-gRO4Cvlf.js.map +1 -0
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/load.js +0 -1
- package/dist/load.js.map +1 -1
- package/dist/narrowing.js +0 -1
- package/dist/narrowing.js.map +1 -1
- package/dist/on-resize.js +0 -1
- package/dist/on-resize.js.map +1 -1
- package/dist/{otp-C6hCCXKx.js → otp-7rz1PWP0.js} +6 -7
- package/dist/otp-7rz1PWP0.js.map +1 -0
- package/dist/proper-links.js +0 -1
- package/dist/proper-links.js.map +1 -1
- package/dist/qp.js +0 -1
- package/dist/qp.js.map +1 -1
- package/dist/rating-BrIiwDLw.js +152 -0
- package/dist/rating-BrIiwDLw.js.map +1 -0
- package/dist/resize-observer.js +0 -1
- package/dist/resize-observer.js.map +1 -1
- package/dist/service.js +0 -1
- package/dist/service.js.map +1 -1
- package/dist/store.js +0 -1
- package/dist/store.js.map +1 -1
- package/dist/styles.css.js +0 -1
- package/dist/tabster.js +0 -1
- package/dist/tabster.js.map +1 -1
- package/dist/test-support.js +0 -1
- package/dist/test-support.js.map +1 -1
- package/dist/{utils-C5796IKA.js → utils-D0v9WKmV.js} +1 -2
- package/dist/utils-D0v9WKmV.js.map +1 -0
- package/dist/utils.js +4 -1
- package/dist/utils.js.map +1 -1
- package/dist/viewport/in-viewport.js +82 -0
- package/dist/viewport/in-viewport.js.map +1 -0
- package/dist/viewport/viewport.js +92 -0
- package/dist/viewport/viewport.js.map +1 -0
- package/dist/viewport.js +3 -0
- package/dist/viewport.js.map +1 -0
- package/package.json +24 -20
- 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/component-Bs3N-G9z.js.map +0 -1
- package/dist/index-DKE67I8L.js.map +0 -1
- package/dist/otp-C6hCCXKx.js.map +0 -1
- package/dist/rating-D052JWRa.js +0 -149
- package/dist/rating-D052JWRa.js.map +0 -1
- 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
|
+
}
|
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 };
|