@@ -2,7 +2,9 @@ let React;
22let ReactNoop ;
33let Scheduler ;
44let LegacyHidden ;
5+ let Offscreen ;
56let useState ;
7+ let useLayoutEffect ;
68
79describe ( 'ReactOffscreen' , ( ) => {
810 beforeEach ( ( ) => {
@@ -12,7 +14,9 @@ describe('ReactOffscreen', () => {
1214 ReactNoop = require ( 'react-noop-renderer' ) ;
1315 Scheduler = require ( 'scheduler' ) ;
1416 LegacyHidden = React . unstable_LegacyHidden ;
17+ Offscreen = React . unstable_Offscreen ;
1518 useState = React . useState ;
19+ useLayoutEffect = React . useLayoutEffect ;
1620 } ) ;
1721
1822 function Text ( props ) {
@@ -169,4 +173,150 @@ describe('ReactOffscreen', () => {
169173 </ > ,
170174 ) ;
171175 } ) ;
176+
177+ // @gate experimental
178+ // @gate enableSuspenseLayoutEffectSemantics
179+ it ( 'mounts without layout effects when hidden' , async ( ) => {
180+ function Child ( { text} ) {
181+ useLayoutEffect ( ( ) => {
182+ Scheduler . unstable_yieldValue ( 'Mount layout' ) ;
183+ return ( ) => {
184+ Scheduler . unstable_yieldValue ( 'Unmount layout' ) ;
185+ } ;
186+ } , [ ] ) ;
187+ return < Text text = "Child" /> ;
188+ }
189+
190+ const root = ReactNoop . createRoot ( ) ;
191+
192+ // Mount hidden tree.
193+ await ReactNoop . act ( async ( ) => {
194+ root . render (
195+ < Offscreen mode = "hidden" >
196+ < Child />
197+ </ Offscreen > ,
198+ ) ;
199+ } ) ;
200+ // No layout effect.
201+ expect ( Scheduler ) . toHaveYielded ( [ 'Child' ] ) ;
202+ // TODO: Offscreen does not yet hide/unhide children correctly. Until we do,
203+ // it should only be used inside a host component wrapper whose visibility
204+ // is toggled simultaneously.
205+ expect ( root ) . toMatchRenderedOutput ( < span prop = "Child" /> ) ;
206+
207+ // Unhide the tree. The layout effect is mounted.
208+ await ReactNoop . act ( async ( ) => {
209+ root . render (
210+ < Offscreen mode = "visible" >
211+ < Child />
212+ </ Offscreen > ,
213+ ) ;
214+ } ) ;
215+ expect ( Scheduler ) . toHaveYielded ( [ 'Child' , 'Mount layout' ] ) ;
216+ expect ( root ) . toMatchRenderedOutput ( < span prop = "Child" /> ) ;
217+ } ) ;
218+
219+ // @gate experimental
220+ // @gate enableSuspenseLayoutEffectSemantics
221+ it ( 'mounts/unmounts layout effects when visibility changes (starting visible)' , async ( ) => {
222+ function Child ( { text} ) {
223+ useLayoutEffect ( ( ) => {
224+ Scheduler . unstable_yieldValue ( 'Mount layout' ) ;
225+ return ( ) => {
226+ Scheduler . unstable_yieldValue ( 'Unmount layout' ) ;
227+ } ;
228+ } , [ ] ) ;
229+ return < Text text = "Child" /> ;
230+ }
231+
232+ const root = ReactNoop . createRoot ( ) ;
233+ await ReactNoop . act ( async ( ) => {
234+ root . render (
235+ < Offscreen mode = "visible" >
236+ < Child />
237+ </ Offscreen > ,
238+ ) ;
239+ } ) ;
240+ expect ( Scheduler ) . toHaveYielded ( [ 'Child' , 'Mount layout' ] ) ;
241+ expect ( root ) . toMatchRenderedOutput ( < span prop = "Child" /> ) ;
242+
243+ // Hide the tree. The layout effect is unmounted.
244+ await ReactNoop . act ( async ( ) => {
245+ root . render (
246+ < Offscreen mode = "hidden" >
247+ < Child />
248+ </ Offscreen > ,
249+ ) ;
250+ } ) ;
251+ expect ( Scheduler ) . toHaveYielded ( [ 'Unmount layout' , 'Child' ] ) ;
252+ // TODO: Offscreen does not yet hide/unhide children correctly. Until we do,
253+ // it should only be used inside a host component wrapper whose visibility
254+ // is toggled simultaneously.
255+ expect ( root ) . toMatchRenderedOutput ( < span prop = "Child" /> ) ;
256+
257+ // Unhide the tree. The layout effect is re-mounted.
258+ await ReactNoop . act ( async ( ) => {
259+ root . render (
260+ < Offscreen mode = "visible" >
261+ < Child />
262+ </ Offscreen > ,
263+ ) ;
264+ } ) ;
265+ expect ( Scheduler ) . toHaveYielded ( [ 'Child' , 'Mount layout' ] ) ;
266+ expect ( root ) . toMatchRenderedOutput ( < span prop = "Child" /> ) ;
267+ } ) ;
268+
269+ // @gate experimental
270+ // @gate enableSuspenseLayoutEffectSemantics
271+ it ( 'mounts/unmounts layout effects when visibility changes (starting hidden)' , async ( ) => {
272+ function Child ( { text} ) {
273+ useLayoutEffect ( ( ) => {
274+ Scheduler . unstable_yieldValue ( 'Mount layout' ) ;
275+ return ( ) => {
276+ Scheduler . unstable_yieldValue ( 'Unmount layout' ) ;
277+ } ;
278+ } , [ ] ) ;
279+ return < Text text = "Child" /> ;
280+ }
281+
282+ const root = ReactNoop . createRoot ( ) ;
283+ await ReactNoop . act ( async ( ) => {
284+ // Start the tree hidden. The layout effect is not mounted.
285+ root . render (
286+ < Offscreen mode = "hidden" >
287+ < Child />
288+ </ Offscreen > ,
289+ ) ;
290+ } ) ;
291+ expect ( Scheduler ) . toHaveYielded ( [ 'Child' ] ) ;
292+ // TODO: Offscreen does not yet hide/unhide children correctly. Until we do,
293+ // it should only be used inside a host component wrapper whose visibility
294+ // is toggled simultaneously.
295+ expect ( root ) . toMatchRenderedOutput ( < span prop = "Child" /> ) ;
296+
297+ // Show the tree. The layout effect is mounted.
298+ await ReactNoop . act ( async ( ) => {
299+ root . render (
300+ < Offscreen mode = "visible" >
301+ < Child />
302+ </ Offscreen > ,
303+ ) ;
304+ } ) ;
305+ expect ( Scheduler ) . toHaveYielded ( [ 'Child' , 'Mount layout' ] ) ;
306+ expect ( root ) . toMatchRenderedOutput ( < span prop = "Child" /> ) ;
307+
308+ // Hide the tree again. The layout effect is un-mounted.
309+ await ReactNoop . act ( async ( ) => {
310+ root . render (
311+ < Offscreen mode = "hidden" >
312+ < Child />
313+ </ Offscreen > ,
314+ ) ;
315+ } ) ;
316+ expect ( Scheduler ) . toHaveYielded ( [ 'Unmount layout' , 'Child' ] ) ;
317+ // TODO: Offscreen does not yet hide/unhide children correctly. Until we do,
318+ // it should only be used inside a host component wrapper whose visibility
319+ // is toggled simultaneously.
320+ expect ( root ) . toMatchRenderedOutput ( < span prop = "Child" /> ) ;
321+ } ) ;
172322} ) ;
0 commit comments