From e2a4b0c0c822422dcb2b79a3702561b286c7de54 Mon Sep 17 00:00:00 2001 From: Ronak Patel <84742904+ronaktruss@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:54:26 -0400 Subject: [PATCH 1/3] Add 24h view for time picker --- .../forms/TimePicker/TimePicker.stories.tsx | 4 ++ .../forms/TimePicker/TimePicker.test.tsx | 61 +++++++++++++++++++ .../forms/TimePicker/TimePicker.tsx | 17 +++++- src/components/forms/TimePicker/constants.ts | 11 ++++ src/components/forms/TimePicker/utils.test.ts | 39 ++++++++++++ src/components/forms/TimePicker/utils.ts | 10 ++- src/index.ts | 5 +- 7 files changed, 140 insertions(+), 7 deletions(-) diff --git a/src/components/forms/TimePicker/TimePicker.stories.tsx b/src/components/forms/TimePicker/TimePicker.stories.tsx index a95d46b384..4517dadbf3 100644 --- a/src/components/forms/TimePicker/TimePicker.stories.tsx +++ b/src/components/forms/TimePicker/TimePicker.stories.tsx @@ -47,6 +47,10 @@ export const WithMinAndMaxTimes: Story = { args: { hint: recommendedHintText, minTime: '9:00', maxTime: '17:00' }, } +export const With24HourFormat: Story = { + args: { hint: recommendedHintText, format: '24h' }, +} + export const WithDefaultValue: Story = { args: { hint: recommendedHintText, defaultValue: '12:00' }, } diff --git a/src/components/forms/TimePicker/TimePicker.test.tsx b/src/components/forms/TimePicker/TimePicker.test.tsx index 8679cd7d28..b57532fbf6 100644 --- a/src/components/forms/TimePicker/TimePicker.test.tsx +++ b/src/components/forms/TimePicker/TimePicker.test.tsx @@ -44,6 +44,67 @@ describe('TimePicker Component', () => { expect(comboBoxDropdownList.children.length).toEqual(7) }) + it('displays military time labels without am/pm when format is 24h', () => { + const { getByTestId } = render( + + ) + + const comboBoxDropdownList = getByTestId('combo-box-option-list') + expect(within(comboBoxDropdownList).getByText('12:00')).toBeInTheDocument() + expect(within(comboBoxDropdownList).getByText('13:00')).toBeInTheDocument() + expect(within(comboBoxDropdownList).getByText('14:00')).toBeInTheDocument() + expect( + within(comboBoxDropdownList).queryByText(/am|pm/i) + ).not.toBeInTheDocument() + }) + + it('filters on the minute as soon as a colon and digit are typed in 24h format', async () => { + const { getByTestId } = render() + + const comboBoxTextInput = getByTestId('combo-box-input') + const ninePm = getByTestId('combo-box-option-21:00') + const ninethirtyPm = getByTestId('combo-box-option-21:30') + + await userEvent.click(comboBoxTextInput) + + // A single minute digit picks the matching half hour + await userEvent.type(comboBoxTextInput, '21:0') + expect(ninePm).toHaveClass('usa-combo-box__list-option--focused') + expect(ninethirtyPm).not.toHaveClass( + 'usa-combo-box__list-option--focused' + ) + + await userEvent.clear(comboBoxTextInput) + await userEvent.type(comboBoxTextInput, '21:3') + expect(ninethirtyPm).toHaveClass('usa-combo-box__list-option--focused') + expect(ninePm).not.toHaveClass('usa-combo-box__list-option--focused') + }) + + it('filters midnight and tolerates a leading zero in 24h format', async () => { + const { getByTestId } = render() + + const comboBoxTextInput = getByTestId('combo-box-input') + const midnight = getByTestId('combo-box-option-00:00') + const nineAm = getByTestId('combo-box-option-09:00') + + await userEvent.click(comboBoxTextInput) + + // A single "0" focuses midnight + await userEvent.type(comboBoxTextInput, '0') + expect(midnight).toHaveClass('usa-combo-box__list-option--focused') + + // A single-digit hour still matches its zero-padded option + await userEvent.clear(comboBoxTextInput) + await userEvent.type(comboBoxTextInput, '9') + expect(nineAm).toHaveClass('usa-combo-box__list-option--focused') + }) + it('renders a label', () => { const { queryByText } = render( diff --git a/src/components/forms/TimePicker/TimePicker.tsx b/src/components/forms/TimePicker/TimePicker.tsx index 561aefb3b6..811af4ce20 100644 --- a/src/components/forms/TimePicker/TimePicker.tsx +++ b/src/components/forms/TimePicker/TimePicker.tsx @@ -10,10 +10,15 @@ import { DEFAULT_MIN_TIME, DEFAULT_MIN_TIME_MINUTES, DEFAULT_STEP, + DEFAULT_TIME_FORMAT, MIN_STEP, TIME_PICKER_CUSTOM_FILTER, + TIME_PICKER_CUSTOM_FILTER_24H, + TimePickerFormat, } from './constants' +export type { TimePickerFormat } + type BaseTimePickerProps = { id: string name: string @@ -24,6 +29,8 @@ type BaseTimePickerProps = { minTime?: string maxTime?: string step?: number + /** Display times in 12-hour (e.g. "1:00pm") or 24-hour military format (e.g. "13:00"). Defaults to "12h". */ + format?: TimePickerFormat /** Recommended text: "Select a time from the dropdown. Type into the input to filter options." */ hint?: string className?: string @@ -42,6 +49,7 @@ export const TimePicker = ({ minTime = DEFAULT_MIN_TIME, maxTime = DEFAULT_MAX_TIME, step = DEFAULT_STEP, + format = DEFAULT_TIME_FORMAT, hint, className, }: TimePickerProps): JSX.Element => { @@ -51,10 +59,13 @@ export const TimePicker = ({ const parsedMaxTime = parseTimeString(maxTime) || DEFAULT_MAX_TIME_MINUTES const validStep = step < MIN_STEP ? MIN_STEP : step const timeOptions = useMemo( - () => getTimeOptions(parsedMinTime, parsedMaxTime, validStep), - [minTime, maxTime, step] + () => getTimeOptions(parsedMinTime, parsedMaxTime, validStep, format), + [minTime, maxTime, step, format] ) + const customFilter = + format === '24h' ? TIME_PICKER_CUSTOM_FILTER_24H : TIME_PICKER_CUSTOM_FILTER + const labelId = `${name}-label` const hintId = `${name}-hint` @@ -76,7 +87,7 @@ export const TimePicker = ({ defaultValue={defaultValue} options={timeOptions} disabled={disabled} - customFilter={TIME_PICKER_CUSTOM_FILTER} + customFilter={customFilter} disableFiltering /> diff --git a/src/components/forms/TimePicker/constants.ts b/src/components/forms/TimePicker/constants.ts index 84b36e182a..0e44ce7b5e 100644 --- a/src/components/forms/TimePicker/constants.ts +++ b/src/components/forms/TimePicker/constants.ts @@ -1,5 +1,8 @@ import { CustomizableFilter } from '../ComboBox/ComboBox' +export type TimePickerFormat = '12h' | '24h' + +export const DEFAULT_TIME_FORMAT: TimePickerFormat = '12h' export const DEFAULT_MAX_TIME = '23:59' export const DEFAULT_MAX_TIME_MINUTES = 24 * 60 - 1 export const DEFAULT_MIN_TIME = '00:00' @@ -16,3 +19,11 @@ export const TIME_PICKER_CUSTOM_FILTER: CustomizableFilter = { minuteQueryFilter: '[\\d]+:([0-9]{0,2})', }, } + +export const TIME_PICKER_CUSTOM_FILTER_24H: CustomizableFilter = { + filter: '0?{{ hourQueryFilter }}:{{ minuteQueryFilter }}.*', + extras: { + hourQueryFilter: '([0-9]{1,2})', + minuteQueryFilter: '[\\d]+:([0-9]{0,2})', + }, +} diff --git a/src/components/forms/TimePicker/utils.test.ts b/src/components/forms/TimePicker/utils.test.ts index fbb0cd83c2..ae35c07d55 100644 --- a/src/components/forms/TimePicker/utils.test.ts +++ b/src/components/forms/TimePicker/utils.test.ts @@ -209,6 +209,25 @@ describe('getTimeOptions', () => { ]) }) + it('returns military time labels without am/pm when format is 24h', () => { + const nineAM = parseTimeString('09:00') as number + const fivePM = parseTimeString('17:00') as number + const oneHourInMinutes = 60 + const timeOptions = getTimeOptions(nineAM, fivePM, oneHourInMinutes, '24h') + + expect(timeOptions).toEqual([ + { value: '09:00', label: '09:00' }, + { value: '10:00', label: '10:00' }, + { value: '11:00', label: '11:00' }, + { value: '12:00', label: '12:00' }, + { value: '13:00', label: '13:00' }, + { value: '14:00', label: '14:00' }, + { value: '15:00', label: '15:00' }, + { value: '16:00', label: '16:00' }, + { value: '17:00', label: '17:00' }, + ]) + }) + it('returns the expected list of times options with a custom step size', () => { const fourHoursInMinutes = 4 * 60 const timeOptions = getTimeOptions( @@ -245,6 +264,26 @@ describe('getTimeOptions', () => { ]) }) + it('returns 24 hour formatted labels when format is 24', () => { + const nineAM = parseTimeString('09:00') as number + const sixPM = parseTimeString('18:00') as number + const oneHourInMinutes = 60 + const timeOptions = getTimeOptions(nineAM, sixPM, oneHourInMinutes, '24h') + + expect(timeOptions).toEqual([ + { value: '09:00', label: '09:00' }, + { value: '10:00', label: '10:00' }, + { value: '11:00', label: '11:00' }, + { value: '12:00', label: '12:00' }, + { value: '13:00', label: '13:00' }, + { value: '14:00', label: '14:00' }, + { value: '15:00', label: '15:00' }, + { value: '16:00', label: '16:00' }, + { value: '17:00', label: '17:00' }, + { value: '18:00', label: '18:00' }, + ]) + }) + it('returns the expected list of times options with custom min and max times', () => { const nineAM = parseTimeString('09:00') as number const fivePM = parseTimeString('17:00') as number diff --git a/src/components/forms/TimePicker/utils.ts b/src/components/forms/TimePicker/utils.ts index 643b3ed09b..48c7a96409 100644 --- a/src/components/forms/TimePicker/utils.ts +++ b/src/components/forms/TimePicker/utils.ts @@ -1,4 +1,5 @@ import { ComboBoxOption } from '../ComboBox/ComboBox' +import type { TimePickerFormat } from './TimePicker' /** * Parse a string of hh:mm into minutes @@ -53,7 +54,8 @@ const padZeros = (value: number, length: number): string => { export const getTimeOptions = ( minTimeMinutes: number, maxTimeMinutes: number, - step: number + step: number, + format: TimePickerFormat = '12h' ): ComboBoxOption[] => { const timeOptions: ComboBoxOption[] = [] @@ -63,10 +65,12 @@ export const getTimeOptions = ( minutes += step ) { const { minute, hour24, hour12, ampm } = getTimeContext(minutes) + const value = `${padZeros(hour24, 2)}:${padZeros(minute, 2)}` timeOptions.push({ - value: `${padZeros(hour24, 2)}:${padZeros(minute, 2)}`, - label: `${hour12}:${padZeros(minute, 2)}${ampm}`, + value, + label: + format === '24h' ? value : `${hour12}:${padZeros(minute, 2)}${ampm}`, }) } diff --git a/src/index.ts b/src/index.ts index e050483d14..d591b5a3a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -145,7 +145,10 @@ export type { TextInputProps } from './components/forms/TextInput/TextInput' export { TextInputMask } from './components/forms/TextInputMask/TextInputMask' export type { TextInputMaskProps } from './components/forms/TextInputMask/TextInputMask' export { TimePicker } from './components/forms/TimePicker/TimePicker' -export type { TimePickerProps } from './components/forms/TimePicker/TimePicker' +export type { + TimePickerProps, + TimePickerFormat, +} from './components/forms/TimePicker/TimePicker' export { ValidationChecklist } from './components/forms/Validation/ValidationChecklist' export type { ValidationChecklistProps } from './components/forms/Validation/ValidationChecklist' export { ValidationItem } from './components/forms/Validation/ValidationItem' From baa4c28b8d2af50313d969240d266711ac9c93df Mon Sep 17 00:00:00 2001 From: Ronak Patel <84742904+ronaktruss@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:15:05 -0400 Subject: [PATCH 2/3] Update formatting and cleanup --- src/components/forms/TimePicker/TimePicker.test.tsx | 6 ++---- src/components/forms/TimePicker/TimePicker.tsx | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/forms/TimePicker/TimePicker.test.tsx b/src/components/forms/TimePicker/TimePicker.test.tsx index b57532fbf6..03c7d755c8 100644 --- a/src/components/forms/TimePicker/TimePicker.test.tsx +++ b/src/components/forms/TimePicker/TimePicker.test.tsx @@ -44,7 +44,7 @@ describe('TimePicker Component', () => { expect(comboBoxDropdownList.children.length).toEqual(7) }) - it('displays military time labels without am/pm when format is 24h', () => { + it('displays 24 hour time labels without am/pm when format is 24h', () => { const { getByTestId } = render( { // A single minute digit picks the matching half hour await userEvent.type(comboBoxTextInput, '21:0') expect(ninePm).toHaveClass('usa-combo-box__list-option--focused') - expect(ninethirtyPm).not.toHaveClass( - 'usa-combo-box__list-option--focused' - ) + expect(ninethirtyPm).not.toHaveClass('usa-combo-box__list-option--focused') await userEvent.clear(comboBoxTextInput) await userEvent.type(comboBoxTextInput, '21:3') diff --git a/src/components/forms/TimePicker/TimePicker.tsx b/src/components/forms/TimePicker/TimePicker.tsx index 811af4ce20..6a47bed07a 100644 --- a/src/components/forms/TimePicker/TimePicker.tsx +++ b/src/components/forms/TimePicker/TimePicker.tsx @@ -29,7 +29,7 @@ type BaseTimePickerProps = { minTime?: string maxTime?: string step?: number - /** Display times in 12-hour (e.g. "1:00pm") or 24-hour military format (e.g. "13:00"). Defaults to "12h". */ + /** Display times in 12-hour (e.g. "1:00pm") or 24-hour format (e.g. "13:00"). Defaults to "12h". */ format?: TimePickerFormat /** Recommended text: "Select a time from the dropdown. Type into the input to filter options." */ hint?: string From 2c1a66867e9547b0de25ed69817aa4b2f22dc2aa Mon Sep 17 00:00:00 2001 From: Ronak Patel <84742904+ronaktruss@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:16:10 -0400 Subject: [PATCH 3/3] Minor refactor and restructring code to handle feedback --- src/components/forms/TimePicker/TimePicker.tsx | 4 +--- src/components/forms/TimePicker/constants.ts | 3 +-- src/components/forms/TimePicker/utils.ts | 5 +++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/forms/TimePicker/TimePicker.tsx b/src/components/forms/TimePicker/TimePicker.tsx index 6a47bed07a..29fd61e949 100644 --- a/src/components/forms/TimePicker/TimePicker.tsx +++ b/src/components/forms/TimePicker/TimePicker.tsx @@ -14,10 +14,9 @@ import { MIN_STEP, TIME_PICKER_CUSTOM_FILTER, TIME_PICKER_CUSTOM_FILTER_24H, - TimePickerFormat, } from './constants' -export type { TimePickerFormat } +export type TimePickerFormat = '12h' | '24h' type BaseTimePickerProps = { id: string @@ -29,7 +28,6 @@ type BaseTimePickerProps = { minTime?: string maxTime?: string step?: number - /** Display times in 12-hour (e.g. "1:00pm") or 24-hour format (e.g. "13:00"). Defaults to "12h". */ format?: TimePickerFormat /** Recommended text: "Select a time from the dropdown. Type into the input to filter options." */ hint?: string diff --git a/src/components/forms/TimePicker/constants.ts b/src/components/forms/TimePicker/constants.ts index 0e44ce7b5e..d85d987512 100644 --- a/src/components/forms/TimePicker/constants.ts +++ b/src/components/forms/TimePicker/constants.ts @@ -1,6 +1,5 @@ import { CustomizableFilter } from '../ComboBox/ComboBox' - -export type TimePickerFormat = '12h' | '24h' +import type { TimePickerFormat } from './TimePicker' export const DEFAULT_TIME_FORMAT: TimePickerFormat = '12h' export const DEFAULT_MAX_TIME = '23:59' diff --git a/src/components/forms/TimePicker/utils.ts b/src/components/forms/TimePicker/utils.ts index 48c7a96409..4bcf6487ab 100644 --- a/src/components/forms/TimePicker/utils.ts +++ b/src/components/forms/TimePicker/utils.ts @@ -66,11 +66,12 @@ export const getTimeOptions = ( ) { const { minute, hour24, hour12, ampm } = getTimeContext(minutes) const value = `${padZeros(hour24, 2)}:${padZeros(minute, 2)}` + const label = + format === '24h' ? value : `${hour12}:${padZeros(minute, 2)}${ampm}` timeOptions.push({ value, - label: - format === '24h' ? value : `${hour12}:${padZeros(minute, 2)}${ampm}`, + label, }) }