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,373 @@
1
+ import Component from "@glimmer/component";
2
+ import { hash } from "@ember/helper";
3
+ import { on } from "@ember/modifier";
4
+ import { guidFor } from "@ember/object/internals";
5
+
6
+ import { modifier as eModifier } from "ember-modifier";
7
+ import { cell } from "ember-resources";
8
+ import { getTabster, getTabsterAttribute, MoverDirections, setTabsterAttribute } from "tabster";
9
+
10
+ import { Link, type Signature as LinkSignature } from "./link.gts";
11
+ import { Popover, type Signature as PopoverSignature } from "./popover.gts";
12
+
13
+ import type { TOC } from "@ember/component/template-only";
14
+ import type { WithBoundArgs } from "@glint/template";
15
+
16
+ type Cell<V> = ReturnType<typeof cell<V>>;
17
+ type LinkArgs = LinkSignature["Args"];
18
+ type PopoverArgs = PopoverSignature["Args"];
19
+ type PopoverBlockParams = PopoverSignature["Blocks"]["default"][0];
20
+
21
+ const TABSTER_CONFIG_CONTENT = getTabsterAttribute(
22
+ {
23
+ mover: {
24
+ direction: MoverDirections.Both,
25
+ cyclic: true,
26
+ },
27
+ deloser: {},
28
+ },
29
+ true,
30
+ );
31
+
32
+ const TABSTER_CONFIG_TRIGGER = {
33
+ deloser: {},
34
+ };
35
+
36
+ export interface Signature {
37
+ Args: PopoverArgs;
38
+ Blocks: {
39
+ default: [
40
+ {
41
+ arrow: PopoverBlockParams["arrow"];
42
+ trigger: WithBoundArgs<
43
+ typeof trigger,
44
+ "triggerElement" | "contentId" | "isOpen" | "setReference"
45
+ >;
46
+ Trigger: WithBoundArgs<typeof Trigger, "triggerModifier">;
47
+ Content: WithBoundArgs<
48
+ typeof Content,
49
+ "triggerElement" | "contentId" | "isOpen" | "PopoverContent"
50
+ >;
51
+ isOpen: boolean;
52
+ },
53
+ ];
54
+ };
55
+ }
56
+
57
+ export interface SeparatorSignature {
58
+ Element: HTMLDivElement;
59
+ Blocks: { default: [] };
60
+ }
61
+
62
+ const Separator: TOC<SeparatorSignature> = <template>
63
+ <div role="separator" ...attributes>
64
+ {{yield}}
65
+ </div>
66
+ </template>;
67
+
68
+ /**
69
+ * We focus items on `pointerMove` to achieve the following:
70
+ *
71
+ * - Mouse over an item (it focuses)
72
+ * - Leave mouse where it is and use keyboard to focus a different item
73
+ * - Wiggle mouse without it leaving previously focused item
74
+ * - Previously focused item should re-focus
75
+ *
76
+ * If we used `mouseOver`/`mouseEnter` it would not re-focus when the mouse
77
+ * wiggles. This is to match native menu implementation.
78
+ */
79
+ function focusOnHover(e: PointerEvent) {
80
+ const item = e.currentTarget;
81
+
82
+ if (item instanceof HTMLElement) {
83
+ item?.focus();
84
+ }
85
+ }
86
+
87
+ interface PrivateItemSignature {
88
+ Element: HTMLButtonElement;
89
+ Args: { onSelect?: (event: Event) => void; toggle: () => void };
90
+ Blocks: { default: [] };
91
+ }
92
+
93
+ export interface ItemSignature {
94
+ Element: PrivateItemSignature["Element"];
95
+ Args: Omit<PrivateItemSignature["Args"], "toggle">;
96
+ Blocks: PrivateItemSignature["Blocks"];
97
+ }
98
+
99
+ const Item: TOC<PrivateItemSignature> = <template>
100
+ {{! @glint-expect-error }}
101
+ {{#let (if @onSelect (modifier on "click" @onSelect)) as |maybeClick|}}
102
+ <button
103
+ type="button"
104
+ role="menuitem"
105
+ {{! @glint-expect-error }}
106
+ {{maybeClick}}
107
+ {{on "click" @toggle}}
108
+ {{on "pointermove" focusOnHover}}
109
+ ...attributes
110
+ >
111
+ {{yield}}
112
+ </button>
113
+ {{/let}}
114
+ </template>;
115
+
116
+ interface LinkItemArgs extends LinkArgs {
117
+ toggle: () => void;
118
+ }
119
+
120
+ interface PrivateLinkItemSignature {
121
+ Element: HTMLAnchorElement;
122
+ Args: LinkItemArgs;
123
+ Blocks: { default: [] };
124
+ }
125
+
126
+ export interface LinkItemSignature {
127
+ Element: PrivateLinkItemSignature["Element"];
128
+ Args: LinkArgs;
129
+ Blocks: PrivateLinkItemSignature["Blocks"];
130
+ }
131
+
132
+ const LinkItem: TOC<PrivateLinkItemSignature> = <template>
133
+ <Link
134
+ role="menuitem"
135
+ @href={{@href}}
136
+ @includeActiveQueryParams={{@includeActiveQueryParams}}
137
+ @activeOnSubPaths={{@activeOnSubPaths}}
138
+ {{on "click" @toggle}}
139
+ {{on "pointermove" focusOnHover}}
140
+ ...attributes
141
+ >
142
+ {{yield}}
143
+ </Link>
144
+ </template>;
145
+
146
+ const installContent = eModifier<{
147
+ Element: HTMLElement;
148
+ Args: {
149
+ Named: {
150
+ isOpen: Cell<boolean>;
151
+ triggerElement: Cell<HTMLElement>;
152
+ };
153
+ };
154
+ }>((element, _: [], { isOpen, triggerElement }) => {
155
+ // focus first focusable element on the content
156
+ const tabster = getTabster(window);
157
+ const firstFocusable = tabster?.focusable.findFirst({
158
+ container: element,
159
+ });
160
+
161
+ firstFocusable?.focus();
162
+
163
+ // listen for "outside" clicks
164
+ function onDocumentClick(e: MouseEvent) {
165
+ if (
166
+ isOpen.current &&
167
+ e.target &&
168
+ !element.contains(e.target as HTMLElement) &&
169
+ !triggerElement.current?.contains(e.target as HTMLElement)
170
+ ) {
171
+ isOpen.current = false;
172
+ }
173
+ }
174
+
175
+ // listen for the escape key
176
+ function onDocumentKeydown(e: KeyboardEvent) {
177
+ if (isOpen.current && e.key === "Escape") {
178
+ isOpen.current = false;
179
+ }
180
+ }
181
+
182
+ document.addEventListener("click", onDocumentClick);
183
+ document.addEventListener("keydown", onDocumentKeydown);
184
+
185
+ return () => {
186
+ document.removeEventListener("click", onDocumentClick);
187
+ document.removeEventListener("keydown", onDocumentKeydown);
188
+ };
189
+ });
190
+
191
+ interface PrivateContentSignature {
192
+ Element: HTMLDivElement;
193
+ Args: {
194
+ triggerElement: Cell<HTMLElement>;
195
+ contentId: string;
196
+ isOpen: Cell<boolean>;
197
+ PopoverContent: PopoverBlockParams["Content"];
198
+ };
199
+ Blocks: {
200
+ default: [
201
+ {
202
+ Item: WithBoundArgs<typeof Item, "toggle">;
203
+ LinkItem: WithBoundArgs<typeof LinkItem, "toggle">;
204
+ Separator: typeof Separator;
205
+ },
206
+ ];
207
+ };
208
+ }
209
+
210
+ export interface ContentSignature {
211
+ Element: PrivateContentSignature["Element"];
212
+ Blocks: PrivateContentSignature["Blocks"];
213
+ }
214
+
215
+ const Content: TOC<PrivateContentSignature> = <template>
216
+ {{#if @isOpen.current}}
217
+ <@PopoverContent
218
+ id={{@contentId}}
219
+ role="menu"
220
+ data-tabster={{TABSTER_CONFIG_CONTENT}}
221
+ tabindex="0"
222
+ {{installContent isOpen=@isOpen triggerElement=@triggerElement}}
223
+ ...attributes
224
+ >
225
+ {{yield
226
+ (hash
227
+ Item=(component Item toggle=@isOpen.toggle)
228
+ LinkItem=(component LinkItem toggle=@isOpen.toggle)
229
+ Separator=Separator
230
+ )
231
+ }}
232
+ </@PopoverContent>
233
+ {{/if}}
234
+ </template>;
235
+
236
+ interface PrivateTriggerModifierSignature {
237
+ Element: HTMLElement;
238
+ Args: {
239
+ Named: {
240
+ triggerElement: Cell<HTMLElement>;
241
+ isOpen: Cell<boolean>;
242
+ contentId: string;
243
+ setReference: PopoverBlockParams["setReference"];
244
+ stopPropagation?: boolean;
245
+ preventDefault?: boolean;
246
+ };
247
+ };
248
+ }
249
+
250
+ export interface TriggerModifierSignature {
251
+ Element: PrivateTriggerModifierSignature["Element"];
252
+ }
253
+
254
+ const trigger = eModifier<PrivateTriggerModifierSignature>(
255
+ (
256
+ element,
257
+ _: [],
258
+ { triggerElement, isOpen, contentId, setReference, stopPropagation, preventDefault },
259
+ ) => {
260
+ element.setAttribute("aria-haspopup", "menu");
261
+
262
+ if (isOpen.current) {
263
+ element.setAttribute("aria-controls", contentId);
264
+ element.setAttribute("aria-expanded", "true");
265
+ } else {
266
+ element.removeAttribute("aria-controls");
267
+ element.setAttribute("aria-expanded", "false");
268
+ }
269
+
270
+ setTabsterAttribute(element, TABSTER_CONFIG_TRIGGER);
271
+
272
+ const onTriggerClick = (event: MouseEvent) => {
273
+ if (stopPropagation) {
274
+ event.stopPropagation();
275
+ }
276
+
277
+ if (preventDefault) {
278
+ event.preventDefault();
279
+ }
280
+
281
+ isOpen.toggle();
282
+ };
283
+
284
+ element.addEventListener("click", onTriggerClick);
285
+
286
+ triggerElement.current = element;
287
+
288
+ setReference(element);
289
+
290
+ return () => {
291
+ element.removeEventListener("click", onTriggerClick);
292
+ };
293
+ },
294
+ );
295
+
296
+ interface PrivateTriggerSignature {
297
+ Element: HTMLButtonElement;
298
+ Args: {
299
+ triggerModifier: WithBoundArgs<
300
+ typeof trigger,
301
+ "triggerElement" | "contentId" | "isOpen" | "setReference"
302
+ >;
303
+ stopPropagation?: boolean;
304
+ preventDefault?: boolean;
305
+ };
306
+ Blocks: { default: [] };
307
+ }
308
+
309
+ export interface TriggerSignature {
310
+ Element: PrivateTriggerSignature["Element"];
311
+ Blocks: PrivateTriggerSignature["Blocks"];
312
+ }
313
+
314
+ const Trigger: TOC<PrivateTriggerSignature> = <template>
315
+ <button
316
+ type="button"
317
+ {{@triggerModifier stopPropagation=@stopPropagation preventDefault=@preventDefault}}
318
+ ...attributes
319
+ >
320
+ {{yield}}
321
+ </button>
322
+ </template>;
323
+
324
+ const IsOpen = () => cell<boolean>(false);
325
+ const TriggerElement = () => cell<HTMLElement>();
326
+
327
+ export class Menu extends Component<Signature> {
328
+ contentId = guidFor(this);
329
+
330
+ <template>
331
+ {{#let (IsOpen) (TriggerElement) as |isOpen triggerEl|}}
332
+ <Popover
333
+ @flipOptions={{@flipOptions}}
334
+ @middleware={{@middleware}}
335
+ @offsetOptions={{@offsetOptions}}
336
+ @placement={{@placement}}
337
+ @shiftOptions={{@shiftOptions}}
338
+ @strategy={{@strategy}}
339
+ @inline={{@inline}}
340
+ as |p|
341
+ >
342
+ {{#let
343
+ (modifier
344
+ trigger
345
+ triggerElement=triggerEl
346
+ isOpen=isOpen
347
+ contentId=this.contentId
348
+ setReference=p.setReference
349
+ )
350
+ as |triggerModifier|
351
+ }}
352
+ {{yield
353
+ (hash
354
+ trigger=triggerModifier
355
+ Trigger=(component Trigger triggerModifier=triggerModifier)
356
+ Content=(component
357
+ Content
358
+ PopoverContent=p.Content
359
+ isOpen=isOpen
360
+ triggerElement=triggerEl
361
+ contentId=this.contentId
362
+ )
363
+ arrow=p.arrow
364
+ isOpen=isOpen.current
365
+ )
366
+ }}
367
+ {{/let}}
368
+ </Popover>
369
+ {{/let}}
370
+ </template>
371
+ }
372
+
373
+ export default Menu;
@@ -0,0 +1,31 @@
1
+ import { assert } from "@ember/debug";
2
+ import { on } from "@ember/modifier";
3
+
4
+ import type { TOC } from "@ember/component/template-only";
5
+
6
+ const reset = (event: Event) => {
7
+ assert("[BUG]: reset called without an event.target", event.target instanceof HTMLElement);
8
+
9
+ const form = event.target.closest("form");
10
+
11
+ assert(
12
+ "Form is missing. Cannot use <Reset> without being contained within a <form>",
13
+ form instanceof HTMLFormElement,
14
+ );
15
+
16
+ form.reset();
17
+ };
18
+
19
+ export const Submit: TOC<{
20
+ Element: HTMLButtonElement;
21
+ Blocks: { default: [] };
22
+ }> = <template>
23
+ <button type="submit" ...attributes>Submit</button>
24
+ </template>;
25
+
26
+ export const Reset: TOC<{
27
+ Element: HTMLButtonElement;
28
+ Blocks: { default: [] };
29
+ }> = <template>
30
+ <button type="button" {{on "click" reset}} ...attributes>{{yield}}</button>
31
+ </template>;
@@ -0,0 +1,198 @@
1
+ import Component from "@glimmer/component";
2
+ import { warn } from "@ember/debug";
3
+ import { isDestroyed, isDestroying } from "@ember/destroyable";
4
+ import { on } from "@ember/modifier";
5
+ import { buildWaiter } from "@ember/test-waiters";
6
+
7
+ import {
8
+ autoAdvance,
9
+ getCollectiveValue,
10
+ handleNavigation,
11
+ handlePaste,
12
+ selectAll,
13
+ } from "./utils.ts";
14
+
15
+ import type { TOC } from "@ember/component/template-only";
16
+ import type { WithBoundArgs } from "@glint/template";
17
+
18
+ const DEFAULT_LENGTH = 6;
19
+
20
+ function labelFor(inputIndex: number, labelFn: undefined | ((index: number) => string)) {
21
+ if (labelFn) {
22
+ return labelFn(inputIndex);
23
+ }
24
+
25
+ return `Please enter OTP character ${inputIndex + 1}`;
26
+ }
27
+
28
+ const waiter = buildWaiter("ember-primitives:OTPInput:handleChange");
29
+
30
+ const Fields: TOC<{
31
+ /**
32
+ * Any attributes passed to this component will be applied to each input.
33
+ */
34
+ Element: HTMLInputElement;
35
+ Args: {
36
+ fields: unknown[];
37
+ labelFn: (index: number) => string;
38
+ handleChange: (event: Event) => void;
39
+ };
40
+ }> = <template>
41
+ {{#each @fields as |_field i|}}
42
+ <label>
43
+ <span class="ember-primitives__sr-only">{{labelFor i @labelFn}}</span>
44
+ <input
45
+ name="code{{i}}"
46
+ type="text"
47
+ inputmode="numeric"
48
+ autocomplete="off"
49
+ ...attributes
50
+ {{on "click" selectAll}}
51
+ {{on "paste" handlePaste}}
52
+ {{on "input" autoAdvance}}
53
+ {{on "input" @handleChange}}
54
+ {{on "keydown" handleNavigation}}
55
+ />
56
+ </label>
57
+ {{/each}}
58
+ </template>;
59
+
60
+ export class OTPInput extends Component<{
61
+ /**
62
+ * The collection of individual OTP inputs are contained by a fieldset.
63
+ * Applying the `disabled` attribute to this fieldset will disable
64
+ * all of the inputs, if that's desired.
65
+ */
66
+ Element: HTMLFieldSetElement;
67
+ Args: {
68
+ /**
69
+ * How many characters the one-time-password field should be
70
+ * Defaults to 6
71
+ */
72
+ length?: number;
73
+
74
+ /**
75
+ * To Customize the label of the input fields, you may pass a function.
76
+ * By default, this is `Please enter OTP character ${index + 1}`.
77
+ */
78
+ labelFn?: (index: number) => string;
79
+
80
+ /**
81
+ * If passed, this function will be called when the <Input> changes.
82
+ * All fields are considered one input.
83
+ */
84
+ onChange?: (
85
+ data: {
86
+ /**
87
+ * The text from the collective `<Input>`
88
+ *
89
+ * `code` _may_ be shorter than `length`
90
+ * if the user has not finished typing / pasting their code
91
+ */
92
+ code: string;
93
+ /**
94
+ * will be `true` if `code`'s length matches the passed `@length` or the default of 6
95
+ */
96
+ complete: boolean;
97
+ },
98
+ /**
99
+ * The last input event received
100
+ */
101
+ event: Event,
102
+ ) => void;
103
+ };
104
+ Blocks: {
105
+ /**
106
+ * Optionally, you may control how the Fields are rendered, with proceeding text,
107
+ * additional attributes added, etc.
108
+ *
109
+ * This is how you can add custom validation to each input field.
110
+ */
111
+ default?: [fields: WithBoundArgs<typeof Fields, "fields" | "handleChange" | "labelFn">];
112
+ };
113
+ }> {
114
+ /**
115
+ * This is debounced, because we bind to each input,
116
+ * but only want to emit one change event if someone pastes
117
+ * multiple characters
118
+ */
119
+ handleChange = (event: Event) => {
120
+ if (!this.args.onChange) return;
121
+
122
+ if (!this.#token) {
123
+ this.#token = waiter.beginAsync();
124
+ }
125
+
126
+ if (this.#frame) {
127
+ cancelAnimationFrame(this.#frame);
128
+ }
129
+
130
+ // We use requestAnimationFrame to be friendly to rendering.
131
+ // We don't know if onChange is going to want to cause paints
132
+ // (it's also how we debounce, under the assumption that "paste" behavior
133
+ // would be fast enough to be quicker than individual frames
134
+ // (see logic in autoAdvance)
135
+ // )
136
+ this.#frame = requestAnimationFrame(() => {
137
+ waiter.endAsync(this.#token);
138
+
139
+ if (isDestroyed(this) || isDestroying(this)) return;
140
+ if (!this.args.onChange) return;
141
+
142
+ const value = getCollectiveValue(event.target, this.length);
143
+
144
+ if (value === undefined) {
145
+ warn(`Value could not be determined for the OTP field. was it removed from the DOM?`, {
146
+ id: "ember-primitives.OTPInput.missing-value",
147
+ });
148
+
149
+ return;
150
+ }
151
+
152
+ this.args.onChange({ code: value, complete: value.length === this.length }, event);
153
+ });
154
+ };
155
+
156
+ #token: unknown;
157
+ #frame: number | undefined;
158
+
159
+ get length() {
160
+ return this.args.length ?? DEFAULT_LENGTH;
161
+ }
162
+
163
+ get fields() {
164
+ // We only need to iterate a number of times,
165
+ // so we don't care about the actual value or
166
+ // referential integrity here
167
+ return new Array<undefined>(this.length);
168
+ }
169
+
170
+ <template>
171
+ <fieldset ...attributes>
172
+ {{#let
173
+ (component Fields fields=this.fields handleChange=this.handleChange labelFn=@labelFn)
174
+ as |CurriedFields|
175
+ }}
176
+ {{#if (has-block)}}
177
+ {{yield CurriedFields}}
178
+ {{else}}
179
+ <CurriedFields />
180
+ {{/if}}
181
+ {{/let}}
182
+
183
+ <style>
184
+ .ember-primitives__sr-only {
185
+ position: absolute;
186
+ width: 1px;
187
+ height: 1px;
188
+ padding: 0;
189
+ margin: -1px;
190
+ overflow: hidden;
191
+ clip: rect(0, 0, 0, 0);
192
+ white-space: nowrap;
193
+ border-width: 0;
194
+ }
195
+ </style>
196
+ </fieldset>
197
+ </template>
198
+ }