@schukai/monster 4.136.2 → 4.136.4

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.136.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.136.4"}
@@ -13,7 +13,10 @@
13
13
  */
14
14
 
15
15
  import { assembleMethodSymbol } from "../../dom/customelement.mjs";
16
- import { resolveClippingBoundaryElement } from "./util/floating-ui.mjs";
16
+ import {
17
+ resolveClippingBoundaryElement,
18
+ resolveParentPopperContentBoundary,
19
+ } from "./util/floating-ui.mjs";
17
20
  import { Popper } from "../layout/popper.mjs";
18
21
 
19
22
  export { ContextBase };
@@ -85,17 +88,40 @@ class ContextBase extends Popper {
85
88
  */
86
89
  resolvePopperOptions() {
87
90
  const options = super.resolvePopperOptions();
88
- const controlElement =
89
- this.shadowRoot?.querySelector('[data-monster-role="control"]') || null;
90
- const popperElement =
91
- this.shadowRoot?.querySelector('[data-monster-role="popper"]') || null;
91
+ const { controlElement, popperElement } = getPopperElements.call(this);
92
+ const parentPopperBoundary = resolveParentPopperContentBoundary(
93
+ controlElement,
94
+ popperElement,
95
+ );
92
96
 
93
- if (resolveClippingBoundaryElement(controlElement, popperElement)) {
97
+ if (
98
+ resolveClippingBoundaryElement(controlElement, popperElement) ||
99
+ parentPopperBoundary
100
+ ) {
94
101
  options.strategy = "fixed";
95
102
  }
96
103
 
97
104
  return options;
98
105
  }
106
+
107
+ /**
108
+ * Nested context poppers inside another popper need bounded height.
109
+ *
110
+ * @return {string}
111
+ */
112
+ resolveContentOverflowMode() {
113
+ const configuredMode = super.resolveContentOverflowMode();
114
+ if (configuredMode !== "visible") {
115
+ return configuredMode;
116
+ }
117
+
118
+ const { controlElement, popperElement } = getPopperElements.call(this);
119
+ if (resolveParentPopperContentBoundary(controlElement, popperElement)) {
120
+ return "both";
121
+ }
122
+
123
+ return configuredMode;
124
+ }
99
125
  }
100
126
 
101
127
  /**
@@ -126,6 +152,15 @@ function initContentObserver() {
126
152
  });
127
153
  }
128
154
 
155
+ function getPopperElements() {
156
+ return {
157
+ controlElement:
158
+ this.shadowRoot?.querySelector('[data-monster-role="control"]') || null,
159
+ popperElement:
160
+ this.shadowRoot?.querySelector('[data-monster-role="popper"]') || null,
161
+ };
162
+ }
163
+
129
164
  /**
130
165
  * @private
131
166
  * @return {void}
@@ -1765,55 +1765,20 @@ function getTranslations() {
1765
1765
  */
1766
1766
  function lookupSelection() {
1767
1767
  const self = this;
1768
+ const IntersectionObserverImplementation =
1769
+ getGlobal().IntersectionObserver;
1768
1770
 
1769
- const observer = new IntersectionObserver(
1771
+ if (!(IntersectionObserverImplementation instanceof Function)) {
1772
+ runSelectionLookupWhenVisible(self);
1773
+ return;
1774
+ }
1775
+
1776
+ const observer = new IntersectionObserverImplementation(
1770
1777
  (entries, obs) => {
1771
1778
  for (const entry of entries) {
1772
1779
  if (entry.isIntersecting) {
1773
1780
  obs.disconnect();
1774
-
1775
- setTimeout(() => {
1776
- const selection = self.getOption("selection");
1777
- if (
1778
- selection.length === 0 ||
1779
- self[isLoadingSymbol] ||
1780
- self[lazyLoadDoneSymbol]
1781
- ) {
1782
- return;
1783
- }
1784
-
1785
- let url = self.getOption("lookup.url") || self.getOption("url");
1786
- self[cleanupOptionsListSymbol] = false;
1787
-
1788
- if (self.getOption("lookup.grouping") === true) {
1789
- const values = selection
1790
- .map((s) => s?.["value"])
1791
- .filter(
1792
- (value) => isEmptyLookupValue.call(self, value) === false,
1793
- );
1794
- if (values.length === 0) {
1795
- return;
1796
- }
1797
- filterFromRemoteByValue
1798
- .call(self, url, { filter: values.join(",") })
1799
- .catch((e) => {
1800
- addErrorAttribute(self, e);
1801
- });
1802
- return;
1803
- }
1804
-
1805
- for (const s of selection) {
1806
- const value = s?.["value"];
1807
- if (isEmptyLookupValue.call(self, value)) {
1808
- continue;
1809
- }
1810
- filterFromRemoteByValue
1811
- .call(self, url, { filter: value })
1812
- .catch((e) => {
1813
- addErrorAttribute(self, e);
1814
- });
1815
- }
1816
- }, 100);
1781
+ runSelectionLookupWhenVisible(self);
1817
1782
  }
1818
1783
  }
1819
1784
  },
@@ -1823,6 +1788,49 @@ function lookupSelection() {
1823
1788
  observer.observe(self);
1824
1789
  }
1825
1790
 
1791
+ function runSelectionLookupWhenVisible(self) {
1792
+ setTimeout(() => {
1793
+ const selection = self.getOption("selection");
1794
+ if (
1795
+ selection.length === 0 ||
1796
+ self[isLoadingSymbol] ||
1797
+ self[lazyLoadDoneSymbol]
1798
+ ) {
1799
+ return;
1800
+ }
1801
+
1802
+ let url = self.getOption("lookup.url") || self.getOption("url");
1803
+ self[cleanupOptionsListSymbol] = false;
1804
+
1805
+ if (self.getOption("lookup.grouping") === true) {
1806
+ const values = selection
1807
+ .map((s) => s?.["value"])
1808
+ .filter((value) => isEmptyLookupValue.call(self, value) === false);
1809
+ if (values.length === 0) {
1810
+ return;
1811
+ }
1812
+ filterFromRemoteByValue
1813
+ .call(self, url, { filter: values.join(",") })
1814
+ .catch((e) => {
1815
+ addErrorAttribute(self, e);
1816
+ });
1817
+ return;
1818
+ }
1819
+
1820
+ for (const s of selection) {
1821
+ const value = s?.["value"];
1822
+ if (isEmptyLookupValue.call(self, value)) {
1823
+ continue;
1824
+ }
1825
+ filterFromRemoteByValue
1826
+ .call(self, url, { filter: value })
1827
+ .catch((e) => {
1828
+ addErrorAttribute(self, e);
1829
+ });
1830
+ }
1831
+ }, 100);
1832
+ }
1833
+
1826
1834
  /**
1827
1835
  * @private
1828
1836
  * @param {*} value
@@ -38,8 +38,6 @@ div[data-monster-role="popper"] {
38
38
  & > [part="content"] {
39
39
  display: block;
40
40
  max-width: 100%;
41
- max-height: none;
42
- overflow: visible;
43
41
  white-space: normal;
44
42
  text-wrap: pretty;
45
43
  word-break: break-word;
@@ -31,8 +31,6 @@ div[data-monster-role="popper"] {
31
31
  & > [part="content"] {
32
32
  display: block;
33
33
  max-width: 100%;
34
- max-height: none;
35
- overflow: visible;
36
34
  white-space: normal;
37
35
  text-wrap: pretty;
38
36
  word-break: break-word;
@@ -10,10 +10,10 @@
10
10
  * For more information about purchasing a commercial license, please contact Volker Schukai.
11
11
  */
12
12
 
13
- import { addAttributeToken } from "../../../dom/attributes.mjs";
14
- import { ATTRIBUTE_ERRORMESSAGE } from "../../../dom/constants.mjs";
13
+ import {addAttributeToken} from "../../../dom/attributes.mjs";
14
+ import {ATTRIBUTE_ERRORMESSAGE} from "../../../dom/constants.mjs";
15
15
 
16
- export { ContextErrorStyleSheet };
16
+ export {ContextErrorStyleSheet}
17
17
 
18
18
  /**
19
19
  * @private
@@ -22,17 +22,10 @@ export { ContextErrorStyleSheet };
22
22
  const ContextErrorStyleSheet = new CSSStyleSheet();
23
23
 
24
24
  try {
25
- ContextErrorStyleSheet.insertRule(
26
- `
25
+ ContextErrorStyleSheet.insertRule(`
27
26
  @layer contexterror {
28
- [data-monster-role=control]{box-sizing:border-box;outline:none;width:100%}[data-monster-role=control].flex{align-items:center;display:flex;flex-direction:row}:host{box-sizing:border-box;display:block}div[data-monster-role=popper]{align-content:center;background:var(--monster-bg-color-primary-1);border-color:var(--monster-bg-color-primary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);box-shadow:var(--monster-box-shadow-1);box-sizing:border-box;color:var(--monster-color-primary-1);display:none;justify-content:space-between;left:0;max-height:var(--monster-popper-max-height,calc(100vh - 2rem));max-width:var(--monster-popper-max-width,calc(100vw - 2rem));padding:1.1em;position:absolute;top:0;width:-moz-max-content;width:max-content;z-index:var(--monster-z-index-modal)}div[data-monster-role=popper]>[part=content]{max-height:var(--monster-popper-content-max-height,calc(100vh - 4.2rem));overflow:auto}div[data-monster-role=popper]>[part=content][data-monster-overflow-mode=horizontal]{clip-path:none;overflow:visible}div[data-monster-role=popper]>[part=content][data-monster-overflow-mode=visible]{clip-path:none;max-height:none;max-width:none;overflow:visible}div[data-monster-role=popper] div[data-monster-role=arrow]{background:var(--monster-bg-color-primary-1);height:calc(max(var(--monster-popper-witharrrow-distance), -1 * var(--monster-popper-witharrrow-distance))*2);pointer-events:none;position:absolute;width:calc(max(var(--monster-popper-witharrrow-distance), -1 * var(--monster-popper-witharrrow-distance))*2);z-index:-1}[data-monster-role=control]{line-height:1em;margin:0;padding:0;position:relative}[data-monster-role=control] [data-monster-role=button]{display:inline-block;position:relative}:is([data-monster-role=control] [data-monster-role=button]) svg{cursor:pointer}:is([data-monster-role=control] [data-monster-role=button]) svg.hidden{cursor:default;pointer-events:none;visibility:hidden}:host{display:inline-block;margin:0 .2em;padding:0;position:relative;vertical-align:bottom}div[data-monster-role=popper]{max-width:min(var(--monster-popper-max-width,calc(100vw - 2rem)),32rem)}div[data-monster-role=popper]>[part=content]{display:block;max-height:none;max-width:100%;overflow:visible;overflow-wrap:anywhere;text-wrap:pretty;white-space:normal;word-break:break-word}:host([disabled]) [data-monster-role=button] svg{cursor:default;pointer-events:none}
29
- }`,
30
- 0,
31
- );
27
+ [data-monster-role=control]{box-sizing:border-box;outline:none;width:100%}[data-monster-role=control].flex{align-items:center;display:flex;flex-direction:row}:host{box-sizing:border-box;display:block}div[data-monster-role=popper]{align-content:center;background:var(--monster-bg-color-primary-1);border-color:var(--monster-bg-color-primary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);box-shadow:var(--monster-box-shadow-1);box-sizing:border-box;color:var(--monster-color-primary-1);display:none;justify-content:space-between;left:0;max-height:var(--monster-popper-max-height,calc(100vh - 2rem));max-width:var(--monster-popper-max-width,calc(100vw - 2rem));padding:1.1em;position:absolute;top:0;width:-moz-max-content;width:max-content;z-index:var(--monster-z-index-modal)}div[data-monster-role=popper]>[part=content]{max-height:var(--monster-popper-content-max-height,calc(100vh - 4.2rem));overflow:auto}div[data-monster-role=popper]>[part=content][data-monster-overflow-mode=horizontal]{clip-path:none;overflow:visible}div[data-monster-role=popper]>[part=content][data-monster-overflow-mode=visible]{clip-path:none;max-height:none;max-width:none;overflow:visible}div[data-monster-role=popper] div[data-monster-role=arrow]{background:var(--monster-bg-color-primary-1);height:calc(max(var(--monster-popper-witharrrow-distance), -1 * var(--monster-popper-witharrrow-distance))*2);pointer-events:none;position:absolute;width:calc(max(var(--monster-popper-witharrrow-distance), -1 * var(--monster-popper-witharrrow-distance))*2);z-index:-1}[data-monster-role=control]{line-height:1em;margin:0;padding:0;position:relative}[data-monster-role=control] [data-monster-role=button]{display:inline-block;position:relative}:is([data-monster-role=control] [data-monster-role=button]) svg{cursor:pointer}:is([data-monster-role=control] [data-monster-role=button]) svg.hidden{cursor:default;pointer-events:none;visibility:hidden}:host{display:inline-block;margin:0 .2em;padding:0;position:relative;vertical-align:bottom}div[data-monster-role=popper]{max-width:min(var(--monster-popper-max-width,calc(100vw - 2rem)),32rem)}div[data-monster-role=popper]>[part=content]{display:block;max-width:100%;overflow-wrap:anywhere;text-wrap:pretty;white-space:normal;word-break:break-word}:host([disabled]) [data-monster-role=button] svg{cursor:default;pointer-events:none}
28
+ }`, 0);
32
29
  } catch (e) {
33
- addAttributeToken(
34
- document.getRootNode().querySelector("html"),
35
- ATTRIBUTE_ERRORMESSAGE,
36
- e + "",
37
- );
30
+ addAttributeToken(document.getRootNode().querySelector('html'), ATTRIBUTE_ERRORMESSAGE, e + "");
38
31
  }
@@ -10,10 +10,10 @@
10
10
  * For more information about purchasing a commercial license, please contact Volker Schukai.
11
11
  */
12
12
 
13
- import { addAttributeToken } from "../../../dom/attributes.mjs";
14
- import { ATTRIBUTE_ERRORMESSAGE } from "../../../dom/constants.mjs";
13
+ import {addAttributeToken} from "../../../dom/attributes.mjs";
14
+ import {ATTRIBUTE_ERRORMESSAGE} from "../../../dom/constants.mjs";
15
15
 
16
- export { ContextHelpStyleSheet };
16
+ export {ContextHelpStyleSheet}
17
17
 
18
18
  /**
19
19
  * @private
@@ -22,17 +22,10 @@ export { ContextHelpStyleSheet };
22
22
  const ContextHelpStyleSheet = new CSSStyleSheet();
23
23
 
24
24
  try {
25
- ContextHelpStyleSheet.insertRule(
26
- `
25
+ ContextHelpStyleSheet.insertRule(`
27
26
  @layer contexthelp {
28
- [data-monster-role=control]{box-sizing:border-box;outline:none;width:100%}[data-monster-role=control].flex{align-items:center;display:flex;flex-direction:row}:host{box-sizing:border-box;display:block}div[data-monster-role=popper]{align-content:center;background:var(--monster-bg-color-primary-1);border-color:var(--monster-bg-color-primary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);box-shadow:var(--monster-box-shadow-1);box-sizing:border-box;color:var(--monster-color-primary-1);display:none;justify-content:space-between;left:0;max-height:var(--monster-popper-max-height,calc(100vh - 2rem));max-width:var(--monster-popper-max-width,calc(100vw - 2rem));padding:1.1em;position:absolute;top:0;width:-moz-max-content;width:max-content;z-index:var(--monster-z-index-modal)}div[data-monster-role=popper]>[part=content]{max-height:var(--monster-popper-content-max-height,calc(100vh - 4.2rem));overflow:auto}div[data-monster-role=popper]>[part=content][data-monster-overflow-mode=horizontal]{clip-path:none;overflow:visible}div[data-monster-role=popper]>[part=content][data-monster-overflow-mode=visible]{clip-path:none;max-height:none;max-width:none;overflow:visible}div[data-monster-role=popper] div[data-monster-role=arrow]{background:var(--monster-bg-color-primary-1);height:calc(max(var(--monster-popper-witharrrow-distance), -1 * var(--monster-popper-witharrrow-distance))*2);pointer-events:none;position:absolute;width:calc(max(var(--monster-popper-witharrrow-distance), -1 * var(--monster-popper-witharrrow-distance))*2);z-index:-1}[data-monster-role=control]{line-height:1em;margin:0;padding:0;position:relative}[data-monster-role=control] [data-monster-role=button]{display:inline-block;position:relative}:is([data-monster-role=control] [data-monster-role=button]) svg{cursor:pointer}:is([data-monster-role=control] [data-monster-role=button]) svg.hidden{cursor:default;pointer-events:none;visibility:hidden}div[data-monster-role=popper]{max-width:min(var(--monster-popper-max-width,calc(100vw - 2rem)),32rem);z-index:var(--monster-z-index-tooltip-overlay)}div[data-monster-role=popper]>[part=content]{display:block;max-height:none;max-width:100%;overflow:visible;overflow-wrap:anywhere;text-wrap:pretty;white-space:normal;word-break:break-word}:host{display:inline-block;margin:0 .2em;padding:0;position:relative;vertical-align:bottom}:host([disabled]) [data-monster-role=button] svg{cursor:default;pointer-events:none}
29
- }`,
30
- 0,
31
- );
27
+ [data-monster-role=control]{box-sizing:border-box;outline:none;width:100%}[data-monster-role=control].flex{align-items:center;display:flex;flex-direction:row}:host{box-sizing:border-box;display:block}div[data-monster-role=popper]{align-content:center;background:var(--monster-bg-color-primary-1);border-color:var(--monster-bg-color-primary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);box-shadow:var(--monster-box-shadow-1);box-sizing:border-box;color:var(--monster-color-primary-1);display:none;justify-content:space-between;left:0;max-height:var(--monster-popper-max-height,calc(100vh - 2rem));max-width:var(--monster-popper-max-width,calc(100vw - 2rem));padding:1.1em;position:absolute;top:0;width:-moz-max-content;width:max-content;z-index:var(--monster-z-index-modal)}div[data-monster-role=popper]>[part=content]{max-height:var(--monster-popper-content-max-height,calc(100vh - 4.2rem));overflow:auto}div[data-monster-role=popper]>[part=content][data-monster-overflow-mode=horizontal]{clip-path:none;overflow:visible}div[data-monster-role=popper]>[part=content][data-monster-overflow-mode=visible]{clip-path:none;max-height:none;max-width:none;overflow:visible}div[data-monster-role=popper] div[data-monster-role=arrow]{background:var(--monster-bg-color-primary-1);height:calc(max(var(--monster-popper-witharrrow-distance), -1 * var(--monster-popper-witharrrow-distance))*2);pointer-events:none;position:absolute;width:calc(max(var(--monster-popper-witharrrow-distance), -1 * var(--monster-popper-witharrrow-distance))*2);z-index:-1}[data-monster-role=control]{line-height:1em;margin:0;padding:0;position:relative}[data-monster-role=control] [data-monster-role=button]{display:inline-block;position:relative}:is([data-monster-role=control] [data-monster-role=button]) svg{cursor:pointer}:is([data-monster-role=control] [data-monster-role=button]) svg.hidden{cursor:default;pointer-events:none;visibility:hidden}div[data-monster-role=popper]{max-width:min(var(--monster-popper-max-width,calc(100vw - 2rem)),32rem);z-index:var(--monster-z-index-tooltip-overlay)}div[data-monster-role=popper]>[part=content]{display:block;max-width:100%;overflow-wrap:anywhere;text-wrap:pretty;white-space:normal;word-break:break-word}:host{display:inline-block;margin:0 .2em;padding:0;position:relative;vertical-align:bottom}:host([disabled]) [data-monster-role=button] svg{cursor:default;pointer-events:none}
28
+ }`, 0);
32
29
  } catch (e) {
33
- addAttributeToken(
34
- document.getRootNode().querySelector("html"),
35
- ATTRIBUTE_ERRORMESSAGE,
36
- e + "",
37
- );
30
+ addAttributeToken(document.getRootNode().querySelector('html'), ATTRIBUTE_ERRORMESSAGE, e + "");
38
31
  }
@@ -30,6 +30,7 @@ export {
30
30
  applyAdaptiveFloatingElementSize,
31
31
  closePositionedPopper,
32
32
  resolveClippingBoundaryElement,
33
+ resolveParentPopperContentBoundary,
33
34
  isPositionedPopperOpen,
34
35
  openPositionedPopper,
35
36
  positionPopper,
@@ -144,10 +145,12 @@ function normalizePopperConfig(options, controlElement, popperElement) {
144
145
  options,
145
146
  );
146
147
 
147
- config.boundaryElement = resolveClippingBoundaryElement(
148
- controlElement,
149
- popperElement,
150
- );
148
+ if (!(config.boundaryElement instanceof HTMLElement)) {
149
+ config.boundaryElement = resolveClippingBoundaryElement(
150
+ controlElement,
151
+ popperElement,
152
+ );
153
+ }
151
154
  config.detectOverflowOptions = buildDetectOverflowOptions(
152
155
  config.boundaryElement,
153
156
  );
@@ -283,6 +286,10 @@ function applyAdaptiveFloatingElementSize(
283
286
  floatingElement,
284
287
  { availableWidth, availableHeight },
285
288
  ) {
289
+ const contentElement = getFloatingContentElement(floatingElement);
290
+ const usesVisibleOverflow =
291
+ contentElement instanceof HTMLElement &&
292
+ contentElement.dataset.monsterOverflowMode === "visible";
286
293
  const maxWidth = clampAvailableDimension(
287
294
  availableWidth,
288
295
  readMaxDimension(floatingElement, "maxWidth"),
@@ -301,7 +308,7 @@ function applyAdaptiveFloatingElementSize(
301
308
  nextStyle.maxWidth = "";
302
309
  }
303
310
 
304
- if (Number.isFinite(maxHeight) && maxHeight > 0) {
311
+ if (!usesVisibleOverflow && Number.isFinite(maxHeight) && maxHeight > 0) {
305
312
  nextStyle.maxHeight = `${maxHeight}px`;
306
313
  } else {
307
314
  nextStyle.maxHeight = "";
@@ -332,14 +339,19 @@ function applyAdaptiveFloatingContentSize(floatingElement, maxHeight) {
332
339
  Number.isFinite(maxHeight) ? maxHeight - reservedHeight : null,
333
340
  readMaxDimension(contentElement, "maxHeight"),
334
341
  );
335
-
336
- if (Number.isFinite(contentMaxHeight) && contentMaxHeight > 0) {
337
- contentElement.style.maxHeight = `${contentMaxHeight}px`;
342
+ const minimumReadableHeight = getMinimumReadableContentHeight(contentElement);
343
+ const nextContentMaxHeight =
344
+ Number.isFinite(contentMaxHeight) && contentMaxHeight > 0
345
+ ? Math.max(contentMaxHeight, minimumReadableHeight)
346
+ : contentMaxHeight;
347
+
348
+ if (Number.isFinite(nextContentMaxHeight) && nextContentMaxHeight > 0) {
349
+ contentElement.style.maxHeight = `${nextContentMaxHeight}px`;
338
350
  } else {
339
351
  contentElement.style.maxHeight = "";
340
352
  }
341
353
 
342
- syncNestedScrollContainerHeight(contentElement, contentMaxHeight);
354
+ syncNestedScrollContainerHeight(contentElement, nextContentMaxHeight);
343
355
  }
344
356
 
345
357
  function getFloatingContentElement(floatingElement) {
@@ -391,6 +403,63 @@ function readBoxDimension(rawValue) {
391
403
  return Number.isFinite(value) ? value : 0;
392
404
  }
393
405
 
406
+ function getMinimumReadableContentHeight(contentElement) {
407
+ if (!(contentElement instanceof HTMLElement)) {
408
+ return 0;
409
+ }
410
+
411
+ const measurementTarget =
412
+ getPrimaryReadableContentElement(contentElement) || contentElement;
413
+ const style = getComputedStyle(measurementTarget);
414
+ const lineHeight = readLineHeight(style);
415
+ const paddingHeight =
416
+ readBoxDimension(getComputedStyle(contentElement).paddingTop) +
417
+ readBoxDimension(getComputedStyle(contentElement).paddingBottom);
418
+
419
+ return Math.max(0, lineHeight + paddingHeight);
420
+ }
421
+
422
+ function readLineHeight(style) {
423
+ if (!style) {
424
+ return 0;
425
+ }
426
+
427
+ const lineHeight = Number.parseFloat(style.lineHeight);
428
+ if (Number.isFinite(lineHeight) && lineHeight > 0) {
429
+ return lineHeight;
430
+ }
431
+
432
+ const fontSize = Number.parseFloat(style.fontSize);
433
+ if (Number.isFinite(fontSize) && fontSize > 0) {
434
+ return fontSize * 1.4;
435
+ }
436
+
437
+ return 0;
438
+ }
439
+
440
+ function getPrimaryReadableContentElement(contentElement) {
441
+ if (!(contentElement instanceof HTMLElement)) {
442
+ return null;
443
+ }
444
+
445
+ const slotElement = contentElement.querySelector("slot");
446
+ if (slotElement?.assignedElements instanceof Function) {
447
+ for (const element of slotElement.assignedElements({ flatten: true })) {
448
+ if (element instanceof HTMLElement) {
449
+ return element;
450
+ }
451
+ }
452
+ }
453
+
454
+ for (const child of contentElement.children) {
455
+ if (child instanceof HTMLElement) {
456
+ return child;
457
+ }
458
+ }
459
+
460
+ return null;
461
+ }
462
+
394
463
  function syncNestedScrollContainerHeight(contentElement, contentMaxHeight) {
395
464
  const nestedScrollableElement = getNestedScrollableElement(contentElement);
396
465
  if (!(nestedScrollableElement instanceof HTMLElement)) {
@@ -398,8 +467,12 @@ function syncNestedScrollContainerHeight(contentElement, contentMaxHeight) {
398
467
  }
399
468
 
400
469
  if (Number.isFinite(contentMaxHeight) && contentMaxHeight > 0) {
401
- nestedScrollableElement.style.height = `${contentMaxHeight}px`;
402
- nestedScrollableElement.style.maxHeight = `${contentMaxHeight}px`;
470
+ const nextNestedHeight = resolveNestedScrollContainerHeight(
471
+ nestedScrollableElement,
472
+ contentMaxHeight,
473
+ );
474
+ nestedScrollableElement.style.height = `${nextNestedHeight}px`;
475
+ nestedScrollableElement.style.maxHeight = `${nextNestedHeight}px`;
403
476
  return;
404
477
  }
405
478
 
@@ -407,6 +480,41 @@ function syncNestedScrollContainerHeight(contentElement, contentMaxHeight) {
407
480
  nestedScrollableElement.style.maxHeight = "";
408
481
  }
409
482
 
483
+ function resolveNestedScrollContainerHeight(
484
+ nestedScrollableElement,
485
+ contentMaxHeight,
486
+ ) {
487
+ const declaredHeight = readDeclaredDimension(nestedScrollableElement, "height");
488
+ const declaredMaxHeight = readDeclaredDimension(
489
+ nestedScrollableElement,
490
+ "maxHeight",
491
+ );
492
+ const scrollHeight = nestedScrollableElement.scrollHeight;
493
+ const preferredHeightCandidates = [
494
+ declaredHeight,
495
+ declaredMaxHeight,
496
+ scrollHeight,
497
+ ];
498
+ const preferredHeight = preferredHeightCandidates.find((value) => {
499
+ return Number.isFinite(value) && value > 0;
500
+ });
501
+
502
+ if (Number.isFinite(preferredHeight) && preferredHeight > 0) {
503
+ return Math.min(contentMaxHeight, preferredHeight);
504
+ }
505
+
506
+ return contentMaxHeight;
507
+ }
508
+
509
+ function readDeclaredDimension(element, property) {
510
+ if (!(element instanceof HTMLElement)) {
511
+ return NaN;
512
+ }
513
+
514
+ const value = Number.parseFloat(element.style?.[property] || "");
515
+ return Number.isFinite(value) ? value : NaN;
516
+ }
517
+
410
518
  function syncPreferredFloatingWidth(floatingElement, maxWidth) {
411
519
  const preferredWidth = Number.parseFloat(
412
520
  floatingElement.dataset.monsterPreferredWidth || "",
@@ -539,6 +647,17 @@ function resolveClippingBoundaryElement(...elements) {
539
647
  return null;
540
648
  }
541
649
 
650
+ function resolveParentPopperContentBoundary(...elements) {
651
+ for (const element of elements) {
652
+ const clippingBoundary = findNearestIgnoredClippingContainer(element);
653
+ if (clippingBoundary instanceof HTMLElement) {
654
+ return clippingBoundary;
655
+ }
656
+ }
657
+
658
+ return null;
659
+ }
660
+
542
661
  function findNearestClippingContainer(element) {
543
662
  let current = getComposedParent(element);
544
663
 
@@ -557,11 +676,46 @@ function findNearestClippingContainer(element) {
557
676
  return null;
558
677
  }
559
678
 
679
+ function findNearestIgnoredClippingContainer(element) {
680
+ let current = getComposedParent(element);
681
+
682
+ while (current) {
683
+ if (
684
+ current instanceof HTMLElement &&
685
+ shouldIgnoreClippingContainer(current) &&
686
+ shouldEscapeParentPopperContentWrapper(current)
687
+ ) {
688
+ return current;
689
+ }
690
+
691
+ current = getComposedParent(current);
692
+ }
693
+
694
+ return null;
695
+ }
696
+
697
+ function shouldEscapeParentPopperContentWrapper(element) {
698
+ if (!(element instanceof HTMLElement) || !isPopperContentWrapper(element)) {
699
+ return false;
700
+ }
701
+
702
+ const overflowMode = element.getAttribute("data-monster-overflow-mode");
703
+ if (overflowMode === "both") {
704
+ return true;
705
+ }
706
+
707
+ return isClippingContainer(getComputedStyle(element));
708
+ }
709
+
560
710
  function getComposedParent(node) {
561
711
  if (!node) {
562
712
  return null;
563
713
  }
564
714
 
715
+ if (node instanceof Element && node.assignedSlot) {
716
+ return node.assignedSlot;
717
+ }
718
+
565
719
  if (node instanceof ShadowRoot) {
566
720
  return node.host || null;
567
721
  }
@@ -11,6 +11,7 @@ const global = getGlobal();
11
11
 
12
12
  let ContextHelp;
13
13
  let resolveClippingBoundaryElement;
14
+ let resolveParentPopperContentBoundary;
14
15
 
15
16
  describe("ContextHelp", function () {
16
17
  before(function (done) {
@@ -29,6 +30,8 @@ describe("ContextHelp", function () {
29
30
  ContextHelp = contextHelpModule.ContextHelp;
30
31
  resolveClippingBoundaryElement =
31
32
  floatingUiModule.resolveClippingBoundaryElement;
33
+ resolveParentPopperContentBoundary =
34
+ floatingUiModule.resolveParentPopperContentBoundary;
32
35
  done();
33
36
  })
34
37
  .catch((e) => done(e));
@@ -114,4 +117,48 @@ describe("ContextHelp", function () {
114
117
  }
115
118
  }, 0);
116
119
  });
120
+
121
+ it("should switch to fixed positioning inside a parent popper content wrapper with overflow both", function (done) {
122
+ let mocks = document.getElementById("mocks");
123
+ const host = document.createElement("div");
124
+ const help = document.createElement("monster-context-help");
125
+
126
+ mocks.appendChild(host);
127
+ const shadowRoot = host.attachShadow({ mode: "open" });
128
+ shadowRoot.innerHTML = `
129
+ <div data-monster-role="popper">
130
+ <div part="content"
131
+ data-monster-overflow-mode="both">
132
+ </div>
133
+ </div>
134
+ `;
135
+ shadowRoot.querySelector('[part="content"]').appendChild(help);
136
+ help.innerHTML = "<p>Nested help</p>";
137
+
138
+ setTimeout(() => {
139
+ try {
140
+ const control = help.shadowRoot.querySelector(
141
+ '[data-monster-role="control"]',
142
+ );
143
+ const popper = help.shadowRoot.querySelector(
144
+ '[data-monster-role="popper"]',
145
+ );
146
+
147
+ expect(
148
+ resolveParentPopperContentBoundary(control, popper),
149
+ ).to.equal(shadowRoot.querySelector('[part="content"]'));
150
+
151
+ const content = help.shadowRoot.querySelector('[part="content"]');
152
+ expect(content.getAttribute("data-monster-overflow-mode")).to.equal(
153
+ "both",
154
+ );
155
+
156
+ help.showDialog();
157
+ expect(popper.style.position).to.equal("fixed");
158
+ done();
159
+ } catch (e) {
160
+ done(e);
161
+ }
162
+ }, 0);
163
+ });
117
164
  });
@@ -135,4 +135,106 @@ describe("form floating-ui boundary resolution", function () {
135
135
  expect(content.style.maxWidth).to.equal("");
136
136
  expect(content.style.maxHeight).to.equal("100px");
137
137
  });
138
+
139
+ it("should not clamp the floating element height when content overflow is visible", function () {
140
+ const mocks = document.getElementById("mocks");
141
+ const popper = document.createElement("div");
142
+ const content = document.createElement("div");
143
+
144
+ popper.style.maxHeight = "300px";
145
+ content.style.maxHeight = "240px";
146
+ content.setAttribute("part", "content");
147
+ content.setAttribute("data-monster-overflow-mode", "visible");
148
+
149
+ popper.appendChild(content);
150
+ mocks.appendChild(popper);
151
+
152
+ applyAdaptiveFloatingElementSize(popper, {
153
+ availableWidth: 220,
154
+ availableHeight: 160,
155
+ });
156
+
157
+ expect(popper.style.maxHeight).to.equal("");
158
+ expect(content.style.maxHeight).to.equal("240px");
159
+ });
160
+
161
+ it("should keep at least one readable line for scrollable content", function () {
162
+ const mocks = document.getElementById("mocks");
163
+ const popper = document.createElement("div");
164
+ const content = document.createElement("div");
165
+
166
+ popper.style.maxHeight = "300px";
167
+ content.setAttribute("part", "content");
168
+ content.textContent = "A long help text that still needs one readable line.";
169
+ content.style.fontSize = "16px";
170
+ content.style.lineHeight = "24px";
171
+ popper.appendChild(content);
172
+ mocks.appendChild(popper);
173
+
174
+ applyAdaptiveFloatingElementSize(popper, {
175
+ availableWidth: 220,
176
+ availableHeight: 10,
177
+ });
178
+
179
+ expect(content.style.maxHeight).to.equal("24px");
180
+ });
181
+
182
+ it("should use the first slotted element line height for the minimum readable size", function () {
183
+ const mocks = document.getElementById("mocks");
184
+ const popperHost = document.createElement("div");
185
+ const slottedParagraph = document.createElement("p");
186
+ slottedParagraph.textContent = "Readable help line";
187
+ slottedParagraph.style.lineHeight = "26px";
188
+
189
+ mocks.appendChild(popperHost);
190
+ const shadowRoot = popperHost.attachShadow({ mode: "open" });
191
+ shadowRoot.innerHTML = `
192
+ <div data-monster-role="popper">
193
+ <div part="content">
194
+ <slot></slot>
195
+ </div>
196
+ </div>
197
+ `;
198
+
199
+ const popper = shadowRoot.querySelector('[data-monster-role="popper"]');
200
+ const content = shadowRoot.querySelector('[part="content"]');
201
+ popperHost.appendChild(slottedParagraph);
202
+
203
+ applyAdaptiveFloatingElementSize(popper, {
204
+ availableWidth: 220,
205
+ availableHeight: 10,
206
+ });
207
+
208
+ expect(content.style.maxHeight).to.equal("26px");
209
+ });
210
+
211
+ it("should respect a smaller nested scroll container height", function () {
212
+ const mocks = document.getElementById("mocks");
213
+ const popper = document.createElement("div");
214
+ const content = document.createElement("div");
215
+ const options = document.createElement("div");
216
+
217
+ content.setAttribute("part", "content");
218
+ content.style.overflowY = "hidden";
219
+ options.style.overflowY = "auto";
220
+ options.style.height = "72px";
221
+ options.style.maxHeight = "72px";
222
+ Object.defineProperty(options, "scrollHeight", {
223
+ configurable: true,
224
+ value: 72,
225
+ });
226
+
227
+ content.appendChild(options);
228
+ popper.appendChild(content);
229
+ mocks.appendChild(popper);
230
+
231
+ applyAdaptiveFloatingElementSize(popper, {
232
+ availableWidth: 220,
233
+ availableHeight: 180,
234
+ });
235
+
236
+ expect(content.style.maxHeight).to.equal("180px");
237
+ expect(options.style.height).to.equal("72px");
238
+ expect(options.style.maxHeight).to.equal("72px");
239
+ });
138
240
  });
@@ -261,44 +261,39 @@ describe("MessageStateButton", function () {
261
261
  }, 0);
262
262
  });
263
263
 
264
- it("should resolve nested select message content to horizontal clipping only", function (done) {
264
+ it("should resolve nested select message content to horizontal clipping only", async function () {
265
265
  let mocks = document.getElementById("mocks");
266
266
  const button = document.createElement("monster-message-state-button");
267
267
  button.innerHTML = "Save";
268
268
  mocks.appendChild(button);
269
269
 
270
- setTimeout(() => {
271
- try {
272
- const wrapper = document.createElement("div");
273
- wrapper.appendChild(document.createElement("monster-select"));
274
- button.setMessage(wrapper);
275
- button.showMessage();
270
+ const wrapper = document.createElement("div");
271
+ wrapper.appendChild(document.createElement("monster-select"));
272
+ button.setMessage(wrapper);
273
+ button.showMessage();
274
+
275
+ await waitForCondition(() => {
276
+ const content = button.shadowRoot?.querySelector('[part="content"]');
277
+ return (
278
+ content?.getAttribute("data-monster-overflow-mode") === "horizontal"
279
+ );
280
+ });
276
281
 
277
- setTimeout(() => {
278
- try {
279
- const content = button.shadowRoot.querySelector('[part="content"]');
280
- const message = button.shadowRoot.querySelector(
281
- '[data-monster-role="message"]',
282
- );
283
- expect(content).to.exist;
284
- expect(content.getAttribute("data-monster-overflow-mode")).to.equal(
285
- "horizontal",
286
- );
287
- expect(
288
- content.getAttribute("data-monster-message-layout"),
289
- ).to.equal("overlay");
290
- expect(
291
- message.getAttribute("data-monster-message-layout"),
292
- ).to.equal("overlay");
293
- done();
294
- } catch (e) {
295
- done(e);
296
- }
297
- }, 0);
298
- } catch (e) {
299
- done(e);
300
- }
301
- }, 0);
282
+ const content = button.shadowRoot.querySelector('[part="content"]');
283
+ const message = button.shadowRoot.querySelector(
284
+ '[data-monster-role="message"]',
285
+ );
286
+
287
+ expect(content).to.exist;
288
+ expect(content.getAttribute("data-monster-overflow-mode")).to.equal(
289
+ "horizontal",
290
+ );
291
+ expect(content.getAttribute("data-monster-message-layout")).to.equal(
292
+ "overlay",
293
+ );
294
+ expect(message.getAttribute("data-monster-message-layout")).to.equal(
295
+ "overlay",
296
+ );
302
297
  });
303
298
 
304
299
  it("should resolve wide plain content to the wide layout", function (done) {
@@ -489,3 +484,30 @@ describe("MessageStateButton", function () {
489
484
  });
490
485
  });
491
486
  });
487
+
488
+ function waitForCondition(check, { timeout = 4000, interval = 25 } = {}) {
489
+ return new Promise((resolve, reject) => {
490
+ const start = Date.now();
491
+
492
+ const poll = () => {
493
+ try {
494
+ if (check()) {
495
+ resolve();
496
+ return;
497
+ }
498
+ } catch (error) {
499
+ reject(error);
500
+ return;
501
+ }
502
+
503
+ if (Date.now() - start >= timeout) {
504
+ reject(new Error("Timed out while waiting for test condition."));
505
+ return;
506
+ }
507
+
508
+ setTimeout(poll, interval);
509
+ };
510
+
511
+ poll();
512
+ });
513
+ }
@@ -549,6 +549,46 @@ describe('Select', function () {
549
549
  }, 50);
550
550
  });
551
551
 
552
+ it('should not throw when IntersectionObserver is unavailable', function (done) {
553
+ this.timeout(2000);
554
+
555
+ let mocks = document.getElementById('mocks');
556
+ const savedIntersectionObserver = global.IntersectionObserver;
557
+ global.IntersectionObserver = undefined;
558
+ window.IntersectionObserver = undefined;
559
+ const failures = [];
560
+ const onError = (event) => {
561
+ failures.push(event?.error || event);
562
+ };
563
+ window.addEventListener('error', onError);
564
+
565
+ const select = document.createElement('monster-select');
566
+ select.setOption('url', 'https://example.com/items?filter={filter}&page={page}');
567
+ select.setOption('filter.mode', 'remote');
568
+ select.setOption('mapping.selector', 'items.*');
569
+ select.setOption('mapping.labelTemplate', '${name}');
570
+ select.setOption('mapping.valueTemplate', '${id}');
571
+ select.setOption('mapping.total', 'pagination.total');
572
+ select.setOption('mapping.currentPage', 'pagination.page');
573
+ select.setOption('mapping.objectsPerPage', 'pagination.perPage');
574
+ select.setOption('selection', [{value: 'alpha'}]);
575
+ mocks.appendChild(select);
576
+
577
+ setTimeout(() => {
578
+ try {
579
+ expect(failures).to.have.length(0);
580
+ } catch (e) {
581
+ return done(e);
582
+ } finally {
583
+ window.removeEventListener('error', onError);
584
+ global.IntersectionObserver = savedIntersectionObserver;
585
+ window.IntersectionObserver = savedIntersectionObserver;
586
+ }
587
+
588
+ done();
589
+ }, 250);
590
+ });
591
+
552
592
  });
553
593
 
554
594
 
@@ -14,6 +14,7 @@ export function setupIntersectionObserverMock(
14
14
  } = {}) {
15
15
 
16
16
  const savedImplementation = window.IntersectionObserver;
17
+ const savedGlobalImplementation = global.IntersectionObserver;
17
18
 
18
19
  let lastObject;
19
20
 
@@ -61,9 +62,10 @@ export function setupIntersectionObserverMock(
61
62
  return {
62
63
  restore: function () {
63
64
  window.IntersectionObserver = savedImplementation;
65
+ global.IntersectionObserver = savedGlobalImplementation;
64
66
  },
65
67
  getInstance: function () {
66
68
  return lastObject;
67
69
  }
68
70
  }
69
- }
71
+ }