@schukai/monster 4.139.1 → 4.140.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.
@@ -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;
@@ -61,6 +61,90 @@ let htmlOverflow = `
61
61
  </monster-tabs>
62
62
  `;
63
63
 
64
+ // language=html
65
+ let htmlAvailability = `
66
+ <monster-tabs id="availability-tabs">
67
+ <div id="overview-panel" data-monster-name="overview" data-monster-button-label="Overview">
68
+ Overview content
69
+ </div>
70
+ <div id="vacation-panel" data-monster-name="vacation" data-monster-button-label="Vacation" data-monster-tab-available="false">
71
+ Vacation content
72
+ </div>
73
+ <div id="details-panel" data-monster-name="details" data-monster-button-label="Details">
74
+ Details content
75
+ </div>
76
+ </monster-tabs>
77
+ `;
78
+
79
+ // language=html
80
+ let htmlActiveUnavailable = `
81
+ <monster-tabs id="fallback-tabs">
82
+ <div id="first-panel" data-monster-name="first" data-monster-button-label="First">
83
+ First content
84
+ </div>
85
+ <div id="second-panel" data-monster-name="second" data-monster-button-label="Second" class="active">
86
+ Second content
87
+ </div>
88
+ <div id="third-panel" data-monster-name="third" data-monster-button-label="Third">
89
+ Third content
90
+ </div>
91
+ </monster-tabs>
92
+ `;
93
+
94
+ // language=html
95
+ let htmlRemoveFallback = `
96
+ <monster-tabs id="remove-fallback-tabs" data-monster-options='{"features":{"openFirst":false}}'>
97
+ <div id="remove-first-panel" data-monster-name="first" data-monster-button-label="First">
98
+ First content
99
+ </div>
100
+ <div id="remove-second-panel" data-monster-name="second" data-monster-button-label="Second" class="active">
101
+ Second content
102
+ </div>
103
+ <div id="remove-third-panel" data-monster-name="third" data-monster-button-label="Third">
104
+ Third content
105
+ </div>
106
+ </monster-tabs>
107
+ `;
108
+
109
+ // language=html
110
+ let htmlDisabled = `
111
+ <monster-tabs id="disabled-tabs">
112
+ <div id="enabled-panel" data-monster-name="enabled" data-monster-button-label="Enabled">
113
+ Enabled content
114
+ </div>
115
+ <div id="disabled-panel"
116
+ data-monster-name="disabled"
117
+ data-monster-button-label="Disabled"
118
+ data-monster-tab-disabled="true"
119
+ data-monster-tab-disabled-reason="Only available for employees">
120
+ Disabled content
121
+ </div>
122
+ </monster-tabs>
123
+ `;
124
+
125
+ // language=html
126
+ let htmlMetadata = `
127
+ <monster-tabs id="metadata-tabs">
128
+ <div id="metadata-panel"
129
+ data-monster-name="metadata"
130
+ data-monster-button-label="Metadata"
131
+ data-monster-tab-kind="profile"
132
+ data-monster-tab-priority="10"
133
+ data-monster-tab-group="main">
134
+ Metadata content
135
+ </div>
136
+ </monster-tabs>
137
+ `;
138
+
139
+ // language=html
140
+ let htmlNoDerivedLabel = `
141
+ <monster-tabs id="label-tabs" data-monster-options='{"features":{"deriveLabelFromContent":false}}'>
142
+ <div id="content-panel">
143
+ Overview about your profile and vacation configuration
144
+ </div>
145
+ </monster-tabs>
146
+ `;
147
+
64
148
  let Tabs;
65
149
  let restoreBoundingClientRect = null;
66
150
  let restoreResizeObserver = null;
@@ -407,6 +491,245 @@ describe('Tabs', function () {
407
491
  });
408
492
  });
409
493
 
494
+ it('should ignore unavailable panels when creating tabs and buttons', function (done) {
495
+ let mocks = document.getElementById('mocks');
496
+ mocks.innerHTML = htmlAvailability;
497
+
498
+ waitForCondition(() => {
499
+ const tabs = document.getElementById('availability-tabs');
500
+ return (
501
+ tabs instanceof Tabs &&
502
+ tabs.shadowRoot.querySelectorAll('button[part=button]').length === 2 &&
503
+ tabs.getActiveTab() === 'overview'
504
+ );
505
+ }).then(() => {
506
+ try {
507
+ const tabs = document.getElementById('availability-tabs');
508
+ const vacation = document.getElementById('vacation-panel');
509
+ const buttons = tabs.shadowRoot.querySelectorAll('button[part=button]');
510
+
511
+ expect(tabs.getTabs().length).to.equal(2);
512
+ expect(tabs.getTabs().includes(vacation)).to.equal(false);
513
+ expect(tabs.shadowRoot.querySelector(
514
+ `button[part=button][data-monster-tab-reference="${vacation.getAttribute('id')}"]`,
515
+ )).to.equal(null);
516
+ expect(buttons[0].textContent.trim()).to.equal('Overview');
517
+ expect(buttons[1].textContent.trim()).to.equal('Details');
518
+ expect(tabs.getActiveTab()).to.equal('overview');
519
+ done();
520
+ } catch (e) {
521
+ done(e);
522
+ }
523
+ }).catch(done);
524
+ });
525
+
526
+ it('should ignore unavailable tabs in activeTab and rebuild when they become available', function (done) {
527
+ let mocks = document.getElementById('mocks');
528
+ mocks.innerHTML = htmlAvailability;
529
+
530
+ waitForCondition(() => {
531
+ const tabs = document.getElementById('availability-tabs');
532
+ return tabs instanceof Tabs && tabs.getActiveTab() === 'overview';
533
+ }).then(() => {
534
+ const tabs = document.getElementById('availability-tabs');
535
+ const vacation = document.getElementById('vacation-panel');
536
+ let availabilityEvent = null;
537
+ tabs.addEventListener('monster-tab-availability-changed', (event) => {
538
+ availabilityEvent = event;
539
+ });
540
+
541
+ tabs.activeTab('vacation');
542
+ expect(tabs.getActiveTab()).to.equal('overview');
543
+
544
+ vacation.setAttribute('data-monster-tab-available', 'true');
545
+
546
+ return waitForCondition(() => {
547
+ return tabs.shadowRoot.querySelectorAll('button[part=button]').length === 3;
548
+ }).then(() => {
549
+ try {
550
+ expect(availabilityEvent).to.not.equal(null);
551
+ expect(availabilityEvent.detail.name).to.equal('vacation');
552
+ expect(availabilityEvent.detail.available).to.equal(true);
553
+
554
+ tabs.activeTab('vacation');
555
+ expect(tabs.getActiveTab()).to.equal('vacation');
556
+ done();
557
+ } catch (e) {
558
+ done(e);
559
+ }
560
+ }, 100);
561
+ }).catch(done);
562
+ });
563
+
564
+ it('should fallback when the active tab becomes unavailable', function (done) {
565
+ let mocks = document.getElementById('mocks');
566
+ mocks.innerHTML = htmlActiveUnavailable;
567
+
568
+ waitForCondition(() => {
569
+ const tabs = document.getElementById('fallback-tabs');
570
+ return tabs instanceof Tabs && tabs.getActiveTab() === 'second';
571
+ }).then(() => {
572
+ const tabs = document.getElementById('fallback-tabs');
573
+ const activePanel = document.getElementById('second-panel');
574
+ activePanel.setAttribute('data-monster-tab-available', 'false');
575
+
576
+ return waitForCondition(() => tabs.getActiveTab() === 'first').then(() => {
577
+ try {
578
+ expect(activePanel.classList.contains('active')).to.equal(false);
579
+ expect(tabs.shadowRoot.querySelectorAll('button[part=button]').length).to.equal(2);
580
+ done();
581
+ } catch (e) {
582
+ done(e);
583
+ }
584
+ });
585
+ }).catch(done);
586
+ });
587
+
588
+ it('should fallback when the active tab is removed directly', function (done) {
589
+ let mocks = document.getElementById('mocks');
590
+ mocks.innerHTML = htmlRemoveFallback;
591
+
592
+ waitForCondition(() => {
593
+ const tabs = document.getElementById('remove-fallback-tabs');
594
+ return (
595
+ tabs instanceof Tabs &&
596
+ tabs.getActiveTab() === 'second' &&
597
+ tabs.shadowRoot.querySelectorAll('button[part=button]').length === 3
598
+ );
599
+ }).then(() => {
600
+ const tabs = document.getElementById('remove-fallback-tabs');
601
+ setTimeout(() => {
602
+ document.getElementById('remove-second-panel').remove();
603
+
604
+ waitForCondition(() => tabs.getActiveTab() === 'first').then(() => {
605
+ try {
606
+ expect(tabs.shadowRoot.querySelectorAll('button[part=button]').length).to.equal(2);
607
+ done();
608
+ } catch (e) {
609
+ done(e);
610
+ }
611
+ }).catch(done);
612
+ });
613
+ }).catch(done);
614
+ });
615
+
616
+ it('should keep disabled tabs visible but not activatable', function (done) {
617
+ let mocks = document.getElementById('mocks');
618
+ mocks.innerHTML = htmlDisabled;
619
+
620
+ waitForCondition(() => {
621
+ const tabs = document.getElementById('disabled-tabs');
622
+ const disabledPanel = document.getElementById('disabled-panel');
623
+ const disabledButton = tabs?.shadowRoot?.querySelector(
624
+ `button[part=button][data-monster-tab-reference="${disabledPanel?.getAttribute('id')}"]`,
625
+ );
626
+ return (
627
+ tabs instanceof Tabs &&
628
+ disabledButton instanceof HTMLButtonElement &&
629
+ tabs.getActiveTab() === 'enabled'
630
+ );
631
+ }).then(() => {
632
+ try {
633
+ const tabs = document.getElementById('disabled-tabs');
634
+ const disabledPanel = document.getElementById('disabled-panel');
635
+ const disabledButton = tabs.shadowRoot.querySelector(
636
+ `button[part=button][data-monster-tab-reference="${disabledPanel.getAttribute('id')}"]`,
637
+ );
638
+
639
+ expect(disabledButton.disabled).to.equal(true);
640
+ expect(disabledButton.getAttribute('title')).to.equal('Only available for employees');
641
+ tabs.activeTab('disabled');
642
+ expect(tabs.getActiveTab()).to.equal('enabled');
643
+ done();
644
+ } catch (e) {
645
+ done(e);
646
+ }
647
+ }).catch(done);
648
+ });
649
+
650
+ it('should expose refresh, sync, hide and show APIs', function (done) {
651
+ let mocks = document.getElementById('mocks');
652
+ mocks.innerHTML = htmlAvailability;
653
+
654
+ waitForCondition(() => {
655
+ const tabs = document.getElementById('availability-tabs');
656
+ return tabs instanceof Tabs && tabs.shadowRoot.querySelectorAll('button[part=button]').length === 2;
657
+ }).then(() => {
658
+ const tabs = document.getElementById('availability-tabs');
659
+
660
+ tabs.showTab('vacation');
661
+ return tabs.refreshTabs().then((result) => {
662
+ expect(result).to.equal(tabs);
663
+ expect(tabs.getTabs().length).to.equal(3);
664
+
665
+ tabs.hideTab('vacation');
666
+ return tabs.syncTabs();
667
+ }).then((result) => {
668
+ try {
669
+ expect(result).to.equal(tabs);
670
+ expect(tabs.getTabs().length).to.equal(2);
671
+ expect(document.getElementById('vacation-panel').getAttribute('data-monster-tab-available')).to.equal('false');
672
+ done();
673
+ } catch (e) {
674
+ done(e);
675
+ }
676
+ });
677
+ }).catch(done);
678
+ });
679
+
680
+ it('should not derive labels from content when disabled by option', function (done) {
681
+ let mocks = document.getElementById('mocks');
682
+ mocks.innerHTML = htmlNoDerivedLabel;
683
+
684
+ waitForCondition(() => {
685
+ const tabs = document.getElementById('label-tabs');
686
+ return tabs instanceof Tabs && tabs.shadowRoot.querySelector('button[part=button]') !== null;
687
+ }).then(() => {
688
+ try {
689
+ const tabs = document.getElementById('label-tabs');
690
+ const button = tabs.shadowRoot.querySelector('button[part=button]');
691
+ expect(button.textContent.trim()).to.equal('New Tab');
692
+ expect(document.getElementById('content-panel').hasAttribute('data-monster-button-label')).to.equal(false);
693
+ done();
694
+ } catch (e) {
695
+ done(e);
696
+ }
697
+ }).catch(done);
698
+ });
699
+
700
+ it('should include stable names and metadata in tab events and buttons', function (done) {
701
+ let mocks = document.getElementById('mocks');
702
+ mocks.innerHTML = htmlMetadata;
703
+
704
+ waitForCondition(() => {
705
+ const tabs = document.getElementById('metadata-tabs');
706
+ return tabs instanceof Tabs && tabs.shadowRoot.querySelector('button[part=button]') !== null;
707
+ }).then(() => {
708
+ try {
709
+ const tabs = document.getElementById('metadata-tabs');
710
+ const button = tabs.shadowRoot.querySelector('button[part=button]');
711
+ let changedEvent = null;
712
+ tabs.addEventListener('monster-tab-changed', (event) => {
713
+ changedEvent = event;
714
+ });
715
+
716
+ expect(button.getAttribute('data-monster-tab-name')).to.equal('metadata');
717
+ expect(button.getAttribute('data-monster-tab-kind')).to.equal('profile');
718
+ expect(button.getAttribute('data-monster-tab-priority')).to.equal('10');
719
+ expect(button.getAttribute('data-monster-tab-group')).to.equal('main');
720
+
721
+ tabs.activeTab('metadata');
722
+ expect(changedEvent).to.not.equal(null);
723
+ expect(changedEvent.detail.name).to.equal('metadata');
724
+ expect(changedEvent.detail.tab).to.equal('metadata');
725
+ expect(changedEvent.detail.metadata.kind).to.equal('profile');
726
+ done();
727
+ } catch (e) {
728
+ done(e);
729
+ }
730
+ }).catch(done);
731
+ });
732
+
410
733
  });
411
734
 
412
735