@lion/ui 0.8.1 → 0.8.3

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 (132) hide show
  1. package/components/calendar/src/LionCalendar.js +6 -0
  2. package/components/core/src/DisabledWithTabIndexMixin.js +7 -0
  3. package/components/core/src/ScopedElementsMixin.js +58 -37
  4. package/components/core/test/ScopedElementsMixin.test.js +131 -0
  5. package/components/core/test/ssr-definitions/ScopedElementsHost.define.js +1 -0
  6. package/components/form-core/src/FormControlMixin.js +6 -0
  7. package/components/form-core/src/utils/SyncUpdatableMixin.js +7 -0
  8. package/components/form-core/src/validate/LionValidationFeedback.js +4 -2
  9. package/components/form-core/test/validate/lion-validation-feedback.test.js +12 -0
  10. package/components/icon/src/LionIcon.js +6 -0
  11. package/components/localize/src/LocalizeManager.js +258 -178
  12. package/components/overlays/src/OverlayController.js +40 -13
  13. package/components/overlays/src/OverlayMixin.js +7 -0
  14. package/components/overlays/test/OverlayController.test.js +233 -30
  15. package/components/overlays/test-helpers/mimicClick.js +2 -2
  16. package/components/progress-indicator/src/LionProgressIndicator.js +6 -0
  17. package/components/steps/src/LionStep.js +6 -0
  18. package/components/steps/src/LionSteps.js +6 -0
  19. package/components/tabs/src/LionTabs.js +6 -0
  20. package/components/tooltip/src/LionTooltip.js +6 -0
  21. package/custom-elements.json +716 -163
  22. package/dist-types/components/calendar/src/LionCalendar.d.ts +5 -0
  23. package/dist-types/components/calendar/src/LionCalendar.d.ts.map +1 -1
  24. package/dist-types/components/core/src/DisabledWithTabIndexMixin.d.ts.map +1 -1
  25. package/dist-types/components/core/src/ScopedElementsMixin.d.ts +43 -6
  26. package/dist-types/components/core/src/ScopedElementsMixin.d.ts.map +1 -1
  27. package/dist-types/components/core/test/ScopedElementsMixin.test.d.ts +2 -0
  28. package/dist-types/components/core/test/ScopedElementsMixin.test.d.ts.map +1 -0
  29. package/dist-types/components/core/test/ssr-definitions/ScopedElementsHost.define.d.ts +2 -0
  30. package/dist-types/components/core/test/ssr-definitions/ScopedElementsHost.define.d.ts.map +1 -0
  31. package/dist-types/components/form-core/src/FormControlMixin.d.ts.map +1 -1
  32. package/dist-types/components/form-core/src/utils/SyncUpdatableMixin.d.ts.map +1 -1
  33. package/dist-types/components/form-core/src/validate/LionValidationFeedback.d.ts.map +1 -1
  34. package/dist-types/components/form-integrations/test/helpers/helpers.d.ts +3 -3
  35. package/dist-types/components/form-integrations/test/helpers/helpers.d.ts.map +1 -1
  36. package/dist-types/components/icon/src/LionIcon.d.ts +5 -0
  37. package/dist-types/components/icon/src/LionIcon.d.ts.map +1 -1
  38. package/dist-types/components/input-datepicker/src/LionInputDatepicker.d.ts +1 -1
  39. package/dist-types/components/input-file/src/LionInputFile.d.ts +1 -1
  40. package/dist-types/components/input-file/src/LionSelectedFileList.d.ts +1 -1
  41. package/dist-types/components/localize/src/LocalizeManager.d.ts +108 -93
  42. package/dist-types/components/localize/src/LocalizeManager.d.ts.map +1 -1
  43. package/dist-types/components/overlays/src/OverlayController.d.ts +9 -5
  44. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  45. package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
  46. package/dist-types/components/overlays/test/OverlayController.test.d.ts +1 -1
  47. package/dist-types/components/overlays/test/OverlayController.test.d.ts.map +1 -1
  48. package/dist-types/components/overlays/test-helpers/mimicClick.d.ts +1 -1
  49. package/dist-types/components/progress-indicator/src/LionProgressIndicator.d.ts +5 -0
  50. package/dist-types/components/progress-indicator/src/LionProgressIndicator.d.ts.map +1 -1
  51. package/dist-types/components/select-rich/src/LionSelectRich.d.ts +1 -1
  52. package/dist-types/components/steps/src/LionStep.d.ts +5 -0
  53. package/dist-types/components/steps/src/LionStep.d.ts.map +1 -1
  54. package/dist-types/components/steps/src/LionSteps.d.ts +5 -0
  55. package/dist-types/components/steps/src/LionSteps.d.ts.map +1 -1
  56. package/dist-types/components/switch/src/LionSwitch.d.ts +1 -1
  57. package/dist-types/components/tabs/src/LionTabs.d.ts +5 -0
  58. package/dist-types/components/tabs/src/LionTabs.d.ts.map +1 -1
  59. package/dist-types/components/tooltip/src/LionTooltip.d.ts +5 -0
  60. package/dist-types/components/tooltip/src/LionTooltip.d.ts.map +1 -1
  61. package/dist-types/tsconfig.tsbuildinfo +1 -1
  62. package/docs/components/checkbox-group/overview.md +2 -2
  63. package/docs/components/collapsible/overview.md +1 -1
  64. package/docs/components/combobox/overview.md +1 -1
  65. package/docs/components/combobox/use-cases.md +1 -1
  66. package/docs/components/dialog/overview.md +1 -1
  67. package/docs/components/fieldset/overview.md +5 -5
  68. package/docs/components/form/overview.md +3 -3
  69. package/docs/components/icon/use-cases.md +1 -1
  70. package/docs/components/input/api-table.md +1 -1
  71. package/docs/components/input/overview.md +1 -1
  72. package/docs/components/input-amount/api-table.md +1 -1
  73. package/docs/components/input-amount/overview.md +3 -3
  74. package/docs/components/input-date/api-table.md +1 -1
  75. package/docs/components/input-date/overview.md +3 -3
  76. package/docs/components/input-datepicker/api-table.md +1 -1
  77. package/docs/components/input-datepicker/overview.md +5 -5
  78. package/docs/components/input-email/api-table.md +1 -1
  79. package/docs/components/input-email/overview.md +2 -2
  80. package/docs/components/input-file/api-table.md +1 -1
  81. package/docs/components/input-file/overview.md +1 -1
  82. package/docs/components/input-iban/api-table.md +1 -1
  83. package/docs/components/input-iban/overview.md +2 -2
  84. package/docs/components/input-range/api-table.md +1 -1
  85. package/docs/components/input-range/overview.md +2 -2
  86. package/docs/components/input-stepper/api-table.md +1 -1
  87. package/docs/components/input-stepper/overview.md +1 -1
  88. package/docs/components/input-tel/api-table.md +1 -1
  89. package/docs/components/input-tel/overview.md +1 -1
  90. package/docs/components/input-tel/use-cases.md +6 -6
  91. package/docs/components/input-tel-dropdown/api-table.md +1 -1
  92. package/docs/components/input-tel-dropdown/overview.md +1 -1
  93. package/docs/components/listbox/use-cases.md +1 -1
  94. package/docs/components/radio-group/api-table.md +1 -1
  95. package/docs/components/radio-group/overview.md +3 -3
  96. package/docs/components/select/api-table.md +1 -1
  97. package/docs/components/select/use-cases.md +1 -1
  98. package/docs/components/select-rich/api-table.md +1 -1
  99. package/docs/components/select-rich/use-cases.md +1 -1
  100. package/docs/components/switch/api-table.md +1 -1
  101. package/docs/components/tabs/examples.md +1 -1
  102. package/docs/components/textarea/api-table.md +1 -1
  103. package/docs/fundamentals/index.md +4 -4
  104. package/docs/fundamentals/node-tools/providence-analytics/LocalConfiguration.md +1 -1
  105. package/docs/fundamentals/node-tools/providence-analytics/QueryResult.md +1 -1
  106. package/docs/fundamentals/node-tools/providence-analytics/analyzer.md +1 -1
  107. package/docs/fundamentals/node-tools/providence-analytics/dashboard.md +1 -1
  108. package/docs/fundamentals/node-tools/providence-analytics/overview.md +3 -3
  109. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  110. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  111. package/docs/fundamentals/rationales/accessibility.md +1 -1
  112. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  113. package/docs/fundamentals/systems/core/overview.md +1 -1
  114. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  115. package/docs/fundamentals/systems/form/model-value.md +1 -1
  116. package/docs/fundamentals/systems/form/overview.md +28 -28
  117. package/docs/fundamentals/systems/form/validate.md +2 -2
  118. package/docs/fundamentals/systems/localize/api-table.md +38 -37
  119. package/docs/fundamentals/systems/localize/overview.md +4 -4
  120. package/docs/fundamentals/systems/overlays/api-table.md +1 -0
  121. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  122. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  123. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  124. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  125. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  126. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  127. package/docs/guides/how-to/create-a-form.md +4 -4
  128. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  129. package/docs/guides/index.md +8 -8
  130. package/docs/guides/principles/index.md +5 -5
  131. package/docs/guides/principles/styling.md +1 -1
  132. package/package.json +1 -1
@@ -1,202 +1,182 @@
1
- // @ts-ignore
1
+ // @ts-expect-error
2
2
  import MessageFormat from '@bundled-es-modules/message-format/MessageFormat.js';
3
+ import { isServer } from 'lit';
4
+
3
5
  import isLocalizeESModule from './isLocalizeESModule.js';
4
6
 
5
7
  /**
8
+ * @typedef {import('../types/LocalizeMixinTypes.js').NumberPostProcessor} NumberPostProcessor
9
+ * @typedef {import('../types/LocalizeMixinTypes.js').DatePostProcessor} DatePostProcessor
6
10
  * @typedef {import('../types/LocalizeMixinTypes.js').NamespaceObject} NamespaceObject
7
11
  */
8
12
 
9
- /** @typedef {import('../types/LocalizeMixinTypes.js').DatePostProcessor} DatePostProcessor */
10
- /** @typedef {import('../types/LocalizeMixinTypes.js').NumberPostProcessor} NumberPostProcessor */
13
+ /**
14
+ * We can't access `window.document.documentElement` on the server,
15
+ * so we write to and read from this object on the server.
16
+ * N.B.: for now, the goal is to make LocalizeManager not crash on the server, and let localization happen on the client.
17
+ * In the future, we might want to look into more advanced SSR of localized messages
18
+ */
19
+ const documentElement = isServer
20
+ ? { getAttribute: () => null, lang: '' }
21
+ : globalThis.document?.documentElement;
11
22
 
12
23
  /**
13
24
  * `LocalizeManager` manages your translations (includes loading)
14
25
  */
15
26
  export class LocalizeManager extends EventTarget {
16
- // eslint-disable-line no-unused-vars
17
- constructor({
18
- autoLoadOnLocaleChange = false,
19
- fallbackLocale = '',
20
- showKeyAsFallback = false,
21
- allowOverridesForExistingNamespaces = false,
22
- } = {}) {
23
- super();
24
-
25
- /** @protected */
26
- this._autoLoadOnLocaleChange = !!autoLoadOnLocaleChange;
27
- /** @protected */
28
- this._fallbackLocale = fallbackLocale;
29
- /** @protected */
30
- this._showKeyAsFallback = showKeyAsFallback;
31
-
32
- /** @private */
33
- this.__allowOverridesForExistingNamespaces = allowOverridesForExistingNamespaces;
34
-
35
- /**
36
- * @type {Object.<string, Object.<string, Object>>}
37
- * @private
38
- */
39
- this.__storage = {};
40
-
41
- /**
42
- * @type {Map.<RegExp|string, function>}
43
- * @private
44
- */
45
- this.__namespacePatternsMap = new Map();
46
-
47
- /**
48
- * @type {Object.<string, function|null>}
49
- * @private
50
- */
51
- this.__namespaceLoadersCache = {};
27
+ /**
28
+ * The localize system uses (normalized) Intl for formatting numbers.
29
+ * It's possible to customize this output per locale
30
+ */
31
+ formatNumberOptions = {
32
+ returnIfNaN: '',
33
+ /** @type {Map<string, NumberPostProcessor>} */
34
+ postProcessors: new Map(),
35
+ };
52
36
 
53
- /**
54
- * @type {Object.<string, Object.<string, Promise.<Object|void>>>}
55
- * @private
56
- */
57
- this.__namespaceLoaderPromisesCache = {};
37
+ /**
38
+ * The localize system uses (normalized) Intl for formatting dates.
39
+ * It's possible to customize this output per locale
40
+ */
41
+ formatDateOptions = {
42
+ /** @type {Map<string, DatePostProcessor>} */
43
+ postProcessors: new Map(),
44
+ };
58
45
 
59
- this.formatNumberOptions = {
60
- returnIfNaN: '',
61
- /** @type {Map<string,DatePostProcessor>} */
62
- postProcessors: new Map(),
63
- };
64
-
65
- this.formatDateOptions = {
66
- /** @type {Map<string,DatePostProcessor>} */
67
- postProcessors: new Map(),
68
- };
69
-
70
- /**
71
- * Via html[data-localize-lang], developers are allowed to set the initial locale, without
72
- * having to worry about whether locale is initialized before 3rd parties like Google Translate.
73
- * When this value differs from html[lang], we assume the 3rd party took
74
- * control over the page language and we set this._langAttrSetByTranslationTool to html[lang]
75
- */
76
- const initialLocale = document.documentElement.getAttribute('data-localize-lang');
46
+ /**
47
+ * Although it's common to configure the language via the html[lang] attribute,
48
+ * the lang attribute can be changed by 3rd party translation tools like Google Translate.
49
+ *
50
+ * ## Why is this a potential problem?
51
+ * The localize system reads from html[lang] for its original configuration. Let's say it's
52
+ * configured as "en-US" by the developer. This means all translation data are fetched for locale "en-US".
53
+ *
54
+ * Now the Google Translate plugin kicks in. It will automatically translate all English texts found into
55
+ * Chinese texts. Everything looks fine... But Google Translate also sets html[lang] to "zh-CN":
56
+ * our localize system responds by trying to fetch Chinese translation data.
57
+ * Two problems can occur here:
58
+ * - the Chinese translations don't exist
59
+ * - Google Translate expects to translate from English to Chinese... and now we suddenly serve Chinese text
60
+ * ourselves... not what we intended.
61
+ *
62
+ * ## How can we solve this?
63
+ * Via `html[data-localize-lang]`, developers are allowed to set the initial locale, without
64
+ * having to worry about whether locale is initialized before 3rd parties like Google Translate.
65
+ * When this value differs from html[lang], we assume the 3rd party took
66
+ * control over the page language and we set this.#localeSetByTranslationTool to html[lang]
67
+ */
68
+ #shouldHandleTranslationTools = false;
77
69
 
78
- /** @protected */
79
- this._supportExternalTranslationTools = Boolean(initialLocale);
70
+ /**
71
+ * The locale that is configured on html[data-localize-lang]
72
+ */
73
+ #localeProvidedViaDataLangAttr = '';
80
74
 
81
- if (this._supportExternalTranslationTools) {
82
- this.locale = initialLocale || 'en-GB';
83
- this._setupTranslationToolSupport();
84
- }
75
+ /**
76
+ * The locale that is set on html[lang] by a 3rd party translation tool like Googl Translate
77
+ * @type {string|null}
78
+ */
79
+ #localeSetByTranslationTool = null;
85
80
 
86
- if (!document.documentElement.lang) {
87
- document.documentElement.lang = this.locale || 'en-GB';
88
- }
81
+ /**
82
+ * @type {Object<string, Object<string, Object>>}
83
+ * @private
84
+ */
85
+ __storage = {};
89
86
 
90
- /** @protected */
91
- this._setupHtmlLangAttributeObserver();
92
- }
87
+ /**
88
+ * @type {Map<RegExp|string, function>}
89
+ * @private
90
+ */
91
+ __namespacePatternsMap = new Map();
93
92
 
94
- /** @protected */
95
- _setupTranslationToolSupport() {
96
- /**
97
- * This value allows for support for Google Translate (or other 3rd parties taking control
98
- * of the html[lang] attribute).
99
- *
100
- * Have the following scenario in mind:
101
- * 1. locale is initialized by developer via html[data-localize-lang="en-US"] and
102
- * html[lang="en-US"]. When localize is loaded (note that this also can be after step 2 below),
103
- * it will sync its initial state from html[data-localize-lang]
104
- * 2. Google Translate kicks in for the French language. It will set html[lang="fr"].
105
- * This new language is not one known by us, so we most likely don't have translations for
106
- * this file. Therefore, we do NOT sync this value to LocalizeManager. The manager should
107
- * still ask for known resources (in this case for locale 'en-US')
108
- * 3. locale is changed (think of a language dropdown)
109
- * It's a bit of a weird case, because we would not expect an end user to do this. If he/she
110
- * does, make sure that we do not go against Google Translate, so we maintain accessibility
111
- * (by not altering html[lang]). We detect this by reading _langAttrSetByTranslationTool:
112
- * when its value is null, we consider Google translate 'not active'.
113
- *
114
- * When Google Translate is turned off by the user (html[lang=auto]),
115
- * `localize.locale` will be synced to html[lang] again
116
- *
117
- * Keep in mind that all of the above also works with other tools than Google Translate,
118
- * but this is the most widely used tool and therefore used as an example.
119
- */
120
- this._langAttrSetByTranslationTool = document.documentElement.lang || null;
121
- }
93
+ /**
94
+ * @type {Object<string, function|null>}
95
+ * @private
96
+ */
97
+ __namespaceLoadersCache = {};
122
98
 
123
- teardown() {
124
- this._teardownHtmlLangAttributeObserver();
125
- }
99
+ /**
100
+ * @type {Object<string, Object<string, Promise<Object|void>>>}
101
+ * @private
102
+ */
103
+ __namespaceLoaderPromisesCache = {};
126
104
 
127
105
  /**
128
106
  * @returns {string}
129
107
  */
130
108
  get locale() {
131
- if (this._supportExternalTranslationTools) {
132
- return this.__locale || '';
109
+ if (!this.#shouldHandleTranslationTools) {
110
+ return documentElement.lang || '';
133
111
  }
134
- return document.documentElement.lang;
112
+ return this.#localeProvidedViaDataLangAttr || '';
135
113
  }
136
114
 
137
115
  /**
138
- * @param {string} value
116
+ * @param {string} newLocale
139
117
  */
140
- set locale(value) {
141
- /** @type {string} */
142
- let oldLocale;
143
- if (this._supportExternalTranslationTools) {
144
- oldLocale = /** @type {string} */ (this.__locale);
145
- this.__locale = value;
146
- if (this._langAttrSetByTranslationTool === null) {
147
- this._setHtmlLangAttribute(value);
148
- }
149
- } else {
150
- oldLocale = document.documentElement.lang;
151
- this._setHtmlLangAttribute(value);
152
- }
118
+ set locale(newLocale) {
119
+ this.#assertCorrectLocale(newLocale);
153
120
 
154
- if (!value.includes('-')) {
155
- this.__handleLanguageOnly(value);
121
+ if (!this.#shouldHandleTranslationTools) {
122
+ const oldLocale = documentElement.lang;
123
+ this._setHtmlLangAttribute(newLocale);
124
+ this._onLocaleChanged(newLocale, oldLocale);
125
+ return;
156
126
  }
157
127
 
158
- this._onLocaleChanged(value, oldLocale);
128
+ const oldLocale = /** @type {string} */ (this.#localeProvidedViaDataLangAttr);
129
+ this.#localeProvidedViaDataLangAttr = newLocale;
130
+ const isLangAutoOrTranslationToolNotProcessed = this.#localeSetByTranslationTool === null;
131
+ if (isLangAutoOrTranslationToolNotProcessed) {
132
+ this._setHtmlLangAttribute(newLocale);
133
+ }
134
+ this._onLocaleChanged(newLocale, oldLocale);
159
135
  }
160
136
 
161
137
  /**
162
- * @param {string} locale
163
- * @protected
138
+ * @readonly
139
+ * @returns {Promise<Object|void>}
164
140
  */
165
- _setHtmlLangAttribute(locale) {
166
- this._teardownHtmlLangAttributeObserver();
167
- document.documentElement.lang = locale;
168
- this._setupHtmlLangAttributeObserver();
169
- }
141
+ get loadingComplete() {
142
+ const hasPendingCacheForLocale =
143
+ typeof this.__namespaceLoaderPromisesCache[this.locale] === 'object';
170
144
 
171
- /**
172
- * @param {string} value
173
- * @throws {Error} Language only locales are not allowed(Use 'en-GB' instead of 'en')
174
- * @private
175
- */
176
- // eslint-disable-next-line class-methods-use-this
177
- __handleLanguageOnly(value) {
178
- throw new Error(`
179
- Locale was set to ${value}.
180
- Language only locales are not allowed, please use the full language locale e.g. 'en-GB' instead of 'en'.
181
- See https://github.com/ing-bank/lion/issues/187 for more information.
182
- `);
145
+ return !hasPendingCacheForLocale
146
+ ? Promise.resolve()
147
+ : Promise.all(Object.values(this.__namespaceLoaderPromisesCache[this.locale]));
183
148
  }
184
149
 
185
- /**
186
- * @returns {Promise.<Object|void>}
187
- */
188
- get loadingComplete() {
189
- if (typeof this.__namespaceLoaderPromisesCache[this.locale] === 'object') {
190
- return Promise.all(Object.values(this.__namespaceLoaderPromisesCache[this.locale]));
150
+ constructor({
151
+ allowOverridesForExistingNamespaces = false,
152
+ autoLoadOnLocaleChange = false,
153
+ showKeyAsFallback = false,
154
+ fallbackLocale = '',
155
+ } = {}) {
156
+ super();
157
+
158
+ /** @private */
159
+ this.__allowOverridesForExistingNamespaces = allowOverridesForExistingNamespaces;
160
+ /** @protected */
161
+ this._autoLoadOnLocaleChange = !!autoLoadOnLocaleChange;
162
+ /** @protected */
163
+ this._showKeyAsFallback = showKeyAsFallback;
164
+ /** @protected */
165
+ this._fallbackLocale = fallbackLocale;
166
+
167
+ const localeProvidedViaDataLangAttr = documentElement.getAttribute('data-localize-lang');
168
+ this.#shouldHandleTranslationTools = Boolean(localeProvidedViaDataLangAttr);
169
+
170
+ if (this.#shouldHandleTranslationTools) {
171
+ this.locale = /** @type {string} */ (localeProvidedViaDataLangAttr);
172
+ this._setupTranslationToolSupport();
191
173
  }
192
- return Promise.resolve();
193
- }
194
174
 
195
- reset() {
196
- this.__storage = {};
197
- this.__namespacePatternsMap = new Map();
198
- this.__namespaceLoadersCache = {};
199
- this.__namespaceLoaderPromisesCache = {};
175
+ if (!documentElement.lang) {
176
+ documentElement.lang = this.locale || 'en-GB';
177
+ }
178
+
179
+ this._setupHtmlLangAttributeObserver();
200
180
  }
201
181
 
202
182
  /**
@@ -240,7 +220,7 @@ export class LocalizeManager extends EventTarget {
240
220
  * @param {NamespaceObject[]} namespaces
241
221
  * @param {Object} options
242
222
  * @param {string} [options.locale]
243
- * @returns {Promise.<Object>}
223
+ * @returns {Promise<Object>}
244
224
  */
245
225
  loadNamespaces(namespaces, { locale } = {}) {
246
226
  return Promise.all(
@@ -255,7 +235,7 @@ export class LocalizeManager extends EventTarget {
255
235
  * @param {NamespaceObject} namespaceObj
256
236
  * @param {Object} options
257
237
  * @param {string} [options.locale]
258
- * @returns {Promise.<Object|void>}
238
+ * @returns {Promise<Object|void>}
259
239
  */
260
240
  loadNamespace(namespaceObj, { locale = this.locale } = { locale: this.locale }) {
261
241
  const isDynamicImport = typeof namespaceObj === 'object';
@@ -278,7 +258,7 @@ export class LocalizeManager extends EventTarget {
278
258
 
279
259
  /**
280
260
  * @param {string | string[]} keys
281
- * @param {Object.<string,?>} [vars]
261
+ * @param {Object<string,?>} [vars]
282
262
  * @param {Object} [opts]
283
263
  * @param {string} [opts.locale]
284
264
  * @returns {string}
@@ -293,18 +273,84 @@ export class LocalizeManager extends EventTarget {
293
273
  return formatter.format(vars);
294
274
  }
295
275
 
276
+ teardown() {
277
+ this._teardownHtmlLangAttributeObserver();
278
+ }
279
+
280
+ reset() {
281
+ this.__storage = {};
282
+ this.__namespacePatternsMap = new Map();
283
+ this.__namespaceLoadersCache = {};
284
+ this.__namespaceLoaderPromisesCache = {};
285
+ }
286
+
287
+ /**
288
+ * @param {{locale:string, postProcessor:DatePostProcessor}} options
289
+ */
290
+ setDatePostProcessorForLocale({ locale, postProcessor }) {
291
+ this.formatDateOptions?.postProcessors.set(locale, postProcessor);
292
+ }
293
+
294
+ /**
295
+ * @param {{locale:string, postProcessor:NumberPostProcessor}} options
296
+ */
297
+ setNumberPostProcessorForLocale({ locale, postProcessor }) {
298
+ this.formatNumberOptions?.postProcessors.set(locale, postProcessor);
299
+ }
300
+
301
+ /**
302
+ * This value allows for support for Google Translate (or other 3rd parties taking control
303
+ * of the html[lang] attribute).
304
+ *
305
+ * Have the following scenario in mind:
306
+ * 1. locale is initialized by developer via html[data-localize-lang="en-US"] and
307
+ * html[lang="en-US"]. When localize is loaded (note that this also can be after step 2 below),
308
+ * it will sync its initial state from html[data-localize-lang]
309
+ * 2. Google Translate kicks in for the French language. It will set html[lang="fr"].
310
+ * This new language is not one known by us, so we most likely don't have translations for
311
+ * this file. Therefore, we do NOT sync this value to LocalizeManager. The manager should
312
+ * still ask for known resources (in this case for locale 'en-US')
313
+ * 3. locale is changed (think of a language dropdown)
314
+ * It's a bit of a weird case, because we would not expect an end user to do this. If he/she
315
+ * does, make sure that we do not go against Google Translate, so we maintain accessibility
316
+ * (by not altering html[lang]). We detect this by reading #localeSetByTranslationTool:
317
+ * when its value is null, we consider Google translate 'not active'.
318
+ *
319
+ * When Google Translate is turned off by the user (html[lang=auto]),
320
+ * `localize.locale` will be synced to html[lang] again
321
+ *
322
+ * Keep in mind that all of the above also works with other tools than Google Translate,
323
+ * but this is the most widely used tool and therefore used as an example.
324
+ * @protected
325
+ */
326
+ _setupTranslationToolSupport() {
327
+ this.#localeSetByTranslationTool = documentElement.lang || null;
328
+ }
329
+
330
+ /**
331
+ * @param {string} locale
332
+ * @protected
333
+ */
334
+ _setHtmlLangAttribute(locale) {
335
+ this._teardownHtmlLangAttributeObserver();
336
+ documentElement.lang = locale;
337
+ this._setupHtmlLangAttributeObserver();
338
+ }
339
+
296
340
  /** @protected */
297
341
  _setupHtmlLangAttributeObserver() {
342
+ if (isServer) return;
343
+
298
344
  if (!this._htmlLangAttributeObserver) {
299
345
  this._htmlLangAttributeObserver = new MutationObserver(mutations => {
300
346
  mutations.forEach(mutation => {
301
- if (this._supportExternalTranslationTools) {
302
- if (document.documentElement.lang === 'auto') {
347
+ if (this.#shouldHandleTranslationTools) {
348
+ if (documentElement.lang === 'auto') {
303
349
  // Google Translate is switched off
304
- this._langAttrSetByTranslationTool = null;
350
+ this.#localeSetByTranslationTool = null;
305
351
  this._setHtmlLangAttribute(this.locale);
306
352
  } else {
307
- this._langAttrSetByTranslationTool = document.documentElement.lang;
353
+ this.#localeSetByTranslationTool = document.documentElement.lang;
308
354
  }
309
355
  } else {
310
356
  this._onLocaleChanged(document.documentElement.lang, mutation.oldValue || '');
@@ -352,7 +398,7 @@ export class LocalizeManager extends EventTarget {
352
398
  * @param {NamespaceObject} namespaceObj
353
399
  * @param {boolean} isDynamicImport
354
400
  * @param {string} namespace
355
- * @returns {Promise.<Object|void>}
401
+ * @returns {Promise<Object|void>}
356
402
  * @protected
357
403
  */
358
404
  _loadNamespaceData(locale, namespaceObj, isDynamicImport, namespace) {
@@ -388,7 +434,7 @@ export class LocalizeManager extends EventTarget {
388
434
  let loader = this.__namespaceLoadersCache[namespace];
389
435
  if (!loader) {
390
436
  if (isDynamicImport) {
391
- const _namespaceObj = /** @type {Object.<string,function>} */ (namespaceObj);
437
+ const _namespaceObj = /** @type {Object<string,function>} */ (namespaceObj);
392
438
  loader = _namespaceObj[namespace];
393
439
  this.__namespaceLoadersCache[namespace] = loader;
394
440
  } else {
@@ -411,7 +457,7 @@ export class LocalizeManager extends EventTarget {
411
457
  * @param {string} locale
412
458
  * @param {string} namespace
413
459
  * @param {string} [fallbackLocale]
414
- * @returns {Promise.<any>}
460
+ * @returns {Promise<any>}
415
461
  * @throws {Error} Data for namespace and (locale or fallback locale) could not be loaded.
416
462
  * @protected
417
463
  */
@@ -441,7 +487,7 @@ export class LocalizeManager extends EventTarget {
441
487
  /**
442
488
  * @param {string} locale
443
489
  * @param {string} namespace
444
- * @param {Promise.<Object|void>} promise
490
+ * @param {Promise<Object|void>} promise
445
491
  * @protected
446
492
  */
447
493
  _cacheNamespaceLoaderPromise(locale, namespace, promise) {
@@ -489,9 +535,8 @@ export class LocalizeManager extends EventTarget {
489
535
  _onLocaleChanged(newLocale, oldLocale) {
490
536
  // Event firing immediately, does not wait for loading the translations
491
537
  this.dispatchEvent(new CustomEvent('__localeChanging'));
492
- if (newLocale === oldLocale) {
493
- return;
494
- }
538
+ if (newLocale === oldLocale) return;
539
+
495
540
  if (this._autoLoadOnLocaleChange) {
496
541
  this._loadAllMissing(newLocale, oldLocale);
497
542
  this.loadingComplete.then(() => {
@@ -563,7 +608,7 @@ export class LocalizeManager extends EventTarget {
563
608
  const names = namesString.split('.');
564
609
  const result = names.reduce(
565
610
  /**
566
- * @param {Object.<string, any> | string} message
611
+ * @param {Object<string, any> | string} message
567
612
  * @param {string} name
568
613
  * @returns {string}
569
614
  */
@@ -575,16 +620,51 @@ export class LocalizeManager extends EventTarget {
575
620
  }
576
621
 
577
622
  /**
578
- * @param {{locale:string, postProcessor:DatePostProcessor}} options
623
+ * @param {string} value
624
+ * @throws {Error} Language only locales are not allowed(Use 'en-GB' instead of 'en')
579
625
  */
580
- setDatePostProcessorForLocale({ locale, postProcessor }) {
581
- this.formatDateOptions.postProcessors.set(locale, postProcessor);
626
+ // eslint-disable-next-line class-methods-use-this
627
+ #assertCorrectLocale(value) {
628
+ if (value.includes('-')) return;
629
+
630
+ throw new Error(`
631
+ Locale was set to ${value}.
632
+ Language only locales are not allowed, please use the full language locale e.g. 'en-GB' instead of 'en'.
633
+ See https://github.com/ing-bank/lion/issues/187 for more information.
634
+ `);
582
635
  }
583
636
 
637
+ // === TODO: delete below in a next breaking release ---
638
+
584
639
  /**
585
- * @param {{locale:string, postProcessor:NumberPostProcessor}} options
640
+ * @deprecated
641
+ * @protected
586
642
  */
587
- setNumberPostProcessorForLocale({ locale, postProcessor }) {
588
- this.formatNumberOptions.postProcessors.set(locale, postProcessor);
643
+ get _supportExternalTranslationTools() {
644
+ return this.#shouldHandleTranslationTools;
645
+ }
646
+
647
+ /**
648
+ * @deprecated
649
+ * @protected
650
+ */
651
+ set _supportExternalTranslationTools(supportsThem) {
652
+ this.#shouldHandleTranslationTools = supportsThem;
653
+ }
654
+
655
+ /**
656
+ * @deprecated
657
+ * @protected
658
+ */
659
+ get _langAttrSetByTranslationTool() {
660
+ return this.#localeProvidedViaDataLangAttr;
661
+ }
662
+
663
+ /**
664
+ * @deprecated
665
+ * @protected
666
+ */
667
+ set _langAttrSetByTranslationTool(newValue) {
668
+ this.#localeProvidedViaDataLangAttr = newValue;
589
669
  }
590
670
  }
@@ -1,16 +1,17 @@
1
1
  import { overlays } from './singleton.js';
2
2
  import { containFocus } from './utils/contain-focus.js';
3
+ import { deepContains } from './utils/deep-contains.js';
3
4
  import { overlayShadowDomStyle } from './overlayShadowDomStyle.js';
4
5
  import { _adoptStyleUtils } from './utils/adopt-styles.js';
5
6
 
6
7
  /**
7
- * @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
8
+ * @typedef {'setup'|'init'|'teardown'|'before-show'|'show'|'hide'|'add'|'remove'} OverlayPhase
8
9
  * @typedef {import('@lion/ui/types/overlays.js').ViewportConfig} ViewportConfig
9
- * @typedef {import('@popperjs/core').createPopper} Popper
10
+ * @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
10
11
  * @typedef {import('@popperjs/core').Options} PopperOptions
11
12
  * @typedef {import('@popperjs/core').Placement} Placement
13
+ * @typedef {import('@popperjs/core').createPopper} Popper
12
14
  * @typedef {{ createPopper: Popper }} PopperModule
13
- * @typedef {'setup'|'init'|'teardown'|'before-show'|'show'|'hide'|'add'|'remove'} OverlayPhase
14
15
  */
15
16
 
16
17
  /**
@@ -103,6 +104,8 @@ async function preloadPopper() {
103
104
  return /** @type {* & Promise<PopperModule>} */ (import('@popperjs/core/dist/esm/popper.js'));
104
105
  }
105
106
 
107
+ const childDialogsClosedInEventLoopWeakmap = new WeakMap();
108
+
106
109
  /**
107
110
  * OverlayController is the fundament for every single type of overlay. With the right
108
111
  * configuration, it can be used to build (modal) dialogs, tooltips, dropdowns, popovers,
@@ -498,9 +501,6 @@ export class OverlayController extends EventTarget {
498
501
  '[OverlayController] .isTooltip only takes effect when .handlesAccessibility is enabled',
499
502
  );
500
503
  }
501
- // if (newConfig.popperConfig.modifiers.arrow && !newConfig.contentWrapperNode) {
502
- // throw new Error('You need to provide a .contentWrapperNode when Popper arrow is enabled');
503
- // }
504
504
  }
505
505
 
506
506
  /**
@@ -1146,11 +1146,40 @@ export class OverlayController extends EventTarget {
1146
1146
  ev.preventDefault();
1147
1147
  }
1148
1148
 
1149
- /** @private */
1150
- __escKeyHandler(/** @type {KeyboardEvent} */ ev) {
1151
- return ev.key === 'Escape' && this.hide();
1149
+ /**
1150
+ * @param {KeyboardEvent} event
1151
+ * @returns {void}
1152
+ */
1153
+ __escKeyHandler(event) {
1154
+ if (event.key !== 'Escape' || childDialogsClosedInEventLoopWeakmap.has(event)) return;
1155
+
1156
+ const hasPressedInside =
1157
+ event.composedPath().includes(this.contentNode) ||
1158
+ deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
1159
+ if (hasPressedInside) {
1160
+ this.hide();
1161
+ // We could do event.stopPropagation() here, but we don't want to hide info for
1162
+ // the outside world about user interactions. Instead, we store the event in a WeakMap
1163
+ // that will be garbage collected after the event loop.
1164
+ childDialogsClosedInEventLoopWeakmap.set(event, this);
1165
+ }
1152
1166
  }
1153
1167
 
1168
+ /**
1169
+ * @param {KeyboardEvent} event
1170
+ * @returns {void}
1171
+ */
1172
+ #outsideEscKeyHandler = event => {
1173
+ if (event.key !== 'Escape') return;
1174
+
1175
+ const hasPressedInside =
1176
+ event.composedPath().includes(this.contentNode) ||
1177
+ deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
1178
+ if (!hasPressedInside) {
1179
+ this.hide();
1180
+ }
1181
+ };
1182
+
1154
1183
  /**
1155
1184
  * @param {{ phase: OverlayPhase }} config
1156
1185
  * @protected
@@ -1175,11 +1204,9 @@ export class OverlayController extends EventTarget {
1175
1204
  */
1176
1205
  _handleHidesOnOutsideEsc({ phase }) {
1177
1206
  if (phase === 'show') {
1178
- this.__escKeyHandler = (/** @type {KeyboardEvent} */ ev) =>
1179
- ev.key === 'Escape' && this.hide();
1180
- document.addEventListener('keyup', this.__escKeyHandler);
1207
+ document.addEventListener('keyup', this.#outsideEscKeyHandler);
1181
1208
  } else if (phase === 'hide') {
1182
- document.removeEventListener('keyup', this.__escKeyHandler);
1209
+ document.removeEventListener('keyup', this.#outsideEscKeyHandler);
1183
1210
  }
1184
1211
  }
1185
1212
 
@@ -189,6 +189,13 @@ export const OverlayMixinImplementation = superclass =>
189
189
  }
190
190
  }
191
191
 
192
+ /**
193
+ * // TODO: check if this is a false positive or if we can improve
194
+ * @configure ReactiveElement
195
+ */
196
+ // @ts-expect-error
197
+ static enabledWarnings = super.enabledWarnings?.filter(w => w !== 'change-in-update') || [];
198
+
192
199
  get _overlayInvokerNode() {
193
200
  return /** @type {HTMLElement | undefined} */ (
194
201
  Array.from(this.children).find(child => child.slot === 'invoker')