-
Notifications
You must be signed in to change notification settings - Fork 13.4k
fix(react): Inline overlays can be conditionally rendered (outdated) #25663
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
Conversation
| componentWillUnmount() { | ||
| if (this.ref.current) { | ||
| this.ref.current.dismiss(); | ||
| } |
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.
While playing around in a small test app, I noticed some errors when doing replace or push operations that I think are due to the timing of this dismiss call.
Repo/branch: https://github.com/amandaejohnston/sandbox-ionic-react/tree/FW-1889
Screencast: https://user-images.githubusercontent.com/90629384/194619453-2bab84cd-bfa6-421f-a9dd-ea68dec8b36c.mp4
My guess is this is happening because the dismiss event is firing and/or being picked up by the Home component after the modal has been removed from the DOM. This would cause the onDidDismiss handler for the modal to try and set the isOpen state on the modal when it's unmounted, throwing the error. Although, the fact that I don't get the error when doing a goBack operation may throw a wrench in that. 🤔
Let me know if this is an implementation issue on my end, of course! The code is mostly copy-pasted from the test app page in this PR but maybe something went screwy in the process.
| @@ -0,0 +1,41 @@ | |||
| describe('IonModal', () => { | |||
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.
Could we name this file something more descriptive, maybe conditional-render.spec.ts or something? I would rather us not have to dig through the code and/or click through to the GH issue to figure out the context.
| tabindex="-1" | ||
| {...(htmlAttributes as any)} | ||
| style={{ | ||
| zIndex: `${20000 + this.overlayIndex}`, |
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.
Are these changes required for this fix to land? If not, can we move them to a separate PR? Same goes for the changes in alert.tsx.
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.
Yes, these changes are required. In main the id of these internal elements is dependent on the overlayIndex. In main, the overlayIndex is always static after render. In this PR, top overlays are re-assigned a higher overlay index, so that they always display as the top most visible overlay. In doing so, the internal ids are updated to the new z-index value. This is not desired and would be a breaking change and defeat the purpose of these selectors.
With this change, the ids remain constant during re-render, even when the overlay is assigned a new overlayIndex as a result of being presented.
| clearTimeout(this.durationTimeout); | ||
| } | ||
| return dismiss(this, data, role, 'toastLeave', iosLeaveAnimation, mdLeaveAnimation, this.position); | ||
| return dismiss(this, data, role, 'toastLeave', iosLeaveAnimation, mdLeaveAnimation, true, this.position); |
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.
Why does toast need the true param but others like alert/action-sheet/etc do not?
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.
The last parameter of the dismiss utility is optional. Alert/ActionSheet/etc. do not use the last parameter to specify additional options, toast does.
The new property introduced to dismiss has a default value of true that matches the base behavior on main (removing the overlay element).
Since Toast specifies a value for the last optional parameter, is has to specify a value for the parameter before it. Alert/ActionSheet/etc. do not, so they do not need to specify the parameter, as it will use the default value.
| 'modalLeave', | ||
| iosLeaveAnimation, | ||
| mdLeaveAnimation, | ||
| inline === false || delegate === undefined, |
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.
Can we extract this to a variable that better defines what it is checking? My understanding is we want to only remove the overlay if the overlay is presented via a controller, right?
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.
Correct, the overlay should only be removed if presented via a controller.
Should it be something like:
const hasController = inline === false || delegate === undefined;or explicit to the removal of the overlay element?
const removeOverlay = inline === false || delegate === undefined;| }, children) : | ||
| null | ||
| const keepContentsMounted = | ||
| this.props.trigger !== undefined || this.props.keepContentsMounted; |
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.
Why do we check for a trigger with keepContentsMounted?
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.
Trigger-based overlays require that the web component is mounted in the DOM in order to attach the event listeners to the trigger element.
This flag, keepContentsMounted, tracks the two states when we should keep the React component mounted; either when the overlay is using a trigger (otherwise it will never open, since it is in a React Portal) or when it explicitly has opted into keepContentsMounted.
| this.props.trigger !== undefined || this.props.keepContentsMounted; | ||
|
|
||
| const mountOverlay = | ||
| this.state.isOpen || this.props.isOpen || this.isDismissing || keepContentsMounted; |
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.
Why do we need to check both this.state.isOpen and this.props.isOpen?
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.
I will need to dig back into this decision. It was deliberate to solve an issue with the overlay not mounting when using the isOpen property directly. I will see if I can reproduce the issue that influenced it.
| delegate: this.delegate, | ||
| }; | ||
|
|
||
| attachProps( |
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.
Why do we need to attach the props here? Shouldn't the render function pass the updated props via newProps?
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.
This behavior exists on main. This change just assigns the React instance of the delegate to the element.
This is also similar to createOverlayComponent: https://github.com/ionic-team/ionic-framework/blob/main/packages/react/src/components/createOverlayComponent.tsx#L90
I am not sure as to why we attach props this way vs. the render function, but I opted for changing as little of the overlay implementation as possible here, unless we are having an issue with the existing pattern.
Edit:
You are correct, this should be refactored to the newProps assignment in the render function.
|
Closing this PR in favor of #26111. I will keep the branch around for reference if we decide to move to React Portals in a future major version. |
Pull request checklist
Please check if your PR fulfills the following requirements:
ionic-docsrepo, in a separate PR. See the contributing guide for details.npm run build) was run locally and any changes were pushednpm run lint) has passed locally and any fixes were made for failuresPull request type
Please check the type of change your PR introduces:
What is the current behavior?
Inline overlays (modal, popover) were never configured with a framework delegate. This resulted in us removing DOM nodes through manual manipulation, instead of relying on React to clear the React component and associated DOM node.
This resulted in two problems:
Issue URL: #25590, #25775
What is the new behavior?
IonModalandIonPopovercan be conditionally rendered.IonModalandIonPopoverwill not throw exceptions when sibling content is dynamically rendered before its node.z-indexwhen they are presentedz-indexare statically computedDoes this introduce a breaking change?
Other information
Open to feedback on naming/folder structure for Github specific issue test-cases.
Dev-build:
6.2.8-dev.11663187107.1a1cbb3f