@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