@schukai/monster 4.141.1 → 4.141.3

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.141.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.141.3"}
@@ -186,6 +186,13 @@ const ATTRIBUTE_OPTION_LAYOUT_STACKED_ALIGNMENT =
186
186
  const ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT =
187
187
  "data-monster-option-layout-stacked-breakpoint";
188
188
 
189
+ /**
190
+ * @private
191
+ * @type {string}
192
+ */
193
+ const ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT_CONTAINER =
194
+ "data-monster-option-layout-stacked-breakpoint-container";
195
+
189
196
  /**
190
197
  * @private
191
198
  * @type {string}
@@ -241,6 +248,7 @@ class ControlBar extends CustomElement {
241
248
  alignment: "left",
242
249
  stackedAlignment: undefined,
243
250
  stackedBreakpoint: undefined,
251
+ stackedBreakpointContainer: undefined,
244
252
  },
245
253
  popper: {
246
254
  placement: "left",
@@ -266,11 +274,14 @@ class ControlBar extends CustomElement {
266
274
  needsMeasure: true,
267
275
  needsLayout: true,
268
276
  needsObserve: true,
277
+ initialLayoutPending: true,
278
+ initialLayoutOpacity: undefined,
269
279
  suppressSlotChange: false,
270
280
  suppressMutation: false,
271
281
  };
272
282
 
273
283
  initControlReferences.call(this);
284
+ hideControlBarUntilInitialLayout.call(this);
274
285
  initEventHandler.call(this);
275
286
 
276
287
  // setup structure
@@ -298,9 +309,12 @@ class ControlBar extends CustomElement {
298
309
  if (
299
310
  path === "layout.alignment" ||
300
311
  path === "layout.stackedAlignment" ||
301
- path === "layout.stackedBreakpoint"
312
+ path === "layout.stackedBreakpoint" ||
313
+ path === "layout.stackedBreakpointContainer"
302
314
  ) {
303
- syncLayoutState.call(this);
315
+ syncLayoutState.call(this, {
316
+ observe: path === "layout.stackedBreakpointContainer",
317
+ });
304
318
  }
305
319
  return this;
306
320
  }
@@ -352,6 +366,7 @@ class ControlBar extends CustomElement {
352
366
  attributes.push(ATTRIBUTE_OPTION_LAYOUT_ALIGNMENT);
353
367
  attributes.push(ATTRIBUTE_OPTION_LAYOUT_STACKED_ALIGNMENT);
354
368
  attributes.push(ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT);
369
+ attributes.push(ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT_CONTAINER);
355
370
  return attributes;
356
371
  }
357
372
 
@@ -462,6 +477,44 @@ function isElementSelfHidden(element) {
462
477
  );
463
478
  }
464
479
 
480
+ /**
481
+ * @private
482
+ * @return {void}
483
+ */
484
+ function hideControlBarUntilInitialLayout() {
485
+ const state = this[layoutStateSymbol];
486
+ if (!state || !(this[controlBarElementSymbol] instanceof HTMLElement)) {
487
+ return;
488
+ }
489
+
490
+ state.initialLayoutOpacity = this[controlBarElementSymbol].style.opacity;
491
+ this[controlBarElementSymbol].style.opacity = "0";
492
+ }
493
+
494
+ /**
495
+ * @private
496
+ * @return {void}
497
+ */
498
+ function revealControlBarAfterInitialLayout() {
499
+ const state = this[layoutStateSymbol];
500
+ if (
501
+ !state ||
502
+ state.initialLayoutPending !== true ||
503
+ !(this[controlBarElementSymbol] instanceof HTMLElement)
504
+ ) {
505
+ return;
506
+ }
507
+
508
+ state.initialLayoutPending = false;
509
+ const initialOpacity = state.initialLayoutOpacity;
510
+ if (typeof initialOpacity === "string" && initialOpacity !== "") {
511
+ this[controlBarElementSymbol].style.opacity = initialOpacity;
512
+ return;
513
+ }
514
+
515
+ this[controlBarElementSymbol].style.removeProperty("opacity");
516
+ }
517
+
465
518
  /**
466
519
  * @private
467
520
  */
@@ -549,6 +602,12 @@ function initEventHandler() {
549
602
  syncLayoutState.call(self);
550
603
  };
551
604
 
605
+ self[attributeObserverSymbol][
606
+ ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT_CONTAINER
607
+ ] = () => {
608
+ syncLayoutState.call(self, { observe: true });
609
+ };
610
+
552
611
  self[resizeObserverSymbol] = new ResizeObserver(() => {
553
612
  scheduleLayout.call(self, { measure: true, layout: true });
554
613
  });
@@ -664,6 +723,7 @@ function runLayout() {
664
723
  })
665
724
  .finally(() => {
666
725
  state.running = false;
726
+ revealControlBarAfterInitialLayout.call(this);
667
727
  if (state.needsObserve || state.needsMeasure || state.needsLayout) {
668
728
  scheduleLayoutFrame.call(this);
669
729
  }
@@ -1230,6 +1290,10 @@ function calculateControlBarDimensions() {
1230
1290
 
1231
1291
  this[dimensionsSymbol].setVia("data.visible", !(containerWidth === 0));
1232
1292
  this[dimensionsSymbol].setVia("data.containerWidth", containerWidth);
1293
+ this[dimensionsSymbol].setVia(
1294
+ "data.stackedBreakpointContainerWidth",
1295
+ getStackedBreakpointContainerWidth.call(this),
1296
+ );
1233
1297
 
1234
1298
  const itemReferences = [];
1235
1299
 
@@ -1294,6 +1358,65 @@ function calculateControlBarDimensions() {
1294
1358
  this[dimensionsSymbol].setVia("data.itemReferences", itemReferences);
1295
1359
  }
1296
1360
 
1361
+ /**
1362
+ * @private
1363
+ * @return {number}
1364
+ */
1365
+ function getStackedBreakpointContainerWidth() {
1366
+ const container = resolveStackedBreakpointContainer.call(this);
1367
+ if (!(container instanceof HTMLElement)) {
1368
+ return 0;
1369
+ }
1370
+
1371
+ return getElementLayoutWidth(container);
1372
+ }
1373
+
1374
+ /**
1375
+ * @private
1376
+ * @return {HTMLElement|undefined}
1377
+ */
1378
+ function resolveStackedBreakpointContainer() {
1379
+ const selector = this.getOption("layout.stackedBreakpointContainer");
1380
+ if (typeof selector !== "string" || selector.trim() === "") {
1381
+ return undefined;
1382
+ }
1383
+
1384
+ try {
1385
+ const closest = this.closest(selector);
1386
+ if (closest instanceof HTMLElement) {
1387
+ return closest;
1388
+ }
1389
+
1390
+ const root = this.getRootNode();
1391
+ if (root && typeof root.querySelector === "function") {
1392
+ const found = root.querySelector(selector);
1393
+ if (found instanceof HTMLElement) {
1394
+ return found;
1395
+ }
1396
+ }
1397
+ } catch {}
1398
+
1399
+ return undefined;
1400
+ }
1401
+
1402
+ /**
1403
+ * @private
1404
+ * @param {HTMLElement} element
1405
+ * @return {number}
1406
+ */
1407
+ function getElementLayoutWidth(element) {
1408
+ const computedStyle = getComputedStyle(element);
1409
+ if (computedStyle === null) {
1410
+ return 0;
1411
+ }
1412
+
1413
+ if (computedStyle.getPropertyValue("box-sizing") !== "border-box") {
1414
+ return getComputedCssPixels(computedStyle.getPropertyValue("width"));
1415
+ }
1416
+
1417
+ return element.clientWidth;
1418
+ }
1419
+
1297
1420
  /**
1298
1421
  * @private
1299
1422
  */
@@ -1327,20 +1450,21 @@ function updateResizeObserverObservation() {
1327
1450
  */
1328
1451
  function getLayoutObservedNodes() {
1329
1452
  const observedNodes = [];
1330
- Array.from(this.children).forEach((node) => {
1331
- if (node instanceof HTMLElement) {
1453
+ const addObservedNode = (node) => {
1454
+ if (node instanceof HTMLElement && !observedNodes.includes(node)) {
1332
1455
  observedNodes.push(node);
1333
1456
  }
1334
- });
1457
+ };
1458
+
1459
+ Array.from(this.children).forEach(addObservedNode);
1335
1460
 
1336
1461
  let parent = this.parentNode;
1337
1462
  while (!(parent instanceof HTMLElement) && parent !== null) {
1338
1463
  parent = parent.parentNode;
1339
1464
  }
1340
1465
 
1341
- if (parent instanceof HTMLElement) {
1342
- observedNodes.push(parent);
1343
- }
1466
+ addObservedNode(parent);
1467
+ addObservedNode(resolveStackedBreakpointContainer.call(this));
1344
1468
 
1345
1469
  return observedNodes;
1346
1470
  }
@@ -1525,11 +1649,16 @@ function applyLayoutAlignment() {
1525
1649
 
1526
1650
  /**
1527
1651
  * @private
1652
+ * @param {{observe?: boolean}} options
1528
1653
  * @return {void}
1529
1654
  */
1530
- function syncLayoutState() {
1655
+ function syncLayoutState(options = {}) {
1531
1656
  applyLayoutAlignment.call(this);
1532
- scheduleLayout.call(this, { measure: true, layout: true });
1657
+ scheduleLayout.call(this, {
1658
+ measure: true,
1659
+ layout: true,
1660
+ observe: options.observe === true,
1661
+ });
1533
1662
  }
1534
1663
 
1535
1664
  /**
@@ -1544,11 +1673,19 @@ function isStackedBreakpointMatched() {
1544
1673
 
1545
1674
  let width = 0;
1546
1675
  try {
1547
- width = this[dimensionsSymbol].getVia("data.containerWidth");
1548
- } catch {
1676
+ width = this[dimensionsSymbol].getVia(
1677
+ "data.stackedBreakpointContainerWidth",
1678
+ );
1679
+ } catch {}
1680
+
1681
+ if (!(width > 0)) {
1549
1682
  try {
1550
- width = this[dimensionsSymbol].getVia("data.space");
1551
- } catch {}
1683
+ width = this[dimensionsSymbol].getVia("data.containerWidth");
1684
+ } catch {
1685
+ try {
1686
+ width = this[dimensionsSymbol].getVia("data.space");
1687
+ } catch {}
1688
+ }
1552
1689
  }
1553
1690
 
1554
1691
  if (!(width > 0)) {
@@ -1678,6 +1815,9 @@ function getLayoutChangedEventDetail() {
1678
1815
  stacked,
1679
1816
  stackedAlignment: this.getOption("layout.stackedAlignment"),
1680
1817
  stackedBreakpoint: this.getOption("layout.stackedBreakpoint"),
1818
+ stackedBreakpointContainer: this.getOption(
1819
+ "layout.stackedBreakpointContainer",
1820
+ ),
1681
1821
  };
1682
1822
  }
1683
1823
 
@@ -162,6 +162,7 @@ describe("ControlBar", function () {
162
162
  stacked: false,
163
163
  stackedAlignment: undefined,
164
164
  stackedBreakpoint: undefined,
165
+ stackedBreakpointContainer: undefined,
165
166
  });
166
167
  });
167
168
 
@@ -734,12 +735,15 @@ describe("ControlBar", function () {
734
735
  value: 20,
735
736
  });
736
737
 
738
+ expect(controlBar.style.opacity).to.equal("0");
739
+
737
740
  await flushFrames();
738
741
  await new Promise((resolve) => setTimeout(resolve, 0));
739
742
  await new Promise((resolve) => setTimeout(resolve, 0));
740
743
 
741
744
  expect(switchButton.hasAttribute("hidden")).to.be.true;
742
745
  expect(button.hasAttribute("slot")).to.be.false;
746
+ expect(controlBar.style.opacity).to.equal("");
743
747
  expect(controlBar.getAttribute("data-monster-layout-stacked")).to.equal(
744
748
  "true",
745
749
  );
@@ -840,4 +844,458 @@ describe("ControlBar", function () {
840
844
  globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
841
845
  }
842
846
  });
847
+
848
+ it("should keep the configured alignment when the reference container is above the stacked breakpoint", async function () {
849
+ const originalRequestAnimationFrame = window.requestAnimationFrame;
850
+ const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
851
+
852
+ const scheduledCallbacks = [];
853
+ const flushFrames = async () => {
854
+ while (scheduledCallbacks.length > 0) {
855
+ scheduledCallbacks.shift()();
856
+ await new Promise((resolve) => setTimeout(resolve, 0));
857
+ }
858
+ };
859
+
860
+ try {
861
+ window.requestAnimationFrame = (callback) => {
862
+ scheduledCallbacks.push(callback);
863
+ return scheduledCallbacks.length;
864
+ };
865
+ globalThis.requestAnimationFrame = window.requestAnimationFrame;
866
+
867
+ const mocks = document.getElementById("mocks");
868
+ mocks.innerHTML = `
869
+ <div id="headline-wide" class="page-headline-bar">
870
+ <div id="headline-wide-control-column">
871
+ <monster-control-bar
872
+ id="headline-wide-bar"
873
+ data-monster-option-layout-alignment="right"
874
+ data-monster-option-layout-stacked-alignment="left"
875
+ data-monster-option-layout-stacked-breakpoint="480px"
876
+ data-monster-option-layout-stacked-breakpoint-container=".page-headline-bar"
877
+ >
878
+ <button id="headline-wide-button">Run</button>
879
+ </monster-control-bar>
880
+ </div>
881
+ </div>
882
+ `;
883
+
884
+ const headline = document.getElementById("headline-wide");
885
+ const column = document.getElementById("headline-wide-control-column");
886
+ const button = document.getElementById("headline-wide-button");
887
+ const bar = document.getElementById("headline-wide-bar");
888
+ const controlBar = bar.shadowRoot.querySelector(
889
+ '[data-monster-role="control-bar"]',
890
+ );
891
+
892
+ headline.style.boxSizing = "border-box";
893
+ Object.defineProperty(headline, "clientWidth", {
894
+ configurable: true,
895
+ value: 640,
896
+ });
897
+ column.style.boxSizing = "border-box";
898
+ Object.defineProperty(column, "clientWidth", {
899
+ configurable: true,
900
+ value: 459.469,
901
+ });
902
+ Object.defineProperty(button, "offsetWidth", {
903
+ configurable: true,
904
+ value: 40,
905
+ });
906
+ Object.defineProperty(button, "offsetHeight", {
907
+ configurable: true,
908
+ value: 30,
909
+ });
910
+ button.getBoundingClientRect = () => ({
911
+ width: 40,
912
+ height: 30,
913
+ top: 0,
914
+ right: 40,
915
+ bottom: 30,
916
+ left: 0,
917
+ x: 0,
918
+ y: 0,
919
+ toJSON: () => {},
920
+ });
921
+
922
+ await flushFrames();
923
+ await new Promise((resolve) => setTimeout(resolve, 0));
924
+ await new Promise((resolve) => setTimeout(resolve, 0));
925
+
926
+ expect(controlBar.getAttribute("data-monster-layout-stacked")).to.equal(
927
+ "false",
928
+ );
929
+ expect(controlBar.getAttribute("data-monster-layout-alignment")).to.equal(
930
+ "right",
931
+ );
932
+ } finally {
933
+ window.requestAnimationFrame = originalRequestAnimationFrame;
934
+ globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
935
+ }
936
+ });
937
+
938
+ it("should apply stacked alignment when the reference container is below the stacked breakpoint", async function () {
939
+ const originalRequestAnimationFrame = window.requestAnimationFrame;
940
+ const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
941
+
942
+ const scheduledCallbacks = [];
943
+ const flushFrames = async () => {
944
+ while (scheduledCallbacks.length > 0) {
945
+ scheduledCallbacks.shift()();
946
+ await new Promise((resolve) => setTimeout(resolve, 0));
947
+ }
948
+ };
949
+
950
+ try {
951
+ window.requestAnimationFrame = (callback) => {
952
+ scheduledCallbacks.push(callback);
953
+ return scheduledCallbacks.length;
954
+ };
955
+ globalThis.requestAnimationFrame = window.requestAnimationFrame;
956
+
957
+ const mocks = document.getElementById("mocks");
958
+ mocks.innerHTML = `
959
+ <div id="headline-narrow" class="page-headline-bar">
960
+ <div id="headline-narrow-control-column">
961
+ <monster-control-bar
962
+ id="headline-narrow-bar"
963
+ data-monster-option-layout-alignment="right"
964
+ data-monster-option-layout-stacked-alignment="left"
965
+ data-monster-option-layout-stacked-breakpoint="480px"
966
+ data-monster-option-layout-stacked-breakpoint-container=".page-headline-bar"
967
+ >
968
+ <button id="headline-narrow-button">Run</button>
969
+ </monster-control-bar>
970
+ </div>
971
+ </div>
972
+ `;
973
+
974
+ const headline = document.getElementById("headline-narrow");
975
+ const column = document.getElementById("headline-narrow-control-column");
976
+ const button = document.getElementById("headline-narrow-button");
977
+ const bar = document.getElementById("headline-narrow-bar");
978
+ const controlBar = bar.shadowRoot.querySelector(
979
+ '[data-monster-role="control-bar"]',
980
+ );
981
+
982
+ headline.style.boxSizing = "border-box";
983
+ Object.defineProperty(headline, "clientWidth", {
984
+ configurable: true,
985
+ value: 350,
986
+ });
987
+ column.style.boxSizing = "border-box";
988
+ Object.defineProperty(column, "clientWidth", {
989
+ configurable: true,
990
+ value: 459.469,
991
+ });
992
+ Object.defineProperty(button, "offsetWidth", {
993
+ configurable: true,
994
+ value: 40,
995
+ });
996
+ Object.defineProperty(button, "offsetHeight", {
997
+ configurable: true,
998
+ value: 30,
999
+ });
1000
+ button.getBoundingClientRect = () => ({
1001
+ width: 40,
1002
+ height: 30,
1003
+ top: 0,
1004
+ right: 40,
1005
+ bottom: 30,
1006
+ left: 0,
1007
+ x: 0,
1008
+ y: 0,
1009
+ toJSON: () => {},
1010
+ });
1011
+
1012
+ await flushFrames();
1013
+ await new Promise((resolve) => setTimeout(resolve, 0));
1014
+ await new Promise((resolve) => setTimeout(resolve, 0));
1015
+
1016
+ expect(controlBar.getAttribute("data-monster-layout-stacked")).to.equal(
1017
+ "true",
1018
+ );
1019
+ expect(controlBar.getAttribute("data-monster-layout-alignment")).to.equal(
1020
+ "left",
1021
+ );
1022
+ } finally {
1023
+ window.requestAnimationFrame = originalRequestAnimationFrame;
1024
+ globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
1025
+ }
1026
+ });
1027
+
1028
+ it("should fall back to the control bar width when the breakpoint container selector cannot resolve", async function () {
1029
+ const originalRequestAnimationFrame = window.requestAnimationFrame;
1030
+ const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
1031
+
1032
+ const scheduledCallbacks = [];
1033
+ const flushFrames = async () => {
1034
+ while (scheduledCallbacks.length > 0) {
1035
+ scheduledCallbacks.shift()();
1036
+ await new Promise((resolve) => setTimeout(resolve, 0));
1037
+ }
1038
+ };
1039
+
1040
+ try {
1041
+ window.requestAnimationFrame = (callback) => {
1042
+ scheduledCallbacks.push(callback);
1043
+ return scheduledCallbacks.length;
1044
+ };
1045
+ globalThis.requestAnimationFrame = window.requestAnimationFrame;
1046
+
1047
+ const mocks = document.getElementById("mocks");
1048
+ mocks.innerHTML = `
1049
+ <div id="missing-reference-wrapper">
1050
+ <monster-control-bar
1051
+ id="missing-reference-bar"
1052
+ data-monster-option-layout-alignment="right"
1053
+ data-monster-option-layout-stacked-alignment="left"
1054
+ data-monster-option-layout-stacked-breakpoint="480px"
1055
+ data-monster-option-layout-stacked-breakpoint-container=".missing-reference"
1056
+ >
1057
+ <button id="missing-reference-button">Run</button>
1058
+ </monster-control-bar>
1059
+ </div>
1060
+ `;
1061
+
1062
+ const wrapper = document.getElementById("missing-reference-wrapper");
1063
+ const button = document.getElementById("missing-reference-button");
1064
+ const bar = document.getElementById("missing-reference-bar");
1065
+ const controlBar = bar.shadowRoot.querySelector(
1066
+ '[data-monster-role="control-bar"]',
1067
+ );
1068
+
1069
+ wrapper.style.boxSizing = "border-box";
1070
+ Object.defineProperty(wrapper, "clientWidth", {
1071
+ configurable: true,
1072
+ value: 350,
1073
+ });
1074
+ Object.defineProperty(button, "offsetWidth", {
1075
+ configurable: true,
1076
+ value: 40,
1077
+ });
1078
+ Object.defineProperty(button, "offsetHeight", {
1079
+ configurable: true,
1080
+ value: 30,
1081
+ });
1082
+ button.getBoundingClientRect = () => ({
1083
+ width: 40,
1084
+ height: 30,
1085
+ top: 0,
1086
+ right: 40,
1087
+ bottom: 30,
1088
+ left: 0,
1089
+ x: 0,
1090
+ y: 0,
1091
+ toJSON: () => {},
1092
+ });
1093
+
1094
+ await flushFrames();
1095
+ await new Promise((resolve) => setTimeout(resolve, 0));
1096
+ await new Promise((resolve) => setTimeout(resolve, 0));
1097
+
1098
+ expect(controlBar.getAttribute("data-monster-layout-stacked")).to.equal(
1099
+ "true",
1100
+ );
1101
+ expect(controlBar.getAttribute("data-monster-layout-alignment")).to.equal(
1102
+ "left",
1103
+ );
1104
+ } finally {
1105
+ window.requestAnimationFrame = originalRequestAnimationFrame;
1106
+ globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
1107
+ }
1108
+ });
1109
+
1110
+ it("should fall back without throwing when the breakpoint container selector is invalid", async function () {
1111
+ const originalRequestAnimationFrame = window.requestAnimationFrame;
1112
+ const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
1113
+
1114
+ const scheduledCallbacks = [];
1115
+ const flushFrames = async () => {
1116
+ while (scheduledCallbacks.length > 0) {
1117
+ scheduledCallbacks.shift()();
1118
+ await new Promise((resolve) => setTimeout(resolve, 0));
1119
+ }
1120
+ };
1121
+
1122
+ try {
1123
+ window.requestAnimationFrame = (callback) => {
1124
+ scheduledCallbacks.push(callback);
1125
+ return scheduledCallbacks.length;
1126
+ };
1127
+ globalThis.requestAnimationFrame = window.requestAnimationFrame;
1128
+
1129
+ const mocks = document.getElementById("mocks");
1130
+ mocks.innerHTML = `
1131
+ <div id="invalid-reference-wrapper">
1132
+ <monster-control-bar
1133
+ id="invalid-reference-bar"
1134
+ data-monster-option-layout-alignment="right"
1135
+ data-monster-option-layout-stacked-alignment="left"
1136
+ data-monster-option-layout-stacked-breakpoint="480px"
1137
+ >
1138
+ <button id="invalid-reference-button">Run</button>
1139
+ </monster-control-bar>
1140
+ </div>
1141
+ `;
1142
+
1143
+ const wrapper = document.getElementById("invalid-reference-wrapper");
1144
+ const button = document.getElementById("invalid-reference-button");
1145
+ const bar = document.getElementById("invalid-reference-bar");
1146
+ const controlBar = bar.shadowRoot.querySelector(
1147
+ '[data-monster-role="control-bar"]',
1148
+ );
1149
+
1150
+ bar.setOption("layout.stackedBreakpointContainer", "[");
1151
+ wrapper.style.boxSizing = "border-box";
1152
+ Object.defineProperty(wrapper, "clientWidth", {
1153
+ configurable: true,
1154
+ value: 350,
1155
+ });
1156
+ Object.defineProperty(button, "offsetWidth", {
1157
+ configurable: true,
1158
+ value: 40,
1159
+ });
1160
+ Object.defineProperty(button, "offsetHeight", {
1161
+ configurable: true,
1162
+ value: 30,
1163
+ });
1164
+ button.getBoundingClientRect = () => ({
1165
+ width: 40,
1166
+ height: 30,
1167
+ top: 0,
1168
+ right: 40,
1169
+ bottom: 30,
1170
+ left: 0,
1171
+ x: 0,
1172
+ y: 0,
1173
+ toJSON: () => {},
1174
+ });
1175
+
1176
+ await flushFrames();
1177
+ await new Promise((resolve) => setTimeout(resolve, 0));
1178
+ await new Promise((resolve) => setTimeout(resolve, 0));
1179
+
1180
+ expect(controlBar.getAttribute("data-monster-layout-stacked")).to.equal(
1181
+ "true",
1182
+ );
1183
+ expect(controlBar.getAttribute("data-monster-layout-alignment")).to.equal(
1184
+ "left",
1185
+ );
1186
+ } finally {
1187
+ window.requestAnimationFrame = originalRequestAnimationFrame;
1188
+ globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
1189
+ }
1190
+ });
1191
+
1192
+ it("should observe the resolved breakpoint container and update observation when it changes", async function () {
1193
+ const OriginalResizeObserver = window.ResizeObserver;
1194
+ const originalGlobalResizeObserver = globalThis.ResizeObserver;
1195
+ const originalRequestAnimationFrame = window.requestAnimationFrame;
1196
+ const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
1197
+
1198
+ class TrackingResizeObserver extends ResizeObserverMock {
1199
+ static instances = [];
1200
+
1201
+ constructor(callback) {
1202
+ super(callback);
1203
+ TrackingResizeObserver.instances.push(this);
1204
+ }
1205
+ }
1206
+
1207
+ const scheduledCallbacks = [];
1208
+ const flushFrames = async () => {
1209
+ while (scheduledCallbacks.length > 0) {
1210
+ scheduledCallbacks.shift()();
1211
+ await new Promise((resolve) => setTimeout(resolve, 0));
1212
+ }
1213
+ };
1214
+
1215
+ try {
1216
+ window.ResizeObserver = TrackingResizeObserver;
1217
+ globalThis.ResizeObserver = TrackingResizeObserver;
1218
+ window.requestAnimationFrame = (callback) => {
1219
+ scheduledCallbacks.push(callback);
1220
+ return scheduledCallbacks.length;
1221
+ };
1222
+ globalThis.requestAnimationFrame = window.requestAnimationFrame;
1223
+
1224
+ const mocks = document.getElementById("mocks");
1225
+ mocks.innerHTML = `
1226
+ <div id="observed-reference-wrapper">
1227
+ <monster-control-bar
1228
+ id="observed-reference-bar"
1229
+ data-monster-option-layout-stacked-breakpoint="480px"
1230
+ data-monster-option-layout-stacked-breakpoint-container="#reference-a"
1231
+ >
1232
+ <button id="observed-reference-button">Run</button>
1233
+ </monster-control-bar>
1234
+ </div>
1235
+ <div id="reference-a"></div>
1236
+ <div id="reference-b"></div>
1237
+ `;
1238
+
1239
+ const wrapper = document.getElementById("observed-reference-wrapper");
1240
+ const referenceA = document.getElementById("reference-a");
1241
+ const referenceB = document.getElementById("reference-b");
1242
+ const button = document.getElementById("observed-reference-button");
1243
+ const bar = document.getElementById("observed-reference-bar");
1244
+
1245
+ wrapper.style.boxSizing = "border-box";
1246
+ referenceA.style.boxSizing = "border-box";
1247
+ referenceB.style.boxSizing = "border-box";
1248
+ Object.defineProperty(wrapper, "clientWidth", {
1249
+ configurable: true,
1250
+ value: 459,
1251
+ });
1252
+ Object.defineProperty(referenceA, "clientWidth", {
1253
+ configurable: true,
1254
+ value: 640,
1255
+ });
1256
+ Object.defineProperty(referenceB, "clientWidth", {
1257
+ configurable: true,
1258
+ value: 350,
1259
+ });
1260
+ Object.defineProperty(button, "offsetWidth", {
1261
+ configurable: true,
1262
+ value: 40,
1263
+ });
1264
+ Object.defineProperty(button, "offsetHeight", {
1265
+ configurable: true,
1266
+ value: 30,
1267
+ });
1268
+ button.getBoundingClientRect = () => ({
1269
+ width: 40,
1270
+ height: 30,
1271
+ top: 0,
1272
+ right: 40,
1273
+ bottom: 30,
1274
+ left: 0,
1275
+ x: 0,
1276
+ y: 0,
1277
+ toJSON: () => {},
1278
+ });
1279
+
1280
+ await flushFrames();
1281
+ await new Promise((resolve) => setTimeout(resolve, 0));
1282
+ await new Promise((resolve) => setTimeout(resolve, 0));
1283
+
1284
+ const observer = TrackingResizeObserver.instances[0];
1285
+ expect(observer.elements).to.include(referenceA);
1286
+
1287
+ bar.setOption("layout.stackedBreakpointContainer", "#reference-b");
1288
+ await flushFrames();
1289
+ await new Promise((resolve) => setTimeout(resolve, 0));
1290
+ await new Promise((resolve) => setTimeout(resolve, 0));
1291
+
1292
+ expect(observer.elements).to.not.include(referenceA);
1293
+ expect(observer.elements).to.include(referenceB);
1294
+ } finally {
1295
+ window.ResizeObserver = OriginalResizeObserver;
1296
+ globalThis.ResizeObserver = originalGlobalResizeObserver;
1297
+ window.requestAnimationFrame = originalRequestAnimationFrame;
1298
+ globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
1299
+ }
1300
+ });
843
1301
  });
@@ -1613,9 +1613,7 @@ describe('Select', function () {
1613
1613
  }
1614
1614
  });
1615
1615
 
1616
- it('should keep empty equivalent matching type-safe for numeric zero', function (done) {
1617
- this.timeout(2000);
1618
-
1616
+ it('should keep empty equivalent matching type-safe for numeric zero', async function () {
1619
1617
  let mocks = document.getElementById('mocks');
1620
1618
  const stringEquivalentSelect = document.createElement('monster-select');
1621
1619
  stringEquivalentSelect.setOption('empty.equivalents', ['0']);
@@ -1625,31 +1623,27 @@ describe('Select', function () {
1625
1623
  numericEquivalentSelect.setOption('empty.equivalents', [0]);
1626
1624
  mocks.appendChild(numericEquivalentSelect);
1627
1625
 
1628
- setTimeout(() => {
1629
- stringEquivalentSelect.value = 0;
1630
- numericEquivalentSelect.value = 0;
1626
+ stringEquivalentSelect.value = 0;
1627
+ numericEquivalentSelect.value = 0;
1631
1628
 
1632
- setTimeout(() => {
1633
- try {
1634
- expect(stringEquivalentSelect.value).to.equal('0');
1635
- expect(stringEquivalentSelect.getOption('selection')).to.deep.equal([
1636
- {
1637
- label: '0',
1638
- value: 0,
1639
- class: 'monster-badge-primary',
1640
- unresolved: false
1641
- }
1642
- ]);
1643
-
1644
- expect(numericEquivalentSelect.value).to.equal('');
1645
- expect(numericEquivalentSelect.getOption('selection')).to.deep.equal([]);
1646
- } catch (e) {
1647
- return done(e);
1648
- }
1629
+ await waitForCondition(() => {
1630
+ return stringEquivalentSelect.value === '0' &&
1631
+ Array.isArray(stringEquivalentSelect.getOption('selection')) &&
1632
+ stringEquivalentSelect.getOption('selection').length === 1 &&
1633
+ numericEquivalentSelect.value === '' &&
1634
+ Array.isArray(numericEquivalentSelect.getOption('selection')) &&
1635
+ numericEquivalentSelect.getOption('selection').length === 0;
1636
+ });
1649
1637
 
1650
- done();
1651
- }, 50);
1652
- }, 50);
1638
+ expect(stringEquivalentSelect.getOption('selection')).to.deep.equal([
1639
+ {
1640
+ label: '0',
1641
+ value: 0,
1642
+ class: 'monster-badge-primary',
1643
+ unresolved: false
1644
+ }
1645
+ ]);
1646
+ expect(numericEquivalentSelect.getOption('selection')).to.deep.equal([]);
1653
1647
  });
1654
1648
 
1655
1649
  it('should not refetch after selecting an already loaded remote option', function (done) {