-
Notifications
You must be signed in to change notification settings - Fork 186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ARGG-876] Add support for withScrimmedPortal with SSR #3085
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,35 +17,36 @@ | |
*/ | ||
|
||
import '@testing-library/jest-dom'; | ||
import { render, within, screen } from '@testing-library/react'; | ||
import { render, within, screen, isInaccessible } from '@testing-library/react'; | ||
import { useEffect, useState } from 'react'; | ||
import { renderToString } from 'react-dom/server'; | ||
|
||
import withScrimmedPortal from './withScrimmedPortal'; | ||
import type { Props } from './withScrimmedPortal'; | ||
|
||
describe('withScrimmedPortal', () => { | ||
it('renders the wrapped component inside a portal correctly with fallback to document.body', () => { | ||
const DialogContent = () => <div>Dialog content</div>; | ||
const DialogContent = () => <div data-testid="dialog-content">Dialog content</div>; | ||
const ScrimmedComponent = withScrimmedPortal(DialogContent); | ||
|
||
render( | ||
<div id="pagewrap"> | ||
<div> Content hidden from AT</div> | ||
<div data-testid="hidden"> Content hidden from AT</div> | ||
<ScrimmedComponent | ||
getApplicationElement={() => document.getElementById('pagewrap')} | ||
/> | ||
</div> | ||
); | ||
expect(document.body).toMatchSnapshot(); | ||
expect(document.body).toContainElement(screen.getByTestId('dialog-content')); | ||
}); | ||
|
||
it('renders the wrapped component inside a portal with renderTarget provided', () => { | ||
const WrappedComponent = () => <div>Wrapped Component</div>; | ||
const ScrimmedComponent = withScrimmedPortal(WrappedComponent); | ||
const DialogContent = () => <div data-testid="dialog-content">Dialog content</div>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what's the difference between this test and the next one? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, yeah, you're right. Nothing really, I'll remove it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what's the difference between this test and the next one? |
||
const ScrimmedComponent = withScrimmedPortal(DialogContent); | ||
render( | ||
<div> | ||
<div id="pagewrap"> | ||
<div> Content hidden from AT</div> | ||
<div data-testid="hidden"> Content hidden from AT</div> | ||
<ScrimmedComponent | ||
getApplicationElement={() => document.getElementById('pagewrap')} | ||
renderTarget={() => document.getElementById('modal-container')} | ||
|
@@ -54,11 +55,13 @@ describe('withScrimmedPortal', () => { | |
<div id="modal-container" /> | ||
</div> | ||
); | ||
expect(document.body).toMatchSnapshot(); | ||
expect( | ||
document.getElementById('modal-container') | ||
).toContainElement(screen.getByTestId('dialog-content')); | ||
}); | ||
|
||
it('renders the wrapped component outside the applicationElement', () => { | ||
const WrappedComponent = () => <div>Wrapped Component</div>; | ||
const WrappedComponent = () => <div data-testid="dialog-content">Wrapped Component</div>; | ||
const ScrimmedComponent = withScrimmedPortal(WrappedComponent); | ||
render( | ||
<div> | ||
|
@@ -105,4 +108,51 @@ describe('withScrimmedPortal', () => { | |
|
||
expect(screen.getByText('Wrapped Component / portal is not ready yet / portal is now ready')).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders the wrapped component inside a portal correctly when runOnServer is false (default)', () => { | ||
const DialogContent = () => <div data-testid="dialog-content">Dialog content</div>; | ||
anambl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const ScrimmedComponent = withScrimmedPortal(DialogContent); | ||
|
||
render( | ||
<div id="pagewrap"> | ||
<div> Content hidden from AT</div> | ||
<ScrimmedComponent | ||
getApplicationElement={() => document.getElementById('pagewrap')} | ||
runOnServer={false} | ||
/> | ||
</div> | ||
); | ||
expect(document.body).toContainElement(screen.getByTestId('dialog-content')); | ||
}); | ||
|
||
it('renders the wrapped component inside a portal correctly when runOnServer is true', () => { | ||
const DialogContent = () => <div data-testid="dialog-content">Dialog content</div>; | ||
const ScrimmedComponent = withScrimmedPortal(DialogContent); | ||
|
||
render( | ||
<div id="pagewrap"> | ||
<div> Content hidden from AT</div> | ||
<ScrimmedComponent | ||
anambl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
getApplicationElement={() => document.getElementById('pagewrap')} | ||
runOnServer | ||
/> | ||
</div> | ||
); | ||
expect(document.body).toContainElement(screen.getByTestId('dialog-content')); | ||
}); | ||
|
||
it('throws an error when runOnServer is false and component is rendered on the server', () => { | ||
anambl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const DialogContent = () => <div>Dialog content</div>; | ||
const ScrimmedComponent = withScrimmedPortal(DialogContent); | ||
|
||
expect(() => renderToString( | ||
<div id="pagewrap"> | ||
<div> Content hidden from AT</div> | ||
<ScrimmedComponent | ||
getApplicationElement={() => document.getElementById('pagewrap')} | ||
runOnServer={false} | ||
/> | ||
</div> | ||
)).toThrow(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,10 +21,15 @@ import { useState, useEffect } from 'react'; | |
import { createPortal } from 'react-dom'; | ||
|
||
import withScrim from './withScrim'; | ||
import BpkScrim from './BpkScrim'; | ||
import type { Props as ScrimProps } from './withScrim'; | ||
|
||
export type Props = ScrimProps & { | ||
renderTarget?: (() => HTMLElement | null) | null; | ||
/** | ||
* If true, the component will run on both server and client. | ||
*/ | ||
runOnServer?: boolean; | ||
}; | ||
|
||
const getPortalElement = (target: (() => HTMLElement | null) | null | undefined) => { | ||
|
@@ -37,22 +42,34 @@ const getPortalElement = (target: (() => HTMLElement | null) | null | undefined) | |
if (document.body) { | ||
return document.body; | ||
} | ||
throw new Error('Render target and fallback unavailable'); | ||
throw new Error('Render target and fallback unavailable. The ScrimmedComponent uses a portal making it unsuitable for SSR. If you are trying to render on SSR, please pass the `runOnServer` prop.'); | ||
} | ||
|
||
const withScrimmedPortal = (WrappedComponent: ComponentType<ScrimProps>) => { | ||
const Scrimmed = withScrim(WrappedComponent); | ||
|
||
const ScrimmedComponent = ({ renderTarget, ...rest}: Props) => { | ||
const portalElement = getPortalElement(renderTarget); | ||
|
||
const ScrimmedComponent = ({ renderTarget, runOnServer = false, ...rest}: Props) => { | ||
const [isPortalReady, setIsPortalReady] = useState(false); | ||
|
||
useEffect(() => { | ||
setIsPortalReady(true); | ||
}, []); | ||
Comment on lines
50
to
52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given all the discussion regarding double render and the general strategy being taken here, I recommend linking to the react docs: https://react.dev/reference/react-dom/hydrate#handling-different-client-and-server-content and popping some of the context from the comments directly into code. |
||
|
||
return createPortal(<Scrimmed {...rest} isPortalReady={isPortalReady} />, portalElement); | ||
/** | ||
* The following code runs only on the client. | ||
* If consumer passes runOnServer as false (meaning they want to render the component on client only), we render the component as normal. | ||
* If consumer passes runOnServer as true (meaning they want to render the component on server and client), we render the component as normal once the component has been mounted. | ||
* This is to ensure the snapshotted markup (initial render before the component has been mounted) is the same on both server and client. | ||
*/ | ||
if (!runOnServer || isPortalReady) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would we expect a consumer to be aware of where the component is being rendered (server or browser) and act accordingly to pass a prop in? If so, this is a code smell to me. Modern web frameworks like Remix have no real concept of "running on server" or "running on browser". The expectation is all components are executed in both environments and it's common practice for components to be required to be self aware of the where they are being rendered, and thus act accordingly. What's the reason we don't want this to be self aware? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I didn't know that, but I think that's still ok as then you just pass the
How can we tell if a component is running on server? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Then the naming here should be improved, But it still raises the question, do we need flexibility here? Why can't this component decide what should happen depending on the environment?
common practice is to check for the presence of window, but it depends what the requirements are. If we need the if (typeof window === 'undefined') There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. totally agree with Chris and probably that's the source of my confusion in the other comment There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I thought we always run on either just client, or both client and server so it's clear that this will run on both (although also added a comment above to explain it)
The component does decide what should happen depending on the env, I just didn't want to force the Maybe this comment helps here https://github.com/Skyscanner/backpack/pull/3085/files#r1395443998 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There's a big mix. Banana is CSR only. Falcon and hotels do SSR. OCs SSR the HTML and deliver it when requested. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree with previous comments here, replacing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, yes, that'll work too, we don't need the prop There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've removed the prop, although just to clarify - the point of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Can you explain what you mean by this? I don't quite follow
anambl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const portalElement = getPortalElement(renderTarget); | ||
return createPortal(<Scrimmed {...rest} isPortalReady={isPortalReady} />, portalElement); | ||
} | ||
|
||
/** | ||
* The following code will run on both server and on the intial render on the client when consumers opts for both SSR and CSR i.e. by passing the `runOnServer` prop. | ||
*/ | ||
return <BpkScrim />; | ||
} | ||
|
||
return ScrimmedComponent; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is
isInaccessible
used somewhere?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohh no, I'd added it, but then removed it
I'll remove this, I thought the linting would do that 😅