Conversation
Re-provide `ReduceMotionContext` in `Portal`, alongside the settings, locale and theme contexts already forwarded across the portal boundary, so portal content stops falling back to the context default of `false`.
Compare the key when looking up the queued `mount` to replace, so an update that arrives before the `PortalManager` ref is attached no longer overwrites an unrelated queued portal.
Add an opt-in `overlay` prop to `Portal` that hides every layer below it -- the app content and any portal mounted earlier -- from assistive technology and from the web focus order, while portals mounted on top stay reachable.
| import OverlayLayer from './OverlayLayer'; | ||
|
|
||
| type Props = { | ||
| pageContent?: React.ReactNode; |
There was a problem hiding this comment.
pageContent is misleading name as a portal provider doesn't render a page. it should just be content or children.
it should also not be optional.
| pageContent?: React.ReactNode; | |
| children: React.ReactNode; |
| /** | ||
| * Whether this portal hides everything below it -- the app content and any | ||
| * portal mounted before it -- from screen readers and the focus order. | ||
| * Portals mounted after it stay reachable. | ||
| * | ||
| * Tie it to whether the overlay is open rather than to how long it stays | ||
| * painted: a layer gives the screen back the moment it starts closing, so | ||
| * what is underneath is reachable again while the overlay fades out. | ||
| */ | ||
| overlay?: boolean; |
There was a problem hiding this comment.
modal maybe a better name instead overlay
|
|
||
| return ( | ||
| <> | ||
| {/* Need collapsable=false here to clip the elevations, otherwise they appear above Portal components */} |
There was a problem hiding this comment.
move the comment to collapsable like the other one
| /** | ||
| * Whether this portal hides everything below it -- the app content and any | ||
| * portal mounted before it -- from screen readers and the focus order. | ||
| * Portals mounted after it stay reachable. | ||
| * | ||
| * Tie it to whether the overlay is open rather than to how long it stays | ||
| * painted: a layer gives the screen back the moment it starts closing, so | ||
| * what is underneath is reachable again while the overlay fades out. | ||
| */ |
There was a problem hiding this comment.
please write the comment by hand. don't use claude for documentation.
| /** | |
| * Whether this portal hides everything below it -- the app content and any | |
| * portal mounted before it -- from screen readers and the focus order. | |
| * Portals mounted after it stay reachable. | |
| * | |
| * Tie it to whether the overlay is open rather than to how long it stays | |
| * painted: a layer gives the screen back the moment it starts closing, so | |
| * what is underneath is reachable again while the overlay fades out. | |
| */ | |
| /** | |
| * Whether the portal hides items below it from screen readers and focus order. | |
| * | |
| * Ensure it's set to true only when the modal is open. | |
| */ |
| import { Button, Portal, Dialog, Palette } from 'react-native-paper'; | ||
|
|
||
| import { TextComponent } from './DialogTextComponent'; | ||
|
|
||
| const DialogWithOverlay = ({ | ||
| visible, | ||
| close, | ||
| }: { | ||
| visible: boolean; | ||
| close: () => void; | ||
| }) => ( | ||
| <Portal overlay={visible}> | ||
| <Dialog onDismiss={close} visible={visible}> | ||
| <Dialog.Title>Alert</Dialog.Title> | ||
| <Dialog.Content> | ||
| <TextComponent> | ||
| While this dialog is open, everything behind it is hidden from screen | ||
| readers and skipped by the focus order! | ||
| </TextComponent> | ||
| </Dialog.Content> | ||
| <Dialog.Actions> | ||
| <Button textColor={Palette.tertiary50} disabled> | ||
| Disagree | ||
| </Button> | ||
| <Button onPress={close}>Agree</Button> | ||
| </Dialog.Actions> | ||
| </Dialog> | ||
| </Portal> | ||
| ); | ||
|
|
||
| export default DialogWithOverlay; |
There was a problem hiding this comment.
I don't think a separate example is needed. all dialogs need to be used this way. update all the existing examples
There was a problem hiding this comment.
Updated. Now Modals render the Portal inside them
Address review feedback on callstack#5126: - rename the `overlay` prop to `modal` - rename `PortalManager`'s `pageContent` prop to `children` and make it required, since a portal host doesn't render a page - move the `collapsable` comment onto the prop it explains - rewrite the `modal` prop documentation
A `Modal` is an overlay, so it always needs a `Portal` with `modal` set to hide the content behind it. Render one itself instead of asking every call site to wrap the modal and pass the prop. BREAKING CHANGE: `Modal` and `Dialog` no longer need to be wrapped in a `Portal`.
Every dialog now hides the content behind it, so the dedicated "Inert background" example no longer has anything of its own to show.
`Dialog` renders itself in a `Portal`, so the examples no longer need to wrap it in one.
|
Found potential problems with the pull request:
|
Motivation
With a dialog open, a screen reader can still wander into the app content behind it.
Adds an opt-in
modalprop toPortal. A portal marked as a modal hides every layer below it - the app content and any portal mounted earlier - from assistive technology and from the web focus order. Portals mounted on top stay reachable, so a menu or snackbar above a dialog still works.Modalnow renders itself in a Portal withmodalprop tied to visible. Other overlay components will adopt it separately.Breaking:
ModalandDialogrender themselves in aPortal, so they now need aPortal.Hostabove them in the tree.Related issue
Notion
Screenshots / Videos
inert-background-android-after.mp4
inert-background-ios-after.mp4
Test plan
yarn test,yarn lint,yarn typecheckpass. Six newPortaltests.On a device, open any dialog in the example app and keep swiping past its last button with a screen reader, or hold Tab on web: focus stays inside the dialog, and the screen behind it is reachable again once it closes.