@lion/ui 0.11.6 → 0.13.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 (101) hide show
  1. package/README.md +1 -1
  2. package/components/combobox/src/LionCombobox.js +26 -8
  3. package/components/combobox/test/lion-combobox.test.js +206 -57
  4. package/components/form-core/src/FocusMixin.js +1 -0
  5. package/components/form-core/src/FormControlMixin.js +23 -18
  6. package/components/form-core/src/validate/ValidateMixin.js +17 -1
  7. package/components/form-core/test/FormControlMixin.test.js +68 -0
  8. package/components/form-core/test-suites/ValidateMixin.suite.js +19 -3
  9. package/components/input-amount/src/parsers.js +1 -1
  10. package/components/input-amount/test/parsers.test.js +5 -0
  11. package/components/overlays/src/OverlayController.js +32 -5
  12. package/components/overlays/src/OverlayMixin.js +1 -0
  13. package/components/overlays/src/configurations/withDropdownConfig.js +0 -1
  14. package/components/overlays/src/configurations/withTooltipConfig.js +0 -1
  15. package/components/overlays/test/OverlayController.test.js +16 -4
  16. package/components/overlays/test-suites/OverlayMixin.suite.js +71 -0
  17. package/custom-elements.json +1144 -327
  18. package/dist-types/components/combobox/src/LionCombobox.d.ts.map +1 -1
  19. package/dist-types/components/core/src/ScopedElementsMixin.d.ts.map +1 -1
  20. package/dist-types/components/form-core/src/FocusMixin.d.ts.map +1 -1
  21. package/dist-types/components/form-core/src/FormControlMixin.d.ts.map +1 -1
  22. package/dist-types/components/form-core/src/validate/ValidateMixin.d.ts.map +1 -1
  23. package/dist-types/components/form-core/test-suites/ValidateMixin.suite.d.ts.map +1 -1
  24. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  25. package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
  26. package/dist-types/components/overlays/src/configurations/withDropdownConfig.d.ts.map +1 -1
  27. package/dist-types/components/overlays/src/configurations/withTooltipConfig.d.ts.map +1 -1
  28. package/dist-types/components/overlays/test/OverlayController.test.d.ts.map +1 -1
  29. package/dist-types/components/overlays/test-suites/OverlayMixin.suite.d.ts.map +1 -1
  30. package/dist-types/tsconfig.tsbuildinfo +1 -1
  31. package/docs/components/checkbox-group/api-table.md +6 -6
  32. package/docs/components/checkbox-group/overview.md +2 -2
  33. package/docs/components/collapsible/overview.md +1 -1
  34. package/docs/components/combobox/api-table.md +2 -2
  35. package/docs/components/combobox/overview.md +1 -1
  36. package/docs/components/combobox/use-cases.md +90 -1
  37. package/docs/components/dialog/overview.md +1 -1
  38. package/docs/components/fieldset/api-table.md +2 -2
  39. package/docs/components/fieldset/overview.md +5 -5
  40. package/docs/components/form/overview.md +3 -3
  41. package/docs/components/form/use-cases.md +2 -2
  42. package/docs/components/icon/use-cases.md +1 -1
  43. package/docs/components/input/api-table.md +2 -2
  44. package/docs/components/input/overview.md +1 -1
  45. package/docs/components/input-amount/api-table.md +2 -2
  46. package/docs/components/input-amount/overview.md +3 -3
  47. package/docs/components/input-date/api-table.md +2 -2
  48. package/docs/components/input-date/overview.md +3 -3
  49. package/docs/components/input-datepicker/api-table.md +2 -2
  50. package/docs/components/input-datepicker/overview.md +5 -5
  51. package/docs/components/input-email/api-table.md +2 -2
  52. package/docs/components/input-email/overview.md +2 -2
  53. package/docs/components/input-file/api-table.md +2 -2
  54. package/docs/components/input-file/overview.md +1 -1
  55. package/docs/components/input-iban/api-table.md +2 -2
  56. package/docs/components/input-iban/overview.md +2 -2
  57. package/docs/components/input-range/api-table.md +2 -2
  58. package/docs/components/input-range/overview.md +2 -2
  59. package/docs/components/input-stepper/api-table.md +2 -2
  60. package/docs/components/input-stepper/overview.md +1 -1
  61. package/docs/components/input-tel/api-table.md +2 -2
  62. package/docs/components/input-tel/overview.md +1 -1
  63. package/docs/components/input-tel/use-cases.md +6 -6
  64. package/docs/components/input-tel-dropdown/api-table.md +2 -2
  65. package/docs/components/input-tel-dropdown/overview.md +1 -1
  66. package/docs/components/listbox/api-table.md +2 -2
  67. package/docs/components/listbox/use-cases.md +1 -1
  68. package/docs/components/radio-group/api-table.md +4 -4
  69. package/docs/components/radio-group/overview.md +3 -3
  70. package/docs/components/select/api-table.md +2 -2
  71. package/docs/components/select/use-cases.md +1 -1
  72. package/docs/components/select-rich/api-table.md +2 -2
  73. package/docs/components/select-rich/use-cases.md +1 -1
  74. package/docs/components/switch/api-table.md +2 -2
  75. package/docs/components/tabs/examples.md +1 -1
  76. package/docs/components/textarea/api-table.md +2 -2
  77. package/docs/fundamentals/index.md +4 -4
  78. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  79. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  80. package/docs/fundamentals/rationales/accessibility.md +1 -1
  81. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  82. package/docs/fundamentals/systems/core/overview.md +1 -1
  83. package/docs/fundamentals/systems/form/api-table.md +2 -2
  84. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  85. package/docs/fundamentals/systems/form/model-value.md +1 -1
  86. package/docs/fundamentals/systems/form/overview.md +28 -28
  87. package/docs/fundamentals/systems/form/styling.md +3 -20
  88. package/docs/fundamentals/systems/form/validate.md +2 -2
  89. package/docs/fundamentals/systems/localize/overview.md +4 -4
  90. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  91. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  92. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  93. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  94. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  95. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  96. package/docs/guides/how-to/create-a-form.md +4 -4
  97. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  98. package/docs/guides/index.md +8 -8
  99. package/docs/guides/principles/index.md +5 -5
  100. package/docs/guides/principles/styling.md +1 -1
  101. package/package.json +3 -3
@@ -31,7 +31,7 @@ function round(value, decimals) {
31
31
  * @param {FormatNumberOptions} [givenOptions] Locale Options
32
32
  */
33
33
  export function parseAmount(value, givenOptions) {
34
- const unmatchedInput = value.match(/[^0-9,.\- ]/g);
34
+ const unmatchedInput = value.match(/[^0-9,.\-\u2212 ]/g);
35
35
  // for the full paste behavior documentation:
36
36
  // ./docs/components/input-amount/use-cases.md#paste-behavior
37
37
  if (unmatchedInput && givenOptions?.mode !== 'pasted') {
@@ -71,4 +71,9 @@ describe('parseAmount()', async () => {
71
71
  parseAmount('123.456,78', { mode: 'user-edited', viewValueStates: ['formatted'] }),
72
72
  ).to.equal(123456.78);
73
73
  });
74
+
75
+ it('parses negative numbers', async () => {
76
+ expect(parseAmount('−15.00')).to.equal(-15);
77
+ expect(parseAmount('-333.33')).to.equal(-333.33);
78
+ });
74
79
  });
@@ -528,8 +528,6 @@ export class OverlayController extends EventTarget {
528
528
  this.__contentHasBeenInitialized = true;
529
529
  }
530
530
 
531
- this.__wrappingDialogNode?.addEventListener('cancel', this.__cancelHandler);
532
-
533
531
  // Reset all positioning styles (local, c.q. Popper) and classes (global)
534
532
  this.contentWrapperNode.removeAttribute('style');
535
533
  this.contentWrapperNode.removeAttribute('class');
@@ -618,8 +616,9 @@ export class OverlayController extends EventTarget {
618
616
  contentWrapperNodeL2: this.contentWrapperNode,
619
617
  contentNodeL3: this.contentNode,
620
618
  });
621
- // @ts-ignore
619
+ // @ts-expect-error
622
620
  wrappingDialogElement.open = true;
621
+
623
622
  if (this.isTooltip) {
624
623
  // needed to prevent tooltip getting focus in Safari and Firefox
625
624
  wrappingDialogElement.setAttribute('tabindex', '-1');
@@ -633,6 +632,35 @@ export class OverlayController extends EventTarget {
633
632
  // computed height of 0...
634
633
  this.contentNode.style.position = 'static';
635
634
  }
635
+
636
+ // Here we prevent any interference of the native <dialog> element with the keyboard behavior
637
+ // as defined by the OverlayController. This is needed until we can configure `closedby="none"`
638
+ // on the native dialog for all browsers: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#closedby
639
+ const hasClosedBySupport = HTMLDialogElement && 'closedBy' in HTMLDialogElement.prototype;
640
+ if (hasClosedBySupport) {
641
+ // @ts-expect-error
642
+ wrappingDialogElement.closedBy = 'none';
643
+ } else {
644
+ wrappingDialogElement.addEventListener(
645
+ 'keydown',
646
+ (/** @type {* & KeyboardEvent} */ event) => {
647
+ if (event.key === 'Escape') {
648
+ event.preventDefault();
649
+ }
650
+ },
651
+ );
652
+ wrappingDialogElement.addEventListener('keyup', (/** @type {* & KeyboardEvent} */ event) => {
653
+ if (event.key === 'Escape') {
654
+ event.preventDefault();
655
+ }
656
+ });
657
+ wrappingDialogElement.addEventListener('cancel', event => {
658
+ event.stopPropagation();
659
+ });
660
+ wrappingDialogElement.addEventListener('close', event => {
661
+ event.stopPropagation();
662
+ });
663
+ }
636
664
  }
637
665
 
638
666
  /**
@@ -1188,6 +1216,7 @@ export class OverlayController extends EventTarget {
1188
1216
  const hasPressedInside =
1189
1217
  event.composedPath().includes(this.contentNode) ||
1190
1218
  deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
1219
+
1191
1220
  if (hasPressedInside) {
1192
1221
  this.hide();
1193
1222
  // We could do event.stopPropagation() here, but we don't want to hide info for
@@ -1384,8 +1413,6 @@ export class OverlayController extends EventTarget {
1384
1413
  teardown() {
1385
1414
  this.__handleOverlayStyles({ phase: 'teardown' });
1386
1415
  this._handleFeatures({ phase: 'teardown' });
1387
- this.__wrappingDialogNode?.removeEventListener('cancel', this.__cancelHandler);
1388
-
1389
1416
  if (this.#isRegisteredOnManager()) {
1390
1417
  this.manager.remove(this);
1391
1418
  }
@@ -179,6 +179,7 @@ export const OverlayMixinImplementation = superclass =>
179
179
  super.connectedCallback();
180
180
 
181
181
  this.updateComplete.then(() => {
182
+ if (!this.isConnected) return;
182
183
  if (this.#hasSetup) return;
183
184
  this._setupOverlayCtrl();
184
185
  this.#hasSetup = true;
@@ -9,7 +9,6 @@ export const withDropdownConfig = () =>
9
9
  placementMode: 'local',
10
10
  inheritsReferenceWidth: 'min',
11
11
  hidesOnOutsideClick: true,
12
- hidesOnOutsideEsc: true,
13
12
  hidesOnEsc: true,
14
13
  popperConfig: {
15
14
  placement: 'bottom-start',
@@ -18,7 +18,6 @@ export const withTooltipConfig = ({
18
18
  placementMode: 'local',
19
19
  elementToFocusAfterHide: undefined,
20
20
  hidesOnEsc: true,
21
- hidesOnOutsideEsc: true,
22
21
  handlesAccessibility: true,
23
22
  isTooltip: true,
24
23
  invokerRelation,
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable no-new */
2
2
  import { OverlayController, overlays } from '@lion/ui/overlays.js';
3
3
  import { mimicClick } from '@lion/ui/overlays-test-helpers.js';
4
+ import { sendKeys } from '@web/test-runner-commands';
4
5
  import sinon from 'sinon';
5
6
  import {
6
7
  unsafeStatic,
@@ -28,12 +29,22 @@ const wrappingDialogNodeStyle = 'display: none; z-index: 9999; padding: 0px;';
28
29
  /**
29
30
  * A small wrapper function that closely mimics an escape press from a user
30
31
  * (prevents common mistakes like no bubbling or keydown)
31
- * @param {Element|Document} element
32
+ * @param {HTMLElement|Document} element
32
33
  */
33
34
  async function mimicEscapePress(element) {
34
- element.dispatchEvent(
35
- new KeyboardEvent('keyup', { key: 'Escape', bubbles: true, composed: true }),
36
- );
35
+ // Make sure that the element inside the dialog is focusable (and cleanup after)
36
+ if (element instanceof HTMLElement) {
37
+ const { tabIndex: tabIndexBefore } = element;
38
+ // eslint-disable-next-line no-param-reassign
39
+ element.tabIndex = -1;
40
+ element.focus();
41
+ // eslint-disable-next-line no-param-reassign
42
+ element.tabIndex = tabIndexBefore; // make sure element is focusable
43
+ }
44
+
45
+ // Send the event
46
+ await sendKeys({ press: 'Escape' });
47
+ // Wait for at least a microtask, so that possible property effects are performed
37
48
  await aTimeout(0);
38
49
  }
39
50
 
@@ -726,6 +737,7 @@ describe('OverlayController', () => {
726
737
  )
727
738
  );
728
739
  const { parentOverlay, childOverlay } = await createNestedEscControllers(parentContent);
740
+
729
741
  await mimicEscapePress(childOverlay.contentNode);
730
742
 
731
743
  expect(parentOverlay.isShown).to.be.true;
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  unsafeStatic,
3
+ fixtureSync,
3
4
  nextFrame,
4
5
  aTimeout,
5
6
  defineCE,
@@ -8,6 +9,7 @@ import {
8
9
  html,
9
10
  } from '@open-wc/testing';
10
11
  import { overlays as overlaysManager, OverlayController } from '@lion/ui/overlays.js';
12
+ import { sendKeys } from '@web/test-runner-commands';
11
13
  import { browserDetection } from '@lion/ui/core.js';
12
14
  import { cache } from 'lit/directives/cache.js';
13
15
  import '@lion/ui/define/lion-dialog.js';
@@ -30,6 +32,28 @@ function resetOverlaysManager() {
30
32
  overlaysManager.list.forEach(overlayCtrl => overlaysManager.remove(overlayCtrl));
31
33
  }
32
34
 
35
+ /**
36
+ * A small wrapper function that closely mimics an escape press from a user
37
+ * (prevents common mistakes like no bubbling or keydown)
38
+ * @param {HTMLElement|Document} element
39
+ */
40
+ async function mimicEscapePress(element) {
41
+ // Make sure that the element inside the dialog is focusable (and cleanup after)
42
+ if (element instanceof HTMLElement) {
43
+ const { tabIndex: tabIndexBefore } = element;
44
+ // eslint-disable-next-line no-param-reassign
45
+ element.tabIndex = -1;
46
+ element.focus();
47
+ // eslint-disable-next-line no-param-reassign
48
+ element.tabIndex = tabIndexBefore; // make sure element is focusable
49
+ }
50
+
51
+ // Send the event
52
+ await sendKeys({ press: 'Escape' });
53
+ // Wait for at least a microtask, so that possible property effects are performed
54
+ await aTimeout(0);
55
+ }
56
+
33
57
  /**
34
58
  * @param {{tagString:string, tag: object, suffix?:string}} config
35
59
  */
@@ -315,6 +339,33 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
315
339
  expect(getComputedStyle(el._overlayCtrl.contentWrapperNode).display).not.to.equal('none');
316
340
  });
317
341
 
342
+ it('does not run setup when disconnected', async () => {
343
+ const el = /** @type {OverlayEl} */ (
344
+ fixtureSync(html`
345
+ <${tag}>
346
+ <div slot="content">content</div>
347
+ <button slot="invoker">invoker button</button>
348
+ </${tag}>
349
+ `)
350
+ );
351
+
352
+ const parentOfEl = /** @type {HTMLDivElement} */ (el.parentElement);
353
+
354
+ // @ts-expect-error [allow-protected-in-tests]
355
+ const setupSpy = sinon.spy(el, '_setupOverlayCtrl');
356
+ // In some client apps, this sync disconnect happens...
357
+ parentOfEl.removeChild(el);
358
+ await el.updateComplete;
359
+ expect(setupSpy.callCount).to.equal(0);
360
+
361
+ // Now add el again...
362
+ parentOfEl.appendChild(el);
363
+ await el.updateComplete;
364
+ expect(setupSpy.callCount).to.equal(1);
365
+
366
+ setupSpy.restore();
367
+ });
368
+
318
369
  /** Prevent unnecessary reset side effects, such as show animation. See: https://github.com/ing-bank/lion/issues/1075 */
319
370
  it('does not call updateConfig on equivalent config change', async () => {
320
371
  const el = /** @type {OverlayEl} */ (
@@ -339,6 +390,26 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
339
390
  stub.restore();
340
391
  });
341
392
 
393
+ it('should not visually hide when hidesOnEsc / hidesOnOutsideEsc is not configured', async () => {
394
+ const el = /** @type {OverlayEl} */ (
395
+ await fixture(html`
396
+ <${tag} .config="${{ hidesOnEsc: false, hidesOnOutsideEsc: false }}" opened>
397
+ <div slot="content">content of the overlay</div>
398
+ <button slot="invoker">invoker button</button>
399
+ </${tag}>
400
+ `)
401
+ );
402
+
403
+ const dialogEl = /** @type {HTMLDialogElement} */ (el._overlayCtrl.__wrappingDialogNode);
404
+
405
+ // @ts-expect-error [allow-protected-in-tests]
406
+ expect(dialogEl.checkVisibility()).to.be.true;
407
+ // @ts-expect-error [allow-protected-in-tests]
408
+ await mimicEscapePress(el._overlayContentNode);
409
+ // @ts-expect-error [allow-protected-in-tests]
410
+ expect(dialogEl.checkVisibility()).to.be.true;
411
+ });
412
+
342
413
  describe('Teardown', () => {
343
414
  it('does not teardown when moving dom nodes', async () => {
344
415
  const el = /** @type {OverlayEl} */ (