@codecademy/gamut

68.6.268.6.3-alpha.92d8ae.0
dist/DatePicker/DatePickerInput/DatePickerInputShell/index.js
+dist/DatePicker/DatePickerInput/DatePickerInputShell/index.jsNew file
+219
Index: package/dist/DatePicker/DatePickerInput/DatePickerInputShell/index.js
===================================================================
--- package/dist/DatePicker/DatePickerInput/DatePickerInputShell/index.js
+++ package/dist/DatePicker/DatePickerInput/DatePickerInputShell/index.js
@@ -0,0 +1,219 @@
+import { MiniCalendarIcon } from '@codecademy/gamut-icons';
+import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
+import { FlexBox } from '../../../Box';
+import { IconButton } from '../../../Button';
+import { isSameDay } from '../../DatePickerCalendar/Calendar/utils/dateGrid';
+import { handleDateSelectRange } from '../../DatePickerCalendar/utils/dateSelect';
+import { useDatePicker } from '../../DatePickerContext';
+import { DatePickerInputSegment } from '../Segment';
+import { SegmentLiteral } from '../Segment/elements';
+import { getDateSegmentsFromDate, getSegmentValidationState, parseSegmentsToDate, resolveSegmentsOnBlur } from '../Segment/utils';
+import { DatePickerInputShellContainer, DatePickerInputShellError, DatePickerInputShellErrorSpacer, DatePickerInputShellField, SegmentedShell } from './elements';
+import { formatDateISO8601DateOnly, getDateFieldOrder, getDateFormatLayout } from './utils';
+import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
+export const DatePickerInputShell = /*#__PURE__*/forwardRef(({
+  disabled,
+  error,
+  form,
+  labelledById,
+  name,
+  rangePart,
+  shellId,
+  size = 'base',
+  ...rest
+}, ref) => {
+  const context = useDatePicker();
+  const {
+    mode,
+    openCalendar,
+    focusCalendar,
+    locale,
+    isCalendarOpen,
+    disableDate,
+    translations
+  } = context;
+  const isRange = mode === 'range';
+  const endDate = isRange ? context.endDate : null;
+  const date = isRange ? context.startDate : context.selectedDate;
+  const buttonRef = useRef(null);
+  const errorId = useId();
+  const {
+    layout,
+    fieldOrder
+  } = useMemo(() => {
+    const layout = getDateFormatLayout(locale);
+    return {
+      layout,
+      fieldOrder: getDateFieldOrder(layout)
+    };
+  }, [locale]);
+  const boundDate = isRange && rangePart === 'end' ? endDate : date;
+  const segmentsFromBound = useMemo(() => getDateSegmentsFromDate(boundDate), [boundDate]);
+  const [segments, setSegments] = useState(segmentsFromBound);
+  const [validationError, setValidationError] = useState(null);
+  const parsedForHidden = parseSegmentsToDate(segments);
+  const hiddenValue = parsedForHidden ? formatDateISO8601DateOnly(parsedForHidden) : '';
+  const showError = Boolean(error) || Boolean(validationError);
+  const isInputFocusedRef = useRef(false);
+  const segmentsRef = useRef(segments);
+  segmentsRef.current = segments;
+  const containerRef = useRef(null);
+  const segmentElRefs = useRef({});
+  const assignSegmentRef = useCallback((field, el) => {
+    segmentElRefs.current[field] = el;
+  }, []);
+  const onSiblingSegmentFocus = useCallback(field => {
+    segmentElRefs.current[field]?.focus();
+  }, []);
+  const shellRef = useCallback(el => {
+    containerRef.current = el;
+    if (typeof ref === 'function') ref(el);else if (ref !== null) ref.current = el;
+  }, [ref]);
+  useEffect(() => {
+    if (!isInputFocusedRef.current) {
+      setSegments(segmentsFromBound);
+      setValidationError(null);
+    }
+  }, [segmentsFromBound]);
+  const commitParsedDate = useCallback(parsed => {
+    if (!isRange) {
+      context.onSelection(parsed);
+    }
+    if (isRange && rangePart) {
+      handleDateSelectRange({
+        date: parsed,
+        activeRangePart: rangePart,
+        startDate: date,
+        endDate,
+        onRangeSelection: context.onRangeSelection,
+        disableDate
+      });
+    }
+  }, [isRange, rangePart, context, endDate, date, disableDate]);
+  const clearSelection = useCallback(() => {
+    if (!isRange) {
+      context.onSelection(null);
+    }
+    if (isRange && rangePart) {
+      if (rangePart === 'start') context.onRangeSelection(null, endDate);else context.onRangeSelection(date, null);
+    }
+  }, [isRange, rangePart, context, endDate, date]);
+  const onSegmentChange = useCallback(next => {
+    const validation = getSegmentValidationState(next);
+    if (validation?.parsedDate) {
+      setValidationError(null);
+      commitParsedDate(validation.parsedDate);
+      return;
+    }
+    if (validation?.isInvalid) {
+      setValidationError(translations.invalidDateError);
+      return;
+    }
+    if (!next.month && !next.day && !next.year) {
+      setValidationError(null);
+      clearSelection();
+      return;
+    }
+    setValidationError(null);
+  }, [clearSelection, commitParsedDate, translations.invalidDateError]);
+  const onContainerBlur = useCallback(e => {
+    if (containerRef.current?.contains(e.relatedTarget)) return;
+    isInputFocusedRef.current = false;
+    const resolution = resolveSegmentsOnBlur(segmentsRef.current, boundDate);
+    setValidationError(resolution.isInvalid ? translations.invalidDateError : null);
+    setSegments(resolution.segments);
+    if (resolution.shouldClear) {
+      clearSelection();
+    } else if (resolution.parsedDate && isCalendarOpen && !isSameDay(resolution.parsedDate, boundDate)) {
+      commitParsedDate(resolution.parsedDate);
+    }
+  }, [boundDate, clearSelection, commitParsedDate, isCalendarOpen, translations.invalidDateError]);
+  const setActiveRangePartForField = useCallback(() => {
+    if (isRange && rangePart) context.setActiveRangePart(rangePart);
+  }, [isRange, rangePart, context]);
+  const onSegmentFocus = useCallback(() => {
+    isInputFocusedRef.current = true;
+    setActiveRangePartForField();
+  }, [setActiveRangePartForField]);
+  const onShellFocus = useCallback(() => {
+    setActiveRangePartForField();
+  }, [setActiveRangePartForField]);
+  const onShellClick = useCallback(() => {
+    if (disabled) return;
+    setActiveRangePartForField();
+    openCalendar();
+  }, [disabled, setActiveRangePartForField, openCalendar]);
+  const onSegmentAltArrowDown = useCallback(() => {
+    if (!isCalendarOpen) openCalendar();
+    focusCalendar();
+  }, [isCalendarOpen, openCalendar, focusCalendar]);
+  return /*#__PURE__*/_jsxs(DatePickerInputShellContainer, {
+    children: [/*#__PURE__*/_jsxs(DatePickerInputShellField, {
+      children: [/*#__PURE__*/_jsxs(SegmentedShell, {
+        "aria-describedby": validationError ? errorId : undefined,
+        "aria-labelledby": labelledById,
+        id: shellId,
+        inputSize: size,
+        ref: shellRef,
+        role: "group",
+        variant: showError ? 'error' : 'default',
+        onBlur: onContainerBlur,
+        onClick: onShellClick,
+        onFocus: onShellFocus,
+        ...rest,
+        children: [/*#__PURE__*/_jsx(FlexBox, {
+          alignItems: "center",
+          justifyContent: "center",
+          children: layout.map((item, index) => {
+            if (item.kind === 'literal') {
+              return /*#__PURE__*/_jsx(SegmentLiteral, {
+                "aria-hidden": true
+                // eslint-disable-next-line react/no-array-index-key
+                ,
+                children: `${item.text}`
+              }, `literal-${item.text}-${index}`);
+            }
+            const idx = fieldOrder.indexOf(item.field);
+            const prevField = idx > 0 ? fieldOrder[idx - 1] : null;
+            const nextField = idx < fieldOrder.length - 1 ? fieldOrder[idx + 1] : null;
+            return /*#__PURE__*/_jsx(DatePickerInputSegment, {
+              applySegments: onSegmentChange,
+              assignSegmentRef: assignSegmentRef,
+              disabled: !!disabled,
+              error: showError,
+              field: item.field,
+              nextField: nextField,
+              prevField: prevField,
+              segments: segments,
+              setSegments: setSegments,
+              onAltArrowDown: onSegmentAltArrowDown,
+              onFocus: onSegmentFocus,
+              onSiblingFocus: onSiblingSegmentFocus
+            }, item.field);
+          })
+        }), /*#__PURE__*/_jsx("input", {
+          "aria-hidden": true,
+          form: form,
+          name: name ?? 'datePickerInput',
+          tabIndex: -1,
+          type: "hidden",
+          value: hiddenValue
+        }), /*#__PURE__*/_jsx(IconButton, {
+          mx: 4,
+          icon: MiniCalendarIcon,
+          size: "small",
+          tip: translations.openCalendarLabel,
+          ref: buttonRef,
+          onClick: () => buttonRef.current?.blur()
+        })]
+      }), validationError ? /*#__PURE__*/_jsx(DatePickerInputShellError, {
+        "aria-live": "polite",
+        id: errorId,
+        role: "alert",
+        children: validationError
+      }) : null]
+    }), /*#__PURE__*/_jsx(DatePickerInputShellErrorSpacer, {
+      "aria-hidden": true
+    })]
+  });
+});
\ No newline at end of file