@codecademy/gamut

68.6.268.6.3-alpha.92d8ae.0
~

Modified (13 files)

Index: package/dist/DatePicker/DatePickerCalendar/Calendar/CalendarBody.js
===================================================================
--- package/dist/DatePicker/DatePickerCalendar/Calendar/CalendarBody.js
+++ package/dist/DatePicker/DatePickerCalendar/Calendar/CalendarBody.js
@@ -125,8 +125,9 @@
     children: [/*#__PURE__*/_jsx("thead", {
       children: /*#__PURE__*/_jsx("tr", {
         children: weekdayLabels.map((label, i) => /*#__PURE__*/_jsx(TableHeader, {
           abbr: weekdayFullNames[i],
+          "aria-label": weekdayFullNames[i],
           scope: "col",
           children: label
         }, label))
       })
Index: package/dist/DatePicker/DatePicker.js
===================================================================
--- package/dist/DatePicker/DatePicker.js
+++ package/dist/DatePicker/DatePicker.js
@@ -1,13 +1,11 @@
-import { MiniArrowLeftIcon, MiniArrowRightIcon } from '@codecademy/gamut-icons';
-import { useElementDir } from '@codecademy/gamut-styles';
 import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
-import { Box, FlexBox } from '../Box';
 import { PopoverContainer } from '../PopoverContainer';
 import { DatePickerCalendar } from './DatePickerCalendar';
 import { getDefaultRangeQuickActions, getDefaultSingleQuickActions } from './DatePickerCalendar/utils/quickActions';
 import { DatePickerProvider } from './DatePickerContext';
 import { DatePickerInput } from './DatePickerInput';
+import { DatePickerRangeInputWrapper } from './DatePickerInput/DatePickerRangeInputWrapper';
 import { useResolvedLocale } from './utils/locale';
 import { DEFAULT_DATE_PICKER_TRANSLATIONS } from './utils/translations';
 import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
 export const DatePicker = props => {
@@ -18,18 +16,18 @@
     mode,
     translations: translationsProp,
     inputSize,
     quickActions,
-    placement = 'inline'
+    placement = 'inline',
+    description
   } = props;
   const [isCalendarOpen, setIsCalendarOpen] = useState(false);
   const [focusGridSignal, setFocusGridSignal] = useState(false);
   const [gridFocusRequested, setGridFocusRequested] = useState(false);
   const [activeRangePart, setActiveRangePart] = useState(null);
   const inputRef = useRef(null);
   const dialogId = useId();
   const calendarDialogId = `datepicker-dialog-${dialogId.replace(/:/g, '')}`;
-  const isRtl = useElementDir() === 'rtl';
   const clearGridFocusRequest = useCallback(() => {
     setGridFocusRequested(false);
   }, []);
   const resolvedLocale = useResolvedLocale(locale);
@@ -96,29 +94,16 @@
       onSelection: props.onSelected
     };
   }, [translationsProp, quickActions, mode, resolvedLocale, isCalendarOpen, openCalendar, focusCalendar, focusGridSignal, gridFocusRequested, clearGridFocusRequest, closeCalendar, disableDate, props, activeRangePart]);
   const content = children !== undefined ? children : /*#__PURE__*/_jsxs(_Fragment, {
-    children: [/*#__PURE__*/_jsx(FlexBox, {
-      gap: inputSize === 'small' ? 4 : 8,
+    children: [mode === 'range' ? /*#__PURE__*/_jsx(DatePickerRangeInputWrapper, {
+      description: description,
       ref: inputRef,
-      width: "fit-content",
-      children: mode === 'range' ? /*#__PURE__*/_jsxs(_Fragment, {
-        children: [/*#__PURE__*/_jsx(DatePickerInput, {
-          name: "datePickerInputStart",
-          rangePart: "start",
-          size: inputSize
-        }), /*#__PURE__*/_jsx(Box, {
-          alignSelf: "center",
-          mt: 32,
-          children: isRtl ? /*#__PURE__*/_jsx(MiniArrowLeftIcon, {}) : /*#__PURE__*/_jsx(MiniArrowRightIcon, {})
-        }), /*#__PURE__*/_jsx(DatePickerInput, {
-          name: "datePickerInputEnd",
-          rangePart: "end",
-          size: inputSize
-        })]
-      }) : /*#__PURE__*/_jsx(DatePickerInput, {
-        size: inputSize
-      })
+      size: inputSize
+    }) : /*#__PURE__*/_jsx(DatePickerInput, {
+      description: description,
+      ref: inputRef,
+      size: inputSize
     }), /*#__PURE__*/_jsx(PopoverContainer, {
       alignment: "bottom-left",
       allowPageInteraction: true,
       focusOnProps: {
Index: package/dist/DatePicker/DatePickerInput/elements.js
===================================================================
--- package/dist/DatePicker/DatePickerInput/elements.js
+++ package/dist/DatePicker/DatePickerInput/elements.js
@@ -1,33 +1,9 @@
 import _styled from "@emotion/styled/base";
-import { variant } from '@codecademy/gamut-styles';
-import { FlexBox } from '../../Box';
-import { formFieldFocusStyles, formFieldStyles, inputSizeStyles } from '../../Form/styles';
-const shellFocusStyles = variant({
-  variants: {
-    error: {
-      borderColor: 'feedback-error',
-      '&:hover': {
-        borderColor: 'feedback-error'
-      },
-      '&:focus': {
-        borderColor: 'feedback-error',
-        boxShadow: `inset 0 0 0 1px feedback-error`
-      },
-      '&:focus-within': {
-        borderColor: 'feedback-error',
-        boxShadow: `inset 0 0 0 1px feedback-error`
-      }
-    },
-    default: {
-      '&:focus-within': formFieldFocusStyles
-    }
-  }
-});
-/**
- * Shell uses the same styles as `Input`. `formFieldStyles` targets `&:focus`, but the host is a
- * `div` — focus is on inner spinbuttons, so we mirror `Input` focus visuals with `&:focus-within`.
- */
-export const SegmentedShell = /*#__PURE__*/_styled(FlexBox, {
+import { css } from '@codecademy/gamut-styles';
+import { FormGroupDescription } from '../../Form';
+export const DatePickerDescription = /*#__PURE__*/_styled(FormGroupDescription, {
   target: "ex306dz0",
-  label: "SegmentedShell"
-})(formFieldStyles, inputSizeStyles, shellFocusStyles, process.env.NODE_ENV === "production" ? "" : "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9EYXRlUGlja2VyL0RhdGVQaWNrZXJJbnB1dC9lbGVtZW50cy50c3giXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBeUM4QiIsImZpbGUiOiIuLi8uLi8uLi9zcmMvRGF0ZVBpY2tlci9EYXRlUGlja2VySW5wdXQvZWxlbWVudHMudHN4Iiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgdmFyaWFudCB9IGZyb20gJ0Bjb2RlY2FkZW15L2dhbXV0LXN0eWxlcyc7XG5pbXBvcnQgeyBTdHlsZVByb3BzIH0gZnJvbSAnQGNvZGVjYWRlbXkvdmFyaWFuY2UnO1xuaW1wb3J0IHN0eWxlZCBmcm9tICdAZW1vdGlvbi9zdHlsZWQnO1xuXG5pbXBvcnQgeyBGbGV4Qm94IH0gZnJvbSAnLi4vLi4vQm94JztcbmltcG9ydCB7XG4gIGZvcm1GaWVsZEZvY3VzU3R5bGVzLFxuICBmb3JtRmllbGRTdHlsZXMsXG4gIGlucHV0U2l6ZVN0eWxlcyxcbn0gZnJvbSAnLi4vLi4vRm9ybS9zdHlsZXMnO1xuXG5jb25zdCBzaGVsbEZvY3VzU3R5bGVzID0gdmFyaWFudCh7XG4gIHZhcmlhbnRzOiB7XG4gICAgZXJyb3I6IHtcbiAgICAgIGJvcmRlckNvbG9yOiAnZmVlZGJhY2stZXJyb3InLFxuICAgICAgJyY6aG92ZXInOiB7XG4gICAgICAgIGJvcmRlckNvbG9yOiAnZmVlZGJhY2stZXJyb3InLFxuICAgICAgfSxcbiAgICAgICcmOmZvY3VzJzoge1xuICAgICAgICBib3JkZXJDb2xvcjogJ2ZlZWRiYWNrLWVycm9yJyxcbiAgICAgICAgYm94U2hhZG93OiBgaW5zZXQgMCAwIDAgMXB4IGZlZWRiYWNrLWVycm9yYCxcbiAgICAgIH0sXG4gICAgICAnJjpmb2N1cy13aXRoaW4nOiB7XG4gICAgICAgIGJvcmRlckNvbG9yOiAnZmVlZGJhY2stZXJyb3InLFxuICAgICAgICBib3hTaGFkb3c6IGBpbnNldCAwIDAgMCAxcHggZmVlZGJhY2stZXJyb3JgLFxuICAgICAgfSxcbiAgICB9LFxuICAgIGRlZmF1bHQ6IHtcbiAgICAgICcmOmZvY3VzLXdpdGhpbic6IGZvcm1GaWVsZEZvY3VzU3R5bGVzLFxuICAgIH0sXG4gIH0sXG59KTtcblxuaW50ZXJmYWNlIFNlZ21lbnRlZFNoZWxsUHJvcHNcbiAgZXh0ZW5kcyBTdHlsZVByb3BzPHR5cGVvZiBpbnB1dFNpemVTdHlsZXM+LFxuICAgIFN0eWxlUHJvcHM8dHlwZW9mIHNoZWxsRm9jdXNTdHlsZXM+IHt9XG5cbi8qKlxuICogU2hlbGwgdXNlcyB0aGUgc2FtZSBzdHlsZXMgYXMgYElucHV0YC4gYGZvcm1GaWVsZFN0eWxlc2AgdGFyZ2V0cyBgJjpmb2N1c2AsIGJ1dCB0aGUgaG9zdCBpcyBhXG4gKiBgZGl2YCDigJQgZm9jdXMgaXMgb24gaW5uZXIgc3BpbmJ1dHRvbnMsIHNvIHdlIG1pcnJvciBgSW5wdXRgIGZvY3VzIHZpc3VhbHMgd2l0aCBgJjpmb2N1cy13aXRoaW5gLlxuICovXG5leHBvcnQgY29uc3QgU2VnbWVudGVkU2hlbGwgPSBzdHlsZWQoRmxleEJveCk8U2VnbWVudGVkU2hlbGxQcm9wcz4oXG4gIGZvcm1GaWVsZFN0eWxlcyxcbiAgaW5wdXRTaXplU3R5bGVzLFxuICBzaGVsbEZvY3VzU3R5bGVzXG4pO1xuIl19 */");
\ No newline at end of file
+  label: "DatePickerDescription"
+})(css({
+  whiteSpace: 'nowrap'
+}), process.env.NODE_ENV === "production" ? "" : "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9EYXRlUGlja2VyL0RhdGVQaWNrZXJJbnB1dC9lbGVtZW50cy50c3giXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBS3FDIiwiZmlsZSI6Ii4uLy4uLy4uL3NyYy9EYXRlUGlja2VyL0RhdGVQaWNrZXJJbnB1dC9lbGVtZW50cy50c3giLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBjc3MgfSBmcm9tICdAY29kZWNhZGVteS9nYW11dC1zdHlsZXMnO1xuaW1wb3J0IHN0eWxlZCBmcm9tICdAZW1vdGlvbi9zdHlsZWQnO1xuXG5pbXBvcnQgeyBGb3JtR3JvdXBEZXNjcmlwdGlvbiB9IGZyb20gJy4uLy4uL0Zvcm0nO1xuXG5leHBvcnQgY29uc3QgRGF0ZVBpY2tlckRlc2NyaXB0aW9uID0gc3R5bGVkKEZvcm1Hcm91cERlc2NyaXB0aW9uKShcbiAgY3NzKHtcbiAgICB3aGl0ZVNwYWNlOiAnbm93cmFwJyxcbiAgfSlcbik7XG4iXX0= */");
\ No newline at end of file
Index: package/dist/DatePicker/DatePickerInput/index.js
===================================================================
--- package/dist/DatePicker/DatePickerInput/index.js
+++ package/dist/DatePicker/DatePickerInput/index.js
@@ -1,215 +1,88 @@
-import { MiniCalendarIcon } from '@codecademy/gamut-icons';
-import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
-import { FlexBox } from '../../Box';
+import { forwardRef, useId } from 'react';
+import { Box, FlexBox } from '../../Box';
 import { FormGroup } from '../../Form/elements/FormGroup';
-import { isSameDay } from '../DatePickerCalendar/Calendar/utils/dateGrid';
-import { handleDateSelectRange } from '../DatePickerCalendar/utils/dateSelect';
+import { FormGroupLabel } from '../../Form/elements/FormGroupLabel';
+import { DATE_PICKER_FIELD_WIDTH } from '../constants';
 import { useDatePicker } from '../DatePickerContext';
-import { SegmentedShell } from './elements';
-import { DatePickerInputSegment } from './Segment';
-import { SegmentLiteral } from './Segment/elements';
-import { getDateSegmentsFromDate, normalizeSegmentValues, parseSegmentsToDate } from './Segment/utils';
-import { formatDateISO8601DateOnly, getDateFieldOrder, getDateFormatLayout } from './utils';
+import { createDatePickerFieldIds, createDatePickerShellId } from '../utils/fieldIds';
+import { DatePickerInputShell } from './DatePickerInputShell';
+import { DatePickerDescription } from './elements';
 import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
+export { DatePickerDescription } from './elements';
 export const DatePickerInput = /*#__PURE__*/forwardRef(({
   disabled,
   error,
   form,
   label,
   name,
   rangePart,
   size = 'base',
+  description,
   ...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,
-    openCalendar,
-    focusCalendar,
-    locale,
-    isCalendarOpen,
-    translations,
-    disableDate
+    translations
   } = context;
-  const isRange = mode === 'range';
-  const endDate = isRange ? context.endDate : null;
-  const date = isRange ? context.startDate : context.selectedDate;
-  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 defaultLabel = !isRange ? translations.dateLabel : rangePart === 'end' ? translations.endDateLabel : translations.startDateLabel;
-  const boundDate = isRange && rangePart === 'end' ? endDate : date;
-  const segmentsFromBound = useMemo(() => 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 segmentElRefs = useRef({});
-  const assignSegmentRef = useCallback((field, el) => {
-    segmentElRefs.current[field] = el;
-  }, [segmentElRefs]);
-  const onSiblingSegmentFocus = useCallback(field => {
-    segmentElRefs.current[field]?.focus();
-  }, [segmentElRefs]);
-  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);
-    }
-  }, [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 parsed = parseSegmentsToDate(next);
-    if (parsed) commitParsedDate(parsed);else if (!next.month && !next.day && !next.year) clearSelection();
-  }, [clearSelection, commitParsedDate]);
-  const onContainerBlur = useCallback(e => {
-    if (containerRef.current?.contains(e.relatedTarget)) return;
-    isInputFocusedRef.current = false;
-    setSegments(prev => {
-      const normalized = normalizeSegmentValues(prev);
-      const parsed = parseSegmentsToDate(normalized);
-      if (parsed) {
-        const sameAsBound = isSameDay(parsed, boundDate);
-        if (isCalendarOpen && !sameAsBound) {
-          queueMicrotask(() => {
-            commitParsedDate(parsed);
-          });
-        }
-        return normalized;
-      }
-      if (!normalized.month && !normalized.day && !normalized.year) {
-        queueMicrotask(() => {
-          clearSelection();
-        });
-        return getDateSegmentsFromDate(null);
-      }
-      return segmentsFromBound;
+  const labelUid = useId();
+  const inputUid = useId();
+  const shellProps = {
+    disabled,
+    error,
+    form,
+    size,
+    ...rest
+  };
+  if (rangePart) {
+    const shellId = createDatePickerShellId(inputUid);
+    const defaultLabel = rangePart === 'end' ? translations.endDateLabel : translations.startDateLabel;
+    return /*#__PURE__*/_jsxs(FormGroup, {
+      alignItems: "flex-start",
+      id: shellId,
+      isSoloField: true,
+      label: label ?? defaultLabel,
+      mb: 0,
+      pb: 0,
+      spacing: "tight",
+      width: "fit-content",
+      children: [description ? /*#__PURE__*/_jsx(DatePickerDescription, {
+        "aria-live": "assertive",
+        children: description
+      }) : null, /*#__PURE__*/_jsx(DatePickerInputShell, {
+        ...shellProps,
+        labelledById: shellId,
+        name: name,
+        rangePart: rangePart,
+        ref: ref,
+        shellId: shellId
+      })]
     });
-  }, [containerRef, boundDate, segmentsFromBound, clearSelection, commitParsedDate, isCalendarOpen]);
-  const setActiveRangePartForField = useCallback(() => {
-    if (isRange && rangePart) context.setActiveRangePart(rangePart);
-  }, [isRange, rangePart, context]);
-  const onSegmentFocus = useCallback(() => {
-    isInputFocusedRef.current = true;
-    setActiveRangePartForField();
-  }, [isInputFocusedRef, 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__*/_jsx(FormGroup, {
-    htmlFor: inputId,
-    isSoloField: true,
-    label: label ?? defaultLabel,
-    mb: 0,
-    pb: 0,
-    spacing: "tight",
+  }
+  const fieldIds = createDatePickerFieldIds(inputUid, labelUid);
+  return /*#__PURE__*/_jsxs(Box, {
     width: "fit-content",
-    children: /*#__PURE__*/_jsxs(SegmentedShell, {
-      id: inputId,
-      inputSize: size,
-      ref: shellRef,
-      role: "group",
-      variant: error ? 'error' : 'default',
-      width: "113px",
-      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: !!error,
-            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,
+    children: [/*#__PURE__*/_jsx(Box, {
+      id: fieldIds.labelledById,
+      width: DATE_PICKER_FIELD_WIDTH,
+      children: /*#__PURE__*/_jsx(FormGroupLabel, {
+        htmlFor: fieldIds.shellId,
+        isSoloField: true,
+        children: label ?? translations.dateLabel
+      })
+    }), description ? /*#__PURE__*/_jsx(DatePickerDescription, {
+      "aria-live": "assertive",
+      children: description
+    }) : null, /*#__PURE__*/_jsx(FlexBox, {
+      ref: ref,
+      children: /*#__PURE__*/_jsx(DatePickerInputShell, {
+        ...shellProps,
+        labelledById: fieldIds.labelledById,
         name: name ?? 'datePickerInput',
-        tabIndex: -1,
-        type: "hidden",
-        value: hiddenValue
-      }), /*#__PURE__*/_jsx(FlexBox, {
-        alignItems: "center",
-        justifyContent: "center",
-        pl: 16,
-        pr: 8,
-        role: "presentation",
-        children: /*#__PURE__*/_jsx(MiniCalendarIcon, {
-          "aria-hidden": true,
-          size: 16
-        })
-      })]
-    })
+        shellId: fieldIds.shellId
+      })
+    })]
   });
 });
\ No newline at end of file
Index: package/dist/DatePicker/utils/translations.js
===================================================================
--- package/dist/DatePicker/utils/translations.js
+++ package/dist/DatePicker/utils/translations.js
@@ -3,8 +3,10 @@
   dateLabel: 'Date',
   startDateLabel: 'Start date',
   endDateLabel: 'End date',
   calendarDialogAriaLabel: 'Choose date',
+  openCalendarLabel: 'Open calendar',
+  invalidDateError: 'Enter a valid date',
   last7DaysDisplayText: 'Last 7 days',
   last30DaysDisplayText: 'Last 30 days',
   last90DaysDisplayText: 'Last 90 days'
 };
\ No newline at end of file
Index: package/dist/DatePicker/DatePickerInput/Segment/utils.js
===================================================================
--- package/dist/DatePicker/DatePickerInput/Segment/utils.js
+++ package/dist/DatePicker/DatePickerInput/Segment/utils.js
@@ -41,8 +41,23 @@
     year
   } = strictSegments;
   return year.length === 4 && month.length === 2 && day.length === 2;
 };
+
+/** Year is full length and both month and day have digits (e.g. 2/30/2026). */
+export const isCompleteDateEntryAttempt = strictSegments => {
+  const {
+    month,
+    day,
+    year
+  } = strictSegments;
+  return year.length === 4 && month.length > 0 && day.length > 0;
+};
+export const padSegmentDigitsForParse = strictSegments => ({
+  month: strictSegments.month.padStart(2, '0'),
+  day: strictSegments.day.padStart(2, '0'),
+  year: strictSegments.year
+});
 export const normalizeSegmentValues = segments => {
   const strictSegments = getStrictSegmentDigits(segments);
   if (isStrictlyCompleteDateEntry(strictSegments)) {
     const parsed = parseSegmentsToDate(strictSegments);
@@ -77,8 +92,64 @@
     day,
     year
   };
 };
+export const getSegmentValidationState = segments => {
+  const strictSegments = getStrictSegmentDigits(segments);
+  if (isStrictlyCompleteDateEntry(strictSegments)) {
+    const parsed = parseSegmentsToDate(strictSegments);
+    return {
+      isInvalid: parsed === null,
+      parsedDate: parsed,
+      segments: parsed ? getDateSegmentsFromDate(parsed) : strictSegments
+    };
+  }
+  if (isCompleteDateEntryAttempt(strictSegments)) {
+    const paddedSegments = padSegmentDigitsForParse(strictSegments);
+    const parsed = parseSegmentsToDate(paddedSegments);
+    return {
+      isInvalid: parsed === null,
+      parsedDate: parsed,
+      segments: parsed ? getDateSegmentsFromDate(parsed) : paddedSegments
+    };
+  }
+  return null;
+};
+export const resolveSegmentsOnBlur = (segments, boundDate) => {
+  const validation = getSegmentValidationState(segments);
+  if (validation) {
+    return {
+      isInvalid: validation.isInvalid,
+      parsedDate: validation.parsedDate,
+      segments: validation.segments,
+      shouldClear: false
+    };
+  }
+  const normalized = normalizeSegmentValues(segments);
+  const parsed = parseSegmentsToDate(normalized);
+  if (parsed) {
+    return {
+      isInvalid: false,
+      parsedDate: parsed,
+      segments: normalized,
+      shouldClear: false
+    };
+  }
+  if (!normalized.month && !normalized.day && !normalized.year) {
+    return {
+      isInvalid: false,
+      parsedDate: null,
+      segments: getDateSegmentsFromDate(null),
+      shouldClear: true
+    };
+  }
+  return {
+    isInvalid: false,
+    parsedDate: null,
+    segments: getDateSegmentsFromDate(boundDate),
+    shouldClear: false
+  };
+};
 export const getSegmentPlaceholder = field => field === 'year' ? 'YYYY' : field === 'month' ? 'MM' : 'DD';
 export const segmentMaxLength = field => field === 'year' ? 4 : 2;
 export const getSegmentSpinBounds = ({
   field,
Index: package/package.json
===================================================================
--- package/package.json
+++ package/package.json
@@ -1,15 +1,15 @@
 {
   "name": "@codecademy/gamut",
   "description": "Styleguide & Component library for Codecademy",
-  "version": "68.6.2",
+  "version": "68.6.3-alpha.92d8ae.0",
   "author": "Codecademy Engineering <[email protected]>",
   "dependencies": {
-    "@codecademy/gamut-icons": "9.57.5",
-    "@codecademy/gamut-illustrations": "0.58.11",
-    "@codecademy/gamut-patterns": "0.10.30",
-    "@codecademy/gamut-styles": "18.0.0",
-    "@codecademy/variance": "0.26.1",
+    "@codecademy/gamut-icons": "9.57.6-alpha.92d8ae.0",
+    "@codecademy/gamut-illustrations": "0.58.12-alpha.92d8ae.0",
+    "@codecademy/gamut-patterns": "0.10.31-alpha.92d8ae.0",
+    "@codecademy/gamut-styles": "18.0.1-alpha.92d8ae.0",
+    "@codecademy/variance": "0.26.2-alpha.92d8ae.0",
     "@formatjs/intl-locale": "5.3.1",
     "@react-aria/interactions": "3.25.0",
     "@types/marked": "^4.0.8",
     "@vidstack/react": "^1.12.12",
Index: package/dist/DatePicker/DatePickerInput/elements.d.ts
===================================================================
--- package/dist/DatePicker/DatePickerInput/elements.d.ts
+++ package/dist/DatePicker/DatePickerInput/elements.d.ts
@@ -1,18 +1,8 @@
-import { StyleProps } from '@codecademy/variance';
-import { inputSizeStyles } from '../../Form/styles';
-declare const shellFocusStyles: (props: import("@codecademy/variance/dist/types/config").VariantProps<"variant", false | "error" | "default"> & {
+export declare const DatePickerDescription: import("@emotion/styled").StyledComponent<{
     theme?: import("@emotion/react").Theme;
-}) => import("@codecademy/variance").CSSObject;
-interface SegmentedShellProps extends StyleProps<typeof inputSizeStyles>, StyleProps<typeof shellFocusStyles> {
-}
-/**
- * Shell uses the same styles as `Input`. `formFieldStyles` targets `&:focus`, but the host is a
- * `div` — focus is on inner spinbuttons, so we mirror `Input` focus visuals with `&:focus-within`.
- */
-export declare const SegmentedShell: import("@emotion/styled").StyledComponent<{
-    theme?: import("@emotion/react").Theme;
     as?: React.ElementType;
-} & import("../../Box").FlexBoxProps & Pick<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "slot" | "style" | "title" | "dir" | "children" | "className" | "aria-hidden" | "onAnimationStart" | "onDragStart" | "onDragEnd" | "onDrag" | keyof import("react").ClassAttributes<HTMLDivElement> | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "autoCapitalize" | "autoFocus" | "contentEditable" | "contextMenu" | "draggable" | "enterKeyHint" | "hidden" | "id" | "lang" | "nonce" | "spellCheck" | "tabIndex" | "translate" | "radioGroup" | "role" | "about" | "content" | "datatype" | "inlist" | "prefix" | "property" | "rel" | "resource" | "rev" | "typeof" | "vocab" | "autoCorrect" | "autoSave" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "inputMode" | "is" | "exportparts" | "part" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-braillelabel" | "aria-brailleroledescription" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colindextext" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-description" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowindextext" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDragCapture" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerLeave" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture"> & {
+} & {
     theme?: import("@emotion/react").Theme;
-} & SegmentedShellProps, {}, {}>;
-export {};
+} & import("react").ClassAttributes<HTMLDivElement> & import("react").HTMLAttributes<HTMLDivElement> & {
+    theme?: import("@emotion/react").Theme;
+}, {}, {}>;
Index: package/dist/DatePicker/DatePickerInput/index.d.ts
===================================================================
--- package/dist/DatePicker/DatePickerInput/index.d.ts
+++ package/dist/DatePicker/DatePickerInput/index.d.ts
@@ -1,9 +1,14 @@
 import type { InputWrapperProps } from '../../Form/inputs/Input';
+export { DatePickerDescription } from './elements';
 export type DatePickerInputProps = Omit<InputWrapperProps, 'className' | 'type' | 'icon' | 'value' | 'onChange' | 'color'> & {
     /** In range mode: which part of the range this input edits. Omit for single-date or combined display. */
     rangePart?: 'start' | 'end';
+    /** Description to display between the label and the input. */
+    description?: string;
 };
 export declare const DatePickerInput: import("react").ForwardRefExoticComponent<Omit<InputWrapperProps, "color" | "className" | "onChange" | "type" | "icon" | "value"> & {
     /** In range mode: which part of the range this input edits. Omit for single-date or combined display. */
     rangePart?: "start" | "end";
+    /** Description to display between the label and the input. */
+    description?: string;
 } & import("react").RefAttributes<HTMLDivElement>>;
Index: package/dist/DatePicker/DatePickerInput/Segment/index.d.ts
===================================================================
--- package/dist/DatePicker/DatePickerInput/Segment/index.d.ts
+++ package/dist/DatePicker/DatePickerInput/Segment/index.d.ts
@@ -1,6 +1,6 @@
 import { type Dispatch, type SetStateAction } from 'react';
-import type { DatePartKind } from '../utils';
+import type { DatePartKind } from '../DatePickerInputShell/utils';
 import { SegmentValues } from './utils';
 export type AssignSegmentRef = (field: DatePartKind, el: HTMLSpanElement | null) => void;
 export type DatePickerInputSegmentProps = {
     field: DatePartKind;
Index: package/dist/DatePicker/utils/translations.d.ts
===================================================================
--- package/dist/DatePicker/utils/translations.d.ts
+++ package/dist/DatePicker/utils/translations.d.ts
@@ -8,8 +8,12 @@
     /** Label for the end date input in range mode (default: "End date"). */
     endDateLabel?: string;
     /** aria-label for the calendar dialog (default: "Choose date"). */
     calendarDialogAriaLabel?: string;
+    /** aria-label for the calendar icon trigger (default: "Open calendar"). */
+    openCalendarLabel?: string;
+    /** Error message when typed segments do not form a valid date (default: "Enter a valid date"). */
+    invalidDateError?: string;
     /** Label for the last 7 days quick action (default: "Last 7 days"). */
     last7DaysDisplayText?: string;
     /** Label for the last 30 days quick action (default: "Last 30 days"). */
     last30DaysDisplayText?: string;
Index: package/dist/DatePicker/types.d.ts
===================================================================
--- package/dist/DatePicker/types.d.ts
+++ package/dist/DatePicker/types.d.ts
@@ -63,8 +63,10 @@
      * Whether the calendar popover renders inside the current DOM context (inline) or escapes with a portal (floating)
      * @default "inline"
      */
     placement?: 'inline' | 'floating';
+    /** Description to display between the label and the input(s). In range mode, spans both fields. */
+    description?: string;
 }
 export interface DatePickerSingleProps extends DatePickerBaseProps<'single'> {
     /** Controlled selected date. Pass `null` to not have a default selected date. Pass a `Date` to have a default selected date.
      *
Index: package/dist/DatePicker/DatePickerInput/Segment/utils.d.ts
===================================================================
--- package/dist/DatePicker/DatePickerInput/Segment/utils.d.ts
+++ package/dist/DatePicker/DatePickerInput/Segment/utils.d.ts
@@ -1,5 +1,5 @@
-import type { DateFormatLayoutItem, DatePartKind } from '../utils';
+import type { DateFormatLayoutItem, DatePartKind } from '../DatePickerInputShell/utils';
 export type SegmentValues = {
     month: string;
     day: string;
     year: string;
@@ -11,9 +11,25 @@
     day: string;
     year: string;
 };
 export declare const isStrictlyCompleteDateEntry: (strictSegments: SegmentValues) => boolean;
+/** Year is full length and both month and day have digits (e.g. 2/30/2026). */
+export declare const isCompleteDateEntryAttempt: (strictSegments: SegmentValues) => boolean;
+export declare const padSegmentDigitsForParse: (strictSegments: SegmentValues) => SegmentValues;
 export declare const normalizeSegmentValues: (segments: SegmentValues) => SegmentValues;
+export type SegmentBlurResolution = {
+    isInvalid: boolean;
+    parsedDate: Date | null;
+    segments: SegmentValues;
+    shouldClear: boolean;
+};
+export type SegmentValidationState = {
+    isInvalid: boolean;
+    parsedDate: Date | null;
+    segments: SegmentValues;
+};
+export declare const getSegmentValidationState: (segments: SegmentValues) => SegmentValidationState | null;
+export declare const resolveSegmentsOnBlur: (segments: SegmentValues, boundDate: Date | null) => SegmentBlurResolution;
 export declare const getSegmentPlaceholder: (field: DatePartKind) => "YYYY" | "MM" | "DD";
 export declare const segmentMaxLength: (field: DatePartKind) => 2 | 4;
 export declare const getSegmentSpinBounds: ({ field, segments, }: {
     field: DatePartKind;