@schukai/monster 4.145.0 → 4.145.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/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.145.0"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.145.1"}
@@ -34,6 +34,8 @@ import {
34
34
  export {
35
35
  applyAdaptiveFloatingElementSize,
36
36
  closePositionedPopper,
37
+ createVisibilityRecoveryConfig,
38
+ getFloatingVisibleRatio,
37
39
  resolveClippingBoundaryElement,
38
40
  resolveParentPopperContentBoundary,
39
41
  isPositionedPopperOpen,
@@ -43,11 +45,13 @@ export {
43
45
 
44
46
  const autoUpdateCleanupMap = new WeakMap();
45
47
  const settlingFrameMap = new WeakMap();
48
+ const visibilityRecoveryMap = new WeakMap();
46
49
  const floatingResizeObserverMap = new WeakMap();
47
50
  const floatingSyncCycleMap = new WeakMap();
48
51
  const floatingAppearanceFrameMap = new WeakMap();
49
52
  const floatingAppearanceTimeoutMap = new WeakMap();
50
53
  const adaptiveScrollHeightDatasetKey = "monsterAdaptiveScrollHeight";
54
+ const MINIMUM_VISIBLE_FLOATING_RATIO = 0.8;
51
55
 
52
56
  /**
53
57
  * @private
@@ -76,6 +80,7 @@ function openPositionedPopper(controlElement, popperElement, options) {
76
80
 
77
81
  stopAutoUpdate(popperElement);
78
82
  cancelFloatingAppearanceFrame(popperElement);
83
+ visibilityRecoveryMap.delete(popperElement);
79
84
  popperElement.dataset.monsterAppearance = "opening";
80
85
  popperElement.style.display = "block";
81
86
  popperElement.style.position = config.strategy;
@@ -187,6 +192,13 @@ function syncFloatingPopover(
187
192
  if (allowSettlingPass) {
188
193
  scheduleSettlingPass(controlElement, popperElement, config);
189
194
  }
195
+
196
+ scheduleVisibilityRecoveryPass(
197
+ controlElement,
198
+ popperElement,
199
+ config,
200
+ syncCycleId,
201
+ );
190
202
  });
191
203
  }
192
204
 
@@ -194,6 +206,7 @@ function closePositionedPopper(popperElement) {
194
206
  cancelFloatingLayout(popperElement);
195
207
  stopAutoUpdate(popperElement);
196
208
  cancelFloatingAppearanceFrame(popperElement);
209
+ visibilityRecoveryMap.delete(popperElement);
197
210
  delete popperElement.dataset.monsterAppearance;
198
211
  popperElement.style.display = "none";
199
212
  popperElement.style.removeProperty("visibility");
@@ -358,6 +371,203 @@ function normalizeShiftOptions(tokens, detectOverflowOptions) {
358
371
  return options;
359
372
  }
360
373
 
374
+ function scheduleVisibilityRecoveryPass(
375
+ controlElement,
376
+ popperElement,
377
+ config,
378
+ syncCycleId,
379
+ ) {
380
+ if (config.visibilityRecovery === false || config.visibilityRecoveryPass) {
381
+ return;
382
+ }
383
+
384
+ if (!isActiveFloatingSyncCycle(popperElement, syncCycleId)) {
385
+ return;
386
+ }
387
+
388
+ const visibleRatio = getFloatingVisibleRatio(popperElement);
389
+ if (visibleRatio >= MINIMUM_VISIBLE_FLOATING_RATIO) {
390
+ visibilityRecoveryMap.delete(popperElement);
391
+ return;
392
+ }
393
+
394
+ if (visibilityRecoveryMap.has(popperElement)) {
395
+ return;
396
+ }
397
+ visibilityRecoveryMap.set(popperElement, true);
398
+
399
+ enqueueFloatingLayout({
400
+ popperElement,
401
+ reason: FLOATING_LAYOUT_REASON.SETTLE | FLOATING_LAYOUT_REASON.VIEWPORT,
402
+ isActive: () => isPositionedPopperOpen(popperElement),
403
+ position: () => {
404
+ runFloatingUpdateHook(popperElement);
405
+ syncFloatingPopover(
406
+ controlElement,
407
+ popperElement,
408
+ createVisibilityRecoveryConfig(config),
409
+ false,
410
+ );
411
+ },
412
+ });
413
+ }
414
+
415
+ function getFloatingVisibleRatio(floatingElement) {
416
+ if (!(floatingElement instanceof HTMLElement)) {
417
+ return 1;
418
+ }
419
+
420
+ const rect = floatingElement.getBoundingClientRect();
421
+ const area = rect.width * rect.height;
422
+ if (!Number.isFinite(area) || area <= 0) {
423
+ return 1;
424
+ }
425
+
426
+ const visibleRect = getVisibleFloatingRect(floatingElement, rect);
427
+ if (!visibleRect) {
428
+ return 0;
429
+ }
430
+
431
+ const visibleArea = visibleRect.width * visibleRect.height;
432
+ return Math.max(0, Math.min(1, visibleArea / area));
433
+ }
434
+
435
+ function getVisibleFloatingRect(floatingElement, rect) {
436
+ let visibleRect = normalizeRect(rect);
437
+ const viewportRect = {
438
+ top: 0,
439
+ left: 0,
440
+ right: window.innerWidth || document.documentElement.clientWidth || 0,
441
+ bottom: window.innerHeight || document.documentElement.clientHeight || 0,
442
+ };
443
+ viewportRect.width = Math.max(0, viewportRect.right - viewportRect.left);
444
+ viewportRect.height = Math.max(0, viewportRect.bottom - viewportRect.top);
445
+
446
+ visibleRect = intersectRects(visibleRect, viewportRect);
447
+ if (!visibleRect) {
448
+ return null;
449
+ }
450
+
451
+ for (const clippingContainer of getFloatingClippingContainers(floatingElement)) {
452
+ visibleRect = intersectRects(
453
+ visibleRect,
454
+ normalizeRect(clippingContainer.getBoundingClientRect()),
455
+ );
456
+ if (!visibleRect) {
457
+ return null;
458
+ }
459
+ }
460
+
461
+ return visibleRect;
462
+ }
463
+
464
+ function getFloatingClippingContainers(element) {
465
+ const result = [];
466
+ let current = getComposedParent(element);
467
+
468
+ while (current) {
469
+ if (
470
+ current instanceof HTMLElement &&
471
+ isClippingContainer(getComputedStyle(current))
472
+ ) {
473
+ result.push(current);
474
+ }
475
+
476
+ current = getComposedParent(current);
477
+ }
478
+
479
+ return result;
480
+ }
481
+
482
+ function normalizeRect(rect) {
483
+ const left = Number.isFinite(rect.left) ? rect.left : rect.x || 0;
484
+ const top = Number.isFinite(rect.top) ? rect.top : rect.y || 0;
485
+ const right = Number.isFinite(rect.right) ? rect.right : left + rect.width;
486
+ const bottom = Number.isFinite(rect.bottom)
487
+ ? rect.bottom
488
+ : top + rect.height;
489
+
490
+ return {
491
+ top,
492
+ left,
493
+ right,
494
+ bottom,
495
+ width: Math.max(0, right - left),
496
+ height: Math.max(0, bottom - top),
497
+ };
498
+ }
499
+
500
+ function intersectRects(firstRect, secondRect) {
501
+ const left = Math.max(firstRect.left, secondRect.left);
502
+ const top = Math.max(firstRect.top, secondRect.top);
503
+ const right = Math.min(firstRect.right, secondRect.right);
504
+ const bottom = Math.min(firstRect.bottom, secondRect.bottom);
505
+
506
+ if (right <= left || bottom <= top) {
507
+ return null;
508
+ }
509
+
510
+ return {
511
+ top,
512
+ left,
513
+ right,
514
+ bottom,
515
+ width: right - left,
516
+ height: bottom - top,
517
+ };
518
+ }
519
+
520
+ function createVisibilityRecoveryConfig(config) {
521
+ return Object.assign({}, config, {
522
+ visibilityRecoveryPass: true,
523
+ middleware: createVisibilityRecoveryMiddleware(config.middleware),
524
+ });
525
+ }
526
+
527
+ function createVisibilityRecoveryMiddleware(middleware) {
528
+ const source = isArray(middleware) ? middleware : [];
529
+ const offsetMiddleware = [];
530
+ const remainingMiddleware = [];
531
+ let hasShift = false;
532
+
533
+ for (const entry of source) {
534
+ const tokenName = getMiddlewareTokenName(entry);
535
+ if (tokenName === "flip" || tokenName === "autoPlacement") {
536
+ continue;
537
+ }
538
+ if (tokenName === "offset") {
539
+ offsetMiddleware.push(entry);
540
+ continue;
541
+ }
542
+ if (tokenName === "shift") {
543
+ hasShift = true;
544
+ }
545
+ remainingMiddleware.push(entry);
546
+ }
547
+
548
+ if (!hasShift) {
549
+ remainingMiddleware.unshift("shift:crossAxis");
550
+ }
551
+
552
+ return [
553
+ ...offsetMiddleware,
554
+ "autoPlacement:top,bottom,right,left",
555
+ ...remainingMiddleware,
556
+ ];
557
+ }
558
+
559
+ function getMiddlewareTokenName(entry) {
560
+ if (isString(entry)) {
561
+ return entry.split(":").shift();
562
+ }
563
+
564
+ if (isObject(entry) && isString(entry.name)) {
565
+ return entry.name;
566
+ }
567
+
568
+ return null;
569
+ }
570
+
361
571
  function createAdaptiveSizeMiddleware(
362
572
  detectOverflowOptions,
363
573
  popperElement,
@@ -6,6 +6,29 @@ import { ResizeObserverMock } from "../../../util/resize-observer.mjs";
6
6
  let expect = chai.expect;
7
7
  chai.use(chaiDom);
8
8
 
9
+ function waitForCondition(check, { timeout = 4000, interval = 25 } = {}) {
10
+ return new Promise((resolve, reject) => {
11
+ const start = Date.now();
12
+ const poll = () => {
13
+ try {
14
+ if (check()) {
15
+ resolve();
16
+ return;
17
+ }
18
+ } catch {}
19
+
20
+ if (Date.now() - start >= timeout) {
21
+ reject(new Error("Timed out waiting for condition"));
22
+ return;
23
+ }
24
+
25
+ setTimeout(poll, interval);
26
+ };
27
+
28
+ poll();
29
+ });
30
+ }
31
+
9
32
  const waitForLayout = () => new Promise((resolve) => setTimeout(resolve, 120));
10
33
 
11
34
  const html = `
@@ -92,7 +115,12 @@ describe("ButtonBar", function () {
92
115
  `;
93
116
  const bar = document.getElementById("empty-auto-hidden-button-bar");
94
117
 
95
- await waitForLayout();
118
+ await waitForCondition(() => {
119
+ return (
120
+ bar.hasAttribute("hidden") &&
121
+ bar.hasAttribute("data-monster-empty-hidden")
122
+ );
123
+ });
96
124
 
97
125
  expect(bar.getOption("layout.hideWhenEmpty")).to.equal(true);
98
126
  expect(bar.hasAttribute("hidden")).to.be.true;
@@ -6,7 +6,8 @@ import { ResizeObserverMock } from "../../../util/resize-observer.mjs";
6
6
  const expect = chai.expect;
7
7
  chai.use(chaiDom);
8
8
 
9
- const waitForLayout = () => new Promise((resolve) => setTimeout(resolve, 120));
9
+ const waitForLayout = (timeout = 120) =>
10
+ new Promise((resolve) => setTimeout(resolve, timeout));
10
11
 
11
12
  const html = `
12
13
  <div id="test1">
@@ -156,13 +157,13 @@ describe("ControlBar", function () {
156
157
  expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.true;
157
158
 
158
159
  button.removeAttribute("hidden");
159
- await waitForLayout();
160
+ await waitForLayout(360);
160
161
 
161
162
  expect(bar.hasAttribute("hidden")).to.be.false;
162
163
  expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.false;
163
164
 
164
165
  button.style.display = "none";
165
- await waitForLayout();
166
+ await waitForLayout(360);
166
167
 
167
168
  expect(bar.hasAttribute("hidden")).to.be.true;
168
169
  expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.true;
@@ -5,6 +5,8 @@ let expect = chai.expect;
5
5
 
6
6
  let resolveClippingBoundaryElement;
7
7
  let applyAdaptiveFloatingElementSize;
8
+ let createVisibilityRecoveryConfig;
9
+ let getFloatingVisibleRatio;
8
10
 
9
11
  describe("form floating-ui boundary resolution", function () {
10
12
  before(function (done) {
@@ -17,6 +19,8 @@ describe("form floating-ui boundary resolution", function () {
17
19
  .then((m) => {
18
20
  resolveClippingBoundaryElement = m.resolveClippingBoundaryElement;
19
21
  applyAdaptiveFloatingElementSize = m.applyAdaptiveFloatingElementSize;
22
+ createVisibilityRecoveryConfig = m.createVisibilityRecoveryConfig;
23
+ getFloatingVisibleRatio = m.getFloatingVisibleRatio;
20
24
  done();
21
25
  })
22
26
  .catch((e) => done(e));
@@ -398,4 +402,78 @@ describe("form floating-ui boundary resolution", function () {
398
402
  expect(content.style.height).to.equal("200px");
399
403
  expect(content.style.maxHeight).to.equal("200px");
400
404
  });
405
+
406
+ it("should calculate the visible popper ratio against clipping containers", function () {
407
+ const mocks = document.getElementById("mocks");
408
+ const wrapper = document.createElement("div");
409
+ const popper = document.createElement("div");
410
+ const originalInnerWidth = window.innerWidth;
411
+ const originalInnerHeight = window.innerHeight;
412
+
413
+ Object.defineProperty(window, "innerWidth", {
414
+ configurable: true,
415
+ value: 200,
416
+ });
417
+ Object.defineProperty(window, "innerHeight", {
418
+ configurable: true,
419
+ value: 200,
420
+ });
421
+
422
+ wrapper.style.overflow = "hidden";
423
+ wrapper.getBoundingClientRect = () => {
424
+ return {
425
+ width: 100,
426
+ height: 100,
427
+ top: 0,
428
+ left: 0,
429
+ right: 100,
430
+ bottom: 100,
431
+ x: 0,
432
+ y: 0,
433
+ };
434
+ };
435
+ popper.getBoundingClientRect = () => {
436
+ return {
437
+ width: 100,
438
+ height: 100,
439
+ top: -50,
440
+ left: 50,
441
+ right: 150,
442
+ bottom: 50,
443
+ x: 50,
444
+ y: -50,
445
+ };
446
+ };
447
+
448
+ wrapper.appendChild(popper);
449
+ mocks.appendChild(wrapper);
450
+
451
+ try {
452
+ expect(getFloatingVisibleRatio(popper)).to.equal(0.25);
453
+ } finally {
454
+ Object.defineProperty(window, "innerWidth", {
455
+ configurable: true,
456
+ value: originalInnerWidth,
457
+ });
458
+ Object.defineProperty(window, "innerHeight", {
459
+ configurable: true,
460
+ value: originalInnerHeight,
461
+ });
462
+ }
463
+ });
464
+
465
+ it("should build a bounded visibility recovery pass with auto placement", function () {
466
+ const recoveryConfig = createVisibilityRecoveryConfig({
467
+ placement: "top",
468
+ middleware: ["flip", "shift", "offset:15", "arrow"],
469
+ });
470
+
471
+ expect(recoveryConfig.visibilityRecoveryPass).to.equal(true);
472
+ expect(recoveryConfig.middleware).to.deep.equal([
473
+ "offset:15",
474
+ "autoPlacement:top,bottom,right,left",
475
+ "shift",
476
+ "arrow",
477
+ ]);
478
+ });
401
479
  });