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 = [
Select time |
+
+
+ timePeriodAM
+ |
+ AM |
+
+
+
+ timePeriodPM
+ |
+ PM |
+
),
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 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..acbdcddb0 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,20 +90,28 @@ 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(" ")[formatInfo.dayPeriodPosition === "before" ? 1 : 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(" ")) {
- 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 +122,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,13 +150,25 @@ 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"
+ 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 +179,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 +214,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
+ )
);
}
}}
@@ -217,7 +301,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 +325,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 +355,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 +381,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 +414,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 +477,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 5d43d71b8..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);
@@ -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 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 (
{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 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(
(
{
@@ -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 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 def5a10bf..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) => {
@@ -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,49 @@ 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,
+ };
+};