diff --git a/packages/@internationalized/date/src/manipulation.ts b/packages/@internationalized/date/src/manipulation.ts index 87863dafaf6..1c1c968cc36 100644 --- a/packages/@internationalized/date/src/manipulation.ts +++ b/packages/@internationalized/date/src/manipulation.ts @@ -115,7 +115,7 @@ function balanceDay(date: Mutable) { function constrainMonthDay(date: Mutable) { date.month = Math.max(1, Math.min(date.calendar.getMonthsInYear(date), date.month)); - date.day = Math.max(1, Math.min(date.calendar.getDaysInMonth(date), date.day)); + date.day = Math.max(1, Math.min(31, date.day)); } export function constrain(date: Mutable): void { diff --git a/packages/@react-aria/datepicker/src/useDateSegment.ts b/packages/@react-aria/datepicker/src/useDateSegment.ts index 2aad84bc0ea..bdf2751f7f3 100644 --- a/packages/@react-aria/datepicker/src/useDateSegment.ts +++ b/packages/@react-aria/datepicker/src/useDateSegment.ts @@ -275,6 +275,11 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref: selection?.collapse(ref.current); }; + + const onBlur = () => { + state.constrain() + } + let documentRef = useRef(typeof document !== 'undefined' ? document : null); useEvent(documentRef, 'selectionchange', () => { // Enforce that the selection is collapsed when inside a date segment. @@ -420,6 +425,7 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref: onPointerDown(e) { e.stopPropagation(); }, + onBlur, onMouseDown(e) { e.stopPropagation(); } diff --git a/packages/@react-spectrum/datepicker/test/DateField.test.js b/packages/@react-spectrum/datepicker/test/DateField.test.js index 829607e366b..58c4c45ceae 100644 --- a/packages/@react-spectrum/datepicker/test/DateField.test.js +++ b/packages/@react-spectrum/datepicker/test/DateField.test.js @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {act, pointerMap, render as render_, within} from '@react-spectrum/test-utils-internal'; +import {act, pointerMap, fireEvent, render as render_, within} from '@react-spectrum/test-utils-internal'; import {Button} from '@react-spectrum/button'; import {CalendarDate, CalendarDateTime, ZonedDateTime} from '@internationalized/date'; import {DateField} from '../'; @@ -20,6 +20,12 @@ import React from 'react'; import {theme} from '@react-spectrum/theme-default'; import userEvent from '@testing-library/user-event'; +function beforeInput(target, key) { + // JSDOM doesn't support the beforeinput event + let e = new InputEvent('beforeinput', {cancelable: true, data: key, inputType: 'insertText'}); + fireEvent(target, e); +} + function render(el) { if (el.type === Provider) { return render_(el); @@ -674,4 +680,55 @@ describe('DateField', function () { }); }); }); + +describe("validation", () => { + it("Should limit day to 31", async () => { + let onChange = jest.fn(); + let { getByTestId } = render( + , + ); + + let segment = getByTestId("day"); + act(() => { + segment.focus(); + }); + beforeInput(segment, "32"); + expect(onChange).toHaveBeenCalledWith(new CalendarDate(2019, 2, 31)); + }); + it.only("Constrain day on blur", async () => { + let onChange = jest.fn(); + let { getByTestId } = render( + , + ); + + let segment; + + segment = getByTestId("year"); + act(() => { + segment.focus(); + }); + beforeInput(segment, "2025"); + + segment = getByTestId("month"); + act(() => { + segment.focus(); + }); + beforeInput(segment, "2"); + + segment = getByTestId("day"); + act(() => { + segment.focus(); + }); + beforeInput(segment, "29"); + + act(() => document.activeElement.blur()); + + expect(onChange).toHaveBeenCalledWith(new CalendarDate(2025, 2, 28)); + }); +}); + }); diff --git a/packages/@react-stately/datepicker/src/useDateFieldState.ts b/packages/@react-stately/datepicker/src/useDateFieldState.ts index ba6498a31e1..6de146cdc6c 100644 --- a/packages/@react-stately/datepicker/src/useDateFieldState.ts +++ b/packages/@react-stately/datepicker/src/useDateFieldState.ts @@ -95,7 +95,8 @@ export interface DateFieldState extends FormValidationState { /** Formats the current date value using the given options. */ formatValue(fieldOptions: FieldOptions): string, /** Gets a formatter based on state's props. */ - getDateFormatter(locale: string, formatOptions: FormatterOptions): DateFormatter + getDateFormatter(locale: string, formatOptions: FormatterOptions): DateFormatter, + constrain(): void, } const EDITABLE_SEGMENTS = { @@ -190,6 +191,7 @@ export function useDateFieldState(props: DateFi () => createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone) ); + let val = calendarValue || placeholderDate; let showEra = calendar.identifier === 'gregory' && val.era === 'BC'; let formatOpts = useMemo(() => ({ @@ -419,6 +421,19 @@ export function useDateFieldState(props: DateFi let newOptions = {...formatOpts, ...formatOptions}; let newFormatOptions = getFormatOptions({}, newOptions); return new DateFormatter(locale, newFormatOptions); + }, + constrain() { + setValidSegments(validSegments => { + let validKeys = Object.keys(validSegments); + let allKeys = Object.keys(allSegments); + + if (validKeys.length >= allKeys.length || (validKeys.length === allKeys.length - 1 && allSegments.dayPeriod && !validSegments.dayPeriod)) { + const value = Math.max(1, Math.min( displayValue.calendar.getDaysInMonth(displayValue),displayValue.day)); + console.log(value) + setValue(setSegment(displayValue, "day", value, resolvedOptions)) + } + return validSegments + }) } }; } @@ -437,9 +452,24 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption let isPlaceholder = EDITABLE_SEGMENTS[type] && !validSegments[type]; let placeholder = EDITABLE_SEGMENTS[type] ? getPlaceholder(type, segment.value, locale) : null; + let numberFormatter = new Intl.NumberFormat(locale, { + useGrouping: false + }); + + let twoDigitFormatter = new Intl.NumberFormat(locale, { + useGrouping: false, + minimumIntegerDigits: 2 + }) + + let segmentValue + if(segment.type === "day") segmentValue = displayValue.day + else if(segment.type === "month") segmentValue = displayValue.month + + let value = segment.type === "day" || segment.type === "month" ? twoDigitFormatter.format(segmentValue) : segment.value + let dateSegment = { type, - text: isPlaceholder ? placeholder : segment.value, + text: isPlaceholder ? placeholder : value, ...getSegmentLimits(displayValue, type, resolvedOptions), isPlaceholder, placeholder, @@ -517,7 +547,7 @@ function getSegmentLimits(date: DateValue, type: string, options: Intl.ResolvedD return { value: date.day, minValue: getMinimumDayInMonth(date), - maxValue: date.calendar.getDaysInMonth(date) + maxValue: 31 }; }