Skip to content

Commit 98cad3f

Browse files
authored
Fix RAC collections in React canary versions (#4518)
1 parent 27ea95f commit 98cad3f

File tree

4 files changed

+89
-20
lines changed

4 files changed

+89
-20
lines changed

.circleci/config.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,26 @@ jobs:
108108
- ~/react-spectrum
109109
key: react-spectrum17-{{ .Environment.CACHE_VERSION }}-{{ .Environment.CIRCLE_SHA1 }}
110110

111+
install-canary:
112+
executor: rsp-large
113+
steps:
114+
- checkout
115+
- restore_cache:
116+
keys:
117+
- rsp-yarn-{{ .Environment.CACHE_VERSION }}-{{ .Branch }}-{{ checksum "yarn.lock" }}
118+
- rsp-yarn-{{ .Environment.CACHE_VERSION }}-{{ .Branch }}-
119+
- rsp-yarn-{{ .Environment.CACHE_VERSION }}-
120+
121+
- run:
122+
name: build
123+
command: |
124+
yarn install --pure-lockfile --cache-folder ~/.cache/yarn && yarn install-canary --cache-folder ~/.cache/yarn
125+
126+
- save_cache:
127+
paths:
128+
- ~/react-spectrum
129+
key: react-spectrum-canary-{{ .Environment.CACHE_VERSION }}-{{ .Environment.CIRCLE_SHA1 }}
130+
111131
test-ssr:
112132
executor: rsp-xlarge
113133
steps:
@@ -166,6 +186,17 @@ jobs:
166186
command: |
167187
yarn test:ssr
168188
189+
test-ssr-canary:
190+
executor: rsp-xlarge
191+
steps:
192+
- restore_cache:
193+
key: react-spectrum-canary-{{ .Environment.CACHE_VERSION }}-{{ .Environment.CIRCLE_SHA1 }}
194+
195+
- run:
196+
name: test ssr
197+
command: |
198+
yarn test:ssr
199+
169200
test-16:
170201
parallelism: 3
171202
executor: rsp-xlarge
@@ -214,6 +245,29 @@ jobs:
214245
- store_artifacts:
215246
path: ~/junit
216247

248+
test-canary:
249+
parallelism: 3
250+
executor: rsp-xlarge
251+
steps:
252+
- restore_cache:
253+
key: react-spectrum-canary-{{ .Environment.CACHE_VERSION }}-{{ .Environment.CIRCLE_SHA1 }}
254+
255+
- run: mkdir ~/junit
256+
257+
- run:
258+
name: test
259+
command: |
260+
shopt -s globstar
261+
TESTFILES=$(circleci tests glob "packages/**/*.test.[tj]{s,sx}" | circleci tests split --split-by=timings)
262+
JEST_JUNIT_OUTPUT_NAME="junit-canary.xml" yarn test ${TESTFILES}
263+
264+
- run:
265+
command: cp junit-canary.xml ~/junit/
266+
when: always
267+
- store_test_results:
268+
path: ~/junit
269+
- store_artifacts:
270+
path: ~/junit
217271

218272
test-esm:
219273
executor: rsp-xlarge-nodeupdate
@@ -465,6 +519,7 @@ workflows:
465519
- install
466520
- install-16
467521
- install-17
522+
- install-canary
468523
- test-ssr:
469524
requires:
470525
- install
@@ -483,6 +538,12 @@ workflows:
483538
- test-17:
484539
requires:
485540
- install-17
541+
- test-ssr-canary:
542+
requires:
543+
- install-canary
544+
- test-canary:
545+
requires:
546+
- install-canary
486547
- test-esm:
487548
requires:
488549
- install
@@ -544,6 +605,9 @@ workflows:
544605
- test-16
545606
- test-ssr-17
546607
- test-17
608+
- test-ssr-canary
609+
- test-canary
610+
- test-ssr-canary
547611
- test-esm
548612
- storybook
549613
- storybook-16

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"check-types": "tsc && tsc-strict",
1212
"install-16": "yarn add -W react@^16.8.0 react-dom@^16.8.0 @testing-library/react@^12 @testing-library/react-hooks@^8 @testing-library/dom@^8",
1313
"install-17": "yarn add -W react@^17 react-dom@^17 @testing-library/react@^12 @testing-library/react-hooks@^8 @testing-library/dom@^8",
14+
"install-canary": "yarn add -W react@canary react-dom@canary",
1415
"start": "cross-env NODE_ENV=storybook start-storybook -p 9003 --ci -c '.storybook'",
1516
"build:storybook": "build-storybook -c .storybook -o dist/$(git rev-parse HEAD)/storybook",
1617
"build:storybook-16": "build-storybook -c .storybook -o dist/$(git rev-parse HEAD)/storybook-16",

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

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ export class ElementNode<T> extends BaseNode<T> {
262262
nodeType = 8; // COMMENT_NODE (we'd use ELEMENT_NODE but React DevTools will fail to get its dimensions)
263263
node: NodeValue<T>;
264264
private _index: number = 0;
265+
private hasSetProps = false;
265266

266267
constructor(type: string, ownerDocument: Document<T, any>) {
267268
super(ownerDocument);
@@ -297,21 +298,20 @@ export class ElementNode<T> extends BaseNode<T> {
297298
node.lastChildKey = this.lastChild?.node.key ?? null;
298299
}
299300

300-
// Special property that React passes through as an object rather than a string via setAttribute.
301-
// See below for details.
302-
set multiple(obj: any) {
301+
setProps(obj: any, rendered?: ReactNode) {
303302
let node = this.ownerDocument.getMutableNode(this);
304-
let {rendered, value, textValue, id, ...props} = obj;
303+
let {value, textValue, id, ...props} = obj;
305304
node.props = props;
306305
node.rendered = rendered;
307306
node.value = value;
308307
node.textValue = textValue || (typeof rendered === 'string' ? rendered : '') || obj['aria-label'] || '';
309308
if (id != null && id !== node.key) {
310-
if (this.parentNode) {
309+
if (this.hasSetProps) {
311310
throw new Error('Cannot change the id of an item');
312311
}
313312
node.key = id;
314313
}
314+
this.hasSetProps = true;
315315
}
316316

317317
get style() {
@@ -672,11 +672,13 @@ export function useCollection<T extends object, C extends BaseCollection<T>>(pro
672672
}
673673

674674
/** Renders a DOM element (e.g. separator or header) shallowly when inside a collection. */
675-
export function useShallowRender<T extends Element>(Element: string, props: React.HTMLAttributes<T>, ref: React.RefObject<T>): ReactElement | null {
675+
export function useShallowRender<T extends Element>(Element: string, props: React.HTMLAttributes<T>, ref: React.Ref<T>): ReactElement | null {
676676
let isShallow = useContext(ShallowRenderContext);
677+
props = useMemo(() => ({...props, ref}), [props, ref]);
678+
ref = useCollectionItemRef(props, props.children);
677679
if (isShallow) {
678680
// @ts-ignore
679-
return <Element multiple={{...props, ref, rendered: props.children}} />;
681+
return <Element ref={ref} />;
680682
}
681683

682684
return null;
@@ -738,6 +740,13 @@ export interface ItemRenderProps {
738740
isDropTarget?: boolean
739741
}
740742

743+
export function useCollectionItemRef(props: any, rendered?: ReactNode) {
744+
// Return a callback ref that sets the props object on the fake DOM node.
745+
return useCallback((element) => {
746+
element?.setProps(props, rendered);
747+
}, [props, rendered]);
748+
}
749+
741750
export interface ItemProps<T = object> extends Omit<SharedItemProps<T>, 'children'>, RenderProps<ItemRenderProps> {
742751
/** The unique id of the item. */
743752
id?: Key,
@@ -746,13 +755,8 @@ export interface ItemProps<T = object> extends Omit<SharedItemProps<T>, 'childre
746755
}
747756

748757
export function Item<T extends object>(props: ItemProps<T>): JSX.Element {
749-
// HACK: the `multiple` prop is special in that React will pass it through as a property rather
750-
// than converting to a string and using setAttribute. This allows our custom fake DOM to receive
751-
// the props as an object. Once React supports custom elements, we can switch to that instead.
752-
// https://github.com/facebook/react/issues/11347
753-
// https://github.com/facebook/react/blob/82c64e1a49239158c0daa7f0d603d2ad2ee667a9/packages/react-dom/src/shared/DOMProperty.js#L386
754758
// @ts-ignore
755-
return <item multiple={{...props, rendered: props.children}} />;
759+
return <item ref={useCollectionItemRef(props, props.children)} />;
756760
}
757761

758762
export interface SectionProps<T> extends Omit<SharedSectionProps<T>, 'children' | 'title'>, DOMProps {
@@ -768,7 +772,7 @@ export function Section<T extends object>(props: SectionProps<T>): JSX.Element {
768772
let children = useCollectionChildren(props);
769773

770774
// @ts-ignore
771-
return <section multiple={{...props, rendered: props.title}}>{children}</section>;
775+
return <section ref={useCollectionItemRef(props)}>{children}</section>;
772776
}
773777

774778
export const CollectionContext = createContext<CachedChildrenOptions<unknown> | null>(null);

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {AriaLabelingProps} from '@react-types/shared';
2-
import {BaseCollection, CollectionContext, CollectionProps, CollectionRendererContext, ItemRenderProps, NodeValue, useCachedChildren, useCollection, useCollectionChildren} from './Collection';
2+
import {BaseCollection, CollectionContext, CollectionProps, CollectionRendererContext, ItemRenderProps, NodeValue, useCachedChildren, useCollection, useCollectionChildren, useCollectionItemRef} from './Collection';
33
import {buildHeaderRows} from '@react-stately/table';
44
import {ButtonContext} from './Button';
55
import {CheckboxContext} from './Checkbox';
@@ -388,7 +388,7 @@ export function TableHeader<T extends object>(props: TableHeaderProps<T>) {
388388
return (
389389
<CollectionRendererContext.Provider value={renderer}>
390390
{/* @ts-ignore */}
391-
<tableheader multiple={props}>{children}</tableheader>
391+
<tableheader ref={useCollectionItemRef(props)}>{children}</tableheader>
392392
</CollectionRendererContext.Provider>
393393
);
394394
}
@@ -442,7 +442,7 @@ export function Column<T extends object>(props: ColumnProps<T>): JSX.Element {
442442
});
443443

444444
// @ts-ignore
445-
return <column multiple={{...props, rendered: props.title ?? props.children}}>{children}</column>;
445+
return <column ref={useCollectionItemRef(props, props.title ?? props.children)}>{children}</column>;
446446
}
447447

448448
export interface TableBodyRenderProps {
@@ -465,7 +465,7 @@ export function TableBody<T extends object>(props: TableBodyProps<T>) {
465465
let children = useCollectionChildren(props);
466466

467467
// @ts-ignore
468-
return <tablebody multiple={props}>{children}</tablebody>;
468+
return <tablebody ref={useCollectionItemRef(props)}>{children}</tablebody>;
469469
}
470470

471471
export interface RowRenderProps extends ItemRenderProps {}
@@ -494,7 +494,7 @@ export function Row<T extends object>(props: RowProps<T>) {
494494

495495
return (
496496
// @ts-ignore
497-
<item multiple={props}>
497+
<item ref={useCollectionItemRef(props)}>
498498
<CollectionContext.Provider value={ctx}>
499499
{children}
500500
</CollectionContext.Provider>
@@ -534,7 +534,7 @@ export interface CellProps extends RenderProps<CellRenderProps> {
534534
*/
535535
export function Cell(props: CellProps): JSX.Element {
536536
// @ts-ignore
537-
return <cell multiple={{...props, rendered: props.children}} />;
537+
return <cell ref={useCollectionItemRef(props, props.children)} />;
538538
}
539539

540540
function TableHeaderRowGroup<T>({collection}: {collection: TableCollection<T>}) {

0 commit comments

Comments
 (0)