@lion/ui 0.10.0 → 0.10.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.
Files changed (110) hide show
  1. package/components/button/src/LionButton.js +5 -1
  2. package/components/button/test-suites/LionButton.suite.js +1 -1
  3. package/components/calendar/src/LionCalendar.js +5 -14
  4. package/components/calendar/test/lion-calendar.test.js +88 -0
  5. package/components/calendar/translations/bg.js +5 -0
  6. package/components/calendar/translations/cs.js +5 -0
  7. package/components/calendar/translations/de.js +5 -0
  8. package/components/calendar/translations/en.js +5 -0
  9. package/components/calendar/translations/es.js +5 -0
  10. package/components/calendar/translations/fr.js +5 -0
  11. package/components/calendar/translations/hu.js +5 -0
  12. package/components/calendar/translations/it.js +5 -0
  13. package/components/calendar/translations/nl.js +5 -0
  14. package/components/calendar/translations/pl.js +5 -0
  15. package/components/calendar/translations/ro.js +5 -0
  16. package/components/calendar/translations/ru.js +5 -0
  17. package/components/calendar/translations/sk.js +5 -0
  18. package/components/calendar/translations/uk.js +3 -0
  19. package/components/calendar/translations/zh.js +3 -0
  20. package/components/core/src/SlotMixin.js +15 -0
  21. package/components/core/test/SlotMixin.test.js +50 -0
  22. package/components/dialog/src/LionDialog.js +13 -0
  23. package/components/dialog/test/lion-dialog.test.js +56 -0
  24. package/components/form-core/src/choice-group/ChoiceInputMixin.js +3 -1
  25. package/components/input-tel-dropdown/src/LionInputTelDropdown.js +1 -1
  26. package/components/overlays/src/OverlayController.js +17 -1
  27. package/components/overlays/test/OverlayController.test.js +29 -0
  28. package/components/overlays/types/OverlayConfig.ts +4 -2
  29. package/custom-elements.json +38 -13
  30. package/dist-types/components/button/src/LionButton.d.ts.map +1 -1
  31. package/dist-types/components/calendar/src/LionCalendar.d.ts.map +1 -1
  32. package/dist-types/components/calendar/translations/bg.d.ts +3 -0
  33. package/dist-types/components/calendar/translations/cs.d.ts +3 -0
  34. package/dist-types/components/calendar/translations/de.d.ts +3 -0
  35. package/dist-types/components/calendar/translations/en.d.ts +3 -0
  36. package/dist-types/components/calendar/translations/es.d.ts +3 -0
  37. package/dist-types/components/calendar/translations/fr.d.ts +3 -0
  38. package/dist-types/components/calendar/translations/hu.d.ts +3 -0
  39. package/dist-types/components/calendar/translations/it.d.ts +3 -0
  40. package/dist-types/components/calendar/translations/nl.d.ts +3 -0
  41. package/dist-types/components/calendar/translations/pl.d.ts +3 -0
  42. package/dist-types/components/calendar/translations/ro.d.ts +3 -0
  43. package/dist-types/components/calendar/translations/ru.d.ts +3 -0
  44. package/dist-types/components/calendar/translations/sk.d.ts +3 -0
  45. package/dist-types/components/calendar/translations/uk.d.ts +3 -0
  46. package/dist-types/components/calendar/translations/zh.d.ts +3 -0
  47. package/dist-types/components/core/src/SlotMixin.d.ts.map +1 -1
  48. package/dist-types/components/dialog/src/LionDialog.d.ts +32 -0
  49. package/dist-types/components/dialog/src/LionDialog.d.ts.map +1 -1
  50. package/dist-types/components/form-core/src/choice-group/ChoiceInputMixin.d.ts.map +1 -1
  51. package/dist-types/components/input-tel-dropdown/src/LionInputTelDropdown.d.ts +1 -1
  52. package/dist-types/components/overlays/src/OverlayController.d.ts +5 -0
  53. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  54. package/dist-types/components/overlays/types/OverlayConfig.d.ts +4 -2
  55. package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
  56. package/dist-types/components/select-rich/src/LionSelectRich.d.ts +1 -0
  57. package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
  58. package/dist-types/tsconfig.tsbuildinfo +1 -1
  59. package/docs/components/checkbox-group/overview.md +2 -2
  60. package/docs/components/collapsible/overview.md +1 -1
  61. package/docs/components/combobox/overview.md +1 -1
  62. package/docs/components/combobox/use-cases.md +1 -1
  63. package/docs/components/dialog/api-table.md +5 -3
  64. package/docs/components/dialog/overview.md +1 -1
  65. package/docs/components/dialog/use-cases.md +32 -1
  66. package/docs/components/fieldset/overview.md +5 -5
  67. package/docs/components/form/overview.md +3 -3
  68. package/docs/components/form/use-cases.md +2 -2
  69. package/docs/components/icon/use-cases.md +1 -1
  70. package/docs/components/input/overview.md +1 -1
  71. package/docs/components/input-amount/overview.md +3 -3
  72. package/docs/components/input-date/overview.md +3 -3
  73. package/docs/components/input-datepicker/overview.md +5 -5
  74. package/docs/components/input-email/overview.md +2 -2
  75. package/docs/components/input-file/overview.md +1 -1
  76. package/docs/components/input-iban/overview.md +2 -2
  77. package/docs/components/input-range/overview.md +2 -2
  78. package/docs/components/input-stepper/overview.md +1 -1
  79. package/docs/components/input-tel/overview.md +1 -1
  80. package/docs/components/input-tel/use-cases.md +6 -6
  81. package/docs/components/input-tel-dropdown/overview.md +1 -1
  82. package/docs/components/listbox/use-cases.md +1 -1
  83. package/docs/components/radio-group/overview.md +3 -3
  84. package/docs/components/select/use-cases.md +1 -1
  85. package/docs/components/select-rich/use-cases.md +1 -1
  86. package/docs/components/tabs/examples.md +1 -1
  87. package/docs/fundamentals/index.md +4 -4
  88. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  89. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  90. package/docs/fundamentals/rationales/accessibility.md +1 -1
  91. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  92. package/docs/fundamentals/systems/core/overview.md +1 -1
  93. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  94. package/docs/fundamentals/systems/form/model-value.md +1 -1
  95. package/docs/fundamentals/systems/form/overview.md +28 -28
  96. package/docs/fundamentals/systems/form/validate.md +2 -2
  97. package/docs/fundamentals/systems/localize/overview.md +4 -4
  98. package/docs/fundamentals/systems/overlays/api-table.md +35 -34
  99. package/docs/fundamentals/systems/overlays/configuration.md +32 -2
  100. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  101. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  102. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  103. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  104. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  105. package/docs/guides/how-to/create-a-form.md +10 -9
  106. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  107. package/docs/guides/index.md +8 -8
  108. package/docs/guides/principles/index.md +5 -5
  109. package/docs/guides/principles/styling.md +1 -1
  110. package/package.json +1 -1
@@ -149,7 +149,11 @@ export class LionButton extends DisabledWithTabIndexMixin(LitElement) {
149
149
  super.updated(changedProperties);
150
150
 
151
151
  if (changedProperties.has('disabled')) {
152
- this.setAttribute('aria-disabled', `${this.disabled}`); // create mixin if we need it in more places
152
+ if (this.disabled) {
153
+ this.setAttribute('aria-disabled', 'true');
154
+ } else if (this.getAttribute('aria-disabled') !== null) {
155
+ this.removeAttribute('aria-disabled');
156
+ }
153
157
  }
154
158
  }
155
159
 
@@ -40,7 +40,7 @@ export function LionButtonSuite({ klass = LionButton } = {}) {
40
40
  el.disabled = false;
41
41
  await el.updateComplete;
42
42
  expect(el.getAttribute('tabindex')).to.equal('0');
43
- expect(el.getAttribute('aria-disabled')).to.equal('false');
43
+ expect(el.getAttribute('aria-disabled')).to.not.exist;
44
44
  expect(el.hasAttribute('disabled')).to.equal(false);
45
45
 
46
46
  el.disabled = true;
@@ -658,25 +658,16 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
658
658
  day.ariaCurrent = day.today ? 'date' : undefined;
659
659
  day.disabledInfo = '';
660
660
 
661
+ if (day.disabled) {
662
+ day.disabledInfo = `${this.msgLit(`lion-calendar:defaultDisabledDate`)}`;
663
+ }
661
664
  if (this.minDate && normalizeDateTime(day.date) < normalizeDateTime(this.minDate)) {
662
665
  day.disabled = true;
663
- // TODO: turn this into a translated string
664
- day.disabledInfo = `This date is unavailable. Earliest date to select is ${
665
- this.__getSelectableDateRange().earliestSelectableDate
666
- }. Please select another date.`;
666
+ day.disabledInfo = `${this.msgLit(`lion-calendar:beforeDisabledDate`, { params: this.__getSelectableDateRange().earliestSelectableDate })}`;
667
667
  }
668
-
669
668
  if (this.maxDate && normalizeDateTime(day.date) > normalizeDateTime(this.maxDate)) {
670
669
  day.disabled = true;
671
- // TODO: turn this into a translated string
672
- day.disabledInfo = `This date is unavailable. Latest date to select is ${
673
- this.__getSelectableDateRange().latestSelectableDate
674
- }. Please select another date.`;
675
- }
676
-
677
- if (day.disabled) {
678
- // TODO: turn this into a translated string
679
- day.disabledInfo = `This date is unavailable. Please select another date`;
670
+ day.disabledInfo = `${this.msgLit(`lion-calendar:afterDisabledDate`, { params: this.__getSelectableDateRange().latestSelectableDate })}`;
680
671
  }
681
672
 
682
673
  return this.dayPreprocessor(day);
@@ -896,6 +896,94 @@ describe('<lion-calendar>', () => {
896
896
  'Next year, December 2001',
897
897
  );
898
898
  });
899
+
900
+ it('disables a date with disableDates function provide a correct message', async () => {
901
+ /** @param {Date} d */
902
+ const disable15th = d => d.getDate() === 15;
903
+ const el = await fixture(html`
904
+ <lion-calendar
905
+ .selectedDate="${new Date('2000/12/01')}"
906
+ .disableDates=${disable15th}
907
+ ></lion-calendar>
908
+ `);
909
+ const elObj = new CalendarObject(el);
910
+ expect(elObj.getDayObj(15).isDisabled).to.equal(true);
911
+ expect(elObj.getDayObj(15).el).dom.to.equal(`
912
+ <div
913
+ class="calendar__day-button"
914
+ aria-disabled="true"
915
+ role="button"
916
+ tabindex="-1"
917
+ aria-pressed="false"
918
+ past=""
919
+ current-month="">
920
+ <span class="calendar__day-button__text">
921
+ 15
922
+ </span>
923
+ <span class="u-sr-only">
924
+ December 2000 Friday This date is unavailable. Please choose another date.
925
+ </span>
926
+ </div>
927
+ `);
928
+ });
929
+
930
+ it('disables days before "minDate" property provide a correct message', async () => {
931
+ const el = await fixture(html`
932
+ <lion-calendar
933
+ .selectedDate="${new Date('2000/12/31')}"
934
+ .minDate="${new Date('2000/12/09')}"
935
+ >
936
+ </lion-calendar>
937
+ `);
938
+ const elObj = new CalendarObject(el);
939
+ expect(elObj.getDayObj(1).isDisabled).to.equal(true);
940
+ expect(elObj.getDayObj(1).el).dom.to.equal(`
941
+ <div
942
+ class="calendar__day-button"
943
+ aria-disabled="true"
944
+ role="button"
945
+ tabindex="-1"
946
+ aria-pressed="false"
947
+ past=""
948
+ current-month="">
949
+ <span class="calendar__day-button__text">
950
+ 1
951
+ </span>
952
+ <span class="u-sr-only">
953
+ December 2000 Friday This date is unavailable. Earliest available date is 9 December 2000. Please choose another date.
954
+ </span>
955
+ </div>
956
+ `);
957
+ });
958
+
959
+ it('disables days after "maxDate" property provide a correct message', async () => {
960
+ const el = await fixture(html`
961
+ <lion-calendar
962
+ .selectedDate="${new Date('2000/12/31')}"
963
+ .maxDate="${new Date('2000/12/09')}"
964
+ >
965
+ </lion-calendar>
966
+ `);
967
+ const elObj = new CalendarObject(el);
968
+ expect(elObj.getDayObj(30).isDisabled).to.equal(true);
969
+ expect(elObj.getDayObj(30).el).dom.to.equal(`
970
+ <div
971
+ class="calendar__day-button"
972
+ aria-disabled="true"
973
+ role="button"
974
+ tabindex="-1"
975
+ aria-pressed="false"
976
+ past=""
977
+ current-month="">
978
+ <span class="calendar__day-button__text">
979
+ 30
980
+ </span>
981
+ <span class="u-sr-only">
982
+ December 2000 Saturday This date is unavailable. Latest available date is 9 December 2000. Please choose another date.
983
+ </span>
984
+ </div>
985
+ `);
986
+ });
899
987
  });
900
988
  });
901
989
  });
@@ -3,4 +3,9 @@ export default {
3
3
  previousMonth: 'Предишен месец',
4
4
  nextFullYear: 'Следващ година',
5
5
  previousFullYear: 'Предишен година',
6
+ defaultDisabledDate: 'Тази дата не е налична. Моля, изберете друга дата.',
7
+ beforeDisabledDate:
8
+ 'Тази дата не е налична. Най-ранната налична да е {params}. Моля, изберете друга дата.',
9
+ afterDisabledDate:
10
+ 'Тази дата не е налична. Най-късната налична дата е {params}. Моля, изберете друга дата.',
6
11
  };
@@ -3,4 +3,9 @@ export default {
3
3
  previousMonth: 'Předchozí měsíc',
4
4
  nextFullYear: 'Příští rok',
5
5
  previousFullYear: 'Předchozí rok',
6
+ defaultDisabledDate: 'Toto datum není k dispozici. Vyberte jiné datum.',
7
+ beforeDisabledDate:
8
+ 'Toto datum není k dispozici. Nejbližší dostupné datum je {params}. Vyberte jiné datum.',
9
+ afterDisabledDate:
10
+ 'Toto datum není k dispozici. Poslední dostupné datum je {params}. Vyberte jiné datum.',
6
11
  };
@@ -3,4 +3,9 @@ export default {
3
3
  previousMonth: 'Vorheriger Monat',
4
4
  nextFullYear: 'Nächstes Jahr',
5
5
  previousFullYear: 'Vorheriges Jahr',
6
+ defaultDisabledDate: 'Dieses Datum ist nicht verfügbar. Bitte wählen Sie ein anderes Datum aus.',
7
+ beforeDisabledDate:
8
+ 'Dieses Datum ist nicht verfügbar. Das früheste verfügbare Datum ist {params}. Bitte wählen Sie ein anderes Datum aus.',
9
+ afterDisabledDate:
10
+ 'Dieses Datum ist nicht verfügbar. Das späteste verfügbare Datum ist {params}. Bitte wählen Sie ein anderes Datum aus.',
6
11
  };
@@ -3,4 +3,9 @@ export default {
3
3
  previousMonth: 'Previous month',
4
4
  nextFullYear: 'Next year',
5
5
  previousFullYear: 'Previous year',
6
+ defaultDisabledDate: 'This date is unavailable. Please choose another date.',
7
+ beforeDisabledDate:
8
+ 'This date is unavailable. Earliest available date is {params}. Please choose another date.',
9
+ afterDisabledDate:
10
+ 'This date is unavailable. Latest available date is {params}. Please choose another date.',
6
11
  };
@@ -3,4 +3,9 @@ export default {
3
3
  previousMonth: 'Mes anterior',
4
4
  nextFullYear: 'Año siguiente',
5
5
  previousFullYear: 'Año anterior',
6
+ defaultDisabledDate: 'Esta fecha no está disponible. Elija otra.',
7
+ beforeDisabledDate:
8
+ 'Esta fecha no está disponible. La fecha más próxima disponible es {params}. Elija otra.',
9
+ afterDisabledDate:
10
+ 'Esta fecha no está disponible. La fecha más lejana disponible es {params}. Elija otra.',
6
11
  };
@@ -3,4 +3,9 @@ export default {
3
3
  previousMonth: 'Mois précédent',
4
4
  nextFullYear: 'An prochain',
5
5
  previousFullYear: 'An précédent',
6
+ defaultDisabledDate: "Cette date n'est pas disponible. Veuillez choisir une autre date.",
7
+ beforeDisabledDate:
8
+ "Cette date n'est pas disponible. La prochaine date disponible est la suivante : {params}. Veuillez choisir une autre date.",
9
+ afterDisabledDate:
10
+ "Cette date n'est pas disponible. La dernière date disponible est la suivante : {params}. Veuillez choisir une autre date.",
6
11
  };
@@ -3,4 +3,9 @@ export default {
3
3
  previousMonth: 'Előző hónap',
4
4
  nextFullYear: 'Következő év',
5
5
  previousFullYear: 'Előző év',
6
+ defaultDisabledDate: 'Ez a dátum nem választható. Válasszon másik dátumot.',
7
+ beforeDisabledDate:
8
+ 'Ez a dátum nem választható. A legkorábbi elérhető dátum {params}. Válasszon másik dátumot.',
9
+ afterDisabledDate:
10
+ 'Ez a dátum nem választható. A legkésőbbi elérhető dátum {params}. Válasszon másik dátumot.',
6
11
  };
@@ -3,4 +3,9 @@ export default {
3
3
  previousMonth: 'Mese precedente',
4
4
  nextFullYear: 'Anno successivo',
5
5
  previousFullYear: 'Anno precedente',
6
+ defaultDisabledDate: "Questa data non è disponibile. Scegline un'altra.",
7
+ beforeDisabledDate:
8
+ "Questa data non è disponibile. La prima data disponibile è {params}. Scegline un'altra.",
9
+ afterDisabledDate:
10
+ "Questa data non è disponibile. L'ultima data disponibile è {params}. Scegline un'altra.",
6
11
  };
@@ -3,4 +3,9 @@ export default {
3
3
  previousMonth: 'Vorige maand',
4
4
  nextFullYear: 'Volgend jaar',
5
5
  previousFullYear: 'Vorig jaar',
6
+ defaultDisabledDate: 'Deze datum is niet beschikbaar. Kies een andere datum.',
7
+ beforeDisabledDate:
8
+ 'Deze datum is niet beschikbaar. Vroegste beschikbare datum is {params}. Kies een andere datum.',
9
+ afterDisabledDate:
10
+ 'Deze datum is niet beschikbaar. Laatste beschikbare datum is {params}. Kies een andere datum.',
6
11
  };
@@ -3,4 +3,9 @@ export default {
3
3
  previousMonth: 'Poprzedni miesiąc',
4
4
  nextFullYear: 'Następny rok',
5
5
  previousFullYear: 'Poprzedni rok',
6
+ defaultDisabledDate: 'Ta data jest niedostępna. Wybierz inną datę.',
7
+ beforeDisabledDate:
8
+ 'Ta data jest niedostępna. Najwcześniejsza dostępna data to {params}. Wybierz inną datę.',
9
+ afterDisabledDate:
10
+ 'Ta data jest niedostępna. Najpóźniejsza dostępna data to {params}. Wybierz inną datę.',
6
11
  };
@@ -3,4 +3,9 @@ export default {
3
3
  previousMonth: 'Luna anterioară',
4
4
  nextFullYear: 'An viitoare',
5
5
  previousFullYear: 'An anterioară',
6
+ defaultDisabledDate: 'Această dată nu este disponibilă. Vă rugăm să alegeți altă dată.',
7
+ beforeDisabledDate:
8
+ 'Această dată nu este disponibilă. Cea mai apropiată dată disponibilă este {params}. Vă rugăm să alegeți altă dată.',
9
+ afterDisabledDate:
10
+ 'Această dată nu este disponibilă. Ultima dată disponibilă este {params}. Vă rugăm să alegeți altă dată.',
6
11
  };
@@ -3,4 +3,9 @@ export default {
3
3
  previousMonth: 'Предыдущий месяц',
4
4
  nextFullYear: 'Следующий год',
5
5
  previousFullYear: 'Предыдущий год',
6
+ defaultDisabledDate: 'Эта дата недоступна. Выберите другую дату.',
7
+ beforeDisabledDate:
8
+ 'Эта дата недоступна. Самая ранняя доступная дата — {params}. Выберите другую дату.',
9
+ afterDisabledDate:
10
+ 'Эта дата недоступна. Самая поздняя доступная дата — {params}. Выберите другую дату.',
6
11
  };
@@ -3,4 +3,9 @@ export default {
3
3
  previousMonth: 'Predchádzajúci mesiac',
4
4
  nextFullYear: 'Nasledujúci rok',
5
5
  previousFullYear: 'Predchádzajúci rok',
6
+ defaultDisabledDate: 'Tento dátum nie je k dispozícii. Vyberte iný dátum.',
7
+ beforeDisabledDate:
8
+ 'Tento dátum nie je k dispozícii. Najbližší možný dátum je {params}. Vyberte iný dátum.',
9
+ afterDisabledDate:
10
+ 'Tento dátum nie je k dispozícii. Posledný možný dátum je {params}. Vyberte iný dátum.',
6
11
  };
@@ -3,4 +3,7 @@ export default {
3
3
  previousMonth: 'Попередній місяць',
4
4
  nextFullYear: 'Наступний року',
5
5
  previousFullYear: 'Попередній року',
6
+ defaultDisabledDate: 'Ця дата недоступна. Виберіть іншу дату.',
7
+ beforeDisabledDate: 'Ця дата недоступна. Найближча доступна дата {params}. Виберіть іншу дату.',
8
+ afterDisabledDate: 'Ця дата недоступна. Остання доступна дата {params}. Виберіть іншу дату.',
6
9
  };
@@ -3,4 +3,7 @@ export default {
3
3
  previousMonth: '前一个月',
4
4
  nextFullYear: '明一年',
5
5
  previousFullYear: '前一年',
6
+ defaultDisabledDate: '此日期不可选。请选择另一个日期。',
7
+ beforeDisabledDate: '此日期不可选。最早可选日期为 {params}。请选择另一个日期。',
8
+ afterDisabledDate: '此日期不可选。最晚可选日期为 {params}。请选择另一个日期。',
6
9
  };
@@ -182,6 +182,21 @@ const SlotMixinImplementation = superclass =>
182
182
  // Providing all options breaks Safari: we keep host and creationScope
183
183
  const { creationScope, host } = this.renderOptions;
184
184
  render(template, rerenderTarget, { creationScope, host, renderBefore });
185
+
186
+ // Assume we had this config:
187
+ // `'my-slot': () => ({ template: myBool ? html`<div id=a></div>` : html`<span id=b></span>`, renderAsDirectHostChild: true })`
188
+ // If myBool started as true, <div id=a></div> would be rendered in first render above, a slot would be applied,
189
+ // resulting in <div id=a slot=my-slot></div>
190
+ // However, when myBool changes to false, the <span id=b></span> would be rendered as root instead...
191
+ // We need to make sure that this "replaced root" gets the slot applied as well => <span id=b slot=my-slot></span>
192
+ const isRerenderingRootOfTemplate =
193
+ renderAsDirectHostChild &&
194
+ renderBefore.previousElementSibling &&
195
+ !renderBefore.previousElementSibling.slot;
196
+
197
+ if (isRerenderingRootOfTemplate) {
198
+ renderBefore.previousElementSibling.slot = slotName;
199
+ }
185
200
  }
186
201
 
187
202
  /**
@@ -345,6 +345,56 @@ describe('SlotMixin', () => {
345
345
  expect(isActiveElement(el._focusableNode._buttonNode, { deep: true })).to.be.true;
346
346
  });
347
347
 
348
+ it('allows for switching template root in slot as a direct child', async () => {
349
+ const tagName = defineCE(
350
+ // @ts-expect-error
351
+ class extends SlotMixin(LitElement) {
352
+ static properties = { isSwitched: Boolean };
353
+
354
+ constructor() {
355
+ super();
356
+ this.isSwitched = false;
357
+ }
358
+
359
+ get slots() {
360
+ return {
361
+ ...super.slots,
362
+ 'my-root-switcher-node': () => ({
363
+ template: this.isSwitched
364
+ ? html`<div id="is-switched"></div>`
365
+ : html`<span id="is-not-switched"> </span> `,
366
+ renderAsDirectHostChild: true,
367
+ }),
368
+ };
369
+ }
370
+
371
+ render() {
372
+ return html`<slot name="my-root-switcher-node"></slot>`;
373
+ }
374
+
375
+ get _myRootSwitcherNode() {
376
+ return /** @type HTMLSpanElement */ (
377
+ Array.from(this.children).find(elm => elm.slot === 'my-root-switcher-node')
378
+ );
379
+ }
380
+ },
381
+ );
382
+ const el = /** @type {* & SlotHost} */ (await fixture(`<${tagName}></${tagName}>`));
383
+
384
+ expect(el._myRootSwitcherNode.id).to.equal('is-not-switched');
385
+ expect(el.innerHTML).to.equal(
386
+ `<!--_start_slot_my-root-switcher-node_--><!----><span id="is-not-switched" slot="my-root-switcher-node"> </span> <!--_end_slot_my-root-switcher-node_-->`,
387
+ );
388
+
389
+ el.isSwitched = true;
390
+ await el.updateComplete;
391
+
392
+ expect(el._myRootSwitcherNode.id).to.equal('is-switched');
393
+ expect(el.innerHTML).to.equal(
394
+ `<!--_start_slot_my-root-switcher-node_--><!----><div id="is-switched" slot="my-root-switcher-node"></div><!--_end_slot_my-root-switcher-node_-->`,
395
+ );
396
+ });
397
+
348
398
  describe('firstRenderOnConnected (for backwards compatibility)', () => {
349
399
  it('does render on connected when firstRenderOnConnected:true', async () => {
350
400
  // Start with elem that does not render on connectedCallback
@@ -2,6 +2,18 @@ import { html, LitElement } from 'lit';
2
2
  import { OverlayMixin, withModalDialogConfig } from '@lion/ui/overlays.js';
3
3
 
4
4
  export class LionDialog extends OverlayMixin(LitElement) {
5
+ /** @type {any} */
6
+ static get properties() {
7
+ return {
8
+ isAlertDialog: { type: Boolean, attribute: 'is-alert-dialog' },
9
+ };
10
+ }
11
+
12
+ constructor() {
13
+ super();
14
+ this.isAlertDialog = false;
15
+ }
16
+
5
17
  /**
6
18
  * @protected
7
19
  */
@@ -9,6 +21,7 @@ export class LionDialog extends OverlayMixin(LitElement) {
9
21
  _defineOverlayConfig() {
10
22
  return {
11
23
  ...withModalDialogConfig(),
24
+ isAlertDialog: this.isAlertDialog,
12
25
  };
13
26
  }
14
27
 
@@ -170,6 +170,27 @@ describe('lion-dialog', () => {
170
170
  });
171
171
 
172
172
  describe('Accessibility', () => {
173
+ it('passes a11y audit', async () => {
174
+ const el = await fixture(html`
175
+ <lion-dialog>
176
+ <button slot="invoker">Invoker</button>
177
+ <div slot="content" class="dialog" aria-label="Dialog">Hey there</div>
178
+ </lion-dialog>
179
+ `);
180
+ await expect(el).to.be.accessible();
181
+ });
182
+
183
+ it('passes a11y audit when opened', async () => {
184
+ const el = await fixture(html`
185
+ <lion-dialog opened>
186
+ <button slot="invoker">Invoker</button>
187
+ <div slot="content" class="dialog" aria-label="Dialog">Hey there</div>
188
+ </lion-dialog>
189
+ `);
190
+ // error expected since we put role="none" on the dialog itself, which is valid but not recognized by Axe
191
+ await expect(el).to.be.accessible({ ignoredRules: ['aria-allowed-role'] });
192
+ });
193
+
173
194
  it('does not add [aria-expanded] to invoker button', async () => {
174
195
  const el = await fixture(
175
196
  html` <lion-dialog>
@@ -187,6 +208,41 @@ describe('lion-dialog', () => {
187
208
  await aTimeout(0);
188
209
  expect(invokerButton.getAttribute('aria-expanded')).to.equal(null);
189
210
  });
211
+
212
+ it('has role="dialog" by default', async () => {
213
+ const el = await fixture(
214
+ html` <lion-dialog>
215
+ <div slot="content" class="dialog">Hey there</div>
216
+ <button slot="invoker">Popup button</button>
217
+ </lion-dialog>`,
218
+ );
219
+ const contentNode = /** @type {HTMLElement} */ (el.querySelector('[slot="content"]'));
220
+
221
+ expect(contentNode.getAttribute('role')).to.equal('dialog');
222
+ });
223
+
224
+ it('has role="alertdialog" by when "is-alert-dialog" is set', async () => {
225
+ const el = await fixture(
226
+ html` <lion-dialog is-alert-dialog>
227
+ <div slot="content" class="dialog">Hey there</div>
228
+ <button slot="invoker">Popup button</button>
229
+ </lion-dialog>`,
230
+ );
231
+ const contentNode = /** @type {HTMLElement} */ (el.querySelector('[slot="content"]'));
232
+
233
+ expect(contentNode.getAttribute('role')).to.equal('alertdialog');
234
+ });
235
+
236
+ it('passes a11y audit when opened and role="alertdialog"', async () => {
237
+ const el = await fixture(html`
238
+ <lion-dialog opened is-alert-dialog>
239
+ <button slot="invoker">Invoker</button>
240
+ <div slot="content" class="dialog" aria-label="Dialog">Hey there</div>
241
+ </lion-dialog>
242
+ `);
243
+ // error expected since we put role="none" on the dialog itself, which is valid but not recognized by Axe
244
+ await expect(el).to.be.accessible({ ignoredRules: ['aria-allowed-role'] });
245
+ });
190
246
  });
191
247
 
192
248
  describe('Edge cases', () => {
@@ -171,7 +171,9 @@ const ChoiceInputMixinImplementation = superclass =>
171
171
  render() {
172
172
  return html`
173
173
  <slot name="input"></slot>
174
- <div class="choice-field__graphic-container">${this._choiceGraphicTemplate()}</div>
174
+ <div class="choice-field__graphic-container" aria-hidden="true">
175
+ ${this._choiceGraphicTemplate()}
176
+ </div>
175
177
  <div class="choice-field__label">
176
178
  <slot name="label"></slot>
177
179
  </div>
@@ -181,7 +181,7 @@ export class LionInputTelDropdown extends LionInputTel {
181
181
 
182
182
  return {
183
183
  template: templates.dropdown(this._templateDataDropdown),
184
- renderAsDirectHostChild: Boolean,
184
+ renderAsDirectHostChild: true,
185
185
  };
186
186
  },
187
187
  };
@@ -151,6 +151,7 @@ export class OverlayController extends EventTarget {
151
151
  hidesOnOutsideEsc: false,
152
152
  hidesOnOutsideClick: false,
153
153
  isTooltip: false,
154
+ isAlertDialog: false,
154
155
  invokerRelation: 'description',
155
156
  visibilityTriggerFunction: undefined,
156
157
  handlesAccessibility: false,
@@ -381,6 +382,14 @@ export class OverlayController extends EventTarget {
381
382
  return /** @type {boolean} */ (this.config?.isTooltip);
382
383
  }
383
384
 
385
+ /**
386
+ * The alertdialog role is to be used on modal alert dialogs that interrupt a user's workflow
387
+ * to communicate an important message and require a response.
388
+ */
389
+ get isAlertDialog() {
390
+ return /** @type {boolean} */ (this.config?.isAlertDialog);
391
+ }
392
+
384
393
  /**
385
394
  * By default, the tooltip content is a 'description' for the invoker (uses aria-describedby).
386
395
  * Setting this property to 'label' makes the content function as a label (via aria-labelledby)
@@ -611,6 +620,10 @@ export class OverlayController extends EventTarget {
611
620
  });
612
621
  // @ts-ignore
613
622
  wrappingDialogElement.open = true;
623
+ if (this.isTooltip) {
624
+ // needed to prevent tooltip getting focus in Safari and Firefox
625
+ wrappingDialogElement.setAttribute('tabindex', '-1');
626
+ }
614
627
 
615
628
  this.__wrappingDialogNode.style.display = 'none';
616
629
  this.contentWrapperNode.style.zIndex = '1';
@@ -672,7 +685,10 @@ export class OverlayController extends EventTarget {
672
685
  if (this.invokerNode && !isModal) {
673
686
  this.invokerNode.setAttribute('aria-expanded', `${this.isShown}`);
674
687
  }
675
- if (!this.contentNode.getAttribute('role')) {
688
+ if (this.isAlertDialog) {
689
+ this.contentNode.setAttribute('role', 'alertdialog');
690
+ } else if (!this.contentNode.getAttribute('role')) {
691
+ // N.B. if we did not explicitly set `isTooltip` or `isAlertDialog`, a role potentially already provided by a user (like 'listbox') takes precedence
676
692
  this.contentNode.setAttribute('role', 'dialog');
677
693
  }
678
694
  }
@@ -1876,6 +1876,22 @@ describe('OverlayController', () => {
1876
1876
  it.skip('adds attributes inert and aria-hidden="true" on all siblings of rootNode if an overlay is shown', async () => {});
1877
1877
  it.skip('disables pointer events and selection on inert elements', async () => {});
1878
1878
 
1879
+ describe('Alert dialog', () => {
1880
+ it('sets role="alertdialog" when isAlertDialog is set', async () => {
1881
+ const invokerNode = /** @type {HTMLElement} */ (
1882
+ await fixture('<div role="button">invoker</div>')
1883
+ );
1884
+ const ctrl = new OverlayController({
1885
+ ...withLocalTestConfig(),
1886
+ handlesAccessibility: true,
1887
+ isAlertDialog: true,
1888
+ invokerNode,
1889
+ });
1890
+
1891
+ expect(ctrl.contentNode?.getAttribute('role')).to.equal('alertdialog');
1892
+ });
1893
+ });
1894
+
1879
1895
  describe('Tooltip', () => {
1880
1896
  it('adds [aria-describedby] on invoker', async () => {
1881
1897
  const invokerNode = /** @type {HTMLElement} */ (
@@ -1922,6 +1938,19 @@ describe('OverlayController', () => {
1922
1938
  expect(ctrl.contentNode.getAttribute('role')).to.equal('tooltip');
1923
1939
  });
1924
1940
 
1941
+ it('adds tabindex="-1" to the wrappingDialog element', async () => {
1942
+ const invokerNode = /** @type {HTMLElement} */ (
1943
+ await fixture('<div role="button">invoker</div>')
1944
+ );
1945
+ const ctrl = new OverlayController({
1946
+ ...withLocalTestConfig(),
1947
+ handlesAccessibility: true,
1948
+ isTooltip: true,
1949
+ invokerNode,
1950
+ });
1951
+ expect(ctrl.__wrappingDialogNode?.getAttribute('tabindex')).to.equal('-1');
1952
+ });
1953
+
1925
1954
  describe('Teardown', () => {
1926
1955
  it('restores [role] on dialog content', async () => {
1927
1956
  const invokerNode = /** @type {HTMLElement} */ (
@@ -41,7 +41,7 @@ export interface OverlayConfig {
41
41
  contentNode?: HTMLElement;
42
42
  /** The wrapper element of contentNode, used to supply inline positioning styles. When a Popper arrow is needed, it acts as parent of the arrow node. Will be automatically created for global and non projected contentNodes. Required when used in shadow dom mode or when Popper arrow is supplied. Essential for allowing webcomponents to style their projected contentNodes */
43
43
  contentWrapperNode?: HTMLElement;
44
- /** The element that is placed behin the contentNode. When not provided and `hasBackdrop` is true, a backdropNode will be automatically created */
44
+ /** The element that is placed behind the contentNode. When not provided and `hasBackdrop` is true, a backdropNode will be automatically created */
45
45
  backdropNode?: HTMLElement;
46
46
  /** The element that should be called `.focus()` on after dialog closes */
47
47
  elementToFocusAfterHide?: HTMLElement;
@@ -59,7 +59,7 @@ export interface OverlayConfig {
59
59
  trapsKeyboardFocus?: boolean;
60
60
  /** Hides the overlay when pressing [ esc ] */
61
61
  hidesOnEsc?: boolean;
62
- /** Hides the overlay when clicking next to it, exluding invoker */
62
+ /** Hides the overlay when clicking next to it, excluding invoker */
63
63
  hidesOnOutsideClick?: boolean;
64
64
  /** Hides the overlay when pressing esc, even when contentNode has no focus */
65
65
  hidesOnOutsideEsc?: boolean;
@@ -82,6 +82,8 @@ export interface OverlayConfig {
82
82
 
83
83
  /** Has a totally different interaction- and accessibility pattern from all other overlays. Will behave as role="tooltip" element instead of a role="dialog" element */
84
84
  isTooltip?: boolean;
85
+ /** The alertdialog role is to be used on modal alert dialogs that interrupt a user's workflow to communicate an important message and require a response. */
86
+ isAlertDialog?: boolean;
85
87
  /** By default, the tooltip content is a 'description' for the invoker (uses aria-describedby) Setting this property to 'label' makes the content function as a label (via aria-labelledby) */
86
88
  invokerRelation?: 'label' | 'description';
87
89