Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
24 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
346cb74
feat(time-input): add getFormattedDateTimeParts utility with tests fo…
shaneeza Dec 19, 2025
31dba0b
refactor(time-input): rename TimeParts to DateTimeParts and update re…
shaneeza Dec 19, 2025
388339e
refactor(time-input): update documentation in getFormattedDateTimePar…
shaneeza Dec 19, 2025
5455489
Merge branch 'shaneeza/segment-logic-integration' of github.com:mongo…
shaneeza Dec 19, 2025
237c4ee
chore(version): update version to 5.2.0
shaneeza Dec 19, 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 @@ -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 is12hFormat = !!hasDayPeriod(providerValue.locale);
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
const is12hFormat = !!hasDayPeriod(providerValue.locale);
const is12hFormat = 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,
is12hFormat,
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
*/
is12hFormat: 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: () => {},
is12hFormat: false,
showSeconds: true,
};
37 changes: 35 additions & 2 deletions packages/time-input/src/TimeInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,52 @@
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',
},
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'),
);

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

export const LiveExample = Template.bind({});
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
29 changes: 23 additions & 6 deletions packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ 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';
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: we could add barrel files to clean up this line and the imports in the following lines

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ended up updating this in the last PR in this train

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 +18,39 @@ import { TimeInputInputsProps } from './TimeInputInputs.types';
*/
export const TimeInputInputs = forwardRef<HTMLDivElement, TimeInputInputsProps>(
(_props: TimeInputInputsProps, forwardedRef) => {
const { is12hFormat, timeZone, locale } = useTimeInputDisplayContext();
const [selectUnit, setSelectUnit] = useState<UnitOption>(unitOptions[0]);

const { value } = useTimeInputContext();

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

const timeParts = getFormatPartsValues({
Copy link
Collaborator

Choose a reason for hiding this comment

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

rm?

locale: locale,
timeZone: timeZone,
value: value,
});

// eslint-disable-next-line no-console
console.log('timeParts 🍎🍎🍎', timeParts);
Copy link
Collaborator

Choose a reason for hiding this comment

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

rm


// 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);
}}
/>
{is12hFormat && (
<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 = {

Copy link
Collaborator

Choose a reason for hiding this comment

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

What's the reasoning for not including Date* in this? Otherwise, I don't think I would expect month, day, and year to be included

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> = [
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are there any formats that we support that have a different order? Will we need to use the formatter here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Based on my research, I don't think the order will change. The only thing that can change is that some locales don't use :, like fi-FI, which uses . or fr-FR, which uses h but I chatted with Sooa about this and we're always going to show :. This is similar to what material ui does. Material ui always shows : but you can override it with a format prop. However, if we want to support different literals, it should be easy to update this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

hmm seems okay since these are our SupportedLocales

export const SupportedLocales = {
  ISO_8601: 'iso-8601',
  en_US: 'en-US',
  en_GB: 'en-GB',
} as const;

Although it is a bit sus that our list is so short... I would expect us to have much more supported locales. Is this the latest list based off product requirements?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@TheSonOfThomp do you remember why we only support these? My guess is that we would have to add tests for every locale, and there are hundreds of locales?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll merge this in and can address it in a new PR if we end up wanting to make changes.

{ type: 'hour', value: '' },
{ type: 'literal', value: ':' },
{ type: 'minute', value: '' },
...(showSeconds
? ([
{ type: 'literal', value: ':' },
{ type: 'second', value: '' },
] as Array<Intl.DateTimeFormatPart>)
: []),
];

return formatParts;
};
Loading
Loading