@aurodesignsystem-dev/auro-formkit 0.0.0-pr1506.1 → 0.0.0-pr1506.3
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/checkbox/demo/customize.html +1 -2
- package/components/checkbox/demo/customize.min.js +1 -1
- package/components/checkbox/demo/getting-started.min.js +1 -1
- package/components/checkbox/demo/index.min.js +1 -1
- package/components/checkbox/dist/index.js +1 -1
- package/components/checkbox/dist/registered.js +1 -1
- package/components/combobox/demo/customize.html +1 -2
- package/components/combobox/demo/customize.md +108 -132
- package/components/combobox/demo/customize.min.js +10 -5
- package/components/combobox/demo/getting-started.min.js +10 -5
- package/components/combobox/demo/index.min.js +10 -5
- package/components/combobox/dist/index.js +8 -3
- package/components/combobox/dist/registered.js +8 -3
- package/components/counter/demo/customize.min.js +2 -2
- package/components/counter/demo/index.min.js +2 -2
- package/components/counter/dist/index.js +2 -2
- package/components/counter/dist/registered.js +2 -2
- package/components/datepicker/demo/customize.min.js +8 -3
- package/components/datepicker/demo/index.min.js +8 -3
- package/components/datepicker/dist/index.js +8 -3
- package/components/datepicker/dist/registered.js +8 -3
- package/components/dropdown/demo/customize.min.js +1 -1
- package/components/dropdown/demo/getting-started.min.js +1 -1
- package/components/dropdown/demo/index.min.js +1 -1
- package/components/dropdown/dist/index.js +1 -1
- package/components/dropdown/dist/registered.js +1 -1
- package/components/form/demo/customize.html +6 -6
- package/components/form/demo/customize.js +19 -0
- package/components/form/demo/customize.md +203 -51
- package/components/form/demo/customize.min.js +465 -52
- package/components/form/demo/getting-started.min.js +393 -52
- package/components/form/demo/index.min.js +393 -52
- package/components/form/demo/registerDemoDeps.min.js +30 -15
- package/components/form/dist/auro-form.d.ts +122 -4
- package/components/form/dist/index.js +363 -37
- package/components/form/dist/registered.js +363 -37
- package/components/input/demo/customize.html +1 -2
- package/components/input/demo/customize.min.js +6 -1
- package/components/input/demo/getting-started.min.js +6 -1
- package/components/input/demo/index.min.js +6 -1
- package/components/input/dist/base-input.d.ts +1 -1
- package/components/input/dist/index.js +6 -1
- package/components/input/dist/registered.js +6 -1
- package/components/menu/demo/index.min.js +2 -2
- package/components/menu/dist/index.js +2 -2
- package/components/menu/dist/registered.js +2 -2
- package/components/radio/demo/customize.min.js +1 -1
- package/components/radio/demo/getting-started.min.js +1 -1
- package/components/radio/demo/index.min.js +1 -1
- package/components/radio/dist/index.js +1 -1
- package/components/radio/dist/registered.js +1 -1
- package/components/select/demo/customize.html +1 -2
- package/components/select/demo/customize.min.js +4 -4
- package/components/select/demo/getting-started.min.js +4 -4
- package/components/select/demo/index.min.js +4 -4
- package/components/select/dist/index.js +2 -2
- package/components/select/dist/registered.js +2 -2
- package/custom-elements.json +123 -2
- package/package.json +1 -1
|
@@ -85,7 +85,7 @@ class AuroLibraryRuntimeUtils {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
/* eslint-disable no-underscore-dangle, max-lines, object-property-newline */
|
|
88
|
+
/* eslint-disable no-underscore-dangle, max-lines, object-property-newline, dot-location */
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
/**
|
|
@@ -93,7 +93,7 @@ class AuroLibraryRuntimeUtils {
|
|
|
93
93
|
* @property {string | number | boolean | string[] | null} value - The value of the form element.
|
|
94
94
|
* @property {ValidityState} validity - The validity state of the form element, stored when fired from the form element.
|
|
95
95
|
* @property {boolean} required - Whether the form element is required or not.
|
|
96
|
-
* @property {
|
|
96
|
+
* @property {boolean} disabled - Whether the form element is currently disabled. Cached from the live attribute via the MutationObserver in `connectedCallback` and refreshed from `_handleAttributeMutations`.
|
|
97
97
|
*/
|
|
98
98
|
|
|
99
99
|
/**
|
|
@@ -177,6 +177,31 @@ class AuroForm extends LitElement {
|
|
|
177
177
|
*/
|
|
178
178
|
this.mutationObservers = [];
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Captured initial (default) value per field `name`. Populated on first
|
|
182
|
+
* sight of each name in `_addElementToState` and preserved across
|
|
183
|
+
* subsequent `initializeState` cycles (slot change, rename, reset) so
|
|
184
|
+
* `_setInitialState` can detect user edits as `current !== initial`,
|
|
185
|
+
* matching HTML's `dirtyValueFlag` semantics.
|
|
186
|
+
* @private
|
|
187
|
+
* @type {Record<string, string | number | boolean | string[] | null | undefined>}
|
|
188
|
+
*/
|
|
189
|
+
this._initialValues = {};
|
|
190
|
+
|
|
191
|
+
// Single subtree observer that watches `disabled` and `name` attribute
|
|
192
|
+
// changes across all tracked form elements. The `name` watch is required:
|
|
193
|
+
// without it, renaming a tracked field at runtime leaves a stale key in
|
|
194
|
+
// `formState` that `_isNameDisabled` cannot resolve, so a renamed-but-
|
|
195
|
+
// disabled field would re-appear in `.value`.
|
|
196
|
+
/**
|
|
197
|
+
* @private
|
|
198
|
+
* @type {MutationObserver | null}
|
|
199
|
+
*/
|
|
200
|
+
this._attributeObserver = null;
|
|
201
|
+
|
|
202
|
+
/** @private */
|
|
203
|
+
this._handleAttributeMutations = this._handleAttributeMutations.bind(this);
|
|
204
|
+
|
|
180
205
|
// Bind listeners
|
|
181
206
|
/** @private */
|
|
182
207
|
this.reset = this.reset.bind(this);
|
|
@@ -245,6 +270,45 @@ class AuroForm extends LitElement {
|
|
|
245
270
|
return this._isInElementCollection(AuroForm.formElementTags, element);
|
|
246
271
|
}
|
|
247
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Whether a given element is currently disabled. Disabled controls are excluded
|
|
275
|
+
* from submission, validity, and initial-state checks per the HTML spec
|
|
276
|
+
* (section 4.10.19.2 "Enabling and disabling form controls":
|
|
277
|
+
* https://www.w3.org/TR/2011/WD-html5-20110113/association-of-controls-and-forms.html).
|
|
278
|
+
*
|
|
279
|
+
* Implementation note: we deliberately read only the attribute. Every Auro
|
|
280
|
+
* form element in `formElementTags` declares `disabled` with `reflect: true`,
|
|
281
|
+
* so the attribute and property stay in sync. Reading the attribute also
|
|
282
|
+
* lets the MutationObserver in `connectedCallback` (which is filtered to
|
|
283
|
+
* `['disabled', 'name']`) be the single source of truth for re-renders.
|
|
284
|
+
* If a future form-element type ships without attribute reflection, expand
|
|
285
|
+
* this helper to also read `element.disabled`.
|
|
286
|
+
* @param {HTMLElement | undefined | null} element - The element to check.
|
|
287
|
+
* @returns {boolean}
|
|
288
|
+
* @private
|
|
289
|
+
*/
|
|
290
|
+
_isDisabled(element) {
|
|
291
|
+
return Boolean(element?.hasAttribute('disabled'));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Whether the tracked form element registered under `name` is currently disabled.
|
|
296
|
+
* See `_isDisabled` for the HTML-spec rationale behind excluding disabled
|
|
297
|
+
* controls from form state.
|
|
298
|
+
*
|
|
299
|
+
* Reads a cached flag on `formState[name]` populated by `_addElementToState`
|
|
300
|
+
* at registration and refreshed by `_handleAttributeMutations` whenever the
|
|
301
|
+
* element's `disabled` attribute toggles. The cache is fed by the same
|
|
302
|
+
* `hasAttribute('disabled')` read as `_isDisabled`, so the "future form-element
|
|
303
|
+
* type without attribute reflection" caveat documented there applies here too.
|
|
304
|
+
* @param {string} name - The `name` attribute used to register the element.
|
|
305
|
+
* @returns {boolean}
|
|
306
|
+
* @private
|
|
307
|
+
*/
|
|
308
|
+
_isNameDisabled(name) {
|
|
309
|
+
return Boolean(this.formState[name]?.disabled);
|
|
310
|
+
}
|
|
311
|
+
|
|
248
312
|
/**
|
|
249
313
|
* Validates if an event is from a valid form element with a name.
|
|
250
314
|
* @param {Event} event - The event to validate.
|
|
@@ -284,6 +348,10 @@ class AuroForm extends LitElement {
|
|
|
284
348
|
*/
|
|
285
349
|
get value() {
|
|
286
350
|
return Object.keys(this.formState).reduce((acc, key) => {
|
|
351
|
+
if (this._isNameDisabled(key)) {
|
|
352
|
+
return acc;
|
|
353
|
+
}
|
|
354
|
+
|
|
287
355
|
acc[key] = this.formState[key].value;
|
|
288
356
|
return acc;
|
|
289
357
|
}, {});
|
|
@@ -307,23 +375,113 @@ class AuroForm extends LitElement {
|
|
|
307
375
|
return this._resetElements;
|
|
308
376
|
}
|
|
309
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Raw constraint-validation check. Returns `true` when no enabled field
|
|
380
|
+
* has a validity error. Unlike the public `validity` getter, this does
|
|
381
|
+
* NOT gate on `isInitialState` — callers that need to make a decision
|
|
382
|
+
* based on the actual constraint state (submit-button enablement, the
|
|
383
|
+
* internal `submit()` gate) read this so a pre-filled valid form is
|
|
384
|
+
* correctly recognized as submittable at first render.
|
|
385
|
+
* @returns {boolean}
|
|
386
|
+
* @private
|
|
387
|
+
*/
|
|
388
|
+
_isFormValid() {
|
|
389
|
+
return !Object.keys(this.formState).some((key) => {
|
|
390
|
+
if (this._isNameDisabled(key)) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const formKey = this.formState[key];
|
|
395
|
+
// `null` validity means "not yet validated" — auro-input doesn't
|
|
396
|
+
// re-validate on every keystroke, so validity stays `null` between
|
|
397
|
+
// input events until blur. Treating `null` as invalid disables Submit
|
|
398
|
+
// the moment a user types into any field, which is the wrong UX.
|
|
399
|
+
// We treat a field as invalid in two cases:
|
|
400
|
+
// 1. validity is known-bad (non-null, non-'valid')
|
|
401
|
+
// 2. it's `required` and structurally empty — `valueMissing` is
|
|
402
|
+
// certain even without a validation pass.
|
|
403
|
+
// `submit()` calls `validate(true)` on every enabled field before
|
|
404
|
+
// reading `_isFormValid()`, so any not-yet-validated field that turns
|
|
405
|
+
// out to fail another constraint still blocks dispatch.
|
|
406
|
+
const knownInvalid = formKey.validity !== null && formKey.validity !== 'valid';
|
|
407
|
+
const requiredAndEmpty = formKey.required && this._normalizeEmpty(formKey.value) === null;
|
|
408
|
+
return knownInvalid || requiredAndEmpty;
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Whether the reset button should be enabled. True when the form has
|
|
414
|
+
* diverged from its initial state (so the user can always return to
|
|
415
|
+
* defaults — even if the dirty value lives behind a now-disabled field),
|
|
416
|
+
* OR when any non-disabled field has a current value or captured initial
|
|
417
|
+
* value (covers pre-filled forms and user-cleared-back-to-empty cases).
|
|
418
|
+
* @returns {boolean}
|
|
419
|
+
* @private
|
|
420
|
+
*/
|
|
421
|
+
_hasResetableState() {
|
|
422
|
+
// Form is dirty — always allow Reset, even if the dirt is on a field
|
|
423
|
+
// that has since been disabled. Without this branch, the user would
|
|
424
|
+
// have no UI path to return the form to its initial state.
|
|
425
|
+
if (!this._isInitialState) {
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return Object.keys(this.formState).some((key) => {
|
|
430
|
+
if (this._isNameDisabled(key)) {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
const current = this._normalizeEmpty(this.formState[key].value);
|
|
434
|
+
const initial = this._normalizeEmpty(this._initialValues[key]);
|
|
435
|
+
return current !== null || initial !== null;
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Collapse empty representations to a single canonical `null`.
|
|
441
|
+
*
|
|
442
|
+
* `_addElementToState` captures `null` for a field that mounts without a
|
|
443
|
+
* `value` attribute (`element.value || element.getAttribute('value')` is
|
|
444
|
+
* falsy → resolves to `null`), but `sharedInputListener` later stores the
|
|
445
|
+
* raw `event.target.value` — which is `''` for a user-cleared text input.
|
|
446
|
+
* Without this normalization, backspacing back to empty would taint the
|
|
447
|
+
* form forever (`'' !== null`) and Reset would stay enabled with nothing
|
|
448
|
+
* to actually reset.
|
|
449
|
+
*
|
|
450
|
+
* `''`, `undefined`, and `[]` all collapse to `null`. The empty-array case
|
|
451
|
+
* covers checkbox-group, radio-group, and multiselect, where `[]` means
|
|
452
|
+
* "no selection" — semantically the same as `null`/`''`. Genuine values
|
|
453
|
+
* — including `0`, `false`, non-empty strings, and non-empty arrays —
|
|
454
|
+
* pass through unchanged so number, boolean, and populated multi-value
|
|
455
|
+
* fields still compare correctly.
|
|
456
|
+
* @param {*} value - Value to normalize.
|
|
457
|
+
* @returns {*}
|
|
458
|
+
* @private
|
|
459
|
+
*/
|
|
460
|
+
_normalizeEmpty(value) {
|
|
461
|
+
if (value === '' || value === undefined) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
return value;
|
|
468
|
+
}
|
|
469
|
+
|
|
310
470
|
/**
|
|
311
471
|
* Infer validity status based on current formState.
|
|
472
|
+
*
|
|
473
|
+
* Validity stays `null` while the form is in its initial state — this is
|
|
474
|
+
* the "stay quiet until the user interacts" UX contract that consumers
|
|
475
|
+
* depend on to delay error indicators. Code that needs the raw
|
|
476
|
+
* constraint-validation result regardless of interaction (e.g.,
|
|
477
|
+
* submit-button enablement) should call `_isFormValid()` directly.
|
|
312
478
|
* @private
|
|
313
479
|
*/
|
|
314
480
|
_calculateValidity() {
|
|
315
481
|
if (this.isInitialState) {
|
|
316
482
|
this._validity = null;
|
|
317
483
|
} else {
|
|
318
|
-
|
|
319
|
-
const invalidKey = Object.keys(this.formState).
|
|
320
|
-
find((key) => {
|
|
321
|
-
const formKey = this.formState[key];
|
|
322
|
-
// these are NOT extra parens
|
|
323
|
-
// eslint-disable-next-line no-extra-parens
|
|
324
|
-
return (formKey.validity !== 'valid' && formKey.required) || (formKey.validity !== 'valid' && formKey.value !== null);
|
|
325
|
-
});
|
|
326
|
-
this._validity = invalidKey ? 'invalid' : 'valid';
|
|
484
|
+
this._validity = this._isFormValid() ? 'valid' : 'invalid';
|
|
327
485
|
}
|
|
328
486
|
}
|
|
329
487
|
|
|
@@ -339,20 +497,38 @@ class AuroForm extends LitElement {
|
|
|
339
497
|
}
|
|
340
498
|
|
|
341
499
|
/**
|
|
342
|
-
* Determines whether the form is in its initial (untouched) state
|
|
500
|
+
* Determines whether the form is in its initial (untouched) state.
|
|
501
|
+
*
|
|
502
|
+
* A field is tainted if either:
|
|
503
|
+
* - its value differs from the value captured on first render, OR
|
|
504
|
+
* - its validity is failing (anything other than `null` or `'valid'`).
|
|
505
|
+
*
|
|
506
|
+
* Validity acts as a backup signal: it catches users who interact with a
|
|
507
|
+
* field without changing its value (e.g., focusing and blurring a required
|
|
508
|
+
* field). We skip `null` (not yet validated) and `'valid'` (the default
|
|
509
|
+
* after Auro's auto-validation on mount) because neither proves the user
|
|
510
|
+
* touched anything.
|
|
343
511
|
* @returns {void}
|
|
344
512
|
* @private
|
|
345
513
|
*/
|
|
346
514
|
_setInitialState() {
|
|
347
|
-
const anyTainted = Object.keys(this.formState).some((key) =>
|
|
515
|
+
const anyTainted = Object.keys(this.formState).some((key) => {
|
|
516
|
+
// Normalize empty values so a freshly-captured `null` (no `value`
|
|
517
|
+
// attribute at mount) and a user-cleared `''` (input emptied via
|
|
518
|
+
// backspace) compare equal. Without this, backspacing back to an
|
|
519
|
+
// empty field leaves the form permanently tainted.
|
|
520
|
+
const initialValue = this._normalizeEmpty(this._initialValues[key]);
|
|
521
|
+
const currentValue = this._normalizeEmpty(this.formState[key].value);
|
|
522
|
+
const fieldValidity = this.formState[key].validity;
|
|
523
|
+
// eslint-disable-next-line no-extra-parens
|
|
524
|
+
return currentValue !== initialValue || (fieldValidity !== null && fieldValidity !== 'valid');
|
|
525
|
+
});
|
|
348
526
|
|
|
349
527
|
this._isInitialState = !anyTainted;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
});
|
|
528
|
+
// Button state is owned by setDisabledStateOnButtons (called from updated()
|
|
529
|
+
// and reset()). Touching resetElement.disabled here causes a visible flicker
|
|
530
|
+
// in reset(), where the final setDisabledStateOnButtons is deferred behind
|
|
531
|
+
// an extra updateComplete.
|
|
356
532
|
}
|
|
357
533
|
|
|
358
534
|
/**
|
|
@@ -370,19 +546,19 @@ class AuroForm extends LitElement {
|
|
|
370
546
|
*/
|
|
371
547
|
setDisabledStateOnButtons() {
|
|
372
548
|
this._resetElements.forEach((element) => {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
549
|
+
// Reset is meaningful whenever any non-disabled field has a current
|
|
550
|
+
// value OR a captured default value — i.e., whenever the click would
|
|
551
|
+
// either clear something or restore a default.
|
|
552
|
+
element.disabled = !this._hasResetableState();
|
|
378
553
|
});
|
|
379
554
|
|
|
380
555
|
this._submitElements.forEach((element) => {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
556
|
+
// Submit enablement reads raw validity (not the gated public getter)
|
|
557
|
+
// so a pre-filled valid form is submittable at first render — the
|
|
558
|
+
// public `validity` stays `null` during initial state to keep error
|
|
559
|
+
// indicators quiet until the user interacts, but the button decision
|
|
560
|
+
// bypasses that gate.
|
|
561
|
+
element.disabled = !this._isFormValid();
|
|
386
562
|
});
|
|
387
563
|
}
|
|
388
564
|
|
|
@@ -428,9 +604,18 @@ class AuroForm extends LitElement {
|
|
|
428
604
|
value: element.value || element.getAttribute('value'),
|
|
429
605
|
validity: element.validity || null,
|
|
430
606
|
required: element.hasAttribute('required'),
|
|
431
|
-
|
|
607
|
+
disabled: element.hasAttribute('disabled'),
|
|
432
608
|
};
|
|
433
609
|
|
|
610
|
+
// Capture the initial (default) value once per name. Use `in` rather
|
|
611
|
+
// than `??=` so a captured `null` (an empty field at first sight) is
|
|
612
|
+
// preserved across rename/slot/reset cycles — `??=` would treat the
|
|
613
|
+
// stored `null` as nullish and overwrite it with whatever value the
|
|
614
|
+
// field has now, defeating the `current !== initial` taint check.
|
|
615
|
+
if (!(targetName in this._initialValues)) {
|
|
616
|
+
this._initialValues[targetName] = this.formState[targetName].value;
|
|
617
|
+
}
|
|
618
|
+
|
|
434
619
|
this._elements.push(element);
|
|
435
620
|
}
|
|
436
621
|
|
|
@@ -467,6 +652,15 @@ class AuroForm extends LitElement {
|
|
|
467
652
|
}
|
|
468
653
|
});
|
|
469
654
|
|
|
655
|
+
// Drop captured initial values for fields that no longer exist in the form.
|
|
656
|
+
// Rename migration in _handleAttributeMutations has already re-keyed surviving
|
|
657
|
+
// fields, so anything left here is a field that was removed from the DOM.
|
|
658
|
+
for (const key of Object.keys(this._initialValues)) {
|
|
659
|
+
if (!(key in this.formState)) {
|
|
660
|
+
delete this._initialValues[key];
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
470
664
|
this.dispatchEvent(new Event('change', {
|
|
471
665
|
bubbles: true,
|
|
472
666
|
composed: true,
|
|
@@ -511,16 +705,21 @@ class AuroForm extends LitElement {
|
|
|
511
705
|
* @returns {Promise<void>}
|
|
512
706
|
*/
|
|
513
707
|
async submit() {
|
|
514
|
-
// Force validation on
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
708
|
+
// Force validation on all enabled elements. Disabled fields are skipped
|
|
709
|
+
// because disabled controls are not validated nor submitted per the HTML spec.
|
|
710
|
+
this._elements.
|
|
711
|
+
filter((element) => !this._isDisabled(element)).
|
|
712
|
+
forEach((element) => {
|
|
713
|
+
element.validate(true);
|
|
714
|
+
});
|
|
518
715
|
|
|
519
716
|
// Wait for validation to complete and formState to update
|
|
520
717
|
await this.updateComplete;
|
|
521
718
|
|
|
522
|
-
//
|
|
523
|
-
|
|
719
|
+
// Gate on raw constraint-validation rather than the public `validity`
|
|
720
|
+
// getter (which is `null` while in initial state). A pre-filled valid
|
|
721
|
+
// form should be submittable without a prior user edit.
|
|
722
|
+
if (this._isFormValid()) {
|
|
524
723
|
this.dispatchEvent(new CustomEvent('submit', {
|
|
525
724
|
bubbles: true,
|
|
526
725
|
composed: true,
|
|
@@ -590,7 +789,14 @@ class AuroForm extends LitElement {
|
|
|
590
789
|
this._addElementToState(event.target);
|
|
591
790
|
}
|
|
592
791
|
|
|
593
|
-
|
|
792
|
+
// `auroFormElement-validated` can fire with `detail.validity === undefined`
|
|
793
|
+
// when auro-input's updated() lifecycle invokes `validate()` mid-edit but
|
|
794
|
+
// the validation framework's gating conditions (not focused, touched-or-
|
|
795
|
+
// has-value) aren't met — the dispatch still fires, just with the current
|
|
796
|
+
// (untouched) validity. Normalize to `null` so the rest of the form treats
|
|
797
|
+
// "no known status" identically whether it came from `_addElementToState`
|
|
798
|
+
// at mount or from this passthrough mid-typing.
|
|
799
|
+
this.formState[targetName].validity = event.detail.validity ?? null;
|
|
594
800
|
this._calculateValidity();
|
|
595
801
|
this.requestUpdate('formState');
|
|
596
802
|
}
|
|
@@ -602,6 +808,11 @@ class AuroForm extends LitElement {
|
|
|
602
808
|
*/
|
|
603
809
|
handleKeyDown(event) {
|
|
604
810
|
if (event.key === 'Enter' && this.isFormElement(event.target)) {
|
|
811
|
+
// Disabled controls do not submit a form natively.
|
|
812
|
+
if (this._isDisabled(event.target)) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
605
816
|
// Don't submit if it's a textarea (allow new lines)
|
|
606
817
|
if (event.target.tagName.toLowerCase() === 'textarea' ||
|
|
607
818
|
event.target.hasAttribute('textarea')) {
|
|
@@ -633,6 +844,121 @@ class AuroForm extends LitElement {
|
|
|
633
844
|
});
|
|
634
845
|
}
|
|
635
846
|
|
|
847
|
+
/**
|
|
848
|
+
* Handle batched MutationObserver records for `disabled` and `name`
|
|
849
|
+
* attribute changes on tracked form elements. A `name` change invalidates
|
|
850
|
+
* the formState keying — we resolve it by re-initializing state. A `disabled`
|
|
851
|
+
* change simply needs a re-render (so `value` / `validity` getters re-evaluate)
|
|
852
|
+
* and a refresh of the submit/reset button enablement.
|
|
853
|
+
* @param {MutationRecord[]} mutations - The batched mutation records.
|
|
854
|
+
* @returns {void}
|
|
855
|
+
* @private
|
|
856
|
+
*/
|
|
857
|
+
_handleAttributeMutations(mutations) {
|
|
858
|
+
// Only mutations on tracked FORM elements matter here. The same observer
|
|
859
|
+
// also sees attribute changes on the submit/reset buttons (which this
|
|
860
|
+
// component itself toggles via `setDisabledStateOnButtons`); reacting to
|
|
861
|
+
// those would create an infinite observer/update loop.
|
|
862
|
+
const relevant = mutations.filter((mutation) => this.isFormElement(mutation.target));
|
|
863
|
+
if (relevant.length === 0) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const renameMutations = relevant.filter((mutation) => mutation.attributeName === 'name');
|
|
868
|
+
if (renameMutations.length > 0) {
|
|
869
|
+
// Migrate each renamed field's captured initial value from the old key
|
|
870
|
+
// to the new key before `initializeState()` re-runs `_addElementToState`.
|
|
871
|
+
// Without this, the new-name lookup in `_initialValues` would miss, the
|
|
872
|
+
// field's current (possibly user-edited) value would be captured as the
|
|
873
|
+
// new initial, and the form would incorrectly flip back to its initial
|
|
874
|
+
// state. The old key would also leak in `_initialValues` indefinitely.
|
|
875
|
+
renameMutations.forEach((mutation) => {
|
|
876
|
+
const oldName = mutation.oldValue;
|
|
877
|
+
const newName = mutation.target.getAttribute('name');
|
|
878
|
+
if (!oldName || oldName === newName) {
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (newName === null) {
|
|
882
|
+
// `name` attribute removed — field will fall out of formState on re-init.
|
|
883
|
+
// Drop its captured initial so it doesn't leak in _initialValues.
|
|
884
|
+
delete this._initialValues[oldName];
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (oldName in this._initialValues) {
|
|
888
|
+
this._initialValues[newName] = this._initialValues[oldName];
|
|
889
|
+
delete this._initialValues[oldName];
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
// initializeState() rebuilds formState from scratch (re-keying any
|
|
893
|
+
// renamed element) and also dispatches `change` + refreshes button state.
|
|
894
|
+
// We also re-run _attachEventListeners() because elements that previously
|
|
895
|
+
// had no `name` were skipped by queryAuroElements() (which selects
|
|
896
|
+
// `[name]`) and therefore never received input/validation/keydown
|
|
897
|
+
// listeners. Re-attaching is safe — the listener-removal step inside
|
|
898
|
+
// _attachEventListeners() prevents duplicates on already-wired elements.
|
|
899
|
+
this.initializeState();
|
|
900
|
+
this._attachEventListeners();
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Refresh the cached `disabled` flag on each affected formState entry
|
|
905
|
+
// before the re-render, so getters that read `_isNameDisabled` see the
|
|
906
|
+
// current attribute state in the same tick.
|
|
907
|
+
relevant
|
|
908
|
+
.filter((mutation) => mutation.attributeName === 'disabled')
|
|
909
|
+
.forEach((mutation) => {
|
|
910
|
+
const name = mutation.target.getAttribute('name');
|
|
911
|
+
if (name && this.formState[name]) {
|
|
912
|
+
this.formState[name].disabled = mutation.target.hasAttribute('disabled');
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
this.requestUpdate('formState');
|
|
917
|
+
this.setDisabledStateOnButtons();
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* @returns {void}
|
|
922
|
+
*/
|
|
923
|
+
connectedCallback() {
|
|
924
|
+
super.connectedCallback();
|
|
925
|
+
|
|
926
|
+
// One observer rooted at the host catches `disabled` / `name` changes on
|
|
927
|
+
// any tracked form element (light-DOM children, including those nested in
|
|
928
|
+
// wrapper elements). Cheaper than allocating an observer per element and
|
|
929
|
+
// resilient to runtime DOM mutations.
|
|
930
|
+
if (!this._attributeObserver) {
|
|
931
|
+
this._attributeObserver = new MutationObserver(this._handleAttributeMutations);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
this._attributeObserver.observe(this, {
|
|
935
|
+
attributes: true,
|
|
936
|
+
subtree: true,
|
|
937
|
+
attributeOldValue: true,
|
|
938
|
+
attributeFilter: [
|
|
939
|
+
'disabled',
|
|
940
|
+
'name'
|
|
941
|
+
]
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* @returns {void}
|
|
947
|
+
*/
|
|
948
|
+
disconnectedCallback() {
|
|
949
|
+
// Disconnect everything we own to avoid leaking observers (and the strong
|
|
950
|
+
// refs they hold to DOM nodes) past the form's lifetime.
|
|
951
|
+
this._attributeObserver?.disconnect();
|
|
952
|
+
this.mutationObservers.forEach((observer) => observer.disconnect());
|
|
953
|
+
this.mutationObservers = [];
|
|
954
|
+
// Intentionally do NOT clear _initialValues here. Slot-moves trigger a
|
|
955
|
+
// disconnect/reconnect cycle; clearing would cause the next initializeState
|
|
956
|
+
// to capture the user's edited values as the new "initial" and silently
|
|
957
|
+
// flip the form back to its initial state.
|
|
958
|
+
|
|
959
|
+
super.disconnectedCallback();
|
|
960
|
+
}
|
|
961
|
+
|
|
636
962
|
/**
|
|
637
963
|
* @param {import('lit').PropertyValues} _changedProperties - Map of changed properties with their previous values.
|
|
638
964
|
* @returns {void}
|