@lion/ui 0.6.0 → 0.7.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.
- package/components/core/src/SlotMixin.js +107 -65
- package/components/core/src/browserDetection.js +27 -17
- package/components/core/test/SlotMixin.test.js +64 -1
- package/components/core/types/SlotMixinTypes.ts +8 -0
- package/components/form-core/src/registration/FormRegisteringMixin.js +14 -0
- package/components/form-core/test-suites/FormRegistrationMixins.suite.js +57 -2
- package/components/form-core/test-suites/form-group/FormGroupMixin.suite.js +1 -1
- package/components/form-core/types/registration/FormRegisteringMixinTypes.ts +9 -1
- package/components/input-amount/src/parsers.js +11 -3
- package/components/input-amount/test/formatters.test.js +11 -0
- package/components/input-amount/test/parsers.test.js +32 -4
- package/components/input-file/src/LionInputFile.js +2 -1
- package/components/input-tel-dropdown/src/LionInputTelDropdown.js +2 -13
- package/components/input-tel-dropdown/test-suites/LionInputTelDropdown.suite.js +3 -1
- package/components/input-tel-dropdown/types/index.ts +1 -0
- package/components/localize/src/number/formatNumber.js +1 -1
- package/components/localize/test/number/formatNumber.test.js +9 -0
- package/components/overlays/test/local-positioning.test.js +13 -21
- package/components/overlays/test/utils-tests/contain-focus.test.js +2 -2
- package/components/overlays/test/utils-tests/deep-contains.test.js +3 -3
- package/components/overlays/test/utils-tests/get-focusable-elements.test.js +1 -1
- package/components/select-rich/src/LionSelectRich.js +1 -1
- package/components/select-rich/test/lion-select-rich-interaction.test.js +16 -40
- package/components/select-rich/test/lion-select-rich.test.js +16 -14
- package/components/select-rich/test-helpers/getSelectRichMembers.js +18 -0
- package/custom-elements.json +1318 -205
- package/dist-types/components/core/src/SlotMixin.d.ts +5 -1
- package/dist-types/components/core/src/SlotMixin.d.ts.map +1 -1
- package/dist-types/components/core/src/browserDetection.d.ts +3 -3
- package/dist-types/components/core/types/SlotMixinTypes.d.ts +8 -0
- package/dist-types/components/core/types/SlotMixinTypes.d.ts.map +1 -1
- package/dist-types/components/form-core/src/registration/FormRegisteringMixin.d.ts.map +1 -1
- package/dist-types/components/form-core/test-suites/FormRegistrationMixins.suite.d.ts.map +1 -1
- package/dist-types/components/form-core/types/registration/FormRegisteringMixinTypes.d.ts +9 -1
- package/dist-types/components/form-core/types/registration/FormRegisteringMixinTypes.d.ts.map +1 -1
- package/dist-types/components/input-amount/src/parsers.d.ts.map +1 -1
- package/dist-types/components/input-file/src/LionInputFile.d.ts +1 -0
- package/dist-types/components/input-file/src/LionInputFile.d.ts.map +1 -1
- package/dist-types/components/input-tel-dropdown/src/LionInputTelDropdown.d.ts +1 -0
- package/dist-types/components/input-tel-dropdown/src/LionInputTelDropdown.d.ts.map +1 -1
- package/dist-types/components/input-tel-dropdown/test-suites/LionInputTelDropdown.suite.d.ts.map +1 -1
- package/dist-types/components/input-tel-dropdown/types/index.d.ts +1 -0
- package/dist-types/components/input-tel-dropdown/types/index.d.ts.map +1 -1
- package/dist-types/components/select-rich/test/lion-select-rich-interaction.test.d.ts.map +1 -1
- package/dist-types/components/select-rich/test/lion-select-rich.test.d.ts.map +1 -1
- package/dist-types/components/select-rich/test-helpers/getSelectRichMembers.d.ts +19 -0
- package/dist-types/components/select-rich/test-helpers/getSelectRichMembers.d.ts.map +1 -0
- package/dist-types/exports/select-rich-test-helpers.d.ts +2 -0
- package/dist-types/exports/select-rich-test-helpers.d.ts.map +1 -0
- 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/src/gh-combobox/gh-combobox.js +1 -1
- package/docs/components/combobox/src/google-combobox/google-combobox.js +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 +4 -4
- 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/overview.md +1 -1
- 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/input-tel-dropdown/src/intl-input-tel-dropdown.js +1 -0
- 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/interaction-states.md +6 -0
- package/docs/fundamentals/systems/form/model-value.md +10 -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 +2 -2
- 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/exports/select-rich-test-helpers.js +1 -0
- package/package.json +1 -1
- package/select-rich-test-helpers.js +1 -0
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
/* eslint-disable class-methods-use-this */
|
|
2
|
+
import { isTemplateResult } from 'lit/directive-helpers.js';
|
|
2
3
|
import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
|
3
4
|
import { render } from 'lit';
|
|
4
|
-
import { isTemplateResult } from 'lit/directive-helpers.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* @typedef {
|
|
8
|
-
* @typedef {import('../types/SlotMixinTypes.js').SlotsMap} SlotsMap
|
|
7
|
+
* @typedef {{renderBefore:Comment; renderTargetThatRespectsShadowRootScoping: HTMLDivElement}} RenderMetaObj
|
|
9
8
|
* @typedef {import('../types/SlotMixinTypes.js').SlotFunctionResult} SlotFunctionResult
|
|
10
9
|
* @typedef {import('../types/SlotMixinTypes.js').SlotRerenderObject} SlotRerenderObject
|
|
10
|
+
* @typedef {import('../types/SlotMixinTypes.js').SlotMixin} SlotMixin
|
|
11
|
+
* @typedef {import('../types/SlotMixinTypes.js').SlotsMap} SlotsMap
|
|
11
12
|
* @typedef {import('lit').LitElement} LitElement
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* @param {SlotFunctionResult} slotFunctionResult
|
|
17
|
+
* @returns {'template-result'|'node'|'slot-rerender-object'|null}
|
|
18
|
+
*/
|
|
19
|
+
function determineSlotFunctionResultType(slotFunctionResult) {
|
|
20
|
+
if (slotFunctionResult instanceof Node) {
|
|
21
|
+
return 'node';
|
|
22
|
+
}
|
|
23
|
+
if (isTemplateResult(slotFunctionResult)) {
|
|
24
|
+
return 'template-result';
|
|
25
|
+
}
|
|
26
|
+
if (
|
|
27
|
+
!Array.isArray(slotFunctionResult) &&
|
|
28
|
+
typeof slotFunctionResult === 'object' &&
|
|
29
|
+
'template' in slotFunctionResult
|
|
30
|
+
) {
|
|
31
|
+
return 'slot-rerender-object';
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
16
35
|
|
|
17
36
|
/**
|
|
18
37
|
* All intricacies involved in managing light dom can be delegated to SlotMixin. Amongst others, it automatically:
|
|
@@ -45,8 +64,8 @@ const SlotMixinImplementation = superclass =>
|
|
|
45
64
|
* so that all interactions work as intended and no focus issues can arise (which would be the case
|
|
46
65
|
* when (cloned) nodes of a render outcome would be moved around)
|
|
47
66
|
* @private
|
|
48
|
-
* @type { Map<string,
|
|
49
|
-
this.
|
|
67
|
+
* @type { Map<string, RenderMetaObj> } */
|
|
68
|
+
this.__renderMetaPerSlot = new Map();
|
|
50
69
|
/**
|
|
51
70
|
* @private
|
|
52
71
|
* @type {Set<string>}
|
|
@@ -79,9 +98,9 @@ const SlotMixinImplementation = superclass =>
|
|
|
79
98
|
__rerenderSlot(slotName) {
|
|
80
99
|
const slotFunctionResult = /** @type {SlotRerenderObject} */ (this.slots[slotName]());
|
|
81
100
|
this.__renderTemplateInScopedContext({
|
|
101
|
+
renderAsDirectHostChild: slotFunctionResult.renderAsDirectHostChild,
|
|
82
102
|
template: slotFunctionResult.template,
|
|
83
103
|
slotName,
|
|
84
|
-
shouldRerender: true,
|
|
85
104
|
});
|
|
86
105
|
// TODO: this is deprecated, remove later
|
|
87
106
|
slotFunctionResult.afterRender?.();
|
|
@@ -94,14 +113,8 @@ const SlotMixinImplementation = superclass =>
|
|
|
94
113
|
update(changedProperties) {
|
|
95
114
|
super.update(changedProperties);
|
|
96
115
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this.__rerenderSlot(slotName);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (this.__isFirstSlotUpdate) {
|
|
104
|
-
this.__isFirstSlotUpdate = false;
|
|
116
|
+
for (const slotName of this.__slotsThatNeedRerender) {
|
|
117
|
+
this.__rerenderSlot(slotName);
|
|
105
118
|
}
|
|
106
119
|
}
|
|
107
120
|
|
|
@@ -110,32 +123,59 @@ const SlotMixinImplementation = superclass =>
|
|
|
110
123
|
* @param {object} opts
|
|
111
124
|
* @param {import('lit').TemplateResult} opts.template
|
|
112
125
|
* @param {string} opts.slotName
|
|
113
|
-
* @param {boolean} [opts.
|
|
126
|
+
* @param {boolean} [opts.renderAsDirectHostChild] when false, the render parent (wrapper div) will be kept in the light dom
|
|
127
|
+
* @returns {void}
|
|
114
128
|
*/
|
|
115
|
-
__renderTemplateInScopedContext({ template, slotName,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (this.__scopedRenderRoots.has(slotName)) {
|
|
123
|
-
renderTarget = this.__scopedRenderRoots.get(slotName);
|
|
124
|
-
} else {
|
|
129
|
+
__renderTemplateInScopedContext({ template, slotName, renderAsDirectHostChild }) {
|
|
130
|
+
const isFirstRender = !this.__renderMetaPerSlot.has(slotName);
|
|
131
|
+
if (isFirstRender) {
|
|
132
|
+
// @ts-expect-error wait for browser support
|
|
133
|
+
const supportsScopedRegistry = !!ShadowRoot.prototype.createElement;
|
|
134
|
+
const registryRoot = supportsScopedRegistry ? this.shadowRoot || document : document;
|
|
135
|
+
|
|
125
136
|
// @ts-expect-error wait for browser support
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
137
|
+
const renderTargetThatRespectsShadowRootScoping = registryRoot.createElement('div');
|
|
138
|
+
const startComment = document.createComment(`_start_slot_${slotName}_`);
|
|
139
|
+
const endComment = document.createComment(`_end_slot_${slotName}_`);
|
|
140
|
+
|
|
141
|
+
renderTargetThatRespectsShadowRootScoping.appendChild(startComment);
|
|
142
|
+
renderTargetThatRespectsShadowRootScoping.appendChild(endComment);
|
|
143
|
+
|
|
144
|
+
// Providing all options breaks Safari; keep host and creationScope
|
|
145
|
+
const { creationScope, host } = this.renderOptions;
|
|
146
|
+
render(template, renderTargetThatRespectsShadowRootScoping, {
|
|
147
|
+
renderBefore: endComment,
|
|
148
|
+
creationScope,
|
|
149
|
+
host,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (renderAsDirectHostChild) {
|
|
153
|
+
const nodes = Array.from(renderTargetThatRespectsShadowRootScoping.childNodes);
|
|
154
|
+
this.__appendNodes({ nodes, renderParent: this, slotName });
|
|
155
|
+
} else {
|
|
156
|
+
renderTargetThatRespectsShadowRootScoping.slot = slotName;
|
|
157
|
+
this.appendChild(renderTargetThatRespectsShadowRootScoping);
|
|
130
158
|
}
|
|
131
|
-
|
|
159
|
+
|
|
160
|
+
this.__renderMetaPerSlot.set(slotName, {
|
|
161
|
+
renderTargetThatRespectsShadowRootScoping,
|
|
162
|
+
renderBefore: endComment,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return;
|
|
132
166
|
}
|
|
133
167
|
|
|
134
|
-
//
|
|
135
|
-
const {
|
|
136
|
-
|
|
168
|
+
// Rerender
|
|
169
|
+
const { renderBefore, renderTargetThatRespectsShadowRootScoping } =
|
|
170
|
+
/** @type {RenderMetaObj} */ (this.__renderMetaPerSlot.get(slotName));
|
|
171
|
+
|
|
172
|
+
const rerenderTarget = renderAsDirectHostChild
|
|
173
|
+
? this
|
|
174
|
+
: renderTargetThatRespectsShadowRootScoping;
|
|
137
175
|
|
|
138
|
-
|
|
176
|
+
// Providing all options breaks Safari: we keep host and creationScope
|
|
177
|
+
const { creationScope, host } = this.renderOptions;
|
|
178
|
+
render(template, rerenderTarget, { creationScope, host, renderBefore });
|
|
139
179
|
}
|
|
140
180
|
|
|
141
181
|
/**
|
|
@@ -145,13 +185,8 @@ const SlotMixinImplementation = superclass =>
|
|
|
145
185
|
* which can be used as a render target for most
|
|
146
186
|
* @param {string} options.slotName For the first render, it's best to use slotName
|
|
147
187
|
*/
|
|
148
|
-
|
|
188
|
+
__appendNodes({ nodes, renderParent = this, slotName }) {
|
|
149
189
|
for (const node of nodes) {
|
|
150
|
-
if (!(node instanceof Node)) {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
// Here, we add the slot name to the node that is an element
|
|
154
|
-
// (ignoring helper comment nodes that might be in our nodes array)
|
|
155
190
|
if (node instanceof Element && slotName && slotName !== '') {
|
|
156
191
|
node.setAttribute('slot', slotName);
|
|
157
192
|
}
|
|
@@ -182,29 +217,36 @@ const SlotMixinImplementation = superclass =>
|
|
|
182
217
|
this.__privateSlots.add(slotName);
|
|
183
218
|
}
|
|
184
219
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
220
|
+
const slotFunctionResultType = determineSlotFunctionResultType(slotFunctionResult);
|
|
221
|
+
|
|
222
|
+
switch (slotFunctionResultType) {
|
|
223
|
+
case 'template-result':
|
|
224
|
+
this.__renderTemplateInScopedContext({
|
|
225
|
+
template: /** @type {import('lit').TemplateResult} */ (slotFunctionResult),
|
|
226
|
+
renderAsDirectHostChild: true,
|
|
227
|
+
slotName,
|
|
228
|
+
});
|
|
229
|
+
break;
|
|
230
|
+
case 'node':
|
|
231
|
+
this.__appendNodes({
|
|
232
|
+
nodes: [/** @type {Node} */ (slotFunctionResult)],
|
|
233
|
+
renderParent: this,
|
|
234
|
+
slotName,
|
|
235
|
+
});
|
|
236
|
+
break;
|
|
237
|
+
case 'slot-rerender-object':
|
|
238
|
+
// Rerenderable slots are scheduled in the "update loop"
|
|
239
|
+
this.__slotsThatNeedRerender.add(slotName);
|
|
240
|
+
// For backw. compat, we allow a first render on connectedCallback
|
|
241
|
+
if (/** @type {SlotRerenderObject} */ (slotFunctionResult).firstRenderOnConnected) {
|
|
242
|
+
this.__rerenderSlot(slotName);
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
default:
|
|
246
|
+
throw new Error(
|
|
247
|
+
`Slot "${slotName}" configured inside "get slots()" (in prototype) of ${this.constructor.name} may return these types: TemplateResult | Node | {template:TemplateResult, afterRender?:function} | undefined.
|
|
248
|
+
You provided: ${slotFunctionResult}`,
|
|
249
|
+
);
|
|
208
250
|
}
|
|
209
251
|
}
|
|
210
252
|
}
|
|
@@ -1,17 +1,27 @@
|
|
|
1
|
+
import { isServer } from 'lit';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* From https://stackoverflow.com/questions/4565112/javascript-how-to-find-out-if-the-user-browser-is-chrome
|
|
3
|
-
* @param {string} [flavor]
|
|
5
|
+
* @param {string} [flavor='google-chrome']
|
|
4
6
|
*/
|
|
5
7
|
function checkChrome(flavor = 'google-chrome') {
|
|
6
|
-
|
|
8
|
+
if (isServer) {
|
|
9
|
+
return flavor === 'google-chrome';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const isChromium = /** @type {window & { chrome?: boolean}} */ (globalThis).chrome;
|
|
13
|
+
|
|
7
14
|
if (flavor === 'chromium') {
|
|
8
15
|
return isChromium;
|
|
9
16
|
}
|
|
10
|
-
const winNav =
|
|
11
|
-
const vendorName = winNav
|
|
12
|
-
const isOpera =
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
const winNav = globalThis.navigator;
|
|
18
|
+
const vendorName = winNav?.vendor;
|
|
19
|
+
const isOpera =
|
|
20
|
+
typeof (/** @type {window & { opr?: boolean}} */ (globalThis).opr) !== 'undefined';
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
const isIEedge = globalThis.userAgent?.indexOf('Edge') > -1;
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
const isIOSChrome = globalThis.userAgent?.match('CriOS');
|
|
15
25
|
|
|
16
26
|
if (flavor === 'ios') {
|
|
17
27
|
return isIOSChrome;
|
|
@@ -31,18 +41,18 @@ function checkChrome(flavor = 'google-chrome') {
|
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
export const browserDetection = {
|
|
34
|
-
isIE11: /Trident/.test(
|
|
44
|
+
isIE11: /Trident/.test(globalThis.navigator?.userAgent),
|
|
35
45
|
isChrome: checkChrome(),
|
|
36
46
|
isIOSChrome: checkChrome('ios'),
|
|
37
47
|
isChromium: checkChrome('chromium'),
|
|
38
|
-
isFirefox: navigator
|
|
39
|
-
isMac: navigator
|
|
40
|
-
isIOS: /iPhone|iPad|iPod/i.test(navigator
|
|
48
|
+
isFirefox: globalThis.navigator?.userAgent.toLowerCase().indexOf('firefox') > -1,
|
|
49
|
+
isMac: globalThis.navigator?.appVersion?.indexOf('Mac') !== -1,
|
|
50
|
+
isIOS: /iPhone|iPad|iPod/i.test(globalThis.navigator?.userAgent),
|
|
41
51
|
isMacSafari:
|
|
42
|
-
navigator
|
|
43
|
-
navigator
|
|
44
|
-
navigator
|
|
45
|
-
navigator
|
|
46
|
-
navigator
|
|
47
|
-
navigator
|
|
52
|
+
globalThis.navigator?.vendor &&
|
|
53
|
+
globalThis.navigator?.vendor.indexOf('Apple') > -1 &&
|
|
54
|
+
globalThis.navigator?.userAgent &&
|
|
55
|
+
globalThis.navigator?.userAgent.indexOf('CriOS') === -1 &&
|
|
56
|
+
globalThis.navigator?.userAgent.indexOf('FxiOS') === -1 &&
|
|
57
|
+
globalThis.navigator?.appVersion.indexOf('Mac') !== -1,
|
|
48
58
|
};
|
|
@@ -208,6 +208,7 @@ describe('SlotMixin', () => {
|
|
|
208
208
|
...super.slots,
|
|
209
209
|
'focusable-node': () => ({
|
|
210
210
|
template: html`<input /> `,
|
|
211
|
+
renderAsDirectHostChild: false,
|
|
211
212
|
}),
|
|
212
213
|
};
|
|
213
214
|
}
|
|
@@ -225,7 +226,6 @@ describe('SlotMixin', () => {
|
|
|
225
226
|
},
|
|
226
227
|
);
|
|
227
228
|
const el = /** @type {* & SlotHost} */ (await fixture(`<${tag}></${tag}>`));
|
|
228
|
-
|
|
229
229
|
el._focusableNode.focus();
|
|
230
230
|
expect(document.activeElement).to.equal(el._focusableNode);
|
|
231
231
|
|
|
@@ -298,6 +298,69 @@ describe('SlotMixin', () => {
|
|
|
298
298
|
expect(el._focusableNode.shadowRoot.activeElement).to.equal(el._focusableNode._buttonNode);
|
|
299
299
|
});
|
|
300
300
|
|
|
301
|
+
it('allows for rerendering complex shadow root into slot as a direct child', async () => {
|
|
302
|
+
const complexSlotTagName = defineCE(
|
|
303
|
+
class extends LitElement {
|
|
304
|
+
render() {
|
|
305
|
+
return html`
|
|
306
|
+
<input />
|
|
307
|
+
<button>I will be focused</button>
|
|
308
|
+
`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
get _buttonNode() {
|
|
312
|
+
// @ts-expect-error
|
|
313
|
+
return this.shadowRoot.querySelector('button');
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const complexSlotTag = unsafeStatic(complexSlotTagName);
|
|
319
|
+
|
|
320
|
+
const tagName = defineCE(
|
|
321
|
+
// @ts-expect-error
|
|
322
|
+
class extends SlotMixin(LitElement) {
|
|
323
|
+
static properties = { currentValue: Number };
|
|
324
|
+
|
|
325
|
+
constructor() {
|
|
326
|
+
super();
|
|
327
|
+
this.currentValue = 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
get slots() {
|
|
331
|
+
return {
|
|
332
|
+
...super.slots,
|
|
333
|
+
'focusable-node': () => ({
|
|
334
|
+
template: html`<${complexSlotTag}> </${complexSlotTag}> `,
|
|
335
|
+
renderAsDirectHostChild: true,
|
|
336
|
+
}),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
render() {
|
|
341
|
+
return html`<slot name="focusable-node"></slot>`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
get _focusableNode() {
|
|
345
|
+
return /** @type HTMLSpanElement */ (
|
|
346
|
+
Array.from(this.children).find(elm => elm.slot === 'focusable-node')
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
);
|
|
351
|
+
const el = /** @type {* & SlotHost} */ (await fixture(`<${tagName}></${tagName}>`));
|
|
352
|
+
|
|
353
|
+
el._focusableNode._buttonNode.focus();
|
|
354
|
+
|
|
355
|
+
expect(el._focusableNode.shadowRoot.activeElement).to.equal(el._focusableNode._buttonNode);
|
|
356
|
+
|
|
357
|
+
el.currentValue = 1;
|
|
358
|
+
await el.updateComplete;
|
|
359
|
+
|
|
360
|
+
expect(document.activeElement).to.equal(el._focusableNode);
|
|
361
|
+
expect(el._focusableNode.shadowRoot.activeElement).to.equal(el._focusableNode._buttonNode);
|
|
362
|
+
});
|
|
363
|
+
|
|
301
364
|
describe('firstRenderOnConnected (for backwards compatibility)', () => {
|
|
302
365
|
it('does render on connected when firstRenderOnConnected:true', async () => {
|
|
303
366
|
// Start with elem that does not render on connectedCallback
|
|
@@ -18,6 +18,14 @@ export type SlotRerenderObject = {
|
|
|
18
18
|
* For new components, please align with ReactiveElement/LitElement reactive cycle callbacks.
|
|
19
19
|
*/
|
|
20
20
|
firstRenderOnConnected?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* This is recommended to set to true always, as its behavior is usually desired and more in line with slot nodes that
|
|
23
|
+
* are not configured as rerenderable.
|
|
24
|
+
* For backward compatibility, it is set to false by default.
|
|
25
|
+
* When not configured, content is wrapped in a div (this can be problematic for ::slotted css selectors and for
|
|
26
|
+
* querySelectors that expect [slot=x] to have some semantic or (presentational) value).
|
|
27
|
+
*/
|
|
28
|
+
renderAsDirectHostChild?: boolean;
|
|
21
29
|
};
|
|
22
30
|
|
|
23
31
|
export type SlotFunctionResult = TemplateResult | Element | SlotRerenderObject | undefined;
|
|
@@ -28,6 +28,19 @@ const FormRegisteringMixinImplementation = superclass =>
|
|
|
28
28
|
* @type {FormRegistrarHost | undefined}
|
|
29
29
|
*/
|
|
30
30
|
this._parentFormGroup = undefined;
|
|
31
|
+
/**
|
|
32
|
+
* To encourage accessibility best practices, `form-element-register` events
|
|
33
|
+
* do not pierce through shadow roots. This forces the developer to create form groups and fieldsets that automatically allow the creation of accessible relationships in the same dom tree.
|
|
34
|
+
Use this option if you know what you're doing. It will then be possible to nest FormControls
|
|
35
|
+
inside shadow dom. See https://lion-web.netlify.app/fundamentals/rationales/accessibility/#shadow-roots-and-accessibility
|
|
36
|
+
*/
|
|
37
|
+
this.allowCrossRootRegistration = false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static get properties() {
|
|
41
|
+
return {
|
|
42
|
+
allowCrossRootRegistration: { type: Boolean, attribute: 'allow-cross-root-registration' },
|
|
43
|
+
};
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
connectedCallback() {
|
|
@@ -36,6 +49,7 @@ const FormRegisteringMixinImplementation = superclass =>
|
|
|
36
49
|
new CustomEvent('form-element-register', {
|
|
37
50
|
detail: { element: this },
|
|
38
51
|
bubbles: true,
|
|
52
|
+
composed: Boolean(this.allowCrossRootRegistration),
|
|
39
53
|
}),
|
|
40
54
|
);
|
|
41
55
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { LitElement } from 'lit';
|
|
2
1
|
import { uuid } from '@lion/ui/core.js';
|
|
3
|
-
import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
|
4
2
|
import {
|
|
5
3
|
FormRegisteringMixin,
|
|
6
4
|
FormRegistrarMixin,
|
|
7
5
|
FormRegistrarPortalMixin,
|
|
8
6
|
} from '@lion/ui/form-core.js';
|
|
7
|
+
import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
|
8
|
+
import { LitElement } from 'lit';
|
|
9
|
+
import sinon from 'sinon';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @typedef {Object} customConfig
|
|
@@ -256,6 +257,60 @@ export const runRegistrationSuite = customConfig => {
|
|
|
256
257
|
]);
|
|
257
258
|
});
|
|
258
259
|
|
|
260
|
+
describe('FormRegisteringMixin', () => {
|
|
261
|
+
it('propagates the form-element-register event through the shadowDom', async () => {
|
|
262
|
+
const eventSpy = sinon.spy();
|
|
263
|
+
const withShadowFormControlStr = defineCE(
|
|
264
|
+
class extends FormRegistrarMixin(LitElement) {
|
|
265
|
+
render() {
|
|
266
|
+
return html`
|
|
267
|
+
<${childTag}
|
|
268
|
+
id="child"
|
|
269
|
+
@form-element-register=${eventSpy}
|
|
270
|
+
allow-cross-root-registration
|
|
271
|
+
>
|
|
272
|
+
</${childTag}>`;
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
const withShadowFormControlTag = unsafeStatic(withShadowFormControlStr);
|
|
277
|
+
|
|
278
|
+
const el = /** @type {RegistrarClass} */ (
|
|
279
|
+
await fixture(html`
|
|
280
|
+
<${withShadowFormControlTag}>
|
|
281
|
+
</${withShadowFormControlTag}>
|
|
282
|
+
`)
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
expect(eventSpy).to.have.been.calledOnce;
|
|
286
|
+
expect(eventSpy.getCall(0).args[0].composed).to.equal(true);
|
|
287
|
+
expect(el.formElements).to.deep.equal([el.shadowRoot?.querySelector('#child')]);
|
|
288
|
+
});
|
|
289
|
+
it('dispatches the form-element-register event with compose true if allowCrossRootRegistration is set', async () => {
|
|
290
|
+
const eventSpy = sinon.spy();
|
|
291
|
+
/** @type {RegisteringClass} */ (
|
|
292
|
+
await fixture(html`
|
|
293
|
+
<${childTag}
|
|
294
|
+
@form-element-register=${eventSpy}
|
|
295
|
+
allow-cross-root-registration
|
|
296
|
+
>
|
|
297
|
+
</${childTag}>
|
|
298
|
+
`)
|
|
299
|
+
);
|
|
300
|
+
expect(eventSpy).to.have.been.calledOnce;
|
|
301
|
+
expect(eventSpy.getCall(0).args[0].composed).to.equal(true);
|
|
302
|
+
});
|
|
303
|
+
it('dispatches the form-element-register event with compose false if allowCrossRootRegistration is not set', async () => {
|
|
304
|
+
const eventSpy = sinon.spy();
|
|
305
|
+
/** @type {RegisteringClass} */ (
|
|
306
|
+
await fixture(html`
|
|
307
|
+
<${childTag} @form-element-register=${eventSpy}></${childTag}>
|
|
308
|
+
`)
|
|
309
|
+
);
|
|
310
|
+
expect(eventSpy).to.have.been.calledOnce;
|
|
311
|
+
expect(eventSpy.getCall(0).args[0].composed).to.equal(false);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
259
314
|
describe('FormRegistrarPortalMixin', () => {
|
|
260
315
|
it('forwards registrations to the .registrationTarget', async () => {
|
|
261
316
|
const el = /** @type {RegistrarClass} */ (
|
|
@@ -666,7 +666,7 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
|
|
666
666
|
|
|
667
667
|
await triggerFocusFor(hobbyInputNode);
|
|
668
668
|
await triggerFocusFor(genderInputNode);
|
|
669
|
-
const button = /** @type {FormGroup} */ (await fixture(html`<button
|
|
669
|
+
const button = /** @type {FormGroup} */ (await fixture(html`<button>Button</button>`));
|
|
670
670
|
button.focus();
|
|
671
671
|
|
|
672
672
|
expect(el.touched).to.be.true;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { LitElement } from 'lit';
|
|
2
1
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
|
2
|
+
import { LitElement } from 'lit';
|
|
3
3
|
|
|
4
4
|
import { FormRegistrarHost } from './FormRegistrarMixinTypes.js';
|
|
5
5
|
|
|
@@ -8,6 +8,14 @@ export declare class FormRegisteringHost {
|
|
|
8
8
|
* The name the host is registered with to a parent
|
|
9
9
|
*/
|
|
10
10
|
name: string;
|
|
11
|
+
/**
|
|
12
|
+
* To encourage accessibility best practices, `form-element-register` events
|
|
13
|
+
* do not pierce through shadow roots. This forces the developer to create form groups and fieldsets that
|
|
14
|
+
* automatically allow the creation of accessible relationships in the same dom tree.
|
|
15
|
+
* Use this option if you know what you're doing. It will then be possible to nest FormControls
|
|
16
|
+
* inside shadow dom. See https://lion-web.netlify.app/fundamentals/rationales/accessibility#shadow-roots-and-accessibility
|
|
17
|
+
*/
|
|
18
|
+
allowCrossRootRegistration: boolean;
|
|
11
19
|
|
|
12
20
|
/**
|
|
13
21
|
* The registrar this FormControl registers to, Usually a descendant of FormGroup or
|
|
@@ -11,7 +11,8 @@ import { parseNumber, getFractionDigits } from '@lion/ui/localize-no-side-effect
|
|
|
11
11
|
* @return {number} new value with rounded up decimals
|
|
12
12
|
*/
|
|
13
13
|
function round(value, decimals) {
|
|
14
|
-
|
|
14
|
+
const numberContainsExponent = value?.toString().includes('e');
|
|
15
|
+
if (typeof decimals === 'undefined' || numberContainsExponent) {
|
|
15
16
|
return Number(value);
|
|
16
17
|
}
|
|
17
18
|
return Number(`${Math.round(Number(`${value}e${decimals}`))}e-${decimals}`);
|
|
@@ -30,10 +31,17 @@ function round(value, decimals) {
|
|
|
30
31
|
* @param {FormatOptions} [givenOptions] Locale Options
|
|
31
32
|
*/
|
|
32
33
|
export function parseAmount(value, givenOptions) {
|
|
34
|
+
const unmatchedInput = value.match(/[^0-9,.\- ]/g);
|
|
35
|
+
// for the full paste behavior documentation:
|
|
36
|
+
// ./docs/components/input-amount/use-cases.md#paste-behavior
|
|
37
|
+
if (unmatchedInput && givenOptions?.mode !== 'pasted') {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
const number = parseNumber(value, givenOptions);
|
|
34
42
|
|
|
35
|
-
if (typeof number !== 'number') {
|
|
36
|
-
return
|
|
43
|
+
if (typeof number !== 'number' || Number.isNaN(number)) {
|
|
44
|
+
return undefined;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
/** @type {FormatOptions} */
|
|
@@ -74,4 +74,15 @@ describe('formatAmount()', () => {
|
|
|
74
74
|
localizeManager.locale = 'nl-NL';
|
|
75
75
|
expect(formatAmount(12345678)).to.equal('12.345.678,00');
|
|
76
76
|
});
|
|
77
|
+
|
|
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
|
+
);
|
|
87
|
+
});
|
|
77
88
|
});
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { expect } from '@open-wc/testing';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { getLocalizeManager } from '@lion/ui/localize-no-side-effects.js';
|
|
3
|
+
import { localizeTearDown } from '@lion/ui/localize-test-helpers.js';
|
|
4
4
|
import { parseAmount } from '@lion/ui/input-amount.js';
|
|
5
5
|
|
|
6
6
|
describe('parseAmount()', async () => {
|
|
7
|
+
const localizeManager = getLocalizeManager();
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
localizeManager.locale = 'en-GB';
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
localizeTearDown();
|
|
15
|
+
});
|
|
16
|
+
|
|
7
17
|
it('with currency set to correct amount of decimals', async () => {
|
|
8
|
-
localize.locale = 'en-GB';
|
|
9
18
|
expect(
|
|
10
19
|
parseAmount('1.015', {
|
|
11
20
|
currency: 'EUR',
|
|
@@ -29,7 +38,26 @@ describe('parseAmount()', async () => {
|
|
|
29
38
|
});
|
|
30
39
|
|
|
31
40
|
it('with no currency keeps all decimals', async () => {
|
|
32
|
-
localize.locale = 'en-GB';
|
|
33
41
|
expect(parseAmount('1.015')).to.equal(1.015);
|
|
34
42
|
});
|
|
43
|
+
|
|
44
|
+
// TODO: make it work with big numbers, e.g. make use of BigInt
|
|
45
|
+
it.skip('rounds up big numbers', async () => {
|
|
46
|
+
// eslint-disable-next-line no-loss-of-precision
|
|
47
|
+
expect(parseAmount('999999999999999999999,42')).to.equal(999999999999999999999.42);
|
|
48
|
+
// eslint-disable-next-line no-loss-of-precision
|
|
49
|
+
expect(parseAmount('12,345,678,987,654,321.42')).to.equal(12345678987654321.42);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('returns undefined if an invalid value is entered', async () => {
|
|
53
|
+
expect(parseAmount('foo')).to.equal(undefined);
|
|
54
|
+
expect(parseAmount('foo1')).to.equal(undefined);
|
|
55
|
+
expect(parseAmount('EUR 1,50')).to.equal(undefined);
|
|
56
|
+
expect(parseAmount('--1')).to.equal(undefined);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('ignores letters when "pasted" mode used', async () => {
|
|
60
|
+
expect(parseAmount('foo1', { mode: 'pasted' })).to.equal(1);
|
|
61
|
+
expect(parseAmount('EUR 1,50', { mode: 'pasted' })).to.equal(1.5);
|
|
62
|
+
});
|
|
35
63
|
});
|
|
@@ -84,6 +84,7 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
|
|
|
84
84
|
.multiple=${this.multiple}
|
|
85
85
|
></lion-selected-file-list>
|
|
86
86
|
`,
|
|
87
|
+
renderAsDirectHostChild: true,
|
|
87
88
|
}),
|
|
88
89
|
};
|
|
89
90
|
}
|
|
@@ -183,7 +184,7 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
|
|
|
183
184
|
*/
|
|
184
185
|
get _fileListNode() {
|
|
185
186
|
return /** @type {LionSelectedFileList} */ (
|
|
186
|
-
Array.from(this.children).find(child => child.slot === 'selected-file-list')
|
|
187
|
+
Array.from(this.children).find(child => child.slot === 'selected-file-list')
|
|
187
188
|
);
|
|
188
189
|
}
|
|
189
190
|
|