@schukai/monster 4.142.0 → 4.142.2

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.142.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.142.2"}
@@ -204,6 +204,7 @@ const EVENT_LAYOUT_CHANGED = "monster-control-bar-layout-changed";
204
204
  * @type {number}
205
205
  */
206
206
  const LAYOUT_CHANGED_EVENT_DELAY = 20;
207
+ const LAYOUT_SWITCH_HYSTERESIS = 16;
207
208
 
208
209
  /**
209
210
  * A control bar control.
@@ -610,6 +611,9 @@ function initEventHandler() {
610
611
  };
611
612
 
612
613
  self[resizeObserverSymbol] = new ResizeObserver((entries) => {
614
+ if (self[layoutStateSymbol]?.suppressResize) {
615
+ return;
616
+ }
613
617
  if (!hasContainerResizeEntry.call(self, entries)) {
614
618
  return;
615
619
  }
@@ -798,6 +802,7 @@ function rearrangeItems() {
798
802
  availableSpace = 0;
799
803
  }
800
804
  let sum = 0;
805
+ let overflowStarted = false;
801
806
  const visibleItemsInMainSlot = [];
802
807
  const itemsToMoveToPopper = [];
803
808
 
@@ -807,7 +812,8 @@ function rearrangeItems() {
807
812
  continue;
808
813
  }
809
814
 
810
- if (sum + entry.width > availableSpace) {
815
+ if (overflowStarted || sum + entry.width > availableSpace) {
816
+ overflowStarted = true;
811
817
  itemsToMoveToPopper.push(entry.element);
812
818
  } else {
813
819
  sum += entry.width;
@@ -818,12 +824,41 @@ function rearrangeItems() {
818
824
  return { visibleItemsInMainSlot, itemsToMoveToPopper };
819
825
  };
820
826
 
821
- let layout = layoutItems(space);
822
- if (layout.itemsToMoveToPopper.length > 0) {
823
- layout = layoutItems(space - switchWidth);
827
+ const switchCurrentlyVisible =
828
+ this[switchElementSymbol] instanceof HTMLElement &&
829
+ !this[switchElementSymbol].hasAttribute("hidden");
830
+ let layout;
831
+ if (switchCurrentlyVisible) {
832
+ layout = layoutItems(space - switchWidth - LAYOUT_SWITCH_HYSTERESIS);
833
+ if (layout.itemsToMoveToPopper.length === 0) {
834
+ layout = layoutItems(space);
835
+ }
836
+ } else {
837
+ layout = layoutItems(space - LAYOUT_SWITCH_HYSTERESIS);
838
+ if (layout.itemsToMoveToPopper.length > 0) {
839
+ layout = layoutItems(space - switchWidth);
840
+ } else {
841
+ layout = layoutItems(space);
842
+ }
824
843
  }
825
844
 
845
+ layout = normalizeSpacerOverflowLayout(
846
+ layout,
847
+ itemEntries.map((entry) => entry.element),
848
+ );
849
+ layout = stabilizeOverflowBoundary.call(
850
+ this,
851
+ layout,
852
+ itemEntries,
853
+ switchCurrentlyVisible,
854
+ );
855
+
826
856
  const shouldShowSwitch = layout.itemsToMoveToPopper.length > 0 && hasItems;
857
+ updateLastOverflowBoundary.call(
858
+ this,
859
+ layout,
860
+ itemEntries.map((entry) => entry.element),
861
+ );
827
862
  const shouldUseStackedLayout =
828
863
  shouldShowSwitch || isStackedBreakpointMatched.call(this);
829
864
 
@@ -851,6 +886,147 @@ function rearrangeItems() {
851
886
  }
852
887
  }
853
888
 
889
+ /**
890
+ * @private
891
+ * @param {{visibleItemsInMainSlot: HTMLElement[], itemsToMoveToPopper: HTMLElement[]}} layout
892
+ * @param {{element: HTMLElement, hidden: boolean}[]} itemEntries
893
+ * @param {boolean} switchCurrentlyVisible
894
+ * @return {{visibleItemsInMainSlot: HTMLElement[], itemsToMoveToPopper: HTMLElement[]}}
895
+ */
896
+ function stabilizeOverflowBoundary(layout, itemEntries, switchCurrentlyVisible) {
897
+ const state = this[layoutStateSymbol];
898
+ if (!switchCurrentlyVisible || !state) {
899
+ return layout;
900
+ }
901
+
902
+ const orderedItems = itemEntries.map((entry) => entry.element);
903
+ const nextBoundary = getFirstOverflowIndex(layout, orderedItems);
904
+ const previousBoundary = state.lastOverflowStartIndex;
905
+ if (
906
+ typeof previousBoundary !== "number" ||
907
+ typeof nextBoundary !== "number" ||
908
+ nextBoundary <= previousBoundary ||
909
+ previousBoundary < 0 ||
910
+ previousBoundary >= itemEntries.length
911
+ ) {
912
+ return layout;
913
+ }
914
+
915
+ const popperItems = new Set();
916
+ const mainItems = new Set();
917
+ for (let index = 0; index < itemEntries.length; index++) {
918
+ const entry = itemEntries[index];
919
+ if (index >= previousBoundary && !entry.hidden) {
920
+ popperItems.add(entry.element);
921
+ } else {
922
+ mainItems.add(entry.element);
923
+ }
924
+ }
925
+
926
+ return normalizeSpacerOverflowLayout(
927
+ {
928
+ visibleItemsInMainSlot: orderedItems.filter((item) => mainItems.has(item)),
929
+ itemsToMoveToPopper: orderedItems.filter((item) => popperItems.has(item)),
930
+ },
931
+ orderedItems,
932
+ );
933
+ }
934
+
935
+ /**
936
+ * @private
937
+ * @param {{itemsToMoveToPopper: HTMLElement[]}} layout
938
+ * @param {HTMLElement[]} orderedItems
939
+ * @return {void}
940
+ */
941
+ function updateLastOverflowBoundary(layout, orderedItems) {
942
+ const state = this[layoutStateSymbol];
943
+ if (!state) {
944
+ return;
945
+ }
946
+
947
+ const boundary = getFirstOverflowIndex(layout, orderedItems);
948
+ if (typeof boundary === "number") {
949
+ state.lastOverflowStartIndex = boundary;
950
+ return;
951
+ }
952
+ delete state.lastOverflowStartIndex;
953
+ }
954
+
955
+ /**
956
+ * @private
957
+ * @param {{itemsToMoveToPopper: HTMLElement[]}} layout
958
+ * @param {HTMLElement[]} orderedItems
959
+ * @return {number|undefined}
960
+ */
961
+ function getFirstOverflowIndex(layout, orderedItems) {
962
+ const popperItems = new Set(layout.itemsToMoveToPopper);
963
+ for (let index = 0; index < orderedItems.length; index++) {
964
+ if (popperItems.has(orderedItems[index])) {
965
+ return index;
966
+ }
967
+ }
968
+ return undefined;
969
+ }
970
+
971
+ /**
972
+ * @private
973
+ * @param {{visibleItemsInMainSlot: HTMLElement[], itemsToMoveToPopper: HTMLElement[]}} layout
974
+ * @param {HTMLElement[]} orderedItems
975
+ * @return {{visibleItemsInMainSlot: HTMLElement[], itemsToMoveToPopper: HTMLElement[]}}
976
+ */
977
+ function normalizeSpacerOverflowLayout(layout, orderedItems) {
978
+ if (
979
+ !layout.itemsToMoveToPopper.some(
980
+ (item) => !isControlBarSpacerElement(item),
981
+ )
982
+ ) {
983
+ return layout;
984
+ }
985
+
986
+ const popperItems = new Set(layout.itemsToMoveToPopper);
987
+ const mainItems = new Set(layout.visibleItemsInMainSlot);
988
+
989
+ for (let index = 0; index < orderedItems.length; index++) {
990
+ const item = orderedItems[index];
991
+ if (!isControlBarSpacerElement(item) || !mainItems.has(item)) {
992
+ continue;
993
+ }
994
+
995
+ const previous = findAdjacentNonSpacerItem(orderedItems, index, -1);
996
+ const next = findAdjacentNonSpacerItem(orderedItems, index, 1);
997
+ if (popperItems.has(previous) || popperItems.has(next)) {
998
+ mainItems.delete(item);
999
+ popperItems.add(item);
1000
+ }
1001
+ }
1002
+
1003
+ return {
1004
+ visibleItemsInMainSlot: orderedItems.filter((item) => mainItems.has(item)),
1005
+ itemsToMoveToPopper: orderedItems.filter((item) => popperItems.has(item)),
1006
+ };
1007
+ }
1008
+
1009
+ /**
1010
+ * @private
1011
+ * @param {HTMLElement[]} orderedItems
1012
+ * @param {number} startIndex
1013
+ * @param {1|-1} direction
1014
+ * @return {HTMLElement|undefined}
1015
+ */
1016
+ function findAdjacentNonSpacerItem(orderedItems, startIndex, direction) {
1017
+ for (
1018
+ let index = startIndex + direction;
1019
+ index >= 0 && index < orderedItems.length;
1020
+ index += direction
1021
+ ) {
1022
+ const item = orderedItems[index];
1023
+ if (!isControlBarSpacerElement(item)) {
1024
+ return item;
1025
+ }
1026
+ }
1027
+ return undefined;
1028
+ }
1029
+
854
1030
  /**
855
1031
  * @private
856
1032
  * @param {HTMLElement} node
@@ -211,4 +211,115 @@ describe("ButtonBar", function () {
211
211
  globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
212
212
  }
213
213
  });
214
+
215
+ it("should ignore resize feedback caused by its own layout writes", async function () {
216
+ const OriginalResizeObserver = window.ResizeObserver;
217
+ const originalGlobalResizeObserver = globalThis.ResizeObserver;
218
+ const originalRequestAnimationFrame = window.requestAnimationFrame;
219
+ const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
220
+
221
+ class TrackingResizeObserver extends ResizeObserverMock {
222
+ static instances = [];
223
+
224
+ constructor(callback) {
225
+ super(callback);
226
+ TrackingResizeObserver.instances.push(this);
227
+ }
228
+ }
229
+
230
+ const scheduledCallbacks = [];
231
+ const flushOneFrame = async () => {
232
+ const callback = scheduledCallbacks.shift();
233
+ if (callback instanceof Function) {
234
+ callback();
235
+ await new Promise((resolve) => setTimeout(resolve, 0));
236
+ }
237
+ };
238
+ const flushFrames = async () => {
239
+ while (scheduledCallbacks.length > 0) {
240
+ await flushOneFrame();
241
+ }
242
+ };
243
+
244
+ try {
245
+ window.ResizeObserver = TrackingResizeObserver;
246
+ globalThis.ResizeObserver = TrackingResizeObserver;
247
+ window.requestAnimationFrame = (callback) => {
248
+ scheduledCallbacks.push(callback);
249
+ return scheduledCallbacks.length;
250
+ };
251
+ globalThis.requestAnimationFrame = window.requestAnimationFrame;
252
+
253
+ const mocks = document.getElementById("mocks");
254
+ mocks.innerHTML = `
255
+ <div id="feedback-bar-wrapper">
256
+ <monster-button-bar id="feedback-bar">
257
+ <button type="button">One</button>
258
+ <button type="button">Two</button>
259
+ <button type="button">Three</button>
260
+ </monster-button-bar>
261
+ </div>
262
+ `;
263
+
264
+ const wrapper = document.getElementById("feedback-bar-wrapper");
265
+ const bar = document.getElementById("feedback-bar");
266
+
267
+ wrapper.style.boxSizing = "border-box";
268
+ wrapper.style.width = "80px";
269
+ Object.defineProperty(wrapper, "clientWidth", {
270
+ configurable: true,
271
+ value: 80,
272
+ });
273
+
274
+ const buttons = Array.from(bar.querySelectorAll("button"));
275
+ for (const button of buttons) {
276
+ Object.defineProperty(button, "offsetWidth", {
277
+ configurable: true,
278
+ value: 48,
279
+ });
280
+ button.getBoundingClientRect = () => ({
281
+ width: 48,
282
+ height: 32,
283
+ top: 0,
284
+ left: 0,
285
+ right: 48,
286
+ bottom: 32,
287
+ x: 0,
288
+ y: 0,
289
+ });
290
+ }
291
+
292
+ const switchButton = bar.shadowRoot.querySelector(
293
+ '[data-monster-role="switch"]',
294
+ );
295
+ Object.defineProperty(switchButton, "offsetWidth", {
296
+ configurable: true,
297
+ value: 20,
298
+ });
299
+
300
+ await flushOneFrame();
301
+
302
+ const resizeObserver = TrackingResizeObserver.instances.find((observer) =>
303
+ observer.elements.includes(wrapper),
304
+ );
305
+ expect(resizeObserver).to.exist;
306
+
307
+ const suppressedFrameCount = scheduledCallbacks.length;
308
+ resizeObserver.triggerResize([{ target: wrapper }]);
309
+ expect(scheduledCallbacks.length).to.equal(suppressedFrameCount);
310
+
311
+ await flushFrames();
312
+
313
+ resizeObserver.triggerResize([{ target: wrapper }]);
314
+ expect(scheduledCallbacks.length).to.equal(1);
315
+
316
+ await flushFrames();
317
+ expect(scheduledCallbacks.length).to.equal(0);
318
+ } finally {
319
+ window.ResizeObserver = OriginalResizeObserver;
320
+ globalThis.ResizeObserver = originalGlobalResizeObserver;
321
+ window.requestAnimationFrame = originalRequestAnimationFrame;
322
+ globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
323
+ }
324
+ });
214
325
  });
@@ -756,6 +756,479 @@ describe("ControlBar", function () {
756
756
  }
757
757
  });
758
758
 
759
+ it("should keep the overflow switch stable across the switch-width threshold", async function () {
760
+ const OriginalResizeObserver = window.ResizeObserver;
761
+ const originalGlobalResizeObserver = globalThis.ResizeObserver;
762
+ const originalRequestAnimationFrame = window.requestAnimationFrame;
763
+ const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
764
+
765
+ class TrackingResizeObserver extends ResizeObserverMock {
766
+ static instances = [];
767
+
768
+ constructor(callback) {
769
+ super(callback);
770
+ TrackingResizeObserver.instances.push(this);
771
+ }
772
+ }
773
+
774
+ const scheduledCallbacks = [];
775
+ const flushFrames = async () => {
776
+ while (scheduledCallbacks.length > 0) {
777
+ scheduledCallbacks.shift()();
778
+ await new Promise((resolve) => setTimeout(resolve, 0));
779
+ }
780
+ };
781
+
782
+ try {
783
+ window.ResizeObserver = TrackingResizeObserver;
784
+ globalThis.ResizeObserver = TrackingResizeObserver;
785
+ window.requestAnimationFrame = (callback) => {
786
+ scheduledCallbacks.push(callback);
787
+ return scheduledCallbacks.length;
788
+ };
789
+ globalThis.requestAnimationFrame = window.requestAnimationFrame;
790
+
791
+ const mocks = document.getElementById("mocks");
792
+ mocks.innerHTML = `
793
+ <div id="threshold-bar-wrapper">
794
+ <monster-control-bar id="threshold-bar">
795
+ <button id="threshold-one">One</button>
796
+ <button id="threshold-two">Two</button>
797
+ <button id="threshold-three">Three</button>
798
+ </monster-control-bar>
799
+ </div>
800
+ `;
801
+
802
+ const wrapper = document.getElementById("threshold-bar-wrapper");
803
+ const bar = document.getElementById("threshold-bar");
804
+ const buttons = [
805
+ document.getElementById("threshold-one"),
806
+ document.getElementById("threshold-two"),
807
+ document.getElementById("threshold-three"),
808
+ ];
809
+ let wrapperWidth = 90;
810
+
811
+ wrapper.style.boxSizing = "border-box";
812
+ Object.defineProperty(wrapper, "clientWidth", {
813
+ configurable: true,
814
+ get: () => wrapperWidth,
815
+ });
816
+
817
+ for (const button of buttons) {
818
+ Object.defineProperty(button, "offsetWidth", {
819
+ configurable: true,
820
+ value: 50,
821
+ });
822
+ Object.defineProperty(button, "offsetHeight", {
823
+ configurable: true,
824
+ value: 30,
825
+ });
826
+ button.getBoundingClientRect = () => ({
827
+ width: 50,
828
+ height: 30,
829
+ top: 0,
830
+ right: 50,
831
+ bottom: 30,
832
+ left: 0,
833
+ x: 0,
834
+ y: 0,
835
+ toJSON: () => {},
836
+ });
837
+ }
838
+
839
+ const switchButton = bar.shadowRoot.querySelector(
840
+ '[data-monster-role="switch"]',
841
+ );
842
+ Object.defineProperty(switchButton, "offsetWidth", {
843
+ configurable: true,
844
+ value: 20,
845
+ });
846
+
847
+ await flushFrames();
848
+ expect(switchButton.hasAttribute("hidden")).to.be.false;
849
+
850
+ const resizeObserver = TrackingResizeObserver.instances.find((observer) =>
851
+ observer.elements.includes(wrapper),
852
+ );
853
+ expect(resizeObserver).to.exist;
854
+
855
+ wrapperWidth = 155;
856
+ resizeObserver.triggerResize([{ target: wrapper }]);
857
+ await flushFrames();
858
+ expect(switchButton.hasAttribute("hidden")).to.be.false;
859
+ expect(document.getElementById("threshold-three").getAttribute("slot")).to
860
+ .equal("popper");
861
+
862
+ wrapperWidth = 175;
863
+ resizeObserver.triggerResize([{ target: wrapper }]);
864
+ await flushFrames();
865
+ expect(switchButton.hasAttribute("hidden")).to.be.false;
866
+ expect(document.getElementById("threshold-three").getAttribute("slot")).to
867
+ .equal("popper");
868
+
869
+ wrapperWidth = 190;
870
+ resizeObserver.triggerResize([{ target: wrapper }]);
871
+ await flushFrames();
872
+ expect(switchButton.hasAttribute("hidden")).to.be.true;
873
+ expect(document.getElementById("threshold-three").hasAttribute("slot")).to
874
+ .be.false;
875
+
876
+ wrapperWidth = 160;
877
+ resizeObserver.triggerResize([{ target: wrapper }]);
878
+ await flushFrames();
879
+ expect(switchButton.hasAttribute("hidden")).to.be.false;
880
+ expect(document.getElementById("threshold-three").getAttribute("slot")).to
881
+ .equal("popper");
882
+ } finally {
883
+ window.ResizeObserver = OriginalResizeObserver;
884
+ globalThis.ResizeObserver = originalGlobalResizeObserver;
885
+ window.requestAnimationFrame = originalRequestAnimationFrame;
886
+ globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
887
+ }
888
+ });
889
+
890
+ it("should move spacers with adjacent overflow controls", async function () {
891
+ const OriginalResizeObserver = window.ResizeObserver;
892
+ const originalGlobalResizeObserver = globalThis.ResizeObserver;
893
+ const originalRequestAnimationFrame = window.requestAnimationFrame;
894
+ const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
895
+
896
+ class TrackingResizeObserver extends ResizeObserverMock {
897
+ static instances = [];
898
+
899
+ constructor(callback) {
900
+ super(callback);
901
+ TrackingResizeObserver.instances.push(this);
902
+ }
903
+ }
904
+
905
+ const scheduledCallbacks = [];
906
+ const flushFrames = async () => {
907
+ while (scheduledCallbacks.length > 0) {
908
+ scheduledCallbacks.shift()();
909
+ await new Promise((resolve) => setTimeout(resolve, 0));
910
+ }
911
+ };
912
+
913
+ try {
914
+ window.ResizeObserver = TrackingResizeObserver;
915
+ globalThis.ResizeObserver = TrackingResizeObserver;
916
+ window.requestAnimationFrame = (callback) => {
917
+ scheduledCallbacks.push(callback);
918
+ return scheduledCallbacks.length;
919
+ };
920
+ globalThis.requestAnimationFrame = window.requestAnimationFrame;
921
+
922
+ const mocks = document.getElementById("mocks");
923
+ mocks.innerHTML = `
924
+ <div id="spacer-overflow-wrapper">
925
+ <monster-control-bar id="spacer-overflow-bar">
926
+ <button id="spacer-overflow-one">One</button>
927
+ <monster-control-bar-spacer id="spacer-overflow-spacer-one"></monster-control-bar-spacer>
928
+ <button id="spacer-overflow-two">Two</button>
929
+ <monster-control-bar-spacer id="spacer-overflow-spacer-two"></monster-control-bar-spacer>
930
+ <button id="spacer-overflow-three">Three</button>
931
+ </monster-control-bar>
932
+ </div>
933
+ `;
934
+
935
+ const wrapper = document.getElementById("spacer-overflow-wrapper");
936
+ const bar = document.getElementById("spacer-overflow-bar");
937
+ const sizes = new Map([
938
+ ["spacer-overflow-one", 50],
939
+ ["spacer-overflow-spacer-one", 10],
940
+ ["spacer-overflow-two", 100],
941
+ ["spacer-overflow-spacer-two", 10],
942
+ ["spacer-overflow-three", 50],
943
+ ]);
944
+
945
+ wrapper.style.boxSizing = "border-box";
946
+ Object.defineProperty(wrapper, "clientWidth", {
947
+ configurable: true,
948
+ value: 100,
949
+ });
950
+
951
+ for (const [id, width] of sizes) {
952
+ const element = document.getElementById(id);
953
+ Object.defineProperty(element, "offsetWidth", {
954
+ configurable: true,
955
+ value: width,
956
+ });
957
+ Object.defineProperty(element, "offsetHeight", {
958
+ configurable: true,
959
+ value: 30,
960
+ });
961
+ element.getBoundingClientRect = () => ({
962
+ width,
963
+ height: 30,
964
+ top: 0,
965
+ right: width,
966
+ bottom: 30,
967
+ left: 0,
968
+ x: 0,
969
+ y: 0,
970
+ toJSON: () => {},
971
+ });
972
+ }
973
+
974
+ const switchButton = bar.shadowRoot.querySelector(
975
+ '[data-monster-role="switch"]',
976
+ );
977
+ Object.defineProperty(switchButton, "offsetWidth", {
978
+ configurable: true,
979
+ value: 20,
980
+ });
981
+
982
+ await flushFrames();
983
+ expect(document.getElementById("spacer-overflow-one").hasAttribute("slot"))
984
+ .to.be.false;
985
+ expect(
986
+ document.getElementById("spacer-overflow-spacer-one").getAttribute("slot"),
987
+ ).to.equal("popper");
988
+ expect(document.getElementById("spacer-overflow-two").getAttribute("slot"))
989
+ .to.equal("popper");
990
+ expect(
991
+ document.getElementById("spacer-overflow-spacer-two").getAttribute("slot"),
992
+ ).to.equal("popper");
993
+ expect(
994
+ document.getElementById("spacer-overflow-three").getAttribute("slot"),
995
+ ).to.equal("popper");
996
+
997
+ const resizeObserver = TrackingResizeObserver.instances.find((observer) =>
998
+ observer.elements.includes(wrapper),
999
+ );
1000
+ expect(resizeObserver).to.exist;
1001
+
1002
+ resizeObserver.triggerResize([{ target: wrapper }]);
1003
+ await flushFrames();
1004
+ expect(
1005
+ document.getElementById("spacer-overflow-spacer-one").getAttribute("slot"),
1006
+ ).to.equal("popper");
1007
+ expect(
1008
+ document.getElementById("spacer-overflow-spacer-two").getAttribute("slot"),
1009
+ ).to.equal("popper");
1010
+ } finally {
1011
+ window.ResizeObserver = OriginalResizeObserver;
1012
+ globalThis.ResizeObserver = originalGlobalResizeObserver;
1013
+ window.requestAnimationFrame = originalRequestAnimationFrame;
1014
+ globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
1015
+ }
1016
+ });
1017
+
1018
+ it("should keep overflow as an ordered suffix when later controls would fit", async function () {
1019
+ const originalRequestAnimationFrame = window.requestAnimationFrame;
1020
+ const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
1021
+
1022
+ const scheduledCallbacks = [];
1023
+ const flushFrames = async () => {
1024
+ while (scheduledCallbacks.length > 0) {
1025
+ scheduledCallbacks.shift()();
1026
+ await new Promise((resolve) => setTimeout(resolve, 0));
1027
+ }
1028
+ };
1029
+
1030
+ try {
1031
+ window.requestAnimationFrame = (callback) => {
1032
+ scheduledCallbacks.push(callback);
1033
+ return scheduledCallbacks.length;
1034
+ };
1035
+ globalThis.requestAnimationFrame = window.requestAnimationFrame;
1036
+
1037
+ const mocks = document.getElementById("mocks");
1038
+ mocks.innerHTML = `
1039
+ <div id="ordered-overflow-wrapper">
1040
+ <monster-control-bar id="ordered-overflow-bar">
1041
+ <button id="ordered-overflow-one">One</button>
1042
+ <button id="ordered-overflow-wide">Wide</button>
1043
+ <button id="ordered-overflow-small">Small</button>
1044
+ </monster-control-bar>
1045
+ </div>
1046
+ `;
1047
+
1048
+ const wrapper = document.getElementById("ordered-overflow-wrapper");
1049
+ const bar = document.getElementById("ordered-overflow-bar");
1050
+ const sizes = new Map([
1051
+ ["ordered-overflow-one", 50],
1052
+ ["ordered-overflow-wide", 100],
1053
+ ["ordered-overflow-small", 20],
1054
+ ]);
1055
+
1056
+ wrapper.style.boxSizing = "border-box";
1057
+ Object.defineProperty(wrapper, "clientWidth", {
1058
+ configurable: true,
1059
+ value: 90,
1060
+ });
1061
+
1062
+ for (const [id, width] of sizes) {
1063
+ const element = document.getElementById(id);
1064
+ Object.defineProperty(element, "offsetWidth", {
1065
+ configurable: true,
1066
+ value: width,
1067
+ });
1068
+ Object.defineProperty(element, "offsetHeight", {
1069
+ configurable: true,
1070
+ value: 30,
1071
+ });
1072
+ element.getBoundingClientRect = () => ({
1073
+ width,
1074
+ height: 30,
1075
+ top: 0,
1076
+ right: width,
1077
+ bottom: 30,
1078
+ left: 0,
1079
+ x: 0,
1080
+ y: 0,
1081
+ toJSON: () => {},
1082
+ });
1083
+ }
1084
+
1085
+ const switchButton = bar.shadowRoot.querySelector(
1086
+ '[data-monster-role="switch"]',
1087
+ );
1088
+ Object.defineProperty(switchButton, "offsetWidth", {
1089
+ configurable: true,
1090
+ value: 20,
1091
+ });
1092
+
1093
+ await flushFrames();
1094
+
1095
+ expect(document.getElementById("ordered-overflow-one").hasAttribute("slot"))
1096
+ .to.be.false;
1097
+ expect(document.getElementById("ordered-overflow-wide").getAttribute("slot"))
1098
+ .to.equal("popper");
1099
+ expect(document.getElementById("ordered-overflow-small").getAttribute("slot"))
1100
+ .to.equal("popper");
1101
+ } finally {
1102
+ window.requestAnimationFrame = originalRequestAnimationFrame;
1103
+ globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
1104
+ }
1105
+ });
1106
+
1107
+ it("should keep the previous overflow boundary while the switch remains visible", async function () {
1108
+ const OriginalResizeObserver = window.ResizeObserver;
1109
+ const originalGlobalResizeObserver = globalThis.ResizeObserver;
1110
+ const originalRequestAnimationFrame = window.requestAnimationFrame;
1111
+ const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
1112
+
1113
+ class TrackingResizeObserver extends ResizeObserverMock {
1114
+ static instances = [];
1115
+
1116
+ constructor(callback) {
1117
+ super(callback);
1118
+ TrackingResizeObserver.instances.push(this);
1119
+ }
1120
+ }
1121
+
1122
+ const scheduledCallbacks = [];
1123
+ const flushFrames = async () => {
1124
+ while (scheduledCallbacks.length > 0) {
1125
+ scheduledCallbacks.shift()();
1126
+ await new Promise((resolve) => setTimeout(resolve, 0));
1127
+ }
1128
+ };
1129
+
1130
+ try {
1131
+ window.ResizeObserver = TrackingResizeObserver;
1132
+ globalThis.ResizeObserver = TrackingResizeObserver;
1133
+ window.requestAnimationFrame = (callback) => {
1134
+ scheduledCallbacks.push(callback);
1135
+ return scheduledCallbacks.length;
1136
+ };
1137
+ globalThis.requestAnimationFrame = window.requestAnimationFrame;
1138
+
1139
+ const mocks = document.getElementById("mocks");
1140
+ mocks.innerHTML = `
1141
+ <div id="sticky-overflow-wrapper">
1142
+ <monster-control-bar id="sticky-overflow-bar">
1143
+ <button id="sticky-overflow-one">One</button>
1144
+ <button id="sticky-overflow-wide">Wide</button>
1145
+ <button id="sticky-overflow-small">Small</button>
1146
+ </monster-control-bar>
1147
+ </div>
1148
+ `;
1149
+
1150
+ const wrapper = document.getElementById("sticky-overflow-wrapper");
1151
+ const bar = document.getElementById("sticky-overflow-bar");
1152
+ let wrapperWidth = 90;
1153
+ const sizes = new Map([
1154
+ ["sticky-overflow-one", 50],
1155
+ ["sticky-overflow-wide", 100],
1156
+ ["sticky-overflow-small", 20],
1157
+ ]);
1158
+
1159
+ wrapper.style.boxSizing = "border-box";
1160
+ Object.defineProperty(wrapper, "clientWidth", {
1161
+ configurable: true,
1162
+ get: () => wrapperWidth,
1163
+ });
1164
+
1165
+ for (const [id, width] of sizes) {
1166
+ const element = document.getElementById(id);
1167
+ Object.defineProperty(element, "offsetWidth", {
1168
+ configurable: true,
1169
+ value: width,
1170
+ });
1171
+ Object.defineProperty(element, "offsetHeight", {
1172
+ configurable: true,
1173
+ value: 30,
1174
+ });
1175
+ element.getBoundingClientRect = () => ({
1176
+ width,
1177
+ height: 30,
1178
+ top: 0,
1179
+ right: width,
1180
+ bottom: 30,
1181
+ left: 0,
1182
+ x: 0,
1183
+ y: 0,
1184
+ toJSON: () => {},
1185
+ });
1186
+ }
1187
+
1188
+ const switchButton = bar.shadowRoot.querySelector(
1189
+ '[data-monster-role="switch"]',
1190
+ );
1191
+ Object.defineProperty(switchButton, "offsetWidth", {
1192
+ configurable: true,
1193
+ value: 20,
1194
+ });
1195
+
1196
+ await flushFrames();
1197
+ expect(document.getElementById("sticky-overflow-wide").getAttribute("slot"))
1198
+ .to.equal("popper");
1199
+ expect(document.getElementById("sticky-overflow-small").getAttribute("slot"))
1200
+ .to.equal("popper");
1201
+
1202
+ const resizeObserver = TrackingResizeObserver.instances.find((observer) =>
1203
+ observer.elements.includes(wrapper),
1204
+ );
1205
+ expect(resizeObserver).to.exist;
1206
+
1207
+ wrapperWidth = 190;
1208
+ resizeObserver.triggerResize([{ target: wrapper }]);
1209
+ await flushFrames();
1210
+ expect(switchButton.hasAttribute("hidden")).to.be.false;
1211
+ expect(document.getElementById("sticky-overflow-wide").getAttribute("slot"))
1212
+ .to.equal("popper");
1213
+ expect(document.getElementById("sticky-overflow-small").getAttribute("slot"))
1214
+ .to.equal("popper");
1215
+
1216
+ wrapperWidth = 220;
1217
+ resizeObserver.triggerResize([{ target: wrapper }]);
1218
+ await flushFrames();
1219
+ expect(switchButton.hasAttribute("hidden")).to.be.true;
1220
+ expect(document.getElementById("sticky-overflow-wide").hasAttribute("slot"))
1221
+ .to.be.false;
1222
+ expect(document.getElementById("sticky-overflow-small").hasAttribute("slot"))
1223
+ .to.be.false;
1224
+ } finally {
1225
+ window.ResizeObserver = OriginalResizeObserver;
1226
+ globalThis.ResizeObserver = originalGlobalResizeObserver;
1227
+ window.requestAnimationFrame = originalRequestAnimationFrame;
1228
+ globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
1229
+ }
1230
+ });
1231
+
759
1232
  it("should match the stacked breakpoint when the content-box container width is a css string", async function () {
760
1233
  const originalRequestAnimationFrame = window.requestAnimationFrame;
761
1234
  const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;