@codecademy/gamut
68.6.268.6.3-alpha.92d8ae.0
−
Removed (2 files)
+
Added (12 files)
~
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;