@lion/ui 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +35 -0
  2. package/components/combobox/src/LionCombobox.js +10 -2
  3. package/components/combobox/test/lion-combobox.test.js +31 -0
  4. package/components/form-core/src/choice-group/ChoiceGroupMixin.js +7 -0
  5. package/components/form-core/types/choice-group/ChoiceGroupMixinTypes.ts +5 -0
  6. package/components/input-amount/src/LionInputAmount.js +17 -6
  7. package/components/input-amount/src/formatters.js +5 -3
  8. package/components/input-amount/test/formatters.test.js +88 -74
  9. package/components/input-amount/test/lion-input-amount.test.js +51 -36
  10. package/components/input-tel/test/validators.test.js +9 -0
  11. package/components/listbox/src/ListboxMixin.js +19 -3
  12. package/components/listbox/types/ListboxMixinTypes.ts +7 -1
  13. package/components/menu/README.md +360 -0
  14. package/components/menu/src/InteractiveListMixin.js +867 -0
  15. package/components/menu/src/LionMenu.js +436 -0
  16. package/components/menu/src/MoreButtonMenuMixin.js +295 -0
  17. package/components/menu/src/MultiLevelListMixin.js +312 -0
  18. package/components/menu/src/utils/isInView.js +26 -0
  19. package/components/menu/src/utils/listItemInteractions.js +124 -0
  20. package/components/menu/test/InteractiveListMixin.test.js +3 -0
  21. package/components/menu/test/MoreButtonMenuMixin.test.js +4 -0
  22. package/components/menu/test/MultiLevelListMixin.test.js +3 -0
  23. package/components/menu/test/lion-menu-interactions.test.js +3 -0
  24. package/components/menu/test/lion-menu.test.js +14 -0
  25. package/components/menu/test-helpers/mimicKeyPress.js +9 -0
  26. package/components/menu/test-suites/InteractiveListMixin.suite.js +537 -0
  27. package/components/menu/test-suites/LionMenu.suite.js +114 -0
  28. package/components/menu/test-suites/LionMenuInteractions.suite.js +583 -0
  29. package/components/menu/test-suites/MoreButtonMenuMixin.suite.js +372 -0
  30. package/components/menu/test-suites/MultiLevelListMixin.suite.js +309 -0
  31. package/components/menu/types/InteractiveListMixinTypes.ts +126 -0
  32. package/components/menu/types/MoreButtonMenuMixinTypes.ts +51 -0
  33. package/components/menu/types/MultiLevelListMixinTypes.ts +28 -0
  34. package/components/menu/types/OverlayWithListInvokerMixinTypes.ts +61 -0
  35. package/components/overlays/NEXT.md +49 -0
  36. package/components/overlays/src/OverlayController.js +304 -125
  37. package/components/overlays/src/OverlayMixin.js +81 -17
  38. package/components/overlays/src/utils/adopt-styles.js +1 -1
  39. package/components/overlays/src/utils/is-equal-config.js +15 -6
  40. package/components/overlays/test/OverlayController.test.js +223 -25
  41. package/components/overlays/test-suites/OverlayMixin.suite.js +8 -8
  42. package/components/overlays/types/ArrowMixinTypes.ts +1 -3
  43. package/components/overlays/types/OverlayConfig.ts +20 -5
  44. package/components/overlays/types/OverlayMixinTypes.ts +2 -0
  45. package/components/select-rich/src/LionSelectRich.js +12 -0
  46. package/components/select-rich/test/lion-select-rich-interaction.test.js +53 -0
  47. package/components/select-rich/test/lion-select-rich-upgrade.test.js +30 -0
  48. package/custom-elements.json +3120 -149
  49. package/dist-types/components/combobox/src/LionCombobox.d.ts +1 -1
  50. package/dist-types/components/combobox/src/LionCombobox.d.ts.map +1 -1
  51. package/dist-types/components/dialog/src/LionDialog.d.ts +6 -2
  52. package/dist-types/components/dialog/src/LionDialog.d.ts.map +1 -1
  53. package/dist-types/components/form-core/src/choice-group/ChoiceGroupMixin.d.ts.map +1 -1
  54. package/dist-types/components/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts +5 -0
  55. package/dist-types/components/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts.map +1 -1
  56. package/dist-types/components/input-amount/src/LionInputAmount.d.ts +3 -1
  57. package/dist-types/components/input-amount/src/LionInputAmount.d.ts.map +1 -1
  58. package/dist-types/components/input-amount/src/formatters.d.ts +6 -4
  59. package/dist-types/components/input-amount/src/formatters.d.ts.map +1 -1
  60. package/dist-types/components/input-datepicker/src/LionInputDatepicker.d.ts +1 -1
  61. package/dist-types/components/listbox/src/ListboxMixin.d.ts.map +1 -1
  62. package/dist-types/components/listbox/types/ListboxMixinTypes.d.ts +7 -1
  63. package/dist-types/components/listbox/types/ListboxMixinTypes.d.ts.map +1 -1
  64. package/dist-types/components/menu/src/InteractiveListMixin.d.ts +35 -0
  65. package/dist-types/components/menu/src/InteractiveListMixin.d.ts.map +1 -0
  66. package/dist-types/components/menu/src/LionMenu.d.ts +129 -0
  67. package/dist-types/components/menu/src/LionMenu.d.ts.map +1 -0
  68. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts +8 -0
  69. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts.map +1 -0
  70. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts +7 -0
  71. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts.map +1 -0
  72. package/dist-types/components/menu/src/utils/isInView.d.ts +9 -0
  73. package/dist-types/components/menu/src/utils/isInView.d.ts.map +1 -0
  74. package/dist-types/components/menu/src/utils/listItemInteractions.d.ts +48 -0
  75. package/dist-types/components/menu/src/utils/listItemInteractions.d.ts.map +1 -0
  76. package/dist-types/components/menu/test/InteractiveListMixin.test.d.ts +2 -0
  77. package/dist-types/components/menu/test/InteractiveListMixin.test.d.ts.map +1 -0
  78. package/dist-types/components/menu/test/MoreButtonMenuMixin.test.d.ts +2 -0
  79. package/dist-types/components/menu/test/MoreButtonMenuMixin.test.d.ts.map +1 -0
  80. package/dist-types/components/menu/test/MultiLevelListMixin.test.d.ts +2 -0
  81. package/dist-types/components/menu/test/MultiLevelListMixin.test.d.ts.map +1 -0
  82. package/dist-types/components/menu/test/lion-menu-interactions.test.d.ts +2 -0
  83. package/dist-types/components/menu/test/lion-menu-interactions.test.d.ts.map +1 -0
  84. package/dist-types/components/menu/test/lion-menu.test.d.ts +2 -0
  85. package/dist-types/components/menu/test/lion-menu.test.d.ts.map +1 -0
  86. package/dist-types/components/menu/test-helpers/mimicKeyPress.d.ts +7 -0
  87. package/dist-types/components/menu/test-helpers/mimicKeyPress.d.ts.map +1 -0
  88. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts +11 -0
  89. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts.map +1 -0
  90. package/dist-types/components/menu/test-suites/LionMenu.suite.d.ts +9 -0
  91. package/dist-types/components/menu/test-suites/LionMenu.suite.d.ts.map +1 -0
  92. package/dist-types/components/menu/test-suites/LionMenuInteractions.suite.d.ts +5 -0
  93. package/dist-types/components/menu/test-suites/LionMenuInteractions.suite.d.ts.map +1 -0
  94. package/dist-types/components/menu/test-suites/MoreButtonMenuMixin.suite.d.ts +11 -0
  95. package/dist-types/components/menu/test-suites/MoreButtonMenuMixin.suite.d.ts.map +1 -0
  96. package/dist-types/components/menu/test-suites/MultiLevelListMixin.suite.d.ts +11 -0
  97. package/dist-types/components/menu/test-suites/MultiLevelListMixin.suite.d.ts.map +1 -0
  98. package/dist-types/components/menu/types/InteractiveListMixinTypes.d.ts +72 -0
  99. package/dist-types/components/menu/types/InteractiveListMixinTypes.d.ts.map +1 -0
  100. package/dist-types/components/menu/types/MoreButtonMenuMixinTypes.d.ts +30 -0
  101. package/dist-types/components/menu/types/MoreButtonMenuMixinTypes.d.ts.map +1 -0
  102. package/dist-types/components/menu/types/MultiLevelListMixinTypes.d.ts +16 -0
  103. package/dist-types/components/menu/types/MultiLevelListMixinTypes.d.ts.map +1 -0
  104. package/dist-types/components/menu/types/OverlayWithListInvokerMixinTypes.d.ts +35 -0
  105. package/dist-types/components/menu/types/OverlayWithListInvokerMixinTypes.d.ts.map +1 -0
  106. package/dist-types/components/overlays/src/OverlayController.d.ts +22 -5
  107. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  108. package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
  109. package/dist-types/components/overlays/src/utils/is-equal-config.d.ts.map +1 -1
  110. package/dist-types/components/overlays/types/ArrowMixinTypes.d.ts.map +1 -1
  111. package/dist-types/components/overlays/types/OverlayConfig.d.ts +12 -2
  112. package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
  113. package/dist-types/components/overlays/types/OverlayMixinTypes.d.ts +1 -0
  114. package/dist-types/components/overlays/types/OverlayMixinTypes.d.ts.map +1 -1
  115. package/dist-types/components/select-rich/src/LionSelectRich.d.ts +13 -2
  116. package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
  117. package/dist-types/components/select-rich/test/lion-select-rich-interaction.test.d.ts.map +1 -1
  118. package/dist-types/components/select-rich/test/lion-select-rich-upgrade.test.d.ts +2 -0
  119. package/dist-types/components/select-rich/test/lion-select-rich-upgrade.test.d.ts.map +1 -0
  120. package/dist-types/components/tooltip/src/LionTooltip.d.ts +1 -1
  121. package/dist-types/exports/define/lion-item.d.ts +2 -0
  122. package/dist-types/exports/define/lion-item.d.ts.map +1 -0
  123. package/dist-types/exports/define/lion-menu.d.ts +2 -0
  124. package/dist-types/exports/define/lion-menu.d.ts.map +1 -0
  125. package/dist-types/exports/define/lion-menuitem.d.ts +2 -0
  126. package/dist-types/exports/define/lion-menuitem.d.ts.map +1 -0
  127. package/dist-types/exports/menu-test-suites.d.ts +4 -0
  128. package/dist-types/exports/menu-test-suites.d.ts.map +1 -0
  129. package/dist-types/exports/menu.d.ts +3 -0
  130. package/dist-types/exports/menu.d.ts.map +1 -0
  131. package/dist-types/exports/overlays.d.ts +6 -0
  132. package/dist-types/exports/overlays.d.ts.map +1 -1
  133. package/dist-types/tsconfig.tsbuildinfo +1 -1
  134. package/docs/components/checkbox-group/overview.md +2 -2
  135. package/docs/components/collapsible/overview.md +1 -1
  136. package/docs/components/combobox/api-table.md +53 -39
  137. package/docs/components/combobox/overview.md +1 -1
  138. package/docs/components/combobox/use-cases.md +1 -1
  139. package/docs/components/dialog/api-table.md +32 -6
  140. package/docs/components/dialog/overview.md +1 -1
  141. package/docs/components/fieldset/overview.md +5 -5
  142. package/docs/components/form/overview.md +3 -3
  143. package/docs/components/form/use-cases.md +2 -2
  144. package/docs/components/icon/use-cases.md +1 -1
  145. package/docs/components/input/overview.md +1 -1
  146. package/docs/components/input-amount/api-table.md +9 -7
  147. package/docs/components/input-amount/overview.md +3 -3
  148. package/docs/components/input-amount/use-cases.md +15 -4
  149. package/docs/components/input-amount-dropdown/api-table.md +2 -0
  150. package/docs/components/input-date/overview.md +3 -3
  151. package/docs/components/input-datepicker/api-table.md +63 -45
  152. package/docs/components/input-datepicker/overview.md +5 -5
  153. package/docs/components/input-email/overview.md +2 -2
  154. package/docs/components/input-file/overview.md +1 -1
  155. package/docs/components/input-iban/overview.md +2 -2
  156. package/docs/components/input-range/overview.md +2 -2
  157. package/docs/components/input-stepper/overview.md +1 -1
  158. package/docs/components/input-tel/overview.md +1 -1
  159. package/docs/components/input-tel/use-cases.md +6 -6
  160. package/docs/components/input-tel-dropdown/overview.md +1 -1
  161. package/docs/components/listbox/use-cases.md +1 -1
  162. package/docs/components/menu/api-table.md +44 -0
  163. package/docs/components/menu/applyMenuDemoStyles.js +173 -0
  164. package/docs/components/menu/extensions.md +262 -0
  165. package/docs/components/menu/index.md +13 -0
  166. package/docs/components/menu/migration.md +129 -0
  167. package/docs/components/menu/overview.md +448 -0
  168. package/docs/components/menu/src/DemoNavigationBar.js +441 -0
  169. package/docs/components/menu/src/DemoSpinButton.js +215 -0
  170. package/docs/components/menu/src/DemoToolbar.js +101 -0
  171. package/docs/components/menu/src/DemoTree.js +55 -0
  172. package/docs/components/menu/src/navigation-bar-types.js +41 -0
  173. package/docs/components/menu/use-cases.md +74 -0
  174. package/docs/components/radio-group/overview.md +3 -3
  175. package/docs/components/select/use-cases.md +1 -1
  176. package/docs/components/select-rich/api-table.md +51 -33
  177. package/docs/components/select-rich/use-cases.md +2 -2
  178. package/docs/components/tabs/examples.md +1 -1
  179. package/docs/components/tooltip/api-table.md +33 -7
  180. package/docs/fundamentals/index.md +4 -4
  181. package/docs/fundamentals/node-tools/providence-analytics/overview.md +1 -1
  182. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  183. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  184. package/docs/fundamentals/rationales/accessibility.md +1 -1
  185. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  186. package/docs/fundamentals/systems/core/overview.md +1 -1
  187. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  188. package/docs/fundamentals/systems/form/model-value.md +1 -1
  189. package/docs/fundamentals/systems/form/overview.md +28 -28
  190. package/docs/fundamentals/systems/form/validate.md +2 -2
  191. package/docs/fundamentals/systems/localize/overview.md +4 -4
  192. package/docs/fundamentals/systems/overlays/api-table.md +46 -43
  193. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  194. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  195. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  196. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  197. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  198. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  199. package/docs/guides/how-to/create-a-form.md +4 -4
  200. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  201. package/docs/guides/how-to/index.md +1 -1
  202. package/docs/guides/index.md +8 -8
  203. package/docs/guides/principles/index.md +5 -5
  204. package/docs/guides/principles/styling.md +1 -1
  205. package/exports/define/lion-item.js +3 -0
  206. package/exports/define/lion-menu.js +3 -0
  207. package/exports/define/lion-menuitem.js +3 -0
  208. package/exports/menu-test-suites.js +4 -0
  209. package/exports/menu.js +3 -0
  210. package/exports/overlays.js +6 -0
  211. package/menu-test-suites.js +4 -0
  212. package/menu.js +3 -0
  213. package/overlays.js +6 -0
  214. package/package.json +11 -4
  215. package/vendor/README.md +61 -0
  216. package/vendor/awesome-phonenumber-7.9.0-tlouisse.libphonenumber-9.0.41.tgz +0 -0
  217. package/vscode.html-custom-data.json +76 -8
  218. package/web-types.json +202 -17
package/README.md CHANGED
@@ -112,6 +112,41 @@ They provide an unopinionated, white-label layer that can be extended to your ow
112
112
  npm i @lion/ui
113
113
  ```
114
114
 
115
+ ### Temporary phone-number metadata workaround
116
+
117
+ We temporarily vendor `awesome-phonenumber@7.9.0-tlouisse.libphonenumber-9.0.41`,
118
+ rebuilt from Google's **libphonenumber v9.0.41**, instead of the registry dependency.
119
+ This includes updated phone-number metadata, such as the French Guiana premium-rate
120
+ example `+594890123456`, which the previous `awesome-phonenumber@7.8.0` rejected.
121
+ We keep this workaround until the next upstream awesome-phonenumber release
122
+ incorporating the required metadata is available.
123
+
124
+ The artifact lives in `packages/ui/vendor/awesome-phonenumber-7.9.0-tlouisse.libphonenumber-9.0.41.tgz`.
125
+ `@lion/ui` uses a package-relative `file:vendor/...` dependency and bundles the
126
+ installed `awesome-phonenumber` package when published. This preserves normal
127
+ consumer installation and lazy imports without access to the Lion checkout.
128
+ The private root also declares the same tarball as a development dependency so
129
+ npm installs it during a clean workspace install rather than assuming the
130
+ workspace already contains its bundled dependencies. The legacy docs dependency
131
+ keeps its separate registry version.
132
+ The temporary `prepack` helper verifies the checksum and installs only the tarball
133
+ in an isolated temporary prefix using npm's offline mode (without running dependency
134
+ scripts or writing a lockfile). It copies that installed package into the local
135
+ bundle and removes the temporary prefix, because npm packing does not include
136
+ hoisted workspace bundles. Other Lion dependencies remain unchanged.
137
+ The original MIT license and Google's Apache-2.0 notices remain in the artifact
138
+ and bundled package. See [vendor provenance](https://github.com/ing-bank/lion/blob/4649554021224763994601dd8d202c511d35013e/packages/ui/vendor/README.md) for the
139
+ source commit, checksum, and reproducible packaging instructions.
140
+
141
+ Once the next upstream awesome-phonenumber release includes the required
142
+ metadata, restore the normal registry dependency in `packages/ui/package.json`,
143
+ remove `awesome-phonenumber` from `bundledDependencies`, remove the vendor asset
144
+ and its `files` entry, remove the root development dependency, and regenerate
145
+ `package-lock.json`. Remove the temporary `prepack` lifecycle and
146
+ `scripts/prepare-vendored-phone.js` helper as well. Keep the metadata
147
+ regression test and rerun the phone tests, typechecks, and packed-consumer install
148
+ check before removing this workaround documentation.
149
+
115
150
  ## How to use
116
151
 
117
152
  ### Extend a Web Component
@@ -13,6 +13,10 @@ const matchA11ySpanReverseFns = new WeakMap();
13
13
  // TODO: make ListboxOverlayMixin that is shared between SelectRich and Combobox
14
14
  // TODO: extract option matching based on 'typed character cache' and share that logic
15
15
  // on Listbox or ListNavigationWithActiveDescendantMixin
16
+ // TODO: this component inherits `multipleChoice` (ChoiceGroupMixin) and `rotateKeyboardNavigation`
17
+ // (ListboxMixin) and reads both in many places. When those are renamed to `multiple` and `wrap`, move
18
+ // these usages to the new names in the same release; the deprecated aliases only exist for consumers.
19
+ // See the TODO at the top of ../../listbox/src/ListboxMixin.js.
16
20
 
17
21
  /**
18
22
  * @typedef {import('@lion/ui/listbox.js').LionOption} LionOption
@@ -1228,9 +1232,13 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
1228
1232
  * @param {{phase?:string}} config
1229
1233
  * @protected
1230
1234
  */
1231
- // eslint-disable-next-line no-unused-vars
1232
1235
  _syncToTextboxCondition(modelValue, oldModelValue, { phase } = {}) {
1233
- return this.autocomplete === 'both' || this.autocomplete === 'inline' || !this.focused;
1236
+ return (
1237
+ phase === 'overlay-close' ||
1238
+ this.autocomplete === 'both' ||
1239
+ this.autocomplete === 'inline' ||
1240
+ !this.focused
1241
+ );
1234
1242
  }
1235
1243
 
1236
1244
  /**
@@ -1392,6 +1392,37 @@ describe('lion-combobox', () => {
1392
1392
  expect(el.opened).to.equal(true);
1393
1393
  });
1394
1394
 
1395
+ it('syncs a manually checked selection on overlay close while the input remains focused', async () => {
1396
+ const el = /** @type {LionCombobox} */ (
1397
+ await fixture(html`
1398
+ <lion-combobox name="foo" autocomplete="list">
1399
+ <lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
1400
+ <lion-option .choiceValue="${'Chard'}">Chard</lion-option>
1401
+ </lion-combobox>
1402
+ `)
1403
+ );
1404
+ const { _inputNode } = getComboboxMembers(el);
1405
+
1406
+ _inputNode.focus();
1407
+ el.opened = true;
1408
+ await el.updateComplete;
1409
+ expect(el.opened).to.equal(true);
1410
+ expect(isActiveElement(_inputNode)).to.be.true;
1411
+
1412
+ el.setCheckedIndex(0);
1413
+ await el.updateComplete;
1414
+
1415
+ expect(el.modelValue).to.equal('Artichoke');
1416
+ expect(el.checkedIndex).to.equal(0);
1417
+ expect(_inputNode.value).to.equal('');
1418
+
1419
+ el.opened = false;
1420
+ await el.updateComplete;
1421
+
1422
+ expect(_inputNode.value).to.equal('Artichoke');
1423
+ expect(isActiveElement(_inputNode)).to.be.true;
1424
+ });
1425
+
1395
1426
  it('hides (and clears) overlay on [Escape]', async () => {
1396
1427
  const el = /** @type {LionCombobox} */ (
1397
1428
  await fixture(html`
@@ -30,6 +30,13 @@ const ChoiceGroupMixinImplementation = /** @type {ChoiceGroupMixin} */ (supercla
30
30
  /** @type {any} */
31
31
  static get properties() {
32
32
  return {
33
+ // TODO: rename to `multiple` (the platform name, `<select multiple>`) so it matches the
34
+ // vocabulary the menu's InteractiveListMixin uses. Must be non-breaking for the released
35
+ // components that inherit it (listbox/select-rich/combobox, radio-group/checkbox-group): add
36
+ // `multiple` with its own attribute, keep `multipleChoice`/`multiple-choice` as a deprecated
37
+ // alias that delegates to it (dev-only deprecation warning) for a release, mark it
38
+ // `@deprecated` only then and remove the alias in the next major. Coordinate with the TODOs in
39
+ // packages/ui/components/listbox/src/ListboxMixin.js and combobox/src/LionCombobox.js.
33
40
  multipleChoice: { type: Boolean, attribute: 'multiple-choice' },
34
41
  };
35
42
  }
@@ -7,6 +7,11 @@ import { FormRegistrarHost } from '../registration/FormRegistrarMixinTypes.js';
7
7
  import { ChoiceInputHost } from './ChoiceInputMixinTypes.js';
8
8
 
9
9
  export declare class ChoiceGroupHost extends FormControlHost {
10
+ /**
11
+ * TODO: rename to `multiple` (the platform name, `<select multiple>`), non-breaking: keep this
12
+ * property and the `multiple-choice` attribute as a deprecated alias for a release before removing
13
+ * them in a major. See the TODO in src/choice-group/ChoiceGroupMixin.js.
14
+ */
10
15
  multipleChoice: boolean;
11
16
  get serializedValue(): string;
12
17
  set serializedValue(value: string);
@@ -7,7 +7,7 @@ import { parseAmount } from './parsers.js';
7
7
 
8
8
  /**
9
9
  * @typedef {import('../../form-core/types/FormatMixinTypes.js').FormatOptions} FormatOptions
10
- * @typedef {FormatOptions & {locale?:string;currency:string|undefined}} AmountFormatOptions
10
+ * @typedef {FormatOptions & {locale?:string;currency:string|undefined;currencySymbol:string|undefined;}} AmountFormatOptions
11
11
  */
12
12
 
13
13
  /**
@@ -25,6 +25,10 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
25
25
  * reader users
26
26
  */
27
27
  currency: String,
28
+ currencyLabel: {
29
+ type: String,
30
+ attribute: 'currency-label',
31
+ },
28
32
  /**
29
33
  * @desc the modelValue of the input-amount has the 'Number' type. This allows
30
34
  * Application Developers to easily read from and write to this input or write custom
@@ -71,6 +75,7 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
71
75
  this.formatter = formatAmount;
72
76
  /** @type {string | undefined} */
73
77
  this.currency = undefined;
78
+ this.currencyLabel = '';
74
79
  /** @type {string | undefined} */
75
80
  this.locale = undefined;
76
81
  this.__currencyDisplayNodeIsConnected = true;
@@ -201,19 +206,25 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
201
206
 
202
207
  /** @private */
203
208
  __setCurrencyDisplayLabel() {
204
- // TODO: (@erikkroes) for optimal a11y, abbreviations should be part of aria-label
205
- // example, for a language switch with text 'en', an aria-label of 'english' is not
206
- // sufficient, it should also contain the abbreviation.
207
209
  if (this.__currencyDisplayNode) {
210
+ const label = this.currencyLabel || this.currency || '';
211
+ let currencyName = '';
212
+ if (this.currency) {
213
+ currencyName = getCurrencyName(this.currency, {});
214
+ }
208
215
  this.__currencyDisplayNode.setAttribute(
209
216
  'aria-label',
210
- this.currency ? getCurrencyName(this.currency, {}) : '',
217
+ `${label} ${currencyName ? `(${currencyName})` : ''}`,
211
218
  );
212
219
  }
213
220
  }
214
221
 
215
222
  get __currencyLabel() {
216
- return this.currency ? formatCurrencyLabel(this.currency, this._localizeManager.locale) : '';
223
+ let label = this.currencyLabel || '';
224
+ if (!label && this.currency) {
225
+ label = formatCurrencyLabel(this.currency, this._localizeManager.locale);
226
+ }
227
+ return label;
217
228
  }
218
229
 
219
230
  __reformat() {
@@ -5,17 +5,18 @@ import {
5
5
  } from '@lion/ui/localize-no-side-effects.js';
6
6
 
7
7
  /**
8
- * @typedef {import('../../localize/types/LocalizeMixinTypes.js').FormatNumberOptions} FormatOptions
8
+ * @typedef {import('../../localize/types/LocalizeMixinTypes.js').FormatNumberOptions} FormatNumberOptions
9
+ * @typedef {import('../../localize/types/LocalizeMixinTypes.js').FormatNumberPart} FormatNumberPart
9
10
  */
10
11
 
11
12
  /**
12
13
  * Formats a number considering the default fraction digits provided by Intl.
13
14
  *
14
15
  * @param {number} modelValue Number to format
15
- * @param {FormatOptions} [givenOptions]
16
+ * @param {FormatNumberOptions} [givenOptions]
16
17
  */
17
18
  export function formatAmount(modelValue, givenOptions) {
18
- /** @type {FormatOptions} */
19
+ /** @type {FormatNumberOptions} */
19
20
  const options = {
20
21
  currency: 'EUR',
21
22
  ...givenOptions,
@@ -40,5 +41,6 @@ export function formatCurrencyLabel(currency, locale) {
40
41
  if (currency === '') {
41
42
  return '';
42
43
  }
44
+
43
45
  return normalizeCurrencyLabel(currency, locale);
44
46
  }
@@ -3,86 +3,100 @@ import { getLocalizeManager } from '@lion/ui/localize-no-side-effects.js';
3
3
  import { localizeTearDown } from '@lion/ui/localize-test-helpers.js';
4
4
 
5
5
  import { formatAmount } from '@lion/ui/input-amount.js';
6
+ import { formatCurrencyLabel } from '../src/formatters.js';
6
7
 
7
- describe('formatAmount()', () => {
8
- const localizeManager = getLocalizeManager();
8
+ describe('formatters', () => {
9
+ describe('formatAmount()', () => {
10
+ const localizeManager = getLocalizeManager();
9
11
 
10
- afterEach(() => {
11
- localizeTearDown();
12
- });
12
+ afterEach(() => {
13
+ localizeTearDown();
14
+ });
13
15
 
14
- it('formats number with options', async () => {
15
- expect(
16
- formatAmount(12.345678, {
17
- locale: 'en-GB',
18
- minimumFractionDigits: 2,
19
- maximumFractionDigits: 2,
20
- }),
21
- ).to.equal('12.35');
22
- expect(
23
- formatAmount(12.345678, {
24
- locale: 'nl-NL',
25
- minimumFractionDigits: 2,
26
- maximumFractionDigits: 2,
27
- }),
28
- ).to.equal('12,35');
29
- expect(
30
- formatAmount(12345678, {
31
- locale: 'en-GB',
32
- minimumFractionDigits: 2,
33
- maximumFractionDigits: 2,
34
- }),
35
- ).to.equal('12,345,678.00');
36
- expect(
37
- formatAmount(12345678, {
38
- locale: 'nl-NL',
39
- minimumFractionDigits: 2,
40
- maximumFractionDigits: 2,
41
- }),
42
- ).to.equal('12.345.678,00');
43
- expect(
44
- formatAmount(12.345678, {
45
- locale: 'en-GB',
46
- minimumFractionDigits: 0,
47
- maximumFractionDigits: 1,
48
- }),
49
- ).to.equal('12.3');
50
- expect(
51
- formatAmount(12.345678, {
52
- locale: 'en-GB',
53
- minimumFractionDigits: 3,
54
- maximumFractionDigits: 3,
55
- }),
56
- ).to.equal('12.346');
57
- expect(
58
- formatAmount(-12.345678, {
59
- locale: 'en-GB',
60
- minimumFractionDigits: 2,
61
- maximumFractionDigits: 2,
62
- }),
63
- ).to.equal('−12.35');
64
- });
16
+ it('formats number with options', async () => {
17
+ expect(
18
+ formatAmount(12.345678, {
19
+ locale: 'en-GB',
20
+ minimumFractionDigits: 2,
21
+ maximumFractionDigits: 2,
22
+ }),
23
+ ).to.equal('12.35');
24
+ expect(
25
+ formatAmount(12.345678, {
26
+ locale: 'nl-NL',
27
+ minimumFractionDigits: 2,
28
+ maximumFractionDigits: 2,
29
+ }),
30
+ ).to.equal('12,35');
31
+ expect(
32
+ formatAmount(12345678, {
33
+ locale: 'en-GB',
34
+ minimumFractionDigits: 2,
35
+ maximumFractionDigits: 2,
36
+ }),
37
+ ).to.equal('12,345,678.00');
38
+ expect(
39
+ formatAmount(12345678, {
40
+ locale: 'nl-NL',
41
+ minimumFractionDigits: 2,
42
+ maximumFractionDigits: 2,
43
+ }),
44
+ ).to.equal('12.345.678,00');
45
+ expect(
46
+ formatAmount(12.345678, {
47
+ locale: 'en-GB',
48
+ minimumFractionDigits: 0,
49
+ maximumFractionDigits: 1,
50
+ }),
51
+ ).to.equal('12.3');
52
+ expect(
53
+ formatAmount(12.345678, {
54
+ locale: 'en-GB',
55
+ minimumFractionDigits: 3,
56
+ maximumFractionDigits: 3,
57
+ }),
58
+ ).to.equal('12.346');
59
+ expect(
60
+ formatAmount(-12.345678, {
61
+ locale: 'en-GB',
62
+ minimumFractionDigits: 2,
63
+ maximumFractionDigits: 2,
64
+ }),
65
+ ).to.equal('−12.35');
66
+ });
65
67
 
66
- it('formats the right amount of fraction digits for a certain currency', async () => {
67
- expect(formatAmount(123456.78, { locale: 'en-GB', currency: 'EUR' })).to.equal('123,456.78');
68
- expect(formatAmount(123456.78, { locale: 'en-GB', currency: 'JOD' })).to.equal('123,456.780');
69
- });
68
+ it('formats the right amount of fraction digits for a certain currency', async () => {
69
+ expect(formatAmount(123456.78, { locale: 'en-GB', currency: 'EUR' })).to.equal('123,456.78');
70
+ expect(formatAmount(123456.78, { locale: 'en-GB', currency: 'JOD' })).to.equal('123,456.780');
71
+ });
70
72
 
71
- it('fallbacks to global locale and EUR by default', async () => {
72
- localizeManager.locale = 'en-GB';
73
- expect(formatAmount(12345678)).to.equal('12,345,678.00');
74
- localizeManager.locale = 'nl-NL';
75
- expect(formatAmount(12345678)).to.equal('12.345.678,00');
73
+ it('fallbacks to global locale and EUR by default', async () => {
74
+ localizeManager.locale = 'en-GB';
75
+ expect(formatAmount(12345678)).to.equal('12,345,678.00');
76
+ localizeManager.locale = 'nl-NL';
77
+ expect(formatAmount(12345678)).to.equal('12.345.678,00');
78
+ });
79
+
80
+ // TODO: make it work with big numbers, e.g. make use of BigInt
81
+ it.skip('rounds up big numbers', async () => {
82
+ expect(formatAmount(1e21, { locale: 'en-GB', currency: 'EUR' })).to.equal(
83
+ '1,000,000,000,000,000,000,000.00',
84
+ );
85
+ // eslint-disable-next-line no-loss-of-precision
86
+ expect(formatAmount(12345678987654321.42, { locale: 'en-GB', currency: 'EUR' })).to.equal(
87
+ '12,345,678,987,654,321.42',
88
+ );
89
+ });
76
90
  });
77
91
 
78
- // TODO: make it work with big numbers, e.g. make use of BigInt
79
- it.skip('rounds up big numbers', async () => {
80
- expect(formatAmount(1e21, { locale: 'en-GB', currency: 'EUR' })).to.equal(
81
- '1,000,000,000,000,000,000,000.00',
82
- );
83
- // eslint-disable-next-line no-loss-of-precision
84
- expect(formatAmount(12345678987654321.42, { locale: 'en-GB', currency: 'EUR' })).to.equal(
85
- '12,345,678,987,654,321.42',
86
- );
92
+ describe('formatCurrencyLabel()', () => {
93
+ it('gives back the correct currency label', async () => {
94
+ expect(formatCurrencyLabel('EUR', 'en-GB')).to.equal('EUR');
95
+ expect(formatCurrencyLabel('USD', 'en-GB')).to.equal('USD');
96
+ });
97
+
98
+ it('gives nothing back if no currency has been provided', async () => {
99
+ expect(formatCurrencyLabel('', 'en-GB')).to.equal('');
100
+ });
87
101
  });
88
102
  });
@@ -225,42 +225,57 @@ describe('<lion-input-amount>', () => {
225
225
  expect(_inputNode.type).to.equal('text');
226
226
  });
227
227
 
228
- it('shows no currency by default', async () => {
229
- const el = await fixture(`<lion-input-amount></lion-input-amount>`);
230
- expect(Array.from(el.children).find(child => child.slot === 'after')).to.be.undefined;
231
- });
228
+ describe('currencyLabel', async () => {
229
+ it('shows no currency by default', async () => {
230
+ const el = await fixture(`<lion-input-amount></lion-input-amount>`);
231
+ expect(Array.from(el.children).find(child => child.slot === 'after')).to.be.undefined;
232
+ });
232
233
 
233
- it('displays currency if provided', async () => {
234
- const el = /** @type {LionInputAmount} */ (
235
- await fixture(`<lion-input-amount currency="EUR"></lion-input-amount>`)
236
- );
237
- expect(
238
- /** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'after')
239
- ?.innerText,
240
- ).to.equal('EUR');
241
- });
234
+ it('displays currency if provided', async () => {
235
+ const el = /** @type {LionInputAmount} */ (
236
+ await fixture(`<lion-input-amount currency="EUR"></lion-input-amount>`)
237
+ );
238
+ expect(
239
+ /** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'after')
240
+ ?.innerText,
241
+ ).to.equal('EUR');
242
+ });
242
243
 
243
- it('displays correct currency for TRY if locale is tr-TR', async () => {
244
- localize.locale = 'tr-TR';
245
- const el = /** @type {LionInputAmount} */ (
246
- await fixture(`<lion-input-amount currency="TRY"></lion-input-amount>`)
247
- );
248
- expect(
249
- /** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'after')
250
- ?.innerText,
251
- ).to.equal('TL');
252
- });
244
+ it('displays correct currency for TRY if locale is tr-TR', async () => {
245
+ localize.locale = 'tr-TR';
246
+ const el = /** @type {LionInputAmount} */ (
247
+ await fixture(`<lion-input-amount currency="TRY"></lion-input-amount>`)
248
+ );
249
+ expect(
250
+ /** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'after')
251
+ ?.innerText,
252
+ ).to.equal('TL');
253
+ });
253
254
 
254
- it('can update currency', async () => {
255
- const el = /** @type {LionInputAmount} */ (
256
- await fixture(`<lion-input-amount currency="EUR"></lion-input-amount>`)
257
- );
258
- el.currency = 'USD';
259
- await el.updateComplete;
260
- expect(
261
- /** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'after')
262
- ?.innerText,
263
- ).to.equal('USD');
255
+ it('displays currency symbol if provided via formatOptions', async () => {
256
+ const el = /** @type {LionInputAmount} */ (
257
+ await fixture(
258
+ html`<lion-input-amount currency="EUR" currency-label="€"></lion-input-amount>`,
259
+ )
260
+ );
261
+ const currLabel = /** @type {HTMLElement[]} */ (Array.from(el.children)).find(
262
+ child => child.slot === 'after',
263
+ );
264
+ expect(currLabel?.innerText).to.equal('€');
265
+ expect(currLabel?.getAttribute('aria-label')).to.equal('€ (euros)');
266
+ });
267
+
268
+ it('can update currency', async () => {
269
+ const el = /** @type {LionInputAmount} */ (
270
+ await fixture(`<lion-input-amount currency="EUR"></lion-input-amount>`)
271
+ );
272
+ el.currency = 'USD';
273
+ await el.updateComplete;
274
+ expect(
275
+ /** @type {HTMLElement[]} */ (Array.from(el.children)).find(child => child.slot === 'after')
276
+ ?.innerText,
277
+ ).to.equal('USD');
278
+ });
264
279
  });
265
280
 
266
281
  it('reformats on locale changes', async () => {
@@ -311,7 +326,7 @@ describe('<lion-input-amount>', () => {
311
326
  child => child.slot === 'after',
312
327
  );
313
328
  expect(currLabel?.innerText).to.equal('EUR');
314
- expect(currLabel?.getAttribute('aria-label')).to.equal('euros');
329
+ expect(currLabel?.getAttribute('aria-label')).to.equal('EUR (euros)');
315
330
  });
316
331
 
317
332
  it('sets currency label on the after element', async () => {
@@ -416,10 +431,10 @@ describe('<lion-input-amount>', () => {
416
431
  const label = /** @type {HTMLElement[]} */ (Array.from(el.children)).find(
417
432
  child => child.slot === 'after',
418
433
  );
419
- expect(label?.getAttribute('aria-label')).to.equal('euros');
434
+ expect(label?.getAttribute('aria-label')).to.equal('EUR (euros)');
420
435
  el.currency = 'USD';
421
436
  await el.updateComplete;
422
- expect(label?.getAttribute('aria-label')).to.equal('US dollars');
437
+ expect(label?.getAttribute('aria-label')).to.equal('USD (US dollars)');
423
438
  el.currency = 'PHP';
424
439
  await el.updateComplete;
425
440
  // TODO: Chrome Intl now thinks this should be pesos instead of pisos. They're probably right.
@@ -46,6 +46,15 @@ describe('PhoneNumber validation', () => {
46
46
  expect(validator.execute('+31612345678', 'NL')).to.be.false;
47
47
  });
48
48
 
49
+ it('accepts the French Guiana premium-rate example from libphonenumber v9.0.41', () => {
50
+ const validator = new PhoneNumber();
51
+ expect(validator.execute('+594890123456', 'GF')).to.be.false;
52
+ const parsed = PhoneUtilManager.PhoneUtil.parsePhoneNumber('+594890123456');
53
+ expect(parsed.valid).to.be.true;
54
+ expect(parsed.type).to.equal('premium-rate');
55
+ expect(parsed.regionCode).to.equal('GF');
56
+ });
57
+
49
58
  it('handles validation via awesome-phonenumber', () => {
50
59
  const validator = new PhoneNumber();
51
60
  // @ts-ignore
@@ -6,9 +6,21 @@ import { ScopedElementsMixin } from '../../core/src/ScopedElementsMixin.js';
6
6
  import { LionOptions } from './LionOptions.js';
7
7
  import { moveUserProvidedDefaultSlottablesToTarget } from '../../core/src/SlotMixin.js';
8
8
 
9
- // TODO: extract ListNavigationWithActiveDescendantMixin that can be reused in [role="menu"]
10
- // having children with [role="menuitem|menuitemcheckbox|menuitemradio|option"] and
11
- // list items that can be found via MutationObserver or registration (.formElements)
9
+ // TODO: make this mixin reuse the interactive-list implementation instead of its own copy of the
10
+ // navigation logic. The extraction this TODO used to ask for ("ListNavigationWithActiveDescendantMixin
11
+ // that can be reused in [role="menu"]") now exists as `InteractiveListMixin`, the base of the menu
12
+ // components (packages/ui/components/menu). Rebasing this mixin on it is also what makes the next step
13
+ // possible, without breaking consumers in between:
14
+ // - rename the api to the focusgroup/platform vocabulary the menu already speaks:
15
+ // `orientation` ('vertical'/'horizontal') -> `axis` ('block'/'inline'),
16
+ // `rotateKeyboardNavigation` -> `wrap`, `multipleChoice` -> `multiple`
17
+ // - map the new axis values to the ARIA ones where `aria-orientation` is set (block -> vertical,
18
+ // inline -> horizontal); ARIA does not know the focusgroup tokens
19
+ // - keep the old properties and attributes working in the same release: add the new property with
20
+ // its own attribute, turn the old one into a deprecated alias that delegates to it (with a
21
+ // dev-only deprecation warning) and mark it `@deprecated` in ListboxMixinTypes, then remove the
22
+ // aliases in the next major
23
+ // - `hasNoDefaultSelected` and `selectionFollowsFocus` can stay: the menu uses those names too
12
24
 
13
25
  /**
14
26
  * @typedef {import('../../form-core/types/FormControlMixinTypes.js').HTMLElementWithValue} HTMLElementWithValue
@@ -34,11 +46,15 @@ const ListboxMixinImplementation = /** @type {ListboxMixin} */ (superclass =>
34
46
  /** @type {any} */
35
47
  static get properties() {
36
48
  return {
49
+ // TODO: rename to `axis` with the values 'block'|'inline' (see the TODO at the top of this
50
+ // file); keep `orientation`/`orientation="vertical"|"horizontal"` working as a deprecated alias
37
51
  orientation: String,
38
52
  selectionFollowsFocus: {
39
53
  type: Boolean,
40
54
  attribute: 'selection-follows-focus',
41
55
  },
56
+ // TODO: rename to `wrap` (see the TODO at the top of this file); keep the old property and
57
+ // attribute working as a deprecated alias
42
58
  rotateKeyboardNavigation: {
43
59
  type: Boolean,
44
60
  attribute: 'rotate-keyboard-navigation',
@@ -25,7 +25,13 @@ export declare class ListboxHost {
25
25
  public rotateKeyboardNavigation: Boolean;
26
26
  /**
27
27
  * Informs screen reader and affects keyboard navigation.
28
- * By default 'vertical'
28
+ * By default 'vertical'.
29
+ *
30
+ * TODO: rename to `axis: 'inline' | 'block'` (the vocabulary the menu's `InteractiveListMixin` uses),
31
+ * together with `rotateKeyboardNavigation` -> `wrap` and `multipleChoice` -> `multiple`. Must be
32
+ * non-breaking: the old properties and their attributes stay available as deprecated aliases for a
33
+ * release (dev-only deprecation warning), and only then get `@deprecated` here and disappear in a
34
+ * major. See the TODO at the top of src/ListboxMixin.js.
29
35
  */
30
36
  public orientation: 'vertical' | 'horizontal';
31
37