From b64ce097dc6cccc754d4daed293bea238afb117b Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Thu, 30 Mar 2023 12:12:56 -0400 Subject: [PATCH 1/4] Add ESM build for react-aria-components --- Makefile | 2 +- packages/react-aria-components/package.json | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index facd0ac8f4b..ed5e2a8e375 100644 --- a/Makefile +++ b/Makefile @@ -84,7 +84,7 @@ publish-nightly: build build: parcel build packages/@react-{spectrum,aria,stately}/*/ packages/@internationalized/{message,string,date,number}/ packages/react-aria-components --no-optimize yarn lerna run prepublishOnly - for pkg in packages/@react-{spectrum,aria,stately}/*/ packages/@internationalized/{message,string,date,number}/ packages/@adobe/react-spectrum/ packages/react-aria/ packages/react-stately/; \ + for pkg in packages/@react-{spectrum,aria,stately}/*/ packages/@internationalized/{message,string,date,number}/ packages/@adobe/react-spectrum/ packages/react-aria/ packages/react-stately/ packages/react-aria-components/; \ do cp $$pkg/dist/module.js $$pkg/dist/import.mjs; \ done sed -i.bak s/\.js/\.mjs/ packages/@react-aria/i18n/dist/import.mjs diff --git a/packages/react-aria-components/package.json b/packages/react-aria-components/package.json index 60b1a7bfa38..50114048f02 100644 --- a/packages/react-aria-components/package.json +++ b/packages/react-aria-components/package.json @@ -7,6 +7,11 @@ "module": "dist/module.js", "types": "dist/types.d.ts", "source": "src/index.ts", + "exports": { + "types": "./dist/types.d.ts", + "import": "./dist/import.mjs", + "require": "./dist/main.js" + }, "files": [ "dist" ], From 8c8719b873d0005819593b609f2166b90425c215 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Thu, 30 Mar 2023 12:37:30 -0400 Subject: [PATCH 2/4] Don't render collection portal in SSR --- packages/react-aria-components/src/Collection.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-aria-components/src/Collection.tsx b/packages/react-aria-components/src/Collection.tsx index c798eb3ef6f..ad2262c5fda 100644 --- a/packages/react-aria-components/src/Collection.tsx +++ b/packages/react-aria-components/src/Collection.tsx @@ -13,7 +13,7 @@ import {CollectionBase} from '@react-types/shared'; import {createPortal} from 'react-dom'; import {DOMProps, RenderProps} from './utils'; import {Collection as ICollection, Node, SelectionBehavior, SelectionMode, ItemProps as SharedItemProps, SectionProps as SharedSectionProps} from 'react-stately'; -import {mergeProps} from 'react-aria'; +import {mergeProps, useIsSSR} from 'react-aria'; import React, {cloneElement, createContext, Key, ReactElement, ReactNode, ReactPortal, useCallback, useContext, useMemo} from 'react'; import {useLayoutEffect} from '@react-aria/utils'; import {useSyncExternalStore} from 'use-sync-external-store/shim/index.js'; @@ -643,7 +643,7 @@ export function useCollectionChildren(props: CachedChildrenOpt const ShallowRenderContext = createContext(false); interface CollectionResult { - portal: ReactPortal, + portal: ReactPortal | null, collection: C } @@ -660,7 +660,7 @@ export function useCollection>(pro {children} ), [children]); - let portal = createPortal(wrappedChildren, document as unknown as Element); + let portal = useIsSSR() ? null : createPortal(wrappedChildren, document as unknown as Element); useLayoutEffect(() => { if (document.dirtyNodes.size > 0) { From 5275c39a81e6f22876c428f480e8e4bdde054093 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Thu, 30 Mar 2023 12:38:26 -0400 Subject: [PATCH 3/4] Only return true from useIsSSR during initial hydration --- packages/@react-aria/ssr/src/SSRProvider.tsx | 39 ++++++++++--------- .../combobox/test/ComboBox.test.js | 13 +++++++ 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/packages/@react-aria/ssr/src/SSRProvider.tsx b/packages/@react-aria/ssr/src/SSRProvider.tsx index 920c03a089f..920811b5c3c 100644 --- a/packages/@react-aria/ssr/src/SSRProvider.tsx +++ b/packages/@react-aria/ssr/src/SSRProvider.tsx @@ -23,7 +23,8 @@ import React, {ReactNode, useContext, useLayoutEffect, useMemo, useRef, useState // consistent ids regardless of the loading order. interface SSRContextValue { prefix: string, - current: number + current: number, + isSSR: boolean } // Default context value to use in case there is no SSRProvider. This is fine for @@ -33,7 +34,8 @@ interface SSRContextValue { // SSR case multiple copies of React Aria is not supported. const defaultContext: SSRContextValue = { prefix: String(Math.round(Math.random() * 10000000000)), - current: 0 + current: 0, + isSSR: false }; const SSRContext = React.createContext(defaultContext); @@ -50,12 +52,25 @@ export interface SSRProviderProps { export function SSRProvider(props: SSRProviderProps): JSX.Element { let cur = useContext(SSRContext); let counter = useCounter(cur === defaultContext); + let [isSSR, setIsSSR] = useState(true); let value: SSRContextValue = useMemo(() => ({ // If this is the first SSRProvider, start with an empty string prefix, otherwise // append and increment the counter. prefix: cur === defaultContext ? '' : `${cur.prefix}-${counter}`, - current: 0 - }), [cur, counter]); + current: 0, + isSSR + }), [cur, counter, isSSR]); + + // If on the client, and the component was initially server rendered, + // then schedule a layout effect to update the component after hydration. + if (typeof window !== 'undefined') { + // This if statement technically breaks the rules of hooks, but is safe + // because the condition never changes after mounting. + // eslint-disable-next-line react-hooks/rules-of-hooks + useLayoutEffect(() => { + setIsSSR(false); + }, []); + } return ( @@ -131,19 +146,5 @@ export function useSSRSafeId(defaultId?: string): string { */ export function useIsSSR(): boolean { let cur = useContext(SSRContext); - let isInSSRContext = cur !== defaultContext; - let [isSSR, setIsSSR] = useState(isInSSRContext); - - // If on the client, and the component was initially server rendered, - // then schedule a layout effect to update the component after hydration. - if (typeof window !== 'undefined' && isInSSRContext) { - // This if statement technically breaks the rules of hooks, but is safe - // because the condition never changes after mounting. - // eslint-disable-next-line react-hooks/rules-of-hooks - useLayoutEffect(() => { - setIsSSR(false); - }, []); - } - - return isSSR; + return cur.isSSR; } diff --git a/packages/@react-spectrum/combobox/test/ComboBox.test.js b/packages/@react-spectrum/combobox/test/ComboBox.test.js index ee78159b27a..09d6d0b01a3 100644 --- a/packages/@react-spectrum/combobox/test/ComboBox.test.js +++ b/packages/@react-spectrum/combobox/test/ComboBox.test.js @@ -19,6 +19,7 @@ import {ComboBox, Item, Section} from '../'; import {Provider} from '@react-spectrum/provider'; import React from 'react'; import scaleMedium from '@adobe/spectrum-css-temp/vars/spectrum-medium-unique.css'; +import {SSRProvider} from '@react-aria/ssr'; import themeLight from '@adobe/spectrum-css-temp/vars/spectrum-light-unique.css'; import {useAsyncList} from '@react-stately/data'; import {useFilter} from '@react-aria/i18n'; @@ -1210,6 +1211,18 @@ describe('ComboBox', function () { expect(queryByRole('listbox')).toBeNull(); }); }); + + it('works with SSR', () => { + let {getByRole} = render(); + + let button = getByRole('button'); + triggerPress(button); + act(() => jest.runAllTimers()); + + let listbox = getByRole('listbox'); + let items = within(listbox).getAllByRole('option'); + expect(items).toHaveLength(3); + }); }); describe('typing in the textfield', function () { From 36a7194f307e9a08dd3d3b907753901c4e334888 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Thu, 30 Mar 2023 15:42:33 -0400 Subject: [PATCH 4/4] Make useInteractionModality return null during SSR hydration --- packages/@react-aria/interactions/package.json | 1 + packages/@react-aria/interactions/src/useFocusVisible.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@react-aria/interactions/package.json b/packages/@react-aria/interactions/package.json index 9a0fb37e824..3e97baaf0c3 100644 --- a/packages/@react-aria/interactions/package.json +++ b/packages/@react-aria/interactions/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@react-aria/utils": "^3.15.0", + "@react-aria/ssr": "^3.5.0", "@react-types/shared": "^3.17.0", "@swc/helpers": "^0.4.14" }, diff --git a/packages/@react-aria/interactions/src/useFocusVisible.ts b/packages/@react-aria/interactions/src/useFocusVisible.ts index 59ad8c87fd5..60fd6b5a77f 100644 --- a/packages/@react-aria/interactions/src/useFocusVisible.ts +++ b/packages/@react-aria/interactions/src/useFocusVisible.ts @@ -17,6 +17,7 @@ import {isMac, isVirtualClick} from '@react-aria/utils'; import {useEffect, useState} from 'react'; +import {useIsSSR} from '@react-aria/ssr'; export type Modality = 'keyboard' | 'pointer' | 'virtual'; type HandlerEvent = PointerEvent | MouseEvent | KeyboardEvent | FocusEvent; @@ -192,7 +193,7 @@ export function useInteractionModality(): Modality { }; }, []); - return modality; + return useIsSSR() ? null : modality; } /**