Repository: AlaskaAirlines/auro-formkit
Pull request #569: Validation methodology update
Author: login: StephenRiosDev
Labels: name: released
description: Completed work has been released
Created: 2025-05-07T01:46:02Z | Merged: 2025-05-12T15:15:52Z

# Alaska Airlines Pull Request

Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.

## Checklist:

- [x] My update follows the CONTRIBUTING guidelines of this project
- [x] I have performed a self-review of my own update

**By submitting this Pull Request, I confirm that my contribution is made under the terms of the Apache 2.0 license.**

_Pull Requests will be evaluated by their quality of update and whether it is consistent with the goals and values of this project. Any submission is to be considered a conversation between the submitter and the maintainers of this project and may require changes to your submission._

**Thank you for your submission!**<br>
-- Auro Design System Team

## Summary by Sourcery

Update validation methodology across multiple Auro components by introducing a new 'touched' state to improve form validation behavior

New Features:
- Added 'touched' state to datepicker, input, radio group, and checkbox group components

Enhancements:
- Introduced a new 'touched' property to track user interaction across form components
- Refined validation logic to only trigger errors after user interaction

Tests:
- Updated test cases to reflect new validation behavior


Diff:
diff --git a/components/checkbox/src/auro-checkbox-group.js b/components/checkbox/src/auro-checkbox-group.js
index c3740b899..5c47f15fe 100644
--- a/components/checkbox/src/auro-checkbox-group.js
+++ b/components/checkbox/src/auro-checkbox-group.js
@@ -40,6 +40,14 @@ export class AuroCheckboxGroup extends LitElement {
     this.horizontal = false;
     this.onDark = false;
 
+    /**
+     * Indicates whether the checkbox group is in a dirty state (has been interacted with).
+     * @type {boolean}
+     * @default false
+     * @private
+     */
+    this.touched = false;
+
     /**
      * @private
      */
@@ -229,6 +237,8 @@ export class AuroCheckboxGroup extends LitElement {
     };
 
     this.addEventListener('auroCheckbox-focusin', () => {
+      this.touched = true;
+
       if (!this.value) {
         this.value = [];
       }
diff --git a/components/checkbox/src/auro-checkbox.js b/components/checkbox/src/auro-checkbox.js
index 667c06ba8..053f95b75 100644
--- a/components/checkbox/src/auro-checkbox.js
+++ b/components/checkbox/src/auro-checkbox.js
@@ -31,6 +31,7 @@ export class AuroCheckbox extends LitElement {
     this.disabled = false;
     this.error = false;
     this.onDark = false;
+    this.touched = false;
 
     /**
      * @private
@@ -107,6 +108,15 @@ export class AuroCheckbox extends LitElement {
       value: {
         type: String,
         reflect: true
+      },
+
+      /**
+       * Indicates whether the checkbox has been interacted with.
+       * @type {boolean}
+       */
+      touched: {
+        type: Boolean,
+        reflect: true
       }
     };
   }
@@ -161,6 +171,7 @@ export class AuroCheckbox extends LitElement {
    * @returns {void}
    */
   handleFocusin() {
+    this.touched = true;
     this.dispatchEvent(new CustomEvent('auroCheckbox-focusin', {
       bubbles: true,
       cancelable: false,
@@ -189,6 +200,7 @@ export class AuroCheckbox extends LitElement {
   reset() {
     this.checked = false;
     this.error = false;
+    this.touched = false;
   }
 
   firstUpdated() {
diff --git a/components/datepicker/src/auro-datepicker.js b/components/datepicker/src/auro-datepicker.js
index afdceb3e4..ef31c0a16 100644
--- a/components/datepicker/src/auro-datepicker.js
+++ b/components/datepicker/src/auro-datepicker.js
@@ -77,6 +77,7 @@ export class AuroDatePicker extends LitElement {
       this.calendarRenderUtil.updateCentralDate(this, new Date());
     }
 
+    this.touched = false;
     this.disabled = false;
     this.required = false;
     this.onDark = false;
@@ -408,6 +409,18 @@ export class AuroDatePicker extends LitElement {
        */
       valueEnd: {
         type: String
+      },
+
+      /**
+       * Indicates whether the datepicker is in a dirty state (has been interacted with).
+       * @type {boolean}
+       * @default false
+       * @private
+       */
+      touched: {
+        type: Boolean,
+        reflect: true,
+        attribute: false
       }
     };
   }
@@ -684,27 +697,15 @@ export class AuroDatePicker extends LitElement {
   configureDatepicker() {
     this.addEventListener('focusin', () => {
 
-      /**
-       * The datepicker is considered to be in it's initial state based on
-       * if this.value === undefined. The first time we interact with the
-       * datepicker manually, by applying focusin, we need to flag the
-       * datepicker as no longer in the initial state.
-       */
-      if (this.value === undefined) {
-        this.value = '';
-      }
-
-      if (this.valueEnd === undefined) {
-        this.valueEnd = '';
-      }
+      this.touched = true;
     });
 
     this.addEventListener('focusout', (evt) => {
-      if (!this.noValidate && !evt.detail.expanded && this.inputList[0].value !== undefined) {
+      if (!this.noValidate && !evt.detail.expanded && this.touched) {
         if (!this.contains(document.activeElement)) {
           this.validation.validate(this.inputList[0]);
 
-          if (this.inputList[1] && this.inputList[1].value !== undefined) {
+          if (this.inputList[1] && this.inputList[1].touched) {
             this.validation.validate(this.inputList[1]);
           }
         }
diff --git a/components/datepicker/test/auro-datepicker.test.js b/components/datepicker/test/auro-datepicker.test.js
index fa9bccced..ca88cfc60 100644
--- a/components/datepicker/test/auro-datepicker.test.js
+++ b/components/datepicker/test/auro-datepicker.test.js
@@ -263,10 +263,10 @@ describe('auro-datepicker', () => {
     await expect(el.getAttribute('validity')).to.be.equal('tooShort');
 
     // empty
-    el.value = "";
+    el.reset();
     el.validate();
     await elementUpdated(el);
-    await expect(el.getAttribute('validity')).to.be.equal('valid');
+    await expect(el.getAttribute('validity')).to.be.null;
 
     // set another imcomplete value
     el.value = "02/0";
diff --git a/components/input/src/base-input.js b/components/input/src/base-input.js
index 2de427049..1163b6fec 100644
--- a/components/input/src/base-input.js
+++ b/components/input/src/base-input.js
@@ -61,6 +61,7 @@ export default class BaseInput extends LitElement {
    * @returns {void}
    */
   privateDefaults() {
+    this.touched = false;
     this.util = new AuroInputUtilities();
     this.validation = new AuroFormValidation();
     this.inputIconName = undefined;
@@ -422,6 +423,18 @@ export default class BaseInput extends LitElement {
       validity: {
         type: String,
         reflect: true
+      },
+
+      /**
+       * Indicates whether the input is in a dirty state (has been interacted with).
+       * @type {boolean}
+       * @default false
+       * @private
+       */
+      touched: {
+        type: Boolean,
+        reflect: true,
+        attribute: false
       }
     };
   }
@@ -708,15 +721,7 @@ export default class BaseInput extends LitElement {
    */
   handleFocusin() {
 
-    /**
-     * The input is considered to be in it's initial state based on
-     * if this.value === undefined. The first time we interact with the
-     * input manually, by applying focusin, we need to flag the
-     * input as no longer in the initial state.
-     */
-    if (this.value === undefined) {
-      this.value = '';
-    }
+    this.touched = true;
   }
 
   /**
diff --git a/components/input/test/auro-input.test.js b/components/input/test/auro-input.test.js
index dfa1a4d2e..cb22e5b4b 100644
--- a/components/input/test/auro-input.test.js
+++ b/components/input/test/auro-input.test.js
@@ -297,6 +297,7 @@ describe('auro-input', () => {
     expect(el.hasAttribute('aria-invalid')).to.be.false;
 
     el.value = '';
+    el.touched = true;
     await elementUpdated(el);
 
     expect(el.getAttribute('validity')).to.be.equal('valueMissing');
diff --git a/components/radio/src/auro-radio-group.js b/components/radio/src/auro-radio-group.js
index 9960f0a6b..bca31e81f 100644
--- a/components/radio/src/auro-radio-group.js
+++ b/components/radio/src/auro-radio-group.js
@@ -55,6 +55,7 @@ export class AuroRadioGroup extends LitElement {
     this.value = undefined;
     this.optionSelected = undefined;
     this.onDark = false;
+    this.touched = false;
 
     /**
      * @private
@@ -136,6 +137,18 @@ export class AuroRadioGroup extends LitElement {
       },
       optionSelected: {
         type: Object
+      },
+
+      /**
+       * Indicates whether the radio group is in a dirty state (has been interacted with).
+       * @type {boolean}
+       * @default false
+       * @private
+       */
+      touched: {
+        type: Boolean,
+        reflect: true,
+        attribute: false
       }
     };
   }
@@ -191,10 +204,7 @@ export class AuroRadioGroup extends LitElement {
    * @returns {void}
    */
   handleRadioBlur() {
-    if (this.value === undefined) {
-      this.value = '';
-    }
-
+    this.touched = true;
     this.validation.validate(this);
   }
 
diff --git a/components/radio/src/auro-radio.js b/components/radio/src/auro-radio.js
index c5202b01d..e5422cf6b 100644
--- a/components/radio/src/auro-radio.js
+++ b/components/radio/src/auro-radio.js
@@ -40,6 +40,7 @@ export class AuroRadio extends LitElement {
     this.error = false;
     this.onDark = false;
     this.tabIndex = -1;
+    this.touched = false;
 
     /**
      * @private
@@ -85,6 +86,10 @@ export class AuroRadio extends LitElement {
       tabIndex: {
         type: Number,
         reflect: true
+      },
+      touched: {
+        type: Boolean,
+        reflect: true
       }
     };
   }
@@ -136,6 +141,7 @@ export class AuroRadio extends LitElement {
    * @returns {void}
    */
   handleFocus(event) {
+    this.touched = true;
     this.dispatchEvent(new CustomEvent('focusSelected', {
       bubbles: true,
       composed: true,
@@ -162,6 +168,7 @@ export class AuroRadio extends LitElement {
    * @returns {void}
    */
   reset() {
+    this.touched = false;
     this.checked = false;
     this.error = false;
   }
diff --git a/components/select/src/auro-select.js b/components/select/src/auro-select.js
index 98eeca0e1..ae1005a19 100644
--- a/components/select/src/auro-select.js
+++ b/components/select/src/auro-select.js
@@ -306,6 +306,18 @@ export class AuroSelect extends LitElement {
         reflect: true,
         attribute: 'multiselect'
       },
+
+      /**
+       * Indicates whether the input is in a dirty state (has been interacted with).
+       * @type {boolean}
+       * @default false
+       * @private
+       */
+      touched: {
+        type: Boolean,
+        reflect: true,
+        attribute: false
+      }
     };
   }
 
@@ -577,16 +589,7 @@ export class AuroSelect extends LitElement {
    */
   handleFocusin() {
 
-    /**
-     * The input is considered to be in it's initial state based on
-     * if this.value === undefined. The first time we interact with the
-     * input manually, by applying focusin, we need to flag the
-     * input as no longer in the initial state.
-     */
-    if (this.value === undefined) {
-      this.value = undefined;
-      this.removeEventListener('focusin', this.handleFocusin);
-    }
+    this.touched = true;
   }
 
   /**
diff --git a/packages/form-validation/src/validation.js b/packages/form-validation/src/validation.js
index 32aa4fff6..de1ee4986 100644
--- a/packages/form-validation/src/validation.js
+++ b/packages/form-validation/src/validation.js
@@ -22,6 +22,7 @@ export default class AuroFormValidation {
   reset(elem) {
     elem.validity = undefined;
     elem.value = undefined;
+    elem.touched = false;
 
     // Resets the second value of the datepicker in range state
     if (elem.valueEnd) {
@@ -159,7 +160,7 @@ export default class AuroFormValidation {
       } else if (elem.type === 'date' && elem.value?.length > 0) {
 
         // Guard Clause: if the value is too short
-        if (elem.value.length < elem.lengthForType) {
+        if (elem.value?.length < elem.lengthForType) {
           
           elem.validity = 'tooShort';
           elem.errorMessage = elem.setCustomValidityForType || elem.setCustomValidity || '';
@@ -229,11 +230,17 @@ export default class AuroFormValidation {
     this.getAuroInputs(elem);
 
     // Validate only if noValidate is not true and the input does not have focus
-    const validationShouldRun = force || (!elem.contains(document.activeElement) && elem.value !== undefined) || elem.validateOnInput;
+    let validationShouldRun =
+      force ||
+      (!elem.contains(document.activeElement) &&
+        (elem.touched ||
+          (!elem.touched && typeof elem.value !== "undefined"))) ||
+      elem.validateOnInput;
 
     if (elem.hasAttribute('error')) {
       elem.validity = 'customError';
       elem.errorMessage = elem.setCustomValidityCustomError || elem.error || elem.setCustomValidity || '';
+      validationShouldRun = false;
     } else if (validationShouldRun) {
       elem.validity = 'valid';
       elem.errorMessage = '';
@@ -254,7 +261,7 @@ export default class AuroFormValidation {
         }
       }
 
-      if (!hasValue && elem.required) {
+      if (!hasValue && elem.required && elem.touched) {
         elem.validity = 'valueMissing';
         elem.errorMessage = elem.setCustomValidityValueMissing || elem.setCustomValidity || '';
       } else if (this.runtimeUtils.elementMatch(elem, 'auro-input')) {
@@ -265,7 +272,7 @@ export default class AuroFormValidation {
       }
     }
 
-    if (this.auroInputElements?.length > 0) {
+    if (this.auroInputElements?.length > 0 && elem.validity !== "valueMissing") {
       elem.validity = this.auroInputElements[0].validity;
       elem.errorMessage = this.auroInputElements[0].errorMessage;
 
@@ -344,7 +351,7 @@ export default class AuroFormValidation {
         }
       }
     } else {
-      elem.errorMessage = undefined;
+      elem.errorMessage = '';
     }
   }
 }


Reviews:
user: login: sourcery-ai[bot]
body: Hey @StephenRiosDev - I've reviewed your changes and they look great!

<details>
<summary>Here's what I looked at during the review</summary>

- 🟢 **General issues**: all looks good
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-07T01:49:21Z
user: login: sun-mota
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-07T14:30:05Z
user: login: sun-mota
body: 1. Please update the PR title to represent the purpose of this PR. 
2. issue ticket should be linked. 
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-07T14:31:49Z
user: login: StephenRiosDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T15:51:35Z
user: login: StephenRiosDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T15:59:59Z
user: login: sun-mota
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T16:08:23Z
user: login: sourcery-ai[bot]
body: Hey @StephenRiosDev - I've reviewed your changes - here's some feedback:

- The `reset()` methods in group components like `auro-radio-group` and `auro-checkbox-group` set `touched = false` locally before `this.validation.reset(this)` is called, which also resets `touched`; consider removing the redundant local assignment of `touched = false`.
- The `valueMissing` error for required fields now only appears if the component is `touched`; confirm this behavior is intended for all validation scenarios, including forced validation (e.g., on form submission) where previously `valueMissing` might have shown for untouched fields.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 1 issue found
- 🟢 **Security**: all looks good
- 🟡 **Testing**: 2 issues found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-07T18:40:55Z
user: login: sun-mota
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T18:57:29Z
user: login: StephenRiosDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T19:20:59Z
user: login: sun-mota
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T20:11:24Z
user: login: jordanjones243
body: - `radio` required validation is NOT working
- Not seeing help text for `patternMistmatch` state in input (validate on input example)
- `select` required validation is NOT working
- Datepicker range support has interesting UI: When having two values, deleting the second one via the `x` icon puts the datepicker in a `valueMissing` state without shifting focus from input. Then, after selecting a date with the calendar, we still see the error icon until the input loses focus. Same can actually be said with non-range datepicker.

state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T22:30:04Z
user: login: StephenRiosDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T03:20:57Z
user: login: StephenRiosDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T03:33:11Z
user: login: jordanjones243
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T16:33:16Z
user: login: sun-mota
body: <img width="911" alt="image" src="https://github.com/user-attachments/assets/a46fa6a0-27c9-44b1-a717-f7da3d57ea92" />
1. no error message showing on required select.


2. required checkbox group validation does not run on blur. 


state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-09T16:55:31Z
user: login: jordanjones243
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T19:16:31Z
user: login: sun-mota
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-12T15:02:12Z

Review comments:
diff_hunk: @@ -39,6 +39,8 @@ export class AuroCheckboxGroup extends LitElement {
     this.required = false;
     this.horizontal = false;
     this.onDark = false;
+    this.pristine = true;
+    this.dirty = false;
path: components/checkbox/src/auro-checkbox-group.js
user: login: sun-mota
body: JSDoc comments are needed for these variables. 
created_at: 2025-05-07T14:23:23Z
author_association: MEMBER
diff_hunk: @@ -422,7 +424,19 @@ export default class BaseInput extends LitElement {
       validity: {
         type: String,
         reflect: true
-      }
+      },
+
+      pristine: {
+        type: Boolean,
+        reflect: true,
+        attribute: 'pristine'
+      },
+
+      dirty: {
+        type: Boolean,
+        reflect: true,
+        attribute: 'dirty'
+      },
path: components/input/src/base-input.js
user: login: sun-mota
body: 1. Please add JSDoc comments. 
2. Do these need to be public? What's the usecase of consumers set these on purpose? 
created_at: 2025-05-07T14:28:07Z
author_association: MEMBER
diff_hunk: @@ -136,6 +138,14 @@ export class AuroRadioGroup extends LitElement {
       },
       optionSelected: {
         type: Object
+      },
+      pristine: {
+        type: Boolean,
+        reflect: true
+      },
+      dirty: {
+        type: Boolean,
+        reflect: true
       }
path: components/radio/src/auro-radio-group.js
user: login: sun-mota
body: Same as base-input's comment. JSDoc comments needed. 
created_at: 2025-05-07T14:28:53Z
author_association: MEMBER
diff_hunk: @@ -22,6 +22,8 @@ export default class AuroFormValidation {
   reset(elem) {
     elem.validity = undefined;
     elem.value = undefined;
+    elem.pristine = true;
+    elem.dirty = false;
path: packages/form-validation/src/validation.js
user: login: sun-mota
body: if pristine and dirty will be always opposite to each other, do we really need 2 variables? can we just have only one? 
created_at: 2025-05-07T14:29:35Z
author_association: MEMBER
diff_hunk: @@ -22,6 +22,8 @@ export default class AuroFormValidation {
   reset(elem) {
     elem.validity = undefined;
     elem.value = undefined;
+    elem.pristine = true;
+    elem.dirty = false;
path: packages/form-validation/src/validation.js
user: login: StephenRiosDev
body: This is an industry-standard best practice for form validation. You don't strictly need both properties, but it speaks to semantics/intent.

`if (input.dirty)` is more semantic and specific than `if (!input.pristine)` when it comes to reading code.
created_at: 2025-05-07T15:51:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -422,7 +424,19 @@ export default class BaseInput extends LitElement {
       validity: {
         type: String,
         reflect: true
-      }
+      },
+
+      pristine: {
+        type: Boolean,
+        reflect: true,
+        attribute: 'pristine'
+      },
+
+      dirty: {
+        type: Boolean,
+        reflect: true,
+        attribute: 'dirty'
+      },
path: components/input/src/base-input.js
user: login: StephenRiosDev
body: The most common use case for exposing these is allowing for external state management by consumers.
created_at: 2025-05-07T15:59:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -422,7 +424,19 @@ export default class BaseInput extends LitElement {
       validity: {
         type: String,
         reflect: true
-      }
+      },
+
+      pristine: {
+        type: Boolean,
+        reflect: true,
+        attribute: 'pristine'
+      },
+
+      dirty: {
+        type: Boolean,
+        reflect: true,
+        attribute: 'dirty'
+      },
path: components/input/src/base-input.js
user: login: sun-mota
body: will they actually want to do that? what if they mess this up? such as set pristine and dirty to both true or false? 
created_at: 2025-05-07T16:08:23Z
author_association: MEMBER
diff_hunk: @@ -159,7 +160,7 @@ export default class AuroFormValidation {
       } else if (elem.type === 'date' && elem.value?.length > 0) {
 
         // Guard Clause: if the value is too short
-        if (elem.value.length < elem.lengthForType) {
+        if (elem.value?.length < elem.lengthForType) {
           
path: packages/form-validation/src/validation.js
user: login: sourcery-ai[bot]
body: **suggestion:** Improve robustness with optional chaining.

Verify the length check still behaves correctly when elem.value is undefined.

```suggestion
        if ((elem.value?.length ?? 0) < elem.lengthForType) {
```
created_at: 2025-05-07T18:40:55Z
author_association: NONE
diff_hunk: @@ -297,6 +297,7 @@ describe('auro-input', () => {
     expect(el.hasAttribute('aria-invalid')).to.be.false;
 
     el.value = '';
+    el.touched = true;
     await elementUpdated(el);
 
     expect(el.getAttribute('validity')).to.be.equal('valueMissing');
path: components/input/test/auro-input.test.js
user: login: sourcery-ai[bot]
body: **suggestion (testing):** Add test for pristine state of required input not showing errors.

Add a test for a required input initially rendered (empty, touched=false) to ensure `valueMissing` isn't shown when `validate()` runs (without force). The error should only appear after the field is touched and remains empty.

Suggested implementation:

```javascript
it('should not show valueMissing error for pristine required input when validate() is run', async () => {
  // Create an instance of the required input.
  const el = await fixture(html`<auro-input required></auro-input>`);

  // Ensure pristine state: input is empty and not touched.
  el.value = '';
  el.touched = false;
  await elementUpdated(el);

  // Run the validate method without forcing validation.
  el.validate && el.validate();
  await elementUpdated(el);

  // Expect no "valueMissing" error to be set since the input is pristine.
  expect(el.getAttribute('validity')).to.not.equal('valueMissing');
});

```

If your component does not have a validate() method or uses a different mechanism for triggering validation, adjust the test accordingly. Also, make sure to import any necessary test utilities like fixture, html, and elementUpdated at the top of the file.
created_at: 2025-05-07T18:40:55Z
author_association: NONE
diff_hunk: @@ -263,10 +263,10 @@ describe('auro-datepicker', () => {
     await expect(el.getAttribute('validity')).to.be.equal('tooShort');
 
     // empty
-    el.value = "";
+    el.reset();
path: components/datepicker/test/auro-datepicker.test.js
user: login: sourcery-ai[bot]
body: **suggestion (testing):** Expand tests for `reset()` functionality to cover `touched` state and validation clearing.

Add tests for reset() that verify it: 1. clears values, 2. sets touched to false, 3. removes validation messages and resets validity (errorMessage and validity), and 4. prevents a required field from showing a valueMissing error until after user interaction.

Suggested implementation:

```javascript
    // set another incomplete value
    el.value = "02/0";

    // New tests for reset() functionality:
    it('should reset value, touched state, errorMessage, and validity, and delay required error until user interaction', async () => {
      // Simulate user providing an invalid input and having interacted with the field.
      el.value = "123";
      el.touched = true; // assuming the component uses a touched flag to indicate user interaction
      el.validate();
      await elementUpdated(el);

      // Invoke reset() to clear all state.
      el.reset();
      el.validate();
      await elementUpdated(el);

      // Verify that the value is cleared.
      expect(el.value).to.equal("");

      // Verify that touched state is reset.
      expect(el.touched).to.be.false;

      // Verify that the error message is cleared.
      expect(el.errorMessage).to.equal("");

      // Verify that validity attribute is reset.
      expect(el.getAttribute('validity')).to.be.null;

      // Set the field as required. Validation error (e.g., valueMissing) should not appear until the user interacts.
      el.required = true;
      el.validate();
      await elementUpdated(el);

      // Confirm that there is no valueMissing error at this point.
      expect(el.getAttribute('validity')).to.be.null;
    });

```

Ensure that the properties "touched" and "errorMessage" exist on your element and are used to track the user interaction state and the validation messages respectively. If these properties have different names in your code, please update the test accordingly. Also, make sure that the test framework (e.g., Mocha, Chai) is properly configured to run asynchronous tests.
created_at: 2025-05-07T18:40:55Z
author_association: NONE
diff_hunk: @@ -350,6 +365,8 @@ export class AuroRadioGroup extends LitElement {
    * @returns {void}
    */
   handleToggleSelected(event) {
+    this.touched = true;
path: components/radio/src/auro-radio-group.js
user: login: sun-mota
body: input and checkbox's touched become `true` on  `focusin` but radio does on `toggleSelected`. 
It'll be better to keep them consistent on the timing. 
created_at: 2025-05-07T18:57:29Z
author_association: MEMBER
diff_hunk: @@ -350,6 +365,8 @@ export class AuroRadioGroup extends LitElement {
    * @returns {void}
    */
   handleToggleSelected(event) {
+    this.touched = true;
path: components/radio/src/auro-radio-group.js
user: login: StephenRiosDev
body: This is due to Radio Group not having the same focus tracking functionality as Checkbox Group. I didn't want to make such large changes to a component as part of this work.

Since this solution works as expected, would it be OK to make a ticket for this work and take it up separately, or do you think we should address it as part of this ticket?
created_at: 2025-05-07T19:20:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -350,6 +365,8 @@ export class AuroRadioGroup extends LitElement {
    * @returns {void}
    */
   handleToggleSelected(event) {
+    this.touched = true;
path: components/radio/src/auro-radio-group.js
user: login: sun-mota
body: Adding `focusin` event handler is not a huge thing IMO. I'd rather not leave it as another tech debt. 
created_at: 2025-05-07T20:11:24Z
author_association: MEMBER
diff_hunk: @@ -229,11 +230,17 @@ export default class AuroFormValidation {
     this.getAuroInputs(elem);
 
     // Validate only if noValidate is not true and the input does not have focus
-    const validationShouldRun = force || (!elem.contains(document.activeElement) && elem.value !== undefined) || elem.validateOnInput;
+    let validationShouldRun =
+      force ||
+      (!elem.contains(document.activeElement) &&
+        (elem.touched ||
+          (!elem.touched && typeof elem.value !== "undefined"))) ||
path: packages/form-validation/src/validation.js
user: login: jordanjones243
body: Why was the addition of `typeof` needed?
created_at: 2025-05-07T22:10:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -350,6 +365,8 @@ export class AuroRadioGroup extends LitElement {
    * @returns {void}
    */
   handleToggleSelected(event) {
+    this.touched = true;
path: components/radio/src/auro-radio-group.js
user: login: StephenRiosDev
body: We can absolutely copy the lines of code that handle this in checkbox over to radio and make the necessary adjustments to get it to work, but I have some concerns with this approach, especially since the naming conventions for the events aren't consistent and the code appears rather fragile, and we'd be duplicating a lot of code that should probably be standardized instead.

I'm happy to take this work up before moving on to my next sprint task if you're worried about it sitting, it won't be an issue for me to get it resolved quickly before moving on.

Let's connect in the morning and see if we can come to a solution that works for us to get this work out the door and still leave ourselves room for any necessary improvements we need to make in the future.



For reference, this is the code that would need to be copied and modified:

```javascript
// must declare this function as a variable to correctly pass the reference to the removeEventListener
const checkFocusWithin = function(evt) {
  if (document.auroCheckboxGroupActive && !document.auroCheckboxGroupActive.contains(evt.target)) {
    // if focus has left the group, cleanup and validate
    document.auroCheckboxGroupActive.focusWithin = false;
    window.removeEventListener('focusin', checkFocusWithin);
    document.removeEventListener('click', checkFocusWithin);
    // execute the validation
    document.auroCheckboxGroupActive.validation.validate(document.auroCheckboxGroupActive);
  }
};

this.addEventListener('auroCheckbox-focusin', () => {
  this.touched = true;

  if (!this.value) {
    this.value = [];
  }

  // handle click outside the group
  if (!this.focusWithin) {
    document.addEventListener('click', checkFocusWithin);
  }

  this.focusWithin = true;
});

this.addEventListener('auroCheckbox-focusout', () => {
  document.auroCheckboxGroupActive = this;

  // Only add the focusWithin check event listener once as you move focus through the options
  if (this.focusWithin) {
    window.addEventListener('focusin', checkFocusWithin);
  } else {
    this.focusWithin = true;
  }
});
```
created_at: 2025-05-08T03:20:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -229,11 +230,17 @@ export default class AuroFormValidation {
     this.getAuroInputs(elem);
 
     // Validate only if noValidate is not true and the input does not have focus
-    const validationShouldRun = force || (!elem.contains(document.activeElement) && elem.value !== undefined) || elem.validateOnInput;
+    let validationShouldRun =
+      force ||
+      (!elem.contains(document.activeElement) &&
+        (elem.touched ||
+          (!elem.touched && typeof elem.value !== "undefined"))) ||
path: packages/form-validation/src/validation.js
user: login: StephenRiosDev
body: `typeof value !== 'undefined'` is a safeguard against reference errors for undeclared variables and properties, kind of like how optional chaining helps us with reference errors but for `undefined` values specifically.
created_at: 2025-05-08T03:33:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,6 +317,7 @@ export class AuroCheckboxGroup extends LitElement {
       checkbox.reset();
     });
 
+    this.touched = false;
path: components/checkbox/src/auro-checkbox-group.js
user: login: jordanjones243
body: You should be setting `this.touched = false` in all component's `reset()` method
created_at: 2025-05-09T16:33:13Z
author_association: CONTRIBUTOR

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-05-07T01:46:06Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide

This pull request implements a "touched" state (akin to pristine/dirty) for core form components by adding a `touched` boolean property. This property is initialized to `false`, set to `true` upon first user interaction (e.g., `focusin`, selection change), and reset via component `reset()` methods. The central `AuroFormValidation` logic has been refactored to leverage this `touched` state, notably by only triggering certain validations (like "valueMissing" for required fields) after a component has been interacted with. Additionally, custom error attributes now prevent further standard validation, and error message initialization has been standardized to use empty strings.

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Introduced a `touched` boolean property to form components to track user interaction. | <ul><li>Initialized `touched` property to `false` in relevant components.</li><li>Set `touched` to `true` upon first user interaction events like `focusin` or selection changes.</li></ul> | `components/datepicker/src/auro-datepicker.js`<br/>`components/input/src/base-input.js`<br/>`components/radio/src/auro-radio-group.js`<br/>`components/checkbox/src/auro-checkbox-group.js` |
| Updated the central form validation logic to incorporate the `touched` state. | <ul><li>Modified validation conditions to check the `touched` status before applying rules like 'valueMissing' for required fields.</li><li>Adjusted the main validation trigger logic (`validationShouldRun`) to consider if an element is `touched` or if it is not `touched` but has a pre-existing value.</li><li>Ensured that elements with a pre-set `error` attribute bypass subsequent standard validation routines.</li></ul> | `packages/form-validation/src/validation.js` |
| Standardized component reset behavior and error message initialization. | <ul><li>Component `reset()` methods and the `AuroFormValidation.reset()` utility now also reset the `touched` state to `false`.</li><li>Changed default/empty error messages to be an empty string (`''`) rather than `undefined` for consistency.</li></ul> | `packages/form-validation/src/validation.js`<br/>`components/radio/src/auro-radio-group.js`<br/>`components/checkbox/src/auro-checkbox-group.js`<br/>`components/datepicker/src/auro-datepicker.js` |
| Updated unit tests to reflect the new validation behavior considering the `touched` state. | <ul><li>Modified test cases to set the `touched` state appropriately before asserting validation outcomes (e.g., for 'valueMissing').</li><li>Incorporated component `reset()` methods in test setups where applicable.</li></ul> | `components/datepicker/test/auro-datepicker.test.js`<br/>`components/input/test/auro-input.test.js` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: NONE
user: login: StephenRiosDev
created_at: 2025-05-07T01:46:59Z
body: Target branch should probably be updated to beta, but beta needs to be updated first. Targeting main for now just to get the PR out.
author_association: CONTRIBUTOR
user: login: StephenRiosDev
created_at: 2025-05-07T18:35:54Z
body: @sourcery-ai review
author_association: CONTRIBUTOR
user: login: StephenRiosDev
created_at: 2025-05-08T03:48:58Z
body: > * `radio` required validation is NOT working

Found and fixed.
 
> * Not seeing help text for `patternMistmatch` state in input (validate on input example)

This seems to be working for me, but only on blur, which is the same way this works on the docs site and on the current `main` branch, which I do agree seems odd. I totally expected it to validate as I typed.

> * `select` required validation is NOT working

This got skipped somehow? Good catch, thank you. It's been added.

> * Datepicker range support has interesting UI: When having two values, deleting the second one via the `x` icon puts the datepicker in a `valueMissing` state without shifting focus from input. Then, after selecting a date with the calendar, we still see the error icon until the input loses focus. Same can actually be said with non-range datepicker.

Yah this is definitely strange behavior, but it's doing this on the docs site and main as well.


author_association: CONTRIBUTOR
user: login: CLAassistant
created_at: 2025-05-09T17:33:27Z
body: [![CLA assistant check](https://cla-assistant.io/pull/badge/signed)](https://cla-assistant.io/AlaskaAirlines/auro-formkit?pullRequest=569) <br/>All committers have signed the CLA.
author_association: NONE
user: login: StephenRiosDev
created_at: 2025-05-12T14:47:28Z
body: @sun-mota 

Bug's opened to addressed the issues we discussed:

Pattern Input Issues: https://github.com/AlaskaAirlines/auro-formkit/issues/583
Error messaging checkup: https://github.com/AlaskaAirlines/auro-formkit/issues/573
author_association: CONTRIBUTOR
user: login: jason-capsule42
created_at: 2025-05-12T20:54:19Z
body: :tada: This PR is included in version 3.3.0 :tada:

The release is available on:
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@aurodesignsystem/auro-formkit/v/3.3.0)
- [GitHub release](https://github.com/AlaskaAirlines/auro-formkit/releases/tag/v3.3.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: MEMBER