@cwellt_software/cwellt-reactjs-lib 1.4.11 → 1.4.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs.js +1039 -13
- package/dist/index.css +2 -2
- package/dist/index.d.ts +253 -12
- package/dist/index.es.js +1036 -15
- package/dist/src/components/control/action/search/CwSearchAsync.d.ts +105 -0
- package/dist/src/components/control/action/search/CwSearchAsync.d.ts.map +1 -0
- package/dist/src/components/control/input/new-dates/CwDatePickerTemporal.d.ts +10 -1
- package/dist/src/components/control/input/new-dates/CwDatePickerTemporal.d.ts.map +1 -1
- package/dist/src/components/control/input/new-dates/CwDateRangePickerTemporal.d.ts +75 -0
- package/dist/src/components/control/input/new-dates/CwDateRangePickerTemporal.d.ts.map +1 -0
- package/dist/src/components/control/input/new-dates/CwDateTimePickerTemporal.d.ts +52 -0
- package/dist/src/components/control/input/new-dates/CwDateTimePickerTemporal.d.ts.map +1 -0
- package/dist/src/components/control/input/new-dates/CwTimePicker.d.ts.map +1 -1
- package/dist/src/components/control/input/new-dates/dateRangePresetsTemporal.d.ts +16 -0
- package/dist/src/components/control/input/new-dates/dateRangePresetsTemporal.d.ts.map +1 -0
- package/dist/src/components/control/input/new-dates/domain/DateTimePickerState.d.ts +78 -0
- package/dist/src/components/control/input/new-dates/domain/DateTimePickerState.d.ts.map +1 -0
- package/dist/src/components/control/input/new-dates/useCwDateTimePickerTemporal.d.ts +29 -0
- package/dist/src/components/control/input/new-dates/useCwDateTimePickerTemporal.d.ts.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/playground/pages/DatePickerTemporalPage.d.ts.map +1 -1
- package/dist/src/playground/pages/SelectorsPage.d.ts.map +1 -1
- package/dist/test/components/control/input/new-dates/DateTimePickerState.test.d.ts +2 -0
- package/dist/test/components/control/input/new-dates/DateTimePickerState.test.d.ts.map +1 -0
- package/dist/test/components/control/input/new-dates/dateRangePresetsTemporal.test.d.ts +2 -0
- package/dist/test/components/control/input/new-dates/dateRangePresetsTemporal.test.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.es.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import React__default, { useState, useRef, useEffect, useCallback, useLayoutEffect, createContext, useContext, cloneElement, useMemo, forwardRef, useImperativeHandle, createElement, memo, Fragment as Fragment$1, Component } from 'react';
|
|
3
|
+
import React__default, { useState, useRef, useEffect, useCallback, useLayoutEffect, createContext, useContext, cloneElement, useMemo, forwardRef, useImperativeHandle, createElement, useId, memo, Fragment as Fragment$1, Component } from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { createRoot } from 'react-dom/client';
|
|
6
6
|
import Draggable from 'react-draggable';
|
|
@@ -5017,6 +5017,265 @@ function CwSearchInput(optionsProps) {
|
|
|
5017
5017
|
return (jsxs("div", { className: `cw-search-input ${className || ""}`, style: style, id: id, ...otherProps, "data-direction": direction, children: [jsxs(CwAlign, { ...alignProps, children: [labelProps && (jsx(CwLabel, { ...labelProps, children: labelProps.text })), jsxs("div", { className: "cw-search-input-wrapper", ref: wrapperRef, children: [jsx("input", { type: "text", value: searchText, onChange: handleInputChange, onFocus: handleInputFocus, onBlur: handleInputBlur, placeholder: placeholder, className: "cw-input-search", disabled: disabled }), isLoading && (jsx("div", { className: "cw-search-input-loading", children: jsx(CwIcon, { iconId: "spinner" }) })), jsx("div", { className: "cw-search-input-icons", children: searchText && !disabled && !isLoading ? (jsx(CwButton, { type: "button", onClick: handleClearClick, "aria-label": "Clear search", variant: "icon", icon: "close", color: "neutral" })) : (iconProps ? jsx(CwIcon, { ...iconProps }) : jsx(CwIcon, { iconId: "search" })) })] })] }), renderPanel(jsx("div", { ref: panelRef, className: dropdownStyles.dropdown, style: panelStyle, children: jsx("ul", { children: filteredOptions.map((option) => (jsx("li", { className: dropdownStyles.option, onClick: () => handleOptionSelect(option), onMouseDown: (e) => e.preventDefault(), children: renderOption ? renderOption(option) : option.description }, option.id))) }) }))] }));
|
|
5018
5018
|
}
|
|
5019
5019
|
|
|
5020
|
+
/**
|
|
5021
|
+
* Generic API-backed search input. Fetches matching options from a caller-supplied `onSearch`
|
|
5022
|
+
* callback as the user types (debounced), renders them in a portal-based dropdown with keyboard
|
|
5023
|
+
* navigation, and commits the selected id via `onChange`.
|
|
5024
|
+
*
|
|
5025
|
+
* All domain logic — API URLs, auth/config, display formatting, provider/secondary lookups — lives
|
|
5026
|
+
* in the caller's callbacks, so this component stays application-agnostic. It is the base for thin
|
|
5027
|
+
* wrappers such as `FindAirport` / `FindCrewmember`.
|
|
5028
|
+
*
|
|
5029
|
+
* Improvements over the legacy `CwFindAirport` / `CwFindCrewmember`: an `AbortController` cancels
|
|
5030
|
+
* superseded in-flight requests (fixes out-of-order results), and `minChars` / `debounceMs` are
|
|
5031
|
+
* configurable.
|
|
5032
|
+
*
|
|
5033
|
+
* @example
|
|
5034
|
+
* <CwSearchAsync
|
|
5035
|
+
* onSearch={(text, signal) => repo.search(text, signal)}
|
|
5036
|
+
* onChange={(id) => setSelectedId(id)}
|
|
5037
|
+
* labelProps={{ text: 'Airport' }}
|
|
5038
|
+
* icon={{ iconId: 'control-tower' }}
|
|
5039
|
+
* />
|
|
5040
|
+
*/
|
|
5041
|
+
const CwSearchAsync = ({ onSearch, onChange, value, onResolveInitial, onResolveSelection, icon, placeholder = "Search…", minChars = 2, debounceMs = 300, disabled = false, required = false, initialDisplayText, searchTrigger, labelProps, alignProps, width, className = "", }) => {
|
|
5042
|
+
// State
|
|
5043
|
+
const [inputValue, setInputValue] = useState("");
|
|
5044
|
+
const [options, setOptions] = useState([]);
|
|
5045
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
5046
|
+
const [isInitialLoading, setIsInitialLoading] = useState(false);
|
|
5047
|
+
const [showDropdown, setShowDropdown] = useState(false);
|
|
5048
|
+
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
5049
|
+
const [tooltipText, setTooltipText] = useState("");
|
|
5050
|
+
// Refs
|
|
5051
|
+
const inputRef = useRef(null);
|
|
5052
|
+
const wrapperRef = useRef(null);
|
|
5053
|
+
const debounceRef = useRef();
|
|
5054
|
+
const abortRef = useRef(null);
|
|
5055
|
+
const lastQueryRef = useRef("");
|
|
5056
|
+
const didMountRef = useRef(false);
|
|
5057
|
+
const lastResolvedValueRef = useRef(undefined);
|
|
5058
|
+
const initialResolveAbortRef = useRef(null);
|
|
5059
|
+
// Stable ids wiring the combobox input to its listbox popup for assistive tech.
|
|
5060
|
+
const listboxId = useId();
|
|
5061
|
+
const optionId = (index) => `${listboxId}-opt-${index}`;
|
|
5062
|
+
// Cancel a pending initial-value resolve when the user takes over (typing / clearing / reset),
|
|
5063
|
+
// so a slow resolve can't overwrite newer input and the loading flag never sticks.
|
|
5064
|
+
const cancelInitialResolve = useCallback(() => {
|
|
5065
|
+
initialResolveAbortRef.current?.abort();
|
|
5066
|
+
initialResolveAbortRef.current = null;
|
|
5067
|
+
setIsInitialLoading(false);
|
|
5068
|
+
}, []);
|
|
5069
|
+
const { panelRef, panelStyle, renderPanel } = useDropdownPortal({
|
|
5070
|
+
anchorRef: wrapperRef,
|
|
5071
|
+
isOpen: showDropdown && options.length > 0,
|
|
5072
|
+
onClose: () => { setShowDropdown(false); setHighlightedIndex(-1); },
|
|
5073
|
+
});
|
|
5074
|
+
// Core search runner — aborts superseded requests and drops stale responses.
|
|
5075
|
+
const runSearch = useCallback(async (text) => {
|
|
5076
|
+
abortRef.current?.abort();
|
|
5077
|
+
if (text.length < minChars) {
|
|
5078
|
+
setOptions([]);
|
|
5079
|
+
setShowDropdown(false);
|
|
5080
|
+
setHighlightedIndex(-1);
|
|
5081
|
+
return;
|
|
5082
|
+
}
|
|
5083
|
+
const controller = new AbortController();
|
|
5084
|
+
abortRef.current = controller;
|
|
5085
|
+
lastQueryRef.current = text;
|
|
5086
|
+
setIsLoading(true);
|
|
5087
|
+
try {
|
|
5088
|
+
const results = await onSearch(text, controller.signal);
|
|
5089
|
+
if (controller.signal.aborted)
|
|
5090
|
+
return; // superseded → drop
|
|
5091
|
+
setOptions(results);
|
|
5092
|
+
setShowDropdown(results.length > 0); // empty → close silently (dark cockpit)
|
|
5093
|
+
setHighlightedIndex(-1);
|
|
5094
|
+
}
|
|
5095
|
+
catch (error) {
|
|
5096
|
+
if (controller.signal.aborted)
|
|
5097
|
+
return; // aborts reject with AbortError → ignore
|
|
5098
|
+
console.error("CwSearchAsync onSearch failed:", error);
|
|
5099
|
+
setOptions([]);
|
|
5100
|
+
setShowDropdown(false);
|
|
5101
|
+
}
|
|
5102
|
+
finally {
|
|
5103
|
+
if (abortRef.current === controller) {
|
|
5104
|
+
setIsLoading(false);
|
|
5105
|
+
abortRef.current = null;
|
|
5106
|
+
}
|
|
5107
|
+
}
|
|
5108
|
+
}, [minChars, onSearch]);
|
|
5109
|
+
// Keep a stable handle to the latest runSearch for the searchTrigger effect.
|
|
5110
|
+
const runSearchRef = useRef(runSearch);
|
|
5111
|
+
useEffect(() => { runSearchRef.current = runSearch; });
|
|
5112
|
+
const debouncedSearch = useCallback((text) => {
|
|
5113
|
+
if (debounceRef.current)
|
|
5114
|
+
window.clearTimeout(debounceRef.current);
|
|
5115
|
+
// Abort eagerly on every keystroke so a fast typist never has two live fetches.
|
|
5116
|
+
abortRef.current?.abort();
|
|
5117
|
+
debounceRef.current = window.setTimeout(() => runSearch(text), debounceMs);
|
|
5118
|
+
}, [runSearch, debounceMs]);
|
|
5119
|
+
const handleInputChange = (e) => {
|
|
5120
|
+
const newValue = e.target.value;
|
|
5121
|
+
setInputValue(newValue);
|
|
5122
|
+
if (newValue !== inputValue) {
|
|
5123
|
+
cancelInitialResolve();
|
|
5124
|
+
debouncedSearch(newValue);
|
|
5125
|
+
}
|
|
5126
|
+
};
|
|
5127
|
+
const handleOptionSelect = async (optionId) => {
|
|
5128
|
+
const picked = options.find(o => o.id === optionId);
|
|
5129
|
+
if (!picked)
|
|
5130
|
+
return;
|
|
5131
|
+
setShowDropdown(false);
|
|
5132
|
+
let committed = picked;
|
|
5133
|
+
if (onResolveSelection) {
|
|
5134
|
+
abortRef.current?.abort();
|
|
5135
|
+
const controller = new AbortController();
|
|
5136
|
+
abortRef.current = controller;
|
|
5137
|
+
try {
|
|
5138
|
+
const resolved = await onResolveSelection(picked, controller.signal);
|
|
5139
|
+
if (controller.signal.aborted)
|
|
5140
|
+
return;
|
|
5141
|
+
committed = resolved;
|
|
5142
|
+
setOptions([resolved]);
|
|
5143
|
+
}
|
|
5144
|
+
catch (error) {
|
|
5145
|
+
if (controller.signal.aborted)
|
|
5146
|
+
return;
|
|
5147
|
+
console.error("CwSearchAsync onResolveSelection failed:", error);
|
|
5148
|
+
return; // abort the commit, mirroring the legacy provider-airport behaviour
|
|
5149
|
+
}
|
|
5150
|
+
finally {
|
|
5151
|
+
if (abortRef.current === controller)
|
|
5152
|
+
abortRef.current = null;
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
setInputValue(committed.inputLabel ?? committed.label);
|
|
5156
|
+
setTooltipText(committed.label);
|
|
5157
|
+
onChange(committed.id);
|
|
5158
|
+
};
|
|
5159
|
+
const handleKeyDown = (e) => {
|
|
5160
|
+
if (!showDropdown || options.length === 0)
|
|
5161
|
+
return;
|
|
5162
|
+
switch (e.key) {
|
|
5163
|
+
case "ArrowDown":
|
|
5164
|
+
e.preventDefault();
|
|
5165
|
+
setHighlightedIndex(prev => (prev < options.length - 1 ? prev + 1 : 0));
|
|
5166
|
+
break;
|
|
5167
|
+
case "ArrowUp":
|
|
5168
|
+
e.preventDefault();
|
|
5169
|
+
setHighlightedIndex(prev => (prev > 0 ? prev - 1 : options.length - 1));
|
|
5170
|
+
break;
|
|
5171
|
+
case "Enter":
|
|
5172
|
+
e.preventDefault();
|
|
5173
|
+
if (highlightedIndex >= 0 && highlightedIndex < options.length) {
|
|
5174
|
+
handleOptionSelect(options[highlightedIndex].id);
|
|
5175
|
+
}
|
|
5176
|
+
break;
|
|
5177
|
+
case "Escape":
|
|
5178
|
+
setShowDropdown(false);
|
|
5179
|
+
setHighlightedIndex(-1);
|
|
5180
|
+
break;
|
|
5181
|
+
}
|
|
5182
|
+
};
|
|
5183
|
+
const handleInputFocus = () => {
|
|
5184
|
+
if (options.length > 0)
|
|
5185
|
+
setShowDropdown(true);
|
|
5186
|
+
};
|
|
5187
|
+
const handleClear = () => {
|
|
5188
|
+
if (debounceRef.current)
|
|
5189
|
+
window.clearTimeout(debounceRef.current);
|
|
5190
|
+
abortRef.current?.abort();
|
|
5191
|
+
cancelInitialResolve();
|
|
5192
|
+
setInputValue("");
|
|
5193
|
+
setTooltipText("");
|
|
5194
|
+
setOptions([]);
|
|
5195
|
+
setShowDropdown(false);
|
|
5196
|
+
lastQueryRef.current = "";
|
|
5197
|
+
onChange(null);
|
|
5198
|
+
inputRef.current?.focus();
|
|
5199
|
+
};
|
|
5200
|
+
// Resolve a preselected value to display text.
|
|
5201
|
+
useEffect(() => {
|
|
5202
|
+
if (value == null)
|
|
5203
|
+
return;
|
|
5204
|
+
if (initialDisplayText) {
|
|
5205
|
+
setInputValue(initialDisplayText);
|
|
5206
|
+
setTooltipText(initialDisplayText);
|
|
5207
|
+
lastResolvedValueRef.current = value;
|
|
5208
|
+
return;
|
|
5209
|
+
}
|
|
5210
|
+
if (!onResolveInitial)
|
|
5211
|
+
return;
|
|
5212
|
+
if (lastResolvedValueRef.current === value)
|
|
5213
|
+
return; // already resolved this value
|
|
5214
|
+
const controller = new AbortController();
|
|
5215
|
+
initialResolveAbortRef.current = controller;
|
|
5216
|
+
setIsInitialLoading(true);
|
|
5217
|
+
onResolveInitial(value, controller.signal)
|
|
5218
|
+
.then(opt => {
|
|
5219
|
+
if (controller.signal.aborted)
|
|
5220
|
+
return;
|
|
5221
|
+
lastResolvedValueRef.current = value; // mark resolved only on success
|
|
5222
|
+
setOptions([opt]);
|
|
5223
|
+
setInputValue(opt.inputLabel ?? opt.label);
|
|
5224
|
+
setTooltipText(opt.label);
|
|
5225
|
+
})
|
|
5226
|
+
.catch(error => {
|
|
5227
|
+
if (!controller.signal.aborted)
|
|
5228
|
+
console.error("CwSearchAsync onResolveInitial failed:", error);
|
|
5229
|
+
})
|
|
5230
|
+
.finally(() => {
|
|
5231
|
+
// Clear the spinner only if this resolve is still the current one (not superseded/cancelled).
|
|
5232
|
+
if (initialResolveAbortRef.current === controller) {
|
|
5233
|
+
setIsInitialLoading(false);
|
|
5234
|
+
initialResolveAbortRef.current = null;
|
|
5235
|
+
}
|
|
5236
|
+
});
|
|
5237
|
+
return () => controller.abort();
|
|
5238
|
+
}, [value, initialDisplayText, onResolveInitial]);
|
|
5239
|
+
// Reset when the controlled value is cleared by the parent.
|
|
5240
|
+
useEffect(() => {
|
|
5241
|
+
if (value == null) {
|
|
5242
|
+
if (debounceRef.current)
|
|
5243
|
+
window.clearTimeout(debounceRef.current);
|
|
5244
|
+
abortRef.current?.abort();
|
|
5245
|
+
cancelInitialResolve();
|
|
5246
|
+
setInputValue("");
|
|
5247
|
+
setTooltipText("");
|
|
5248
|
+
setOptions([]);
|
|
5249
|
+
setShowDropdown(false);
|
|
5250
|
+
setHighlightedIndex(-1);
|
|
5251
|
+
lastQueryRef.current = "";
|
|
5252
|
+
lastResolvedValueRef.current = undefined;
|
|
5253
|
+
}
|
|
5254
|
+
}, [value, cancelInitialResolve]);
|
|
5255
|
+
// Re-run the last query when searchTrigger changes (skip first mount).
|
|
5256
|
+
useEffect(() => {
|
|
5257
|
+
if (!didMountRef.current) {
|
|
5258
|
+
didMountRef.current = true;
|
|
5259
|
+
return;
|
|
5260
|
+
}
|
|
5261
|
+
if (searchTrigger === undefined)
|
|
5262
|
+
return;
|
|
5263
|
+
if (lastQueryRef.current)
|
|
5264
|
+
runSearchRef.current(lastQueryRef.current);
|
|
5265
|
+
}, [searchTrigger]);
|
|
5266
|
+
// Cleanup on unmount.
|
|
5267
|
+
useEffect(() => () => {
|
|
5268
|
+
if (debounceRef.current)
|
|
5269
|
+
window.clearTimeout(debounceRef.current);
|
|
5270
|
+
abortRef.current?.abort();
|
|
5271
|
+
}, []);
|
|
5272
|
+
const direction = alignProps?.flexDirection || "row";
|
|
5273
|
+
return (jsxs("div", { className: `cw-search-input ${className}`, style: {
|
|
5274
|
+
...(width ? { width } : {}),
|
|
5275
|
+
...(labelProps?.labelWidth ? { "--label-width": labelProps.labelWidth } : {}),
|
|
5276
|
+
}, "data-direction": direction, children: [jsxs(CwAlign, { ...alignProps, itemProp: required ? "required" : "", children: [labelProps && (jsx(CwLabel, { ...labelProps, children: labelProps.text })), jsxs("div", { className: "cw-search-input-wrapper", ref: wrapperRef, children: [jsx("input", { ref: inputRef, type: "text", value: inputValue, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: handleInputFocus, placeholder: isInitialLoading ? "Loading…" : placeholder, disabled: disabled, required: required, autoComplete: "off", role: "combobox", "aria-haspopup": "listbox", "aria-expanded": showDropdown, "aria-controls": listboxId, "aria-activedescendant": highlightedIndex >= 0 ? optionId(highlightedIndex) : undefined, title: tooltipText }), (isLoading || isInitialLoading) && (jsx("div", { className: "cw-search-input-loading", children: jsx(CwIcon, { iconId: "spinner" }) })), jsx("div", { className: "cw-search-input-icons", children: inputValue && !disabled && !isInitialLoading ? (jsx(CwButton, { type: "button", onClick: handleClear, "aria-label": "Clear selection", variant: "icon", icon: "close", color: "neutral" })) : (icon ? jsx(CwIcon, { ...icon }) : jsx(CwIcon, { iconId: "search" })) })] })] }), renderPanel(jsx("div", { ref: panelRef, id: listboxId, className: dropdownStyles.dropdown, style: panelStyle, role: "listbox", children: jsx("ul", { children: options.map((option, index) => (jsx("li", { id: optionId(index), className: `${dropdownStyles.option}${index === highlightedIndex ? ` ${dropdownStyles.optionFocused}` : ""}`, onClick: () => handleOptionSelect(option.id), onMouseDown: (e) => e.preventDefault(), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": index === highlightedIndex, children: option.subLabel ? (jsxs("span", { className: "cw-search-async-option-lines", children: [jsx("span", { children: option.label }), jsx("span", { className: "cw-search-async-option-sub", children: option.subLabel })] })) : (option.label) }, option.id))) }) }))] }));
|
|
5277
|
+
};
|
|
5278
|
+
|
|
5020
5279
|
var styles$5 = {"menu":"cw-context-menu-module__menu__tXmun","item":"cw-context-menu-module__item__-ko8L","arrow":"cw-context-menu-module__arrow__LHZmQ"};
|
|
5021
5280
|
|
|
5022
5281
|
const ARROW_SIZE = 8;
|
|
@@ -6233,7 +6492,10 @@ function CwTimePicker({ value, onChange, interval = 15, minTime, maxTime, labelP
|
|
|
6233
6492
|
const handleClear = (e) => {
|
|
6234
6493
|
e.stopPropagation();
|
|
6235
6494
|
onChange(undefined);
|
|
6236
|
-
|
|
6495
|
+
// Keep showing the current value prop: a controlled parent that accepts the
|
|
6496
|
+
// clear empties it via the value-sync effect, while a parent that rejects or
|
|
6497
|
+
// transforms the clear (e.g. resets to a default) stays visually in sync.
|
|
6498
|
+
setInputValue(value ?? "");
|
|
6237
6499
|
};
|
|
6238
6500
|
const handleInputClick = () => {
|
|
6239
6501
|
if (!disabled) {
|
|
@@ -6261,15 +6523,18 @@ function CwTimePicker({ value, onChange, interval = 15, minTime, maxTime, labelP
|
|
|
6261
6523
|
const [hours, minutes] = newValue.split(":");
|
|
6262
6524
|
const h = parseInt(hours, 10);
|
|
6263
6525
|
const m = parseInt(minutes, 10);
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6526
|
+
const formattedTime = `${hours.padStart(2, "0")}:${minutes.padStart(2, "0")}`;
|
|
6527
|
+
// Check well-formedness and min/max time restrictions
|
|
6528
|
+
const isValid = h >= 0 && h <= 23 && m >= 0 && m <= 59 &&
|
|
6529
|
+
(!minTime || formattedTime >= minTime) &&
|
|
6530
|
+
(!maxTime || formattedTime <= maxTime);
|
|
6531
|
+
if (isValid) {
|
|
6532
|
+
onChange(formattedTime);
|
|
6533
|
+
setInputValue(formattedTime);
|
|
6534
|
+
}
|
|
6535
|
+
else {
|
|
6536
|
+
// Completed entry is invalid/out of range: snap back to the previous value
|
|
6537
|
+
setInputValue(value ?? "");
|
|
6273
6538
|
}
|
|
6274
6539
|
}
|
|
6275
6540
|
else if (newValue === "") {
|
|
@@ -6983,9 +7248,10 @@ function formatPlainDate(pd, fmt) {
|
|
|
6983
7248
|
.replace("dd", String(pd.day).padStart(2, "0"));
|
|
6984
7249
|
}
|
|
6985
7250
|
|
|
6986
|
-
function CwDatePickerTemporal({ value, onChange, minDate, maxDate, disabledDates, disabledMatcher, defaultMonth, labelProps, alignProps, placeholder = "Select a date", displayFormat = "dd.MM.yyyy", disabled, required, className, showClear = true, popupPosition = "left-bottom", numberOfMonths = 1, showTodayButton = false, todayLabel = "Today", feedback, }) {
|
|
7251
|
+
function CwDatePickerTemporal({ value, onChange, minDate, maxDate, disabledDates, disabledMatcher, defaultMonth, labelProps, alignProps, placeholder = "Select a date", displayFormat = "dd.MM.yyyy", disabled, required, className, showClear = true, popupPosition = "left-bottom", numberOfMonths = 1, showTodayButton = false, locale = enGB, todayLabel = "Today", yearRangeStart, yearRangeEnd, feedback, }) {
|
|
6987
7252
|
const [isOpen, setIsOpen] = useState(false);
|
|
6988
|
-
|
|
7253
|
+
// Initialize with the formatted value so the input is controlled from the first render
|
|
7254
|
+
const [inputValue, setInputValue] = useState(() => value ? formatPlainDate(value, displayFormat) : "");
|
|
6989
7255
|
const containerRef = useRef(null);
|
|
6990
7256
|
const feedbackMessages = feedback
|
|
6991
7257
|
? Array.isArray(feedback) ? feedback : [feedback]
|
|
@@ -7072,6 +7338,19 @@ function CwDatePickerTemporal({ value, onChange, minDate, maxDate, disabledDates
|
|
|
7072
7338
|
setIsOpen(false);
|
|
7073
7339
|
}
|
|
7074
7340
|
}, [minDate, maxDate, onChange]);
|
|
7341
|
+
// Derive start/end months for the year dropdown range, guarding against inverted yearRange props
|
|
7342
|
+
const dropdownStartMonth = useMemo(() => {
|
|
7343
|
+
const inferredStart = minDate ? minDate.year : Temporal.Now.plainDateISO().year - 10;
|
|
7344
|
+
const inferredEnd = yearRangeEnd ?? (maxDate ? maxDate.year : Temporal.Now.plainDateISO().year + 5);
|
|
7345
|
+
const year = Math.min(yearRangeStart ?? inferredStart, inferredEnd);
|
|
7346
|
+
return new Date(year, 0, 1);
|
|
7347
|
+
}, [yearRangeStart, yearRangeEnd, minDate, maxDate]);
|
|
7348
|
+
const dropdownEndMonth = useMemo(() => {
|
|
7349
|
+
const inferredEnd = maxDate ? maxDate.year : Temporal.Now.plainDateISO().year + 5;
|
|
7350
|
+
const inferredStart = yearRangeStart ?? (minDate ? minDate.year : Temporal.Now.plainDateISO().year - 10);
|
|
7351
|
+
const year = Math.max(yearRangeEnd ?? inferredEnd, inferredStart);
|
|
7352
|
+
return new Date(year, 11, 31);
|
|
7353
|
+
}, [yearRangeStart, yearRangeEnd, minDate, maxDate]);
|
|
7075
7354
|
// Memoize disabled days (convert PlainDate[] to Date[] for DayPicker)
|
|
7076
7355
|
const disabledDays = useMemo(() => [
|
|
7077
7356
|
...(disabledDates?.map(plainDateToJsDate) || []),
|
|
@@ -7083,13 +7362,755 @@ function CwDatePickerTemporal({ value, onChange, minDate, maxDate, disabledDates
|
|
|
7083
7362
|
const defaultMonthJsDate = defaultMonth
|
|
7084
7363
|
? new Date(defaultMonth.year, defaultMonth.month - 1, 1)
|
|
7085
7364
|
: selectedJsDate;
|
|
7086
|
-
return (jsxs("div", { ref: containerRef, className: `cw-datepicker ${className || ""}`, children: [jsxs(CwAlign, { ...alignProps, itemProp: required ? "required" : "", children: [labelProps && (jsx(CwLabel, { ...labelProps, children: labelProps.text })), jsxs("div", { ref: wrapperRef, className: styles$4.pickerWrapper, children: [jsx("input", { ref: inputRef, type: "text", value: inputValue, placeholder: placeholder, onChange: handleInputChange, onBlur: handleInputBlur, onClick: handleInputClick, onKeyDown: handleInputKeyDown, disabled: disabled, required: required }), jsx("div", { className: styles$4.pickerIcons, children: showClear && value && !disabled ? (jsx(CwButton, { type: "button", variant: "icon", color: "neutral", icon: "close", onClick: handleClear, tabIndex: -1, "aria-label": "Clear date" })) : (jsx(CwIcon, { iconId: "calendar" })) }), renderPopup(jsxs("div", { ref: popupRef, className: styles$4.pickerPopup, style: popupStyle, children: [jsx(DayPicker, { mode: "single", selected: selectedJsDate, defaultMonth: defaultMonthJsDate, onSelect: handleDaySelect, disabled: disabledDays, numberOfMonths: numberOfMonths, modifiers: {
|
|
7365
|
+
return (jsxs("div", { ref: containerRef, className: `cw-datepicker ${className || ""}`, children: [jsxs(CwAlign, { ...alignProps, itemProp: required ? "required" : "", children: [labelProps && (jsx(CwLabel, { ...labelProps, children: labelProps.text })), jsxs("div", { ref: wrapperRef, className: styles$4.pickerWrapper, children: [jsx("input", { ref: inputRef, type: "text", value: inputValue, placeholder: placeholder, onChange: handleInputChange, onBlur: handleInputBlur, onClick: handleInputClick, onKeyDown: handleInputKeyDown, disabled: disabled, required: required }), jsx("div", { className: styles$4.pickerIcons, children: showClear && value && !disabled ? (jsx(CwButton, { type: "button", variant: "icon", color: "neutral", icon: "close", onClick: handleClear, tabIndex: -1, "aria-label": "Clear date" })) : (jsx(CwIcon, { iconId: "calendar" })) }), renderPopup(jsxs("div", { ref: popupRef, className: styles$4.pickerPopup, style: popupStyle, children: [jsx(DayPicker, { mode: "single", selected: selectedJsDate, defaultMonth: defaultMonthJsDate, onSelect: handleDaySelect, disabled: disabledDays, numberOfMonths: numberOfMonths, locale: locale, captionLayout: "dropdown", startMonth: dropdownStartMonth, endMonth: dropdownEndMonth, modifiers: {
|
|
7087
7366
|
today: new Date(),
|
|
7088
7367
|
}, modifiersClassNames: {
|
|
7089
7368
|
today: "rdp-day-today",
|
|
7090
7369
|
} }), showTodayButton && (jsx("footer", { className: "cw-flex-row cw-align-right-center", children: jsx(CwButton, { type: "button", variant: "outline", onClick: handleTodayClick, text: todayLabel }) }))] }))] })] }), feedbackMessages.map((feedbackItem, index) => (jsx("p", { className: "cw-input-info", "data-color": feedbackItem.type, children: feedbackItem.message }, index)))] }));
|
|
7091
7370
|
}
|
|
7092
7371
|
|
|
7372
|
+
// DateTimePickerState - immutable domain model for the Temporal date-time picker.
|
|
7373
|
+
//
|
|
7374
|
+
// Invariants:
|
|
7375
|
+
// - `time` is NEVER null: this is a date-TIME picker, so the time always has a
|
|
7376
|
+
// value and defaults to 00:00. Only the date can be unselected, and without
|
|
7377
|
+
// a date the whole `value` is undefined.
|
|
7378
|
+
// - Minute precision everywhere: times AND constraints are truncated to HH:mm
|
|
7379
|
+
// on the way in, so emitted values always carry :00 seconds and boundary
|
|
7380
|
+
// checks never fail on sub-minute residue.
|
|
7381
|
+
// - NO reachable state holds an out-of-range selection when minDateTime /
|
|
7382
|
+
// maxDateTime are set. User operations that would leave the state invalid
|
|
7383
|
+
// return THE SAME instance (===), which the hook uses to skip the emission
|
|
7384
|
+
// and let the UI snap back; entry paths that cannot reject (fromValue,
|
|
7385
|
+
// withConstraints, withTimeCleared) clamp into the range instead.
|
|
7386
|
+
//
|
|
7387
|
+
// Because of these invariants the emission is state-scoped: after any accepted
|
|
7388
|
+
// operation the value to emit is simply the new state's `value`. Rejected
|
|
7389
|
+
// operations are detected by identity (`next === prev`).
|
|
7390
|
+
/** Truncate to minute precision (HH:mm) - the picker's working precision */
|
|
7391
|
+
function truncateToMinute(time) {
|
|
7392
|
+
return new Temporal.PlainTime(time.hour, time.minute);
|
|
7393
|
+
}
|
|
7394
|
+
function midnight() {
|
|
7395
|
+
return new Temporal.PlainTime(0, 0);
|
|
7396
|
+
}
|
|
7397
|
+
function truncateDateTimeToMinute(dateTime) {
|
|
7398
|
+
return dateTime
|
|
7399
|
+
? dateTime.toPlainDate().toPlainDateTime(truncateToMinute(dateTime.toPlainTime()))
|
|
7400
|
+
: undefined;
|
|
7401
|
+
}
|
|
7402
|
+
/** Normalize constraints to the picker's minute precision */
|
|
7403
|
+
function normalizeConstraints(constraints) {
|
|
7404
|
+
return {
|
|
7405
|
+
minDateTime: truncateDateTimeToMinute(constraints.minDateTime),
|
|
7406
|
+
maxDateTime: truncateDateTimeToMinute(constraints.maxDateTime),
|
|
7407
|
+
minTime: constraints.minTime ? truncateToMinute(constraints.minTime) : undefined,
|
|
7408
|
+
maxTime: constraints.maxTime ? truncateToMinute(constraints.maxTime) : undefined,
|
|
7409
|
+
};
|
|
7410
|
+
}
|
|
7411
|
+
class DateTimePickerState {
|
|
7412
|
+
date;
|
|
7413
|
+
time;
|
|
7414
|
+
constraints;
|
|
7415
|
+
constructor(date, time, constraints) {
|
|
7416
|
+
this.date = date;
|
|
7417
|
+
this.time = time;
|
|
7418
|
+
this.constraints = normalizeConstraints(constraints);
|
|
7419
|
+
Object.freeze(this);
|
|
7420
|
+
}
|
|
7421
|
+
static empty(constraints = {}) {
|
|
7422
|
+
return new DateTimePickerState(undefined, midnight(), constraints);
|
|
7423
|
+
}
|
|
7424
|
+
/**
|
|
7425
|
+
* Build a state from an external value (the value-prop sync path).
|
|
7426
|
+
* The time is truncated to minute precision and the whole value is
|
|
7427
|
+
* clamped into [minDateTime, maxDateTime]: no state can ever hold an
|
|
7428
|
+
* out-of-range selection, regardless of where the value came from.
|
|
7429
|
+
*/
|
|
7430
|
+
static fromValue(value, constraints = {}) {
|
|
7431
|
+
if (!value)
|
|
7432
|
+
return new DateTimePickerState(undefined, midnight(), constraints);
|
|
7433
|
+
const base = new DateTimePickerState(value.toPlainDate(), truncateToMinute(value.toPlainTime()), constraints);
|
|
7434
|
+
return base.clampedIntoRange();
|
|
7435
|
+
}
|
|
7436
|
+
/** Combined date+time; undefined when no date is selected. */
|
|
7437
|
+
get value() {
|
|
7438
|
+
return this.date ? this.date.toPlainDateTime(this.time) : undefined;
|
|
7439
|
+
}
|
|
7440
|
+
/**
|
|
7441
|
+
* Effective minimum time of day for the currently selected date:
|
|
7442
|
+
* when the selected date is the minDateTime boundary date, the boundary's
|
|
7443
|
+
* time (minute-truncated) replaces the static minTime.
|
|
7444
|
+
*/
|
|
7445
|
+
get effectiveMinTime() {
|
|
7446
|
+
return this.effectiveMinTimeFor(this.date);
|
|
7447
|
+
}
|
|
7448
|
+
/** Effective maximum time of day for the currently selected date (see effectiveMinTime). */
|
|
7449
|
+
get effectiveMaxTime() {
|
|
7450
|
+
return this.effectiveMaxTimeFor(this.date);
|
|
7451
|
+
}
|
|
7452
|
+
/**
|
|
7453
|
+
* Select (or clear) the date.
|
|
7454
|
+
* - Clearing the date resets the whole selection (time back to 00:00).
|
|
7455
|
+
* - When the current time is out of range for the new date, it falls back
|
|
7456
|
+
* to the effective minimum time for that date (or 00:00).
|
|
7457
|
+
* - When even the fallback is out of range the date is rejected and the
|
|
7458
|
+
* same instance is returned.
|
|
7459
|
+
*/
|
|
7460
|
+
withDate(date) {
|
|
7461
|
+
if (!date) {
|
|
7462
|
+
return new DateTimePickerState(undefined, midnight(), this.constraints);
|
|
7463
|
+
}
|
|
7464
|
+
if (this.isInRange(date.toPlainDateTime(this.time))) {
|
|
7465
|
+
return new DateTimePickerState(date, this.time, this.constraints);
|
|
7466
|
+
}
|
|
7467
|
+
const fallbackTime = this.effectiveMinTimeFor(date) ?? midnight();
|
|
7468
|
+
if (this.isInRange(date.toPlainDateTime(fallbackTime))) {
|
|
7469
|
+
return new DateTimePickerState(date, fallbackTime, this.constraints);
|
|
7470
|
+
}
|
|
7471
|
+
return this;
|
|
7472
|
+
}
|
|
7473
|
+
/**
|
|
7474
|
+
* Select the time (minute-truncated). An out-of-range time for the
|
|
7475
|
+
* selected date is rejected: the same instance is returned so the UI can
|
|
7476
|
+
* snap back to the previous time. Without a date any time is accepted
|
|
7477
|
+
* (there is no combination to validate yet).
|
|
7478
|
+
*/
|
|
7479
|
+
withTime(time) {
|
|
7480
|
+
const truncated = truncateToMinute(time);
|
|
7481
|
+
if (this.date && !this.isInRange(this.date.toPlainDateTime(truncated))) {
|
|
7482
|
+
return this;
|
|
7483
|
+
}
|
|
7484
|
+
return new DateTimePickerState(this.date, truncated, this.constraints);
|
|
7485
|
+
}
|
|
7486
|
+
/**
|
|
7487
|
+
* Apply "now" (Now button), truncated to minute precision. Rejected
|
|
7488
|
+
* (same instance returned) when the truncated now is out of range.
|
|
7489
|
+
*/
|
|
7490
|
+
withNow(now) {
|
|
7491
|
+
const date = now.toPlainDate();
|
|
7492
|
+
const time = truncateToMinute(now.toPlainTime());
|
|
7493
|
+
if (!this.isInRange(date.toPlainDateTime(time))) {
|
|
7494
|
+
return this;
|
|
7495
|
+
}
|
|
7496
|
+
return new DateTimePickerState(date, time, this.constraints);
|
|
7497
|
+
}
|
|
7498
|
+
/**
|
|
7499
|
+
* Reset the time to its default: the effective minimum for the current
|
|
7500
|
+
* date when 00:00 would be out of range (boundary dates), else 00:00.
|
|
7501
|
+
* The result is always in range - this is the clear-time path.
|
|
7502
|
+
*/
|
|
7503
|
+
withTimeCleared() {
|
|
7504
|
+
if (!this.date) {
|
|
7505
|
+
return new DateTimePickerState(undefined, midnight(), this.constraints);
|
|
7506
|
+
}
|
|
7507
|
+
return new DateTimePickerState(this.date, midnight(), this.constraints).clampedIntoRange();
|
|
7508
|
+
}
|
|
7509
|
+
/** Reset the selection (no date, time back to 00:00), keeping the constraints. */
|
|
7510
|
+
clear() {
|
|
7511
|
+
return new DateTimePickerState(undefined, midnight(), this.constraints);
|
|
7512
|
+
}
|
|
7513
|
+
/**
|
|
7514
|
+
* Replace the constraints. The current selection is clamped into the new
|
|
7515
|
+
* [minDateTime, maxDateTime] range so it can never be left out of range.
|
|
7516
|
+
*/
|
|
7517
|
+
withConstraints(constraints) {
|
|
7518
|
+
return new DateTimePickerState(this.date, this.time, constraints).clampedIntoRange();
|
|
7519
|
+
}
|
|
7520
|
+
/**
|
|
7521
|
+
* Clamp the selection into [minDateTime, maxDateTime]: a combined value
|
|
7522
|
+
* below the minimum becomes the minimum (date and time), one above the
|
|
7523
|
+
* maximum becomes the maximum. In-range states return themselves.
|
|
7524
|
+
*/
|
|
7525
|
+
clampedIntoRange() {
|
|
7526
|
+
const combined = this.value;
|
|
7527
|
+
if (!combined)
|
|
7528
|
+
return this;
|
|
7529
|
+
const { minDateTime, maxDateTime } = this.constraints;
|
|
7530
|
+
if (minDateTime && Temporal.PlainDateTime.compare(combined, minDateTime) < 0) {
|
|
7531
|
+
return new DateTimePickerState(minDateTime.toPlainDate(), truncateToMinute(minDateTime.toPlainTime()), this.constraints);
|
|
7532
|
+
}
|
|
7533
|
+
if (maxDateTime && Temporal.PlainDateTime.compare(combined, maxDateTime) > 0) {
|
|
7534
|
+
return new DateTimePickerState(maxDateTime.toPlainDate(), truncateToMinute(maxDateTime.toPlainTime()), this.constraints);
|
|
7535
|
+
}
|
|
7536
|
+
return this;
|
|
7537
|
+
}
|
|
7538
|
+
effectiveMinTimeFor(date) {
|
|
7539
|
+
const { minDateTime, minTime } = this.constraints;
|
|
7540
|
+
if (date && minDateTime && date.equals(minDateTime.toPlainDate())) {
|
|
7541
|
+
return truncateToMinute(minDateTime.toPlainTime());
|
|
7542
|
+
}
|
|
7543
|
+
return minTime;
|
|
7544
|
+
}
|
|
7545
|
+
effectiveMaxTimeFor(date) {
|
|
7546
|
+
const { maxDateTime, maxTime } = this.constraints;
|
|
7547
|
+
if (date && maxDateTime && date.equals(maxDateTime.toPlainDate())) {
|
|
7548
|
+
return truncateToMinute(maxDateTime.toPlainTime());
|
|
7549
|
+
}
|
|
7550
|
+
return maxTime;
|
|
7551
|
+
}
|
|
7552
|
+
isInRange(combined) {
|
|
7553
|
+
const { minDateTime, maxDateTime } = this.constraints;
|
|
7554
|
+
if (minDateTime && Temporal.PlainDateTime.compare(combined, minDateTime) < 0)
|
|
7555
|
+
return false;
|
|
7556
|
+
if (maxDateTime && Temporal.PlainDateTime.compare(combined, maxDateTime) > 0)
|
|
7557
|
+
return false;
|
|
7558
|
+
return true;
|
|
7559
|
+
}
|
|
7560
|
+
}
|
|
7561
|
+
|
|
7562
|
+
// useCwDateTimePickerTemporal - thin glue between DateTimePickerState and the
|
|
7563
|
+
// CwDateTimePickerTemporal component. All picker logic lives in the domain
|
|
7564
|
+
// class; this hook only holds the instance in React state, syncs it with the
|
|
7565
|
+
// external value prop, and converts PlainTime <-> "HH:mm" at the CwTimePicker
|
|
7566
|
+
// string boundary.
|
|
7567
|
+
//
|
|
7568
|
+
// Emission model: an accepted operation returns a NEW instance and the hook
|
|
7569
|
+
// emits its `value`; a rejected operation returns the SAME instance and
|
|
7570
|
+
// nothing is emitted (CwTimePicker snaps its text back to the value prop as
|
|
7571
|
+
// soon as the typed entry is complete and invalid).
|
|
7572
|
+
//
|
|
7573
|
+
// The time string is empty until a date is selected (visual only): without a
|
|
7574
|
+
// date the whole value is undefined, so showing the default 00:00 would
|
|
7575
|
+
// suggest a selection that doesn't exist.
|
|
7576
|
+
function plainTimeToHHmm(time) {
|
|
7577
|
+
return `${String(time.hour).padStart(2, "0")}:${String(time.minute).padStart(2, "0")}`;
|
|
7578
|
+
}
|
|
7579
|
+
/** "HH:mm" -> PlainTime; unparseable input yields undefined (ignored by the caller) */
|
|
7580
|
+
function hhmmToPlainTime(time) {
|
|
7581
|
+
try {
|
|
7582
|
+
return Temporal.PlainTime.from(time);
|
|
7583
|
+
}
|
|
7584
|
+
catch {
|
|
7585
|
+
return undefined;
|
|
7586
|
+
}
|
|
7587
|
+
}
|
|
7588
|
+
function optionalTimeToHHmm(time) {
|
|
7589
|
+
return time ? plainTimeToHHmm(time) : undefined;
|
|
7590
|
+
}
|
|
7591
|
+
function plainDateTimesEqual(a, b) {
|
|
7592
|
+
if (!a || !b)
|
|
7593
|
+
return a === b;
|
|
7594
|
+
return a.equals(b);
|
|
7595
|
+
}
|
|
7596
|
+
function useCwDateTimePickerTemporal({ value, onChange, minDateTime, maxDateTime, minTime, maxTime, }) {
|
|
7597
|
+
const constraints = useMemo(() => ({
|
|
7598
|
+
minDateTime,
|
|
7599
|
+
maxDateTime,
|
|
7600
|
+
minTime: minTime ? hhmmToPlainTime(minTime) : undefined,
|
|
7601
|
+
maxTime: maxTime ? hhmmToPlainTime(maxTime) : undefined,
|
|
7602
|
+
// ISO-string deps keep the object stable across re-created Temporal instances
|
|
7603
|
+
}), [minDateTime?.toString(), maxDateTime?.toString(), minTime, maxTime]);
|
|
7604
|
+
const [state, setState] = useState(() => DateTimePickerState.fromValue(value, constraints));
|
|
7605
|
+
// Track the previous value prop to detect external changes.
|
|
7606
|
+
const prevValueRef = useRef(value);
|
|
7607
|
+
useEffect(() => {
|
|
7608
|
+
if (!plainDateTimesEqual(value, prevValueRef.current)) {
|
|
7609
|
+
prevValueRef.current = value;
|
|
7610
|
+
setState(prev => DateTimePickerState.fromValue(value, prev.constraints));
|
|
7611
|
+
}
|
|
7612
|
+
}, [value]);
|
|
7613
|
+
useEffect(() => {
|
|
7614
|
+
setState(prev => prev.withConstraints(constraints));
|
|
7615
|
+
}, [constraints]);
|
|
7616
|
+
const apply = useCallback((next) => {
|
|
7617
|
+
if (next === state)
|
|
7618
|
+
return; // operation rejected by the domain - no emission
|
|
7619
|
+
setState(next);
|
|
7620
|
+
onChange(next.value);
|
|
7621
|
+
}, [state, onChange]);
|
|
7622
|
+
const selectDate = useCallback((date) => {
|
|
7623
|
+
apply(state.withDate(date));
|
|
7624
|
+
}, [state, apply]);
|
|
7625
|
+
const selectTime = useCallback((time) => {
|
|
7626
|
+
if (!time) {
|
|
7627
|
+
// Cleared time input: reset to the in-range default for the current date
|
|
7628
|
+
apply(state.withTimeCleared());
|
|
7629
|
+
return;
|
|
7630
|
+
}
|
|
7631
|
+
const parsed = hhmmToPlainTime(time);
|
|
7632
|
+
if (parsed) {
|
|
7633
|
+
apply(state.withTime(parsed));
|
|
7634
|
+
}
|
|
7635
|
+
}, [state, apply]);
|
|
7636
|
+
const selectNow = useCallback(() => {
|
|
7637
|
+
apply(state.withNow(Temporal.Now.plainDateTimeISO()));
|
|
7638
|
+
}, [state, apply]);
|
|
7639
|
+
return {
|
|
7640
|
+
date: state.date,
|
|
7641
|
+
time: state.date ? plainTimeToHHmm(state.time) : "",
|
|
7642
|
+
minDate: minDateTime?.toPlainDate(),
|
|
7643
|
+
maxDate: maxDateTime?.toPlainDate(),
|
|
7644
|
+
effectiveMinTime: optionalTimeToHHmm(state.effectiveMinTime),
|
|
7645
|
+
effectiveMaxTime: optionalTimeToHHmm(state.effectiveMaxTime),
|
|
7646
|
+
selectDate,
|
|
7647
|
+
selectTime,
|
|
7648
|
+
selectNow,
|
|
7649
|
+
};
|
|
7650
|
+
}
|
|
7651
|
+
|
|
7652
|
+
function CwDateTimePickerTemporal({ value, onChange, minDateTime, maxDateTime, disabledDates, disabledMatcher, timeInterval = 15, minTime, maxTime, labelProps, alignProps, datePlaceholder = "dd.MM.yyyy", timePlaceholder = "HH:mm", disabled, required, className, showClear = true, popupPosition = "left-bottom", numberOfMonths = 1, showNowButton = false, nowLabel = "Now", locale = enGB, }) {
|
|
7653
|
+
const picker = useCwDateTimePickerTemporal({
|
|
7654
|
+
value,
|
|
7655
|
+
onChange,
|
|
7656
|
+
minDateTime,
|
|
7657
|
+
maxDateTime,
|
|
7658
|
+
minTime,
|
|
7659
|
+
maxTime,
|
|
7660
|
+
});
|
|
7661
|
+
return jsx("div", { className: `cw-datetimepicker cw-datetimepicker-separate ${className || ""}`, children: jsxs(CwAlign, { ...alignProps, itemProp: required ? "required" : "", children: [labelProps && (jsx(CwLabel, { ...labelProps, children: labelProps.text })), jsxs("div", { className: "cw-flex-row cw-align-left-center cw-gap-small", children: [jsx(CwDatePickerTemporal, { value: picker.date, onChange: picker.selectDate, minDate: picker.minDate, maxDate: picker.maxDate, disabledDates: disabledDates, disabledMatcher: disabledMatcher, placeholder: datePlaceholder, disabled: disabled, required: required, showClear: showClear, numberOfMonths: numberOfMonths, popupPosition: popupPosition, locale: locale }), jsx(CwTimePicker, { value: picker.time, onChange: picker.selectTime, interval: timeInterval, minTime: picker.effectiveMinTime, maxTime: picker.effectiveMaxTime, placeholder: timePlaceholder, disabled: disabled, required: required, showClear: showClear, popupPosition: popupPosition }), showNowButton && (jsx(CwButton, { type: "button", variant: "outline", onClick: picker.selectNow, disabled: disabled, text: nowLabel }))] })] }) });
|
|
7662
|
+
}
|
|
7663
|
+
|
|
7664
|
+
// dateRangePresetsTemporal.ts
|
|
7665
|
+
// Temporal-typed counterpart of dateRangePresets.ts (used by CwDateRangePickerTemporal).
|
|
7666
|
+
// Each preset is evaluated at click time; a reference date can be injected for testing.
|
|
7667
|
+
const ref = (today) => today ?? Temporal.Now.plainDateISO();
|
|
7668
|
+
/** Monday as first day of the week (ISO dayOfWeek: 1 = Mon … 7 = Sun). */
|
|
7669
|
+
const startOfWeek = (d) => d.subtract({ days: d.dayOfWeek - 1 });
|
|
7670
|
+
const startOfMonth = (d) => d.with({ day: 1 });
|
|
7671
|
+
const endOfMonth = (d) => d.with({ day: d.daysInMonth });
|
|
7672
|
+
/** First day of the quarter that contains `d`. */
|
|
7673
|
+
const startOfQuarter = (d) => Temporal.PlainDate.from({ year: d.year, month: Math.floor((d.month - 1) / 3) * 3 + 1, day: 1 });
|
|
7674
|
+
const PRESET_LIBRARY_TEMPORAL = {
|
|
7675
|
+
// === PAST DAYS ===
|
|
7676
|
+
today: {
|
|
7677
|
+
key: "today",
|
|
7678
|
+
label: "Today",
|
|
7679
|
+
getValue: (t) => { const d = ref(t); return { from: d, to: d }; },
|
|
7680
|
+
},
|
|
7681
|
+
yesterday: {
|
|
7682
|
+
key: "yesterday",
|
|
7683
|
+
label: "Yesterday",
|
|
7684
|
+
getValue: (t) => { const d = ref(t).subtract({ days: 1 }); return { from: d, to: d }; },
|
|
7685
|
+
},
|
|
7686
|
+
past2Days: {
|
|
7687
|
+
key: "past2Days",
|
|
7688
|
+
label: "Past 2 Days",
|
|
7689
|
+
getValue: (t) => { const d = ref(t); return { from: d.subtract({ days: 2 }), to: d }; },
|
|
7690
|
+
},
|
|
7691
|
+
past3Days: {
|
|
7692
|
+
key: "past3Days",
|
|
7693
|
+
label: "Past 3 Days",
|
|
7694
|
+
getValue: (t) => { const d = ref(t); return { from: d.subtract({ days: 3 }), to: d }; },
|
|
7695
|
+
},
|
|
7696
|
+
past7Days: {
|
|
7697
|
+
key: "past7Days",
|
|
7698
|
+
label: "Past 7 days",
|
|
7699
|
+
getValue: (t) => { const d = ref(t); return { from: d.subtract({ days: 7 }), to: d }; },
|
|
7700
|
+
},
|
|
7701
|
+
past14Days: {
|
|
7702
|
+
key: "past14Days",
|
|
7703
|
+
label: "Past 14 days",
|
|
7704
|
+
getValue: (t) => { const d = ref(t); return { from: d.subtract({ days: 14 }), to: d }; },
|
|
7705
|
+
},
|
|
7706
|
+
past30Days: {
|
|
7707
|
+
key: "past30Days",
|
|
7708
|
+
label: "Past 30 days",
|
|
7709
|
+
getValue: (t) => { const d = ref(t); return { from: d.subtract({ days: 30 }), to: d }; },
|
|
7710
|
+
},
|
|
7711
|
+
past90Days: {
|
|
7712
|
+
key: "past90Days",
|
|
7713
|
+
label: "Past 90 days",
|
|
7714
|
+
getValue: (t) => { const d = ref(t); return { from: d.subtract({ days: 90 }), to: d }; },
|
|
7715
|
+
},
|
|
7716
|
+
// === NEXT DAYS ===
|
|
7717
|
+
next7Days: {
|
|
7718
|
+
key: "next7Days",
|
|
7719
|
+
label: "Next 7 days",
|
|
7720
|
+
getValue: (t) => { const d = ref(t); return { from: d, to: d.add({ days: 7 }) }; },
|
|
7721
|
+
},
|
|
7722
|
+
next14Days: {
|
|
7723
|
+
key: "next14Days",
|
|
7724
|
+
label: "Next 14 days",
|
|
7725
|
+
getValue: (t) => { const d = ref(t); return { from: d, to: d.add({ days: 14 }) }; },
|
|
7726
|
+
},
|
|
7727
|
+
next30Days: {
|
|
7728
|
+
key: "next30Days",
|
|
7729
|
+
label: "Next 30 days",
|
|
7730
|
+
getValue: (t) => { const d = ref(t); return { from: d, to: d.add({ days: 30 }) }; },
|
|
7731
|
+
},
|
|
7732
|
+
// === WEEKS (Monday-first) ===
|
|
7733
|
+
thisWeek: {
|
|
7734
|
+
key: "thisWeek",
|
|
7735
|
+
label: "This week",
|
|
7736
|
+
getValue: (t) => { const s = startOfWeek(ref(t)); return { from: s, to: s.add({ days: 6 }) }; },
|
|
7737
|
+
},
|
|
7738
|
+
lastWeek: {
|
|
7739
|
+
key: "lastWeek",
|
|
7740
|
+
label: "Last week",
|
|
7741
|
+
getValue: (t) => { const end = startOfWeek(ref(t)).subtract({ days: 1 }); return { from: end.subtract({ days: 6 }), to: end }; },
|
|
7742
|
+
},
|
|
7743
|
+
nextWeek: {
|
|
7744
|
+
key: "nextWeek",
|
|
7745
|
+
label: "Next week",
|
|
7746
|
+
getValue: (t) => { const s = startOfWeek(ref(t)).add({ days: 7 }); return { from: s, to: s.add({ days: 6 }) }; },
|
|
7747
|
+
},
|
|
7748
|
+
// === MONTHS ===
|
|
7749
|
+
thisMonth: {
|
|
7750
|
+
key: "thisMonth",
|
|
7751
|
+
label: "This month",
|
|
7752
|
+
getValue: (t) => { const d = ref(t); return { from: startOfMonth(d), to: endOfMonth(d) }; },
|
|
7753
|
+
},
|
|
7754
|
+
lastMonth: {
|
|
7755
|
+
key: "lastMonth",
|
|
7756
|
+
label: "Last month",
|
|
7757
|
+
getValue: (t) => { const d = startOfMonth(ref(t)).subtract({ months: 1 }); return { from: d, to: endOfMonth(d) }; },
|
|
7758
|
+
},
|
|
7759
|
+
nextMonth: {
|
|
7760
|
+
key: "nextMonth",
|
|
7761
|
+
label: "Next month",
|
|
7762
|
+
getValue: (t) => { const d = startOfMonth(ref(t)).add({ months: 1 }); return { from: d, to: endOfMonth(d) }; },
|
|
7763
|
+
},
|
|
7764
|
+
// === QUARTERS ===
|
|
7765
|
+
thisQuarter: {
|
|
7766
|
+
key: "thisQuarter",
|
|
7767
|
+
label: "This quarter",
|
|
7768
|
+
getValue: (t) => { const from = startOfQuarter(ref(t)); return { from, to: from.add({ months: 3 }).subtract({ days: 1 }) }; },
|
|
7769
|
+
},
|
|
7770
|
+
lastQuarter: {
|
|
7771
|
+
key: "lastQuarter",
|
|
7772
|
+
label: "Last quarter",
|
|
7773
|
+
getValue: (t) => { const from = startOfQuarter(ref(t)).subtract({ months: 3 }); return { from, to: from.add({ months: 3 }).subtract({ days: 1 }) }; },
|
|
7774
|
+
},
|
|
7775
|
+
nextQuarter: {
|
|
7776
|
+
key: "nextQuarter",
|
|
7777
|
+
label: "Next quarter",
|
|
7778
|
+
getValue: (t) => { const from = startOfQuarter(ref(t)).add({ months: 3 }); return { from, to: from.add({ months: 3 }).subtract({ days: 1 }) }; },
|
|
7779
|
+
},
|
|
7780
|
+
// === YEAR ===
|
|
7781
|
+
thisYear: {
|
|
7782
|
+
key: "thisYear",
|
|
7783
|
+
label: "This year",
|
|
7784
|
+
getValue: (t) => {
|
|
7785
|
+
const y = ref(t).year;
|
|
7786
|
+
return { from: Temporal.PlainDate.from({ year: y, month: 1, day: 1 }), to: Temporal.PlainDate.from({ year: y, month: 12, day: 31 }) };
|
|
7787
|
+
},
|
|
7788
|
+
},
|
|
7789
|
+
lastYear: {
|
|
7790
|
+
key: "lastYear",
|
|
7791
|
+
label: "Last year",
|
|
7792
|
+
getValue: (t) => {
|
|
7793
|
+
const y = ref(t).year - 1;
|
|
7794
|
+
return { from: Temporal.PlainDate.from({ year: y, month: 1, day: 1 }), to: Temporal.PlainDate.from({ year: y, month: 12, day: 31 }) };
|
|
7795
|
+
},
|
|
7796
|
+
},
|
|
7797
|
+
};
|
|
7798
|
+
/**
|
|
7799
|
+
* Helper to get presets by their keys.
|
|
7800
|
+
* @example getPresetsByKeysTemporal(['thisWeek', 'past7Days', 'past30Days'])
|
|
7801
|
+
*/
|
|
7802
|
+
const getPresetsByKeysTemporal = (keys) => keys.map((key) => PRESET_LIBRARY_TEMPORAL[key]).filter((preset) => preset !== undefined);
|
|
7803
|
+
|
|
7804
|
+
function CwDateRangePickerTemporal({ value, onChange, minDate, maxDate, disabledDates, disabledMatcher, maxRangeDays, defaultMonth, labelProps, alignProps, placeholderFrom = "From date", placeholderTo = "To date", displayFormat = "dd.MM.yyyy", disabled, required, className, showClear = true, popupPosition = "left-bottom", numberOfMonths = 2, locale = enGB, yearRangeStart, yearRangeEnd, showPresets = false, presetKeys, customPresets, }) {
|
|
7805
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
7806
|
+
const [inputFromValue, setInputFromValue] = useState("");
|
|
7807
|
+
const [inputToValue, setInputToValue] = useState("");
|
|
7808
|
+
const [rangeErrorMessage, setRangeErrorMessage] = useState(null);
|
|
7809
|
+
const [focusedInput, setFocusedInput] = useState();
|
|
7810
|
+
const containerRef = useRef(null);
|
|
7811
|
+
const wrapperRef = useRef(null);
|
|
7812
|
+
const inputFromRef = useRef(null);
|
|
7813
|
+
const inputToRef = useRef(null);
|
|
7814
|
+
const prevValueRef = useRef(undefined);
|
|
7815
|
+
const prevDisplayFormatRef = useRef(displayFormat);
|
|
7816
|
+
const rangeError = useCallback(() => (maxRangeDays ? `Range cannot exceed ${maxRangeDays} days` : "Invalid date range"), [maxRangeDays]);
|
|
7817
|
+
const handleClose = useCallback(() => {
|
|
7818
|
+
setIsOpen(false);
|
|
7819
|
+
setFocusedInput(undefined);
|
|
7820
|
+
}, []);
|
|
7821
|
+
const { popupRef, popupStyle, renderPopup } = usePickerPopup({
|
|
7822
|
+
anchorRef: wrapperRef,
|
|
7823
|
+
isOpen,
|
|
7824
|
+
onClose: handleClose,
|
|
7825
|
+
position: popupPosition,
|
|
7826
|
+
});
|
|
7827
|
+
// Calculate presets to render
|
|
7828
|
+
const presetsToRender = useMemo(() => {
|
|
7829
|
+
let presets = [];
|
|
7830
|
+
if (presetKeys && presetKeys.length > 0) {
|
|
7831
|
+
presets = getPresetsByKeysTemporal(presetKeys);
|
|
7832
|
+
}
|
|
7833
|
+
if (customPresets && customPresets.length > 0) {
|
|
7834
|
+
presets = [...presets, ...customPresets];
|
|
7835
|
+
}
|
|
7836
|
+
return presets;
|
|
7837
|
+
}, [presetKeys, customPresets]);
|
|
7838
|
+
// Validate date range
|
|
7839
|
+
const validateRange = useCallback((from, to) => {
|
|
7840
|
+
if (!from || !to)
|
|
7841
|
+
return true; // Partial ranges are OK during selection
|
|
7842
|
+
if (Temporal.PlainDate.compare(from, to) > 0)
|
|
7843
|
+
return false;
|
|
7844
|
+
if (maxRangeDays) {
|
|
7845
|
+
const daysDiff = from.until(to, { largestUnit: "day" }).days;
|
|
7846
|
+
if (daysDiff > maxRangeDays)
|
|
7847
|
+
return false;
|
|
7848
|
+
}
|
|
7849
|
+
return true;
|
|
7850
|
+
}, [maxRangeDays]);
|
|
7851
|
+
// Sync input values with prop value
|
|
7852
|
+
useEffect(() => {
|
|
7853
|
+
const currentFrom = value?.from?.toString();
|
|
7854
|
+
const currentTo = value?.to?.toString();
|
|
7855
|
+
const prevFrom = prevValueRef.current?.from;
|
|
7856
|
+
const prevTo = prevValueRef.current?.to;
|
|
7857
|
+
if (currentFrom !== prevFrom ||
|
|
7858
|
+
currentTo !== prevTo ||
|
|
7859
|
+
prevDisplayFormatRef.current !== displayFormat) {
|
|
7860
|
+
prevValueRef.current = { from: currentFrom, to: currentTo };
|
|
7861
|
+
prevDisplayFormatRef.current = displayFormat;
|
|
7862
|
+
setInputFromValue(value?.from ? formatPlainDate(value.from, displayFormat) : "");
|
|
7863
|
+
setInputToValue(value?.to ? formatPlainDate(value.to, displayFormat) : "");
|
|
7864
|
+
}
|
|
7865
|
+
}, [value, displayFormat]);
|
|
7866
|
+
const handleRangeSelect = useCallback((range, selectedDay) => {
|
|
7867
|
+
const selected = jsDateToPlainDate(selectedDay);
|
|
7868
|
+
let newRange;
|
|
7869
|
+
if (focusedInput === 'from') {
|
|
7870
|
+
newRange = { from: selected, to: value?.to };
|
|
7871
|
+
if (newRange.to && Temporal.PlainDate.compare(selected, newRange.to) > 0) {
|
|
7872
|
+
newRange.to = undefined;
|
|
7873
|
+
setFocusedInput('to');
|
|
7874
|
+
setRangeErrorMessage(null);
|
|
7875
|
+
}
|
|
7876
|
+
else if (newRange.to) {
|
|
7877
|
+
if (!validateRange(newRange.from, newRange.to)) {
|
|
7878
|
+
setRangeErrorMessage(rangeError());
|
|
7879
|
+
return;
|
|
7880
|
+
}
|
|
7881
|
+
setRangeErrorMessage(null);
|
|
7882
|
+
setIsOpen(false);
|
|
7883
|
+
setFocusedInput(undefined);
|
|
7884
|
+
}
|
|
7885
|
+
else {
|
|
7886
|
+
setFocusedInput('to');
|
|
7887
|
+
setRangeErrorMessage(null);
|
|
7888
|
+
}
|
|
7889
|
+
}
|
|
7890
|
+
else if (focusedInput === 'to') {
|
|
7891
|
+
newRange = { from: value?.from, to: selected };
|
|
7892
|
+
if (newRange.from && Temporal.PlainDate.compare(selected, newRange.from) < 0) {
|
|
7893
|
+
newRange.from = undefined;
|
|
7894
|
+
setFocusedInput('from');
|
|
7895
|
+
setRangeErrorMessage(null);
|
|
7896
|
+
}
|
|
7897
|
+
else if (newRange.from) {
|
|
7898
|
+
if (!validateRange(newRange.from, newRange.to)) {
|
|
7899
|
+
setRangeErrorMessage(rangeError());
|
|
7900
|
+
return;
|
|
7901
|
+
}
|
|
7902
|
+
setRangeErrorMessage(null);
|
|
7903
|
+
setIsOpen(false);
|
|
7904
|
+
setFocusedInput(undefined);
|
|
7905
|
+
}
|
|
7906
|
+
else {
|
|
7907
|
+
setFocusedInput('from');
|
|
7908
|
+
setRangeErrorMessage(null);
|
|
7909
|
+
}
|
|
7910
|
+
}
|
|
7911
|
+
else {
|
|
7912
|
+
if (!range) {
|
|
7913
|
+
onChange(undefined);
|
|
7914
|
+
return;
|
|
7915
|
+
}
|
|
7916
|
+
newRange = {
|
|
7917
|
+
from: range.from ? jsDateToPlainDate(range.from) : undefined,
|
|
7918
|
+
to: range.to ? jsDateToPlainDate(range.to) : undefined,
|
|
7919
|
+
};
|
|
7920
|
+
if (range.from && range.to) {
|
|
7921
|
+
if (!validateRange(newRange.from, newRange.to)) {
|
|
7922
|
+
setRangeErrorMessage(rangeError());
|
|
7923
|
+
return;
|
|
7924
|
+
}
|
|
7925
|
+
setRangeErrorMessage(null);
|
|
7926
|
+
setIsOpen(false);
|
|
7927
|
+
setFocusedInput(undefined);
|
|
7928
|
+
}
|
|
7929
|
+
else if (range.from) {
|
|
7930
|
+
setFocusedInput('to');
|
|
7931
|
+
setRangeErrorMessage(null);
|
|
7932
|
+
}
|
|
7933
|
+
else {
|
|
7934
|
+
setFocusedInput('from');
|
|
7935
|
+
setRangeErrorMessage(null);
|
|
7936
|
+
}
|
|
7937
|
+
}
|
|
7938
|
+
onChange(newRange);
|
|
7939
|
+
}, [onChange, validateRange, value, focusedInput, rangeError]);
|
|
7940
|
+
const handleClearFrom = useCallback((e) => {
|
|
7941
|
+
e.stopPropagation();
|
|
7942
|
+
onChange({ from: undefined, to: value?.to });
|
|
7943
|
+
setInputFromValue("");
|
|
7944
|
+
}, [onChange, value]);
|
|
7945
|
+
const handleClearTo = useCallback((e) => {
|
|
7946
|
+
e.stopPropagation();
|
|
7947
|
+
onChange({ from: value?.from, to: undefined });
|
|
7948
|
+
setInputToValue("");
|
|
7949
|
+
}, [onChange, value]);
|
|
7950
|
+
const handleInputFromClick = useCallback(() => {
|
|
7951
|
+
if (!disabled) {
|
|
7952
|
+
setIsOpen(true);
|
|
7953
|
+
setFocusedInput('from');
|
|
7954
|
+
}
|
|
7955
|
+
}, [disabled]);
|
|
7956
|
+
const handleInputToClick = useCallback(() => {
|
|
7957
|
+
if (!disabled) {
|
|
7958
|
+
setIsOpen(true);
|
|
7959
|
+
setFocusedInput('to');
|
|
7960
|
+
}
|
|
7961
|
+
}, [disabled]);
|
|
7962
|
+
const handleInputFromChange = useCallback((e) => {
|
|
7963
|
+
const newValue = e.currentTarget.value;
|
|
7964
|
+
setInputFromValue(newValue);
|
|
7965
|
+
const parsed = parseDate(newValue, displayFormat);
|
|
7966
|
+
if (parsed) {
|
|
7967
|
+
if (validate(parsed, minDate, maxDate) && validateRange(parsed, value?.to)) {
|
|
7968
|
+
onChange({ from: parsed, to: value?.to });
|
|
7969
|
+
}
|
|
7970
|
+
}
|
|
7971
|
+
else if (newValue === "") {
|
|
7972
|
+
onChange({ from: undefined, to: value?.to });
|
|
7973
|
+
}
|
|
7974
|
+
}, [displayFormat, minDate, maxDate, validateRange, onChange, value]);
|
|
7975
|
+
const handleInputToChange = useCallback((e) => {
|
|
7976
|
+
const newValue = e.currentTarget.value;
|
|
7977
|
+
setInputToValue(newValue);
|
|
7978
|
+
const parsed = parseDate(newValue, displayFormat);
|
|
7979
|
+
if (parsed) {
|
|
7980
|
+
if (validate(parsed, minDate, maxDate) && validateRange(value?.from, parsed)) {
|
|
7981
|
+
onChange({ from: value?.from, to: parsed });
|
|
7982
|
+
}
|
|
7983
|
+
}
|
|
7984
|
+
else if (newValue === "") {
|
|
7985
|
+
onChange({ from: value?.from, to: undefined });
|
|
7986
|
+
}
|
|
7987
|
+
}, [displayFormat, minDate, maxDate, validateRange, onChange, value]);
|
|
7988
|
+
const handleInputBlur = useCallback((type) => {
|
|
7989
|
+
const inputValue = type === 'from' ? inputFromValue : inputToValue;
|
|
7990
|
+
const currentValue = type === 'from' ? value?.from : value?.to;
|
|
7991
|
+
const trimmed = inputValue?.trim();
|
|
7992
|
+
if (trimmed) {
|
|
7993
|
+
const parsed = parseDate(trimmed, displayFormat);
|
|
7994
|
+
if (parsed && validate(parsed, minDate, maxDate)) {
|
|
7995
|
+
const newRange = type === 'from'
|
|
7996
|
+
? { from: parsed, to: value?.to }
|
|
7997
|
+
: { from: value?.from, to: parsed };
|
|
7998
|
+
if (validateRange(newRange.from, newRange.to)) {
|
|
7999
|
+
onChange(newRange);
|
|
8000
|
+
if (type === 'from') {
|
|
8001
|
+
setInputFromValue(formatPlainDate(parsed, displayFormat));
|
|
8002
|
+
}
|
|
8003
|
+
else {
|
|
8004
|
+
setInputToValue(formatPlainDate(parsed, displayFormat));
|
|
8005
|
+
}
|
|
8006
|
+
return;
|
|
8007
|
+
}
|
|
8008
|
+
}
|
|
8009
|
+
}
|
|
8010
|
+
// Reset to current value if invalid
|
|
8011
|
+
if (type === 'from') {
|
|
8012
|
+
setInputFromValue(currentValue ? formatPlainDate(currentValue, displayFormat) : "");
|
|
8013
|
+
}
|
|
8014
|
+
else {
|
|
8015
|
+
setInputToValue(currentValue ? formatPlainDate(currentValue, displayFormat) : "");
|
|
8016
|
+
}
|
|
8017
|
+
}, [inputFromValue, inputToValue, value, displayFormat, minDate, maxDate, validateRange, onChange]);
|
|
8018
|
+
const handleInputKeyDown = useCallback((e, type) => {
|
|
8019
|
+
if (e.key === "Enter") {
|
|
8020
|
+
e.preventDefault();
|
|
8021
|
+
if (type === 'from') {
|
|
8022
|
+
inputFromRef.current?.blur();
|
|
8023
|
+
inputToRef.current?.focus();
|
|
8024
|
+
}
|
|
8025
|
+
else {
|
|
8026
|
+
inputToRef.current?.blur();
|
|
8027
|
+
setIsOpen(false);
|
|
8028
|
+
}
|
|
8029
|
+
}
|
|
8030
|
+
else if (e.key === "Escape") {
|
|
8031
|
+
setIsOpen(false);
|
|
8032
|
+
if (type === 'from') {
|
|
8033
|
+
inputFromRef.current?.blur();
|
|
8034
|
+
}
|
|
8035
|
+
else {
|
|
8036
|
+
inputToRef.current?.blur();
|
|
8037
|
+
}
|
|
8038
|
+
}
|
|
8039
|
+
}, []);
|
|
8040
|
+
// Handle preset click
|
|
8041
|
+
const handlePresetClick = useCallback((preset, e) => {
|
|
8042
|
+
e.preventDefault();
|
|
8043
|
+
e.stopPropagation();
|
|
8044
|
+
const nextRange = preset.getValue();
|
|
8045
|
+
if (!validate(nextRange.from, minDate, maxDate) ||
|
|
8046
|
+
!validate(nextRange.to, minDate, maxDate) ||
|
|
8047
|
+
!validateRange(nextRange.from, nextRange.to)) {
|
|
8048
|
+
setRangeErrorMessage(rangeError());
|
|
8049
|
+
return;
|
|
8050
|
+
}
|
|
8051
|
+
setRangeErrorMessage(null);
|
|
8052
|
+
onChange(nextRange);
|
|
8053
|
+
setIsOpen(false);
|
|
8054
|
+
}, [maxDate, minDate, onChange, rangeError, validateRange]);
|
|
8055
|
+
// Derive start/end months for the year dropdown range, guarding against inverted yearRange props
|
|
8056
|
+
const dropdownStartMonth = useMemo(() => {
|
|
8057
|
+
const inferredStart = minDate ? minDate.year : Temporal.Now.plainDateISO().year - 10;
|
|
8058
|
+
const inferredEnd = yearRangeEnd ?? (maxDate ? maxDate.year : Temporal.Now.plainDateISO().year + 5);
|
|
8059
|
+
const year = Math.min(yearRangeStart ?? inferredStart, inferredEnd);
|
|
8060
|
+
return new Date(year, 0, 1);
|
|
8061
|
+
}, [yearRangeStart, yearRangeEnd, minDate, maxDate]);
|
|
8062
|
+
const dropdownEndMonth = useMemo(() => {
|
|
8063
|
+
const inferredEnd = maxDate ? maxDate.year : Temporal.Now.plainDateISO().year + 5;
|
|
8064
|
+
const inferredStart = yearRangeStart ?? (minDate ? minDate.year : Temporal.Now.plainDateISO().year - 10);
|
|
8065
|
+
const year = Math.max(yearRangeEnd ?? inferredEnd, inferredStart);
|
|
8066
|
+
return new Date(year, 11, 31);
|
|
8067
|
+
}, [yearRangeStart, yearRangeEnd, minDate, maxDate]);
|
|
8068
|
+
// Memoize disabled days (convert Temporal → Date for DayPicker)
|
|
8069
|
+
const disabledDays = useMemo(() => {
|
|
8070
|
+
const baseDisabled = [
|
|
8071
|
+
...(disabledDates?.map(plainDateToJsDate) || []),
|
|
8072
|
+
...(minDate ? [{ before: plainDateToJsDate(minDate) }] : []),
|
|
8073
|
+
...(maxDate ? [{ after: plainDateToJsDate(maxDate) }] : []),
|
|
8074
|
+
...(disabledMatcher ? [disabledMatcher] : []),
|
|
8075
|
+
];
|
|
8076
|
+
// If maxRangeDays is set and we have a "from" date, disable dates beyond the range
|
|
8077
|
+
if (maxRangeDays) {
|
|
8078
|
+
if (value?.from) {
|
|
8079
|
+
const maxAllowedDate = value.from.add({ days: maxRangeDays });
|
|
8080
|
+
if (focusedInput !== 'from') {
|
|
8081
|
+
baseDisabled.push({ after: plainDateToJsDate(maxAllowedDate) });
|
|
8082
|
+
}
|
|
8083
|
+
if (focusedInput === 'to' && !value?.to) {
|
|
8084
|
+
const minAllowedDate = value.from.subtract({ days: maxRangeDays });
|
|
8085
|
+
baseDisabled.push({ before: plainDateToJsDate(minAllowedDate) });
|
|
8086
|
+
}
|
|
8087
|
+
}
|
|
8088
|
+
if (value?.to && !value?.from && focusedInput === 'from') {
|
|
8089
|
+
const minAllowedDate = value.to.subtract({ days: maxRangeDays });
|
|
8090
|
+
baseDisabled.push({ before: plainDateToJsDate(minAllowedDate) });
|
|
8091
|
+
}
|
|
8092
|
+
}
|
|
8093
|
+
return baseDisabled;
|
|
8094
|
+
}, [disabledDates, minDate, maxDate, disabledMatcher, maxRangeDays, value, focusedInput]);
|
|
8095
|
+
const selectedRange = useMemo(() => {
|
|
8096
|
+
if (value?.from || value?.to) {
|
|
8097
|
+
return {
|
|
8098
|
+
from: value?.from ? plainDateToJsDate(value.from) : undefined,
|
|
8099
|
+
to: value?.to ? plainDateToJsDate(value.to) : undefined,
|
|
8100
|
+
};
|
|
8101
|
+
}
|
|
8102
|
+
return undefined;
|
|
8103
|
+
}, [value]);
|
|
8104
|
+
const defaultMonthJsDate = defaultMonth
|
|
8105
|
+
? new Date(defaultMonth.year, defaultMonth.month - 1, 1)
|
|
8106
|
+
: (value?.from ? plainDateToJsDate(value.from) : undefined);
|
|
8107
|
+
return (jsx("div", { ref: containerRef, className: `cw-rangepicker ${className || ""}`, children: jsxs(CwAlign, { ...alignProps, itemProp: required ? "required" : "", children: [labelProps && (jsx(CwLabel, { ...labelProps, children: labelProps.text })), jsxs("div", { ref: wrapperRef, className: rangeStyles.rangeWrapper, children: [jsxs("div", { className: styles$4.pickerWrapper, children: [jsx("input", { ref: inputFromRef, type: "text", value: inputFromValue, placeholder: placeholderFrom, onChange: handleInputFromChange, onBlur: () => handleInputBlur('from'), onClick: handleInputFromClick, onKeyDown: (e) => handleInputKeyDown(e, 'from'), disabled: disabled, required: required, "data-focused": focusedInput === 'from' }), jsx("div", { className: styles$4.pickerIcons, children: showClear && value?.from && !disabled ? (jsx(CwButton, { type: "button", variant: "icon", color: "neutral", icon: "close", onClick: handleClearFrom, tabIndex: -1, "aria-label": "Clear from date" })) : (jsx(CwIcon, { iconId: "calendar" })) })] }), jsx(CwIcon, { iconId: "arrow-right", size: "medium" }), jsxs("div", { className: styles$4.pickerWrapper, children: [jsx("input", { ref: inputToRef, type: "text", value: inputToValue, placeholder: placeholderTo, onChange: handleInputToChange, onBlur: () => handleInputBlur('to'), onClick: handleInputToClick, onKeyDown: (e) => handleInputKeyDown(e, 'to'), disabled: disabled, required: required, "data-focused": focusedInput === 'to' }), jsx("div", { className: styles$4.pickerIcons, children: showClear && value?.to && !disabled ? (jsx(CwButton, { type: "button", variant: "icon", color: "neutral", icon: "close", onClick: handleClearTo, tabIndex: -1, "aria-label": "Clear to date" })) : (jsx(CwIcon, { iconId: "calendar" })) })] }), renderPopup(jsxs("div", { ref: popupRef, className: `${styles$4.pickerPopup} ${rangeStyles.rangePopup}`, style: popupStyle, children: [jsxs("div", { className: rangeStyles.rangeCalendarRow, children: [showPresets && presetsToRender.length > 0 && (jsx("div", { className: rangeStyles.presetList, children: presetsToRender.map((preset) => (jsx("button", { type: "button", onClick: (e) => handlePresetClick(preset, e), children: preset.label }, preset.key))) })), jsx(DayPicker, { mode: "range", selected: selectedRange, onSelect: handleRangeSelect, disabled: disabledDays, numberOfMonths: numberOfMonths, defaultMonth: defaultMonthJsDate, locale: locale, captionLayout: "dropdown", startMonth: dropdownStartMonth, endMonth: dropdownEndMonth, modifiers: {
|
|
8108
|
+
today: new Date(),
|
|
8109
|
+
}, modifiersClassNames: {
|
|
8110
|
+
today: "rdp-day-today",
|
|
8111
|
+
} })] }), jsx("div", { role: "alert", "aria-live": "polite", className: rangeStyles.rangeError, children: rangeErrorMessage })] }))] })] }) }));
|
|
8112
|
+
}
|
|
8113
|
+
|
|
7093
8114
|
const ResourcesTitleList = ({ resources, canBePinned, includesPinned, onCrewPinning, onClickResourceContextMenu, ResourceTitleComponent, }) => {
|
|
7094
8115
|
const onClickContextMenu = useCallback((key, resource) => {
|
|
7095
8116
|
if (onClickResourceContextMenu) {
|
|
@@ -10025,4 +11046,4 @@ const CwFindCrewmember = ({ handleChange, placeHolder = "Search crew…", requir
|
|
|
10025
11046
|
}, "data-direction": direction, children: [jsxs(CwAlign, { ...alignProps, itemProp: required ? "required" : "", children: [labelProps && (jsx(CwLabel, { ...labelProps, children: labelProps.text })), jsxs("div", { className: "cw-search-input-wrapper", ref: wrapperRef, children: [jsx("input", { ref: inputRef, type: "text", value: inputValue, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: handleInputFocus, placeholder: isInitialLoading ? "Loading…" : placeHolder, disabled: disabled, required: required, autoComplete: "off", "aria-expanded": showDropdown, "aria-haspopup": "listbox", role: "combobox", title: tooltipText }), (isLoading || isInitialLoading) && (jsx("div", { className: "cw-search-input-loading", children: jsx(CwIcon, { iconId: "spinner" }) })), jsx("div", { className: "cw-search-input-icons", children: inputValue && !disabled && !isInitialLoading ? (jsx(CwButton, { type: "button", onClick: handleClear, "aria-label": "Clear selected crewmember", variant: "icon", icon: "close", color: "neutral" })) : (jsx(CwIcon, { iconId: "person" })) })] })] }), renderPanel(jsx("div", { ref: panelRef, className: dropdownStyles.dropdown, style: panelStyle, role: "listbox", children: jsx("ul", { children: options.map((option, index) => (jsx("li", { className: `${dropdownStyles.option}${index === highlightedIndex ? ` ${dropdownStyles.optionFocused}` : ""}`, onClick: () => handleOptionSelect(option.value), onMouseDown: (e) => e.preventDefault(), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": index === highlightedIndex, children: option.text }, option.value))) }) }))] }));
|
|
10026
11047
|
};
|
|
10027
11048
|
|
|
10028
|
-
export { CblDragAndDrop, CwAccordionContainer, CwAlign, CwAnchoredMenu, CwBtnApprove, CwBtnCancel, CwBtnDelete, CwBtnEdit, CwBtnSave, CwButton, CwCard, CwCardList, CwCheck, CwCheckbox, CwCheckboxGroup, CwChip, CwColorPicker, CwConfirmationPopup, CwContextMenu, CwDatePicker, CwDatePickerTemporal, CwDateRangePicker, CwDateTimePicker, CwDateTimePickerCompact, CwDialog, CwDigit, CwDisplayMessage, CwDropdown, CwDropdownFilter, CwExpandable, CwFileUpload, CwFileUploadMultiple, CwFindAirport, CwFindCrewmember, CwGenericTooltip, CwHeadingMain, CwHeadingSecond, CwIcon, CwImageArea, CwImageGallery, CwImageZoom, CwInput, CwInputColor, CwInputDateText, CwInputDatetime, CwInputImage, CwInputNumber, CwInputPhone, CwInputText, CwKeyValueList, CwLabel, CwLoading, CwLoadingSmall, CwMasterDetail, CwMessage, CwMessageManager, CwMessageType, CwModal, CwModalHover, CwModalReportFunctional, CwMultiFilter, CwMultiFilterTag, CwNote, CwOption, CwPopoverButton, CwReportModal, CwScheduler, CwSearchInput, CwSelect, CwSelectList, CwSelectListItems, CwSortableList, CwSortableTable, CwSuperScheduler, CwTable, CwTableGrouped, CwTableServerSide, CwTabs, CwTagSelector, CwTextArea, CwTime, CwTimePicker, CwToggle, CwTooltipManager, CwTooltipNew, CwTreeView, CwWeekdaySelector, DefaultRowHeader, OnClearPinned, OnClickContextMenu, OnClickEvent, OnClickRowEvent, OnClickRowHeader, OnClickUtc, OnDoubleClickEvent, OnDoubleClickRowEvent, OnDragEvent, OnDropCtrlEvent, OnDropEvent, OnEndClickHeaderEvent, OnLeftDragStart, OnMultiClickEvent, OnPinRow, OnRangeClickEvent, OnRightClickEvent, OnRightClickRow, OnRightDragStart, OnStartClickHeaderEvent, OnUnpinRow, PinRowHeader, Resource, Scheduler, SchedulerEvent, SuperScheduler, TooltipConstraintContext, UiEvent, Weekdays, cblEvent, eventIsVisible, filterVisibleEvents, getDefaultDivisions, getEventSizes, itemsToMultiFilterTags, useCwMessage, useSortableList, useSortableTable, useTooltipConstraint };
|
|
11049
|
+
export { CblDragAndDrop, CwAccordionContainer, CwAlign, CwAnchoredMenu, CwBtnApprove, CwBtnCancel, CwBtnDelete, CwBtnEdit, CwBtnSave, CwButton, CwCard, CwCardList, CwCheck, CwCheckbox, CwCheckboxGroup, CwChip, CwColorPicker, CwConfirmationPopup, CwContextMenu, CwDatePicker, CwDatePickerTemporal, CwDateRangePicker, CwDateRangePickerTemporal, CwDateTimePicker, CwDateTimePickerCompact, CwDateTimePickerTemporal, CwDialog, CwDigit, CwDisplayMessage, CwDropdown, CwDropdownFilter, CwExpandable, CwFileUpload, CwFileUploadMultiple, CwFindAirport, CwFindCrewmember, CwGenericTooltip, CwHeadingMain, CwHeadingSecond, CwIcon, CwImageArea, CwImageGallery, CwImageZoom, CwInput, CwInputColor, CwInputDateText, CwInputDatetime, CwInputImage, CwInputNumber, CwInputPhone, CwInputText, CwKeyValueList, CwLabel, CwLoading, CwLoadingSmall, CwMasterDetail, CwMessage, CwMessageManager, CwMessageType, CwModal, CwModalHover, CwModalReportFunctional, CwMultiFilter, CwMultiFilterTag, CwNote, CwOption, CwPopoverButton, CwReportModal, CwScheduler, CwSearchAsync, CwSearchInput, CwSelect, CwSelectList, CwSelectListItems, CwSortableList, CwSortableTable, CwSuperScheduler, CwTable, CwTableGrouped, CwTableServerSide, CwTabs, CwTagSelector, CwTextArea, CwTime, CwTimePicker, CwToggle, CwTooltipManager, CwTooltipNew, CwTreeView, CwWeekdaySelector, DefaultRowHeader, OnClearPinned, OnClickContextMenu, OnClickEvent, OnClickRowEvent, OnClickRowHeader, OnClickUtc, OnDoubleClickEvent, OnDoubleClickRowEvent, OnDragEvent, OnDropCtrlEvent, OnDropEvent, OnEndClickHeaderEvent, OnLeftDragStart, OnMultiClickEvent, OnPinRow, OnRangeClickEvent, OnRightClickEvent, OnRightClickRow, OnRightDragStart, OnStartClickHeaderEvent, OnUnpinRow, PRESET_LIBRARY_TEMPORAL, PinRowHeader, Resource, Scheduler, SchedulerEvent, SuperScheduler, TooltipConstraintContext, UiEvent, Weekdays, cblEvent, eventIsVisible, filterVisibleEvents, getDefaultDivisions, getEventSizes, getPresetsByKeysTemporal, itemsToMultiFilterTags, useCwMessage, useSortableList, useSortableTable, useTooltipConstraint };
|