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,130 @@
|
|
|
1
|
+
import { assert } from "@ember/debug";
|
|
2
|
+
import { fn, hash } from "@ember/helper";
|
|
3
|
+
import { on } from "@ember/modifier";
|
|
4
|
+
import { buildWaiter } from "@ember/test-waiters";
|
|
5
|
+
|
|
6
|
+
import { Reset, Submit } from "./buttons.gts";
|
|
7
|
+
import { OTPInput } from "./input.gts";
|
|
8
|
+
|
|
9
|
+
import type { TOC } from "@ember/component/template-only";
|
|
10
|
+
import type { WithBoundArgs } from "@glint/template";
|
|
11
|
+
|
|
12
|
+
const waiter = buildWaiter("ember-primitives:OTP:handleAutoSubmitAttempt");
|
|
13
|
+
|
|
14
|
+
const handleFormSubmit = (submit: (data: { code: string }) => void, event: SubmitEvent) => {
|
|
15
|
+
event.preventDefault();
|
|
16
|
+
|
|
17
|
+
assert(
|
|
18
|
+
"[BUG]: handleFormSubmit was not attached to a form. Please open an issue.",
|
|
19
|
+
event.currentTarget instanceof HTMLFormElement,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const formData = new FormData(event.currentTarget);
|
|
23
|
+
|
|
24
|
+
let code = "";
|
|
25
|
+
|
|
26
|
+
for (const [key, value] of formData.entries()) {
|
|
27
|
+
if (key.startsWith("code")) {
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands, @typescript-eslint/no-base-to-string
|
|
29
|
+
code += value;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
submit({
|
|
34
|
+
code,
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function handleChange(
|
|
39
|
+
autoSubmit: boolean | undefined,
|
|
40
|
+
data: { code: string; complete: boolean },
|
|
41
|
+
event: Event,
|
|
42
|
+
) {
|
|
43
|
+
if (!autoSubmit) return;
|
|
44
|
+
if (!data.complete) return;
|
|
45
|
+
|
|
46
|
+
assert(
|
|
47
|
+
"[BUG]: event target is not a known element type",
|
|
48
|
+
event.target instanceof HTMLElement || event.target instanceof SVGElement,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const form = event.target.closest("form");
|
|
52
|
+
|
|
53
|
+
assert("[BUG]: Cannot handle event when <OTP> Inputs are not rendered within their <form>", form);
|
|
54
|
+
|
|
55
|
+
const token = waiter.beginAsync();
|
|
56
|
+
const finished = () => {
|
|
57
|
+
waiter.endAsync(token);
|
|
58
|
+
form.removeEventListener("submit", finished);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
form.addEventListener("submit", finished);
|
|
62
|
+
|
|
63
|
+
// NOTE: when calling .submit() the submit event handlers are not run
|
|
64
|
+
form.requestSubmit();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const OTP: TOC<{
|
|
68
|
+
/**
|
|
69
|
+
* The overall OTP Input is in its own form.
|
|
70
|
+
* Modern UI/UX Patterns usually have this sort of field
|
|
71
|
+
* as its own page, thus within its own form.
|
|
72
|
+
*
|
|
73
|
+
* By default, only the 'submit' event is bound, and is
|
|
74
|
+
* what calls the `@onSubmit` argument.
|
|
75
|
+
*/
|
|
76
|
+
Element: HTMLFormElement;
|
|
77
|
+
Args: {
|
|
78
|
+
/**
|
|
79
|
+
* How many characters the one-time-password field should be
|
|
80
|
+
* Defaults to 6
|
|
81
|
+
*/
|
|
82
|
+
length?: number;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The on submit callback will give you the entered
|
|
86
|
+
* one-time-password code.
|
|
87
|
+
*
|
|
88
|
+
* It will be called when the user manually clicks the 'submit'
|
|
89
|
+
* button or when the full code is pasted and meats the validation
|
|
90
|
+
* criteria.
|
|
91
|
+
*/
|
|
92
|
+
onSubmit: (data: { code: string }) => void;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Whether or not to auto-submit after the code has been pasted
|
|
96
|
+
* in to the collective "field". Default is true
|
|
97
|
+
*/
|
|
98
|
+
autoSubmit?: boolean;
|
|
99
|
+
};
|
|
100
|
+
Blocks: {
|
|
101
|
+
default: [
|
|
102
|
+
{
|
|
103
|
+
/**
|
|
104
|
+
* The collective input field that the OTP code will be typed/pasted in to
|
|
105
|
+
*/
|
|
106
|
+
Input: WithBoundArgs<typeof OTPInput, "length" | "onChange">;
|
|
107
|
+
/**
|
|
108
|
+
* Button with `type="submit"` to submit the form
|
|
109
|
+
*/
|
|
110
|
+
Submit: typeof Submit;
|
|
111
|
+
/**
|
|
112
|
+
* Pre-wired button to reset the form
|
|
113
|
+
*/
|
|
114
|
+
Reset: typeof Reset;
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
};
|
|
118
|
+
}> = <template>
|
|
119
|
+
<form {{on "submit" (fn handleFormSubmit @onSubmit)}} ...attributes>
|
|
120
|
+
{{yield
|
|
121
|
+
(hash
|
|
122
|
+
Input=(component
|
|
123
|
+
OTPInput length=@length onChange=(if @autoSubmit (fn handleChange @autoSubmit))
|
|
124
|
+
)
|
|
125
|
+
Submit=Submit
|
|
126
|
+
Reset=Reset
|
|
127
|
+
)
|
|
128
|
+
}}
|
|
129
|
+
</form>
|
|
130
|
+
</template>;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { assert } from '@ember/debug';
|
|
2
|
+
|
|
3
|
+
function getInputs(current: HTMLInputElement) {
|
|
4
|
+
const fieldset = current.closest('fieldset');
|
|
5
|
+
|
|
6
|
+
assert('[BUG]: fieldset went missing', fieldset);
|
|
7
|
+
|
|
8
|
+
return [...fieldset.querySelectorAll('input')];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function nextInput(current: HTMLInputElement) {
|
|
12
|
+
const inputs = getInputs(current);
|
|
13
|
+
const currentIndex = inputs.indexOf(current);
|
|
14
|
+
|
|
15
|
+
return inputs[currentIndex + 1];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function selectAll(event: Event) {
|
|
19
|
+
const target = event.target;
|
|
20
|
+
|
|
21
|
+
assert(`selectAll is only meant for use with input elements`, target instanceof HTMLInputElement);
|
|
22
|
+
|
|
23
|
+
target.select();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function handlePaste(event: Event) {
|
|
27
|
+
const target = event.target;
|
|
28
|
+
|
|
29
|
+
assert(
|
|
30
|
+
`handlePaste is only meant for use with input elements`,
|
|
31
|
+
target instanceof HTMLInputElement
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const clipboardData = (event as ClipboardEvent).clipboardData;
|
|
35
|
+
|
|
36
|
+
assert(
|
|
37
|
+
`Could not get clipboardData while handling the paste event on OTP. Please report this issue on the ember-primitives repo with a reproduction. Thanks!`,
|
|
38
|
+
clipboardData
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// This is typically not good to prevent paste.
|
|
42
|
+
// But because of the UX we're implementing,
|
|
43
|
+
// we want to split the pasted value across
|
|
44
|
+
// multiple text fields
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
|
|
47
|
+
const value = clipboardData.getData('Text');
|
|
48
|
+
const digits = value;
|
|
49
|
+
let i = 0;
|
|
50
|
+
let currElement: HTMLInputElement | null = target;
|
|
51
|
+
|
|
52
|
+
while (currElement) {
|
|
53
|
+
currElement.value = digits[i++] || '';
|
|
54
|
+
|
|
55
|
+
const next = nextInput(currElement);
|
|
56
|
+
|
|
57
|
+
if (next instanceof HTMLInputElement) {
|
|
58
|
+
currElement = next;
|
|
59
|
+
} else {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// We want to select the first field again
|
|
65
|
+
// so that if someone holds paste, or
|
|
66
|
+
// pastes again, they get the same result.
|
|
67
|
+
target.select();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function handleNavigation(event: KeyboardEvent) {
|
|
71
|
+
switch (event.key) {
|
|
72
|
+
case 'Backspace':
|
|
73
|
+
return handleBackspace(event);
|
|
74
|
+
case 'ArrowLeft':
|
|
75
|
+
return focusLeft(event);
|
|
76
|
+
case 'ArrowRight':
|
|
77
|
+
return focusRight(event);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function focusLeft(event: Pick<Event, 'target'>) {
|
|
82
|
+
const target = event.target;
|
|
83
|
+
|
|
84
|
+
assert(`only allowed on input elements`, target instanceof HTMLInputElement);
|
|
85
|
+
|
|
86
|
+
const input = previousInput(target);
|
|
87
|
+
|
|
88
|
+
input?.focus();
|
|
89
|
+
requestAnimationFrame(() => {
|
|
90
|
+
input?.select();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function focusRight(event: Pick<Event, 'target'>) {
|
|
95
|
+
const target = event.target;
|
|
96
|
+
|
|
97
|
+
assert(`only allowed on input elements`, target instanceof HTMLInputElement);
|
|
98
|
+
|
|
99
|
+
const input = nextInput(target);
|
|
100
|
+
|
|
101
|
+
input?.focus();
|
|
102
|
+
requestAnimationFrame(() => {
|
|
103
|
+
input?.select();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const syntheticEvent = new InputEvent('input');
|
|
108
|
+
|
|
109
|
+
function handleBackspace(event: KeyboardEvent) {
|
|
110
|
+
if (event.key !== 'Backspace') return;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* We have to prevent default because we
|
|
114
|
+
* - want to clear the whole field
|
|
115
|
+
* - have the focus behavior keep up with the key-repeat
|
|
116
|
+
* speed of the user's computer
|
|
117
|
+
*/
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
|
|
120
|
+
const target = event.target;
|
|
121
|
+
|
|
122
|
+
if (target && 'value' in target) {
|
|
123
|
+
if (target.value === '') {
|
|
124
|
+
focusLeft({ target });
|
|
125
|
+
} else {
|
|
126
|
+
target.value = '';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
target?.dispatchEvent(syntheticEvent);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function previousInput(current: HTMLInputElement) {
|
|
134
|
+
const inputs = getInputs(current);
|
|
135
|
+
const currentIndex = inputs.indexOf(current);
|
|
136
|
+
|
|
137
|
+
return inputs[currentIndex - 1];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const autoAdvance = (event: Event) => {
|
|
141
|
+
assert(
|
|
142
|
+
'[BUG]: autoAdvance called on non-input element',
|
|
143
|
+
event.target instanceof HTMLInputElement
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const value = event.target.value;
|
|
147
|
+
|
|
148
|
+
if (value.length === 0) return;
|
|
149
|
+
|
|
150
|
+
if (value.length > 0) {
|
|
151
|
+
if ('data' in event && event.data && typeof event.data === 'string') {
|
|
152
|
+
event.target.value = event.data;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return focusRight(event);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export function getCollectiveValue(elementTarget: EventTarget | null, length: number) {
|
|
160
|
+
if (!elementTarget) return;
|
|
161
|
+
|
|
162
|
+
assert(
|
|
163
|
+
`[BUG]: somehow the element target is not HTMLElement`,
|
|
164
|
+
elementTarget instanceof HTMLElement
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
let parent: null | HTMLElement | ShadowRoot;
|
|
168
|
+
|
|
169
|
+
// TODO: should this logic be extracted?
|
|
170
|
+
// why is getting the target element within a shadow root hard?
|
|
171
|
+
if (!(elementTarget instanceof HTMLInputElement)) {
|
|
172
|
+
if (elementTarget.shadowRoot) {
|
|
173
|
+
parent = elementTarget.shadowRoot;
|
|
174
|
+
} else {
|
|
175
|
+
parent = elementTarget.closest('fieldset');
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
parent = elementTarget.closest('fieldset');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
assert(`[BUG]: somehow the input fields were rendered without a parent element`, parent);
|
|
182
|
+
|
|
183
|
+
const elements = parent.querySelectorAll('input');
|
|
184
|
+
|
|
185
|
+
let value = '';
|
|
186
|
+
|
|
187
|
+
assert(
|
|
188
|
+
`found elements (${elements.length}) do not match length (${length}). Was the same OTP input rendered more than once?`,
|
|
189
|
+
elements.length === length
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
for (const element of elements) {
|
|
193
|
+
assert(
|
|
194
|
+
'[BUG]: how did the queried elements become a non-input element?',
|
|
195
|
+
element instanceof HTMLInputElement
|
|
196
|
+
);
|
|
197
|
+
value += element.value;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { hash } from "@ember/helper";
|
|
2
|
+
|
|
3
|
+
import { arrow } from "@floating-ui/dom";
|
|
4
|
+
import { element } from "ember-element-helper";
|
|
5
|
+
import { modifier as eModifier } from "ember-modifier";
|
|
6
|
+
import { cell } from "ember-resources";
|
|
7
|
+
|
|
8
|
+
import { FloatingUI } from "../floating-ui.ts";
|
|
9
|
+
import { Portal } from "./portal.gts";
|
|
10
|
+
import { TARGETS } from "./portal-targets.gts";
|
|
11
|
+
|
|
12
|
+
import type { Signature as FloatingUiComponentSignature } from "../floating-ui/component.ts";
|
|
13
|
+
import type { Signature as HookSignature } from "../floating-ui/modifier.ts";
|
|
14
|
+
import type { TOC } from "@ember/component/template-only";
|
|
15
|
+
import type { ElementContext, Middleware } from "@floating-ui/dom";
|
|
16
|
+
import type { ModifierLike, WithBoundArgs } from "@glint/template";
|
|
17
|
+
|
|
18
|
+
export interface Signature {
|
|
19
|
+
Args: {
|
|
20
|
+
/**
|
|
21
|
+
* See the Floating UI's [flip docs](https://floating-ui.com/docs/flip) for possible values.
|
|
22
|
+
*
|
|
23
|
+
* This argument is forwarded to the `<FloatingUI>` component.
|
|
24
|
+
*/
|
|
25
|
+
flipOptions?: HookSignature["Args"]["Named"]["flipOptions"];
|
|
26
|
+
/**
|
|
27
|
+
* Array of one or more objects to add to Floating UI's list of [middleware](https://floating-ui.com/docs/middleware)
|
|
28
|
+
*
|
|
29
|
+
* This argument is forwarded to the `<FloatingUI>` component.
|
|
30
|
+
*/
|
|
31
|
+
middleware?: HookSignature["Args"]["Named"]["middleware"];
|
|
32
|
+
/**
|
|
33
|
+
* See the Floating UI's [offset docs](https://floating-ui.com/docs/offset) for possible values.
|
|
34
|
+
*
|
|
35
|
+
* This argument is forwarded to the `<FloatingUI>` component.
|
|
36
|
+
*/
|
|
37
|
+
offsetOptions?: HookSignature["Args"]["Named"]["offsetOptions"];
|
|
38
|
+
/**
|
|
39
|
+
* One of the possible [`placements`](https://floating-ui.com/docs/computeposition#placement). The default is 'bottom'.
|
|
40
|
+
*
|
|
41
|
+
* Possible values are
|
|
42
|
+
* - top
|
|
43
|
+
* - bottom
|
|
44
|
+
* - right
|
|
45
|
+
* - left
|
|
46
|
+
*
|
|
47
|
+
* And may optionally have `-start` or `-end` added to adjust position along the side.
|
|
48
|
+
*
|
|
49
|
+
* This argument is forwarded to the `<FloatingUI>` component.
|
|
50
|
+
*/
|
|
51
|
+
placement?: `${"top" | "bottom" | "left" | "right"}${"" | "-start" | "-end"}`;
|
|
52
|
+
/**
|
|
53
|
+
* See the Floating UI's [shift docs](https://floating-ui.com/docs/shift) for possible values.
|
|
54
|
+
*
|
|
55
|
+
* This argument is forwarded to the `<FloatingUI>` component.
|
|
56
|
+
*/
|
|
57
|
+
shiftOptions?: HookSignature["Args"]["Named"]["shiftOptions"];
|
|
58
|
+
/**
|
|
59
|
+
* CSS position property, either `fixed` or `absolute`.
|
|
60
|
+
*
|
|
61
|
+
* Pros and cons of each strategy are explained on [Floating UI's Docs](https://floating-ui.com/docs/computePosition#strategy)
|
|
62
|
+
*
|
|
63
|
+
* This argument is forwarded to the `<FloatingUI>` component.
|
|
64
|
+
*/
|
|
65
|
+
strategy?: HookSignature["Args"]["Named"]["strategy"];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* By default, the popover is portaled.
|
|
69
|
+
* If you don't control your CSS, and the positioning of the popover content
|
|
70
|
+
* is misbehaving, you may pass "@inline={{true}}" to opt out of portalling.
|
|
71
|
+
*
|
|
72
|
+
* Inline may also be useful in nested menus, where you know exactly how the nesting occurs
|
|
73
|
+
*/
|
|
74
|
+
inline?: boolean;
|
|
75
|
+
};
|
|
76
|
+
Blocks: {
|
|
77
|
+
default: [
|
|
78
|
+
{
|
|
79
|
+
reference: FloatingUiComponentSignature["Blocks"]["default"][0];
|
|
80
|
+
setReference: FloatingUiComponentSignature["Blocks"]["default"][2]["setReference"];
|
|
81
|
+
Content: WithBoundArgs<typeof Content, "floating">;
|
|
82
|
+
data: FloatingUiComponentSignature["Blocks"]["default"][2]["data"];
|
|
83
|
+
arrow: ModifierLike<{ Element: HTMLElement }>;
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getElementTag(tagName: undefined | string) {
|
|
90
|
+
return tagName || "div";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Allows lazy evaluation of the portal target (do nothing until rendered)
|
|
95
|
+
* This is useful because the algorithm for finding the portal target isn't cheap.
|
|
96
|
+
*/
|
|
97
|
+
const Content: TOC<{
|
|
98
|
+
Element: HTMLDivElement;
|
|
99
|
+
Args: {
|
|
100
|
+
floating: ModifierLike<{ Element: HTMLElement }>;
|
|
101
|
+
inline?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* By default the popover content is wrapped in a div.
|
|
104
|
+
* You may change this by supplying the name of an element here.
|
|
105
|
+
*
|
|
106
|
+
* For example:
|
|
107
|
+
* ```gjs
|
|
108
|
+
* <Popover as |p|>
|
|
109
|
+
* <p.Content @as="dialog">
|
|
110
|
+
* this is now focus trapped
|
|
111
|
+
* </p.Content>
|
|
112
|
+
* </Popover>
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
as?: string;
|
|
116
|
+
};
|
|
117
|
+
Blocks: { default: [] };
|
|
118
|
+
}> = <template>
|
|
119
|
+
{{#let (element (getElementTag @as)) as |El|}}
|
|
120
|
+
{{#if @inline}}
|
|
121
|
+
{{! @glint-ignore
|
|
122
|
+
https://github.com/tildeio/ember-element-helper/issues/91
|
|
123
|
+
https://github.com/typed-ember/glint/issues/610
|
|
124
|
+
}}
|
|
125
|
+
<El {{@floating}} ...attributes>
|
|
126
|
+
{{yield}}
|
|
127
|
+
</El>
|
|
128
|
+
{{else}}
|
|
129
|
+
<Portal @to={{TARGETS.popover}}>
|
|
130
|
+
{{! @glint-ignore
|
|
131
|
+
https://github.com/tildeio/ember-element-helper/issues/91
|
|
132
|
+
https://github.com/typed-ember/glint/issues/610
|
|
133
|
+
}}
|
|
134
|
+
<El {{@floating}} ...attributes>
|
|
135
|
+
{{yield}}
|
|
136
|
+
</El>
|
|
137
|
+
</Portal>
|
|
138
|
+
{{/if}}
|
|
139
|
+
{{/let}}
|
|
140
|
+
</template>;
|
|
141
|
+
|
|
142
|
+
interface AttachArrowSignature {
|
|
143
|
+
Element: HTMLElement;
|
|
144
|
+
Args: {
|
|
145
|
+
Named: {
|
|
146
|
+
arrowElement: ReturnType<typeof ArrowElement>;
|
|
147
|
+
data:
|
|
148
|
+
| undefined
|
|
149
|
+
| {
|
|
150
|
+
placement: string;
|
|
151
|
+
middlewareData?: {
|
|
152
|
+
arrow?: { x?: number; y?: number };
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const arrowSides = {
|
|
160
|
+
top: "bottom",
|
|
161
|
+
right: "left",
|
|
162
|
+
bottom: "top",
|
|
163
|
+
left: "right",
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
type Direction = "top" | "bottom" | "left" | "right";
|
|
167
|
+
type Placement = `${Direction}${"" | "-start" | "-end"}`;
|
|
168
|
+
|
|
169
|
+
const attachArrow: ModifierLike<AttachArrowSignature> = eModifier<AttachArrowSignature>(
|
|
170
|
+
(element, _: [], named) => {
|
|
171
|
+
if (element === named.arrowElement.current) {
|
|
172
|
+
if (!named.data) return;
|
|
173
|
+
if (!named.data.middlewareData) return;
|
|
174
|
+
|
|
175
|
+
const { arrow } = named.data.middlewareData;
|
|
176
|
+
const { placement } = named.data;
|
|
177
|
+
|
|
178
|
+
if (!arrow) return;
|
|
179
|
+
if (!placement) return;
|
|
180
|
+
|
|
181
|
+
const { x: arrowX, y: arrowY } = arrow;
|
|
182
|
+
const otherSide = (placement as Placement).split("-")[0] as Direction;
|
|
183
|
+
const staticSide = arrowSides[otherSide];
|
|
184
|
+
|
|
185
|
+
Object.assign(named.arrowElement.current.style, {
|
|
186
|
+
left: arrowX != null ? `${arrowX}px` : "",
|
|
187
|
+
top: arrowY != null ? `${arrowY}px` : "",
|
|
188
|
+
right: "",
|
|
189
|
+
bottom: "",
|
|
190
|
+
[staticSide]: "-4px",
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
void (async () => {
|
|
197
|
+
await Promise.resolve();
|
|
198
|
+
named.arrowElement.set(element);
|
|
199
|
+
})();
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const ArrowElement: () => ReturnType<typeof cell<HTMLElement>> = () => cell<HTMLElement>();
|
|
204
|
+
|
|
205
|
+
function maybeAddArrow(middleware: Middleware[] | undefined, element: Element | undefined) {
|
|
206
|
+
const result = [...(middleware || [])];
|
|
207
|
+
|
|
208
|
+
if (element) {
|
|
209
|
+
result.push(arrow({ element }));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function flipOptions(options: HookSignature["Args"]["Named"]["flipOptions"]) {
|
|
216
|
+
return {
|
|
217
|
+
elementContext: "reference" as ElementContext,
|
|
218
|
+
...options,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const Popover: TOC<Signature> = <template>
|
|
223
|
+
{{#let (ArrowElement) as |arrowElement|}}
|
|
224
|
+
<FloatingUI
|
|
225
|
+
@placement={{@placement}}
|
|
226
|
+
@strategy={{@strategy}}
|
|
227
|
+
@middleware={{maybeAddArrow @middleware arrowElement.current}}
|
|
228
|
+
@flipOptions={{flipOptions @flipOptions}}
|
|
229
|
+
@shiftOptions={{@shiftOptions}}
|
|
230
|
+
@offsetOptions={{@offsetOptions}}
|
|
231
|
+
as |reference floating extra|
|
|
232
|
+
>
|
|
233
|
+
{{#let (modifier attachArrow arrowElement=arrowElement data=extra.data) as |arrow|}}
|
|
234
|
+
{{yield
|
|
235
|
+
(hash
|
|
236
|
+
reference=reference
|
|
237
|
+
setReference=extra.setReference
|
|
238
|
+
Content=(component Content floating=floating inline=@inline)
|
|
239
|
+
data=extra.data
|
|
240
|
+
arrow=arrow
|
|
241
|
+
)
|
|
242
|
+
}}
|
|
243
|
+
{{/let}}
|
|
244
|
+
</FloatingUI>
|
|
245
|
+
{{/let}}
|
|
246
|
+
</template>;
|
|
247
|
+
|
|
248
|
+
export default Popover;
|