@neo4j-ndl/react
Version:
React implementation of Neo4j Design System
334 lines • 22 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;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TimeZonePicker = void 0;
const jsx_runtime_1 = require("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/>.
*/
const react_1 = require("@floating-ui/react");
const outline_1 = require("@heroicons/react/24/outline");
const base_1 = require("@neo4j-ndl/base");
const classnames_1 = __importDefault(require("classnames"));
const react_2 = require("react");
const conditional_wrap_1 = require("../conditional-wrap");
const dialog_context_1 = require("../dialog/dialog-context");
const icons_1 = require("../icons");
const theme_1 = require("../theme");
const typography_1 = require("../typography");
const generate_timezone_options_1 = require("./generate-timezone-options");
const timezone_picker_hooks_1 = require("./timezone-picker-hooks");
const TimeZonePicker = (_a) => {
var _b, _c;
var { isDisabled, isFluid, isReadOnly, isRequired, value, label, onChange, className, onError, style, size = 'medium', errorText, htmlAttributes, floatingStrategy, isPortaled: isPortaledProp, referenceDate, mode = 'city', ref } = _a, restProps = __rest(_a, ["isDisabled", "isFluid", "isReadOnly", "isRequired", "value", "label", "onChange", "className", "onError", "style", "size", "errorText", "htmlAttributes", "floatingStrategy", "isPortaled", "referenceDate", "mode", "ref"]);
const [inputValue, setInputValue] = (0, react_2.useState)(value
? (0, generate_timezone_options_1.formatTimeZone)(value, referenceDate)
: (0, generate_timezone_options_1.formatTimeZone)(new Date().toISOString().split('T')[1].split('.')[0], referenceDate));
const [searchQuery, setSearchQuery] = (0, react_2.useState)('');
const listRef = (0, react_2.useRef)(null);
const inputRef = (0, react_2.useRef)(null);
(0, react_2.useImperativeHandle)(ref, () => inputRef.current, []);
const [error, setError] = (0, react_2.useState)(undefined);
const timezoneOptions = (0, react_2.useMemo)(() => {
if (mode === 'utc') {
return (0, generate_timezone_options_1.generateUTCTimeZoneOptions)(referenceDate);
}
else if (mode === 'city') {
return (0, generate_timezone_options_1.generateTimeZoneOptions)(referenceDate);
}
else {
// 'both' mode - combine both lists, excluding UTC from city list to avoid duplication
return [
...(0, generate_timezone_options_1.generateUTCTimeZoneOptions)(referenceDate),
...(0, generate_timezone_options_1.generateTimeZoneOptions)(referenceDate, true), // excludeUTC = true
];
}
}, [referenceDate]);
const { themeClassName } = (0, theme_1.useNeedleTheme)();
const { isPopoverOpen, setIsPopoverOpen } = (0, timezone_picker_hooks_1.useTimeZonePickerPopover)(false);
// Filter options based on search query
const filteredOptions = (0, react_2.useMemo)(() => {
if (!searchQuery) {
return timezoneOptions;
}
const query = searchQuery.toLowerCase();
return timezoneOptions.filter((option) => option.label.toLowerCase().includes(query) ||
option.value.toLowerCase().includes(query));
}, [timezoneOptions, searchQuery]);
// Split options into UTC and city-based sections for 'both' mode
const { utcOptions, cityOptions } = (0, react_2.useMemo)(() => {
if (mode !== 'both') {
return { utcOptions: [], cityOptions: filteredOptions };
}
const utc = [];
const city = [];
filteredOptions.forEach((option) => {
// UTC options are those that start with "UTC" in their value or label
if (option.value.toUpperCase().startsWith('UTC') ||
option.label.toUpperCase().startsWith('UTC')) {
utc.push(option);
}
else {
city.push(option);
}
});
return { cityOptions: city, utcOptions: utc };
}, [filteredOptions, mode]);
const { focusedIndex, setFocusedIndex, handleArrowNavigation } = (0, timezone_picker_hooks_1.useKeyboardNavigation)(filteredOptions.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%' : '16rem';
const errorToShow = errorText !== undefined && errorText !== '' ? errorText : error;
(0, react_2.useEffect)(() => {
if (error !== undefined) {
onError === null || onError === void 0 ? void 0 : onError(error, inputValue);
}
}, [error, inputValue, onError]);
(0, react_2.useEffect)(() => {
setInputValue(value ? (0, generate_timezone_options_1.formatTimeZone)(value, referenceDate) : '');
}, [value, referenceDate]);
(0, react_2.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 = filteredOptions.findIndex((opt) => opt.value === value)) !== null && _b !== void 0 ? _b : 0;
setFocusedIndex(selectedIndex >= 0 ? selectedIndex : 0);
setSearchQuery('');
}, 0);
}
}, [focusedIndex, isPopoverOpen, value, setFocusedIndex, filteredOptions]);
const handleOnClick = (timezone) => {
if (onChange) {
onChange(timezone);
}
setError(undefined);
setInputValue((0, generate_timezone_options_1.formatTimeZone)(timezone, referenceDate));
setIsPopoverOpen(false);
setSearchQuery('');
};
const handleInputChange = (e) => {
const query = e.target.value;
setSearchQuery(query);
setInputValue(query);
// In UTC mode (utc or both), validate custom UTC input
if ((mode === 'utc' || mode === 'both') &&
query.trim().toUpperCase().startsWith('UTC')) {
if ((0, generate_timezone_options_1.isValidUTCFormat)(query.trim())) {
setError(undefined);
}
else {
setError('Invalid UTC format. Use UTC+H:MM or UTC-H:MM');
}
return;
}
// Try to find a matching timezone
const matchingOption = timezoneOptions.find((opt) => opt.label.toLowerCase().includes(query.toLowerCase()) ||
opt.value.toLowerCase().includes(query.toLowerCase()));
if (matchingOption) {
setError(undefined);
}
};
const handleKeyDown = (e) => {
if (isReadOnly === true || isDisabled === true) {
e.preventDefault();
return;
}
if (e.key === 'Escape') {
setIsPopoverOpen(false);
setSearchQuery('');
setInputValue(value ? (0, generate_timezone_options_1.formatTimeZone)(value, referenceDate) : '');
}
setIsPopoverOpen(true);
if (e.key === 'Enter') {
// In UTC mode (utc or both), allow custom UTC input
if ((mode === 'utc' || mode === 'both') &&
inputValue.trim().toUpperCase().startsWith('UTC')) {
const parsedOffset = (0, generate_timezone_options_1.parseCustomUTCOffset)(inputValue.trim());
if (parsedOffset) {
handleOnClick(parsedOffset);
return;
}
}
if (filteredOptions.length > 0) {
const selectedOption = filteredOptions[focusedIndex];
if (selectedOption) {
handleOnClick(selectedOption.value);
}
}
}
else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.preventDefault();
handleArrowNavigation(e.key);
}
};
/** Custom floating ui solution */
const isInsideDialog = (0, dialog_context_1.useIsInsideDialog)();
const isPortaled = isPortaledProp !== null && isPortaledProp !== void 0 ? isPortaledProp : !isInsideDialog;
const containerRef = (0, react_2.useRef)(null);
const { refs, floatingStyles, context } = (0, react_1.useFloating)({
elements: {
reference: containerRef.current,
},
middleware: [
(0, react_1.offset)(10),
(0, react_1.flip)({
fallbackPlacements: ['top'],
fallbackStrategy: 'bestFit',
}),
(0, react_1.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: react_1.autoUpdate,
});
const dismiss = (0, react_1.useDismiss)(context);
const { getReferenceProps, getFloatingProps } = (0, react_1.useInteractions)([dismiss]);
const { styles: transitionStyles } = (0, react_1.useTransitionStyles)(context, {
duration: (_b = Number.parseInt(base_1.tokens.motion.duration.quick)) !== null && _b !== void 0 ? _b : 0,
});
const mergedRef = (0, react_1.useMergeRefs)([refs.setReference, containerRef]);
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", Object.assign({ className: (0, classnames_1.default)('ndl-timezone-picker', className, Object.assign({}, classes)), ref: mergedRef }, getReferenceProps(), { style: {
width: isFluid === true ? '100%' : 'fit-content',
}, children: [(0, jsx_runtime_1.jsxs)("label", { className: "ndl-timezone-picker-label", children: [label !== undefined && ((0, jsx_runtime_1.jsx)(typography_1.Typography, { variant: size === 'large' ? 'body-large' : 'body-medium', children: label })), (0, jsx_runtime_1.jsxs)("div", { className: "ndl-timezone-picker-input-wrapper", style: Object.assign({ width: inputWidth }, style), children: [(0, jsx_runtime_1.jsx)("input", Object.assign({ "aria-label": label, className: "ndl-timezone-picker-input", type: "text", ref: inputRef, value: inputValue, disabled: isDisabled, readOnly: isReadOnly, required: isRequired, onChange: handleInputChange, onKeyDown: handleKeyDown, onClick: (e) => {
if (isReadOnly === true || isDisabled === true) {
return;
}
setIsPopoverOpen(true);
// Select all text on click for easy searching
e.currentTarget.select();
}, onFocus: (e) => {
if (isReadOnly === true || isDisabled === true) {
return;
}
// Select all text on focus for easy searching
e.currentTarget.select();
}, onBlur: (e) => {
var _a;
if (isReadOnly === true || isDisabled === true) {
return;
}
if (((_a = e.relatedTarget) === null || _a === void 0 ? void 0 : _a.classList.contains('ndl-timezone-picker-popover-item')) === true) {
return;
}
// In UTC mode (utc or both), try to parse custom input on blur
if ((mode === 'utc' || mode === 'both') &&
inputValue.trim().toUpperCase().startsWith('UTC')) {
const parsedOffset = (0, generate_timezone_options_1.parseCustomUTCOffset)(inputValue.trim());
if (parsedOffset) {
handleOnClick(parsedOffset);
setError(undefined);
return;
}
}
// Reset to the current value if no valid selection was made
if (value) {
setInputValue((0, generate_timezone_options_1.formatTimeZone)(value, referenceDate));
}
else {
setInputValue('');
}
setSearchQuery('');
setError(undefined);
}, placeholder: mode === 'city'
? 'Select timezone'
: mode === 'utc'
? 'Select or type UTC offset (e.g., UTC+5:30)'
: 'Select timezone or type UTC offset' }, htmlAttributes, restProps)), (0, jsx_runtime_1.jsx)(outline_1.ChevronDownIcon, { className: "ndl-icon-svg ndl-timezone-picker-icon" })] })] }), errorToShow !== undefined && ((0, jsx_runtime_1.jsxs)("div", { className: "ndl-timezone-picker-error-wrapper", children: [(0, jsx_runtime_1.jsx)(icons_1.ExclamationCircleIconSolid, { className: "ndl-timezone-picker-error-icon" }), (0, jsx_runtime_1.jsx)(typography_1.Typography, { variant: size === 'large' ? 'body-medium' : 'body-small', className: "ndl-timezone-picker-error-text", children: errorToShow })] }))] })), context.open && ((0, jsx_runtime_1.jsx)(conditional_wrap_1.ConditionalWrap, { shouldWrap: isPortaled, wrap: (wrapChildren) => ((0, jsx_runtime_1.jsx)(react_1.FloatingPortal, { children: wrapChildren })), children: (0, jsx_runtime_1.jsx)(react_1.FloatingFocusManager, { context: context, modal: false, initialFocus: -1, children: (0, jsx_runtime_1.jsx)("div", Object.assign({ ref: refs.setFloating, className: (0, classnames_1.default)(themeClassName, 'ndl-timezone-picker-popover', Object.assign({}, classes)), style: Object.assign(Object.assign(Object.assign({}, transitionStyles), floatingStyles), { width: isFluid === true
? (_c = containerRef.current) === null || _c === void 0 ? void 0 : _c.clientWidth
: undefined }) }, getFloatingProps(), { children: (0, jsx_runtime_1.jsx)("ul", { ref: listRef, tabIndex: -1, children: mode === 'both' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [utcOptions.length > 0 && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("li", { className: "ndl-timezone-picker-section-header", children: "UTC Offsets" }), utcOptions.map((option, i) => ((0, jsx_runtime_1.jsx)("li", { role: "option", "aria-selected": value === option.value, value: option.value, onClick: (e) => {
e.stopPropagation();
e.preventDefault();
handleOnClick(option.value);
setIsPopoverOpen(false);
}, onMouseDown: (e) => {
e.stopPropagation();
e.preventDefault();
}, tabIndex: -1, className: (0, classnames_1.default)(focusedIndex === i ? 'focused' : '', 'ndl-timezone-picker-popover-item'), onKeyDown: (e) => {
if (e.key === 'Enter') {
e.stopPropagation();
handleOnClick(option.value);
}
}, children: option.label }, option.value)))] })), cityOptions.length > 0 && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("li", { className: "ndl-timezone-picker-section-header", children: "City-Based Timezones" }), cityOptions.map((option, i) => {
const adjustedIndex = i + utcOptions.length;
return ((0, jsx_runtime_1.jsx)("li", { role: "option", "aria-selected": value === option.value, value: option.value, onClick: (e) => {
e.stopPropagation();
e.preventDefault();
handleOnClick(option.value);
setIsPopoverOpen(false);
}, onMouseDown: (e) => {
e.stopPropagation();
e.preventDefault();
}, tabIndex: -1, className: (0, classnames_1.default)(focusedIndex === adjustedIndex ? 'focused' : '', 'ndl-timezone-picker-popover-item'), onKeyDown: (e) => {
if (e.key === 'Enter') {
e.stopPropagation();
handleOnClick(option.value);
}
}, children: option.label }, option.value));
})] })), utcOptions.length === 0 && cityOptions.length === 0 && ((0, jsx_runtime_1.jsx)("li", { className: "ndl-timezone-picker-popover-item ndl-timezone-picker-no-results", children: "No timezones found" }))] })) : ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: filteredOptions.length > 0 ? (filteredOptions.map((option, i) => ((0, jsx_runtime_1.jsx)("li", { role: "option", "aria-selected": value === option.value, value: option.value, onClick: (e) => {
e.stopPropagation();
e.preventDefault();
handleOnClick(option.value);
setIsPopoverOpen(false);
}, onMouseDown: (e) => {
e.stopPropagation();
e.preventDefault();
}, tabIndex: -1, className: (0, classnames_1.default)(focusedIndex === i ? 'focused' : '', 'ndl-timezone-picker-popover-item'), onKeyDown: (e) => {
if (e.key === 'Enter') {
e.stopPropagation();
handleOnClick(option.value);
}
}, children: option.label }, option.value)))) : ((0, jsx_runtime_1.jsx)("li", { className: "ndl-timezone-picker-popover-item ndl-timezone-picker-no-results", children: "No timezones found" })) })) }) })) }) }))] }));
};
exports.TimeZonePicker = TimeZonePicker;
//# sourceMappingURL=TimeZonePicker.js.map