@schukai/monster 4.141.2 → 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.2"}
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",
@@ -301,9 +309,12 @@ class ControlBar extends CustomElement {
301
309
  if (
302
310
  path === "layout.alignment" ||
303
311
  path === "layout.stackedAlignment" ||
304
- path === "layout.stackedBreakpoint"
312
+ path === "layout.stackedBreakpoint" ||
313
+ path === "layout.stackedBreakpointContainer"
305
314
  ) {
306
- syncLayoutState.call(this);
315
+ syncLayoutState.call(this, {
316
+ observe: path === "layout.stackedBreakpointContainer",
317
+ });
307
318
  }
308
319
  return this;
309
320
  }
@@ -355,6 +366,7 @@ class ControlBar extends CustomElement {
355
366
  attributes.push(ATTRIBUTE_OPTION_LAYOUT_ALIGNMENT);
356
367
  attributes.push(ATTRIBUTE_OPTION_LAYOUT_STACKED_ALIGNMENT);
357
368
  attributes.push(ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT);
369
+ attributes.push(ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT_CONTAINER);
358
370
  return attributes;
359
371
  }
360
372
 
@@ -590,6 +602,12 @@ function initEventHandler() {
590
602
  syncLayoutState.call(self);
591
603
  };
592
604
 
605
+ self[attributeObserverSymbol][
606
+ ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT_CONTAINER
607
+ ] = () => {
608
+ syncLayoutState.call(self, { observe: true });
609
+ };
610
+
593
611
  self[resizeObserverSymbol] = new ResizeObserver(() => {
594
612
  scheduleLayout.call(self, { measure: true, layout: true });
595
613
  });
@@ -1272,6 +1290,10 @@ function calculateControlBarDimensions() {
1272
1290
 
1273
1291
  this[dimensionsSymbol].setVia("data.visible", !(containerWidth === 0));
1274
1292
  this[dimensionsSymbol].setVia("data.containerWidth", containerWidth);
1293
+ this[dimensionsSymbol].setVia(
1294
+ "data.stackedBreakpointContainerWidth",
1295
+ getStackedBreakpointContainerWidth.call(this),
1296
+ );
1275
1297
 
1276
1298
  const itemReferences = [];
1277
1299
 
@@ -1336,6 +1358,65 @@ function calculateControlBarDimensions() {
1336
1358
  this[dimensionsSymbol].setVia("data.itemReferences", itemReferences);
1337
1359
  }
1338
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
+
1339
1420
  /**
1340
1421
  * @private
1341
1422
  */
@@ -1369,20 +1450,21 @@ function updateResizeObserverObservation() {
1369
1450
  */
1370
1451
  function getLayoutObservedNodes() {
1371
1452
  const observedNodes = [];
1372
- Array.from(this.children).forEach((node) => {
1373
- if (node instanceof HTMLElement) {
1453
+ const addObservedNode = (node) => {
1454
+ if (node instanceof HTMLElement && !observedNodes.includes(node)) {
1374
1455
  observedNodes.push(node);
1375
1456
  }
1376
- });
1457
+ };
1458
+
1459
+ Array.from(this.children).forEach(addObservedNode);
1377
1460
 
1378
1461
  let parent = this.parentNode;
1379
1462
  while (!(parent instanceof HTMLElement) && parent !== null) {
1380
1463
  parent = parent.parentNode;
1381
1464
  }
1382
1465
 
1383
- if (parent instanceof HTMLElement) {
1384
- observedNodes.push(parent);
1385
- }
1466
+ addObservedNode(parent);
1467
+ addObservedNode(resolveStackedBreakpointContainer.call(this));
1386
1468
 
1387
1469
  return observedNodes;
1388
1470
  }
@@ -1567,11 +1649,16 @@ function applyLayoutAlignment() {
1567
1649
 
1568
1650
  /**
1569
1651
  * @private
1652
+ * @param {{observe?: boolean}} options
1570
1653
  * @return {void}
1571
1654
  */
1572
- function syncLayoutState() {
1655
+ function syncLayoutState(options = {}) {
1573
1656
  applyLayoutAlignment.call(this);
1574
- scheduleLayout.call(this, { measure: true, layout: true });
1657
+ scheduleLayout.call(this, {
1658
+ measure: true,
1659
+ layout: true,
1660
+ observe: options.observe === true,
1661
+ });
1575
1662
  }
1576
1663
 
1577
1664
  /**
@@ -1586,11 +1673,19 @@ function isStackedBreakpointMatched() {
1586
1673
 
1587
1674
  let width = 0;
1588
1675
  try {
1589
- width = this[dimensionsSymbol].getVia("data.containerWidth");
1590
- } catch {
1676
+ width = this[dimensionsSymbol].getVia(
1677
+ "data.stackedBreakpointContainerWidth",
1678
+ );
1679
+ } catch {}
1680
+
1681
+ if (!(width > 0)) {
1591
1682
  try {
1592
- width = this[dimensionsSymbol].getVia("data.space");
1593
- } catch {}
1683
+ width = this[dimensionsSymbol].getVia("data.containerWidth");
1684
+ } catch {
1685
+ try {
1686
+ width = this[dimensionsSymbol].getVia("data.space");
1687
+ } catch {}
1688
+ }
1594
1689
  }
1595
1690
 
1596
1691
  if (!(width > 0)) {
@@ -1720,6 +1815,9 @@ function getLayoutChangedEventDetail() {
1720
1815
  stacked,
1721
1816
  stackedAlignment: this.getOption("layout.stackedAlignment"),
1722
1817
  stackedBreakpoint: this.getOption("layout.stackedBreakpoint"),
1818
+ stackedBreakpointContainer: this.getOption(
1819
+ "layout.stackedBreakpointContainer",
1820
+ ),
1723
1821
  };
1724
1822
  }
1725
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
 
@@ -843,4 +844,458 @@ describe("ControlBar", function () {
843
844
  globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
844
845
  }
845
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
+ });
846
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) {