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
@@ -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>;