@codecademy/gamut

68.2.268.2.3-alpha.02a91b.0
dist/DatePicker/DatePickerInput/index.js
+dist/DatePicker/DatePickerInput/index.jsNew file
+187
Index: package/dist/DatePicker/DatePickerInput/index.js
===================================================================
--- package/dist/DatePicker/DatePickerInput/index.js
+++ package/dist/DatePicker/DatePickerInput/index.js
@@ -0,0 +1,187 @@
+import { MiniCalendarIcon } from '@codecademy/gamut-icons';
+import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
+import { FlexBox } from '../../Box';
+import { FormGroup } from '../../Form/elements/FormGroup';
+import { useDatePicker } from '../DatePickerContext';
+import { SegmentedShell } from './elements';
+import { DatePickerInputSegment, getDateSegmentsFromDate, normalizeSegmentValues, parseSegmentsToDate, SegmentLiteral } from './Segment';
+import { formatDateISO8601DateOnly, getDateFieldOrder, getDateFormatLayout } from './utils';
+
+/**
+ * Props for DatePickerInput. When used inside DatePicker, only overrides (e.g. placeholder, label).
+ * In range mode, use rangePart to bind to start or end date. When outside DatePicker, pass value, onChange, etc.
+ */
+import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
+export const DatePickerInput = /*#__PURE__*/forwardRef(({
+  disabled,
+  error,
+  form,
+  label,
+  name,
+  rangePart,
+  size = 'base',
+  ...rest
+}, ref) => {
+  const context = useDatePicker();
+  if (context == null) {
+    throw new Error('DatePickerInput must be used inside a DatePicker (it reads shared state from context).');
+  }
+  const {
+    mode,
+    startOrSelectedDate,
+    setSelection,
+    openCalendar,
+    focusCalendarGrid,
+    locale,
+    isCalendarOpen,
+    translations
+  } = context;
+  const isRange = mode === 'range';
+  const endDate = isRange ? context.endDate : null;
+  const inputID = useId();
+  const inputId = `datepicker-input-${inputID.replace(/:/g, '')}`;
+  const {
+    layout,
+    fieldOrder
+  } = useMemo(() => {
+    const layout = getDateFormatLayout(locale);
+    return {
+      layout,
+      fieldOrder: getDateFieldOrder(layout)
+    };
+  }, [locale]);
+  const firstField = fieldOrder[0];
+  const firstFieldId = `${inputId}-${firstField}`;
+  const defaultLabel = !isRange ? translations.dateLabel : rangePart === 'end' ? translations.endDateLabel : translations.startDateLabel;
+  const boundDate = isRange && rangePart === 'end' ? endDate : startOrSelectedDate;
+  const segmentsFromBound = useCallback(() => getDateSegmentsFromDate(boundDate), [boundDate]);
+  const [segments, setSegments] = useState(segmentsFromBound);
+  const parsedForHidden = parseSegmentsToDate(segments);
+  const hiddenValue = parsedForHidden ? formatDateISO8601DateOnly(parsedForHidden) : '';
+  const isInputFocusedRef = useRef(false);
+  const containerRef = useRef(null);
+  const setShellRef = 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());
+    }
+  }, [segmentsFromBound]);
+  const commitParsedDate = useCallback(parsed => {
+    if (isRange && rangePart) {
+      if (rangePart === 'start') setSelection(parsed, endDate);else setSelection(startOrSelectedDate, parsed);
+    } else setSelection(parsed);
+  }, [isRange, rangePart, setSelection, endDate, startOrSelectedDate]);
+  const clearSelection = useCallback(() => {
+    if (isRange && rangePart) {
+      if (rangePart === 'start') setSelection(null, endDate);else setSelection(startOrSelectedDate, null);
+    } else setSelection(null);
+  }, [isRange, rangePart, setSelection, endDate, startOrSelectedDate]);
+  const applySegments = useCallback(next => {
+    const parsed = parseSegmentsToDate(next);
+    if (parsed) commitParsedDate(parsed);else if (!next.month && !next.day && !next.year) clearSelection();
+  }, [clearSelection, commitParsedDate]);
+  const handleContainerBlur = e => {
+    if (containerRef.current?.contains(e.relatedTarget)) return;
+    isInputFocusedRef.current = false;
+    setSegments(prev => {
+      const normalized = normalizeSegmentValues(prev);
+      const parsed = parseSegmentsToDate(normalized);
+      if (parsed) {
+        commitParsedDate(parsed);
+        return normalized;
+      }
+      if (!normalized.month && !normalized.day && !normalized.year) {
+        clearSelection();
+        return getDateSegmentsFromDate(null);
+      }
+      return segmentsFromBound();
+    });
+  };
+  const handleContainerFocus = () => {
+    isInputFocusedRef.current = true;
+  };
+  const handleSegmentFocus = () => {
+    handleContainerFocus();
+    if (isRange && rangePart) context.setActiveRangePart(rangePart);
+  };
+  const handleOpenCalendar = () => {
+    if (!disabled) openCalendar({
+      moveFocusIntoCalendar: false
+    });
+  };
+  const focusOrOpenCalendarGrid = () => {
+    if (isCalendarOpen) focusCalendarGrid();else openCalendar({
+      moveFocusIntoCalendar: true
+    });
+  };
+  return /*#__PURE__*/_jsx(FormGroup, {
+    htmlFor: firstFieldId,
+    isSoloField: true,
+    label: label ?? defaultLabel,
+    mb: 0,
+    pb: 0,
+    spacing: "tight",
+    width: "fit-content",
+    children: /*#__PURE__*/_jsxs(SegmentedShell, {
+      inputSize: size === 'small' ? 'small' : 'base',
+      ref: setShellRef,
+      role: "group",
+      variant: error ? 'error' : undefined,
+      width: "113px",
+      onBlur: handleContainerBlur,
+      onFocus: handleContainerFocus,
+      ...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: applySegments,
+            disabled: Boolean(disabled),
+            error: Boolean(error),
+            field: item.field,
+            focusOrOpenCalendarGrid: focusOrOpenCalendarGrid,
+            handleOnClick: handleOpenCalendar,
+            handleOnFocus: handleSegmentFocus,
+            nextField: nextField,
+            prevField: prevField,
+            segments: segments,
+            setSegments: setSegments
+          }, item.field);
+        })
+      }), /*#__PURE__*/_jsx("input", {
+        "aria-hidden": true,
+        form: form,
+        name: name,
+        tabIndex: -1,
+        type: "hidden",
+        value: hiddenValue
+      }), /*#__PURE__*/_jsx(FlexBox, {
+        alignItems: "center",
+        justifyContent: "center",
+        pl: 16,
+        pr: 8,
+        role: "presentation",
+        onClick: handleOpenCalendar,
+        children: /*#__PURE__*/_jsx(MiniCalendarIcon, {
+          "aria-hidden": true,
+          size: 16
+        })
+      })]
+    })
+  });
+});
\ No newline at end of file