@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.
- package/components/form/src/LionForm.js +11 -2
- package/components/form/test/lion-form.test.js +21 -2
- package/components/input-stepper/src/LionInputStepper.js +6 -5
- package/components/input-stepper/test/lion-input-stepper-integrations.test.js +20 -0
- package/components/input-stepper/test/lion-input-stepper.test.js +48 -3
- package/components/overlays/src/OverlayController.js +16 -0
- package/components/overlays/src/utils/deep-contains.js +134 -3
- package/components/overlays/test/OverlayController.test.js +11 -0
- package/components/overlays/test/utils-tests/deep-contains.test.js +98 -0
- package/custom-elements.json +50 -30
- package/dist-types/components/form/src/LionForm.d.ts.map +1 -1
- package/dist-types/components/form/test/lion-form.test.d.ts.map +1 -1
- package/dist-types/components/input-stepper/src/LionInputStepper.d.ts +4 -1
- package/dist-types/components/input-stepper/src/LionInputStepper.d.ts.map +1 -1
- package/dist-types/components/input-stepper/test/lion-input-stepper-integrations.test.d.ts +2 -0
- package/dist-types/components/input-stepper/test/lion-input-stepper-integrations.test.d.ts.map +1 -0
- package/dist-types/components/input-stepper/test/lion-input-stepper.test.d.ts.map +1 -1
- package/dist-types/components/overlays/src/OverlayController.d.ts +7 -0
- package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
- package/dist-types/components/overlays/src/utils/deep-contains.d.ts +15 -1
- package/dist-types/components/overlays/src/utils/deep-contains.d.ts.map +1 -1
- package/dist-types/tsconfig.tsbuildinfo +1 -1
- package/docs/components/checkbox-group/overview.md +2 -2
- package/docs/components/collapsible/overview.md +1 -1
- package/docs/components/combobox/overview.md +1 -1
- package/docs/components/combobox/use-cases.md +1 -1
- package/docs/components/dialog/overview.md +1 -1
- package/docs/components/fieldset/overview.md +5 -5
- package/docs/components/form/overview.md +3 -3
- package/docs/components/icon/use-cases.md +1 -1
- package/docs/components/input/overview.md +1 -1
- package/docs/components/input-amount/overview.md +3 -3
- package/docs/components/input-date/overview.md +3 -3
- package/docs/components/input-datepicker/overview.md +5 -5
- package/docs/components/input-email/overview.md +2 -2
- package/docs/components/input-file/overview.md +1 -1
- package/docs/components/input-iban/overview.md +2 -2
- package/docs/components/input-range/overview.md +2 -2
- package/docs/components/input-stepper/api-table.md +2 -2
- package/docs/components/input-stepper/overview.md +1 -1
- package/docs/components/input-stepper/use-cases.md +21 -0
- package/docs/components/input-tel/overview.md +1 -1
- package/docs/components/input-tel/use-cases.md +6 -6
- package/docs/components/input-tel-dropdown/overview.md +1 -1
- package/docs/components/listbox/use-cases.md +1 -1
- package/docs/components/radio-group/overview.md +3 -3
- package/docs/components/select/use-cases.md +1 -1
- package/docs/components/select-rich/use-cases.md +1 -1
- package/docs/components/tabs/examples.md +1 -1
- package/docs/fundamentals/index.md +4 -4
- package/docs/fundamentals/node-tools/providence-analytics/LocalConfiguration.md +1 -1
- package/docs/fundamentals/node-tools/providence-analytics/QueryResult.md +1 -1
- package/docs/fundamentals/node-tools/providence-analytics/analyzer.md +1 -1
- package/docs/fundamentals/node-tools/providence-analytics/dashboard.md +1 -1
- package/docs/fundamentals/node-tools/providence-analytics/overview.md +3 -3
- package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
- package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
- package/docs/fundamentals/rationales/accessibility.md +1 -1
- package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
- package/docs/fundamentals/systems/core/overview.md +1 -1
- package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
- package/docs/fundamentals/systems/form/model-value.md +1 -1
- package/docs/fundamentals/systems/form/overview.md +28 -28
- package/docs/fundamentals/systems/form/validate.md +2 -2
- package/docs/fundamentals/systems/localize/overview.md +4 -4
- package/docs/fundamentals/systems/overlays/configuration.md +29 -3
- package/docs/fundamentals/systems/overlays/overview.md +5 -5
- package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
- package/docs/fundamentals/tools/helpers/overview.md +1 -1
- package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
- package/docs/guides/how-to/create-a-custom-field.md +5 -5
- package/docs/guides/how-to/create-a-form.md +4 -4
- package/docs/guides/how-to/extend-a-native-input.md +2 -2
- package/docs/guides/index.md +8 -8
- package/docs/guides/principles/index.md +5 -5
- package/docs/guides/principles/styling.md +1 -1
- 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
|
|
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
|
|
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
|
|
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 =
|
|
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.
|
|
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.
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
-
|
|
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
|
});
|
package/custom-elements.json
CHANGED
|
@@ -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":"
|
|
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":"
|
|
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"}
|