UNPKG

react-accessible-time-picker

Version:

A simple and customizable time picker component for React

531 lines (527 loc) 21 kB
// 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 };