UNPKG

@neo4j-ndl/react

Version:

React implementation of Neo4j Design System

223 lines 10.9 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; /** * * Copyright (c) "Neo4j" * Neo4j Sweden AB [http://neo4j.com] * * This file is part of Neo4j. * * Neo4j is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import cn from 'classnames'; import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react'; import { ClockIconOutline } from '../icons'; import { Popover } from '../popover'; import { TextInput } from '../text-input'; import { useNeedleTheme } from '../theme'; import { generateTimeOptions } from './generate-time-options'; import { NeedleTime } from './needle-time'; import { useKeyboardNavigation, useTimePickerPopover, } from './time-picker-hooks'; export const TimePicker = forwardRef(function TimePicker(props, ref) { var _a; const { format = 'hh:mm', isDisabled, isFluid, isReadOnly, isRequired, selected, timeInterval = 15, label, onChange, className, onError, style, size = 'medium', errorText, htmlAttributes, } = props; const [editedInput, setEditedInput] = useState(selected); const is24Hour = format === 'hh:mm'; const [inputValue, setInputValue] = useState((selected === null || selected === void 0 ? void 0 : selected.toString(is24Hour)) || ''); const listRef = useRef(null); const inputRef = useRef(null); useImperativeHandle(ref, () => inputRef.current, []); const [lastAction, setLastAction] = useState('type'); const [error, setError] = useState(undefined); const timeOptions = useMemo(() => generateTimeOptions(is24Hour, timeInterval), [is24Hour, timeInterval]); const { themeClassName } = useNeedleTheme(); const { isPopoverOpen, setIsPopoverOpen } = useTimePickerPopover(false); const { focusedIndex, setFocusedIndex, handleArrowNavigation } = useKeyboardNavigation(listRef, timeOptions.length); const isValidTime = useCallback((hours, minutes) => { if (hours > 23 || hours < 0) { return false; } if (minutes > 59 || minutes < 0) { return false; } return true; }, []); useEffect(() => { if (error) { onError === null || onError === void 0 ? void 0 : onError(error, NeedleTime.fromString(inputValue)); } }, [error, inputValue, onError]); const handleOnClick = (timeText) => { if (onChange) { onChange(NeedleTime.fromString(timeText)); } setError(undefined); setInputValue(timeText); setEditedInput(NeedleTime.fromString(timeText)); setIsPopoverOpen(false); }; useEffect(() => { setEditedInput(selected); setInputValue((selected === null || selected === void 0 ? void 0 : selected.toString(is24Hour)) || ''); }, [is24Hour, selected]); useEffect(() => { if (isPopoverOpen) { setTimeout(() => { var _a, _b; (_b = (_a = listRef.current) === null || _a === void 0 ? void 0 : _a.children[focusedIndex]) === null || _b === void 0 ? void 0 : _b.scrollIntoView({ block: 'center', inline: 'nearest', }); }, 0); } else { setTimeout(() => { var _a, _b; (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.blur(); const selectedIndex = (_b = timeOptions.indexOf((selected === null || selected === void 0 ? void 0 : selected.toString(is24Hour)) || '')) !== null && _b !== void 0 ? _b : 0; setFocusedIndex(selectedIndex); }, 0); } }, [ focusedIndex, is24Hour, isPopoverOpen, selected, setFocusedIndex, timeOptions, ]); const sizeClasses = { 'ndl-small': size === 'small', 'ndl-medium': size === 'medium', 'ndl-large': size === 'large', }; const newHandleInputChange = (e) => { scrollToTime(e.target.value); setInputValue(e.target.value); setLastAction('type'); const time = NeedleTime.fromString(e.target.value); if (!isValidTime(time.hour, time.minute)) { setError('Invalid time'); } else { setError(undefined); } }; const handleKeyDown = (e) => { var _a; if (e.key === 'Escape') { setIsPopoverOpen(false); } setIsPopoverOpen(true); if (e.key === 'Enter') { if (lastAction === 'type' && inputValue.length >= (is24Hour ? 5 : 6)) { const time = NeedleTime.fromString(inputValue); handleOnClick(time.toString(is24Hour)); } else if (lastAction === 'type' && !inputValue.includes(':')) { const time = NeedleTime.fromString(inputValue); if (isValidTime(time.hour, time.minute)) { handleOnClick(time.toString(is24Hour)); } setIsPopoverOpen(false); } else { handleOnClick(timeOptions[focusedIndex]); } (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.blur(); } else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { e.preventDefault(); setLastAction('arrow'); handleArrowNavigation(e.key); } }; const scrollToTime = (startOfTime) => { const parsedTime = startOfTime.toLowerCase().replace(/[^0-9amp]/g, ''); let index = -1; // Handle AM/PM input const isPM = parsedTime.includes('p'); const isAM = parsedTime.includes('a'); const timeDigits = parsedTime.replace(/[amp]/g, ''); if (timeDigits.length === 1) { // Single digit - assume it's an hour const hour = `0${timeDigits}`; const searchTime = is24Hour ? `${hour}:00` : `${hour}:00 ${isPM ? 'PM' : 'AM'}`; index = timeOptions.indexOf(searchTime); } else { // Find closest matching time const closestTime = timeOptions.find((time) => { const timeWithoutSpecialChars = time .toLowerCase() .replace(/[^0-9amp]/g, ''); if (isPM || isAM) { // If AM/PM specified, match that specifically return timeWithoutSpecialChars.startsWith(parsedTime); } else { // Otherwise just match the numbers return timeWithoutSpecialChars .replace(/[amp]/g, '') .startsWith(timeDigits); } }); index = closestTime ? timeOptions.indexOf(closestTime) : -1; } if (index !== -1) { setFocusedIndex(index); } return index; }; const inputWidth = isFluid ? undefined : is24Hour ? '6rem' : '9rem'; return (_jsxs(_Fragment, { children: [_jsx(TextInput, { onChange: newHandleInputChange, isFluid: isFluid, label: label, placeholder: is24Hour ? 'hh:mm' : 'hh:mm aa', ref: inputRef, value: inputValue, size: size, style: Object.assign({ width: inputWidth }, style), className: cn(className, 'ndl-time-picker'), isRequired: isRequired, isDisabled: isDisabled, isReadOnly: isReadOnly, errorText: errorText || error, rightElement: _jsx(ClockIconOutline, { className: "ndl-icon-svg" }), htmlAttributes: Object.assign({ onClick: () => { if (isReadOnly) return; setIsPopoverOpen(true); }, onBlur: (e) => { var _a; if ((_a = e.relatedTarget) === null || _a === void 0 ? void 0 : _a.classList.contains('ndl-time-picker-popover-item')) { return; } if (inputValue.length < 4) { setError('Invalid time'); return; } const time = NeedleTime.fromString(inputValue); if (!time) { setError('Invalid time'); return; } if (!isValidTime(time.hour, time.minute)) { setError('Invalid time'); } else { setError(undefined); setInputValue(time.toString(is24Hour)); } }, onKeyDown: handleKeyDown, maxLength: is24Hour ? 5 : 8 }, htmlAttributes) }), _jsx(Popover, { isOpen: !isDisabled && isPopoverOpen, onOpenChange: setIsPopoverOpen, anchorElement: inputRef.current, initialFocus: -1, children: _jsx(Popover.Content, { className: cn(themeClassName, 'ndl-time-picker-popover', Object.assign(Object.assign({}, sizeClasses), { 'ndl-fluid': isFluid })), style: { width: isFluid ? (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.clientWidth : undefined, }, children: _jsx("ul", { ref: listRef, tabIndex: -1, children: timeOptions.map((time, i) => (_jsx("li", { role: "option", "aria-selected": (selected === null || selected === void 0 ? void 0 : selected.toString(is24Hour)) === time || (editedInput === null || editedInput === void 0 ? void 0 : editedInput.toString(is24Hour)) === time, value: time, onClick: (e) => { e.stopPropagation(); handleOnClick(time); }, onMouseDown: (e) => e.stopPropagation(), tabIndex: -1, className: cn(focusedIndex === i ? 'focused' : '', 'ndl-time-picker-popover-item'), onKeyDown: (e) => { if (e.key === 'Enter') { e.stopPropagation(); handleOnClick(time); } }, children: time }, i))) }) }) })] })); }); //# sourceMappingURL=TimePicker.js.map