@aurodesignsystem-dev/auro-formkit 0.0.0-pr1506.0 → 0.0.0-pr1506.2

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 (56) hide show
  1. package/components/checkbox/demo/customize.html +1 -2
  2. package/components/checkbox/demo/customize.min.js +1 -1
  3. package/components/checkbox/demo/getting-started.min.js +1 -1
  4. package/components/checkbox/demo/index.min.js +1 -1
  5. package/components/checkbox/dist/index.js +1 -1
  6. package/components/checkbox/dist/registered.js +1 -1
  7. package/components/combobox/demo/customize.html +1 -2
  8. package/components/combobox/demo/customize.md +108 -132
  9. package/components/combobox/demo/customize.min.js +26 -130
  10. package/components/combobox/demo/getting-started.min.js +26 -130
  11. package/components/combobox/demo/index.min.js +26 -130
  12. package/components/combobox/dist/index.js +26 -130
  13. package/components/combobox/dist/registered.js +26 -130
  14. package/components/counter/demo/customize.min.js +2 -2
  15. package/components/counter/demo/index.min.js +2 -2
  16. package/components/counter/dist/index.js +2 -2
  17. package/components/counter/dist/registered.js +2 -2
  18. package/components/datepicker/demo/customize.min.js +12 -124
  19. package/components/datepicker/demo/index.min.js +12 -124
  20. package/components/datepicker/dist/index.js +12 -124
  21. package/components/datepicker/dist/registered.js +12 -124
  22. package/components/dropdown/demo/customize.min.js +1 -1
  23. package/components/dropdown/demo/getting-started.min.js +1 -1
  24. package/components/dropdown/demo/index.min.js +1 -1
  25. package/components/dropdown/dist/index.js +1 -1
  26. package/components/dropdown/dist/registered.js +1 -1
  27. package/components/form/demo/customize.html +6 -6
  28. package/components/form/demo/customize.js +19 -0
  29. package/components/form/demo/customize.md +203 -51
  30. package/components/form/demo/customize.min.js +489 -419
  31. package/components/form/demo/getting-started.min.js +417 -419
  32. package/components/form/demo/index.min.js +417 -419
  33. package/components/form/demo/registerDemoDeps.min.js +54 -382
  34. package/components/form/dist/auro-form.d.ts +122 -4
  35. package/components/form/dist/index.js +363 -37
  36. package/components/form/dist/registered.js +363 -37
  37. package/components/input/demo/customize.html +1 -2
  38. package/components/input/demo/customize.min.js +10 -122
  39. package/components/input/demo/getting-started.min.js +10 -122
  40. package/components/input/demo/index.min.js +10 -122
  41. package/components/input/dist/base-input.d.ts +1 -49
  42. package/components/input/dist/index.js +10 -122
  43. package/components/input/dist/registered.js +10 -122
  44. package/components/radio/demo/customize.min.js +1 -1
  45. package/components/radio/demo/getting-started.min.js +1 -1
  46. package/components/radio/demo/index.min.js +1 -1
  47. package/components/radio/dist/index.js +1 -1
  48. package/components/radio/dist/registered.js +1 -1
  49. package/components/select/demo/customize.html +1 -2
  50. package/components/select/demo/customize.min.js +2 -2
  51. package/components/select/demo/getting-started.min.js +2 -2
  52. package/components/select/demo/index.min.js +2 -2
  53. package/components/select/dist/index.js +2 -2
  54. package/components/select/dist/registered.js +2 -2
  55. package/custom-elements.json +123 -266
  56. 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 {HTMLElement} element - Whether the form element is required or not.
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
- // go through validity states and return the first invalid state (if any)
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 and updates `_isInitialState` accordingly.
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) => this.formState[key].validity !== null || this.formState[key].value !== null);
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
- this._resetElements.forEach((resetElement) => {
352
- if (resetElement.hasAttribute("disabled")) {
353
- resetElement.removeAttribute("disabled");
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
- if (this.isInitialState) {
374
- element.setAttribute("disabled", "");
375
- } else {
376
- element.removeAttribute("disabled");
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
- if (this.isInitialState || this.validity !== "valid") {
382
- element.setAttribute("disabled", "");
383
- } else {
384
- element.removeAttribute("disabled");
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
- // element
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 ALL elements
515
- this._elements.forEach((element) => {
516
- element.validate(true);
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
- // Only dispatch submit event if form is valid
523
- if (this.validity === 'valid') {
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
- this.formState[targetName].validity = event.detail.validity;
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}
@@ -42,12 +42,11 @@
42
42
  <script src="https://cdn.jsdelivr.net/npm/@aurodesignsystem/auro-icon@latest/+esm" type="module"></script>
43
43
  <script src="https://cdn.jsdelivr.net/npm/@aurodesignsystem/auro-button@latest/+esm" type="module"></script>
44
44
  <script src="https://cdn.jsdelivr.net/npm/@aurodesignsystem/auro-formkit@latest/auro-radio/+esm" type="module"></script>
45
- <script type="module">
45
+ <script type="module" data-demo-script="true">
46
46
  import { renderPage } from './demo-support.min.js';
47
47
  await renderPage('./customize.md');
48
48
  import { initExamples } from "./customize.min.js";
49
49
  initExamples();
50
50
  </script>
51
- <script src="./customize.min.js" data-demo-script="true" type="module"></script>
52
51
  </body>
53
52
  </html>