@lion/ui 0.6.0-beta.0 → 0.6.1
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/test/SlotMixin.test.js +64 -1
- package/components/core/types/SlotMixinTypes.ts +8 -0
- package/components/form-core/src/choice-group/ChoiceGroupMixin.js +10 -1
- package/components/form-core/src/registration/FormRegisteringMixin.js +14 -0
- package/components/form-core/src/validate/ValidateMixin.js +10 -0
- package/components/form-core/src/validate/Validator.js +6 -3
- package/components/form-core/test-suites/FormRegistrationMixins.suite.js +64 -9
- package/components/form-core/types/FormControlMixinTypes.ts +2 -0
- package/components/form-core/types/registration/FormRegisteringMixinTypes.ts +9 -1
- package/components/form-core/types/validate/ValidateMixinTypes.ts +2 -0
- package/components/form-core/types/validate/validate.ts +2 -2
- package/components/input-file/src/LionInputFile.js +9 -2
- package/components/input-tel-dropdown/src/LionInputTelDropdown.js +12 -3
- package/components/input-tel-dropdown/test/LionInputTelDropdown.test.js +110 -2
- package/components/input-tel-dropdown/test-helpers/mimicUserChangingDropdown.js +22 -0
- package/components/input-tel-dropdown/test-suites/LionInputTelDropdown.suite.js +2 -39
- package/components/localize/src/LocalizeMixin.js +2 -2
- package/components/overlays/src/OverlayController.js +1 -1
- package/components/select/src/LionSelect.js +5 -0
- package/components/validate-messages/src/getLocalizedMessage.js +12 -1
- package/components/validate-messages/test/loadDefaultFeedbackMessagesNoSideEffects.test.js +105 -0
- package/components/validate-messages/translations/bg.js +4 -0
- package/components/validate-messages/translations/cs.js +4 -0
- package/components/validate-messages/translations/de.js +4 -0
- package/components/validate-messages/translations/en.js +5 -1
- package/components/validate-messages/translations/es.js +4 -0
- package/components/validate-messages/translations/fr.js +4 -0
- package/components/validate-messages/translations/hu.js +4 -0
- package/components/validate-messages/translations/it.js +4 -0
- package/components/validate-messages/translations/nl.js +4 -0
- package/components/validate-messages/translations/pl.js +4 -0
- package/components/validate-messages/translations/ro.js +4 -0
- package/components/validate-messages/translations/ru.js +4 -0
- package/components/validate-messages/translations/sk.js +4 -0
- package/components/validate-messages/translations/uk.js +4 -0
- package/components/validate-messages/translations/zh.js +4 -0
- package/custom-elements.json +5712 -1960
- 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/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/choice-group/ChoiceGroupMixin.d.ts +1 -0
- package/dist-types/components/form-core/src/choice-group/ChoiceGroupMixin.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/src/validate/ValidateMixin.d.ts +1 -0
- package/dist-types/components/form-core/src/validate/ValidateMixin.d.ts.map +1 -1
- package/dist-types/components/form-core/src/validate/Validator.d.ts +5 -2
- package/dist-types/components/form-core/src/validate/Validator.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/FormControlMixinTypes.d.ts +1 -0
- package/dist-types/components/form-core/types/FormControlMixinTypes.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/form-core/types/validate/ValidateMixinTypes.d.ts +1 -0
- package/dist-types/components/form-core/types/validate/ValidateMixinTypes.d.ts.map +1 -1
- package/dist-types/components/form-core/types/validate/validate.d.ts +2 -2
- 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-helpers/mimicUserChangingDropdown.d.ts +12 -0
- package/dist-types/components/input-tel-dropdown/test-helpers/mimicUserChangingDropdown.d.ts.map +1 -0
- package/dist-types/components/input-tel-dropdown/test-suites/LionInputTelDropdown.suite.d.ts.map +1 -1
- package/dist-types/components/select/src/LionSelect.d.ts.map +1 -1
- package/dist-types/components/validate-messages/src/getLocalizedMessage.d.ts.map +1 -1
- package/dist-types/components/validate-messages/test/loadDefaultFeedbackMessagesNoSideEffects.test.d.ts +4 -0
- package/dist-types/components/validate-messages/test/loadDefaultFeedbackMessagesNoSideEffects.test.d.ts.map +1 -0
- package/dist-types/components/validate-messages/translations/bg-BG.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/bg.d.ts +6 -0
- package/dist-types/components/validate-messages/translations/cs-CZ.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/cs.d.ts +6 -0
- package/dist-types/components/validate-messages/translations/de-DE.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/de.d.ts +6 -0
- package/dist-types/components/validate-messages/translations/en-AU.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/en-GB.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/en-PH.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/en-US.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/en.d.ts +6 -0
- package/dist-types/components/validate-messages/translations/es-ES.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/es.d.ts +6 -0
- package/dist-types/components/validate-messages/translations/fr-BE.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/fr-FR.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/fr.d.ts +6 -0
- package/dist-types/components/validate-messages/translations/hu-HU.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/hu.d.ts +6 -0
- package/dist-types/components/validate-messages/translations/it-IT.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/it.d.ts +6 -0
- package/dist-types/components/validate-messages/translations/nl-BE.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/nl-NL.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/nl.d.ts +6 -0
- package/dist-types/components/validate-messages/translations/pl-PL.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/pl.d.ts +6 -0
- package/dist-types/components/validate-messages/translations/ro-RO.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/ro.d.ts +6 -0
- package/dist-types/components/validate-messages/translations/ru-RU.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/ru.d.ts +6 -0
- package/dist-types/components/validate-messages/translations/sk-SK.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/sk.d.ts +6 -0
- package/dist-types/components/validate-messages/translations/uk-UA.d.ts +4 -0
- package/dist-types/components/validate-messages/translations/uk.d.ts +6 -0
- package/dist-types/components/validate-messages/translations/zh.d.ts +6 -0
- package/dist-types/exports/input-tel-dropdown-test-helpers.d.ts +2 -0
- package/dist-types/exports/input-tel-dropdown-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/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-file/use-cases.md +8 -8
- 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/listbox/use-cases.md +90 -95
- package/docs/components/radio-group/overview.md +3 -3
- package/docs/components/select/use-cases.md +34 -33
- 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/styling.md +34 -39
- package/docs/fundamentals/systems/form/validate.md +7 -4
- 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/ajax/overview.md +2 -1
- 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/input-tel-dropdown-test-helpers.js +1 -0
- package/input-tel-dropdown-test-helpers.js +1 -0
- package/package.json +1 -1
- package/components/validate-messages/test/loadDefaultFeedbackMessagesNoSideEffects.js +0 -51
- package/dist-types/components/validate-messages/test/loadDefaultFeedbackMessagesNoSideEffects.d.ts +0 -2
- package/dist-types/components/validate-messages/test/loadDefaultFeedbackMessagesNoSideEffects.d.ts.map +0 -1
|
@@ -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
|
}
|
|
@@ -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;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
|
2
2
|
import { FormRegistrarMixin } from '../registration/FormRegistrarMixin.js';
|
|
3
3
|
import { InteractionStateMixin } from '../InteractionStateMixin.js';
|
|
4
|
+
import { ValidateMixin } from '../validate/ValidateMixin.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @typedef {import('../../types/choice-group/ChoiceGroupMixinTypes.js').ChoiceGroupMixin} ChoiceGroupMixin
|
|
@@ -8,6 +9,7 @@ import { InteractionStateMixin } from '../InteractionStateMixin.js';
|
|
|
8
9
|
* @typedef {import('../../types/registration/FormRegistrarMixinTypes.js').ElementWithParentFormGroup} ElementWithParentFormGroup
|
|
9
10
|
* @typedef {import('../../types/form-group/FormGroupMixinTypes.js').FormControl} FormControl
|
|
10
11
|
* @typedef {import('../../types/choice-group/ChoiceInputMixinTypes.js').ChoiceInputHost} ChoiceInputHost
|
|
12
|
+
* @typedef {import('../validate/Validator.js').Validator} Validator
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -22,7 +24,9 @@ import { InteractionStateMixin } from '../InteractionStateMixin.js';
|
|
|
22
24
|
*/
|
|
23
25
|
const ChoiceGroupMixinImplementation = superclass =>
|
|
24
26
|
// @ts-ignore https://github.com/microsoft/TypeScript/issues/36821#issuecomment-588375051
|
|
25
|
-
class ChoiceGroupMixin extends FormRegistrarMixin(
|
|
27
|
+
class ChoiceGroupMixin extends FormRegistrarMixin(
|
|
28
|
+
ValidateMixin(InteractionStateMixin(superclass)),
|
|
29
|
+
) {
|
|
26
30
|
/** @type {any} */
|
|
27
31
|
static get properties() {
|
|
28
32
|
return {
|
|
@@ -123,6 +127,11 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|
|
123
127
|
}
|
|
124
128
|
}
|
|
125
129
|
|
|
130
|
+
get operationMode() {
|
|
131
|
+
// @ts-ignore
|
|
132
|
+
return this._repropagationRole === 'choice-group' ? 'select' : 'enter';
|
|
133
|
+
}
|
|
134
|
+
|
|
126
135
|
constructor() {
|
|
127
136
|
super();
|
|
128
137
|
|
|
@@ -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
|
}
|
|
@@ -20,6 +20,7 @@ import { FormControlMixin } from '../FormControlMixin.js';
|
|
|
20
20
|
* @typedef {import('../../types/validate/ValidateMixinTypes.js').ValidateMixin} ValidateMixin
|
|
21
21
|
* @typedef {import('../../types/validate/ValidateMixinTypes.js').ValidationType} ValidationType
|
|
22
22
|
* @typedef {import('../../types/validate/ValidateMixinTypes.js').ValidateHost} ValidateHost
|
|
23
|
+
* @typedef {import('../../types/validate/ValidateMixinTypes.js').OperationMode} OperationMode
|
|
23
24
|
* @typedef {import('../../types/validate/index.js').ValidatorOutcome} ValidatorOutcome
|
|
24
25
|
* @typedef {typeof import('../../types/validate/ValidateMixinTypes.js').ValidateHost} ValidateHostConstructor
|
|
25
26
|
* @typedef {{validator:Validator; outcome:boolean|string}} ValidationResultEntry
|
|
@@ -98,6 +99,15 @@ export const ValidateMixinImplementation = superclass =>
|
|
|
98
99
|
return ['error'];
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Types of input interaction of the FormControl (for instance 'enter'|'select'|'upload')
|
|
104
|
+
* @overridable
|
|
105
|
+
* @type {OperationMode}
|
|
106
|
+
*/
|
|
107
|
+
get operationMode() {
|
|
108
|
+
return 'enter';
|
|
109
|
+
}
|
|
110
|
+
|
|
101
111
|
/**
|
|
102
112
|
* Adds "._feedbackNode" as described below
|
|
103
113
|
* @public
|
|
@@ -27,12 +27,15 @@ export class Validator extends EventTarget {
|
|
|
27
27
|
this.__param = param;
|
|
28
28
|
/** @type {ValidatorConfig} */
|
|
29
29
|
this.__config = config || {};
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Default type supported by ValidateMixin
|
|
32
|
+
* @type {ValidationType}
|
|
33
|
+
*/
|
|
34
|
+
this.type = config?.type || 'error';
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
|
-
* The name under which validation results get registered. For
|
|
38
|
+
* The name under which validation results get registered. For convenience and predictability, this
|
|
36
39
|
* should always be the same as the constructor name (since it will be obfuscated in js builds,
|
|
37
40
|
* we need to provide it separately).
|
|
38
41
|
* @type {ValidatorName}
|
|
@@ -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
|
|
@@ -163,20 +164,20 @@ export const runRegistrationSuite = customConfig => {
|
|
|
163
164
|
});
|
|
164
165
|
|
|
165
166
|
it('works for components that have a delayed render', async () => {
|
|
166
|
-
class
|
|
167
|
-
async
|
|
167
|
+
class ScheduleUpdate extends FormRegistrarMixin(LitElement) {
|
|
168
|
+
async scheduleUpdate() {
|
|
168
169
|
// eslint-disable-next-line no-promise-executor-return
|
|
169
170
|
await new Promise(resolve => setTimeout(() => resolve(undefined), 10));
|
|
170
|
-
await super.
|
|
171
|
+
await super.scheduleUpdate();
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
render() {
|
|
174
175
|
return html`<slot></slot>`;
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
|
-
const tagWrapperString = defineCE(
|
|
178
|
+
const tagWrapperString = defineCE(ScheduleUpdate);
|
|
178
179
|
const tagWrapper = unsafeStatic(tagWrapperString);
|
|
179
|
-
const el = /** @type {
|
|
180
|
+
const el = /** @type {ScheduleUpdate} */ (
|
|
180
181
|
await fixture(html`
|
|
181
182
|
<${tagWrapper}>
|
|
182
183
|
<${childTag}></${childTag}>
|
|
@@ -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} */ (
|
|
@@ -359,10 +414,10 @@ export const runRegistrationSuite = customConfig => {
|
|
|
359
414
|
it('works for portals that have a delayed render', async () => {
|
|
360
415
|
const delayedPortalString = defineCE(
|
|
361
416
|
class extends FormRegistrarPortalMixin(LitElement) {
|
|
362
|
-
async
|
|
417
|
+
async scheduleUpdate() {
|
|
363
418
|
// eslint-disable-next-line no-promise-executor-return
|
|
364
419
|
await new Promise(resolve => setTimeout(() => resolve(undefined), 10));
|
|
365
|
-
await super.
|
|
420
|
+
await super.scheduleUpdate();
|
|
366
421
|
}
|
|
367
422
|
|
|
368
423
|
render() {
|
|
@@ -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
|
|
@@ -16,6 +16,8 @@ export type FeedbackMessage = {
|
|
|
16
16
|
|
|
17
17
|
export type ValidationType = 'error' | 'warning' | 'info' | 'success' | string;
|
|
18
18
|
|
|
19
|
+
export type OperationMode = 'enter' | 'select' | 'upload';
|
|
20
|
+
|
|
19
21
|
export declare class ValidateHost {
|
|
20
22
|
/**
|
|
21
23
|
* Used by Application Developers to add Validators to a FormControl.
|
|
@@ -2,7 +2,7 @@ import { FormControlHost } from '../../src/FormControlMixin.js';
|
|
|
2
2
|
import { ValidationType } from './ValidateMixinTypes.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* The name under which validation results get registered. For
|
|
5
|
+
* The name under which validation results get registered. For convenience and predictability, this
|
|
6
6
|
* should always be the same as the constructor name (since it will be obfuscated in js builds,
|
|
7
7
|
* we need to provide it separately).
|
|
8
8
|
* @example
|
|
@@ -34,7 +34,7 @@ export type ValidatorConfig = {
|
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Output of the `execute` function that returns a validity outcome. When we need to
|
|
37
|
+
* Output of the `execute` function that returns a validity outcome. When we need to show feedback,
|
|
38
38
|
* it should return true, otherwise false. So when an error\info|warning|success message
|
|
39
39
|
* needs to be shown, return true.
|
|
40
40
|
* It's also possible to return an enum. Let's say that a phone number can have multiple
|
|
@@ -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
|
|
|
@@ -215,6 +216,11 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
|
|
|
215
216
|
}
|
|
216
217
|
}
|
|
217
218
|
|
|
219
|
+
// eslint-disable-next-line class-methods-use-this
|
|
220
|
+
get operationMode() {
|
|
221
|
+
return 'upload';
|
|
222
|
+
}
|
|
223
|
+
|
|
218
224
|
get _acceptCriteria() {
|
|
219
225
|
/** @type {string[]} */
|
|
220
226
|
let allowedFileTypes = [];
|
|
@@ -433,7 +439,8 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
|
|
|
433
439
|
];
|
|
434
440
|
}
|
|
435
441
|
});
|
|
436
|
-
//
|
|
442
|
+
// TODO: add a unit test for this
|
|
443
|
+
this._selectedFilesMetaData = [...this._selectedFilesMetaData];
|
|
437
444
|
}
|
|
438
445
|
});
|
|
439
446
|
}
|