Skip to content

Commit 18c31cf

Browse files
committed
Remove changes to @react-aria - change ListStateContext and properly memoize ListBoxItemInner
1 parent 3c852b5 commit 18c31cf

File tree

13 files changed

+58
-61
lines changed

13 files changed

+58
-61
lines changed

packages/@react-aria/interactions/src/useHover.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,8 @@ export function useHover(props: HoverProps): HoverResult {
213213
// eslint-disable-next-line react-hooks/exhaustive-deps
214214
}, [isDisabled]);
215215

216-
return useMemo(() => ({
216+
return {
217217
hoverProps,
218218
isHovered
219-
}), [hoverProps, isHovered]);
219+
};
220220
}

packages/@react-aria/listbox/src/useOption.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {getItemId, listData} from './utils';
1717
import {isFocusVisible, useHover} from '@react-aria/interactions';
1818
import {ListState} from '@react-stately/list';
1919
import {SelectableItemStates, useSelectableItem} from '@react-aria/selection';
20-
import {useCallback} from 'react';
2120

2221
export interface OptionAria extends SelectableItemStates {
2322
/** Props for the option element. */
@@ -143,12 +142,12 @@ export function useOption<T>(props: AriaOptionProps, state: ListState<T>, ref: R
143142

144143
let {hoverProps} = useHover({
145144
isDisabled: isDisabled || !shouldFocusOnHover,
146-
onHoverStart: useCallback(() => {
145+
onHoverStart() {
147146
if (!isFocusVisible()) {
148147
state.selectionManager.setFocused(true);
149148
state.selectionManager.setFocusedKey(key);
150149
}
151-
}, [state.selectionManager])
150+
}
152151
});
153152

154153
let domProps = filterDOMProps(item?.props);

packages/@react-aria/selection/src/useSelectableItem.ts

+8-11
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {getCollectionId, isNonContiguousSelectionModifier} from './utils';
1616
import {isCtrlKeyPressed, mergeProps, openLink, useId, useRouter} from '@react-aria/utils';
1717
import {moveVirtualFocus} from '@react-aria/focus';
1818
import {MultipleSelectionManager} from '@react-stately/selection';
19-
import {useEffect, useRef, useCallback} from 'react';
19+
import {useEffect, useRef} from 'react';
2020

2121
export interface SelectableItemOptions extends DOMProps {
2222
/**
@@ -184,17 +184,14 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
184184
// item is tabbable. If using virtual focus, don't set a tabIndex at all so that VoiceOver
185185
// on iOS 14 doesn't try to move real DOM focus to the item anyway.
186186
let itemProps: SelectableItemAria['itemProps'] = {};
187-
188-
const onFocus = useCallback(e => {
189-
if (e.target === ref.current) {
190-
manager.setFocusedKey(key);
191-
}
192-
}, [manager]);
193-
194187
if (!shouldUseVirtualFocus && !isDisabled) {
195188
itemProps = {
196189
tabIndex: key === manager.focusedKey ? 0 : -1,
197-
onFocus
190+
onFocus(e) {
191+
if (e.target === ref.current) {
192+
manager.setFocusedKey(key);
193+
}
194+
}
198195
};
199196
} else if (isDisabled) {
200197
itemProps.onMouseDown = (e) => {
@@ -368,11 +365,11 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
368365
// Once the user is in selection mode, they can long press again to drag.
369366
// Use a capturing listener to ensure this runs before useDrag, regardless of
370367
// the order the props get merged.
371-
let onDragStartCapture = useCallback(e => {
368+
let onDragStartCapture = e => {
372369
if (modality.current === 'touch' && longPressEnabledOnPressStart.current) {
373370
e.preventDefault();
374371
}
375-
}, []);
372+
};
376373

377374
// Prevent default on link clicks so that we control exactly
378375
// when they open (to match selection behavior).

packages/@react-aria/utils/src/useGlobalListeners.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {useCallback, useEffect, useRef, useMemo} from 'react';
13+
import {useCallback, useEffect, useRef} from 'react';
1414

1515
interface GlobalListeners {
1616
addGlobalListener<K extends keyof WindowEventMap>(el: Window, type: K, listener: (this: Document, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void,
@@ -48,5 +48,5 @@ export function useGlobalListeners(): GlobalListeners {
4848
return removeAllGlobalListeners;
4949
}, [removeAllGlobalListeners]);
5050

51-
return useMemo(() => ({addGlobalListener, removeGlobalListener, removeAllGlobalListeners}), [addGlobalListener, removeGlobalListener, removeAllGlobalListeners]);
51+
return {addGlobalListener, removeGlobalListener, removeAllGlobalListeners};
5252
}

packages/@react-stately/list/src/useListState.ts

+5-9
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@ export interface ListState<T> {
3535
/** A set of items that are disabled. */
3636
disabledKeys: Set<Key>,
3737

38-
/** The focused key */
39-
focusedKey: Key | null,
40-
4138
/** A selection manager to read and update multiple selection state. */
4239
selectionManager: SelectionManager
4340
}
@@ -69,9 +66,8 @@ export function useListState<T extends object>(props: ListProps<T>): ListState<T
6966
return useMemo(() => ({
7067
collection,
7168
disabledKeys,
72-
focusedKey: selectionState.focusedKey,
7369
selectionManager
74-
}), [collection, disabledKeys, selectionState.focusedKey, selectionManager]);
70+
}), [collection, disabledKeys, selectionManager]);
7571
}
7672

7773
/**
@@ -81,12 +77,12 @@ export function UNSTABLE_useFilteredListState<T extends object>(state: ListState
8177
let collection = useMemo(() => filter ? state.collection.UNSTABLE_filter!(filter) : state.collection, [state.collection, filter]);
8278
let selectionManager = useMemo(() => state.selectionManager.withCollection(collection), [state, collection]);
8379
useFocusedKeyReset(collection, selectionManager);
80+
8481
return useMemo(() => ({
8582
collection,
86-
selectionManager,
87-
focusedKey: selectionManager.focusedKey,
88-
disabledKeys: state.disabledKeys
89-
}), [collection, selectionManager, selectionManager.focusedKey, state.disabledKeys]);
83+
disabledKeys: state.disabledKeys,
84+
selectionManager
85+
}), [collection, state.disabledKeys, selectionManager]);
9086
}
9187

9288
function useFocusedKeyReset<T>(collection: Collection<Node<T>>, selectionManager: SelectionManager) {

packages/@react-stately/list/src/useSingleSelectListState.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface SingleSelectListState<T> extends ListState<T> {
4040
export function useSingleSelectListState<T extends object>(props: SingleSelectListProps<T>): SingleSelectListState<T> {
4141
let [selectedKey, setSelectedKey] = useControlledState(props.selectedKey, props.defaultSelectedKey ?? null, props.onSelectionChange);
4242
let selectedKeys = useMemo(() => selectedKey != null ? [selectedKey] : [], [selectedKey]);
43+
let onSelectionChange = props.onSelectionChange;
4344
let {collection, disabledKeys, selectionManager} = useListState({
4445
...props,
4546
selectionMode: 'single',
@@ -55,12 +56,12 @@ export function useSingleSelectListState<T extends object>(props: SingleSelectLi
5556

5657
// Always fire onSelectionChange, even if the key is the same
5758
// as the current key (useControlledState does not).
58-
if (key === selectedKey && props.onSelectionChange) {
59-
props.onSelectionChange(key);
59+
if (key === selectedKey && onSelectionChange) {
60+
onSelectionChange(key);
6061
}
6162

6263
setSelectedKey(key);
63-
}, [props.onSelectionChange, selectedKey, setSelectedKey])
64+
}, [onSelectionChange, selectedKey, setSelectedKey])
6465
});
6566

6667
let selectedItem = selectedKey != null

packages/@react-stately/select/src/useSelectState.ts

-4
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,6 @@ export function useSelectState<T extends object>(props: SelectStateOptions<T>):
6767

6868
let [isFocused, setFocused] = useState(false);
6969

70-
useEffect(() => {
71-
console.log("come on");
72-
}, [triggerState])
73-
7470
return useMemo(() => ({
7571
...validationState,
7672
...listState,

packages/react-aria-components/docs/TagGroup.mdx

+7-2
Original file line numberDiff line numberDiff line change
@@ -707,9 +707,14 @@ import {ListStateContext} from 'react-aria-components';
707707

708708
function SelectionCount() {
709709
/*- begin highlight -*/
710-
let state = React.useContext(ListStateContext);
710+
let context = React.useContext(ListStateContext);
711711
/*- end highlight -*/
712-
let selected = state?.selectionManager.selectedKeys.size ?? 0;
712+
let selected = 0;
713+
if(context) {
714+
let [state] = context;
715+
selected = state.selectionManager.selectedKeys.size;
716+
}
717+
713718
return <small>{selected} tags selected.</small>;
714719
}
715720

packages/react-aria-components/src/ComboBox.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ function ComboBoxInner<T extends object>({props, collection, comboBoxRef: ref}:
200200
style: {'--trigger-width': menuWidth} as React.CSSProperties
201201
}],
202202
[ListBoxContext, {...listBoxProps, ref: listBoxRef}],
203-
[ListStateContext, state],
203+
[ListStateContext, [state, state.selectionManager.focusedKey]],
204204
[TextContext, {
205205
slots: {
206206
description: descriptionProps,

packages/react-aria-components/src/GridList.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ function GridListInner<T extends object>({props, collection, gridListRef: ref}:
237237
data-layout={layout}>
238238
<Provider
239239
values={[
240-
[ListStateContext, state],
240+
[ListStateContext, [state, state.selectionManager.focusedKey]],
241241
[DragAndDropContext, {dragAndDropHooks, dragState, dropState}],
242242
[DropIndicatorContext, {render: GridListDropIndicatorWrapper}]
243243
]}>
@@ -277,7 +277,7 @@ export interface GridListItemProps<T = object> extends RenderProps<GridListItemR
277277
* A GridListItem represents an individual item in a GridList.
278278
*/
279279
export const GridListItem = /*#__PURE__*/ createLeafComponent('item', function GridListItem<T extends object>(props: GridListItemProps<T>, forwardedRef: ForwardedRef<HTMLDivElement>, item: Node<T>) {
280-
let state = useContext(ListStateContext)!;
280+
let [state] = useContext(ListStateContext)!;
281281
let {dragAndDropHooks, dragState, dropState} = useContext(DragAndDropContext);
282282
let ref = useObjectRef<HTMLDivElement>(forwardedRef);
283283
let {isVirtualized} = useContext(CollectionRendererContext);

packages/react-aria-components/src/ListBox.tsx

+21-18
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import React, {createContext, ForwardedRef, forwardRef, JSX, memo, ReactNode, us
2424
import {SeparatorContext} from './Separator';
2525
import {TextContext} from './Text';
2626
import {UNSTABLE_InternalAutocompleteContext} from './Autocomplete';
27-
import {SelectionManager} from '@react-stately/selection';
2827

2928
export interface ListBoxRenderProps {
3029
/**
@@ -79,22 +78,24 @@ export interface ListBoxProps<T> extends Omit<AriaListBoxProps<T>, 'children' |
7978
}
8079

8180
export const ListBoxContext = createContext<ContextValue<ListBoxProps<any>, HTMLDivElement>>(null);
82-
export const ListStateContext = createContext<ListState<any> | null>(null);
81+
export const ListStateContext = createContext<[ListState<any>, Key | null] | null>(null);
8382

8483
/**
8584
* A listbox displays a list of options and allows a user to select one or more of them.
8685
*/
8786
export const ListBox = /*#__PURE__*/ (forwardRef as forwardRefType)(function ListBox<T extends object>(props: ListBoxProps<T>, ref: ForwardedRef<HTMLDivElement>) {
8887
[props, ref] = useContextProps(props, ref, ListBoxContext);
89-
let state = useContext(ListStateContext);
88+
let context = useContext(ListStateContext);
9089

9190
// The structure of ListBox is a bit strange because it needs to work inside other components like ComboBox and Select.
9291
// Those components render two copies of their children so that the collection can be built even when the popover is closed.
9392
// The first copy sends a collection document via context which we render the collection portal into.
9493
// The second copy sends a ListState object via context which we use to render the ListBox without rebuilding the state.
9594
// Otherwise, we have a standalone ListBox, so we need to create a collection and state ourselves.
9695

97-
if (state) {
96+
if (context) {
97+
let [state] = context;
98+
9899
return <ListBoxInner state={state} props={props} listBoxRef={ref} />;
99100
}
100101

@@ -249,7 +250,7 @@ function ListBoxInner<T extends object>({state: inputState, props, listBoxRef}:
249250
<Provider
250251
values={[
251252
[ListBoxContext, props],
252-
[ListStateContext, state],
253+
[ListStateContext, [state, state.selectionManager.focusedKey]],
253254
[DragAndDropContext, useMemo(() => ({dragAndDropHooks, dragState, dropState}), [dragAndDropHooks, dragState, dropState])],
254255
[SeparatorContext, {elementType: 'div'}],
255256
[DropIndicatorContext, {render: ListBoxDropIndicatorWrapper}],
@@ -271,7 +272,7 @@ function ListBoxInner<T extends object>({state: inputState, props, listBoxRef}:
271272
export interface ListBoxSectionProps<T> extends SectionProps<T> {}
272273

273274
function ListBoxSectionInner<T extends object>(props: ListBoxSectionProps<T>, ref: ForwardedRef<HTMLElement>, section: Node<T>, className = 'react-aria-ListBoxSection') {
274-
let state = useContext(ListStateContext)!;
275+
let [state] = useContext(ListStateContext)!;
275276
let {dragAndDropHooks, dropState} = useContext(DragAndDropContext)!;
276277
let {CollectionBranch} = useContext(CollectionRendererContext);
277278
let [headingRef, heading] = useSlot();
@@ -332,22 +333,24 @@ export interface ListBoxItemProps<T = object> extends RenderProps<ListBoxItemRen
332333
*/
333334
export const ListBoxItem = /*#__PURE__*/ createLeafComponent('item', function ListBoxItem<T extends object>(props: ListBoxItemProps<T>, forwardedRef: ForwardedRef<HTMLDivElement>, item: Node<T>) {
334335
let ref = useObjectRef<any>(forwardedRef);
335-
let state = useContext(ListStateContext)!;
336+
let [state, focusedKey] = useContext(ListStateContext)!;
337+
338+
// ListBoxItemInner is memoized so that a focus change does not re-render all items in the ListBox.
339+
// The data-focused attribute tells React which list boxes are affected by a focus change. It does not actually
340+
// get passed through to the component - it could be named anything, as long as it changes so React knows to re-render.
341+
return <ListBoxItemInner data-focused={item.key === focusedKey} props={props} state={state} passRef={ref} item={item} />;
342+
});
336343

344+
const ListBoxItemInner = memo(function ListBoxItemInner<T extends object>({props, item, state, passRef}: {props: ListBoxItemProps<T>, state: ListState<T>, item: Node<T>, passRef: React.MutableRefObject<any>}) {
345+
const ref = passRef;
346+
347+
let {dragAndDropHooks, dragState, dropState} = useContext(DragAndDropContext)!;
337348
let options = useOption(
338349
{key: item.key, 'aria-label': props?.['aria-label']},
339350
state,
340351
ref
341352
);
342353

343-
return <ListBoxItemInner selectionManager={state.selectionManager} options={options} focused={item.key === state.focusedKey} passRef={ref} props={props} item={item} />
344-
});
345-
346-
const ListBoxItemInner = memo(function ListBoxItemInner<T extends object>({props, item, selectionManager, options, focused, passRef}:
347-
{options: OptionAria, props: ListBoxItemProps<T>, focused: boolean, selectionManager: SelectionManager, item: Node<T>, passRef: React.MutableRefObject<any>}) {
348-
const ref = passRef;
349-
350-
let {dragAndDropHooks, dragState, dropState} = useContext(DragAndDropContext)!;
351354
let {optionProps, labelProps, descriptionProps, ...states} = options;
352355

353356
let {hoverProps, isHovered} = useHover({
@@ -378,8 +381,8 @@ const ListBoxItemInner = memo(function ListBoxItemInner<T extends object>({props
378381
values: {
379382
...states,
380383
isHovered,
381-
selectionMode: selectionManager.selectionMode,
382-
selectionBehavior: selectionManager.selectionBehavior,
384+
selectionMode: state.selectionManager.selectionMode,
385+
selectionBehavior: state.selectionManager.selectionBehavior,
383386
allowsDragging: !!dragState,
384387
isDragging,
385388
isDropTarget: droppableItem?.isDropTarget
@@ -408,7 +411,7 @@ const ListBoxItemInner = memo(function ListBoxItemInner<T extends object>({props
408411
data-pressed={states.isPressed || undefined}
409412
data-dragging={isDragging || undefined}
410413
data-drop-target={droppableItem?.isDropTarget || undefined}
411-
data-selection-mode={selectionManager.selectionMode === 'none' ? undefined : selectionManager.selectionMode}>
414+
data-selection-mode={state.selectionManager.selectionMode === 'none' ? undefined : state.selectionManager.selectionMode}>
412415
<Provider
413416
values={[
414417
[TextContext, {

packages/react-aria-components/src/Select.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ function SelectInner<T extends object>({props, selectRef: ref, collection}: Sele
183183
'aria-labelledby': menuProps['aria-labelledby']
184184
}],
185185
[ListBoxContext, {...menuProps, ref: scrollRef}],
186-
[ListStateContext, state],
186+
[ListStateContext, [state, state.selectionManager.focusedKey]],
187187
[TextContext, {
188188
slots: {
189189
description: descriptionProps,

packages/react-aria-components/src/TagGroup.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ function TagGroupInner({props, forwardedRef: ref, collection}: TagGroupInnerProp
109109
values={[
110110
[LabelContext, {...labelProps, elementType: 'span', ref: labelRef}],
111111
[TagListContext, {...gridProps, ref: tagListRef}],
112-
[ListStateContext, state],
112+
[ListStateContext, [state, state.selectionManager.focusedKey]],
113113
[TextContext, {
114114
slots: {
115115
description: descriptionProps,
@@ -139,7 +139,7 @@ interface TagListInnerProps<T> {
139139
}
140140

141141
function TagListInner<T extends object>({props, forwardedRef}: TagListInnerProps<T>) {
142-
let state = useContext(ListStateContext)!;
142+
let [state] = useContext(ListStateContext)!;
143143
let {CollectionRoot} = useContext(CollectionRendererContext);
144144
let [gridProps, ref] = useContextProps(props, forwardedRef, TagListContext);
145145
delete gridProps.items;
@@ -200,7 +200,7 @@ export interface TagProps extends RenderProps<TagRenderProps>, LinkDOMProps, Hov
200200
* A Tag is an individual item within a TagList.
201201
*/
202202
export const Tag = /*#__PURE__*/ createLeafComponent('item', (props: TagProps, forwardedRef: ForwardedRef<HTMLDivElement>, item: Node<unknown>) => {
203-
let state = useContext(ListStateContext)!;
203+
let [state] = useContext(ListStateContext)!;
204204
let ref = useObjectRef<HTMLDivElement>(forwardedRef);
205205
let {focusProps, isFocusVisible} = useFocusRing({within: true});
206206
let {rowProps, gridCellProps, removeButtonProps, ...states} = useTag({item}, state, ref);

0 commit comments

Comments
 (0)