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,
})
}