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
25 changes: 22 additions & 3 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import reactHooks from "eslint-plugin-react-hooks";
import jest from "eslint-plugin-jest";
import monorepo from "@jdb8/eslint-plugin-monorepo";
import * as rspRules from "eslint-plugin-rsp-rules";
import { fixupPluginRules } from "@eslint/compat";
import globals from "globals";
import babelParser from "@babel/eslint-parser";
import typescriptEslint from "@typescript-eslint/eslint-plugin";
Expand Down Expand Up @@ -67,7 +66,7 @@ export default [{
react,
rulesdir,
"jsx-a11y": jsxA11Y,
"react-hooks": fixupPluginRules(reactHooks),
"react-hooks": reactHooks,
jest,
monorepo,
"rsp-rules": rspRules,
Expand Down Expand Up @@ -225,8 +224,28 @@ export default [{
"react/jsx-boolean-value": ERROR,
"react/jsx-first-prop-new-line": [ERROR, "multiline"],
"react/self-closing-comp": ERROR,

// Core hooks rules
"react-hooks/rules-of-hooks": ERROR, // https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md
"react-hooks/exhaustive-deps": WARN,

// React Compiler rules
'react-hooks/config': ERROR,
'react-hooks/error-boundaries': ERROR,
'react-hooks/component-hook-factories': ERROR,
'react-hooks/gating': ERROR,
// 'react-hooks/globals': ERROR,
// 'react-hooks/immutability': ERROR,
// 'react-hooks/preserve-manual-memoization': ERROR,
// 'react-hooks/purity': ERROR,
// 'react-hooks/refs': ERROR,
// 'react-hooks/set-state-in-effect': ERROR,
'react-hooks/set-state-in-render': ERROR,
// 'react-hooks/static-components': ERROR,
'react-hooks/unsupported-syntax': WARN,
'react-hooks/use-memo': ERROR,
'react-hooks/incompatible-library': WARN,

"rsp-rules/no-react-key": [ERROR],
"rsp-rules/sort-imports": [ERROR],
"rulesdir/imports": [ERROR],
Expand Down Expand Up @@ -332,7 +351,7 @@ export default [{
react,
rulesdir,
"jsx-a11y": jsxA11Y,
"react-hooks": fixupPluginRules(reactHooks),
"react-hooks": reactHooks,
jest,
"@typescript-eslint": typescriptEslint,
monorepo,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@
"eslint-plugin-jsdoc": "^50.4.1",
"eslint-plugin-jsx-a11y": "^6.10.0",
"eslint-plugin-react": "^7.37.1",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-rulesdir": "^0.2.2",
"fast-check": "^2.19.0",
"fast-glob": "^3.1.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/actiongroup/src/useActionGroupItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function useActionGroupItem<T>(props: AriaActionGroupItemProps, state: Li
return () => {
onRemovedWithFocus();
};
}, [onRemovedWithFocus]);
}, []);

return {
buttonProps: mergeProps(buttonProps, {
Expand Down
43 changes: 23 additions & 20 deletions packages/@react-aria/autocomplete/src/useAutocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import {AriaLabelingProps, BaseEvent, DOMProps, FocusableElement, FocusEvents, KeyboardEvents, Node, RefObject, ValueBase} from '@react-types/shared';
import {AriaTextFieldProps} from '@react-aria/textfield';
import {AutocompleteProps, AutocompleteState} from '@react-stately/autocomplete';
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getOwnerDocument, isAndroid, isCtrlKeyPressed, isIOS, mergeProps, mergeRefs, useEffectEvent, useEvent, useId, useLabels, useObjectRef} from '@react-aria/utils';
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getOwnerDocument, isAndroid, isCtrlKeyPressed, isIOS, mergeProps, mergeRefs, useEffectEvent, useEvent, useId, useLabels, useLayoutEffect, useObjectRef} from '@react-aria/utils';
import {dispatchVirtualBlur, dispatchVirtualFocus, getVirtuallyFocusedElement, moveVirtualFocus} from '@react-aria/focus';
import {getInteractionModality} from '@react-aria/interactions';
// @ts-ignore
Expand Down Expand Up @@ -92,7 +92,6 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
let timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
let delayNextActiveDescendant = useRef(false);
let queuedActiveDescendant = useRef<string | null>(null);
let lastCollectionNode = useRef<HTMLElement>(null);

// For mobile screen readers, we don't want virtual focus, instead opting to disable FocusScope's restoreFocus and manually
// moving focus back to the subtriggers
Expand All @@ -106,7 +105,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
return () => clearTimeout(timeout.current);
}, []);

let updateActiveDescendant = useEffectEvent((e: Event) => {
let updateActiveDescendantEvent = useEffectEvent((e: Event) => {
// Ensure input is focused if the user clicks on the collection directly.
if (!e.isTrusted && shouldUseVirtualFocus && inputRef.current && getActiveElement(getOwnerDocument(inputRef.current)) !== inputRef.current) {
inputRef.current.focus();
Expand Down Expand Up @@ -140,32 +139,36 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
delayNextActiveDescendant.current = false;
});

let callbackRef = useCallback((collectionNode) => {
if (collectionNode != null) {
// When typing forward, we want to delay the setting of active descendant to not interrupt the native screen reader announcement
// of the letter you just typed. If we recieve another focus event then we clear the queued update
// We track lastCollectionNode to do proper cleanup since callbackRefs just pass null when unmounting. This also handles
// React 19's extra call of the callback ref in strict mode
lastCollectionNode.current?.removeEventListener('focusin', updateActiveDescendant);
lastCollectionNode.current = collectionNode;
collectionNode.addEventListener('focusin', updateActiveDescendant);
let [collectionNode, setCollectionNode] = useState<HTMLElement | null>(null);
let callbackRef = useCallback((node) => {
setCollectionNode(node);
if (node != null) {
// If useSelectableCollection isn't passed shouldUseVirtualFocus even when useAutocomplete provides it
// that means the collection doesn't support it (e.g. Table). If that is the case, we need to disable it here regardless
// of what the user's provided so that the input doesn't recieve the onKeyDown and autocomplete props.
if (collectionNode.getAttribute('tabindex') != null) {
if (node.getAttribute('tabindex') != null) {
setShouldUseVirtualFocus(false);
}
setHasCollection(true);
} else {
lastCollectionNode.current?.removeEventListener('focusin', updateActiveDescendant);
setHasCollection(false);
}
}, [updateActiveDescendant]);
}, []);
useLayoutEffect(() => {
if (collectionNode != null) {
// When typing forward, we want to delay the setting of active descendant to not interrupt the native screen reader announcement
// of the letter you just typed. If we recieve another focus event then we clear the queued update
collectionNode.addEventListener('focusin', updateActiveDescendantEvent);
}
return () => {
collectionNode?.removeEventListener('focusin', updateActiveDescendantEvent);
};
}, [collectionNode]);

// Make sure to memo so that React doesn't keep registering a new event listeners on every rerender of the wrapped collection
let mergedCollectionRef = useObjectRef(useMemo(() => mergeRefs(collectionRef, callbackRef), [collectionRef, callbackRef]));

let focusFirstItem = useEffectEvent(() => {
let focusFirstItem = useCallback(() => {
delayNextActiveDescendant.current = true;
collectionRef.current?.dispatchEvent(
new CustomEvent(FOCUS_EVENT, {
Expand All @@ -176,9 +179,9 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
}
})
);
});
}, [collectionRef]);

let clearVirtualFocus = useEffectEvent((clearFocusKey?: boolean) => {
let clearVirtualFocus = useCallback((clearFocusKey?: boolean) => {
moveVirtualFocus(getActiveElement());
queuedActiveDescendant.current = null;
state.setFocusedNodeId(null);
Expand All @@ -192,7 +195,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
clearTimeout(timeout.current);
delayNextActiveDescendant.current = false;
collectionRef.current?.dispatchEvent(clearFocusEvent);
});
}, [collectionRef, state]);

let lastInputType = useRef('');
useEvent(inputRef, 'input', e => {
Expand Down Expand Up @@ -346,7 +349,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
return () => {
document.removeEventListener('keyup', onKeyUpCapture, true);
};
}, [onKeyUpCapture]);
}, []);

let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/autocomplete');
let collectionProps = useLabels({
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/dnd/src/useClipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export function useClipboard(options: ClipboardProps): ClipboardResult {
addGlobalEventListener('beforepaste', onBeforePaste),
addGlobalEventListener('paste', onPaste)
);
}, [isDisabled, onBeforeCopy, onCopy, onBeforeCut, onCut, onBeforePaste, onPaste]);
}, [isDisabled]);

return {
clipboardProps: focusProps
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/dnd/src/useDrop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ export function useDrop(options: DropOptions): DropResult {
onDrop: onKeyboardDrop,
onDropActivate
});
}, [isDisabled, ref, getDropOperationKeyboard, onDropEnter, onDropExit, onKeyboardDrop, onDropActivate]);
}, [isDisabled, ref]);

let {dropProps} = useVirtualDrop();
if (isDisabled) {
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/form/src/useFormValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export function useFormValidation<T>(props: FormValidationProps<T>, state: FormV
form.reset = reset;
}
};
}, [ref, onInvalid, onChange, onReset, validationBehavior]);
}, [ref, validationBehavior]);
}

function getValidity(input: ValidatableElement) {
Expand Down
20 changes: 15 additions & 5 deletions packages/@react-aria/grid/src/useGridSelectionAnnouncement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import {Collection, Key, Node, Selection} from '@react-types/shared';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {SelectionManager} from '@react-stately/selection';
import {useEffectEvent, useUpdateEffect} from '@react-aria/utils';
import {useCallback, useRef} from 'react';
import {useLocalizedStringFormatter} from '@react-aria/i18n';
import {useRef} from 'react';
import {useUpdateEffect} from '@react-aria/utils';

export interface GridSelectionAnnouncementProps {
/**
Expand Down Expand Up @@ -46,7 +46,7 @@ export function useGridSelectionAnnouncement<T>(props: GridSelectionAnnouncement
// We do this using an ARIA live region.
let selection = state.selectionManager.rawSelection;
let lastSelection = useRef(selection);
let announceSelectionChange = useEffectEvent(() => {
let announceSelectionChange = useCallback(() => {
if (!state.selectionManager.isFocused || selection === lastSelection.current) {
lastSelection.current = selection;

Expand Down Expand Up @@ -101,8 +101,18 @@ export function useGridSelectionAnnouncement<T>(props: GridSelectionAnnouncement
}

lastSelection.current = selection;
});

}, [
selection,
state.selectionManager.selectedKeys,
state.selectionManager.isFocused,
state.selectionManager.selectionBehavior,
state.selectionManager.selectionMode,
state.collection,
getRowText,
stringFormatter
]);

// useUpdateEffect will handle using useEffectEvent, no need to stabilize anything on this end
useUpdateEffect(() => {
if (state.selectionManager.isFocused) {
announceSelectionChange();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export function useInteractOutside(props: InteractOutsideProps): void {
documentObject.removeEventListener('touchend', onTouchEnd, true);
};
}
}, [ref, isDisabled, onPointerDown, triggerInteractOutside]);
}, [ref, isDisabled]);
}

function isValidEvent(event, ref) {
Expand Down
Loading