From 87fa353a3ad9f3cd19c0f2a47f0173209cc44ebc Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Sun, 14 Dec 2025 18:34:10 -0500 Subject: [PATCH 01/10] feat(date-utils): add newUTCFromTimeZone function to create UTC dates from specified time zones --- packages/date-utils/src/index.ts | 1 + .../src/newUTCFromTimeZone/index.ts | 1 + .../newUTCFromTimeZone.spec.ts | 54 ++ .../newUTCFromTimeZone/newUTCFromTimeZone.ts | 50 ++ .../convert12hTo24h/convert12hTo24h.spec.ts | 31 + .../utils/convert12hTo24h/convert12hTo24h.ts | 36 ++ .../doesSomeSegmentExist.spec.ts | 21 + .../doesSomeSegmentExist.ts | 12 + .../findUnitOptionByDayPeriod.spec.ts | 18 + .../findUnitOptionByDayPeriod.ts | 18 + .../getFormattedTimeSegments.spec.ts | 42 ++ .../getFormattedTimeSegments.ts | 28 + .../getFormattedTimeSegmentsFromDate.spec.ts | 16 + .../getFormattedTimeSegmentsFromDate.ts | 33 + .../getNewUTCDateFromSegments.spec.ts | 595 ++++++++++++++++++ .../getNewUTCDateFromSegments.ts | 71 +++ packages/time-input/src/utils/index.ts | 11 + .../isEverySegmentFilled.spec.ts | 21 + .../isEverySegmentFilled.ts | 19 + .../isEverySegmentValid.spec.ts | 173 +++++ .../isEverySegmentValid.ts | 36 ++ .../isEverySegmentValueExplicit.spec.ts | 160 +++++ .../isEverySegmentValueExplicit.ts | 32 + .../isSameUTCDayAndTime.spec.ts | 27 + .../isSameUTCDayAndTime.ts | 25 + .../shouldSetValue/shouldSetValue.spec.ts | 158 +++++ .../utils/shouldSetValue/shouldSetValue.ts | 49 ++ 27 files changed, 1738 insertions(+) create mode 100644 packages/date-utils/src/newUTCFromTimeZone/index.ts create mode 100644 packages/date-utils/src/newUTCFromTimeZone/newUTCFromTimeZone.spec.ts create mode 100644 packages/date-utils/src/newUTCFromTimeZone/newUTCFromTimeZone.ts create mode 100644 packages/time-input/src/utils/convert12hTo24h/convert12hTo24h.spec.ts create mode 100644 packages/time-input/src/utils/convert12hTo24h/convert12hTo24h.ts create mode 100644 packages/time-input/src/utils/doesSomeSegmentExist/doesSomeSegmentExist.spec.ts create mode 100644 packages/time-input/src/utils/doesSomeSegmentExist/doesSomeSegmentExist.ts create mode 100644 packages/time-input/src/utils/findUnitOptionByDayPeriod/findUnitOptionByDayPeriod.spec.ts create mode 100644 packages/time-input/src/utils/findUnitOptionByDayPeriod/findUnitOptionByDayPeriod.ts create mode 100644 packages/time-input/src/utils/getFormattedTimeSegments/getFormattedTimeSegments.spec.ts create mode 100644 packages/time-input/src/utils/getFormattedTimeSegments/getFormattedTimeSegments.ts create mode 100644 packages/time-input/src/utils/getFormattedTimeSegmentsFromDate/getFormattedTimeSegmentsFromDate.spec.ts create mode 100644 packages/time-input/src/utils/getFormattedTimeSegmentsFromDate/getFormattedTimeSegmentsFromDate.ts create mode 100644 packages/time-input/src/utils/getNewUTCDateFromSegments/getNewUTCDateFromSegments.spec.ts create mode 100644 packages/time-input/src/utils/getNewUTCDateFromSegments/getNewUTCDateFromSegments.ts create mode 100644 packages/time-input/src/utils/isEverySegmentFilled/isEverySegmentFilled.spec.ts create mode 100644 packages/time-input/src/utils/isEverySegmentFilled/isEverySegmentFilled.ts create mode 100644 packages/time-input/src/utils/isEverySegmentValid/isEverySegmentValid.spec.ts create mode 100644 packages/time-input/src/utils/isEverySegmentValid/isEverySegmentValid.ts create mode 100644 packages/time-input/src/utils/isEverySegmentValueExplicit/isEverySegmentValueExplicit.spec.ts create mode 100644 packages/time-input/src/utils/isEverySegmentValueExplicit/isEverySegmentValueExplicit.ts create mode 100644 packages/time-input/src/utils/isSameUTCDayAndTime/isSameUTCDayAndTime.spec.ts create mode 100644 packages/time-input/src/utils/isSameUTCDayAndTime/isSameUTCDayAndTime.ts create mode 100644 packages/time-input/src/utils/shouldSetValue/shouldSetValue.spec.ts create mode 100644 packages/time-input/src/utils/shouldSetValue/shouldSetValue.ts diff --git a/packages/date-utils/src/index.ts b/packages/date-utils/src/index.ts index 57c6a0eaeb..124cbc1d5f 100644 --- a/packages/date-utils/src/index.ts +++ b/packages/date-utils/src/index.ts @@ -31,6 +31,7 @@ export { maxDate } from './maxDate'; export { minDate } from './minDate'; export { newTZDate } from './newTZDate'; export { newUTC } from './newUTC'; +export { newUTCFromTimeZone } from './newUTCFromTimeZone'; export { setToUTCMidnight } from './setToUTCMidnight'; export { setUTCDate } from './setUTCDate'; export { setUTCMonth } from './setUTCMonth'; diff --git a/packages/date-utils/src/newUTCFromTimeZone/index.ts b/packages/date-utils/src/newUTCFromTimeZone/index.ts new file mode 100644 index 0000000000..6e1f0a30fd --- /dev/null +++ b/packages/date-utils/src/newUTCFromTimeZone/index.ts @@ -0,0 +1 @@ +export { newUTCFromTimeZone } from './newUTCFromTimeZone'; diff --git a/packages/date-utils/src/newUTCFromTimeZone/newUTCFromTimeZone.spec.ts b/packages/date-utils/src/newUTCFromTimeZone/newUTCFromTimeZone.spec.ts new file mode 100644 index 0000000000..7a1e9a0f24 --- /dev/null +++ b/packages/date-utils/src/newUTCFromTimeZone/newUTCFromTimeZone.spec.ts @@ -0,0 +1,54 @@ +import { newUTCFromTimeZone } from './newUTCFromTimeZone'; + +describe('packages/date-utils/newUTCFromTimeZone', () => { + describe('UTC', () => { + test('creates a new UTC date from a given time zone', () => { + const date = newUTCFromTimeZone({ + year: '2026', + month: '02', + day: '20', + hour: '23', + minute: '00', + second: '00', + timeZone: 'UTC', + }); + + // February 20, 2026 11:00:00 PM/23:00:00 in UTC is February 20, 2026 23:00:00 UTC + expect(date).toEqual(new Date('2026-02-20T23:00:00Z')); + }); + }); + + describe('America/New_York', () => { + test('creates a new UTC date from a given time zone', () => { + const date = newUTCFromTimeZone({ + year: '2026', + month: '02', + day: '20', + hour: '23', + minute: '00', + second: '00', + timeZone: 'America/New_York', + }); + + // February 20, 2026 11:00:00 PM/23:00:00 in America/New_York is February 21, 2026 04:00:00 UTC (UTC-5 hours) + expect(date).toEqual(new Date('2026-02-21T04:00:00Z')); + }); + }); + + describe('Pacific/Kiritimati', () => { + test('creates a new UTC date from a given time zone', () => { + const date = newUTCFromTimeZone({ + year: '2026', + month: '02', + day: '20', + hour: '23', + minute: '00', + second: '00', + timeZone: 'Pacific/Kiritimati', + }); + + // February 20, 2026 11:00:00 PM/23:00:00 in Pacific/Kiritimati is February 20, 2026 09:00:00 UTC (UTC+14 hours) + expect(date).toEqual(new Date('2026-02-20T09:00:00Z')); + }); + }); +}); diff --git a/packages/date-utils/src/newUTCFromTimeZone/newUTCFromTimeZone.ts b/packages/date-utils/src/newUTCFromTimeZone/newUTCFromTimeZone.ts new file mode 100644 index 0000000000..f1f722b8aa --- /dev/null +++ b/packages/date-utils/src/newUTCFromTimeZone/newUTCFromTimeZone.ts @@ -0,0 +1,50 @@ +import { zonedTimeToUtc } from 'date-fns-tz'; + +/** + * Creates a new UTC date from a given time zone. + * This takes the local date created above and converts it to UTC using the `zonedTimeToUtc` helper function. + * + * @param year - The year + * @param month - The month (1-12) + * @param day - The day + * @param hour - The hour in 24 hour format + * @param minute - The minute + * @param second - The second + * @param timeZone - The time zone + * @returns The new UTC date + * + * @example + * ```js + * // February 20, 2026 11:00:00 PM/23:00:00 in America/New_York is February 21, 2026 04:00:00 UTC + * newUTCFromTimeZone({ year: '2026', month: '02', day: '20', hour: '11', minute: '00', second: '00', timeZone: 'America/New_York' }); + * // returns new Date('2026-02-21T04:00:00Z') + * ``` + */ +export const newUTCFromTimeZone = ({ + year, + month, + day, + hour, + minute, + second, + timeZone, +}: { + year: string; + month: string; + day: string; + hour: string; + minute: string; + second: string; + timeZone: string; +}) => { + const newDate = new Date( + Number(year), + Number(month) - 1, + Number(day), + Number(hour), + Number(minute), + Number(second), + ); + + return zonedTimeToUtc(newDate, timeZone); +}; diff --git a/packages/time-input/src/utils/convert12hTo24h/convert12hTo24h.spec.ts b/packages/time-input/src/utils/convert12hTo24h/convert12hTo24h.spec.ts new file mode 100644 index 0000000000..4df48899cb --- /dev/null +++ b/packages/time-input/src/utils/convert12hTo24h/convert12hTo24h.spec.ts @@ -0,0 +1,31 @@ +import range from 'lodash/range'; + +import { convert12hTo24h } from './convert12hTo24h'; + +describe('convert12hTo24h', () => { + describe('AM', () => { + test('12 AM converts to 0', () => { + expect(convert12hTo24h('12', 'AM')).toEqual('0'); + }); + + test.each(range(1, 12).map(i => [i, i]))( + '%i AM converts to %i', + (input, expected) => { + expect(convert12hTo24h(`${input}`, 'AM')).toEqual(`${expected}`); + }, + ); + }); + + describe('PM', () => { + test('12 PM converts to 12', () => { + expect(convert12hTo24h('12', 'PM')).toEqual('12'); + }); + + test.each(range(1, 12).map(i => [i, i + 12]))( + '%i PM converts to %i', + (input, expected) => { + expect(convert12hTo24h(`${input}`, 'PM')).toEqual(`${expected}`); + }, + ); + }); +}); diff --git a/packages/time-input/src/utils/convert12hTo24h/convert12hTo24h.ts b/packages/time-input/src/utils/convert12hTo24h/convert12hTo24h.ts new file mode 100644 index 0000000000..53acc9f8ec --- /dev/null +++ b/packages/time-input/src/utils/convert12hTo24h/convert12hTo24h.ts @@ -0,0 +1,36 @@ +/** + * Converts a 12 hour format hour to a 24 hour format hour + * + * @example + * ```js + * convert12hTo24h('12', 'AM'); // '0' + * convert12hTo24h('12', 'PM'); // '12' + * convert12hTo24h('1', 'AM'); // '1' + * convert12hTo24h('1', 'PM'); // '13' + * ``` + * + * @param hour - The hour to convert + * @param dayPeriod - The day period to use for the conversion (AM or PM) + * @returns The converted hour + */ +export const convert12hTo24h = (hour: string, dayPeriod: string) => { + if (hour === '') return hour; + + // if dayPeriod is AM and hour is 12, return 0 since 12 AM is 00:00 + if (dayPeriod === 'AM') { + if (hour === '12') { + return '0'; + } + + // else return hour as-is + return hour; + } + + // if dayPeriod is PM and hour is 12, return 12 since 12 PM is 12:00 + if (hour === '12') { + return '12'; + } + + // else return hour + 12 + return `${parseInt(hour) + 12}`; +}; diff --git a/packages/time-input/src/utils/doesSomeSegmentExist/doesSomeSegmentExist.spec.ts b/packages/time-input/src/utils/doesSomeSegmentExist/doesSomeSegmentExist.spec.ts new file mode 100644 index 0000000000..0b71109f63 --- /dev/null +++ b/packages/time-input/src/utils/doesSomeSegmentExist/doesSomeSegmentExist.spec.ts @@ -0,0 +1,21 @@ +import { doesSomeSegmentExist } from './doesSomeSegmentExist'; + +describe('doesSomeSegmentExist', () => { + test('returns true if at least one segment is filled', () => { + expect(doesSomeSegmentExist({ hour: '', minute: '', second: '00' })).toBe( + true, + ); + }); + + test('returns true if at all segments are filled', () => { + expect( + doesSomeSegmentExist({ hour: '12', minute: '00', second: '00' }), + ).toBe(true); + }); + + test('returns false if no segments are filled', () => { + expect(doesSomeSegmentExist({ hour: '', minute: '', second: '' })).toBe( + false, + ); + }); +}); diff --git a/packages/time-input/src/utils/doesSomeSegmentExist/doesSomeSegmentExist.ts b/packages/time-input/src/utils/doesSomeSegmentExist/doesSomeSegmentExist.ts new file mode 100644 index 0000000000..1537c4039a --- /dev/null +++ b/packages/time-input/src/utils/doesSomeSegmentExist/doesSomeSegmentExist.ts @@ -0,0 +1,12 @@ +import { TimeSegmentsState } from '../../shared.types'; + +/** + * Checks if some segment exists + * + * @param segments - The segments to check + * @returns Whether some segment exists + */ +export const doesSomeSegmentExist = (segments: TimeSegmentsState) => { + // check if all segments are not empty + return Object.values(segments).some(segment => segment !== ''); +}; diff --git a/packages/time-input/src/utils/findUnitOptionByDayPeriod/findUnitOptionByDayPeriod.spec.ts b/packages/time-input/src/utils/findUnitOptionByDayPeriod/findUnitOptionByDayPeriod.spec.ts new file mode 100644 index 0000000000..74142e3d89 --- /dev/null +++ b/packages/time-input/src/utils/findUnitOptionByDayPeriod/findUnitOptionByDayPeriod.spec.ts @@ -0,0 +1,18 @@ +import { findUnitOptionByDayPeriod } from './findUnitOptionByDayPeriod'; + +describe('packages/time-input/utils/findUnitOptionByDayPeriod', () => { + test('returns the unit option by day period', () => { + expect( + findUnitOptionByDayPeriod('AM', [ + { displayName: 'AM', value: 'AM' }, + { displayName: 'PM', value: 'PM' }, + ]), + ).toEqual({ displayName: 'AM', value: 'AM' }); + expect( + findUnitOptionByDayPeriod('PM', [ + { displayName: 'AM', value: 'AM' }, + { displayName: 'PM', value: 'PM' }, + ]), + ).toEqual({ displayName: 'PM', value: 'PM' }); + }); +}); diff --git a/packages/time-input/src/utils/findUnitOptionByDayPeriod/findUnitOptionByDayPeriod.ts b/packages/time-input/src/utils/findUnitOptionByDayPeriod/findUnitOptionByDayPeriod.ts new file mode 100644 index 0000000000..91657491a5 --- /dev/null +++ b/packages/time-input/src/utils/findUnitOptionByDayPeriod/findUnitOptionByDayPeriod.ts @@ -0,0 +1,18 @@ +import { UnitOption } from '../../TimeInputSelect/TimeInputSelect.types'; + +/** + * Finds the select unit option based on the day period. + * + * @param dayPeriod - The day period to use for the select unit. + * @param unitOptions - The valid unit options to use for the select unit. + * @returns The select unit option. + */ +export const findUnitOptionByDayPeriod = ( + dayPeriod: string, + unitOptions: Array, +): UnitOption => { + const selectUnitOption = unitOptions.find( + option => option.displayName === dayPeriod, + ) as UnitOption; + return selectUnitOption; +}; diff --git a/packages/time-input/src/utils/getFormattedTimeSegments/getFormattedTimeSegments.spec.ts b/packages/time-input/src/utils/getFormattedTimeSegments/getFormattedTimeSegments.spec.ts new file mode 100644 index 0000000000..83da5b974b --- /dev/null +++ b/packages/time-input/src/utils/getFormattedTimeSegments/getFormattedTimeSegments.spec.ts @@ -0,0 +1,42 @@ +import { getFormattedTimeSegments } from './getFormattedTimeSegments'; + +describe('packages/time-input/utils/getFormattedTimeSegments', () => { + test('returns the formatted time segments if all segments are 0', () => { + const formattedTimeSegments = getFormattedTimeSegments({ + hour: '0', + minute: '0', + second: '0', + }); + expect(formattedTimeSegments).toEqual({ + hour: '00', + minute: '00', + second: '00', + }); + }); + + test('returns the formatted time segments', () => { + const formattedTimeSegments = getFormattedTimeSegments({ + hour: '2', + minute: '3', + second: '1', + }); + expect(formattedTimeSegments).toEqual({ + hour: '02', + minute: '03', + second: '01', + }); + }); + + test('does not format segments that are already formatted', () => { + const formattedTimeSegments = getFormattedTimeSegments({ + hour: '02', + minute: '03', + second: '01', + }); + expect(formattedTimeSegments).toEqual({ + hour: '02', + minute: '03', + second: '01', + }); + }); +}); diff --git a/packages/time-input/src/utils/getFormattedTimeSegments/getFormattedTimeSegments.ts b/packages/time-input/src/utils/getFormattedTimeSegments/getFormattedTimeSegments.ts new file mode 100644 index 0000000000..f8f034f5ee --- /dev/null +++ b/packages/time-input/src/utils/getFormattedTimeSegments/getFormattedTimeSegments.ts @@ -0,0 +1,28 @@ +import { getValueFormatter } from '@leafygreen-ui/input-box'; + +import { TimeSegmentsState } from '../../shared.types'; + +/** + * Formats the time segments to a string with 2 digits for each segment. + * + * @param segments - The time segments to format + * @returns The formatted time segments + * + * @example + * ```js + * getFormattedTimeSegments({ hour: '2', minute: '30', second: '0' }); + * // returns: { hour: '02', minute: '30', second: '00' } + * ``` + */ +export const getFormattedTimeSegments = (segments: TimeSegmentsState) => { + const hour = getValueFormatter({ charsCount: 2, allowZero: true })( + segments.hour, + ); + const minute = getValueFormatter({ charsCount: 2, allowZero: true })( + segments.minute, + ); + const second = getValueFormatter({ charsCount: 2, allowZero: true })( + segments.second, + ); + return { hour, minute, second }; +}; diff --git a/packages/time-input/src/utils/getFormattedTimeSegmentsFromDate/getFormattedTimeSegmentsFromDate.spec.ts b/packages/time-input/src/utils/getFormattedTimeSegmentsFromDate/getFormattedTimeSegmentsFromDate.spec.ts new file mode 100644 index 0000000000..86247442db --- /dev/null +++ b/packages/time-input/src/utils/getFormattedTimeSegmentsFromDate/getFormattedTimeSegmentsFromDate.spec.ts @@ -0,0 +1,16 @@ +import { getFormattedTimeSegmentsFromDate } from './getFormattedTimeSegmentsFromDate'; + +describe('packages/time-input/utils/getFormattedTimeSegmentsFromDate', () => { + test('returns the formatted time segments from a date', () => { + const formattedTimeSegments = getFormattedTimeSegmentsFromDate( + new Date('2025-01-01T01:00:00Z'), + 'en-US', + 'America/New_York', + ); + expect(formattedTimeSegments).toEqual({ + hour: '08', + minute: '00', + second: '00', + }); + }); +}); diff --git a/packages/time-input/src/utils/getFormattedTimeSegmentsFromDate/getFormattedTimeSegmentsFromDate.ts b/packages/time-input/src/utils/getFormattedTimeSegmentsFromDate/getFormattedTimeSegmentsFromDate.ts new file mode 100644 index 0000000000..ef6692ee20 --- /dev/null +++ b/packages/time-input/src/utils/getFormattedTimeSegmentsFromDate/getFormattedTimeSegmentsFromDate.ts @@ -0,0 +1,33 @@ +import { DateType, LocaleString } from '@leafygreen-ui/date-utils'; + +import { TimeSegmentsState } from '../../shared.types'; +import { getFormatPartsValues } from '../getFormatPartsValues/getFormatPartsValues'; +import { getFormattedTimeSegments } from '../getFormattedTimeSegments/getFormattedTimeSegments'; + +/** + * Gets the formatted time segments from a date + * + * @param date - The date to get the formatted time segments from + * @param locale - The locale to use + * @param timeZone - The time zone to use + * @returns The formatted time segments + * + * @example + * ```js + * getFormattedTimeSegmentsFromDate(new Date('2025-01-01T12:00:00Z'), 'en-US', 'America/New_York'); + * // returns: { hour: '12', minute: '00', second: '00' } + * ``` + */ +export const getFormattedTimeSegmentsFromDate = ( + date: DateType, + locale: LocaleString, + timeZone: string, +): TimeSegmentsState => { + const { hour, minute, second } = getFormatPartsValues({ + locale, + timeZone, + value: date, + }); + + return getFormattedTimeSegments({ hour, minute, second }); +}; diff --git a/packages/time-input/src/utils/getNewUTCDateFromSegments/getNewUTCDateFromSegments.spec.ts b/packages/time-input/src/utils/getNewUTCDateFromSegments/getNewUTCDateFromSegments.spec.ts new file mode 100644 index 0000000000..8e28aef51f --- /dev/null +++ b/packages/time-input/src/utils/getNewUTCDateFromSegments/getNewUTCDateFromSegments.spec.ts @@ -0,0 +1,595 @@ +import { Month } from '@leafygreen-ui/date-utils'; + +import { getNewUTCDateFromSegments } from './getNewUTCDateFromSegments'; + +describe('getNewUTCDateFromSegments', () => { + describe('When all segments are filled and valid, a valid date is returned', () => { + describe('UTC (UTC+0)', () => { + describe('12h format', () => { + describe('PM', () => { + test('returns the UTC date', () => { + const segments = { + hour: '11', + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: true, + timeZone: 'UTC', + dayPeriod: 'PM', + dateValues: { + day: '20', + month: '02', + year: '2026', + }, + }); // February 20, 2026 11:00:00 PM + + // February 20, 2026 11:00:00 PM in UTC is February 20, 2026 23:00:00 UTC + expect(newDate).toEqual( + new Date(Date.UTC(2026, Month.February, 20, 23, 0, 0)), + ); + }); + }); + describe('AM', () => { + test('returns the UTC date', () => { + const segments = { + hour: '11', + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: true, + timeZone: 'UTC', + dayPeriod: 'AM', + dateValues: { + day: '20', + month: '02', + year: '2026', + }, + }); // February 20, 2026 11:00:00 AM + + // February 20, 2026 11:00:00 AM in UTC is February 20, 2026 11:00:00 UTC + expect(newDate).toEqual( + new Date(Date.UTC(2026, Month.February, 20, 11, 0, 0)), + ); + }); + }); + }); + describe('24h format', () => {}); + }); + + describe('America/New_York', () => { + describe('EST (UTC-5)', () => { + describe('12h format', () => { + describe('PM', () => { + test('returns the UTC date', () => { + const segments = { + hour: '11', + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: true, + timeZone: 'America/New_York', + dayPeriod: 'PM', + dateValues: { + day: '20', + month: '02', + year: '2026', + }, + }); // February 20, 2026 11:00:00 PM America/New_York + + // February 20, 2026 11:00:00 PM in America/New_York is February 21, 2026 04:00:00 UTC + expect(newDate).toEqual( + new Date(Date.UTC(2026, Month.February, 21, 4, 0, 0)), + ); + }); + }); + describe('AM', () => { + test('returns the UTC date', () => { + const segments = { + hour: '11', + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: true, + timeZone: 'America/New_York', + dayPeriod: 'AM', + dateValues: { + day: '20', + month: '02', + year: '2026', + }, + }); // February 20, 2026 11:00:00 AM America/New_York + + // February 20, 2026 11:00:00 AM in America/New_York is February 20, 2026 16:00:00 UTC + expect(newDate).toEqual( + new Date(Date.UTC(2026, Month.February, 20, 16, 0, 0)), + ); + }); + }); + }); + describe('24h format', () => { + test('returns the UTC date', () => { + const segments = { + hour: '14', + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: false, + timeZone: 'America/New_York', + dayPeriod: 'AM', // This is not used for 24h format + dateValues: { + day: '20', + month: '02', + year: '2026', + }, + }); // February 20, 2026 14:00:00 America/New_York + + // February 20, 2026 14:00:00 in America/New_York is February 20, 2026 19:00:00 UTC + expect(newDate).toEqual( + new Date(Date.UTC(2026, Month.February, 20, 19, 0, 0)), + ); + }); + }); + }); + describe('EDT (UTC-4)', () => { + describe('12h format', () => { + describe('PM', () => { + test('returns the UTC date', () => { + const segments = { + hour: '11', + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: true, + timeZone: 'America/New_York', + dayPeriod: 'PM', + dateValues: { + day: '20', + month: '03', + year: '2026', + }, + }); // March 20, 2026 11:00:00 PM America/New_York + + // March 20, 2026 11:00:00 PM in America/New_York is March 21, 2026 03:00:00 UTC + expect(newDate).toEqual( + new Date(Date.UTC(2026, Month.March, 21, 3, 0, 0)), + ); + }); + }); + describe('AM', () => { + test('returns the UTC date', () => { + const segments = { + hour: '11', + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: true, + timeZone: 'America/New_York', + dayPeriod: 'AM', + dateValues: { + day: '20', + month: '03', + year: '2026', + }, + }); // March 20, 2026 11:00:00 AM America/New_York + + // March 20, 2026 11:00:00 AM in America/New_York is March 20, 2026 15:00:00 UTC + expect(newDate).toEqual( + new Date(Date.UTC(2026, Month.March, 20, 15, 0, 0)), + ); + }); + }); + }); + describe('24h format', () => { + test('returns the UTC date', () => { + const segments = { + hour: '14', + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: false, + timeZone: 'America/New_York', + dayPeriod: 'AM', // This is not used for 24h format + dateValues: { + day: '20', + month: '03', + year: '2026', + }, + }); // March 20, 2026 14:00:00 America/New_York + + // March 20, 2026 14:00:00 in America/New_York is March 20, 2026 18:00:00 UTC + expect(newDate).toEqual( + new Date(Date.UTC(2026, Month.March, 20, 18, 0, 0)), + ); + }); + }); + }); + }); + + describe('Pacific/Auckland', () => { + describe('NZST (UTC+12)', () => { + describe('12h format', () => { + describe('PM', () => { + test('returns the UTC date', () => { + const segments = { + hour: '11', + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: true, + timeZone: 'Pacific/Auckland', + dayPeriod: 'PM', + dateValues: { + day: '01', + month: '05', + year: '2026', + }, + }); // May 1, 2026 11:00:00 PM Pacific/Auckland + + // May 1, 2026 11:00:00 PM in Pacific/Auckland is May 1, 2026 11:00:00 UTC + expect(newDate).toEqual( + new Date(Date.UTC(2026, Month.May, 1, 11, 0, 0)), + ); + }); + }); + describe('AM', () => { + test('returns the UTC date', () => { + const segments = { + hour: '11', + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: true, + timeZone: 'Pacific/Auckland', + dayPeriod: 'AM', + dateValues: { + day: '01', + month: '05', + year: '2026', + }, + }); // May 1, 2026 11:00:00 AM Pacific/Auckland + + // May 1, 2026 11:00:00 AM in Pacific/Auckland is April 30, 2026 23:00:00 UTC + expect(newDate).toEqual( + new Date(Date.UTC(2026, Month.April, 30, 23, 0, 0)), + ); + }); + }); + }); + describe('24h format', () => { + test('returns the UTC date', () => { + const segments = { + hour: '13', + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: false, + timeZone: 'Pacific/Auckland', + dayPeriod: 'AM', // This is not used for 24h format + dateValues: { + day: '01', + month: '05', + year: '2026', + }, + }); // May 1, 2026 13:00:00 Pacific/Auckland + + // May 1, 2026 13:00:00 Pacific/Auckland is May 1, 2026 01:00:00 UTC + expect(newDate).toEqual( + new Date(Date.UTC(2026, Month.May, 1, 1, 0, 0)), + ); + }); + }); + }); + describe('NZDT (UTC+13)', () => { + describe('PM', () => { + test('returns the UTC date', () => { + const segments = { + hour: '11', + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: true, + timeZone: 'Pacific/Auckland', + dayPeriod: 'PM', + dateValues: { + day: '20', + month: '02', + year: '2026', + }, + }); // February 20, 2026 11:00:00 PM Pacific/Auckland + + // February 20, 2026 11:00:00 PM in Pacific/Auckland is February 20, 2026 10:00:00 UTC + expect(newDate).toEqual( + new Date(Date.UTC(2026, Month.February, 20, 10, 0, 0)), + ); + }); + + describe('AM', () => { + test('returns the UTC date', () => { + const segments = { + hour: '11', + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: true, + timeZone: 'Pacific/Auckland', + dayPeriod: 'AM', + dateValues: { + day: '20', + month: '02', + year: '2026', + }, + }); // February 20, 2026 11:00:00 AM Pacific/Auckland + + // February 20, 2026 11:00:00 AM in Pacific/Auckland is February 19, 2026 22:00:00 UTC + expect(newDate).toEqual( + new Date(Date.UTC(2026, Month.February, 19, 22, 0, 0)), + ); + }); + }); + }); + describe('24h format', () => { + test('returns the UTC date', () => { + const segments = { + hour: '13', + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: false, + timeZone: 'Pacific/Auckland', + dayPeriod: 'AM', // This is not used for 24h format + dateValues: { + day: '20', + month: '02', + year: '2026', + }, + }); // February 20, 2026 13:00:00 Pacific/Auckland + + // February 20, 2026 13:00:00 Pacific/Auckland is February 20, 2026 00:00:00 UTC + expect(newDate).toEqual( + new Date(Date.UTC(2026, Month.February, 20, 0, 0, 0)), + ); + }); + }); + }); + }); + }); + + describe('When all segments are filled but not all segments are valid, an invalid date object is returned', () => { + describe('12h format', () => { + describe('hour', () => { + test('is invalid', () => { + const segments = { + hour: '13', // Invalid hour + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: true, + timeZone: 'UTC', + dayPeriod: 'PM', + dateValues: { + day: '20', + month: '02', + year: '2026', + }, + }); + + expect(newDate?.getTime()).toBeNaN(); + expect(newDate).not.toBeNull(); + }); + + test('is empty', () => { + const segments = { + hour: '', // Empty hour + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: true, + timeZone: 'UTC', + dayPeriod: 'PM', + dateValues: { + day: '20', + month: '02', + year: '2026', + }, + }); + + expect(newDate?.getTime()).toBeNaN(); + expect(newDate).not.toBeNull(); + }); + }); + }); + + describe('24h format', () => { + describe('hour', () => { + test('is invalid', () => { + const segments = { + hour: '24', // Invalid hour + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: false, + timeZone: 'UTC', + dayPeriod: 'PM', + dateValues: { + day: '20', + month: '02', + year: '2026', + }, + }); + + expect(newDate?.getTime()).toBeNaN(); + expect(newDate).not.toBeNull(); + }); + + test('is empty', () => { + const segments = { + hour: '', // Empty hour + minute: '00', + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: false, + timeZone: 'UTC', + dayPeriod: 'PM', + dateValues: { + day: '20', + month: '02', + year: '2026', + }, + }); + + expect(newDate?.getTime()).toBeNaN(); + expect(newDate).not.toBeNull(); + }); + }); + }); + + describe('minute', () => { + test('is invalid', () => { + const segments = { + hour: '11', + minute: '60', // Invalid minute + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: true, + timeZone: 'UTC', + dayPeriod: 'PM', + dateValues: { + day: '20', + month: '02', + year: '2026', + }, + }); + + expect(newDate?.getTime()).toBeNaN(); + expect(newDate).not.toBeNull(); + }); + + test('is empty', () => { + const segments = { + hour: '11', + minute: '', // Empty minute + second: '00', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: true, + timeZone: 'UTC', + dayPeriod: 'PM', + dateValues: { + day: '20', + month: '02', + year: '2026', + }, + }); + + expect(newDate?.getTime()).toBeNaN(); + expect(newDate).not.toBeNull(); + }); + }); + + describe('second', () => { + test('is invalid', () => { + const segments = { + hour: '11', + minute: '00', + second: '60', // Invalid second + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: true, + timeZone: 'UTC', + dayPeriod: 'PM', + dateValues: { + day: '20', + month: '02', + year: '2026', + }, + }); + + expect(newDate?.getTime()).toBeNaN(); + expect(newDate).not.toBeNull(); + }); + + test('is empty', () => { + const segments = { + hour: '11', + minute: '00', + second: '', // Empty second + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: true, + timeZone: 'UTC', + dayPeriod: 'PM', + dateValues: { + day: '20', + month: '02', + year: '2026', + }, + }); + + expect(newDate?.getTime()).toBeNaN(); + expect(newDate).not.toBeNull(); + }); + }); + }); + + test('When all segments are empty, null is returned', () => { + const segments = { + hour: '', + minute: '', + second: '', + }; + const newDate = getNewUTCDateFromSegments({ + segments, + is12HourFormat: true, + timeZone: 'UTC', + dayPeriod: 'PM', + dateValues: { + day: '20', + month: '02', + year: '2026', + }, + }); + + expect(newDate).toBeNull(); + }); +}); diff --git a/packages/time-input/src/utils/getNewUTCDateFromSegments/getNewUTCDateFromSegments.ts b/packages/time-input/src/utils/getNewUTCDateFromSegments/getNewUTCDateFromSegments.ts new file mode 100644 index 0000000000..eda07af3a3 --- /dev/null +++ b/packages/time-input/src/utils/getNewUTCDateFromSegments/getNewUTCDateFromSegments.ts @@ -0,0 +1,71 @@ +import { newUTCFromTimeZone } from '@leafygreen-ui/date-utils'; + +import { TimeSegmentsState } from '../../shared.types'; +import { convert12hTo24h } from '../convert12hTo24h/convert12hTo24h'; +import { doesSomeSegmentExist } from '../doesSomeSegmentExist/doesSomeSegmentExist'; +import { isEverySegmentFilled } from '../isEverySegmentFilled/isEverySegmentFilled'; +import { isEverySegmentValid } from '../isEverySegmentValid/isEverySegmentValid'; + +/** + * Takes local time segments, creates a local date object and converts it to UTC. + * + * @param segments - The segments to create the date from + * @param is12HourFormat - Whether the time is in 12 hour format + * @param dateValues - The date values to create the date from + * @param timeZone - The time zone to create the date in + * @returns The either a new date object in UTC, null, or an invalid date object + */ +export const getNewUTCDateFromSegments = ({ + segments, + is12HourFormat, + dateValues, + timeZone, + dayPeriod, +}: { + segments: TimeSegmentsState; + is12HourFormat: boolean; + timeZone: string; + dateValues: { + day: string; + month: string; + year: string; + }; + dayPeriod: string; +}) => { + const { day, month, year } = dateValues; + const { hour, minute, second } = segments; + + const convertedHour = is12HourFormat + ? convert12hTo24h(hour, dayPeriod) + : hour; + + /** + * Check if all segments are filled and valid. If they are, return the UTC date. + */ + if ( + isEverySegmentFilled(segments) && + isEverySegmentValid({ segments, is12HourFormat }) + ) { + return newUTCFromTimeZone({ + year, + month, + day, + hour: convertedHour, + minute, + second, + timeZone, + }); + } + + /** + * Check if any segments are filled. If not, return null. This means all segments are empty. + */ + if (!doesSomeSegmentExist(segments)) { + return null; + } + + /** + * Return an invalid date object if some segments are empty or invalid. + */ + return new Date('invalid'); +}; diff --git a/packages/time-input/src/utils/index.ts b/packages/time-input/src/utils/index.ts index 4f64fa8efe..7c9e69dada 100644 --- a/packages/time-input/src/utils/index.ts +++ b/packages/time-input/src/utils/index.ts @@ -1,3 +1,14 @@ +export { convert12hTo24h } from './convert12hTo24h/convert12hTo24h'; +export { doesSomeSegmentExist } from './doesSomeSegmentExist/doesSomeSegmentExist'; +export { findUnitOptionByDayPeriod } from './findUnitOptionByDayPeriod/findUnitOptionByDayPeriod'; export { getFormatPartsValues } from './getFormatPartsValues/getFormatPartsValues'; +export { getFormattedTimeSegments } from './getFormattedTimeSegments/getFormattedTimeSegments'; +export { getFormattedTimeSegmentsFromDate } from './getFormattedTimeSegmentsFromDate/getFormattedTimeSegmentsFromDate'; export { getFormatter } from './getFormatter/getFormatter'; +export { getNewUTCDateFromSegments } from './getNewUTCDateFromSegments/getNewUTCDateFromSegments'; export { hasDayPeriod } from './hasDayPeriod/hasDayPeriod'; +export { isEverySegmentFilled } from './isEverySegmentFilled/isEverySegmentFilled'; +export { isEverySegmentValid } from './isEverySegmentValid/isEverySegmentValid'; +export { isEverySegmentValueExplicit } from './isEverySegmentValueExplicit/isEverySegmentValueExplicit'; +export { isSameUTCDayAndTime } from './isSameUTCDayAndTime/isSameUTCDayAndTime'; +export { shouldSetValue } from './shouldSetValue/shouldSetValue'; diff --git a/packages/time-input/src/utils/isEverySegmentFilled/isEverySegmentFilled.spec.ts b/packages/time-input/src/utils/isEverySegmentFilled/isEverySegmentFilled.spec.ts new file mode 100644 index 0000000000..f136625e1b --- /dev/null +++ b/packages/time-input/src/utils/isEverySegmentFilled/isEverySegmentFilled.spec.ts @@ -0,0 +1,21 @@ +import { isEverySegmentFilled } from './isEverySegmentFilled'; + +describe('isEverySegmentFilled', () => { + test('returns true if all segments are filled', () => { + expect( + isEverySegmentFilled({ hour: '12', minute: '00', second: '00' }), + ).toBe(true); + }); + + test('returns false if any segment is empty', () => { + expect(isEverySegmentFilled({ hour: '', minute: '00', second: '00' })).toBe( + false, + ); + }); + + test('returns false is all segments are empty', () => { + expect(isEverySegmentFilled({ hour: '', minute: '', second: '' })).toBe( + false, + ); + }); +}); diff --git a/packages/time-input/src/utils/isEverySegmentFilled/isEverySegmentFilled.ts b/packages/time-input/src/utils/isEverySegmentFilled/isEverySegmentFilled.ts new file mode 100644 index 0000000000..318178bd54 --- /dev/null +++ b/packages/time-input/src/utils/isEverySegmentFilled/isEverySegmentFilled.ts @@ -0,0 +1,19 @@ +import { isDefined } from '@leafygreen-ui/lib'; + +import { TimeSegmentsState } from '../../shared.types'; + +/** + * Checks if every segment is filled + * + * @param segments - The segments to check + * @returns Whether every segment is filled + */ +export const isEverySegmentFilled = (segments: TimeSegmentsState) => { + const isEverySegmentFilled = Object.values(segments).every(segment => { + const isSegmentDefined = isDefined(segment); + const isSegmentEmpty = segment === ''; + return !isSegmentEmpty && isSegmentDefined; + }); + // check if all segments are not empty + return isEverySegmentFilled; +}; diff --git a/packages/time-input/src/utils/isEverySegmentValid/isEverySegmentValid.spec.ts b/packages/time-input/src/utils/isEverySegmentValid/isEverySegmentValid.spec.ts new file mode 100644 index 0000000000..ef0d038fd0 --- /dev/null +++ b/packages/time-input/src/utils/isEverySegmentValid/isEverySegmentValid.spec.ts @@ -0,0 +1,173 @@ +import { getDefaultMax, getDefaultMin } from '../../constants'; + +import { isEverySegmentValid } from './isEverySegmentValid'; + +describe('isEverySegmentValueValid', () => { + describe('12 hour format', () => { + test('returns false if all segments are 00', () => { + // when 12 hour format, 00 is not a valid value for the hour segment + expect( + isEverySegmentValid({ + segments: { hour: '00', minute: '00', second: '00' }, + is12HourFormat: true, + }), + ).toBe(false); + }); + + test('returns true if all segments are at the default min', () => { + expect( + isEverySegmentValid({ + segments: { + hour: getDefaultMin({ is12HourFormat: true })['hour'].toString(), + minute: getDefaultMin({ is12HourFormat: true })[ + 'minute' + ].toString(), + second: getDefaultMin({ is12HourFormat: true })[ + 'second' + ].toString(), + }, + is12HourFormat: true, + }), + ).toBe(true); + }); + + test('returns true if all segments are at the default max', () => { + expect( + isEverySegmentValid({ + segments: { + hour: getDefaultMax({ is12HourFormat: true })['hour'].toString(), + minute: getDefaultMax({ is12HourFormat: true })[ + 'minute' + ].toString(), + second: getDefaultMax({ is12HourFormat: true })[ + 'second' + ].toString(), + }, + is12HourFormat: true, + }), + ).toBe(true); + }); + + describe('hour', () => { + test('returns false if hour is greater than the default max', () => { + expect( + isEverySegmentValid({ + segments: { + hour: ( + getDefaultMax({ is12HourFormat: true })['hour'] + 1 + ).toString(), + minute: '00', + second: '00', + }, + is12HourFormat: true, + }), + ).toBe(false); + }); + }); + + describe('minute', () => { + test('returns false if minute is greater than the default max', () => { + expect( + isEverySegmentValid({ + segments: { + hour: '00', + minute: ( + getDefaultMax({ is12HourFormat: true })['minute'] + 1 + ).toString(), + second: '00', + }, + is12HourFormat: true, + }), + ).toBe(false); + }); + }); + + describe('second', () => { + test('returns false if second is greater than the default max', () => { + expect( + isEverySegmentValid({ + segments: { + hour: '00', + minute: '00', + second: ( + getDefaultMax({ is12HourFormat: true })['second'] + 1 + ).toString(), + }, + is12HourFormat: true, + }), + ).toBe(false); + }); + }); + }); + + describe('24 hour format', () => { + test('returns true if all segments are 00', () => { + expect( + isEverySegmentValid({ + segments: { hour: '00', minute: '00', second: '00' }, + is12HourFormat: false, + }), + ).toBe(true); + }); + + test('returns true if all segments are valid', () => { + expect( + isEverySegmentValid({ + segments: { hour: '12', minute: '00', second: '00' }, + is12HourFormat: false, + }), + ).toBe(true); + }); + + describe('hour', () => { + test('returns false if hour is greater than the default max', () => { + expect( + isEverySegmentValid({ + segments: { + hour: ( + getDefaultMax({ is12HourFormat: false })['hour'] + 1 + ).toString(), + minute: '00', + second: '00', + }, + is12HourFormat: false, + }), + ).toBe(false); + }); + }); + + describe('minute', () => { + test('returns false if minute is greater than the default max', () => { + expect( + isEverySegmentValid({ + segments: { + hour: '00', + minute: ( + getDefaultMax({ is12HourFormat: false })['minute'] + 1 + ).toString(), + second: '00', + }, + is12HourFormat: false, + }), + ).toBe(false); + }); + }); + + describe('second', () => { + test('returns false if second is greater than the default max', () => { + expect( + isEverySegmentValid({ + segments: { + hour: '00', + minute: '00', + second: ( + getDefaultMax({ is12HourFormat: false })['second'] + 1 + ).toString(), + }, + is12HourFormat: false, + }), + ).toBe(false); + }); + }); + }); +}); diff --git a/packages/time-input/src/utils/isEverySegmentValid/isEverySegmentValid.ts b/packages/time-input/src/utils/isEverySegmentValid/isEverySegmentValid.ts new file mode 100644 index 0000000000..0f3443605e --- /dev/null +++ b/packages/time-input/src/utils/isEverySegmentValid/isEverySegmentValid.ts @@ -0,0 +1,36 @@ +import { isValidValueForSegment } from '@leafygreen-ui/input-box'; + +import { getDefaultMax, getDefaultMin } from '../../constants'; +import { TimeSegment } from '../../shared.types'; +import { TimeSegmentsState } from '../../shared.types'; + +/** + * Checks if every segment is valid + * + * @param segments - The segments to check + * @param is12HourFormat - Whether the time is in 12 hour format + * @returns Whether every segment is valid + */ +export const isEverySegmentValid = ({ + segments, + is12HourFormat, +}: { + segments: TimeSegmentsState; + is12HourFormat: boolean; +}) => { + const isEverySegmentValid = Object.entries(segments).every( + ([segment, value]) => { + const isSegmentValid = isValidValueForSegment({ + segment: segment as TimeSegment, + value: value as string, + defaultMin: getDefaultMin({ is12HourFormat })[segment as TimeSegment], + defaultMax: getDefaultMax({ is12HourFormat })[segment as TimeSegment], + segmentEnum: TimeSegment, + }); + + return isSegmentValid; + }, + ); + + return isEverySegmentValid; +}; diff --git a/packages/time-input/src/utils/isEverySegmentValueExplicit/isEverySegmentValueExplicit.spec.ts b/packages/time-input/src/utils/isEverySegmentValueExplicit/isEverySegmentValueExplicit.spec.ts new file mode 100644 index 0000000000..dd2ffc111f --- /dev/null +++ b/packages/time-input/src/utils/isEverySegmentValueExplicit/isEverySegmentValueExplicit.spec.ts @@ -0,0 +1,160 @@ +import range from 'lodash/range'; + +import { isEverySegmentValueExplicit } from './isEverySegmentValueExplicit'; + +describe('isEverySegmentValueExplicit', () => { + describe('12 hour format', () => { + test('returns false if all values are not explicit', () => { + expect( + isEverySegmentValueExplicit({ + segments: { hour: '1', minute: '1', second: '1' }, + is12HourFormat: true, + }), + ).toBe(false); + }); + + describe('hour', () => { + describe('returns false', () => { + test('if hour is 0', () => { + // in 12 hour format, 00 is not a valid hour + expect( + isEverySegmentValueExplicit({ + segments: { hour: '00', minute: '00', second: '00' }, + is12HourFormat: true, + }), + ).toBe(false); + }); + + test('if hour is a single digit below the min explicit value (2)', () => { + expect( + isEverySegmentValueExplicit({ + segments: { hour: '1', minute: '00', second: '00' }, + is12HourFormat: true, + }), + ).toBe(false); + }); + }); + + describe('returns true', () => { + describe('when single digit and value is greater than or equal to the min explicit value (2)', () => { + test.each(range(2, 10))('%i', i => { + expect( + isEverySegmentValueExplicit({ + segments: { hour: i.toString(), minute: '00', second: '00' }, + is12HourFormat: true, + }), + ).toBe(true); + }); + }); + describe('when double digit and value is greater than the min explicit value (2)', () => { + test.each(range(11, 13))('%i', i => { + expect( + isEverySegmentValueExplicit({ + segments: { hour: i.toString(), minute: '00', second: '00' }, + is12HourFormat: true, + }), + ).toBe(true); + }); + }); + }); + }); + }); + + describe('24 hour format', () => { + test('returns false if all values are not explicit', () => { + expect( + isEverySegmentValueExplicit({ + segments: { hour: '1', minute: '1', second: '1' }, + is12HourFormat: false, + }), + ).toBe(false); + }); + + describe('hour', () => { + describe('returns false', () => { + test.each(range(0, 3))('if hour is %i', i => { + expect( + isEverySegmentValueExplicit({ + segments: { hour: i.toString(), minute: '00', second: '00' }, + is12HourFormat: false, + }), + ).toBe(false); + }); + }); + + describe('returns true', () => { + describe('if is single digit and greater than or equal to the min explicit value (3)', () => { + test.each(range(3, 10))('%i', i => { + expect( + isEverySegmentValueExplicit({ + segments: { hour: i.toString(), minute: '00', second: '00' }, + is12HourFormat: false, + }), + ).toBe(true); + }); + }); + + describe('if is double digit and greater than the min explicit value (3)', () => { + test.each(range(10, 24))('%i', i => { + expect( + isEverySegmentValueExplicit({ + segments: { hour: i.toString(), minute: '00', second: '00' }, + is12HourFormat: false, + }), + ).toBe(true); + }); + }); + }); + }); + + describe.each(['minute', 'second'])('%s', segment => { + describe('returns false', () => { + describe('if is single digit and less than the min explicit value (6)', () => { + test.each(range(0, 6))('%i', i => { + expect( + isEverySegmentValueExplicit({ + segments: { + hour: '12', + minute: segment === 'minute' ? i.toString() : '00', + second: segment === 'second' ? i.toString() : '00', + }, + is12HourFormat: true, + }), + ).toBe(false); + }); + }); + describe('if is double digit and greater than the min explicit value (6)', () => { + test.each(range(10, 60))('%i', i => { + expect( + isEverySegmentValueExplicit({ + segments: { + hour: '12', + minute: segment === 'minute' ? i.toString() : '00', + second: segment === 'second' ? i.toString() : '00', + }, + is12HourFormat: true, + }), + ).toBe(true); + }); + }); + }); + + describe('returns true', () => { + describe('if is single digit and greater than the min explicit value (6)', () => { + test.each(range(6, 10))('%i', i => { + expect( + isEverySegmentValueExplicit({ + segments: { + hour: '12', + minute: segment === 'minute' ? i.toString() : '00', + second: segment === 'second' ? i.toString() : '00', + }, + is12HourFormat: true, + }), + ).toBe(true); + }); + }); + }); + }); + }); +}); diff --git a/packages/time-input/src/utils/isEverySegmentValueExplicit/isEverySegmentValueExplicit.ts b/packages/time-input/src/utils/isEverySegmentValueExplicit/isEverySegmentValueExplicit.ts new file mode 100644 index 0000000000..1fa6cfc413 --- /dev/null +++ b/packages/time-input/src/utils/isEverySegmentValueExplicit/isEverySegmentValueExplicit.ts @@ -0,0 +1,32 @@ +import { createExplicitSegmentValidator } from '@leafygreen-ui/input-box'; + +import { getTimeSegmentRules } from '../../constants'; +import { TimeSegment, TimeSegmentsState } from '../../shared.types'; + +export const isExplicitSegmentValue = (is12HourFormat: boolean) => + createExplicitSegmentValidator({ + segmentEnum: TimeSegment, + rules: getTimeSegmentRules({ is12HourFormat }), + }); + +/** + * Returns whether every segment's value is explicit and unambiguous. + * (see {@link isExplicitSegmentValue}) + */ +export const isEverySegmentValueExplicit = ({ + segments, + is12HourFormat, +}: { + segments: TimeSegmentsState; + is12HourFormat: boolean; +}): boolean => { + return Object.entries(segments).every(([segment, value]) => { + const isExplicit = isExplicitSegmentValue(is12HourFormat)({ + segment: segment as TimeSegment, + value, + allowZero: segment === TimeSegment.Hour ? !is12HourFormat : true, + }); + + return isExplicit; + }); +}; diff --git a/packages/time-input/src/utils/isSameUTCDayAndTime/isSameUTCDayAndTime.spec.ts b/packages/time-input/src/utils/isSameUTCDayAndTime/isSameUTCDayAndTime.spec.ts new file mode 100644 index 0000000000..c1889f66c8 --- /dev/null +++ b/packages/time-input/src/utils/isSameUTCDayAndTime/isSameUTCDayAndTime.spec.ts @@ -0,0 +1,27 @@ +import { isSameUTCDayAndTime } from './isSameUTCDayAndTime'; + +describe('packages/time-input/utils/isSameUTCDayAndTime', () => { + test('returns true if the two dates are the same day and time in UTC', () => { + const date1 = new Date('2025-01-01T12:00:00Z'); + const date2 = new Date('2025-01-01T12:00:00Z'); + expect(isSameUTCDayAndTime(date1, date2)).toBe(true); + }); + + test('returns false if the two dates are not the same day in UTC', () => { + const date1 = new Date('2025-01-01T12:00:00Z'); + const date2 = new Date('2025-01-02T12:00:00Z'); + expect(isSameUTCDayAndTime(date1, date2)).toBe(false); + }); + + test('returns false if the two dates are not the same time in UTC', () => { + const date1 = new Date('2025-01-01T12:00:00Z'); + const date2 = new Date('2025-01-01T12:00:01Z'); + expect(isSameUTCDayAndTime(date1, date2)).toBe(false); + }); + + test('returns false if the two dates are not the same date and time in UTC', () => { + const date1 = new Date('2025-02-01T12:00:00Z'); + const date2 = new Date('2025-01-01T12:00:01Z'); + expect(isSameUTCDayAndTime(date1, date2)).toBe(false); + }); +}); diff --git a/packages/time-input/src/utils/isSameUTCDayAndTime/isSameUTCDayAndTime.ts b/packages/time-input/src/utils/isSameUTCDayAndTime/isSameUTCDayAndTime.ts new file mode 100644 index 0000000000..8739d708ad --- /dev/null +++ b/packages/time-input/src/utils/isSameUTCDayAndTime/isSameUTCDayAndTime.ts @@ -0,0 +1,25 @@ +import { DateType, isValidDate } from '@leafygreen-ui/date-utils'; + +/** + * Checks if two dates are the same day and time in UTC. + * + * @param day1 - The first date to check + * @param day2 - The second date to check + * @returns Whether the two dates are the same day and time in UTC + */ +export const isSameUTCDayAndTime = ( + day1?: DateType, + day2?: DateType, +): boolean => { + if (!isValidDate(day1) || !isValidDate(day2)) return false; + + return ( + day1.getUTCDate() === day2.getUTCDate() && + day1.getUTCMonth() === day2.getUTCMonth() && + day1.getUTCFullYear() === day2.getUTCFullYear() && + day1.getUTCHours() === day2.getUTCHours() && + day1.getUTCMinutes() === day2.getUTCMinutes() && + day1.getUTCSeconds() === day2.getUTCSeconds() && + day1.getUTCMilliseconds() === day2.getUTCMilliseconds() + ); +}; diff --git a/packages/time-input/src/utils/shouldSetValue/shouldSetValue.spec.ts b/packages/time-input/src/utils/shouldSetValue/shouldSetValue.spec.ts new file mode 100644 index 0000000000..5cbe45ab0a --- /dev/null +++ b/packages/time-input/src/utils/shouldSetValue/shouldSetValue.spec.ts @@ -0,0 +1,158 @@ +import { Month, newUTC } from '@leafygreen-ui/date-utils'; + +import { TimeSegmentsState } from '../../shared.types'; + +import { shouldSetValue } from './shouldSetValue'; + +describe('packages/time-input/utils/shouldSetValue', () => { + describe('when the date is valid', () => { + describe('should return true', () => { + describe('when all the segments are explicit', () => { + test('12 hour format', () => { + const newDate = new Date(newUTC(2021, Month.January, 1)); + const segments: TimeSegmentsState = { + hour: '01', + minute: '01', + second: '01', + }; + + const shouldSetNewValue = shouldSetValue({ + newDate, + isDirty: true, + segments, + is12HourFormat: true, + }); + + expect(shouldSetNewValue).toBe(true); + }); + test('24 hour format', () => { + const newDate = new Date(newUTC(2021, Month.January, 1)); + const segments: TimeSegmentsState = { + hour: '13', + minute: '01', + second: '01', + }; + + const shouldSetNewValue = shouldSetValue({ + newDate, + isDirty: true, + segments, + is12HourFormat: false, + }); + + expect(shouldSetNewValue).toBe(true); + }); + }); + }); + describe('should return false', () => { + describe('when not all the segments are explicit', () => { + test('12 hour format', () => { + const newDate = new Date(newUTC(2021, Month.January, 1)); + const segments: TimeSegmentsState = { + hour: '1', + minute: '01', + second: '01', + }; + + const shouldSetNewValue = shouldSetValue({ + newDate, + isDirty: true, + segments, + is12HourFormat: true, + }); + + expect(shouldSetNewValue).toBe(false); + }); + test('24 hour format', () => { + const newDate = new Date(newUTC(2021, Month.January, 1)); + const segments: TimeSegmentsState = { + hour: '2', + minute: '01', + second: '01', + }; + const shouldSetNewValue = shouldSetValue({ + newDate, + isDirty: true, + segments, + is12HourFormat: false, + }); + + expect(shouldSetNewValue).toBe(false); + }); + }); + }); + }); + describe('when the date is invalid', () => { + describe('should return true', () => { + test('when the date is invalid and the component is dirty', () => { + const newDate = new Date('invalid'); + const segments: TimeSegmentsState = { + hour: '01', + minute: '01', + second: '01', + }; + const shouldSetNewValue = shouldSetValue({ + newDate, + isDirty: true, + segments, + is12HourFormat: true, + }); + + expect(shouldSetNewValue).toBe(true); + }); + test('when the date is invalid and the component is not dirty and every segment is filled', () => { + const newDate = new Date('invalid'); + const segments: TimeSegmentsState = { + hour: '01', + minute: '01', + second: '01', + }; + const shouldSetNewValue = shouldSetValue({ + newDate, + isDirty: false, + segments, + is12HourFormat: true, + }); + + expect(shouldSetNewValue).toBe(true); + }); + }); + describe('should return false', () => { + test('when the date is invalid and the component is not dirty and not every segment is filled', () => { + const newDate = new Date('invalid'); + const segments: TimeSegmentsState = { + hour: '', + minute: '01', + second: '01', + }; + const shouldSetNewValue = shouldSetValue({ + newDate, + isDirty: false, + segments, + is12HourFormat: true, + }); + + expect(shouldSetNewValue).toBe(false); + }); + }); + + describe('when the date is null', () => { + test('should return true', () => { + const newDate = null; + const segments: TimeSegmentsState = { + hour: '01', + minute: '01', + second: '01', + }; + const shouldSetNewValue = shouldSetValue({ + newDate, + isDirty: true, + segments, + is12HourFormat: true, + }); + + expect(shouldSetNewValue).toBe(true); + }); + }); + }); +}); diff --git a/packages/time-input/src/utils/shouldSetValue/shouldSetValue.ts b/packages/time-input/src/utils/shouldSetValue/shouldSetValue.ts new file mode 100644 index 0000000000..48924dd3c9 --- /dev/null +++ b/packages/time-input/src/utils/shouldSetValue/shouldSetValue.ts @@ -0,0 +1,49 @@ +import isNull from 'lodash/isNull'; + +import { DateType, isValidDate } from '@leafygreen-ui/date-utils'; +import { isInvalidDateObject } from '@leafygreen-ui/date-utils'; + +import { TimeSegmentsState } from '../../shared.types'; +import { isEverySegmentFilled } from '../isEverySegmentFilled/isEverySegmentFilled'; +import { isEverySegmentValueExplicit } from '../isEverySegmentValueExplicit/isEverySegmentValueExplicit'; + +/** + * Checks if the new date should be set. + * + * @param newDate - The new date to check + * @param isDirty - Whether the component is dirty + * @param segments - The segments to check + * @param is12HourFormat - Whether the time is in 12 hour format + * @returns Whether the new date should be set + */ +export const shouldSetValue = ({ + newDate, + isDirty, + segments, + is12HourFormat, +}: { + newDate: DateType; + isDirty: boolean; + segments: TimeSegmentsState; + is12HourFormat: boolean; +}): boolean => { + // If the date is valid and all segments are explicit, then the value should be set. + const isValidDateAndSegmentsAreExplicit = + isValidDate(newDate) && + isEverySegmentValueExplicit({ + segments, + is12HourFormat, + }); + + // If the date is invalid and the component is dirty, it means the user has interacted with the component and the value should be set. + // If the date is invalid and every segment is filled, then the value should be set. + const isInvalidDateObjectAndDirty = + isInvalidDateObject(newDate) && (isDirty || isEverySegmentFilled(segments)); + + const shouldSetValue = + isNull(newDate) || + isValidDateAndSegmentsAreExplicit || + isInvalidDateObjectAndDirty; + + return shouldSetValue; +}; From bf0c1a98dda5c9e1baaf364807878361af46d456 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Mon, 15 Dec 2025 11:52:53 -0500 Subject: [PATCH 02/10] fix(time-input): update minExplicitValue for hour segment to ensure correct input validation --- packages/time-input/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/time-input/src/constants.ts b/packages/time-input/src/constants.ts index b1c6df8cca..547bef5e23 100644 --- a/packages/time-input/src/constants.ts +++ b/packages/time-input/src/constants.ts @@ -21,7 +21,7 @@ export const getTimeSegmentRules = ({ return { [TimeSegment.Hour]: { maxChars: 2, - minExplicitValue: is12HourFormat ? 1 : 2, + minExplicitValue: is12HourFormat ? 2 : 3, }, [TimeSegment.Minute]: { maxChars: 2, From fdcbd447bcff89bb596ef8d7c36fce92ce49ef08 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Mon, 15 Dec 2025 12:16:08 -0500 Subject: [PATCH 03/10] fix(time-input): correct typos in test descriptions for segment validation functions --- .../src/utils/doesSomeSegmentExist/doesSomeSegmentExist.spec.ts | 2 +- .../src/utils/isEverySegmentFilled/isEverySegmentFilled.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/time-input/src/utils/doesSomeSegmentExist/doesSomeSegmentExist.spec.ts b/packages/time-input/src/utils/doesSomeSegmentExist/doesSomeSegmentExist.spec.ts index 0b71109f63..a23eba4d27 100644 --- a/packages/time-input/src/utils/doesSomeSegmentExist/doesSomeSegmentExist.spec.ts +++ b/packages/time-input/src/utils/doesSomeSegmentExist/doesSomeSegmentExist.spec.ts @@ -7,7 +7,7 @@ describe('doesSomeSegmentExist', () => { ); }); - test('returns true if at all segments are filled', () => { + test('returns true if all segments are filled', () => { expect( doesSomeSegmentExist({ hour: '12', minute: '00', second: '00' }), ).toBe(true); diff --git a/packages/time-input/src/utils/isEverySegmentFilled/isEverySegmentFilled.spec.ts b/packages/time-input/src/utils/isEverySegmentFilled/isEverySegmentFilled.spec.ts index f136625e1b..6f0b4da2d5 100644 --- a/packages/time-input/src/utils/isEverySegmentFilled/isEverySegmentFilled.spec.ts +++ b/packages/time-input/src/utils/isEverySegmentFilled/isEverySegmentFilled.spec.ts @@ -13,7 +13,7 @@ describe('isEverySegmentFilled', () => { ); }); - test('returns false is all segments are empty', () => { + test('returns false if all segments are empty', () => { expect(isEverySegmentFilled({ hour: '', minute: '', second: '' })).toBe( false, ); From 819fbdf09774b0fe2108869a247e94810fa9166f Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Fri, 19 Dec 2025 17:40:17 -0500 Subject: [PATCH 04/10] feat(time-input): add utility functions for padded time segments and corresponding tests --- .../getPaddedTimeSegments/getPaddedTimeSegments.spec.ts} | 0 .../getPaddedTimeSegments/getPaddedTimeSegments.ts} | 0 .../getPaddedTimeSegmentsFromDate.spec.ts} | 0 .../getPaddedTimeSegmentsFromDate.ts} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename packages/time-input/src/utils/{getFormattedTimeSegments/getFormattedTimeSegments.spec.ts => getPaddedTimeSegmentsFromDate/getPaddedTimeSegments/getPaddedTimeSegments.spec.ts} (100%) rename packages/time-input/src/utils/{getFormattedTimeSegments/getFormattedTimeSegments.ts => getPaddedTimeSegmentsFromDate/getPaddedTimeSegments/getPaddedTimeSegments.ts} (100%) rename packages/time-input/src/utils/{getFormattedTimeSegmentsFromDate/getFormattedTimeSegmentsFromDate.spec.ts => getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.spec.ts} (100%) rename packages/time-input/src/utils/{getFormattedTimeSegmentsFromDate/getFormattedTimeSegmentsFromDate.ts => getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.ts} (100%) diff --git a/packages/time-input/src/utils/getFormattedTimeSegments/getFormattedTimeSegments.spec.ts b/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegments/getPaddedTimeSegments.spec.ts similarity index 100% rename from packages/time-input/src/utils/getFormattedTimeSegments/getFormattedTimeSegments.spec.ts rename to packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegments/getPaddedTimeSegments.spec.ts diff --git a/packages/time-input/src/utils/getFormattedTimeSegments/getFormattedTimeSegments.ts b/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegments/getPaddedTimeSegments.ts similarity index 100% rename from packages/time-input/src/utils/getFormattedTimeSegments/getFormattedTimeSegments.ts rename to packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegments/getPaddedTimeSegments.ts diff --git a/packages/time-input/src/utils/getFormattedTimeSegmentsFromDate/getFormattedTimeSegmentsFromDate.spec.ts b/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.spec.ts similarity index 100% rename from packages/time-input/src/utils/getFormattedTimeSegmentsFromDate/getFormattedTimeSegmentsFromDate.spec.ts rename to packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.spec.ts diff --git a/packages/time-input/src/utils/getFormattedTimeSegmentsFromDate/getFormattedTimeSegmentsFromDate.ts b/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.ts similarity index 100% rename from packages/time-input/src/utils/getFormattedTimeSegmentsFromDate/getFormattedTimeSegmentsFromDate.ts rename to packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.ts From cfcca09495aae35f42e711e5a08c118ba6b64a4f Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Fri, 19 Dec 2025 17:40:35 -0500 Subject: [PATCH 05/10] feat(time-input): introduce DayPeriod type and update related utilities for time conversion and selection --- .../src/TimeInputInputs/TimeInputInputs.tsx | 4 +- .../src/hooks/useSelectUnit/useSelectUnit.ts | 5 +- packages/time-input/src/shared.types.ts | 10 ++++ .../convert12hTo24h/convert12hTo24h.spec.ts | 34 +++++++++++-- .../utils/convert12hTo24h/convert12hTo24h.ts | 50 ++++++++++--------- .../findUnitOptionByDayPeriod.spec.ts | 9 ++++ .../findUnitOptionByDayPeriod.ts | 10 ++-- .../getNewUTCDateFromSegments.ts | 9 ++-- .../getPaddedTimeSegments.spec.ts | 22 ++++---- .../getPaddedTimeSegments.ts | 6 +-- .../getPaddedTimeSegmentsFromDate.spec.ts | 6 +-- .../getPaddedTimeSegmentsFromDate.ts | 8 +-- packages/time-input/src/utils/index.ts | 3 +- 13 files changed, 114 insertions(+), 62 deletions(-) diff --git a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx index 71f30079a8..2995743652 100644 --- a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx +++ b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx @@ -4,7 +4,7 @@ import { unitOptions } from '../constants'; import { useTimeInputContext } from '../Context/TimeInputContext/TimeInputContext'; import { useTimeInputDisplayContext } from '../Context/TimeInputDisplayContext/TimeInputDisplayContext'; import { useSelectUnit } from '../hooks'; -import { TimeSegmentsState } from '../shared.types'; +import { DayPeriod, TimeSegmentsState } from '../shared.types'; import { TimeFormField, TimeFormFieldInputContainer } from '../TimeFormField'; import { TimeInputBox } from '../TimeInputBox/TimeInputBox'; import { TimeInputSelect } from '../TimeInputSelect/TimeInputSelect'; @@ -52,7 +52,7 @@ export const TimeInputInputs = forwardRef( * // TODO: This is temp and will be replaced in the next PR */ const { selectUnit, setSelectUnit } = useSelectUnit({ - dayPeriod: timeParts.dayPeriod, + dayPeriod: timeParts.dayPeriod as DayPeriod, value, unitOptions, }); diff --git a/packages/time-input/src/hooks/useSelectUnit/useSelectUnit.ts b/packages/time-input/src/hooks/useSelectUnit/useSelectUnit.ts index fa4ab18565..f02c4e7430 100644 --- a/packages/time-input/src/hooks/useSelectUnit/useSelectUnit.ts +++ b/packages/time-input/src/hooks/useSelectUnit/useSelectUnit.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { DateType, isValidDate } from '@leafygreen-ui/date-utils'; +import { DayPeriod } from '../../shared.types'; import { UnitOption } from '../../TimeInputSelect/TimeInputSelect.types'; interface UseSelectUnitReturn { @@ -17,7 +18,7 @@ interface UseSelectUnitReturn { * @returns The select unit option. */ const findSelectUnit = ( - dayPeriod: string, + dayPeriod: DayPeriod, unitOptions: Array, ): UnitOption => { const selectUnitOption = unitOptions.find( @@ -39,7 +40,7 @@ export const useSelectUnit = ({ value, unitOptions, }: { - dayPeriod: string; + dayPeriod: DayPeriod; value: DateType | undefined; unitOptions: Array; }): UseSelectUnitReturn => { diff --git a/packages/time-input/src/shared.types.ts b/packages/time-input/src/shared.types.ts index 5bd1258158..7e55b000db 100644 --- a/packages/time-input/src/shared.types.ts +++ b/packages/time-input/src/shared.types.ts @@ -25,3 +25,13 @@ export const TimeSegment = { export type TimeSegment = (typeof TimeSegment)[keyof typeof TimeSegment]; export type TimeSegmentsState = Record; + +/* + * An enumerable object that maps the day period names to their values + */ +export const DayPeriod = { + AM: 'AM', + PM: 'PM', +} as const; + +export type DayPeriod = (typeof DayPeriod)[keyof typeof DayPeriod]; diff --git a/packages/time-input/src/utils/convert12hTo24h/convert12hTo24h.spec.ts b/packages/time-input/src/utils/convert12hTo24h/convert12hTo24h.spec.ts index 4df48899cb..8105454dd3 100644 --- a/packages/time-input/src/utils/convert12hTo24h/convert12hTo24h.spec.ts +++ b/packages/time-input/src/utils/convert12hTo24h/convert12hTo24h.spec.ts @@ -1,31 +1,57 @@ import range from 'lodash/range'; +import { consoleOnce } from '@leafygreen-ui/lib'; + import { convert12hTo24h } from './convert12hTo24h'; describe('convert12hTo24h', () => { describe('AM', () => { test('12 AM converts to 0', () => { - expect(convert12hTo24h('12', 'AM')).toEqual('0'); + expect(convert12hTo24h(12, 'AM')).toEqual(0); }); test.each(range(1, 12).map(i => [i, i]))( '%i AM converts to %i', (input, expected) => { - expect(convert12hTo24h(`${input}`, 'AM')).toEqual(`${expected}`); + expect(convert12hTo24h(input, 'AM')).toEqual(expected); }, ); }); describe('PM', () => { test('12 PM converts to 12', () => { - expect(convert12hTo24h('12', 'PM')).toEqual('12'); + expect(convert12hTo24h(12, 'PM')).toEqual(12); }); test.each(range(1, 12).map(i => [i, i + 12]))( '%i PM converts to %i', (input, expected) => { - expect(convert12hTo24h(`${input}`, 'PM')).toEqual(`${expected}`); + expect(convert12hTo24h(input, 'PM')).toEqual(expected); }, ); }); + + describe('Invalid hour', () => { + test('less than 1 returns the hour', () => { + const consoleWarnSpy = jest + .spyOn(consoleOnce, 'warn') + .mockImplementation(() => {}); + expect(convert12hTo24h(0, 'AM')).toEqual(0); + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'convert12hTo24h > Invalid hour: 0', + ); + consoleWarnSpy.mockRestore(); + }); + + test('greater than 12 returns the hour', () => { + const consoleWarnSpy = jest + .spyOn(consoleOnce, 'warn') + .mockImplementation(() => {}); + expect(convert12hTo24h(13, 'AM')).toEqual(13); + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'convert12hTo24h > Invalid hour: 13', + ); + consoleWarnSpy.mockRestore(); + }); + }); }); diff --git a/packages/time-input/src/utils/convert12hTo24h/convert12hTo24h.ts b/packages/time-input/src/utils/convert12hTo24h/convert12hTo24h.ts index 53acc9f8ec..1b1d38f9a4 100644 --- a/packages/time-input/src/utils/convert12hTo24h/convert12hTo24h.ts +++ b/packages/time-input/src/utils/convert12hTo24h/convert12hTo24h.ts @@ -1,36 +1,40 @@ +import { consoleOnce } from '@leafygreen-ui/lib'; + +import { DayPeriod } from '../../shared.types'; + /** * Converts a 12 hour format hour to a 24 hour format hour * + * @param hour - The hour to convert + * @param dayPeriod - The day period to use for the conversion (AM or PM) + * @returns The converted hour or the original hour if it is invalid + * * @example * ```js - * convert12hTo24h('12', 'AM'); // '0' - * convert12hTo24h('12', 'PM'); // '12' - * convert12hTo24h('1', 'AM'); // '1' - * convert12hTo24h('1', 'PM'); // '13' + * convert12hTo24h(12, 'AM'); // 0 + * convert12hTo24h(12, 'PM'); // 12 + * convert12hTo24h(1, 'AM'); // 1 + * convert12hTo24h(1, 'PM'); // 13 + * convert12hTo24h(0, 'AM'); // 0 + * convert12hTo24h(13, 'AM'); // 13 * ``` - * - * @param hour - The hour to convert - * @param dayPeriod - The day period to use for the conversion (AM or PM) - * @returns The converted hour */ -export const convert12hTo24h = (hour: string, dayPeriod: string) => { - if (hour === '') return hour; - - // if dayPeriod is AM and hour is 12, return 0 since 12 AM is 00:00 - if (dayPeriod === 'AM') { - if (hour === '12') { - return '0'; - } - - // else return hour as-is +export const convert12hTo24h = (hour: number, dayPeriod: DayPeriod): number => { + if (hour < 1 || hour > 12) { + consoleOnce.warn(`convert12hTo24h > Invalid hour: ${hour}`); return hour; } - // if dayPeriod is PM and hour is 12, return 12 since 12 PM is 12:00 - if (hour === '12') { - return '12'; + if (hour === 12) { + // 12AM -> 0:00 + // 12PM -> 12:00 + return dayPeriod === DayPeriod.AM ? 0 : 12; + } + + // if dayPeriod is PM, return hour + 12 + if (dayPeriod === DayPeriod.PM) { + return hour + 12; } - // else return hour + 12 - return `${parseInt(hour) + 12}`; + return hour; }; diff --git a/packages/time-input/src/utils/findUnitOptionByDayPeriod/findUnitOptionByDayPeriod.spec.ts b/packages/time-input/src/utils/findUnitOptionByDayPeriod/findUnitOptionByDayPeriod.spec.ts index 74142e3d89..f62604d1d6 100644 --- a/packages/time-input/src/utils/findUnitOptionByDayPeriod/findUnitOptionByDayPeriod.spec.ts +++ b/packages/time-input/src/utils/findUnitOptionByDayPeriod/findUnitOptionByDayPeriod.spec.ts @@ -15,4 +15,13 @@ describe('packages/time-input/utils/findUnitOptionByDayPeriod', () => { ]), ).toEqual({ displayName: 'PM', value: 'PM' }); }); + test('returns the first unit option if the day period is not found', () => { + expect( + // @ts-expect-error - invalid day period + findUnitOptionByDayPeriod('', [ + { displayName: 'AM', value: 'AM' }, + { displayName: 'PM', value: 'PM' }, + ]), + ).toEqual({ displayName: 'AM', value: 'AM' }); + }); }); diff --git a/packages/time-input/src/utils/findUnitOptionByDayPeriod/findUnitOptionByDayPeriod.ts b/packages/time-input/src/utils/findUnitOptionByDayPeriod/findUnitOptionByDayPeriod.ts index 91657491a5..474dd14503 100644 --- a/packages/time-input/src/utils/findUnitOptionByDayPeriod/findUnitOptionByDayPeriod.ts +++ b/packages/time-input/src/utils/findUnitOptionByDayPeriod/findUnitOptionByDayPeriod.ts @@ -1,3 +1,4 @@ +import { DayPeriod } from '../../shared.types'; import { UnitOption } from '../../TimeInputSelect/TimeInputSelect.types'; /** @@ -5,14 +6,15 @@ import { UnitOption } from '../../TimeInputSelect/TimeInputSelect.types'; * * @param dayPeriod - The day period to use for the select unit. * @param unitOptions - The valid unit options to use for the select unit. - * @returns The select unit option. + * @returns The select unit option or the first unit option if the day period is not found */ export const findUnitOptionByDayPeriod = ( - dayPeriod: string, + dayPeriod: DayPeriod, unitOptions: Array, ): UnitOption => { const selectUnitOption = unitOptions.find( option => option.displayName === dayPeriod, - ) as UnitOption; - return selectUnitOption; + ); + + return selectUnitOption ?? unitOptions[0]; }; diff --git a/packages/time-input/src/utils/getNewUTCDateFromSegments/getNewUTCDateFromSegments.ts b/packages/time-input/src/utils/getNewUTCDateFromSegments/getNewUTCDateFromSegments.ts index eda07af3a3..3be087a6f2 100644 --- a/packages/time-input/src/utils/getNewUTCDateFromSegments/getNewUTCDateFromSegments.ts +++ b/packages/time-input/src/utils/getNewUTCDateFromSegments/getNewUTCDateFromSegments.ts @@ -1,6 +1,7 @@ import { newUTCFromTimeZone } from '@leafygreen-ui/date-utils'; import { TimeSegmentsState } from '../../shared.types'; +import { DayPeriod } from '../../shared.types'; import { convert12hTo24h } from '../convert12hTo24h/convert12hTo24h'; import { doesSomeSegmentExist } from '../doesSomeSegmentExist/doesSomeSegmentExist'; import { isEverySegmentFilled } from '../isEverySegmentFilled/isEverySegmentFilled'; @@ -30,13 +31,13 @@ export const getNewUTCDateFromSegments = ({ month: string; year: string; }; - dayPeriod: string; + dayPeriod: DayPeriod; }) => { const { day, month, year } = dateValues; const { hour, minute, second } = segments; - const convertedHour = is12HourFormat - ? convert12hTo24h(hour, dayPeriod) + const converted12hTo24hHour = is12HourFormat + ? convert12hTo24h(Number(hour), dayPeriod).toString() : hour; /** @@ -50,7 +51,7 @@ export const getNewUTCDateFromSegments = ({ year, month, day, - hour: convertedHour, + hour: converted12hTo24hHour, minute, second, timeZone, diff --git a/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegments/getPaddedTimeSegments.spec.ts b/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegments/getPaddedTimeSegments.spec.ts index 83da5b974b..37f065a1aa 100644 --- a/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegments/getPaddedTimeSegments.spec.ts +++ b/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegments/getPaddedTimeSegments.spec.ts @@ -1,39 +1,39 @@ -import { getFormattedTimeSegments } from './getFormattedTimeSegments'; +import { getPaddedTimeSegments } from './getPaddedTimeSegments'; -describe('packages/time-input/utils/getFormattedTimeSegments', () => { - test('returns the formatted time segments if all segments are 0', () => { - const formattedTimeSegments = getFormattedTimeSegments({ +describe('packages/time-input/utils/getPaddedTimeSegments', () => { + test('returns the padded time segments if all segments are 0', () => { + const paddedTimeSegments = getPaddedTimeSegments({ hour: '0', minute: '0', second: '0', }); - expect(formattedTimeSegments).toEqual({ + expect(paddedTimeSegments).toEqual({ hour: '00', minute: '00', second: '00', }); }); - test('returns the formatted time segments', () => { - const formattedTimeSegments = getFormattedTimeSegments({ + test('returns the padded time segments', () => { + const paddedTimeSegments = getPaddedTimeSegments({ hour: '2', minute: '3', second: '1', }); - expect(formattedTimeSegments).toEqual({ + expect(paddedTimeSegments).toEqual({ hour: '02', minute: '03', second: '01', }); }); - test('does not format segments that are already formatted', () => { - const formattedTimeSegments = getFormattedTimeSegments({ + test('does not pad segments that are already padded', () => { + const paddedTimeSegments = getPaddedTimeSegments({ hour: '02', minute: '03', second: '01', }); - expect(formattedTimeSegments).toEqual({ + expect(paddedTimeSegments).toEqual({ hour: '02', minute: '03', second: '01', diff --git a/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegments/getPaddedTimeSegments.ts b/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegments/getPaddedTimeSegments.ts index f8f034f5ee..ed92bea8db 100644 --- a/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegments/getPaddedTimeSegments.ts +++ b/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegments/getPaddedTimeSegments.ts @@ -1,6 +1,6 @@ import { getValueFormatter } from '@leafygreen-ui/input-box'; -import { TimeSegmentsState } from '../../shared.types'; +import { TimeSegmentsState } from '../../../shared.types'; /** * Formats the time segments to a string with 2 digits for each segment. @@ -10,11 +10,11 @@ import { TimeSegmentsState } from '../../shared.types'; * * @example * ```js - * getFormattedTimeSegments({ hour: '2', minute: '30', second: '0' }); + * getPaddedTimeSegments({ hour: '2', minute: '30', second: '0' }); * // returns: { hour: '02', minute: '30', second: '00' } * ``` */ -export const getFormattedTimeSegments = (segments: TimeSegmentsState) => { +export const getPaddedTimeSegments = (segments: TimeSegmentsState) => { const hour = getValueFormatter({ charsCount: 2, allowZero: true })( segments.hour, ); diff --git a/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.spec.ts b/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.spec.ts index 86247442db..290f5d8a9d 100644 --- a/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.spec.ts +++ b/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.spec.ts @@ -1,8 +1,8 @@ -import { getFormattedTimeSegmentsFromDate } from './getFormattedTimeSegmentsFromDate'; +import { getPaddedTimeSegmentsFromDate } from './getPaddedTimeSegmentsFromDate'; -describe('packages/time-input/utils/getFormattedTimeSegmentsFromDate', () => { +describe('packages/time-input/utils/getPaddedTimeSegmentsFromDate', () => { test('returns the formatted time segments from a date', () => { - const formattedTimeSegments = getFormattedTimeSegmentsFromDate( + const formattedTimeSegments = getPaddedTimeSegmentsFromDate( new Date('2025-01-01T01:00:00Z'), 'en-US', 'America/New_York', diff --git a/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.ts b/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.ts index ef6692ee20..fc1a5479db 100644 --- a/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.ts +++ b/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.ts @@ -2,7 +2,7 @@ import { DateType, LocaleString } from '@leafygreen-ui/date-utils'; import { TimeSegmentsState } from '../../shared.types'; import { getFormatPartsValues } from '../getFormatPartsValues/getFormatPartsValues'; -import { getFormattedTimeSegments } from '../getFormattedTimeSegments/getFormattedTimeSegments'; +import { getPaddedTimeSegments } from './getPaddedTimeSegments/getPaddedTimeSegments'; /** * Gets the formatted time segments from a date @@ -14,11 +14,11 @@ import { getFormattedTimeSegments } from '../getFormattedTimeSegments/getFormatt * * @example * ```js - * getFormattedTimeSegmentsFromDate(new Date('2025-01-01T12:00:00Z'), 'en-US', 'America/New_York'); + * getPaddedTimeSegmentsFromDate(new Date('2025-01-01T12:00:00Z'), 'en-US', 'America/New_York'); * // returns: { hour: '12', minute: '00', second: '00' } * ``` */ -export const getFormattedTimeSegmentsFromDate = ( +export const getPaddedTimeSegmentsFromDate = ( date: DateType, locale: LocaleString, timeZone: string, @@ -29,5 +29,5 @@ export const getFormattedTimeSegmentsFromDate = ( value: date, }); - return getFormattedTimeSegments({ hour, minute, second }); + return getPaddedTimeSegments({ hour, minute, second }); }; diff --git a/packages/time-input/src/utils/index.ts b/packages/time-input/src/utils/index.ts index e93ddb55d0..9aa49af872 100644 --- a/packages/time-input/src/utils/index.ts +++ b/packages/time-input/src/utils/index.ts @@ -3,8 +3,7 @@ export { doesSomeSegmentExist } from './doesSomeSegmentExist/doesSomeSegmentExis export { findUnitOptionByDayPeriod } from './findUnitOptionByDayPeriod/findUnitOptionByDayPeriod'; export { getFormatParts } from './getFormatParts/getFormatParts'; export { getFormatPartsValues } from './getFormatPartsValues'; -export { getFormattedTimeSegments } from './getFormattedTimeSegments/getFormattedTimeSegments'; -export { getFormattedTimeSegmentsFromDate } from './getFormattedTimeSegmentsFromDate/getFormattedTimeSegmentsFromDate'; +export { getPaddedTimeSegmentsFromDate } from './getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate'; export { getFormatter } from './getFormatter/getFormatter'; export { getNewUTCDateFromSegments } from './getNewUTCDateFromSegments/getNewUTCDateFromSegments'; export { hasDayPeriod } from './hasDayPeriod/hasDayPeriod'; From 36e9fd974ef1817a19ee18b9316ec09289c60b64 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Fri, 19 Dec 2025 18:01:25 -0500 Subject: [PATCH 06/10] feat(date-utils): add isSameUTCDayAndTime utility and corresponding tests for date comparison in UTC --- .../src}/isSameUTCDayAndTime/isSameUTCDayAndTime.spec.ts | 0 .../src}/isSameUTCDayAndTime/isSameUTCDayAndTime.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename packages/{time-input/src/utils => date-utils/src}/isSameUTCDayAndTime/isSameUTCDayAndTime.spec.ts (100%) rename packages/{time-input/src/utils => date-utils/src}/isSameUTCDayAndTime/isSameUTCDayAndTime.ts (100%) diff --git a/packages/time-input/src/utils/isSameUTCDayAndTime/isSameUTCDayAndTime.spec.ts b/packages/date-utils/src/isSameUTCDayAndTime/isSameUTCDayAndTime.spec.ts similarity index 100% rename from packages/time-input/src/utils/isSameUTCDayAndTime/isSameUTCDayAndTime.spec.ts rename to packages/date-utils/src/isSameUTCDayAndTime/isSameUTCDayAndTime.spec.ts diff --git a/packages/time-input/src/utils/isSameUTCDayAndTime/isSameUTCDayAndTime.ts b/packages/date-utils/src/isSameUTCDayAndTime/isSameUTCDayAndTime.ts similarity index 100% rename from packages/time-input/src/utils/isSameUTCDayAndTime/isSameUTCDayAndTime.ts rename to packages/date-utils/src/isSameUTCDayAndTime/isSameUTCDayAndTime.ts From 97e751bc80a7125e16dfc7a7436855bf12806084 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Fri, 19 Dec 2025 18:01:32 -0500 Subject: [PATCH 07/10] feat(date-utils): export isSameUTCDayAndTime utility and remove it from time-input exports --- packages/date-utils/src/index.ts | 1 + packages/date-utils/src/isSameUTCDayAndTime/index.ts | 1 + .../date-utils/src/isSameUTCDayAndTime/isSameUTCDayAndTime.ts | 3 +-- packages/time-input/src/utils/index.ts | 1 - 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 packages/date-utils/src/isSameUTCDayAndTime/index.ts diff --git a/packages/date-utils/src/index.ts b/packages/date-utils/src/index.ts index 124cbc1d5f..9b398f6b5a 100644 --- a/packages/date-utils/src/index.ts +++ b/packages/date-utils/src/index.ts @@ -22,6 +22,7 @@ export { isOnOrBefore } from './isOnOrBefore'; export { isSameTZDay } from './isSameTZDay'; export { isSameTZMonth } from './isSameTZMonth'; export { isSameUTCDay } from './isSameUTCDay'; +export { isSameUTCDayAndTime } from './isSameUTCDayAndTime'; export { isSameUTCMonth } from './isSameUTCMonth'; export { isSameUTCRange } from './isSameUTCRange'; export { isTodayTZ } from './isTodayTZ'; diff --git a/packages/date-utils/src/isSameUTCDayAndTime/index.ts b/packages/date-utils/src/isSameUTCDayAndTime/index.ts new file mode 100644 index 0000000000..4e30a2babb --- /dev/null +++ b/packages/date-utils/src/isSameUTCDayAndTime/index.ts @@ -0,0 +1 @@ +export { isSameUTCDayAndTime } from './isSameUTCDayAndTime'; diff --git a/packages/date-utils/src/isSameUTCDayAndTime/isSameUTCDayAndTime.ts b/packages/date-utils/src/isSameUTCDayAndTime/isSameUTCDayAndTime.ts index 8739d708ad..3b56185890 100644 --- a/packages/date-utils/src/isSameUTCDayAndTime/isSameUTCDayAndTime.ts +++ b/packages/date-utils/src/isSameUTCDayAndTime/isSameUTCDayAndTime.ts @@ -19,7 +19,6 @@ export const isSameUTCDayAndTime = ( day1.getUTCFullYear() === day2.getUTCFullYear() && day1.getUTCHours() === day2.getUTCHours() && day1.getUTCMinutes() === day2.getUTCMinutes() && - day1.getUTCSeconds() === day2.getUTCSeconds() && - day1.getUTCMilliseconds() === day2.getUTCMilliseconds() + day1.getUTCSeconds() === day2.getUTCSeconds() ); }; diff --git a/packages/time-input/src/utils/index.ts b/packages/time-input/src/utils/index.ts index 9aa49af872..7811bf407e 100644 --- a/packages/time-input/src/utils/index.ts +++ b/packages/time-input/src/utils/index.ts @@ -10,5 +10,4 @@ export { hasDayPeriod } from './hasDayPeriod/hasDayPeriod'; export { isEverySegmentFilled } from './isEverySegmentFilled/isEverySegmentFilled'; export { isEverySegmentValid } from './isEverySegmentValid/isEverySegmentValid'; export { isEverySegmentValueExplicit } from './isEverySegmentValueExplicit/isEverySegmentValueExplicit'; -export { isSameUTCDayAndTime } from './isSameUTCDayAndTime/isSameUTCDayAndTime'; export { shouldSetValue } from './shouldSetValue/shouldSetValue'; From 01c6487484ece57826e581cb5c7d3ea88aa15074 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Fri, 19 Dec 2025 18:04:14 -0500 Subject: [PATCH 08/10] fix(time-input): correct import structure for getPaddedTimeSegmentsFromDate utility --- .../getPaddedTimeSegmentsFromDate.ts | 1 + packages/time-input/src/utils/index.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.ts b/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.ts index fc1a5479db..55aaf4b55c 100644 --- a/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.ts +++ b/packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate.ts @@ -2,6 +2,7 @@ import { DateType, LocaleString } from '@leafygreen-ui/date-utils'; import { TimeSegmentsState } from '../../shared.types'; import { getFormatPartsValues } from '../getFormatPartsValues/getFormatPartsValues'; + import { getPaddedTimeSegments } from './getPaddedTimeSegments/getPaddedTimeSegments'; /** diff --git a/packages/time-input/src/utils/index.ts b/packages/time-input/src/utils/index.ts index 7811bf407e..992a154015 100644 --- a/packages/time-input/src/utils/index.ts +++ b/packages/time-input/src/utils/index.ts @@ -3,9 +3,9 @@ export { doesSomeSegmentExist } from './doesSomeSegmentExist/doesSomeSegmentExis export { findUnitOptionByDayPeriod } from './findUnitOptionByDayPeriod/findUnitOptionByDayPeriod'; export { getFormatParts } from './getFormatParts/getFormatParts'; export { getFormatPartsValues } from './getFormatPartsValues'; -export { getPaddedTimeSegmentsFromDate } from './getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate'; export { getFormatter } from './getFormatter/getFormatter'; export { getNewUTCDateFromSegments } from './getNewUTCDateFromSegments/getNewUTCDateFromSegments'; +export { getPaddedTimeSegmentsFromDate } from './getPaddedTimeSegmentsFromDate/getPaddedTimeSegmentsFromDate'; export { hasDayPeriod } from './hasDayPeriod/hasDayPeriod'; export { isEverySegmentFilled } from './isEverySegmentFilled/isEverySegmentFilled'; export { isEverySegmentValid } from './isEverySegmentValid/isEverySegmentValid'; From 373080651bd7eb7d43bab9ea41842807802bb07f Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Fri, 19 Dec 2025 18:24:47 -0500 Subject: [PATCH 09/10] refactor(time-input): update tests for shouldSetValue utility to improve clarity and consistency --- .../getNewUTCDateFromSegments.ts | 2 +- .../shouldSetValue/shouldSetValue.spec.ts | 39 ++++++++++--------- .../utils/shouldSetValue/shouldSetValue.ts | 8 +++- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/packages/time-input/src/utils/getNewUTCDateFromSegments/getNewUTCDateFromSegments.ts b/packages/time-input/src/utils/getNewUTCDateFromSegments/getNewUTCDateFromSegments.ts index 3be087a6f2..073ab2c226 100644 --- a/packages/time-input/src/utils/getNewUTCDateFromSegments/getNewUTCDateFromSegments.ts +++ b/packages/time-input/src/utils/getNewUTCDateFromSegments/getNewUTCDateFromSegments.ts @@ -41,7 +41,7 @@ export const getNewUTCDateFromSegments = ({ : hour; /** - * Check if all segments are filled and valid. If they are, return the UTC date. + * Check if all segments are filled and valid (not necessarily explicit). If they are, return the UTC date. */ if ( isEverySegmentFilled(segments) && diff --git a/packages/time-input/src/utils/shouldSetValue/shouldSetValue.spec.ts b/packages/time-input/src/utils/shouldSetValue/shouldSetValue.spec.ts index 5cbe45ab0a..b3ced7b08d 100644 --- a/packages/time-input/src/utils/shouldSetValue/shouldSetValue.spec.ts +++ b/packages/time-input/src/utils/shouldSetValue/shouldSetValue.spec.ts @@ -82,9 +82,10 @@ describe('packages/time-input/utils/shouldSetValue', () => { }); }); }); + describe('when the date is invalid', () => { describe('should return true', () => { - test('when the date is invalid and the component is dirty', () => { + test('when the component is dirty', () => { const newDate = new Date('invalid'); const segments: TimeSegmentsState = { hour: '01', @@ -100,7 +101,7 @@ describe('packages/time-input/utils/shouldSetValue', () => { expect(shouldSetNewValue).toBe(true); }); - test('when the date is invalid and the component is not dirty and every segment is filled', () => { + test('when the component is not dirty and every segment is filled', () => { const newDate = new Date('invalid'); const segments: TimeSegmentsState = { hour: '01', @@ -118,7 +119,7 @@ describe('packages/time-input/utils/shouldSetValue', () => { }); }); describe('should return false', () => { - test('when the date is invalid and the component is not dirty and not every segment is filled', () => { + test('when the component is not dirty and not every segment is filled', () => { const newDate = new Date('invalid'); const segments: TimeSegmentsState = { hour: '', @@ -135,24 +136,24 @@ describe('packages/time-input/utils/shouldSetValue', () => { expect(shouldSetNewValue).toBe(false); }); }); + }); - describe('when the date is null', () => { - test('should return true', () => { - const newDate = null; - const segments: TimeSegmentsState = { - hour: '01', - minute: '01', - second: '01', - }; - const shouldSetNewValue = shouldSetValue({ - newDate, - isDirty: true, - segments, - is12HourFormat: true, - }); - - expect(shouldSetNewValue).toBe(true); + describe('when the date is null', () => { + test('should return true', () => { + const newDate = null; + const segments: TimeSegmentsState = { + hour: '01', + minute: '01', + second: '01', + }; + const shouldSetNewValue = shouldSetValue({ + newDate, + isDirty: true, + segments, + is12HourFormat: true, }); + + expect(shouldSetNewValue).toBe(true); }); }); }); diff --git a/packages/time-input/src/utils/shouldSetValue/shouldSetValue.ts b/packages/time-input/src/utils/shouldSetValue/shouldSetValue.ts index 48924dd3c9..93a835b805 100644 --- a/packages/time-input/src/utils/shouldSetValue/shouldSetValue.ts +++ b/packages/time-input/src/utils/shouldSetValue/shouldSetValue.ts @@ -10,6 +10,12 @@ import { isEverySegmentValueExplicit } from '../isEverySegmentValueExplicit/isEv /** * Checks if the new date should be set. * + * The date should be set if one of the following conditions is met: + * - The date is null + * - The date is valid and all segments are explicit + * - The date is invalid and the component is dirty + * - The date is invalid, the component is not dirty, and every segment is filled + * * @param newDate - The new date to check * @param isDirty - Whether the component is dirty * @param segments - The segments to check @@ -36,7 +42,7 @@ export const shouldSetValue = ({ }); // If the date is invalid and the component is dirty, it means the user has interacted with the component and the value should be set. - // If the date is invalid and every segment is filled, then the value should be set. + // If the date is invalid and the component is not dirty and every segment is filled, then the value should be set. (This prevents the value from being set on the very first interaction when not all the segments are filled) const isInvalidDateObjectAndDirty = isInvalidDateObject(newDate) && (isDirty || isEverySegmentFilled(segments)); From f3d310284832418d7557998176d06c46a6e5786a Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Fri, 19 Dec 2025 21:03:29 -0500 Subject: [PATCH 10/10] refactor(date-utils): streamline imports in isSameUTCDayAndTime utility for improved clarity --- .../date-utils/src/isSameUTCDayAndTime/isSameUTCDayAndTime.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/date-utils/src/isSameUTCDayAndTime/isSameUTCDayAndTime.ts b/packages/date-utils/src/isSameUTCDayAndTime/isSameUTCDayAndTime.ts index 3b56185890..5314416b18 100644 --- a/packages/date-utils/src/isSameUTCDayAndTime/isSameUTCDayAndTime.ts +++ b/packages/date-utils/src/isSameUTCDayAndTime/isSameUTCDayAndTime.ts @@ -1,4 +1,5 @@ -import { DateType, isValidDate } from '@leafygreen-ui/date-utils'; +import { isValidDate } from '../isValidDate'; +import { DateType } from '../types'; /** * Checks if two dates are the same day and time in UTC.