@schukai/monster 4.140.2 → 4.141.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.140.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.0"}
@@ -36,11 +36,13 @@ import { getDocument } from "../../dom/util.mjs";
36
36
  import { getGlobal } from "../../types/global.mjs";
37
37
  import { ID } from "../../types/id.mjs";
38
38
  import { Observer } from "../../types/observer.mjs";
39
+ import { Queue } from "../../types/queue.mjs";
39
40
  import { STYLE_DISPLAY_MODE_BLOCK } from "./constants.mjs";
40
41
  import { ControlBarStyleSheet } from "./stylesheet/control-bar.mjs";
41
42
  import { positionPopper } from "./util/floating-ui.mjs";
42
43
  import { convertToPixels } from "../../dom/dimension.mjs";
43
44
  import { addErrorAttribute } from "../../dom/error.mjs";
45
+ import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
44
46
  import { Processing } from "../../util/processing.mjs";
45
47
  export { ControlBar };
46
48
 
@@ -132,6 +134,11 @@ const layoutTokenSymbol = Symbol("layoutToken");
132
134
  const observedLayoutNodesSignatureSymbol = Symbol(
133
135
  "observedLayoutNodesSignature",
134
136
  );
137
+ const layoutChangedEventQueueSymbol = Symbol("layoutChangedEventQueue");
138
+ const layoutChangedEventSwitchSymbol = Symbol("layoutChangedEventSwitch");
139
+ const layoutChangedEventLastSignatureSymbol = Symbol(
140
+ "layoutChangedEventLastSignature",
141
+ );
135
142
 
136
143
  /**
137
144
  * @private
@@ -152,6 +159,45 @@ const ATTRIBUTE_POPPER_POSITION = "data-monster-popper-position";
152
159
  */
153
160
  const ATTRIBUTE_LAYOUT_ALIGNMENT = "data-monster-layout-alignment";
154
161
 
162
+ /**
163
+ * @private
164
+ * @type {string}
165
+ */
166
+ const ATTRIBUTE_LAYOUT_STACKED = "data-monster-layout-stacked";
167
+
168
+ /**
169
+ * @private
170
+ * @type {string}
171
+ */
172
+ const ATTRIBUTE_OPTION_LAYOUT_ALIGNMENT =
173
+ "data-monster-option-layout-alignment";
174
+
175
+ /**
176
+ * @private
177
+ * @type {string}
178
+ */
179
+ const ATTRIBUTE_OPTION_LAYOUT_STACKED_ALIGNMENT =
180
+ "data-monster-option-layout-stacked-alignment";
181
+
182
+ /**
183
+ * @private
184
+ * @type {string}
185
+ */
186
+ const ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT =
187
+ "data-monster-option-layout-stacked-breakpoint";
188
+
189
+ /**
190
+ * @private
191
+ * @type {string}
192
+ */
193
+ const EVENT_LAYOUT_CHANGED = "monster-control-bar-layout-changed";
194
+
195
+ /**
196
+ * @private
197
+ * @type {number}
198
+ */
199
+ const LAYOUT_CHANGED_EVENT_DELAY = 20;
200
+
155
201
  /**
156
202
  * A control bar control.
157
203
  *
@@ -193,6 +239,8 @@ class ControlBar extends CustomElement {
193
239
  labels: {},
194
240
  layout: {
195
241
  alignment: "left",
242
+ stackedAlignment: undefined,
243
+ stackedBreakpoint: undefined,
196
244
  },
197
245
  popper: {
198
246
  placement: "left",
@@ -228,7 +276,9 @@ class ControlBar extends CustomElement {
228
276
  // setup structure
229
277
  initControlBar.call(this);
230
278
  initPopperSwitch.call(this);
279
+ setLayoutStackedState.call(this, false);
231
280
  applyLayoutAlignment.call(this);
281
+ this[layoutChangedEventQueueSymbol] = new Queue();
232
282
  this.attachObserver(
233
283
  new Observer(() => {
234
284
  applyLayoutAlignment.call(this);
@@ -236,6 +286,25 @@ class ControlBar extends CustomElement {
236
286
  );
237
287
  }
238
288
 
289
+ /**
290
+ * Set option and sync layout state for reactive layout options.
291
+ *
292
+ * @param {string} path
293
+ * @param {*} value
294
+ * @return {ControlBar}
295
+ */
296
+ setOption(path, value) {
297
+ super.setOption(path, value);
298
+ if (
299
+ path === "layout.alignment" ||
300
+ path === "layout.stackedAlignment" ||
301
+ path === "layout.stackedBreakpoint"
302
+ ) {
303
+ syncLayoutState.call(this);
304
+ }
305
+ return this;
306
+ }
307
+
239
308
  /**
240
309
  * This method is called internal and should not be called directly.
241
310
  *
@@ -280,6 +349,9 @@ class ControlBar extends CustomElement {
280
349
  static get observedAttributes() {
281
350
  const attributes = super.observedAttributes;
282
351
  attributes.push(ATTRIBUTE_POPPER_POSITION);
352
+ attributes.push(ATTRIBUTE_OPTION_LAYOUT_ALIGNMENT);
353
+ attributes.push(ATTRIBUTE_OPTION_LAYOUT_STACKED_ALIGNMENT);
354
+ attributes.push(ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT);
283
355
  return attributes;
284
356
  }
285
357
 
@@ -305,6 +377,7 @@ class ControlBar extends CustomElement {
305
377
  this[layoutTokenSymbol] = (this[layoutTokenSymbol] || 0) + 1;
306
378
 
307
379
  disconnectResizeObserver.call(this);
380
+ defuseLayoutChangedEvent.call(this);
308
381
  if (this[mutationObserverSymbol]) {
309
382
  this[mutationObserverSymbol].disconnect();
310
383
  }
@@ -457,11 +530,25 @@ function initEventHandler() {
457
530
  });
458
531
  }
459
532
 
460
- self[attributeObserverSymbol][ATTRIBUTE_POPPER_POSITION] = function (value) {
533
+ self[attributeObserverSymbol][ATTRIBUTE_POPPER_POSITION] = (value) => {
461
534
  self.setOption("popper.placement", value);
462
535
  updatePopper.call(self);
463
536
  };
464
537
 
538
+ self[attributeObserverSymbol][ATTRIBUTE_OPTION_LAYOUT_ALIGNMENT] = () => {
539
+ syncLayoutState.call(self);
540
+ };
541
+
542
+ self[attributeObserverSymbol][ATTRIBUTE_OPTION_LAYOUT_STACKED_ALIGNMENT] =
543
+ () => {
544
+ syncLayoutState.call(self);
545
+ };
546
+
547
+ self[attributeObserverSymbol][ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT] =
548
+ () => {
549
+ syncLayoutState.call(self);
550
+ };
551
+
465
552
  self[resizeObserverSymbol] = new ResizeObserver(() => {
466
553
  scheduleLayout.call(self, { measure: true, layout: true });
467
554
  });
@@ -628,7 +715,7 @@ function rearrangeItems() {
628
715
  let itemWidth = 0;
629
716
  try {
630
717
  itemWidth = this[dimensionsSymbol].getVia(`data.item.${ref}`);
631
- } catch (e) {
718
+ } catch {
632
719
  // If the path does not exist, pathfinder throws an error.
633
720
  // In this case, we assume the width is 0.
634
721
  // This can happen for items that have never been visible.
@@ -674,8 +761,11 @@ function rearrangeItems() {
674
761
 
675
762
  const shouldShowSwitch =
676
763
  layout.itemsToMoveToPopper.length > 0 && hasItems;
764
+ const shouldUseStackedLayout =
765
+ shouldShowSwitch || isStackedBreakpointMatched.call(this);
677
766
 
678
767
  suppressLayoutFeedback.call(this);
768
+ setLayoutStackedState.call(this, shouldUseStackedLayout);
679
769
 
680
770
  for (const item of layout.itemsToMoveToPopper) {
681
771
  if (item.getAttribute("slot") !== "popper") {
@@ -692,6 +782,7 @@ function rearrangeItems() {
692
782
  setSwitchVisible.call(this, shouldShowSwitch);
693
783
  updateControlSizing.call(this, layout, shouldShowSwitch);
694
784
  updateJoinedBorders.call(this, layout, shouldShowSwitch);
785
+ applyLayoutAlignment.call(this);
695
786
  if (!shouldShowSwitch) {
696
787
  hide.call(this);
697
788
  }
@@ -1136,6 +1227,7 @@ function calculateControlBarDimensions() {
1136
1227
  }
1137
1228
 
1138
1229
  this[dimensionsSymbol].setVia("data.visible", !(width === 0));
1230
+ this[dimensionsSymbol].setVia("data.containerWidth", width);
1139
1231
 
1140
1232
  const itemReferences = [];
1141
1233
 
@@ -1398,17 +1490,204 @@ function applyLayoutAlignment() {
1398
1490
  return;
1399
1491
  }
1400
1492
 
1401
- const alignment = this.getOption("layout.alignment", "left");
1493
+ const layoutState = this[layoutStateSymbol] || {};
1494
+ const stackedAlignment = this.getOption("layout.stackedAlignment");
1495
+ let alignment = this.getOption("layout.alignment", "left");
1496
+ if (
1497
+ layoutState.stacked === true &&
1498
+ typeof stackedAlignment === "string" &&
1499
+ stackedAlignment !== ""
1500
+ ) {
1501
+ alignment = stackedAlignment;
1502
+ }
1402
1503
 
1403
1504
  if (alignment === "right") {
1404
- this[controlBarElementSymbol].setAttribute(
1505
+ if (setAttributeIfChanged(
1506
+ this[controlBarElementSymbol],
1405
1507
  ATTRIBUTE_LAYOUT_ALIGNMENT,
1406
1508
  "right",
1407
- );
1509
+ )) {
1510
+ queueLayoutChangedEvent.call(this);
1511
+ }
1512
+ return;
1513
+ }
1514
+
1515
+ if (setAttributeIfChanged(
1516
+ this[controlBarElementSymbol],
1517
+ ATTRIBUTE_LAYOUT_ALIGNMENT,
1518
+ "left",
1519
+ )) {
1520
+ queueLayoutChangedEvent.call(this);
1521
+ }
1522
+ }
1523
+
1524
+ /**
1525
+ * @private
1526
+ * @return {void}
1527
+ */
1528
+ function syncLayoutState() {
1529
+ applyLayoutAlignment.call(this);
1530
+ scheduleLayout.call(this, { measure: true, layout: true });
1531
+ }
1532
+
1533
+ /**
1534
+ * @private
1535
+ * @return {boolean}
1536
+ */
1537
+ function isStackedBreakpointMatched() {
1538
+ const breakpoint = this.getOption("layout.stackedBreakpoint");
1539
+ if (typeof breakpoint !== "string" || breakpoint.trim() === "") {
1540
+ return false;
1541
+ }
1542
+
1543
+ let width = 0;
1544
+ try {
1545
+ width = this[dimensionsSymbol].getVia("data.containerWidth");
1546
+ } catch {
1547
+ try {
1548
+ width = this[dimensionsSymbol].getVia("data.space");
1549
+ } catch {}
1550
+ }
1551
+
1552
+ if (!(width > 0)) {
1553
+ return false;
1554
+ }
1555
+
1556
+ const breakpointWidth = getComputedCssPixels(breakpoint);
1557
+ if (!(breakpointWidth > 0)) {
1558
+ return false;
1559
+ }
1560
+
1561
+ return width <= breakpointWidth;
1562
+ }
1563
+
1564
+ /**
1565
+ * @private
1566
+ * @param {boolean} stacked
1567
+ * @return {void}
1568
+ */
1569
+ function setLayoutStackedState(stacked) {
1570
+ if (!this[layoutStateSymbol]) {
1571
+ return;
1572
+ }
1573
+ this[layoutStateSymbol].stacked = stacked === true;
1574
+
1575
+ if (!(this[controlBarElementSymbol] instanceof HTMLElement)) {
1576
+ return;
1577
+ }
1578
+
1579
+ if (setAttributeIfChanged(
1580
+ this[controlBarElementSymbol],
1581
+ ATTRIBUTE_LAYOUT_STACKED,
1582
+ this[layoutStateSymbol].stacked ? "true" : "false",
1583
+ )) {
1584
+ queueLayoutChangedEvent.call(this);
1585
+ }
1586
+ }
1587
+
1588
+ /**
1589
+ * @private
1590
+ * @return {void}
1591
+ */
1592
+ function queueLayoutChangedEvent() {
1593
+ if (!(this[layoutChangedEventQueueSymbol] instanceof Queue)) {
1594
+ return;
1595
+ }
1596
+
1597
+ this[layoutChangedEventQueueSymbol].add(getLayoutChangedEventDetail.call(this));
1598
+
1599
+ if (this[layoutChangedEventSwitchSymbol] instanceof DeadMansSwitch) {
1600
+ this[layoutChangedEventSwitchSymbol].touch();
1601
+ return;
1602
+ }
1603
+
1604
+ this[layoutChangedEventSwitchSymbol] = new DeadMansSwitch(
1605
+ LAYOUT_CHANGED_EVENT_DELAY,
1606
+ () => {
1607
+ this[layoutChangedEventSwitchSymbol] = undefined;
1608
+ dispatchQueuedLayoutChangedEvent.call(this);
1609
+ },
1610
+ );
1611
+ }
1612
+
1613
+ /**
1614
+ * @private
1615
+ * @return {void}
1616
+ */
1617
+ function dispatchQueuedLayoutChangedEvent() {
1618
+ if (!(this[layoutChangedEventQueueSymbol] instanceof Queue)) {
1619
+ return;
1620
+ }
1621
+
1622
+ let detail;
1623
+ while (!this[layoutChangedEventQueueSymbol].isEmpty()) {
1624
+ detail = this[layoutChangedEventQueueSymbol].poll();
1625
+ }
1626
+
1627
+ if (!detail) {
1628
+ return;
1629
+ }
1630
+
1631
+ const signature = JSON.stringify(detail);
1632
+ if (this[layoutChangedEventLastSignatureSymbol] === signature) {
1408
1633
  return;
1409
1634
  }
1635
+ this[layoutChangedEventLastSignatureSymbol] = signature;
1636
+
1637
+ this.dispatchEvent(
1638
+ new CustomEvent(EVENT_LAYOUT_CHANGED, {
1639
+ bubbles: true,
1640
+ composed: true,
1641
+ detail,
1642
+ }),
1643
+ );
1644
+ }
1645
+
1646
+ /**
1647
+ * @private
1648
+ * @return {void}
1649
+ */
1650
+ function defuseLayoutChangedEvent() {
1651
+ if (this[layoutChangedEventSwitchSymbol] instanceof DeadMansSwitch) {
1652
+ this[layoutChangedEventSwitchSymbol].defuse();
1653
+ this[layoutChangedEventSwitchSymbol] = undefined;
1654
+ }
1655
+ }
1656
+
1657
+ /**
1658
+ * @private
1659
+ * @return {Object}
1660
+ */
1661
+ function getLayoutChangedEventDetail() {
1662
+ const layoutState = this[layoutStateSymbol] || {};
1663
+ const stacked = layoutState.stacked === true;
1664
+ const alignment = this[controlBarElementSymbol]?.getAttribute(
1665
+ ATTRIBUTE_LAYOUT_ALIGNMENT,
1666
+ );
1667
+
1668
+ return {
1669
+ alignment: alignment || this.getOption("layout.alignment", "left"),
1670
+ configuredAlignment: this.getOption("layout.alignment", "left"),
1671
+ stacked,
1672
+ stackedAlignment: this.getOption("layout.stackedAlignment"),
1673
+ stackedBreakpoint: this.getOption("layout.stackedBreakpoint"),
1674
+ };
1675
+ }
1676
+
1677
+ /**
1678
+ * @private
1679
+ * @param {HTMLElement} element
1680
+ * @param {string} attribute
1681
+ * @param {string} value
1682
+ * @return {boolean}
1683
+ */
1684
+ function setAttributeIfChanged(element, attribute, value) {
1685
+ if (element.getAttribute(attribute) !== value) {
1686
+ element.setAttribute(attribute, value);
1687
+ return true;
1688
+ }
1410
1689
 
1411
- this[controlBarElementSymbol].setAttribute(ATTRIBUTE_LAYOUT_ALIGNMENT, "left");
1690
+ return false;
1412
1691
  }
1413
1692
 
1414
1693
  /**
@@ -3,7 +3,7 @@ import { chaiDom } from "../../../util/chai-dom.mjs";
3
3
  import { initJSDOM } from "../../../util/jsdom.mjs";
4
4
  import { ResizeObserverMock } from "../../../util/resize-observer.mjs";
5
5
 
6
- let expect = chai.expect;
6
+ const expect = chai.expect;
7
7
  chai.use(chaiDom);
8
8
 
9
9
  const html = `
@@ -103,6 +103,68 @@ describe("ControlBar", function () {
103
103
  }, 50);
104
104
  });
105
105
 
106
+ it("should update the rendered layout alignment when the option changes", async function () {
107
+ const bar = document.getElementById("bar-right");
108
+
109
+ await new Promise((resolve) => setTimeout(resolve, 0));
110
+
111
+ const controlBar = bar.shadowRoot.querySelector(
112
+ '[data-monster-role="control-bar"]',
113
+ );
114
+
115
+ expect(controlBar.getAttribute("data-monster-layout-alignment")).to.equal(
116
+ "right",
117
+ );
118
+
119
+ bar.setOption("layout.alignment", "left");
120
+ await new Promise((resolve) => setTimeout(resolve, 0));
121
+
122
+ expect(controlBar.getAttribute("data-monster-layout-alignment")).to.equal(
123
+ "left",
124
+ );
125
+ });
126
+
127
+ it("should update the rendered layout alignment when the host option attribute changes", async function () {
128
+ const bar = document.getElementById("bar");
129
+ const controlBar = bar.shadowRoot.querySelector(
130
+ '[data-monster-role="control-bar"]',
131
+ );
132
+
133
+ bar.setAttribute("data-monster-option-layout-alignment", "right");
134
+ await new Promise((resolve) => setTimeout(resolve, 0));
135
+
136
+ expect(bar.getOption("layout.alignment")).to.equal("right");
137
+ expect(controlBar.getAttribute("data-monster-layout-alignment")).to.equal(
138
+ "right",
139
+ );
140
+ });
141
+
142
+ it("should dispatch one final layout changed event for rapid alignment changes", async function () {
143
+ const bar = document.getElementById("bar-right");
144
+ const events = [];
145
+
146
+ await new Promise((resolve) => setTimeout(resolve, 0));
147
+
148
+ bar.addEventListener("monster-control-bar-layout-changed", (event) => {
149
+ events.push(event.detail);
150
+ });
151
+
152
+ bar.setOption("layout.alignment", "left");
153
+ bar.setOption("layout.alignment", "right");
154
+ bar.setOption("layout.alignment", "left");
155
+
156
+ await new Promise((resolve) => setTimeout(resolve, 60));
157
+
158
+ expect(events).to.have.length(1);
159
+ expect(events[0]).to.deep.equal({
160
+ alignment: "left",
161
+ configuredAlignment: "left",
162
+ stacked: false,
163
+ stackedAlignment: undefined,
164
+ stackedBreakpoint: undefined,
165
+ });
166
+ });
167
+
106
168
  it("should map popper position attributes to the popper placement option", function () {
107
169
  const bar = document.getElementById("bar");
108
170
 
@@ -515,7 +577,11 @@ describe("ControlBar", function () {
515
577
  const mocks = document.getElementById("mocks");
516
578
  mocks.innerHTML = `
517
579
  <div id="control-bar-wrapper">
518
- <monster-control-bar id="overflow-bar">
580
+ <monster-control-bar
581
+ id="overflow-bar"
582
+ data-monster-option-layout-alignment="right"
583
+ data-monster-option-layout-stacked-alignment="left"
584
+ >
519
585
  <input id="overflow-input" type="search">
520
586
  <select id="overflow-select"><option>One</option></select>
521
587
  <button id="overflow-button">Run</button>
@@ -567,12 +633,21 @@ describe("ControlBar", function () {
567
633
  configurable: true,
568
634
  value: 20,
569
635
  });
636
+ const controlBar = bar.shadowRoot.querySelector(
637
+ '[data-monster-role="control-bar"]',
638
+ );
570
639
 
571
640
  await flushFrames();
572
641
  await new Promise((resolve) => setTimeout(resolve, 0));
573
642
  await new Promise((resolve) => setTimeout(resolve, 0));
574
643
 
575
644
  expect(switchButton.hasAttribute("hidden")).to.be.false;
645
+ expect(controlBar.getAttribute("data-monster-layout-stacked")).to.equal(
646
+ "true",
647
+ );
648
+ expect(controlBar.getAttribute("data-monster-layout-alignment")).to.equal(
649
+ "left",
650
+ );
576
651
  expect(document.getElementById("overflow-input").hasAttribute("slot")).to
577
652
  .be.false;
578
653
  expect(document.getElementById("overflow-select").getAttribute("slot")).to
@@ -586,4 +661,94 @@ describe("ControlBar", function () {
586
661
  globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
587
662
  }
588
663
  });
664
+
665
+ it("should apply stacked alignment below the configured container breakpoint without overflow", async function () {
666
+ const originalRequestAnimationFrame = window.requestAnimationFrame;
667
+ const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
668
+
669
+ const scheduledCallbacks = [];
670
+ const flushFrames = async () => {
671
+ while (scheduledCallbacks.length > 0) {
672
+ scheduledCallbacks.shift()();
673
+ await new Promise((resolve) => setTimeout(resolve, 0));
674
+ }
675
+ };
676
+
677
+ try {
678
+ window.requestAnimationFrame = (callback) => {
679
+ scheduledCallbacks.push(callback);
680
+ return scheduledCallbacks.length;
681
+ };
682
+ globalThis.requestAnimationFrame = window.requestAnimationFrame;
683
+
684
+ const mocks = document.getElementById("mocks");
685
+ mocks.innerHTML = `
686
+ <div id="breakpoint-bar-wrapper">
687
+ <monster-control-bar
688
+ id="breakpoint-bar"
689
+ data-monster-option-layout-alignment="right"
690
+ data-monster-option-layout-stacked-alignment="left"
691
+ data-monster-option-layout-stacked-breakpoint="100px"
692
+ >
693
+ <button id="breakpoint-button">Run</button>
694
+ </monster-control-bar>
695
+ </div>
696
+ `;
697
+
698
+ const wrapper = document.getElementById("breakpoint-bar-wrapper");
699
+ const button = document.getElementById("breakpoint-button");
700
+ const bar = document.getElementById("breakpoint-bar");
701
+ const controlBar = bar.shadowRoot.querySelector(
702
+ '[data-monster-role="control-bar"]',
703
+ );
704
+ const switchButton = bar.shadowRoot.querySelector(
705
+ '[data-monster-role="switch"]',
706
+ );
707
+
708
+ wrapper.style.boxSizing = "border-box";
709
+ Object.defineProperty(wrapper, "clientWidth", {
710
+ configurable: true,
711
+ value: 90,
712
+ });
713
+ Object.defineProperty(button, "offsetWidth", {
714
+ configurable: true,
715
+ value: 40,
716
+ });
717
+ Object.defineProperty(button, "offsetHeight", {
718
+ configurable: true,
719
+ value: 30,
720
+ });
721
+ button.getBoundingClientRect = () => ({
722
+ width: 40,
723
+ height: 30,
724
+ top: 0,
725
+ right: 40,
726
+ bottom: 30,
727
+ left: 0,
728
+ x: 0,
729
+ y: 0,
730
+ toJSON: () => {},
731
+ });
732
+ Object.defineProperty(switchButton, "offsetWidth", {
733
+ configurable: true,
734
+ value: 20,
735
+ });
736
+
737
+ await flushFrames();
738
+ await new Promise((resolve) => setTimeout(resolve, 0));
739
+ await new Promise((resolve) => setTimeout(resolve, 0));
740
+
741
+ expect(switchButton.hasAttribute("hidden")).to.be.true;
742
+ expect(button.hasAttribute("slot")).to.be.false;
743
+ expect(controlBar.getAttribute("data-monster-layout-stacked")).to.equal(
744
+ "true",
745
+ );
746
+ expect(controlBar.getAttribute("data-monster-layout-alignment")).to.equal(
747
+ "left",
748
+ );
749
+ } finally {
750
+ window.requestAnimationFrame = originalRequestAnimationFrame;
751
+ globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
752
+ }
753
+ });
589
754
  });