@schukai/monster 4.150.0 → 4.150.2

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 (67) hide show
  1. package/package.json +1 -1
  2. package/source/components/accessibility/locale-picker.mjs +22 -14
  3. package/source/components/accessibility/locale-select.mjs +2 -0
  4. package/source/components/content/stylesheet/camera-capture.mjs +1 -1
  5. package/source/components/content/stylesheet/copy.mjs +1 -1
  6. package/source/components/content/viewer/message.mjs +22 -0
  7. package/source/components/content/viewer/stylesheet/message.mjs +1 -1
  8. package/source/components/content/viewer.mjs +6 -6
  9. package/source/components/datatable/datatable.mjs +139 -7
  10. package/source/components/datatable/filter/date-range.mjs +168 -3
  11. package/source/components/datatable/filter/input.mjs +47 -3
  12. package/source/components/datatable/filter/range.mjs +91 -3
  13. package/source/components/datatable/filter-button.mjs +47 -3
  14. package/source/components/datatable/filter.mjs +11 -0
  15. package/source/components/datatable/save-button.mjs +47 -3
  16. package/source/components/datatable/style/datatable.pcss +8 -0
  17. package/source/components/datatable/stylesheet/datatable.mjs +1 -1
  18. package/source/components/datatable/stylesheet/filter-controls-defaults.mjs +1 -1
  19. package/source/components/files/file-manager.mjs +96 -13
  20. package/source/components/form/buy-box.mjs +87 -87
  21. package/source/components/form/cart-control.mjs +24 -24
  22. package/source/components/form/control-bar.mjs +26 -6
  23. package/source/components/form/credential-button.mjs +13 -0
  24. package/source/components/form/field-set.mjs +80 -3
  25. package/source/components/form/login.mjs +143 -96
  26. package/source/components/form/register-wizard.mjs +298 -298
  27. package/source/components/form/select.mjs +103 -79
  28. package/source/components/form/style/button-bar.pcss +17 -1
  29. package/source/components/form/style/control-bar.pcss +6 -0
  30. package/source/components/form/style/mixin/field-set-layout.pcss +2 -0
  31. package/source/components/form/style/toggle-switch.pcss +4 -0
  32. package/source/components/form/stylesheet/button-bar.mjs +6 -4
  33. package/source/components/form/stylesheet/confirm-button.mjs +1 -1
  34. package/source/components/form/stylesheet/context-error.mjs +1 -1
  35. package/source/components/form/stylesheet/context-help.mjs +1 -1
  36. package/source/components/form/stylesheet/control-bar.mjs +3 -3
  37. package/source/components/form/stylesheet/digits.mjs +1 -1
  38. package/source/components/form/stylesheet/field-set.mjs +2 -2
  39. package/source/components/form/stylesheet/login.mjs +1 -1
  40. package/source/components/form/stylesheet/popper-button.mjs +1 -1
  41. package/source/components/form/stylesheet/repeat-field-set-items.mjs +1 -1
  42. package/source/components/form/stylesheet/select.mjs +1 -1
  43. package/source/components/form/stylesheet/toggle-switch.mjs +2 -2
  44. package/source/components/form/variant-select.mjs +17 -17
  45. package/source/components/form/wizard.mjs +20 -20
  46. package/source/components/layout/details.mjs +47 -3
  47. package/source/components/layout/stylesheet/popper.mjs +1 -1
  48. package/source/components/layout/tabs.mjs +65 -10
  49. package/source/components/layout/vertical-tabs.mjs +65 -10
  50. package/source/components/style/floating-ui.css +13 -0
  51. package/source/components/style/floating-ui.pcss +4 -0
  52. package/source/components/stylesheet/floating-ui.mjs +1 -1
  53. package/source/components/time/month-calendar.mjs +75 -2
  54. package/source/i18n/internal.mjs +60 -18
  55. package/source/i18n/map/languages.mjs +2 -1
  56. package/source/i18n/time-ago.mjs +129 -0
  57. package/test/cases/components/datatable/datasource-rest-lifecycle.mjs +9 -1
  58. package/test/cases/components/datatable/mobile-layout.mjs +13 -0
  59. package/test/cases/components/form/button-bar.mjs +21 -0
  60. package/test/cases/components/form/control-bar.mjs +123 -0
  61. package/test/cases/components/form/field-set.mjs +11 -0
  62. package/test/cases/components/form/locale-defaults.mjs +58 -0
  63. package/test/cases/components/form/popper-button.mjs +12 -0
  64. package/test/cases/components/form/register-wizard.mjs +15 -0
  65. package/test/cases/components/form/toggle-switch.mjs +13 -0
  66. package/test/cases/i18n/built-in-locales.mjs +151 -0
  67. package/test/cases/i18n/time-ago.mjs +52 -32
@@ -207,11 +207,84 @@ function getTranslations() {
207
207
  switch (locale.language) {
208
208
  case "de":
209
209
  return {
210
- more: "weitere Termin",
210
+ more: "Weitere Termine",
211
211
  };
212
+ case "es":
213
+ return {
214
+ more: "Más citas",
215
+ };
216
+ case "zh":
217
+ return {
218
+ more: "更多日程",
219
+ };
220
+ case "hi":
221
+ return {
222
+ more: "और अपॉइंटमेंट",
223
+ };
224
+ case "bn":
225
+ return {
226
+ more: "আরও অ্যাপয়েন্টমেন্ট",
227
+ };
228
+ case "pt":
229
+ return {
230
+ more: "Mais compromissos",
231
+ };
232
+ case "ru":
233
+ return {
234
+ more: "Другие события",
235
+ };
236
+ case "ja":
237
+ return {
238
+ more: "その他の予定",
239
+ };
240
+ case "pa":
241
+ return {
242
+ more: "ਹੋਰ ਮੁਲਾਕਾਤਾਂ",
243
+ };
244
+ case "mr":
245
+ return {
246
+ more: "आणखी भेटी",
247
+ };
248
+ case "fr":
249
+ return {
250
+ more: "Autres rendez-vous",
251
+ };
252
+ case "it":
253
+ return {
254
+ more: "Altri appuntamenti",
255
+ };
256
+ case "nl":
257
+ return {
258
+ more: "Meer afspraken",
259
+ };
260
+ case "sv":
261
+ return {
262
+ more: "Fler möten",
263
+ };
264
+ case "pl":
265
+ return {
266
+ more: "Więcej spotkań",
267
+ };
268
+ case "da":
269
+ return {
270
+ more: "Flere aftaler",
271
+ };
272
+ case "fi":
273
+ return {
274
+ more: "Lisää tapaamisia",
275
+ };
276
+ case "no":
277
+ return {
278
+ more: "Flere avtaler",
279
+ };
280
+ case "cs":
281
+ return {
282
+ more: "Další události",
283
+ };
284
+ case "en":
212
285
  default:
213
286
  return {
214
- more: "more appointments",
287
+ more: "More appointments",
215
288
  };
216
289
  }
217
290
  }
@@ -41,72 +41,114 @@ function getInternalTranslations() {
41
41
  case "de":
42
42
  messages = {
43
43
  "the-response-does-not-contain-an-accepted-status": `Der Server hat die Anfrage nicht akzeptiert (Status \${status}).`,
44
- "the-response-does-not-contain-a-valid-json": `Die Antwort des Servers ist kein gültiges JSON (actual=\${actual}).`,
44
+ "the-response-does-not-contain-a-valid-json": `Die Antwort des Servers ist kein gültiges JSON (tatsächlich=\${actual}).`,
45
45
  };
46
46
  break;
47
47
  case "es":
48
48
  messages = {
49
- "the-response-does-not-contain-an-accepted-status": `El servidor no ha aceptado la solicitud (Estado \${status}).`,
50
- "the-response-does-not-contain-a-valid-json": `La respuesta del servidor no es un JSON válido (actual=\${actual}).`,
49
+ "the-response-does-not-contain-an-accepted-status": `El servidor no ha aceptado la solicitud (estado \${status}).`,
50
+ "the-response-does-not-contain-a-valid-json": `La respuesta del servidor no es un JSON válido (real=\${actual}).`,
51
51
  };
52
52
  break;
53
53
  case "zh":
54
54
  messages = {
55
55
  "the-response-does-not-contain-an-accepted-status": `服务器未接受请求(状态 \${status})。`,
56
- "the-response-does-not-contain-a-valid-json": `服务器响应不是有效的JSON(实际=\${actual})。`,
56
+ "the-response-does-not-contain-a-valid-json": `服务器响应不是有效的 JSON(实际=\${actual})。`,
57
+ };
58
+ break;
59
+ case "hi":
60
+ messages = {
61
+ "the-response-does-not-contain-an-accepted-status": `सर्वर ने अनुरोध स्वीकार नहीं किया (स्थिति \${status})।`,
62
+ "the-response-does-not-contain-a-valid-json": `सर्वर की प्रतिक्रिया मान्य JSON नहीं है (वास्तविक=\${actual})।`,
63
+ };
64
+ break;
65
+ case "bn":
66
+ messages = {
67
+ "the-response-does-not-contain-an-accepted-status": `সার্ভার অনুরোধটি গ্রহণ করেনি (স্থিতি \${status})।`,
68
+ "the-response-does-not-contain-a-valid-json": `সার্ভারের প্রতিক্রিয়াটি বৈধ JSON নয় (প্রকৃত=\${actual})।`,
69
+ };
70
+ break;
71
+ case "pt":
72
+ messages = {
73
+ "the-response-does-not-contain-an-accepted-status": `O servidor não aceitou a solicitação (status \${status}).`,
74
+ "the-response-does-not-contain-a-valid-json": `A resposta do servidor não é um JSON válido (real=\${actual}).`,
75
+ };
76
+ break;
77
+ case "ru":
78
+ messages = {
79
+ "the-response-does-not-contain-an-accepted-status": `Сервер не принял запрос (статус \${status}).`,
80
+ "the-response-does-not-contain-a-valid-json": `Ответ сервера не является допустимым JSON (фактическое значение=\${actual}).`,
81
+ };
82
+ break;
83
+ case "ja":
84
+ messages = {
85
+ "the-response-does-not-contain-an-accepted-status": `サーバーがリクエストを受け付けませんでした(ステータス \${status})。`,
86
+ "the-response-does-not-contain-a-valid-json": `サーバーの応答は有効な JSON ではありません(実際=\${actual})。`,
87
+ };
88
+ break;
89
+ case "pa":
90
+ messages = {
91
+ "the-response-does-not-contain-an-accepted-status": `ਸਰਵਰ ਨੇ ਬੇਨਤੀ ਸਵੀਕਾਰ ਨਹੀਂ ਕੀਤੀ (ਸਥਿਤੀ \${status})।`,
92
+ "the-response-does-not-contain-a-valid-json": `ਸਰਵਰ ਦਾ ਜਵਾਬ ਵੈਧ JSON ਨਹੀਂ ਹੈ (ਅਸਲ=\${actual})।`,
93
+ };
94
+ break;
95
+ case "mr":
96
+ messages = {
97
+ "the-response-does-not-contain-an-accepted-status": `सर्व्हरने विनंती स्वीकारली नाही (स्थिती \${status}).`,
98
+ "the-response-does-not-contain-a-valid-json": `सर्व्हरचा प्रतिसाद वैध JSON नाही (प्रत्यक्ष=\${actual}).`,
57
99
  };
58
100
  break;
59
101
  case "fr":
60
102
  messages = {
61
- "the-response-does-not-contain-an-accepted-status": `Le serveur n'a pas accepté la demande (Statut \${status}).`,
62
- "the-response-does-not-contain-a-valid-json": `La réponse du serveur n'est pas un JSON valide (actuel=\${actual}).`,
103
+ "the-response-does-not-contain-an-accepted-status": `Le serveur n'a pas accepté la demande (statut \${status}).`,
104
+ "the-response-does-not-contain-a-valid-json": `La réponse du serveur n'est pas un JSON valide (réel=\${actual}).`,
63
105
  };
64
106
  break;
65
107
  case "it":
66
108
  messages = {
67
- "the-response-does-not-contain-an-accepted-status": `Il server non ha accettato la richiesta (Stato \${status}).`,
68
- "the-response-does-not-contain-a-valid-json": `La risposta del server non è un JSON valido (attuale=\${actual}).`,
109
+ "the-response-does-not-contain-an-accepted-status": `Il server non ha accettato la richiesta (stato \${status}).`,
110
+ "the-response-does-not-contain-a-valid-json": `La risposta del server non è un JSON valido (effettivo=\${actual}).`,
69
111
  };
70
112
  break;
71
113
  case "nl":
72
114
  messages = {
73
- "the-response-does-not-contain-an-accepted-status": `De server heeft het verzoek niet geaccepteerd (Status \${status}).`,
74
- "the-response-does-not-contain-a-valid-json": `De serverrespons is geen geldige JSON (actueel=\${actual}).`,
115
+ "the-response-does-not-contain-an-accepted-status": `De server heeft het verzoek niet geaccepteerd (status \${status}).`,
116
+ "the-response-does-not-contain-a-valid-json": `De serverrespons is geen geldige JSON (werkelijk=\${actual}).`,
75
117
  };
76
118
  break;
77
119
  case "sv":
78
120
  messages = {
79
- "the-response-does-not-contain-an-accepted-status": `Servern accepterade inte begäran (Status \${status}).`,
121
+ "the-response-does-not-contain-an-accepted-status": `Servern accepterade inte begäran (status \${status}).`,
80
122
  "the-response-does-not-contain-a-valid-json": `Serverns svar är inte en giltig JSON (faktisk=\${actual}).`,
81
123
  };
82
124
  break;
83
125
  case "pl":
84
126
  messages = {
85
- "the-response-does-not-contain-an-accepted-status": `Serwer nie zaakceptował żądania (Status \${status}).`,
86
- "the-response-does-not-contain-a-valid-json": `Odpowiedź serwera nie jest prawidłowym JSON-em (aktualne=\${actual}).`,
127
+ "the-response-does-not-contain-an-accepted-status": `Serwer nie zaakceptował żądania (status \${status}).`,
128
+ "the-response-does-not-contain-a-valid-json": `Odpowiedź serwera nie jest prawidłowym JSON-em (rzeczywiste=\${actual}).`,
87
129
  };
88
130
  break;
89
131
  case "da":
90
132
  messages = {
91
- "the-response-does-not-contain-an-accepted-status": `Serveren accepterede ikke forespørgslen (Status \${status}).`,
92
- "the-response-does-not-contain-a-valid-json": `Serverens svar er ikke en gyldig JSON (aktuel=\${actual}).`,
133
+ "the-response-does-not-contain-an-accepted-status": `Serveren accepterede ikke forespørgslen (status \${status}).`,
134
+ "the-response-does-not-contain-a-valid-json": `Serverens svar er ikke gyldig JSON (faktisk=\${actual}).`,
93
135
  };
94
136
  break;
95
137
  case "fi":
96
138
  messages = {
97
- "the-response-does-not-contain-an-accepted-status": `Palvelin ei hyväksynyt pyyntöä (Tila \${status}).`,
139
+ "the-response-does-not-contain-an-accepted-status": `Palvelin ei hyväksynyt pyyntöä (tila \${status}).`,
98
140
  "the-response-does-not-contain-a-valid-json": `Palvelimen vastaus ei ole kelvollinen JSON (todellinen=\${actual}).`,
99
141
  };
100
142
  break;
101
143
  case "no":
102
144
  messages = {
103
- "the-response-does-not-contain-an-accepted-status": `Serveren aksepterte ikke forespørselen (Status \${status}).`,
145
+ "the-response-does-not-contain-an-accepted-status": `Serveren aksepterte ikke forespørselen (status \${status}).`,
104
146
  "the-response-does-not-contain-a-valid-json": `Serverens respons er ikke en gyldig JSON (faktisk=\${actual}).`,
105
147
  };
106
148
  break;
107
149
  case "cs":
108
150
  messages = {
109
- "the-response-does-not-contain-an-accepted-status": `Server nepřijal požadavek (Stav \${status}).`,
151
+ "the-response-does-not-contain-an-accepted-status": `Server nepřijal požadavek (stav \${status}).`,
110
152
  "the-response-does-not-contain-a-valid-json": `Odpověď serveru není platný JSON (skutečný=\${actual}).`,
111
153
  };
112
154
  break;
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  /**
16
- * Curretnly from Monster supported languages.
16
+ * Languages currently supported by Monster.
17
17
  * @type {string[]}
18
18
  */
19
19
  export const currentSupportedLanguages = [
@@ -74,6 +74,7 @@ export const languages = {
74
74
  hi: "हिन्दी",
75
75
  bn: "বাংলা",
76
76
  pa: "ਪੰਜਾਬੀ",
77
+ mr: "मराठी",
77
78
  id: "Bahasa Indonesia",
78
79
  vi: "Tiếng Việt",
79
80
  tr: "Türkçe",
@@ -439,6 +439,135 @@ const translations = {
439
439
  },
440
440
  },
441
441
 
442
+ bn: {
443
+ template: {
444
+ on: (value) => `${value}-এ`,
445
+ past: (value) => `${value} আগে`,
446
+ future: (value) => `${value} পরে`,
447
+ justNow: () => `এইমাত্র`,
448
+ },
449
+ units: {
450
+ day: {
451
+ zero: "দিন",
452
+ one: "দিন",
453
+ two: "দিন",
454
+ few: "দিন",
455
+ many: "দিন",
456
+ other: "দিন",
457
+ },
458
+ hour: {
459
+ zero: "ঘণ্টা",
460
+ one: "ঘণ্টা",
461
+ two: "ঘণ্টা",
462
+ few: "ঘণ্টা",
463
+ many: "ঘণ্টা",
464
+ other: "ঘণ্টা",
465
+ },
466
+ minute: {
467
+ zero: "মিনিট",
468
+ one: "মিনিট",
469
+ two: "মিনিট",
470
+ few: "মিনিট",
471
+ many: "মিনিট",
472
+ other: "মিনিট",
473
+ },
474
+ second: {
475
+ zero: "সেকেন্ড",
476
+ one: "সেকেন্ড",
477
+ two: "সেকেন্ড",
478
+ few: "সেকেন্ড",
479
+ many: "সেকেন্ড",
480
+ other: "সেকেন্ড",
481
+ },
482
+ },
483
+ },
484
+
485
+ pa: {
486
+ template: {
487
+ on: (value) => `${value} ਨੂੰ`,
488
+ past: (value) => `${value} ਪਹਿਲਾਂ`,
489
+ future: (value) => `${value} ਬਾਅਦ`,
490
+ justNow: () => `ਹੁਣੇ`,
491
+ },
492
+ units: {
493
+ day: {
494
+ zero: "ਦਿਨ",
495
+ one: "ਦਿਨ",
496
+ two: "ਦਿਨ",
497
+ few: "ਦਿਨ",
498
+ many: "ਦਿਨ",
499
+ other: "ਦਿਨ",
500
+ },
501
+ hour: {
502
+ zero: "ਘੰਟੇ",
503
+ one: "ਘੰਟਾ",
504
+ two: "ਘੰਟੇ",
505
+ few: "ਘੰਟੇ",
506
+ many: "ਘੰਟੇ",
507
+ other: "ਘੰਟੇ",
508
+ },
509
+ minute: {
510
+ zero: "ਮਿੰਟ",
511
+ one: "ਮਿੰਟ",
512
+ two: "ਮਿੰਟ",
513
+ few: "ਮਿੰਟ",
514
+ many: "ਮਿੰਟ",
515
+ other: "ਮਿੰਟ",
516
+ },
517
+ second: {
518
+ zero: "ਸਕਿੰਟ",
519
+ one: "ਸਕਿੰਟ",
520
+ two: "ਸਕਿੰਟ",
521
+ few: "ਸਕਿੰਟ",
522
+ many: "ਸਕਿੰਟ",
523
+ other: "ਸਕਿੰਟ",
524
+ },
525
+ },
526
+ },
527
+
528
+ mr: {
529
+ template: {
530
+ on: (value) => `${value} रोजी`,
531
+ past: (value) => `${value} पूर्वी`,
532
+ future: (value) => `${value} नंतर`,
533
+ justNow: () => `आत्ताच`,
534
+ },
535
+ units: {
536
+ day: {
537
+ zero: "दिवस",
538
+ one: "दिवस",
539
+ two: "दिवस",
540
+ few: "दिवस",
541
+ many: "दिवस",
542
+ other: "दिवस",
543
+ },
544
+ hour: {
545
+ zero: "तास",
546
+ one: "तास",
547
+ two: "तास",
548
+ few: "तास",
549
+ many: "तास",
550
+ other: "तास",
551
+ },
552
+ minute: {
553
+ zero: "मिनिटे",
554
+ one: "मिनिट",
555
+ two: "मिनिटे",
556
+ few: "मिनिटे",
557
+ many: "मिनिटे",
558
+ other: "मिनिटे",
559
+ },
560
+ second: {
561
+ zero: "सेकंद",
562
+ one: "सेकंद",
563
+ two: "सेकंद",
564
+ few: "सेकंद",
565
+ many: "सेकंद",
566
+ other: "सेकंद",
567
+ },
568
+ },
569
+ },
570
+
442
571
  pt: {
443
572
  template: {
444
573
  on: (value) => `em ${value}`,
@@ -335,7 +335,7 @@ describe("REST datasource lifecycle", function () {
335
335
  expectNoPendingChanges(second);
336
336
  });
337
337
 
338
- it("preserves a read baseline when the save write finishes later", async function () {
338
+ it("preserves the exact read baseline when the save write finishes later", async function () {
339
339
  const datasource = createDatasource("read-first-datasource");
340
340
  const loaded = { data: [{ id: "area-1", name: "Inbound" }] };
341
341
  const written = { data: [{ id: "area-1", name: "Written change" }] };
@@ -373,6 +373,14 @@ describe("REST datasource lifecycle", function () {
373
373
  writeResult.resolve({ ok: true });
374
374
  await waitForUpdates();
375
375
  expectPendingChanges(button);
376
+
377
+ datasource.data = refreshed;
378
+ await waitForUpdates();
379
+ expectNoPendingChanges(button);
380
+
381
+ datasource.data = written;
382
+ await waitForUpdates();
383
+ expectPendingChanges(button);
376
384
  });
377
385
 
378
386
  it("establishes the read baseline when the save write finishes first", async function () {
@@ -80,6 +80,19 @@ describe("Datatable mobile layout", () => {
80
80
  expect(generated).not.to.include("--monster-button-bar-gap:0.5rem");
81
81
  });
82
82
 
83
+ it("keeps icon-only row overflow menus compact", () => {
84
+ expect(source).to.include("& > monster-button-bar");
85
+ expect(source).to.include("--monster-action-menu-min-inline-size: auto");
86
+ expect(source).to.include("inline-size: 100%");
87
+ expect(source).to.include("max-inline-size: 100%");
88
+ expect(source).to.include("min-inline-size: 0");
89
+ expect(generated).to.include(
90
+ "--monster-action-menu-min-inline-size:auto",
91
+ );
92
+ expect(generated).to.include("inline-size:100%");
93
+ expect(generated).to.include("max-inline-size:100%");
94
+ });
95
+
83
96
  it("stacks labels above values when a responsive card becomes narrow", () => {
84
97
  expect(source).to.include("container-name: monster-datatable-responsive");
85
98
  expect(source).to.include("container-type: inline-size");
@@ -416,6 +416,27 @@ describe("ButtonBar", function () {
416
416
  expect(cssText).to.contain("--monster-button-bar-popper-item-spacing");
417
417
  expect(cssText).to.match(/--monster-button-bar-item-spacing,\s*0/u);
418
418
  expect(cssText).to.match(/--monster-button-bar-popper-item-spacing,\s*0/u);
419
+ expect(cssText).to.contain(
420
+ ":has(>[data-monster-role=switch][hidden])",
421
+ );
422
+ });
423
+
424
+ it("should stack a nested button bar inside a parent overflow item", function () {
425
+ const cssText = ButtonBar.getCSSStyleSheet()
426
+ .flatMap((styleSheet) => Array.from(styleSheet.cssRules))
427
+ .map((rule) => rule.cssText)
428
+ .join("\n");
429
+
430
+ expect(cssText).to.match(
431
+ /flex-direction:\s*var\(--monster-button-bar-flex-direction,\s*row\)/u,
432
+ );
433
+ expect(cssText).to.match(
434
+ /--monster-button-bar-flex-direction:\s*column/u,
435
+ );
436
+ expect(cssText).to.match(/--monster-button-bar-height:\s*auto/u);
437
+ expect(cssText).to.match(
438
+ /slot:not\(\[name\]\)[\s\S]*?inline-size:\s*100%/u,
439
+ );
419
440
  });
420
441
 
421
442
  it("should clear joined-border offsets when a button gap is configured", async function () {
@@ -448,6 +448,11 @@ describe("ControlBar", function () {
448
448
  "--monster-action-menu-item-hover-background-color",
449
449
  );
450
450
  expect(cssText).to.contain("--monster-action-menu-item-focus-outline-offset");
451
+ expect(cssText).to.contain("--monster-button-bar-flex-direction: column");
452
+ expect(cssText).to.contain("--monster-button-bar-height: auto");
453
+ expect(cssText).to.contain(
454
+ ":has(>[data-monster-role=switch][hidden])",
455
+ );
451
456
  expect(cssText).to.contain("height: auto !important");
452
457
  });
453
458
 
@@ -2192,6 +2197,124 @@ describe("ControlBar", function () {
2192
2197
  }
2193
2198
  });
2194
2199
 
2200
+ it("should restore controls when a hidden parent becomes visible during feedback suppression", async function () {
2201
+ const OriginalResizeObserver = window.ResizeObserver;
2202
+ const originalGlobalResizeObserver = globalThis.ResizeObserver;
2203
+ const originalRequestAnimationFrame = window.requestAnimationFrame;
2204
+ const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
2205
+
2206
+ class TrackingResizeObserver extends ResizeObserverMock {
2207
+ static instances = [];
2208
+
2209
+ constructor(callback) {
2210
+ super(callback);
2211
+ TrackingResizeObserver.instances.push(this);
2212
+ }
2213
+ }
2214
+
2215
+ const scheduledCallbacks = [];
2216
+ const flushFrames = async () => {
2217
+ while (scheduledCallbacks.length > 0) {
2218
+ scheduledCallbacks.shift()();
2219
+ await new Promise((resolve) => setTimeout(resolve, 0));
2220
+ }
2221
+ };
2222
+
2223
+ try {
2224
+ window.ResizeObserver = TrackingResizeObserver;
2225
+ globalThis.ResizeObserver = TrackingResizeObserver;
2226
+ window.requestAnimationFrame = (callback) => {
2227
+ scheduledCallbacks.push(callback);
2228
+ return scheduledCallbacks.length;
2229
+ };
2230
+ globalThis.requestAnimationFrame = window.requestAnimationFrame;
2231
+
2232
+ const mocks = document.getElementById("mocks");
2233
+ mocks.innerHTML = `
2234
+ <div id="hidden-parent-wrapper">
2235
+ <monster-control-bar id="hidden-parent-bar">
2236
+ <button id="hidden-parent-one">One</button>
2237
+ <button id="hidden-parent-two">Two</button>
2238
+ </monster-control-bar>
2239
+ </div>
2240
+ `;
2241
+
2242
+ const wrapper = document.getElementById("hidden-parent-wrapper");
2243
+ const bar = document.getElementById("hidden-parent-bar");
2244
+ const popperNav = bar.shadowRoot.querySelector(
2245
+ '[data-monster-role="popper-nav"]',
2246
+ );
2247
+ const controls = [
2248
+ document.getElementById("hidden-parent-one"),
2249
+ document.getElementById("hidden-parent-two"),
2250
+ ];
2251
+ let wrapperWidth = 120;
2252
+
2253
+ wrapper.style.boxSizing = "border-box";
2254
+ Object.defineProperty(wrapper, "clientWidth", {
2255
+ configurable: true,
2256
+ get: () => wrapperWidth,
2257
+ });
2258
+
2259
+ for (const control of controls) {
2260
+ Object.defineProperty(control, "offsetWidth", {
2261
+ configurable: true,
2262
+ value: 50,
2263
+ });
2264
+ Object.defineProperty(control, "offsetHeight", {
2265
+ configurable: true,
2266
+ value: 30,
2267
+ });
2268
+ control.getBoundingClientRect = () => ({
2269
+ width: 50,
2270
+ height: 30,
2271
+ top: 0,
2272
+ right: 50,
2273
+ bottom: 30,
2274
+ left: 0,
2275
+ x: 0,
2276
+ y: 0,
2277
+ toJSON: () => {},
2278
+ });
2279
+ }
2280
+
2281
+ const switchButton = bar.shadowRoot.querySelector(
2282
+ '[data-monster-role="switch"]',
2283
+ );
2284
+ Object.defineProperty(switchButton, "offsetWidth", {
2285
+ configurable: true,
2286
+ value: 20,
2287
+ });
2288
+
2289
+ await flushFrames();
2290
+ expect(controls.every((control) => control.slot === "")).to.be.true;
2291
+
2292
+ const resizeObserver = TrackingResizeObserver.instances.find((observer) =>
2293
+ observer.elements.includes(wrapper),
2294
+ );
2295
+ expect(resizeObserver).to.exist;
2296
+
2297
+ wrapperWidth = 0;
2298
+ resizeObserver.triggerResize([{ target: wrapper }]);
2299
+ scheduledCallbacks.shift()();
2300
+ await new Promise((resolve) => setTimeout(resolve, 0));
2301
+ expect(controls.every((control) => control.slot === "popper")).to.be.true;
2302
+
2303
+ wrapperWidth = 120;
2304
+ resizeObserver.triggerResize([{ target: wrapper }]);
2305
+ await flushFrames();
2306
+
2307
+ expect(controls.every((control) => control.slot === "")).to.be.true;
2308
+ expect(switchButton.hasAttribute("hidden")).to.be.true;
2309
+ expect(popperNav.style.display).to.equal("");
2310
+ } finally {
2311
+ window.ResizeObserver = OriginalResizeObserver;
2312
+ globalThis.ResizeObserver = originalGlobalResizeObserver;
2313
+ window.requestAnimationFrame = originalRequestAnimationFrame;
2314
+ globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
2315
+ }
2316
+ });
2317
+
2195
2318
  it("should keep an open overflow menu through inherited hidden positioning", async function () {
2196
2319
  const OriginalResizeObserver = window.ResizeObserver;
2197
2320
  const originalGlobalResizeObserver = globalThis.ResizeObserver;
@@ -51,4 +51,15 @@ describe("FieldSet", function () {
51
51
  expect(cssText).to.contain("justify-content: flex-start");
52
52
  expect(cssText).to.contain("--monster-control-min-block-size");
53
53
  });
54
+
55
+ it("should size toggle rows from the themed control height", function () {
56
+ const cssText = FieldSet.getCSSStyleSheet()
57
+ .flatMap((styleSheet) => Array.from(styleSheet.cssRules))
58
+ .map((rule) => rule.cssText)
59
+ .join("\n");
60
+
61
+ expect(cssText).to.match(
62
+ /::slotted\(monster-toggle-switch\)[\s\S]*?min-height:\s*var\(--monster-control-min-block-size,\s*1\.8rem\)/u,
63
+ );
64
+ });
54
65
  });
@@ -0,0 +1,58 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { getGlobal } from "../../../../source/types/global.mjs";
3
+ import { initJSDOM } from "../../../util/jsdom.mjs";
4
+ import { ResizeObserverMock } from "../../../util/resize-observer.mjs";
5
+
6
+ describe("localized form defaults", () => {
7
+ before(async () => {
8
+ await initJSDOM();
9
+ await import("element-internals-polyfill");
10
+
11
+ const global = getGlobal();
12
+ global.ResizeObserver ??= ResizeObserverMock;
13
+
14
+ await Promise.all([
15
+ import("../../../../source/components/form/buy-box.mjs"),
16
+ import("../../../../source/components/form/cart-control.mjs"),
17
+ import("../../../../source/components/form/login.mjs"),
18
+ import("../../../../source/components/form/select.mjs"),
19
+ import("../../../../source/components/form/variant-select.mjs"),
20
+ import("../../../../source/components/form/wizard.mjs"),
21
+ ]);
22
+ });
23
+
24
+ afterEach(() => {
25
+ document.documentElement.lang = "en";
26
+ document.getElementById("mocks").replaceChildren();
27
+ });
28
+
29
+ it("uses native text for localized component options", () => {
30
+ for (const [tag, locale, option, expected] of [
31
+ [
32
+ "monster-buy-box",
33
+ "de",
34
+ "labels.selectVariant",
35
+ "Bitte eine Variante wählen.",
36
+ ],
37
+ ["monster-buy-box", "ru", "labels.addToCart", "В корзину"],
38
+ ["monster-cart-control", "cs", "labels.items", "Položky"],
39
+ ["monster-login", "hi", "labels.username", "उपयोगकर्ता नाम या ई-मेल"],
40
+ ["monster-login", "bn", "labels.password", "পাসওয়ার্ড"],
41
+ ["monster-login", "pa", "labels.login", "ਲਾਗ ਇਨ ਕਰੋ"],
42
+ ["monster-login", "mr", "labels.mailAddress", "ई-मेल पत्ता"],
43
+ ["monster-login", "fi", "labels.login", "Kirjaudu sisään"],
44
+ ["monster-variant-select", "pt", "messages.valid", "A seleção é válida."],
45
+ [
46
+ "monster-select",
47
+ "fr",
48
+ "labels.no-options-available",
49
+ "Aucune option disponible.",
50
+ ],
51
+ ["monster-wizard", "sv", "labels.submit", "Slutför"],
52
+ ]) {
53
+ document.documentElement.lang = locale;
54
+ const element = document.createElement(tag);
55
+ assert.equal(element.getOption(option), expected, `${tag}:${locale}`);
56
+ }
57
+ });
58
+ });
@@ -48,6 +48,18 @@ describe("PopperButton", function () {
48
48
  expect(cssText).to.contain("textarea{resize:vertical;min-height:6rem");
49
49
  });
50
50
 
51
+ it("should preserve focus indicator space inside clipped popper content", function () {
52
+ const cssText = PopperButton.getCSSStyleSheet()
53
+ .flatMap((styleSheet) => Array.from(styleSheet.cssRules))
54
+ .map((rule) => rule.cssText)
55
+ .join("\n")
56
+ .replaceAll(/\s+/g, "");
57
+
58
+ expect(cssText).to.contain(
59
+ "[part=content]:not([data-monster-overflow-mode=visible]){padding:var(--monster-popper-focus-bleed,max(0px,calc(var(--monster-focus-ring-width,2px)+var(--monster-focus-ring-offset,2px))))",
60
+ );
61
+ });
62
+
51
63
  it("should apply content overflow mode to the rendered content wrapper", function (done) {
52
64
  let mocks = document.getElementById("mocks");
53
65
  const button = document.createElement("monster-popper-button");