diff --git a/packages/lib/src/time-input/TimeInput.stories.tsx b/packages/lib/src/time-input/TimeInput.stories.tsx
index 9a8632bda..0b0eefdee 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,32 @@ const TimeInput = () => {
+
+
+
+
+
+
+
+
+
+ console.log(val)}
+ showSeconds
+ />
+
+
+ console.log(val)}
+ timeFormat="12"
+ showSeconds
+ />
+
+
>
);
};
@@ -237,6 +264,17 @@ const TimePickerExamples = () => {
/>
+
+
+ {}}
+ timeFormat="12"
+ dayPeriodPosition="before"
+ id="testId"
+ tabIndex={0}
+ showSeconds
+ />
+
>
);
};
diff --git a/packages/lib/src/time-input/TimeInput.test.tsx b/packages/lib/src/time-input/TimeInput.test.tsx
index b1c9451ca..b260f2d16 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,95 @@ 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(
+
+
+
+ );
+ 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(
+
+
+
+ );
+ 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(
+
+
+
+ );
+ 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");
+ });
+
+ it("Time input with chinese locale, but using time picker", () => {
+ const mockOnChange = jest.fn();
+ const { getByRole, getAllByText } = render(
+
+
+
+ );
+ 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 cb0eb0c23..57cb7713b 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(
showSeconds = false,
size = "medium",
tabIndex = 0,
- timeFormat = "12",
+ timeFormat,
value,
},
ref
@@ -90,19 +90,20 @@ const DxcTimeInput = forwardRef(
const secondRef = useRef(null);
const dayPeriodRef = useRef(null);
const isControlled = value !== undefined;
- const translatedLabels = useContext(HalstackLanguageContext).labels;
-
+ const languageContext = useContext(HalstackLanguageContext);
+ const translatedLabels = languageContext.labels;
+ const formatInfo = useMemo(() => getTimeInputLocale(languageContext.locale, timeFormat), [languageContext.locale]);
useEffect(() => {
const time = value || defaultValue || undefined;
if (time) {
- const numberPart = timeFormat === "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 (timeFormat === "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 {
@@ -114,13 +115,22 @@ const DxcTimeInput = forwardRef(
setSecondValue(undefined);
setDayPeriodValue(undefined);
}
- }, [value, defaultValue, timeFormat]);
+ }, [value, defaultValue, formatInfo.format]);
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,
+ formatInfo.format,
+ formatInfo.separator,
+ formatInfo.dayPeriodPosition
+ );
}
};
@@ -132,13 +142,24 @@ const DxcTimeInput = forwardRef(
setDayPeriodValue(undefined);
}
if (typeof onChange === "function") {
- onChange(generateEventValue(undefined, undefined, undefined, undefined, showSeconds, timeFormat));
+ onChange(
+ generateEventValue(
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ showSeconds,
+ formatInfo.format,
+ formatInfo.separator,
+ formatInfo.dayPeriodPosition
+ )
+ );
}
};
const validateTimeValue = (value: string) => {
const timeRegex =
- timeFormat === "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)) {
@@ -149,7 +170,7 @@ const DxcTimeInput = forwardRef(
(hourValue === undefined ||
minuteValue === undefined ||
(showSeconds && secondValue === undefined) ||
- (timeFormat === "12" && dayPeriodValue === undefined))
+ (formatInfo.format === "12" && dayPeriodValue === undefined))
) {
return "This field is required";
}
@@ -184,12 +205,55 @@ const DxcTimeInput = forwardRef(
)}
+ {formatInfo.format === "12" && formatInfo.dayPeriodPosition === "before" && (
+ {
+ if (!isControlled) {
+ setDayPeriodValue(value);
+ }
+ if (typeof onChange === "function") {
+ onChange(
+ generateEventValue(
+ hourValue,
+ minuteValue,
+ secondValue,
+ value,
+ showSeconds,
+ formatInfo.format,
+ formatInfo.separator,
+ formatInfo.dayPeriodPosition
+ )
+ );
+ }
+ }}
+ onComplete={() => {
+ if (hourRef.current) {
+ hourRef.current.focus();
+ }
+ }}
+ onNext={() => {
+ if (hourRef.current) {
+ hourRef.current.focus();
+ }
+ }}
+ ref={dayPeriodRef}
+ />
+ )}
(
}
if (typeof onChange === "function") {
onChange(
- generateEventValue(value, minuteValue, secondValue, dayPeriodValue, showSeconds, timeFormat)
+ generateEventValue(
+ value,
+ minuteValue,
+ secondValue,
+ dayPeriodValue,
+ showSeconds,
+ formatInfo.format,
+ formatInfo.separator,
+ formatInfo.dayPeriodPosition
+ )
);
}
}}
@@ -217,7 +290,7 @@ const DxcTimeInput = forwardRef(
}}
ref={hourRef}
/>
- :
+ {formatInfo.separator}
(
onComplete={() => {
if (showSeconds && secondRef.current) {
secondRef.current.focus();
- } else if (timeFormat === "12" && dayPeriodRef.current) {
+ } else if (formatInfo.format === "12" && dayPeriodRef.current) {
dayPeriodRef.current.focus();
}
}}
@@ -241,14 +314,23 @@ const DxcTimeInput = forwardRef(
}
if (typeof onChange === "function") {
onChange(
- generateEventValue(hourValue, value, secondValue, dayPeriodValue, showSeconds, timeFormat)
+ generateEventValue(
+ hourValue,
+ value,
+ secondValue,
+ dayPeriodValue,
+ showSeconds,
+ formatInfo.format,
+ formatInfo.separator,
+ formatInfo.dayPeriodPosition
+ )
);
}
}}
onNext={() => {
if (showSeconds && secondRef.current) {
secondRef.current.focus();
- } else if (timeFormat === "12" && dayPeriodRef.current) {
+ } else if (formatInfo.format === "12" && dayPeriodRef.current) {
dayPeriodRef.current.focus();
}
}}
@@ -261,7 +343,7 @@ const DxcTimeInput = forwardRef(
/>
{showSeconds && (
<>
- :
+ {formatInfo.separator}
(
disabled={disabled}
isControlled={isControlled}
onComplete={() => {
- if (timeFormat === "12" && dayPeriodRef.current) {
+ if (
+ formatInfo.format === "12" &&
+ formatInfo.dayPeriodPosition === "after" &&
+ dayPeriodRef.current
+ ) {
dayPeriodRef.current.focus();
}
}}
@@ -283,12 +369,25 @@ const DxcTimeInput = forwardRef(
}
if (typeof onChange === "function") {
onChange(
- generateEventValue(hourValue, minuteValue, value, dayPeriodValue, showSeconds, timeFormat)
+ generateEventValue(
+ hourValue,
+ minuteValue,
+ value,
+ dayPeriodValue,
+ showSeconds,
+ formatInfo.format,
+ formatInfo.separator,
+ formatInfo.dayPeriodPosition
+ )
);
}
}}
onNext={() => {
- if (timeFormat === "12" && dayPeriodRef.current) {
+ if (
+ formatInfo.format === "12" &&
+ formatInfo.dayPeriodPosition === "after" &&
+ dayPeriodRef.current
+ ) {
dayPeriodRef.current.focus();
}
}}
@@ -302,7 +401,7 @@ const DxcTimeInput = forwardRef(
>
)}
- {timeFormat === "12" && (
+ {formatInfo.format === "12" && formatInfo.dayPeriodPosition === "after" && (
(
setDayPeriodValue(value);
}
if (typeof onChange === "function") {
- onChange(generateEventValue(hourValue, minuteValue, secondValue, value, showSeconds, timeFormat));
+ onChange(
+ generateEventValue(
+ hourValue,
+ minuteValue,
+ secondValue,
+ value,
+ showSeconds,
+ formatInfo.format,
+ formatInfo.separator,
+ formatInfo.dayPeriodPosition
+ )
+ );
}
}}
onPrevious={() => {
@@ -353,10 +463,22 @@ const DxcTimeInput = forwardRef(
setHourValue(hour);
}
if (typeof onChange === "function") {
- onChange(generateEventValue(hour, minute, second, dayPeriod, showSeconds, timeFormat));
+ onChange(
+ generateEventValue(
+ hour,
+ minute,
+ second,
+ dayPeriod,
+ showSeconds,
+ formatInfo.format,
+ formatInfo.separator,
+ formatInfo.dayPeriodPosition
+ )
+ );
}
}}
- timeFormat={timeFormat}
+ 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 (
+ {timeFormat === "12" && dayPeriodPosition === "before" && (
+ {
+ onPickerSelect(value, "dayPeriod");
+ }}
+ onKeyboardEvent={(event: React.KeyboardEvent, value: number) =>
+ handleColumnKeyDown(event, "dayPeriod", value, 2, setDayPeriodToFocus, (value) =>
+ onPickerSelect(value, "dayPeriod")
+ )
+ }
+ />
+ )}
index)}
id={id}
@@ -128,7 +147,7 @@ const TimePicker = ({
}
/>
)}
- {timeFormat === "12" && (
+ {timeFormat === "12" && dayPeriodPosition !== "before" && (
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 def5a10bf..5ae44f08c 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,44 @@ export const handleColumnKeyDown = (
}
}
};
+
+export const getTimeInputLocale = (
+ locale?: string,
+ formatProp?: "12" | "24"
+): { separator: string; format: "12" | "24"; dayPeriodPosition: "before" | "after" } => {
+ if (!locale) {
+ return {
+ separator: ":",
+ format: formatProp || "12",
+ dayPeriodPosition: "after",
+ };
+ }
+ 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, {
+ 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,
+ };
+};