@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
@@ -118,6 +118,8 @@ const updateCloneDataSymbol = Symbol("@schukai/monster/dom/@@updateCloneData");
118
118
  */
119
119
  const scriptHostElementSymbol = Symbol("scriptHostElement");
120
120
  const managedShadowRootSymbol = Symbol("managedShadowRoot");
121
+ const visibilityStateSymbol = Symbol("visibilityState");
122
+ let hostVisibilityStyleSheet = null;
121
123
 
122
124
  /**
123
125
  * The `CustomElement` class provides a way to define a new HTML element using the power of Custom Elements.
@@ -278,6 +280,24 @@ class CustomElement extends HTMLElement {
278
280
  return this;
279
281
  }
280
282
 
283
+ /**
284
+ * Returns whether the host element is currently visible.
285
+ *
286
+ * @returns {boolean}
287
+ */
288
+ get visible() {
289
+ return !this.hidden;
290
+ }
291
+
292
+ /**
293
+ * Alias for the current host visibility state.
294
+ *
295
+ * @returns {boolean}
296
+ */
297
+ get isVisible() {
298
+ return this.visible;
299
+ }
300
+
281
301
  /**
282
302
  * The `customization` property allows overwriting the defaults.
283
303
  * Unlike the defaults that expect an object, the customization is a Map.
@@ -541,6 +561,54 @@ class CustomElement extends HTMLElement {
541
561
  return this;
542
562
  }
543
563
 
564
+ /**
565
+ * Shows the host element.
566
+ *
567
+ * @returns {CustomElement}
568
+ */
569
+ show() {
570
+ return this.setVisible(true);
571
+ }
572
+
573
+ /**
574
+ * Hides the host element.
575
+ *
576
+ * @returns {CustomElement}
577
+ */
578
+ hide() {
579
+ return this.setVisible(false);
580
+ }
581
+
582
+ /**
583
+ * Sets the host visibility.
584
+ *
585
+ * @param {boolean} visible
586
+ * @returns {CustomElement}
587
+ */
588
+ setVisible(visible) {
589
+ if (visible) {
590
+ this.removeAttribute("hidden");
591
+ } else {
592
+ this.setAttribute("hidden", "");
593
+ }
594
+
595
+ return this;
596
+ }
597
+
598
+ /**
599
+ * Toggles the host visibility.
600
+ *
601
+ * @param {boolean} [force]
602
+ * @returns {CustomElement}
603
+ */
604
+ toggleVisibility(force) {
605
+ if (typeof force === "boolean") {
606
+ return this.setVisible(force);
607
+ }
608
+
609
+ return this.setVisible(!this.visible);
610
+ }
611
+
544
612
  /**
545
613
  * Is called once via the constructor
546
614
  *
@@ -674,6 +742,8 @@ class CustomElement extends HTMLElement {
674
742
  this[internalSymbol].syncDisabledState();
675
743
  }
676
744
 
745
+ syncVisibilityState.call(this);
746
+
677
747
  return this;
678
748
  }
679
749
 
@@ -747,6 +817,10 @@ class CustomElement extends HTMLElement {
747
817
  );
748
818
  }
749
819
 
820
+ if (attrName === "hidden") {
821
+ syncVisibilityState.call(this);
822
+ }
823
+
750
824
  const callback = this[attributeObserverSymbol]?.[attrName];
751
825
  if (isFunction(callback)) {
752
826
  try {
@@ -1296,6 +1370,73 @@ function initHtmlContent() {
1296
1370
  return this;
1297
1371
  }
1298
1372
 
1373
+ /**
1374
+ * @private
1375
+ * @returns {void}
1376
+ */
1377
+ function syncVisibilityState() {
1378
+ const visible = !this.hidden;
1379
+ if (typeof this[visibilityStateSymbol] === "undefined") {
1380
+ this[visibilityStateSymbol] = visible;
1381
+ if (!visible) {
1382
+ blurFocusedElement.call(this);
1383
+ }
1384
+ return;
1385
+ }
1386
+
1387
+ if (this[visibilityStateSymbol] === visible) {
1388
+ return;
1389
+ }
1390
+
1391
+ if (!visible) {
1392
+ blurFocusedElement.call(this);
1393
+ }
1394
+
1395
+ this[visibilityStateSymbol] = visible;
1396
+ dispatchVisibilityChangedEvent.call(this, visible);
1397
+ }
1398
+
1399
+ /**
1400
+ * @private
1401
+ * @returns {void}
1402
+ */
1403
+ function blurFocusedElement() {
1404
+ const shadowRoot = getManagedShadowRoot.call(this);
1405
+ const activeShadowElement = shadowRoot?.activeElement;
1406
+
1407
+ if (activeShadowElement instanceof HTMLElement) {
1408
+ activeShadowElement.blur();
1409
+ }
1410
+
1411
+ if (document.activeElement === this && typeof this.blur === "function") {
1412
+ this.blur();
1413
+ }
1414
+
1415
+ if (shadowRoot?.activeElement instanceof HTMLElement) {
1416
+ const body = getDocument()?.body;
1417
+ if (body instanceof HTMLElement) {
1418
+ body.setAttribute("tabindex", "-1");
1419
+ body.focus();
1420
+ body.removeAttribute("tabindex");
1421
+ }
1422
+ }
1423
+ }
1424
+
1425
+ /**
1426
+ * @private
1427
+ * @param {boolean} visible
1428
+ * @returns {void}
1429
+ */
1430
+ function dispatchVisibilityChangedEvent(visible) {
1431
+ this.dispatchEvent(
1432
+ new CustomEvent("monster-visibility-changed", {
1433
+ bubbles: true,
1434
+ composed: true,
1435
+ detail: { visible },
1436
+ }),
1437
+ );
1438
+ }
1439
+
1299
1440
  /**
1300
1441
  * @private
1301
1442
  * @return {CustomElement}
@@ -1309,15 +1450,18 @@ function initCSSStylesheet() {
1309
1450
  if (!(shadowRoot instanceof ShadowRoot)) {
1310
1451
  return this;
1311
1452
  }
1453
+ const visibilityStyleSheet = getHostVisibilityStyleSheet();
1312
1454
 
1313
1455
  const styleSheet = this.constructor.getCSSStyleSheet();
1314
1456
 
1315
1457
  if (styleSheet instanceof CSSStyleSheet) {
1316
1458
  if (styleSheet.cssRules.length > 0) {
1317
- shadowRoot.adoptedStyleSheets = [styleSheet];
1459
+ shadowRoot.adoptedStyleSheets = [visibilityStyleSheet, styleSheet];
1460
+ } else {
1461
+ shadowRoot.adoptedStyleSheets = [visibilityStyleSheet];
1318
1462
  }
1319
1463
  } else if (isArray(styleSheet)) {
1320
- const assign = [];
1464
+ const assign = [visibilityStyleSheet];
1321
1465
  for (const s of styleSheet) {
1322
1466
  if (isString(s)) {
1323
1467
  const trimedStyleSheet = s.trim();
@@ -1340,17 +1484,53 @@ function initCSSStylesheet() {
1340
1484
  shadowRoot.adoptedStyleSheets = assign;
1341
1485
  }
1342
1486
  } else if (isString(styleSheet)) {
1487
+ shadowRoot.adoptedStyleSheets = [visibilityStyleSheet];
1343
1488
  const trimedStyleSheet = styleSheet.trim();
1344
1489
  if (trimedStyleSheet !== "") {
1345
1490
  const style = document.createElement("style");
1346
1491
  style.innerHTML = styleSheet;
1347
1492
  shadowRoot.prepend(style);
1348
1493
  }
1494
+ } else {
1495
+ shadowRoot.adoptedStyleSheets = [visibilityStyleSheet];
1349
1496
  }
1350
1497
 
1351
1498
  return this;
1352
1499
  }
1353
1500
 
1501
+ /**
1502
+ * @private
1503
+ * @returns {CSSStyleSheet}
1504
+ */
1505
+ function getHostVisibilityStyleSheet() {
1506
+ if (hostVisibilityStyleSheet instanceof CSSStyleSheet) {
1507
+ return hostVisibilityStyleSheet;
1508
+ }
1509
+
1510
+ hostVisibilityStyleSheet = new CSSStyleSheet();
1511
+ hostVisibilityStyleSheet.replaceSync(
1512
+ ":host([hidden]){display:none !important}",
1513
+ );
1514
+
1515
+ return hostVisibilityStyleSheet;
1516
+ }
1517
+
1518
+ /**
1519
+ * @private
1520
+ * @param {ShadowRoot} shadowRoot
1521
+ * @returns {void}
1522
+ */
1523
+ function appendVisibilityHostStyle(shadowRoot) {
1524
+ if (!(shadowRoot instanceof ShadowRoot)) {
1525
+ return;
1526
+ }
1527
+
1528
+ const style = document.createElement("style");
1529
+ style.setAttribute("data-monster-host-visibility", "true");
1530
+ style.textContent = ":host([hidden]){display:none !important}";
1531
+ shadowRoot.prepend(style);
1532
+ }
1533
+
1354
1534
  /**
1355
1535
  * @private
1356
1536
  * @return {CustomElement}
@@ -32,32 +32,68 @@ function extractKeys(
32
32
  ) {
33
33
  const resultMap = new Map();
34
34
 
35
- function helper(currentObj, currentKeyPrefix, currentValuePrefix) {
35
+ function normalizeKeySegment(value) {
36
+ return String(value).toLowerCase();
37
+ }
38
+
39
+ function toKebabCase(value) {
40
+ return String(value)
41
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
42
+ .toLowerCase();
43
+ }
44
+
45
+ function appendKeys(compactKey, kebabKey, valuePath) {
46
+ resultMap.set(compactKey, valuePath);
47
+ if (kebabKey !== compactKey) {
48
+ resultMap.set(kebabKey, valuePath);
49
+ }
50
+ }
51
+
52
+ function helper(
53
+ currentObj,
54
+ currentCompactKeyPrefix,
55
+ currentKebabKeyPrefix,
56
+ currentValuePrefix,
57
+ ) {
36
58
  for (const key in currentObj) {
59
+ const compactSegment = normalizeKeySegment(key);
60
+ const kebabSegment = toKebabCase(key);
61
+
37
62
  if (
38
63
  currentObj[key] !== null &&
39
64
  typeof currentObj[key] === "object" &&
40
65
  !Array.isArray(currentObj[key])
41
66
  ) {
42
- const newKeyPrefix = currentKeyPrefix
43
- ? currentKeyPrefix + keySeparator + key.toLowerCase()
44
- : key.toLowerCase();
67
+ const newCompactKeyPrefix = currentCompactKeyPrefix
68
+ ? currentCompactKeyPrefix + keySeparator + compactSegment
69
+ : compactSegment;
70
+ const newKebabKeyPrefix = currentKebabKeyPrefix
71
+ ? currentKebabKeyPrefix + keySeparator + kebabSegment
72
+ : kebabSegment;
45
73
  const newValuePrefix = currentValuePrefix
46
74
  ? currentValuePrefix + valueSeparator + key
47
75
  : key;
48
- helper(currentObj[key], newKeyPrefix, newValuePrefix);
76
+ helper(
77
+ currentObj[key],
78
+ newCompactKeyPrefix,
79
+ newKebabKeyPrefix,
80
+ newValuePrefix,
81
+ );
49
82
  } else {
50
- const finalKey = currentKeyPrefix
51
- ? currentKeyPrefix + keySeparator + key.toLowerCase()
52
- : key.toLowerCase();
83
+ const finalCompactKey = currentCompactKeyPrefix
84
+ ? currentCompactKeyPrefix + keySeparator + compactSegment
85
+ : compactSegment;
86
+ const finalKebabKey = currentKebabKeyPrefix
87
+ ? currentKebabKeyPrefix + keySeparator + kebabSegment
88
+ : kebabSegment;
53
89
  const finalValue = currentValuePrefix
54
90
  ? currentValuePrefix + valueSeparator + key
55
91
  : key;
56
- resultMap.set(finalKey, finalValue);
92
+ appendKeys(finalCompactKey, finalKebabKey, finalValue);
57
93
  }
58
94
  }
59
95
  }
60
96
 
61
- helper(obj, keyPrefix, keyPrefix);
97
+ helper(obj, keyPrefix, keyPrefix, keyPrefix);
62
98
  return resultMap;
63
99
  }
@@ -29,8 +29,10 @@ export { initOptionsFromAttributes };
29
29
  /**
30
30
  * Initializes the given options object based on the attributes of the current DOM element.
31
31
  * The function looks for attributes with the prefix 'data-monster-option-', and maps them to
32
- * properties in the options object. It replaces the dashes with dots to form the property path.
33
- * For example, the attribute 'data-monster-option-url' maps to the 'url' property in the options object.
32
+ * properties in the options object. It supports both the historic compact form and kebab-case aliases
33
+ * for camelCase option names.
34
+ * For example, the attributes 'data-monster-option-url' and
35
+ * 'data-monster-option-popper-content-overflow' map to 'url' and 'popper.contentOverflow'.
34
36
  *
35
37
  * With the mapping parameter, the attribute value can be mapped to a different value.
36
38
  * For example, the attribute 'data-monster-option-foo' maps to the 'bar' property in the options object.
@@ -30,8 +30,10 @@ export { setOptionFromAttribute };
30
30
  /**
31
31
  * Set the given options object based on the attributes of the current DOM element.
32
32
  * The function looks for attributes with the prefix 'data-monster-option-', and maps them to
33
- * properties in the options object. It replaces the dashes with dots to form the property path.
34
- * For example, the attribute 'data-monster-option-url' maps to the 'url' property in the options object.
33
+ * properties in the options object. It supports both the historic compact form and kebab-case aliases
34
+ * for camelCase option names.
35
+ * For example, the attributes 'data-monster-option-url' and
36
+ * 'data-monster-option-popper-content-overflow' map to 'url' and 'popper.contentOverflow'.
35
37
  *
36
38
  * With the mapping parameter, the attribute value can be mapped to a different value.
37
39
  * For example, the attribute 'data-monster-option-foo' maps to the 'bar' property in the options object.
@@ -156,7 +156,7 @@ function getMonsterVersion() {
156
156
  }
157
157
 
158
158
  /** don't touch, replaced by make with package.json version */
159
- monsterVersion = new Version("4.128.2");
159
+ monsterVersion = new Version("4.128.3");
160
160
 
161
161
  return monsterVersion;
162
162
  }
@@ -0,0 +1,168 @@
1
+ import * as chai from "chai";
2
+ import { chaiDom } from "../../../util/chai-dom.mjs";
3
+ import { initJSDOM } from "../../../util/jsdom.mjs";
4
+
5
+ let expect = chai.expect;
6
+ chai.use(chaiDom);
7
+
8
+ let Login;
9
+
10
+ describe("Login", function () {
11
+ before(function (done) {
12
+ initJSDOM().then(() => {
13
+ import("element-internals-polyfill")
14
+ .then(() => import("../../../../source/components/form/login.mjs"))
15
+ .then((module) => {
16
+ Login = module.Login;
17
+ done();
18
+ })
19
+ .catch((error) => done(error));
20
+ });
21
+ });
22
+
23
+ beforeEach(() => {
24
+ document.getElementById("mocks").innerHTML = "";
25
+ });
26
+
27
+ afterEach(() => {
28
+ document.getElementById("mocks").innerHTML = "";
29
+ });
30
+
31
+ it("should create a monster-login element", function () {
32
+ expect(document.createElement("monster-login")).is.instanceof(Login);
33
+ });
34
+
35
+ it("should respect the native hidden attribute on the host", function (done) {
36
+ const login = document.createElement("monster-login");
37
+ login.setAttribute("hidden", "");
38
+ document.getElementById("mocks").appendChild(login);
39
+
40
+ setTimeout(() => {
41
+ try {
42
+ expect(login.hidden).to.be.true;
43
+ expect(login.visible).to.be.false;
44
+ expect(login.isVisible).to.be.false;
45
+ done();
46
+ } catch (error) {
47
+ done(error);
48
+ }
49
+ }, 100);
50
+ });
51
+
52
+ it("should hide and show the host with the public visibility API", function (done) {
53
+ const login = document.createElement("monster-login");
54
+ document.getElementById("mocks").appendChild(login);
55
+
56
+ const states = [];
57
+ login.addEventListener("monster-visibility-changed", (event) => {
58
+ states.push(event.detail.visible);
59
+ });
60
+
61
+ setTimeout(() => {
62
+ try {
63
+ const username = login.shadowRoot.querySelector("input[name='username']");
64
+ username.focus();
65
+
66
+ login.hide();
67
+ } catch (error) {
68
+ done(error);
69
+ }
70
+ }, 150);
71
+
72
+ setTimeout(() => {
73
+ try {
74
+ expect(login.hidden).to.be.true;
75
+ expect(login.visible).to.be.false;
76
+
77
+ login.show();
78
+
79
+ setTimeout(() => {
80
+ try {
81
+ expect(login.hidden).to.be.false;
82
+ expect(login.visible).to.be.true;
83
+ expect(states).to.deep.equal([false, true]);
84
+ done();
85
+ } catch (error) {
86
+ done(error);
87
+ }
88
+ }, 25);
89
+ } catch (error) {
90
+ done(error);
91
+ }
92
+ }, 200);
93
+ });
94
+
95
+ it("should keep host visibility separate from openLogin", function (done) {
96
+ const login = document.createElement("monster-login");
97
+ document.getElementById("mocks").appendChild(login);
98
+
99
+ setTimeout(() => {
100
+ try {
101
+ login.hide();
102
+ login.openLogin();
103
+
104
+ expect(login.hidden).to.be.true;
105
+ expect(login.visible).to.be.false;
106
+ done();
107
+ } catch (error) {
108
+ done(error);
109
+ }
110
+ }, 150);
111
+ });
112
+
113
+ it("should show the custom collapse and close the login collapse", function (done) {
114
+ const login = document.createElement("monster-login");
115
+ document.getElementById("mocks").appendChild(login);
116
+
117
+ setTimeout(() => {
118
+ try {
119
+ const loginCollapse = login.shadowRoot.querySelector(
120
+ '[data-monster-role="login-collapse"]',
121
+ );
122
+ const customCollapse = login.shadowRoot.querySelector(
123
+ '[data-monster-role="custom-collapse"]',
124
+ );
125
+
126
+ expect(loginCollapse.isOpen()).to.be.true;
127
+ expect(customCollapse.isClosed()).to.be.true;
128
+
129
+ const button = document.createElement("button");
130
+ button.textContent = "Weiter";
131
+
132
+ login.showCustomCollapse({
133
+ header: "Hinweis",
134
+ content: ["Manueller Inhalt", button],
135
+ footer: "Footer",
136
+ });
137
+
138
+ setTimeout(() => {
139
+ try {
140
+ expect(customCollapse.isOpen()).to.be.true;
141
+ expect(loginCollapse.isClosed()).to.be.true;
142
+ expect(
143
+ login.shadowRoot.getElementById("customCollapseHeader").textContent,
144
+ ).to.equal("Hinweis");
145
+ expect(
146
+ login.shadowRoot
147
+ .getElementById("customCollapseContent")
148
+ .textContent.includes("Manueller Inhalt"),
149
+ ).to.be.true;
150
+ expect(
151
+ login.shadowRoot
152
+ .getElementById("customCollapseContent")
153
+ .querySelector("button"),
154
+ ).to.equal(button);
155
+ expect(
156
+ login.shadowRoot.getElementById("customCollapseFooter").textContent,
157
+ ).to.equal("Footer");
158
+ done();
159
+ } catch (error) {
160
+ done(error);
161
+ }
162
+ }, 700);
163
+ } catch (error) {
164
+ done(error);
165
+ }
166
+ }, 150);
167
+ });
168
+ });