Skip to content

Conversation

@sean-perkins
Copy link
Contributor

@sean-perkins sean-perkins commented Jul 20, 2022

Pull request checklist

Please check if your PR fulfills the following requirements:

  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been reviewed and added / updated if needed (for bug fixes / features)
    • Some docs updates need to be made in the ionic-docs repo, in a separate PR. See the contributing guide for details.
  • Build (npm run build) was run locally and any changes were pushed
  • Lint (npm run lint) has passed locally and any fixes were made for failures

Pull request type

Please check the type of change your PR introduces:

  • Bugfix
  • Feature
  • Code style update (formatting, renaming)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • Documentation content changes
  • Other (please describe):

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:

  1. Developers could not conditionally render an inline modal/popover.
  2. Dynamic sibling nodes rendered before an IonModal/IonPopover that were mutated while the overlay was dismissing, would result in the same exception from 1.

Issue URL: #25590, #25775

What is the new behavior?

  • Inline overlays are configured with a framework delegate implementation that teleports the inline overlay to the root of the app and back to it's original DOM location (when dismissed)
  • IonModal and IonPopover can be conditionally rendered.
  • IonModal and IonPopover will not throw exceptions when sibling content is dynamically rendered before its node.
  • Overlays are unmounted when history push/pop/replace occurs
  • Overlays are moved to the top z-index when they are presented
    • ActionSheet and Alert internal elements assigned an id based on the z-index are statically computed

Does this introduce a breaking change?

  • Yes
  • No

Other information

Open to feedback on naming/folder structure for Github specific issue test-cases.

Dev-build: 6.2.8-dev.11663187107.1a1cbb3f

@github-actions github-actions bot added the package: react @ionic/react package label Jul 20, 2022
@github-actions github-actions bot added the package: core @ionic/core package label Aug 12, 2022
@sean-perkins sean-perkins changed the title fix(react): IonModal can be conditionally rendered fix(react): Inline overlays can be conditionally rendered Aug 12, 2022
@sean-perkins sean-perkins marked this pull request as ready for review August 12, 2022 21:19
@sean-perkins sean-perkins requested review from a team, averyjohnston and liamdebeasi August 12, 2022 21:19
@sean-perkins sean-perkins requested a review from a team as a code owner September 27, 2022 20:41
Comment on lines +143 to +146
componentWillUnmount() {
if (this.ref.current) {
this.ref.current.dismiss();
}
Copy link
Contributor

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', () => {
Copy link
Contributor

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}`,
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

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?

Copy link
Contributor Author

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,
Copy link
Contributor

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?

Copy link
Contributor Author

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;
Copy link
Contributor

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?

Copy link
Contributor Author

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;
Copy link
Contributor

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?

Copy link
Contributor Author

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(
Copy link
Contributor

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?

Copy link
Contributor Author

@sean-perkins sean-perkins Oct 12, 2022

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.

@sean-perkins sean-perkins changed the title fix(react): Inline overlays can be conditionally rendered fix(react): Inline overlays can be conditionally rendered (outdated) Oct 13, 2022
@sean-perkins
Copy link
Contributor Author

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.

@sean-perkins sean-perkins deleted the FW-1889 branch March 24, 2023 18:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: core @ionic/core package package: react @ionic/react package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants