@schukai/monster 4.136.4 → 4.136.6

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.
@@ -33,7 +33,27 @@ let html2 = `
33
33
  </div>
34
34
  `;
35
35
 
36
- let Select, fetchReference;
36
+ function createJsonResponse(data, status = 200) {
37
+ let headers = new Map();
38
+ headers.set('content-type', 'application/json');
39
+
40
+ return Promise.resolve({
41
+ ok: status >= 200 && status < 300,
42
+ status,
43
+ headers,
44
+ text: function () {
45
+ return Promise.resolve(JSON.stringify(data));
46
+ }
47
+ });
48
+ }
49
+
50
+ let Select,
51
+ getDefaultSelectPopperPositionProfile,
52
+ resolveSelectListDimension,
53
+ resolveSelectPopperWidthConstraints,
54
+ resolveSelectVisibleRect,
55
+ resolveSelectViewportMetrics,
56
+ fetchReference;
37
57
 
38
58
  describe('Select', function () {
39
59
 
@@ -50,6 +70,11 @@ describe('Select', function () {
50
70
 
51
71
  import("../../../../source/components/form/select.mjs").then((m) => {
52
72
  Select = m['Select'];
73
+ getDefaultSelectPopperPositionProfile = m['getDefaultSelectPopperPositionProfile'];
74
+ resolveSelectListDimension = m['resolveSelectListDimension'];
75
+ resolveSelectPopperWidthConstraints = m['resolveSelectPopperWidthConstraints'];
76
+ resolveSelectVisibleRect = m['resolveSelectVisibleRect'];
77
+ resolveSelectViewportMetrics = m['resolveSelectViewportMetrics'];
53
78
  done()
54
79
  }).catch(e => done(e))
55
80
 
@@ -185,6 +210,191 @@ describe('Select', function () {
185
210
 
186
211
  });
187
212
 
213
+ describe('Popper sizing', function () {
214
+ this.timeout(5000);
215
+
216
+ afterEach(() => {
217
+ let mocks = document.getElementById('mocks');
218
+ mocks.innerHTML = "";
219
+ });
220
+
221
+ it('should allow the popper to become wider than a narrow control', function (done) {
222
+ const mocks = document.getElementById('mocks');
223
+ const select = document.createElement('monster-select');
224
+
225
+ select.setOption('options', [
226
+ {label: 'Alpha', value: 'alpha'},
227
+ {label: 'Beta', value: 'beta'}
228
+ ]);
229
+
230
+ mocks.appendChild(select);
231
+
232
+ const shadowRoot = select.shadowRoot;
233
+ const control = shadowRoot.querySelector('[data-monster-role=control]');
234
+ const popper = shadowRoot.querySelector('[data-monster-role=popper]');
235
+
236
+ control.getBoundingClientRect = () => ({
237
+ width: 120,
238
+ height: 36,
239
+ top: 100,
240
+ left: 40,
241
+ right: 160,
242
+ bottom: 136,
243
+ x: 40,
244
+ y: 100
245
+ });
246
+
247
+ setTimeout(() => {
248
+ try {
249
+ shadowRoot.querySelector('[data-monster-role=container]').click();
250
+ setTimeout(() => {
251
+ try {
252
+ expect(popper.style.minWidth).to.equal('');
253
+ expect(popper.dataset.monsterPreferredWidth).to.equal('240');
254
+ done();
255
+ } catch (e) {
256
+ done(e);
257
+ }
258
+ }, 80);
259
+ } catch (e) {
260
+ done(e);
261
+ }
262
+ }, 20);
263
+ });
264
+
265
+ it('should keep the option list height tight for short lists', function () {
266
+ const result = resolveSelectListDimension({
267
+ visibleOptionHeights: [28, 28],
268
+ maxVisibleOptions: 20,
269
+ availableHeight: 500,
270
+ padding: 0,
271
+ margin: 0
272
+ });
273
+
274
+ expect(result.desiredHeight).to.equal(56);
275
+ expect(result.maxHeight).to.equal(500);
276
+ expect(result.overflowY).to.equal('hidden');
277
+ });
278
+
279
+ it('should refresh the content max height when the available popper height grows again', function (done) {
280
+ const mocks = document.getElementById('mocks');
281
+ const select = document.createElement('monster-select');
282
+
283
+ select.setOption('options', Array.from({length: 24}, (_, index) => ({
284
+ label: `Option ${index + 1}`,
285
+ value: `${index + 1}`
286
+ })));
287
+
288
+ mocks.appendChild(select);
289
+
290
+ const shadowRoot = select.shadowRoot;
291
+ const control = shadowRoot.querySelector('[data-monster-role=control]');
292
+ const popper = shadowRoot.querySelector('[data-monster-role=popper]');
293
+ const content = shadowRoot.querySelector('[part=content]');
294
+ const options = shadowRoot.querySelector('[data-monster-role=options]');
295
+
296
+ control.getBoundingClientRect = () => ({
297
+ width: 200,
298
+ height: 36,
299
+ top: 100,
300
+ left: 40,
301
+ right: 240,
302
+ bottom: 136,
303
+ x: 40,
304
+ y: 100
305
+ });
306
+
307
+ setTimeout(() => {
308
+ try {
309
+ shadowRoot.querySelector('[data-monster-role=container]').click();
310
+ setTimeout(() => {
311
+ try {
312
+ content.style.maxHeight = '20px';
313
+ options.style.height = '20px';
314
+ options.style.maxHeight = '20px';
315
+
316
+ select.calcAndSetOptionsDimension();
317
+
318
+ expect(parseFloat(content.style.maxHeight)).to.be.greaterThan(20);
319
+ expect(content.style.maxHeight).to.equal(options.style.maxHeight);
320
+ expect(parseFloat(popper.style.maxHeight)).to.be.at.least(parseFloat(content.style.maxHeight));
321
+ done();
322
+ } catch (e) {
323
+ done(e);
324
+ }
325
+ }, 80);
326
+ } catch (e) {
327
+ done(e);
328
+ }
329
+ }, 20);
330
+ });
331
+
332
+ it('should clamp preferred width to the visible viewport width', function () {
333
+ const result = resolveSelectPopperWidthConstraints({
334
+ controlWidth: 120,
335
+ availableWidth: 176
336
+ });
337
+
338
+ expect(result.preferredWidth).to.equal(240);
339
+ expect(result.maxWidth).to.equal(176);
340
+ });
341
+
342
+ it('should keep start placement while enabling cross-axis shifting', function () {
343
+ const result = getDefaultSelectPopperPositionProfile();
344
+
345
+ expect(result.placement).to.equal('bottom-start');
346
+ expect(result.middleware).to.deep.equal([
347
+ 'flip',
348
+ 'offset:4',
349
+ 'shift:crossAxis',
350
+ 'size'
351
+ ]);
352
+ });
353
+
354
+ it('should prefer the larger live viewport metrics after a resize', function () {
355
+ const result = resolveSelectViewportMetrics({
356
+ layoutWidth: 1400,
357
+ layoutHeight: 900,
358
+ visualWidth: 1024,
359
+ visualHeight: 700,
360
+ offsetLeft: 20,
361
+ offsetTop: 30,
362
+ padding: 12
363
+ });
364
+
365
+ expect(result.width).to.equal(1400);
366
+ expect(result.height).to.equal(900);
367
+ expect(result.left).to.equal(20);
368
+ expect(result.top).to.equal(30);
369
+ expect(result.padding).to.equal(12);
370
+ });
371
+
372
+ it('should intersect viewport and split container for the visible rect', function () {
373
+ const result = resolveSelectVisibleRect({
374
+ viewportMetrics: {
375
+ width: 1400,
376
+ height: 900,
377
+ left: 0,
378
+ top: 0,
379
+ padding: 12
380
+ },
381
+ boundaryRect: {
382
+ left: 100,
383
+ top: 80,
384
+ right: 620,
385
+ bottom: 700
386
+ }
387
+ });
388
+
389
+ expect(result.left).to.equal(100);
390
+ expect(result.top).to.equal(80);
391
+ expect(result.right).to.equal(620);
392
+ expect(result.bottom).to.equal(700);
393
+ expect(result.width).to.equal(520);
394
+ expect(result.height).to.equal(620);
395
+ });
396
+ });
397
+
188
398
  describe('Remote filter pagination', function () {
189
399
  let requestCount = 0;
190
400
 
@@ -528,25 +738,286 @@ describe('Select', function () {
528
738
 
529
739
  setTimeout(() => {
530
740
  try {
531
- const observer = mockIntersectionObserver.getInstance();
532
- observer.enterNode();
741
+ expect(lookupCount).to.equal(0);
533
742
  } catch (e) {
534
743
  mockIntersectionObserver.restore();
535
744
  return done(e);
536
745
  }
537
746
 
747
+ mockIntersectionObserver.restore();
748
+ done();
749
+ }, 150);
750
+ });
751
+
752
+ it('should not refetch after selecting an already loaded remote option', function (done) {
753
+ this.timeout(3000);
754
+
755
+ let mocks = document.getElementById('mocks');
756
+ const requests = [];
757
+
758
+ global['fetch'] = function (url) {
759
+ requests.push(String(url));
760
+ return createJsonResponse({
761
+ items: [
762
+ {id: 'alpha', name: 'Alpha'}
763
+ ],
764
+ pagination: {
765
+ total: 1,
766
+ page: 1,
767
+ perPage: 1
768
+ }
769
+ });
770
+ };
771
+
772
+ const select = document.createElement('monster-select');
773
+ select.setOption('url', 'https://example.com/items?filter={filter}&page={page}');
774
+ select.setOption('filter.mode', 'remote');
775
+ select.setOption('filter.defaultValue', '*');
776
+ select.setOption('mapping.selector', 'items.*');
777
+ select.setOption('mapping.labelTemplate', '${name}');
778
+ select.setOption('mapping.valueTemplate', '${id}');
779
+ select.setOption('mapping.total', 'pagination.total');
780
+ select.setOption('mapping.currentPage', 'pagination.page');
781
+ select.setOption('mapping.objectsPerPage', 'pagination.perPage');
782
+ mocks.appendChild(select);
783
+
784
+ setTimeout(() => {
785
+ select.fetch('https://example.com/items?filter=*&page=1')
786
+ .then(() => {
787
+ try {
788
+ expect(requests).to.deep.equal([
789
+ 'https://example.com/items?filter=*&page=1'
790
+ ]);
791
+
792
+ select.value = 'alpha';
793
+ } catch (e) {
794
+ return done(e);
795
+ }
796
+
797
+ setTimeout(() => {
798
+ try {
799
+ expect(requests).to.deep.equal([
800
+ 'https://example.com/items?filter=*&page=1'
801
+ ]);
802
+ } catch (e) {
803
+ return done(e);
804
+ }
805
+
806
+ done();
807
+ }, 200);
808
+ })
809
+ .catch((e) => done(e));
810
+ }, 50);
811
+ });
812
+
813
+ it('should reuse defaultOptionsUrl after clearing the filter and reopening', function (done) {
814
+ this.timeout(5000);
815
+
816
+ let mocks = document.getElementById('mocks');
817
+ const requests = [];
818
+
819
+ global['fetch'] = function (url) {
820
+ const requestUrl = String(url);
821
+ requests.push(requestUrl);
822
+
823
+ if (requestUrl.indexOf('/defaults') !== -1) {
824
+ return createJsonResponse({
825
+ items: [
826
+ {id: 'seed', name: 'Seed'}
827
+ ],
828
+ pagination: {
829
+ total: 1,
830
+ page: 1,
831
+ perPage: 1
832
+ }
833
+ });
834
+ }
835
+
836
+ return createJsonResponse({
837
+ items: [
838
+ {id: 'match', name: 'Match'}
839
+ ],
840
+ pagination: {
841
+ total: 1,
842
+ page: 1,
843
+ perPage: 1
844
+ }
845
+ });
846
+ };
847
+
848
+ const select = document.createElement('monster-select');
849
+ select.setOption('url', 'https://example.com/items?filter={filter}&page={page}');
850
+ select.setOption('filter.mode', 'remote');
851
+ select.setOption('filter.position', 'popper');
852
+ select.setOption('filter.defaultOptionsUrl', 'https://example.com/defaults?page={page}');
853
+ select.setOption('mapping.selector', 'items.*');
854
+ select.setOption('mapping.labelTemplate', '${name}');
855
+ select.setOption('mapping.valueTemplate', '${id}');
856
+ select.setOption('mapping.total', 'pagination.total');
857
+ select.setOption('mapping.currentPage', 'pagination.page');
858
+ select.setOption('mapping.objectsPerPage', 'pagination.perPage');
859
+ mocks.appendChild(select);
860
+
861
+ const dispatchFilterKey = (input, code, key) => {
862
+ input.dispatchEvent(new KeyboardEvent('keydown', {
863
+ bubbles: true,
864
+ composed: true,
865
+ code,
866
+ key
867
+ }));
868
+ };
869
+
870
+ setTimeout(() => {
871
+ let container;
872
+ let filterInput;
873
+
874
+ try {
875
+ container = select.shadowRoot.querySelector('[data-monster-role=container]');
876
+ filterInput = select.shadowRoot.querySelector('[data-monster-role=filter][name="popper-filter"]');
877
+ container.click();
878
+ } catch (e) {
879
+ return done(e);
880
+ }
881
+
538
882
  setTimeout(() => {
539
883
  try {
540
- expect(lookupCount).to.equal(0);
884
+ expect(requests[0]).to.equal('https://example.com/defaults?page=1');
885
+
886
+ filterInput.value = 'alpha';
887
+ dispatchFilterKey(filterInput, 'KeyA', 'a');
888
+ } catch (e) {
889
+ return done(e);
890
+ }
891
+
892
+ setTimeout(() => {
893
+ try {
894
+ expect(requests[1]).to.equal('https://example.com/items?filter=alpha&page=1');
895
+
896
+ filterInput.value = '';
897
+ dispatchFilterKey(filterInput, 'Backspace', 'Backspace');
898
+ } catch (e) {
899
+ return done(e);
900
+ }
901
+
902
+ setTimeout(() => {
903
+ try {
904
+ expect(requests[2]).to.equal('https://example.com/defaults?page=1');
905
+
906
+ container.click();
907
+ } catch (e) {
908
+ return done(e);
909
+ }
910
+
911
+ setTimeout(() => {
912
+ try {
913
+ container.click();
914
+ } catch (e) {
915
+ return done(e);
916
+ }
917
+
918
+ setTimeout(() => {
919
+ try {
920
+ expect(requests[3]).to.equal('https://example.com/defaults?page=1');
921
+ } catch (e) {
922
+ return done(e);
923
+ }
924
+
925
+ done();
926
+ }, 250);
927
+ }, 50);
928
+ }, 300);
929
+ }, 300);
930
+ }, 250);
931
+ }, 50);
932
+ });
933
+
934
+ it('should keep unresolved lookup values visible and mark their badge', function (done) {
935
+ this.timeout(3000);
936
+
937
+ let mocks = document.getElementById('mocks');
938
+ global['fetch'] = function () {
939
+ return createJsonResponse({
940
+ items: [],
941
+ pagination: {
942
+ total: 0,
943
+ page: 1,
944
+ perPage: 1
945
+ }
946
+ });
947
+ };
948
+
949
+ const select = document.createElement('monster-select');
950
+ select.setOption('lookup.url', 'https://example.com/items?filter={filter}');
951
+ select.setOption('mapping.selector', 'items.*');
952
+ select.setOption('mapping.labelTemplate', '${name}');
953
+ select.setOption('mapping.valueTemplate', '${id}');
954
+ select.setOption('selection', [{value: 'missing-key'}]);
955
+ mocks.appendChild(select);
956
+
957
+ setTimeout(() => {
958
+ try {
959
+ const badge = select.shadowRoot.querySelector('[data-monster-role=badge]');
960
+ const badgeLabel = select.shadowRoot.querySelector('[data-monster-role=badge-label]');
961
+
962
+ expect(badge).to.be.instanceof(HTMLDivElement);
963
+ expect(badgeLabel).to.be.instanceof(HTMLDivElement);
964
+ expect(badgeLabel.textContent.trim()).to.equal('missing-key');
965
+ expect(badge.className).to.contain('monster-badge-warning');
966
+ expect(badge.className).to.not.contain('monster-badge-primary');
967
+ expect(badge.getAttribute('data-monster-unresolved')).to.equal('true');
968
+ expect(badgeLabel.textContent).to.not.contain(select.getOption('labels.cannot-be-loaded'));
969
+ } catch (e) {
970
+ return done(e);
971
+ }
972
+
973
+ done();
974
+ }, 250);
975
+ });
976
+
977
+ it('should clear the unresolved badge state once a local option can resolve the value', function (done) {
978
+ this.timeout(4000);
979
+
980
+ let mocks = document.getElementById('mocks');
981
+ global['fetch'] = function () {
982
+ return createJsonResponse({
983
+ items: [],
984
+ pagination: {
985
+ total: 0,
986
+ page: 1,
987
+ perPage: 1
988
+ }
989
+ });
990
+ };
991
+
992
+ const select = document.createElement('monster-select');
993
+ select.setOption('lookup.url', 'https://example.com/items?filter={filter}');
994
+ select.setOption('mapping.selector', 'items.*');
995
+ select.setOption('mapping.labelTemplate', '${name}');
996
+ select.setOption('mapping.valueTemplate', '${id}');
997
+ select.setOption('selection', [{value: 'missing-key'}]);
998
+ mocks.appendChild(select);
999
+
1000
+ setTimeout(() => {
1001
+ select.setOption('options', [{label: 'Resolved Label', value: 'missing-key'}]);
1002
+ select.value = 'missing-key';
1003
+
1004
+ setTimeout(() => {
1005
+ try {
1006
+ const badge = select.shadowRoot.querySelector('[data-monster-role=badge]');
1007
+ const badgeLabel = select.shadowRoot.querySelector('[data-monster-role=badge-label]');
1008
+
1009
+ expect(badge).to.be.instanceof(HTMLDivElement);
1010
+ expect(badgeLabel.textContent.trim()).to.equal('Resolved Label');
1011
+ expect(badge.className).to.contain('monster-badge-primary');
1012
+ expect(badge.className).to.not.contain('monster-badge-warning');
1013
+ expect(badge.getAttribute('data-monster-unresolved')).to.not.equal('true');
541
1014
  } catch (e) {
542
- mockIntersectionObserver.restore();
543
1015
  return done(e);
544
1016
  }
545
1017
 
546
- mockIntersectionObserver.restore();
547
1018
  done();
548
1019
  }, 150);
549
- }, 50);
1020
+ }, 250);
550
1021
  });
551
1022
 
552
1023
  it('should not throw when IntersectionObserver is unavailable', function (done) {