UNPKG

27.2 kBTypeScriptView Raw
1/*
2 * Copyright 2016 Palantir Technologies, Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17/**
18 * @fileoverview This component is DEPRECATED, and the code is frozen.
19 * All changes & bugfixes should be made to DateInput3 in the datetime2
20 * package instead.
21 */
22
23/* eslint-disable deprecation/deprecation, @blueprintjs/no-deprecated-components, react-hooks/exhaustive-deps */
24
25import classNames from "classnames";
26import * as React from "react";
27import type { DayPickerProps } from "react-day-picker";
28
29import {
30 type ButtonProps,
31 DISPLAYNAME_PREFIX,
32 InputGroup,
33 type InputGroupProps,
34 mergeRefs,
35 Popover,
36 type PopoverClickTargetHandlers,
37 type PopoverTargetProps,
38 type Props,
39 Tag,
40 Utils,
41} from "@blueprintjs/core";
42
43import { Classes, type DateFormatProps, type DatePickerBaseProps } from "../../common";
44import { getFormattedDateString } from "../../common/dateFormatProps";
45import type { DatetimePopoverProps } from "../../common/datetimePopoverProps";
46import { hasMonthChanged, hasTimeChanged, isDateValid, isDayInRange } from "../../common/dateUtils";
47import * as Errors from "../../common/errors";
48import { getCurrentTimezone } from "../../common/getTimezone";
49import { UTC_TIME } from "../../common/timezoneItems";
50import { getTimezoneShortName, isValidTimezone } from "../../common/timezoneNameUtils";
51import {
52 convertLocalDateToTimezoneTime,
53 getDateObjectFromIsoString,
54 getIsoEquivalentWithUpdatedTimezone,
55} from "../../common/timezoneUtils";
56import { DatePicker } from "../date-picker/datePicker";
57import { DatePickerUtils } from "../date-picker/datePickerUtils";
58import type { DatePickerShortcut } from "../shortcuts/shortcuts";
59import { TimezoneSelect } from "../timezone-select/timezoneSelect";
60
61export interface DateInputProps extends DatePickerBaseProps, DateFormatProps, DatetimePopoverProps, Props {
62 /**
63 * Allows the user to clear the selection by clicking the currently selected day.
64 * Passed to `DatePicker` component.
65 *
66 * @default true
67 */
68 canClearSelection?: boolean;
69
70 /**
71 * Text for the reset button in the date picker action bar.
72 * Passed to `DatePicker` component.
73 *
74 * @default "Clear"
75 */
76 clearButtonText?: string;
77
78 /**
79 * Whether the calendar popover should close when a date is selected.
80 *
81 * @default true
82 */
83 closeOnSelection?: boolean;
84
85 /**
86 * The default timezone selected. Defaults to the user's local timezone.
87 *
88 * Mutually exclusive with `timezone` prop.
89 *
90 * @see https://www.iana.org/time-zones
91 */
92 defaultTimezone?: string;
93
94 /**
95 * Whether to disable the timezone select.
96 *
97 * @default false
98 */
99 disableTimezoneSelect?: boolean;
100
101 /**
102 * The default date to be used in the component when uncontrolled, represented as an ISO string.
103 */
104 defaultValue?: string;
105
106 /**
107 * Whether the date input is non-interactive.
108 *
109 * @default false
110 */
111 disabled?: boolean;
112
113 /**
114 * Whether the component should take up the full width of its container.
115 */
116 fill?: boolean;
117
118 /**
119 * Props to pass to the [InputGroup component](#core/components/input-group).
120 *
121 * Some properties are unavailable:
122 * - `inputProps.value`: use `value` instead
123 * - `inputProps.disabled`: use `disabled` instead
124 * - `inputProps.type`: cannot be customized, always set to "text"
125 *
126 * Note that `inputProps.tagName` will override `popoverProps.targetTagName`.
127 */
128 inputProps?: Partial<Omit<InputGroupProps, "disabled" | "type" | "value">>;
129
130 /**
131 * Callback invoked whenever the date or timezone has changed.
132 *
133 * @param newDate ISO string or `null` (if the date is invalid or text input has been cleared)
134 * @param isUserChange `true` if the user clicked on a date in the calendar, changed the input value,
135 * or cleared the selection; `false` if the date was changed by changing the month or year.
136 */
137 onChange?: (newDate: string | null, isUserChange: boolean) => void;
138
139 /**
140 * Called when the user finishes typing in a new date and the date causes an error state.
141 * If the date is invalid, `new Date(undefined)` will be returned. If the date is out of range,
142 * the out of range date will be returned (`onChange` is not called in this case).
143 */
144 onError?: (errorDate: Date) => void;
145
146 /**
147 * Callback invoked when the user selects a timezone.
148 *
149 * @param timezone the new timezone's IANA code
150 */
151 onTimezoneChange?: (timezone: string) => void;
152
153 /**
154 * Element to render on right side of input.
155 */
156 rightElement?: React.JSX.Element;
157
158 /**
159 * Whether the bottom bar displaying "Today" and "Clear" buttons should be shown below the calendar.
160 *
161 * @default false
162 */
163 showActionsBar?: boolean;
164
165 /**
166 * Whether to show the timezone select dropdown on the right side of the input.
167 * If `timePrecision` is undefined, this will always be false.
168 *
169 * @default false
170 */
171 showTimezoneSelect?: boolean;
172
173 /**
174 * Whether shortcuts to quickly select a date are displayed or not.
175 * If `true`, preset shortcuts will be displayed.
176 * If `false`, no shortcuts will be displayed.
177 * If an array is provided, the custom shortcuts will be displayed.
178 *
179 * @default false
180 */
181 shortcuts?: boolean | DatePickerShortcut[];
182
183 /**
184 * The currently selected timezone UTC identifier, e.g. "Pacific/Honolulu".
185 *
186 * If you set this prop, the TimezoneSelect will behave in a controlled manner and you are responsible
187 * for updating this value using the `onTimezoneChange` callback.
188 *
189 * Mutually exclusive with `defaultTimezone` prop.
190 *
191 * @see https://www.iana.org/time-zones
192 */
193 timezone?: string;
194
195 /**
196 * Text for the today button in the date picker action bar.
197 * Passed to `DatePicker` component.
198 *
199 * @default "Today"
200 */
201 todayButtonText?: string;
202
203 /** An ISO string representing the selected time. */
204 value?: string | null;
205}
206
207const timezoneSelectButtonProps: Partial<ButtonProps> = {
208 fill: false,
209 minimal: true,
210 outlined: true,
211};
212
213const INVALID_DATE = new Date(undefined!);
214const DEFAULT_MAX_DATE = DatePickerUtils.getDefaultMaxDate();
215const DEFAULT_MIN_DATE = DatePickerUtils.getDefaultMinDate();
216
217/**
218 * Date input component.
219 *
220 * @see https://blueprintjs.com/docs/#datetime/date-input
221 * @deprecated use `{ DateInput3 } from "@blueprintjs/datetime2"` instead
222 */
223export const DateInput: React.FC<DateInputProps> = React.memo(function _DateInput(props) {
224 const {
225 defaultTimezone,
226 defaultValue,
227 disableTimezoneSelect,
228 fill,
229 inputProps = {},
230 // defaults duplicated here for TypeScript convenience
231 maxDate = DEFAULT_MAX_DATE,
232 minDate = DEFAULT_MIN_DATE,
233 placeholder,
234 popoverProps = {},
235 popoverRef,
236 showTimezoneSelect,
237 timePrecision,
238 timezone,
239 value,
240 ...datePickerProps
241 } = props;
242
243 // Refs
244 // ------------------------------------------------------------------------
245
246 const inputRef = React.useRef<HTMLInputElement | null>(null);
247 const popoverContentRef = React.useRef<HTMLDivElement | null>(null);
248 const popoverId = Utils.uniqueId("date-picker");
249
250 // State
251 // ------------------------------------------------------------------------
252
253 const [isOpen, setIsOpen] = React.useState(false);
254 const [timezoneValue, setTimezoneValue] = React.useState(getInitialTimezoneValue(props));
255 const valueFromProps = React.useMemo(
256 () => getDateObjectFromIsoString(value, timezoneValue),
257 [timezoneValue, value],
258 );
259 const isControlled = valueFromProps !== undefined;
260 const defaultValueFromProps = React.useMemo(
261 () => getDateObjectFromIsoString(defaultValue, timezoneValue),
262 [defaultValue, defaultTimezone],
263 );
264 const [valueAsDate, setValue] = React.useState<Date | null>(isControlled ? valueFromProps : defaultValueFromProps!);
265
266 const [selectedShortcutIndex, setSelectedShortcutIndex] = React.useState<number | undefined>(undefined);
267 const [isInputFocused, setIsInputFocused] = React.useState(false);
268
269 // rendered as the text input's value
270 const formattedDateString = React.useMemo(() => {
271 return valueAsDate === null ? undefined : getFormattedDateString(valueAsDate, props);
272 }, [
273 valueAsDate,
274 minDate,
275 maxDate,
276 // HACKHACK: ESLint false positive
277 // eslint-disable-next-line @typescript-eslint/unbound-method
278 props.formatDate,
279 props.locale,
280 props.invalidDateMessage,
281 props.outOfRangeMessage,
282 ]);
283 const [inputValue, setInputValue] = React.useState(formattedDateString ?? undefined);
284
285 const isErrorState =
286 valueAsDate != null && (!isDateValid(valueAsDate) || !isDayInRange(valueAsDate, [minDate, maxDate]));
287
288 // Effects
289 // ------------------------------------------------------------------------
290
291 React.useEffect(() => {
292 if (isControlled) {
293 setValue(valueFromProps);
294 }
295 }, [valueFromProps]);
296
297 React.useEffect(() => {
298 // uncontrolled mode, updating initial timezone value
299 if (defaultTimezone !== undefined && isValidTimezone(defaultTimezone)) {
300 setTimezoneValue(defaultTimezone);
301 }
302 }, [defaultTimezone]);
303
304 React.useEffect(() => {
305 // controlled mode, updating timezone value
306 if (timezone !== undefined && isValidTimezone(timezone)) {
307 setTimezoneValue(timezone);
308 }
309 }, [timezone]);
310
311 React.useEffect(() => {
312 if (isControlled && !isInputFocused) {
313 setInputValue(formattedDateString);
314 }
315 }, [formattedDateString]);
316
317 // Popover contents (date picker)
318 // ------------------------------------------------------------------------
319
320 const handlePopoverClose = React.useCallback((e: React.SyntheticEvent<HTMLElement>) => {
321 popoverProps.onClose?.(e);
322 setIsOpen(false);
323 }, []);
324
325 const handleDateChange = React.useCallback(
326 (newDate: Date | null, isUserChange: boolean, didSubmitWithEnter = false) => {
327 const prevDate = valueAsDate;
328
329 if (newDate === null) {
330 if (!isControlled && !didSubmitWithEnter) {
331 // user clicked on current day in the calendar, so we should clear the input when uncontrolled
332 setInputValue("");
333 }
334 props.onChange?.(null, isUserChange);
335 return;
336 }
337
338 // this change handler was triggered by a change in month, day, or (if
339 // enabled) time. for UX purposes, we want to close the popover only if
340 // the user explicitly clicked a day within the current month.
341 const newIsOpen =
342 !isUserChange ||
343 !props.closeOnSelection ||
344 (prevDate != null &&
345 (hasMonthChanged(prevDate, newDate) ||
346 (timePrecision !== undefined && hasTimeChanged(prevDate, newDate))));
347
348 // if selecting a date via click or Tab, the input will already be
349 // blurred by now, so sync isInputFocused to false. if selecting via
350 // Enter, setting isInputFocused to false won't do anything by itself,
351 // plus we want the field to retain focus anyway.
352 // (note: spelling out the ternary explicitly reads more clearly.)
353 const newIsInputFocused = didSubmitWithEnter ? true : false;
354
355 if (isControlled) {
356 setIsInputFocused(newIsInputFocused);
357 setIsOpen(newIsOpen);
358 } else {
359 const newFormattedDateString = getFormattedDateString(newDate, props);
360 setIsInputFocused(newIsInputFocused);
361 setIsOpen(newIsOpen);
362 setValue(newDate);
363 setInputValue(newFormattedDateString);
364 }
365
366 const newIsoDateString = getIsoEquivalentWithUpdatedTimezone(newDate, timezoneValue, timePrecision);
367 props.onChange?.(newIsoDateString, isUserChange);
368 },
369 [props.onChange, timezoneValue, timePrecision, valueAsDate],
370 );
371
372 const dayPickerProps: DayPickerProps = {
373 ...props.dayPickerProps,
374 onDayKeyDown: (day, modifiers, e) => {
375 props.dayPickerProps?.onDayKeyDown?.(day, modifiers, e);
376 },
377 onMonthChange: (month: Date) => {
378 props.dayPickerProps?.onMonthChange?.(month);
379 },
380 };
381
382 const handleShortcutChange = React.useCallback((_: DatePickerShortcut, index: number) => {
383 setSelectedShortcutIndex(index);
384 }, []);
385
386 const handleStartFocusBoundaryFocusIn = React.useCallback((e: React.FocusEvent<HTMLDivElement>) => {
387 if (popoverContentRef.current?.contains(getRelatedTargetWithFallback(e))) {
388 // Not closing Popover to allow user to freely switch between manually entering a date
389 // string in the input and selecting one via the Popover
390 inputRef.current?.focus();
391 } else {
392 getKeyboardFocusableElements(popoverContentRef).shift()?.focus();
393 }
394 }, []);
395
396 const handleEndFocusBoundaryFocusIn = React.useCallback((e: React.FocusEvent<HTMLDivElement>) => {
397 if (popoverContentRef.current?.contains(getRelatedTargetWithFallback(e))) {
398 inputRef.current?.focus();
399 handlePopoverClose(e);
400 } else {
401 getKeyboardFocusableElements(popoverContentRef).pop()?.focus();
402 }
403 }, []);
404
405 // React's onFocus prop listens to the focusin browser event under the hood, so it's safe to
406 // provide it the focusIn event handlers instead of using a ref and manually adding the
407 // event listeners ourselves.
408 const popoverContent = (
409 <div ref={popoverContentRef} role="dialog" aria-label="date picker" id={popoverId}>
410 <div onFocus={handleStartFocusBoundaryFocusIn} tabIndex={0} />
411 <DatePicker
412 {...datePickerProps}
413 dayPickerProps={dayPickerProps}
414 maxDate={maxDate}
415 minDate={minDate}
416 onChange={handleDateChange}
417 onShortcutChange={handleShortcutChange}
418 selectedShortcutIndex={selectedShortcutIndex}
419 timePrecision={timePrecision}
420 // the rest of this component handles invalid dates gracefully (to show error messages),
421 // but DatePicker does not, so we must take care to filter those out
422 value={isErrorState ? null : valueAsDate}
423 />
424 <div onFocus={handleEndFocusBoundaryFocusIn} tabIndex={0} />
425 </div>
426 );
427
428 // Timezone select
429 // ------------------------------------------------------------------------
430
431 // we need a date which is guaranteed to be non-null here; if necessary,
432 // we use today's date and shift it to the desired/current timezone
433 const tzSelectDate = React.useMemo(
434 () =>
435 valueAsDate != null && isDateValid(valueAsDate)
436 ? valueAsDate
437 : convertLocalDateToTimezoneTime(new Date(), timezoneValue),
438 [timezoneValue, valueAsDate],
439 );
440
441 const isTimezoneSelectHidden = timePrecision === undefined || showTimezoneSelect === false;
442 const isTimezoneSelectDisabled = props.disabled || disableTimezoneSelect;
443
444 const handleTimezoneChange = React.useCallback(
445 (newTimezone: string) => {
446 if (timezone === undefined) {
447 // uncontrolled timezone
448 setTimezoneValue(newTimezone);
449 }
450 props.onTimezoneChange?.(newTimezone);
451
452 if (valueAsDate != null) {
453 const newDateString = getIsoEquivalentWithUpdatedTimezone(valueAsDate, newTimezone, timePrecision);
454 props.onChange?.(newDateString, true);
455 }
456 },
457 [props.onChange, valueAsDate, timePrecision],
458 );
459
460 const maybeTimezonePicker = isTimezoneSelectHidden ? undefined : (
461 <TimezoneSelect
462 buttonProps={timezoneSelectButtonProps}
463 className={Classes.DATE_INPUT_TIMEZONE_SELECT}
464 date={tzSelectDate}
465 disabled={isTimezoneSelectDisabled}
466 onChange={handleTimezoneChange}
467 value={timezoneValue}
468 >
469 <Tag
470 interactive={!isTimezoneSelectDisabled}
471 minimal={true}
472 rightIcon={isTimezoneSelectDisabled ? undefined : "caret-down"}
473 >
474 {getTimezoneShortName(timezoneValue, tzSelectDate)}
475 </Tag>
476 </TimezoneSelect>
477 );
478
479 // Text input
480 // ------------------------------------------------------------------------
481
482 const parseDate = React.useCallback(
483 (dateString: string) => {
484 if (dateString === props.outOfRangeMessage || dateString === props.invalidDateMessage) {
485 return null;
486 }
487 const newDate = props.parseDate(dateString, props.locale);
488 return newDate === false ? INVALID_DATE : newDate;
489 },
490 // HACKHACK: ESLint false positive
491 // eslint-disable-next-line @typescript-eslint/unbound-method
492 [props.outOfRangeMessage, props.invalidDateMessage, props.parseDate, props.locale],
493 );
494
495 const handleInputFocus = React.useCallback(
496 (e: React.FocusEvent<HTMLInputElement>) => {
497 setIsInputFocused(true);
498 setIsOpen(true);
499 setInputValue(formattedDateString);
500 props.inputProps?.onFocus?.(e);
501 },
502 [formattedDateString, props.inputProps?.onFocus],
503 );
504
505 const handleInputBlur = React.useCallback(
506 (e: React.FocusEvent<HTMLInputElement>) => {
507 if (inputValue == null || valueAsDate == null) {
508 return;
509 }
510
511 const date = parseDate(inputValue);
512
513 if (
514 inputValue.length > 0 &&
515 inputValue !== formattedDateString &&
516 (!isDateValid(date) || !isDayInRange(date, [minDate, maxDate]))
517 ) {
518 if (isControlled) {
519 setIsInputFocused(false);
520 } else {
521 setIsInputFocused(false);
522 setValue(date);
523 setInputValue(undefined);
524 }
525
526 if (date === null) {
527 props.onChange?.(null, true);
528 } else {
529 props.onError?.(date);
530 }
531 } else {
532 if (inputValue.length === 0) {
533 setIsInputFocused(false);
534 setValue(null);
535 setInputValue(undefined);
536 } else {
537 setIsInputFocused(false);
538 }
539 }
540 props.inputProps?.onBlur?.(e);
541 },
542 [
543 parseDate,
544 formattedDateString,
545 inputValue,
546 valueAsDate,
547 minDate,
548 maxDate,
549 props.onChange,
550 props.onError,
551 props.inputProps?.onBlur,
552 ],
553 );
554
555 const handleInputChange = React.useCallback(
556 (e: React.ChangeEvent<HTMLInputElement>) => {
557 const valueString = (e.target as HTMLInputElement).value;
558 const inputValueAsDate = parseDate(valueString);
559
560 if (isDateValid(inputValueAsDate) && isDayInRange(inputValueAsDate, [minDate, maxDate])) {
561 if (isControlled) {
562 setInputValue(valueString);
563 } else {
564 setValue(inputValueAsDate);
565 setInputValue(valueString);
566 }
567 const newIsoDateString = getIsoEquivalentWithUpdatedTimezone(
568 inputValueAsDate,
569 timezoneValue,
570 timePrecision,
571 );
572 props.onChange?.(newIsoDateString, true);
573 } else {
574 if (valueString.length === 0) {
575 props.onChange?.(null, true);
576 }
577 setValue(inputValueAsDate);
578 setInputValue(valueString);
579 }
580 props.inputProps?.onChange?.(e);
581 },
582 [minDate, maxDate, timezoneValue, timePrecision, parseDate, props.onChange, props.inputProps?.onChange],
583 );
584
585 const handleInputClick = React.useCallback(
586 (e: React.MouseEvent<HTMLInputElement>) => {
587 // stop propagation to the Popover's internal handleTargetClick handler;
588 // otherwise, the popover will flicker closed as soon as it opens.
589 e.stopPropagation();
590 props.inputProps?.onClick?.(e);
591 },
592 [props.inputProps?.onClick],
593 );
594
595 const handleInputKeyDown = React.useCallback(
596 (e: React.KeyboardEvent<HTMLInputElement>) => {
597 if (e.key === "Tab" && e.shiftKey) {
598 // close popover on SHIFT+TAB key press
599 handlePopoverClose(e);
600 } else if (e.key === "Tab" && isOpen) {
601 getKeyboardFocusableElements(popoverContentRef).shift()?.focus();
602 // necessary to prevent focusing the second focusable element
603 e.preventDefault();
604 } else if (e.key === "Escape") {
605 setIsOpen(false);
606 inputRef.current?.blur();
607 } else if (e.key === "Enter" && inputValue != null) {
608 const nextDate = parseDate(inputValue);
609 if (isDateValid(nextDate)) {
610 handleDateChange(nextDate, true, true);
611 }
612 }
613
614 props.inputProps?.onKeyDown?.(e);
615 },
616 [inputValue, parseDate, props.inputProps?.onKeyDown],
617 );
618
619 // Main render
620 // ------------------------------------------------------------------------
621
622 const shouldShowErrorStyling =
623 !isInputFocused || inputValue === props.outOfRangeMessage || inputValue === props.invalidDateMessage;
624
625 // We use the renderTarget API to flatten the rendered DOM and make it easier to implement features like the "fill" prop.
626 const renderTarget = React.useCallback(
627 ({ isOpen: targetIsOpen, ref, ...targetProps }: PopoverTargetProps & PopoverClickTargetHandlers) => {
628 return (
629 <InputGroup
630 autoComplete="off"
631 className={classNames(targetProps.className, inputProps.className)}
632 intent={shouldShowErrorStyling && isErrorState ? "danger" : "none"}
633 placeholder={placeholder}
634 rightElement={
635 <>
636 {props.rightElement}
637 {maybeTimezonePicker}
638 </>
639 }
640 tagName={popoverProps.targetTagName}
641 type="text"
642 role="combobox"
643 {...targetProps}
644 {...inputProps}
645 aria-controls={popoverId}
646 aria-expanded={targetIsOpen}
647 disabled={props.disabled}
648 fill={fill}
649 inputRef={mergeRefs(ref, inputRef, props.inputProps?.inputRef ?? null)}
650 onBlur={handleInputBlur}
651 onChange={handleInputChange}
652 onClick={handleInputClick}
653 onFocus={handleInputFocus}
654 onKeyDown={handleInputKeyDown}
655 value={(isInputFocused ? inputValue : formattedDateString) ?? ""}
656 />
657 );
658 },
659 [
660 fill,
661 formattedDateString,
662 inputValue,
663 isInputFocused,
664 isTimezoneSelectDisabled,
665 isTimezoneSelectHidden,
666 placeholder,
667 shouldShowErrorStyling,
668 timezoneValue,
669 props.disabled,
670 props.inputProps,
671 props.rightElement,
672 ],
673 );
674
675 // N.B. no need to set `fill` since that is unused with the `renderTarget` API
676 return (
677 <Popover
678 isOpen={isOpen && !props.disabled}
679 {...popoverProps}
680 autoFocus={false}
681 className={classNames(Classes.DATE_INPUT, popoverProps.className, props.className)}
682 content={popoverContent}
683 enforceFocus={false}
684 onClose={handlePopoverClose}
685 popoverClassName={classNames(Classes.DATE_INPUT_POPOVER, popoverProps.popoverClassName)}
686 ref={popoverRef}
687 renderTarget={renderTarget}
688 />
689 );
690});
691DateInput.displayName = `${DISPLAYNAME_PREFIX}.DateInput`;
692DateInput.defaultProps = {
693 closeOnSelection: true,
694 disabled: false,
695 invalidDateMessage: "Invalid date",
696 maxDate: DEFAULT_MAX_DATE,
697 minDate: DEFAULT_MIN_DATE,
698 outOfRangeMessage: "Out of range",
699 reverseMonthAndYearMenus: false,
700};
701
702function getInitialTimezoneValue({ defaultTimezone, timezone }: DateInputProps) {
703 if (timezone !== undefined) {
704 // controlled mode
705 if (isValidTimezone(timezone)) {
706 return timezone;
707 } else {
708 console.error(Errors.DATEINPUT_INVALID_TIMEZONE);
709 return UTC_TIME.ianaCode;
710 }
711 } else if (defaultTimezone !== undefined) {
712 // uncontrolled mode with initial value
713 if (isValidTimezone(defaultTimezone)) {
714 return defaultTimezone;
715 } else {
716 console.error(Errors.DATEINPUT_INVALID_DEFAULT_TIMEZONE);
717 return UTC_TIME.ianaCode;
718 }
719 } else {
720 // uncontrolled mode
721 return getCurrentTimezone();
722 }
723}
724
725function getRelatedTargetWithFallback(e: React.FocusEvent<HTMLElement>) {
726 return (e.relatedTarget ?? Utils.getActiveElement(e.currentTarget)) as HTMLElement;
727}
728
729function getKeyboardFocusableElements(popoverContentRef: React.MutableRefObject<HTMLDivElement | null>) {
730 if (popoverContentRef.current === null) {
731 return [];
732 }
733
734 const elements: HTMLElement[] = Array.from(
735 popoverContentRef.current.querySelectorAll("button:not([disabled]),input,[tabindex]:not([tabindex='-1'])"),
736 );
737 // Remove focus boundary div elements
738 elements.pop();
739 elements.shift();
740 return elements;
741}