Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
Loading