Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
79293d1
feat(time-input): enhance time input components with segment refs and…
shaneeza Dec 14, 2025
6d5f282
feat(time-input): add 24-hour format text constant and update styles …
shaneeza Dec 14, 2025
2c45050
test(time-input): enhance tests for 24-hour and 12-hour format rendering
shaneeza Dec 14, 2025
c680139
Merge branch 'LG-5532/segments-display-values-state' of github.com:mo…
shaneeza Dec 14, 2025
f0f4076
Merge branch 'LG-5532/segments-display-values-state' of github.com:mo…
shaneeza Dec 14, 2025
ae8da18
Merge branch 'LG-5532/segments-display-values-state' of github.com:mo…
shaneeza Dec 15, 2025
3dcea71
feat(time-input): enhance TimeInput component with new story formats …
shaneeza Dec 15, 2025
1ff97c7
merge conflict
shaneeza Dec 19, 2025
ebd417e
refactor(time-input): enhance event handling types and streamline imp…
shaneeza Dec 19, 2025
4b6ebc4
Merge branch 'LG-5532/segments-display-values-state' of github.com:mo…
shaneeza Dec 19, 2025
d4ae168
merge conflict
shaneeza Dec 23, 2025
0b03136
refactor(time-input): enhance TimeFormFieldInputContainer to accept a…
shaneeza Dec 23, 2025
61905a1
merge conflict
shaneeza Dec 30, 2025
38656bc
refactor(time-input): update TimeInputBox test to verify correct segm…
shaneeza Dec 30, 2025
fca14ca
refactor(time-input): remove unnecessary undefined checks in useTimeI…
shaneeza Dec 30, 2025
ee0dfd9
refactor(time-input): add data attributes for input container and enh…
shaneeza Dec 30, 2025
1d44357
merge conflict
shaneeza Dec 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'),
minute: getSegmentRef('minute'),
second: getSegmentRef('second'),
}),
[getSegmentRef],
);

return {
segmentRefs,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ import { TimeFormFieldInputContainerProps } from './TimeFormFieldInputContainer.
export const TimeFormFieldInputContainer = React.forwardRef<
HTMLDivElement,
TimeFormFieldInputContainerProps
>(({ children }: TimeFormFieldInputContainerProps, fwdRef) => {
const { is12HourFormat } = useTimeInputDisplayContext();
>(({ children, ...rest }: TimeFormFieldInputContainerProps, fwdRef) => {
const { is12HourFormat, lgIds } = useTimeInputDisplayContext();

return (
<FormFieldInputContainer
ref={fwdRef}
className={getContainerStyles({ is12HourFormat })}
data-lgid={lgIds.inputContainer}
data-testid={lgIds.inputContainer}
{...rest}
>
{children}
</FormFieldInputContainer>
Expand Down
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 {
TimeInputDisplayProvider,
TimeInputDisplayProviderProps,
} from '../Context';
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 @@ -80,8 +82,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({ segment: 'hour', value: '1' }),
);
});
});
});
24 changes: 23 additions & 1 deletion packages/time-input/src/TimeInputBox/TimeInputBox.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
import { TimeSegment, TimeSegmentsState } from '../shared.types';
import {
SegmentRefs,
TimeInputSegmentChangeEventHandler,
TimeSegment,
TimeSegmentsState,
} from '../shared.types';

export interface TimeInputBoxProps extends React.ComponentPropsWithRef<'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;
}
101 changes: 94 additions & 7 deletions packages/time-input/src/TimeInputInputs/TimeInputInputs.spec.tsx
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 @@ -67,6 +68,9 @@ const renderTimeInputInputs = ({
const secondInput = result.container.querySelector(
'input[aria-label="second"]',
) as HTMLInputElement;
const inputContainer = result.container.querySelector(
`[data-lgid="${lgIds.inputContainer}"]`,
) as HTMLDivElement;

if (!(hourInput && minuteInput && secondInput)) {
throw new Error('Some or all input segments are missing');
Expand All @@ -78,6 +82,7 @@ const renderTimeInputInputs = ({
hourInput,
minuteInput,
secondInput,
inputContainer,
};
};

Expand Down Expand Up @@ -256,16 +261,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 Expand Up @@ -940,4 +975,56 @@ describe('packages/time-input-inputs', () => {
});
});
});

describe('Clicking the input', () => {
test('focuses the hour segment when clicked', async () => {
const { hourInput } = renderTimeInputInputs({
providerProps: { value: null },
});
userEvent.click(hourInput);
expect(hourInput).toHaveFocus();
});

test('focuses the minute segment when clicked', async () => {
const { minuteInput } = renderTimeInputInputs({
providerProps: { value: null },
});
userEvent.click(minuteInput);
expect(minuteInput).toHaveFocus();
});

test('focuses the second segment when clicked', async () => {
const { secondInput } = renderTimeInputInputs({
providerProps: { value: null },
});
userEvent.click(secondInput);
expect(secondInput).toHaveFocus();
});

test('focuses the first segment when all are empty', async () => {
const { hourInput, inputContainer } = renderTimeInputInputs({
providerProps: { value: null },
});
userEvent.click(inputContainer);
expect(hourInput).toHaveFocus();
});

test('focuses the first empty segment when some are empty', async () => {
const { hourInput, minuteInput, inputContainer } = renderTimeInputInputs({
providerProps: { value: null },
});
hourInput.value = '08';
hourInput.blur();
userEvent.click(inputContainer);
expect(minuteInput).toHaveFocus();
});

test('focuses the last segment when all are filled', async () => {
const { inputContainer, secondInput } = renderTimeInputInputs({
providerProps: { value: new Date('2025-01-01T00:00:00Z') },
});
userEvent.click(inputContainer);
expect(secondInput).toHaveFocus();
});
});
});
Loading
Loading