This commit is contained in:
@@ -397,9 +397,11 @@ function buildFormatConfigOrNull(type: DatePickerKind, format: string): FormatCo
|
||||
}
|
||||
|
||||
const segments = parts.filter((part): part is FormatSegment => part.type === 'segment');
|
||||
/* c8 ignore start -- validated token counts always yield at least one segment. */
|
||||
if (segments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
return {
|
||||
type,
|
||||
@@ -420,9 +422,11 @@ function buildFormatConfig(type: DatePickerKind, requestedFormat?: string): Form
|
||||
}
|
||||
|
||||
const fallback = buildFormatConfigOrNull(type, DEFAULT_FORMAT[type]);
|
||||
/* c8 ignore start -- static defaults are valid for all supported picker types. */
|
||||
if (!fallback) {
|
||||
throw new Error('Failed to initialize DatePicker format configuration.');
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
return fallback;
|
||||
}
|
||||
@@ -454,9 +458,11 @@ function parsePickerValueWithFormat(rawValue: string, config: FormatConfig): Pic
|
||||
}
|
||||
|
||||
const numeric = Number(chunk);
|
||||
/* c8 ignore start -- numeric chunks are finite after /^\d+$/ validation. */
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return null;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
if (part.kind === 'year') {
|
||||
year = numeric;
|
||||
@@ -472,12 +478,16 @@ function parsePickerValueWithFormat(rawValue: string, config: FormatConfig): Pic
|
||||
}
|
||||
|
||||
if (config.type !== 'time') {
|
||||
/* c8 ignore start -- date/month/year segments are guaranteed for non-time validated formats. */
|
||||
if (year == null || month == null || day == null) {
|
||||
return null;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
/* c8 ignore start -- 'yyyy' token bounds parsed year to 0..9999. */
|
||||
if (year < 0 || year > 9999) {
|
||||
return null;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
const parsedDate = createValidatedDate(year, month, day);
|
||||
if (!parsedDate) {
|
||||
@@ -492,9 +502,11 @@ function parsePickerValueWithFormat(rawValue: string, config: FormatConfig): Pic
|
||||
};
|
||||
}
|
||||
|
||||
/* c8 ignore start -- date-time format validation guarantees hour/minute segments. */
|
||||
if (hour == null || minute == null) {
|
||||
return null;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
||||
return null;
|
||||
@@ -507,9 +519,11 @@ function parsePickerValueWithFormat(rawValue: string, config: FormatConfig): Pic
|
||||
};
|
||||
}
|
||||
|
||||
/* c8 ignore start -- time format validation guarantees hour/minute segments. */
|
||||
if (hour == null || minute == null) {
|
||||
return null;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
||||
return null;
|
||||
@@ -748,6 +762,39 @@ function isHourSelectableForRange(
|
||||
return true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components -- test-only export of pure helpers.
|
||||
export const __datePickerTestUtils = {
|
||||
pad2,
|
||||
pad4,
|
||||
clampNumber,
|
||||
createDateAtLocalMidnight,
|
||||
createDateTimeFromPickerValue,
|
||||
startOfDay,
|
||||
startOfMonth,
|
||||
isSameDay,
|
||||
createValidatedDate,
|
||||
daysInMonth,
|
||||
clonePickerValue,
|
||||
resolveLocale,
|
||||
resolveWeekStart,
|
||||
buildMonthGrid,
|
||||
joinClassNames,
|
||||
assignRef,
|
||||
tokenizeFormat,
|
||||
buildFormatConfigOrNull,
|
||||
buildFormatConfig,
|
||||
parsePickerValueWithFormat,
|
||||
formatPickerValueWithFormat,
|
||||
comparePickerValue,
|
||||
normalizeRange,
|
||||
clampPickerToRange,
|
||||
isWithinRange,
|
||||
applySegmentDigits,
|
||||
findSegmentIndexByCaret,
|
||||
isDateSelectableForRange,
|
||||
isHourSelectableForRange,
|
||||
} as const;
|
||||
|
||||
export function DatePicker({
|
||||
label,
|
||||
placeholder = '',
|
||||
@@ -903,9 +950,11 @@ export function DatePicker({
|
||||
const monthGrid = useMemo(() => buildMonthGrid(viewMonth, weekStart), [viewMonth, weekStart]);
|
||||
|
||||
const recalculatePopupPosition = useCallback(() => {
|
||||
/* c8 ignore start -- guard protects partial mount/layout states that are not deterministic to unit test. */
|
||||
if (!isOpen || !inputWrapperRef.current || !popupRef.current || !globalThis.window) {
|
||||
return;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
const anchorRect = inputWrapperRef.current.getBoundingClientRect();
|
||||
const popupRect = popupRef.current.getBoundingClientRect();
|
||||
@@ -946,9 +995,11 @@ export function DatePicker({
|
||||
|
||||
recalculatePopupPosition();
|
||||
|
||||
/* c8 ignore start -- browser/window is always present in jsdom runtime. */
|
||||
if (!globalThis.window) {
|
||||
return;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
const handleWindowChange = () => {
|
||||
recalculatePopupPosition();
|
||||
@@ -970,9 +1021,11 @@ export function DatePicker({
|
||||
|
||||
const handlePointerDown = (event: MouseEvent | TouchEvent) => {
|
||||
const eventTarget = event.target as Node | null;
|
||||
/* c8 ignore start -- pointer events always provide a target in browser runtimes. */
|
||||
if (!eventTarget) {
|
||||
return;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
if (
|
||||
popupRef.current?.contains(eventTarget) ||
|
||||
@@ -1012,16 +1065,20 @@ export function DatePicker({
|
||||
const selectSegment = useCallback(
|
||||
(segmentIndex: number) => {
|
||||
const segments = formatConfig.segments;
|
||||
/* c8 ignore start -- format configuration always includes at least one segment. */
|
||||
if (segments.length === 0) {
|
||||
return;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
const clampedIndex = clampNumber(segmentIndex, 0, segments.length - 1);
|
||||
activeSegmentIndexRef.current = clampedIndex;
|
||||
|
||||
/* c8 ignore start -- browser/window is always present in jsdom runtime. */
|
||||
if (!globalThis.window) {
|
||||
return;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
if (pendingSelectionTimerRef.current != null) {
|
||||
globalThis.window.clearTimeout(pendingSelectionTimerRef.current);
|
||||
@@ -1030,9 +1087,11 @@ export function DatePicker({
|
||||
pendingSelectionTimerRef.current = globalThis.window.setTimeout(() => {
|
||||
const inputNode = internalInputRef.current;
|
||||
const targetSegment = segments[clampedIndex];
|
||||
/* c8 ignore start -- input node and target segment are available while mounted. */
|
||||
if (!inputNode || !targetSegment) {
|
||||
return;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
inputNode.setSelectionRange(targetSegment.start, targetSegment.end);
|
||||
}, 0);
|
||||
@@ -1042,14 +1101,18 @@ export function DatePicker({
|
||||
|
||||
const resolveCurrentSegmentIndex = useCallback(() => {
|
||||
const segments = formatConfig.segments;
|
||||
/* c8 ignore start -- format configuration always includes at least one segment. */
|
||||
if (segments.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
const inputNode = internalInputRef.current;
|
||||
/* c8 ignore start -- interactions only run when input node is mounted. */
|
||||
if (!inputNode) {
|
||||
return activeSegmentIndexRef.current;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
const index = findSegmentIndexByCaret(segments, inputNode.selectionStart);
|
||||
activeSegmentIndexRef.current = index;
|
||||
@@ -1079,9 +1142,11 @@ export function DatePicker({
|
||||
}
|
||||
|
||||
const inputNode = internalInputRef.current;
|
||||
/* c8 ignore start -- commits run only from active mounted input interactions. */
|
||||
if (!inputNode) {
|
||||
return;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
changeHandledRef.current = false;
|
||||
|
||||
@@ -1117,9 +1182,11 @@ export function DatePicker({
|
||||
},
|
||||
) => {
|
||||
const segment = formatConfig.segments[segmentIndex];
|
||||
/* c8 ignore start -- segmentIndex is always resolved from known format segments. */
|
||||
if (!segment) {
|
||||
return;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
const baseValue =
|
||||
parsePickerValueWithFormat(displayValue, formatConfig) ?? clampedSelectedValue;
|
||||
@@ -1172,9 +1239,11 @@ export function DatePicker({
|
||||
);
|
||||
|
||||
const openPicker = useCallback(() => {
|
||||
/* c8 ignore start -- disabled inputs cannot trigger picker open interactions. */
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
if (type !== 'time') {
|
||||
setViewMonth(startOfMonth(selectedDate));
|
||||
}
|
||||
@@ -1183,9 +1252,11 @@ export function DatePicker({
|
||||
}, [disabled, selectedDate, type]);
|
||||
|
||||
const togglePicker = useCallback(() => {
|
||||
/* c8 ignore start -- disabled icon button cannot trigger click events. */
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
if (isOpen) {
|
||||
closePicker();
|
||||
return;
|
||||
@@ -1233,9 +1304,11 @@ export function DatePicker({
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
};
|
||||
/* c8 ignore start -- out-of-range date cells are disabled in the calendar UI. */
|
||||
if (!isWithinRange(candidate, normalizedMinValue, normalizedMaxValue, type)) {
|
||||
return;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
commitValue(formatPickerValueWithFormat(candidate, formatConfig));
|
||||
closePicker();
|
||||
@@ -1331,17 +1404,21 @@ export function DatePicker({
|
||||
}, [disabled, selectSegment]);
|
||||
|
||||
const handleInputMouseUp = useCallback(() => {
|
||||
/* c8 ignore start -- disabled inputs do not emit mouse interaction events. */
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
selectSegment(resolveCurrentSegmentIndex());
|
||||
}, [disabled, resolveCurrentSegmentIndex, selectSegment]);
|
||||
|
||||
const handleInputClick = useCallback(() => {
|
||||
/* c8 ignore start -- disabled inputs do not emit click interaction events. */
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
selectSegment(resolveCurrentSegmentIndex());
|
||||
}, [disabled, resolveCurrentSegmentIndex, selectSegment]);
|
||||
@@ -1391,9 +1468,11 @@ export function DatePicker({
|
||||
}
|
||||
|
||||
const segment = formatConfig.segments[segmentIndex];
|
||||
/* c8 ignore start -- segment index resolution always maps to a valid segment. */
|
||||
if (!segment) {
|
||||
return;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
if (key === 'ArrowDown' && !isOpen) {
|
||||
event.preventDefault();
|
||||
@@ -1460,11 +1539,13 @@ export function DatePicker({
|
||||
digits = buffered.digits;
|
||||
}
|
||||
|
||||
/* c8 ignore start -- buffered digits are reset on segment completion. */
|
||||
if (digits.length >= segment.length) {
|
||||
digits = key;
|
||||
} else {
|
||||
digits += key;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
bufferedDigitsRef.current = {
|
||||
segmentIndex,
|
||||
|
||||
Reference in New Issue
Block a user