react-accessible-time-picker
Version:
A simple and customizable time picker component for React
531 lines (527 loc) • 21 kB
JavaScript
// src/component/TimePicker/TimePicker.tsx
import "./assets/style-LAC5NIQT.css";
import { useState, useRef } from "react";
import * as Select from "@radix-ui/react-select";
import * as Popover from "@radix-ui/react-popover";
import * as Toolbar from "@radix-ui/react-toolbar";
import { CheckIcon, ChevronDownIcon, TimerIcon } from "@radix-ui/react-icons";
import styles2 from "./assets/TimePicker.module-LJD4BTOI.module.css";
import classNames2 from "classnames";
// src/component/ScrollArea/ScrollArea.tsx
import * as RadixScroll from "@radix-ui/react-scroll-area";
import styles from "./assets/ScrollArea.module-3UBZVTPC.module.css";
import classNames from "classnames";
import { jsx, jsxs } from "react/jsx-runtime";
function ScrollArea({ children, className, ...rest }) {
return /* @__PURE__ */ jsxs(RadixScroll.Root, { className: styles.scrollArea, children: [
/* @__PURE__ */ jsx(
RadixScroll.Viewport,
{
className: classNames(styles.viewport, className),
...rest,
children
}
),
/* @__PURE__ */ jsx(
RadixScroll.Scrollbar,
{
className: styles.scrollbar,
orientation: "vertical",
children: /* @__PURE__ */ jsx(RadixScroll.Thumb, { className: styles.thumb })
}
),
/* @__PURE__ */ jsx(
RadixScroll.Scrollbar,
{
className: styles.scrollbar,
orientation: "horizontal",
children: /* @__PURE__ */ jsx(RadixScroll.Thumb, { className: styles.thumb })
}
)
] });
}
var ScrollArea_default = ScrollArea;
// src/component/TimePicker/TimePicker.tsx
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
function TimePicker({
is24Hour = true,
value,
onChange,
label,
id,
disabled,
required,
minuteStep = 5,
hourStep = 1,
hourPlaceholder = "HH",
minutePlaceholder = "MM",
popoverColumnHourTitle = "Hours",
popoverColumnMinuteTitle = "Minutes",
classes = {}
}) {
const [internalHour, setInternalHour] = useState("");
const [internalMinute, setInternalMinute] = useState("");
const [internalPeriod, setInternalPeriod] = useState("AM");
const [popoverOpen, setPopoverOpen] = useState(false);
const hour = value?.hour ?? internalHour;
const minute = value?.minute ?? internalMinute;
const period = value?.period ?? internalPeriod;
const hourRef = useRef(null);
const minuteRef = useRef(null);
const updateTime = (newTime) => {
const updated = {
hour,
minute,
period,
...newTime
};
if (onChange) {
onChange(updated);
} else {
if (newTime.hour !== void 0) setInternalHour(newTime.hour);
if (newTime.minute !== void 0) setInternalMinute(newTime.minute);
if (newTime.period !== void 0) setInternalPeriod(newTime.period);
}
};
const handleHourChange = (e) => {
const val = e.target.value;
if (disabled) return;
if (val === "" || /^\d+$/.test(val)) {
if (val.length <= 1) {
updateTime({ hour: val });
} else if (val.length === 2) {
const numVal = parseInt(val, 10);
const minHour = is24Hour ? 0 : 1;
const maxHour = is24Hour ? 23 : 12;
if (numVal >= minHour && numVal <= maxHour) {
updateTime({ hour: val });
setTimeout(() => {
minuteRef.current?.focus();
}, 0);
}
}
}
};
const handleMinuteChange = (e) => {
const val = e.target.value;
if (disabled) return;
if (val === "" || /^\d+$/.test(val)) {
const numVal = val === "" ? 0 : parseInt(val, 10);
if (val === "" || numVal >= 0 && numVal <= 59) {
updateTime({ minute: val });
}
}
};
const handleKeyDown = (e, type) => {
const items = Array.from(
document.querySelectorAll(
type === "hour" ? "[data-hour]" : "[data-minute]"
)
);
if (!items.length) return;
const currentIndex = items.findIndex(
(item) => document.activeElement === item
);
let nextIndex;
if (e.key === "ArrowDown") {
nextIndex = (currentIndex + 1) % items.length;
} else if (e.key === "ArrowUp") {
nextIndex = (currentIndex - 1 + items.length) % items.length;
} else {
return;
}
e.preventDefault();
items[nextIndex].focus();
};
const handleHourKeyDown = (e) => {
if (disabled) return;
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
e.preventDefault();
const currentHour = hour === "" ? is24Hour ? 0 : 1 : parseInt(hour, 10);
const minHour = is24Hour ? 0 : 1;
const maxHour = is24Hour ? 23 : 12;
let newHour;
if (e.key === "ArrowUp") {
newHour = currentHour >= maxHour - (hourStep - 1) ? minHour : currentHour + hourStep;
if (newHour > maxHour) newHour = minHour;
} else {
newHour = currentHour <= minHour + (hourStep - 1) ? maxHour : currentHour - hourStep;
if (newHour < minHour) newHour = maxHour - maxHour % hourStep;
}
updateTime({ hour: newHour.toString().padStart(2, "0") });
}
};
const handleMinuteKeyDown = (e) => {
if (disabled) return;
if (e.key === "Backspace" && minute.length <= 1) {
setTimeout(() => {
hourRef.current?.focus();
}, 0);
}
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
e.preventDefault();
const currentMinute = minute === "" ? 0 : parseInt(minute, 10);
let newMinute;
if (e.key === "ArrowUp") {
newMinute = (currentMinute + minuteStep) % 60;
} else {
newMinute = (currentMinute - minuteStep + 60) % 60;
}
updateTime({ minute: newMinute.toString().padStart(2, "0") });
}
};
const handleHourBlur = () => {
if (disabled) return;
if (!is24Hour && hour === "0") {
updateTime({ hour: "12" });
} else if (hour.length === 1) {
updateTime({ hour: `0${hour}` });
}
};
const handleMinuteBlur = () => {
if (disabled) return;
if (minute === "") {
updateTime({ minute: "00" });
} else if (minute.length === 1) {
updateTime({ minute: `0${minute}` });
} else {
const minuteNum = parseInt(minute, 10);
const roundedMinute = Math.round(minuteNum / minuteStep) * minuteStep;
const finalMinute = roundedMinute % 60;
updateTime({ minute: finalMinute.toString().padStart(2, "0") });
}
};
const generateHours = () => {
const minHour = is24Hour ? 0 : 1;
const maxHour = is24Hour ? 23 : 12;
const hours2 = [];
for (let i = minHour; i <= maxHour; i += hourStep) {
hours2.push(i.toString().padStart(2, "0"));
}
return hours2;
};
const generateMinutes = () => {
const minutes2 = [];
for (let i = 0; i < 60; i += minuteStep) {
minutes2.push(i.toString().padStart(2, "0"));
}
return minutes2;
};
const hours = generateHours();
const minutes = generateMinutes();
const handleHourClick = (h) => {
updateTime({ hour: h });
};
const handleMinuteClick = (m) => {
updateTime({ minute: m });
if (hour !== "") {
setPopoverOpen(false);
}
};
return /* @__PURE__ */ jsxs2(
"div",
{
className: classNames2(styles2.container, classes.container),
role: "group",
"aria-labelledby": id ? `${id}-label` : void 0,
children: [
label && /* @__PURE__ */ jsxs2(
"label",
{
id: id ? `${id}-label` : void 0,
className: classNames2(styles2.label, classes.label),
children: [
label,
required && /* @__PURE__ */ jsx2("span", { "aria-hidden": "true", children: "*" })
]
}
),
/* @__PURE__ */ jsxs2(
"div",
{
className: classNames2(styles2.timePicker, classes.timePicker, {
[styles2.disabled]: disabled
}),
children: [
/* @__PURE__ */ jsxs2("div", { className: classNames2(styles2.timeInputs, classes.timeInputs), children: [
/* @__PURE__ */ jsx2(
"input",
{
ref: hourRef,
type: "text",
inputMode: "numeric",
pattern: "[0-9]*",
value: hour,
onChange: handleHourChange,
onKeyDown: handleHourKeyDown,
onBlur: handleHourBlur,
placeholder: hourPlaceholder,
className: classNames2(styles2.timeInput, classes.timeInput),
"aria-label": "Hour",
id: id ? `${id}-hour` : void 0,
disabled,
required
}
),
/* @__PURE__ */ jsx2("span", { className: classNames2(styles2.separator, classes.separator), children: ":" }),
/* @__PURE__ */ jsx2(
"input",
{
ref: minuteRef,
type: "text",
inputMode: "numeric",
pattern: "[0-9]*",
value: minute,
onChange: handleMinuteChange,
onKeyDown: handleMinuteKeyDown,
onBlur: handleMinuteBlur,
placeholder: minutePlaceholder,
className: classNames2(styles2.timeInput, classes.timeInput),
"aria-label": "Minute",
id: id ? `${id}-minute` : void 0,
disabled,
required
}
)
] }),
!is24Hour && /* @__PURE__ */ jsxs2(Fragment, { children: [
/* @__PURE__ */ jsx2(
"span",
{
className: classNames2(styles2.pipe, classes.pipe),
"aria-hidden": "true",
children: "|"
}
),
/* @__PURE__ */ jsxs2(
Select.Root,
{
value: period,
onValueChange: (val) => updateTime({ period: val }),
disabled,
children: [
/* @__PURE__ */ jsxs2(
Select.Trigger,
{
className: classNames2(
styles2.periodSelect,
classes.periodSelect
),
"aria-label": "AM/PM",
tabIndex: 0,
children: [
/* @__PURE__ */ jsx2(Select.Value, { placeholder: "AM/PM" }),
/* @__PURE__ */ jsx2(Select.Icon, { children: /* @__PURE__ */ jsx2(ChevronDownIcon, {}) })
]
}
),
/* @__PURE__ */ jsx2(Select.Portal, { children: /* @__PURE__ */ jsxs2(
Select.Content,
{
className: classNames2(
styles2.selectContent,
classes.selectContent
),
children: [
/* @__PURE__ */ jsx2(
Select.ScrollUpButton,
{
className: classNames2(
styles2.selectScrollButton,
classes.selectScrollButton
)
}
),
/* @__PURE__ */ jsxs2(Select.Viewport, { children: [
/* @__PURE__ */ jsxs2(
Select.Item,
{
value: "AM",
className: classNames2(
styles2.selectItem,
classes.selectItem
),
children: [
/* @__PURE__ */ jsx2(
Select.ItemIndicator,
{
className: classNames2(
styles2.selectIndicator,
classes.selectIndicator
),
children: /* @__PURE__ */ jsx2(CheckIcon, {})
}
),
/* @__PURE__ */ jsx2(Select.ItemText, { children: "AM" })
]
}
),
/* @__PURE__ */ jsxs2(
Select.Item,
{
value: "PM",
className: classNames2(
styles2.selectItem,
classes.selectItem
),
children: [
/* @__PURE__ */ jsx2(
Select.ItemIndicator,
{
className: classNames2(
styles2.selectIndicator,
classes.selectIndicator
),
children: /* @__PURE__ */ jsx2(CheckIcon, {})
}
),
/* @__PURE__ */ jsx2(Select.ItemText, { children: "PM" })
]
}
)
] }),
/* @__PURE__ */ jsx2(
Select.ScrollDownButton,
{
className: classNames2(
styles2.selectScrollButton,
classes.selectScrollButton
)
}
)
]
}
) })
]
}
)
] }),
/* @__PURE__ */ jsxs2(Popover.Root, { open: popoverOpen, onOpenChange: setPopoverOpen, children: [
/* @__PURE__ */ jsx2(
Popover.Trigger,
{
className: classNames2(styles2.timerTrigger, classes.timeTrigger),
disabled,
"data-state": popoverOpen ? "open" : "closed",
tabIndex: 0,
children: /* @__PURE__ */ jsx2(TimerIcon, {})
}
),
/* @__PURE__ */ jsx2(Popover.Portal, { children: /* @__PURE__ */ jsxs2(
Popover.Content,
{
className: classNames2(
styles2.popoverContent,
classes.popoverContent
),
sideOffset: 5,
"data-ignore-outside-click": true,
children: [
/* @__PURE__ */ jsxs2(
"div",
{
className: classNames2(
styles2.popoverColumns,
classes.popoverColumns
),
onKeyDown: (e) => {
e.stopPropagation();
},
children: [
/* @__PURE__ */ jsxs2(
Toolbar.Root,
{
className: classNames2(
styles2.popoverColumn,
classes.popoverColumn
),
"aria-label": "Select Hour",
onKeyDown: (e) => handleKeyDown(e, "hour"),
children: [
/* @__PURE__ */ jsx2(
"div",
{
className: classNames2(
styles2.popoverColumnTitle,
classes.popoverColumnTitle
),
children: popoverColumnHourTitle
}
),
/* @__PURE__ */ jsx2(ScrollArea_default, { children: /* @__PURE__ */ jsx2("div", { className: styles2.popoverColumnContent, children: hours.map((h) => /* @__PURE__ */ jsx2(Toolbar.Button, { asChild: true, value: h, children: /* @__PURE__ */ jsx2(
"button",
{
type: "button",
"data-hour": true,
className: classNames2(
styles2.popoverItem,
hour === h && styles2.popoverActiveItem,
classes.popoverItem,
hour === h && classes.popoverActiveItem
),
onClick: () => handleHourClick(h),
children: h
}
) }, h)) }) })
]
}
),
/* @__PURE__ */ jsxs2(
Toolbar.Root,
{
className: classNames2(
styles2.popoverColumn,
classes.popoverColumn
),
"aria-label": "Select Minute",
onKeyDown: (e) => handleKeyDown(e, "minute"),
children: [
/* @__PURE__ */ jsx2(
"div",
{
className: classNames2(
styles2.popoverColumnTitle,
classes.popoverColumnTitle
),
children: popoverColumnMinuteTitle
}
),
/* @__PURE__ */ jsx2(ScrollArea_default, { children: /* @__PURE__ */ jsx2("div", { className: styles2.popoverColumnContent, children: minutes.map((m) => /* @__PURE__ */ jsx2(Toolbar.Button, { asChild: true, value: m, children: /* @__PURE__ */ jsx2(
"button",
{
type: "button",
"data-minute": true,
className: classNames2(
styles2.popoverItem,
minute === m && styles2.popoverActiveItem,
classes.popoverItem,
minute === m && classes.popoverActiveItem
),
onClick: () => handleMinuteClick(m),
children: m
}
) }, m)) }) })
]
}
)
]
}
),
/* @__PURE__ */ jsx2(Popover.Arrow, {})
]
}
) })
] })
]
}
)
]
}
);
}
// src/index.ts
var index_default = TimePicker;
export {
TimePicker,
index_default as default
};