Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 5 additions & 29 deletions packages/@react-aria/textfield/src/useTextField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -124,20 +122,9 @@ export function useTextField<T extends TextFieldIntrinsicElements = DefaultEleme
isRequired = false,
isReadOnly = false,
type = 'text',
validationBehavior = 'aria',
onChange: onChangeProp
validationBehavior = 'aria'
} = props;

let isComposing = useRef(false);
let onChange = useCallback((val) => {
if (isComposing.current) {
return;
}

onChangeProp?.(val);
}, [onChangeProp]);

let [value, setValue] = useControlledState<string>(props.value, props.defaultValue || '', onChange);
let [value, setValue] = useControlledState<string>(props.value, props.defaultValue || '', props.onChange);
let {focusableProps} = useFocusable<TextFieldHTMLElementType[T]>(props, ref);
let validationState = useFormValidationState({
...props,
Expand Down Expand Up @@ -178,17 +165,6 @@ export function useTextField<T extends TextFieldIntrinsicElements = DefaultEleme
}
}, [ref]);

let onCompositionStart = useCallback(() => {
isComposing.current = true;
}, []);

let onCompositionEnd = useCallback((e) => {
isComposing.current = false;
if (e.data !== '') {
onChangeProp?.(value);
}
}, [onChangeProp, value]);

return {
labelProps,
inputProps: mergeProps(
Expand Down Expand Up @@ -225,8 +201,8 @@ export function useTextField<T extends TextFieldIntrinsicElements = DefaultEleme
onPaste: props.onPaste,

// Composition events
onCompositionEnd: chain(onCompositionEnd, props.onCompositionEnd),
onCompositionStart: chain(onCompositionStart, props.onCompositionStart),
onCompositionEnd: props.onCompositionEnd,
onCompositionStart: props.onCompositionStart,
onCompositionUpdate: props.onCompositionUpdate,

// Selection events
Expand Down
27 changes: 8 additions & 19 deletions packages/@react-spectrum/s2/src/ActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ export const btnStyles = style<ButtonRenderProps & ActionButtonStyleProps & Togg
...focusRing(),
...staticColor(),
...controlStyle,
display: 'grid',
justifyContent: 'center',
flexShrink: {
default: 1,
Expand All @@ -80,21 +79,9 @@ export const btnStyles = style<ButtonRenderProps & ActionButtonStyleProps & Togg
isJustified: 0
},
fontWeight: 'medium',
width: 'fit',
userSelect: 'none',
transition: 'default',
forcedColorAdjust: 'none',
position: 'relative',
gridTemplateAreas: {
default: ['icon text'],
[iconOnly]: ['icon'],
[textOnly]: ['text']
},
gridTemplateColumns: {
default: ['auto', 'auto'],
[iconOnly]: ['auto'],
[textOnly]: ['auto']
},
backgroundColor: {
default: {
...baseColor('gray-100'),
Expand Down Expand Up @@ -236,7 +223,8 @@ export const btnStyles = style<ButtonRenderProps & ActionButtonStyleProps & Togg
'--badgePosition': {
type: 'width',
value: {
default: '--iconWidth',
default: 'calc(self(paddingStart) + var(--iconWidth))',
[iconOnly]: 'calc(self(minWidth)/2 + var(--iconWidth)/2)',
[textOnly]: 'full'
}
},
Expand Down Expand Up @@ -301,10 +289,10 @@ export const ActionButton = forwardRef(function ActionButton(props: ActionButton
<Provider
values={[
[SkeletonContext, null],
[TextContext, {styles: style({truncate: true, gridArea: 'text'})}],
[TextContext, {styles: style({order: 1, truncate: true})}],
[IconContext, {
render: centerBaseline({slot: 'icon', styles: style({gridArea: 'icon'})}),
styles: style({size: fontRelative(20), marginStart: '--iconMargin'})
render: centerBaseline({slot: 'icon', styles: style({order: 0})}),
styles: style({size: fontRelative(20), marginStart: '--iconMargin', flexShrink: 0})
}],
[AvatarContext, {
size: avatarSize[size],
Expand All @@ -313,14 +301,15 @@ export const ActionButton = forwardRef(function ActionButton(props: ActionButton
default: '--iconMargin',
':last-child': 0
},
gridArea: 'icon'
flexShrink: 0,
order: 0
})
}],
[NotificationBadgeContext, {
staticColor: staticColor,
size: props.size === 'XS' ? undefined : props.size,
isDisabled: props.isDisabled,
styles: style({position: 'absolute', top: '--badgeTop', marginTop: 'calc((self(height) * -1)/2)', marginStart: 'calc(var(--iconMargin) * 2 + (self(height) * -1)/4)', gridColumnStart: 1, insetStart: '--badgePosition'})
styles: style({position: 'absolute', top: '--badgeTop', insetStart: '--badgePosition', marginTop: 'calc((self(height) * -1)/2)', marginStart: 'calc((self(height) * -1)/2)'})
}]
]}>
{typeof props.children === 'string' ? <Text>{props.children}</Text> : props.children}
Expand Down
14 changes: 7 additions & 7 deletions packages/@react-stately/datepicker/src/useDateFieldState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,8 @@ export function useDateFieldState<T extends DateValue = DateValue>(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.
Expand All @@ -286,8 +286,8 @@ export function useDateFieldState<T extends DateValue = DateValue>(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.
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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);
}
}
Expand Down
15 changes: 13 additions & 2 deletions packages/react-aria-components/src/DragAndDrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
}
}

Expand Down
26 changes: 26 additions & 0 deletions packages/react-aria-components/test/DateField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<DateField>
<Label>Date</Label>
<DateInput>
{segment => <DateSegment segment={segment} />}
</DateInput>
</DateField>
);

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');
});
});
Loading