@codecademy/gamut
68.2.268.2.3-alpha.794432.0
dist/DatePicker/DatePickerInput.js+
dist/DatePicker/DatePickerInput.jsNew file+134
Index: package/dist/DatePicker/DatePickerInput.js
===================================================================
--- package/dist/DatePicker/DatePickerInput.js
+++ package/dist/DatePicker/DatePickerInput.js
@@ -0,0 +1,134 @@
+import { MiniCalendarIcon } from '@codecademy/gamut-icons';
+import { forwardRef, useEffect, useId, useRef, useState } from 'react';
+import { FormGroup } from '../Form/elements/FormGroup';
+import { Input } from '../Form/inputs/Input';
+import { formatDateForInput, getDateFormatPattern, parseDateFromInput } from './Calendar/utils/format';
+import { useDatePicker } from './DatePickerContext';
+
+/**
+ * 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 } from "react/jsx-runtime";
+/**
+ * Date input. When inside DatePicker: owns local input value state and syncs to
+ * shared selectedDate via context on blur/parse; opens calendar on click/arrow down.
+ * When outside DatePicker: fully controlled by props.
+ */
+export const DatePickerInput = /*#__PURE__*/forwardRef(({
+ placeholder,
+ label,
+ rangePart,
+ ...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,
+ calendarDialogId,
+ translations
+ } = context;
+ const isRange = mode === 'range';
+ const inputID = useId();
+ const inputId = `datepicker-input-${inputID.replace(/:/g, '')}`;
+
+ // Range with two inputs: each input binds to one part. Single or range combined: one value.
+ const boundDate = isRange && rangePart === 'end' ? context.endDate : startOrSelectedDate;
+ const formattedValue = boundDate != null ? formatDateForInput(boundDate, locale) : '';
+ const [inputValue, setInputValue] = useState(() => formattedValue);
+ const isInputFocusedRef = useRef(false);
+
+ // Sync input from shared state. Skip when focused so we don't overwrite while typing.
+ useEffect(() => {
+ if (!isInputFocusedRef.current) {
+ setInputValue(formattedValue);
+ }
+ }, [formattedValue]);
+
+ /** Apply raw input string to selection state. Returns formatted string if parsed so caller can sync input (e.g. on blur). */
+ const applyValueToSelection = raw => {
+ const trimmed = raw.trim();
+ if (!trimmed) {
+ if (isRange && rangePart) {
+ if (rangePart === 'start') setSelection(null, context.endDate);else setSelection(startOrSelectedDate, null);
+ } else setSelection(null);
+ return undefined;
+ }
+ const parsed = parseDateFromInput(trimmed, locale);
+ if (!parsed) return undefined;
+ if (isRange && rangePart) {
+ if (rangePart === 'start') setSelection(parsed, context.endDate);else setSelection(startOrSelectedDate, parsed);
+ } else setSelection(parsed);
+ return formatDateForInput(parsed, locale);
+ };
+ const handleChange = e => {
+ const raw = e.target.value;
+ setInputValue(raw);
+ applyValueToSelection(raw);
+ };
+ const handleBlur = () => {
+ isInputFocusedRef.current = false;
+ const formatted = applyValueToSelection(inputValue.trim());
+ if (formatted) setInputValue(formatted);else if (inputValue.trim()) setInputValue(formattedValue);
+ };
+ const handleKeyDown = e => {
+ if (e.key === 'ArrowDown' || e.key === 'Down') {
+ e.preventDefault();
+ if (isCalendarOpen) {
+ focusCalendarGrid();
+ } else {
+ openCalendar({
+ moveFocusIntoCalendar: true
+ });
+ }
+ }
+ };
+ const handleOpenCalendar = () => {
+ openCalendar({
+ moveFocusIntoCalendar: false
+ });
+ };
+ const defaultLabel = !isRange ? translations.dateLabel : rangePart === 'end' ? translations.endDateLabel : translations.startDateLabel;
+ return /*#__PURE__*/_jsx(FormGroup, {
+ htmlFor: inputId,
+ isSoloField: true // should probaly be based on a prop
+ ,
+ label: label ?? defaultLabel,
+ mb: 0,
+ pb: 0,
+ spacing: "tight",
+ width: "170px",
+ children: /*#__PURE__*/_jsx(Input, {
+ ...rest,
+ "aria-autocomplete": "none",
+ "aria-controls": calendarDialogId,
+ "aria-expanded": isCalendarOpen,
+ "aria-haspopup": "dialog",
+ icon: () => /*#__PURE__*/_jsx(MiniCalendarIcon, {
+ size: 16
+ }),
+ id: inputId,
+ placeholder: placeholder ?? getDateFormatPattern(locale),
+ ref: ref,
+ role: "combobox",
+ type: "text",
+ value: inputValue,
+ onBlur: handleBlur,
+ onChange: handleChange,
+ onClick: handleOpenCalendar,
+ onFocus: () => {
+ isInputFocusedRef.current = true;
+ if (isRange && rangePart) context.setActiveRangePart(rangePart);
+ },
+ onKeyDown: handleKeyDown
+ })
+ });
+});
\ No newline at end of file