@schukai/monster 4.129.0 → 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.
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.129.0"}
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"}
@@ -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
  /**
@@ -16,20 +16,63 @@ button {
16
16
  max-width: 100%;
17
17
  }
18
18
 
19
+ [part=content] {
20
+ display: block;
21
+ min-width: 0;
22
+ }
23
+
19
24
  [data-monster-role=message] {
20
- max-width: 100%;
25
+ display: block;
21
26
  min-width: 0;
27
+ }
28
+
29
+ [data-monster-role=message][data-monster-message-layout=prose] {
30
+ width: 100%;
31
+ max-width: 100%;
32
+ overflow-wrap: break-word;
33
+ word-break: break-word;
34
+
35
+ & > * {
36
+ max-width: 100%;
37
+ min-width: 0;
38
+ }
39
+ }
40
+
41
+ [data-monster-role=message][data-monster-message-layout=wide] {
42
+ width: max-content;
43
+ max-width: none;
44
+
45
+ & > * {
46
+ min-width: 0;
47
+ }
48
+ }
49
+
50
+ [data-monster-role=message][data-monster-message-layout=overlay] {
51
+ width: 100%;
52
+ max-width: 100%;
22
53
  overflow-wrap: break-word;
23
54
  word-break: break-word;
24
55
 
25
56
  & > * {
26
57
  max-width: 100%;
58
+ min-width: 0;
27
59
  }
28
60
 
29
61
  & input,
30
62
  & select,
31
- & textarea,
32
- & monster-select {
63
+ & textarea {
64
+ max-width: 100%;
65
+ min-width: 0;
66
+ }
67
+
68
+ & monster-select,
69
+ & monster-details,
70
+ & monster-popper,
71
+ & monster-popper-button,
72
+ & monster-message-state-button {
73
+ display: block;
74
+ width: 100%;
33
75
  max-width: 100%;
76
+ min-width: 0;
34
77
  }
35
78
  }