ember-primitives 0.49.0 → 0.50.1
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/color-scheme.d.ts +1 -1
- package/declarations/color-scheme.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/dist/color-scheme.js +13 -4
- package/dist/color-scheme.js.map +1 -1
- package/dist/components/rating.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{rating-CjBVsX6q.js → rating-BrIiwDLw.js} +21 -17
- package/dist/rating-BrIiwDLw.js.map +1 -0
- package/package.json +6 -2
- package/src/-private.ts +4 -0
- package/src/color-scheme.ts +177 -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/rating-CjBVsX6q.js.map +0 -1
|
@@ -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;
|
|
@@ -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;
|