Skip to content

Commit 19b695e

Browse files
committed
prevent breaking change in CollectionBuilder by still accepting string for CollectionNodeClass
1 parent 9d65d5b commit 19b695e

File tree

2 files changed

+55
-9
lines changed

2 files changed

+55
-9
lines changed

packages/@react-aria/collections/src/CollectionBuilder.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,20 @@ export type CollectionNodeClass<T> = {
132132
readonly type: string
133133
};
134134

135-
// TODO: discuss the former Type arg, renamed to CollectionNodeClass
136-
function useSSRCollectionNode<T extends Element>(CollectionNodeClass: CollectionNodeClass<T>, props: object, ref: ForwardedRef<T>, rendered?: any, children?: ReactNode, render?: (node: Node<any>) => ReactElement) {
135+
function createCollectionNodeClass(type: string): CollectionNodeClass<any> {
136+
let NodeClass = function (key: Key) {
137+
return new CollectionNode(type, key);
138+
} as any;
139+
NodeClass.type = type;
140+
return NodeClass;
141+
}
142+
143+
function useSSRCollectionNode<T extends Element>(CollectionNodeClass: CollectionNodeClass<T> | string, props: object, ref: ForwardedRef<T>, rendered?: any, children?: ReactNode, render?: (node: Node<any>) => ReactElement) {
144+
// To prevent breaking change, if CollectionNodeClass is a string, create a CollectionNodeClass using the string as the type
145+
if (typeof CollectionNodeClass === 'string') {
146+
CollectionNodeClass = createCollectionNodeClass(CollectionNodeClass);
147+
}
148+
137149
// During SSR, portals are not supported, so the collection children will be wrapped in an SSRContext.
138150
// Since SSR occurs only once, we assume that the elements are rendered in order and never re-render.
139151
// Therefore we can create elements in our collection document during render so that they are in the
@@ -164,11 +176,9 @@ function useSSRCollectionNode<T extends Element>(CollectionNodeClass: Collection
164176
return <CollectionNodeClass.type ref={itemRef}>{children}</CollectionNodeClass.type>;
165177
}
166178

167-
// TODO: have it still accept a string along side a collectionNodeClass, just have it default to a base node class if so
168-
// TODO: check the signature of the CollectionNodeClass here and other places (aka useSSRCollectionNode and branchCompoennt). If I use the generic it complains. Perhaps it should be unknown? Or maybe the definitions in Listbox and stuff shouldn't use a generic?
169-
export function createLeafComponent<T extends object, P extends object, E extends Element>(CollectionNodeClass: CollectionNodeClass<any>, render: (props: P, ref: ForwardedRef<E>) => ReactElement | null): (props: P & React.RefAttributes<T>) => ReactElement | null;
170-
export function createLeafComponent<T extends object, P extends object, E extends Element>(CollectionNodeClass: CollectionNodeClass<any>, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement | null): (props: P & React.RefAttributes<T>) => ReactElement | null;
171-
export function createLeafComponent<P extends object, E extends Element>(CollectionNodeClass: CollectionNodeClass<any>, render: (props: P, ref: ForwardedRef<E>, node?: any) => ReactElement | null): (props: P & React.RefAttributes<any>) => ReactElement | null {
179+
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;
180+
export function createLeafComponent<T extends object, P extends object, E extends Element>(CollectionNodeClass: CollectionNodeClass<any> | string, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement | null): (props: P & React.RefAttributes<T>) => ReactElement | null;
181+
export function createLeafComponent<P extends object, E extends Element>(CollectionNodeClass: CollectionNodeClass<any> | string, render: (props: P, ref: ForwardedRef<E>, node?: any) => ReactElement | null): (props: P & React.RefAttributes<any>) => ReactElement | null {
172182
let Component = ({node}) => render(node.props, node.props.ref, node);
173183
let Result = (forwardRef as forwardRefType)((props: P, ref: ForwardedRef<E>) => {
174184
let focusableProps = useContext(FocusableContext);
@@ -199,7 +209,7 @@ export function createLeafComponent<P extends object, E extends Element>(Collect
199209
return Result;
200210
}
201211

202-
export function createBranchComponent<T extends object, P extends {children?: any}, E extends Element>(CollectionNodeClass: CollectionNodeClass<any>, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement | null, useChildren: (props: P) => ReactNode = useCollectionChildren): (props: P & React.RefAttributes<E>) => ReactElement | null {
212+
export function createBranchComponent<T extends object, P extends {children?: any}, E extends Element>(CollectionNodeClass: CollectionNodeClass<any> | string, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement | null, useChildren: (props: P) => ReactNode = useCollectionChildren): (props: P & React.RefAttributes<E>) => ReactElement | null {
203213
let Component = ({node}) => render(node.props, node.props.ref, node);
204214
let Result = (forwardRef as forwardRefType)((props: P, ref: ForwardedRef<E>) => {
205215
let children = useChildren(props);

packages/@react-aria/collections/test/CollectionBuilder.test.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Collection, CollectionBuilder, CollectionNode, createLeafComponent} from '../src';
1+
import {Collection, CollectionBuilder, CollectionNode, createBranchComponent, createLeafComponent} from '../src';
22
import React from 'react';
33
import {render} from '@testing-library/react';
44

@@ -14,6 +14,14 @@ const Item = createLeafComponent(ItemNode, () => {
1414
return <div />;
1515
});
1616

17+
const ItemsOld = createLeafComponent('item', () => {
18+
return <div />;
19+
});
20+
21+
const SectionOld = createBranchComponent('section', () => {
22+
return <div />;
23+
});
24+
1725
const renderItems = (items, spyCollection) => (
1826
<CollectionBuilder content={<Collection>{items.map((item) => <Item key={item} />)}</Collection>}>
1927
{collection => {
@@ -23,6 +31,24 @@ const renderItems = (items, spyCollection) => (
2331
</CollectionBuilder>
2432
);
2533

34+
const renderItemsOld = (items, spyCollection) => (
35+
<CollectionBuilder
36+
content={
37+
<Collection>
38+
<SectionOld>
39+
{items.map((item) => (
40+
<ItemsOld key={item} />
41+
))}
42+
</SectionOld>
43+
</Collection>
44+
}>
45+
{collection => {
46+
spyCollection.current = collection;
47+
return null;
48+
}}
49+
</CollectionBuilder>
50+
);
51+
2652
describe('CollectionBuilder', () => {
2753
it('should be frozen even in case of empty initial collection', () => {
2854
let spyCollection = {};
@@ -38,4 +64,14 @@ describe('CollectionBuilder', () => {
3864
expect(spyCollection.current.firstKey).toBe(null);
3965
expect(spyCollection.current.lastKey).toBe(null);
4066
});
67+
68+
it('should still support using strings for the collection node class in createLeafComponent/createBranchComponent', () => {
69+
let spyCollection = {};
70+
render(renderItemsOld(['a'], spyCollection));
71+
expect(spyCollection.current.frozen).toBe(true);
72+
expect(spyCollection.current.firstKey).toBe('react-aria-2');
73+
expect(spyCollection.current.keyMap.get('react-aria-2').type).toBe('section');
74+
expect(spyCollection.current.keyMap.get('react-aria-2').firstChildKey).toBe('react-aria-1');
75+
expect(spyCollection.current.keyMap.get('react-aria-1').type).toBe('item');
76+
});
4177
});

0 commit comments

Comments
 (0)