@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
|
@@ -0,0 +1,345 @@
|
|
|
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(_ref) {
|
|
26
|
+
var element = _ref.element;
|
|
27
|
+
var tag = element.tagName ? element.tagName.toLowerCase() : 'unknown';
|
|
28
|
+
var id = element.id ? "#".concat(element.id) : '';
|
|
29
|
+
var classes = element.classList && element.classList.length > 0 ? ".".concat(Array.from(element.classList).join('.')) : '';
|
|
30
|
+
return "<".concat(tag).concat(id).concat(classes, ">");
|
|
31
|
+
}
|
|
32
|
+
function createToggleEvent(_ref2) {
|
|
33
|
+
var type = _ref2.type,
|
|
34
|
+
oldState = _ref2.oldState,
|
|
35
|
+
newState = _ref2.newState,
|
|
36
|
+
cancelable = _ref2.cancelable;
|
|
37
|
+
if (typeof globalThis.ToggleEvent === 'function') {
|
|
38
|
+
// ToggleEvent is not in lib.dom.d.ts at the time of writing; cast to any for construction.
|
|
39
|
+
var ToggleEventCtor = globalThis.ToggleEvent;
|
|
40
|
+
return new ToggleEventCtor(type, {
|
|
41
|
+
oldState: oldState,
|
|
42
|
+
newState: newState,
|
|
43
|
+
cancelable: cancelable,
|
|
44
|
+
bubbles: false
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
var event = new Event(type, {
|
|
48
|
+
cancelable: cancelable,
|
|
49
|
+
bubbles: false
|
|
50
|
+
});
|
|
51
|
+
event.oldState = oldState;
|
|
52
|
+
event.newState = newState;
|
|
53
|
+
return event;
|
|
54
|
+
}
|
|
55
|
+
function fireToggleEvent(_ref3) {
|
|
56
|
+
var element = _ref3.element,
|
|
57
|
+
type = _ref3.type,
|
|
58
|
+
oldState = _ref3.oldState,
|
|
59
|
+
newState = _ref3.newState,
|
|
60
|
+
cancelable = _ref3.cancelable;
|
|
61
|
+
return element.dispatchEvent(createToggleEvent({
|
|
62
|
+
type: type,
|
|
63
|
+
oldState: oldState,
|
|
64
|
+
newState: newState,
|
|
65
|
+
cancelable: cancelable
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
function oppositeState(_ref4) {
|
|
69
|
+
var state = _ref4.state;
|
|
70
|
+
if (state === 'open') {
|
|
71
|
+
return 'closed';
|
|
72
|
+
}
|
|
73
|
+
return 'open';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Coalesces rapid show/hide into one toggle event per element.
|
|
77
|
+
// Uses setTimeout (task) not Promise.resolve (microtask) to match browser timing.
|
|
78
|
+
function createToggleScheduler() {
|
|
79
|
+
var pending = new Map();
|
|
80
|
+
return function schedule(_ref5) {
|
|
81
|
+
var element = _ref5.element,
|
|
82
|
+
newState = _ref5.newState;
|
|
83
|
+
var existing = pending.get(element);
|
|
84
|
+
if (existing) {
|
|
85
|
+
// Coalesce: keep the original oldState, update newState
|
|
86
|
+
pending.set(element, {
|
|
87
|
+
oldState: existing.oldState,
|
|
88
|
+
newState: newState
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
pending.set(element, {
|
|
93
|
+
oldState: oppositeState({
|
|
94
|
+
state: newState
|
|
95
|
+
}),
|
|
96
|
+
newState: newState
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Task queue (setTimeout) — browser fires toggle after microtasks drain, not during
|
|
100
|
+
setTimeout(function () {
|
|
101
|
+
var entry = pending.get(element);
|
|
102
|
+
if (!entry) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
pending.delete(element);
|
|
106
|
+
fireToggleEvent({
|
|
107
|
+
element: element,
|
|
108
|
+
type: 'toggle',
|
|
109
|
+
oldState: entry.oldState,
|
|
110
|
+
newState: entry.newState,
|
|
111
|
+
cancelable: false
|
|
112
|
+
});
|
|
113
|
+
}, 0);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Like createToggleScheduler but also fires a close event as a separate task after toggle.
|
|
118
|
+
function createDialogToggleScheduler() {
|
|
119
|
+
var pending = new Map();
|
|
120
|
+
return function schedule(_ref6) {
|
|
121
|
+
var dialog = _ref6.dialog,
|
|
122
|
+
newState = _ref6.newState,
|
|
123
|
+
shouldFireCloseEvent = _ref6.shouldFireCloseEvent;
|
|
124
|
+
var existing = pending.get(dialog);
|
|
125
|
+
if (existing) {
|
|
126
|
+
// Coalesce: keep the original oldState, merge shouldFireCloseEvent
|
|
127
|
+
pending.set(dialog, {
|
|
128
|
+
oldState: existing.oldState,
|
|
129
|
+
newState: newState,
|
|
130
|
+
shouldFireCloseEvent: existing.shouldFireCloseEvent || shouldFireCloseEvent
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
pending.set(dialog, {
|
|
135
|
+
oldState: oppositeState({
|
|
136
|
+
state: newState
|
|
137
|
+
}),
|
|
138
|
+
newState: newState,
|
|
139
|
+
shouldFireCloseEvent: shouldFireCloseEvent
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Browser fires toggle as a task, not synchronously or as a microtask
|
|
143
|
+
setTimeout(function () {
|
|
144
|
+
var entry = pending.get(dialog);
|
|
145
|
+
if (!entry) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
pending.delete(dialog);
|
|
149
|
+
fireToggleEvent({
|
|
150
|
+
element: dialog,
|
|
151
|
+
type: 'toggle',
|
|
152
|
+
oldState: entry.oldState,
|
|
153
|
+
newState: entry.newState,
|
|
154
|
+
cancelable: false
|
|
155
|
+
});
|
|
156
|
+
if (entry.shouldFireCloseEvent) {
|
|
157
|
+
// Browser fires close as a separate task after toggle
|
|
158
|
+
setTimeout(function () {
|
|
159
|
+
dialog.dispatchEvent(new Event('close', {
|
|
160
|
+
bubbles: false,
|
|
161
|
+
cancelable: false
|
|
162
|
+
}));
|
|
163
|
+
}, 0);
|
|
164
|
+
}
|
|
165
|
+
}, 0);
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ─── Popover API ────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
if (typeof HTMLElement !== 'undefined' && !HTMLElement.prototype.showPopover) {
|
|
172
|
+
var schedulePopoverToggle = createToggleScheduler();
|
|
173
|
+
HTMLElement.prototype.showPopover = function showPopover() {
|
|
174
|
+
if (!this.hasAttribute('popover')) {
|
|
175
|
+
throw new DOMException("Failed to execute 'showPopover' on 'HTMLElement': ".concat(describeElement({
|
|
176
|
+
element: this
|
|
177
|
+
}), " is not a popover element. Ensure the element has a 'popover' attribute (e.g. <div popover>)."), 'NotSupportedError');
|
|
178
|
+
}
|
|
179
|
+
if (!this.isConnected) {
|
|
180
|
+
throw new DOMException("Failed to execute 'showPopover' on 'HTMLElement': ".concat(describeElement({
|
|
181
|
+
element: this
|
|
182
|
+
}), " 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');
|
|
183
|
+
}
|
|
184
|
+
if (this.hasAttribute('data-popover-open')) {
|
|
185
|
+
return; // already showing — no-op
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// sync, cancelable on show only
|
|
189
|
+
var allowed = fireToggleEvent({
|
|
190
|
+
element: this,
|
|
191
|
+
type: 'beforetoggle',
|
|
192
|
+
oldState: 'closed',
|
|
193
|
+
newState: 'open',
|
|
194
|
+
cancelable: true
|
|
195
|
+
});
|
|
196
|
+
if (!allowed) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.setAttribute('data-popover-open', '');
|
|
200
|
+
schedulePopoverToggle({
|
|
201
|
+
element: this,
|
|
202
|
+
newState: 'open'
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
HTMLElement.prototype.hidePopover = function hidePopover() {
|
|
206
|
+
if (!this.hasAttribute('popover')) {
|
|
207
|
+
throw new DOMException("Failed to execute 'hidePopover' on 'HTMLElement': ".concat(describeElement({
|
|
208
|
+
element: this
|
|
209
|
+
}), " is not a popover element. Ensure the element has a 'popover' attribute (e.g. <div popover>)."), 'NotSupportedError');
|
|
210
|
+
}
|
|
211
|
+
// Browser does not throw for disconnected elements on hide — just no-op
|
|
212
|
+
if (!this.isConnected || !this.hasAttribute('data-popover-open')) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// not cancelable on hide
|
|
217
|
+
fireToggleEvent({
|
|
218
|
+
element: this,
|
|
219
|
+
type: 'beforetoggle',
|
|
220
|
+
oldState: 'open',
|
|
221
|
+
newState: 'closed',
|
|
222
|
+
cancelable: false
|
|
223
|
+
});
|
|
224
|
+
this.removeAttribute('data-popover-open');
|
|
225
|
+
schedulePopoverToggle({
|
|
226
|
+
element: this,
|
|
227
|
+
newState: 'closed'
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
HTMLElement.prototype.togglePopover = function togglePopover(force) {
|
|
231
|
+
var isOpen = this.hasAttribute('data-popover-open');
|
|
232
|
+
if (typeof force === 'boolean') {
|
|
233
|
+
if (force && !isOpen) {
|
|
234
|
+
this.showPopover();
|
|
235
|
+
} else if (!force && isOpen) {
|
|
236
|
+
this.hidePopover();
|
|
237
|
+
}
|
|
238
|
+
return force;
|
|
239
|
+
}
|
|
240
|
+
if (isOpen) {
|
|
241
|
+
this.hidePopover();
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
this.showPopover();
|
|
245
|
+
return true;
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ─── HTMLDialogElement ──────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
if (typeof HTMLDialogElement !== 'undefined') {
|
|
252
|
+
var proto = HTMLDialogElement.prototype;
|
|
253
|
+
var scheduleDialogToggle = createDialogToggleScheduler();
|
|
254
|
+
if (typeof proto.showModal !== 'function') {
|
|
255
|
+
proto.showModal = function showModal() {
|
|
256
|
+
if (!this.isConnected) {
|
|
257
|
+
throw new DOMException("Failed to execute 'showModal' on 'HTMLDialogElement': ".concat(describeElement({
|
|
258
|
+
element: this
|
|
259
|
+
}), " 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');
|
|
260
|
+
}
|
|
261
|
+
if (this.hasAttribute('open')) {
|
|
262
|
+
if (this._isModal) {
|
|
263
|
+
return; // already modal — no-op
|
|
264
|
+
}
|
|
265
|
+
throw new DOMException("Failed to execute 'showModal' on 'HTMLDialogElement': ".concat(describeElement({
|
|
266
|
+
element: this
|
|
267
|
+
}), " 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');
|
|
268
|
+
}
|
|
269
|
+
fireToggleEvent({
|
|
270
|
+
element: this,
|
|
271
|
+
type: 'beforetoggle',
|
|
272
|
+
oldState: 'closed',
|
|
273
|
+
newState: 'open',
|
|
274
|
+
cancelable: false
|
|
275
|
+
});
|
|
276
|
+
this.setAttribute('open', '');
|
|
277
|
+
this._isModal = true;
|
|
278
|
+
scheduleDialogToggle({
|
|
279
|
+
dialog: this,
|
|
280
|
+
newState: 'open',
|
|
281
|
+
shouldFireCloseEvent: false
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (typeof proto.show !== 'function') {
|
|
286
|
+
proto.show = function show() {
|
|
287
|
+
// Browser does not throw for disconnected elements on show — just no-op
|
|
288
|
+
if (!this.isConnected || this.hasAttribute('open')) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
fireToggleEvent({
|
|
292
|
+
element: this,
|
|
293
|
+
type: 'beforetoggle',
|
|
294
|
+
oldState: 'closed',
|
|
295
|
+
newState: 'open',
|
|
296
|
+
cancelable: false
|
|
297
|
+
});
|
|
298
|
+
this.setAttribute('open', '');
|
|
299
|
+
this._isModal = false;
|
|
300
|
+
scheduleDialogToggle({
|
|
301
|
+
dialog: this,
|
|
302
|
+
newState: 'open',
|
|
303
|
+
shouldFireCloseEvent: false
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (typeof proto.close !== 'function') {
|
|
308
|
+
proto.close = function close(returnValue) {
|
|
309
|
+
if (!this.hasAttribute('open')) {
|
|
310
|
+
return; // already closed — no-op
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Sync beforetoggle (not cancelable) — matches browser behavior
|
|
314
|
+
fireToggleEvent({
|
|
315
|
+
element: this,
|
|
316
|
+
type: 'beforetoggle',
|
|
317
|
+
oldState: 'open',
|
|
318
|
+
newState: 'closed',
|
|
319
|
+
cancelable: false
|
|
320
|
+
});
|
|
321
|
+
this.removeAttribute('open');
|
|
322
|
+
this._isModal = false;
|
|
323
|
+
if (returnValue !== undefined) {
|
|
324
|
+
this.returnValue = returnValue;
|
|
325
|
+
}
|
|
326
|
+
scheduleDialogToggle({
|
|
327
|
+
dialog: this,
|
|
328
|
+
newState: 'closed',
|
|
329
|
+
shouldFireCloseEvent: true
|
|
330
|
+
});
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (!('returnValue' in proto)) {
|
|
334
|
+
Object.defineProperty(proto, 'returnValue', {
|
|
335
|
+
get: function get() {
|
|
336
|
+
return this._returnValue === undefined ? '' : this._returnValue;
|
|
337
|
+
},
|
|
338
|
+
set: function set(value) {
|
|
339
|
+
this._returnValue = String(value);
|
|
340
|
+
},
|
|
341
|
+
configurable: true,
|
|
342
|
+
enumerable: true
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
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: function toBeVisible(element) {
|
|
62
|
+
var originalResult = originalToBeVisible.call(this, element);
|
|
63
|
+
var 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: function message() {
|
|
70
|
+
return "expected element to be visible, but it is inside a closed ".concat(getNearestTopLayerType(element));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return originalResult;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Original says not visible — override if nearest top-layer host is open.
|
|
78
|
+
// jsdom may hide [popover] elements even when they are logically open.
|
|
79
|
+
if (topLayerState === 'open') {
|
|
80
|
+
return {
|
|
81
|
+
pass: true,
|
|
82
|
+
message: function message() {
|
|
83
|
+
return "expected element not to be visible, but it is inside an open ".concat(getNearestTopLayerType(element));
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return originalResult;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { type TPlacement } from './resolve-placement';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Computes popover position from trigger rect and viewport, flipping to
|
|
4
|
+
* the side with more space when needed. Re-runs on scroll/resize.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - "auto" placement: show on the edge with the most available space
|
|
9
|
-
*
|
|
10
|
-
* Re-runs on scroll (capture on window) and resize events.
|
|
6
|
+
* `gap` and `crossAxisShift` are pre-resolved to pixels by the
|
|
7
|
+
* caller (see `resolveCssLengthToPixels`), so this function stays purely
|
|
8
|
+
* numeric.
|
|
11
9
|
*/
|
|
12
|
-
export declare function computeFallbackPosition({ triggerRect, popoverEl, placement, viewport,
|
|
10
|
+
export declare function computeFallbackPosition({ triggerRect, popoverEl, placement, viewport, gap, crossAxisShift, }: {
|
|
13
11
|
triggerRect: DOMRect;
|
|
14
12
|
popoverEl: HTMLElement;
|
|
15
13
|
placement: TPlacement;
|
|
@@ -17,7 +15,11 @@ export declare function computeFallbackPosition({ triggerRect, popoverEl, placem
|
|
|
17
15
|
width: number;
|
|
18
16
|
height: number;
|
|
19
17
|
};
|
|
20
|
-
|
|
18
|
+
gap: number;
|
|
19
|
+
crossAxisShift: {
|
|
20
|
+
value: number;
|
|
21
|
+
direction: 'forwards' | 'backwards';
|
|
22
|
+
};
|
|
21
23
|
}): {
|
|
22
24
|
top: number;
|
|
23
25
|
left: number;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a CSS length value to a pixel number using a hidden DOM probe.
|
|
3
|
+
*
|
|
4
|
+
* Used by the JS positioning fallback: consumer offsets can be numbers,
|
|
5
|
+
* `${n}px` strings, tokenised `var(--ds-space-100, 8px)` strings, `calc()`
|
|
6
|
+
* expressions, viewport units, etc. Parsing arbitrary CSS in JS is fragile
|
|
7
|
+
* (and impossible for `calc(var(...))` without a layout context), so we let
|
|
8
|
+
* the browser do the math.
|
|
9
|
+
*
|
|
10
|
+
* The probe is appended to `container` so it inherits the same containing
|
|
11
|
+
* block, font size, and custom-property scope as the popover. We use `width`
|
|
12
|
+
* because it accepts any CSS length, including negative `calc()` results
|
|
13
|
+
* (negative widths are clamped to 0 — see note below).
|
|
14
|
+
*
|
|
15
|
+
* Cost: one synchronous reflow per call. The fallback only resolves on
|
|
16
|
+
* placement/offset changes, not on every scroll/resize update, so this is
|
|
17
|
+
* not a hot path.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveCssLengthToPixels({ value, container, }: {
|
|
20
|
+
value: number | string;
|
|
21
|
+
container: HTMLElement;
|
|
22
|
+
}): number;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes an offset to a CSS length string. Numbers become `${n}px`;
|
|
3
|
+
* strings pass through. Apply at the API boundary so internal code only
|
|
4
|
+
* deals with strings.
|
|
5
|
+
*/
|
|
6
|
+
export declare function toCssLengthString({ value }: {
|
|
7
|
+
value: number | string;
|
|
8
|
+
}): string;
|
|
@@ -1,16 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Cross-axis shift. `value` is a CSS length string (consumer numbers are
|
|
3
|
+
* normalized to `${n}px`). `direction` is `'forwards'` (toward end) or
|
|
4
|
+
* `'backwards'` (toward start).
|
|
5
|
+
*/
|
|
6
|
+
export type TCrossAxisShiftOffset = {
|
|
7
|
+
value: string;
|
|
8
|
+
direction: 'forwards' | 'backwards';
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Fully-resolved placement (internal). Use `getPlacement` for partial input.
|
|
12
|
+
* `offset.gap` and `offset.crossAxisShift.value` are always CSS
|
|
13
|
+
* length strings.
|
|
4
14
|
*/
|
|
5
15
|
export type TPlacement = {
|
|
6
16
|
axis: 'block' | 'inline';
|
|
7
17
|
edge: 'start' | 'end';
|
|
8
18
|
align: 'start' | 'center' | 'end';
|
|
19
|
+
offset: {
|
|
20
|
+
gap: string;
|
|
21
|
+
crossAxisShift: {
|
|
22
|
+
value: string;
|
|
23
|
+
direction: 'forwards' | 'backwards';
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Partial placement options. `offset.gap` and
|
|
29
|
+
* `offset.crossAxisShift.value` accept either a number (pixels) or a
|
|
30
|
+
* CSS length string (e.g. `token('space.100')`).
|
|
31
|
+
*
|
|
32
|
+
* The two offset axes are named for clarity at the call site:
|
|
33
|
+
* - `gap` — distance away from the trigger along the placement axis
|
|
34
|
+
* - `crossAxisShift` — nudge along the trigger edge (perpendicular to the placement axis)
|
|
35
|
+
*/
|
|
36
|
+
export type TPlacementOptions = {
|
|
37
|
+
axis?: 'block' | 'inline';
|
|
38
|
+
edge?: 'start' | 'end';
|
|
39
|
+
align?: 'start' | 'center' | 'end';
|
|
40
|
+
offset?: {
|
|
41
|
+
gap?: number | string;
|
|
42
|
+
crossAxisShift?: {
|
|
43
|
+
value?: number | string;
|
|
44
|
+
direction?: 'forwards' | 'backwards';
|
|
45
|
+
};
|
|
46
|
+
};
|
|
9
47
|
};
|
|
10
48
|
/**
|
|
11
|
-
* Resolves a partial placement to its fully-specified form.
|
|
12
|
-
*
|
|
49
|
+
* Resolves a partial placement to its fully-specified form. Number offsets
|
|
50
|
+
* are normalized to `${n}px` strings.
|
|
51
|
+
*
|
|
52
|
+
* Defaults: `axis: 'block'`, `edge: 'end'`, `align: 'center'`,
|
|
53
|
+
* `offset.gap: token('space.100', '8px')`,
|
|
54
|
+
* `offset.crossAxisShift: { value: '0px', direction: 'forwards' }`.
|
|
13
55
|
*/
|
|
14
56
|
export declare function getPlacement({ placement }: {
|
|
15
|
-
placement:
|
|
57
|
+
placement: TPlacementOptions;
|
|
16
58
|
}): TPlacement;
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anchor positioning for top-layer popovers. See
|
|
3
|
+
* `notes/architecture/positioning.md` for the full model.
|
|
4
|
+
*
|
|
5
|
+
* Two runtime paths: CSS Anchor Positioning (modern browsers) and a JS
|
|
6
|
+
* fallback that measures the trigger and writes `top`/`left`.
|
|
7
|
+
*
|
|
8
|
+
* Both paths honour `offset.gap` and `offset.crossAxisShift`. The
|
|
9
|
+
* CSS path passes the values through as strings; the JS fallback resolves
|
|
10
|
+
* them to pixels via a hidden DOM probe (see `resolveCssLengthToPixels`)
|
|
11
|
+
* so tokens, `calc()`, `var()`, etc all work. The JS fallback hides the
|
|
12
|
+
* popover with `opacity: 0` until the first measurement completes, so it
|
|
13
|
+
* is never painted at the wrong location.
|
|
14
|
+
*/
|
|
1
15
|
import { type RefObject } from 'react';
|
|
2
16
|
import { type TArrowPreset } from '../arrow/types';
|
|
3
|
-
import { type TPlacementOptions } from '../
|
|
17
|
+
import { type TPlacementOptions } from '../internal/resolve-placement';
|
|
4
18
|
export { getPlacement } from './resolve-placement';
|
|
5
19
|
/**
|
|
6
20
|
* Resolves placement options to CSS `position-area` syntax.
|
|
@@ -59,7 +73,7 @@ export declare function placementToTryFallbacks({ placement }: {
|
|
|
59
73
|
* `position: fixed` with measured coordinates, re-running on scroll (capture)
|
|
60
74
|
* and resize events.
|
|
61
75
|
*/
|
|
62
|
-
export declare function useAnchorPosition({ anchorRef, popoverRef, placement,
|
|
76
|
+
export declare function useAnchorPosition({ anchorRef, popoverRef, placement, forceFallbackPositioning, arrow, }: {
|
|
63
77
|
/**
|
|
64
78
|
* Element to position relative to.
|
|
65
79
|
*/
|
|
@@ -70,13 +84,10 @@ export declare function useAnchorPosition({ anchorRef, popoverRef, placement, of
|
|
|
70
84
|
popoverRef: RefObject<HTMLElement | null>;
|
|
71
85
|
/**
|
|
72
86
|
* Where to place the element relative to the anchor.
|
|
87
|
+
* `offset.gap` and `offset.crossAxisShift` are part of the
|
|
88
|
+
* placement object.
|
|
73
89
|
*/
|
|
74
90
|
placement: TPlacementOptions;
|
|
75
|
-
/**
|
|
76
|
-
* Gap between the positioned element and the anchor, in pixels.
|
|
77
|
-
* Defaults to 8 (`space.100`). Tooltip uses 4.
|
|
78
|
-
*/
|
|
79
|
-
offset?: number;
|
|
80
91
|
/**
|
|
81
92
|
* Forces the JavaScript positioning fallback even when the browser
|
|
82
93
|
* supports CSS Anchor Positioning. Useful for testing fallback
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type TPlacementOptions } from '../popup/types';
|
|
1
|
+
import { type TPlacementOptions } from '../internal/resolve-placement';
|
|
3
2
|
/**
|
|
4
3
|
* Legacy Popper.js placement values (used by `@atlaskit/popper`, `@atlaskit/popup`,
|
|
5
4
|
* `@atlaskit/tooltip`, etc.).
|
|
@@ -41,12 +40,19 @@ export type TLegacyPlacement = 'auto' | 'auto-start' | 'auto-end' | 'top' | 'top
|
|
|
41
40
|
* // newPlacement === { axis: 'block', edge: 'end', align: 'start' }
|
|
42
41
|
* ```
|
|
43
42
|
*/
|
|
44
|
-
declare const placementMapping: Record<TLegacyPlacement,
|
|
43
|
+
declare const placementMapping: Record<TLegacyPlacement, TPlacementOptions>;
|
|
45
44
|
/**
|
|
46
|
-
* Convert a legacy Popper.js placement to the new
|
|
45
|
+
* Convert a legacy Popper.js placement to the new `position-area`-based Placement.
|
|
46
|
+
* Optional `offset` is the legacy Popper `[along, away]` tuple (also known as
|
|
47
|
+
* `[skidding, distance]`); negative `along` becomes `direction: 'backwards'`.
|
|
48
|
+
*
|
|
49
|
+
* The tuple keeps the historical popper names so call sites that already pass
|
|
50
|
+
* `[along, away]` migrate without thinking; the new internal field names
|
|
51
|
+
* (`gap` / `crossAxisShift`) are produced inside this adapter.
|
|
47
52
|
*/
|
|
48
|
-
export declare function fromLegacyPlacement({ legacy }: {
|
|
53
|
+
export declare function fromLegacyPlacement({ legacy, offset, }: {
|
|
49
54
|
legacy: TLegacyPlacement;
|
|
55
|
+
offset?: [along: number, away: number];
|
|
50
56
|
}): TPlacementOptions;
|
|
51
57
|
/**
|
|
52
58
|
* The full mapping record, exposed for consumers that need to iterate or introspect.
|