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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { assert } from "@ember/debug";
|
|
2
|
+
import { isDevelopingApp, macroCondition } from "@embroider/macros";
|
|
3
|
+
|
|
4
|
+
import { modifier } from "ember-modifier";
|
|
5
|
+
import { TrackedMap, TrackedSet } from "tracked-built-ins";
|
|
6
|
+
|
|
7
|
+
import type { TOC } from "@ember/component/template-only";
|
|
8
|
+
|
|
9
|
+
const cache = new TrackedMap<string, Set<Element>>();
|
|
10
|
+
|
|
11
|
+
export const TARGETS = Object.freeze({
|
|
12
|
+
popover: "ember-primitives__portal-targets__popover",
|
|
13
|
+
tooltip: "ember-primitives__portal-targets__tooltip",
|
|
14
|
+
modal: "ember-primitives__portal-targets__modal",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export function findNearestTarget(origin: Element, name: string): Element | undefined {
|
|
18
|
+
assert(`first argument to \`findNearestTarget\` must be an element`, origin instanceof Element);
|
|
19
|
+
assert(`second argument to \`findNearestTarget\` must be a string`, typeof name === `string`);
|
|
20
|
+
|
|
21
|
+
let element: Element | undefined | null = null;
|
|
22
|
+
|
|
23
|
+
let parent = origin.parentNode;
|
|
24
|
+
|
|
25
|
+
const manuallyRegisteredSet = cache.get(name);
|
|
26
|
+
const manuallyRegistered: Element[] | null = manuallyRegisteredSet?.size
|
|
27
|
+
? [...manuallyRegisteredSet]
|
|
28
|
+
: null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* For use with <PortalTarget @name="hi" />
|
|
32
|
+
*/
|
|
33
|
+
function findRegistered(host: ParentNode): Element | undefined {
|
|
34
|
+
return manuallyRegistered?.find((element) => {
|
|
35
|
+
if (host.contains(element)) {
|
|
36
|
+
return element;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const selector = Object.values(TARGETS as Record<string, string>).includes(name)
|
|
42
|
+
? `[data-portal-name=${name}]`
|
|
43
|
+
: name;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Default portals / non-registered -- here we match a query selector instead of an element
|
|
47
|
+
*/
|
|
48
|
+
function findDefault(host: ParentNode): Element | undefined {
|
|
49
|
+
return host.querySelector(selector) as Element;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const finder = manuallyRegistered ? findRegistered : findDefault;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Crawl up the ancestry looking for our portal target
|
|
56
|
+
*/
|
|
57
|
+
while (!element && parent) {
|
|
58
|
+
element = finder(parent);
|
|
59
|
+
if (element) break;
|
|
60
|
+
parent = parent.parentNode;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (macroCondition(isDevelopingApp())) {
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
65
|
+
(window as any).prime0 = origin;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (name.startsWith("ember-primitives")) {
|
|
69
|
+
assert(
|
|
70
|
+
`Could not find element by the given name: \`${name}\`.` +
|
|
71
|
+
` The known names are ` +
|
|
72
|
+
`${Object.values(TARGETS).join(", ")} ` +
|
|
73
|
+
`-- but any name will work as long as it is set to the \`data-portal-name\` attribute ` +
|
|
74
|
+
`(or if the name has been specifically registered via the <PortalTarget /> component). ` +
|
|
75
|
+
`Double check that the element you're wanting to portal to is rendered. ` +
|
|
76
|
+
`The element passed to \`findNearestTarget\` is stored on \`window.prime0\` ` +
|
|
77
|
+
`You can debug in your browser's console via ` +
|
|
78
|
+
`\`document.querySelector('[data-portal-name="${name}"]')\``,
|
|
79
|
+
element,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return element ?? undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const register = modifier((element: Element, [name]: [name: string]) => {
|
|
87
|
+
assert(`@name is required when using <PortalTarget>`, name);
|
|
88
|
+
|
|
89
|
+
void (async () => {
|
|
90
|
+
// Bad TypeScript lint.
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/await-thenable
|
|
92
|
+
await 0;
|
|
93
|
+
|
|
94
|
+
let existing = cache.get(name);
|
|
95
|
+
|
|
96
|
+
if (!existing) {
|
|
97
|
+
existing = new TrackedSet<Element>();
|
|
98
|
+
cache.set(name, existing);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
existing.add(element);
|
|
102
|
+
})();
|
|
103
|
+
|
|
104
|
+
return () => {
|
|
105
|
+
cache.delete(name);
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export interface Signature {
|
|
110
|
+
Element: null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const PortalTargets: TOC<Signature> = <template>
|
|
114
|
+
<div data-portal-name={{TARGETS.popover}}></div>
|
|
115
|
+
<div data-portal-name={{TARGETS.tooltip}}></div>
|
|
116
|
+
<div data-portal-name={{TARGETS.modal}}></div>
|
|
117
|
+
</template>;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* For manually registering a PortalTarget for use with Portal
|
|
121
|
+
*/
|
|
122
|
+
export const PortalTarget: TOC<{
|
|
123
|
+
Element: HTMLDivElement;
|
|
124
|
+
Args: {
|
|
125
|
+
/**
|
|
126
|
+
* The name of the PortalTarget
|
|
127
|
+
*
|
|
128
|
+
* This exact string may be passed to `Portal`'s `@to` argument.
|
|
129
|
+
*/
|
|
130
|
+
name: string;
|
|
131
|
+
};
|
|
132
|
+
}> = <template>
|
|
133
|
+
<div {{register @name}} ...attributes></div>
|
|
134
|
+
</template>;
|
|
135
|
+
|
|
136
|
+
export default PortalTargets;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { assert } from "@ember/debug";
|
|
2
|
+
import { schedule } from "@ember/runloop";
|
|
3
|
+
import { buildWaiter } from "@ember/test-waiters";
|
|
4
|
+
|
|
5
|
+
import { modifier } from "ember-modifier";
|
|
6
|
+
import { cell, resource, resourceFactory } from "ember-resources";
|
|
7
|
+
|
|
8
|
+
import { isElement } from "../narrowing.ts";
|
|
9
|
+
import { findNearestTarget, type TARGETS } from "./portal-targets.gts";
|
|
10
|
+
|
|
11
|
+
import type { TOC } from "@ember/component/template-only";
|
|
12
|
+
|
|
13
|
+
type Targets = (typeof TARGETS)[keyof typeof TARGETS];
|
|
14
|
+
|
|
15
|
+
interface ToSignature {
|
|
16
|
+
Args: {
|
|
17
|
+
to: string;
|
|
18
|
+
append?: boolean;
|
|
19
|
+
};
|
|
20
|
+
Blocks: {
|
|
21
|
+
default: [];
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
interface ElementSignature {
|
|
25
|
+
Args: {
|
|
26
|
+
to: Element;
|
|
27
|
+
append?: boolean;
|
|
28
|
+
};
|
|
29
|
+
Blocks: {
|
|
30
|
+
default: [];
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Signature {
|
|
35
|
+
Args: {
|
|
36
|
+
/**
|
|
37
|
+
* The name of the PortalTarget to render in to.
|
|
38
|
+
* This is the value of the `data-portal-name` attribute
|
|
39
|
+
* of the element you wish to render in to.
|
|
40
|
+
*
|
|
41
|
+
* This can also be an Element which pairs nicely with query-utilities such as the platform-native `querySelector`
|
|
42
|
+
*/
|
|
43
|
+
to?: (Targets | (string & {})) | Element;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Set to true to append to the portal instead of replace
|
|
47
|
+
*
|
|
48
|
+
* Default: false
|
|
49
|
+
*/
|
|
50
|
+
append?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* For ember-wormhole style behavior, this argument may be an id,
|
|
53
|
+
* or a selector.
|
|
54
|
+
* This can also be an element, in which case the behavior is identical to `@to`
|
|
55
|
+
*/
|
|
56
|
+
wormhole?: string | Element;
|
|
57
|
+
};
|
|
58
|
+
Blocks: {
|
|
59
|
+
/**
|
|
60
|
+
* The portaled content
|
|
61
|
+
*/
|
|
62
|
+
default: [];
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Polyfill for ember-wormhole behavior
|
|
68
|
+
*
|
|
69
|
+
* Example usage:
|
|
70
|
+
* ```gjs
|
|
71
|
+
* import { wormhole, Portal } from 'ember-primitives/components/portal';
|
|
72
|
+
*
|
|
73
|
+
* <template>
|
|
74
|
+
* <div id="the-portal"></div>
|
|
75
|
+
*
|
|
76
|
+
* <Portal @to={{wormhole "the-portal"}}>
|
|
77
|
+
* content renders in the above div
|
|
78
|
+
* </Portal>
|
|
79
|
+
* </template>
|
|
80
|
+
*
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function wormhole(query: string | null | undefined | Element) {
|
|
84
|
+
assert(`Expected query/element to be truthy.`, query);
|
|
85
|
+
|
|
86
|
+
if (isElement(query)) {
|
|
87
|
+
return query;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let found = document.getElementById(query);
|
|
91
|
+
|
|
92
|
+
found ??= document.querySelector(query);
|
|
93
|
+
|
|
94
|
+
return found;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const anchor = modifier(
|
|
98
|
+
(element: Element, [to, update]: [string, ReturnType<typeof ElementValue>["set"]]) => {
|
|
99
|
+
const found = findNearestTarget(element, to);
|
|
100
|
+
|
|
101
|
+
update(found);
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const ElementValue = () => cell<Element | ShadowRoot | null | undefined>();
|
|
106
|
+
|
|
107
|
+
const waiter = buildWaiter("ember-primitives:portal");
|
|
108
|
+
|
|
109
|
+
function wormholeCompat(selector: string | Element) {
|
|
110
|
+
const target = wormhole(selector);
|
|
111
|
+
|
|
112
|
+
if (target) return target;
|
|
113
|
+
|
|
114
|
+
return resource(() => {
|
|
115
|
+
const target = cell<Element | undefined | null>();
|
|
116
|
+
|
|
117
|
+
const token = waiter.beginAsync();
|
|
118
|
+
|
|
119
|
+
// eslint-disable-next-line ember/no-runloop
|
|
120
|
+
schedule("afterRender", () => {
|
|
121
|
+
const result = wormhole(selector);
|
|
122
|
+
|
|
123
|
+
waiter.endAsync(token);
|
|
124
|
+
target.current = result;
|
|
125
|
+
assert(
|
|
126
|
+
`Could not find element with id/selector \`${typeof selector === "string" ? selector : "<Element>"}\``,
|
|
127
|
+
result,
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return () => target.current;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
resourceFactory(wormholeCompat);
|
|
136
|
+
|
|
137
|
+
export const Portal: TOC<Signature> = <template>
|
|
138
|
+
{{#if (isElement @to)}}
|
|
139
|
+
<ToElement @to={{@to}} @append={{@append}}>
|
|
140
|
+
{{yield}}
|
|
141
|
+
</ToElement>
|
|
142
|
+
{{else if @wormhole}}
|
|
143
|
+
{{#let (wormholeCompat @wormhole) as |target|}}
|
|
144
|
+
{{#if target}}
|
|
145
|
+
{{#in-element target insertBefore=null}}
|
|
146
|
+
{{yield}}
|
|
147
|
+
{{/in-element}}
|
|
148
|
+
{{/if}}
|
|
149
|
+
{{/let}}
|
|
150
|
+
{{else if @to}}
|
|
151
|
+
<Nestable @to={{@to}} @append={{@append}}>
|
|
152
|
+
{{yield}}
|
|
153
|
+
</Nestable>
|
|
154
|
+
{{else}}
|
|
155
|
+
{{assert "either @to or @wormhole is required. Received neither"}}
|
|
156
|
+
{{/if}}
|
|
157
|
+
</template>;
|
|
158
|
+
|
|
159
|
+
const ToElement: TOC<ElementSignature> = <template>
|
|
160
|
+
{{#if @append}}
|
|
161
|
+
{{#in-element @to insertBefore=null}}
|
|
162
|
+
{{yield}}
|
|
163
|
+
{{/in-element}}
|
|
164
|
+
{{else}}
|
|
165
|
+
{{#in-element @to}}
|
|
166
|
+
{{yield}}
|
|
167
|
+
{{/in-element}}
|
|
168
|
+
{{/if}}
|
|
169
|
+
</template>;
|
|
170
|
+
|
|
171
|
+
const Nestable: TOC<ToSignature> = <template>
|
|
172
|
+
{{#let (ElementValue) as |target|}}
|
|
173
|
+
{{! This div is always going to be empty,
|
|
174
|
+
because it'll either find the portal and render content elsewhere,
|
|
175
|
+
it it won't find the portal and won't render anything.
|
|
176
|
+
}}
|
|
177
|
+
{{! template-lint-disable no-inline-styles }}
|
|
178
|
+
<div style="display:contents;" {{anchor @to target.set}}>
|
|
179
|
+
{{#if target.current}}
|
|
180
|
+
{{#if @append}}
|
|
181
|
+
{{#in-element target.current insertBefore=null}}
|
|
182
|
+
{{yield}}
|
|
183
|
+
{{/in-element}}
|
|
184
|
+
{{else}}
|
|
185
|
+
{{#in-element target.current}}
|
|
186
|
+
{{yield}}
|
|
187
|
+
{{/in-element}}
|
|
188
|
+
{{/if}}
|
|
189
|
+
{{/if}}
|
|
190
|
+
</div>
|
|
191
|
+
{{/let}}
|
|
192
|
+
</template>;
|
|
193
|
+
|
|
194
|
+
export default Portal;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import Component from "@glimmer/component";
|
|
2
|
+
import { hash } from "@ember/helper";
|
|
3
|
+
|
|
4
|
+
import type { TOC } from "@ember/component/template-only";
|
|
5
|
+
import type { WithBoundArgs } from "@glint/template";
|
|
6
|
+
|
|
7
|
+
export interface Signature {
|
|
8
|
+
Element: HTMLDivElement;
|
|
9
|
+
Args: {
|
|
10
|
+
/**
|
|
11
|
+
* The current progress
|
|
12
|
+
* This may be less than 0 or more than `max`,
|
|
13
|
+
* but the resolved value (managed internally, and yielded out)
|
|
14
|
+
* does not exceed the range [0, max]
|
|
15
|
+
*/
|
|
16
|
+
value: number;
|
|
17
|
+
/**
|
|
18
|
+
* The max value, defaults to 100
|
|
19
|
+
*/
|
|
20
|
+
max?: number;
|
|
21
|
+
};
|
|
22
|
+
Blocks: {
|
|
23
|
+
default: [
|
|
24
|
+
{
|
|
25
|
+
/**
|
|
26
|
+
* The indicator element with some state applied.
|
|
27
|
+
* This can be used to style the progress of bar.
|
|
28
|
+
*/
|
|
29
|
+
Indicator: WithBoundArgs<typeof Indicator, "value" | "max" | "percent">;
|
|
30
|
+
/**
|
|
31
|
+
* The value as a percent of how far along the indicator should be
|
|
32
|
+
* positioned, between 0 and 100.
|
|
33
|
+
* Will be rounded to two decimal places.
|
|
34
|
+
*/
|
|
35
|
+
percent: number;
|
|
36
|
+
/**
|
|
37
|
+
* The value as a percent of how far along the indicator should be positioned,
|
|
38
|
+
* between 0 and 1
|
|
39
|
+
*/
|
|
40
|
+
decimal: number;
|
|
41
|
+
/**
|
|
42
|
+
* The resolved value within the limits of the progress bar.
|
|
43
|
+
*/
|
|
44
|
+
value: number;
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type ProgressState = "indeterminate" | "complete" | "loading";
|
|
51
|
+
|
|
52
|
+
const DEFAULT_MAX = 100;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Non-negative, non-NaN, non-Infinite, positive, rational
|
|
56
|
+
*/
|
|
57
|
+
function isValidProgressNumber(value: number | undefined | null): value is number {
|
|
58
|
+
if (typeof value !== "number") return false;
|
|
59
|
+
if (!Number.isFinite(value)) return false;
|
|
60
|
+
|
|
61
|
+
return value >= 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function progressState(value: number | undefined | null, maxValue: number): ProgressState {
|
|
65
|
+
return value == null ? "indeterminate" : value === maxValue ? "complete" : "loading";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getMax(userMax: number | undefined | null): number {
|
|
69
|
+
return isValidProgressNumber(userMax) ? userMax : DEFAULT_MAX;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getValue(userValue: number | undefined | null, maxValue: number): number {
|
|
73
|
+
const max = getMax(maxValue);
|
|
74
|
+
|
|
75
|
+
if (!isValidProgressNumber(userValue)) {
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (userValue > max) {
|
|
80
|
+
return max;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return userValue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getValueLabel(value: number, max: number) {
|
|
87
|
+
return `${Math.round((value / max) * 100)}%`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const Indicator: TOC<{
|
|
91
|
+
Element: HTMLDivElement;
|
|
92
|
+
Args: { max: number; value: number; percent: number };
|
|
93
|
+
Blocks: { default: [] };
|
|
94
|
+
}> = <template>
|
|
95
|
+
<div
|
|
96
|
+
...attributes
|
|
97
|
+
data-max={{@max}}
|
|
98
|
+
data-value={{@value}}
|
|
99
|
+
data-state={{progressState @value @max}}
|
|
100
|
+
data-percent={{@percent}}
|
|
101
|
+
>
|
|
102
|
+
{{yield}}
|
|
103
|
+
</div>
|
|
104
|
+
</template>;
|
|
105
|
+
|
|
106
|
+
export class Progress extends Component<Signature> {
|
|
107
|
+
get max() {
|
|
108
|
+
return getMax(this.args.max);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get value() {
|
|
112
|
+
return getValue(this.args.value, this.max);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get valueLabel() {
|
|
116
|
+
return getValueLabel(this.value, this.max);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
get decimal() {
|
|
120
|
+
return this.value / this.max;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get percent() {
|
|
124
|
+
return Math.round(this.decimal * 100 * 100) / 100;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
<template>
|
|
128
|
+
<div
|
|
129
|
+
...attributes
|
|
130
|
+
aria-valuemax={{this.max}}
|
|
131
|
+
aria-valuemin="0"
|
|
132
|
+
aria-valuenow={{this.value}}
|
|
133
|
+
aria-valuetext={{this.valueLabel}}
|
|
134
|
+
role="progressbar"
|
|
135
|
+
data-value={{this.value}}
|
|
136
|
+
data-state={{progressState this.value this.max}}
|
|
137
|
+
data-max={{this.max}}
|
|
138
|
+
data-min="0"
|
|
139
|
+
data-percent={{this.percent}}
|
|
140
|
+
>
|
|
141
|
+
|
|
142
|
+
{{yield
|
|
143
|
+
(hash
|
|
144
|
+
Indicator=(component Indicator value=this.value max=this.max percent=this.percent)
|
|
145
|
+
value=this.value
|
|
146
|
+
percent=this.percent
|
|
147
|
+
decimal=this.decimal
|
|
148
|
+
)
|
|
149
|
+
}}
|
|
150
|
+
</div>
|
|
151
|
+
</template>
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export default Progress;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ComponentLike } from '@glint/template';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
export interface ComponentIcons {
|
|
7
|
+
/**
|
|
8
|
+
* It's possible to completely manage the state of an individual Icon yourself
|
|
9
|
+
* by passing a component that has ...attributes on its outer element and receives
|
|
10
|
+
* a @isSelected argument which is true for selected and false for unselected.
|
|
11
|
+
*
|
|
12
|
+
* There is also argument passed which is the percent-amount of selection if you want fractional ratings, @selectedPercent
|
|
13
|
+
*/
|
|
14
|
+
icon: ComponentLike<{
|
|
15
|
+
Element: HTMLElement;
|
|
16
|
+
Args: {
|
|
17
|
+
/**
|
|
18
|
+
* Is this item selected?
|
|
19
|
+
*/
|
|
20
|
+
isSelected: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Which number of item is this item within the overall rating group.
|
|
23
|
+
*/
|
|
24
|
+
value: number;
|
|
25
|
+
/**
|
|
26
|
+
* Should this be marked as readonly
|
|
27
|
+
*/
|
|
28
|
+
readonly: boolean;
|
|
29
|
+
};
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @public
|
|
35
|
+
*/
|
|
36
|
+
export interface StringIcons {
|
|
37
|
+
/**
|
|
38
|
+
* The symbol to use for an unselected variant of the icon
|
|
39
|
+
*
|
|
40
|
+
* Defaults to "★";
|
|
41
|
+
* Can change color when selected.
|
|
42
|
+
*/
|
|
43
|
+
icon?: string;
|
|
44
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { on } from "@ember/modifier";
|
|
2
|
+
|
|
3
|
+
import type { TOC } from "@ember/component/template-only";
|
|
4
|
+
|
|
5
|
+
export const RatingRange: TOC<{
|
|
6
|
+
Element: HTMLInputElement;
|
|
7
|
+
Args: {
|
|
8
|
+
name: string;
|
|
9
|
+
max: number;
|
|
10
|
+
value: number;
|
|
11
|
+
handleChange: (event: Event) => void;
|
|
12
|
+
};
|
|
13
|
+
}> = <template>
|
|
14
|
+
<input
|
|
15
|
+
...attributes
|
|
16
|
+
name={{@name}}
|
|
17
|
+
type="range"
|
|
18
|
+
max={{@max}}
|
|
19
|
+
value={{@value}}
|
|
20
|
+
{{on "change" @handleChange}}
|
|
21
|
+
/>
|
|
22
|
+
</template>;
|