@schukai/monster 4.139.0 → 4.140.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,6 +25,10 @@ export function attachTabsHashSync(
25
25
  let lastKnownActiveTabId = null;
26
26
  let lastKnownAllTabIds = [];
27
27
 
28
+ function getTabKey(tab) {
29
+ return tab.getAttribute("data-monster-name") || tab.getAttribute("id");
30
+ }
31
+
28
32
  /**
29
33
  * Reads active and all tab IDs from the URL hash.
30
34
  * @returns {{activeTabId: string|null, allTabIds: string[]}}
@@ -53,7 +57,7 @@ export function attachTabsHashSync(
53
57
  }
54
58
 
55
59
  // Sync all tabs (add/remove tabs based on hash)
56
- const currentTabs = tabsEl.getTabs().map((tab) => tab.getAttribute("id"));
60
+ const currentTabs = tabsEl.getTabs().map(getTabKey);
57
61
 
58
62
  // Add tabs that are in hash but not in DOM
59
63
  for (const tabId of allTabIds) {
@@ -120,8 +124,9 @@ export function attachTabsHashSync(
120
124
  // Listen for tab changes (active tab)
121
125
  tabsEl.addEventListener("monster-tab-changed", (e) => {
122
126
  if (e.target !== tabsEl) return; // Ignore bubbled events
123
- const newActiveTabId = e.detail?.reference;
124
- const currentTabs = tabsEl.getTabs().map((tab) => tab.getAttribute("id"));
127
+ const newActiveTabId =
128
+ e.detail?.tab || e.detail?.name || e.detail?.reference;
129
+ const currentTabs = tabsEl.getTabs().map(getTabKey);
125
130
  writeHash(newActiveTabId, currentTabs);
126
131
  });
127
132
 
@@ -154,7 +159,7 @@ export function attachTabsHashSync(
154
159
  }
155
160
  if (tabsChanged) {
156
161
  const currentActiveTabId = tabsEl.getActiveTab();
157
- const currentTabs = tabsEl.getTabs().map((tab) => tab.getAttribute("id"));
162
+ const currentTabs = tabsEl.getTabs().map(getTabKey);
158
163
  // Only update hash if the list of tabs has actually changed
159
164
  if (
160
165
  currentTabs.length !== lastKnownAllTabIds.length ||
@@ -170,6 +175,6 @@ export function attachTabsHashSync(
170
175
 
171
176
  // Initial write of all existing tabs to the hash
172
177
  const initialActiveTab = tabsEl.getActiveTab();
173
- const initialTabs = tabsEl.getTabs().map((tab) => tab.getAttribute("id"));
178
+ const initialTabs = tabsEl.getTabs().map(getTabKey);
174
179
  writeHash(initialActiveTab, initialTabs);
175
180
  }
@@ -55,6 +55,7 @@ export * from "./components/form/util/floating-ui.mjs";
55
55
  export * from "./components/form/context-help.mjs";
56
56
  export * from "./components/form/api-bar.mjs";
57
57
  export * from "./components/form/tabs.mjs";
58
+ export * from "./components/form/control-bar-spacer.mjs";
58
59
  export * from "./components/form/state-button.mjs";
59
60
  export * from "./components/form/popper.mjs";
60
61
  export * from "./components/form/cart-control.mjs";
@@ -69,6 +70,7 @@ export * from "./components/form/context-warning.mjs";
69
70
  export * from "./components/form/context-note.mjs";
70
71
  export * from "./components/form/context-error.mjs";
71
72
  export * from "./components/form/variant-select.mjs";
73
+ export * from "./components/form/choice-cards.mjs";
72
74
  export * from "./components/form/register-wizard.mjs";
73
75
  export * from "./components/form/action-button.mjs";
74
76
  export * from "./components/form/form.mjs";
@@ -83,7 +85,6 @@ export * from "./components/form/credential-button.mjs";
83
85
  export * from "./components/form/wizard.mjs";
84
86
  export * from "./components/form/input-group.mjs";
85
87
  export * from "./components/form/checklist.mjs";
86
- export * from "./components/form/choice-cards.mjs";
87
88
  export * from "./components/form/shadow-reload.mjs";
88
89
  export * from "./components/form/button.mjs";
89
90
  export * from "./components/form/field-set.mjs";
@@ -0,0 +1,51 @@
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 ControlBarSpacer;
9
+
10
+ describe("ControlBarSpacer", function () {
11
+ before(function (done) {
12
+ initJSDOM()
13
+ .then(() =>
14
+ import("../../../../source/components/form/control-bar-spacer.mjs"),
15
+ )
16
+ .then((m) => {
17
+ ControlBarSpacer = m.ControlBarSpacer;
18
+ done();
19
+ })
20
+ .catch((e) => done(e));
21
+ });
22
+
23
+ it("should create a control bar spacer element", function () {
24
+ const spacer = document.createElement("monster-control-bar-spacer");
25
+
26
+ expect(spacer).to.be.instanceof(ControlBarSpacer);
27
+ });
28
+
29
+ it("should render a non-interactive separator line", function () {
30
+ const spacer = document.createElement("monster-control-bar-spacer");
31
+ document.getElementById("mocks").appendChild(spacer);
32
+
33
+ const separator = spacer.shadowRoot.querySelector(
34
+ '[data-monster-role="separator"]',
35
+ );
36
+
37
+ expect(separator).to.be.instanceof(HTMLDivElement);
38
+ expect(spacer.getAttribute("aria-hidden")).to.equal("true");
39
+ });
40
+
41
+ it("should define spacer dimensions through css variables", function () {
42
+ const cssText = ControlBarSpacer.getCSSStyleSheet()
43
+ .flatMap((styleSheet) => Array.from(styleSheet.cssRules))
44
+ .map((rule) => rule.cssText)
45
+ .join("\n");
46
+
47
+ expect(cssText).to.contain("--monster-control-bar-spacer-inline-size");
48
+ expect(cssText).to.contain("--monster-control-bar-spacer-block-size");
49
+ expect(cssText).to.contain("pointer-events: none");
50
+ });
51
+ });
@@ -27,9 +27,12 @@ describe("ControlBar", function () {
27
27
  this.timeout(5000);
28
28
  initJSDOM()
29
29
  .then(() => {
30
- import("../../../../source/components/form/control-bar.mjs")
30
+ Promise.all([
31
+ import("../../../../source/components/form/control-bar.mjs"),
32
+ import("../../../../source/components/form/control-bar-spacer.mjs"),
33
+ ])
31
34
  .then((m) => {
32
- ControlBar = m.ControlBar;
35
+ ControlBar = m[0].ControlBar;
33
36
  done();
34
37
  })
35
38
  .catch((e) => done(e));
@@ -126,6 +129,13 @@ describe("ControlBar", function () {
126
129
  expect(cssText).to.contain("appearance: none");
127
130
  expect(cssText).to.contain("--monster-select-container-overflow: hidden");
128
131
  expect(cssText).to.contain("::slotted(monster-input-group)");
132
+ expect(cssText).to.contain("::slotted(monster-control-bar-spacer)");
133
+ expect(cssText).to.contain(
134
+ "--monster-control-bar-spacer-line-block-size: 60%",
135
+ );
136
+ expect(cssText).to.contain(
137
+ "--monster-control-bar-spacer-line-inline-size: calc(100% - 1rem)",
138
+ );
129
139
  });
130
140
 
131
141
  it("should join adjacent borders using the smaller computed border width", async function () {
@@ -301,6 +311,175 @@ describe("ControlBar", function () {
301
311
  }
302
312
  });
303
313
 
314
+ it("should size inline spacer lines from adjacent border widths", async function () {
315
+ const originalRequestAnimationFrame = window.requestAnimationFrame;
316
+ const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
317
+
318
+ const scheduledCallbacks = [];
319
+ const flushFrames = async () => {
320
+ while (scheduledCallbacks.length > 0) {
321
+ scheduledCallbacks.shift()();
322
+ await new Promise((resolve) => setTimeout(resolve, 0));
323
+ }
324
+ };
325
+
326
+ try {
327
+ window.requestAnimationFrame = (callback) => {
328
+ scheduledCallbacks.push(callback);
329
+ return scheduledCallbacks.length;
330
+ };
331
+ globalThis.requestAnimationFrame = window.requestAnimationFrame;
332
+
333
+ const mocks = document.getElementById("mocks");
334
+ mocks.innerHTML = `
335
+ <div id="spacer-border-bar-wrapper">
336
+ <monster-control-bar id="spacer-border-bar">
337
+ <button id="spacer-border-left">A</button>
338
+ <monster-control-bar-spacer id="spacer-border-spacer"></monster-control-bar-spacer>
339
+ <input id="spacer-border-right">
340
+ </monster-control-bar>
341
+ </div>
342
+ `;
343
+
344
+ const wrapper = document.getElementById("spacer-border-bar-wrapper");
345
+ const left = document.getElementById("spacer-border-left");
346
+ const spacer = document.getElementById("spacer-border-spacer");
347
+ const right = document.getElementById("spacer-border-right");
348
+
349
+ wrapper.style.boxSizing = "border-box";
350
+ wrapper.style.width = "300px";
351
+ Object.defineProperty(wrapper, "clientWidth", {
352
+ configurable: true,
353
+ value: 300,
354
+ });
355
+
356
+ for (const control of [left, spacer, right]) {
357
+ Object.defineProperty(control, "offsetWidth", {
358
+ configurable: true,
359
+ value: 40,
360
+ });
361
+ Object.defineProperty(control, "offsetHeight", {
362
+ configurable: true,
363
+ value: 30,
364
+ });
365
+ control.getBoundingClientRect = () => ({
366
+ width: 40,
367
+ height: 30,
368
+ top: 0,
369
+ right: 40,
370
+ bottom: 30,
371
+ left: 0,
372
+ x: 0,
373
+ y: 0,
374
+ toJSON: () => {},
375
+ });
376
+ }
377
+
378
+ left.style.borderRightWidth = "4px";
379
+ right.style.borderLeftWidth = "2px";
380
+
381
+ await flushFrames();
382
+ await new Promise((resolve) => setTimeout(resolve, 0));
383
+ await new Promise((resolve) => setTimeout(resolve, 0));
384
+
385
+ expect(
386
+ spacer.style.getPropertyValue(
387
+ "--monster-control-bar-spacer-line-inline-size",
388
+ ),
389
+ ).to.equal("4px");
390
+ expect(spacer.style.marginLeft).to.equal("");
391
+ expect(right.style.marginLeft).to.equal("");
392
+ } finally {
393
+ window.requestAnimationFrame = originalRequestAnimationFrame;
394
+ globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
395
+ }
396
+ });
397
+
398
+ it("should size popper spacer lines from adjacent border widths", async function () {
399
+ const originalRequestAnimationFrame = window.requestAnimationFrame;
400
+ const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
401
+
402
+ const scheduledCallbacks = [];
403
+ const flushFrames = async () => {
404
+ while (scheduledCallbacks.length > 0) {
405
+ scheduledCallbacks.shift()();
406
+ await new Promise((resolve) => setTimeout(resolve, 0));
407
+ }
408
+ };
409
+
410
+ try {
411
+ window.requestAnimationFrame = (callback) => {
412
+ scheduledCallbacks.push(callback);
413
+ return scheduledCallbacks.length;
414
+ };
415
+ globalThis.requestAnimationFrame = window.requestAnimationFrame;
416
+
417
+ const mocks = document.getElementById("mocks");
418
+ mocks.innerHTML = `
419
+ <div id="popper-spacer-border-bar-wrapper">
420
+ <monster-control-bar id="popper-spacer-border-bar">
421
+ <button id="popper-spacer-border-top">A</button>
422
+ <monster-control-bar-spacer id="popper-spacer-border-spacer"></monster-control-bar-spacer>
423
+ <input id="popper-spacer-border-bottom">
424
+ </monster-control-bar>
425
+ </div>
426
+ `;
427
+
428
+ const wrapper = document.getElementById("popper-spacer-border-bar-wrapper");
429
+ const top = document.getElementById("popper-spacer-border-top");
430
+ const spacer = document.getElementById("popper-spacer-border-spacer");
431
+ const bottom = document.getElementById("popper-spacer-border-bottom");
432
+
433
+ wrapper.style.boxSizing = "border-box";
434
+ wrapper.style.width = "1px";
435
+ Object.defineProperty(wrapper, "clientWidth", {
436
+ configurable: true,
437
+ value: 1,
438
+ });
439
+
440
+ for (const control of [top, spacer, bottom]) {
441
+ Object.defineProperty(control, "offsetWidth", {
442
+ configurable: true,
443
+ value: 40,
444
+ });
445
+ Object.defineProperty(control, "offsetHeight", {
446
+ configurable: true,
447
+ value: 30,
448
+ });
449
+ control.getBoundingClientRect = () => ({
450
+ width: 40,
451
+ height: 30,
452
+ top: 0,
453
+ right: 40,
454
+ bottom: 30,
455
+ left: 0,
456
+ x: 0,
457
+ y: 0,
458
+ toJSON: () => {},
459
+ });
460
+ }
461
+
462
+ top.style.borderBottomWidth = "2px";
463
+ bottom.style.borderTopWidth = "5px";
464
+
465
+ await flushFrames();
466
+ await new Promise((resolve) => setTimeout(resolve, 0));
467
+ await new Promise((resolve) => setTimeout(resolve, 0));
468
+
469
+ expect(spacer.getAttribute("slot")).to.equal("popper");
470
+ expect(
471
+ spacer.style.getPropertyValue(
472
+ "--monster-control-bar-spacer-line-block-size",
473
+ ),
474
+ ).to.equal("5px");
475
+ expect(spacer.style.marginTop).to.equal("");
476
+ expect(bottom.style.marginTop).to.equal("");
477
+ } finally {
478
+ window.requestAnimationFrame = originalRequestAnimationFrame;
479
+ globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
480
+ }
481
+ });
482
+
304
483
  it("should move overflowing mixed controls into the popper", async function () {
305
484
  const OriginalResizeObserver = window.ResizeObserver;
306
485
  const originalGlobalResizeObserver = globalThis.ResizeObserver;
@@ -110,6 +110,8 @@ describe('Select', function () {
110
110
 
111
111
  import("../../../../source/components/host/host.mjs").then(() => {
112
112
  return import("../../../../source/components/form/popper-button.mjs");
113
+ }).then(() => {
114
+ return import("../../../../source/components/form/control-bar.mjs");
113
115
  }).then(() => {
114
116
  return import("../../../../source/components/form/select.mjs");
115
117
  }).then((m) => {
@@ -322,6 +324,52 @@ describe('Select', function () {
322
324
  }, 20);
323
325
  });
324
326
 
327
+ it('should use fixed positioning inside a control bar', function (done) {
328
+ const mocks = document.getElementById('mocks');
329
+ mocks.innerHTML = '<monster-control-bar id="select-bar"></monster-control-bar>';
330
+
331
+ const bar = document.getElementById('select-bar');
332
+ const select = document.createElement('monster-select');
333
+
334
+ select.setOption('options', [
335
+ {label: 'Alpha', value: 'alpha'},
336
+ {label: 'Beta', value: 'beta'}
337
+ ]);
338
+
339
+ bar.appendChild(select);
340
+
341
+ const shadowRoot = select.shadowRoot;
342
+ const control = shadowRoot.querySelector('[data-monster-role=control]');
343
+ const popper = shadowRoot.querySelector('[data-monster-role=popper]');
344
+
345
+ control.getBoundingClientRect = () => ({
346
+ width: 120,
347
+ height: 40,
348
+ top: 100,
349
+ left: 40,
350
+ right: 160,
351
+ bottom: 140,
352
+ x: 40,
353
+ y: 100
354
+ });
355
+
356
+ setTimeout(() => {
357
+ try {
358
+ shadowRoot.querySelector('[data-monster-role=container]').click();
359
+ setTimeout(() => {
360
+ try {
361
+ expect(popper.style.position).to.equal('fixed');
362
+ done();
363
+ } catch (e) {
364
+ done(e);
365
+ }
366
+ }, 80);
367
+ } catch (e) {
368
+ done(e);
369
+ }
370
+ }, 100);
371
+ });
372
+
325
373
  it('should keep the option list height tight for short lists', function () {
326
374
  const result = resolveSelectListDimension({
327
375
  visibleOptionHeights: [28, 28],