From bf86e0710f9505b9c2d77b76ec980f714239d36c Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Wed, 26 Nov 2025 12:11:54 -0500 Subject: [PATCH 01/49] feat(time-input): add utils for toggling select and formatparts --- .../TimeInputDisplayContext.tsx | 15 +++ .../TimeInputDisplayContext.types.ts | 11 ++ .../TimePickerDisplayContext.utils.ts | 3 + packages/time-input/src/TimeInput.stories.tsx | 29 ++++- .../time-input/src/TimeInput/TimeInput.tsx | 2 + .../src/TimeInput/TimeInput.types.ts | 7 ++ .../src/TimeInputInputs/TimeInputInputs.tsx | 5 + .../getFormatParts/getFormatParts.spec.ts | 0 .../utils/getFormatParts/getFormatParts.ts | 105 ++++++++++++++++++ .../utils/getFormatter/getFormatter.spec.ts | 14 +++ .../src/utils/getFormatter/getFormatter.ts | 33 ++++++ .../utils/hasDayPeriod/hasDayPeriod.spec.ts | 16 +++ .../src/utils/hasDayPeriod/hasDayPeriod.ts | 28 +++++ packages/time-input/src/utils/index.ts | 2 + tools/install/src/ALL_PACKAGES.ts | 1 + 15 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 packages/time-input/src/utils/getFormatParts/getFormatParts.spec.ts create mode 100644 packages/time-input/src/utils/getFormatParts/getFormatParts.ts create mode 100644 packages/time-input/src/utils/getFormatter/getFormatter.spec.ts create mode 100644 packages/time-input/src/utils/getFormatter/getFormatter.ts create mode 100644 packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.spec.ts create mode 100644 packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.ts create mode 100644 packages/time-input/src/utils/index.ts diff --git a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx index 3f069ddaae..5985c4c8e3 100644 --- a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx +++ b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx @@ -11,6 +11,8 @@ import { TimeInputDisplayProviderProps, } from './TimeInputDisplayContext.types'; import { defaultTimeInputDisplayContext } from './TimePickerDisplayContext.utils'; +import { hasDayPeriod } from '../../utils'; +import { getFormatParts } from '../../utils/getFormatParts/getFormatParts'; export const TimeInputDisplayContext = createContext(defaultTimeInputDisplayContext); @@ -39,6 +41,17 @@ 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({ + locale: providerValue.locale, + showSeconds: providerValue.showSeconds, + }); + + // TODO: timezone + return ( {children} diff --git a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.types.ts b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.types.ts index 6a77f2c07c..982fab07a8 100644 --- a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.types.ts +++ b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.types.ts @@ -38,6 +38,17 @@ export type TimeInputDisplayContextProps = Omit< * Setter for whether the input has been interacted with */ setIsDirty: React.Dispatch>; + + /** + * 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; }; /** diff --git a/packages/time-input/src/Context/TimeInputDisplayContext/TimePickerDisplayContext.utils.ts b/packages/time-input/src/Context/TimeInputDisplayContext/TimePickerDisplayContext.utils.ts index 12a26a9e7f..75fbd6f7f2 100644 --- a/packages/time-input/src/Context/TimeInputDisplayContext/TimePickerDisplayContext.utils.ts +++ b/packages/time-input/src/Context/TimeInputDisplayContext/TimePickerDisplayContext.utils.ts @@ -31,6 +31,7 @@ export const displayContextPropNames: Array = [ 'size', 'errorMessage', 'state', + 'showSeconds', ]; /** @@ -51,4 +52,6 @@ export const defaultTimeInputDisplayContext: TimeInputDisplayContextProps = { errorMessage: '', isDirty: false, setIsDirty: () => {}, + shouldShowSelect: false, + showSeconds: true, }; diff --git a/packages/time-input/src/TimeInput.stories.tsx b/packages/time-input/src/TimeInput.stories.tsx index 1b0411f54e..41802d3d40 100644 --- a/packages/time-input/src/TimeInput.stories.tsx +++ b/packages/time-input/src/TimeInput.stories.tsx @@ -1,19 +1,44 @@ -import React from 'react'; +import React, { useState } from 'react'; import { type StoryMetaType } from '@lg-tools/storybook-utils'; import { StoryFn } from '@storybook/react'; import { TimeInput } from '.'; +import { DateType, SupportedLocales } from '@leafygreen-ui/date-utils'; const meta: StoryMetaType = { 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, + }, + argTypes: { + locale: { control: 'select', options: Object.values(SupportedLocales) }, }, }; export default meta; -const Template: StoryFn = props => ; +const Template: StoryFn = props => { + const [value, setValue] = useState(new Date()); + + return ( + setValue(time)} /> + ); +}; export const LiveExample = Template.bind({}); diff --git a/packages/time-input/src/TimeInput/TimeInput.tsx b/packages/time-input/src/TimeInput/TimeInput.tsx index b97481ed36..5339530932 100644 --- a/packages/time-input/src/TimeInput/TimeInput.tsx +++ b/packages/time-input/src/TimeInput/TimeInput.tsx @@ -36,6 +36,8 @@ export const TimeInput = forwardRef( const { darkMode } = useDarkMode(darkModeProp); const baseFontSize = useUpdatedBaseFontSize(basefontSizeProp); + // console.log('🪼', { valueProp }); + const { value, updateValue } = useControlled( valueProp, onChangeProp, diff --git a/packages/time-input/src/TimeInput/TimeInput.types.ts b/packages/time-input/src/TimeInput/TimeInput.types.ts index d784cd3c80..cd9268318a 100644 --- a/packages/time-input/src/TimeInput/TimeInput.types.ts +++ b/packages/time-input/src/TimeInput/TimeInput.types.ts @@ -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; diff --git a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx index 3be13b2b50..ae446a885d 100644 --- a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx +++ b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx @@ -9,6 +9,7 @@ import { UnitOption } from '../TimeInputSelect/TimeInputSelect.types'; import { wrapperBaseStyles } from './TimeInputInputs.styles'; import { TimeInputInputsProps } from './TimeInputInputs.types'; +import { useTimeInputDisplayContext } from '../Context/TimeInputDisplayContext/TimeInputDisplayContext'; /** * @internal @@ -17,6 +18,10 @@ export const TimeInputInputs = forwardRef( (_props: TimeInputInputsProps, forwardedRef) => { const [selectUnit, setSelectUnit] = useState(unitOptions[0]); + const { shouldShowSelect, formatParts } = useTimeInputDisplayContext(); + + console.log('TimeInputInputs ⏰', { shouldShowSelect, formatParts }); + const handleSelectChange = (unit: UnitOption) => { setSelectUnit(unit); }; diff --git a/packages/time-input/src/utils/getFormatParts/getFormatParts.spec.ts b/packages/time-input/src/utils/getFormatParts/getFormatParts.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/time-input/src/utils/getFormatParts/getFormatParts.ts b/packages/time-input/src/utils/getFormatParts/getFormatParts.ts new file mode 100644 index 0000000000..ba0c4fbd35 --- /dev/null +++ b/packages/time-input/src/utils/getFormatParts/getFormatParts.ts @@ -0,0 +1,105 @@ +import { SupportedLocales } from '@leafygreen-ui/date-utils'; +import { getFormatter } from '..'; + +/** A sample date to use for formatting */ +const now = new Date(); + +/** + * Returns the ISO format parts for the given locale + * @param showSeconds - Whether to show seconds + * @returns The ISO format parts + * + * @example + * ```js + * getIsoFormatParts(true); + * + * // [ + * // { type: 'hour', value: '' }, + * // { type: 'literal', value: ':' }, + * // { type: 'minute', value: '' }, + * // { type: 'literal', value: ':' }, + * // { type: 'second', value: '' }, + * // ] + * ``` + */ +const getIsoFormatParts = ( + showSeconds: boolean, +): Array => { + const formatParts: Array = [ + { type: 'hour', value: '' }, + { type: 'literal', value: ':' }, + { type: 'minute', value: '' }, + ...(showSeconds + ? ([ + { type: 'literal', value: ':' }, + { type: 'second', value: '' }, + ] as Array) + : []), + ]; + + return formatParts; +}; + +// TODO: confirm with Sooa if we want to change the presentation value based on the locale. If that is the case then we only need to return a predefined format with seconds or not. +/** + * 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 + * + * @param locale - The locale to get the format parts for + * @param showSeconds - Whether to show seconds + * @returns The format parts + * + * @example + * + * ```js + * getFormatParts({ locale: 'en-US', showSeconds: true }); + * + * // [ + * // { type: 'hour', value: '' }, + * // { type: 'literal', value: ':' }, + * // { type: 'minute', value: '' }, + * // { type: 'literal', value: ':' }, + * // { type: 'second', value: '' }, + * // ] + */ +export const getFormatParts = ({ + locale, + showSeconds = false, +}: { + locale: string; + showSeconds?: boolean; +}): Array | undefined => { + // If the locale is ISO_8601, return the predefined ISO format parts + if (locale === SupportedLocales.ISO_8601) { + return getIsoFormatParts(showSeconds); + } + + // Otherwise, get the formatter and format the date + const formatter = getFormatter({ locale, showSeconds }); + const formatParts = formatter?.formatToParts(now); + + if (!formatParts) return undefined; + + // Find the dayPeriod and the empty literal before it and remove them both from the format parts + const dayPeriodIndex = formatParts.findIndex( + part => part.type === 'dayPeriod', + ); + + // If no dayPeriod found, return the format parts as is + if (dayPeriodIndex === -1) return formatParts; + + // Filter out the dayPeriod and the empty literal before it + const filteredFormatParts = formatParts.filter((part, index) => { + if (part.type === 'dayPeriod') { + return false; + } + if (part.type === 'literal' && index === dayPeriodIndex - 1) { + return false; + } + return true; + }); + + return filteredFormatParts; +}; diff --git a/packages/time-input/src/utils/getFormatter/getFormatter.spec.ts b/packages/time-input/src/utils/getFormatter/getFormatter.spec.ts new file mode 100644 index 0000000000..9f331adfed --- /dev/null +++ b/packages/time-input/src/utils/getFormatter/getFormatter.spec.ts @@ -0,0 +1,14 @@ +import { SupportedLocales } from '@leafygreen-ui/date-utils'; +import { getFormatter } from './getFormatter'; + +describe('packages/time-input/utils/getFormatter', () => { + test('returns a formatter a valid locale', () => { + const formatter = getFormatter({ locale: SupportedLocales.en_US }); + expect(formatter).toBeDefined(); + }); + + test('returns undefined for an invalid locale', () => { + const formatter = getFormatter({ locale: '!!!' }); + expect(formatter).toBeUndefined(); + }); +}); diff --git a/packages/time-input/src/utils/getFormatter/getFormatter.ts b/packages/time-input/src/utils/getFormatter/getFormatter.ts new file mode 100644 index 0000000000..d1f343350f --- /dev/null +++ b/packages/time-input/src/utils/getFormatter/getFormatter.ts @@ -0,0 +1,33 @@ +import { isValidLocale } from '@leafygreen-ui/date-utils'; + +/** + * Returns a formatter for the given locale. If the locale is invalid, returns undefined. + * + * @param locale - The locale to get the formatter for + * @param showSeconds - Whether to show seconds + * @returns A formatter for the given locale + * + * @example + * ```js + * const formatter = getFormatter({ locale: 'en-US' }); + * ``` + */ +export const getFormatter = ({ + locale, + showSeconds = true, +}: { + locale: string; + showSeconds?: boolean; +}) => { + const isValid = isValidLocale(locale); + + if (isValid) { + return new Intl.DateTimeFormat(locale, { + hour: 'numeric', + minute: 'numeric', + second: showSeconds ? 'numeric' : undefined, + }); + } + + return undefined; +}; diff --git a/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.spec.ts b/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.spec.ts new file mode 100644 index 0000000000..bc3fd39314 --- /dev/null +++ b/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.spec.ts @@ -0,0 +1,16 @@ +import { hasDayPeriod } from './hasDayPeriod'; +import { SupportedLocales } from '@leafygreen-ui/date-utils'; + +describe('packages/time-input/utils/hasDayPeriod', () => { + test('returns false for ISO_8601', () => { + expect(hasDayPeriod(SupportedLocales.ISO_8601)).toBe(false); + }); + + test('returns true for en-US', () => { + expect(hasDayPeriod(SupportedLocales.en_US)).toBe(true); + }); + + test('returns false for en-GB', () => { + expect(hasDayPeriod(SupportedLocales.en_GB)).toBe(false); + }); +}); diff --git a/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.ts b/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.ts new file mode 100644 index 0000000000..f4cacb02da --- /dev/null +++ b/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.ts @@ -0,0 +1,28 @@ +import { SupportedLocales } from '@leafygreen-ui/date-utils'; +import { getFormatter } from '../getFormatter/getFormatter'; + +/** + * Checks if the locale has a day period (AM/PM) + * + * @param locale - The locale to check + * @returns Whether the locale has a day period (AM/PM) + * + * @example + * ```js + * hasDayPeriod('en-US'); // true + * hasDayPeriod('en-GB'); // false + * hasDayPeriod('iso-8601'); // false + * ``` + */ +export const hasDayPeriod = (locale: string) => { + // If the locale is ISO_8601, return false + if (locale === SupportedLocales.ISO_8601) return false; + + const formatter = getFormatter({ locale }); + + // Format a sample time and check for dayPeriod (AM/PM) + const parts = formatter?.formatToParts(new Date()); + const hasDayPeriod = parts?.some(part => part.type === 'dayPeriod'); + + return hasDayPeriod; +}; diff --git a/packages/time-input/src/utils/index.ts b/packages/time-input/src/utils/index.ts new file mode 100644 index 0000000000..cff9fee513 --- /dev/null +++ b/packages/time-input/src/utils/index.ts @@ -0,0 +1,2 @@ +export { getFormatter } from './getFormatter/getFormatter'; +export { hasDayPeriod } from './hasDayPeriod/hasDayPeriod'; diff --git a/tools/install/src/ALL_PACKAGES.ts b/tools/install/src/ALL_PACKAGES.ts index aa2376d623..1b0a6f521e 100644 --- a/tools/install/src/ALL_PACKAGES.ts +++ b/tools/install/src/ALL_PACKAGES.ts @@ -8,6 +8,7 @@ export const ALL_PACKAGES = [ '@leafygreen-ui/banner', '@leafygreen-ui/button', '@leafygreen-ui/callout', + '@leafygreen-ui/canvas-header', '@leafygreen-ui/card', '@leafygreen-ui/checkbox', '@leafygreen-ui/chip', From 612b474dcf6d9bf341bbf706ed6baadf01ae4333 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Sun, 30 Nov 2025 14:34:41 -0500 Subject: [PATCH 02/49] wip --- .../TimeInputContext/TimeInputContext.tsx | 1 + .../TimeInputDisplayContext.tsx | 9 +++ packages/time-input/src/TimeInput.stories.tsx | 10 ++- .../time-input/src/TimeInput/TimeInput.tsx | 4 +- .../src/TimeInputInputs/TimeInputInputs.tsx | 22 +++++- .../utils/getFormatParts/getFormatParts.ts | 69 +++++++------------ .../getFormatPartsValues.ts | 30 ++++++++ .../src/utils/getFormatter/getFormatter.ts | 16 ++++- packages/time-input/src/utils/index.ts | 1 + 9 files changed, 112 insertions(+), 50 deletions(-) create mode 100644 packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts diff --git a/packages/time-input/src/Context/TimeInputContext/TimeInputContext.tsx b/packages/time-input/src/Context/TimeInputContext/TimeInputContext.tsx index 56ad9d5616..e94f083493 100644 --- a/packages/time-input/src/Context/TimeInputContext/TimeInputContext.tsx +++ b/packages/time-input/src/Context/TimeInputContext/TimeInputContext.tsx @@ -11,6 +11,7 @@ export const TimeInputContext = createContext( {} as TimeInputContextProps, ); +// TODO: get todays date if value is not provided /** * This provider is used for the state context of the TimeInput component */ diff --git a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx index 5985c4c8e3..5c8b76b20c 100644 --- a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx +++ b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx @@ -13,6 +13,7 @@ import { import { defaultTimeInputDisplayContext } from './TimePickerDisplayContext.utils'; import { hasDayPeriod } from '../../utils'; import { getFormatParts } from '../../utils/getFormatParts/getFormatParts'; +import defaultTo from 'lodash/defaultTo'; export const TimeInputDisplayContext = createContext(defaultTimeInputDisplayContext); @@ -50,6 +51,13 @@ export const TimeInputDisplayProvider = ({ showSeconds: providerValue.showSeconds, }); + const timeZone = defaultTo( + providerValue.timeZone, + Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + + // console.log('🍓', { timeZone }); + // TODO: timezone return ( @@ -63,6 +71,7 @@ export const TimeInputDisplayProvider = ({ setIsDirty, shouldShowSelect, formatParts, + timeZone, }} > {children} diff --git a/packages/time-input/src/TimeInput.stories.tsx b/packages/time-input/src/TimeInput.stories.tsx index 41802d3d40..7ba91b82ec 100644 --- a/packages/time-input/src/TimeInput.stories.tsx +++ b/packages/time-input/src/TimeInput.stories.tsx @@ -25,16 +25,24 @@ const meta: StoryMetaType = { 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 = props => { - const [value, setValue] = useState(new Date()); + const [value, setValue] = useState( + new Date('1990-02-20T14:30:50Z'), + ); return ( setValue(time)} /> diff --git a/packages/time-input/src/TimeInput/TimeInput.tsx b/packages/time-input/src/TimeInput/TimeInput.tsx index 5339530932..261b0e4573 100644 --- a/packages/time-input/src/TimeInput/TimeInput.tsx +++ b/packages/time-input/src/TimeInput/TimeInput.tsx @@ -36,14 +36,14 @@ export const TimeInput = forwardRef( const { darkMode } = useDarkMode(darkModeProp); const baseFontSize = useUpdatedBaseFontSize(basefontSizeProp); - // console.log('🪼', { valueProp }); - const { value, updateValue } = useControlled( valueProp, onChangeProp, 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 */ diff --git a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx index ae446a885d..3d087a3265 100644 --- a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx +++ b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx @@ -10,6 +10,8 @@ import { UnitOption } from '../TimeInputSelect/TimeInputSelect.types'; import { wrapperBaseStyles } from './TimeInputInputs.styles'; import { TimeInputInputsProps } from './TimeInputInputs.types'; import { useTimeInputDisplayContext } from '../Context/TimeInputDisplayContext/TimeInputDisplayContext'; +import { useTimeInputContext } from '../Context/TimeInputContext/TimeInputContext'; +import { getFormatPartsValues } from '../utils'; /** * @internal @@ -18,14 +20,30 @@ export const TimeInputInputs = forwardRef( (_props: TimeInputInputsProps, forwardedRef) => { const [selectUnit, setSelectUnit] = useState(unitOptions[0]); - const { shouldShowSelect, formatParts } = useTimeInputDisplayContext(); + const { shouldShowSelect, formatParts, timeZone, locale, showSeconds } = + useTimeInputDisplayContext(); - console.log('TimeInputInputs ⏰', { shouldShowSelect, formatParts }); + const { value } = useTimeInputContext(); const handleSelectChange = (unit: UnitOption) => { setSelectUnit(unit); }; + const timeParts = getFormatPartsValues({ + locale: locale, + timeZone: timeZone, + value: value, + hasDayPeriod: shouldShowSelect, + }); + + console.log('TimeInputInputs 🍉', { + shouldShowSelect, + formatParts, + timeZone, + value: value?.toUTCString(), + timeParts, + }); + // TODO: break this out more return ( diff --git a/packages/time-input/src/utils/getFormatParts/getFormatParts.ts b/packages/time-input/src/utils/getFormatParts/getFormatParts.ts index ba0c4fbd35..28ea280430 100644 --- a/packages/time-input/src/utils/getFormatParts/getFormatParts.ts +++ b/packages/time-input/src/utils/getFormatParts/getFormatParts.ts @@ -22,23 +22,23 @@ const now = new Date(); * // ] * ``` */ -const getIsoFormatParts = ( - showSeconds: boolean, -): Array => { - const formatParts: Array = [ - { type: 'hour', value: '' }, - { type: 'literal', value: ':' }, - { type: 'minute', value: '' }, - ...(showSeconds - ? ([ - { type: 'literal', value: ':' }, - { type: 'second', value: '' }, - ] as Array) - : []), - ]; +// const getIsoFormatParts = ( +// showSeconds: boolean, +// ): Array => { +// const formatParts: Array = [ +// { type: 'hour', value: '' }, +// { type: 'literal', value: ':' }, +// { type: 'minute', value: '' }, +// ...(showSeconds +// ? ([ +// { type: 'literal', value: ':' }, +// { type: 'second', value: '' }, +// ] as Array) +// : []), +// ]; - return formatParts; -}; +// return formatParts; +// }; // TODO: confirm with Sooa if we want to change the presentation value based on the locale. If that is the case then we only need to return a predefined format with seconds or not. /** @@ -71,35 +71,16 @@ export const getFormatParts = ({ locale: string; showSeconds?: boolean; }): Array | undefined => { - // If the locale is ISO_8601, return the predefined ISO format parts - if (locale === SupportedLocales.ISO_8601) { - return getIsoFormatParts(showSeconds); - } - - // Otherwise, get the formatter and format the date - const formatter = getFormatter({ locale, showSeconds }); - const formatParts = formatter?.formatToParts(now); - - if (!formatParts) return undefined; + const isIsoLocale = locale === SupportedLocales.ISO_8601; - // Find the dayPeriod and the empty literal before it and remove them both from the format parts - const dayPeriodIndex = formatParts.findIndex( - part => part.type === 'dayPeriod', - ); - - // If no dayPeriod found, return the format parts as is - if (dayPeriodIndex === -1) return formatParts; - - // Filter out the dayPeriod and the empty literal before it - const filteredFormatParts = formatParts.filter((part, index) => { - if (part.type === 'dayPeriod') { - return false; - } - if (part.type === 'literal' && index === dayPeriodIndex - 1) { - return false; - } - return true; + const formatter = getFormatter({ + locale: locale, + isIsoLocale, + showSeconds, + options: { hourCycle: 'h23' }, }); - return filteredFormatParts; + const formatParts = formatter?.formatToParts(now); + + return formatParts; }; diff --git a/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts b/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts new file mode 100644 index 0000000000..7f4d14f9bb --- /dev/null +++ b/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts @@ -0,0 +1,30 @@ +import { DateType } from '@leafygreen-ui/date-utils'; +import { getFormatter } from '../getFormatter/getFormatter'; + +export const getFormatPartsValues = ({ + locale, + timeZone, + value, + hasDayPeriod, +}: { + locale: string; + timeZone: string; + value: DateType | undefined; + hasDayPeriod: boolean; +}) => { + // Get the formatter + const formatter = getFormatter({ + locale: locale, + withFullDate: true, + options: { + timeZone: timeZone, + hourCycle: hasDayPeriod ? 'h12' : 'h23', + }, + }); + + // Get the time parts + const timeParts = formatter?.formatToParts(value); + const filteredTimeParts = timeParts?.filter(part => part.type !== 'literal'); + + return filteredTimeParts; +}; diff --git a/packages/time-input/src/utils/getFormatter/getFormatter.ts b/packages/time-input/src/utils/getFormatter/getFormatter.ts index d1f343350f..bcee1b6085 100644 --- a/packages/time-input/src/utils/getFormatter/getFormatter.ts +++ b/packages/time-input/src/utils/getFormatter/getFormatter.ts @@ -15,17 +15,31 @@ import { isValidLocale } from '@leafygreen-ui/date-utils'; export const getFormatter = ({ locale, showSeconds = true, + isIsoLocale = false, + withFullDate = false, + options = {}, }: { locale: string; showSeconds?: boolean; + isIsoLocale?: boolean; + withFullDate?: boolean; + options?: Intl.DateTimeFormatOptions; }) => { const isValid = isValidLocale(locale); - if (isValid) { + if (isValid || isIsoLocale) { return new Intl.DateTimeFormat(locale, { hour: 'numeric', minute: 'numeric', second: showSeconds ? 'numeric' : undefined, + ...(withFullDate + ? { + year: 'numeric', + month: 'numeric', + day: 'numeric', + } + : {}), + ...options, }); } diff --git a/packages/time-input/src/utils/index.ts b/packages/time-input/src/utils/index.ts index cff9fee513..1b9137605c 100644 --- a/packages/time-input/src/utils/index.ts +++ b/packages/time-input/src/utils/index.ts @@ -1,2 +1,3 @@ export { getFormatter } from './getFormatter/getFormatter'; export { hasDayPeriod } from './hasDayPeriod/hasDayPeriod'; +export { getFormatPartsValues } from './getFormatPartsValues/getFormatPartsValues'; From a578ecb50fde454865a9edb92fb47d3baf7626ae Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Mon, 1 Dec 2025 12:58:10 -0500 Subject: [PATCH 03/49] refactor(time-input): enhance time formatting utilities and improve default handling --- packages/time-input/src/TimeInput.stories.tsx | 1 + .../src/TimeInputInputs/TimeInputInputs.tsx | 1 + .../getFormatPartsValues.ts | 71 +++++++++++++++++-- .../src/utils/getFormatter/getFormatter.ts | 18 +++-- 4 files changed, 81 insertions(+), 10 deletions(-) diff --git a/packages/time-input/src/TimeInput.stories.tsx b/packages/time-input/src/TimeInput.stories.tsx index 7ba91b82ec..d106b78e11 100644 --- a/packages/time-input/src/TimeInput.stories.tsx +++ b/packages/time-input/src/TimeInput.stories.tsx @@ -43,6 +43,7 @@ const Template: StoryFn = props => { const [value, setValue] = useState( new Date('1990-02-20T14:30:50Z'), ); + // const [value, setValue] = useState(); return ( setValue(time)} /> diff --git a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx index 3d087a3265..f846120a3d 100644 --- a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx +++ b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx @@ -42,6 +42,7 @@ export const TimeInputInputs = forwardRef( timeZone, value: value?.toUTCString(), timeParts, + locale, }); // TODO: break this out more diff --git a/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts b/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts index 7f4d14f9bb..e51aafe3eb 100644 --- a/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts +++ b/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts @@ -1,5 +1,53 @@ -import { DateType } from '@leafygreen-ui/date-utils'; +import { DateType, isValidDate } from '@leafygreen-ui/date-utils'; import { getFormatter } from '../getFormatter/getFormatter'; +import defaultsDeep from 'lodash/defaultsDeep'; + +const defaultTimeParts: Array = [ + { type: 'hour', value: '' }, + { type: 'minute', value: '' }, + { type: 'second', value: '' }, + { type: 'month', value: '' }, + { type: 'day', value: '' }, + { type: 'year', value: '' }, + { type: 'dayPeriod', value: 'AM' }, +]; + +/** + * Used when the component is uncontrolled, and the value is undefined + * @param timeZone + * @param locale + * @param hasDayPeriod + * @returns + */ +export const getDefaultTimeParts = ( + timeZone: string, + locale: string, + hasDayPeriod: boolean, +) => { + const formatter = getFormatter({ + locale: locale, + withDate: true, + withTime: false, + options: { + timeZone: timeZone, + hourCycle: hasDayPeriod ? 'h12' : 'h23', + }, + }); + + const timeParts = formatter?.formatToParts(new Date()); + const filteredTimeParts = + timeParts?.filter(part => part.type !== 'literal') ?? []; + const mergedTimeParts = defaultsDeep(filteredTimeParts, defaultTimeParts); + + // console.log('🪼getDefaultTimeParts', { + // locale, + // timeZone, + // mergedTimeParts, + // hasDayPeriod, + // }); + + return mergedTimeParts; +}; export const getFormatPartsValues = ({ locale, @@ -12,10 +60,23 @@ export const getFormatPartsValues = ({ value: DateType | undefined; hasDayPeriod: boolean; }) => { + if (!value) return getDefaultTimeParts(timeZone, locale, hasDayPeriod); + + if (!isValidDate(value)) { + return defaultTimeParts; + } + + // console.log('🍓getFormatPartsValues', { + // locale, + // timeZone, + // value, + // hasDayPeriod, + // }); + // Get the formatter const formatter = getFormatter({ locale: locale, - withFullDate: true, + withDate: true, options: { timeZone: timeZone, hourCycle: hasDayPeriod ? 'h12' : 'h23', @@ -24,7 +85,9 @@ export const getFormatPartsValues = ({ // Get the time parts const timeParts = formatter?.formatToParts(value); - const filteredTimeParts = timeParts?.filter(part => part.type !== 'literal'); + const filteredTimeParts: Array = + timeParts?.filter(part => part.type !== 'literal') ?? []; + const mergedTimeParts = defaultsDeep(filteredTimeParts, defaultTimeParts); - return filteredTimeParts; + return mergedTimeParts; }; diff --git a/packages/time-input/src/utils/getFormatter/getFormatter.ts b/packages/time-input/src/utils/getFormatter/getFormatter.ts index bcee1b6085..8bca8a49ce 100644 --- a/packages/time-input/src/utils/getFormatter/getFormatter.ts +++ b/packages/time-input/src/utils/getFormatter/getFormatter.ts @@ -16,23 +16,29 @@ export const getFormatter = ({ locale, showSeconds = true, isIsoLocale = false, - withFullDate = false, + withDate = false, + withTime = true, options = {}, }: { locale: string; showSeconds?: boolean; isIsoLocale?: boolean; - withFullDate?: boolean; + withDate?: boolean; + withTime?: boolean; options?: Intl.DateTimeFormatOptions; }) => { const isValid = isValidLocale(locale); if (isValid || isIsoLocale) { return new Intl.DateTimeFormat(locale, { - hour: 'numeric', - minute: 'numeric', - second: showSeconds ? 'numeric' : undefined, - ...(withFullDate + ...(withTime + ? { + hour: 'numeric', + minute: 'numeric', + second: showSeconds ? 'numeric' : undefined, + } + : {}), + ...(withDate ? { year: 'numeric', month: 'numeric', From 804cd8822452cc7e9aa63e9d45fe51a2ab6aba25 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Mon, 1 Dec 2025 14:42:49 -0500 Subject: [PATCH 04/49] refactor(time-input): improve time input handling and formatting logic --- packages/time-input/src/TimeInput.stories.tsx | 3 + .../src/TimeInputInputs/TimeInputInputs.tsx | 47 +++++++--- .../getFormatPartsValues.ts | 90 ++++++++++++------- 3 files changed, 98 insertions(+), 42 deletions(-) diff --git a/packages/time-input/src/TimeInput.stories.tsx b/packages/time-input/src/TimeInput.stories.tsx index d106b78e11..6a1cd320f9 100644 --- a/packages/time-input/src/TimeInput.stories.tsx +++ b/packages/time-input/src/TimeInput.stories.tsx @@ -44,6 +44,9 @@ const Template: StoryFn = props => { new Date('1990-02-20T14:30:50Z'), ); // const [value, setValue] = useState(); + // const [value, setValue] = useState( + // new Date('1990--20T14:30:50Z'), + // ); return ( setValue(time)} /> diff --git a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx index f846120a3d..972f9a58c2 100644 --- a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx +++ b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, useState } from 'react'; +import React, { forwardRef, useEffect, useState } from 'react'; import { cx } from '@leafygreen-ui/emotion'; import { FormField, FormFieldInputContainer } from '@leafygreen-ui/form-field'; @@ -12,15 +12,14 @@ import { TimeInputInputsProps } from './TimeInputInputs.types'; import { useTimeInputDisplayContext } from '../Context/TimeInputDisplayContext/TimeInputDisplayContext'; import { useTimeInputContext } from '../Context/TimeInputContext/TimeInputContext'; import { getFormatPartsValues } from '../utils'; +import { isValidDate } from '@leafygreen-ui/date-utils'; /** * @internal */ export const TimeInputInputs = forwardRef( (_props: TimeInputInputsProps, forwardedRef) => { - const [selectUnit, setSelectUnit] = useState(unitOptions[0]); - - const { shouldShowSelect, formatParts, timeZone, locale, showSeconds } = + const { shouldShowSelect, formatParts, timeZone, locale } = useTimeInputDisplayContext(); const { value } = useTimeInputContext(); @@ -36,6 +35,32 @@ export const TimeInputInputs = forwardRef( hasDayPeriod: shouldShowSelect, }); + // const { selectUnit, setSelectUnit } = useSelectUnit(timeParts); + + // const useSelectUnit = (timeParts: Array) => { + // const [selectUnit, setSelectUnit] = useState(unitOptions[0]); + // return { selectUnit, setSelectUnit }; + // }; + + // get select unit from time parts + const initialSelectUnitFromTimeParts = timeParts.dayPeriod; + const selectUnitOption = unitOptions.find( + option => option.displayName === initialSelectUnitFromTimeParts, + ); + + const [selectUnit, setSelectUnit] = useState(selectUnitOption); + + useEffect(() => { + if (isValidDate(value)) { + const selectUnitFromTimeParts = timeParts.dayPeriod; + const selectUnitOption = unitOptions.find( + option => option.displayName === selectUnitFromTimeParts, + ); + + setSelectUnit(selectUnitOption); + } + }, [value, selectUnitOption]); + console.log('TimeInputInputs 🍉', { shouldShowSelect, formatParts, @@ -52,12 +77,14 @@ export const TimeInputInputs = forwardRef(
TODO: Input segments go here
- { - handleSelectChange(unit); - }} - /> + {shouldShowSelect && ( + { + handleSelectChange(unit); + }} + /> + )}
); diff --git a/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts b/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts index e51aafe3eb..139e219a0f 100644 --- a/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts +++ b/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts @@ -2,15 +2,52 @@ import { DateType, isValidDate } from '@leafygreen-ui/date-utils'; import { getFormatter } from '../getFormatter/getFormatter'; import defaultsDeep from 'lodash/defaultsDeep'; -const defaultTimeParts: Array = [ - { type: 'hour', value: '' }, - { type: 'minute', value: '' }, - { type: 'second', value: '' }, - { type: 'month', value: '' }, - { type: 'day', value: '' }, - { type: 'year', value: '' }, - { type: 'dayPeriod', value: 'AM' }, -]; +const defaultTimePartsObject: Record = { + hour: '', + minute: '', + second: '', + month: '', + day: '', + year: '', + dayPeriod: 'AM', +}; + +type FormattedTimeParts = + | 'hour' + | 'minute' + | 'second' + | 'month' + | 'day' + | 'year' + | 'dayPeriod'; + +const getFilteredTimeParts = ({ + timeParts, +}: { + timeParts?: Array; +}) => { + const filteredTimeParts = + timeParts?.filter(part => part.type !== 'literal') ?? []; + + return filteredTimeParts; +}; + +const getFormattedAndMergedTimeParts = ( + timeParts: Array, +): Record => { + const formattedTimeParts: Record = + timeParts.reduce((acc, part) => { + acc[part.type as FormattedTimeParts] = part.value; + return acc; + }, {} as Record); + + const mergedTimeParts = defaultsDeep( + formattedTimeParts, + defaultTimePartsObject, + ); + + return mergedTimeParts; +}; /** * Used when the component is uncontrolled, and the value is undefined @@ -35,18 +72,10 @@ export const getDefaultTimeParts = ( }); const timeParts = formatter?.formatToParts(new Date()); - const filteredTimeParts = - timeParts?.filter(part => part.type !== 'literal') ?? []; - const mergedTimeParts = defaultsDeep(filteredTimeParts, defaultTimeParts); + const filteredTimeParts = getFilteredTimeParts({ timeParts }); + const formattedTimeParts = getFormattedAndMergedTimeParts(filteredTimeParts); - // console.log('🪼getDefaultTimeParts', { - // locale, - // timeZone, - // mergedTimeParts, - // hasDayPeriod, - // }); - - return mergedTimeParts; + return formattedTimeParts; }; export const getFormatPartsValues = ({ @@ -63,16 +92,14 @@ export const getFormatPartsValues = ({ if (!value) return getDefaultTimeParts(timeZone, locale, hasDayPeriod); if (!isValidDate(value)) { - return defaultTimeParts; + const formattedTimeParts = getDefaultTimeParts( + timeZone, + locale, + hasDayPeriod, + ); + return formattedTimeParts; } - // console.log('🍓getFormatPartsValues', { - // locale, - // timeZone, - // value, - // hasDayPeriod, - // }); - // Get the formatter const formatter = getFormatter({ locale: locale, @@ -85,9 +112,8 @@ export const getFormatPartsValues = ({ // Get the time parts const timeParts = formatter?.formatToParts(value); - const filteredTimeParts: Array = - timeParts?.filter(part => part.type !== 'literal') ?? []; - const mergedTimeParts = defaultsDeep(filteredTimeParts, defaultTimeParts); + const filteredTimeParts = getFilteredTimeParts({ timeParts }); + const formattedTimeParts = getFormattedAndMergedTimeParts(filteredTimeParts); - return mergedTimeParts; + return formattedTimeParts; }; From 1db42af87a24c32481d54058fd8969a1a16e13e5 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Mon, 1 Dec 2025 15:26:09 -0500 Subject: [PATCH 05/49] feat(time-input): add TimeFormField and TimeFormFieldInputContainer components with context integration --- .../{Index.ts => index.ts} | 2 +- .../TimeFormField/TimeFormField.tsx | 41 +++++++++++++++++++ .../TimeFormField/TimeFormField.types.ts | 5 +++ .../TimeFormFieldInputContainer.styles.ts | 18 ++++++++ .../TimeFormFieldInputContainer.tsx | 39 ++++++++++++++++++ .../TimeFormFieldInputContainer.types.ts | 7 ++++ .../time-input/src/TimeFormField/index.ts | 2 + packages/time-input/src/TimeInput.stories.tsx | 1 + .../src/TimeInputInputs/TimeInputInputs.tsx | 13 +++--- 9 files changed, 120 insertions(+), 8 deletions(-) rename packages/time-input/src/Context/TimeInputDisplayContext/{Index.ts => index.ts} (93%) create mode 100644 packages/time-input/src/TimeFormField/TimeFormField/TimeFormField.tsx create mode 100644 packages/time-input/src/TimeFormField/TimeFormField/TimeFormField.types.ts create mode 100644 packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.styles.ts create mode 100644 packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.tsx create mode 100644 packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.types.ts create mode 100644 packages/time-input/src/TimeFormField/index.ts diff --git a/packages/time-input/src/Context/TimeInputDisplayContext/Index.ts b/packages/time-input/src/Context/TimeInputDisplayContext/index.ts similarity index 93% rename from packages/time-input/src/Context/TimeInputDisplayContext/Index.ts rename to packages/time-input/src/Context/TimeInputDisplayContext/index.ts index 72aea2bac3..32a3fcfd70 100644 --- a/packages/time-input/src/Context/TimeInputDisplayContext/Index.ts +++ b/packages/time-input/src/Context/TimeInputDisplayContext/index.ts @@ -7,4 +7,4 @@ export { type TimeInputDisplayContextProps, type TimeInputDisplayProviderProps, } from './TimeInputDisplayContext.types'; -export { defaultTimeInputDisplayContext } from './TimePickerDisplayContext.utils'; +export { defaultTimeInputDisplayContext } from './TimePickerDisplayContext.utils'; // diff --git a/packages/time-input/src/TimeFormField/TimeFormField/TimeFormField.tsx b/packages/time-input/src/TimeFormField/TimeFormField/TimeFormField.tsx new file mode 100644 index 0000000000..3a6564dda6 --- /dev/null +++ b/packages/time-input/src/TimeFormField/TimeFormField/TimeFormField.tsx @@ -0,0 +1,41 @@ +import React from 'react'; + +import { FormField } from '@leafygreen-ui/form-field'; +import { useTimeInputDisplayContext } from '../../Context/TimeInputDisplayContext'; + +import { TimeFormFieldProps } from './TimeFormField.types'; + +/** + * A wrapper around `FormField` that sets the relevant + * attributes, and styling + */ +export const TimeFormField = React.forwardRef< + HTMLDivElement, + TimeFormFieldProps +>(({ children, ...rest }: TimeFormFieldProps, fwdRef) => { + const { + label, + description, + // stateNotification: { state, message: errorMessage }, + disabled, + size, + shouldShowSelect, + } = useTimeInputDisplayContext(); + + return ( + + {children} + + ); +}); + +TimeFormField.displayName = 'TimeFormField'; diff --git a/packages/time-input/src/TimeFormField/TimeFormField/TimeFormField.types.ts b/packages/time-input/src/TimeFormField/TimeFormField/TimeFormField.types.ts new file mode 100644 index 0000000000..dd27e8d1a9 --- /dev/null +++ b/packages/time-input/src/TimeFormField/TimeFormField/TimeFormField.types.ts @@ -0,0 +1,5 @@ +import { FormFieldProps } from '@leafygreen-ui/form-field'; + +export type TimeFormFieldProps = React.ComponentPropsWithoutRef<'div'> & { + children: FormFieldProps['children']; +}; diff --git a/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.styles.ts b/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.styles.ts new file mode 100644 index 0000000000..79be4e4d30 --- /dev/null +++ b/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.styles.ts @@ -0,0 +1,18 @@ +import { css, cx } from '@leafygreen-ui/emotion'; + +const selectStyles = css` + border-top-right-radius: 0; + border-bottom-right-radius: 0; +`; + +const baseStyles = css` + &:hover, + &:focus-within { + z-index: 1; + } +`; + +export const getContainerStyles = (shouldShowSelect: boolean) => + cx(baseStyles, { + [selectStyles]: shouldShowSelect, + }); diff --git a/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.tsx b/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.tsx new file mode 100644 index 0000000000..64b4ff2756 --- /dev/null +++ b/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +import { FormFieldInputContainer } from '@leafygreen-ui/form-field'; +import { useTimeInputDisplayContext } from '../../Context/TimeInputDisplayContext'; + +import { getContainerStyles } from './TimeFormFieldInputContainer.styles'; +import { TimeFormFieldInputContainerProps } from './TimeFormFieldInputContainer.types'; + +/** + * A wrapper around `FormField` that sets the relevant + * attributes, and styling + */ +export const TimeFormFieldInputContainer = React.forwardRef< + HTMLDivElement, + TimeFormFieldInputContainerProps +>(({ children, onInputClick }: TimeFormFieldInputContainerProps, fwdRef) => { + const { label, ariaLabelProp, ariaLabelledbyProp, shouldShowSelect } = + useTimeInputDisplayContext(); + + return ( + + {children} + + ); +}); + +TimeFormFieldInputContainer.displayName = 'TimeFormFieldInputContainer'; diff --git a/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.types.ts b/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.types.ts new file mode 100644 index 0000000000..48be749fa8 --- /dev/null +++ b/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.types.ts @@ -0,0 +1,7 @@ +import { FormFieldProps } from '@leafygreen-ui/form-field'; + +export type TimeFormFieldInputContainerProps = + React.ComponentPropsWithoutRef<'div'> & { + children: FormFieldProps['children']; + onInputClick?: React.MouseEventHandler; + }; diff --git a/packages/time-input/src/TimeFormField/index.ts b/packages/time-input/src/TimeFormField/index.ts new file mode 100644 index 0000000000..9d4badd451 --- /dev/null +++ b/packages/time-input/src/TimeFormField/index.ts @@ -0,0 +1,2 @@ +export { TimeFormField } from './TimeFormField/TimeFormField'; +export { TimeFormFieldInputContainer } from './TimeFormFieldInputContainer/TimeFormFieldInputContainer'; diff --git a/packages/time-input/src/TimeInput.stories.tsx b/packages/time-input/src/TimeInput.stories.tsx index 6a1cd320f9..1820b867b7 100644 --- a/packages/time-input/src/TimeInput.stories.tsx +++ b/packages/time-input/src/TimeInput.stories.tsx @@ -26,6 +26,7 @@ const meta: StoryMetaType = { showSeconds: true, locale: SupportedLocales.ISO_8601, timeZone: 'UTC', + label: 'Time Input', // value: new Date('1990-02-20T12:30:00Z'), }, argTypes: { diff --git a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx index 972f9a58c2..e8f950c5ee 100644 --- a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx +++ b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx @@ -1,7 +1,6 @@ import React, { forwardRef, useEffect, useState } from 'react'; import { cx } from '@leafygreen-ui/emotion'; -import { FormField, FormFieldInputContainer } from '@leafygreen-ui/form-field'; import { unitOptions } from '../constants'; import { TimeInputSelect } from '../TimeInputSelect/TimeInputSelect'; @@ -13,6 +12,7 @@ import { useTimeInputDisplayContext } from '../Context/TimeInputDisplayContext/T import { useTimeInputContext } from '../Context/TimeInputContext/TimeInputContext'; import { getFormatPartsValues } from '../utils'; import { isValidDate } from '@leafygreen-ui/date-utils'; +import { TimeFormField, TimeFormFieldInputContainer } from '../TimeFormField'; /** * @internal @@ -70,13 +70,12 @@ export const TimeInputInputs = forwardRef( locale, }); - // TODO: break this out more return ( - -
- + +
+
TODO: Input segments go here
- +
{shouldShowSelect && ( ( /> )}
- +
); }, ); From 206e1450c793ccea09ed74d5d9b66e56c2311deb Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Mon, 1 Dec 2025 15:28:18 -0500 Subject: [PATCH 06/49] temp remove ts check --- packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx index 972f9a58c2..183073b036 100644 --- a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx +++ b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import React, { forwardRef, useEffect, useState } from 'react'; import { cx } from '@leafygreen-ui/emotion'; From bb3916dbff6ace6fe2270e455166466622673ffe Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Mon, 1 Dec 2025 15:48:06 -0500 Subject: [PATCH 07/49] refactor(time-input): clean up unused select unit logic and improve state handling in TimeInputInputs component --- .../src/TimeInputInputs/TimeInputInputs.tsx | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx index e8f950c5ee..23eaa28bbf 100644 --- a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx +++ b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx @@ -1,7 +1,5 @@ import React, { forwardRef, useEffect, useState } from 'react'; -import { cx } from '@leafygreen-ui/emotion'; - import { unitOptions } from '../constants'; import { TimeInputSelect } from '../TimeInputSelect/TimeInputSelect'; import { UnitOption } from '../TimeInputSelect/TimeInputSelect.types'; @@ -35,27 +33,22 @@ export const TimeInputInputs = forwardRef( hasDayPeriod: shouldShowSelect, }); - // const { selectUnit, setSelectUnit } = useSelectUnit(timeParts); - - // const useSelectUnit = (timeParts: Array) => { - // const [selectUnit, setSelectUnit] = useState(unitOptions[0]); - // return { selectUnit, setSelectUnit }; - // }; - + // TODO: temporary fix for select unit // get select unit from time parts const initialSelectUnitFromTimeParts = timeParts.dayPeriod; const selectUnitOption = unitOptions.find( option => option.displayName === initialSelectUnitFromTimeParts, - ); + ) as UnitOption; const [selectUnit, setSelectUnit] = useState(selectUnitOption); + // TODO: temporary fix for select unit useEffect(() => { if (isValidDate(value)) { const selectUnitFromTimeParts = timeParts.dayPeriod; const selectUnitOption = unitOptions.find( option => option.displayName === selectUnitFromTimeParts, - ); + ) as UnitOption; setSelectUnit(selectUnitOption); } From df69d2299d644a4d0471f08d1565ccdd2441881c Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Tue, 2 Dec 2025 17:12:59 -0500 Subject: [PATCH 08/49] feat(time-input): enhance TimeInput component with new constants for segment rules and default values --- .../hooks/useDateSegments/useDateSegments.ts | 7 +++ packages/time-input/package.json | 1 + packages/time-input/src/TimeInput.stories.tsx | 1 - .../src/TimeInputBox/TimeInputBox.spec.ts | 0 .../src/TimeInputBox/TimeInputBox.styles.ts | 0 .../src/TimeInputBox/TimeInputBox.tsx | 28 +++++++++ .../src/TimeInputBox/TimeInputBox.types.ts | 10 +++ packages/time-input/src/TimeInputBox/index.ts | 0 .../src/TimeInputInputs/TimeInputInputs.tsx | 61 ++++++++++--------- .../TimeInputSegment/TimeInputSegment.spec.ts | 0 .../TimeInputSegment.styles.ts | 0 .../src/TimeInputSegment/TimeInputSegment.tsx | 33 ++++++++++ .../TimeInputSegment.types.ts | 32 ++++++++++ .../time-input/src/TimeInputSegment/index.ts | 1 + packages/time-input/src/constants.ts | 53 ++++++++++++++++ packages/time-input/src/hooks/index.ts | 1 + .../src/hooks/useSelectUnit/index.ts | 1 + .../src/hooks/useSelectUnit/useSelectUnit.ts | 57 +++++++++++++++++ .../getFormatPartsValues.ts | 8 +-- packages/time-input/tsconfig.json | 3 + 20 files changed, 263 insertions(+), 34 deletions(-) create mode 100644 packages/time-input/src/TimeInputBox/TimeInputBox.spec.ts create mode 100644 packages/time-input/src/TimeInputBox/TimeInputBox.styles.ts create mode 100644 packages/time-input/src/TimeInputBox/TimeInputBox.tsx create mode 100644 packages/time-input/src/TimeInputBox/TimeInputBox.types.ts create mode 100644 packages/time-input/src/TimeInputBox/index.ts create mode 100644 packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.ts create mode 100644 packages/time-input/src/TimeInputSegment/TimeInputSegment.styles.ts create mode 100644 packages/time-input/src/TimeInputSegment/TimeInputSegment.tsx create mode 100644 packages/time-input/src/TimeInputSegment/TimeInputSegment.types.ts create mode 100644 packages/time-input/src/TimeInputSegment/index.ts create mode 100644 packages/time-input/src/hooks/index.ts create mode 100644 packages/time-input/src/hooks/useSelectUnit/index.ts create mode 100644 packages/time-input/src/hooks/useSelectUnit/useSelectUnit.ts diff --git a/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.ts b/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.ts index 41ae0fbabe..df1212df87 100644 --- a/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.ts +++ b/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.ts @@ -54,8 +54,12 @@ export const useDateSegments = ( (isNull(date) || isUndefined(date)) && isValidDate(prevDate); if (hasDateValueChanged || hasDateBeenCleared) { + // This returns a new state object with the updated segments from the new date const newSegments = getFormattedSegmentsFromDate(date); + // Pass the new state and a copy of the previous state to the callback onUpdate?.(newSegments, { ...segments }); + // This updates all segments in the internal state of the hook + // This internally invokes `dateSegmentsReducer` and passes `updateObject` as the second argument. `segments` is the first argument. This updates the internal state of the hook. dispatch(newSegments); } }, [date, onUpdate, prevDate, segments]); @@ -69,8 +73,11 @@ export const useDateSegments = ( // finally, commit the new state const updateObject = { [segment]: value }; + // This returns a new state object with the updated segment const nextState = dateSegmentsReducer(segments, updateObject); + // Pass the new state and a copy of the previous state to the callback onUpdate?.(nextState, { ...segments }, segment); + // This internally invokes `dateSegmentsReducer` and passes `updateObject` as the second argument. `segments` is the first argument. This updates the internal state of the hook. dispatch(updateObject); }; diff --git a/packages/time-input/package.json b/packages/time-input/package.json index 1865ee7b9f..66e5135576 100644 --- a/packages/time-input/package.json +++ b/packages/time-input/package.json @@ -34,6 +34,7 @@ "@leafygreen-ui/emotion": "workspace:^", "@leafygreen-ui/form-field": "workspace:^", "@leafygreen-ui/hooks": "workspace:^", + "@leafygreen-ui/input-box": "workspace:^", "@leafygreen-ui/lib": "workspace:^", "@leafygreen-ui/palette": "workspace:^", "@leafygreen-ui/select": "workspace:^", diff --git a/packages/time-input/src/TimeInput.stories.tsx b/packages/time-input/src/TimeInput.stories.tsx index 1820b867b7..6487bb4d5f 100644 --- a/packages/time-input/src/TimeInput.stories.tsx +++ b/packages/time-input/src/TimeInput.stories.tsx @@ -27,7 +27,6 @@ const meta: StoryMetaType = { locale: SupportedLocales.ISO_8601, timeZone: 'UTC', label: 'Time Input', - // value: new Date('1990-02-20T12:30:00Z'), }, argTypes: { locale: { control: 'select', options: Object.values(SupportedLocales) }, diff --git a/packages/time-input/src/TimeInputBox/TimeInputBox.spec.ts b/packages/time-input/src/TimeInputBox/TimeInputBox.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/time-input/src/TimeInputBox/TimeInputBox.styles.ts b/packages/time-input/src/TimeInputBox/TimeInputBox.styles.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/time-input/src/TimeInputBox/TimeInputBox.tsx b/packages/time-input/src/TimeInputBox/TimeInputBox.tsx new file mode 100644 index 0000000000..73c4aa59a0 --- /dev/null +++ b/packages/time-input/src/TimeInputBox/TimeInputBox.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { TimeInputBoxProps } from './TimeInputBox.types'; + +import { InputBox } from '@leafygreen-ui/input-box'; +import { TimeSegments } from '../TimeInputSegment/TimeInputSegment.types'; +import { timeSegmentRules } from '../constants'; +import { useTimeInputDisplayContext } from '../Context/TimeInputDisplayContext/TimeInputDisplayContext'; +import { TimeInputSegment } from '../TimeInputSegment/TimeInputSegment'; + +export const TimeInputBox = React.forwardRef( + ({ children, ...rest }: TimeInputBoxProps, fwdRef) => { + const { disabled, formatParts, size } = useTimeInputDisplayContext(); + return ( + + ); + }, +); + +TimeInputBox.displayName = 'TimeInputBox'; diff --git a/packages/time-input/src/TimeInputBox/TimeInputBox.types.ts b/packages/time-input/src/TimeInputBox/TimeInputBox.types.ts new file mode 100644 index 0000000000..a0d3837347 --- /dev/null +++ b/packages/time-input/src/TimeInputBox/TimeInputBox.types.ts @@ -0,0 +1,10 @@ +import { + TimeSegments, + TimeSegmentsState, +} from '../TimeInputSegment/TimeInputSegment.types'; // TODO: move to a shared types + +export interface TimeInputBoxProps + extends React.ComponentPropsWithoutRef<'div'> { + segments: TimeSegmentsState; + setSegment: (segment: TimeSegments, value: string) => void; +} diff --git a/packages/time-input/src/TimeInputBox/index.ts b/packages/time-input/src/TimeInputBox/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx index 23eaa28bbf..c934bd316a 100644 --- a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx +++ b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, useEffect, useState } from 'react'; +import React, { forwardRef, useMemo } from 'react'; import { unitOptions } from '../constants'; import { TimeInputSelect } from '../TimeInputSelect/TimeInputSelect'; @@ -9,17 +9,17 @@ import { TimeInputInputsProps } from './TimeInputInputs.types'; import { useTimeInputDisplayContext } from '../Context/TimeInputDisplayContext/TimeInputDisplayContext'; import { useTimeInputContext } from '../Context/TimeInputContext/TimeInputContext'; import { getFormatPartsValues } from '../utils'; -import { isValidDate } from '@leafygreen-ui/date-utils'; import { TimeFormField, TimeFormFieldInputContainer } from '../TimeFormField'; +import { TimeSegmentsState } from '../TimeInputSegment/TimeInputSegment.types'; +import { TimeInputBox } from '../TimeInputBox/TimeInputBox'; +import { useSelectUnit } from '../hooks'; /** * @internal */ export const TimeInputInputs = forwardRef( (_props: TimeInputInputsProps, forwardedRef) => { - const { shouldShowSelect, formatParts, timeZone, locale } = - useTimeInputDisplayContext(); - + const { shouldShowSelect, timeZone, locale } = useTimeInputDisplayContext(); const { value } = useTimeInputContext(); const handleSelectChange = (unit: UnitOption) => { @@ -33,41 +33,44 @@ export const TimeInputInputs = forwardRef( hasDayPeriod: shouldShowSelect, }); - // TODO: temporary fix for select unit - // get select unit from time parts - const initialSelectUnitFromTimeParts = timeParts.dayPeriod; - const selectUnitOption = unitOptions.find( - option => option.displayName === initialSelectUnitFromTimeParts, - ) as UnitOption; - - const [selectUnit, setSelectUnit] = useState(selectUnitOption); + const { hour, minute, second } = timeParts; - // TODO: temporary fix for select unit - useEffect(() => { - if (isValidDate(value)) { - const selectUnitFromTimeParts = timeParts.dayPeriod; - const selectUnitOption = unitOptions.find( - option => option.displayName === selectUnitFromTimeParts, - ) as UnitOption; + /** + * Creates a memoized object of the time segments + */ + const segmentObj: TimeSegmentsState = useMemo( + () => ({ + hour, + minute, + second, + }), + [hour, minute, second], + ); - setSelectUnit(selectUnitOption); - } - }, [value, selectUnitOption]); + /** + * Hook to manage the select unit + */ + const { selectUnit, setSelectUnit } = useSelectUnit({ + dayPeriod: timeParts.dayPeriod, + value, + unitOptions, + }); console.log('TimeInputInputs 🍉', { - shouldShowSelect, - formatParts, - timeZone, value: value?.toUTCString(), - timeParts, - locale, + segmentObj, }); return (
-
TODO: Input segments go here
+ { + console.log({ segment, value }); + }} + />
{shouldShowSelect && ( (({ children, segment, ...rest }: TimeInputSegmentProps, fwdRef) => { + return ( + + ); +}); + +TimeInputSegment.displayName = 'TimeInputSegment'; diff --git a/packages/time-input/src/TimeInputSegment/TimeInputSegment.types.ts b/packages/time-input/src/TimeInputSegment/TimeInputSegment.types.ts new file mode 100644 index 0000000000..f4bbbc4e93 --- /dev/null +++ b/packages/time-input/src/TimeInputSegment/TimeInputSegment.types.ts @@ -0,0 +1,32 @@ +import { + InputSegmentChangeEventHandler, + InputSegmentComponentProps, +} from '@leafygreen-ui/input-box'; +import { keyMap } from '@leafygreen-ui/lib'; + +export const TimeSegments = { + Hour: 'hour', + Minute: 'minute', + Second: 'second', +} as const; + +export type TimeSegments = (typeof TimeSegments)[keyof typeof TimeSegments]; + +export type TimeSegmentsState = Record; + +export interface DateInputSegmentChangeEvent { + segment: TimeSegments; + value: string; + meta?: { + key?: (typeof keyMap)[keyof typeof keyMap]; + [key: string]: any; + }; +} + +export type DateInputSegmentChangeEventHandler = InputSegmentChangeEventHandler< + TimeSegments, + string +>; + +export interface TimeInputSegmentProps + extends InputSegmentComponentProps {} diff --git a/packages/time-input/src/TimeInputSegment/index.ts b/packages/time-input/src/TimeInputSegment/index.ts new file mode 100644 index 0000000000..49345707c0 --- /dev/null +++ b/packages/time-input/src/TimeInputSegment/index.ts @@ -0,0 +1 @@ +export { TimeSegments } from './TimeInputSegment.types'; diff --git a/packages/time-input/src/constants.ts b/packages/time-input/src/constants.ts index afb7f735b4..65d3b2a879 100644 --- a/packages/time-input/src/constants.ts +++ b/packages/time-input/src/constants.ts @@ -1,4 +1,57 @@ +import { TimeSegments } from './TimeInputSegment/TimeInputSegment.types'; + export const unitOptions = [ { displayName: 'AM', value: 'AM' }, { displayName: 'PM', value: 'PM' }, ]; + +export const timeSegmentRules = { + [TimeSegments.Hour]: { + maxChars: 2, + minExplicitValue: 2, // TODO: this depends on 12/24h format + }, + [TimeSegments.Minute]: { + maxChars: 2, + minExplicitValue: 6, + }, + [TimeSegments.Second]: { + maxChars: 2, + minExplicitValue: 6, + }, +}; + +/** + * The minimum number for each segment + */ +export const defaultMin = { + hour: 0, // TODO: this depends on 12/24h format + minute: 0, + second: 0, +} as const; + +/** + * The maximum number for each segment + */ +export const defaultMax = { + hour: 23, + minute: 59, + second: 59, +} as const; + +/** + * The default placeholders for each segment + */ +export const defaultPlaceholder = { + hour: 'HH', + minute: 'MM', + second: 'SS', +}; + +/** + * The number of characters per input segment + */ +export const charsPerSegment = { + day: 2, + month: 2, + year: 4, +}; diff --git a/packages/time-input/src/hooks/index.ts b/packages/time-input/src/hooks/index.ts new file mode 100644 index 0000000000..91b8fb4a8b --- /dev/null +++ b/packages/time-input/src/hooks/index.ts @@ -0,0 +1 @@ +export { useSelectUnit } from './useSelectUnit'; diff --git a/packages/time-input/src/hooks/useSelectUnit/index.ts b/packages/time-input/src/hooks/useSelectUnit/index.ts new file mode 100644 index 0000000000..91b8fb4a8b --- /dev/null +++ b/packages/time-input/src/hooks/useSelectUnit/index.ts @@ -0,0 +1 @@ +export { useSelectUnit } from './useSelectUnit'; diff --git a/packages/time-input/src/hooks/useSelectUnit/useSelectUnit.ts b/packages/time-input/src/hooks/useSelectUnit/useSelectUnit.ts new file mode 100644 index 0000000000..98d976ada5 --- /dev/null +++ b/packages/time-input/src/hooks/useSelectUnit/useSelectUnit.ts @@ -0,0 +1,57 @@ +import { useEffect, useState } from 'react'; +import { DateType, isValidDate } from '@leafygreen-ui/date-utils'; + +import { UnitOption } from '../../TimeInputSelect/TimeInputSelect.types'; + +type UseSelectUnitReturn = { + selectUnit: UnitOption; + setSelectUnit: React.Dispatch>; +}; + +/** + * Finds the select unit option based on the day period. + * + * @param dayPeriod - The day period to use for the select unit. + * @param unitOptions - The valid unit options to use for the select unit. + * @returns The select unit option. + */ +const findSelectUnit = ( + dayPeriod: string, + unitOptions: UnitOption[], +): UnitOption => { + const selectUnitOption = unitOptions.find( + option => option.displayName === dayPeriod, + ) as UnitOption; + return selectUnitOption; +}; + +/** + * Hook to manage the select unit. + * + * @param dayPeriod - The day period to use for the select unit. + * @param value - The date value passed to the TimeInput component. + * @param unitOptions - The valid unit options to use for the select unit. + * @returns The select unit and the setSelectUnit function. + */ +export const useSelectUnit = ({ + dayPeriod, + value, + unitOptions, +}: { + dayPeriod: string; + value: DateType | undefined; + unitOptions: UnitOption[]; +}): UseSelectUnitReturn => { + const selectUnitOption = findSelectUnit(dayPeriod, unitOptions); + const [selectUnit, setSelectUnit] = useState(selectUnitOption); + + useEffect(() => { + // Only update the select unit if the value is valid. This way the previous valid value is not lost. + if (isValidDate(value)) { + const selectUnitOption = findSelectUnit(dayPeriod, unitOptions); + setSelectUnit(selectUnitOption); + } + }, [value, dayPeriod, unitOptions, setSelectUnit]); + + return { selectUnit, setSelectUnit }; +}; diff --git a/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts b/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts index 139e219a0f..e9a6c9cf4f 100644 --- a/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts +++ b/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts @@ -21,6 +21,8 @@ type FormattedTimeParts = | 'year' | 'dayPeriod'; +export type TimeParts = Record; + const getFilteredTimeParts = ({ timeParts, }: { @@ -34,7 +36,7 @@ const getFilteredTimeParts = ({ const getFormattedAndMergedTimeParts = ( timeParts: Array, -): Record => { +): TimeParts => { const formattedTimeParts: Record = timeParts.reduce((acc, part) => { acc[part.type as FormattedTimeParts] = part.value; @@ -88,9 +90,7 @@ export const getFormatPartsValues = ({ timeZone: string; value: DateType | undefined; hasDayPeriod: boolean; -}) => { - if (!value) return getDefaultTimeParts(timeZone, locale, hasDayPeriod); - +}): TimeParts => { if (!isValidDate(value)) { const formattedTimeParts = getDefaultTimeParts( timeZone, diff --git a/packages/time-input/tsconfig.json b/packages/time-input/tsconfig.json index 0bef326383..56a788d4a1 100644 --- a/packages/time-input/tsconfig.json +++ b/packages/time-input/tsconfig.json @@ -38,6 +38,9 @@ { "path": "../hooks" }, + { + "path": "../input-box" + }, { "path": "../select" }, From 2f5707f44e670d568bf4bfc395346f7b1be61f7e Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Tue, 2 Dec 2025 17:39:57 -0500 Subject: [PATCH 09/49] refactor(time-input): update time segment rules and default values to support 12/24 hour formats --- .../TimeInputDisplayContext.tsx | 7 +-- .../TimeFormField/TimeFormField.tsx | 2 +- .../TimeFormFieldInputContainer.tsx | 1 + packages/time-input/src/TimeInput.stories.tsx | 3 +- .../src/TimeInputBox/TimeInputBox.tsx | 13 +++-- .../src/TimeInputInputs/TimeInputInputs.tsx | 14 ++--- .../src/TimeInputSegment/TimeInputSegment.tsx | 13 +++-- packages/time-input/src/constants.ts | 51 +++++++++---------- .../src/hooks/useSelectUnit/useSelectUnit.ts | 9 ++-- .../utils/getFormatParts/getFormatParts.ts | 1 + .../getFormatPartsValues.ts | 4 +- .../utils/getFormatter/getFormatter.spec.ts | 1 + .../utils/hasDayPeriod/hasDayPeriod.spec.ts | 3 +- .../src/utils/hasDayPeriod/hasDayPeriod.ts | 1 + packages/time-input/src/utils/index.ts | 2 +- 15 files changed, 68 insertions(+), 57 deletions(-) diff --git a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx index 5c8b76b20c..b39a7efdd9 100644 --- a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx +++ b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx @@ -5,15 +5,16 @@ 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, TimeInputDisplayProviderProps, } from './TimeInputDisplayContext.types'; import { defaultTimeInputDisplayContext } from './TimePickerDisplayContext.utils'; -import { hasDayPeriod } from '../../utils'; -import { getFormatParts } from '../../utils/getFormatParts/getFormatParts'; -import defaultTo from 'lodash/defaultTo'; export const TimeInputDisplayContext = createContext(defaultTimeInputDisplayContext); diff --git a/packages/time-input/src/TimeFormField/TimeFormField/TimeFormField.tsx b/packages/time-input/src/TimeFormField/TimeFormField/TimeFormField.tsx index 3a6564dda6..e306f44f16 100644 --- a/packages/time-input/src/TimeFormField/TimeFormField/TimeFormField.tsx +++ b/packages/time-input/src/TimeFormField/TimeFormField/TimeFormField.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { FormField } from '@leafygreen-ui/form-field'; + import { useTimeInputDisplayContext } from '../../Context/TimeInputDisplayContext'; import { TimeFormFieldProps } from './TimeFormField.types'; @@ -19,7 +20,6 @@ export const TimeFormField = React.forwardRef< // stateNotification: { state, message: errorMessage }, disabled, size, - shouldShowSelect, } = useTimeInputDisplayContext(); return ( diff --git a/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.tsx b/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.tsx index 64b4ff2756..cbd2d373d6 100644 --- a/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.tsx +++ b/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { FormFieldInputContainer } from '@leafygreen-ui/form-field'; + import { useTimeInputDisplayContext } from '../../Context/TimeInputDisplayContext'; import { getContainerStyles } from './TimeFormFieldInputContainer.styles'; diff --git a/packages/time-input/src/TimeInput.stories.tsx b/packages/time-input/src/TimeInput.stories.tsx index 6487bb4d5f..dbb4309445 100644 --- a/packages/time-input/src/TimeInput.stories.tsx +++ b/packages/time-input/src/TimeInput.stories.tsx @@ -2,9 +2,10 @@ import React, { useState } from 'react'; import { type StoryMetaType } from '@lg-tools/storybook-utils'; import { StoryFn } from '@storybook/react'; -import { TimeInput } from '.'; import { DateType, SupportedLocales } from '@leafygreen-ui/date-utils'; +import { TimeInput } from '.'; + const meta: StoryMetaType = { title: 'Components/Inputs/TimeInput', component: TimeInput, diff --git a/packages/time-input/src/TimeInputBox/TimeInputBox.tsx b/packages/time-input/src/TimeInputBox/TimeInputBox.tsx index 73c4aa59a0..2e03fd2d58 100644 --- a/packages/time-input/src/TimeInputBox/TimeInputBox.tsx +++ b/packages/time-input/src/TimeInputBox/TimeInputBox.tsx @@ -1,20 +1,23 @@ import React from 'react'; -import { TimeInputBoxProps } from './TimeInputBox.types'; import { InputBox } from '@leafygreen-ui/input-box'; -import { TimeSegments } from '../TimeInputSegment/TimeInputSegment.types'; -import { timeSegmentRules } from '../constants'; + +import { getTimeSegmentRules } from '../constants'; import { useTimeInputDisplayContext } from '../Context/TimeInputDisplayContext/TimeInputDisplayContext'; import { TimeInputSegment } from '../TimeInputSegment/TimeInputSegment'; +import { TimeSegments } from '../TimeInputSegment/TimeInputSegment.types'; + +import { TimeInputBoxProps } from './TimeInputBox.types'; export const TimeInputBox = React.forwardRef( ({ children, ...rest }: TimeInputBoxProps, fwdRef) => { - const { disabled, formatParts, size } = useTimeInputDisplayContext(); + const { disabled, formatParts, size, shouldShowSelect } = + useTimeInputDisplayContext(); return ( (({ children, segment, ...rest }: TimeInputSegmentProps, fwdRef) => { + const { shouldShowSelect } = useTimeInputDisplayContext(); return ( ); diff --git a/packages/time-input/src/constants.ts b/packages/time-input/src/constants.ts index 65d3b2a879..f68f517fc5 100644 --- a/packages/time-input/src/constants.ts +++ b/packages/time-input/src/constants.ts @@ -5,29 +5,33 @@ export const unitOptions = [ { displayName: 'PM', value: 'PM' }, ]; -export const timeSegmentRules = { - [TimeSegments.Hour]: { - maxChars: 2, - minExplicitValue: 2, // TODO: this depends on 12/24h format - }, - [TimeSegments.Minute]: { - maxChars: 2, - minExplicitValue: 6, - }, - [TimeSegments.Second]: { - maxChars: 2, - minExplicitValue: 6, - }, +export const getTimeSegmentRules = (is12HourFormat: boolean) => { + return { + [TimeSegments.Hour]: { + maxChars: 2, + minExplicitValue: is12HourFormat ? 1 : 2, + }, + [TimeSegments.Minute]: { + maxChars: 2, + minExplicitValue: 6, + }, + [TimeSegments.Second]: { + maxChars: 2, + minExplicitValue: 6, + }, + }; }; /** * The minimum number for each segment */ -export const defaultMin = { - hour: 0, // TODO: this depends on 12/24h format - minute: 0, - second: 0, -} as const; +export const getDefaultMin = (is12HourFormat: boolean) => { + return { + hour: is12HourFormat ? 1 : 0, + minute: 0, + second: 0, + } as const; +}; /** * The maximum number for each segment @@ -45,13 +49,4 @@ export const defaultPlaceholder = { hour: 'HH', minute: 'MM', second: 'SS', -}; - -/** - * The number of characters per input segment - */ -export const charsPerSegment = { - day: 2, - month: 2, - year: 4, -}; +} as const; diff --git a/packages/time-input/src/hooks/useSelectUnit/useSelectUnit.ts b/packages/time-input/src/hooks/useSelectUnit/useSelectUnit.ts index 98d976ada5..fa4ab18565 100644 --- a/packages/time-input/src/hooks/useSelectUnit/useSelectUnit.ts +++ b/packages/time-input/src/hooks/useSelectUnit/useSelectUnit.ts @@ -1,12 +1,13 @@ import { useEffect, useState } from 'react'; + import { DateType, isValidDate } from '@leafygreen-ui/date-utils'; import { UnitOption } from '../../TimeInputSelect/TimeInputSelect.types'; -type UseSelectUnitReturn = { +interface UseSelectUnitReturn { selectUnit: UnitOption; setSelectUnit: React.Dispatch>; -}; +} /** * Finds the select unit option based on the day period. @@ -17,7 +18,7 @@ type UseSelectUnitReturn = { */ const findSelectUnit = ( dayPeriod: string, - unitOptions: UnitOption[], + unitOptions: Array, ): UnitOption => { const selectUnitOption = unitOptions.find( option => option.displayName === dayPeriod, @@ -40,7 +41,7 @@ export const useSelectUnit = ({ }: { dayPeriod: string; value: DateType | undefined; - unitOptions: UnitOption[]; + unitOptions: Array; }): UseSelectUnitReturn => { const selectUnitOption = findSelectUnit(dayPeriod, unitOptions); const [selectUnit, setSelectUnit] = useState(selectUnitOption); diff --git a/packages/time-input/src/utils/getFormatParts/getFormatParts.ts b/packages/time-input/src/utils/getFormatParts/getFormatParts.ts index 28ea280430..f09a33064b 100644 --- a/packages/time-input/src/utils/getFormatParts/getFormatParts.ts +++ b/packages/time-input/src/utils/getFormatParts/getFormatParts.ts @@ -1,4 +1,5 @@ import { SupportedLocales } from '@leafygreen-ui/date-utils'; + import { getFormatter } from '..'; /** A sample date to use for formatting */ diff --git a/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts b/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts index e9a6c9cf4f..ac6ada744b 100644 --- a/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts +++ b/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts @@ -1,6 +1,8 @@ +import defaultsDeep from 'lodash/defaultsDeep'; + import { DateType, isValidDate } from '@leafygreen-ui/date-utils'; + import { getFormatter } from '../getFormatter/getFormatter'; -import defaultsDeep from 'lodash/defaultsDeep'; const defaultTimePartsObject: Record = { hour: '', diff --git a/packages/time-input/src/utils/getFormatter/getFormatter.spec.ts b/packages/time-input/src/utils/getFormatter/getFormatter.spec.ts index 9f331adfed..93650d8842 100644 --- a/packages/time-input/src/utils/getFormatter/getFormatter.spec.ts +++ b/packages/time-input/src/utils/getFormatter/getFormatter.spec.ts @@ -1,4 +1,5 @@ import { SupportedLocales } from '@leafygreen-ui/date-utils'; + import { getFormatter } from './getFormatter'; describe('packages/time-input/utils/getFormatter', () => { diff --git a/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.spec.ts b/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.spec.ts index bc3fd39314..500f69e35d 100644 --- a/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.spec.ts +++ b/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.spec.ts @@ -1,6 +1,7 @@ -import { hasDayPeriod } from './hasDayPeriod'; import { SupportedLocales } from '@leafygreen-ui/date-utils'; +import { hasDayPeriod } from './hasDayPeriod'; + describe('packages/time-input/utils/hasDayPeriod', () => { test('returns false for ISO_8601', () => { expect(hasDayPeriod(SupportedLocales.ISO_8601)).toBe(false); diff --git a/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.ts b/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.ts index f4cacb02da..ed5fcc2608 100644 --- a/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.ts +++ b/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.ts @@ -1,4 +1,5 @@ import { SupportedLocales } from '@leafygreen-ui/date-utils'; + import { getFormatter } from '../getFormatter/getFormatter'; /** diff --git a/packages/time-input/src/utils/index.ts b/packages/time-input/src/utils/index.ts index 1b9137605c..4f64fa8efe 100644 --- a/packages/time-input/src/utils/index.ts +++ b/packages/time-input/src/utils/index.ts @@ -1,3 +1,3 @@ +export { getFormatPartsValues } from './getFormatPartsValues/getFormatPartsValues'; export { getFormatter } from './getFormatter/getFormatter'; export { hasDayPeriod } from './hasDayPeriod/hasDayPeriod'; -export { getFormatPartsValues } from './getFormatPartsValues/getFormatPartsValues'; From 971540758f0ada66b2b310496d5556f862b1b739 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Tue, 2 Dec 2025 17:52:53 -0500 Subject: [PATCH 10/49] refactor(time-input): simplify getFormatParts function by removing locale dependency and enhancing formatting logic --- .../utils/getFormatParts/getFormatParts.ts | 72 ++++--------------- 1 file changed, 15 insertions(+), 57 deletions(-) diff --git a/packages/time-input/src/utils/getFormatParts/getFormatParts.ts b/packages/time-input/src/utils/getFormatParts/getFormatParts.ts index 28ea280430..3b65a09005 100644 --- a/packages/time-input/src/utils/getFormatParts/getFormatParts.ts +++ b/packages/time-input/src/utils/getFormatParts/getFormatParts.ts @@ -1,60 +1,18 @@ -import { SupportedLocales } from '@leafygreen-ui/date-utils'; -import { getFormatter } from '..'; - -/** A sample date to use for formatting */ -const now = new Date(); - -/** - * Returns the ISO format parts for the given locale - * @param showSeconds - Whether to show seconds - * @returns The ISO format parts - * - * @example - * ```js - * getIsoFormatParts(true); - * - * // [ - * // { type: 'hour', value: '' }, - * // { type: 'literal', value: ':' }, - * // { type: 'minute', value: '' }, - * // { type: 'literal', value: ':' }, - * // { type: 'second', value: '' }, - * // ] - * ``` - */ -// const getIsoFormatParts = ( -// showSeconds: boolean, -// ): Array => { -// const formatParts: Array = [ -// { type: 'hour', value: '' }, -// { type: 'literal', value: ':' }, -// { type: 'minute', value: '' }, -// ...(showSeconds -// ? ([ -// { type: 'literal', value: ':' }, -// { type: 'second', value: '' }, -// ] as Array) -// : []), -// ]; - -// return formatParts; -// }; - -// TODO: confirm with Sooa if we want to change the presentation value based on the locale. If that is the case then we only need to return a predefined format with seconds or not. /** * 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 + * since they are not part of the time format parts. + * + * This will return `:` for every literal part regardless of the locale. * - * @param locale - The locale to get the format parts for * @param showSeconds - Whether to show seconds * @returns The format parts * * @example * * ```js - * getFormatParts({ locale: 'en-US', showSeconds: true }); + * getFormatParts({ showSeconds: true }); * * // [ * // { type: 'hour', value: '' }, @@ -65,22 +23,22 @@ const now = new Date(); * // ] */ export const getFormatParts = ({ - locale, showSeconds = false, }: { locale: string; showSeconds?: boolean; }): Array | undefined => { - const isIsoLocale = locale === SupportedLocales.ISO_8601; - - const formatter = getFormatter({ - locale: locale, - isIsoLocale, - showSeconds, - options: { hourCycle: 'h23' }, - }); - - const formatParts = formatter?.formatToParts(now); + const formatParts: Array = [ + { type: 'hour', value: '' }, + { type: 'literal', value: ':' }, + { type: 'minute', value: '' }, + ...(showSeconds + ? ([ + { type: 'literal', value: ':' }, + { type: 'second', value: '' }, + ] as Array) + : []), + ]; return formatParts; }; From dad7b62e3a8ab3479dce4dbb39ad4eb2f4141f4f Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Tue, 2 Dec 2025 17:56:31 -0500 Subject: [PATCH 11/49] test(time-input): add unit tests for getFormatParts utility to verify format output with and without seconds --- .../getFormatParts/getFormatParts.spec.ts | 23 +++++++++++++++++++ .../utils/getFormatParts/getFormatParts.ts | 1 - .../src/utils/getFormatter/getFormatter.ts | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/time-input/src/utils/getFormatParts/getFormatParts.spec.ts b/packages/time-input/src/utils/getFormatParts/getFormatParts.spec.ts index e69de29bb2..39cb06550b 100644 --- a/packages/time-input/src/utils/getFormatParts/getFormatParts.spec.ts +++ b/packages/time-input/src/utils/getFormatParts/getFormatParts.spec.ts @@ -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: '' }, + ]); + }); +}); diff --git a/packages/time-input/src/utils/getFormatParts/getFormatParts.ts b/packages/time-input/src/utils/getFormatParts/getFormatParts.ts index 3b65a09005..506dffd22c 100644 --- a/packages/time-input/src/utils/getFormatParts/getFormatParts.ts +++ b/packages/time-input/src/utils/getFormatParts/getFormatParts.ts @@ -25,7 +25,6 @@ export const getFormatParts = ({ showSeconds = false, }: { - locale: string; showSeconds?: boolean; }): Array | undefined => { const formatParts: Array = [ diff --git a/packages/time-input/src/utils/getFormatter/getFormatter.ts b/packages/time-input/src/utils/getFormatter/getFormatter.ts index 8bca8a49ce..3219063dad 100644 --- a/packages/time-input/src/utils/getFormatter/getFormatter.ts +++ b/packages/time-input/src/utils/getFormatter/getFormatter.ts @@ -1,6 +1,7 @@ import { isValidLocale } from '@leafygreen-ui/date-utils'; /** + * // TODO: what is a formatter? * Returns a formatter for the given locale. If the locale is invalid, returns undefined. * * @param locale - The locale to get the formatter for From f5595933201b31faa2dcab8f13351ca496a2e88c Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Tue, 2 Dec 2025 17:57:46 -0500 Subject: [PATCH 12/49] refactor(time-input): remove locale dependency from formatParts in TimeInputDisplayContext --- .../Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx index 5c8b76b20c..22171cd11c 100644 --- a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx +++ b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx @@ -47,7 +47,6 @@ export const TimeInputDisplayProvider = ({ // Only used to track the presentation format of the segments, not the value itself const formatParts = getFormatParts({ - locale: providerValue.locale, showSeconds: providerValue.showSeconds, }); From 3e6f6bef15f21409f5a34ceffc1cdd225ca7c3fa Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Wed, 3 Dec 2025 14:01:08 -0500 Subject: [PATCH 13/49] feat(time-input): add default time parts and enhance time input context handling --- .../TimeInputDisplayContext.tsx | 11 +- packages/time-input/src/TimeInput.stories.tsx | 3 +- .../src/TimeInputInputs/TimeInputInputs.tsx | 53 ++--- packages/time-input/src/constants.ts | 12 ++ packages/time-input/src/shared.types.ts | 13 ++ .../getFilteredTimeParts.spec.ts | 17 ++ .../getFilteredTimeParts.ts | 25 +++ .../getFormatPartsValues.spec.ts | 190 ++++++++++++++++++ .../getFormatPartsValues.ts | 122 +++-------- .../getFormattedTimeParts.spec.ts | 41 ++++ .../getFormattedTimeParts.ts | 38 ++++ .../utils/getFormatter/getFormatter.spec.ts | 6 + .../src/utils/getFormatter/getFormatter.ts | 16 +- .../utils/hasDayPeriod/hasDayPeriod.spec.ts | 3 +- .../src/utils/hasDayPeriod/hasDayPeriod.ts | 5 + packages/time-input/src/utils/index.ts | 2 +- 16 files changed, 406 insertions(+), 151 deletions(-) create mode 100644 packages/time-input/src/shared.types.ts create mode 100644 packages/time-input/src/utils/getFilteredTimeParts/getFilteredTimeParts.spec.ts create mode 100644 packages/time-input/src/utils/getFilteredTimeParts/getFilteredTimeParts.ts create mode 100644 packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.spec.ts create mode 100644 packages/time-input/src/utils/getFormattedTimeParts/getFormattedTimeParts.spec.ts create mode 100644 packages/time-input/src/utils/getFormattedTimeParts/getFormattedTimeParts.ts diff --git a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx index 22171cd11c..a85052b32d 100644 --- a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx +++ b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx @@ -5,15 +5,16 @@ 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, TimeInputDisplayProviderProps, } from './TimeInputDisplayContext.types'; import { defaultTimeInputDisplayContext } from './TimePickerDisplayContext.utils'; -import { hasDayPeriod } from '../../utils'; -import { getFormatParts } from '../../utils/getFormatParts/getFormatParts'; -import defaultTo from 'lodash/defaultTo'; export const TimeInputDisplayContext = createContext(defaultTimeInputDisplayContext); @@ -55,10 +56,6 @@ export const TimeInputDisplayProvider = ({ Intl.DateTimeFormat().resolvedOptions().timeZone, ); - // console.log('🍓', { timeZone }); - - // TODO: timezone - return ( = { title: 'Components/Inputs/TimeInput', component: TimeInput, diff --git a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx index 183073b036..1cca1bcea2 100644 --- a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx +++ b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx @@ -1,19 +1,18 @@ // @ts-nocheck -import React, { forwardRef, useEffect, useState } from 'react'; +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'; -import { useTimeInputDisplayContext } from '../Context/TimeInputDisplayContext/TimeInputDisplayContext'; -import { useTimeInputContext } from '../Context/TimeInputContext/TimeInputContext'; -import { getFormatPartsValues } from '../utils'; -import { isValidDate } from '@leafygreen-ui/date-utils'; /** * @internal @@ -22,6 +21,7 @@ export const TimeInputInputs = forwardRef( (_props: TimeInputInputsProps, forwardedRef) => { const { shouldShowSelect, formatParts, timeZone, locale } = useTimeInputDisplayContext(); + const [selectUnit, setSelectUnit] = useState(unitOptions[0]); const { value } = useTimeInputContext(); @@ -33,43 +33,16 @@ export const TimeInputInputs = forwardRef( locale: locale, timeZone: timeZone, value: value, - hasDayPeriod: shouldShowSelect, }); - // const { selectUnit, setSelectUnit } = useSelectUnit(timeParts); - - // const useSelectUnit = (timeParts: Array) => { - // const [selectUnit, setSelectUnit] = useState(unitOptions[0]); - // return { selectUnit, setSelectUnit }; - // }; - - // get select unit from time parts - const initialSelectUnitFromTimeParts = timeParts.dayPeriod; - const selectUnitOption = unitOptions.find( - option => option.displayName === initialSelectUnitFromTimeParts, - ); - - const [selectUnit, setSelectUnit] = useState(selectUnitOption); - - useEffect(() => { - if (isValidDate(value)) { - const selectUnitFromTimeParts = timeParts.dayPeriod; - const selectUnitOption = unitOptions.find( - option => option.displayName === selectUnitFromTimeParts, - ); - - setSelectUnit(selectUnitOption); - } - }, [value, selectUnitOption]); - - console.log('TimeInputInputs 🍉', { - shouldShowSelect, - formatParts, - timeZone, - value: value?.toUTCString(), - timeParts, - locale, - }); + // console.log('TimeInputInputs 🍉', { + // shouldShowSelect, + // formatParts, + // timeZone, + // value: value?.toUTCString(), + // timeParts, + // locale, + // }); // TODO: break this out more return ( diff --git a/packages/time-input/src/constants.ts b/packages/time-input/src/constants.ts index afb7f735b4..2bcf85ffd4 100644 --- a/packages/time-input/src/constants.ts +++ b/packages/time-input/src/constants.ts @@ -1,4 +1,16 @@ +import { TimeParts } from './shared.types'; + export const unitOptions = [ { displayName: 'AM', value: 'AM' }, { displayName: 'PM', value: 'PM' }, ]; + +export const defaultTimeParts: TimeParts = { + hour: '', + minute: '', + second: '', + month: '', + day: '', + year: '', + dayPeriod: 'AM', +}; diff --git a/packages/time-input/src/shared.types.ts b/packages/time-input/src/shared.types.ts new file mode 100644 index 0000000000..21199a796c --- /dev/null +++ b/packages/time-input/src/shared.types.ts @@ -0,0 +1,13 @@ +export const TimePartKeys = { + 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; diff --git a/packages/time-input/src/utils/getFilteredTimeParts/getFilteredTimeParts.spec.ts b/packages/time-input/src/utils/getFilteredTimeParts/getFilteredTimeParts.spec.ts new file mode 100644 index 0000000000..be5fc56060 --- /dev/null +++ b/packages/time-input/src/utils/getFilteredTimeParts/getFilteredTimeParts.spec.ts @@ -0,0 +1,17 @@ +import { getFilteredTimeParts } from './getFilteredTimeParts'; + +describe('packages/time-input/utils/getFilteredTimeParts', () => { + 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' }, + ]); + }); +}); diff --git a/packages/time-input/src/utils/getFilteredTimeParts/getFilteredTimeParts.ts b/packages/time-input/src/utils/getFilteredTimeParts/getFilteredTimeParts.ts new file mode 100644 index 0000000000..928e9f557d --- /dev/null +++ b/packages/time-input/src/utils/getFilteredTimeParts/getFilteredTimeParts.ts @@ -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 = ({ + timeParts, +}: { + timeParts?: Array; +}) => { + const filteredTimeParts = + timeParts?.filter(part => part.type !== 'literal') ?? []; + + return filteredTimeParts; +}; diff --git a/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.spec.ts b/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.spec.ts new file mode 100644 index 0000000000..5061aa4e18 --- /dev/null +++ b/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.spec.ts @@ -0,0 +1,190 @@ +import { Month, SupportedLocales } from '@leafygreen-ui/date-utils'; + +import { getFormatPartsValues } from './getFormatPartsValues'; + +describe('packages/time-input/utils/getFormatPartsValues', () => { + describe('returns current day, month, and year format parts values with the value is undefined', () => { + beforeEach(() => { + // Mock the current date/time in UTC + jest.useFakeTimers().setSystemTime( + new Date(Date.UTC(2025, Month.January, 1, 0, 0, 0)), // January 1, 2025 00:00:00 UTC + ); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('and the time zone is', () => { + test('UTC', () => { + const formatPartsValues = getFormatPartsValues({ + locale: SupportedLocales.ISO_8601, + timeZone: 'UTC', + value: undefined, + }); + // January 1, 2025 00:00:00 UTC in UTC is January 1, 2025 00:00:00 (UTC) + expect(formatPartsValues).toEqual({ + hour: '', + minute: '', + second: '', + month: '1', + day: '1', + year: '2025', + dayPeriod: 'AM', // This is the default value for the day period since iso-8601 is 24h format + }); + }); + + test('America/New_York', () => { + const formatPartsValues = getFormatPartsValues({ + locale: SupportedLocales.ISO_8601, + timeZone: 'America/New_York', + value: undefined, + }); + // January 1, 2025 00:00:00 UTC in America/New_York is December 31, 2024 19:00:00 (UTC-5 hours) + expect(formatPartsValues).toEqual({ + hour: '', + minute: '', + second: '', + month: '12', + day: '31', + year: '2024', + dayPeriod: 'AM', // This is the default value for the day period since iso is 24h format + }); + }); + + test('Pacific/Auckland', () => { + const formatPartsValues = getFormatPartsValues({ + locale: SupportedLocales.ISO_8601, + timeZone: 'Pacific/Auckland', + value: undefined, + }); + // January 1, 2025 00:00:00 UTC in Pacific/Auckland is January 1, 2025 (UTC+13 hours) + expect(formatPartsValues).toEqual({ + hour: '', + minute: '', + second: '', + month: '1', + day: '1', + year: '2025', + dayPeriod: 'AM', + }); + }); + }); + }); + + describe('returns day, month, year, hour, minute, second, and day period values when the value is defined', () => { + const utcValue = new Date(Date.UTC(2025, Month.February, 20, 13, 30, 59)); // February 20, 2025 13:30:59 UTC + + describe('and the time zone is', () => { + describe('UTC', () => { + test('24 hour format', () => { + const formatPartsValues = getFormatPartsValues({ + locale: SupportedLocales.ISO_8601, + timeZone: 'UTC', + value: utcValue, + }); + expect(formatPartsValues).toEqual({ + hour: '13', + minute: '30', + second: '59', + month: '2', + day: '20', + year: '2025', + dayPeriod: 'AM', + }); + }); + + test('12 hour format', () => { + const formatPartsValues = getFormatPartsValues({ + locale: SupportedLocales.en_US, + timeZone: 'UTC', + value: utcValue, + }); + expect(formatPartsValues).toEqual({ + hour: '1', + minute: '30', + second: '59', + month: '2', + day: '20', + year: '2025', + dayPeriod: 'PM', + }); + }); + }); + + describe('America/New_York', () => { + test('24 hour format', () => { + const formatPartsValues = getFormatPartsValues({ + locale: SupportedLocales.ISO_8601, + timeZone: 'America/New_York', + value: utcValue, + }); + // February 20, 2025 13:30:59 UTC in America/New_York is 08:30:59 (UTC-5 hours) + expect(formatPartsValues).toEqual({ + hour: '08', + minute: '30', + second: '59', + month: '2', + day: '20', + year: '2025', + dayPeriod: 'AM', + }); + }); + test('12 hour format', () => { + const formatPartsValues = getFormatPartsValues({ + locale: SupportedLocales.en_US, + timeZone: 'America/New_York', + value: utcValue, + }); + // February 20, 2025 13:30:59 UTC in America/New_York is 8:30:59 AM (UTC-5 hours) + expect(formatPartsValues).toEqual({ + hour: '8', + minute: '30', + second: '59', + month: '2', + day: '20', + year: '2025', + dayPeriod: 'AM', + }); + }); + }); + + describe('Pacific/Auckland', () => { + test('24 hour format', () => { + const formatPartsValues = getFormatPartsValues({ + locale: SupportedLocales.ISO_8601, + timeZone: 'Pacific/Auckland', + value: utcValue, + }); + // February 20, 2025 13:30:59 UTC in Pacific/Auckland is February 21, 2025 02:30:59 (UTC+13 hours) + expect(formatPartsValues).toEqual({ + hour: '02', + minute: '30', + second: '59', + month: '2', + day: '21', + year: '2025', + dayPeriod: 'AM', + }); + }); + test('12 hour format', () => { + const formatPartsValues = getFormatPartsValues({ + locale: SupportedLocales.en_US, + timeZone: 'Pacific/Auckland', + value: utcValue, + }); + // February 20, 2025 13:30:59 UTC in Pacific/Auckland is February 21, 2025 2:30:59 AM (UTC+13 hours) + expect(formatPartsValues).toEqual({ + hour: '2', + minute: '30', + second: '59', + month: '2', + day: '21', + year: '2025', + dayPeriod: 'AM', + }); + }); + }); + }); + }); +}); diff --git a/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts b/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts index 139e219a0f..254839c3a3 100644 --- a/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts +++ b/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts @@ -1,119 +1,53 @@ import { DateType, isValidDate } from '@leafygreen-ui/date-utils'; -import { getFormatter } from '../getFormatter/getFormatter'; -import defaultsDeep from 'lodash/defaultsDeep'; - -const defaultTimePartsObject: Record = { - hour: '', - minute: '', - second: '', - month: '', - day: '', - year: '', - dayPeriod: 'AM', -}; - -type FormattedTimeParts = - | 'hour' - | 'minute' - | 'second' - | 'month' - | 'day' - | 'year' - | 'dayPeriod'; -const getFilteredTimeParts = ({ - timeParts, -}: { - timeParts?: Array; -}) => { - const filteredTimeParts = - timeParts?.filter(part => part.type !== 'literal') ?? []; - - return filteredTimeParts; -}; - -const getFormattedAndMergedTimeParts = ( - timeParts: Array, -): Record => { - const formattedTimeParts: Record = - timeParts.reduce((acc, part) => { - acc[part.type as FormattedTimeParts] = part.value; - return acc; - }, {} as Record); - - const mergedTimeParts = defaultsDeep( - formattedTimeParts, - defaultTimePartsObject, - ); - - return mergedTimeParts; -}; +import { TimeParts } from '../../shared.types'; +import { getFilteredTimeParts } from '../getFilteredTimeParts/getFilteredTimeParts'; +import { getFormattedTimeParts } from '../getFormattedTimeParts/getFormattedTimeParts'; +import { getFormatter } from '../getFormatter/getFormatter'; /** - * Used when the component is uncontrolled, and the value is undefined - * @param timeZone - * @param locale - * @param hasDayPeriod - * @returns + * Returns the format parts values for the given locale, time zone, and value. + * @param locale - The locale to get the format parts values for + * @param timeZone - The time zone to get the format parts values for + * @param value - The value to get the format parts values for + * @returns The format parts values + * + * @example + * ```js + * getFormatPartsValues({ + * locale: 'en-US', + * timeZone: 'America/New_York', + * value: new Date('2025-01-01T12:00:00Z'), + * }); + * // returns: { hour: '12', minute: '00', second: '00', month: '01', day: '01', year: '2025', dayPeriod: 'PM' } + * ``` */ -export const getDefaultTimeParts = ( - timeZone: string, - locale: string, - hasDayPeriod: boolean, -) => { - const formatter = getFormatter({ - locale: locale, - withDate: true, - withTime: false, - options: { - timeZone: timeZone, - hourCycle: hasDayPeriod ? 'h12' : 'h23', - }, - }); - - const timeParts = formatter?.formatToParts(new Date()); - const filteredTimeParts = getFilteredTimeParts({ timeParts }); - const formattedTimeParts = getFormattedAndMergedTimeParts(filteredTimeParts); - - return formattedTimeParts; -}; - export const getFormatPartsValues = ({ locale, timeZone, value, - hasDayPeriod, }: { locale: string; timeZone: string; value: DateType | undefined; - hasDayPeriod: boolean; -}) => { - if (!value) return getDefaultTimeParts(timeZone, locale, hasDayPeriod); - - if (!isValidDate(value)) { - const formattedTimeParts = getDefaultTimeParts( - timeZone, - locale, - hasDayPeriod, - ); - return formattedTimeParts; - } +}): TimeParts => { + const isValueValid = isValidDate(value); - // Get the formatter + // Get the formatter that returns day, month, year, hour, minute, and second for the given locale and time zone. const formatter = getFormatter({ - locale: locale, + locale, withDate: true, + withTime: isValueValid ? true : false, // if the value is not valid then we don't want to return hour, minute, and second but we still want to return day, month, and year. options: { timeZone: timeZone, - hourCycle: hasDayPeriod ? 'h12' : 'h23', }, }); - // Get the time parts - const timeParts = formatter?.formatToParts(value); + // This returns the day, month, year, hour, minute, and second based on the value. + const timeParts = formatter?.formatToParts(isValueValid ? value : new Date()); const filteredTimeParts = getFilteredTimeParts({ timeParts }); - const formattedTimeParts = getFormattedAndMergedTimeParts(filteredTimeParts); + // this adds a default value for the day period if it is not present. It's not necessary for 24h format locales but we add it for consistency. + const formattedTimeParts = getFormattedTimeParts(filteredTimeParts); return formattedTimeParts; }; diff --git a/packages/time-input/src/utils/getFormattedTimeParts/getFormattedTimeParts.spec.ts b/packages/time-input/src/utils/getFormattedTimeParts/getFormattedTimeParts.spec.ts new file mode 100644 index 0000000000..02f3960b80 --- /dev/null +++ b/packages/time-input/src/utils/getFormattedTimeParts/getFormattedTimeParts.spec.ts @@ -0,0 +1,41 @@ +import { getFormattedTimeParts } from './getFormattedTimeParts'; + +describe('packages/time-input/utils/getFormattedTimeParts', () => { + test('returns the formatted time parts with the default time parts', () => { + const formattedTimeParts = getFormattedTimeParts([ + { type: 'hour', value: '12' }, + { type: 'minute', value: '30' }, + { type: 'second', value: '00' }, + ]); + expect(formattedTimeParts).toEqual({ + hour: '12', + minute: '30', + second: '00', + month: '', + day: '', + year: '', + dayPeriod: 'AM', + }); + }); + + test('returns the formatted time parts without the default time parts', () => { + const formattedTimeParts = getFormattedTimeParts([ + { type: 'hour', value: '12' }, + { type: 'minute', value: '30' }, + { type: 'second', value: '00' }, + { type: 'month', value: '01' }, + { type: 'day', value: '01' }, + { type: 'year', value: '2025' }, + { type: 'dayPeriod', value: 'PM' }, + ]); + expect(formattedTimeParts).toEqual({ + hour: '12', + minute: '30', + second: '00', + month: '01', + day: '01', + year: '2025', + dayPeriod: 'PM', + }); + }); +}); diff --git a/packages/time-input/src/utils/getFormattedTimeParts/getFormattedTimeParts.ts b/packages/time-input/src/utils/getFormattedTimeParts/getFormattedTimeParts.ts new file mode 100644 index 0000000000..127e7e2977 --- /dev/null +++ b/packages/time-input/src/utils/getFormattedTimeParts/getFormattedTimeParts.ts @@ -0,0 +1,38 @@ +import defaultsDeep from 'lodash/defaultsDeep'; + +import { defaultTimeParts } from '../../constants'; +import { TimePartKeys, TimeParts } from '../../shared.types'; + +/** + * Returns the formatted time parts. + * + * This merges the formatted time parts with the default time parts. E.g., when the component is uncontrolled, and the value is undefined, we set empty defaults for the hour, minute, and second. + * + * @param timeParts - The time parts to get the formatted and merged time parts for + * @returns The formatted and merged time parts + * + * @example + * ```js + * getFormattedTimeParts([ + * { type: 'hour', value: '12' }, + * { type: 'minute', value: '30' }, + * { type: 'second', value: '00' }, + * ]); + * // returns: { hour: '12', minute: '30', second: '00' } + * ``` + */ +export const getFormattedTimeParts = ( + timeParts: Array, +): TimeParts => { + const formattedTimeParts: TimeParts = timeParts.reduce((acc, part) => { + acc[part.type as TimePartKeys] = part.value; + return acc; + }, {} as TimeParts); + + const mergedTimeParts: TimeParts = defaultsDeep( + formattedTimeParts, + defaultTimeParts, + ); + + return mergedTimeParts; +}; diff --git a/packages/time-input/src/utils/getFormatter/getFormatter.spec.ts b/packages/time-input/src/utils/getFormatter/getFormatter.spec.ts index 9f331adfed..15b7011e28 100644 --- a/packages/time-input/src/utils/getFormatter/getFormatter.spec.ts +++ b/packages/time-input/src/utils/getFormatter/getFormatter.spec.ts @@ -1,4 +1,5 @@ import { SupportedLocales } from '@leafygreen-ui/date-utils'; + import { getFormatter } from './getFormatter'; describe('packages/time-input/utils/getFormatter', () => { @@ -7,6 +8,11 @@ describe('packages/time-input/utils/getFormatter', () => { expect(formatter).toBeDefined(); }); + test('returns a formatter for iso-8601', () => { + const formatter = getFormatter({ locale: SupportedLocales.ISO_8601 }); + expect(formatter).toBeDefined(); + }); + test('returns undefined for an invalid locale', () => { const formatter = getFormatter({ locale: '!!!' }); expect(formatter).toBeUndefined(); diff --git a/packages/time-input/src/utils/getFormatter/getFormatter.ts b/packages/time-input/src/utils/getFormatter/getFormatter.ts index 3219063dad..cb19e3dfa9 100644 --- a/packages/time-input/src/utils/getFormatter/getFormatter.ts +++ b/packages/time-input/src/utils/getFormatter/getFormatter.ts @@ -1,8 +1,9 @@ -import { isValidLocale } from '@leafygreen-ui/date-utils'; +import { isValidLocale, SupportedLocales } from '@leafygreen-ui/date-utils'; /** - * // TODO: what is a formatter? - * Returns a formatter for the given locale. If the locale is invalid, returns undefined. + * Returns a formatter for the given locale. This determines the format of the time parts that are returned when called with a date. + * + * If the locale is invalid, returns undefined. * * @param locale - The locale to get the formatter for * @param showSeconds - Whether to show seconds @@ -14,22 +15,22 @@ import { isValidLocale } from '@leafygreen-ui/date-utils'; * ``` */ export const getFormatter = ({ - locale, + locale = SupportedLocales.ISO_8601, showSeconds = true, - isIsoLocale = false, withDate = false, withTime = true, options = {}, }: { - locale: string; + locale?: string; showSeconds?: boolean; - isIsoLocale?: boolean; withDate?: boolean; withTime?: boolean; options?: Intl.DateTimeFormatOptions; }) => { + const isIsoLocale = locale === SupportedLocales.ISO_8601; const isValid = isValidLocale(locale); + // If the locale is iso-8601, the default locale of the runtime environment is used, which is fine since we can explicitly set the format to 24h if (isValid || isIsoLocale) { return new Intl.DateTimeFormat(locale, { ...(withTime @@ -46,6 +47,7 @@ export const getFormatter = ({ day: 'numeric', } : {}), + ...(isIsoLocale ? { hourCycle: 'h23' } : {}), ...options, }); } diff --git a/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.spec.ts b/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.spec.ts index bc3fd39314..500f69e35d 100644 --- a/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.spec.ts +++ b/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.spec.ts @@ -1,6 +1,7 @@ -import { hasDayPeriod } from './hasDayPeriod'; import { SupportedLocales } from '@leafygreen-ui/date-utils'; +import { hasDayPeriod } from './hasDayPeriod'; + describe('packages/time-input/utils/hasDayPeriod', () => { test('returns false for ISO_8601', () => { expect(hasDayPeriod(SupportedLocales.ISO_8601)).toBe(false); diff --git a/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.ts b/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.ts index f4cacb02da..a15fb731f1 100644 --- a/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.ts +++ b/packages/time-input/src/utils/hasDayPeriod/hasDayPeriod.ts @@ -1,4 +1,5 @@ import { SupportedLocales } from '@leafygreen-ui/date-utils'; + import { getFormatter } from '../getFormatter/getFormatter'; /** @@ -7,6 +8,8 @@ import { getFormatter } from '../getFormatter/getFormatter'; * @param locale - The locale to check * @returns Whether the locale has a day period (AM/PM) * + * @default false + * * @example * ```js * hasDayPeriod('en-US'); // true @@ -20,6 +23,8 @@ export const hasDayPeriod = (locale: string) => { const formatter = getFormatter({ locale }); + if (!formatter) return false; + // Format a sample time and check for dayPeriod (AM/PM) const parts = formatter?.formatToParts(new Date()); const hasDayPeriod = parts?.some(part => part.type === 'dayPeriod'); diff --git a/packages/time-input/src/utils/index.ts b/packages/time-input/src/utils/index.ts index 1b9137605c..4f64fa8efe 100644 --- a/packages/time-input/src/utils/index.ts +++ b/packages/time-input/src/utils/index.ts @@ -1,3 +1,3 @@ +export { getFormatPartsValues } from './getFormatPartsValues/getFormatPartsValues'; export { getFormatter } from './getFormatter/getFormatter'; export { hasDayPeriod } from './hasDayPeriod/hasDayPeriod'; -export { getFormatPartsValues } from './getFormatPartsValues/getFormatPartsValues'; From 87023fb1a099c7cfaffc6c3096a81a453681d1ba Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Wed, 3 Dec 2025 15:22:11 -0500 Subject: [PATCH 14/49] feat(time-input): enhance TimeInput stories with new argTypes for darkMode and size, and add console log for debugging in TimeInputInputs --- packages/time-input/src/TimeInput.stories.tsx | 12 +++++++++++- .../src/TimeInputInputs/TimeInputInputs.tsx | 1 + .../src/TimeInputSegment/TimeInputSegment.tsx | 4 +++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/time-input/src/TimeInput.stories.tsx b/packages/time-input/src/TimeInput.stories.tsx index dbb4309445..b1178d61c2 100644 --- a/packages/time-input/src/TimeInput.stories.tsx +++ b/packages/time-input/src/TimeInput.stories.tsx @@ -1,10 +1,14 @@ import React, { useState } from 'react'; -import { type StoryMetaType } from '@lg-tools/storybook-utils'; +import { + storybookArgTypes, + type StoryMetaType, +} from '@lg-tools/storybook-utils'; import { StoryFn } from '@storybook/react'; import { DateType, SupportedLocales } from '@leafygreen-ui/date-utils'; import { TimeInput } from '.'; +import { Size } from './TimeInput/TimeInput.types'; const meta: StoryMetaType = { title: 'Components/Inputs/TimeInput', @@ -20,6 +24,8 @@ const meta: StoryMetaType = { 'onSegmentChange', 'value', 'onTimeChange', + 'data-lgid', + 'data-testid', ], }, }, @@ -28,6 +34,8 @@ const meta: StoryMetaType = { locale: SupportedLocales.ISO_8601, timeZone: 'UTC', label: 'Time Input', + darkMode: false, + size: Size.Default, }, argTypes: { locale: { control: 'select', options: Object.values(SupportedLocales) }, @@ -35,6 +43,8 @@ const meta: StoryMetaType = { control: 'select', options: [undefined, 'UTC', 'America/New_York', 'Europe/London'], }, + darkMode: storybookArgTypes.darkMode, + size: { control: 'select', options: Object.values(Size) }, }, }; diff --git a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx index d09c1978d3..5f52125e96 100644 --- a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx +++ b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx @@ -55,6 +55,7 @@ export const TimeInputInputs = forwardRef( unitOptions, }); + // eslint-disable-next-line no-console console.log('TimeInputInputs 🍉', { value: value?.toUTCString(), segmentObj, diff --git a/packages/time-input/src/TimeInputSegment/TimeInputSegment.tsx b/packages/time-input/src/TimeInputSegment/TimeInputSegment.tsx index 59cbb8f611..b8641b9ee6 100644 --- a/packages/time-input/src/TimeInputSegment/TimeInputSegment.tsx +++ b/packages/time-input/src/TimeInputSegment/TimeInputSegment.tsx @@ -17,6 +17,7 @@ export const TimeInputSegment = React.forwardRef< TimeInputSegmentProps >(({ children, segment, ...rest }: TimeInputSegmentProps, fwdRef) => { const { shouldShowSelect } = useTimeInputDisplayContext(); + return ( From 580c90fb0f5b4604a26f4658953e6b2b4de3860f Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Wed, 3 Dec 2025 17:31:04 -0500 Subject: [PATCH 15/49] test(time-input): add comprehensive unit tests for TimeInputSegment component, covering rendering, value updates, and keyboard interactions --- .../{TimeInputSegment.spec.ts => TimeInputSegment.spec.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/time-input/src/TimeInputSegment/{TimeInputSegment.spec.ts => TimeInputSegment.spec.tsx} (100%) diff --git a/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.ts b/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx similarity index 100% rename from packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.ts rename to packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx From 2495aa1ff26d5c59f8ea5cbcfa2d4f4e715e54bd Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Wed, 3 Dec 2025 17:31:56 -0500 Subject: [PATCH 16/49] test(time-input): add comprehensive unit tests for TimeInputSegment component, covering rendering, value updates, and keyboard interactions --- .../TimeInputDisplayContext.tsx | 1 + packages/time-input/src/TimeInput.stories.tsx | 2 +- .../TimeInputSegment.spec.tsx | 451 ++++++++++++++++++ .../src/TimeInputSegment/TimeInputSegment.tsx | 6 +- .../TimeInputSegment.types.ts | 4 +- packages/time-input/src/constants.ts | 15 +- packages/time-input/src/utils/getLgIds.ts | 1 + 7 files changed, 469 insertions(+), 11 deletions(-) diff --git a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx index a85052b32d..c7cc3cc6e7 100644 --- a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx +++ b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx @@ -44,6 +44,7 @@ export const TimeInputDisplayProvider = ({ // TODO: min, max helpers // Determines if the input should show a select for the day period (AM/PM) + // TODO: make this is12HourFormat an explicit prop const shouldShowSelect = !!hasDayPeriod(providerValue.locale); // Only used to track the presentation format of the segments, not the value itself diff --git a/packages/time-input/src/TimeInput.stories.tsx b/packages/time-input/src/TimeInput.stories.tsx index b1178d61c2..e2ccc4f01a 100644 --- a/packages/time-input/src/TimeInput.stories.tsx +++ b/packages/time-input/src/TimeInput.stories.tsx @@ -7,8 +7,8 @@ import { StoryFn } from '@storybook/react'; import { DateType, SupportedLocales } from '@leafygreen-ui/date-utils'; -import { TimeInput } from '.'; import { Size } from './TimeInput/TimeInput.types'; +import { TimeInput } from '.'; const meta: StoryMetaType = { title: 'Components/Inputs/TimeInput', diff --git a/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx b/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx index e69de29bb2..a17a300758 100644 --- a/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx +++ b/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx @@ -0,0 +1,451 @@ +import React from 'react'; +import { jest } from '@jest/globals'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { SupportedLocales } from '@leafygreen-ui/date-utils'; +import { getValueFormatter } from '@leafygreen-ui/input-box'; + +import { + defaultPlaceholder, + getDefaultMax, + getDefaultMin, + getTimeSegmentRules, +} from '../constants'; +import { TimeInputDisplayContextProps } from '../Context/TimeInputDisplayContext'; +import { TimeInputDisplayProvider } from '../Context/TimeInputDisplayContext'; + +import { TimeInputSegment } from './TimeInputSegment'; +import { + TimeInputSegmentChangeEventHandler, + TimeInputSegmentProps, + TimeSegments, +} from './TimeInputSegment.types'; + +const renderSegment = ( + props?: Partial, + ctx?: Partial, +) => { + const is12HourFormat = !!ctx?.shouldShowSelect; + const defaultSegmentProps = { + value: '', + onChange: () => {}, + segment: 'hour' as TimeSegments, // TODO: should be TimeSegment not TimeSegments + disabled: false, + segmentEnum: TimeSegments, + charsCount: getTimeSegmentRules(is12HourFormat)['hour'].maxChars, + minSegmentValue: getDefaultMin(is12HourFormat)['hour'], + maxSegmentValue: getDefaultMax(is12HourFormat)['hour'], + placeholder: defaultPlaceholder['hour'], + shouldWrap: true, + shouldValidate: true, + step: 1, + }; + + const result = render( + + + , + ); + + const rerenderSegment = (newProps: Partial) => + result.rerender( + + + , + ); + + const getInput = () => + result.getByTestId('lg-time_input_input-segment') as HTMLInputElement; + + return { + ...result, + rerenderSegment, + getInput, + input: getInput(), + }; +}; + +describe('packages/time-input/time-input-segment', () => { + describe('rendering', () => { + describe('segment', () => { + test('renders with an empty value sets the value to empty string', () => { + const { input } = renderSegment({ + value: '', + }); + expect(input.value).toBe(''); + }); + + test('renders with a value sets the value to the value', () => { + const { input } = renderSegment({ + value: '12', + }); + expect(input.value).toBe('12'); + }); + + test('rerendering updates the value', () => { + const { input, getInput, rerenderSegment } = renderSegment({ + value: '12', + }); + expect(input.value).toBe('12'); + rerenderSegment({ + value: '08', + }); + expect(getInput().value).toBe('08'); + }); + }); + }); + + describe('Keyboard', () => { + describe('Arrow Keys', () => { + describe('hour input', () => { + describe('Up arrow', () => {}); + const formatter = getValueFormatter({ + charsCount: getTimeSegmentRules(true)['hour'].maxChars, + }); + test('calls handler with value +1 if value is less than max', () => { + const onChangeHandler = jest.fn(); + const { input } = renderSegment({ + segment: 'hour', + value: formatter(15), + onChange: onChangeHandler, + }); + + userEvent.type(input, '{arrowup}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ value: formatter(16) }), + ); + }); + + describe('12 hour format', () => { + const formatter = getValueFormatter({ + charsCount: getTimeSegmentRules(true)['hour'].maxChars, + }); + + test('calls handler with min if value is undefined', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment( + { + segment: 'hour', + value: '', + onChange: onChangeHandler, + }, + { + locale: SupportedLocales.en_US, + }, + ); + + userEvent.type(input, '{arrowup}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: formatter(getDefaultMin(true)['hour']), + }), + ); + }); + + test('rolls value over to min value if value exceeds `max`', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment( + { + segment: 'hour', + value: formatter(getDefaultMax(true)['hour']), + onChange: onChangeHandler, + }, + { + locale: SupportedLocales.en_US, + }, + ); + + userEvent.type(input, '{arrowup}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: formatter(getDefaultMin(true)['hour']), + }), + ); + }); + }); + + describe('24 hour format', () => { + const formatter = getValueFormatter({ + charsCount: getTimeSegmentRules(false)['hour'].maxChars, + allowZero: true, + }); + + test('calls handler with min if value is undefined', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment( + { + segment: 'hour', + value: '', + onChange: onChangeHandler, + }, + { + locale: SupportedLocales.ISO_8601, + }, + ); + + userEvent.type(input, '{arrowup}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: formatter(getDefaultMin(false)['hour']), + }), + ); + }); + + test('rolls value over to min value if value exceeds `max`', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment( + { + segment: 'hour', + value: formatter(getDefaultMax(false)['hour']), + onChange: onChangeHandler, + }, + { + locale: SupportedLocales.ISO_8601, + }, + ); + + userEvent.type(input, '{arrowup}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: formatter(getDefaultMin(false)['hour']), + }), + ); + }); + }); + + describe('Down arrow', () => { + const formatter = getValueFormatter({ + charsCount: getTimeSegmentRules(true)['hour'].maxChars, + }); + test('calls handler with value -1 if value is greater than min', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment({ + segment: 'hour', + value: formatter(12), + onChange: onChangeHandler, + }); + + userEvent.type(input, '{arrowdown}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ value: formatter(11) }), + ); + }); + + describe('12 hour format', () => { + test('calls handler with max if value is undefined', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment( + { + segment: 'hour', + value: '', + onChange: onChangeHandler, + }, + { + locale: SupportedLocales.en_US, + }, + ); + + userEvent.type(input, '{arrowdown}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: formatter(getDefaultMax(true)['hour']), + }), + ); + }); + + test('rolls value over to max value if value exceeds `min`', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment( + { + segment: 'hour', + value: formatter(getDefaultMin(true)['hour']), + onChange: onChangeHandler, + }, + { + locale: SupportedLocales.en_US, + }, + ); + + userEvent.type(input, '{arrowdown}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: formatter(getDefaultMax(true)['hour']), + }), + ); + }); + }); + + describe('24 hour format', () => { + const formatter = getValueFormatter({ + charsCount: getTimeSegmentRules(true)['hour'].maxChars, + allowZero: true, + }); + + test('calls handler with max if value is undefined', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment( + { + segment: 'hour', + value: '', + onChange: onChangeHandler, + }, + { + locale: SupportedLocales.ISO_8601, + }, + ); + + userEvent.type(input, '{arrowdown}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: formatter(getDefaultMax(false)['hour']), + }), + ); + }); + + test('rolls value over to max value if value exceeds `min`', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment( + { + segment: 'hour', + value: formatter(getDefaultMin(false)['hour']), + onChange: onChangeHandler, + }, + { + locale: SupportedLocales.ISO_8601, + }, + ); + + userEvent.type(input, '{arrowdown}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: formatter(getDefaultMax(false)['hour']), + }), + ); + }); + }); + }); + }); + + describe.each(['minute', 'second'] as Array)( + '%p input', + segment => { + const formatter = getValueFormatter({ + charsCount: getTimeSegmentRules(true)[segment].maxChars, + allowZero: true, + }); + + describe('Up arrow', () => { + test('calls handler with value +1 if value is less than max', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment({ + segment, + value: formatter(15), + onChange: onChangeHandler, + }); + + userEvent.type(input, '{arrowup}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ value: formatter(16) }), + ); + }); + + test('calls handler with min if value is undefined', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment({ + segment, + value: '', + onChange: onChangeHandler, + }); + + userEvent.type(input, '{arrowup}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: formatter(getDefaultMin(true)[segment]), + }), + ); + }); + + test('rolls value over to min value if value exceeds `max`', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment({ + segment, + value: formatter(getDefaultMax(true)[segment]), + onChange: onChangeHandler, + }); + + userEvent.type(input, '{arrowup}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: formatter(getDefaultMin(true)[segment]), + }), + ); + }); + }); + describe('Down arrow', () => { + test('calls handler with value -1 if value is greater than min', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment({ + segment, + value: formatter(12), + onChange: onChangeHandler, + }); + + userEvent.type(input, '{arrowdown}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ value: formatter(11) }), + ); + }); + + test('calls handler with max if value is undefined', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment({ + segment, + value: '', + onChange: onChangeHandler, + }); + + userEvent.type(input, '{arrowdown}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: formatter(getDefaultMax(true)[segment]), + }), + ); + }); + + test('rolls value over to max value if value exceeds `min`', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment({ + segment: 'minute', + value: formatter(getDefaultMin(true)[segment]), + onChange: onChangeHandler, + }); + + userEvent.type(input, '{arrowdown}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: formatter(getDefaultMax(true)[segment]), + }), + ); + }); + }); + }, + ); + }); + }); +}); diff --git a/packages/time-input/src/TimeInputSegment/TimeInputSegment.tsx b/packages/time-input/src/TimeInputSegment/TimeInputSegment.tsx index b8641b9ee6..1aa70c2cf6 100644 --- a/packages/time-input/src/TimeInputSegment/TimeInputSegment.tsx +++ b/packages/time-input/src/TimeInputSegment/TimeInputSegment.tsx @@ -3,8 +3,8 @@ import React from 'react'; import { InputSegment } from '@leafygreen-ui/input-box'; import { - defaultMax, defaultPlaceholder, + getDefaultMax, getDefaultMin, getTimeSegmentRules, } from '../constants'; @@ -24,11 +24,11 @@ export const TimeInputSegment = React.forwardRef< ref={fwdRef} segment={segment} minSegmentValue={getDefaultMin(shouldShowSelect)[segment]} - maxSegmentValue={defaultMax[segment]} + maxSegmentValue={getDefaultMax(shouldShowSelect)[segment]} placeholder={defaultPlaceholder[segment]} // className={cx(segmentWidthStyles[segment])} - // data-testid // data-lgid + data-testid="lg-time_input_input-segment" // TODO: temp charsCount={getTimeSegmentRules(shouldShowSelect)[segment].maxChars} autoComplete="off" /> diff --git a/packages/time-input/src/TimeInputSegment/TimeInputSegment.types.ts b/packages/time-input/src/TimeInputSegment/TimeInputSegment.types.ts index f4bbbc4e93..d96ab8b24e 100644 --- a/packages/time-input/src/TimeInputSegment/TimeInputSegment.types.ts +++ b/packages/time-input/src/TimeInputSegment/TimeInputSegment.types.ts @@ -14,7 +14,7 @@ export type TimeSegments = (typeof TimeSegments)[keyof typeof TimeSegments]; export type TimeSegmentsState = Record; -export interface DateInputSegmentChangeEvent { +export interface TimeInputSegmentChangeEvent { segment: TimeSegments; value: string; meta?: { @@ -23,7 +23,7 @@ export interface DateInputSegmentChangeEvent { }; } -export type DateInputSegmentChangeEventHandler = InputSegmentChangeEventHandler< +export type TimeInputSegmentChangeEventHandler = InputSegmentChangeEventHandler< TimeSegments, string >; diff --git a/packages/time-input/src/constants.ts b/packages/time-input/src/constants.ts index a7d7dfe606..dfa69cf3a8 100644 --- a/packages/time-input/src/constants.ts +++ b/packages/time-input/src/constants.ts @@ -6,6 +6,7 @@ export const unitOptions = [ { displayName: 'PM', value: 'PM' }, ]; +// TODO: make is12HourFormat an explicit prop export const getTimeSegmentRules = (is12HourFormat: boolean) => { return { [TimeSegments.Hour]: { @@ -26,6 +27,7 @@ export const getTimeSegmentRules = (is12HourFormat: boolean) => { /** * The minimum number for each segment */ +// TODO: make is12HourFormat an explicit prop export const getDefaultMin = (is12HourFormat: boolean) => { return { hour: is12HourFormat ? 1 : 0, @@ -37,11 +39,14 @@ export const getDefaultMin = (is12HourFormat: boolean) => { /** * The maximum number for each segment */ -export const defaultMax = { - hour: 23, - minute: 59, - second: 59, -} as const; +// TODO: make is12HourFormat an explicit prop +export const getDefaultMax = (is12HourFormat: boolean) => { + return { + hour: is12HourFormat ? 12 : 23, + minute: 59, + second: 59, + } as const; +}; /** * The default placeholders for each segment diff --git a/packages/time-input/src/utils/getLgIds.ts b/packages/time-input/src/utils/getLgIds.ts index a59854ff3d..e6301b646b 100644 --- a/packages/time-input/src/utils/getLgIds.ts +++ b/packages/time-input/src/utils/getLgIds.ts @@ -5,6 +5,7 @@ export const DEFAULT_LGID_ROOT = 'lg-time_input'; export const getLgIds = (root: LgIdString = DEFAULT_LGID_ROOT) => { const ids = { root, + inputSegment: `${root}-input-segment`, } as const; return ids; }; From 4cad73052c3d0a209afe5b9d5c3e9425a2e9edda Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Wed, 3 Dec 2025 18:01:45 -0500 Subject: [PATCH 17/49] refactor(time-input): update time segment handling to use explicit is12HourFormat prop, replacing TimeSegments with TimeSegment for improved clarity and consistency --- .../TimeInputDisplayContext.tsx | 20 ++-- .../TimeInputDisplayContext.types.ts | 6 +- .../TimePickerDisplayContext.utils.ts | 2 +- .../TimeFormFieldInputContainer.styles.ts | 8 +- .../TimeFormFieldInputContainer.tsx | 4 +- .../src/TimeInputBox/TimeInputBox.tsx | 8 +- .../src/TimeInputBox/TimeInputBox.types.ts | 7 +- .../src/TimeInputInputs/TimeInputInputs.tsx | 39 ++++--- .../TimeInputSegment.spec.tsx | 106 ++++++++++++------ .../src/TimeInputSegment/TimeInputSegment.tsx | 12 +- .../TimeInputSegment.types.ts | 16 +-- .../time-input/src/TimeInputSegment/index.ts | 2 +- packages/time-input/src/constants.ts | 46 ++++++-- packages/time-input/src/shared.types.ts | 16 +++ 14 files changed, 189 insertions(+), 103 deletions(-) diff --git a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx index c7cc3cc6e7..59f67fae4f 100644 --- a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx +++ b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx @@ -41,22 +41,28 @@ export const TimeInputDisplayProvider = ({ ...defaults(rest, defaultTimeInputDisplayContext), }; - // TODO: min, max helpers - - // Determines if the input should show a select for the day period (AM/PM) - // TODO: make this is12HourFormat an explicit prop - const shouldShowSelect = !!hasDayPeriod(providerValue.locale); + /** + * Determines if the input should show a select for the day period (AM/PM) + */ + const is12HourFormat = !!hasDayPeriod(providerValue.locale); - // Only used to track the presentation format of the segments, not the value itself + /** + * Only used to track the presentation format of the segments, not the value itself + */ const formatParts = getFormatParts({ showSeconds: providerValue.showSeconds, }); + /** + * Gets the time zone from the provider value or the browser's default + */ const timeZone = defaultTo( providerValue.timeZone, Intl.DateTimeFormat().resolvedOptions().timeZone, ); + // TODO: min, max helpers coming soon + return ( >; /** - * Whether the AM/PM select should be shown + * Whether the time input is in 12-hour format. Helps determine if the AM/PM select should be shown. + * + * @default false */ - shouldShowSelect: boolean; + is12HourFormat: boolean; /** * An array of {@link Intl.DateTimeFormatPart}, diff --git a/packages/time-input/src/Context/TimeInputDisplayContext/TimePickerDisplayContext.utils.ts b/packages/time-input/src/Context/TimeInputDisplayContext/TimePickerDisplayContext.utils.ts index 75fbd6f7f2..a6e1ce1f7e 100644 --- a/packages/time-input/src/Context/TimeInputDisplayContext/TimePickerDisplayContext.utils.ts +++ b/packages/time-input/src/Context/TimeInputDisplayContext/TimePickerDisplayContext.utils.ts @@ -52,6 +52,6 @@ export const defaultTimeInputDisplayContext: TimeInputDisplayContextProps = { errorMessage: '', isDirty: false, setIsDirty: () => {}, - shouldShowSelect: false, + is12HourFormat: false, showSeconds: true, }; diff --git a/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.styles.ts b/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.styles.ts index 79be4e4d30..139e3ed68f 100644 --- a/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.styles.ts +++ b/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.styles.ts @@ -12,7 +12,11 @@ const baseStyles = css` } `; -export const getContainerStyles = (shouldShowSelect: boolean) => +export const getContainerStyles = ({ + is12HourFormat, +}: { + is12HourFormat: boolean; +}) => cx(baseStyles, { - [selectStyles]: shouldShowSelect, + [selectStyles]: is12HourFormat, }); diff --git a/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.tsx b/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.tsx index cbd2d373d6..f96fb0d74e 100644 --- a/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.tsx +++ b/packages/time-input/src/TimeFormField/TimeFormFieldInputContainer/TimeFormFieldInputContainer.tsx @@ -15,7 +15,7 @@ export const TimeFormFieldInputContainer = React.forwardRef< HTMLDivElement, TimeFormFieldInputContainerProps >(({ children, onInputClick }: TimeFormFieldInputContainerProps, fwdRef) => { - const { label, ariaLabelProp, ariaLabelledbyProp, shouldShowSelect } = + const { label, ariaLabelProp, ariaLabelledbyProp, is12HourFormat } = useTimeInputDisplayContext(); return ( @@ -30,7 +30,7 @@ export const TimeFormFieldInputContainer = React.forwardRef< : undefined } onClick={onInputClick} - className={getContainerStyles(shouldShowSelect)} + className={getContainerStyles({ is12HourFormat })} > {children} diff --git a/packages/time-input/src/TimeInputBox/TimeInputBox.tsx b/packages/time-input/src/TimeInputBox/TimeInputBox.tsx index 2e03fd2d58..1ab56e62e7 100644 --- a/packages/time-input/src/TimeInputBox/TimeInputBox.tsx +++ b/packages/time-input/src/TimeInputBox/TimeInputBox.tsx @@ -4,20 +4,20 @@ import { InputBox } from '@leafygreen-ui/input-box'; import { getTimeSegmentRules } from '../constants'; import { useTimeInputDisplayContext } from '../Context/TimeInputDisplayContext/TimeInputDisplayContext'; +import { TimeSegment } from '../shared.types'; import { TimeInputSegment } from '../TimeInputSegment/TimeInputSegment'; -import { TimeSegments } from '../TimeInputSegment/TimeInputSegment.types'; import { TimeInputBoxProps } from './TimeInputBox.types'; export const TimeInputBox = React.forwardRef( ({ children, ...rest }: TimeInputBoxProps, fwdRef) => { - const { disabled, formatParts, size, shouldShowSelect } = + const { disabled, formatParts, size, is12HourFormat } = useTimeInputDisplayContext(); return ( { segments: TimeSegmentsState; - setSegment: (segment: TimeSegments, value: string) => void; + setSegment: (segment: TimeSegment, value: string) => void; } diff --git a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx index 5f52125e96..f43602e489 100644 --- a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx +++ b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx @@ -1,12 +1,12 @@ -import React, { forwardRef, useMemo } from 'react'; +import React, { forwardRef } from 'react'; import { unitOptions } from '../constants'; import { useTimeInputContext } from '../Context/TimeInputContext/TimeInputContext'; import { useTimeInputDisplayContext } from '../Context/TimeInputDisplayContext/TimeInputDisplayContext'; import { useSelectUnit } from '../hooks'; +import { TimeSegmentsState } from '../shared.types'; import { TimeFormField, TimeFormFieldInputContainer } from '../TimeFormField'; import { TimeInputBox } from '../TimeInputBox/TimeInputBox'; -import { TimeSegmentsState } from '../TimeInputSegment/TimeInputSegment.types'; import { TimeInputSelect } from '../TimeInputSelect/TimeInputSelect'; import { UnitOption } from '../TimeInputSelect/TimeInputSelect.types'; import { getFormatPartsValues } from '../utils'; @@ -19,13 +19,16 @@ import { TimeInputInputsProps } from './TimeInputInputs.types'; */ export const TimeInputInputs = forwardRef( (_props: TimeInputInputsProps, forwardedRef) => { - const { shouldShowSelect, timeZone, locale } = useTimeInputDisplayContext(); + const { is12HourFormat, timeZone, locale } = useTimeInputDisplayContext(); const { value } = useTimeInputContext(); const handleSelectChange = (unit: UnitOption) => { setSelectUnit(unit); }; + /** + * Gets the time parts from the value + */ const timeParts = getFormatPartsValues({ locale: locale, timeZone: timeZone, @@ -35,16 +38,15 @@ export const TimeInputInputs = forwardRef( const { hour, minute, second } = timeParts; /** - * Creates a memoized object of the time segments + * Creates time segments object + * + * // TODO: these are temp */ - const segmentObj: TimeSegmentsState = useMemo( - () => ({ - hour, - minute, - second, - }), - [hour, minute, second], - ); + const segmentObj: TimeSegmentsState = { + hour, + minute, + second, + }; /** * Hook to manage the select unit @@ -55,11 +57,11 @@ export const TimeInputInputs = forwardRef( unitOptions, }); - // eslint-disable-next-line no-console - console.log('TimeInputInputs 🍉', { - value: value?.toUTCString(), - segmentObj, - }); + // // eslint-disable-next-line no-console + // console.log('TimeInputInputs 🍉', { + // value: value?.toUTCString(), + // segmentObj, + // }); return ( @@ -68,11 +70,12 @@ export const TimeInputInputs = forwardRef( { + // eslint-disable-next-line no-console console.log({ segment, value }); }} /> - {shouldShowSelect && ( + {is12HourFormat && ( { diff --git a/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx b/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx index a17a300758..d83f7fe36b 100644 --- a/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx +++ b/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx @@ -14,28 +14,28 @@ import { } from '../constants'; import { TimeInputDisplayContextProps } from '../Context/TimeInputDisplayContext'; import { TimeInputDisplayProvider } from '../Context/TimeInputDisplayContext'; +import { TimeSegment } from '../shared.types'; import { TimeInputSegment } from './TimeInputSegment'; import { TimeInputSegmentChangeEventHandler, TimeInputSegmentProps, - TimeSegments, } from './TimeInputSegment.types'; const renderSegment = ( props?: Partial, ctx?: Partial, ) => { - const is12HourFormat = !!ctx?.shouldShowSelect; + const is12HourFormat = !!ctx?.is12HourFormat; const defaultSegmentProps = { value: '', onChange: () => {}, - segment: 'hour' as TimeSegments, // TODO: should be TimeSegment not TimeSegments + segment: 'hour' as TimeSegment, disabled: false, - segmentEnum: TimeSegments, - charsCount: getTimeSegmentRules(is12HourFormat)['hour'].maxChars, - minSegmentValue: getDefaultMin(is12HourFormat)['hour'], - maxSegmentValue: getDefaultMax(is12HourFormat)['hour'], + segmentEnum: TimeSegment, + charsCount: getTimeSegmentRules({ is12HourFormat })['hour'].maxChars, + minSegmentValue: getDefaultMin({ is12HourFormat })['hour'], + maxSegmentValue: getDefaultMax({ is12HourFormat })['hour'], placeholder: defaultPlaceholder['hour'], shouldWrap: true, shouldValidate: true, @@ -101,7 +101,8 @@ describe('packages/time-input/time-input-segment', () => { describe('hour input', () => { describe('Up arrow', () => {}); const formatter = getValueFormatter({ - charsCount: getTimeSegmentRules(true)['hour'].maxChars, + charsCount: getTimeSegmentRules({ is12HourFormat: true })['hour'] + .maxChars, }); test('calls handler with value +1 if value is less than max', () => { const onChangeHandler = jest.fn(); @@ -119,7 +120,8 @@ describe('packages/time-input/time-input-segment', () => { describe('12 hour format', () => { const formatter = getValueFormatter({ - charsCount: getTimeSegmentRules(true)['hour'].maxChars, + charsCount: getTimeSegmentRules({ is12HourFormat: true })['hour'] + .maxChars, }); test('calls handler with min if value is undefined', () => { @@ -139,7 +141,9 @@ describe('packages/time-input/time-input-segment', () => { userEvent.type(input, '{arrowup}'); expect(onChangeHandler).toHaveBeenCalledWith( expect.objectContaining({ - value: formatter(getDefaultMin(true)['hour']), + value: formatter( + getDefaultMin({ is12HourFormat: true })['hour'], + ), }), ); }); @@ -150,7 +154,9 @@ describe('packages/time-input/time-input-segment', () => { const { input } = renderSegment( { segment: 'hour', - value: formatter(getDefaultMax(true)['hour']), + value: formatter( + getDefaultMax({ is12HourFormat: true })['hour'], + ), onChange: onChangeHandler, }, { @@ -161,7 +167,9 @@ describe('packages/time-input/time-input-segment', () => { userEvent.type(input, '{arrowup}'); expect(onChangeHandler).toHaveBeenCalledWith( expect.objectContaining({ - value: formatter(getDefaultMin(true)['hour']), + value: formatter( + getDefaultMin({ is12HourFormat: true })['hour'], + ), }), ); }); @@ -169,7 +177,8 @@ describe('packages/time-input/time-input-segment', () => { describe('24 hour format', () => { const formatter = getValueFormatter({ - charsCount: getTimeSegmentRules(false)['hour'].maxChars, + charsCount: getTimeSegmentRules({ is12HourFormat: false })['hour'] + .maxChars, allowZero: true, }); @@ -190,7 +199,9 @@ describe('packages/time-input/time-input-segment', () => { userEvent.type(input, '{arrowup}'); expect(onChangeHandler).toHaveBeenCalledWith( expect.objectContaining({ - value: formatter(getDefaultMin(false)['hour']), + value: formatter( + getDefaultMin({ is12HourFormat: false })['hour'], + ), }), ); }); @@ -201,7 +212,9 @@ describe('packages/time-input/time-input-segment', () => { const { input } = renderSegment( { segment: 'hour', - value: formatter(getDefaultMax(false)['hour']), + value: formatter( + getDefaultMax({ is12HourFormat: false })['hour'], + ), onChange: onChangeHandler, }, { @@ -212,7 +225,9 @@ describe('packages/time-input/time-input-segment', () => { userEvent.type(input, '{arrowup}'); expect(onChangeHandler).toHaveBeenCalledWith( expect.objectContaining({ - value: formatter(getDefaultMin(false)['hour']), + value: formatter( + getDefaultMin({ is12HourFormat: false })['hour'], + ), }), ); }); @@ -220,7 +235,8 @@ describe('packages/time-input/time-input-segment', () => { describe('Down arrow', () => { const formatter = getValueFormatter({ - charsCount: getTimeSegmentRules(true)['hour'].maxChars, + charsCount: getTimeSegmentRules({ is12HourFormat: true })['hour'] + .maxChars, }); test('calls handler with value -1 if value is greater than min', () => { const onChangeHandler = @@ -255,7 +271,9 @@ describe('packages/time-input/time-input-segment', () => { userEvent.type(input, '{arrowdown}'); expect(onChangeHandler).toHaveBeenCalledWith( expect.objectContaining({ - value: formatter(getDefaultMax(true)['hour']), + value: formatter( + getDefaultMax({ is12HourFormat: true })['hour'], + ), }), ); }); @@ -266,7 +284,9 @@ describe('packages/time-input/time-input-segment', () => { const { input } = renderSegment( { segment: 'hour', - value: formatter(getDefaultMin(true)['hour']), + value: formatter( + getDefaultMin({ is12HourFormat: true })['hour'], + ), onChange: onChangeHandler, }, { @@ -277,7 +297,9 @@ describe('packages/time-input/time-input-segment', () => { userEvent.type(input, '{arrowdown}'); expect(onChangeHandler).toHaveBeenCalledWith( expect.objectContaining({ - value: formatter(getDefaultMax(true)['hour']), + value: formatter( + getDefaultMax({ is12HourFormat: true })['hour'], + ), }), ); }); @@ -285,7 +307,8 @@ describe('packages/time-input/time-input-segment', () => { describe('24 hour format', () => { const formatter = getValueFormatter({ - charsCount: getTimeSegmentRules(true)['hour'].maxChars, + charsCount: getTimeSegmentRules({ is12HourFormat: false })['hour'] + .maxChars, allowZero: true, }); @@ -306,7 +329,9 @@ describe('packages/time-input/time-input-segment', () => { userEvent.type(input, '{arrowdown}'); expect(onChangeHandler).toHaveBeenCalledWith( expect.objectContaining({ - value: formatter(getDefaultMax(false)['hour']), + value: formatter( + getDefaultMax({ is12HourFormat: false })['hour'], + ), }), ); }); @@ -317,7 +342,9 @@ describe('packages/time-input/time-input-segment', () => { const { input } = renderSegment( { segment: 'hour', - value: formatter(getDefaultMin(false)['hour']), + value: formatter( + getDefaultMin({ is12HourFormat: false })['hour'], + ), onChange: onChangeHandler, }, { @@ -328,7 +355,9 @@ describe('packages/time-input/time-input-segment', () => { userEvent.type(input, '{arrowdown}'); expect(onChangeHandler).toHaveBeenCalledWith( expect.objectContaining({ - value: formatter(getDefaultMax(false)['hour']), + value: formatter( + getDefaultMax({ is12HourFormat: false })['hour'], + ), }), ); }); @@ -336,11 +365,12 @@ describe('packages/time-input/time-input-segment', () => { }); }); - describe.each(['minute', 'second'] as Array)( + describe.each(['minute', 'second'] as Array)( '%p input', segment => { const formatter = getValueFormatter({ - charsCount: getTimeSegmentRules(true)[segment].maxChars, + charsCount: getTimeSegmentRules({ is12HourFormat: true })[segment] + .maxChars, allowZero: true, }); @@ -372,7 +402,9 @@ describe('packages/time-input/time-input-segment', () => { userEvent.type(input, '{arrowup}'); expect(onChangeHandler).toHaveBeenCalledWith( expect.objectContaining({ - value: formatter(getDefaultMin(true)[segment]), + value: formatter( + getDefaultMin({ is12HourFormat: true })[segment], + ), }), ); }); @@ -382,14 +414,18 @@ describe('packages/time-input/time-input-segment', () => { jest.fn(); const { input } = renderSegment({ segment, - value: formatter(getDefaultMax(true)[segment]), + value: formatter( + getDefaultMax({ is12HourFormat: true })[segment], + ), onChange: onChangeHandler, }); userEvent.type(input, '{arrowup}'); expect(onChangeHandler).toHaveBeenCalledWith( expect.objectContaining({ - value: formatter(getDefaultMin(true)[segment]), + value: formatter( + getDefaultMin({ is12HourFormat: true })[segment], + ), }), ); }); @@ -422,7 +458,9 @@ describe('packages/time-input/time-input-segment', () => { userEvent.type(input, '{arrowdown}'); expect(onChangeHandler).toHaveBeenCalledWith( expect.objectContaining({ - value: formatter(getDefaultMax(true)[segment]), + value: formatter( + getDefaultMax({ is12HourFormat: true })[segment], + ), }), ); }); @@ -432,14 +470,18 @@ describe('packages/time-input/time-input-segment', () => { jest.fn(); const { input } = renderSegment({ segment: 'minute', - value: formatter(getDefaultMin(true)[segment]), + value: formatter( + getDefaultMin({ is12HourFormat: true })[segment], + ), onChange: onChangeHandler, }); userEvent.type(input, '{arrowdown}'); expect(onChangeHandler).toHaveBeenCalledWith( expect.objectContaining({ - value: formatter(getDefaultMax(true)[segment]), + value: formatter( + getDefaultMax({ is12HourFormat: true })[segment], + ), }), ); }); diff --git a/packages/time-input/src/TimeInputSegment/TimeInputSegment.tsx b/packages/time-input/src/TimeInputSegment/TimeInputSegment.tsx index 1aa70c2cf6..924e63bc85 100644 --- a/packages/time-input/src/TimeInputSegment/TimeInputSegment.tsx +++ b/packages/time-input/src/TimeInputSegment/TimeInputSegment.tsx @@ -16,20 +16,20 @@ export const TimeInputSegment = React.forwardRef< HTMLInputElement, TimeInputSegmentProps >(({ children, segment, ...rest }: TimeInputSegmentProps, fwdRef) => { - const { shouldShowSelect } = useTimeInputDisplayContext(); + const { is12HourFormat } = useTimeInputDisplayContext(); return ( ); diff --git a/packages/time-input/src/TimeInputSegment/TimeInputSegment.types.ts b/packages/time-input/src/TimeInputSegment/TimeInputSegment.types.ts index d96ab8b24e..73a27a1f88 100644 --- a/packages/time-input/src/TimeInputSegment/TimeInputSegment.types.ts +++ b/packages/time-input/src/TimeInputSegment/TimeInputSegment.types.ts @@ -4,18 +4,10 @@ import { } from '@leafygreen-ui/input-box'; import { keyMap } from '@leafygreen-ui/lib'; -export const TimeSegments = { - Hour: 'hour', - Minute: 'minute', - Second: 'second', -} as const; - -export type TimeSegments = (typeof TimeSegments)[keyof typeof TimeSegments]; - -export type TimeSegmentsState = Record; +import { TimeSegment } from '../shared.types'; export interface TimeInputSegmentChangeEvent { - segment: TimeSegments; + segment: TimeSegment; value: string; meta?: { key?: (typeof keyMap)[keyof typeof keyMap]; @@ -24,9 +16,9 @@ export interface TimeInputSegmentChangeEvent { } export type TimeInputSegmentChangeEventHandler = InputSegmentChangeEventHandler< - TimeSegments, + TimeSegment, string >; export interface TimeInputSegmentProps - extends InputSegmentComponentProps {} + extends InputSegmentComponentProps {} diff --git a/packages/time-input/src/TimeInputSegment/index.ts b/packages/time-input/src/TimeInputSegment/index.ts index 49345707c0..db95e5d49b 100644 --- a/packages/time-input/src/TimeInputSegment/index.ts +++ b/packages/time-input/src/TimeInputSegment/index.ts @@ -1 +1 @@ -export { TimeSegments } from './TimeInputSegment.types'; +export { TimeInputSegment } from './TimeInputSegment'; diff --git a/packages/time-input/src/constants.ts b/packages/time-input/src/constants.ts index dfa69cf3a8..b1c6df8cca 100644 --- a/packages/time-input/src/constants.ts +++ b/packages/time-input/src/constants.ts @@ -1,23 +1,33 @@ -import { TimeSegments } from './TimeInputSegment/TimeInputSegment.types'; -import { TimeParts } from './shared.types'; +import { TimeParts, TimeSegment } from './shared.types'; +/** + * The options for the unit select + */ export const unitOptions = [ { displayName: 'AM', value: 'AM' }, { displayName: 'PM', value: 'PM' }, ]; -// TODO: make is12HourFormat an explicit prop -export const getTimeSegmentRules = (is12HourFormat: boolean) => { +/** + * The rules for the time segments + * + * @param is12HourFormat - Whether the time input is in 12-hour format + */ +export const getTimeSegmentRules = ({ + is12HourFormat, +}: { + is12HourFormat: boolean; +}) => { return { - [TimeSegments.Hour]: { + [TimeSegment.Hour]: { maxChars: 2, minExplicitValue: is12HourFormat ? 1 : 2, }, - [TimeSegments.Minute]: { + [TimeSegment.Minute]: { maxChars: 2, minExplicitValue: 6, }, - [TimeSegments.Second]: { + [TimeSegment.Second]: { maxChars: 2, minExplicitValue: 6, }, @@ -26,9 +36,14 @@ export const getTimeSegmentRules = (is12HourFormat: boolean) => { /** * The minimum number for each segment + * + * @param is12HourFormat - Whether the time input is in 12-hour format */ -// TODO: make is12HourFormat an explicit prop -export const getDefaultMin = (is12HourFormat: boolean) => { +export const getDefaultMin = ({ + is12HourFormat, +}: { + is12HourFormat: boolean; +}) => { return { hour: is12HourFormat ? 1 : 0, minute: 0, @@ -38,9 +53,14 @@ export const getDefaultMin = (is12HourFormat: boolean) => { /** * The maximum number for each segment + * + * @param is12HourFormat - Whether the time input is in 12-hour format */ -// TODO: make is12HourFormat an explicit prop -export const getDefaultMax = (is12HourFormat: boolean) => { +export const getDefaultMax = ({ + is12HourFormat, +}: { + is12HourFormat: boolean; +}) => { return { hour: is12HourFormat ? 12 : 23, minute: 59, @@ -56,6 +76,10 @@ export const defaultPlaceholder = { minute: 'MM', second: 'SS', } as const; + +/** + * The default time parts + */ export const defaultTimeParts: TimeParts = { hour: '', minute: '', diff --git a/packages/time-input/src/shared.types.ts b/packages/time-input/src/shared.types.ts index 21199a796c..8bc5504704 100644 --- a/packages/time-input/src/shared.types.ts +++ b/packages/time-input/src/shared.types.ts @@ -1,3 +1,6 @@ +/** + * An enumerable object that maps the time part names to their values + */ export const TimePartKeys = { hour: 'hour', minute: 'minute', @@ -11,3 +14,16 @@ export const TimePartKeys = { export type TimePartKeys = (typeof TimePartKeys)[keyof typeof TimePartKeys]; export type TimeParts = Record; + +/** + * An enumerable object that maps the time segment names to their values + */ +export const TimeSegment = { + Hour: 'hour', + Minute: 'minute', + Second: 'second', +} as const; + +export type TimeSegment = (typeof TimeSegment)[keyof typeof TimeSegment]; + +export type TimeSegmentsState = Record; From 1afe57edcf88eb885ba61ba372d6c1291a138aba Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Fri, 12 Dec 2025 09:11:36 -0500 Subject: [PATCH 18/49] refactor(time-input): rename shouldShowSelect to is12hFormat for clarity in time input context --- .../TimeInputDisplayContext.tsx | 4 ++-- .../TimeInputDisplayContext.types.ts | 2 +- .../TimePickerDisplayContext.utils.ts | 2 +- .../src/TimeInputInputs/TimeInputInputs.tsx | 14 ++------------ 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx index a85052b32d..581b374a32 100644 --- a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx +++ b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.tsx @@ -44,7 +44,7 @@ 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); + const is12hFormat = !!hasDayPeriod(providerValue.locale); // Only used to track the presentation format of the segments, not the value itself const formatParts = getFormatParts({ @@ -65,7 +65,7 @@ export const TimeInputDisplayProvider = ({ ariaLabelledbyProp, isDirty, setIsDirty, - shouldShowSelect, + is12hFormat, formatParts, timeZone, }} diff --git a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.types.ts b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.types.ts index 982fab07a8..1a438e3729 100644 --- a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.types.ts +++ b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.types.ts @@ -42,7 +42,7 @@ export type TimeInputDisplayContextProps = Omit< /** * Whether the AM/PM select should be shown */ - shouldShowSelect: boolean; + is12hFormat: boolean; /** * An array of {@link Intl.DateTimeFormatPart}, diff --git a/packages/time-input/src/Context/TimeInputDisplayContext/TimePickerDisplayContext.utils.ts b/packages/time-input/src/Context/TimeInputDisplayContext/TimePickerDisplayContext.utils.ts index 75fbd6f7f2..9f178ddb6d 100644 --- a/packages/time-input/src/Context/TimeInputDisplayContext/TimePickerDisplayContext.utils.ts +++ b/packages/time-input/src/Context/TimeInputDisplayContext/TimePickerDisplayContext.utils.ts @@ -52,6 +52,6 @@ export const defaultTimeInputDisplayContext: TimeInputDisplayContextProps = { errorMessage: '', isDirty: false, setIsDirty: () => {}, - shouldShowSelect: false, + is12hFormat: false, showSeconds: true, }; diff --git a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx index 1cca1bcea2..966b87c990 100644 --- a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx +++ b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck import React, { forwardRef, useState } from 'react'; import { cx } from '@leafygreen-ui/emotion'; @@ -19,7 +18,7 @@ import { TimeInputInputsProps } from './TimeInputInputs.types'; */ export const TimeInputInputs = forwardRef( (_props: TimeInputInputsProps, forwardedRef) => { - const { shouldShowSelect, formatParts, timeZone, locale } = + const { is12hFormat, formatParts, timeZone, locale } = useTimeInputDisplayContext(); const [selectUnit, setSelectUnit] = useState(unitOptions[0]); @@ -35,15 +34,6 @@ export const TimeInputInputs = forwardRef( value: value, }); - // console.log('TimeInputInputs 🍉', { - // shouldShowSelect, - // formatParts, - // timeZone, - // value: value?.toUTCString(), - // timeParts, - // locale, - // }); - // TODO: break this out more return ( @@ -51,7 +41,7 @@ export const TimeInputInputs = forwardRef(
TODO: Input segments go here
- {shouldShowSelect && ( + {is12hFormat && ( { From cb84996798ff18bc5df488c70b8aa3e263c150f5 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Fri, 12 Dec 2025 09:13:32 -0500 Subject: [PATCH 19/49] refactor(time-input): clean up unused state and comments in TimeInput stories --- packages/time-input/src/TimeInput.stories.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/time-input/src/TimeInput.stories.tsx b/packages/time-input/src/TimeInput.stories.tsx index 7ef71c40ae..005a8751e0 100644 --- a/packages/time-input/src/TimeInput.stories.tsx +++ b/packages/time-input/src/TimeInput.stories.tsx @@ -27,7 +27,6 @@ const meta: StoryMetaType = { showSeconds: true, locale: SupportedLocales.ISO_8601, timeZone: 'UTC', - // value: new Date('1990-02-20T12:30:00Z'), }, argTypes: { locale: { control: 'select', options: Object.values(SupportedLocales) }, @@ -44,10 +43,6 @@ const Template: StoryFn = props => { const [value, setValue] = useState( new Date('1990-02-20T14:30:50Z'), ); - // const [value, setValue] = useState(); - // const [value, setValue] = useState( - // new Date('1990--20T14:30:50Z'), - // ); return ( setValue(time)} /> From 6e265a73209a9e5f77e8c0a122c30ca8e159352f Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Fri, 12 Dec 2025 09:21:37 -0500 Subject: [PATCH 20/49] chore(pnpm-lock): add workspace links for @leafygreen-ui/input-box to pnpm-lock.yaml --- pnpm-lock.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 415ea5d7e7..ab7e3cd4d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3543,6 +3543,9 @@ importers: '@leafygreen-ui/hooks': specifier: workspace:^ version: link:../hooks + '@leafygreen-ui/input-box': + specifier: workspace:^ + version: link:../input-box '@leafygreen-ui/leafygreen-provider': specifier: workspace:^ version: link:../leafygreen-provider From 2547fdc8d64845c2eebd2f431f552967ee57720c Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Fri, 12 Dec 2025 09:39:04 -0500 Subject: [PATCH 21/49] refactor(time-input): remove unused console logs and simplify time formatting logic --- packages/time-input/src/TimeInput/TimeInput.tsx | 2 -- packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx | 6 ++++-- .../src/utils/getFormatPartsValues/getFormatPartsValues.ts | 4 ++-- .../time-input/src/utils/getFormatter/getFormatter.spec.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/time-input/src/TimeInput/TimeInput.tsx b/packages/time-input/src/TimeInput/TimeInput.tsx index 261b0e4573..b97481ed36 100644 --- a/packages/time-input/src/TimeInput/TimeInput.tsx +++ b/packages/time-input/src/TimeInput/TimeInput.tsx @@ -42,8 +42,6 @@ export const TimeInput = forwardRef( 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 */ diff --git a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx index 966b87c990..36a3377d48 100644 --- a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx +++ b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx @@ -18,8 +18,7 @@ import { TimeInputInputsProps } from './TimeInputInputs.types'; */ export const TimeInputInputs = forwardRef( (_props: TimeInputInputsProps, forwardedRef) => { - const { is12hFormat, formatParts, timeZone, locale } = - useTimeInputDisplayContext(); + const { is12hFormat, timeZone, locale } = useTimeInputDisplayContext(); const [selectUnit, setSelectUnit] = useState(unitOptions[0]); const { value } = useTimeInputContext(); @@ -34,6 +33,9 @@ export const TimeInputInputs = forwardRef( value: value, }); + // eslint-disable-next-line no-console + console.log('timeParts 🍎🍎🍎', timeParts); + // TODO: break this out more return ( diff --git a/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts b/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts index 254839c3a3..922ea8dc00 100644 --- a/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts +++ b/packages/time-input/src/utils/getFormatPartsValues/getFormatPartsValues.ts @@ -37,9 +37,9 @@ export const getFormatPartsValues = ({ const formatter = getFormatter({ locale, withDate: true, - withTime: isValueValid ? true : false, // if the value is not valid then we don't want to return hour, minute, and second but we still want to return day, month, and year. + withTime: isValueValid, // if the value is not valid then we don't want to return hour, minute, and second but we still want to return day, month, and year. options: { - timeZone: timeZone, + timeZone, }, }); diff --git a/packages/time-input/src/utils/getFormatter/getFormatter.spec.ts b/packages/time-input/src/utils/getFormatter/getFormatter.spec.ts index 15b7011e28..30fb4c7f75 100644 --- a/packages/time-input/src/utils/getFormatter/getFormatter.spec.ts +++ b/packages/time-input/src/utils/getFormatter/getFormatter.spec.ts @@ -3,7 +3,7 @@ import { SupportedLocales } from '@leafygreen-ui/date-utils'; import { getFormatter } from './getFormatter'; describe('packages/time-input/utils/getFormatter', () => { - test('returns a formatter a valid locale', () => { + test('returns a formatter for a valid locale', () => { const formatter = getFormatter({ locale: SupportedLocales.en_US }); expect(formatter).toBeDefined(); }); From e46a73c4e2f798f46498a6b408763675e1144437 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Fri, 12 Dec 2025 10:16:49 -0500 Subject: [PATCH 22/49] refactor(time-input): clean up comments and improve test descriptions in TimeInputSegment tests --- .../Context/TimeInputDisplayContext/index.ts | 2 +- .../src/TimeInputInputs/TimeInputInputs.tsx | 1 + .../TimeInputSegment.spec.tsx | 226 +++++++++--------- 3 files changed, 116 insertions(+), 113 deletions(-) diff --git a/packages/time-input/src/Context/TimeInputDisplayContext/index.ts b/packages/time-input/src/Context/TimeInputDisplayContext/index.ts index 32a3fcfd70..72aea2bac3 100644 --- a/packages/time-input/src/Context/TimeInputDisplayContext/index.ts +++ b/packages/time-input/src/Context/TimeInputDisplayContext/index.ts @@ -7,4 +7,4 @@ export { type TimeInputDisplayContextProps, type TimeInputDisplayProviderProps, } from './TimeInputDisplayContext.types'; -export { defaultTimeInputDisplayContext } from './TimePickerDisplayContext.utils'; // +export { defaultTimeInputDisplayContext } from './TimePickerDisplayContext.utils'; diff --git a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx index 0077eb7191..71f30079a8 100644 --- a/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx +++ b/packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx @@ -64,6 +64,7 @@ export const TimeInputInputs = forwardRef( { + // TODO: This is temp and will be replaced in the next PR // eslint-disable-next-line no-console console.log({ segment, value }); }} diff --git a/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx b/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx index d83f7fe36b..c8a629a025 100644 --- a/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx +++ b/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx @@ -99,137 +99,139 @@ describe('packages/time-input/time-input-segment', () => { describe('Keyboard', () => { describe('Arrow Keys', () => { describe('hour input', () => { - describe('Up arrow', () => {}); - const formatter = getValueFormatter({ - charsCount: getTimeSegmentRules({ is12HourFormat: true })['hour'] - .maxChars, - }); - test('calls handler with value +1 if value is less than max', () => { - const onChangeHandler = jest.fn(); - const { input } = renderSegment({ - segment: 'hour', - value: formatter(15), - onChange: onChangeHandler, - }); - - userEvent.type(input, '{arrowup}'); - expect(onChangeHandler).toHaveBeenCalledWith( - expect.objectContaining({ value: formatter(16) }), - ); - }); - - describe('12 hour format', () => { + describe('Up arrow', () => { const formatter = getValueFormatter({ charsCount: getTimeSegmentRules({ is12HourFormat: true })['hour'] .maxChars, }); - - test('calls handler with min if value is undefined', () => { + test('calls handler with value +1 if value is less than max', () => { const onChangeHandler = jest.fn(); - const { input } = renderSegment( - { - segment: 'hour', - value: '', - onChange: onChangeHandler, - }, - { - locale: SupportedLocales.en_US, - }, - ); + const { input } = renderSegment({ + segment: 'hour', + value: formatter(15), + onChange: onChangeHandler, + }); userEvent.type(input, '{arrowup}'); expect(onChangeHandler).toHaveBeenCalledWith( - expect.objectContaining({ - value: formatter( - getDefaultMin({ is12HourFormat: true })['hour'], - ), - }), + expect.objectContaining({ value: formatter(16) }), ); }); - test('rolls value over to min value if value exceeds `max`', () => { - const onChangeHandler = - jest.fn(); - const { input } = renderSegment( - { - segment: 'hour', - value: formatter( - getDefaultMax({ is12HourFormat: true })['hour'], - ), - onChange: onChangeHandler, - }, - { - locale: SupportedLocales.en_US, - }, - ); + describe('12 hour format', () => { + const formatter = getValueFormatter({ + charsCount: getTimeSegmentRules({ is12HourFormat: true })['hour'] + .maxChars, + }); - userEvent.type(input, '{arrowup}'); - expect(onChangeHandler).toHaveBeenCalledWith( - expect.objectContaining({ - value: formatter( - getDefaultMin({ is12HourFormat: true })['hour'], - ), - }), - ); - }); - }); + test('calls handler with min if value is undefined', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment( + { + segment: 'hour', + value: '', + onChange: onChangeHandler, + }, + { + locale: SupportedLocales.en_US, + }, + ); - describe('24 hour format', () => { - const formatter = getValueFormatter({ - charsCount: getTimeSegmentRules({ is12HourFormat: false })['hour'] - .maxChars, - allowZero: true, - }); + userEvent.type(input, '{arrowup}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: formatter( + getDefaultMin({ is12HourFormat: true })['hour'], + ), + }), + ); + }); - test('calls handler with min if value is undefined', () => { - const onChangeHandler = - jest.fn(); - const { input } = renderSegment( - { - segment: 'hour', - value: '', - onChange: onChangeHandler, - }, - { - locale: SupportedLocales.ISO_8601, - }, - ); + test('rolls value over to min value if value exceeds `max`', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment( + { + segment: 'hour', + value: formatter( + getDefaultMax({ is12HourFormat: true })['hour'], + ), + onChange: onChangeHandler, + }, + { + locale: SupportedLocales.en_US, + }, + ); - userEvent.type(input, '{arrowup}'); - expect(onChangeHandler).toHaveBeenCalledWith( - expect.objectContaining({ - value: formatter( - getDefaultMin({ is12HourFormat: false })['hour'], - ), - }), - ); + userEvent.type(input, '{arrowup}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: formatter( + getDefaultMin({ is12HourFormat: true })['hour'], + ), + }), + ); + }); }); - test('rolls value over to min value if value exceeds `max`', () => { - const onChangeHandler = - jest.fn(); - const { input } = renderSegment( - { - segment: 'hour', - value: formatter( - getDefaultMax({ is12HourFormat: false })['hour'], - ), - onChange: onChangeHandler, - }, - { - locale: SupportedLocales.ISO_8601, - }, - ); + describe('24 hour format', () => { + const formatter = getValueFormatter({ + charsCount: getTimeSegmentRules({ is12HourFormat: false })['hour'] + .maxChars, + allowZero: true, + }); - userEvent.type(input, '{arrowup}'); - expect(onChangeHandler).toHaveBeenCalledWith( - expect.objectContaining({ - value: formatter( - getDefaultMin({ is12HourFormat: false })['hour'], - ), - }), - ); + test('calls handler with min if value is undefined', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment( + { + segment: 'hour', + value: '', + onChange: onChangeHandler, + }, + { + locale: SupportedLocales.ISO_8601, + }, + ); + + userEvent.type(input, '{arrowup}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: formatter( + getDefaultMin({ is12HourFormat: false })['hour'], + ), + }), + ); + }); + + test('rolls value over to min value if value exceeds `max`', () => { + const onChangeHandler = + jest.fn(); + const { input } = renderSegment( + { + segment: 'hour', + value: formatter( + getDefaultMax({ is12HourFormat: false })['hour'], + ), + onChange: onChangeHandler, + }, + { + locale: SupportedLocales.ISO_8601, + }, + ); + + userEvent.type(input, '{arrowup}'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: formatter( + getDefaultMin({ is12HourFormat: false })['hour'], + ), + }), + ); + }); }); }); From 94ed98ff918277f683d06a8056fc567cc66a200a Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Fri, 12 Dec 2025 10:38:18 -0500 Subject: [PATCH 23/49] refactor(time-input): remove outdated TODO comment in TimeInputContext for clarity --- .../time-input/src/Context/TimeInputContext/TimeInputContext.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/time-input/src/Context/TimeInputContext/TimeInputContext.tsx b/packages/time-input/src/Context/TimeInputContext/TimeInputContext.tsx index e94f083493..56ad9d5616 100644 --- a/packages/time-input/src/Context/TimeInputContext/TimeInputContext.tsx +++ b/packages/time-input/src/Context/TimeInputContext/TimeInputContext.tsx @@ -11,7 +11,6 @@ export const TimeInputContext = createContext( {} as TimeInputContextProps, ); -// TODO: get todays date if value is not provided /** * This provider is used for the state context of the TimeInput component */ From f07a1b8a88a1b5855259e8f69a5c0cf63dcc9aec Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Fri, 12 Dec 2025 11:45:57 -0500 Subject: [PATCH 24/49] merge conflict --- pnpm-lock.yaml | 239 ------------------------------------------------- 1 file changed, 239 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 861fe7ca64..f1c4a81d2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,34 +121,6 @@ importers: specifier: ~5.8.0 version: 5.8.3 - apps/mcp-ui-app: - dependencies: - '@lg-mcp-ui/list-databases': - specifier: workspace:* - version: link:../../mcp-ui/list-databases - next: - specifier: ^14.2.0 - version: 14.2.33(@babel/core@7.24.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2) - react: - specifier: ^18.2.0 - version: 18.3.1 - react-dom: - specifier: ^18.2.0 - version: 18.3.1(react@18.3.1) - devDependencies: - '@types/node': - specifier: ^20.12.5 - version: 20.19.9 - '@types/react': - specifier: 18.2.23 - version: 18.2.23 - '@types/react-dom': - specifier: 18.2.8 - version: 18.2.8 - typescript: - specifier: ~5.8.0 - version: 5.8.3 - charts/chart-card: dependencies: '@dnd-kit/sortable': @@ -878,25 +850,6 @@ importers: specifier: workspace:^ version: link:../../tools/build - mcp-ui/list-databases: - dependencies: - '@leafygreen-ui/lib': - specifier: workspace:^ - version: link:../../packages/lib - '@leafygreen-ui/palette': - specifier: workspace:^ - version: link:../../packages/palette - '@leafygreen-ui/tokens': - specifier: workspace:^ - version: link:../../packages/tokens - react: - specifier: ^18.2.0 - version: 18.3.1 - devDependencies: - '@lg-tools/build': - specifier: workspace:^ - version: link:../../tools/build - packages/a11y: dependencies: '@leafygreen-ui/emotion': @@ -6139,63 +6092,6 @@ packages: '@types/react': '>=16' react: '>=16' - '@next/env@14.2.33': - resolution: {integrity: sha512-CgVHNZ1fRIlxkLhIX22flAZI/HmpDaZ8vwyJ/B0SDPTBuLZ1PJ+DWMjCHhqnExfmSQzA/PbZi8OAc7PAq2w9IA==} - - '@next/swc-darwin-arm64@14.2.33': - resolution: {integrity: sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@next/swc-darwin-x64@14.2.33': - resolution: {integrity: sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@next/swc-linux-arm64-gnu@14.2.33': - resolution: {integrity: sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@next/swc-linux-arm64-musl@14.2.33': - resolution: {integrity: sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@next/swc-linux-x64-gnu@14.2.33': - resolution: {integrity: sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@next/swc-linux-x64-musl@14.2.33': - resolution: {integrity: sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@next/swc-win32-arm64-msvc@14.2.33': - resolution: {integrity: sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@next/swc-win32-ia32-msvc@14.2.33': - resolution: {integrity: sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - - '@next/swc-win32-x64-msvc@14.2.33': - resolution: {integrity: sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} @@ -6863,12 +6759,6 @@ packages: resolution: {integrity: sha512-zSoeKcbCmfMXjA11uDuCJb+1LWNb3vy6Qw/VHj0Nfcl3UuqwuoZWknHsBIhCWvi4wU9vPui3aq054qjVyZqY4A==} engines: {node: '>=14'} - '@swc/counter@0.1.3': - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - - '@swc/helpers@0.5.5': - resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} - '@tanstack/react-table@8.21.3': resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} engines: {node: '>=12'} @@ -7715,10 +7605,6 @@ packages: builtin-status-codes@3.0.0: resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} - busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -7832,9 +7718,6 @@ packages: resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} engines: {node: '>= 10.0'} - client-only@0.0.1: - resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - clipboard@2.0.11: resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==} @@ -10085,24 +9968,6 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next@14.2.33: - resolution: {integrity: sha512-GiKHLsD00t4ACm1p00VgrI0rUFAC9cRDGReKyERlM57aeEZkOQGcZTpIbsGn0b562FTPJWmYfKwplfO9EaT6ng==} - engines: {node: '>=18.17.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.41.2 - react: ^18.2.0 - react-dom: ^18.2.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': - optional: true - sass: - optional: true - nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} @@ -10494,10 +10359,6 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.4.31: - resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -11130,10 +10991,6 @@ packages: stream@0.0.3: resolution: {integrity: sha512-aMsbn7VKrl4A2T7QAQQbzgN7NVc70vgF5INQrBXqn4dCXN1zy3L9HGgLO5s7PExmdrzTJ8uR/27aviW8or8/+A==} - streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -11231,19 +11088,6 @@ packages: style-to-object@0.4.4: resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} - styled-jsx@5.1.1: - resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@babel/core': '*' - babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' - peerDependenciesMeta: - '@babel/core': - optional: true - babel-plugin-macros: - optional: true - stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} @@ -14592,35 +14436,6 @@ snapshots: '@types/react': 18.2.23 react: 18.3.1 - '@next/env@14.2.33': {} - - '@next/swc-darwin-arm64@14.2.33': - optional: true - - '@next/swc-darwin-x64@14.2.33': - optional: true - - '@next/swc-linux-arm64-gnu@14.2.33': - optional: true - - '@next/swc-linux-arm64-musl@14.2.33': - optional: true - - '@next/swc-linux-x64-gnu@14.2.33': - optional: true - - '@next/swc-linux-x64-musl@14.2.33': - optional: true - - '@next/swc-win32-arm64-msvc@14.2.33': - optional: true - - '@next/swc-win32-ia32-msvc@14.2.33': - optional: true - - '@next/swc-win32-x64-msvc@14.2.33': - optional: true - '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': dependencies: eslint-scope: 5.1.1 @@ -15440,13 +15255,6 @@ snapshots: - supports-color - typescript - '@swc/counter@0.1.3': {} - - '@swc/helpers@0.5.5': - dependencies: - '@swc/counter': 0.1.3 - tslib: 2.8.1 - '@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/table-core': 8.21.3 @@ -16568,10 +16376,6 @@ snapshots: builtin-status-codes@3.0.0: {} - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -16679,8 +16483,6 @@ snapshots: dependencies: source-map: 0.6.1 - client-only@0.0.1: {} - clipboard@2.0.11: dependencies: good-listener: 1.2.2 @@ -19795,32 +19597,6 @@ snapshots: neo-async@2.6.2: {} - next@14.2.33(@babel/core@7.24.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2): - dependencies: - '@next/env': 14.2.33 - '@swc/helpers': 0.5.5 - busboy: 1.6.0 - caniuse-lite: 1.0.30001727 - graceful-fs: 4.2.11 - postcss: 8.4.31 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(@babel/core@7.24.3)(react@18.3.1) - optionalDependencies: - '@next/swc-darwin-arm64': 14.2.33 - '@next/swc-darwin-x64': 14.2.33 - '@next/swc-linux-arm64-gnu': 14.2.33 - '@next/swc-linux-arm64-musl': 14.2.33 - '@next/swc-linux-x64-gnu': 14.2.33 - '@next/swc-linux-x64-musl': 14.2.33 - '@next/swc-win32-arm64-msvc': 14.2.33 - '@next/swc-win32-ia32-msvc': 14.2.33 - '@next/swc-win32-x64-msvc': 14.2.33 - sass: 1.89.2 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - nice-try@1.0.5: {} no-case@3.0.4: @@ -20253,12 +20029,6 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.4.31: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -21025,8 +20795,6 @@ snapshots: dependencies: component-emitter: 2.0.0 - streamsearch@1.1.0: {} - string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -21144,13 +20912,6 @@ snapshots: dependencies: inline-style-parser: 0.1.1 - styled-jsx@5.1.1(@babel/core@7.24.3)(react@18.3.1): - dependencies: - client-only: 0.0.1 - react: 18.3.1 - optionalDependencies: - '@babel/core': 7.24.3 - stylis@4.2.0: {} superstruct@2.0.2: {} From ed87ee7a02438968fa6c4d76f32c58c5a8fb8f5e Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Sat, 13 Dec 2025 19:49:15 -0500 Subject: [PATCH 25/49] test(time-input): add unit tests for TimeInputBox component --- .../TimeInputBox/{TimeInputBox.spec.ts => TimeInputBox.spec.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/time-input/src/TimeInputBox/{TimeInputBox.spec.ts => TimeInputBox.spec.tsx} (100%) diff --git a/packages/time-input/src/TimeInputBox/TimeInputBox.spec.ts b/packages/time-input/src/TimeInputBox/TimeInputBox.spec.tsx similarity index 100% rename from packages/time-input/src/TimeInputBox/TimeInputBox.spec.ts rename to packages/time-input/src/TimeInputBox/TimeInputBox.spec.tsx From e18c444784db89e3d952c1a457759e3fec8fa49c Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Sat, 13 Dec 2025 19:49:54 -0500 Subject: [PATCH 26/49] feat(time-input): integrate LGIDs into TimeInput components for improved accessibility --- .../TimeInputDisplayContext.types.ts | 11 ++ .../TimePickerDisplayContext.utils.ts | 7 +- .../time-input/src/TimeInput/TimeInput.tsx | 6 +- .../src/TimeInputBox/TimeInputBox.spec.tsx | 133 ++++++++++++++++++ .../TimeInputSegment.spec.tsx | 6 +- .../src/TimeInputSegment/TimeInputSegment.tsx | 6 +- .../TimeInputSelect/TimeInputSelect.spec.tsx | 79 ++++++++++- .../src/TimeInputSelect/TimeInputSelect.tsx | 5 +- packages/time-input/src/utils/getLgIds.ts | 1 + 9 files changed, 242 insertions(+), 12 deletions(-) diff --git a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.types.ts b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.types.ts index f87d0759b1..5bc3d5dbf8 100644 --- a/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.types.ts +++ b/packages/time-input/src/Context/TimeInputDisplayContext/TimeInputDisplayContext.types.ts @@ -4,6 +4,7 @@ import { AriaLabelPropsWithLabel } from '@leafygreen-ui/a11y'; import { DarkModeProps } from '@leafygreen-ui/lib'; import { DisplayTimeInputProps } from '../../TimeInput/TimeInput.types'; +import { GetLgIdsReturnType } from '../../utils/getLgIds'; type AriaLabelKeys = keyof AriaLabelPropsWithLabel; type AriaLabelKeysWithoutLabel = Exclude; @@ -51,6 +52,11 @@ export type TimeInputDisplayContextProps = Omit< * used to determine the order of segments in the input */ formatParts?: Array; + + /** + * LGIDs for the code snippet. + */ + lgIds: GetLgIdsReturnType; }; /** @@ -75,4 +81,9 @@ export type TimeInputDisplayProviderProps = Omit< * The aria-labelledby prop */ 'aria-labelledby'?: string; + + /** + * LGIDs for the code snippet. + */ + lgIds?: GetLgIdsReturnType; }; diff --git a/packages/time-input/src/Context/TimeInputDisplayContext/TimePickerDisplayContext.utils.ts b/packages/time-input/src/Context/TimeInputDisplayContext/TimePickerDisplayContext.utils.ts index a6e1ce1f7e..101a1049eb 100644 --- a/packages/time-input/src/Context/TimeInputDisplayContext/TimePickerDisplayContext.utils.ts +++ b/packages/time-input/src/Context/TimeInputDisplayContext/TimePickerDisplayContext.utils.ts @@ -6,13 +6,17 @@ import { import { BaseFontSize } from '@leafygreen-ui/tokens'; import { Size } from '../../TimeInput/TimeInput.types'; +import { getLgIds } from '../../utils/getLgIds'; import { TimeInputDisplayContextProps, TimeInputDisplayProviderProps, } from './TimeInputDisplayContext.types'; -export type DisplayContextPropKeys = keyof TimeInputDisplayProviderProps; +export type DisplayContextPropKeys = Exclude< + keyof TimeInputDisplayProviderProps, + 'lgIds' +>; /** * Props names that that are added to the context and used to pick and omit props @@ -54,4 +58,5 @@ export const defaultTimeInputDisplayContext: TimeInputDisplayContextProps = { setIsDirty: () => {}, is12HourFormat: false, showSeconds: true, + lgIds: getLgIds(), }; diff --git a/packages/time-input/src/TimeInput/TimeInput.tsx b/packages/time-input/src/TimeInput/TimeInput.tsx index b97481ed36..5c38b24710 100644 --- a/packages/time-input/src/TimeInput/TimeInput.tsx +++ b/packages/time-input/src/TimeInput/TimeInput.tsx @@ -16,6 +16,7 @@ import { displayContextPropNames, } from '../Context/TimeInputDisplayContext/TimePickerDisplayContext.utils'; import { TimeInputContent } from '../TimeInputContent'; +import { getLgIds } from '../utils/getLgIds'; import { TimeInputProps } from './TimeInput.types'; @@ -26,7 +27,7 @@ export const TimeInput = forwardRef( onTimeChange: onChangeProp, handleValidation, initialValue: initialValueProp, - 'data-lgid': _dataLgId, + 'data-lgid': dataLgId, darkMode: darkModeProp, baseFontSize: basefontSizeProp, ...props @@ -35,6 +36,7 @@ export const TimeInput = forwardRef( ) => { const { darkMode } = useDarkMode(darkModeProp); const baseFontSize = useUpdatedBaseFontSize(basefontSizeProp); + const lgIds = getLgIds(dataLgId); const { value, updateValue } = useControlled( valueProp, @@ -55,7 +57,7 @@ export const TimeInput = forwardRef( darkMode={darkMode} baseFontSize={baseFontSize === BaseFontSize.Body1 ? 14 : baseFontSize} > - + ; + displayProps?: Partial; +}) => { + const result = render( + + {}} + {...props} + /> + , + ); + + // TODO:: replace with test harnesses + const hourInput = result.container.querySelector( + 'input[aria-label="hour"]', + ) as HTMLInputElement; + const minuteInput = result.container.querySelector( + 'input[aria-label="minute"]', + ) as HTMLInputElement; + const secondInput = result.container.querySelector( + 'input[aria-label="second"]', + ) as HTMLInputElement; + + return { + ...result, + hourInput, + minuteInput, + secondInput, + }; +}; + +describe('packages/time-input/time-input-box', () => { + describe('Rendering', () => { + it('should render the segments', () => { + const { hourInput, minuteInput, secondInput } = renderTimeInputBox({}); + expect(hourInput).toBeInTheDocument(); + expect(minuteInput).toBeInTheDocument(); + expect(secondInput).toBeInTheDocument(); + }); + + it('should render the correct aria labels', () => { + const { hourInput, minuteInput, secondInput } = renderTimeInputBox({}); + expect(hourInput).toHaveAttribute('aria-label', 'hour'); + expect(minuteInput).toHaveAttribute('aria-label', 'minute'); + expect(secondInput).toHaveAttribute('aria-label', 'second'); + }); + }); + + describe('Min/Max', () => { + describe('hour segment', () => { + describe('12 hour format', () => { + it('should have a min of 1 for the hour segment', () => { + const { hourInput } = renderTimeInputBox({ + displayProps: { locale: SupportedLocales.en_US }, + }); + expect(hourInput).toHaveAttribute('min', '1'); + }); + it('should have a max of 12 for the hour segment', () => { + const { hourInput } = renderTimeInputBox({ + displayProps: { locale: SupportedLocales.en_US }, + }); + expect(hourInput).toHaveAttribute('max', '12'); + }); + }); + + describe('24 hour format', () => { + it('should have a min of 0 for the hour segment', () => { + const { hourInput } = renderTimeInputBox({ + displayProps: { locale: SupportedLocales.ISO_8601 }, + }); + expect(hourInput).toHaveAttribute('min', '0'); + }); + it('should have a max of 23 for the hour segment', () => { + const { hourInput } = renderTimeInputBox({ + displayProps: { locale: SupportedLocales.ISO_8601 }, + }); + expect(hourInput).toHaveAttribute('max', '23'); + }); + }); + }); + + describe.each(['minute', 'second'])('%p segment', segment => { + test('should have a min of 0 for the %p segment', () => { + const result = renderTimeInputBox({ + displayProps: { locale: SupportedLocales.ISO_8601 }, + }); + const input = + segment === 'minute' ? result.minuteInput : result.secondInput; + expect(input).toHaveAttribute('min', '0'); + }); + test('should have a max of 59 for the %p segment', () => { + const result = renderTimeInputBox({ + displayProps: { locale: SupportedLocales.ISO_8601 }, + }); + const input = + segment === 'minute' ? result.minuteInput : result.secondInput; + expect(input).toHaveAttribute('max', '59'); + }); + }); + }); + + describe('setSegment', () => { + test('should call setSegment with the segment name and the value', () => { + const setSegment = jest.fn(); + const { hourInput } = renderTimeInputBox({ props: { setSegment } }); + userEvent.type(hourInput, '1'); + expect(setSegment).toHaveBeenCalledWith('hour', '1'); + }); + }); + + describe('onSegmentChange', () => { + test.todo( + 'should call onSegmentChange with the segment name and the value', + ); + }); +}); diff --git a/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx b/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx index c8a629a025..b7a0cc5171 100644 --- a/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx +++ b/packages/time-input/src/TimeInputSegment/TimeInputSegment.spec.tsx @@ -15,6 +15,7 @@ import { import { TimeInputDisplayContextProps } from '../Context/TimeInputDisplayContext'; import { TimeInputDisplayProvider } from '../Context/TimeInputDisplayContext'; import { TimeSegment } from '../shared.types'; +import { getLgIds } from '../utils/getLgIds'; import { TimeInputSegment } from './TimeInputSegment'; import { @@ -22,6 +23,8 @@ import { TimeInputSegmentProps, } from './TimeInputSegment.types'; +const lgIds = getLgIds(); + const renderSegment = ( props?: Partial, ctx?: Partial, @@ -55,8 +58,9 @@ const renderSegment = ( , ); + // TODO:: replace with test harnesses const getInput = () => - result.getByTestId('lg-time_input_input-segment') as HTMLInputElement; + result.getByTestId(lgIds.inputSegment) as HTMLInputElement; return { ...result, diff --git a/packages/time-input/src/TimeInputSegment/TimeInputSegment.tsx b/packages/time-input/src/TimeInputSegment/TimeInputSegment.tsx index 924e63bc85..8d1e44fce3 100644 --- a/packages/time-input/src/TimeInputSegment/TimeInputSegment.tsx +++ b/packages/time-input/src/TimeInputSegment/TimeInputSegment.tsx @@ -16,7 +16,7 @@ export const TimeInputSegment = React.forwardRef< HTMLInputElement, TimeInputSegmentProps >(({ children, segment, ...rest }: TimeInputSegmentProps, fwdRef) => { - const { is12HourFormat } = useTimeInputDisplayContext(); + const { is12HourFormat, lgIds } = useTimeInputDisplayContext(); return ( diff --git a/packages/time-input/src/TimeInputSelect/TimeInputSelect.spec.tsx b/packages/time-input/src/TimeInputSelect/TimeInputSelect.spec.tsx index 43272da620..f977ce780e 100644 --- a/packages/time-input/src/TimeInputSelect/TimeInputSelect.spec.tsx +++ b/packages/time-input/src/TimeInputSelect/TimeInputSelect.spec.tsx @@ -1,9 +1,82 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import React from 'react'; import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; -import { TimeInputSelect } from '.'; +import { getTestUtils as getSelectTestUtils } from '@leafygreen-ui/select/testing'; + +import { getLgIds } from '../utils/getLgIds'; + +import { TimeInputSelect, TimeInputSelectProps } from '.'; + +const lgIds = getLgIds(); + +const renderTimeInputSelect = (props: TimeInputSelectProps) => { + const result = render(); + + const testUtils = getSelectTestUtils(lgIds.select); + + return { + ...result, + ...testUtils, + }; +}; describe('packages/time-input-select', () => { - test('condition', () => {}); + describe('Rendering', () => { + test('is in the document', () => { + const { getInput } = renderTimeInputSelect({ + unit: 'AM', + onChange: () => {}, + }); + expect(getInput()).toBeInTheDocument(); + }); + + test('shows the correct value', () => { + const { getInput } = renderTimeInputSelect({ + unit: 'AM', + onChange: () => {}, + }); + expect(getInput()).toHaveValue('AM'); + }); + + test('has 2 options', () => { + const { getInput, getOptions } = renderTimeInputSelect({ + unit: 'AM', + onChange: () => {}, + }); + + userEvent.click(getInput()); + expect(getOptions()).toHaveLength(2); + }); + + test('has AM and PM options', () => { + const { getInput, getOptionByValue } = renderTimeInputSelect({ + unit: 'AM', + onChange: () => {}, + }); + + userEvent.click(getInput()); + expect(getOptionByValue('AM')).toBeInTheDocument(); + expect(getOptionByValue('PM')).toBeInTheDocument(); + }); + }); + + describe('onChange', () => { + test('is called with the selected option', () => { + const onChange = jest.fn(); + const { getInput, getOptionByValue } = renderTimeInputSelect({ + unit: 'AM', + onChange, + }); + + userEvent.click(getInput()); + userEvent.click(getOptionByValue('PM')!); + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + displayName: 'PM', + value: 'PM', + }), + ); + }); + }); }); diff --git a/packages/time-input/src/TimeInputSelect/TimeInputSelect.tsx b/packages/time-input/src/TimeInputSelect/TimeInputSelect.tsx index d65308e0c6..b2d036029d 100644 --- a/packages/time-input/src/TimeInputSelect/TimeInputSelect.tsx +++ b/packages/time-input/src/TimeInputSelect/TimeInputSelect.tsx @@ -9,6 +9,7 @@ import { } from '@leafygreen-ui/select'; import { unitOptions } from '../constants'; +import { useTimeInputDisplayContext } from '../Context/TimeInputDisplayContext/TimeInputDisplayContext'; import { selectStyles, wrapperBaseStyles } from './TimeInputSelect.styles'; import { TimeInputSelectProps, UnitOption } from './TimeInputSelect.types'; @@ -22,10 +23,9 @@ export const TimeInputSelect = ({ className, onChange, }: TimeInputSelectProps) => { + const { lgIds } = useTimeInputDisplayContext(); /** * Gets the current unit option using the unit string - * - * @internal */ const currentUnitOption = unitOptions.find( u => u.displayName === unit, @@ -50,6 +50,7 @@ export const TimeInputSelect = ({ allowDeselect={false} dropdownWidthBasis={DropdownWidthBasis.Option} renderMode={RenderMode.TopLayer} + data-lgid={lgIds.select} > {unitOptions.map(option => (