@lion/ui 0.7.4 → 0.7.6

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 (77) hide show
  1. package/components/form/src/LionForm.js +11 -2
  2. package/components/form/test/lion-form.test.js +21 -2
  3. package/components/input-stepper/src/LionInputStepper.js +6 -5
  4. package/components/input-stepper/test/lion-input-stepper-integrations.test.js +20 -0
  5. package/components/input-stepper/test/lion-input-stepper.test.js +48 -3
  6. package/components/overlays/src/OverlayController.js +16 -0
  7. package/components/overlays/src/utils/deep-contains.js +134 -3
  8. package/components/overlays/test/OverlayController.test.js +11 -0
  9. package/components/overlays/test/utils-tests/deep-contains.test.js +98 -0
  10. package/custom-elements.json +50 -30
  11. package/dist-types/components/form/src/LionForm.d.ts.map +1 -1
  12. package/dist-types/components/form/test/lion-form.test.d.ts.map +1 -1
  13. package/dist-types/components/input-stepper/src/LionInputStepper.d.ts +4 -1
  14. package/dist-types/components/input-stepper/src/LionInputStepper.d.ts.map +1 -1
  15. package/dist-types/components/input-stepper/test/lion-input-stepper-integrations.test.d.ts +2 -0
  16. package/dist-types/components/input-stepper/test/lion-input-stepper-integrations.test.d.ts.map +1 -0
  17. package/dist-types/components/input-stepper/test/lion-input-stepper.test.d.ts.map +1 -1
  18. package/dist-types/components/overlays/src/OverlayController.d.ts +7 -0
  19. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  20. package/dist-types/components/overlays/src/utils/deep-contains.d.ts +15 -1
  21. package/dist-types/components/overlays/src/utils/deep-contains.d.ts.map +1 -1
  22. package/dist-types/tsconfig.tsbuildinfo +1 -1
  23. package/docs/components/checkbox-group/overview.md +2 -2
  24. package/docs/components/collapsible/overview.md +1 -1
  25. package/docs/components/combobox/overview.md +1 -1
  26. package/docs/components/combobox/use-cases.md +1 -1
  27. package/docs/components/dialog/overview.md +1 -1
  28. package/docs/components/fieldset/overview.md +5 -5
  29. package/docs/components/form/overview.md +3 -3
  30. package/docs/components/icon/use-cases.md +1 -1
  31. package/docs/components/input/overview.md +1 -1
  32. package/docs/components/input-amount/overview.md +3 -3
  33. package/docs/components/input-date/overview.md +3 -3
  34. package/docs/components/input-datepicker/overview.md +5 -5
  35. package/docs/components/input-email/overview.md +2 -2
  36. package/docs/components/input-file/overview.md +1 -1
  37. package/docs/components/input-iban/overview.md +2 -2
  38. package/docs/components/input-range/overview.md +2 -2
  39. package/docs/components/input-stepper/api-table.md +2 -2
  40. package/docs/components/input-stepper/overview.md +1 -1
  41. package/docs/components/input-stepper/use-cases.md +21 -0
  42. package/docs/components/input-tel/overview.md +1 -1
  43. package/docs/components/input-tel/use-cases.md +6 -6
  44. package/docs/components/input-tel-dropdown/overview.md +1 -1
  45. package/docs/components/listbox/use-cases.md +1 -1
  46. package/docs/components/radio-group/overview.md +3 -3
  47. package/docs/components/select/use-cases.md +1 -1
  48. package/docs/components/select-rich/use-cases.md +1 -1
  49. package/docs/components/tabs/examples.md +1 -1
  50. package/docs/fundamentals/index.md +4 -4
  51. package/docs/fundamentals/node-tools/providence-analytics/LocalConfiguration.md +1 -1
  52. package/docs/fundamentals/node-tools/providence-analytics/QueryResult.md +1 -1
  53. package/docs/fundamentals/node-tools/providence-analytics/analyzer.md +1 -1
  54. package/docs/fundamentals/node-tools/providence-analytics/dashboard.md +1 -1
  55. package/docs/fundamentals/node-tools/providence-analytics/overview.md +3 -3
  56. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  57. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  58. package/docs/fundamentals/rationales/accessibility.md +1 -1
  59. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  60. package/docs/fundamentals/systems/core/overview.md +1 -1
  61. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  62. package/docs/fundamentals/systems/form/model-value.md +1 -1
  63. package/docs/fundamentals/systems/form/overview.md +28 -28
  64. package/docs/fundamentals/systems/form/validate.md +2 -2
  65. package/docs/fundamentals/systems/localize/overview.md +4 -4
  66. package/docs/fundamentals/systems/overlays/configuration.md +29 -3
  67. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  68. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  69. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  70. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  71. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  72. package/docs/guides/how-to/create-a-form.md +4 -4
  73. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  74. package/docs/guides/index.md +8 -8
  75. package/docs/guides/principles/index.md +5 -5
  76. package/docs/guides/principles/styling.md +1 -1
  77. package/package.json +1 -1
@@ -10,6 +10,15 @@ const throwFormNodeError = () => {
10
10
  );
11
11
  };
12
12
 
13
+ /**
14
+ * @param {FormRegistrarHost} formEl
15
+ * @returns {boolean}
16
+ */
17
+ function hasFocusableChildren(formEl) {
18
+ // this implies all children have the same type (either all of them are focusable or none of them are)
19
+ return formEl.formElements?.some(child => child._focusableNode);
20
+ }
21
+
13
22
  /**
14
23
  * LionForm: form wrapper providing extra features and integration with lion-field elements.
15
24
  *
@@ -63,7 +72,7 @@ export class LionForm extends LionFieldset {
63
72
  this.dispatchEvent(new Event('submit', { bubbles: true }));
64
73
 
65
74
  if (this.hasFeedbackFor?.includes('error')) {
66
- this._setFocusOnFirstErroneousFormElement(this);
75
+ this._setFocusOnFirstErroneousFormElement(/** @type { * & FormRegistrarHost } */ (this));
67
76
  }
68
77
  }
69
78
 
@@ -95,7 +104,7 @@ export class LionForm extends LionFieldset {
95
104
  element.formElements.find(child => child.hasFeedbackFor.includes('error')) ||
96
105
  element.formElements[0];
97
106
 
98
- if (firstFormElWithError.formElements?.length > 0) {
107
+ if (hasFocusableChildren(firstFormElWithError)) {
99
108
  this._setFocusOnFirstErroneousFormElement(firstFormElWithError);
100
109
  } else {
101
110
  firstFormElWithError._focusableNode.focus();
@@ -3,7 +3,8 @@ import '@lion/ui/define/lion-fieldset.js';
3
3
  import { LionField, Required } from '@lion/ui/form-core.js';
4
4
  import '@lion/ui/define/lion-field.js';
5
5
  import '@lion/ui/define/lion-validation-feedback.js';
6
-
6
+ import '@lion/ui/define/lion-listbox.js';
7
+ import '@lion/ui/define/lion-option.js';
7
8
  import '@lion/ui/define/lion-form.js';
8
9
  import {
9
10
  aTimeout,
@@ -223,7 +224,7 @@ describe('<lion-form>', () => {
223
224
  expect(document.activeElement).to.equal(el.formElements[1]._inputNode);
224
225
  });
225
226
 
226
- it('sets focus on submit to the first erroneous form element with a fieldset', async () => {
227
+ it('sets focus on submit to the first erroneous form element within a fieldset', async () => {
227
228
  const el = await fixture(html`
228
229
  <lion-form>
229
230
  <form>
@@ -266,4 +267,22 @@ describe('<lion-form>', () => {
266
267
  // @ts-ignore [allow-protected] in test
267
268
  expect(document.activeElement).to.equal(fieldset.formElements[0]._inputNode);
268
269
  });
270
+
271
+ it('sets focus on submit to the first form element within a erroneous listbox', async () => {
272
+ const el = await fixture(html`
273
+ <lion-form>
274
+ <form>
275
+ <lion-listbox name="name" .validators="${[new Required()]}">
276
+ <lion-option value="a">a</lion-option>
277
+ <lion-option value="b">b</lion-option>
278
+ </lion-listbox>
279
+ <button type="submit">submit</button>
280
+ </form>
281
+ </lion-form>
282
+ `);
283
+ const button = /** @type {HTMLButtonElement} */ (el.querySelector('button'));
284
+ button.click();
285
+ const listboxEl = el.formElements[0];
286
+ expect(document.activeElement).to.equal(listboxEl._inputNode);
287
+ });
269
288
  });
@@ -1,5 +1,5 @@
1
1
  import { html, css, render } from 'lit';
2
- import { LocalizeMixin } from '@lion/ui/localize-no-side-effects.js';
2
+ import { formatNumber, LocalizeMixin, parseNumber } from '@lion/ui/localize-no-side-effects.js';
3
3
  import { LionInput } from '@lion/ui/input.js';
4
4
  import { IsNumber, MinNumber, MaxNumber } from '@lion/ui/form-core.js';
5
5
  import { localizeNamespaceLoader } from './localizeNamespaceLoader.js';
@@ -52,7 +52,7 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
52
52
  * @returns {number}
53
53
  */
54
54
  get currentValue() {
55
- return parseFloat(this.value) || 0;
55
+ return this.modelValue || 0;
56
56
  }
57
57
 
58
58
  get _inputNode() {
@@ -62,7 +62,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
62
62
  constructor() {
63
63
  super();
64
64
  /** @param {string} modelValue */
65
- this.parser = modelValue => parseFloat(modelValue);
65
+ this.parser = parseNumber;
66
+ this.formatter = formatNumber;
66
67
  this.min = Infinity;
67
68
  this.max = Infinity;
68
69
  this.step = 1;
@@ -229,7 +230,7 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
229
230
  const { step, min, max } = this.values;
230
231
  const newValue = this.currentValue + step;
231
232
  if (newValue <= max || max === Infinity) {
232
- this.value = newValue < min && min !== Infinity ? `${min}` : `${newValue}`;
233
+ this.modelValue = newValue < min && min !== Infinity ? `${min}` : `${newValue}`;
233
234
  this.__toggleSpinnerButtonsState();
234
235
  this._proxyInputEvent();
235
236
  }
@@ -243,7 +244,7 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
243
244
  const { step, min, max } = this.values;
244
245
  const newValue = this.currentValue - step;
245
246
  if (newValue >= min || min === Infinity) {
246
- this.value = newValue > max && max !== Infinity ? `${max}` : `${newValue}`;
247
+ this.modelValue = newValue > max && max !== Infinity ? `${max}` : `${newValue}`;
247
248
  this.__toggleSpinnerButtonsState();
248
249
  this._proxyInputEvent();
249
250
  }
@@ -0,0 +1,20 @@
1
+ import {
2
+ runInteractionStateMixinSuite,
3
+ runFormatMixinSuite,
4
+ } from '@lion/ui/form-core-test-suites.js';
5
+
6
+ import '@lion/ui/define/lion-input-stepper.js';
7
+
8
+ const tagString = 'lion-input-stepper';
9
+
10
+ describe('<lion-input-stepper> integrations', () => {
11
+ runInteractionStateMixinSuite({
12
+ tagString,
13
+ allowedModelValueTypes: [Number],
14
+ });
15
+
16
+ runFormatMixinSuite({
17
+ tagString,
18
+ modelValueType: Number,
19
+ });
20
+ });
@@ -1,6 +1,7 @@
1
1
  import { expect, fixture as _fixture, nextFrame } from '@open-wc/testing';
2
2
  import { html } from 'lit/static-html.js';
3
3
  import sinon from 'sinon';
4
+ import { formatNumber } from '@lion/ui/localize-no-side-effects.js';
4
5
  import '@lion/ui/define/lion-input-stepper.js';
5
6
 
6
7
  /**
@@ -49,6 +50,50 @@ describe('<lion-input-stepper>', () => {
49
50
  });
50
51
  });
51
52
 
53
+ describe('Formatter', () => {
54
+ it('uses formatNumber for formatting', async () => {
55
+ const el = await fixture(defaultInputStepper);
56
+ expect(el.formatter).to.equal(formatNumber);
57
+ });
58
+
59
+ it('formatNumber uses locale provided in formatOptions', async () => {
60
+ let el = await fixture(html`
61
+ <lion-input-stepper
62
+ .formatOptions="${{ locale: 'en-GB' }}"
63
+ .modelValue="${1234.56}"
64
+ ></lion-input-stepper>
65
+ `);
66
+ expect(el.formattedValue).to.equal('1,234.56');
67
+ el = await fixture(html`
68
+ <lion-input-stepper
69
+ .formatOptions="${{ locale: 'nl-NL' }}"
70
+ .modelValue="${1234.56}"
71
+ ></lion-input-stepper>
72
+ `);
73
+ expect(el.formattedValue).to.equal('1.234,56');
74
+ });
75
+
76
+ it('supports overriding decimalSeparator in formatOptions', async () => {
77
+ const el = await fixture(
78
+ html`<lion-input-stepper
79
+ .formatOptions="${{ locale: 'nl-NL', decimalSeparator: '.' }}"
80
+ .modelValue="${12.34}"
81
+ ></lion-input-stepper>`,
82
+ );
83
+ expect(el.formattedValue).to.equal('12.34');
84
+ });
85
+
86
+ it('supports overriding groupSeparator in formatOptions', async () => {
87
+ const el = await fixture(
88
+ html`<lion-input-stepper
89
+ .formatOptions="${{ locale: 'nl-NL', groupSeparator: ',', decimalSeparator: '.' }}"
90
+ .modelValue="${1234.56}"
91
+ ></lion-input-stepper>`,
92
+ );
93
+ expect(el.formattedValue).to.equal('1,234.56');
94
+ });
95
+ });
96
+
52
97
  describe('User interaction', () => {
53
98
  it('should increment the value to 1 on [ArrowUp]', async () => {
54
99
  const el = await fixture(defaultInputStepper);
@@ -71,7 +116,7 @@ describe('<lion-input-stepper>', () => {
71
116
  expect(el.value).to.equal('');
72
117
  el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
73
118
  await el.updateComplete;
74
- expect(el.value).to.equal('-1');
119
+ expect(el.value).to.equal('−1');
75
120
  });
76
121
 
77
122
  it('should increment the value to minValue on [ArrowDown] if value is below min', async () => {
@@ -95,7 +140,7 @@ describe('<lion-input-stepper>', () => {
95
140
  expect(el.value).to.equal('');
96
141
  const decrementButton = el.querySelector('[slot=prefix]');
97
142
  decrementButton?.dispatchEvent(new Event('click'));
98
- expect(el.value).to.equal('-1');
143
+ expect(el.value).to.equal('−1');
99
144
  });
100
145
 
101
146
  it('fires one "user-input-changed" event on + button click', async () => {
@@ -143,7 +188,7 @@ describe('<lion-input-stepper>', () => {
143
188
  decrementButton?.dispatchEvent(new Event('focus'));
144
189
  decrementButton?.dispatchEvent(new Event('click'));
145
190
  decrementButton?.dispatchEvent(new Event('blur'));
146
- expect(el.value).to.equal('-1');
191
+ expect(el.value).to.equal('−1');
147
192
  expect(blurSpy.calledOnce).to.be.true;
148
193
  expect(el.touched).to.be.true;
149
194
 
@@ -201,6 +201,8 @@ export class OverlayController extends EventTarget {
201
201
  this.__hasActiveBackdrop = true;
202
202
  /** @private */
203
203
  this.__escKeyHandler = this.__escKeyHandler.bind(this);
204
+ /** @private */
205
+ this.__cancelHandler = this.__cancelHandler.bind(this);
204
206
  }
205
207
 
206
208
  /**
@@ -510,6 +512,8 @@ export class OverlayController extends EventTarget {
510
512
  this.__contentHasBeenInitialized = true;
511
513
  }
512
514
 
515
+ this.__wrappingDialogNode?.addEventListener('cancel', this.__cancelHandler);
516
+
513
517
  // Reset all positioning styles (local, c.q. Popper) and classes (global)
514
518
  this.contentWrapperNode.removeAttribute('style');
515
519
  this.contentWrapperNode.removeAttribute('class');
@@ -1126,6 +1130,17 @@ export class OverlayController extends EventTarget {
1126
1130
  }
1127
1131
  }
1128
1132
 
1133
+ /**
1134
+ * When the overlay is a modal dialog hidesOnEsc works out of the box, so we prevent that.
1135
+ *
1136
+ * There is currently a bug in chrome that makes the dialog close when pressing Esc the second time
1137
+ * @private
1138
+ */
1139
+ // eslint-disable-next-line class-methods-use-this
1140
+ __cancelHandler(/** @type {Event} */ ev) {
1141
+ ev.preventDefault();
1142
+ }
1143
+
1129
1144
  /** @private */
1130
1145
  __escKeyHandler(/** @type {KeyboardEvent} */ ev) {
1131
1146
  return ev.key === 'Escape' && this.hide();
@@ -1292,6 +1307,7 @@ export class OverlayController extends EventTarget {
1292
1307
  teardown() {
1293
1308
  this.__handleOverlayStyles({ phase: 'teardown' });
1294
1309
  this._handleFeatures({ phase: 'teardown' });
1310
+ this.__wrappingDialogNode?.removeEventListener('cancel', this.__cancelHandler);
1295
1311
  }
1296
1312
 
1297
1313
  /** @private */
@@ -1,20 +1,149 @@
1
+ /**
2
+ * A number, or a string containing a number.
3
+ * @typedef {{element: HTMLElement; deepContains: boolean} | null} CacheItem
4
+ */
5
+
1
6
  /**
2
7
  * Whether first element contains the second element, also goes through shadow roots
3
8
  * @param {HTMLElement|ShadowRoot} el
4
9
  * @param {HTMLElement|ShadowRoot} targetEl
10
+ * @param {{[key: string]: CacheItem[]}} cache
5
11
  * @returns {boolean}
6
12
  */
7
- export function deepContains(el, targetEl) {
13
+ export function deepContains(el, targetEl, cache = {}) {
14
+ /**
15
+ * @description A `Typescript` `type guard` for `HTMLElement`
16
+ * @param {Element|ShadowRoot} htmlElement
17
+ * @returns {htmlElement is HTMLElement}
18
+ */
19
+ function isHTMLElement(htmlElement) {
20
+ return 'getAttribute' in htmlElement;
21
+ }
22
+
23
+ /**
24
+ * @description Returns a cached item for the given element or null otherwise
25
+ * @param {HTMLElement|ShadowRoot} element
26
+ * @returns {CacheItem|null}
27
+ */
28
+ function getCachedItem(element) {
29
+ if (!isHTMLElement(element)) {
30
+ return null;
31
+ }
32
+ const slotName = element.getAttribute('slot');
33
+ /** @type {CacheItem|null} */
34
+ let result = null;
35
+ if (slotName) {
36
+ const cachedItemsWithSameName = cache[slotName];
37
+ if (cachedItemsWithSameName) {
38
+ result = cachedItemsWithSameName.filter(item => item?.element === element)[0] || null;
39
+ }
40
+ }
41
+ return result;
42
+ }
43
+
44
+ const cachedItem = getCachedItem(el);
45
+ if (cachedItem) {
46
+ return cachedItem.deepContains;
47
+ }
48
+
49
+ /**
50
+ * @description Cache an html element and its `deepContains` status
51
+ * @param {boolean} contains The `deepContains` status for the element
52
+ * @returns {void}
53
+ */
54
+ function cacheItem(contains) {
55
+ if (!isHTMLElement(el)) {
56
+ return;
57
+ }
58
+ const slotName = el.getAttribute('slot');
59
+ if (slotName) {
60
+ // eslint-disable-next-line no-param-reassign
61
+ cache[slotName] = cache[slotName] || [];
62
+ cache[slotName].push({ element: el, deepContains: contains });
63
+ }
64
+ }
65
+
8
66
  let containsTarget = el.contains(targetEl);
9
67
  if (containsTarget) {
68
+ cacheItem(true);
10
69
  return true;
11
70
  }
12
71
 
72
+ /**
73
+ * A `Typescript` `type guard` for `HTMLSlotElement`
74
+ * @param {HTMLElement|HTMLSlotElement} htmlElement
75
+ * @returns {htmlElement is HTMLSlotElement}
76
+ */
77
+ function isSlot(htmlElement) {
78
+ return htmlElement.tagName === 'SLOT';
79
+ }
80
+
81
+ /**
82
+ * Returns a slot projection or it returns `null` if `htmlElement` is not an `HTMLSlotElement`
83
+ * @example
84
+ * Let's say this is a custom element declared as follows:
85
+ * ```
86
+ * <custom-element>
87
+ * shadowRoot
88
+ * <div id="dialog-wrapper">
89
+ * <div id="dialog-header">Header</div>
90
+ * <div id="dialog-content">
91
+ * <slot id="dialog-content-slot" name="content"></slot>
92
+ * </div>
93
+ * </div>
94
+ * <!-- Light DOM -->
95
+ * <div id="my-slot-content" slot="content">my content</div>
96
+ * </custom-element>
97
+ * ```
98
+ * Then for `slot#dialog-content-slot` which is defined in the ShadowDom the function returns `div#my-slot-content` which is defined in the LightDom
99
+ * @param {HTMLElement|HTMLSlotElement} htmlElement
100
+ * @returns {Element[]}
101
+ * */
102
+ function getSlotProjections(htmlElement) {
103
+ return isSlot(htmlElement) ? /** @type {Element[]} */ (htmlElement.assignedElements()) : [];
104
+ }
105
+
106
+ /**
107
+ * @description A `Typescript` `type guard` for `ShadowRoot`
108
+ * @param {Element|ShadowRoot} htmlElement
109
+ * @returns {htmlElement is ShadowRoot}
110
+ */
111
+ function isShadowRoot(htmlElement) {
112
+ return htmlElement.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
113
+ }
114
+
115
+ /**
116
+ * Check whether any element contains target
117
+ * @param {(Element|ShadowRoot|null)[]} elements
118
+ * */
119
+ function checkElements(elements) {
120
+ let contains = false;
121
+ for (let i = 0; i < elements.length; i += 1) {
122
+ const element = elements[i];
123
+ if (
124
+ element &&
125
+ (isHTMLElement(element) || isShadowRoot(element)) &&
126
+ deepContains(element, targetEl, cache)
127
+ ) {
128
+ contains = true;
129
+ break;
130
+ }
131
+ }
132
+ return contains;
133
+ }
134
+
13
135
  /** @param {HTMLElement|ShadowRoot} elem */
14
136
  function checkChildren(elem) {
15
137
  for (let i = 0; i < elem.children.length; i += 1) {
16
138
  const child = /** @type {HTMLElement} */ (elem.children[i]);
17
- if (child.shadowRoot && deepContains(child.shadowRoot, targetEl)) {
139
+ const cachedChild = getCachedItem(child);
140
+ if (cachedChild) {
141
+ containsTarget = cachedChild.deepContains || containsTarget;
142
+ break;
143
+ }
144
+ const slotProjections = getSlotProjections(child);
145
+ const childSubElements = [child.shadowRoot, ...slotProjections];
146
+ if (checkElements(childSubElements)) {
18
147
  containsTarget = true;
19
148
  break;
20
149
  }
@@ -26,11 +155,13 @@ export function deepContains(el, targetEl) {
26
155
 
27
156
  // If element is not shadowRoot itself
28
157
  if (el instanceof HTMLElement && el.shadowRoot) {
29
- containsTarget = deepContains(el.shadowRoot, targetEl);
158
+ containsTarget = deepContains(el.shadowRoot, targetEl, cache);
30
159
  if (containsTarget) {
160
+ cacheItem(true);
31
161
  return true;
32
162
  }
33
163
  }
34
164
  checkChildren(el);
165
+ cacheItem(containsTarget);
35
166
  return containsTarget;
36
167
  }
@@ -585,6 +585,17 @@ describe('OverlayController', () => {
585
585
  expect(ctrl.isShown).to.be.false;
586
586
  });
587
587
 
588
+ it("doesn't hide when [escape] is pressed and hidesOnEsc is set to false", async () => {
589
+ const ctrl = new OverlayController({
590
+ ...withGlobalTestConfig(),
591
+ hidesOnEsc: false,
592
+ });
593
+ await ctrl.show();
594
+ ctrl.contentNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }));
595
+ await aTimeout(0);
596
+ expect(ctrl.isShown).to.be.true;
597
+ });
598
+
588
599
  it('stays shown when [escape] is pressed on outside element', async () => {
589
600
  const ctrl = new OverlayController({
590
601
  ...withGlobalTestConfig(),
@@ -124,4 +124,102 @@ describe('deepContains()', () => {
124
124
  expect(deepContains(element, elementFirstChildShadowChildShadow)).to.be.true;
125
125
  expect(deepContains(element, elementFirstChildShadowChildShadowLastChild)).to.be.true;
126
126
  });
127
+
128
+ it('returns true if the element, which is located in ShadowsRoot, contains a target element, located in the LightDom', async () => {
129
+ const mainElement = /** @type {HTMLElement} */ (await fixture('<div id="main"></div>'));
130
+ mainElement.innerHTML = `
131
+ <div slot="content" id="light-el-content">
132
+ <input type="text" id="light-el-input-1"></input>
133
+ </div>
134
+ <div slot="content" id="light-el-content">
135
+ <input type="text" id="light-el-input-2"></input>
136
+ </div>
137
+ `;
138
+ const shadowRoot = mainElement.attachShadow({ mode: 'open' });
139
+ shadowRoot.innerHTML = `
140
+ <div id="dialog-wrapper">
141
+ <div id="dialog-header">
142
+ Header
143
+ </div>
144
+ <div id="dialog-content">
145
+ <slot name="content" id="shadow-el-content"></slot>
146
+ </div>
147
+ </div>
148
+ `;
149
+ const inputElement = /** @type {HTMLElement} */ (
150
+ mainElement.querySelector('#light-el-input-2')
151
+ );
152
+ const dialogWrapperElement = /** @type {HTMLElement} */ (
153
+ shadowRoot.querySelector('#dialog-wrapper')
154
+ );
155
+ expect(deepContains(dialogWrapperElement, inputElement)).to.be.true;
156
+ });
157
+
158
+ it(`returns true if the element, which is located in ShadowRoot, contains a target element, located in the ShadowRoot element of the LightDom element `, async () => {
159
+ /**
160
+ * The DOM for the `main` element looks as follows:
161
+ *
162
+ * <div id="main">
163
+ * #shadow-root
164
+ * <div id="dialog-wrapper"> // dialogWrapperElement
165
+ * <div id="dialog-header">
166
+ * Header
167
+ * </div>
168
+ * <div id="dialog-content">
169
+ * <slot name="content" id="shadow-el-content"></slot>
170
+ * </div>
171
+ * </div>
172
+ * <div slot="content" id="light-el-content">
173
+ * <div id="conent-wrapper">
174
+ * #shadow-root
175
+ * <div id="conent-wrapper-sub">
176
+ * #shadow-root
177
+ * <input type="type" id="content-input"></input> //inputElement
178
+ * </div>
179
+ * </div>
180
+ * </div>
181
+ * </div>
182
+ */
183
+ const mainElement = /** @type {HTMLElement} */ (await fixture('<div id="main"></div>'));
184
+ mainElement.innerHTML = `
185
+ <div slot="content" id="light-el-content">
186
+ <div id="content-wrapper"></div>
187
+ </div>
188
+ <div slot="content" id="light-el-content">
189
+ <div id="content-wrapper"></div>
190
+ </div>
191
+ `;
192
+ const contentWrapper = /** @type {HTMLElement} */ (
193
+ mainElement.querySelector('#content-wrapper')
194
+ );
195
+ const contentWrapperShadowRoot = contentWrapper.attachShadow({ mode: 'open' });
196
+ contentWrapperShadowRoot.innerHTML = `
197
+ <div id="conent-wrapper-sub"></div>
198
+ `;
199
+ const contentWrapperSub = /** @type {HTMLElement} */ (
200
+ contentWrapperShadowRoot.querySelector('#conent-wrapper-sub')
201
+ );
202
+ const contentWrapperSubShadowRoot = contentWrapperSub.attachShadow({ mode: 'open' });
203
+ contentWrapperSubShadowRoot.innerHTML = `
204
+ <input type="type" id="content-input"></input>
205
+ `;
206
+ const inputElement = /** @type {HTMLElement} */ (
207
+ contentWrapperSubShadowRoot.querySelector('#content-input')
208
+ );
209
+ const mainElementShadowRoot = mainElement.attachShadow({ mode: 'open' });
210
+ mainElementShadowRoot.innerHTML = `
211
+ <div id="dialog-wrapper">
212
+ <div id="dialog-header">
213
+ Header
214
+ </div>
215
+ <div id="dialog-content">
216
+ <slot name="content" id="shadow-el-content"></slot>
217
+ </div>
218
+ </div>
219
+ `;
220
+ const dialogWrapperElement = /** @type {HTMLElement} */ (
221
+ mainElementShadowRoot.querySelector('#dialog-wrapper')
222
+ );
223
+ expect(deepContains(dialogWrapperElement, inputElement)).to.be.true;
224
+ });
127
225
  });
@@ -91207,6 +91207,38 @@
91207
91207
  "name": "LionField",
91208
91208
  "module": "components/form-core/src/LionField.js"
91209
91209
  },
91210
+ "default": "parseNumber",
91211
+ "privacy": "public"
91212
+ },
91213
+ {
91214
+ "kind": "field",
91215
+ "name": "formatter",
91216
+ "parameters": [
91217
+ {
91218
+ "name": "v",
91219
+ "description": "modelValue: can be an Object, Number, String depending on the\ninput type(date, number, email etc)",
91220
+ "type": {
91221
+ "text": "*"
91222
+ }
91223
+ },
91224
+ {
91225
+ "name": "opts",
91226
+ "type": {
91227
+ "text": "FormatOptions"
91228
+ }
91229
+ }
91230
+ ],
91231
+ "description": "Converts modelValue to formattedValue (formattedValue will be synced with\n`._inputNode.value`)\nFor instance, a Date object to a localized date.",
91232
+ "return": {
91233
+ "type": {
91234
+ "text": "string"
91235
+ }
91236
+ },
91237
+ "inheritedFrom": {
91238
+ "name": "LionField",
91239
+ "module": "components/form-core/src/LionField.js"
91240
+ },
91241
+ "default": "formatNumber",
91210
91242
  "privacy": "public"
91211
91243
  },
91212
91244
  {
@@ -91403,36 +91435,6 @@
91403
91435
  },
91404
91436
  "privacy": "public"
91405
91437
  },
91406
- {
91407
- "kind": "method",
91408
- "name": "formatter",
91409
- "parameters": [
91410
- {
91411
- "name": "v",
91412
- "description": "modelValue: can be an Object, Number, String depending on the\ninput type(date, number, email etc)",
91413
- "type": {
91414
- "text": "*"
91415
- }
91416
- },
91417
- {
91418
- "name": "opts",
91419
- "type": {
91420
- "text": "FormatOptions"
91421
- }
91422
- }
91423
- ],
91424
- "description": "Converts modelValue to formattedValue (formattedValue will be synced with\n`._inputNode.value`)\nFor instance, a Date object to a localized date.",
91425
- "return": {
91426
- "type": {
91427
- "text": "string"
91428
- }
91429
- },
91430
- "inheritedFrom": {
91431
- "name": "LionField",
91432
- "module": "components/form-core/src/LionField.js"
91433
- },
91434
- "privacy": "public"
91435
- },
91436
91438
  {
91437
91439
  "kind": "method",
91438
91440
  "name": "serializer",
@@ -114681,6 +114683,17 @@
114681
114683
  ],
114682
114684
  "privacy": "public"
114683
114685
  },
114686
+ {
114687
+ "kind": "method",
114688
+ "name": "__cancelHandler",
114689
+ "parameters": [
114690
+ {
114691
+ "name": "ev"
114692
+ }
114693
+ ],
114694
+ "description": "When the overlay is a modal dialog hidesOnEsc works out of the box, so we prevent that.\n\nThere is currently a bug in chrome that makes the dialog close when pressing Esc the second time",
114695
+ "privacy": "private"
114696
+ },
114684
114697
  {
114685
114698
  "kind": "method",
114686
114699
  "name": "__escKeyHandler",
@@ -115682,6 +115695,13 @@
115682
115695
  "type": {
115683
115696
  "text": "HTMLElement|ShadowRoot"
115684
115697
  }
115698
+ },
115699
+ {
115700
+ "name": "cache",
115701
+ "default": "{}",
115702
+ "type": {
115703
+ "text": "{[key: string]: CacheItem[]}"
115704
+ }
115685
115705
  }
115686
115706
  ],
115687
115707
  "description": "Whether first element contains the second element, also goes through shadow roots",
@@ -1 +1 @@
1
- {"version":3,"file":"LionForm.d.ts","sourceRoot":"","sources":["../../../../components/form/src/LionForm.js"],"names":[],"mappings":"AAYA;;;;GAIG;AAEH;IAoCE;;;OAGG;IACH,sBAHW,KAAK,QAYf;IAUD;;;OAGG;IACH,qBAHW,KAAK,QAQf;IA3DD,0BAMC;IAOD,iCAEC;IAED,eAQC;IAiBD,cAMC;IAaD;;;OAGG;IACH,wDAHW,iBAAiB,QAa3B;IAED,eAAe;IACf,oCAGC;IAED,eAAe;IACf,oCAGC;CACF;gCAhHY,OAAO,+DAA+D,EAAE,iBAAiB"}
1
+ {"version":3,"file":"LionForm.d.ts","sourceRoot":"","sources":["../../../../components/form/src/LionForm.js"],"names":[],"mappings":"AAqBA;;;;GAIG;AAEH;IAoCE;;;OAGG;IACH,sBAHW,KAAK,QAYf;IAUD;;;OAGG;IACH,qBAHW,KAAK,QAQf;IA3DD,0BAMC;IAOD,iCAEC;IAED,eAQC;IAiBD,cAMC;IAaD;;;OAGG;IACH,wDAHW,iBAAiB,QAa3B;IAED,eAAe;IACf,oCAGC;IAED,eAAe;IACf,oCAGC;CACF;gCAzHY,OAAO,+DAA+D,EAAE,iBAAiB"}
@@ -1 +1 @@
1
- {"version":3,"file":"lion-form.test.d.ts","sourceRoot":"","sources":["../../../../components/form/test/lion-form.test.js"],"names":[],"mappings":"uBAmBa,OAAO,oBAAoB,EAAE,QAAQ;6BACrC,OAAO,KAAK,EAAE,cAAc"}
1
+ {"version":3,"file":"lion-form.test.d.ts","sourceRoot":"","sources":["../../../../components/form/test/lion-form.test.js"],"names":[],"mappings":"uBAoBa,OAAO,oBAAoB,EAAE,QAAQ;6BACrC,OAAO,KAAK,EAAE,cAAc"}