Skip to content
Open
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
3 changes: 1 addition & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export default [{
'react-hooks/component-hook-factories': ERROR,
'react-hooks/gating': ERROR,
'react-hooks/globals': ERROR,
// 'react-hooks/immutability': ERROR,
'react-hooks/immutability': ERROR,
// 'react-hooks/preserve-manual-memoization': ERROR, // No idea how to turn this one on yet
'react-hooks/purity': ERROR,
// 'react-hooks/refs': ERROR, // can't turn on until https://github.com/facebook/react/issues/34775 is fixed
Expand All @@ -250,7 +250,6 @@ export default [{
"rsp-rules/sort-imports": [ERROR],
"rulesdir/imports": [ERROR],
"rulesdir/useLayoutEffectRule": [ERROR],
"rulesdir/pure-render": [ERROR],
"jsx-a11y/accessible-emoji": ERROR,
"jsx-a11y/alt-text": ERROR,
"jsx-a11y/anchor-has-content": ERROR,
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-aria/breadcrumbs/src/useBreadcrumbItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export function useBreadcrumbItem(props: AriaBreadcrumbItemProps, ref: RefObject
...otherProps
} = props;

let {linkProps} = useLink({isDisabled: isDisabled || isCurrent, elementType, ...otherProps}, ref);
let {linkProps: linkBaseLinkProps} = useLink({isDisabled: isDisabled || isCurrent, elementType, ...otherProps}, ref);
let linkProps = {...linkBaseLinkProps};
let isHeading = /^h[1-6]$/.test(elementType);
let itemProps: DOMAttributes = {};

Expand Down
3 changes: 2 additions & 1 deletion packages/@react-aria/button/src/useButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ export function useButton(props: AriaButtonOptions<ElementType>, ref: RefObject<
ref
});

let {focusableProps} = useFocusable(props, ref);
let {focusableProps: focusableBaseFocusableProps} = useFocusable(props, ref);
let focusableProps = {...focusableBaseFocusableProps};
if (allowFocusWhenDisabled) {
focusableProps.tabIndex = isDisabled ? -1 : focusableProps.tabIndex;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-aria/button/src/useToggleButtonGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,12 @@ export function useToggleButtonGroupItem(props: AriaToggleButtonGroupItemOptions
}
};

let {isPressed, isSelected, isDisabled, buttonProps} = useToggleButton({
let {isPressed, isSelected, isDisabled, buttonProps: toggleButtonProps} = useToggleButton({
...props,
id: undefined,
isDisabled: props.isDisabled || state.isDisabled
}, toggleState, ref);
let buttonProps = {...toggleButtonProps};
if (state.selectionMode === 'single') {
buttonProps.role = 'radio';
buttonProps['aria-checked'] = toggleState.isSelected;
Expand Down
7 changes: 4 additions & 3 deletions packages/@react-aria/calendar/src/useRangeCalendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {useRef} from 'react';
* A range calendar displays one or more date grids and allows users to select a contiguous range of dates.
*/
export function useRangeCalendar<T extends DateValue>(props: AriaRangeCalendarProps<T>, state: RangeCalendarState, ref: RefObject<FocusableElement | null>): CalendarAria {
let res = useCalendarBase(props, state);
let {calendarProps: calendarBaseCalendarProps, ...res} = useCalendarBase(props, state);
let calendarProps = {...calendarBaseCalendarProps};

// We need to ignore virtual pointer events from VoiceOver due to these bugs.
// https://bugs.webkit.org/show_bug.cgi?id=222627
Expand Down Expand Up @@ -62,7 +63,7 @@ export function useRangeCalendar<T extends DateValue>(props: AriaRangeCalendarPr
useEvent(windowRef, 'pointerup', endDragging);

// Also stop range selection on blur, e.g. tabbing away from the calendar.
res.calendarProps.onBlur = e => {
calendarProps.onBlur = e => {
if (!ref.current) {
return;
}
Expand All @@ -78,5 +79,5 @@ export function useRangeCalendar<T extends DateValue>(props: AriaRangeCalendarPr
}
}, {passive: false, capture: true});

return res;
return {...res, calendarProps};
}
29 changes: 18 additions & 11 deletions packages/@react-aria/collections/src/CollectionBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,16 @@ export function CollectionBuilder<C extends BaseCollection<object>>(props: Colle

// Otherwise, render a hidden copy of the children so that we can build the collection before constructing the state.
// This should always come before the real DOM content so we have built the collection by the time it renders during SSR.
return (
<CollectionBuilderInner createCollection={props.createCollection} content={props.content}>
{props.children}
</CollectionBuilderInner>
);
}

// This is fine. CollectionDocumentContext never changes after mounting.
// eslint-disable-next-line react-hooks/rules-of-hooks
function CollectionBuilderInner(props) {
// Otherwise, render a hidden copy of the children so that we can build the collection before constructing the state.
// This should always come before the real DOM content so we have built the collection by the time it renders during SSR.
let {collection, document} = useCollectionDocument(props.createCollection);
return (
<>
Expand Down Expand Up @@ -80,7 +87,6 @@ function useSyncExternalStoreFallback<C>(subscribe: (onStoreChange: () => void)
// This is read immediately inside the wrapper, which also runs during render.
// We just need a ref to avoid invalidating the callback itself, which
// would cause React to re-run the callback more than necessary.
// eslint-disable-next-line rulesdir/pure-render
isSSRRef.current = isSSR;

let getSnapshotWrapper = useCallback(() => {
Expand Down Expand Up @@ -109,7 +115,7 @@ function useCollectionDocument<T extends object, C extends BaseCollection<T>>(cr
return collection;
}, [document]);
let getServerSnapshot = useCallback(() => {
document.isSSR = true;
document.setSSR(true);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

had to introduce this because the React compiler is a bit strict about mutating, I think it should be allowed in this case, but React doesn't know that, so I've tricked it. I had tried storing the document in a ref and editing it that way, which fixes the compiler problem, but it broke some tests, I'm not sure why and it makes me a little worried. This is the closest to what we've tested for years though, so I'm ok with this for now.

return document.getCollection();
}, [document]);
let collection = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
Expand All @@ -132,8 +138,9 @@ function createCollectionNodeClass(type: string): CollectionNodeClass<any> {

function useSSRCollectionNode<T extends Element>(CollectionNodeClass: CollectionNodeClass<T> | string, props: object, ref: ForwardedRef<T>, rendered?: any, children?: ReactNode, render?: (node: Node<any>) => ReactElement) {
// To prevent breaking change, if CollectionNodeClass is a string, create a CollectionNodeClass using the string as the type
if (typeof CollectionNodeClass === 'string') {
CollectionNodeClass = createCollectionNodeClass(CollectionNodeClass);
let CollectionNodeClassLocal = CollectionNodeClass;
if (typeof CollectionNodeClassLocal === 'string') {
CollectionNodeClassLocal = createCollectionNodeClass(CollectionNodeClassLocal);
}

// During SSR, portals are not supported, so the collection children will be wrapped in an SSRContext.
Expand All @@ -142,15 +149,15 @@ function useSSRCollectionNode<T extends Element>(CollectionNodeClass: Collection
// collection by the time we need to use the collection to render to the real DOM.
// After hydration, we switch to client rendering using the portal.
let itemRef = useCallback((element: ElementNode<any> | null) => {
element?.setProps(props, ref, CollectionNodeClass, rendered, render);
}, [props, ref, rendered, render, CollectionNodeClass]);
element?.setProps(props, ref, CollectionNodeClassLocal, rendered, render);
}, [props, ref, rendered, render, CollectionNodeClassLocal]);
let parentNode = useContext(SSRContext);
if (parentNode) {
// Guard against double rendering in strict mode.
let element = parentNode.ownerDocument.nodesByProps.get(props);
if (!element) {
element = parentNode.ownerDocument.createElement(CollectionNodeClass.type);
element.setProps(props, ref, CollectionNodeClass, rendered, render);
element = parentNode.ownerDocument.createElement(CollectionNodeClassLocal.type);
element.setProps(props, ref, CollectionNodeClassLocal, rendered, render);
parentNode.appendChild(element);
parentNode.ownerDocument.updateCollection();
parentNode.ownerDocument.nodesByProps.set(props, element);
Expand All @@ -162,7 +169,7 @@ function useSSRCollectionNode<T extends Element>(CollectionNodeClass: Collection
}

// @ts-ignore
return <CollectionNodeClass.type ref={itemRef}>{children}</CollectionNodeClass.type>;
return <CollectionNodeClassLocal.type ref={itemRef}>{children}</CollectionNodeClassLocal.type>;
}

export function createLeafComponent<T extends object, P extends object, E extends Element>(CollectionNodeClass: CollectionNodeClass<any> | string, render: (props: P, ref: ForwardedRef<E>) => ReactElement | null): (props: P & React.RefAttributes<T>) => ReactElement | null;
Expand Down
4 changes: 4 additions & 0 deletions packages/@react-aria/collections/src/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,10 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
return true;
}

setSSR(value: boolean): void {
this.isSSR = value;
}

createElement(type: string): ElementNode<T> {
return new ElementNode(type, this);
}
Expand Down
13 changes: 9 additions & 4 deletions packages/@react-aria/color/src/useColorArea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@ export function useColorArea(props: AriaColorAreaOptions, state: ColorAreaState)

let {direction, locale} = useLocale();

let [focusedInput, setFocusedInput] = useState<'x' | 'y' | null>(null);
let [focusedInput, _setFocusedInput] = useState<'x' | 'y' | null>(null);
let focusedInputRef = useRef<'x' | 'y' | null>(focusedInput);
let setFocusedInput = useCallback((newFocusedInput: 'x' | 'y' | null) => {
focusedInputRef.current = newFocusedInput;
_setFocusedInput(newFocusedInput);
}, [_setFocusedInput]);
let focusInput = useCallback((inputRef:RefObject<HTMLInputElement | null> = inputXRef) => {
if (inputRef.current) {
focusWithoutScrolling(inputRef.current);
Expand Down Expand Up @@ -157,8 +162,8 @@ export function useColorArea(props: AriaColorAreaOptions, state: ColorAreaState)
}
setValueChangedViaKeyboard(valueChanged);
// set the focused input based on which axis has the greater delta
focusedInput = valueChanged && Math.abs(deltaY) > Math.abs(deltaX) ? 'y' : 'x';
setFocusedInput(focusedInput);
let newFocusedInput = valueChanged && Math.abs(deltaY) > Math.abs(deltaX) ? 'y' as const : 'x' as const;
setFocusedInput(newFocusedInput);
} else {
currentPosition.current.x += (direction === 'rtl' ? -1 : 1) * deltaX / width ;
currentPosition.current.y += deltaY / height;
Expand All @@ -168,7 +173,7 @@ export function useColorArea(props: AriaColorAreaOptions, state: ColorAreaState)
onMoveEnd() {
isOnColorArea.current = false;
state.setDragging(false);
let input = focusedInput === 'x' ? inputXRef : inputYRef;
let input = focusedInputRef.current === 'x' ? inputXRef : inputYRef;
focusInput(input);
}
};
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-aria/color/src/useColorSlider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function useColorSlider(props: AriaColorSliderOptions, state: ColorSlider

// @ts-ignore - ignore unused incompatible props
let {groupProps, trackProps, labelProps, outputProps} = useSlider({...props, 'aria-label': ariaLabel}, state, trackRef);
let {inputProps, thumbProps} = useSliderThumb({
let {inputProps: sliderInputProps, thumbProps} = useSliderThumb({
index: 0,
orientation,
isDisabled: props.isDisabled,
Expand All @@ -64,6 +64,7 @@ export function useColorSlider(props: AriaColorSliderOptions, state: ColorSlider
trackRef,
inputRef
}, state);
let inputProps = {...sliderInputProps};

let value = state.getDisplayColor();
let generateBackground = () => {
Expand Down
7 changes: 4 additions & 3 deletions packages/@react-aria/datepicker/src/useDateField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,8 @@ export interface AriaTimeFieldOptions<T extends TimeValue> extends AriaTimeField
* Each part of a time value is displayed in an individually editable segment.
*/
export function useTimeField<T extends TimeValue>(props: AriaTimeFieldOptions<T>, state: TimeFieldState, ref: RefObject<Element | null>): DateFieldAria {
let res = useDateField(props, state, ref);
res.inputProps.value = state.timeValue?.toString() || '';
return res;
let {inputProps: dateFieldInputProps, ...res} = useDateField(props, state, ref);
let inputProps = {...dateFieldInputProps};
inputProps.value = state.timeValue?.toString() || '';
return {...res, inputProps};
}
3 changes: 2 additions & 1 deletion packages/@react-aria/dnd/src/useDraggableItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const MESSAGES = {
export function useDraggableItem(props: DraggableItemProps, state: DraggableCollectionState): DraggableItemResult {
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/dnd');
let isDisabled = state.isDisabled || state.selectionManager.isDisabled(props.key);
let {dragProps, dragButtonProps} = useDrag({
let {dragProps: draggableDragProps, dragButtonProps} = useDrag({
getItems() {
return state.getItems(props.key);
},
Expand All @@ -88,6 +88,7 @@ export function useDraggableItem(props: DraggableItemProps, state: DraggableColl
clearGlobalDnDState();
}
});
let dragProps = {...draggableDragProps};

let item = state.collection.getItem(props.key);
let numKeysForDrag = state.getKeysForDrag(props.key).size;
Expand Down
12 changes: 10 additions & 2 deletions packages/@react-aria/form/src/useFormValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
*/

import {FormValidationState} from '@react-stately/form';
import {getOwnerDocument, useEffectEvent, useLayoutEffect} from '@react-aria/utils';
import {RefObject, Validation, ValidationResult} from '@react-types/shared';
import {setInteractionModality} from '@react-aria/interactions';
import {useEffect, useRef} from 'react';
import {useEffectEvent, useLayoutEffect} from '@react-aria/utils';

type ValidatableElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;

Expand Down Expand Up @@ -84,7 +84,15 @@ export function useFormValidation<T>(props: FormValidationProps<T>, state: FormV
return;
}

let form = input.form;
// Uses closest and querySelector instead of just the form property to work around a React compiler bug.
// https://github.com/facebook/react/issues/34891
let form = input.closest('form');
if (!form) {
let formId = input.getAttribute('form');
if (formId) {
form = getOwnerDocument(input).querySelector(`#${formId}`) as HTMLFormElement | null;
}
}

let reset = form?.reset;
if (form) {
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/i18n/src/useDateFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ export interface DateFormatterOptions extends Intl.DateTimeFormatOptions {
*/
export function useDateFormatter(options?: DateFormatterOptions): DateFormatter {
// Reuse last options object if it is shallowly equal, which allows the useMemo result to also be reused.
options = useDeepMemo(options ?? {}, isEqual);
let memoizedOptions = useDeepMemo(options ?? {}, isEqual);
let {locale} = useLocale();
return useMemo(() => new DateFormatter(locale, options), [locale, options]);
return useMemo(() => new DateFormatter(locale, memoizedOptions), [locale, memoizedOptions]);
}

function isEqual(a: DateFormatterOptions, b: DateFormatterOptions) {
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/interactions/src/PressResponder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ React.forwardRef(({children, ...props}: PressResponderProps, ref: ForwardedRef<F
}
});

useSyncRef(prevContext, ref);
useSyncRef(prevContext.ref, ref);

useEffect(() => {
if (!isRegistered.current) {
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/interactions/src/useFocusable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export let FocusableContext: React.Context<FocusableContextValue | null> = React

function useFocusableContext(ref: RefObject<FocusableElement | null>): FocusableContextValue {
let context = useContext(FocusableContext) || {};
useSyncRef(context, ref);
useSyncRef(context.ref, ref);

// eslint-disable-next-line
let {ref: _, ...otherProps} = context;
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/interactions/src/usePress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ function usePressResponderContext(props: PressHookProps): PressHookProps {
props = mergeProps(contextProps, props) as PressHookProps;
register();
}
useSyncRef(context, props.ref);
useSyncRef(context.ref, props.ref);

return props;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-aria/menu/src/useMenuTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export function useMenuTrigger<T>(props: AriaMenuTriggerProps, state: MenuTrigge
} = props;

let menuTriggerId = useId();
let {triggerProps, overlayProps} = useOverlayTrigger({type}, state, ref);
let {triggerProps: overlayTriggerProps, overlayProps} = useOverlayTrigger({type}, state, ref);
let triggerProps = {...overlayTriggerProps};

let onKeyDown = (e) => {
if (isDisabled) {
Expand Down
5 changes: 3 additions & 2 deletions packages/@react-aria/select/src/useSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export function useSelect<T, M extends SelectionMode = 'single'>(props: AriaSele
if (state.selectionManager.selectionMode === 'multiple') {
return;
}

switch (e.key) {
case 'ArrowLeft': {
// prevent scrolling containers
Expand All @@ -124,13 +124,14 @@ export function useSelect<T, M extends SelectionMode = 'single'>(props: AriaSele
}
};

let {typeSelectProps} = useTypeSelect({
let {typeSelectProps: selectTypeSelectProps} = useTypeSelect({
keyboardDelegate: delegate,
selectionManager: state.selectionManager,
onTypeSelect(key) {
state.setSelectedKey(key);
}
});
let typeSelectProps = {...selectTypeSelectProps};

let {isInvalid, validationErrors, validationDetails} = state.displayValidation;
let {labelProps, fieldProps, descriptionProps, errorMessageProps} = useField({
Expand Down
8 changes: 6 additions & 2 deletions packages/@react-aria/slider/test/useSlider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ describe('useSlider', () => {
function Example(props) {
let trackRef = useRef(null);
let state = useSliderState({...props, numberFormatter});
stateRef.current = state;
React.useEffect(() => {
stateRef.current = state;
}, [state]);
let {trackProps} = useSlider(props, state, trackRef);
return <div data-testid="track" ref={trackRef} {...trackProps} />;
}
Expand Down Expand Up @@ -182,7 +184,9 @@ describe('useSlider', () => {
function Example(props) {
let trackRef = useRef(null);
let state = useSliderState({...props, numberFormatter});
stateRef.current = state;
React.useEffect(() => {
stateRef.current = state;
}, [state]);
let {trackProps} = useSlider(props, state, trackRef);
return <div data-testid="track" ref={trackRef} {...trackProps} />;
}
Expand Down
8 changes: 6 additions & 2 deletions packages/@react-aria/slider/test/useSliderThumb.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ describe('useSliderThumb', () => {
let input0Ref = useRef(null);
let input1Ref = useRef(null);
let state = useSliderState({...props, numberFormatter});
stateRef.current = state;
React.useEffect(() => {
stateRef.current = state;
}, [state]);
let {trackProps, thumbProps: commonThumbProps} = useSlider(props, state, trackRef);
let {inputProps: input0Props, thumbProps: thumb0Props} = useSliderThumb({
...commonThumbProps,
Expand Down Expand Up @@ -273,7 +275,9 @@ describe('useSliderThumb', () => {
let trackRef = useRef(null);
let inputRef = useRef(null);
let state = useSliderState({...props, numberFormatter});
stateRef.current = state;
React.useEffect(() => {
stateRef.current = state;
}, [state]);
let {trackProps} = useSlider(props, state, trackRef);
let {inputProps, thumbProps} = useSliderThumb({
...props,
Expand Down
Loading