diff --git a/packages/@react-aria/textfield/src/useTextField.ts b/packages/@react-aria/textfield/src/useTextField.ts index 8616abeb94b..e232514d052 100644 --- a/packages/@react-aria/textfield/src/useTextField.ts +++ b/packages/@react-aria/textfield/src/useTextField.ts @@ -11,17 +11,15 @@ */ import {AriaTextFieldProps} from '@react-types/textfield'; -import {chain, filterDOMProps, getOwnerWindow, mergeProps, useFormReset} from '@react-aria/utils'; import {DOMAttributes, ValidationResult} from '@react-types/shared'; +import {filterDOMProps, getOwnerWindow, mergeProps, useFormReset} from '@react-aria/utils'; import React, { ChangeEvent, HTMLAttributes, type JSX, LabelHTMLAttributes, RefObject, - useCallback, useEffect, - useRef, useState } from 'react'; import {useControlledState} from '@react-stately/utils'; @@ -124,20 +122,9 @@ export function useTextField { - if (isComposing.current) { - return; - } - - onChangeProp?.(val); - }, [onChangeProp]); - - let [value, setValue] = useControlledState(props.value, props.defaultValue || '', onChange); + let [value, setValue] = useControlledState(props.value, props.defaultValue || '', props.onChange); let {focusableProps} = useFocusable(props, ref); let validationState = useFormValidationState({ ...props, @@ -178,17 +165,6 @@ export function useTextField { - isComposing.current = true; - }, []); - - let onCompositionEnd = useCallback((e) => { - isComposing.current = false; - if (e.data !== '') { - onChangeProp?.(value); - } - }, [onChangeProp, value]); - return { labelProps, inputProps: mergeProps( @@ -225,8 +201,8 @@ export function useTextField {typeof props.children === 'string' ? {props.children} : props.children} diff --git a/packages/@react-stately/datepicker/src/useDateFieldState.ts b/packages/@react-stately/datepicker/src/useDateFieldState.ts index d66f5881c86..2bf28053575 100644 --- a/packages/@react-stately/datepicker/src/useDateFieldState.ts +++ b/packages/@react-stately/datepicker/src/useDateFieldState.ts @@ -264,8 +264,8 @@ export function useDateFieldState(props: DateFi setPlaceholderDate(createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone)); setValidSegments({}); } else if ( - validKeys.length === 0 || - validKeys.length >= allKeys.length || + (validKeys.length === 0 && clearedSegment.current == null) || + validKeys.length >= allKeys.length || (validKeys.length === allKeys.length - 1 && allSegments.dayPeriod && !validSegments.dayPeriod && clearedSegment.current !== 'dayPeriod') ) { // If the field was empty (no valid segments) or all segments are completed, commit the new value. @@ -286,8 +286,8 @@ export function useDateFieldState(props: DateFi }; let dateValue = useMemo(() => displayValue.toDate(timeZone), [displayValue, timeZone]); - let segments = useMemo(() => - processSegments(dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale, granularity), + let segments = useMemo(() => + processSegments(dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale, granularity), [dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale, granularity]); // When the era field appears, mark it valid if the year field is already valid. @@ -452,9 +452,9 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption // There is an issue in RTL languages where time fields render (minute:hour) instead of (hour:minute). // To force an LTR direction on the time field since, we wrap the time segments in LRI (left-to-right) isolate unicode. See https://www.w3.org/International/questions/qa-bidi-unicode-controls. - // These unicode characters will be added to the array of processed segments as literals and will mark the start and end of the embedded direction change. + // These unicode characters will be added to the array of processed segments as literals and will mark the start and end of the embedded direction change. if (type === 'hour') { - // This marks the start of the embedded direction change. + // This marks the start of the embedded direction change. processedSegments.push({ type: 'literal', text: '\u2066', @@ -487,7 +487,7 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption isEditable: false }); } else { - // We only want to "wrap" the unicode around segments that are hour, minute, or second. If they aren't, just process as normal. + // We only want to "wrap" the unicode around segments that are hour, minute, or second. If they aren't, just process as normal. processedSegments.push(dateSegment); } } diff --git a/packages/react-aria-components/src/DragAndDrop.tsx b/packages/react-aria-components/src/DragAndDrop.tsx index f5bfb0e6e61..50764ee1642 100644 --- a/packages/react-aria-components/src/DragAndDrop.tsx +++ b/packages/react-aria-components/src/DragAndDrop.tsx @@ -76,6 +76,7 @@ export function useDndPersistedKeys(selectionManager: MultipleSelectionManager, if (dropState.target.dropPosition === 'after') { // Normalize to the "before" drop position since we only render those to the DOM. let nextKey = dropState.collection.getKeyAfter(dropTargetKey); + let lastDescendantKey: Key | null = null; if (nextKey != null) { let targetLevel = dropState.collection.getItem(dropTargetKey)?.level ?? 0; // Skip over any rows that are descendants of the target ("after" position should be after all children) @@ -85,16 +86,26 @@ export function useDndPersistedKeys(selectionManager: MultipleSelectionManager, if (!node) { break; } - // Stop once we find a node at the same level or higher + // Skip over non-item nodes (e.g., loaders) since they can't be drop targets. + // eslint-disable-next-line max-depth + if (node.type !== 'item') { + nextKey = dropState.collection.getKeyAfter(nextKey); + continue; + } + + // Stop once we find an item at the same level or higher // eslint-disable-next-line max-depth if ((node.level ?? 0) <= targetLevel) { break; } + + lastDescendantKey = nextKey; nextKey = dropState.collection.getKeyAfter(nextKey); } } - dropTargetKey = nextKey ?? dropTargetKey; + // If nextKey is null (end of collection), use the last descendant + dropTargetKey = nextKey ?? lastDescendantKey ?? dropTargetKey; } } diff --git a/packages/react-aria-components/test/DateField.test.js b/packages/react-aria-components/test/DateField.test.js index 2595c63abed..662be07a3e6 100644 --- a/packages/react-aria-components/test/DateField.test.js +++ b/packages/react-aria-components/test/DateField.test.js @@ -427,4 +427,30 @@ describe('DateField', () => { let input = getByRole('group'); expect(input).toHaveTextContent('5/30/2000'); }); + + it('should reset to placeholders when deleting a partially filled DateField', async () => { + let {getAllByRole} = render( + + + + {segment => } + + + ); + + let segements = getAllByRole('spinbutton'); + let monthSegment = segements[0]; + expect(monthSegment).toHaveTextContent('mm'); + await user.click(monthSegment); + expect(monthSegment).toHaveFocus(); + await user.keyboard('11'); + expect(monthSegment).toHaveTextContent('11'); + + await user.click(monthSegment); + await user.keyboard('{backspace}'); + await user.keyboard('{backspace}'); + expect(monthSegment).toHaveTextContent('mm'); + expect(segements[1]).toHaveTextContent('dd'); + expect(segements[2]).toHaveTextContent('yyyy'); + }); });