Skip to content

Commit e10e81c

Browse files
committed
Merge branch 'incremental-function-return-types' into update-typescript-to-5.8
2 parents 4913033 + 83b21a4 commit e10e81c

File tree

15 files changed

+284
-28
lines changed

15 files changed

+284
-28
lines changed

packages/@react-aria/collections/src/BaseCollection.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {Collection as ICollection, Key, Node} from '@react-types/shared';
14-
import {ReactNode} from 'react';
14+
import {ReactElement, ReactNode} from 'react';
1515

1616
export type Mutable<T> = {
1717
-readonly[P in keyof T]: T[P]
@@ -34,7 +34,7 @@ export class CollectionNode<T> implements Node<T> {
3434
readonly firstChildKey: Key | null = null;
3535
readonly lastChildKey: Key | null = null;
3636
readonly props: any = {};
37-
readonly render?: (node: Node<any>) => ReactNode;
37+
readonly render?: (node: Node<any>) => ReactElement;
3838
readonly colSpan: number | null = null;
3939
readonly colIndex: number | null = null;
4040

packages/@react-aria/collections/src/Document.ts

+45-14
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {BaseCollection, CollectionNode, Mutable} from './BaseCollection';
14-
import {CSSProperties, ForwardedRef, ReactNode} from 'react';
14+
import {CSSProperties, ForwardedRef, ReactElement, ReactNode} from 'react';
1515
import {Node} from '@react-types/shared';
1616

1717
// This Collection implementation is perhaps a little unusual. It works by rendering the React tree into a
@@ -38,6 +38,7 @@ export class BaseNode<T> {
3838
private _previousSibling: ElementNode<T> | null = null;
3939
private _nextSibling: ElementNode<T> | null = null;
4040
private _parentNode: BaseNode<T> | null = null;
41+
private _minInvalidChildIndex: ElementNode<T> | null = null;
4142
ownerDocument: Document<T, any>;
4243

4344
constructor(ownerDocument: Document<T, any>) {
@@ -101,6 +102,21 @@ export class BaseNode<T> {
101102
return this.parentNode?.isConnected || false;
102103
}
103104

105+
private invalidateChildIndices(child: ElementNode<T>): void {
106+
if (this._minInvalidChildIndex == null || child.index < this._minInvalidChildIndex.index) {
107+
this._minInvalidChildIndex = child;
108+
}
109+
}
110+
111+
updateChildIndices(): void {
112+
let node = this._minInvalidChildIndex;
113+
while (node) {
114+
node.index = node.previousSibling ? node.previousSibling.index + 1 : 0;
115+
node = node.nextSibling;
116+
}
117+
this._minInvalidChildIndex = null;
118+
}
119+
104120
appendChild(child: ElementNode<T>): void {
105121
this.ownerDocument.startTransaction();
106122
if (child.parentNode) {
@@ -158,11 +174,7 @@ export class BaseNode<T> {
158174
referenceNode.previousSibling = newNode;
159175
newNode.parentNode = referenceNode.parentNode;
160176

161-
let node: ElementNode<T> | null = referenceNode;
162-
while (node) {
163-
node.index++;
164-
node = node.nextSibling;
165-
}
177+
this.invalidateChildIndices(referenceNode);
166178

167179
if (newNode.hasSetProps) {
168180
this.ownerDocument.addNode(newNode);
@@ -178,13 +190,9 @@ export class BaseNode<T> {
178190
}
179191

180192
this.ownerDocument.startTransaction();
181-
let node = child.nextSibling;
182-
while (node) {
183-
node.index--;
184-
node = node.nextSibling;
185-
}
186-
193+
187194
if (child.nextSibling) {
195+
this.invalidateChildIndices(child.nextSibling);
188196
child.nextSibling.previousSibling = child.previousSibling;
189197
}
190198

@@ -272,7 +280,7 @@ export class ElementNode<T> extends BaseNode<T> {
272280
}
273281
}
274282

275-
setProps<E extends Element>(obj: {[key: string]: any}, ref: ForwardedRef<E>, rendered?: ReactNode, render?: (node: Node<T>) => ReactNode): void {
283+
setProps<E extends Element>(obj: {[key: string]: any}, ref: ForwardedRef<E>, rendered?: ReactNode, render?: (node: Node<T>) => ReactElement): void {
276284
let node = this.ownerDocument.getMutableNode(this);
277285
let {value, textValue, id, ...props} = obj;
278286
props.ref = ref;
@@ -330,6 +338,8 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
330338
private mutatedNodes: Set<ElementNode<T>> = new Set();
331339
private subscriptions: Set<() => void> = new Set();
332340
private transactionCount = 0;
341+
private queuedRender = false;
342+
private inSubscription = false;
333343

334344
constructor(collection: C) {
335345
// @ts-ignore
@@ -412,10 +422,22 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
412422
}
413423

414424
this.updateCollection();
425+
426+
// Reset queuedRender to false when getCollection is called during render.
427+
if (!this.inSubscription) {
428+
this.queuedRender = false;
429+
}
430+
415431
return this.collection;
416432
}
417433

418434
updateCollection(): void {
435+
// First, update the indices of dirty element children.
436+
for (let element of this.dirtyNodes) {
437+
element.updateChildIndices();
438+
}
439+
440+
// Next, update dirty collection nodes.
419441
for (let element of this.dirtyNodes) {
420442
if (element instanceof ElementNode && element.isConnected) {
421443
element.updateNode();
@@ -424,6 +446,7 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
424446

425447
this.dirtyNodes.clear();
426448

449+
// Finally, update the collection.
427450
if (this.mutatedNodes.size || this.collectionMutated) {
428451
let collection = this.getMutableCollection();
429452
for (let element of this.mutatedNodes) {
@@ -442,13 +465,21 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
442465
queueUpdate(): void {
443466
// Don't emit any updates if there is a transaction in progress.
444467
// queueUpdate should be called again after the transaction.
445-
if (this.dirtyNodes.size === 0 || this.transactionCount > 0) {
468+
if (this.dirtyNodes.size === 0 || this.transactionCount > 0 || this.queuedRender) {
446469
return;
447470
}
448471

472+
// Only trigger subscriptions once during an update, when the first item changes.
473+
// React's useSyncExternalStore will call getCollection immediately, to check whether the snapshot changed.
474+
// If so, React will queue a render to happen after the current commit to our fake DOM finishes.
475+
// We track whether getCollection is called in a subscription, and once it is called during render,
476+
// we reset queuedRender back to false.
477+
this.queuedRender = true;
478+
this.inSubscription = true;
449479
for (let fn of this.subscriptions) {
450480
fn();
451481
}
482+
this.inSubscription = false;
452483
}
453484

454485
subscribe(fn: () => void) {

packages/@react-aria/slider/stories/StoryMultiSlider.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export function StoryMultiSlider(props: StoryMultiSliderProps) {
5959
<div ref={trackRef} className={styles.track} {...trackProps}>
6060
<div className={styles.rail} />
6161
{React.Children.map(children, ((child, index) =>
62-
React.cloneElement(child as React.ReactNode, {
62+
React.cloneElement(child as React.ReactElement, {
6363
__context: {
6464
sliderProps: props,
6565
state,

packages/@react-aria/table/src/TableKeyboardDelegate.ts

+7
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,13 @@ export class TableKeyboardDelegate<T> extends GridKeyboardDelegate<T, TableColle
178178
return null;
179179
}
180180

181+
if (item.textValue) {
182+
let substring = item.textValue.slice(0, search.length);
183+
if (this.collator.compare(substring, search) === 0) {
184+
return item.key;
185+
}
186+
}
187+
181188
// Check each of the row header cells in this row for a match
182189
for (let cell of getChildNodes(item, this.collection)) {
183190
let column = collection.columns[cell.index];

packages/@react-stately/toast/src/useToastState.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ export interface ToastState<T> {
5757
* of actions, errors, or other events in an application.
5858
*/
5959
export function useToastState<T>(props: ToastStateProps = {}): ToastState<T> {
60-
let {maxVisibleToasts = 1} = props;
61-
let queue = useMemo(() => new ToastQueue<T>({maxVisibleToasts}), [maxVisibleToasts]);
60+
let {maxVisibleToasts = 1, wrapUpdate} = props;
61+
let queue = useMemo(() => new ToastQueue<T>({maxVisibleToasts, wrapUpdate}), [maxVisibleToasts, wrapUpdate]);
6262
return useToastQueue(queue);
6363
}
6464

packages/@react-stately/toast/test/useToastState.test.js

+24
Original file line numberDiff line numberDiff line change
@@ -217,4 +217,28 @@ describe('useToastState', () => {
217217
expect(result.current.visibleToasts.length).toBe(1);
218218
expect(result.current.visibleToasts[0].content).toBe(newValue[0].content);
219219
});
220+
221+
it('should use provided wrapUpdate', () => {
222+
let wrapUpdate = jest.fn(fn => fn());
223+
224+
let {result} = renderHook(() => useToastState({wrapUpdate}));
225+
expect(result.current.visibleToasts).toStrictEqual([]);
226+
227+
act(() => {result.current.add(newValue[0].content, newValue[0].props);});
228+
expect(result.current.visibleToasts[0].content).toBe(newValue[0].content);
229+
230+
expect(wrapUpdate).toHaveBeenCalledTimes(1);
231+
232+
act(() => {result.current.add('Second Toast');});
233+
expect(result.current.visibleToasts.length).toBe(1);
234+
expect(result.current.visibleToasts[0].content).toBe('Second Toast');
235+
236+
expect(wrapUpdate).toHaveBeenCalledTimes(2);
237+
238+
act(() => {result.current.close(result.current.visibleToasts[0].key);});
239+
expect(result.current.visibleToasts.length).toBe(1);
240+
expect(result.current.visibleToasts[0].content).toBe(newValue[0].content);
241+
242+
expect(wrapUpdate).toHaveBeenCalledTimes(3);
243+
});
220244
});

packages/react-aria-components/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@react-aria/focus": "^3.20.1",
4646
"@react-aria/interactions": "^3.24.1",
4747
"@react-aria/live-announcer": "^3.4.1",
48+
"@react-aria/ssr": "^3.9.7",
4849
"@react-aria/toolbar": "3.0.0-beta.14",
4950
"@react-aria/utils": "^3.28.1",
5051
"@react-aria/virtualizer": "^4.1.3",

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ export const SeparatorContext = createContext<ContextValue<SeparatorProps, HTMLE
2323
export const Separator = /*#__PURE__*/ createLeafComponent('separator', function Separator(props: SeparatorProps, ref: ForwardedRef<HTMLElement>) {
2424
[props, ref] = useContextProps(props, ref, SeparatorContext);
2525

26-
let {elementType, orientation, style, className} = props;
26+
let {elementType, orientation, style, className, slot, ...otherProps} = props;
2727
let Element = (elementType as ElementType) || 'hr';
2828
if (Element === 'hr' && orientation === 'vertical') {
2929
Element = 'div';
3030
}
3131

3232
let {separatorProps} = useSeparator({
33+
...otherProps,
3334
elementType,
3435
orientation
3536
});
@@ -41,6 +42,6 @@ export const Separator = /*#__PURE__*/ createLeafComponent('separator', function
4142
style={style}
4243
className={className ?? 'react-aria-Separator'}
4344
ref={ref}
44-
slot={props.slot || undefined} />
45+
slot={slot || undefined} />
4546
);
4647
});

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -912,7 +912,7 @@ export interface TableBodyRenderProps {
912912
isDropTarget: boolean
913913
}
914914

915-
export interface TableBodyProps<T> extends CollectionProps<T>, StyleRenderProps<TableBodyRenderProps> {
915+
export interface TableBodyProps<T> extends Omit<CollectionProps<T>, 'disabledKeys'>, StyleRenderProps<TableBodyRenderProps> {
916916
/** Provides content to display when there are no rows in the table. */
917917
renderEmptyState?: (props: TableBodyRenderProps) => ReactNode
918918
}

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

+12-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {forwardRefType} from '@react-types/shared';
1818
import {QueuedToast, ToastQueue, ToastState, useToastQueue} from 'react-stately';
1919
import React, {createContext, ForwardedRef, forwardRef, HTMLAttributes, JSX, ReactElement, useContext} from 'react';
2020
import {TextContext} from './Text';
21+
import {useIsSSR} from '@react-aria/ssr';
2122
import {useObjectRef} from '@react-aria/utils';
2223

2324
const ToastStateContext = createContext<ToastState<any> | null>(null);
@@ -41,13 +42,19 @@ export interface ToastRegionProps<T> extends AriaToastRegionProps, StyleRenderPr
4142
/** The queue of toasts to display. */
4243
queue: ToastQueue<T>,
4344
/** A function to render each toast. */
44-
children: (renderProps: {toast: QueuedToast<T>}) => ReactElement
45+
children: (renderProps: {toast: QueuedToast<T>}) => ReactElement,
46+
/**
47+
* The container element in which the toast region portal will be placed.
48+
* @default document.body
49+
*/
50+
portalContainer?: Element
4551
}
4652

4753
/**
4854
* A ToastRegion displays one or more toast notifications.
4955
*/
5056
export const ToastRegion = /*#__PURE__*/ (forwardRef as forwardRefType)(function ToastRegion<T>(props: ToastRegionProps<T>, ref: ForwardedRef<HTMLDivElement>): JSX.Element | null {
57+
let isSSR = useIsSSR();
5158
let state = useToastQueue(props.queue);
5259
let objectRef = useObjectRef(ref);
5360
let {regionProps} = useToastRegion(props, state, objectRef);
@@ -64,6 +71,8 @@ export const ToastRegion = /*#__PURE__*/ (forwardRef as forwardRefType)(function
6471
}
6572
});
6673

74+
let {portalContainer = isSSR ? null : document.body} = props;
75+
6776
let region = (
6877
<ToastStateContext.Provider value={state}>
6978
<div
@@ -77,8 +86,8 @@ export const ToastRegion = /*#__PURE__*/ (forwardRef as forwardRefType)(function
7786
</ToastStateContext.Provider>
7887
);
7988

80-
return state.visibleToasts.length > 0 && typeof document !== 'undefined'
81-
? createPortal(region, document.body)
89+
return state.visibleToasts.length > 0 && portalContainer
90+
? createPortal(region, portalContainer)
8291
: null;
8392
});
8493

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

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

13-
import {Button, ComboBox, Input, Label, ListBox, Popover} from 'react-aria-components';
13+
import {Button, ComboBox, Input, Label, ListBox, ListLayout, Popover, useFilter, Virtualizer} from 'react-aria-components';
1414
import {MyListBoxItem} from './utils';
15-
import React from 'react';
15+
import React, {useMemo, useState} from 'react';
1616
import styles from '../example/index.css';
1717
import {useAsyncList} from 'react-stately';
1818

@@ -207,3 +207,32 @@ export const ComboBoxImeExample = () => (
207207
</Popover>
208208
</ComboBox>
209209
);
210+
211+
let manyItems = [...Array(10000)].map((_, i) => ({id: i, name: `Item ${i}`}));
212+
213+
export const VirtualizedComboBox = () => {
214+
const [searchTerm, setSearchTerm] = useState('');
215+
const {contains} = useFilter({sensitivity: 'base'});
216+
const filteredItems = useMemo(() => {
217+
return manyItems.filter((item) => contains(item.name, searchTerm));
218+
}, [searchTerm, contains]);
219+
220+
return (
221+
<ComboBox items={filteredItems} inputValue={searchTerm} onInputChange={setSearchTerm}>
222+
<Label style={{display: 'block'}}>Test</Label>
223+
<div style={{display: 'flex'}}>
224+
<Input />
225+
<Button>
226+
<span aria-hidden="true" style={{padding: '0 2px'}}></span>
227+
</Button>
228+
</div>
229+
<Popover>
230+
<Virtualizer layout={ListLayout} layoutOptions={{rowHeight: 25}}>
231+
<ListBox className={styles.menu}>
232+
{(item: any) => <MyListBoxItem>{item.name}</MyListBoxItem>}
233+
</ListBox>
234+
</Virtualizer>
235+
</Popover>
236+
</ComboBox>
237+
);
238+
};

0 commit comments

Comments
 (0)