Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const getSegmentThemeStyles = (theme: Theme) => {
color: ${color[theme].text[Variant.Primary][InteractionState.Default]};

&::placeholder {
color: ${color[theme].text[Variant.Placeholder][
color: ${color[theme].text[Variant.InverseSecondary][
InteractionState.Default
]};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
TimeInputContextProps,
TimeInputProviderProps,
} from './TimeInputContext.types';
import { useTimeInputComponentRefs } from './useTimeInputComponentRefs';

export const TimeInputContext = createContext<TimeInputContextProps>(
{} as TimeInputContextProps,
Expand All @@ -20,6 +21,8 @@ export const TimeInputProvider = ({
setValue: _setValue,
handleValidation: _handleValidation,
}: PropsWithChildren<TimeInputProviderProps>) => {
const refs = useTimeInputComponentRefs();

const setValue = (newVal?: DateType) => {
_setValue(newVal ?? null);
};
Expand All @@ -31,6 +34,7 @@ export const TimeInputProvider = ({
return (
<TimeInputContext.Provider
value={{
refs,
value,
setValue,
handleValidation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { DateType } from '@leafygreen-ui/date-utils';

import { TimeInputProps } from '../../TimeInput/TimeInput.types';

import { TimeInputComponentRefs } from './useTimeInputComponentRefs';

/**
* Context props for the time input
*/
Expand All @@ -20,6 +22,11 @@ export interface TimeInputContextProps {
* calls the `handleValidation` function provided by the consumer
*/
handleValidation: Required<TimeInputProps>['handleValidation'];

/**
* Ref objects for time input segments
*/
refs: TimeInputComponentRefs;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useMemo } from 'react';

import { useDynamicRefs } from '@leafygreen-ui/hooks';

import { SegmentRefs } from '../../shared.types';

export interface TimeInputComponentRefs {
segmentRefs: SegmentRefs;
}

/**
* Creates `ref` objects for time input segments
* @returns A {@link TimeInputComponentRefs} object to keep track of each time input segment
*/
export const useTimeInputComponentRefs = (): TimeInputComponentRefs => {
const getSegmentRef = useDynamicRefs<HTMLInputElement>();

const segmentRefs: SegmentRefs = useMemo(
() => ({
hour: getSegmentRef('hour') || undefined,
minute: getSegmentRef('minute') || undefined,
second: getSegmentRef('second') || undefined,
Comment on lines +20 to +22
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The || undefined is redundant since getSegmentRef already returns a ref object or undefined. Remove the || undefined clauses from all three lines.

Suggested change
hour: getSegmentRef('hour') || undefined,
minute: getSegmentRef('minute') || undefined,
second: getSegmentRef('second') || undefined,
hour: getSegmentRef('hour'),
minute: getSegmentRef('minute'),
second: getSegmentRef('second'),

Copilot uses AI. Check for mistakes.
}),
[getSegmentRef],
);

return {
segmentRefs,
};
};
50 changes: 50 additions & 0 deletions packages/time-input/src/TimeInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ const meta: StoryMetaType<typeof TimeInput> = {
'data-testid',
],
},
generate: {
storyNames: [
'TwelveHourFormat',
'TwentyFourHourFormat',
'WithoutSeconds',
],
combineArgs: {
darkMode: [false, true],
value: [new Date('2026-02-20T04:00:00Z'), undefined],
disabled: [true, false],
size: Object.values(Size),
timeZone: ['UTC', 'America/New_York', 'Europe/London'],
},
},
},
args: {
showSeconds: true,
Expand All @@ -37,6 +51,7 @@ const meta: StoryMetaType<typeof TimeInput> = {
label: 'Time Input',
darkMode: false,
size: Size.Default,
disabled: false,
},
argTypes: {
locale: { control: 'select', options: Object.values(SupportedLocales) },
Expand Down Expand Up @@ -68,11 +83,46 @@ const Template: StoryFn<typeof TimeInput> = props => {
utcTime: time?.toUTCString(),
});
}}
onChange={e => {
console.log('Storybook: onChange ⏰', { value: e.target.value });
}}
/>
<p>Time zone: {props.timeZone}</p>
<p>UTC value: {value?.toUTCString()}</p>
</div>
);
};

export const TwelveHourFormat = Template.bind({});
TwelveHourFormat.parameters = {
generate: {
args: {
locale: SupportedLocales.en_US,
},
},
};

export const TwentyFourHourFormat = Template.bind({});
TwentyFourHourFormat.parameters = {
generate: {
args: {
locale: SupportedLocales.ISO_8601,
},
},
};

export const WithoutSeconds = Template.bind({});
WithoutSeconds.parameters = {
generate: {
args: {
showSeconds: false,
},
},
};

export const LiveExample = Template.bind({});
LiveExample.parameters = {
chromatic: {
disableSnapshot: true,
},
};
13 changes: 10 additions & 3 deletions packages/time-input/src/TimeInputBox/TimeInputBox.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SupportedLocales } from '@leafygreen-ui/date-utils';

import { TimeInputDisplayProvider } from '../Context/TimeInputDisplayContext/TimeInputDisplayContext';
import { TimeInputDisplayProviderProps } from '../Context/TimeInputDisplayContext/TimeInputDisplayContext.types';
import { timeSegmentRefsMock } from '../testing/testUtils';

import { TimeInputBox } from './TimeInputBox';
import { TimeInputBoxProps } from './TimeInputBox.types';
Expand All @@ -22,6 +23,7 @@ const renderTimeInputBox = ({
<TimeInputBox
segments={{ hour: '', minute: '', second: '' }}
setSegment={() => {}}
segmentRefs={timeSegmentRefsMock}
{...props}
/>
</TimeInputDisplayProvider>,
Expand Down Expand Up @@ -133,8 +135,13 @@ describe('packages/time-input/time-input-box', () => {
});

describe('onSegmentChange', () => {
test.todo(
'should call onSegmentChange with the segment name and the value',
);
test('should call onSegmentChange with the segment name and the value', () => {
const onSegmentChange = jest.fn();
const { hourInput } = renderTimeInputBox({ props: { onSegmentChange } });
userEvent.type(hourInput, '1');
expect(onSegmentChange).toHaveBeenCalledWith(
expect.objectContaining({ value: '1' }),
);
});
});
});
20 changes: 19 additions & 1 deletion packages/time-input/src/TimeInputBox/TimeInputBox.types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
import { TimeSegment, TimeSegmentsState } from '../shared.types';
import { SegmentRefs, TimeSegment, TimeSegmentsState } from '../shared.types';
import { TimeInputSegmentChangeEventHandler } from '../TimeInputSegment/TimeInputSegment.types';

export interface TimeInputBoxProps
extends React.ComponentPropsWithoutRef<'div'> {
/**
* The segments of the time input
*/
segments: TimeSegmentsState;

/**
* The function to set a segment
*/
setSegment: (segment: TimeSegment, value: string) => void;

/**
* The function to handle a segment change, but not necessarily a full value
*/
onSegmentChange?: TimeInputSegmentChangeEventHandler;

/**
* The refs for the segments
*/
segmentRefs: SegmentRefs;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import userEvent from '@testing-library/user-event';
import { Month, newUTC, SupportedLocales } from '@leafygreen-ui/date-utils';
import { getTestUtils as getSelectTestUtils } from '@leafygreen-ui/select/testing';

import { TWENTY_FOUR_HOURS_TEXT } from '../constants';
import { TimeInputProvider } from '../Context/TimeInputContext/TimeInputContext';
import { TimeInputProviderProps } from '../Context/TimeInputContext/TimeInputContext.types';
import { TimeInputDisplayProvider } from '../Context/TimeInputDisplayContext/TimeInputDisplayContext';
Expand Down Expand Up @@ -256,16 +257,46 @@ describe('packages/time-input-inputs', () => {
});
});

test('does not render the select when the locale is 24h', () => {
const { queryByTestId } = renderTimeInputInputs({
displayProps: {
locale: SupportedLocales.ISO_8601,
},
describe('24 hour format', () => {
test('does not render the select', () => {
const { queryByTestId } = renderTimeInputInputs({
displayProps: {
locale: SupportedLocales.ISO_8601,
},
});
expect(queryByTestId(lgIds.select)).not.toBeInTheDocument();
});

test('renders 24 Hour label ', () => {
const { getByText } = renderTimeInputInputs({
displayProps: {
locale: SupportedLocales.ISO_8601,
},
});
expect(getByText(TWENTY_FOUR_HOURS_TEXT)).toBeInTheDocument();
});
expect(queryByTestId(lgIds.select)).not.toBeInTheDocument();
});

test.todo('renders 24 Hour label when the locale is 24h');
describe('12 hour format', () => {
test('renders the select', () => {
renderTimeInputInputs({
displayProps: {
locale: SupportedLocales.en_US,
},
});
const selectTestUtils = getSelectTestUtils(lgIds.select);
expect(selectTestUtils.getInput()).toBeInTheDocument();
});

test('does not render 24 Hour label', () => {
const { queryByText } = renderTimeInputInputs({
displayProps: {
locale: SupportedLocales.en_US,
},
});
expect(queryByText(TWENTY_FOUR_HOURS_TEXT)).not.toBeInTheDocument();
});
});
});

describe('Re-rendering', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,45 @@
import { css } from '@leafygreen-ui/emotion';
import { css, cx } from '@leafygreen-ui/emotion';
import { Theme } from '@leafygreen-ui/lib';
import { color } from '@leafygreen-ui/tokens';

export const wrapperBaseStyles = css`
display: flex;
position: relative;
z-index: 0; // Establish new stacking context
const twentyFourHourFormatStyles = css`
align-items: center;
gap: 12px;
`;

export const getWrapperStyles = ({
is12HourFormat,
}: {
is12HourFormat: boolean;
}) =>
cx(
css`
display: flex;
position: relative;
z-index: 0; // Establish new stacking context
`,
{
[twentyFourHourFormatStyles]: !is12HourFormat,
},
);

const getDisabledThemeStyles = (theme: Theme) => css`
color: ${color[theme].text.disabled.default};
`;

export const getTwentyFourHourStyles = ({
theme,
disabled,
}: {
theme: Theme;
disabled: boolean;
}) =>
cx(
css`
color: ${color[theme].text.secondary.default};
white-space: nowrap;
`,
{
[getDisabledThemeStyles(theme)]: disabled,
},
);
Loading
Loading