From 827796d48e7d4c74b4362cf90955e162082ee46d Mon Sep 17 00:00:00 2001 From: Benjamin Padula Date: Fri, 15 Jan 2021 12:09:32 -0600 Subject: [PATCH] [fixed] Ensure after-open css transitions work in Safari 14 & Mobile Safari --- specs/Modal.events.spec.js | 32 ++++++++---- specs/Modal.spec.js | 94 ++++++++++++++++++++--------------- src/components/ModalPortal.js | 18 ++++--- 3 files changed, 88 insertions(+), 56 deletions(-) diff --git a/specs/Modal.events.spec.js b/specs/Modal.events.spec.js index 5e223288..0e61c1bf 100644 --- a/specs/Modal.events.spec.js +++ b/specs/Modal.events.spec.js @@ -1,5 +1,6 @@ /* eslint-env mocha */ import React from "react"; +import ReactDOM from "react-dom"; import "should"; import sinon from "sinon"; import Modal from "react-modal"; @@ -11,24 +12,37 @@ import { mouseUpAt, escKeyDown, tabKeyDown, - withModal + withModal, + withElementCollector, + createHTMLElement } from "./helper"; export default () => { it("should trigger the onAfterOpen callback", () => { const afterOpenCallback = sinon.spy(); - const props = { isOpen: true, onAfterOpen: afterOpenCallback }; - withModal(props, null, () => {}); - afterOpenCallback.called.should.be.ok(); + withElementCollector(() => { + const props = { isOpen: true, onAfterOpen: afterOpenCallback }; + const node = createHTMLElement("div"); + ReactDOM.render(, node); + requestAnimationFrame(() => { + afterOpenCallback.called.should.be.ok(); + ReactDOM.unmountComponentAtNode(node); + }); + }); }); it("should call onAfterOpen with overlay and content references", () => { const afterOpenCallback = sinon.spy(); - const props = { isOpen: true, onAfterOpen: afterOpenCallback }; - withModal(props, null, modal => { - sinon.assert.calledWith(afterOpenCallback, { - overlayEl: modal.portal.overlay, - contentEl: modal.portal.content + withElementCollector(() => { + const props = { isOpen: true, onAfterOpen: afterOpenCallback }; + const node = createHTMLElement("div"); + const modal = ReactDOM.render(, node); + requestAnimationFrame(() => { + sinon.assert.calledWith(afterOpenCallback, { + overlayEl: modal.portal.overlay, + contentEl: modal.portal.content + }); + ReactDOM.unmountComponentAtNode(node); }); }); }); diff --git a/specs/Modal.spec.js b/specs/Modal.spec.js index 47c25482..c32db56e 100644 --- a/specs/Modal.spec.js +++ b/specs/Modal.spec.js @@ -348,32 +348,42 @@ export default () => { }); it("overrides content classes with custom object className", () => { - const props = { - isOpen: true, - className: { - base: "myClass", - afterOpen: "myClass_after-open", - beforeClose: "myClass_before-close" - } - }; - withModal(props, null, modal => { - mcontent(modal).className.should.be.eql("myClass myClass_after-open"); + withElementCollector(() => { + const props = { + isOpen: true, + className: { + base: "myClass", + afterOpen: "myClass_after-open", + beforeClose: "myClass_before-close" + } + }; + const node = createHTMLElement("div"); + const modal = ReactDOM.render(, node); + requestAnimationFrame(() => { + mcontent(modal).className.should.be.eql("myClass myClass_after-open"); + ReactDOM.unmountComponentAtNode(node); + }); }); }); it("overrides overlay classes with custom object overlayClassName", () => { - const props = { - isOpen: true, - overlayClassName: { - base: "myOverlayClass", - afterOpen: "myOverlayClass_after-open", - beforeClose: "myOverlayClass_before-close" - } - }; - withModal(props, null, modal => { - moverlay(modal).className.should.be.eql( - "myOverlayClass myOverlayClass_after-open" - ); + withElementCollector(() => { + const props = { + isOpen: true, + overlayClassName: { + base: "myOverlayClass", + afterOpen: "myOverlayClass_after-open", + beforeClose: "myOverlayClass_before-close" + } + }; + const node = createHTMLElement("div"); + const modal = ReactDOM.render(, node); + requestAnimationFrame(() => { + moverlay(modal).className.should.be.eql( + "myOverlayClass myOverlayClass_after-open" + ); + ReactDOM.unmountComponentAtNode(node); + }); }); }); @@ -668,11 +678,18 @@ export default () => { }); it("adds --after-open for animations", () => { - const props = { isOpen: true }; - withModal(props, null, modal => { + withElementCollector(() => { const rg = /--after-open/i; - rg.test(mcontent(modal).className).should.be.ok(); - rg.test(moverlay(modal).className).should.be.ok(); + const props = { isOpen: true }; + const node = createHTMLElement("div"); + const modal = ReactDOM.render(, node); + requestAnimationFrame(() => { + const contentName = modal.portal.content.className; + const overlayName = modal.portal.overlay.className; + rg.test(contentName).should.be.ok(); + rg.test(overlayName).should.be.ok(); + ReactDOM.unmountComponentAtNode(node); + }); }); }); @@ -722,25 +739,24 @@ export default () => { }); it("keeps the modal in the DOM until closeTimeoutMS elapses", done => { - const closeTimeoutMS = 100; + function checkDOM(count) { + const overlay = document.querySelectorAll(".ReactModal__Overlay"); + const content = document.querySelectorAll(".ReactModal__Content"); + overlay.length.should.be.eql(count); + content.length.should.be.eql(count); + } + withElementCollector(() => { + const closeTimeoutMS = 100; + const props = { isOpen: true, closeTimeoutMS }; + const node = createHTMLElement("div"); + const modal = ReactDOM.render(, node); - const props = { isOpen: true, closeTimeoutMS }; - withModal(props, null, modal => { modal.portal.closeWithTimeout(); - - function checkDOM(count) { - const overlay = document.querySelectorAll(".ReactModal__Overlay"); - const content = document.querySelectorAll(".ReactModal__Content"); - overlay.length.should.be.eql(count); - content.length.should.be.eql(count); - } - - // content is still mounted after modal is gone checkDOM(1); setTimeout(() => { - // content is unmounted after specified timeout checkDOM(0); + ReactDOM.unmountComponentAtNode(node); done(); }, closeTimeoutMS); }); diff --git a/src/components/ModalPortal.js b/src/components/ModalPortal.js index b7ac6cea..d28ee455 100644 --- a/src/components/ModalPortal.js +++ b/src/components/ModalPortal.js @@ -222,14 +222,16 @@ export default class ModalPortal extends Component { } this.setState({ isOpen: true }, () => { - this.setState({ afterOpen: true }); - - if (this.props.isOpen && this.props.onAfterOpen) { - this.props.onAfterOpen({ - overlayEl: this.overlay, - contentEl: this.content - }); - } + requestAnimationFrame(() => { + this.setState({ afterOpen: true }); + + if (this.props.isOpen && this.props.onAfterOpen) { + this.props.onAfterOpen({ + overlayEl: this.overlay, + contentEl: this.content + }); + } + }); }); } };