From a80b20f1bbe7cd1806df29e675e0c9c528c801d6 Mon Sep 17 00:00:00 2001 From: Leo Singer Date: Mon, 22 Jan 2024 13:16:14 -0500 Subject: [PATCH 1/3] feat: add DateSelector dateFormat prop --- src/components/forms/DatePicker/DatePicker.tsx | 16 ++++++++-------- src/components/forms/DatePicker/constants.ts | 3 +++ src/components/forms/DatePicker/utils.tsx | 14 +++++++++----- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/components/forms/DatePicker/DatePicker.tsx b/src/components/forms/DatePicker/DatePicker.tsx index 411c1a18d7..26adb8c360 100644 --- a/src/components/forms/DatePicker/DatePicker.tsx +++ b/src/components/forms/DatePicker/DatePicker.tsx @@ -12,6 +12,7 @@ import { DEFAULT_EXTERNAL_DATE_FORMAT, VALIDATION_MESSAGE, DEFAULT_MIN_DATE, + type DateFormat, } from './constants' import { DatePickerLocalization, EN_US } from './i18n' import { @@ -33,6 +34,7 @@ type BaseDatePickerProps = { validationStatus?: ValidationStatus disabled?: boolean required?: boolean + dateFormat?: DateFormat defaultValue?: string minDate?: string maxDate?: string @@ -57,6 +59,7 @@ export const DatePicker = ({ name, className, validationStatus, + dateFormat = DEFAULT_EXTERNAL_DATE_FORMAT, defaultValue, disabled, required, @@ -68,6 +71,8 @@ export const DatePicker = ({ i18n = EN_US, ...inputProps }: DatePickerProps): React.ReactElement => { + dateFormat ??= DEFAULT_EXTERNAL_DATE_FORMAT + const datePickerEl = useRef(null) const externalInputEl = useRef(null) @@ -108,8 +113,7 @@ export const DatePicker = ({ const handleSelectDate = (dateString: string, closeCalendar = true): void => { const parsedValue = parseDateString(dateString) - const formattedValue = - parsedValue && formatDate(parsedValue, DEFAULT_EXTERNAL_DATE_FORMAT) + const formattedValue = parsedValue && formatDate(parsedValue, dateFormat) if (parsedValue) setInternalValue(dateString) if (formattedValue) setExternalValue(formattedValue) @@ -128,7 +132,7 @@ export const DatePicker = ({ setExternalValue(value) if (onChange) onChange(value) - const inputDate = parseDateString(value, DEFAULT_EXTERNAL_DATE_FORMAT, true) + const inputDate = parseDateString(value, dateFormat, true) let newValue = '' if (inputDate && !isDateInvalid(value, parsedMinDate, parsedMaxDate)) { newValue = formatDate(inputDate) @@ -179,11 +183,7 @@ export const DatePicker = ({ setStatuses([]) } else { // calendar is closed, show it - const inputDate = parseDateString( - externalValue, - DEFAULT_EXTERNAL_DATE_FORMAT, - true - ) + const inputDate = parseDateString(externalValue, dateFormat, true) const displayDate = keepDateBetweenMinAndMax( inputDate || (defaultValue && parseDateString(defaultValue)) || today(), diff --git a/src/components/forms/DatePicker/constants.ts b/src/components/forms/DatePicker/constants.ts index 3dd7211a54..7ea31ac49f 100644 --- a/src/components/forms/DatePicker/constants.ts +++ b/src/components/forms/DatePicker/constants.ts @@ -34,3 +34,6 @@ export const YEAR_CHUNK = 12 export const DEFAULT_MIN_DATE = '0000-01-01' export const DEFAULT_EXTERNAL_DATE_FORMAT = 'MM/DD/YYYY' export const INTERNAL_DATE_FORMAT = 'YYYY-MM-DD' +export type DateFormat = + | typeof INTERNAL_DATE_FORMAT + | typeof DEFAULT_EXTERNAL_DATE_FORMAT diff --git a/src/components/forms/DatePicker/utils.tsx b/src/components/forms/DatePicker/utils.tsx index 028b874cc6..a4629ef310 100644 --- a/src/components/forms/DatePicker/utils.tsx +++ b/src/components/forms/DatePicker/utils.tsx @@ -1,6 +1,10 @@ import React, { KeyboardEvent } from 'react' -import { DEFAULT_EXTERNAL_DATE_FORMAT, INTERNAL_DATE_FORMAT } from './constants' +import { + type DateFormat, + DEFAULT_EXTERNAL_DATE_FORMAT, + INTERNAL_DATE_FORMAT, +} from './constants' /** * This file contains the USWDS DatePicker date manipulation functions converted to TypeScript @@ -355,13 +359,13 @@ export const isDatesYearOutsideMinOrMax = ( * Parse a date with format M-D-YY * * @param {string} dateString the date string to parse - * @param {string} dateFormat the format of the date string + * @param {DateFormat} dateFormat the format of the date string * @param {boolean} adjustDate should the date be adjusted * @returns {Date} the parsed date */ export const parseDateString = ( dateString: string, - dateFormat: string = INTERNAL_DATE_FORMAT, + dateFormat: DateFormat = INTERNAL_DATE_FORMAT, adjustDate = false ): Date | undefined => { let date @@ -430,12 +434,12 @@ export const parseDateString = ( * Format a date to format YYYY-MM-DD * * @param {Date} date the date to format - * @param {string} dateFormat the format of the date string + * @param {DateFormat} dateFormat the format of the date string * @returns {string} the formatted date string */ export const formatDate = ( date: Date, - dateFormat: string = INTERNAL_DATE_FORMAT + dateFormat: DateFormat = INTERNAL_DATE_FORMAT ): string => { const padZeros = (value: number, length: number): string => { return `0000${value}`.slice(-length) From 5b9182c733a3c7e9b8ee2c89f4c440095fa3e28f Mon Sep 17 00:00:00 2001 From: Andrew Nelson Date: Wed, 7 Feb 2024 14:25:48 -0800 Subject: [PATCH 2/3] added story --- .../forms/DatePicker/DatePicker.stories.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/components/forms/DatePicker/DatePicker.stories.tsx b/src/components/forms/DatePicker/DatePicker.stories.tsx index 91e318206b..d2316865d8 100644 --- a/src/components/forms/DatePicker/DatePicker.stories.tsx +++ b/src/components/forms/DatePicker/DatePicker.stories.tsx @@ -7,11 +7,23 @@ import { FormGroup } from '../FormGroup/FormGroup' import { Label } from '../Label/Label' import { TextInput } from '../TextInput/TextInput' import { ValidationStatus } from '../../../types/validationStatus' +import { + DEFAULT_EXTERNAL_DATE_FORMAT, + DateFormat, + INTERNAL_DATE_FORMAT, +} from './constants' export default { title: 'Components/Date picker', component: DatePicker, argTypes: { + dateFormat: { + control: 'radio', + options: [ + DEFAULT_EXTERNAL_DATE_FORMAT as DateFormat, + INTERNAL_DATE_FORMAT as DateFormat, + ], + }, onSubmit: { action: 'submitted' }, disabled: { control: { type: 'boolean' } }, validationStatus: { @@ -50,6 +62,7 @@ We may find that we want to expose props for custom event handlers or even a ref } type StorybookArguments = { + dateFormat: DateFormat onSubmit: React.FormEventHandler disabled?: boolean validationStatus?: ValidationStatus @@ -66,7 +79,7 @@ export const CompleteDatePicker = { Appointment date
- mm/dd/yyyy + {argTypes.dateFormat ?? DEFAULT_EXTERNAL_DATE_FORMAT}
From 821c09bd36ee34998c0d1fbb63311f178a42d2e9 Mon Sep 17 00:00:00 2001 From: Leo Singer Date: Sun, 26 May 2024 21:32:04 -0400 Subject: [PATCH 3/3] Handle validation for YYYY-MM-DD format --- .../forms/DatePicker/DatePicker.tsx | 12 ++- src/components/forms/DatePicker/utils.test.ts | 79 +++++++++++++------ src/components/forms/DatePicker/utils.tsx | 18 ++++- 3 files changed, 79 insertions(+), 30 deletions(-) diff --git a/src/components/forms/DatePicker/DatePicker.tsx b/src/components/forms/DatePicker/DatePicker.tsx index 26adb8c360..f8a62f5f61 100644 --- a/src/components/forms/DatePicker/DatePicker.tsx +++ b/src/components/forms/DatePicker/DatePicker.tsx @@ -97,7 +97,12 @@ export const DatePicker = ({ const parsedRangeDate = rangeDate ? parseDateString(rangeDate) : undefined const validateInput = (): void => { - const isInvalid = isDateInvalid(externalValue, parsedMinDate, parsedMaxDate) + const isInvalid = isDateInvalid( + externalValue, + dateFormat, + parsedMinDate, + parsedMaxDate + ) if (isInvalid && !externalInputEl?.current?.validationMessage) { externalInputEl?.current?.setCustomValidity(VALIDATION_MESSAGE) @@ -134,7 +139,10 @@ export const DatePicker = ({ const inputDate = parseDateString(value, dateFormat, true) let newValue = '' - if (inputDate && !isDateInvalid(value, parsedMinDate, parsedMaxDate)) { + if ( + inputDate && + !isDateInvalid(value, dateFormat, parsedMinDate, parsedMaxDate) + ) { newValue = formatDate(inputDate) } diff --git a/src/components/forms/DatePicker/utils.test.ts b/src/components/forms/DatePicker/utils.test.ts index 8b7ee8eefc..c2f05c3ba0 100644 --- a/src/components/forms/DatePicker/utils.test.ts +++ b/src/components/forms/DatePicker/utils.test.ts @@ -81,34 +81,63 @@ describe('formatDate', () => { }) describe('isDateInvalid', () => { - it('returns false if the date is within the min & max', () => { - const testMin = new Date('May 1, 1988') - const testMax = new Date('June 1, 1988') - expect(isDateInvalid('05/16/1988', testMin, testMax)).toEqual(false) - }) - - it('returns true if the date is not within the min & max', () => { - const testMin = new Date('May 1, 1988') - const testMax = new Date('June 1, 1988') - expect(isDateInvalid('08/16/1988', testMin, testMax)).toEqual(true) - }) - - it('returns true if the date is not valid', () => { - const testMin = new Date('May 1, 1988') - const testMax = new Date('June 1, 1988') - expect(isDateInvalid('not a date', testMin, testMax)).toEqual(true) - }) + it.each([ + ['05/16/1988', DEFAULT_EXTERNAL_DATE_FORMAT], + ['1988-05-16', INTERNAL_DATE_FORMAT], + ] as const)( + 'returns false if the date is within the min & max', + (date, format) => { + const testMin = new Date('May 1, 1988') + const testMax = new Date('June 1, 1988') + expect(isDateInvalid(date, format, testMin, testMax)).toEqual(false) + } + ) + + it.each([ + ['08/16/1988', DEFAULT_EXTERNAL_DATE_FORMAT], + ['1988-08-16', INTERNAL_DATE_FORMAT], + ] as const)( + 'returns true if the date is not within the min & max', + (date, format) => { + const testMin = new Date('May 1, 1988') + const testMax = new Date('June 1, 1988') + expect(isDateInvalid(date, format, testMin, testMax)).toEqual(true) + } + ) + + it.each([DEFAULT_EXTERNAL_DATE_FORMAT, INTERNAL_DATE_FORMAT] as const)( + 'returns true if the date is not valid', + (format) => { + const testMin = new Date('May 1, 1988') + const testMax = new Date('June 1, 1988') + expect(isDateInvalid('not a date', format, testMin, testMax)).toEqual( + true + ) + } + ) describe('with no max date', () => { - it('returns false if the date is after the min', () => { - const testMin = new Date('May 1, 1988') - expect(isDateInvalid('05/16/1988', testMin)).toEqual(false) - }) + it.each([ + ['05/16/1988', DEFAULT_EXTERNAL_DATE_FORMAT], + ['1988-05-16', INTERNAL_DATE_FORMAT], + ] as const)( + 'returns false if the date is after the min', + (date, format) => { + const testMin = new Date('May 1, 1988') + expect(isDateInvalid(date, format, testMin)).toEqual(false) + } + ) - it('returns true if the date is not after the min', () => { - const testMin = new Date('May 1, 1988') - expect(isDateInvalid('02/16/1988', testMin)).toEqual(true) - }) + it.each([ + ['02/16/1988', DEFAULT_EXTERNAL_DATE_FORMAT], + ['1988-02-16', INTERNAL_DATE_FORMAT], + ] as const)( + 'returns true if the date is not after the min', + (date, format) => { + const testMin = new Date('May 1, 1988') + expect(isDateInvalid(date, format, testMin)).toEqual(true) + } + ) }) }) diff --git a/src/components/forms/DatePicker/utils.tsx b/src/components/forms/DatePicker/utils.tsx index a4629ef310..a4e4ad328c 100644 --- a/src/components/forms/DatePicker/utils.tsx +++ b/src/components/forms/DatePicker/utils.tsx @@ -460,6 +460,7 @@ export const formatDate = ( export const isDateInvalid = ( dateString: string, + dateFormat: DateFormat, minDate: Date, maxDate?: Date ): boolean => { @@ -468,14 +469,25 @@ export const isDateInvalid = ( if (dateString) { isInvalid = true - const dateStringParts = dateString.split('/') - const [month, day, year] = dateStringParts.map((str) => { + const dateStringParts = dateString.split( + dateFormat === DEFAULT_EXTERNAL_DATE_FORMAT ? '/' : '-' + ) + const dateParts = dateStringParts.map((str) => { let value const parsed = parseInt(str, 10) if (!Number.isNaN(parsed)) value = parsed return value }) + let month, day, year, yearStringPart + if (dateFormat === DEFAULT_EXTERNAL_DATE_FORMAT) { + yearStringPart = dateStringParts[2] + ;[month, day, year] = dateParts + } else { + yearStringPart = dateStringParts[0] + ;[year, month, day] = dateParts + } + if (month && day && year != null) { const checkDate = setDate(year, month - 1, day) @@ -483,7 +495,7 @@ export const isDateInvalid = ( checkDate.getMonth() === month - 1 && checkDate.getDate() === day && checkDate.getFullYear() === year && - dateStringParts[2].length === 4 && + yearStringPart.length === 4 && isDateWithinMinAndMax(checkDate, minDate, maxDate) ) { isInvalid = false