@lion/ui 0.4.0-prerelease-bypass-export-map.0 → 0.4.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.
Files changed (99) hide show
  1. package/components/accordion/src/LionAccordion.js +20 -3
  2. package/components/accordion/test/lion-accordion.test.js +19 -0
  3. package/components/calendar/src/LionCalendar.js +5 -2
  4. package/components/calendar/src/utils/createMonth.js +1 -2
  5. package/components/calendar/test/lion-calendar.test.js +83 -4
  6. package/components/calendar/test/utils/createMonth.test.js +15 -0
  7. package/components/form-core/src/form-group/FormGroupMixin.js +7 -0
  8. package/components/form-core/src/validate/ValidateMixin.js +197 -153
  9. package/components/form-core/test-suites/ValidateMixin.suite.js +4 -4
  10. package/components/form-core/test-suites/form-group/FormGroupMixin-input.suite.js +1 -1
  11. package/components/form-core/test-suites/form-group/FormGroupMixin.suite.js +22 -7
  12. package/components/input-datepicker/src/LionInputDatepicker.js +4 -4
  13. package/components/input-datepicker/test/lion-input-datepicker.test.js +23 -0
  14. package/components/localize/types/LocalizeMixinTypes.ts +1 -1
  15. package/components/overlays/src/OverlayController.js +2 -4
  16. package/components/progress-indicator/src/LionProgressIndicator.js +1 -1
  17. package/components/progress-indicator/test/lion-progress-indicator.test.js +18 -1
  18. package/custom-elements.json +442 -140
  19. package/dist-types/components/accordion/src/LionAccordion.d.ts +10 -0
  20. package/dist-types/components/accordion/src/LionAccordion.d.ts.map +1 -1
  21. package/dist-types/components/calendar/src/LionCalendar.d.ts.map +1 -1
  22. package/dist-types/components/calendar/src/utils/createMonth.d.ts.map +1 -1
  23. package/dist-types/components/form-core/src/form-group/FormGroupMixin.d.ts.map +1 -1
  24. package/dist-types/components/form-core/src/validate/ValidateMixin.d.ts +1 -0
  25. package/dist-types/components/form-core/src/validate/ValidateMixin.d.ts.map +1 -1
  26. package/dist-types/components/form-core/test-suites/form-group/FormGroupMixin.suite.d.ts.map +1 -1
  27. package/dist-types/components/input-file/src/LionInputFile.d.ts +1 -1
  28. package/dist-types/components/input-file/src/LionInputFile.d.ts.map +1 -1
  29. package/dist-types/components/input-file/src/LionSelectedFileList.d.ts +1 -1
  30. package/dist-types/components/input-file/src/LionSelectedFileList.d.ts.map +1 -1
  31. package/dist-types/components/input-tel/src/LionInputTel.d.ts +1 -1
  32. package/dist-types/components/input-tel/src/LionInputTel.d.ts.map +1 -1
  33. package/dist-types/components/localize/types/LocalizeMixinTypes.d.ts +1 -1
  34. package/dist-types/components/localize/types/LocalizeMixinTypes.d.ts.map +1 -1
  35. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  36. package/dist-types/exports/types/calendar.d.ts +2 -0
  37. package/dist-types/exports/types/calendar.d.ts.map +1 -0
  38. package/dist-types/exports/types/combobox.d.ts +2 -0
  39. package/dist-types/exports/types/combobox.d.ts.map +1 -0
  40. package/dist-types/exports/types/listbox.d.ts +3 -0
  41. package/dist-types/exports/types/listbox.d.ts.map +1 -0
  42. package/dist-types/exports/types/localize.d.ts +1 -4
  43. package/dist-types/exports/types/localize.d.ts.map +1 -1
  44. package/dist-types/tsconfig.tsbuildinfo +1 -1
  45. package/docs/components/checkbox-group/overview.md +2 -2
  46. package/docs/components/collapsible/overview.md +1 -1
  47. package/docs/components/combobox/overview.md +1 -1
  48. package/docs/components/combobox/use-cases.md +1 -1
  49. package/docs/components/dialog/overview.md +1 -1
  50. package/docs/components/fieldset/overview.md +5 -5
  51. package/docs/components/form/overview.md +3 -3
  52. package/docs/components/icon/use-cases.md +1 -1
  53. package/docs/components/input/overview.md +1 -1
  54. package/docs/components/input-amount/overview.md +3 -3
  55. package/docs/components/input-date/overview.md +3 -3
  56. package/docs/components/input-datepicker/overview.md +5 -5
  57. package/docs/components/input-email/overview.md +2 -2
  58. package/docs/components/input-file/overview.md +1 -1
  59. package/docs/components/input-iban/overview.md +2 -2
  60. package/docs/components/input-range/overview.md +2 -2
  61. package/docs/components/input-stepper/overview.md +1 -1
  62. package/docs/components/input-tel/overview.md +1 -1
  63. package/docs/components/input-tel/use-cases.md +6 -6
  64. package/docs/components/input-tel-dropdown/overview.md +1 -1
  65. package/docs/components/radio-group/overview.md +3 -3
  66. package/docs/components/select-rich/use-cases.md +1 -1
  67. package/docs/components/tabs/examples.md +1 -1
  68. package/docs/fundamentals/index.md +4 -4
  69. package/docs/fundamentals/node-tools/providence-analytics/LocalConfiguration.md +1 -1
  70. package/docs/fundamentals/node-tools/providence-analytics/QueryResult.md +1 -1
  71. package/docs/fundamentals/node-tools/providence-analytics/analyzer.md +1 -1
  72. package/docs/fundamentals/node-tools/providence-analytics/dashboard.md +1 -1
  73. package/docs/fundamentals/node-tools/providence-analytics/overview.md +3 -3
  74. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  75. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  76. package/docs/fundamentals/rationales/accessibility.md +1 -1
  77. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  78. package/docs/fundamentals/systems/core/overview.md +1 -1
  79. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  80. package/docs/fundamentals/systems/form/model-value.md +1 -1
  81. package/docs/fundamentals/systems/form/overview.md +28 -28
  82. package/docs/fundamentals/systems/form/validate.md +1 -1
  83. package/docs/fundamentals/systems/localize/overview.md +4 -4
  84. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  85. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  86. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  87. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  88. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  89. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  90. package/docs/guides/how-to/create-a-form.md +4 -4
  91. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  92. package/docs/guides/index.md +8 -8
  93. package/docs/guides/principles/index.md +5 -5
  94. package/docs/guides/principles/styling.md +1 -1
  95. package/exports/types/calendar.ts +1 -0
  96. package/exports/types/combobox.ts +1 -0
  97. package/exports/types/listbox.ts +2 -0
  98. package/exports/types/localize.ts +13 -4
  99. package/package.json +1 -1
@@ -8,7 +8,7 @@ import { AsyncQueue } from '../utils/AsyncQueue.js';
8
8
  import { pascalCase } from '../utils/pascalCase.js';
9
9
  import { SyncUpdatableMixin } from '../utils/SyncUpdatableMixin.js';
10
10
  import { LionValidationFeedback } from './LionValidationFeedback.js';
11
- import { ResultValidator } from './ResultValidator.js';
11
+ import { ResultValidator as MetaValidator } from './ResultValidator.js';
12
12
  import { Unparseable } from './Unparseable.js';
13
13
  import { Validator } from './Validator.js';
14
14
  import { Required } from './validators/Required.js';
@@ -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/index.js').ValidatorOutcome} ValidatorOutcome
23
24
  * @typedef {typeof import('../../types/validate/ValidateMixinTypes.js').ValidateHost} ValidateHostConstructor
24
25
  * @typedef {{validator:Validator; outcome:boolean|string}} ValidationResultEntry
25
26
  * @typedef {{[type:string]: {[validatorName:string]:boolean|string}}} ValidationStates
@@ -33,6 +34,15 @@ function arrayDiff(array1 = [], array2 = []) {
33
34
  return array1.filter(x => !array2.includes(x)).concat(array2.filter(x => !array1.includes(x)));
34
35
  }
35
36
 
37
+ /**
38
+ * When the modelValue can't be created by FormatMixin.parser, still allow all validators
39
+ * to give valuable feedback to the user based on the current viewValue.
40
+ * @param {any} modelValue
41
+ */
42
+ function getValueForValidators(modelValue) {
43
+ return modelValue instanceof Unparseable ? modelValue.viewValue : modelValue;
44
+ }
45
+
36
46
  /**
37
47
  * Handles all validation, based on modelValue changes. It has no knowledge about dom and
38
48
  * UI. All error visibility, dom interaction and accessibility are handled in FeedbackMixin.
@@ -46,17 +56,13 @@ export const ValidateMixinImplementation = superclass =>
46
56
  SyncUpdatableMixin(DisabledMixin(SlotMixin(ScopedElementsMixin(superclass)))),
47
57
  ) {
48
58
  static get scopedElements() {
49
- const scopedElementsCtor =
50
- /** @type {typeof import('@open-wc/scoped-elements/types.js').ScopedElementsHost} */ (
51
- super.constructor
52
- );
53
59
  return {
54
- ...scopedElementsCtor.scopedElements,
60
+ // @ts-ignore
61
+ ...super.scopedElements,
55
62
  'lion-validation-feedback': LionValidationFeedback,
56
63
  };
57
64
  }
58
65
 
59
- /** @type {any} */
60
66
  static get properties() {
61
67
  return {
62
68
  validators: { attribute: false },
@@ -93,8 +99,8 @@ export const ValidateMixinImplementation = superclass =>
93
99
  }
94
100
 
95
101
  /**
96
- * @overridable
97
102
  * Adds "._feedbackNode" as described below
103
+ * @public
98
104
  */
99
105
  get slots() {
100
106
  /**
@@ -229,7 +235,7 @@ export const ValidateMixinImplementation = superclass =>
229
235
  this.__asyncValidationResult = [];
230
236
 
231
237
  /**
232
- * Aggregated result from sync Validators, async Validators and ResultValidators
238
+ * Aggregated result from sync Validators, async Validators and MetaValidators
233
239
  * @type {ValidationResultEntry[]}
234
240
  * @private
235
241
  */
@@ -242,6 +248,7 @@ export const ValidateMixinImplementation = superclass =>
242
248
  this.__prevValidationResult = [];
243
249
 
244
250
  /**
251
+ * The shown validation result depends on the visibility of the feedback messages
245
252
  * @type {ValidationResultEntry[]}
246
253
  * @private
247
254
  */
@@ -279,7 +286,7 @@ export const ValidateMixinImplementation = superclass =>
279
286
  */
280
287
  firstUpdated(changedProperties) {
281
288
  super.firstUpdated(changedProperties);
282
- this.__validateInitialized = true;
289
+ this.__isValidateInitialized = true;
283
290
  this.validate();
284
291
  if (this._repropagationRole !== 'child') {
285
292
  this.addEventListener('model-value-changed', () => {
@@ -360,8 +367,8 @@ export const ValidateMixinImplementation = superclass =>
360
367
  * Executions are scheduled and awaited and the 'async results' are merged with the
361
368
  * 'sync results'.
362
369
  *
363
- * - b) there are ResultValidators. After steps a1, a2, or a3 are finished, the holistic
364
- * ResultValidators (evaluating the total result of the 'regular' (a1, a2 and a3) validators)
370
+ * - b) there are MetaValidators. After steps a1, a2, or a3 are finished, the holistic
371
+ * MetaValidators (evaluating the total result of the 'regular' (a1, a2 and a3) validators)
365
372
  * will be run...
366
373
  *
367
374
  * Situations a2 and a3 are not mutually exclusive and can be triggered within one `validate()`
@@ -369,14 +376,27 @@ export const ValidateMixinImplementation = superclass =>
369
376
  *
370
377
  * @param {{ clearCurrentResult?: boolean }} opts
371
378
  */
372
- async validate({ clearCurrentResult } = {}) {
379
+ async validate({ clearCurrentResult = false } = {}) {
380
+ /**
381
+ * Allows Application Developer to wait for (async) validation
382
+ * @example
383
+ * ```js
384
+ * await el.validateComplete;
385
+ * ```
386
+ * @type {Promise<boolean>}
387
+ */
388
+ this.validateComplete = new Promise(resolve => {
389
+ this.__validateCompleteResolve = resolve;
390
+ });
391
+
373
392
  if (this.disabled) {
374
393
  this.__clearValidationResults();
375
- this.__finishValidation({ source: 'sync', hasAsync: true });
394
+ this.__finishValidationPass();
376
395
  this._updateFeedbackComponent();
377
396
  return;
378
397
  }
379
- if (!this.__validateInitialized) {
398
+ // We don't validate before firstUpdated has run
399
+ if (!this.__isValidateInitialized) {
380
400
  return;
381
401
  }
382
402
 
@@ -394,131 +414,151 @@ export const ValidateMixinImplementation = superclass =>
394
414
  * @desc step a1-3 + b (as explained in `validate()`)
395
415
  */
396
416
  async __executeValidators() {
397
- /**
398
- * Allows Application Developer to wait for (async) validation
399
- * @example
400
- * ```js
401
- * await el.validateComplete;
402
- * ```
403
- * @type {Promise<boolean>}
404
- */
405
- this.validateComplete = new Promise(resolve => {
406
- this.__validateCompleteResolve = resolve;
407
- });
408
-
409
- // When the modelValue can't be created by FormatMixin.parser, still allow all validators
410
- // to give valuable feedback to the user based on the current viewValue.
411
- const value =
412
- this.modelValue instanceof Unparseable ? this.modelValue.viewValue : this.modelValue;
413
-
414
- /** @type {Validator | undefined} */
415
- const requiredValidator = this._allValidators.find(v => v instanceof Required);
417
+ const value = getValueForValidators(this.modelValue);
416
418
 
417
419
  /**
418
- * 1. Handle the 'exceptional' Required validator:
419
- * - the validatity is dependent on the formControl type and therefore determined
420
- * by the formControl.__isEmpty method. Basically, the Required Validator is a means
421
- * to trigger formControl.__isEmpty.
422
- * - when __isEmpty returns true, the input was empty. This means we need to stop
420
+ * Handle the 'mutually exclusive' Required validator:
421
+ *
422
+ * About the Required Validator:
423
+ * - the validity is dependent on the formControls' modelValue and therefore determined
424
+ * by the formControl._isEmpty method. Basically, the Required Validator is a means
425
+ * to trigger formControl._isEmpty().
426
+ *
427
+ * About the empty state:
428
+ * - when _isEmpty returns true, the modelValue is considered empty. This means we need to stop
423
429
  * validation here, because all other Validators' execute functions assume the
424
430
  * value is not empty (there would be nothing to validate).
431
+ *
425
432
  */
426
- // TODO: Try to remove this when we have a single lion form core package, because then we can
427
- // depend on FormControlMixin directly, and _isEmpty will always be an existing method on the prototype then
433
+
428
434
  const isEmpty = this.__isEmpty(value);
435
+ this.__syncValidationResult = [];
436
+
437
+ /**
438
+ * So we can have the following scenarios:
439
+ * - we're empty
440
+ * - we have a single value (we are an 'endpoint') => we want to halt execution, because validation only makes sense when we have a value
441
+ * - 1. we have Required => we fill .__syncValidationResult and finish validation, because we have a value to validate
442
+ * - 2. we don't have Required => we stop validation
443
+ * - we have a group of values (object/array) => we want to continue execution, because the constellation of the object (even though the individual endpoints are empty) might be interesting for validation.
444
+ * - 3. we have Required => we fill .__syncValidationResult and continue validation (so we can add more to .__syncValidationResult)
445
+ * - 4. we don't have Required => we continue validation
446
+ * - we're not empty
447
+ * - we have a single value or group of values
448
+ * - 5. we may have Required, but if we have it will not be 'active' => we continue execution, because we have a value to validate
449
+ */
450
+
429
451
  if (isEmpty) {
452
+ const hasSingleValue = !(/** @type {* & ValidateHost} */ (this)._isFormOrFieldset);
453
+ const requiredValidator = this._allValidators.find(v => v instanceof Required);
430
454
  if (requiredValidator) {
431
455
  this.__syncValidationResult = [{ validator: requiredValidator, outcome: true }];
432
456
  }
433
- this.__finishValidation({ source: 'sync' });
434
- return;
457
+
458
+ if (hasSingleValue) {
459
+ this.__finishValidationPass({
460
+ syncValidationResult: this.__syncValidationResult,
461
+ });
462
+ return;
463
+ }
435
464
  }
436
465
 
437
- // Separate Validators in sync and async
438
- const /** @type {Validator[]} */ filteredValidators = this._allValidators.filter(
439
- v => !(v instanceof ResultValidator) && !(v instanceof Required),
440
- );
441
- const /** @type {Validator[]} */ syncValidators = filteredValidators.filter(v => {
442
- const vCtor = /** @type {typeof Validator} */ (v.constructor);
443
- return !vCtor.async;
444
- });
445
- const /** @type {Validator[]} */ asyncValidators = filteredValidators.filter(v => {
446
- const vCtor = /** @type {typeof Validator} */ (v.constructor);
447
- return vCtor.async;
448
- });
466
+ const metaValidators = /** @type {MetaValidator[]} */ [];
467
+ const syncValidators = /** @type {Validator[]} */ [];
468
+ const asyncValidators = /** @type {Validator[]} */ [];
469
+
470
+ for (const v of this._allValidators) {
471
+ if (v instanceof MetaValidator) {
472
+ metaValidators.push(v);
473
+ } else if (v instanceof Required) {
474
+ // Required validator was already handled
475
+ } else if (/** @type {typeof Validator} */ (v.constructor).async) {
476
+ asyncValidators.push(v);
477
+ } else {
478
+ syncValidators.push(v);
479
+ }
480
+ }
481
+
482
+ const hasAsync = Boolean(asyncValidators.length);
449
483
 
450
484
  /**
451
485
  * 2. Synchronous validators
452
486
  */
453
- this.__executeSyncValidators(syncValidators, value, {
454
- hasAsync: Boolean(asyncValidators.length),
487
+
488
+ this.__syncValidationResult = [
489
+ // This could eventually contain the Required Validator
490
+ ...this.__syncValidationResult,
491
+ ...this.__executeSyncValidators(syncValidators, value),
492
+ ];
493
+ // Finish the first (synchronous) pass
494
+ this.__finishValidationPass({
495
+ syncValidationResult: this.__syncValidationResult,
496
+ metaValidators,
455
497
  });
456
498
 
457
499
  /**
458
500
  * 3. Asynchronous validators
459
501
  */
460
- await this.__executeAsyncValidators(asyncValidators, value);
502
+ if (hasAsync) {
503
+ // Add a hint in the ui that we're waiting for async validation
504
+ this.isPending = true;
505
+ this.__asyncValidationResult = await this.__executeAsyncValidators(asyncValidators, value);
506
+ this.isPending = false;
507
+ // Now finish the second (asynchronous) pass (including both sync and async results)
508
+ this.__finishValidationPass({
509
+ syncValidationResult: this.__syncValidationResult,
510
+ asyncValidationResult: this.__asyncValidationResult,
511
+ metaValidators,
512
+ });
513
+ this.__validateCompleteResolve?.(true);
514
+ } else {
515
+ this.__validateCompleteResolve?.(true);
516
+ }
461
517
  }
462
518
 
463
519
  /**
464
- * step a2 (as explained in `validate()`): calls `__finishValidation`
520
+ * step a2 (as explained in `validate()`): calls `__finishValidationPass`
465
521
  * @param {Validator[]} syncValidators
466
522
  * @param {unknown} value
467
- * @param {{ hasAsync: boolean }} opts
468
523
  * @private
469
524
  */
470
- __executeSyncValidators(syncValidators, value, { hasAsync }) {
471
- if (syncValidators.length) {
472
- this.__syncValidationResult = syncValidators
473
- .map(v => ({
474
- validator: v,
475
- // TODO: fix this type - ts things this is not a FormControlHost?
476
- // @ts-ignore
477
- outcome: /** @type {boolean|string} */ (v.execute(value, v.param, { node: this })),
478
- }))
479
- .filter(v => Boolean(v.outcome));
480
- }
481
- this.__finishValidation({ source: 'sync', hasAsync });
525
+ __executeSyncValidators(syncValidators, value) {
526
+ return syncValidators
527
+ .map(v => ({
528
+ validator: v,
529
+ // TODO: fix this type - ts things this is not a FormControlHost?
530
+ // @ts-ignore
531
+ outcome: /** @type {boolean|string} */ (v.execute(value, v.param, { node: this })),
532
+ }))
533
+ .filter(v => Boolean(v.outcome));
482
534
  }
483
535
 
484
536
  /**
485
- * step a3 (as explained in `validate()`), calls __finishValidation
486
- * @param {Validator[]} asyncValidators all Validators except required and ResultValidators
537
+ * step a3 (as explained in `validate()`), calls __finishValidationPass
538
+ * @param {Validator[]} asyncValidators all Validators except required and MetaValidators
487
539
  * @param {?} value
488
540
  * @private
489
541
  */
490
542
  async __executeAsyncValidators(asyncValidators, value) {
491
- if (asyncValidators.length) {
492
- this.isPending = true;
493
- const resultPromises = asyncValidators.map(v => v.execute(value, v.param, { node: this }));
494
- const asyncExecutionResults = await Promise.all(resultPromises);
543
+ const outcomePromises = asyncValidators.map(v => v.execute(value, v.param, { node: this }));
544
+ const asyncExecutionResults = await Promise.all(outcomePromises);
495
545
 
496
- this.__asyncValidationResult = asyncExecutionResults
497
- .map((r, i) => ({
498
- validator: asyncValidators[i],
499
- outcome: /** @type {boolean|string} */ (asyncExecutionResults[i]),
500
- }))
501
- .filter(v => Boolean(v.outcome));
502
-
503
- this.__finishValidation({ source: 'async' });
504
- this.isPending = false;
505
- }
546
+ return asyncExecutionResults
547
+ .map((r, i) => ({
548
+ validator: asyncValidators[i],
549
+ outcome: /** @type {boolean|string} */ (asyncExecutionResults[i]),
550
+ }))
551
+ .filter(v => Boolean(v.outcome));
506
552
  }
507
553
 
508
554
  /**
509
- * step b (as explained in `validate()`), called by __finishValidation
555
+ * step b (as explained in `validate()`), called by __finishValidationPass
510
556
  * @param {{validator: Validator;outcome: boolean | string;}[]} regularValidationResult result of steps 1-3
557
+ * @param {MetaValidator[]} metaValidators
511
558
  * @private
512
559
  */
513
- __executeResultValidators(regularValidationResult) {
514
- const resultValidators = /** @type {ResultValidator[]} */ (
515
- this._allValidators.filter(v => {
516
- const vCtor = /** @type {typeof Validator} */ (v.constructor);
517
- return !vCtor.async && v instanceof ResultValidator;
518
- })
519
- );
520
-
521
- if (!resultValidators.length) {
560
+ __executeMetaValidators(regularValidationResult, metaValidators) {
561
+ if (!metaValidators.length) {
522
562
  return [];
523
563
  }
524
564
 
@@ -529,7 +569,7 @@ export const ValidateMixinImplementation = superclass =>
529
569
  }
530
570
 
531
571
  // Map everything to Validator[] for backwards compatibility
532
- return resultValidators
572
+ return metaValidators
533
573
  .map(v => ({
534
574
  validator: v,
535
575
  outcome: /** @type {boolean|string} */ (
@@ -546,19 +586,35 @@ export const ValidateMixinImplementation = superclass =>
546
586
  }
547
587
 
548
588
  /**
589
+ * A 'pass' is a single run of the validation process, which will be triggered in these cases:
590
+ * - on clear or disable
591
+ * - on sync validation
592
+ * - on async validation (can depend on server response)
593
+ *
594
+ * This method inishes a pass by adding the properties to the instance:
595
+ * - validationStates
596
+ * - hasFeedbackFor
597
+ *
598
+ * It sends a private event validate-performed, which is received by parent Formgroups.
599
+ *
549
600
  * @param {object} options
550
- * @param {'sync'|'async'} options.source
551
- * @param {boolean} [options.hasAsync] whether async validators are configured in this run.
601
+ * @param {ValidationResultEntry[]} [options.syncValidationResult]
602
+ * @param {ValidationResultEntry[]} [options.asyncValidationResult]
603
+ * @param {MetaValidator[]} [options.metaValidators] MetaValidators to be executed
552
604
  * @private
553
605
  * If not, we have nothing left to wait for.
554
606
  */
555
- __finishValidation({ source, hasAsync }) {
556
- const syncAndAsyncOutcome = [...this.__syncValidationResult, ...this.__asyncValidationResult];
557
- // if we have any ResultValidators left, now is the time to run them...
558
- const resultOutCome = /** @type {ValidationResultEntry[]} */ (
559
- this.__executeResultValidators(syncAndAsyncOutcome)
607
+ __finishValidationPass({
608
+ syncValidationResult = [],
609
+ asyncValidationResult = [],
610
+ metaValidators = [],
611
+ } = {}) {
612
+ const syncAndAsyncOutcome = [...syncValidationResult, ...asyncValidationResult];
613
+ // if we have any MetaValidators left, now is the time to run them...
614
+ const metaOutCome = /** @type {ValidationResultEntry[]} */ (
615
+ this.__executeMetaValidators(syncAndAsyncOutcome, metaValidators)
560
616
  );
561
- this.__validationResult = [...resultOutCome, ...syncAndAsyncOutcome];
617
+ this.__validationResult = [...metaOutCome, ...syncAndAsyncOutcome];
562
618
 
563
619
  const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
564
620
 
@@ -567,13 +623,13 @@ export const ValidateMixinImplementation = superclass =>
567
623
  (acc, type) => ({ ...acc, [type]: {} }),
568
624
  {},
569
625
  );
570
- this.__validationResult.forEach(({ validator, outcome }) => {
626
+ for (const { validator, outcome } of this.__validationResult) {
571
627
  if (!validationStates[validator.type]) {
572
628
  validationStates[validator.type] = {};
573
629
  }
574
630
  const vCtor = /** @type {typeof Validator} */ (validator.constructor);
575
631
  validationStates[validator.type][vCtor.validatorName] = outcome;
576
- });
632
+ }
577
633
  this.validationStates = validationStates;
578
634
 
579
635
  this.hasFeedbackFor = [
@@ -581,11 +637,6 @@ export const ValidateMixinImplementation = superclass =>
581
637
  ];
582
638
  /** private event that should be listened to by LionFieldSet */
583
639
  this.dispatchEvent(new Event('validate-performed', { bubbles: true }));
584
- if (source === 'async' || !hasAsync) {
585
- if (this.__validateCompleteResolve) {
586
- this.__validateCompleteResolve(true);
587
- }
588
- }
589
640
  }
590
641
 
591
642
  /**
@@ -611,51 +662,49 @@ export const ValidateMixinImplementation = superclass =>
611
662
  */
612
663
  __setupValidators() {
613
664
  const events = ['param-changed', 'config-changed'];
614
- if (this.__prevValidators) {
615
- this.__prevValidators.forEach(v => {
616
- events.forEach(e => {
617
- if (v.removeEventListener) {
618
- v.removeEventListener(e, this._onValidatorUpdated);
619
- }
620
- });
621
- v.onFormControlDisconnect(
622
- /** @type {import('../../types/FormControlMixinTypes.js').FormControlHost} */ (
623
- /** @type {unknown} */ (this)
624
- ),
625
- );
626
- });
665
+
666
+ for (const validatorToCleanup of this.__prevValidators || []) {
667
+ for (const eventToCleanup of events) {
668
+ validatorToCleanup.removeEventListener?.(eventToCleanup, this._onValidatorUpdated);
669
+ }
670
+ validatorToCleanup.onFormControlDisconnect(
671
+ /** @type {import('../../types/FormControlMixinTypes.js').FormControlHost} */ (
672
+ /** @type {unknown} */ (this)
673
+ ),
674
+ );
627
675
  }
628
- this._allValidators.forEach(v => {
629
- if (!(v instanceof Validator)) {
676
+
677
+ for (const validatorToSetup of this._allValidators) {
678
+ if (!(validatorToSetup instanceof Validator)) {
630
679
  // throws in constructor are not visible to end user so we do both
631
- const errorType = Array.isArray(v) ? 'array' : typeof v;
632
- const errorMessage = `Validators array only accepts class instances of Validator. Type "${errorType}" found. This may be caused by having multiple installations of @lion/form-core.`;
680
+ const errorType = Array.isArray(validatorToSetup) ? 'array' : typeof validatorToSetup;
681
+ const errorMessage = `Validators array only accepts class instances of Validator. Type "${errorType}" found. This may be caused by having multiple installations of "@lion/ui/form-core.js".`;
633
682
  // eslint-disable-next-line no-console
634
683
  console.error(errorMessage, this);
635
684
  throw new Error(errorMessage);
636
685
  }
637
686
  const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
638
- if (ctor.validationTypes.indexOf(v.type) === -1) {
639
- const vCtor = /** @type {typeof Validator} */ (v.constructor);
687
+ const vCtor = /** @type {typeof Validator} */ (validatorToSetup.constructor);
688
+ if (ctor.validationTypes.indexOf(validatorToSetup.type) === -1) {
640
689
  // throws in constructor are not visible to end user so we do both
641
- const errorMessage = `This component does not support the validator type "${v.type}" used in "${vCtor.validatorName}". You may change your validators type or add it to the components "static get validationTypes() {}".`;
690
+ const errorMessage = `This component does not support the validator type "${validatorToSetup.type}" used in "${vCtor.validatorName}". You may change your validators type or add it to the components "static get validationTypes() {}".`;
642
691
  // eslint-disable-next-line no-console
643
692
  console.error(errorMessage, this);
644
693
  throw new Error(errorMessage);
645
694
  }
646
- /** Updated the code to fix issue #1607 to sync the calendar date with validators params
647
- * Here _onValidatorUpdated is responsible for responding to the event
695
+
696
+ /**
697
+ * Updated the code to fix issue #1607 to sync the calendar date with validators params
698
+ * Here _onValidatorUpdated is responsible for responding to the event
648
699
  */
649
- events.forEach(eventName => {
650
- if (v.addEventListener) {
651
- v.addEventListener(eventName, e => {
652
- // @ts-ignore for making validator param dynamic
653
- this._onValidatorUpdated(e, { validator: v });
654
- });
655
- }
656
- });
657
- v.onFormControlConnect(this);
658
- });
700
+ for (const eventToSetup of events) {
701
+ validatorToSetup.addEventListener?.(eventToSetup, e => {
702
+ // @ts-ignore for making validator param dynamic
703
+ this._onValidatorUpdated(e, { validator: validatorToSetup });
704
+ });
705
+ }
706
+ validatorToSetup.onFormControlConnect(this);
707
+ }
659
708
  this.__prevValidators = this._allValidators;
660
709
  }
661
710
 
@@ -759,7 +808,7 @@ export const ValidateMixinImplementation = superclass =>
759
808
  }
760
809
 
761
810
  const messageMap = await this.__getFeedbackMessages(this.__prioritizedResult);
762
- _feedbackNode.feedbackData = messageMap.length ? messageMap : [];
811
+ _feedbackNode.feedbackData = messageMap || [];
763
812
  });
764
813
  } else {
765
814
  this.__feedbackQueue.add(async () => {
@@ -827,12 +876,7 @@ export const ValidateMixinImplementation = superclass =>
827
876
  * @protected
828
877
  */
829
878
  _hasFeedbackVisibleFor(type) {
830
- return (
831
- this.hasFeedbackFor &&
832
- this.hasFeedbackFor.includes(type) &&
833
- this.shouldShowFeedbackFor &&
834
- this.shouldShowFeedbackFor.includes(type)
835
- );
879
+ return this.hasFeedbackFor?.includes(type) && this.shouldShowFeedbackFor?.includes(type);
836
880
  }
837
881
 
838
882
  /**
@@ -77,7 +77,7 @@ export function runValidateMixinSuite(customConfig) {
77
77
  const stub = sinon.stub(console, 'error');
78
78
  const el = /** @type {ValidateElement} */ (await fixture(html`<${tag}></${tag}>`));
79
79
  const errorMessage =
80
- 'Validators array only accepts class instances of Validator. Type "array" found. This may be caused by having multiple installations of @lion/form-core.';
80
+ 'Validators array only accepts class instances of Validator. Type "array" found. This may be caused by having multiple installations of "@lion/ui/form-core.js".';
81
81
  expect(() => {
82
82
  // @ts-expect-error putting the wrong value on purpose
83
83
  el.validators = [[new Required()]];
@@ -85,7 +85,7 @@ export function runValidateMixinSuite(customConfig) {
85
85
  expect(stub.args[0][0]).to.equal(errorMessage);
86
86
 
87
87
  const errorMessage2 =
88
- 'Validators array only accepts class instances of Validator. Type "string" found. This may be caused by having multiple installations of @lion/form-core.';
88
+ 'Validators array only accepts class instances of Validator. Type "string" found. This may be caused by having multiple installations of "@lion/ui/form-core.js".';
89
89
  expect(() => {
90
90
  // @ts-expect-error because we purposely put a wrong type
91
91
  el.validators = ['required'];
@@ -322,7 +322,7 @@ export function runValidateMixinSuite(customConfig) {
322
322
  // @ts-ignore [allow-private] in test
323
323
  const syncSpy = sinon.spy(el, '__executeSyncValidators');
324
324
  // @ts-ignore [allow-private] in test
325
- const resultSpy2 = sinon.spy(el, '__executeResultValidators');
325
+ const resultSpy2 = sinon.spy(el, '__executeMetaValidators');
326
326
 
327
327
  el.modelValue = 'nonEmpty';
328
328
  expect(syncSpy.calledBefore(resultSpy2)).to.be.true;
@@ -337,7 +337,7 @@ export function runValidateMixinSuite(customConfig) {
337
337
  // @ts-ignore [allow-private] in test
338
338
  const asyncSpy = sinon.spy(el, '__executeAsyncValidators');
339
339
  // @ts-ignore [allow-private] in test
340
- const resultSpy = sinon.spy(el, '__executeResultValidators');
340
+ const resultSpy = sinon.spy(el, '__executeMetaValidators');
341
341
 
342
342
  el.modelValue = 'nonEmpty';
343
343
  expect(resultSpy.callCount).to.equal(1);
@@ -313,7 +313,7 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
313
313
  });
314
314
 
315
315
  // TODO: wait for updated on disconnected to be fixed: https://github.com/lit/lit/issues/1901
316
- it.skip(`cleans up feedback message belonging to fieldset on disconnect`, async () => {
316
+ it.skip(`cleans up feedback message belonging to fieldset on disconnect`, async () => {
317
317
  const el = await childAriaFixture('feedback');
318
318
  await childAriaTest(el, { cleanupPhase: true });
319
319
  });
@@ -1,5 +1,5 @@
1
1
  import { LitElement } from 'lit';
2
- import { IsNumber, LionField, Validator, FormGroupMixin } from '@lion/ui/form-core.js';
2
+ import { IsNumber, Required, LionField, Validator, FormGroupMixin } from '@lion/ui/form-core.js';
3
3
  import '@lion/ui/define/lion-field.js';
4
4
  import '@lion/ui/define/lion-validation-feedback.js';
5
5
 
@@ -577,6 +577,25 @@ export function runFormGroupMixinSuite(cfg = {}) {
577
577
  expect(el.validationStates.error).to.deep.equal({});
578
578
  });
579
579
 
580
+ it('works with Required', async () => {
581
+ const el = /** @type {FormGroup} */ (
582
+ await fixture(html`
583
+ <${tag} name="myGroup" .validators="${[new Required()]}">
584
+ <${childTag} name="fieldA"></${childTag}>
585
+ <${childTag} name="fieldB"></${childTag}>
586
+ </${tag}>
587
+ `)
588
+ );
589
+ // initially the group is invalid
590
+ expect(el.validationStates.error.Required).to.be.true;
591
+ el.formElements.fieldA.modelValue = 'foo';
592
+ // if at least one child is filled, the group is valid
593
+ expect(el.validationStates.error.Required).to.be.undefined;
594
+ // make Required trigger error state again
595
+ el.formElements.fieldA.modelValue = '';
596
+ expect(el.validationStates.error.Required).to.be.true;
597
+ });
598
+
580
599
  it('validates on children (de)registration', async () => {
581
600
  class HasEvenNumberOfChildren extends Validator {
582
601
  static get validatorName() {
@@ -1129,9 +1148,7 @@ export function runFormGroupMixinSuite(cfg = {}) {
1129
1148
 
1130
1149
  it('has correct validation afterwards', async () => {
1131
1150
  class IsCat extends Validator {
1132
- static get validatorName() {
1133
- return 'IsCat';
1134
- }
1151
+ static validatorName = 'IsCat';
1135
1152
 
1136
1153
  /**
1137
1154
  * @param {string} value
@@ -1142,9 +1159,7 @@ export function runFormGroupMixinSuite(cfg = {}) {
1142
1159
  }
1143
1160
  }
1144
1161
  class ColorContainsA extends Validator {
1145
- static get validatorName() {
1146
- return 'ColorContainsA';
1147
- }
1162
+ static validatorName = 'ColorContainsA';
1148
1163
 
1149
1164
  /**
1150
1165
  * @param {{ [x:string]:any }} value