diff --git a/apps/website/screens/components/date-input/code/DateInputCodePage.tsx b/apps/website/screens/components/date-input/code/DateInputCodePage.tsx
index 766731813b..87fcd5bc4e 100644
--- a/apps/website/screens/components/date-input/code/DateInputCodePage.tsx
+++ b/apps/website/screens/components/date-input/code/DateInputCodePage.tsx
@@ -101,7 +101,7 @@ const sections = [
The format in which the date value will be displayed. User must follow this format when editing the value
or it will be considered as an invalid date. In this case, the onBlur and{" "}
onChange functions will be called with an internal error as a parameter reporting the
- situation.
+ situation. If a locale is provided, it will override this setting's default value.
'dd-MM-yyyy'
diff --git a/apps/website/screens/components/time-input/code/TimeInputCodePage.tsx b/apps/website/screens/components/time-input/code/TimeInputCodePage.tsx
index 2980b878ce..7148d84038 100644
--- a/apps/website/screens/components/time-input/code/TimeInputCodePage.tsx
+++ b/apps/website/screens/components/time-input/code/TimeInputCodePage.tsx
@@ -198,7 +198,10 @@ const sections = [
|
'12' | '24'
|
- Time format of the input. It can be either 12 or 24. |
+
+ Time format of the input. It can be either 12 or 24. If a locale is provided, it will override this
+ setting's default value.
+ |
'12'
|
diff --git a/apps/website/screens/guidelines/localization/LocalizationPage.tsx b/apps/website/screens/guidelines/localization/LocalizationPage.tsx
index 81aebdd1c4..7b2ecd52c0 100644
--- a/apps/website/screens/guidelines/localization/LocalizationPage.tsx
+++ b/apps/website/screens/guidelines/localization/LocalizationPage.tsx
@@ -734,6 +734,18 @@ const sections = [
Select time |
+
+
+ timePeriodAM
+ |
+ AM |
+
+
+
+ timePeriodPM
+ |
+ PM |
+
),
diff --git a/packages/lib/src/common/variables.ts b/packages/lib/src/common/variables.ts
index f5a58b3784..3195db75c1 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 9a8632bdab..0b0eefdee9 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 b1c9451ca9..b260f2d169 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 cb0eb0c235..b0fbeb2c8b 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 { buildTimeRegex, 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,20 +90,31 @@ 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, timeFormat]
+ );
useEffect(() => {
const time = value || defaultValue || undefined;
if (time) {
- const numberPart = timeFormat === "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(":");
+ const [hourStr, minuteStr, secondStr] = numberPart.split(formatInfo.separator);
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(" ")) {
- const dayPeriodValue = time.split(" ")[1] === "AM" ? 0 : time.split(" ")[1] === "PM" ? 1 : undefined;
+ if (formatInfo.format === "12" && time.includes(" ")) {
+ const dayPeriod = time.split(" ")[formatInfo.dayPeriodPosition === "before" ? 0 : 1];
+ const dayPeriodValue =
+ dayPeriod === translatedLabels.timeInput.timePeriodAM
+ ? 0
+ : dayPeriod === translatedLabels.timeInput.timePeriodPM
+ ? 1
+ : undefined;
setDayPeriodValue(dayPeriodValue);
} else {
setDayPeriodValue(undefined);
@@ -114,13 +125,23 @@ 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,
+ translatedLabels
+ );
}
};
@@ -132,16 +153,33 @@ 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,
+ translatedLabels
+ )
+ );
}
};
const validateTimeValue = (value: string) => {
- const timeRegex =
- timeFormat === "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])?$/;
+ const timeRegex = buildTimeRegex(
+ formatInfo.format,
+ formatInfo.separator,
+ showSeconds,
+ formatInfo.dayPeriodPosition,
+ translatedLabels.timeInput.timePeriodAM || "AM",
+ translatedLabels.timeInput.timePeriodPM || "PM"
+ );
if (!timeRegex.test(value)) {
+ console.log("Invalid time format");
return "Invalid time format";
}
if (
@@ -149,7 +187,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 +222,56 @@ 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,
+ translatedLabels
+ )
+ );
+ }
+ }}
+ 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,
+ translatedLabels
+ )
);
}
}}
@@ -215,9 +307,18 @@ const DxcTimeInput = forwardRef(
minuteRef.current.focus();
}
}}
+ onPrevious={() => {
+ if (
+ formatInfo.format === "12" &&
+ formatInfo.dayPeriodPosition === "before" &&
+ dayPeriodRef.current
+ ) {
+ dayPeriodRef.current.focus();
+ }
+ }}
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 +342,24 @@ 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,
+ translatedLabels
+ )
);
}
}}
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 +372,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 +398,26 @@ 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,
+ translatedLabels
+ )
);
}
}}
onNext={() => {
- if (timeFormat === "12" && dayPeriodRef.current) {
+ if (
+ formatInfo.format === "12" &&
+ formatInfo.dayPeriodPosition === "after" &&
+ dayPeriodRef.current
+ ) {
dayPeriodRef.current.focus();
}
}}
@@ -302,7 +431,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,
+ translatedLabels
+ )
+ );
}
}}
onPrevious={() => {
@@ -353,10 +494,23 @@ 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,
+ translatedLabels
+ )
+ );
}
}}
- 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 5d43d71b80..5e690596e1 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);
@@ -16,6 +17,7 @@ const TimePickerContainer = styled.div`
const TimePicker = ({
onPickTime,
timeFormat,
+ dayPeriodPosition,
showSeconds,
hourValue,
minuteValue,
@@ -30,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);
@@ -66,6 +70,25 @@ 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")
+ )
+ }
+ translatedLabels={translatedLabels}
+ />
+ )}
index)}
id={id}
@@ -128,7 +151,7 @@ const TimePicker = ({
}
/>
)}
- {timeFormat === "12" && (
+ {timeFormat === "12" && dayPeriodPosition !== "before" && (
)}
diff --git a/packages/lib/src/time-input/TimePickerColumn.tsx b/packages/lib/src/time-input/TimePickerColumn.tsx
index 20fa97a9bd..a5e9537265 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 (
{dataType !== "dayPeriod"
? pad(returnHourBasedOnIndex(optionValue, dataType))
- : returnDayPeriod(returnHourBasedOnIndex(optionValue, dataType))}
+ : returnDayPeriod(returnHourBasedOnIndex(optionValue, dataType), translatedLabels)}
))}
diff --git a/packages/lib/src/time-input/TimeSpinButton.tsx b/packages/lib/src/time-input/TimeSpinButton.tsx
index ccc1c2c53e..6f35b3a80a 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(
(
{
@@ -63,6 +49,7 @@ const TimeSpinButton = forwardRef(
) => {
const [innerValue, setInnerValue] = useState(value);
const spanRef = useRef(null);
+ const translatedLabels = useContext(HalstackLanguageContext).labels;
const placeholder = useMemo(() => {
switch (dataType) {
@@ -86,18 +73,24 @@ const TimeSpinButton = forwardRef(
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("");
@@ -116,7 +109,11 @@ const TimeSpinButton = forwardRef(
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/types.ts b/packages/lib/src/time-input/types.ts
index e10e787af8..ca00d4bd5d 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 def5a10bf4..ce8a9329ca 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) => {
@@ -95,6 +101,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;
@@ -114,20 +123,53 @@ 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,
second: number | undefined,
dayPeriod: number | undefined,
showSeconds: boolean | undefined,
- timeFormat: "12" | "24" | undefined
+ timeFormat: "12" | "24" | undefined,
+ separator: string,
+ timePeriodPosition: "before" | "after",
+ translatedLabels: TranslatedLabels
) => {
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, 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, translatedLabels)}`;
+ } else {
+ return `${pad(hour)}${separator}${pad(minute)}${showSeconds ? `${separator}${pad(second)}` : ""}`;
+ }
};
export const handleColumnKeyDown = (
@@ -172,3 +214,75 @@ 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"
+): { 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,
+ };
+};
+
+const escapeStringForRegex = (str: string) => {
+ return str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
+};
+
+export const buildTimeRegex = (
+ format: "12" | "24",
+ separator: string,
+ hasSeconds: boolean,
+ dayPeriodPosition: "before" | "after",
+ amLabel: string,
+ pmLabel: string
+): RegExp => {
+ const escapedSeparator = escapeStringForRegex(separator);
+ const hourPattern = format === "12" ? "(0?[1-9]|1[0-2])" : "([01]?[0-9]|2[0-3])";
+ const minutePattern = "[0-5][0-9]";
+ const secondPattern = hasSeconds ? `${escapedSeparator}[0-5][0-9]` : "";
+ const dayPeriodPattern =
+ format === "12" ? `(?:${escapeStringForRegex(amLabel)}|${escapeStringForRegex(pmLabel)})` : "";
+ const timePattern =
+ dayPeriodPosition === "before"
+ ? `${dayPeriodPattern}\\s?${hourPattern}${escapedSeparator}${minutePattern}${secondPattern}`
+ : `${hourPattern}${escapedSeparator}${minutePattern}${secondPattern}\\s?${dayPeriodPattern}`;
+
+ return new RegExp(`^${timePattern}$`);
+};