Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bf86e07
feat(time-input): add utils for toggling select and formatparts
shaneeza Nov 26, 2025
612b474
wip
shaneeza Nov 30, 2025
a578ecb
refactor(time-input): enhance time formatting utilities and improve d…
shaneeza Dec 1, 2025
804cd88
refactor(time-input): improve time input handling and formatting logic
shaneeza Dec 1, 2025
206e145
temp remove ts check
shaneeza Dec 1, 2025
9715407
refactor(time-input): simplify getFormatParts function by removing lo…
shaneeza Dec 2, 2025
dad7b62
test(time-input): add unit tests for getFormatParts utility to verify…
shaneeza Dec 2, 2025
f559593
refactor(time-input): remove locale dependency from formatParts in Ti…
shaneeza Dec 2, 2025
3e6f6be
feat(time-input): add default time parts and enhance time input conte…
shaneeza Dec 3, 2025
1afe57e
refactor(time-input): rename shouldShowSelect to is12hFormat for clar…
shaneeza Dec 12, 2025
cb84996
refactor(time-input): clean up unused state and comments in TimeInput…
shaneeza Dec 12, 2025
2547fdc
refactor(time-input): remove unused console logs and simplify time fo…
shaneeza Dec 12, 2025
94ed98f
refactor(time-input): remove outdated TODO comment in TimeInputContex…
shaneeza Dec 12, 2025
2d2fbd5
Merge branch 'shaneeza/segment-logic-integration' of github.com:mongo…
shaneeza Dec 12, 2025
aa4e7a2
feat(time-input): implement getNonLiteralTimeParts utility function w…
shaneeza Dec 16, 2025
d38ee94
refactor(time-input): rename defaultTimeParts to defaultDateTimeParts…
shaneeza Dec 16, 2025
d57c109
refactor(time-input): simplify import structure for getFormatPartsVal…
shaneeza Dec 16, 2025
aff3376
refactor(time-input): enhance getFormatter utility to conditionally i…
shaneeza Dec 16, 2025
9e343ff
refactor(time-input): consolidate imports for utility functions in Ti…
shaneeza Dec 16, 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 @@ -11,6 +11,7 @@ export const TimeInputContext = createContext<TimeInputContextProps>(
{} as TimeInputContextProps,
);

// TODO: get todays date if value is not provided
/**
* This provider is used for the state context of the TimeInput component
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import React, {
useState,
} from 'react';
import defaults from 'lodash/defaults';
import defaultTo from 'lodash/defaultTo';

import { hasDayPeriod } from '../../utils';
import { getFormatParts } from '../../utils/getFormatParts/getFormatParts';

import {
TimeInputDisplayContextProps,
Expand Down Expand Up @@ -39,6 +43,19 @@ export const TimeInputDisplayProvider = ({

// TODO: min, max helpers

// Determines if the input should show a select for the day period (AM/PM)
const shouldShowSelect = !!hasDayPeriod(providerValue.locale);

// Only used to track the presentation format of the segments, not the value itself
const formatParts = getFormatParts({
showSeconds: providerValue.showSeconds,
});

const timeZone = defaultTo(
providerValue.timeZone,
Intl.DateTimeFormat().resolvedOptions().timeZone,
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this necessary? It looks like since providerValue.timeZone is required, this will always be defined

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good call, i don't need this


return (
<TimeInputDisplayContext.Provider
value={{
Expand All @@ -48,6 +65,9 @@ export const TimeInputDisplayProvider = ({
ariaLabelledbyProp,
isDirty,
setIsDirty,
shouldShowSelect,
formatParts,
timeZone,
}}
>
{children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ export type TimeInputDisplayContextProps = Omit<
* Setter for whether the input has been interacted with
*/
setIsDirty: React.Dispatch<React.SetStateAction<boolean>>;

/**
* Whether the AM/PM select should be shown
*/
shouldShowSelect: boolean;

/**
* An array of {@link Intl.DateTimeFormatPart},
* used to determine the order of segments in the input
*/
formatParts?: Array<Intl.DateTimeFormatPart>;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const displayContextPropNames: Array<DisplayContextPropKeys> = [
'size',
'errorMessage',
'state',
'showSeconds',
];

/**
Expand All @@ -51,4 +52,6 @@ export const defaultTimeInputDisplayContext: TimeInputDisplayContextProps = {
errorMessage: '',
isDirty: false,
setIsDirty: () => {},
shouldShowSelect: false,
showSeconds: true,
};
42 changes: 40 additions & 2 deletions packages/time-input/src/TimeInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,57 @@
import React from 'react';
import React, { useState } from 'react';
import { type StoryMetaType } from '@lg-tools/storybook-utils';
import { StoryFn } from '@storybook/react';

import { DateType, SupportedLocales } from '@leafygreen-ui/date-utils';

import { TimeInput } from '.';

const meta: StoryMetaType<typeof TimeInput> = {
title: 'Components/Inputs/TimeInput',
component: TimeInput,
parameters: {
default: 'LiveExample',
controls: {
exclude: [
'handleValidation',
'initialValue',
'onChange',
'onDateChange',
'onSegmentChange',
'value',
'onTimeChange',
],
},
},
args: {
showSeconds: true,
locale: SupportedLocales.ISO_8601,
timeZone: 'UTC',
// value: new Date('1990-02-20T12:30:00Z'),
},
argTypes: {
locale: { control: 'select', options: Object.values(SupportedLocales) },
timeZone: {
control: 'select',
options: [undefined, 'UTC', 'America/New_York', 'Europe/London'],
},
},
};

export default meta;

const Template: StoryFn<typeof TimeInput> = props => <TimeInput {...props} />;
const Template: StoryFn<typeof TimeInput> = props => {
const [value, setValue] = useState<DateType | undefined>(
new Date('1990-02-20T14:30:50Z'),
);
// const [value, setValue] = useState<DateType | undefined>();
// const [value, setValue] = useState<DateType | undefined>(
// new Date('1990--20T14:30:50Z'),
// );

return (
<TimeInput {...props} value={value} onTimeChange={time => setValue(time)} />
);
};

export const LiveExample = Template.bind({});
2 changes: 2 additions & 0 deletions packages/time-input/src/TimeInput/TimeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export const TimeInput = forwardRef<HTMLDivElement, TimeInputProps>(
initialValueProp,
);

// console.log('🥝', { value: value?.toUTCString() });

/**
* Separate the props that are added to the display context and the props that are added to the component
*/
Expand Down
7 changes: 7 additions & 0 deletions packages/time-input/src/TimeInput/TimeInput.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ export type DisplayTimeInputProps = {
* A message to show in red underneath the input when state is `Error`
*/
errorMessage?: string;

/**
* Whether to show seconds in the input.
*
* @default true
*/
showSeconds?: boolean;
} & DarkModeProps &
AriaLabelPropsWithLabel;

Expand Down
37 changes: 31 additions & 6 deletions packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
// @ts-nocheck
import React, { forwardRef, useState } from 'react';

import { cx } from '@leafygreen-ui/emotion';
import { FormField, FormFieldInputContainer } from '@leafygreen-ui/form-field';

import { unitOptions } from '../constants';
import { useTimeInputContext } from '../Context/TimeInputContext/TimeInputContext';
import { useTimeInputDisplayContext } from '../Context/TimeInputDisplayContext/TimeInputDisplayContext';
import { TimeInputSelect } from '../TimeInputSelect/TimeInputSelect';
import { UnitOption } from '../TimeInputSelect/TimeInputSelect.types';
import { getFormatPartsValues } from '../utils';

import { wrapperBaseStyles } from './TimeInputInputs.styles';
import { TimeInputInputsProps } from './TimeInputInputs.types';
Expand All @@ -15,25 +19,46 @@ import { TimeInputInputsProps } from './TimeInputInputs.types';
*/
export const TimeInputInputs = forwardRef<HTMLDivElement, TimeInputInputsProps>(
(_props: TimeInputInputsProps, forwardedRef) => {
const { shouldShowSelect, formatParts, timeZone, locale } =
useTimeInputDisplayContext();
const [selectUnit, setSelectUnit] = useState<UnitOption>(unitOptions[0]);

const { value } = useTimeInputContext();

const handleSelectChange = (unit: UnitOption) => {
setSelectUnit(unit);
};

const timeParts = getFormatPartsValues({
locale: locale,
timeZone: timeZone,
value: value,
});

// console.log('TimeInputInputs 🍉', {
// shouldShowSelect,
// formatParts,
// timeZone,
// value: value?.toUTCString(),
// timeParts,
// locale,
// });

// TODO: break this out more
return (
<FormField aria-labelledby="temp" label="Time Input" ref={forwardedRef}>
<div className={cx(wrapperBaseStyles)}>
<FormFieldInputContainer>
<div>TODO: Input segments go here</div>
</FormFieldInputContainer>
<TimeInputSelect
unit={selectUnit.displayName}
onChange={unit => {
handleSelectChange(unit);
}}
/>
{shouldShowSelect && (
<TimeInputSelect
unit={selectUnit.displayName}
onChange={unit => {
handleSelectChange(unit);
}}
/>
)}
</div>
</FormField>
);
Expand Down
12 changes: 12 additions & 0 deletions packages/time-input/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import { TimeParts } from './shared.types';

export const unitOptions = [
{ displayName: 'AM', value: 'AM' },
{ displayName: 'PM', value: 'PM' },
];

export const defaultTimeParts: TimeParts = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
export const defaultTimeParts: TimeParts = {
export const defaultDateTimeParts: TimeParts = {

hour: '',
minute: '',
second: '',
month: '',
day: '',
year: '',
dayPeriod: 'AM',
};
13 changes: 13 additions & 0 deletions packages/time-input/src/shared.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const TimePartKeys = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
export const TimePartKeys = {
export const DateTimePartKeys = {

hour: 'hour',
minute: 'minute',
second: 'second',
month: 'month',
day: 'day',
year: 'year',
dayPeriod: 'dayPeriod',
} as const;

export type TimePartKeys = (typeof TimePartKeys)[keyof typeof TimePartKeys];

export type TimeParts = Record<TimePartKeys, string>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { getFilteredTimeParts } from './getFilteredTimeParts';

describe('packages/time-input/utils/getFilteredTimeParts', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

could include cases for undefined or empty array

test('returns the filtered time parts', () => {
const filteredTimeParts = getFilteredTimeParts({
timeParts: [
{ type: 'hour', value: '12' },
{ type: 'literal', value: ':' },
{ type: 'minute', value: '30' },
],
});
expect(filteredTimeParts).toEqual([
{ type: 'hour', value: '12' },
{ type: 'minute', value: '30' },
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Returns the time parts that are not literals (e.g. ':').
* @param timeParts - The time parts to get the filtered time parts for
* @returns The filtered time parts
*
* @example
* ```js
* getFilteredTimeParts([
* { type: 'hour', value: '12' },
* { type: 'literal', value: ':' },
* { type: 'minute', value: '30' },
* ]);
* // returns: [{ type: 'hour', value: '12' }, { type: 'minute', value: '30' }]
* ```
*/
export const getFilteredTimeParts = ({
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: could more explicitly name this getNonLiteralTimeParts or excludeLiteralParts

Copy link
Collaborator

Choose a reason for hiding this comment

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

since it's only used in getFormatPartsValue, what do you think about bucketing it in that subdir with a barrel file exporting getFormatPartsValues?

timeParts,
}: {
timeParts?: Array<Intl.DateTimeFormatPart>;
}) => {
const filteredTimeParts =
timeParts?.filter(part => part.type !== 'literal') ?? [];

return filteredTimeParts;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getFormatParts } from './getFormatParts';

describe('packages/time-input/utils/getFormatParts', () => {
test('returns the correct format parts without seconds', () => {
const formatParts = getFormatParts({});
expect(formatParts).toEqual([
{ type: 'hour', value: '' },
{ type: 'literal', value: ':' },
{ type: 'minute', value: '' },
]);
});

test('returns the correct format parts with seconds', () => {
const formatParts = getFormatParts({ showSeconds: true });
expect(formatParts).toEqual([
{ type: 'hour', value: '' },
{ type: 'literal', value: ':' },
{ type: 'minute', value: '' },
{ type: 'literal', value: ':' },
{ type: 'second', value: '' },
]);
});
});
43 changes: 43 additions & 0 deletions packages/time-input/src/utils/getFormatParts/getFormatParts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Returns an array of {@link Intl.DateTimeFormatPart} for the provided locale.
*
* Filters out the dayPeriod and the empty literal before it
* since they are not part of the time format parts.
*
* This will return `:` for every literal part regardless of the locale.
*
* @param showSeconds - Whether to show seconds
* @returns The format parts
*
* @example
*
* ```js
* getFormatParts({ showSeconds: true });
*
* // [
* // { type: 'hour', value: '' },
* // { type: 'literal', value: ':' },
* // { type: 'minute', value: '' },
* // { type: 'literal', value: ':' },
* // { type: 'second', value: '' },
* // ]
*/
export const getFormatParts = ({
showSeconds = false,
}: {
showSeconds?: boolean;
}): Array<Intl.DateTimeFormatPart> | undefined => {
const formatParts: Array<Intl.DateTimeFormatPart> = [
{ type: 'hour', value: '' },
{ type: 'literal', value: ':' },
{ type: 'minute', value: '' },
...(showSeconds
? ([
{ type: 'literal', value: ':' },
{ type: 'second', value: '' },
] as Array<Intl.DateTimeFormatPart>)
: []),
];

return formatParts;
};
Loading
Loading