Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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') || 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,
};
};
2 changes: 2 additions & 0 deletions packages/time-input/src/Context/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './TimeInputContext/TimeInputContext';
export * from './TimeInputDisplayContext/TimeInputDisplayContext';
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 @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event';

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 @@ -20,6 +21,7 @@ const renderTimeInputBox = ({
<TimeInputBox
segments={{ hour: '', minute: '', second: '' }}
setSegment={() => {}}
segmentRefs={timeSegmentRefsMock}
{...props}
/>
</TimeInputDisplayProvider>,
Expand Down Expand Up @@ -78,8 +80,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' }),
);
});
});
});
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,7 +1,29 @@
import { TimeSegment, TimeSegmentsState } from '../shared.types';
import {
SegmentRefs,
TimeInputSegmentChangeEventHandler,
TimeSegment,
TimeSegmentsState,
} from '../shared.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