@neo4j-ndl/react
Version:
React implementation of Neo4j Design System
286 lines • 15.8 kB
JavaScript
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } 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 { autoUpdate, flip, FloatingFocusManager, FloatingPortal, offset, shift, useDismiss, useFloating, useInteractions, useMergeRefs, useTransitionStyles, } from '@floating-ui/react';
import { tokens } from '@neo4j-ndl/base';
import classNames from 'classnames';
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
import { ConditionalWrap } from '../conditional-wrap';
import { useIsInsideDialog } from '../dialog/dialog-context';
import { ClockIconOutline, ExclamationCircleIconSolid } from '../icons';
import { useNeedleTheme } from '../theme';
import { Typography } from '../typography';
import { generateTimeOptions } from './generate-time-options';
import { NeedleTime } from './needle-time';
import { useKeyboardNavigation, useTimePickerPopover, } from './time-picker-hooks';
export const TimePicker = (_a) => {
var _b, _c, _d;
var { format = 'hh:mm', isDisabled, isFluid, isReadOnly, isRequired, value, timeInterval = 15, label, onChange, className, onError, style, size = 'medium', errorText, htmlAttributes, floatingStrategy, isPortaled: isPortaledProp, ref } = _a, restProps = __rest(_a, ["format", "isDisabled", "isFluid", "isReadOnly", "isRequired", "value", "timeInterval", "label", "onChange", "className", "onError", "style", "size", "errorText", "htmlAttributes", "floatingStrategy", "isPortaled", "ref"]);
const [editedInput, setEditedInput] = useState(value);
const is24Hour = format === 'hh:mm';
const [inputValue, setInputValue] = useState((_b = value === null || value === void 0 ? void 0 : value.toString(is24Hour)) !== null && _b !== void 0 ? _b : '');
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 classes = {
'ndl-small': size === 'small',
'ndl-medium': size === 'medium',
'ndl-large': size === 'large',
'ndl-error': error !== undefined,
'ndl-fluid': isFluid,
'ndl-disabled': isDisabled,
'ndl-read-only': isReadOnly,
};
const inputWidth = isFluid === true ? '100%' : is24Hour === true ? '6rem' : '9rem';
const errorToShow = errorText !== undefined && errorText !== '' ? errorText : error;
const isValidTime = useCallback((hours, minutes) => {
if (hours > 23 || hours < 0) {
return false;
}
if (minutes > 59 || minutes < 0) {
return false;
}
return true;
}, []);
useEffect(() => {
if (error !== undefined) {
onError === null || onError === void 0 ? void 0 : onError(error, NeedleTime.fromString(inputValue));
}
}, [error, inputValue, onError]);
useEffect(() => {
var _a;
setEditedInput(value);
setInputValue((_a = value === null || value === void 0 ? void 0 : value.toString(is24Hour)) !== null && _a !== void 0 ? _a : '');
}, [is24Hour, value]);
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, _c;
(_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.blur();
const selectedIndex = (_c = timeOptions.indexOf((_b = value === null || value === void 0 ? void 0 : value.toString(is24Hour)) !== null && _b !== void 0 ? _b : '')) !== null && _c !== void 0 ? _c : 0;
setFocusedIndex(selectedIndex);
}, 0);
}
}, [
focusedIndex,
is24Hour,
isPopoverOpen,
value,
setFocusedIndex,
timeOptions,
]);
const handleOnClick = (timeText) => {
if (onChange) {
onChange(NeedleTime.fromString(timeText));
}
setError(undefined);
setInputValue(timeText);
setEditedInput(NeedleTime.fromString(timeText));
setIsPopoverOpen(false);
};
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) => {
if (isReadOnly === true || isDisabled === true) {
e.preventDefault();
return;
}
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]);
}
}
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 !== undefined ? timeOptions.indexOf(closestTime) : -1;
}
if (index !== -1) {
setFocusedIndex(index);
}
return index;
};
/** Custom floating ui solution */
const isInsideDialog = useIsInsideDialog();
const isPortaled = isPortaledProp !== null && isPortaledProp !== void 0 ? isPortaledProp : !isInsideDialog;
const containerRef = useRef(null);
const { refs, floatingStyles, context } = useFloating({
elements: {
reference: containerRef.current,
},
middleware: [
offset(10),
flip({
fallbackPlacements: ['top'],
fallbackStrategy: 'bestFit',
}),
shift({ padding: 5 }),
],
onOpenChange: (open) => {
setIsPopoverOpen(open);
},
open: isDisabled !== true && isReadOnly !== true && isPopoverOpen,
placement: 'bottom',
strategy: floatingStrategy !== null && floatingStrategy !== void 0 ? floatingStrategy : (isInsideDialog ? 'fixed' : 'absolute'),
whileElementsMounted: autoUpdate,
});
const dismiss = useDismiss(context);
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]);
const { styles: transitionStyles } = useTransitionStyles(context, {
duration: (_c = Number.parseInt(tokens.motion.duration.quick)) !== null && _c !== void 0 ? _c : 0,
});
const mergedRef = useMergeRefs([refs.setReference, containerRef]);
return (_jsxs(_Fragment, { children: [_jsxs("div", Object.assign({ className: classNames('ndl-time-picker', className, Object.assign({}, classes)), ref: mergedRef }, getReferenceProps(), { style: {
width: isFluid === true ? '100%' : 'fit-content',
}, children: [_jsxs("label", { className: "ndl-time-picker-label", children: [label !== undefined && (_jsx(Typography, { variant: size === 'large' ? 'body-large' : 'body-medium', children: label })), _jsxs("div", { className: "ndl-time-picker-input-wrapper", style: Object.assign({ width: inputWidth }, style), children: [_jsx("input", Object.assign({ "aria-label": label, className: "ndl-time-picker-input", type: "text", ref: inputRef, value: inputValue, disabled: isDisabled, readOnly: isReadOnly, required: isRequired, onChange: newHandleInputChange, onKeyDown: handleKeyDown, onClick: () => {
if (isReadOnly === true || isDisabled === true) {
return;
}
setIsPopoverOpen(true);
}, onBlur: (e) => {
var _a;
if (isReadOnly === true || isDisabled === true) {
return;
}
if (((_a = e.relatedTarget) === null || _a === void 0 ? void 0 : _a.classList.contains('ndl-time-picker-popover-item')) === true) {
return;
}
if (inputValue.length < 4) {
setError('Invalid time');
return;
}
const time = NeedleTime.fromString(inputValue);
if (time === undefined || time === null) {
setError('Invalid time');
return;
}
if (!isValidTime(time.hour, time.minute)) {
setError('Invalid time');
}
else {
setError(undefined);
setInputValue(time.toString(is24Hour));
}
}, placeholder: format, maxLength: is24Hour ? 5 : 8 }, restProps, htmlAttributes)), _jsx(ClockIconOutline, { className: "ndl-icon-svg ndl-time-picker-icon" })] })] }), errorToShow !== undefined && (_jsxs("div", { className: "ndl-time-picker-error-wrapper", children: [_jsx(ExclamationCircleIconSolid, { className: "ndl-time-picker-error-icon" }), _jsx(Typography, { variant: size === 'large' ? 'body-medium' : 'body-small', className: "ndl-time-picker-error-text", children: errorToShow })] }))] })), context.open && (_jsx(ConditionalWrap, { shouldWrap: isPortaled, wrap: (wrapChildren) => (_jsx(FloatingPortal, { children: wrapChildren })), children: _jsx(FloatingFocusManager, { context: context, modal: false, initialFocus: -1, children: _jsx("div", Object.assign({ ref: refs.setFloating, className: classNames(themeClassName, 'ndl-time-picker-popover', Object.assign({}, classes)), style: Object.assign(Object.assign(Object.assign({}, transitionStyles), floatingStyles), { width: isFluid === true
? (_d = containerRef.current) === null || _d === void 0 ? void 0 : _d.clientWidth
: undefined }) }, getFloatingProps(), { children: _jsx("ul", { ref: listRef, tabIndex: -1, children: timeOptions.map((time, i) => (_jsx("li", { role: "option", "aria-selected": (value === null || value === void 0 ? void 0 : value.toString(is24Hour)) === time ||
(editedInput === null || editedInput === void 0 ? void 0 : editedInput.toString(is24Hour)) === time, value: time, onClick: (e) => {
e.stopPropagation();
e.preventDefault();
handleOnClick(time);
setIsPopoverOpen(false);
}, onMouseDown: (e) => {
e.stopPropagation();
e.preventDefault();
}, tabIndex: -1, className: classNames(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