diff --git a/packages/web-components/src/components/va-date/test/va-date.e2e.ts b/packages/web-components/src/components/va-date/test/va-date.e2e.ts index fc7cf0a08..36bd23356 100644 --- a/packages/web-components/src/components/va-date/test/va-date.e2e.ts +++ b/packages/web-components/src/components/va-date/test/va-date.e2e.ts @@ -101,8 +101,10 @@ describe('va-date', () => { ); const date = await page.find('va-date'); const handleYear = await page.$('pierce/[name="testYear"]'); + const handleMonth = await page.$('pierce/[name="testMonth"]'); // Trigger Blur + await handleMonth.press('Tab'); await handleYear.press('Tab'); await page.waitForChanges(); @@ -499,6 +501,9 @@ describe('va-date', () => { const date = await page.find('va-date'); const handleYear = await page.$('pierce/[name="testYear"]'); + const handleMonth = await page.$('pierce/[name="testMonth"]'); + + await handleMonth.press('Tab'); // Year await handleYear.press('2'); diff --git a/packages/web-components/src/components/va-date/va-date.tsx b/packages/web-components/src/components/va-date/va-date.tsx index 3052c13e1..fa6926599 100644 --- a/packages/web-components/src/components/va-date/va-date.tsx +++ b/packages/web-components/src/components/va-date/va-date.tsx @@ -15,7 +15,6 @@ import { days, validate, getErrorParameters, - checkIsNaN, zeroPadStart, } from '../../utils/date-utils'; @@ -103,6 +102,10 @@ export class VaDate { @Prop({ mutable: true }) invalidMonth: boolean = false; @Prop({ mutable: true }) invalidYear: boolean = false; + private dayTouched: boolean = false; + private monthTouched: boolean = false; + private yearTouched: boolean = false; + /** * Whether or not an analytics event will be fired. */ @@ -141,15 +144,21 @@ export class VaDate { .split('-') .map(val => Number(val)); - if(!checkIsNaN(this, year, month, day, this.monthYearOnly)) { - // if any fields are NaN do not continue validation + validate({ + component: this, + year, + month, + day, + yearTouched: this.yearTouched, + monthTouched: this.monthTouched, + dayTouched: this.dayTouched + }); + + if (this.error) { return; } this.setValue(year, month, day); - // Run built-in validation. Any custom validation - // will happen afterwards - validate(this, year, month, day, this.monthYearOnly); this.dateBlur.emit(event); if (this.enableAnalytics) { @@ -191,6 +200,18 @@ export class VaDate { this.dateChange.emit(event); }; + private handleMonthBlur = () => { + this.monthTouched = true; + } + + private handleDayBlur = () => { + this.dayTouched = true; + } + + private handleYearBlur = () => { + this.yearTouched = true; + } + render() { const { required, @@ -236,6 +257,7 @@ export class VaDate { // Value must be a string value={month?.toString()} onVaSelect={handleDateChange} + onBlur={this.handleMonthBlur} invalid={this.invalidMonth} class="select-month" aria-label="Please enter two digits for the month" @@ -256,6 +278,7 @@ export class VaDate { // Value must be a string value={daysForSelectedMonth.length < day ? '' : day?.toString()} onVaSelect={handleDateChange} + onBlur={this.handleDayBlur} invalid={this.invalidDay} class="select-day" aria-label="Please enter two digits for the day" @@ -279,6 +302,7 @@ export class VaDate { value={year ? year.toString() : ''} invalid={this.invalidYear} onInput={handleDateChange} + onBlur={this.handleYearBlur} class="input-year" inputmode="numeric" type="text" diff --git a/packages/web-components/src/components/va-memorable-date/test/va-memorable-date.e2e.ts b/packages/web-components/src/components/va-memorable-date/test/va-memorable-date.e2e.ts index bf65b13ce..5e0771ca1 100644 --- a/packages/web-components/src/components/va-memorable-date/test/va-memorable-date.e2e.ts +++ b/packages/web-components/src/components/va-memorable-date/test/va-memorable-date.e2e.ts @@ -171,8 +171,10 @@ describe('va-memorable-date', () => { ); const date = await page.find('va-memorable-date'); const handleYear = await page.$('pierce/[name="testYear"]'); + const handleMonth = await page.$('pierce/[name="testMonth"]'); // Trigger Blur + await handleMonth.press('Tab'); await handleYear.press('Tab'); await page.waitForChanges(); @@ -850,6 +852,7 @@ describe('va-memorable-date', () => { // Set an invalid value await handleMonth.select(''); + await handleMonth.press('Tab'); // Trigger Blur await handleYear.press('Tab'); @@ -883,8 +886,10 @@ describe('va-memorable-date', () => { ); const date = await page.find('va-memorable-date'); const handleYear = await page.$('pierce/[name="testYear"]'); + const handleMonth = await page.$('pierce/[name="testMonth"]'); // Trigger Blur + await handleMonth.press('Tab'); await handleYear.press('Tab'); await page.waitForChanges(); @@ -995,6 +1000,7 @@ describe('va-memorable-date', () => { // Select month value that doesn't exist await handleMonth.select('39'); + await handleMonth.press('Tab'); // Trigger Blur await handleYear.press('Tab'); @@ -1507,8 +1513,10 @@ describe('va-memorable-date', () => { ); const date = await page.find('va-memorable-date'); const handleYear = await page.$('pierce/[name="testYear"]'); + const handleMonth = await page.$('pierce/[name="testMonth"]'); // Trigger Blur + await handleMonth.press('Tab'); await handleYear.press('Tab'); await page.waitForChanges(); @@ -1525,8 +1533,10 @@ describe('va-memorable-date', () => { ); const date = await page.find('va-memorable-date'); const handleYear = await page.$('pierce/[name="testYear"]'); + const handleMonth = await page.$('pierce/[name="testMonth"]'); // Trigger Blur + await handleMonth.press('Tab'); await handleYear.press('Tab'); await page.waitForChanges(); diff --git a/packages/web-components/src/components/va-memorable-date/va-memorable-date.tsx b/packages/web-components/src/components/va-memorable-date/va-memorable-date.tsx index 404f80cae..609986939 100644 --- a/packages/web-components/src/components/va-memorable-date/va-memorable-date.tsx +++ b/packages/web-components/src/components/va-memorable-date/va-memorable-date.tsx @@ -14,7 +14,6 @@ import { getErrorParameters, months, validate, - checkIsNaN, zeroPadStart, } from '../../utils/date-utils'; @@ -113,14 +112,27 @@ export class VaMemorableDate { @Prop({ mutable: true }) invalidMonth: boolean = false; @Prop({ mutable: true }) invalidYear: boolean = false; + private dayTouched: boolean = false; + private monthTouched: boolean = false; + private yearTouched: boolean = false; + private handleDateBlur = (event: FocusEvent) => { const [year, month, day] = (this.value || '').split('-'); const yearNum = Number(year); const monthNum = Number(month); const dayNum = Number(day); - if(!checkIsNaN(this, yearNum, monthNum, dayNum)) { - // if any fields are NaN do not continue validation + validate({ + component: this, + year: yearNum, + month: monthNum, + day: dayNum, + yearTouched: this.yearTouched, + monthTouched: this.monthTouched, + dayTouched: this.dayTouched + }); + + if (this.error) { return; } @@ -135,10 +147,6 @@ export class VaMemorableDate { // errors will also remove internal errors. this.dateBlur.emit(event); - // Built-in validation is run after custom so internal errors override - // custom errors, e.g. Show invalid date instead of custom error - validate(this, yearNum, monthNum, dayNum); - if (this.enableAnalytics) { const detail = { componentName: 'va-memorable-date', @@ -176,6 +184,18 @@ export class VaMemorableDate { this.dateChange.emit(event); }; + private handleMonthBlur = () => { + this.monthTouched = true; + } + + private handleDayBlur = () => { + this.dayTouched = true; + } + + private handleYearBlur = () => { + this.yearTouched = true; + } + /** * Whether or not an analytics event will be fired. */ @@ -257,6 +277,7 @@ export class VaMemorableDate { aria-describedby={describedbyIds} invalid={this.invalidMonth} onVaSelect={handleDateChange} + onBlur={this.handleMonthBlur} class='usa-form-group--month-select' reflectInputError={error === 'month-range' ? true : false} value={month ? String(parseInt(month)) : month} @@ -284,8 +305,9 @@ export class VaMemorableDate { // if NaN provide empty string value={month?.toString()} onInput={handleDateChange} + onBlur={this.handleMonthBlur} class="usa-form-group--month-input memorable-date-input" - reflectInputError={error === 'month-range' ? true : false} + reflectInputError={error === 'month-range' ? true : false} inputmode="numeric" type="text" error={this.invalidMonth ? getErrorMessage(error) : null} @@ -329,6 +351,7 @@ export class VaMemorableDate { // if NaN provide empty string value={day?.toString()} onInput={handleDateChange} + onBlur={this.handleDayBlur} class="usa-form-group--day-input memorable-date-input" reflectInputError={error === 'day-range' ? true : false} inputmode="numeric" @@ -349,6 +372,7 @@ export class VaMemorableDate { // if NaN provide empty string value={year?.toString()} onInput={handleDateChange} + onBlur={this.handleYearBlur} class="usa-form-group--year-input memorable-date-input" reflectInputError={error === 'year-range' ? true : false} inputmode="numeric" @@ -390,6 +414,7 @@ export class VaMemorableDate { // if NaN provide empty string value={month?.toString()} onInput={handleDateChange} + onBlur={this.handleMonthBlur} class="input-month memorable-date-input" inputmode="numeric" type="text" @@ -407,6 +432,7 @@ export class VaMemorableDate { // if NaN provide empty string value={day?.toString()} onInput={handleDateChange} + onBlur={this.handleDayBlur} class="input-day memorable-date-input" inputmode="numeric" type="text" @@ -424,6 +450,7 @@ export class VaMemorableDate { // if NaN provide empty string value={year?.toString()} onInput={handleDateChange} + onBlur={this.handleYearBlur} class="input-year memorable-date-input" inputmode="numeric" type="text" diff --git a/packages/web-components/src/utils/date-utils.spec.ts b/packages/web-components/src/utils/date-utils.spec.ts index 1383f998d..ff20642d1 100644 --- a/packages/web-components/src/utils/date-utils.spec.ts +++ b/packages/web-components/src/utils/date-utils.spec.ts @@ -6,7 +6,6 @@ import { isDateAfter, isDateBefore, isDateSameDay, - checkIsNaN, zeroPadStart, } from './date-utils'; @@ -21,100 +20,51 @@ describe('checkLeapYear', }); describe('validate', () => { - it('indicates when the year is below the accepted range', () => { - const memorableDateComponent = {} as Components.VaMemorableDate; - const year = 1500; - const month = 1; - const day = 1; - - validate(memorableDateComponent, year, month, day); - - expect(memorableDateComponent.error).toEqual(`year-range`); - expect(memorableDateComponent.invalidYear).toEqual(true); - expect(memorableDateComponent.invalidMonth).toEqual(false); - expect(memorableDateComponent.invalidDay).toEqual(false); - }); - - it('indicates when the year is above the accepted range', () => { - const memorableDateComponent = {} as Components.VaMemorableDate; - const year = 3000; - const month = 1; - const day = 1; - - validate(memorableDateComponent, year, month, day); - - expect(memorableDateComponent.error).toEqual(`year-range`); - expect(memorableDateComponent.invalidYear).toEqual(true); - expect(memorableDateComponent.invalidMonth).toEqual(false); - expect(memorableDateComponent.invalidDay).toEqual(false); - }); - - it('indicates when the month is above the accepted range', () => { - const memorableDateComponent = {} as Components.VaMemorableDate; - const year = 2000; - const month = 15; - const day = 1; - - validate(memorableDateComponent, year, month, day); - - expect(memorableDateComponent.error).toEqual('month-range'); - expect(memorableDateComponent.invalidYear).toEqual(false); - expect(memorableDateComponent.invalidMonth).toEqual(true); - expect(memorableDateComponent.invalidDay).toEqual(false); - }); - - it('indicates when the day is above the accepted range', () => { - const memorableDateComponent = {} as Components.VaMemorableDate; - const year = 2000; - const month = 1; - const day = 35; - - validate(memorableDateComponent, year, month, day); - - expect(memorableDateComponent.error).toEqual('day-range'); - expect(memorableDateComponent.invalidYear).toEqual(false); - expect(memorableDateComponent.invalidMonth).toEqual(false); - expect(memorableDateComponent.invalidDay).toEqual(true); - }); - - it('indicates when the day is above range for non-leap years', () => { - const memorableDateComponent = { required: true} as Components.VaMemorableDate; - const year = 2023; - const month = 2; - const day = 29; + describe('NaN validation', () => { + it('indicates when the year is NaN', () => { + const memorableDateComponent = {} as Components.VaMemorableDate; + const year = NaN; + const month = 1; + const day = 1; + const yearTouched = true; - validate(memorableDateComponent, year, month, day); + validate({ component: memorableDateComponent, year, month, day, yearTouched }); - expect(memorableDateComponent.error).toEqual('day-range'); - expect(memorableDateComponent.invalidDay).toEqual(true); - }); + expect(memorableDateComponent.error).toEqual(`year-range`); + expect(memorableDateComponent.invalidYear).toEqual(true); + expect(memorableDateComponent.invalidMonth).toEqual(false); + expect(memorableDateComponent.invalidDay).toEqual(false); + }); - it('indicates when the day is below the accepted range', () => { - const memorableDateComponent = {} as Components.VaMemorableDate; - const year = 2000; - const month = 1; - const day = null; + it('indicates when the month is NaN', () => { + const memorableDateComponent = {} as Components.VaMemorableDate; + const year = Number('1999'); + const month = Number('1n'); + const day = Number('1'); + const monthTouched = true; - validate(memorableDateComponent, year, month, day); + validate({ component: memorableDateComponent, year, month, day, monthTouched }); - expect(memorableDateComponent.error).toEqual('day-range'); - expect(memorableDateComponent.invalidYear).toEqual(false); - expect(memorableDateComponent.invalidMonth).toEqual(false); - expect(memorableDateComponent.invalidDay).toEqual(true); - }); + expect(memorableDateComponent.error).toEqual(`month-range`); + expect(memorableDateComponent.invalidYear).toEqual(false); + expect(memorableDateComponent.invalidMonth).toEqual(true); + expect(memorableDateComponent.invalidDay).toEqual(false); + }); - it('does not validate day for the monthYearOnly variant', () => { - const memorableDateComponent = {} as Components.VaDate; - const year = 2000; - const month = 1; - const day = null; + it('indicates when the day is NaN', () => { + const memorableDateComponent = {} as Components.VaMemorableDate; + const year = Number('1999'); + const month = Number('1'); + const day = Number('1n'); + const dayTouched = true; - validate(memorableDateComponent, year, month, day, true); + validate({ component: memorableDateComponent, year, month, day, dayTouched }); - expect(memorableDateComponent.error).toEqual(null); - expect(memorableDateComponent.invalidYear).toEqual(false); - expect(memorableDateComponent.invalidMonth).toEqual(false); - expect(memorableDateComponent.invalidDay).toEqual(false); + expect(memorableDateComponent.error).toEqual(`day-range`); + expect(memorableDateComponent.invalidYear).toEqual(false); + expect(memorableDateComponent.invalidMonth).toEqual(false); + expect(memorableDateComponent.invalidDay).toEqual(true); + }); }); describe('required components', () => { @@ -123,8 +73,9 @@ describe('validate', () => { const year = null; const month = 1; const day = 1; + const yearTouched = true; - validate(memorableDateComponent, year, month, day); + validate({ component: memorableDateComponent, year, month, day, yearTouched} ); expect(memorableDateComponent.error).toEqual('date-error'); expect(memorableDateComponent.invalidYear).toEqual(true); @@ -137,8 +88,9 @@ describe('validate', () => { const year = 2000; const month = null; const day = 1; + const monthTouched = true; - validate(memorableDateComponent, year, month, day); + validate({ component: memorableDateComponent, year, month, day, monthTouched} ); expect(memorableDateComponent.error).toEqual('date-error'); expect(memorableDateComponent.invalidYear).toEqual(false); @@ -151,8 +103,9 @@ describe('validate', () => { const year = 2000; const month = 1; const day = null; + const dayTouched = true; - validate(memorableDateComponent, year, month, day); + validate({ component: memorableDateComponent, year, month, day, dayTouched} ); expect(memorableDateComponent.error).toEqual('date-error'); expect(memorableDateComponent.invalidYear).toEqual(false); @@ -165,8 +118,177 @@ describe('validate', () => { const year = 2000; const month = 1; const day = null; + const monthYearOnly = true; + const dayTouched = true; + + validate({ component: memorableDateComponent, year, month, day, monthYearOnly, dayTouched} ); + + expect(memorableDateComponent.error).toEqual(null); + expect(memorableDateComponent.invalidYear).toEqual(false); + expect(memorableDateComponent.invalidMonth).toEqual(false); + expect(memorableDateComponent.invalidDay).toEqual(false); + }); + }); + + describe('touched fields are validated against empty values', () => { + it('validates month when empty', () => { + const memorableDateComponent = {} as Components.VaMemorableDate; + const year = 1999; + const month = null; + const day = 1; + const monthTouched = true; + + validate({ component: memorableDateComponent, year, month, day, monthTouched }); + + expect(memorableDateComponent.error).toEqual(`month-range`); + expect(memorableDateComponent.invalidYear).toEqual(false); + expect(memorableDateComponent.invalidMonth).toEqual(true); + expect(memorableDateComponent.invalidDay).toEqual(false); + }); + + it('validates day when empty', () => { + const memorableDateComponent = {} as Components.VaMemorableDate; + const year = 1999; + const month = 1; + const day = null; + const dayTouched = true; + + validate({ component: memorableDateComponent, year, month, day, dayTouched }); + + expect(memorableDateComponent.error).toEqual(`day-range`); + expect(memorableDateComponent.invalidYear).toEqual(false); + expect(memorableDateComponent.invalidMonth).toEqual(false); + expect(memorableDateComponent.invalidDay).toEqual(true); + }); + + it('validates year when empty', () => { + const memorableDateComponent = {} as Components.VaMemorableDate; + const year = null; + const month = 1; + const day = 1; + const yearTouched = true; + + validate({ component: memorableDateComponent, year, month, day, yearTouched }); + + expect(memorableDateComponent.error).toEqual(`year-range`); + expect(memorableDateComponent.invalidYear).toEqual(true); + expect(memorableDateComponent.invalidMonth).toEqual(false); + expect(memorableDateComponent.invalidDay).toEqual(false); + }); + }); + + describe('validates when values are present', () => { + it('indicates when the year is below the accepted range', () => { + const memorableDateComponent = {} as Components.VaMemorableDate; + const year = 1500; + const month = 1; + const day = 1; + const yearTouched = true; + + validate({ component: memorableDateComponent, year, month, day, yearTouched} ); + + expect(memorableDateComponent.error).toEqual(`year-range`); + expect(memorableDateComponent.invalidYear).toEqual(true); + expect(memorableDateComponent.invalidMonth).toEqual(false); + expect(memorableDateComponent.invalidDay).toEqual(false); + }); + + it('indicates when the year is above the accepted range', () => { + const memorableDateComponent = {} as Components.VaMemorableDate; + const year = 3000; + const month = 1; + const day = 1; + const yearTouched = true; + + validate({ component: memorableDateComponent, year, month, day, yearTouched} ); + + expect(memorableDateComponent.error).toEqual(`year-range`); + expect(memorableDateComponent.invalidYear).toEqual(true); + expect(memorableDateComponent.invalidMonth).toEqual(false); + expect(memorableDateComponent.invalidDay).toEqual(false); + }); + + it('indicates when the month is above the accepted range', () => { + const memorableDateComponent = {} as Components.VaMemorableDate; + const year = 2000; + const month = 15; + const day = 1; + const monthTouched = true; + + validate({ component: memorableDateComponent, year, month, day, monthTouched} ); + + expect(memorableDateComponent.error).toEqual('month-range'); + expect(memorableDateComponent.invalidYear).toEqual(false); + expect(memorableDateComponent.invalidMonth).toEqual(true); + expect(memorableDateComponent.invalidDay).toEqual(false); + }); + + it('indicates when the month is below the accepted range', () => { + const memorableDateComponent = {} as Components.VaMemorableDate; + const year = 2000; + const month = 0; + const day = 1; + const monthTouched = true; + + validate({ component: memorableDateComponent, year, month, day, monthTouched} ); + + expect(memorableDateComponent.error).toEqual('month-range'); + expect(memorableDateComponent.invalidYear).toEqual(false); + expect(memorableDateComponent.invalidMonth).toEqual(true); + expect(memorableDateComponent.invalidDay).toEqual(false); + }); + + it('indicates when the day is above the accepted range', () => { + const memorableDateComponent = {} as Components.VaMemorableDate; + const year = 2000; + const month = 1; + const day = 35; + const dayTouched = true; + + validate({ component: memorableDateComponent, year, month, day, dayTouched} ); + + expect(memorableDateComponent.error).toEqual('day-range'); + expect(memorableDateComponent.invalidYear).toEqual(false); + expect(memorableDateComponent.invalidMonth).toEqual(false); + expect(memorableDateComponent.invalidDay).toEqual(true); + }); + + it('indicates when the day is above range for non-leap years', () => { + const memorableDateComponent = { required: true} as Components.VaMemorableDate; + const year = 2023; + const month = 2; + const day = 29; + const dayTouched = true; + + validate({ component: memorableDateComponent, year, month, day, dayTouched} ); + + expect(memorableDateComponent.error).toEqual('day-range'); + expect(memorableDateComponent.invalidDay).toEqual(true); + }); + + it('indicates when the day is below the accepted range', () => { + const memorableDateComponent = {} as Components.VaMemorableDate; + const year = 2000; + const month = 1; + const day = 0; + const dayTouched = true; - validate(memorableDateComponent, year, month, day, true); + validate({ component: memorableDateComponent, year, month, day, dayTouched} ); + + expect(memorableDateComponent.error).toEqual('day-range'); + expect(memorableDateComponent.invalidYear).toEqual(false); + expect(memorableDateComponent.invalidMonth).toEqual(false); + expect(memorableDateComponent.invalidDay).toEqual(true); + }); + + it('does not validate day for the monthYearOnly variant', () => { + const memorableDateComponent = {} as Components.VaDate; + const year = 2000; + const month = 1; + const day = 0; + const monthYearOnly = true + + validate({ component: memorableDateComponent, year, month, day, monthYearOnly} ); expect(memorableDateComponent.error).toEqual(null); expect(memorableDateComponent.invalidYear).toEqual(false); @@ -180,8 +302,11 @@ describe('validate', () => { const year = 2000; const month = 1; const day = 1; + const yearTouched = true; + const monthTouched = true; + const dayTouched = true; - validate(memorableDateComponent, year, month, day); + validate({ component: memorableDateComponent, year, month, day, yearTouched, monthTouched, dayTouched} ); expect(memorableDateComponent.error).toEqual(null); expect(memorableDateComponent.invalidYear).toEqual(false); @@ -194,8 +319,11 @@ describe('validate', () => { const year = 2000; const month = 1; const day = 1; + const yearTouched = true; + const monthTouched = true; + const dayTouched = true; - validate(memorableDateComponent, year, month, day); + validate({ component: memorableDateComponent, year, month, day, yearTouched, monthTouched, dayTouched} ); expect(memorableDateComponent.error).toEqual('Some error'); expect(memorableDateComponent.invalidYear).toEqual(false); @@ -203,19 +331,60 @@ describe('validate', () => { expect(memorableDateComponent.invalidDay).toEqual(false); }); - it('overrides an invalid day message with an invalid month message', () => { - const memorableDateComponent = {} as Components.VaMemorableDate; - const year = 2000; - const month = null; - const day = 500; + describe('errors on fields have precedence', () => { + it('overrides an invalid year message with an invalid month message', () => { + const memorableDateComponent = { error: 'year-error'} as Components.VaMemorableDate; + const year = 5000; + const month = null; + const day = 5; + const yearTouched = true; + const monthTouched = true; + const dayTouched = true; - validate(memorableDateComponent, year, month, day); + validate({ component: memorableDateComponent, year, month, day, yearTouched, monthTouched, dayTouched }); - expect(memorableDateComponent.error).toEqual('month-range'); - expect(memorableDateComponent.invalidYear).toEqual(false); - expect(memorableDateComponent.invalidMonth).toEqual(true); - // invalid month sets max days to zero - expect(memorableDateComponent.invalidDay).toEqual(true); + expect(memorableDateComponent.error).toEqual('month-range'); + expect(memorableDateComponent.invalidYear).toEqual(false); + expect(memorableDateComponent.invalidMonth).toEqual(true); + // invalid month sets max days to zero + expect(memorableDateComponent.invalidDay).toEqual(false); + }); + + it('overrides an invalid day message with an invalid month message', () => { + const memorableDateComponent = { error: 'day-error'} as Components.VaMemorableDate; + const year = 2000; + const month = null; + const day = 500; + const yearTouched = true; + const monthTouched = true; + const dayTouched = true; + + validate({ component: memorableDateComponent, year, month, day, yearTouched, monthTouched, dayTouched }); + + expect(memorableDateComponent.error).toEqual('month-range'); + expect(memorableDateComponent.invalidYear).toEqual(false); + expect(memorableDateComponent.invalidMonth).toEqual(true); + // invalid month sets max days to zero + expect(memorableDateComponent.invalidDay).toEqual(false); + }); + + it('overrides an invalid year message with an invalid day message', () => { + const memorableDateComponent = { error: 'day-error'} as Components.VaMemorableDate; + const year = 2000; + const month = 1; + const day = 500; + const yearTouched = true; + const monthTouched = true; + const dayTouched = true; + + validate({ component: memorableDateComponent, year, month, day, yearTouched, monthTouched, dayTouched }); + + expect(memorableDateComponent.error).toEqual('day-range'); + expect(memorableDateComponent.invalidYear).toEqual(false); + expect(memorableDateComponent.invalidMonth).toEqual(false); + // invalid month sets max days to zero + expect(memorableDateComponent.invalidDay).toEqual(true); + }); }); }); @@ -272,98 +441,6 @@ describe('isDateSameDay', () => { }); }) -describe('checkIsNaN', () => { - it('indicates when the year is NaN', () => { - const memorableDateComponent = {} as Components.VaMemorableDate; - const year = Number('1999n'); - const month = Number('1'); - const day = Number('1'); - - const result = checkIsNaN(memorableDateComponent, year, month, day); - - expect(result).toEqual(false); - expect(memorableDateComponent.error).toEqual(`year-range`); - expect(memorableDateComponent.invalidYear).toEqual(true); - expect(memorableDateComponent.invalidMonth).toEqual(false); - expect(memorableDateComponent.invalidDay).toEqual(false); - }); - - it('indicates when the month is NaN', () => { - const memorableDateComponent = {} as Components.VaMemorableDate; - const year = Number('1999'); - const month = Number('1n'); - const day = Number('1'); - - const result = checkIsNaN(memorableDateComponent, year, month, day); - - expect(result).toEqual(false); - expect(memorableDateComponent.error).toEqual(`month-range`); - expect(memorableDateComponent.invalidYear).toEqual(false); - expect(memorableDateComponent.invalidMonth).toEqual(true); - expect(memorableDateComponent.invalidDay).toEqual(false); - }); - - it('indicates when the day is NaN', () => { - const memorableDateComponent = {} as Components.VaMemorableDate; - const year = Number('1999'); - const month = Number('1'); - const day = Number('1n'); - - const result = checkIsNaN(memorableDateComponent, year, month, day); - - expect(result).toEqual(false); - expect(memorableDateComponent.error).toEqual(`day-range`); - expect(memorableDateComponent.invalidYear).toEqual(false); - expect(memorableDateComponent.invalidMonth).toEqual(false); - expect(memorableDateComponent.invalidDay).toEqual(true); - }); - - it('passes when the all inputs are number', () => { - const memorableDateComponent = {} as Components.VaMemorableDate; - const year = Number('1999'); - const month = Number('1'); - const day = Number('1'); - - const result = checkIsNaN(memorableDateComponent, year, month, day); - - expect(result).toEqual(true); - expect(memorableDateComponent.error).toEqual(null); - expect(memorableDateComponent.invalidYear).toEqual(false); - expect(memorableDateComponent.invalidMonth).toEqual(false); - expect(memorableDateComponent.invalidDay).toEqual(false); - }); - - it('removes error indicators when the values are valid', () => { - const memorableDateComponent = { error: 'date-error'} as Components.VaMemorableDate; - const year = Number('1999'); - const month = Number('1'); - const day = Number('1'); - - const result = checkIsNaN(memorableDateComponent, year, month, day); - - expect(result).toEqual(true); - expect(memorableDateComponent.error).toEqual(null); - expect(memorableDateComponent.invalidYear).toEqual(false); - expect(memorableDateComponent.invalidMonth).toEqual(false); - expect(memorableDateComponent.invalidDay).toEqual(false); - }); - - it('should not remove custom error even if values are valid', () => { - const memorableDateComponent = { error: 'Some error'} as Components.VaMemorableDate; - const year = Number('1999'); - const month = Number('1'); - const day = Number('1'); - - const result = checkIsNaN(memorableDateComponent, year, month, day); - - expect(result).toEqual(true); - expect(memorableDateComponent.error).toEqual('Some error'); - expect(memorableDateComponent.invalidYear).toEqual(false); - expect(memorableDateComponent.invalidMonth).toEqual(false); - expect(memorableDateComponent.invalidDay).toEqual(false); - }); -}); - describe('zeroPadStart', () => { it('should return "00"', () => { expect(zeroPadStart(null)).toEqual('00'); diff --git a/packages/web-components/src/utils/date-utils.ts b/packages/web-components/src/utils/date-utils.ts index 20e3b61a9..54cfc7bba 100644 --- a/packages/web-components/src/utils/date-utils.ts +++ b/packages/web-components/src/utils/date-utils.ts @@ -14,6 +14,7 @@ const maxYear = new Date().getFullYear() + 100; const minYear = 1900; const maxMonths = 12; const minMonths = 1; +const minDays = 1; export const months = [ { label: 'January', value: 1 }, @@ -159,126 +160,110 @@ export const internalErrors = [ 'date-error' ]; -/** - * Checks if any of the of inputs provided resolve to NaN - * Returns false if any are NaN and highlights component as error - */ -export function checkIsNaN( +interface ValidateConfig { component: Components.VaDate | Components.VaMemorableDate, year: number, month: number, day: number, - monthYearOnly : boolean = false) : boolean { + monthYearOnly?: boolean, + yearTouched?: boolean, + monthTouched?: boolean, + dayTouched?: boolean, +} - // Check for nulls first, so that field specific errors do not get overwritten - if (component.required && (!year || !month || (!monthYearOnly && !day))) { - component.invalidYear = !year; - component.invalidMonth = !month; - component.invalidDay = monthYearOnly ? false : !day; - component.error = 'date-error'; - } - // Begin NaN validation. - if (isNaN(year)) { - component.invalidYear = true; - component.error = 'year-range'; - } - else { - component.invalidYear = false; +export function validate({ + component, + year, + month, + day, + monthYearOnly, + yearTouched, + monthTouched, + dayTouched + }: ValidateConfig): void { + + const maxDays = daysForSelectedMonth(year, month); + + // Reset previous invalid states + component.invalidYear = false; + component.invalidMonth = false; + component.invalidDay = false; + + // Check NaN and set errors based on NaN values + if (isNaN(month) && monthTouched) { + component.invalidMonth = true; + component.error = 'month-range'; + return; } - - if (!monthYearOnly && isNaN(day)) { + if (!monthYearOnly && isNaN(day) && dayTouched) { component.invalidDay = true; component.error = 'day-range'; + return; } - else { - component.invalidDay = false; - } - - if (isNaN(month) || month < 1) { - component.invalidMonth = true; - component.error = 'month-range'; - } - else { - component.invalidMonth = false; + if (isNaN(year) && yearTouched) { + component.invalidYear = true; + component.error = 'year-range'; + return; } - - // Remove any error message if none of the fields are NaN - if ( - !component.invalidYear && - !component.invalidMonth && - !component.invalidDay - ) { - if (!component.error || internalErrors.includes(component.error)) { - component.error = null; + // Validate required fields + if (component.required && (!year || !month || (!monthYearOnly && !day))) { + if (monthTouched && !month) { + component.invalidMonth = true; + component.error = 'date-error'; + return; + } + else if (dayTouched && !day && !monthYearOnly) { + component.invalidDay = true; + component.error = 'date-error'; + return; + } + else if (yearTouched && !year) { + component.invalidYear = true; + component.error = 'date-error'; + return; } - return true; } - return false; -} - -/** - * This is used to validate date components and: - * 1. Indicate which field fails the built-in validation - * 1. Supply an error message to help resolve the issue - * - * It relies on the component's mutable props. - */ -export function validate( - component: Components.VaDate | Components.VaMemorableDate, - year: number, - month: number, - day: number, - monthYearOnly : boolean = false) : void { - const maxDay = daysForSelectedMonth(year, month); - if (component.required && (!year || !month || (!monthYearOnly && !day))) { - component.invalidYear = (!year || year < minYear || year > maxYear); - component.invalidMonth = (!month || month < minMonths || month > maxMonths); - component.invalidDay = monthYearOnly ? false : (!day || day < minMonths || day > maxDay); - component.error = 'date-error'; + // Check for empty values after the fields are touched + if (!month && monthTouched) { + component.invalidMonth = true; + component.error = 'month-range'; return; } - - // Begin built-in validation. - // Empty fields are acceptable unless the component is marked as required - if (year && (year < minYear || year > maxYear)) { - component.invalidYear = true; - component.error = 'year-range'; - } - else { - component.invalidYear = false; - } - - // Check day before month so that the month error message has a change to override - // We don't know the upper limit on days until we know the month - if (!monthYearOnly && (day < minMonths || day > maxDay)) { + if (!day && !monthYearOnly && dayTouched) { component.invalidDay = true; component.error = 'day-range'; + return; } - else { - component.invalidDay = false; + if (!year && yearTouched) { + component.invalidYear = true; + component.error = 'year-range'; + return; } - // The month error message will trigger if the month is outside of the acceptable range, - // but also if the day is invalid and there isn't a month value. - if ((month && (month < minMonths || month > maxMonths)) || - (!month && component.invalidDay)) { + // Validate year, month, and day ranges if they have a value regardless of whether they are required + if (month && (month < minMonths || month > maxMonths) && monthTouched) { component.invalidMonth = true; component.error = 'month-range'; + return; } - else { - component.invalidMonth = false; + if (day && !monthYearOnly && (day < minDays || day > maxDays) && dayTouched) { + component.invalidDay = true; + component.error = 'day-range'; + return; + } + if (year && (year < minYear || year > maxYear) && yearTouched) { + component.invalidYear = true; + component.error = 'year-range'; + return; } - // Remove any error message if none of the fields are marked as invalid - if ( - (!component.error || internalErrors.includes(component.error)) && - !component.invalidYear && - !component.invalidMonth && - !component.invalidDay - ) { - component.error = null; + // Remove any error message if none of the fields are invalid + if (!component.invalidYear && !component.invalidMonth && !component.invalidDay) { + if (!component.error || internalErrors.includes(component.error)) { + component.error = null; + } } }