@schukai/monster 4.141.3 → 4.142.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.
@@ -194,8 +194,9 @@ describe("ButtonBar", function () {
194
194
  return originalSetAttribute(name, value);
195
195
  };
196
196
 
197
- resizeObserver.triggerResize([]);
198
- resizeObserver.triggerResize([]);
197
+ const wrapperResizeEntry = { target: wrapper };
198
+ resizeObserver.triggerResize([wrapperResizeEntry]);
199
+ resizeObserver.triggerResize([wrapperResizeEntry]);
199
200
 
200
201
  expect(scheduledCallbacks.length).to.equal(1);
201
202
  await flushFrames();
@@ -93,7 +93,11 @@ let Select,
93
93
  resolveSelectPopperWidthConstraints,
94
94
  resolveSelectVisibleRect,
95
95
  resolveSelectViewportMetrics,
96
- fetchReference;
96
+ fetchReference,
97
+ FLOATING_LAYOUT_REASON,
98
+ cancelFloatingLayout,
99
+ enqueueFloatingLayout,
100
+ flushFloatingLayoutQueueForTests;
97
101
 
98
102
  describe('Select', function () {
99
103
 
@@ -124,6 +128,12 @@ describe('Select', function () {
124
128
  return import("../../../../source/components/form/stylesheet/select.mjs");
125
129
  }).then((m) => {
126
130
  SelectStyleSheet = m['SelectStyleSheet'];
131
+ return import("../../../../source/components/form/util/floating-layout-queue.mjs");
132
+ }).then((m) => {
133
+ FLOATING_LAYOUT_REASON = m['FLOATING_LAYOUT_REASON'];
134
+ cancelFloatingLayout = m['cancelFloatingLayout'];
135
+ enqueueFloatingLayout = m['enqueueFloatingLayout'];
136
+ flushFloatingLayoutQueueForTests = m['flushFloatingLayoutQueueForTests'];
127
137
  done()
128
138
  }).catch(e => done(e))
129
139
 
@@ -138,9 +148,242 @@ describe('Select', function () {
138
148
  : "";
139
149
 
140
150
  expect(cssText).to.contain("--monster-select-container-overflow");
151
+ expect(cssText).to.contain("--monster-select-container-align-self");
152
+ expect(cssText).to.contain("--monster-select-container-block-size");
141
153
  expect(cssText).to.contain("--monster-select-control-padding");
142
154
  expect(cssText).to.contain("--monster-select-selection-margin");
143
155
  expect(cssText).to.contain("--monster-select-selection-flex-wrap");
156
+ expect(cssText.replace(/\s+/g, '')).to.contain('z-index:var(--monster-z-index-popover)');
157
+ });
158
+
159
+ it('should deduplicate floating layout queue jobs by popper element', async function () {
160
+ const popper = document.createElement('div');
161
+ const reasons = [];
162
+ let mutateCount = 0;
163
+
164
+ enqueueFloatingLayout({
165
+ popperElement: popper,
166
+ reason: FLOATING_LAYOUT_REASON.RESIZE,
167
+ mutate: (measurement, reason) => {
168
+ mutateCount += 1;
169
+ reasons.push(reason);
170
+ }
171
+ });
172
+ enqueueFloatingLayout({
173
+ popperElement: popper,
174
+ reason: FLOATING_LAYOUT_REASON.POSITION,
175
+ mutate: (measurement, reason) => {
176
+ mutateCount += 1;
177
+ reasons.push(reason);
178
+ }
179
+ });
180
+
181
+ await flushFloatingLayoutQueueForTests();
182
+
183
+ expect(mutateCount).to.equal(1);
184
+ expect(reasons[0] & FLOATING_LAYOUT_REASON.RESIZE).to.equal(FLOATING_LAYOUT_REASON.RESIZE);
185
+ expect(reasons[0] & FLOATING_LAYOUT_REASON.POSITION).to.equal(FLOATING_LAYOUT_REASON.POSITION);
186
+ });
187
+
188
+ it('should keep independent floating layout queue jobs for different poppers', async function () {
189
+ const firstPopper = document.createElement('div');
190
+ const secondPopper = document.createElement('div');
191
+ const flushed = [];
192
+
193
+ enqueueFloatingLayout({
194
+ popperElement: firstPopper,
195
+ reason: FLOATING_LAYOUT_REASON.RESIZE,
196
+ mutate: () => {
197
+ flushed.push('first');
198
+ }
199
+ });
200
+ enqueueFloatingLayout({
201
+ popperElement: secondPopper,
202
+ reason: FLOATING_LAYOUT_REASON.POSITION,
203
+ mutate: () => {
204
+ flushed.push('second');
205
+ }
206
+ });
207
+
208
+ await flushFloatingLayoutQueueForTests();
209
+
210
+ expect(flushed).to.deep.equal(['first', 'second']);
211
+ });
212
+
213
+ it('should merge position-only floating layout events without dropping pending mutation', async function () {
214
+ const popper = document.createElement('div');
215
+ const flushed = [];
216
+
217
+ enqueueFloatingLayout({
218
+ popperElement: popper,
219
+ reason: FLOATING_LAYOUT_REASON.CONTENT,
220
+ mutate: () => {
221
+ flushed.push('mutate');
222
+ }
223
+ });
224
+ enqueueFloatingLayout({
225
+ popperElement: popper,
226
+ reason: FLOATING_LAYOUT_REASON.POSITION,
227
+ position: () => {
228
+ flushed.push('position');
229
+ }
230
+ });
231
+
232
+ await flushFloatingLayoutQueueForTests();
233
+
234
+ expect(flushed).to.deep.equal(['mutate', 'position']);
235
+ });
236
+
237
+ it('should defer reentrant floating layout queue jobs to a later flush', async function () {
238
+ const popper = document.createElement('div');
239
+ let mutateCount = 0;
240
+
241
+ enqueueFloatingLayout({
242
+ popperElement: popper,
243
+ reason: FLOATING_LAYOUT_REASON.RESIZE,
244
+ mutate: () => {
245
+ mutateCount += 1;
246
+ enqueueFloatingLayout({
247
+ popperElement: popper,
248
+ reason: FLOATING_LAYOUT_REASON.SETTLE,
249
+ mutate: () => {
250
+ mutateCount += 1;
251
+ }
252
+ });
253
+ }
254
+ });
255
+
256
+ await flushFloatingLayoutQueueForTests();
257
+ expect(mutateCount).to.equal(1);
258
+
259
+ await flushFloatingLayoutQueueForTests();
260
+ expect(mutateCount).to.equal(2);
261
+ });
262
+
263
+ it('should flush floating layout queue jobs when requestAnimationFrame stalls', async function () {
264
+ const originalRequestAnimationFrame = global.requestAnimationFrame;
265
+ const originalCancelAnimationFrame = global.cancelAnimationFrame;
266
+ const popper = document.createElement('div');
267
+ let mutateCount = 0;
268
+
269
+ try {
270
+ global.requestAnimationFrame = () => 1;
271
+ global.cancelAnimationFrame = () => {};
272
+
273
+ const promise = enqueueFloatingLayout({
274
+ popperElement: popper,
275
+ reason: FLOATING_LAYOUT_REASON.POSITION,
276
+ mutate: () => {
277
+ mutateCount += 1;
278
+ }
279
+ });
280
+
281
+ await waitForCondition(() => mutateCount === 1);
282
+ await promise;
283
+
284
+ expect(mutateCount).to.equal(1);
285
+ } finally {
286
+ global.requestAnimationFrame = originalRequestAnimationFrame;
287
+ global.cancelAnimationFrame = originalCancelAnimationFrame;
288
+ await flushFloatingLayoutQueueForTests();
289
+ }
290
+ });
291
+
292
+ it('should cancel stalled floating layout queue jobs before the watchdog flushes', async function () {
293
+ const originalRequestAnimationFrame = global.requestAnimationFrame;
294
+ const originalCancelAnimationFrame = global.cancelAnimationFrame;
295
+ const popper = document.createElement('div');
296
+ let mutateCount = 0;
297
+
298
+ try {
299
+ global.requestAnimationFrame = () => 1;
300
+ global.cancelAnimationFrame = () => {};
301
+
302
+ const promise = enqueueFloatingLayout({
303
+ popperElement: popper,
304
+ reason: FLOATING_LAYOUT_REASON.POSITION,
305
+ mutate: () => {
306
+ mutateCount += 1;
307
+ }
308
+ });
309
+
310
+ cancelFloatingLayout(popper);
311
+ await promise;
312
+ await new Promise((resolve) => setTimeout(resolve, 80));
313
+
314
+ expect(mutateCount).to.equal(0);
315
+ } finally {
316
+ global.requestAnimationFrame = originalRequestAnimationFrame;
317
+ global.cancelAnimationFrame = originalCancelAnimationFrame;
318
+ await flushFloatingLayoutQueueForTests();
319
+ }
320
+ });
321
+
322
+ it('should flush reentrant floating layout queue jobs through the watchdog', async function () {
323
+ const originalRequestAnimationFrame = global.requestAnimationFrame;
324
+ const originalCancelAnimationFrame = global.cancelAnimationFrame;
325
+ const popper = document.createElement('div');
326
+ let mutateCount = 0;
327
+
328
+ try {
329
+ global.requestAnimationFrame = () => 1;
330
+ global.cancelAnimationFrame = () => {};
331
+
332
+ enqueueFloatingLayout({
333
+ popperElement: popper,
334
+ reason: FLOATING_LAYOUT_REASON.RESIZE,
335
+ mutate: () => {
336
+ mutateCount += 1;
337
+ if (mutateCount === 1) {
338
+ enqueueFloatingLayout({
339
+ popperElement: popper,
340
+ reason: FLOATING_LAYOUT_REASON.SETTLE,
341
+ mutate: () => {
342
+ mutateCount += 1;
343
+ }
344
+ });
345
+ }
346
+ }
347
+ });
348
+
349
+ await waitForCondition(() => mutateCount === 2);
350
+
351
+ expect(mutateCount).to.equal(2);
352
+ } finally {
353
+ global.requestAnimationFrame = originalRequestAnimationFrame;
354
+ global.cancelAnimationFrame = originalCancelAnimationFrame;
355
+ await flushFloatingLayoutQueueForTests();
356
+ }
357
+ });
358
+
359
+ it('should flush many independent floating layout queue jobs in one stalled frame', async function () {
360
+ const originalRequestAnimationFrame = global.requestAnimationFrame;
361
+ const originalCancelAnimationFrame = global.cancelAnimationFrame;
362
+ const poppers = Array.from({length: 8}, () => document.createElement('div'));
363
+ const flushed = [];
364
+
365
+ try {
366
+ global.requestAnimationFrame = () => 1;
367
+ global.cancelAnimationFrame = () => {};
368
+
369
+ for (const [index, popper] of poppers.entries()) {
370
+ enqueueFloatingLayout({
371
+ popperElement: popper,
372
+ reason: FLOATING_LAYOUT_REASON.POSITION,
373
+ mutate: () => {
374
+ flushed.push(index);
375
+ }
376
+ });
377
+ }
378
+
379
+ await waitForCondition(() => flushed.length === poppers.length);
380
+
381
+ expect(flushed).to.deep.equal([0, 1, 2, 3, 4, 5, 6, 7]);
382
+ } finally {
383
+ global.requestAnimationFrame = originalRequestAnimationFrame;
384
+ global.cancelAnimationFrame = originalCancelAnimationFrame;
385
+ await flushFloatingLayoutQueueForTests();
386
+ }
144
387
  });
145
388
 
146
389
  describe('With fetch', function () {
@@ -279,6 +522,144 @@ describe('Select', function () {
279
522
  mocks.innerHTML = "";
280
523
  });
281
524
 
525
+ it('should render the popper outside of the internal control clipping container', function () {
526
+ const mocks = document.getElementById('mocks');
527
+ const select = document.createElement('monster-select');
528
+
529
+ select.setOption('options', [
530
+ {label: 'Alpha', value: 'alpha'},
531
+ {label: 'Beta', value: 'beta'}
532
+ ]);
533
+
534
+ mocks.appendChild(select);
535
+
536
+ const shadowRoot = select.shadowRoot;
537
+ const control = shadowRoot.querySelector('[data-monster-role=control]');
538
+ const popper = shadowRoot.querySelector('[data-monster-role=popper]');
539
+
540
+ expect(control).to.exist;
541
+ expect(popper).to.exist;
542
+ expect(control.contains(popper)).to.equal(false);
543
+ expect(popper.parentNode).to.equal(shadowRoot);
544
+ });
545
+
546
+ it('should open in a constrained filter bar without a control overflow override', function (done) {
547
+ const mocks = document.getElementById('mocks');
548
+ const filterBar = document.createElement('div');
549
+ const select = document.createElement('monster-select');
550
+
551
+ filterBar.style.display = 'grid';
552
+ filterBar.style.gridTemplateColumns = 'minmax(0, 1fr)';
553
+ filterBar.style.height = '44px';
554
+ filterBar.style.overflow = 'hidden';
555
+
556
+ select.setOption('options', [
557
+ {label: 'Alpha', value: 'alpha'},
558
+ {label: 'Beta', value: 'beta'}
559
+ ]);
560
+
561
+ filterBar.appendChild(select);
562
+ mocks.appendChild(filterBar);
563
+
564
+ const shadowRoot = select.shadowRoot;
565
+ const control = shadowRoot.querySelector('[data-monster-role=control]');
566
+ const popper = shadowRoot.querySelector('[data-monster-role=popper]');
567
+
568
+ control.style.overflow = 'hidden';
569
+ control.getBoundingClientRect = () => ({
570
+ width: 180,
571
+ height: 40,
572
+ top: 100,
573
+ left: 40,
574
+ right: 220,
575
+ bottom: 140,
576
+ x: 40,
577
+ y: 100
578
+ });
579
+
580
+ setTimeout(() => {
581
+ try {
582
+ shadowRoot.querySelector('[data-monster-role=container]').click();
583
+ setTimeout(() => {
584
+ try {
585
+ expect(control.style.overflow).to.equal('hidden');
586
+ expect(control.contains(popper)).to.equal(false);
587
+ expect(popper.style.display).to.equal('block');
588
+ done();
589
+ } catch (e) {
590
+ done(e);
591
+ }
592
+ }, 80);
593
+ } catch (e) {
594
+ done(e);
595
+ }
596
+ }, 20);
597
+ });
598
+
599
+ it('should preserve an immediate open intent while configured options render', async function () {
600
+ const mocks = document.getElementById('mocks');
601
+ const select = document.createElement('monster-select');
602
+
603
+ mocks.appendChild(select);
604
+ select.setOption('options', [
605
+ {label: 'Alpha', value: 'alpha'},
606
+ {label: 'Beta', value: 'beta'}
607
+ ]);
608
+
609
+ const shadowRoot = select.shadowRoot;
610
+ const container = shadowRoot.querySelector('[data-monster-role=container]');
611
+ const popper = shadowRoot.querySelector('[data-monster-role=popper]');
612
+
613
+ container.click();
614
+
615
+ await waitForCondition(() => {
616
+ return shadowRoot.querySelectorAll('[data-monster-role=option]').length === 2;
617
+ });
618
+ await waitForCondition(() => {
619
+ return popper.style.display === 'block';
620
+ });
621
+
622
+ expect(popper.style.display).to.equal('block');
623
+ });
624
+
625
+ it('should keep an empty select without popper filter closed', async function () {
626
+ const mocks = document.getElementById('mocks');
627
+ const select = document.createElement('monster-select');
628
+
629
+ mocks.appendChild(select);
630
+
631
+ const shadowRoot = select.shadowRoot;
632
+ const container = shadowRoot.querySelector('[data-monster-role=container]');
633
+ const popper = shadowRoot.querySelector('[data-monster-role=popper]');
634
+
635
+ container.click();
636
+
637
+ await new Promise((resolve) => setTimeout(resolve, 160));
638
+
639
+ expect(popper.style.display).to.not.equal('block');
640
+ });
641
+
642
+ it('should still open an empty select with a popper filter', async function () {
643
+ const mocks = document.getElementById('mocks');
644
+ const select = document.createElement('monster-select');
645
+
646
+ select.setOption('filter.mode', 'options');
647
+ select.setOption('filter.position', 'popper');
648
+ mocks.appendChild(select);
649
+
650
+ const shadowRoot = select.shadowRoot;
651
+ const container = shadowRoot.querySelector('[data-monster-role=container]');
652
+ const popper = shadowRoot.querySelector('[data-monster-role=popper]');
653
+
654
+ container.click();
655
+
656
+ await waitForCondition(() => {
657
+ return popper.style.display === 'block';
658
+ });
659
+
660
+ expect(popper.style.display).to.equal('block');
661
+ });
662
+
282
663
  it('should allow the popper to become wider than a narrow control', function (done) {
283
664
  const mocks = document.getElementById('mocks');
284
665
  const select = document.createElement('monster-select');
@@ -370,6 +751,109 @@ describe('Select', function () {
370
751
  }, 100);
371
752
  });
372
753
 
754
+ it('should keep the control bar popper width stable while layout observers settle', function (done) {
755
+ const mocks = document.getElementById('mocks');
756
+ mocks.innerHTML = '<monster-control-bar id="stable-select-bar"></monster-control-bar>';
757
+
758
+ const bar = document.getElementById('stable-select-bar');
759
+ const select = document.createElement('monster-select');
760
+
761
+ select.setOption('options', [
762
+ {label: 'Alpha', value: 'alpha'},
763
+ {label: 'Beta', value: 'beta'}
764
+ ]);
765
+
766
+ bar.appendChild(select);
767
+
768
+ const shadowRoot = select.shadowRoot;
769
+ const control = shadowRoot.querySelector('[data-monster-role=control]');
770
+ const container = shadowRoot.querySelector('[data-monster-role=container]');
771
+ const popper = shadowRoot.querySelector('[data-monster-role=popper]');
772
+ let measuredWidth = 528;
773
+
774
+ const getRect = () => ({
775
+ width: measuredWidth,
776
+ height: 40,
777
+ top: 100,
778
+ left: 40,
779
+ right: 40 + measuredWidth,
780
+ bottom: 140,
781
+ x: 40,
782
+ y: 100
783
+ });
784
+
785
+ control.getBoundingClientRect = getRect;
786
+ select.getBoundingClientRect = getRect;
787
+
788
+ setTimeout(() => {
789
+ try {
790
+ container.click();
791
+ setTimeout(() => {
792
+ try {
793
+ expect(popper.style.display).to.equal('block');
794
+ expect(popper.dataset.monsterPreferredWidth).to.equal('528');
795
+ expect(popper.style.width).to.equal('528px');
796
+ expect(select.style.flex).to.equal('0 0 528px');
797
+
798
+ measuredWidth = 279;
799
+ select.calcAndSetOptionsDimension();
800
+
801
+ expect(popper.dataset.monsterPreferredWidth).to.equal('528');
802
+ expect(popper.style.width).to.equal('528px');
803
+
804
+ container.click();
805
+
806
+ setTimeout(() => {
807
+ try {
808
+ expect(popper.style.display).to.equal('none');
809
+ expect(select.style.flex).to.equal('');
810
+ expect(select.style.inlineSize).to.equal('');
811
+ expect(select.style.minInlineSize).to.equal('');
812
+ done();
813
+ } catch (e) {
814
+ done(e);
815
+ }
816
+ }, 80);
817
+ } catch (e) {
818
+ done(e);
819
+ }
820
+ }, 80);
821
+ } catch (e) {
822
+ done(e);
823
+ }
824
+ }, 100);
825
+ });
826
+
827
+ it('should use the same sibling popper structure for derived selects', function () {
828
+ const tagName = 'issue-460-derived-select';
829
+ const mocks = document.getElementById('mocks');
830
+
831
+ if (!customElements.get(tagName)) {
832
+ customElements.define(tagName, class Issue460DerivedSelect extends Select {
833
+ static getTag() {
834
+ return tagName;
835
+ }
836
+ });
837
+ }
838
+
839
+ const select = document.createElement(tagName);
840
+ select.setOption('options', [
841
+ {label: 'Alpha', value: 'alpha'},
842
+ {label: 'Beta', value: 'beta'}
843
+ ]);
844
+
845
+ mocks.appendChild(select);
846
+
847
+ const shadowRoot = select.shadowRoot;
848
+ const control = shadowRoot.querySelector('[data-monster-role=control]');
849
+ const popper = shadowRoot.querySelector('[data-monster-role=popper]');
850
+
851
+ expect(control).to.exist;
852
+ expect(popper).to.exist;
853
+ expect(control.contains(popper)).to.equal(false);
854
+ expect(popper.parentNode).to.equal(shadowRoot);
855
+ });
856
+
373
857
  it('should keep the option list height tight for short lists', function () {
374
858
  const result = resolveSelectListDimension({
375
859
  visibleOptionHeights: [28, 28],
@@ -1572,6 +2056,9 @@ describe('Select', function () {
1572
2056
 
1573
2057
  expect(select.getOption('total')).to.equal(0);
1574
2058
  expect(select.getOption('messages.total')).to.equal('');
2059
+ await waitForCondition(() => {
2060
+ return select.getOption('messages.emptyOptions').includes('Please consider modifying the filter');
2061
+ });
1575
2062
  expect(select.getOption('messages.emptyOptions')).to.contain('Please consider modifying the filter');
1576
2063
  });
1577
2064