From 65c7db769407c36175abb123db2a3ac278079a95 Mon Sep 17 00:00:00 2001 From: jialecl Date: Mon, 13 Jul 2026 14:46:29 +0200 Subject: [PATCH 1/5] Time input localization support --- .../lib/src/time-input/TimeInput.stories.tsx | 18 ++ .../lib/src/time-input/TimeInput.test.tsx | 69 +++++++ packages/lib/src/time-input/TimeInput.tsx | 180 +++++++++++++++--- packages/lib/src/time-input/utils.ts | 53 +++++- 4 files changed, 289 insertions(+), 31 deletions(-) diff --git a/packages/lib/src/time-input/TimeInput.stories.tsx b/packages/lib/src/time-input/TimeInput.stories.tsx index 9a8632bda..16fbab982 100644 --- a/packages/lib/src/time-input/TimeInput.stories.tsx +++ b/packages/lib/src/time-input/TimeInput.stories.tsx @@ -7,6 +7,7 @@ import preview from "../../.storybook/preview"; import disabledRules from "../../test/accessibility/rules/common/disabledRules"; import DxcContainer from "../container/Container"; import { userEvent, within } from "storybook/internal/test"; +import { HalstackProvider } from "../HalstackContext"; export default { title: "Time Input", @@ -100,6 +101,23 @@ const TimeInput = () => { <TimeInputExamples /> </ExampleContainer> + <ExampleContainer> + <Title title="Localized time input" theme="light" level={3} /> + <HalstackProvider localeTag="fi-FI"> + <DxcTimeInput label="Finnish locale" helperText="Helper text" defaultValue={continentalValue} showSeconds /> + </HalstackProvider> + <HalstackProvider localeTag="en-US"> + <DxcTimeInput label="US locale" helperText="Helper text" defaultValue={value} showSeconds /> + </HalstackProvider> + <HalstackProvider localeTag="zh-TW"> + <DxcTimeInput + label="Taiwanese locale" + helperText="Helper text" + onChange={(val) => console.log(val)} + showSeconds + /> + </HalstackProvider> + </ExampleContainer> </> ); }; diff --git a/packages/lib/src/time-input/TimeInput.test.tsx b/packages/lib/src/time-input/TimeInput.test.tsx index b1c9451ca..b9a1b01d0 100644 --- a/packages/lib/src/time-input/TimeInput.test.tsx +++ b/packages/lib/src/time-input/TimeInput.test.tsx @@ -3,6 +3,7 @@ import DxcTimeInput from "./TimeInput"; import MockDOMRect from "../../test/mocks/domRectMock"; import userEvent from "@testing-library/user-event"; import "@testing-library/jest-dom"; +import { HalstackProvider } from "../HalstackContext"; // Mocking DOMRect for Radix Primitive Popover global.DOMRect = MockDOMRect; @@ -265,4 +266,72 @@ describe("DxcTimeInput rendering", () => { userEvent.keyboard("{5}"); expect(mockOnChange).toHaveBeenCalledWith("12:05:00 AM"); }); + + it("Time input with finnish locale", () => { + const mockOnChange = jest.fn(); + const { getByRole, getAllByText } = render( + <HalstackProvider localeTag="fi-FI"> + <DxcTimeInput label="Finnish locale" showSeconds onChange={mockOnChange} /> + </HalstackProvider> + ); + const button = getByRole("button"); + expect(button).toBeTruthy(); + userEvent.click(button); + const hourbutton = getAllByText("16"); + if (hourbutton[0]) userEvent.click(hourbutton[0]); + expect(mockOnChange).toHaveBeenCalledWith("16.00.00"); + const minuteButton = getAllByText("30"); + if (minuteButton[0]) userEvent.click(minuteButton[0]); + expect(mockOnChange).toHaveBeenCalledWith("16.30.00"); + const secondButton = getAllByText("45"); + if (secondButton[1]) userEvent.click(secondButton[1]); + expect(mockOnChange).toHaveBeenCalledWith("16.30.45"); + }); + + it("Time input with taiwanese locale", () => { + const mockOnChange = jest.fn(); + const { getByRole, getAllByText } = render( + <HalstackProvider localeTag="zh-TW"> + <DxcTimeInput label="Taiwanese locale" showSeconds onChange={mockOnChange} /> + </HalstackProvider> + ); + const button = getByRole("button"); + expect(button).toBeTruthy(); + userEvent.click(button); + const amButton = getAllByText("AM")[0]; + if (amButton) userEvent.click(amButton); + const hourbutton = getAllByText("07"); + if (hourbutton[0]) userEvent.click(hourbutton[0]); + expect(mockOnChange).toHaveBeenCalledWith("AM 07:00:00"); + const minuteButton = getAllByText("30"); + if (minuteButton[0]) userEvent.click(minuteButton[0]); + expect(mockOnChange).toHaveBeenCalledWith("AM 07:30:00"); + const secondButton = getAllByText("45"); + if (secondButton[1]) userEvent.click(secondButton[1]); + expect(mockOnChange).toHaveBeenCalledWith("AM 07:30:45"); + }); + + it("Time input with taiwanese locale, but using keyboard to input time", () => { + const mockOnChange = jest.fn(); + const { getAllByRole } = render( + <HalstackProvider localeTag="zh-TW"> + <DxcTimeInput label="Taiwanese locale" showSeconds onChange={mockOnChange} /> + </HalstackProvider> + ); + const inputs = getAllByRole("spinbutton"); + expect(inputs).toHaveLength(4); + userEvent.tab(); + expect(inputs[0]).toHaveFocus(); + userEvent.keyboard("p"); + expect(inputs[1]).toHaveFocus(); + userEvent.keyboard("1"); + userEvent.keyboard("2"); + expect(inputs[2]).toHaveFocus(); + userEvent.keyboard("{3}"); + userEvent.keyboard("{4}"); + expect(inputs[3]).toHaveFocus(); + userEvent.keyboard("{5}"); + userEvent.keyboard("{6}"); + expect(mockOnChange).toHaveBeenCalledWith("PM 12:34:56"); + }); }); diff --git a/packages/lib/src/time-input/TimeInput.tsx b/packages/lib/src/time-input/TimeInput.tsx index cb0eb0c23..d8b7a09dc 100644 --- a/packages/lib/src/time-input/TimeInput.tsx +++ b/packages/lib/src/time-input/TimeInput.tsx @@ -1,7 +1,7 @@ import styled from "@emotion/styled"; import inputStylesByState from "../styles/forms/inputStylesByState"; import TimeInputPropsType, { RefType } from "./types"; -import { forwardRef, useContext, useEffect, useId, useRef, useState } from "react"; +import { forwardRef, useContext, useEffect, useId, useMemo, useRef, useState } from "react"; import { HalstackLanguageContext } from "../HalstackContext"; import Label from "../styles/forms/Label"; import HelperText from "../styles/forms/HelperText"; @@ -10,7 +10,7 @@ import DxcFlex from "../flex/Flex"; import DxcActionIcon from "../action-icon/ActionIcon"; import DxcPopover from "../popover/Popover"; import TimePicker from "./TimePicker"; -import { generateEventValue } from "./utils"; +import { generateEventValue, getTimeInputLocale } from "./utils"; import ErrorMessage from "../styles/forms/ErrorMessage"; const sizes = { @@ -73,7 +73,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( showSeconds = false, size = "medium", tabIndex = 0, - timeFormat = "12", + timeFormat, value, }, ref @@ -90,19 +90,25 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( const secondRef = useRef<HTMLSpanElement>(null); const dayPeriodRef = useRef<HTMLSpanElement>(null); const isControlled = value !== undefined; - const translatedLabels = useContext(HalstackLanguageContext).labels; - + const languageContext = useContext(HalstackLanguageContext); + const translatedLabels = languageContext.labels; + const formatInfo = useMemo(() => getTimeInputLocale(languageContext.locale), [languageContext.locale]); + // Prop timeFormat takes precedence over the locale format + const timeFormatToUse = useMemo<"12" | "24">( + () => timeFormat || formatInfo.format, + [timeFormat, formatInfo.format] + ); useEffect(() => { const time = value || defaultValue || undefined; if (time) { - const numberPart = timeFormat === "12" ? time.split(" ")[0] : time; + const numberPart = timeFormatToUse === "12" ? time.split(" ")[0] : time; if (numberPart) { const [hourStr, minuteStr, secondStr] = numberPart.split(":"); setHourValue(hourStr && isNumber(hourStr) ? Number(hourStr) : undefined); setMinuteValue(minuteStr && isNumber(minuteStr) ? Number(minuteStr) : undefined); setSecondValue(secondStr && isNumber(secondStr) ? Number(secondStr) : undefined); } - if (timeFormat === "12" && time.includes(" ")) { + if (timeFormatToUse === "12" && time.includes(" ")) { const dayPeriodValue = time.split(" ")[1] === "AM" ? 0 : time.split(" ")[1] === "PM" ? 1 : undefined; setDayPeriodValue(dayPeriodValue); } else { @@ -114,13 +120,22 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( setSecondValue(undefined); setDayPeriodValue(undefined); } - }, [value, defaultValue, timeFormat]); + }, [value, defaultValue, timeFormatToUse]); const generatedInputValue = () => { if (hourValue === undefined && minuteValue === undefined && secondValue === undefined) { return ""; } else { - return generateEventValue(hourValue, minuteValue, secondValue, dayPeriodValue, showSeconds, timeFormat); + return generateEventValue( + hourValue, + minuteValue, + secondValue, + dayPeriodValue, + showSeconds, + timeFormatToUse, + formatInfo.separator, + formatInfo.dayPeriodPosition + ); } }; @@ -132,13 +147,24 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( setDayPeriodValue(undefined); } if (typeof onChange === "function") { - onChange(generateEventValue(undefined, undefined, undefined, undefined, showSeconds, timeFormat)); + onChange( + generateEventValue( + undefined, + undefined, + undefined, + undefined, + showSeconds, + timeFormatToUse, + formatInfo.separator, + formatInfo.dayPeriodPosition + ) + ); } }; const validateTimeValue = (value: string) => { const timeRegex = - timeFormat === "12" + timeFormatToUse === "12" ? /^(0?[1-9]|1[0-2]):[0-5][0-9](?::[0-5][0-9])?\s?(AM|PM)$/i : /^([01]?[0-9]|2[0-3]):[0-5][0-9](?::[0-5][0-9])?$/; if (!timeRegex.test(value)) { @@ -149,7 +175,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( (hourValue === undefined || minuteValue === undefined || (showSeconds && secondValue === undefined) || - (timeFormat === "12" && dayPeriodValue === undefined)) + (timeFormatToUse === "12" && dayPeriodValue === undefined)) ) { return "This field is required"; } @@ -184,12 +210,55 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( )} <TimeInputField disabled={disabled} error={!!error} readOnly={readOnly}> <DxcFlex gap="var(--spacing-gap-xs)" alignItems="center" fullHeight> + {timeFormatToUse === "12" && formatInfo.dayPeriodPosition === "before" && ( + <TimeSpinButton + ariaLabel={label ?? ariaLabel} + value={dayPeriodValue} + minValue={0} + maxValue={1} + tabIndex={tabIndex} + dataType="dayPeriod" + readOnly={readOnly} + disabled={disabled} + isControlled={isControlled} + onChange={(value) => { + if (!isControlled) { + setDayPeriodValue(value); + } + if (typeof onChange === "function") { + onChange( + generateEventValue( + hourValue, + minuteValue, + secondValue, + value, + showSeconds, + timeFormatToUse, + formatInfo.separator, + formatInfo.dayPeriodPosition + ) + ); + } + }} + onComplete={() => { + if (hourRef.current) { + hourRef.current.focus(); + } + }} + onNext={() => { + if (hourRef.current) { + hourRef.current.focus(); + } + }} + ref={dayPeriodRef} + /> + )} <DxcFlex alignItems="center" fullHeight> <TimeSpinButton ariaLabel={label ?? ariaLabel} value={hourValue} - minValue={timeFormat === "12" ? 1 : 0} - maxValue={timeFormat === "12" ? 12 : 23} + minValue={timeFormatToUse === "12" ? 1 : 0} + maxValue={timeFormatToUse === "12" ? 12 : 23} tabIndex={tabIndex} dataType="hour" readOnly={readOnly} @@ -206,7 +275,16 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( } if (typeof onChange === "function") { onChange( - generateEventValue(value, minuteValue, secondValue, dayPeriodValue, showSeconds, timeFormat) + generateEventValue( + value, + minuteValue, + secondValue, + dayPeriodValue, + showSeconds, + timeFormatToUse, + formatInfo.separator, + formatInfo.dayPeriodPosition + ) ); } }} @@ -217,7 +295,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( }} ref={hourRef} /> - <ColonContainer>:</ColonContainer> + <ColonContainer>{formatInfo.separator}</ColonContainer> <TimeSpinButton ariaLabel={label ?? ariaLabel} value={minuteValue} @@ -231,7 +309,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( onComplete={() => { if (showSeconds && secondRef.current) { secondRef.current.focus(); - } else if (timeFormat === "12" && dayPeriodRef.current) { + } else if (timeFormatToUse === "12" && dayPeriodRef.current) { dayPeriodRef.current.focus(); } }} @@ -241,14 +319,23 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( } if (typeof onChange === "function") { onChange( - generateEventValue(hourValue, value, secondValue, dayPeriodValue, showSeconds, timeFormat) + generateEventValue( + hourValue, + value, + secondValue, + dayPeriodValue, + showSeconds, + timeFormatToUse, + formatInfo.separator, + formatInfo.dayPeriodPosition + ) ); } }} onNext={() => { if (showSeconds && secondRef.current) { secondRef.current.focus(); - } else if (timeFormat === "12" && dayPeriodRef.current) { + } else if (timeFormatToUse === "12" && dayPeriodRef.current) { dayPeriodRef.current.focus(); } }} @@ -261,7 +348,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( /> {showSeconds && ( <> - <ColonContainer>:</ColonContainer> + <ColonContainer>{formatInfo.separator}</ColonContainer> <TimeSpinButton ariaLabel={label ?? ariaLabel} value={secondValue} @@ -273,7 +360,11 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( disabled={disabled} isControlled={isControlled} onComplete={() => { - if (timeFormat === "12" && dayPeriodRef.current) { + if ( + timeFormatToUse === "12" && + formatInfo.dayPeriodPosition === "after" && + dayPeriodRef.current + ) { dayPeriodRef.current.focus(); } }} @@ -283,12 +374,25 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( } if (typeof onChange === "function") { onChange( - generateEventValue(hourValue, minuteValue, value, dayPeriodValue, showSeconds, timeFormat) + generateEventValue( + hourValue, + minuteValue, + value, + dayPeriodValue, + showSeconds, + timeFormatToUse, + formatInfo.separator, + formatInfo.dayPeriodPosition + ) ); } }} onNext={() => { - if (timeFormat === "12" && dayPeriodRef.current) { + if ( + timeFormatToUse === "12" && + formatInfo.dayPeriodPosition === "after" && + dayPeriodRef.current + ) { dayPeriodRef.current.focus(); } }} @@ -302,7 +406,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( </> )} </DxcFlex> - {timeFormat === "12" && ( + {timeFormatToUse === "12" && formatInfo.dayPeriodPosition === "after" && ( <TimeSpinButton ariaLabel={label ?? ariaLabel} value={dayPeriodValue} @@ -318,7 +422,18 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( setDayPeriodValue(value); } if (typeof onChange === "function") { - onChange(generateEventValue(hourValue, minuteValue, secondValue, value, showSeconds, timeFormat)); + onChange( + generateEventValue( + hourValue, + minuteValue, + secondValue, + value, + showSeconds, + timeFormatToUse, + formatInfo.separator, + formatInfo.dayPeriodPosition + ) + ); } }} onPrevious={() => { @@ -353,10 +468,21 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( setHourValue(hour); } if (typeof onChange === "function") { - onChange(generateEventValue(hour, minute, second, dayPeriod, showSeconds, timeFormat)); + onChange( + generateEventValue( + hour, + minute, + second, + dayPeriod, + showSeconds, + timeFormatToUse, + formatInfo.separator, + formatInfo.dayPeriodPosition + ) + ); } }} - timeFormat={timeFormat} + timeFormat={timeFormatToUse} showSeconds={showSeconds} hourValue={hourValue} minuteValue={minuteValue} diff --git a/packages/lib/src/time-input/utils.ts b/packages/lib/src/time-input/utils.ts index def5a10bf..cf078f771 100644 --- a/packages/lib/src/time-input/utils.ts +++ b/packages/lib/src/time-input/utils.ts @@ -95,6 +95,9 @@ export const handleKeyDown = ( const isAM = /[aA]/.test(event.key); newValue = isAM ? 0 : 1; rawInput.current = newValue.toString(); + if (typeof onComplete === "function") { + onComplete(); + } } setInnerValue((prevValue) => { return prevValue !== newValue ? newValue : prevValue; @@ -120,14 +123,21 @@ export const generateEventValue = ( second: number | undefined, dayPeriod: number | undefined, showSeconds: boolean | undefined, - timeFormat: "12" | "24" | undefined + timeFormat: "12" | "24" | undefined, + separator: string, + timePeriodPosition: "before" | "after" ) => { if (hour === undefined && minute === undefined && second === undefined && dayPeriod === undefined) { return ""; } - return `${pad(hour)}:${pad(minute)}${showSeconds ? `:${pad(second)}` : ""}${ - timeFormat === "12" ? ` ${returnDayPeriod(dayPeriod)}` : "" - }`; + // consider dayperiod position for 12-hour format otherwise ignore it + if (timeFormat === "12" && timePeriodPosition === "before") { + return `${returnDayPeriod(dayPeriod)} ${pad(hour)}${separator}${pad(minute)}${showSeconds ? `${separator}${pad(second)}` : ""}`; + } else if (timeFormat === "12" && timePeriodPosition === "after") { + return `${pad(hour)}${separator}${pad(minute)}${showSeconds ? `${separator}${pad(second)}` : ""} ${returnDayPeriod(dayPeriod)}`; + } else { + return `${pad(hour)}${separator}${pad(minute)}${showSeconds ? `${separator}${pad(second)}` : ""}`; + } }; export const handleColumnKeyDown = ( @@ -172,3 +182,38 @@ export const handleColumnKeyDown = ( } } }; + +export const getTimeInputLocale = ( + locale?: string +): { separator: string; format: "12" | "24"; dayPeriodPosition: "before" | "after" } => { + if (!locale) { + return { + separator: ":", + format: "12", + dayPeriodPosition: "after", + }; + } + const naturalCycle = new Intl.DateTimeFormat(locale, { hour: "2-digit" }).resolvedOptions().hourCycle; + const hourCycle: "h12" | "h23" = naturalCycle === "h23" || naturalCycle === "h24" ? "h23" : "h12"; + + const formatter = new Intl.DateTimeFormat(locale, { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hourCycle, + }); + const parts = formatter.formatToParts(new Date(1995, 11, 3, 1, 0, 0)); + const dayPeriodPosition = parts[0]?.type === "dayPeriod" ? "before" : "after"; + const format = hourCycle === "h23" ? "24" : "12"; + // get all parts that are "literal" and use the first one or the last one depending on the dayPeriodPosition to determine the separator + const separatorParts = parts.filter((part) => part.type === "literal"); + const separator = + format === "24" || dayPeriodPosition === "after" + ? separatorParts[0]?.value || ":" + : separatorParts[separatorParts.length - 1]?.value || ":"; + return { + separator, + format, + dayPeriodPosition, + }; +}; From 83b5ad57080bd33ddd4f884a6345ae87d5fa900c Mon Sep 17 00:00:00 2001 From: jialecl <jialestrabajos@gmail.com> Date: Tue, 14 Jul 2026 13:21:37 +0200 Subject: [PATCH 2/5] Added better support based on format and locale --- .../lib/src/time-input/TimeInput.stories.tsx | 20 +++++++ .../lib/src/time-input/TimeInput.test.tsx | 23 ++++++++ packages/lib/src/time-input/TimeInput.tsx | 52 +++++++++---------- packages/lib/src/time-input/TimePicker.tsx | 21 +++++++- packages/lib/src/time-input/types.ts | 1 + packages/lib/src/time-input/utils.ts | 12 +++-- 6 files changed, 97 insertions(+), 32 deletions(-) diff --git a/packages/lib/src/time-input/TimeInput.stories.tsx b/packages/lib/src/time-input/TimeInput.stories.tsx index 16fbab982..0b0eefdee 100644 --- a/packages/lib/src/time-input/TimeInput.stories.tsx +++ b/packages/lib/src/time-input/TimeInput.stories.tsx @@ -117,6 +117,15 @@ const TimeInput = () => { showSeconds /> </HalstackProvider> + <HalstackProvider localeTag="zh-CN"> + <DxcTimeInput + label="Chinese locale with 12-hour format" + helperText="Helper text" + onChange={(val) => console.log(val)} + timeFormat="12" + showSeconds + /> + </HalstackProvider> </ExampleContainer> </> ); @@ -255,6 +264,17 @@ const TimePickerExamples = () => { /> </DxcContainer> </ExampleContainer> + <ExampleContainer> + <Title title="Localized time picker" theme="light" level={3} /> + <TimePicker + onPickTime={() => {}} + timeFormat="12" + dayPeriodPosition="before" + id="testId" + tabIndex={0} + showSeconds + /> + </ExampleContainer> </> ); }; diff --git a/packages/lib/src/time-input/TimeInput.test.tsx b/packages/lib/src/time-input/TimeInput.test.tsx index b9a1b01d0..b260f2d16 100644 --- a/packages/lib/src/time-input/TimeInput.test.tsx +++ b/packages/lib/src/time-input/TimeInput.test.tsx @@ -334,4 +334,27 @@ describe("DxcTimeInput rendering", () => { userEvent.keyboard("{6}"); expect(mockOnChange).toHaveBeenCalledWith("PM 12:34:56"); }); + + it("Time input with chinese locale, but using time picker", () => { + const mockOnChange = jest.fn(); + const { getByRole, getAllByText } = render( + <HalstackProvider localeTag="zh-CN"> + <DxcTimeInput label="Chinese locale" timeFormat="12" showSeconds onChange={mockOnChange} /> + </HalstackProvider> + ); + const button = getByRole("button"); + expect(button).toBeTruthy(); + userEvent.click(button); + const amButton = getAllByText("AM")[0]; + if (amButton) userEvent.click(amButton); + const hourbutton = getAllByText("12"); + if (hourbutton[0]) userEvent.click(hourbutton[0]); + expect(mockOnChange).toHaveBeenCalledWith("AM 12:00:00"); + const minuteButton = getAllByText("30"); + if (minuteButton[0]) userEvent.click(minuteButton[0]); + expect(mockOnChange).toHaveBeenCalledWith("AM 12:30:00"); + const secondButton = getAllByText("50"); + if (secondButton[1]) userEvent.click(secondButton[1]); + expect(mockOnChange).toHaveBeenCalledWith("AM 12:30:50"); + }); }); diff --git a/packages/lib/src/time-input/TimeInput.tsx b/packages/lib/src/time-input/TimeInput.tsx index d8b7a09dc..57cb7713b 100644 --- a/packages/lib/src/time-input/TimeInput.tsx +++ b/packages/lib/src/time-input/TimeInput.tsx @@ -92,23 +92,18 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( const isControlled = value !== undefined; const languageContext = useContext(HalstackLanguageContext); const translatedLabels = languageContext.labels; - const formatInfo = useMemo(() => getTimeInputLocale(languageContext.locale), [languageContext.locale]); - // Prop timeFormat takes precedence over the locale format - const timeFormatToUse = useMemo<"12" | "24">( - () => timeFormat || formatInfo.format, - [timeFormat, formatInfo.format] - ); + const formatInfo = useMemo(() => getTimeInputLocale(languageContext.locale, timeFormat), [languageContext.locale]); useEffect(() => { const time = value || defaultValue || undefined; if (time) { - const numberPart = timeFormatToUse === "12" ? time.split(" ")[0] : time; + const numberPart = formatInfo.format === "12" ? time.split(" ")[0] : time; if (numberPart) { const [hourStr, minuteStr, secondStr] = numberPart.split(":"); setHourValue(hourStr && isNumber(hourStr) ? Number(hourStr) : undefined); setMinuteValue(minuteStr && isNumber(minuteStr) ? Number(minuteStr) : undefined); setSecondValue(secondStr && isNumber(secondStr) ? Number(secondStr) : undefined); } - if (timeFormatToUse === "12" && time.includes(" ")) { + if (formatInfo.format === "12" && time.includes(" ")) { const dayPeriodValue = time.split(" ")[1] === "AM" ? 0 : time.split(" ")[1] === "PM" ? 1 : undefined; setDayPeriodValue(dayPeriodValue); } else { @@ -120,7 +115,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( setSecondValue(undefined); setDayPeriodValue(undefined); } - }, [value, defaultValue, timeFormatToUse]); + }, [value, defaultValue, formatInfo.format]); const generatedInputValue = () => { if (hourValue === undefined && minuteValue === undefined && secondValue === undefined) { @@ -132,7 +127,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( secondValue, dayPeriodValue, showSeconds, - timeFormatToUse, + formatInfo.format, formatInfo.separator, formatInfo.dayPeriodPosition ); @@ -154,7 +149,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( undefined, undefined, showSeconds, - timeFormatToUse, + formatInfo.format, formatInfo.separator, formatInfo.dayPeriodPosition ) @@ -164,7 +159,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( const validateTimeValue = (value: string) => { const timeRegex = - timeFormatToUse === "12" + formatInfo.format === "12" ? /^(0?[1-9]|1[0-2]):[0-5][0-9](?::[0-5][0-9])?\s?(AM|PM)$/i : /^([01]?[0-9]|2[0-3]):[0-5][0-9](?::[0-5][0-9])?$/; if (!timeRegex.test(value)) { @@ -175,7 +170,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( (hourValue === undefined || minuteValue === undefined || (showSeconds && secondValue === undefined) || - (timeFormatToUse === "12" && dayPeriodValue === undefined)) + (formatInfo.format === "12" && dayPeriodValue === undefined)) ) { return "This field is required"; } @@ -210,7 +205,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( )} <TimeInputField disabled={disabled} error={!!error} readOnly={readOnly}> <DxcFlex gap="var(--spacing-gap-xs)" alignItems="center" fullHeight> - {timeFormatToUse === "12" && formatInfo.dayPeriodPosition === "before" && ( + {formatInfo.format === "12" && formatInfo.dayPeriodPosition === "before" && ( <TimeSpinButton ariaLabel={label ?? ariaLabel} value={dayPeriodValue} @@ -233,7 +228,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( secondValue, value, showSeconds, - timeFormatToUse, + formatInfo.format, formatInfo.separator, formatInfo.dayPeriodPosition ) @@ -257,8 +252,8 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( <TimeSpinButton ariaLabel={label ?? ariaLabel} value={hourValue} - minValue={timeFormatToUse === "12" ? 1 : 0} - maxValue={timeFormatToUse === "12" ? 12 : 23} + minValue={formatInfo.format === "12" ? 1 : 0} + maxValue={formatInfo.format === "12" ? 12 : 23} tabIndex={tabIndex} dataType="hour" readOnly={readOnly} @@ -281,7 +276,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( secondValue, dayPeriodValue, showSeconds, - timeFormatToUse, + formatInfo.format, formatInfo.separator, formatInfo.dayPeriodPosition ) @@ -309,7 +304,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( onComplete={() => { if (showSeconds && secondRef.current) { secondRef.current.focus(); - } else if (timeFormatToUse === "12" && dayPeriodRef.current) { + } else if (formatInfo.format === "12" && dayPeriodRef.current) { dayPeriodRef.current.focus(); } }} @@ -325,7 +320,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( secondValue, dayPeriodValue, showSeconds, - timeFormatToUse, + formatInfo.format, formatInfo.separator, formatInfo.dayPeriodPosition ) @@ -335,7 +330,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( onNext={() => { if (showSeconds && secondRef.current) { secondRef.current.focus(); - } else if (timeFormatToUse === "12" && dayPeriodRef.current) { + } else if (formatInfo.format === "12" && dayPeriodRef.current) { dayPeriodRef.current.focus(); } }} @@ -361,7 +356,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( isControlled={isControlled} onComplete={() => { if ( - timeFormatToUse === "12" && + formatInfo.format === "12" && formatInfo.dayPeriodPosition === "after" && dayPeriodRef.current ) { @@ -380,7 +375,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( value, dayPeriodValue, showSeconds, - timeFormatToUse, + formatInfo.format, formatInfo.separator, formatInfo.dayPeriodPosition ) @@ -389,7 +384,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( }} onNext={() => { if ( - timeFormatToUse === "12" && + formatInfo.format === "12" && formatInfo.dayPeriodPosition === "after" && dayPeriodRef.current ) { @@ -406,7 +401,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( </> )} </DxcFlex> - {timeFormatToUse === "12" && formatInfo.dayPeriodPosition === "after" && ( + {formatInfo.format === "12" && formatInfo.dayPeriodPosition === "after" && ( <TimeSpinButton ariaLabel={label ?? ariaLabel} value={dayPeriodValue} @@ -429,7 +424,7 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( secondValue, value, showSeconds, - timeFormatToUse, + formatInfo.format, formatInfo.separator, formatInfo.dayPeriodPosition ) @@ -475,14 +470,15 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( second, dayPeriod, showSeconds, - timeFormatToUse, + formatInfo.format, formatInfo.separator, formatInfo.dayPeriodPosition ) ); } }} - timeFormat={timeFormatToUse} + timeFormat={formatInfo.format} + dayPeriodPosition={formatInfo.dayPeriodPosition} showSeconds={showSeconds} hourValue={hourValue} minuteValue={minuteValue} diff --git a/packages/lib/src/time-input/TimePicker.tsx b/packages/lib/src/time-input/TimePicker.tsx index 5d43d71b8..36ce6fd99 100644 --- a/packages/lib/src/time-input/TimePicker.tsx +++ b/packages/lib/src/time-input/TimePicker.tsx @@ -16,6 +16,7 @@ const TimePickerContainer = styled.div` const TimePicker = ({ onPickTime, timeFormat, + dayPeriodPosition, showSeconds, hourValue, minuteValue, @@ -66,6 +67,24 @@ const TimePicker = ({ return ( <TimePickerContainer role="listbox" aria-label="Time picker"> + {timeFormat === "12" && dayPeriodPosition === "before" && ( + <TimePickerColumn + valuesArray={[0, 1]} + id={id} + selectedValue={dayPeriod} + valueToFocus={dayPeriodToFocus} + tabIndex={tabIndex} + dataType="dayPeriod" + onClick={(value: number) => { + onPickerSelect(value, "dayPeriod"); + }} + onKeyboardEvent={(event: React.KeyboardEvent, value: number) => + handleColumnKeyDown(event, "dayPeriod", value, 2, setDayPeriodToFocus, (value) => + onPickerSelect(value, "dayPeriod") + ) + } + /> + )} <TimePickerColumn valuesArray={Array.from({ length: totalHours }, (_, index) => index)} id={id} @@ -128,7 +147,7 @@ const TimePicker = ({ } /> )} - {timeFormat === "12" && ( + {timeFormat === "12" && dayPeriodPosition !== "before" && ( <TimePickerColumn valuesArray={[0, 1]} id={id} diff --git a/packages/lib/src/time-input/types.ts b/packages/lib/src/time-input/types.ts index e10e787af..ca00d4bd5 100644 --- a/packages/lib/src/time-input/types.ts +++ b/packages/lib/src/time-input/types.ts @@ -106,6 +106,7 @@ export type TimeSpinButtonPropsType = { export type TimePickerPropsType = { onPickTime: (hours: number, minutes: number, seconds?: number, dayPeriod?: number) => void; timeFormat: "12" | "24"; + dayPeriodPosition?: "before" | "after"; showSeconds?: boolean; hourValue?: number; minuteValue?: number; diff --git a/packages/lib/src/time-input/utils.ts b/packages/lib/src/time-input/utils.ts index cf078f771..5ae44f08c 100644 --- a/packages/lib/src/time-input/utils.ts +++ b/packages/lib/src/time-input/utils.ts @@ -184,16 +184,21 @@ export const handleColumnKeyDown = ( }; export const getTimeInputLocale = ( - locale?: string + locale?: string, + formatProp?: "12" | "24" ): { separator: string; format: "12" | "24"; dayPeriodPosition: "before" | "after" } => { if (!locale) { return { separator: ":", - format: "12", + format: formatProp || "12", dayPeriodPosition: "after", }; } - const naturalCycle = new Intl.DateTimeFormat(locale, { hour: "2-digit" }).resolvedOptions().hourCycle; + const naturalCycle = formatProp + ? formatProp === "24" + ? "h23" + : "h12" + : new Intl.DateTimeFormat(locale, { hour: "2-digit" }).resolvedOptions().hourCycle; const hourCycle: "h12" | "h23" = naturalCycle === "h23" || naturalCycle === "h24" ? "h23" : "h12"; const formatter = new Intl.DateTimeFormat(locale, { @@ -211,6 +216,7 @@ export const getTimeInputLocale = ( format === "24" || dayPeriodPosition === "after" ? separatorParts[0]?.value || ":" : separatorParts[separatorParts.length - 1]?.value || ":"; + return { separator, format, From 1af6f4d1fe7a143c4800f0bf2028b12b3eb9a96d Mon Sep 17 00:00:00 2001 From: jialecl <jialestrabajos@gmail.com> Date: Thu, 16 Jul 2026 13:29:14 +0200 Subject: [PATCH 3/5] Improved localization and translation --- .../localization/LocalizationPage.tsx | 12 +++++ packages/lib/src/common/variables.ts | 2 + .../lib/src/time-input/TimeInput.stories.tsx | 11 ++++- packages/lib/src/time-input/TimeInput.tsx | 35 ++++++++++---- packages/lib/src/time-input/TimePicker.tsx | 7 ++- .../lib/src/time-input/TimePickerColumn.tsx | 11 ++--- .../lib/src/time-input/TimeSpinButton.tsx | 43 ++++++++--------- packages/lib/src/time-input/utils.ts | 47 +++++++++++++++++-- 8 files changed, 122 insertions(+), 46 deletions(-) diff --git a/apps/website/screens/guidelines/localization/LocalizationPage.tsx b/apps/website/screens/guidelines/localization/LocalizationPage.tsx index 81aebdd1c..7b2ecd52c 100644 --- a/apps/website/screens/guidelines/localization/LocalizationPage.tsx +++ b/apps/website/screens/guidelines/localization/LocalizationPage.tsx @@ -734,6 +734,18 @@ const sections = [ </td> <td>Select time</td> </tr> + <tr> + <td> + <Code>timePeriodAM</Code> + </td> + <td>AM</td> + </tr> + <tr> + <td> + <Code>timePeriodPM</Code> + </td> + <td>PM</td> + </tr> </tbody> </DxcTable> ), diff --git a/packages/lib/src/common/variables.ts b/packages/lib/src/common/variables.ts index f5a58b378..3195db75c 100644 --- a/packages/lib/src/common/variables.ts +++ b/packages/lib/src/common/variables.ts @@ -132,6 +132,8 @@ export const defaultTranslatedComponentLabels = { }, timeInput: { timePickerActionTitle: "Select time", + timePeriodAM: "AM", + timePeriodPM: "PM", }, toast: { clearToastActionTitle: "Clear toast", diff --git a/packages/lib/src/time-input/TimeInput.stories.tsx b/packages/lib/src/time-input/TimeInput.stories.tsx index 0b0eefdee..9ea02c699 100644 --- a/packages/lib/src/time-input/TimeInput.stories.tsx +++ b/packages/lib/src/time-input/TimeInput.stories.tsx @@ -117,11 +117,20 @@ const TimeInput = () => { showSeconds /> </HalstackProvider> - <HalstackProvider localeTag="zh-CN"> + <HalstackProvider + localeTag="zh-CN" + labels={{ + timeInput: { + timePeriodAM: "上午", + timePeriodPM: "下午", + }, + }} + > <DxcTimeInput label="Chinese locale with 12-hour format" helperText="Helper text" onChange={(val) => console.log(val)} + defaultValue="下午 06:30:20" timeFormat="12" showSeconds /> diff --git a/packages/lib/src/time-input/TimeInput.tsx b/packages/lib/src/time-input/TimeInput.tsx index 57cb7713b..dac33e49c 100644 --- a/packages/lib/src/time-input/TimeInput.tsx +++ b/packages/lib/src/time-input/TimeInput.tsx @@ -96,7 +96,8 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( useEffect(() => { const time = value || defaultValue || undefined; if (time) { - const numberPart = formatInfo.format === "12" ? time.split(" ")[0] : time; + const numberPart = + formatInfo.format === "12" ? time.split(" ")[formatInfo.dayPeriodPosition === "before" ? 1 : 0] : time; if (numberPart) { const [hourStr, minuteStr, secondStr] = numberPart.split(":"); setHourValue(hourStr && isNumber(hourStr) ? Number(hourStr) : undefined); @@ -104,7 +105,13 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( setSecondValue(secondStr && isNumber(secondStr) ? Number(secondStr) : undefined); } if (formatInfo.format === "12" && time.includes(" ")) { - const dayPeriodValue = time.split(" ")[1] === "AM" ? 0 : time.split(" ")[1] === "PM" ? 1 : undefined; + const dayPeriod = formatInfo.dayPeriodPosition === "before" ? time.split(" ")[0] : time.split(" ")[1]; + const dayPeriodValue = + dayPeriod === translatedLabels.timeInput.timePeriodAM + ? 0 + : dayPeriod === translatedLabels.timeInput.timePeriodPM + ? 1 + : undefined; setDayPeriodValue(dayPeriodValue); } else { setDayPeriodValue(undefined); @@ -129,7 +136,8 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( showSeconds, formatInfo.format, formatInfo.separator, - formatInfo.dayPeriodPosition + formatInfo.dayPeriodPosition, + translatedLabels ); } }; @@ -151,7 +159,8 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( showSeconds, formatInfo.format, formatInfo.separator, - formatInfo.dayPeriodPosition + formatInfo.dayPeriodPosition, + translatedLabels ) ); } @@ -230,7 +239,8 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( showSeconds, formatInfo.format, formatInfo.separator, - formatInfo.dayPeriodPosition + formatInfo.dayPeriodPosition, + translatedLabels ) ); } @@ -278,7 +288,8 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( showSeconds, formatInfo.format, formatInfo.separator, - formatInfo.dayPeriodPosition + formatInfo.dayPeriodPosition, + translatedLabels ) ); } @@ -322,7 +333,8 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( showSeconds, formatInfo.format, formatInfo.separator, - formatInfo.dayPeriodPosition + formatInfo.dayPeriodPosition, + translatedLabels ) ); } @@ -377,7 +389,8 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( showSeconds, formatInfo.format, formatInfo.separator, - formatInfo.dayPeriodPosition + formatInfo.dayPeriodPosition, + translatedLabels ) ); } @@ -426,7 +439,8 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( showSeconds, formatInfo.format, formatInfo.separator, - formatInfo.dayPeriodPosition + formatInfo.dayPeriodPosition, + translatedLabels ) ); } @@ -472,7 +486,8 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( showSeconds, formatInfo.format, formatInfo.separator, - formatInfo.dayPeriodPosition + formatInfo.dayPeriodPosition, + translatedLabels ) ); } diff --git a/packages/lib/src/time-input/TimePicker.tsx b/packages/lib/src/time-input/TimePicker.tsx index 36ce6fd99..5e690596e 100644 --- a/packages/lib/src/time-input/TimePicker.tsx +++ b/packages/lib/src/time-input/TimePicker.tsx @@ -1,8 +1,9 @@ import styled from "@emotion/styled"; import { TimePickerPropsType } from "./types"; -import { useEffect, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import TimePickerColumn from "./TimePickerColumn"; import { handleColumnKeyDown } from "./utils"; +import { HalstackLanguageContext } from "../HalstackContext"; // Array to be used in seconds and minutes. const STEP = 5; const ARRAY_OF_60 = Array.from({ length: 60 / STEP }, (_, index) => index * STEP); @@ -31,6 +32,8 @@ const TimePicker = ({ const [dayPeriodToFocus, setDayPeriodToFocus] = useState(dayPeriod ?? 0); const totalHours = timeFormat === "12" ? 12 : 24; + const translatedLabels = useContext(HalstackLanguageContext).labels; + const onPickerSelect = (value: number, type: "hour" | "minute" | "second" | "dayPeriod") => { const hourVal = type === "hour" ? value : (hourValue ?? (timeFormat === "12" ? 1 : 0)); const minuteVal = type === "minute" ? value : (minuteValue ?? 0); @@ -83,6 +86,7 @@ const TimePicker = ({ onPickerSelect(value, "dayPeriod") ) } + translatedLabels={translatedLabels} /> )} <TimePickerColumn @@ -163,6 +167,7 @@ const TimePicker = ({ onPickerSelect(value, "dayPeriod") ) } + translatedLabels={translatedLabels} /> )} </TimePickerContainer> diff --git a/packages/lib/src/time-input/TimePickerColumn.tsx b/packages/lib/src/time-input/TimePickerColumn.tsx index 20fa97a9b..a5e953726 100644 --- a/packages/lib/src/time-input/TimePickerColumn.tsx +++ b/packages/lib/src/time-input/TimePickerColumn.tsx @@ -1,7 +1,8 @@ import styled from "@emotion/styled"; import DxcContainer from "../container/Container"; import DxcFlex from "../flex/Flex"; -import { pad } from "./utils"; +import { pad, returnDayPeriod } from "./utils"; +import { TranslatedLabels } from "../common/variables"; const TimePickerOption = styled.li<{ selected: boolean; @@ -46,10 +47,6 @@ const returnHourBasedOnIndex = (index: number, dataType: "hour" | "minute" | "se } }; -const returnDayPeriod = (value: number) => { - return value === 0 ? "AM" : value === 1 ? "PM" : ""; -}; - const TimePickerColumn = ({ valuesArray, id, @@ -59,6 +56,7 @@ const TimePickerColumn = ({ dataType, onClick, onKeyboardEvent, + translatedLabels, }: { valuesArray: number[]; id?: string; @@ -68,6 +66,7 @@ const TimePickerColumn = ({ dataType: "hour" | "minute" | "second" | "dayPeriod"; onClick: (value: number) => void; onKeyboardEvent: (event: React.KeyboardEvent, value: number) => void; + translatedLabels?: TranslatedLabels; }) => { return ( <DxcContainer @@ -95,7 +94,7 @@ const TimePickerColumn = ({ > {dataType !== "dayPeriod" ? pad(returnHourBasedOnIndex(optionValue, dataType)) - : returnDayPeriod(returnHourBasedOnIndex(optionValue, dataType))} + : returnDayPeriod(returnHourBasedOnIndex(optionValue, dataType), translatedLabels)} </TimePickerOption> ))} </DxcFlex> diff --git a/packages/lib/src/time-input/TimeSpinButton.tsx b/packages/lib/src/time-input/TimeSpinButton.tsx index ccc1c2c53..6f35b3a80 100644 --- a/packages/lib/src/time-input/TimeSpinButton.tsx +++ b/packages/lib/src/time-input/TimeSpinButton.tsx @@ -1,7 +1,8 @@ import styled from "@emotion/styled"; import { TimeSpinButtonPropsType } from "./types"; -import { forwardRef, useEffect, useMemo, useRef, useState } from "react"; -import { handleKeyDown, pad, returnDayPeriod } from "./utils"; +import { forwardRef, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { handleKeyDown, pad, returnDayPeriod, generateDisplayValue } from "./utils"; +import { HalstackLanguageContext } from "../HalstackContext"; const TimeSpinButtonContainer = styled.span<{ isPlaceholder: boolean; disabled: boolean }>` caret-color: transparent; @@ -27,21 +28,6 @@ const TimeSpinButtonContainer = styled.span<{ isPlaceholder: boolean; disabled: box-sizing: border-box; `; -const generateDisplayValue = ( - dataType: "hour" | "minute" | "second" | "dayPeriod" | undefined, - value: number | undefined, - placeholder: string, - maxValue: number -) => { - let displayValue; - if (dataType === "dayPeriod") { - displayValue = value != null ? returnDayPeriod(value) : placeholder; - } else { - displayValue = value != null ? value.toString().padStart(maxValue.toString().length, "0") : placeholder; - } - return displayValue; -}; - const TimeSpinButton = forwardRef<HTMLSpanElement, TimeSpinButtonPropsType>( ( { @@ -63,6 +49,7 @@ const TimeSpinButton = forwardRef<HTMLSpanElement, TimeSpinButtonPropsType>( ) => { const [innerValue, setInnerValue] = useState<number | undefined>(value); const spanRef = useRef<HTMLSpanElement | null>(null); + const translatedLabels = useContext(HalstackLanguageContext).labels; const placeholder = useMemo(() => { switch (dataType) { @@ -86,18 +73,24 @@ const TimeSpinButton = forwardRef<HTMLSpanElement, TimeSpinButtonPropsType>( useEffect(() => { if (!spanRef.current) return; if (!isControlled) { - spanRef.current.textContent = generateDisplayValue(dataType, innerValue, placeholder, maxValue); + spanRef.current.textContent = generateDisplayValue( + dataType, + innerValue, + placeholder, + maxValue, + translatedLabels + ); } else { - spanRef.current.textContent = generateDisplayValue(dataType, value, placeholder, maxValue); + spanRef.current.textContent = generateDisplayValue(dataType, value, placeholder, maxValue, translatedLabels); } - }, [innerValue, placeholder, maxValue, dataType, isControlled]); + }, [innerValue, placeholder, maxValue, dataType, isControlled, translatedLabels]); useEffect(() => { setInnerValue(value); if (spanRef.current) { - spanRef.current.textContent = generateDisplayValue(dataType, value, placeholder, maxValue); + spanRef.current.textContent = generateDisplayValue(dataType, value, placeholder, maxValue, translatedLabels); } - }, [value, placeholder, maxValue, dataType]); + }, [value, placeholder, maxValue, dataType, translatedLabels]); // Values used to track the raw input before it's resolved to a valid value. const rawInput = useRef<string>(""); @@ -116,7 +109,11 @@ const TimeSpinButton = forwardRef<HTMLSpanElement, TimeSpinButtonPropsType>( role="spinbutton" aria-valuenow={innerValue ?? undefined} aria-valuetext={ - innerValue != null ? (dataType === "dayPeriod" ? returnDayPeriod(innerValue) : pad(innerValue)) : "Empty" + innerValue != null + ? dataType === "dayPeriod" + ? returnDayPeriod(innerValue, translatedLabels) + : pad(innerValue) + : "Empty" } aria-valuemin={minValue} aria-valuemax={maxValue} diff --git a/packages/lib/src/time-input/utils.ts b/packages/lib/src/time-input/utils.ts index 5ae44f08c..653a0f94d 100644 --- a/packages/lib/src/time-input/utils.ts +++ b/packages/lib/src/time-input/utils.ts @@ -1,10 +1,16 @@ +import { TranslatedLabels } from "../common/variables"; + export const pad = (num?: number) => { if (num === undefined) return ""; return num < 10 ? `0${num}` : `${num}`; }; -export const returnDayPeriod = (value?: number) => { - return value === 0 ? "AM" : value === 1 ? "PM" : ""; +export const returnDayPeriod = (value?: number, translatedLabels?: TranslatedLabels) => { + return value === 0 + ? (translatedLabels?.timeInput.timePeriodAM ?? "AM") + : value === 1 + ? (translatedLabels?.timeInput.timePeriodPM ?? "PM") + : ""; }; const resolveValue = (value: string | number, maxValue: number, minValue: number) => { @@ -117,6 +123,31 @@ export const handleKeyDown = ( } }; +/** + * + * @param dataType The type of time unit (hour, minute, second, dayPeriod) + * @param value The current value of the time unit + * @param placeholder The placeholder text to display when the value is undefined + * @param maxValue The maximum value for the time unit + * @param translatedLabels The translated labels for day periods + * @returns The display value for the spin button, formatted according to the data type and value provided. If the value is undefined, the placeholder will be displayed instead. + */ +export const generateDisplayValue = ( + dataType: "hour" | "minute" | "second" | "dayPeriod" | undefined, + value: number | undefined, + placeholder: string, + maxValue: number, + translatedLabels?: TranslatedLabels +) => { + let displayValue; + if (dataType === "dayPeriod") { + displayValue = value != null ? returnDayPeriod(value, translatedLabels) : placeholder; + } else { + displayValue = value != null ? value.toString().padStart(maxValue.toString().length, "0") : placeholder; + } + return displayValue; +}; + export const generateEventValue = ( hour: number | undefined, minute: number | undefined, @@ -125,16 +156,17 @@ export const generateEventValue = ( showSeconds: boolean | undefined, timeFormat: "12" | "24" | undefined, separator: string, - timePeriodPosition: "before" | "after" + timePeriodPosition: "before" | "after", + translatedLabels: TranslatedLabels ) => { if (hour === undefined && minute === undefined && second === undefined && dayPeriod === undefined) { return ""; } // consider dayperiod position for 12-hour format otherwise ignore it if (timeFormat === "12" && timePeriodPosition === "before") { - return `${returnDayPeriod(dayPeriod)} ${pad(hour)}${separator}${pad(minute)}${showSeconds ? `${separator}${pad(second)}` : ""}`; + return `${returnDayPeriod(dayPeriod, translatedLabels)} ${pad(hour)}${separator}${pad(minute)}${showSeconds ? `${separator}${pad(second)}` : ""}`; } else if (timeFormat === "12" && timePeriodPosition === "after") { - return `${pad(hour)}${separator}${pad(minute)}${showSeconds ? `${separator}${pad(second)}` : ""} ${returnDayPeriod(dayPeriod)}`; + return `${pad(hour)}${separator}${pad(minute)}${showSeconds ? `${separator}${pad(second)}` : ""} ${returnDayPeriod(dayPeriod, translatedLabels)}`; } else { return `${pad(hour)}${separator}${pad(minute)}${showSeconds ? `${separator}${pad(second)}` : ""}`; } @@ -183,6 +215,11 @@ export const handleColumnKeyDown = ( } }; +/** + * @param locale The locale string (e.g., "en-US") used to determine time formatting conventions. + * @param formatProp Optional. Specifies whether to use "12" or "24" hour format. + * @returns An object containing the time separator, preferred format, and day period position. + */ export const getTimeInputLocale = ( locale?: string, formatProp?: "12" | "24" From 541586d2fbeed8471a7801551d758f2df1c8cf1e Mon Sep 17 00:00:00 2001 From: jialecl <jialestrabajos@gmail.com> Date: Thu, 16 Jul 2026 13:43:47 +0200 Subject: [PATCH 4/5] Test reverted for now --- packages/lib/src/time-input/TimeInput.stories.tsx | 11 +---------- packages/lib/src/time-input/TimeInput.tsx | 4 +++- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/lib/src/time-input/TimeInput.stories.tsx b/packages/lib/src/time-input/TimeInput.stories.tsx index 9ea02c699..0b0eefdee 100644 --- a/packages/lib/src/time-input/TimeInput.stories.tsx +++ b/packages/lib/src/time-input/TimeInput.stories.tsx @@ -117,20 +117,11 @@ const TimeInput = () => { showSeconds /> </HalstackProvider> - <HalstackProvider - localeTag="zh-CN" - labels={{ - timeInput: { - timePeriodAM: "上午", - timePeriodPM: "下午", - }, - }} - > + <HalstackProvider localeTag="zh-CN"> <DxcTimeInput label="Chinese locale with 12-hour format" helperText="Helper text" onChange={(val) => console.log(val)} - defaultValue="下午 06:30:20" timeFormat="12" showSeconds /> diff --git a/packages/lib/src/time-input/TimeInput.tsx b/packages/lib/src/time-input/TimeInput.tsx index dac33e49c..4ae0d11d9 100644 --- a/packages/lib/src/time-input/TimeInput.tsx +++ b/packages/lib/src/time-input/TimeInput.tsx @@ -99,13 +99,15 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( const numberPart = formatInfo.format === "12" ? time.split(" ")[formatInfo.dayPeriodPosition === "before" ? 1 : 0] : time; if (numberPart) { + console.log("numberPart", numberPart); const [hourStr, minuteStr, secondStr] = numberPart.split(":"); setHourValue(hourStr && isNumber(hourStr) ? Number(hourStr) : undefined); setMinuteValue(minuteStr && isNumber(minuteStr) ? Number(minuteStr) : undefined); setSecondValue(secondStr && isNumber(secondStr) ? Number(secondStr) : undefined); } if (formatInfo.format === "12" && time.includes(" ")) { - const dayPeriod = formatInfo.dayPeriodPosition === "before" ? time.split(" ")[0] : time.split(" ")[1]; + const dayPeriod = time.split(" ")[formatInfo.dayPeriodPosition === "before" ? 0 : 1]; + console.log("dayPeriod", dayPeriod); const dayPeriodValue = dayPeriod === translatedLabels.timeInput.timePeriodAM ? 0 From e2e2979be8a34c6ac773ae9e0ddee85f84b96bde Mon Sep 17 00:00:00 2001 From: jialecl <jialestrabajos@gmail.com> Date: Thu, 16 Jul 2026 13:45:03 +0200 Subject: [PATCH 5/5] removed console.log --- packages/lib/src/time-input/TimeInput.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/lib/src/time-input/TimeInput.tsx b/packages/lib/src/time-input/TimeInput.tsx index 4ae0d11d9..acbdcddb0 100644 --- a/packages/lib/src/time-input/TimeInput.tsx +++ b/packages/lib/src/time-input/TimeInput.tsx @@ -99,7 +99,6 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( const numberPart = formatInfo.format === "12" ? time.split(" ")[formatInfo.dayPeriodPosition === "before" ? 1 : 0] : time; if (numberPart) { - console.log("numberPart", numberPart); const [hourStr, minuteStr, secondStr] = numberPart.split(":"); setHourValue(hourStr && isNumber(hourStr) ? Number(hourStr) : undefined); setMinuteValue(minuteStr && isNumber(minuteStr) ? Number(minuteStr) : undefined); @@ -107,7 +106,6 @@ const DxcTimeInput = forwardRef<RefType, TimeInputPropsType>( } if (formatInfo.format === "12" && time.includes(" ")) { const dayPeriod = time.split(" ")[formatInfo.dayPeriodPosition === "before" ? 0 : 1]; - console.log("dayPeriod", dayPeriod); const dayPeriodValue = dayPeriod === translatedLabels.timeInput.timePeriodAM ? 0