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,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
|
+
}
|