@schukai/monster 4.128.3 → 4.129.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/package.json +1 -1
  3. package/source/components/datatable/datatable.mjs +4 -1
  4. package/source/components/datatable/pagination.mjs +39 -1
  5. package/source/components/form/login.mjs +197 -0
  6. package/source/components/form/message-state-button.mjs +233 -18
  7. package/source/components/form/select.mjs +4 -4
  8. package/source/components/form/style/message-state-button.pcss +46 -3
  9. package/source/components/form/stylesheet/message-state-button.mjs +7 -14
  10. package/source/components/form/stylesheet/popper-button.mjs +7 -14
  11. package/source/components/form/tree-select.mjs +2 -2
  12. package/source/components/layout/popper.mjs +72 -1
  13. package/source/components/layout/stylesheet/popper.mjs +7 -14
  14. package/source/components/style/floating-ui.css +1 -49
  15. package/source/components/style/floating-ui.pcss +10 -0
  16. package/source/components/stylesheet/floating-ui.mjs +7 -14
  17. package/source/dom/customelement.mjs +182 -2
  18. package/source/dom/util/extract-keys.mjs +46 -10
  19. package/source/dom/util/init-options-from-attributes.mjs +4 -2
  20. package/source/dom/util/set-option-from-attribute.mjs +4 -2
  21. package/source/types/version.mjs +1 -1
  22. package/test/cases/components/form/login.mjs +168 -0
  23. package/test/cases/components/form/message-state-button.mjs +272 -0
  24. package/test/cases/components/form/popper-button.mjs +89 -0
  25. package/test/cases/components/form/select.mjs +24 -0
  26. package/test/cases/components/form/tree-select.mjs +22 -1
  27. package/test/cases/dom/util/extract-keys.mjs +34 -23
  28. package/test/cases/dom/util/init-options-from-attributes.mjs +21 -0
  29. package/test/cases/monster.mjs +1 -1
  30. package/test/web/import.js +1 -0
  31. package/test/web/test.html +2 -2
  32. package/test/web/tests.js +9300 -6050
package/CHANGELOG.md CHANGED
@@ -15,10 +15,13 @@
15
15
  - **customelement:** skip mutation-observer updater reruns for disconnected or disposed instances
16
16
  - **message-state-button:** clear auto-hide timers on disconnect and ignore delayed hides after removal
17
17
  - **popper:** guard show/hide/update flows against disconnected hosts and missing internal elements
18
+ - **message-state-button:** distinguish overlay, prose, and wide message layouts for smart popper sizing and overflow ([#401](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/401))
19
+ - **popper:** support kebab-case camelCase option attributes such as `data-monster-option-popper-content-overflow` ([#401](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/401))
18
20
 
19
21
  ### Changes
20
22
 
21
23
  - document lifecycle ownership rules for Updater-driven and stateful custom element implementations
24
+ - document smart message popper layout behavior for forms and feedback flows ([#401](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/401))
22
25
 
23
26
 
24
27
 
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6","@popperjs/core":"^2.11.8"},"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.128.3"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6","@popperjs/core":"^2.11.8"},"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.129.1"}
@@ -1692,7 +1692,10 @@ function updateGrid() {
1692
1692
  * @return {void}
1693
1693
  */
1694
1694
  function scheduleGridUpdate() {
1695
- if (this[resizeFrameRequestSymbol] !== null && this[resizeFrameRequestSymbol] !== undefined) {
1695
+ if (
1696
+ this[resizeFrameRequestSymbol] !== null &&
1697
+ this[resizeFrameRequestSymbol] !== undefined
1698
+ ) {
1696
1699
  return;
1697
1700
  }
1698
1701
 
@@ -238,6 +238,8 @@ class Pagination extends CustomElement {
238
238
  list.setAttribute("data-monster-adaptive-ready", "true");
239
239
  }
240
240
  }
241
+
242
+ syncPaginationStateToDom.call(this, pagination);
241
243
  }
242
244
  /**
243
245
  *
@@ -803,6 +805,42 @@ function buildPagination(current, max) {
803
805
  };
804
806
  }
805
807
 
808
+ /**
809
+ * @private
810
+ * @param {object} pagination
811
+ * @returns {void}
812
+ */
813
+ function syncPaginationStateToDom(pagination) {
814
+ if (!this.shadowRoot || !pagination) {
815
+ return;
816
+ }
817
+
818
+ const prevLink = this.shadowRoot.querySelector(
819
+ "a[data-monster-role=pagination-prev]",
820
+ );
821
+ if (prevLink instanceof HTMLElement) {
822
+ prevLink.setAttribute("href", pagination.prevHref || "#");
823
+ prevLink.setAttribute("data-page-no", `${pagination.prevNo ?? ""}`);
824
+ prevLink.className = `previous${pagination.prevClass || ""}`.trim();
825
+ }
826
+
827
+ const nextLink = this.shadowRoot.querySelector(
828
+ "a[data-monster-role=pagination-next]",
829
+ );
830
+ if (nextLink instanceof HTMLElement) {
831
+ nextLink.setAttribute("href", pagination.nextHref || "#");
832
+ nextLink.setAttribute("data-page-no", `${pagination.nextNo ?? ""}`);
833
+ nextLink.className = `next${pagination.nextClass || ""}`.trim();
834
+ }
835
+
836
+ const summary = this.shadowRoot.querySelector(
837
+ "[data-monster-role=pagination-summary] span",
838
+ );
839
+ if (summary instanceof HTMLElement) {
840
+ summary.textContent = pagination.summary || "";
841
+ }
842
+ }
843
+
806
844
  /**
807
845
  * @private
808
846
  */
@@ -1226,7 +1264,7 @@ function getTemplate() {
1226
1264
  data-monster-replace="path:items.label"></a></li>
1227
1265
  </template>
1228
1266
 
1229
- <div data-monster-role="control" part="control">
1267
+ <div data-monster-role="control" part="control" data-monster-select-this="true">
1230
1268
  <nav data-monster-role="pagination" role="navigation" aria-label="pagination" part="nav">
1231
1269
  <ul class="pagination-list" data-monster-insert="items path:pagination.items"
1232
1270
  data-monster-adaptive-ready="false"
@@ -89,6 +89,30 @@ const resetLoginProcessLinksSymbol = Symbol("resetLoginProcessLink");
89
89
  */
90
90
  const loggedInCollapseSymbol = Symbol("loggedInCollapse");
91
91
 
92
+ /**
93
+ * @private
94
+ * @type {symbol}
95
+ */
96
+ const customCollapseSymbol = Symbol("customCollapse");
97
+
98
+ /**
99
+ * @private
100
+ * @type {symbol}
101
+ */
102
+ const customCollapseHeaderSymbol = Symbol("customCollapseHeader");
103
+
104
+ /**
105
+ * @private
106
+ * @type {symbol}
107
+ */
108
+ const customCollapseContentSymbol = Symbol("customCollapseContent");
109
+
110
+ /**
111
+ * @private
112
+ * @type {symbol}
113
+ */
114
+ const customCollapseFooterSymbol = Symbol("customCollapseFooter");
115
+
92
116
  /**
93
117
  * @private
94
118
  * @type {symbol}
@@ -473,6 +497,98 @@ class Login extends CustomElement {
473
497
  return this;
474
498
  }
475
499
 
500
+ /**
501
+ * Opens the custom collapse and focuses the first focusable element if available.
502
+ *
503
+ * @returns {Login}
504
+ */
505
+ openCustomCollapse() {
506
+ this[customCollapseSymbol].open();
507
+ focusFirstCustomCollapseElement.call(this);
508
+ return this;
509
+ }
510
+
511
+ /**
512
+ * Replaces the header content of the custom collapse.
513
+ *
514
+ * @param {string|number|boolean|Node|Array<string|number|boolean|Node>|null|undefined} content
515
+ * @returns {Login}
516
+ */
517
+ setCustomCollapseHeader(content) {
518
+ setCustomCollapseSectionContent.call(
519
+ this,
520
+ this[customCollapseHeaderSymbol],
521
+ content,
522
+ );
523
+ return this;
524
+ }
525
+
526
+ /**
527
+ * Replaces the main content of the custom collapse.
528
+ *
529
+ * @param {string|number|boolean|Node|Array<string|number|boolean|Node>|null|undefined} content
530
+ * @returns {Login}
531
+ */
532
+ setCustomCollapseContent(content) {
533
+ setCustomCollapseSectionContent.call(
534
+ this,
535
+ this[customCollapseContentSymbol],
536
+ content,
537
+ );
538
+ return this;
539
+ }
540
+
541
+ /**
542
+ * Replaces the footer content of the custom collapse.
543
+ *
544
+ * @param {string|number|boolean|Node|Array<string|number|boolean|Node>|null|undefined} content
545
+ * @returns {Login}
546
+ */
547
+ setCustomCollapseFooter(content) {
548
+ setCustomCollapseSectionContent.call(
549
+ this,
550
+ this[customCollapseFooterSymbol],
551
+ content,
552
+ );
553
+ return this;
554
+ }
555
+
556
+ /**
557
+ * Clears all custom collapse sections.
558
+ *
559
+ * @returns {Login}
560
+ */
561
+ clearCustomCollapse() {
562
+ this[customCollapseHeaderSymbol].replaceChildren();
563
+ this[customCollapseContentSymbol].replaceChildren();
564
+ this[customCollapseFooterSymbol].replaceChildren();
565
+ return this;
566
+ }
567
+
568
+ /**
569
+ * Replaces the custom collapse content and opens it.
570
+ *
571
+ * @param {string|number|boolean|Node|Array<string|number|boolean|Node>|Object|null|undefined} content
572
+ * @returns {Login}
573
+ */
574
+ showCustomCollapse(content) {
575
+ if (isObject(content) && !(content instanceof Node) && !isArray(content)) {
576
+ if ("header" in content) {
577
+ this.setCustomCollapseHeader(content.header);
578
+ }
579
+ if ("content" in content) {
580
+ this.setCustomCollapseContent(content.content);
581
+ }
582
+ if ("footer" in content) {
583
+ this.setCustomCollapseFooter(content.footer);
584
+ }
585
+ } else if (content !== undefined) {
586
+ this.setCustomCollapseContent(content);
587
+ }
588
+
589
+ return this.openCustomCollapse();
590
+ }
591
+
476
592
  /**
477
593
  * @return {string}
478
594
  */
@@ -1745,6 +1861,20 @@ function initControlReferences() {
1745
1861
  `[data-monster-role="logged-in-collapse"]`,
1746
1862
  );
1747
1863
 
1864
+ this[customCollapseSymbol] = this.shadowRoot.querySelector(
1865
+ `[data-monster-role="custom-collapse"]`,
1866
+ );
1867
+
1868
+ this[customCollapseHeaderSymbol] = this.shadowRoot.getElementById(
1869
+ "customCollapseHeader",
1870
+ );
1871
+ this[customCollapseContentSymbol] = this.shadowRoot.getElementById(
1872
+ "customCollapseContent",
1873
+ );
1874
+ this[customCollapseFooterSymbol] = this.shadowRoot.getElementById(
1875
+ "customCollapseFooter",
1876
+ );
1877
+
1748
1878
  this[digitsCollapseSymbol] = this.shadowRoot.querySelector(
1749
1879
  `[data-monster-role="digits-collapse"]`,
1750
1880
  );
@@ -1957,8 +2087,75 @@ function getTemplate() {
1957
2087
  <slot name="logged-in-footer"></slot>
1958
2088
  </div>
1959
2089
  </monster-collapse>
2090
+ <monster-collapse data-monster-role="custom-collapse"
2091
+ exportparts="
2092
+ control:collapse-custom-control,
2093
+ container:collapse-custom-container,
2094
+ deco:collapse-custom-deco"
2095
+ part="custom-collapse">
2096
+ <div part="custom-collapse-wrapper">
2097
+ <div id="customCollapseHeader" part="custom-collapse-header"></div>
2098
+ <div id="customCollapseContent" part="custom-collapse-content"></div>
2099
+ <div id="customCollapseFooter" part="custom-collapse-footer"></div>
2100
+ </div>
2101
+ </monster-collapse>
1960
2102
 
1961
2103
  </div>`;
1962
2104
  }
1963
2105
 
2106
+ /**
2107
+ * @private
2108
+ * @param {HTMLElement} target
2109
+ * @param {string|number|boolean|Node|Array<string|number|boolean|Node>|null|undefined} content
2110
+ * @returns {void}
2111
+ */
2112
+ function setCustomCollapseSectionContent(target, content) {
2113
+ if (!(target instanceof HTMLElement)) {
2114
+ return;
2115
+ }
2116
+
2117
+ target.replaceChildren(...normalizeCustomCollapseContent.call(this, content));
2118
+
2119
+ if (this[customCollapseSymbol]?.isOpen()) {
2120
+ this[customCollapseSymbol].adjustHeight();
2121
+ }
2122
+ }
2123
+
2124
+ /**
2125
+ * @private
2126
+ * @param {string|number|boolean|Node|Array<string|number|boolean|Node>|null|undefined} content
2127
+ * @returns {Node[]}
2128
+ */
2129
+ function normalizeCustomCollapseContent(content) {
2130
+ if (content === null || content === undefined) {
2131
+ return [];
2132
+ }
2133
+
2134
+ if (content instanceof Node) {
2135
+ return [content];
2136
+ }
2137
+
2138
+ if (isArray(content)) {
2139
+ return content.flatMap((entry) =>
2140
+ normalizeCustomCollapseContent.call(this, entry),
2141
+ );
2142
+ }
2143
+
2144
+ return [this.ownerDocument.createTextNode(String(content))];
2145
+ }
2146
+
2147
+ /**
2148
+ * @private
2149
+ * @returns {void}
2150
+ */
2151
+ function focusFirstCustomCollapseElement() {
2152
+ setTimeout(() => {
2153
+ const focusable = this[customCollapseSymbol]?.querySelector(
2154
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
2155
+ );
2156
+
2157
+ focusable?.focus?.();
2158
+ }, 0);
2159
+ }
2160
+
1964
2161
  registerCustomElement(Login);
@@ -37,9 +37,13 @@ export { MessageStateButton };
37
37
  const buttonElementSymbol = Symbol("buttonElement");
38
38
  const innerDisabledObserverSymbol = Symbol("innerDisabledObserver");
39
39
  const popperElementSymbol = Symbol("popperElement");
40
+ const contentElementSymbol = Symbol("contentElement");
40
41
  const messageElementSymbol = Symbol("messageElement");
41
42
  const measurementPopperSymbol = Symbol("measurementPopper");
42
43
  const autoHideTimerSymbol = Symbol("autoHideTimer");
44
+ const MESSAGE_LAYOUT_OVERLAY = "overlay";
45
+ const MESSAGE_LAYOUT_PROSE = "prose";
46
+ const MESSAGE_LAYOUT_WIDE = "wide";
43
47
 
44
48
  /**
45
49
  * A specialized button component that combines state management with message display capabilities.
@@ -111,6 +115,8 @@ class MessageStateButton extends Popper {
111
115
  * @property {string|number} message.width.min Minimum width (px, rem, em, vw)
112
116
  * @property {string|number} message.width.max Maximum width (px, rem, em, vw)
113
117
  * @property {number} message.width.viewportRatio Max width as ratio of viewport width (0-1)
118
+ * @property {Object} popper Popper options inherited from the base popper
119
+ * @property {string} popper.contentOverflow Content clipping mode: both|horizontal|smart
114
120
  * @property {string} mode The mode of the button, can be `manual` or `submit`
115
121
  * @property {string} labels.button Button label
116
122
  * @property {Object} classes Classes for internal elements
@@ -122,20 +128,25 @@ class MessageStateButton extends Popper {
122
128
  * @property {string} aria.label Aria label for the button
123
129
  */
124
130
  get defaults() {
125
- return Object.assign({}, super.defaults, {
131
+ const defaults = super.defaults;
132
+
133
+ return Object.assign({}, defaults, {
126
134
  message: {
127
135
  title: undefined,
128
136
  content: undefined,
129
137
  icon: undefined,
130
138
  width: {
131
139
  min: "12rem",
132
- max: "32rem",
133
- viewportRatio: 0.7,
140
+ max: null,
141
+ viewportRatio: null,
134
142
  },
135
143
  },
136
144
  templates: {
137
145
  main: getTemplate(),
138
146
  },
147
+ popper: Object.assign({}, defaults.popper, {
148
+ contentOverflow: "smart",
149
+ }),
139
150
  mode: "manual",
140
151
  labels: {
141
152
  button: "<slot></slot>",
@@ -234,6 +245,8 @@ class MessageStateButton extends Popper {
234
245
  );
235
246
  }
236
247
 
248
+ applyResolvedMessagePresentation.call(this);
249
+
237
250
  return this;
238
251
  }
239
252
 
@@ -247,6 +260,7 @@ class MessageStateButton extends Popper {
247
260
  this.setOption("message.content", undefined);
248
261
  this.setOption("message.icon", undefined);
249
262
  clearAutoHideTimer.call(this);
263
+ applyResolvedMessagePresentation.call(this);
250
264
  return this;
251
265
  }
252
266
 
@@ -258,6 +272,7 @@ class MessageStateButton extends Popper {
258
272
  */
259
273
  showMessage(timeout) {
260
274
  clearAutoHideTimer.call(this);
275
+ applyResolvedMessagePresentation.call(this);
261
276
  applyMeasuredMessageWidth.call(this);
262
277
  this.showDialog.call(this);
263
278
 
@@ -287,6 +302,13 @@ class MessageStateButton extends Popper {
287
302
  return this;
288
303
  }
289
304
 
305
+ /**
306
+ * @return {string}
307
+ */
308
+ resolveContentOverflowMode() {
309
+ return resolveContentOverflowMode.call(this);
310
+ }
311
+
290
312
  /**
291
313
  *
292
314
  * @return {MessageStateButton}
@@ -438,9 +460,11 @@ function initControlReferences() {
438
460
  this[popperElementSymbol] = this.shadowRoot.querySelector(
439
461
  `[${ATTRIBUTE_ROLE}=popper]`,
440
462
  );
463
+ this[contentElementSymbol] = this.shadowRoot.querySelector(`[part="content"]`);
441
464
  this[messageElementSymbol] = this.shadowRoot.querySelector(
442
465
  `[${ATTRIBUTE_ROLE}=message]`,
443
466
  );
467
+ applyResolvedMessagePresentation.call(this);
444
468
  }
445
469
 
446
470
  /**
@@ -534,8 +558,11 @@ function getTemplate() {
534
558
 
535
559
  <div data-monster-role="popper" part="popper" tabindex="-1" class="monster-color-primary-1">
536
560
  <div data-monster-role="arrow"></div>
537
- <div data-monster-role="message" part="message" class="flex"
538
- data-monster-patch="path:message.content"></div>
561
+ <div part="content"
562
+ class="flex">
563
+ <div data-monster-role="message" part="message" class="flex"
564
+ data-monster-patch="path:message.content"></div>
565
+ </div>
539
566
  </div>
540
567
  </div>
541
568
  </div>
@@ -561,7 +588,166 @@ function getMeasurementContent(content) {
561
588
 
562
589
  /**
563
590
  * @private
564
- * @return {{popper: HTMLElement, message: HTMLElement}|null}
591
+ * @return {void}
592
+ */
593
+ function applyResolvedMessagePresentation() {
594
+ const contentElement = this[contentElementSymbol];
595
+ const messageElement = this[messageElementSymbol];
596
+ const layoutMode = resolveMessageLayoutMode.call(this);
597
+ const overflowMode = this.resolveContentOverflowMode();
598
+
599
+ if (contentElement instanceof HTMLElement) {
600
+ contentElement.setAttribute("data-monster-overflow-mode", overflowMode);
601
+ contentElement.setAttribute("data-monster-message-layout", layoutMode);
602
+ }
603
+
604
+ if (messageElement instanceof HTMLElement) {
605
+ messageElement.setAttribute("data-monster-message-layout", layoutMode);
606
+ }
607
+
608
+ if (this[measurementPopperSymbol]?.content instanceof HTMLElement) {
609
+ this[measurementPopperSymbol].content.setAttribute(
610
+ "data-monster-overflow-mode",
611
+ overflowMode,
612
+ );
613
+ this[measurementPopperSymbol].content.setAttribute(
614
+ "data-monster-message-layout",
615
+ layoutMode,
616
+ );
617
+ }
618
+
619
+ if (this[measurementPopperSymbol]?.message instanceof HTMLElement) {
620
+ this[measurementPopperSymbol].message.setAttribute(
621
+ "data-monster-message-layout",
622
+ layoutMode,
623
+ );
624
+ }
625
+ }
626
+
627
+ /**
628
+ * @private
629
+ * @return {string}
630
+ */
631
+ function resolveContentOverflowMode() {
632
+ const configuredMode = this.getOption("popper.contentOverflow", "smart");
633
+
634
+ if (configuredMode !== "smart") {
635
+ return configuredMode;
636
+ }
637
+
638
+ if (resolveMessageLayoutMode.call(this) === MESSAGE_LAYOUT_OVERLAY) {
639
+ return "horizontal";
640
+ }
641
+
642
+ return "both";
643
+ }
644
+
645
+ /**
646
+ * @private
647
+ * @return {string}
648
+ */
649
+ function resolveMessageLayoutMode() {
650
+ const content = this.getOption("message.content");
651
+
652
+ if (containsNestedOverlayContent(content)) {
653
+ return MESSAGE_LAYOUT_OVERLAY;
654
+ }
655
+
656
+ if (containsWideContent(content)) {
657
+ return MESSAGE_LAYOUT_WIDE;
658
+ }
659
+
660
+ return MESSAGE_LAYOUT_PROSE;
661
+ }
662
+
663
+ /**
664
+ * @private
665
+ * @param {unknown} content
666
+ * @return {boolean}
667
+ */
668
+ function containsNestedOverlayContent(content) {
669
+ const selector = [
670
+ "monster-details",
671
+ "monster-message-state-button",
672
+ "monster-popper",
673
+ "monster-popper-button",
674
+ "monster-select",
675
+ "details",
676
+ ].join(",");
677
+
678
+ if (isString(content)) {
679
+ const container = document.createElement("div");
680
+ container.innerHTML = content;
681
+ return container.querySelector(selector) instanceof HTMLElement;
682
+ }
683
+
684
+ if (!(content instanceof HTMLElement)) {
685
+ return false;
686
+ }
687
+
688
+ if (content.matches(selector)) {
689
+ return true;
690
+ }
691
+
692
+ return content.querySelector(selector) instanceof HTMLElement;
693
+ }
694
+
695
+ /**
696
+ * @private
697
+ * @param {unknown} content
698
+ * @return {boolean}
699
+ */
700
+ function containsWideContent(content) {
701
+ const root = getContentSearchRoot(content);
702
+ if (!(root instanceof HTMLElement)) {
703
+ return false;
704
+ }
705
+
706
+ const selector = [
707
+ '[data-monster-message-layout="wide"]',
708
+ "pre",
709
+ "table",
710
+ '[style*="white-space: nowrap"]',
711
+ '[style*="white-space:nowrap"]',
712
+ '[style*="overflow-x: auto"]',
713
+ '[style*="overflow-x:auto"]',
714
+ '[style*="overflow-x: scroll"]',
715
+ '[style*="overflow-x:scroll"]',
716
+ '[style*="overflow: auto"]',
717
+ '[style*="overflow:auto"]',
718
+ '[style*="overflow: scroll"]',
719
+ '[style*="overflow:scroll"]',
720
+ ].join(",");
721
+
722
+ if (root.matches(selector)) {
723
+ return true;
724
+ }
725
+
726
+ return root.querySelector(selector) instanceof HTMLElement;
727
+ }
728
+
729
+ /**
730
+ * @private
731
+ * @param {unknown} content
732
+ * @return {HTMLElement|null}
733
+ */
734
+ function getContentSearchRoot(content) {
735
+ if (isString(content)) {
736
+ const container = document.createElement("div");
737
+ container.innerHTML = content;
738
+ return container;
739
+ }
740
+
741
+ if (content instanceof HTMLElement) {
742
+ return content;
743
+ }
744
+
745
+ return null;
746
+ }
747
+
748
+ /**
749
+ * @private
750
+ * @return {{popper: HTMLElement, content: HTMLElement, message: HTMLElement}|null}
565
751
  */
566
752
  function ensureMeasurementPopper() {
567
753
  if (this[measurementPopperSymbol]) {
@@ -588,14 +774,19 @@ function ensureMeasurementPopper() {
588
774
  popper.className = this[popperElementSymbol].className;
589
775
  }
590
776
 
777
+ const content = document.createElement("div");
778
+ content.setAttribute("part", "content");
779
+
591
780
  const message = document.createElement("div");
592
781
  message.setAttribute(ATTRIBUTE_ROLE, "message");
593
782
  message.className = "flex";
594
783
 
595
- popper.appendChild(message);
784
+ content.appendChild(message);
785
+ popper.appendChild(content);
596
786
  this.shadowRoot.appendChild(popper);
597
787
 
598
- this[measurementPopperSymbol] = { popper, message };
788
+ this[measurementPopperSymbol] = { popper, content, message };
789
+ applyResolvedMessagePresentation.call(this);
599
790
  return this[measurementPopperSymbol];
600
791
  }
601
792
 
@@ -623,6 +814,8 @@ function applyMeasuredMessageWidth() {
623
814
  measurement.popper.className = popper.className;
624
815
  }
625
816
 
817
+ applyResolvedMessagePresentation.call(this);
818
+
626
819
  measurement.message.innerHTML = "";
627
820
  if (isString(measureContent)) {
628
821
  measurement.message.innerHTML = measureContent;
@@ -637,6 +830,7 @@ function applyMeasuredMessageWidth() {
637
830
  const rootFontSize =
638
831
  parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
639
832
  const widthOptions = this.getOption("message.width", {});
833
+ const layoutMode = resolveMessageLayoutMode.call(this);
640
834
  const minWidthOption = resolveLength(
641
835
  widthOptions?.min,
642
836
  fontSize,
@@ -649,21 +843,23 @@ function applyMeasuredMessageWidth() {
649
843
  rootFontSize,
650
844
  window.innerWidth,
651
845
  );
652
- const viewportRatio =
653
- typeof widthOptions?.viewportRatio === "number" &&
654
- widthOptions.viewportRatio > 0 &&
655
- widthOptions.viewportRatio <= 1
656
- ? widthOptions.viewportRatio
657
- : 0.7;
846
+ const viewportPadding = rootFontSize * 2;
847
+ const viewportWidthLimit = Math.max(0, window.innerWidth - viewportPadding);
848
+ const viewportRatio = resolveViewportRatio(
849
+ widthOptions?.viewportRatio,
850
+ layoutMode,
851
+ );
852
+ const fallbackMaxWidth =
853
+ layoutMode === MESSAGE_LAYOUT_WIDE ? viewportWidthLimit : fontSize * 32;
658
854
 
659
855
  const minWidth = Math.max(0, minWidthOption ?? Math.round(fontSize * 12));
660
856
  const maxViewportWidth = Math.max(
661
857
  minWidth,
662
- window.innerWidth * viewportRatio,
858
+ Math.min(window.innerWidth * viewportRatio, viewportWidthLimit),
663
859
  );
664
860
  const maxWidth = Math.max(
665
861
  minWidth,
666
- Math.min(maxWidthOption ?? fontSize * 32, maxViewportWidth),
862
+ Math.min(maxWidthOption ?? fallbackMaxWidth, maxViewportWidth),
667
863
  );
668
864
  const targetWidth = Math.max(
669
865
  minWidth,
@@ -673,8 +869,27 @@ function applyMeasuredMessageWidth() {
673
869
  popper.style.width = `${targetWidth}px`;
674
870
  popper.style.minWidth = `${minWidth}px`;
675
871
  popper.style.maxWidth = `${maxWidth}px`;
676
- popper.style.whiteSpace = "normal";
677
- popper.style.overflowWrap = "anywhere";
872
+ popper.style.removeProperty("white-space");
873
+ popper.style.removeProperty("overflow-wrap");
874
+ }
875
+
876
+ /**
877
+ * @private
878
+ * @param {unknown} value
879
+ * @param {string} layoutMode
880
+ * @return {number}
881
+ */
882
+ function resolveViewportRatio(value, layoutMode) {
883
+ if (
884
+ typeof value === "number" &&
885
+ Number.isFinite(value) &&
886
+ value > 0 &&
887
+ value <= 1
888
+ ) {
889
+ return value;
890
+ }
891
+
892
+ return layoutMode === MESSAGE_LAYOUT_WIDE ? 1 : 0.7;
678
893
  }
679
894
 
680
895
  /**