@atlaskit/top-layer 0.1.5 → 0.3.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/CHANGELOG.md +33 -0
- package/dist/cjs/arrow/index.js +1 -1
- package/dist/cjs/internal/anchor-positioning-fallback.js +82 -22
- package/dist/cjs/internal/resolve-css-length-to-pixels.js +53 -0
- package/dist/cjs/internal/resolve-css-length.js +18 -0
- package/dist/cjs/internal/resolve-placement.js +47 -6
- package/dist/cjs/internal/use-anchor-positioning.js +180 -36
- package/dist/cjs/placement-map/index.js +31 -3
- package/dist/cjs/popup/popup-content.js +0 -2
- package/dist/cjs/testing/polyfill.js +347 -0
- package/dist/cjs/testing/to-be-visible.js +92 -0
- package/dist/es2019/arrow/index.js +22 -3
- package/dist/es2019/internal/anchor-positioning-fallback.js +80 -17
- package/dist/es2019/internal/resolve-css-length-to-pixels.js +48 -0
- package/dist/es2019/internal/resolve-css-length.js +13 -0
- package/dist/es2019/internal/resolve-placement.js +48 -6
- package/dist/es2019/internal/use-anchor-positioning.js +173 -28
- package/dist/es2019/placement-map/index.js +25 -3
- package/dist/es2019/popup/popup-content.js +0 -2
- package/dist/es2019/testing/polyfill.js +351 -0
- package/dist/es2019/testing/to-be-visible.js +85 -0
- package/dist/esm/arrow/index.js +1 -1
- package/dist/esm/internal/anchor-positioning-fallback.js +82 -22
- package/dist/esm/internal/resolve-css-length-to-pixels.js +47 -0
- package/dist/esm/internal/resolve-css-length.js +12 -0
- package/dist/esm/internal/resolve-placement.js +48 -6
- package/dist/esm/internal/use-anchor-positioning.js +179 -36
- package/dist/esm/placement-map/index.js +30 -3
- package/dist/esm/popup/popup-content.js +0 -2
- package/dist/esm/testing/polyfill.js +345 -0
- package/dist/esm/testing/to-be-visible.js +89 -0
- package/dist/types/internal/anchor-positioning-fallback.d.ts +11 -9
- package/dist/types/internal/resolve-css-length-to-pixels.d.ts +22 -0
- package/dist/types/internal/resolve-css-length.d.ts +8 -0
- package/dist/types/internal/resolve-placement.d.ts +47 -5
- package/dist/types/internal/use-anchor-positioning.d.ts +18 -7
- package/dist/types/placement-map/index.d.ts +11 -5
- package/dist/types/popup/types.d.ts +15 -30
- package/dist/types/testing/polyfill.d.ts +48 -0
- package/dist/types/testing/to-be-visible.d.ts +23 -0
- package/dist/types-ts4.5/internal/anchor-positioning-fallback.d.ts +11 -9
- package/dist/types-ts4.5/internal/resolve-css-length-to-pixels.d.ts +22 -0
- package/dist/types-ts4.5/internal/resolve-css-length.d.ts +8 -0
- package/dist/types-ts4.5/internal/resolve-placement.d.ts +47 -5
- package/dist/types-ts4.5/internal/use-anchor-positioning.d.ts +18 -7
- package/dist/types-ts4.5/placement-map/index.d.ts +14 -5
- package/dist/types-ts4.5/popup/types.d.ts +15 -30
- package/dist/types-ts4.5/testing/polyfill.d.ts +48 -0
- package/dist/types-ts4.5/testing/to-be-visible.d.ts +23 -0
- package/package.json +3 -2
- package/testing/polyfill/package.json +17 -0
- package/testing/to-be-visible/package.json +17 -0
|
@@ -30,7 +30,6 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
|
|
|
30
30
|
placement: placementProp,
|
|
31
31
|
onClose: onCloseProp,
|
|
32
32
|
testId: testIdProp,
|
|
33
|
-
offset: offsetProp,
|
|
34
33
|
forceFallbackPositioning: forceFallbackPositioningProp
|
|
35
34
|
}, ref) {
|
|
36
35
|
var _ref, _ref2, _ctx$popoverRef;
|
|
@@ -129,7 +128,6 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
|
|
|
129
128
|
},
|
|
130
129
|
popoverRef,
|
|
131
130
|
placement,
|
|
132
|
-
offset: offsetProp,
|
|
133
131
|
forceFallbackPositioning,
|
|
134
132
|
arrow: arrowPreset !== null && arrowPreset !== void 0 ? arrowPreset : undefined
|
|
135
133
|
});
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/* eslint-disable compat/compat */
|
|
2
|
+
/**
|
|
3
|
+
* Popover API & HTMLDialogElement polyfills for jsdom.
|
|
4
|
+
*
|
|
5
|
+
* jsdom does not implement the Popover API or the HTMLDialogElement methods
|
|
6
|
+
* (showModal, show, close). This polyfill provides faithful implementations,
|
|
7
|
+
* including timing semantics validated against Chromium via Playwright.
|
|
8
|
+
*
|
|
9
|
+
* Behaviour:
|
|
10
|
+
* - beforetoggle: sync, cancelable on show only. toggle: async task, not cancelable.
|
|
11
|
+
* - Rapid show/hide coalesces into one toggle event (originalOldState to finalNewState).
|
|
12
|
+
* - Dialog: showModal/show fire beforetoggle+toggle. close fires toggle then close event.
|
|
13
|
+
* - Popover open state tracked via `data-popover-open` (no `:popover-open` in jsdom).
|
|
14
|
+
*
|
|
15
|
+
* Companion `toBeVisible` patch lives at `./to-be-visible`.
|
|
16
|
+
*
|
|
17
|
+
* Importing this module has the side-effect of patching the relevant
|
|
18
|
+
* prototypes. It is safe to import multiple times; each branch is guarded
|
|
19
|
+
* to avoid overwriting native implementations or re-patching.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* e.g. `<div#my-id.class-a.class-b>`
|
|
24
|
+
*/
|
|
25
|
+
function describeElement({
|
|
26
|
+
element
|
|
27
|
+
}) {
|
|
28
|
+
const tag = element.tagName ? element.tagName.toLowerCase() : 'unknown';
|
|
29
|
+
const id = element.id ? `#${element.id}` : '';
|
|
30
|
+
const classes = element.classList && element.classList.length > 0 ? `.${Array.from(element.classList).join('.')}` : '';
|
|
31
|
+
return `<${tag}${id}${classes}>`;
|
|
32
|
+
}
|
|
33
|
+
function createToggleEvent({
|
|
34
|
+
type,
|
|
35
|
+
oldState,
|
|
36
|
+
newState,
|
|
37
|
+
cancelable
|
|
38
|
+
}) {
|
|
39
|
+
if (typeof globalThis.ToggleEvent === 'function') {
|
|
40
|
+
// ToggleEvent is not in lib.dom.d.ts at the time of writing; cast to any for construction.
|
|
41
|
+
const ToggleEventCtor = globalThis.ToggleEvent;
|
|
42
|
+
return new ToggleEventCtor(type, {
|
|
43
|
+
oldState,
|
|
44
|
+
newState,
|
|
45
|
+
cancelable,
|
|
46
|
+
bubbles: false
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const event = new Event(type, {
|
|
50
|
+
cancelable,
|
|
51
|
+
bubbles: false
|
|
52
|
+
});
|
|
53
|
+
event.oldState = oldState;
|
|
54
|
+
event.newState = newState;
|
|
55
|
+
return event;
|
|
56
|
+
}
|
|
57
|
+
function fireToggleEvent({
|
|
58
|
+
element,
|
|
59
|
+
type,
|
|
60
|
+
oldState,
|
|
61
|
+
newState,
|
|
62
|
+
cancelable
|
|
63
|
+
}) {
|
|
64
|
+
return element.dispatchEvent(createToggleEvent({
|
|
65
|
+
type,
|
|
66
|
+
oldState,
|
|
67
|
+
newState,
|
|
68
|
+
cancelable
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
function oppositeState({
|
|
72
|
+
state
|
|
73
|
+
}) {
|
|
74
|
+
if (state === 'open') {
|
|
75
|
+
return 'closed';
|
|
76
|
+
}
|
|
77
|
+
return 'open';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Coalesces rapid show/hide into one toggle event per element.
|
|
81
|
+
// Uses setTimeout (task) not Promise.resolve (microtask) to match browser timing.
|
|
82
|
+
function createToggleScheduler() {
|
|
83
|
+
const pending = new Map();
|
|
84
|
+
return function schedule({
|
|
85
|
+
element,
|
|
86
|
+
newState
|
|
87
|
+
}) {
|
|
88
|
+
const existing = pending.get(element);
|
|
89
|
+
if (existing) {
|
|
90
|
+
// Coalesce: keep the original oldState, update newState
|
|
91
|
+
pending.set(element, {
|
|
92
|
+
oldState: existing.oldState,
|
|
93
|
+
newState
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
pending.set(element, {
|
|
98
|
+
oldState: oppositeState({
|
|
99
|
+
state: newState
|
|
100
|
+
}),
|
|
101
|
+
newState
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Task queue (setTimeout) — browser fires toggle after microtasks drain, not during
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
const entry = pending.get(element);
|
|
107
|
+
if (!entry) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
pending.delete(element);
|
|
111
|
+
fireToggleEvent({
|
|
112
|
+
element,
|
|
113
|
+
type: 'toggle',
|
|
114
|
+
oldState: entry.oldState,
|
|
115
|
+
newState: entry.newState,
|
|
116
|
+
cancelable: false
|
|
117
|
+
});
|
|
118
|
+
}, 0);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Like createToggleScheduler but also fires a close event as a separate task after toggle.
|
|
123
|
+
function createDialogToggleScheduler() {
|
|
124
|
+
const pending = new Map();
|
|
125
|
+
return function schedule({
|
|
126
|
+
dialog,
|
|
127
|
+
newState,
|
|
128
|
+
shouldFireCloseEvent
|
|
129
|
+
}) {
|
|
130
|
+
const existing = pending.get(dialog);
|
|
131
|
+
if (existing) {
|
|
132
|
+
// Coalesce: keep the original oldState, merge shouldFireCloseEvent
|
|
133
|
+
pending.set(dialog, {
|
|
134
|
+
oldState: existing.oldState,
|
|
135
|
+
newState,
|
|
136
|
+
shouldFireCloseEvent: existing.shouldFireCloseEvent || shouldFireCloseEvent
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
pending.set(dialog, {
|
|
141
|
+
oldState: oppositeState({
|
|
142
|
+
state: newState
|
|
143
|
+
}),
|
|
144
|
+
newState,
|
|
145
|
+
shouldFireCloseEvent
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Browser fires toggle as a task, not synchronously or as a microtask
|
|
149
|
+
setTimeout(() => {
|
|
150
|
+
const entry = pending.get(dialog);
|
|
151
|
+
if (!entry) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
pending.delete(dialog);
|
|
155
|
+
fireToggleEvent({
|
|
156
|
+
element: dialog,
|
|
157
|
+
type: 'toggle',
|
|
158
|
+
oldState: entry.oldState,
|
|
159
|
+
newState: entry.newState,
|
|
160
|
+
cancelable: false
|
|
161
|
+
});
|
|
162
|
+
if (entry.shouldFireCloseEvent) {
|
|
163
|
+
// Browser fires close as a separate task after toggle
|
|
164
|
+
setTimeout(() => {
|
|
165
|
+
dialog.dispatchEvent(new Event('close', {
|
|
166
|
+
bubbles: false,
|
|
167
|
+
cancelable: false
|
|
168
|
+
}));
|
|
169
|
+
}, 0);
|
|
170
|
+
}
|
|
171
|
+
}, 0);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Popover API ────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
if (typeof HTMLElement !== 'undefined' && !HTMLElement.prototype.showPopover) {
|
|
178
|
+
const schedulePopoverToggle = createToggleScheduler();
|
|
179
|
+
HTMLElement.prototype.showPopover = function showPopover() {
|
|
180
|
+
if (!this.hasAttribute('popover')) {
|
|
181
|
+
throw new DOMException(`Failed to execute 'showPopover' on 'HTMLElement': ${describeElement({
|
|
182
|
+
element: this
|
|
183
|
+
})} is not a popover element. Ensure the element has a 'popover' attribute (e.g. <div popover>).`, 'NotSupportedError');
|
|
184
|
+
}
|
|
185
|
+
if (!this.isConnected) {
|
|
186
|
+
throw new DOMException(`Failed to execute 'showPopover' on 'HTMLElement': ${describeElement({
|
|
187
|
+
element: this
|
|
188
|
+
})} is not connected to the document. Ensure the element has been added to the DOM (e.g. via render() or document.body.appendChild()) before calling showPopover().`, 'InvalidStateError');
|
|
189
|
+
}
|
|
190
|
+
if (this.hasAttribute('data-popover-open')) {
|
|
191
|
+
return; // already showing — no-op
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// sync, cancelable on show only
|
|
195
|
+
const allowed = fireToggleEvent({
|
|
196
|
+
element: this,
|
|
197
|
+
type: 'beforetoggle',
|
|
198
|
+
oldState: 'closed',
|
|
199
|
+
newState: 'open',
|
|
200
|
+
cancelable: true
|
|
201
|
+
});
|
|
202
|
+
if (!allowed) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
this.setAttribute('data-popover-open', '');
|
|
206
|
+
schedulePopoverToggle({
|
|
207
|
+
element: this,
|
|
208
|
+
newState: 'open'
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
HTMLElement.prototype.hidePopover = function hidePopover() {
|
|
212
|
+
if (!this.hasAttribute('popover')) {
|
|
213
|
+
throw new DOMException(`Failed to execute 'hidePopover' on 'HTMLElement': ${describeElement({
|
|
214
|
+
element: this
|
|
215
|
+
})} is not a popover element. Ensure the element has a 'popover' attribute (e.g. <div popover>).`, 'NotSupportedError');
|
|
216
|
+
}
|
|
217
|
+
// Browser does not throw for disconnected elements on hide — just no-op
|
|
218
|
+
if (!this.isConnected || !this.hasAttribute('data-popover-open')) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// not cancelable on hide
|
|
223
|
+
fireToggleEvent({
|
|
224
|
+
element: this,
|
|
225
|
+
type: 'beforetoggle',
|
|
226
|
+
oldState: 'open',
|
|
227
|
+
newState: 'closed',
|
|
228
|
+
cancelable: false
|
|
229
|
+
});
|
|
230
|
+
this.removeAttribute('data-popover-open');
|
|
231
|
+
schedulePopoverToggle({
|
|
232
|
+
element: this,
|
|
233
|
+
newState: 'closed'
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
HTMLElement.prototype.togglePopover = function togglePopover(force) {
|
|
237
|
+
const isOpen = this.hasAttribute('data-popover-open');
|
|
238
|
+
if (typeof force === 'boolean') {
|
|
239
|
+
if (force && !isOpen) {
|
|
240
|
+
this.showPopover();
|
|
241
|
+
} else if (!force && isOpen) {
|
|
242
|
+
this.hidePopover();
|
|
243
|
+
}
|
|
244
|
+
return force;
|
|
245
|
+
}
|
|
246
|
+
if (isOpen) {
|
|
247
|
+
this.hidePopover();
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
this.showPopover();
|
|
251
|
+
return true;
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── HTMLDialogElement ──────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
if (typeof HTMLDialogElement !== 'undefined') {
|
|
258
|
+
const proto = HTMLDialogElement.prototype;
|
|
259
|
+
const scheduleDialogToggle = createDialogToggleScheduler();
|
|
260
|
+
if (typeof proto.showModal !== 'function') {
|
|
261
|
+
proto.showModal = function showModal() {
|
|
262
|
+
if (!this.isConnected) {
|
|
263
|
+
throw new DOMException(`Failed to execute 'showModal' on 'HTMLDialogElement': ${describeElement({
|
|
264
|
+
element: this
|
|
265
|
+
})} is not connected to the document. Ensure the element has been added to the DOM (e.g. via render() or document.body.appendChild()) before calling showModal().`, 'InvalidStateError');
|
|
266
|
+
}
|
|
267
|
+
if (this.hasAttribute('open')) {
|
|
268
|
+
if (this._isModal) {
|
|
269
|
+
return; // already modal — no-op
|
|
270
|
+
}
|
|
271
|
+
throw new DOMException(`Failed to execute 'showModal' on 'HTMLDialogElement': ${describeElement({
|
|
272
|
+
element: this
|
|
273
|
+
})} already has an 'open' attribute, and therefore cannot be opened modally. The dialog was likely opened non-modally with .show(). Call .close() first, then .showModal().`, 'InvalidStateError');
|
|
274
|
+
}
|
|
275
|
+
fireToggleEvent({
|
|
276
|
+
element: this,
|
|
277
|
+
type: 'beforetoggle',
|
|
278
|
+
oldState: 'closed',
|
|
279
|
+
newState: 'open',
|
|
280
|
+
cancelable: false
|
|
281
|
+
});
|
|
282
|
+
this.setAttribute('open', '');
|
|
283
|
+
this._isModal = true;
|
|
284
|
+
scheduleDialogToggle({
|
|
285
|
+
dialog: this,
|
|
286
|
+
newState: 'open',
|
|
287
|
+
shouldFireCloseEvent: false
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (typeof proto.show !== 'function') {
|
|
292
|
+
proto.show = function show() {
|
|
293
|
+
// Browser does not throw for disconnected elements on show — just no-op
|
|
294
|
+
if (!this.isConnected || this.hasAttribute('open')) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
fireToggleEvent({
|
|
298
|
+
element: this,
|
|
299
|
+
type: 'beforetoggle',
|
|
300
|
+
oldState: 'closed',
|
|
301
|
+
newState: 'open',
|
|
302
|
+
cancelable: false
|
|
303
|
+
});
|
|
304
|
+
this.setAttribute('open', '');
|
|
305
|
+
this._isModal = false;
|
|
306
|
+
scheduleDialogToggle({
|
|
307
|
+
dialog: this,
|
|
308
|
+
newState: 'open',
|
|
309
|
+
shouldFireCloseEvent: false
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (typeof proto.close !== 'function') {
|
|
314
|
+
proto.close = function close(returnValue) {
|
|
315
|
+
if (!this.hasAttribute('open')) {
|
|
316
|
+
return; // already closed — no-op
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Sync beforetoggle (not cancelable) — matches browser behavior
|
|
320
|
+
fireToggleEvent({
|
|
321
|
+
element: this,
|
|
322
|
+
type: 'beforetoggle',
|
|
323
|
+
oldState: 'open',
|
|
324
|
+
newState: 'closed',
|
|
325
|
+
cancelable: false
|
|
326
|
+
});
|
|
327
|
+
this.removeAttribute('open');
|
|
328
|
+
this._isModal = false;
|
|
329
|
+
if (returnValue !== undefined) {
|
|
330
|
+
this.returnValue = returnValue;
|
|
331
|
+
}
|
|
332
|
+
scheduleDialogToggle({
|
|
333
|
+
dialog: this,
|
|
334
|
+
newState: 'closed',
|
|
335
|
+
shouldFireCloseEvent: true
|
|
336
|
+
});
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (!('returnValue' in proto)) {
|
|
340
|
+
Object.defineProperty(proto, 'returnValue', {
|
|
341
|
+
get() {
|
|
342
|
+
return this._returnValue === undefined ? '' : this._returnValue;
|
|
343
|
+
},
|
|
344
|
+
set(value) {
|
|
345
|
+
this._returnValue = String(value);
|
|
346
|
+
},
|
|
347
|
+
configurable: true,
|
|
348
|
+
enumerable: true
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/* eslint-disable compat/compat */
|
|
2
|
+
/* global expect */
|
|
3
|
+
/**
|
|
4
|
+
* Patches `toBeVisible()` to understand popover and dialog visibility.
|
|
5
|
+
*
|
|
6
|
+
* jest-dom's `toBeVisible()` does not know about the Popover API. In real
|
|
7
|
+
* browsers the UA stylesheet hides `[popover]` with `display: none`, but
|
|
8
|
+
* jsdom has no such stylesheet — so closed popovers incorrectly appear visible.
|
|
9
|
+
*
|
|
10
|
+
* Raw jest-dom results in jsdom:
|
|
11
|
+
*
|
|
12
|
+
* closed popover / child → true (WRONG — should be false)
|
|
13
|
+
* open popover / child → true (correct)
|
|
14
|
+
* closed dialog / child → false (correct — jsdom natively hides <dialog>)
|
|
15
|
+
* open dialog / child → true (correct)
|
|
16
|
+
*
|
|
17
|
+
* We correct both false positives and false negatives:
|
|
18
|
+
* 1. Delegate to the original first.
|
|
19
|
+
* 2. If it says "visible" → check for closed top-layer hosts (false positive).
|
|
20
|
+
* 3. If it says "not visible" → check for open top-layer hosts (false negative).
|
|
21
|
+
*
|
|
22
|
+
* Must load via `setupFilesAfterEnv` (after jest-dom registers its matchers).
|
|
23
|
+
* Polyfills live in `./polyfill` (loaded via `setupFiles`).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// Delegating to jest-dom's matcher; types come from @testing-library/jest-dom.
|
|
27
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
28
|
+
import { toBeVisible as originalToBeVisible } from '@testing-library/jest-dom/matchers';
|
|
29
|
+
/**
|
|
30
|
+
* Finds the nearest popover or dialog ancestor (including the element itself)
|
|
31
|
+
* and returns its state: 'open', 'closed', or null if no top-layer ancestor exists.
|
|
32
|
+
*
|
|
33
|
+
* Checking the *nearest* ancestor handles nesting correctly:
|
|
34
|
+
* a closed popover inside an open popover → nearest is closed → not visible.
|
|
35
|
+
*/
|
|
36
|
+
function nearestTopLayerState(element) {
|
|
37
|
+
if (!element) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (element.hasAttribute('popover')) {
|
|
41
|
+
return element.hasAttribute('data-popover-open') ? 'open' : 'closed';
|
|
42
|
+
}
|
|
43
|
+
if (element.nodeName === 'DIALOG') {
|
|
44
|
+
return element.hasAttribute('open') ? 'open' : 'closed';
|
|
45
|
+
}
|
|
46
|
+
return nearestTopLayerState(element.parentElement);
|
|
47
|
+
}
|
|
48
|
+
function getNearestTopLayerType(element) {
|
|
49
|
+
if (!element.closest) {
|
|
50
|
+
return 'top-layer element';
|
|
51
|
+
}
|
|
52
|
+
if (element.closest('[popover]')) {
|
|
53
|
+
return 'popover';
|
|
54
|
+
}
|
|
55
|
+
return 'dialog';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// `expect` is registered globally by jest before this file runs.
|
|
59
|
+
|
|
60
|
+
expect.extend({
|
|
61
|
+
toBeVisible(element) {
|
|
62
|
+
const originalResult = originalToBeVisible.call(this, element);
|
|
63
|
+
const topLayerState = nearestTopLayerState(element);
|
|
64
|
+
if (originalResult.pass) {
|
|
65
|
+
// Original says visible — override if nearest top-layer host is closed.
|
|
66
|
+
if (topLayerState === 'closed') {
|
|
67
|
+
return {
|
|
68
|
+
pass: false,
|
|
69
|
+
message: () => `expected element to be visible, but it is inside a closed ${getNearestTopLayerType(element)}`
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return originalResult;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Original says not visible — override if nearest top-layer host is open.
|
|
76
|
+
// jsdom may hide [popover] elements even when they are logically open.
|
|
77
|
+
if (topLayerState === 'open') {
|
|
78
|
+
return {
|
|
79
|
+
pass: true,
|
|
80
|
+
message: () => `expected element not to be visible, but it is inside an open ${getNearestTopLayerType(element)}`
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return originalResult;
|
|
84
|
+
}
|
|
85
|
+
});
|
package/dist/esm/arrow/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import { getPlacement, placementToPositionArea } from '../internal/use-anchor-po
|
|
|
19
19
|
*
|
|
20
20
|
* @see https://goulet.dev/posts/tooltip-with-popover-and-anchor-positioning/
|
|
21
21
|
*/
|
|
22
|
-
var ARROW_CSS = "\n[data-ds-popover-arrow] {\n clip-path: inset(var(--ds-arrow-offset, 0px)) margin-box;\n box-shadow: none;\n}\n\n/* Top and Bottom arrows (vertical hexagon) */\n[data-ds-popover-arrow]::before {\n content: \"\";\n position: absolute;\n z-index: -1;\n background: inherit;\n left: 50%;\n transform: translateX(-50%);\n width: calc(var(--ds-arrow-size, 8px) * 2);\n height: calc(100% + var(--ds-arrow-size, 8px) * 2);\n top: calc(var(--ds-arrow-size, 8px) * -1);\n clip-path: polygon(\n 0 var(--ds-arrow-size, 8px),\n 50% 0,\n 100% var(--ds-arrow-size, 8px),\n 100% calc(100% - var(--ds-arrow-size, 8px)),\n 50% 100%,\n 0 calc(100% - var(--ds-arrow-size, 8px))\n );\n}\n\n/* Left and Right arrows (horizontal hexagon) */\n[data-ds-popover-arrow]::after {\n content: \"\";\n position: absolute;\n z-index: -1;\n background: inherit;\n top: 50%;\n transform: translateY(-50%);\n height: calc(var(--ds-arrow-size, 8px) * 2);\n width: calc(100% + var(--ds-arrow-size, 8px) * 2);\n left: calc(var(--ds-arrow-size, 8px) * -1);\n clip-path: polygon(\n var(--ds-arrow-size, 8px) 0,\n calc(100% - var(--ds-arrow-size, 8px)) 0,\n 100% 50%,\n calc(100% - var(--ds-arrow-size, 8px)) 100%,\n var(--ds-arrow-size, 8px) 100%,\n 0 50%\n );\n}\n\n/* \u2500\u2500 @position-try rules \u2500\u2500\n * One rule per possible position-area value.\n * Each sets the position-area and the correct margin direction\n * so the arrow on the anchor-facing side is revealed.\n *\n * Margin direction per edge:\n * block-start (above anchor) \u2192 margin-block-end (bottom, facing anchor)\n * block-end (below anchor) \u2192 margin-block-start (top, facing anchor)\n * inline-start (left of anchor) \u2192 margin-inline-end (right, facing anchor)\n * inline-end (right of anchor) \u2192 margin-inline-start (left, facing anchor)\n */\n\n/* Single-axis centered */\n@position-try --ds-arrow-block-start {\n position-area: block-start;\n margin: 0;\n margin-block-end: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-block-end {\n position-area: block-end;\n margin: 0;\n margin-block-start: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-inline-start {\n position-area: inline-start;\n margin: 0;\n margin-inline-end: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-inline-end {\n position-area: inline-end;\n margin: 0;\n margin-inline-start: var(--ds-arrow-size, 8px);\n}\n\n/* Block edge + inline span */\n@position-try --ds-arrow-block-start-span-inline-start {\n position-area: block-start span-inline-start;\n margin: 0;\n margin-block-end: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-block-start-span-inline-end {\n position-area: block-start span-inline-end;\n margin: 0;\n margin-block-end: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-block-end-span-inline-start {\n position-area: block-end span-inline-start;\n margin: 0;\n margin-block-start: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-block-end-span-inline-end {\n position-area: block-end span-inline-end;\n margin: 0;\n margin-block-start: var(--ds-arrow-size, 8px);\n}\n\n/* Inline edge + block span */\n@position-try --ds-arrow-inline-start-span-block-start {\n position-area: inline-start span-block-start;\n margin: 0;\n margin-inline-end: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-inline-start-span-block-end {\n position-area: inline-start span-block-end;\n margin: 0;\n margin-inline-end: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-inline-end-span-block-start {\n position-area: inline-end span-block-start;\n margin: 0;\n margin-inline-start: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-inline-end-span-block-end {\n position-area: inline-end span-block-end;\n margin: 0;\n margin-inline-start: var(--ds-arrow-size, 8px);\n}\n";
|
|
22
|
+
var ARROW_CSS = "\n[data-ds-popover-arrow] {\n clip-path: inset(var(--ds-arrow-offset, 0px)) margin-box;\n box-shadow: none;\n}\n\n/* Top and Bottom arrows (vertical hexagon) */\n[data-ds-popover-arrow]::before {\n content: \"\";\n position: absolute;\n z-index: -1;\n background: inherit;\n left: 50%;\n transform: translateX(-50%);\n width: calc(var(--ds-arrow-size, 8px) * 2);\n height: calc(100% + var(--ds-arrow-size, 8px) * 2);\n top: calc(var(--ds-arrow-size, 8px) * -1);\n clip-path: polygon(\n 0 var(--ds-arrow-size, 8px),\n 50% 0,\n 100% var(--ds-arrow-size, 8px),\n 100% calc(100% - var(--ds-arrow-size, 8px)),\n 50% 100%,\n 0 calc(100% - var(--ds-arrow-size, 8px))\n );\n}\n\n/* Left and Right arrows (horizontal hexagon) */\n[data-ds-popover-arrow]::after {\n content: \"\";\n position: absolute;\n z-index: -1;\n background: inherit;\n top: 50%;\n transform: translateY(-50%);\n height: calc(var(--ds-arrow-size, 8px) * 2);\n width: calc(100% + var(--ds-arrow-size, 8px) * 2);\n left: calc(var(--ds-arrow-size, 8px) * -1);\n clip-path: polygon(\n var(--ds-arrow-size, 8px) 0,\n calc(100% - var(--ds-arrow-size, 8px)) 0,\n 100% 50%,\n calc(100% - var(--ds-arrow-size, 8px)) 100%,\n var(--ds-arrow-size, 8px) 100%,\n 0 50%\n );\n}\n\n/* \u2500\u2500 @position-try rules \u2500\u2500\n * One rule per possible position-area value.\n * Each sets the position-area and the correct margin direction\n * so the arrow on the anchor-facing side is revealed.\n *\n * Margin direction per edge:\n * block-start (above anchor) \u2192 margin-block-end (bottom, facing anchor)\n * block-end (below anchor) \u2192 margin-block-start (top, facing anchor)\n * inline-start (left of anchor) \u2192 margin-inline-end (right, facing anchor)\n * inline-end (right of anchor) \u2192 margin-inline-start (left, facing anchor)\n */\n\n/* Each rule sets two margins: the anchor-facing margin for the gap\n (via --ds-arrow-size) and one cross-axis margin for the shift\n (via one of --ds-cross-axis-shift-margin-start, --ds-cross-axis-shift-margin-end,\n --ds-cross-axis-shift-margin-block-start, or --ds-cross-axis-shift-margin-block-end). The\n side matches the popover's anchored cross-axis edge per align. Only\n one of the four properties is non-zero at a time; useAnchorPosition\n picks it. See notes/architecture/positioning.md for details. */\n/* Single-axis centered (align: center) */\n@position-try --ds-arrow-block-start {\n position-area: block-start;\n margin: 0;\n margin-block-end: var(--ds-arrow-size, 8px);\n margin-inline-start: var(--ds-cross-axis-shift-margin-start, 0px);\n}\n@position-try --ds-arrow-block-end {\n position-area: block-end;\n margin: 0;\n margin-block-start: var(--ds-arrow-size, 8px);\n margin-inline-start: var(--ds-cross-axis-shift-margin-start, 0px);\n}\n@position-try --ds-arrow-inline-start {\n position-area: inline-start;\n margin: 0;\n margin-inline-end: var(--ds-arrow-size, 8px);\n margin-block-start: var(--ds-cross-axis-shift-margin-block-start, 0px);\n}\n@position-try --ds-arrow-inline-end {\n position-area: inline-end;\n margin: 0;\n margin-inline-start: var(--ds-arrow-size, 8px);\n margin-block-start: var(--ds-cross-axis-shift-margin-block-start, 0px);\n}\n\n/* Block edge + inline span (align: start uses span-inline-end; align: end uses span-inline-start) */\n@position-try --ds-arrow-block-start-span-inline-start {\n position-area: block-start span-inline-start;\n margin: 0;\n margin-block-end: var(--ds-arrow-size, 8px);\n margin-inline-end: var(--ds-cross-axis-shift-margin-end, 0px);\n}\n@position-try --ds-arrow-block-start-span-inline-end {\n position-area: block-start span-inline-end;\n margin: 0;\n margin-block-end: var(--ds-arrow-size, 8px);\n margin-inline-start: var(--ds-cross-axis-shift-margin-start, 0px);\n}\n@position-try --ds-arrow-block-end-span-inline-start {\n position-area: block-end span-inline-start;\n margin: 0;\n margin-block-start: var(--ds-arrow-size, 8px);\n margin-inline-end: var(--ds-cross-axis-shift-margin-end, 0px);\n}\n@position-try --ds-arrow-block-end-span-inline-end {\n position-area: block-end span-inline-end;\n margin: 0;\n margin-block-start: var(--ds-arrow-size, 8px);\n margin-inline-start: var(--ds-cross-axis-shift-margin-start, 0px);\n}\n\n/* Inline edge + block span (align: start uses span-block-end; align: end uses span-block-start) */\n@position-try --ds-arrow-inline-start-span-block-start {\n position-area: inline-start span-block-start;\n margin: 0;\n margin-inline-end: var(--ds-arrow-size, 8px);\n margin-block-end: var(--ds-cross-axis-shift-margin-block-end, 0px);\n}\n@position-try --ds-arrow-inline-start-span-block-end {\n position-area: inline-start span-block-end;\n margin: 0;\n margin-inline-end: var(--ds-arrow-size, 8px);\n margin-block-start: var(--ds-cross-axis-shift-margin-block-start, 0px);\n}\n@position-try --ds-arrow-inline-end-span-block-start {\n position-area: inline-end span-block-start;\n margin: 0;\n margin-inline-start: var(--ds-arrow-size, 8px);\n margin-block-end: var(--ds-cross-axis-shift-margin-block-end, 0px);\n}\n@position-try --ds-arrow-inline-end-span-block-end {\n position-area: inline-end span-block-end;\n margin: 0;\n margin-inline-start: var(--ds-arrow-size, 8px);\n margin-block-start: var(--ds-cross-axis-shift-margin-block-start, 0px);\n}\n";
|
|
23
23
|
|
|
24
24
|
// ── Placement utilities ──
|
|
25
25
|
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
2
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
3
3
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
4
|
+
/**
|
|
5
|
+
* JS fallback positioning math. Used only when the browser does not
|
|
6
|
+
* support CSS Anchor Positioning (less than 6% of users).
|
|
7
|
+
*
|
|
8
|
+
* Consumer-supplied `offset.gap` and `offset.crossAxisShift` are
|
|
9
|
+
* honored: the caller (`useAnchorPosition`) resolves CSS length strings
|
|
10
|
+
* (tokens, calc, var, etc) to pixels via a DOM probe before passing them
|
|
11
|
+
* in. The caller also keeps the popover hidden (`opacity: 0`) until the
|
|
12
|
+
* first measurement completes, so the popover never paints at the wrong
|
|
13
|
+
* position.
|
|
14
|
+
*/
|
|
15
|
+
|
|
4
16
|
/**
|
|
5
17
|
* Computes the base {top, left} position based on the primary edge,
|
|
6
18
|
* flipping to the opposite side when there is not enough space.
|
|
19
|
+
*
|
|
20
|
+
* `gap` is applied along the main axis between trigger and popover.
|
|
7
21
|
*/
|
|
8
22
|
function computeEdgePosition(_ref) {
|
|
9
23
|
var axis = _ref.axis,
|
|
@@ -12,13 +26,15 @@ function computeEdgePosition(_ref) {
|
|
|
12
26
|
popoverWidth = _ref.popoverWidth,
|
|
13
27
|
popoverHeight = _ref.popoverHeight,
|
|
14
28
|
viewport = _ref.viewport,
|
|
15
|
-
|
|
29
|
+
gap = _ref.gap;
|
|
16
30
|
if (axis === 'block' && edge === 'end') {
|
|
17
31
|
// Below trigger
|
|
18
32
|
var spaceBelow = viewport.height - triggerRect.bottom;
|
|
19
33
|
var spaceAbove = triggerRect.top;
|
|
34
|
+
var fitsBelow = spaceBelow >= popoverHeight + gap;
|
|
35
|
+
var useBelow = fitsBelow || spaceBelow >= spaceAbove;
|
|
20
36
|
return {
|
|
21
|
-
top:
|
|
37
|
+
top: useBelow ? triggerRect.bottom + gap : triggerRect.top - popoverHeight - gap,
|
|
22
38
|
left: triggerRect.left + triggerRect.width / 2 - popoverWidth / 2
|
|
23
39
|
};
|
|
24
40
|
}
|
|
@@ -26,8 +42,10 @@ function computeEdgePosition(_ref) {
|
|
|
26
42
|
// Above trigger
|
|
27
43
|
var _spaceAbove = triggerRect.top;
|
|
28
44
|
var _spaceBelow = viewport.height - triggerRect.bottom;
|
|
45
|
+
var fitsAbove = _spaceAbove >= popoverHeight + gap;
|
|
46
|
+
var useAbove = fitsAbove || _spaceAbove >= _spaceBelow;
|
|
29
47
|
return {
|
|
30
|
-
top:
|
|
48
|
+
top: useAbove ? triggerRect.top - popoverHeight - gap : triggerRect.bottom + gap,
|
|
31
49
|
left: triggerRect.left + triggerRect.width / 2 - popoverWidth / 2
|
|
32
50
|
};
|
|
33
51
|
}
|
|
@@ -35,24 +53,28 @@ function computeEdgePosition(_ref) {
|
|
|
35
53
|
// Right of trigger
|
|
36
54
|
var spaceRight = viewport.width - triggerRect.right;
|
|
37
55
|
var spaceLeft = triggerRect.left;
|
|
56
|
+
var fitsRight = spaceRight >= popoverWidth + gap;
|
|
57
|
+
var useRight = fitsRight || spaceRight >= spaceLeft;
|
|
38
58
|
return {
|
|
39
59
|
top: triggerRect.top + triggerRect.height / 2 - popoverHeight / 2,
|
|
40
|
-
left:
|
|
60
|
+
left: useRight ? triggerRect.right + gap : triggerRect.left - popoverWidth - gap
|
|
41
61
|
};
|
|
42
62
|
}
|
|
43
63
|
if (axis === 'inline' && edge === 'start') {
|
|
44
64
|
// Left of trigger
|
|
45
65
|
var _spaceLeft = triggerRect.left;
|
|
46
66
|
var _spaceRight = viewport.width - triggerRect.right;
|
|
67
|
+
var fitsLeft = _spaceLeft >= popoverWidth + gap;
|
|
68
|
+
var useLeft = fitsLeft || _spaceLeft >= _spaceRight;
|
|
47
69
|
return {
|
|
48
70
|
top: triggerRect.top + triggerRect.height / 2 - popoverHeight / 2,
|
|
49
|
-
left:
|
|
71
|
+
left: useLeft ? triggerRect.left - popoverWidth - gap : triggerRect.right + gap
|
|
50
72
|
};
|
|
51
73
|
}
|
|
52
74
|
|
|
53
75
|
// Unreachable for valid placements; defaults to below trigger
|
|
54
76
|
return {
|
|
55
|
-
top: triggerRect.bottom +
|
|
77
|
+
top: triggerRect.bottom + gap,
|
|
56
78
|
left: triggerRect.left + triggerRect.width / 2 - popoverWidth / 2
|
|
57
79
|
};
|
|
58
80
|
}
|
|
@@ -99,26 +121,58 @@ function applyAlignment(_ref2) {
|
|
|
99
121
|
}
|
|
100
122
|
|
|
101
123
|
/**
|
|
102
|
-
*
|
|
124
|
+
* Applies the cross-axis shift offset.
|
|
103
125
|
*
|
|
104
|
-
*
|
|
105
|
-
* -
|
|
106
|
-
*
|
|
107
|
-
*
|
|
126
|
+
* The sign mirrors the CSS path (see `crossAxisShiftMargin` in
|
|
127
|
+
* `use-anchor-positioning.tsx`):
|
|
128
|
+
* - `align: 'start' | 'center'` + `forwards` → push toward end (positive)
|
|
129
|
+
* - `align: 'end'` + `forwards` → push toward start (negative)
|
|
130
|
+
* - `backwards` flips the sign in both cases.
|
|
131
|
+
*/
|
|
132
|
+
function applyCrossAxisShift(_ref3) {
|
|
133
|
+
var position = _ref3.position,
|
|
134
|
+
axis = _ref3.axis,
|
|
135
|
+
align = _ref3.align,
|
|
136
|
+
crossAxisShift = _ref3.crossAxisShift;
|
|
137
|
+
if (crossAxisShift.value === 0) {
|
|
138
|
+
return position;
|
|
139
|
+
}
|
|
140
|
+
var directionSign = crossAxisShift.direction === 'forwards' ? 1 : -1;
|
|
141
|
+
var sideSign = align === 'end' ? -1 : 1;
|
|
142
|
+
var delta = crossAxisShift.value * directionSign * sideSign;
|
|
143
|
+
|
|
144
|
+
// Cross-axis is the OPPOSITE of the placement axis.
|
|
145
|
+
if (axis === 'block') {
|
|
146
|
+
return _objectSpread(_objectSpread({}, position), {}, {
|
|
147
|
+
left: position.left + delta
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return _objectSpread(_objectSpread({}, position), {}, {
|
|
151
|
+
top: position.top + delta
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Computes popover position from trigger rect and viewport, flipping to
|
|
157
|
+
* the side with more space when needed. Re-runs on scroll/resize.
|
|
108
158
|
*
|
|
109
|
-
*
|
|
159
|
+
* `gap` and `crossAxisShift` are pre-resolved to pixels by the
|
|
160
|
+
* caller (see `resolveCssLengthToPixels`), so this function stays purely
|
|
161
|
+
* numeric.
|
|
110
162
|
*/
|
|
111
|
-
export function computeFallbackPosition(
|
|
112
|
-
var triggerRect =
|
|
113
|
-
popoverEl =
|
|
114
|
-
placement =
|
|
115
|
-
viewport =
|
|
116
|
-
|
|
163
|
+
export function computeFallbackPosition(_ref4) {
|
|
164
|
+
var triggerRect = _ref4.triggerRect,
|
|
165
|
+
popoverEl = _ref4.popoverEl,
|
|
166
|
+
placement = _ref4.placement,
|
|
167
|
+
viewport = _ref4.viewport,
|
|
168
|
+
gap = _ref4.gap,
|
|
169
|
+
crossAxisShift = _ref4.crossAxisShift;
|
|
117
170
|
var axis = placement.axis,
|
|
118
171
|
edge = placement.edge,
|
|
119
172
|
align = placement.align;
|
|
120
173
|
|
|
121
|
-
// Measure popover dimensions
|
|
174
|
+
// Measure popover dimensions. Callers must ensure the popover has
|
|
175
|
+
// laid out before calling this (it should not be `display: none`).
|
|
122
176
|
var popoverWidth = popoverEl.offsetWidth;
|
|
123
177
|
var popoverHeight = popoverEl.offsetHeight;
|
|
124
178
|
var basePosition = computeEdgePosition({
|
|
@@ -128,7 +182,7 @@ export function computeFallbackPosition(_ref3) {
|
|
|
128
182
|
popoverWidth: popoverWidth,
|
|
129
183
|
popoverHeight: popoverHeight,
|
|
130
184
|
viewport: viewport,
|
|
131
|
-
|
|
185
|
+
gap: gap
|
|
132
186
|
});
|
|
133
187
|
var alignedPosition = applyAlignment({
|
|
134
188
|
position: basePosition,
|
|
@@ -138,10 +192,16 @@ export function computeFallbackPosition(_ref3) {
|
|
|
138
192
|
popoverWidth: popoverWidth,
|
|
139
193
|
popoverHeight: popoverHeight
|
|
140
194
|
});
|
|
195
|
+
var shiftedPosition = applyCrossAxisShift({
|
|
196
|
+
position: alignedPosition,
|
|
197
|
+
axis: axis,
|
|
198
|
+
align: align,
|
|
199
|
+
crossAxisShift: crossAxisShift
|
|
200
|
+
});
|
|
141
201
|
|
|
142
202
|
// Clamp to viewport so the popover is never offscreen
|
|
143
203
|
return {
|
|
144
|
-
top: Math.max(0, Math.min(
|
|
145
|
-
left: Math.max(0, Math.min(
|
|
204
|
+
top: Math.max(0, Math.min(shiftedPosition.top, viewport.height - popoverHeight)),
|
|
205
|
+
left: Math.max(0, Math.min(shiftedPosition.left, viewport.width - popoverWidth))
|
|
146
206
|
};
|
|
147
207
|
}
|