Browse Source

Move Draftail tooltips portal closer to the editor to prevent background flickering

Thibaud Colas 7 năm trước cách đây
mục cha
commit
9861c2a0d4

+ 5 - 0
client/src/components/Draftail/Draftail.scss

@@ -71,6 +71,11 @@ $draftail-editor-font-family: $font-serif;
 
 
 .Draftail-Editor {
 .Draftail-Editor {
     border-radius: 0;
     border-radius: 0;
+
+    &__wrapper {
+        // Ensure elements within the editor are positioned according to this container.
+        position: relative;
+    }
 }
 }
 
 
 // When in a .full container, the editor has a specific appearance
 // When in a .full container, the editor has a specific appearance

+ 23 - 9
client/src/components/Draftail/blocks/MediaBlock.js

@@ -27,13 +27,27 @@ class MediaBlock extends Component {
   }
   }
 
 
   openTooltip(e) {
   openTooltip(e) {
-    const trigger = e.target;
+    const trigger = e.target.closest('[data-draftail-trigger]');
+
+    // Click is within the tooltip.
+    if (!trigger) {
+      return;
+    }
+
+    const container = trigger.closest('[data-draftail-editor-wrapper]');
+    const containerRect = container.getBoundingClientRect();
+    const rect = trigger.getBoundingClientRect();
+    const maxWidth = trigger.parentNode.offsetWidth - rect.width;
 
 
     this.setState({
     this.setState({
-      // Warning: overriding native DOM object. Proceed with caution.
-      showTooltipAt: Object.assign(trigger.getBoundingClientRect(), {
-        containerWidth: trigger.parentNode.offsetWidth,
-      }),
+      showTooltipAt: {
+        container: container,
+        top: rect.top - containerRect.top - (document.documentElement.scrollTop || document.body.scrollTop),
+        left: rect.left - containerRect.left - (document.documentElement.scrollLeft || document.body.scrollLeft),
+        width: rect.width,
+        height: rect.height,
+        direction: maxWidth >= TOOLTIP_MAX_WIDTH ? 'left' : 'top-left',
+      },
     });
     });
   }
   }
 
 
@@ -44,17 +58,16 @@ class MediaBlock extends Component {
   renderTooltip() {
   renderTooltip() {
     const { children } = this.props;
     const { children } = this.props;
     const { showTooltipAt } = this.state;
     const { showTooltipAt } = this.state;
-    const maxWidth = showTooltipAt.containerWidth - showTooltipAt.width;
-    const direction = maxWidth >= TOOLTIP_MAX_WIDTH ? 'left' : 'top-left';
 
 
     return (
     return (
       <Portal
       <Portal
+        node={showTooltipAt.container}
         onClose={this.closeTooltip}
         onClose={this.closeTooltip}
         closeOnClick
         closeOnClick
         closeOnType
         closeOnType
         closeOnResize
         closeOnResize
       >
       >
-        <Tooltip target={showTooltipAt} direction={direction}>
+        <Tooltip target={showTooltipAt} direction={showTooltipAt.direction}>
           <div style={{ maxWidth: OPTIONS_MAX_WIDTH }}>{children}</div>
           <div style={{ maxWidth: OPTIONS_MAX_WIDTH }}>{children}</div>
         </Tooltip>
         </Tooltip>
       </Portal>
       </Portal>
@@ -71,7 +84,8 @@ class MediaBlock extends Component {
         type="button"
         type="button"
         tabIndex={-1}
         tabIndex={-1}
         className="MediaBlock"
         className="MediaBlock"
-        onMouseUp={this.openTooltip}
+        onClick={this.openTooltip}
+        data-draftail-trigger
       >
       >
         <span className="MediaBlock__icon-wrapper" aria-hidden>
         <span className="MediaBlock__icon-wrapper" aria-hidden>
           <Icon icon={entityType.icon} className="MediaBlock__icon" />
           <Icon icon={entityType.icon} className="MediaBlock__icon" />

+ 22 - 16
client/src/components/Draftail/blocks/MediaBlock.test.js

@@ -1,5 +1,5 @@
 import React from 'react';
 import React from 'react';
-import { shallow } from 'enzyme';
+import { shallow, mount } from 'enzyme';
 
 
 import MediaBlock from '../blocks/MediaBlock';
 import MediaBlock from '../blocks/MediaBlock';
 
 
@@ -49,10 +49,16 @@ describe('MediaBlock', () => {
   });
   });
 
 
   describe('tooltip', () => {
   describe('tooltip', () => {
+    let target;
     let wrapper;
     let wrapper;
 
 
     beforeEach(() => {
     beforeEach(() => {
-      wrapper = shallow(
+      target = document.createElement('div');
+      target.setAttribute('data-draftail-trigger', true);
+      document.body.appendChild(target);
+      document.body.setAttribute('data-draftail-editor-wrapper', true);
+
+      wrapper = mount(
         <MediaBlock
         <MediaBlock
           src="example.png"
           src="example.png"
           alt=""
           alt=""
@@ -67,28 +73,32 @@ describe('MediaBlock', () => {
             },
             },
           }}
           }}
         >
         >
-          Test
+          <div id="test">Test</div>
         </MediaBlock>
         </MediaBlock>
       );
       );
     });
     });
 
 
     it('opens', () => {
     it('opens', () => {
-      const target = document.createElement('div');
-      document.body.appendChild(target);
-
-      wrapper.simulate('mouseup', { target });
+      wrapper.simulate('click', { target });
 
 
       expect(
       expect(
         wrapper
         wrapper
           .find('Portal')
           .find('Portal')
-          .dive()
           .instance().portal
           .instance().portal
       ).toMatchSnapshot();
       ).toMatchSnapshot();
     });
     });
 
 
+    it('click in tooltip', () => {
+      wrapper.simulate('click', { target });
+
+      jest.spyOn(target, 'getBoundingClientRect');
+
+      wrapper.simulate('click', { target: document.querySelector('#test') });
+
+      expect(target.getBoundingClientRect).not.toHaveBeenCalled();
+    });
+
     it('large viewport', () => {
     it('large viewport', () => {
-      const target = document.createElement('div');
-      document.body.appendChild(target);
       target.getBoundingClientRect = () => ({
       target.getBoundingClientRect = () => ({
         top: 0,
         top: 0,
         left: 0,
         left: 0,
@@ -96,26 +106,22 @@ describe('MediaBlock', () => {
         height: 0,
         height: 0,
       });
       });
 
 
-      wrapper.simulate('mouseup', { target });
+      wrapper.simulate('click', { target });
 
 
       expect(
       expect(
         wrapper
         wrapper
           .find('Portal')
           .find('Portal')
-          .dive()
           .instance()
           .instance()
           .portal.querySelector('.Tooltip').className
           .portal.querySelector('.Tooltip').className
       ).toBe('Tooltip Tooltip--left');
       ).toBe('Tooltip Tooltip--left');
     });
     });
 
 
     it('closes', () => {
     it('closes', () => {
-      const target = document.createElement('div');
-      document.body.appendChild(target);
-
       jest.spyOn(target, 'getBoundingClientRect');
       jest.spyOn(target, 'getBoundingClientRect');
 
 
       expect(wrapper.state('showTooltipAt')).toBe(null);
       expect(wrapper.state('showTooltipAt')).toBe(null);
 
 
-      wrapper.simulate('mouseup', { target });
+      wrapper.simulate('click', { target });
 
 
       expect(wrapper.state('showTooltipAt')).toMatchObject({
       expect(wrapper.state('showTooltipAt')).toMatchObject({
         top: 0,
         top: 0,

+ 11 - 7
client/src/components/Draftail/blocks/__snapshots__/MediaBlock.test.js.snap

@@ -3,7 +3,8 @@
 exports[`MediaBlock no data 1`] = `
 exports[`MediaBlock no data 1`] = `
 <button
 <button
   className="MediaBlock"
   className="MediaBlock"
-  onMouseUp={[Function]}
+  data-draftail-trigger={true}
+  onClick={[Function]}
   tabIndex={-1}
   tabIndex={-1}
   type="button"
   type="button"
 >
 >
@@ -29,7 +30,8 @@ exports[`MediaBlock no data 1`] = `
 exports[`MediaBlock renders 1`] = `
 exports[`MediaBlock renders 1`] = `
 <button
 <button
   className="MediaBlock"
   className="MediaBlock"
-  onMouseUp={[Function]}
+  data-draftail-trigger={true}
+  onClick={[Function]}
   tabIndex={-1}
   tabIndex={-1}
   type="button"
   type="button"
 >
 >
@@ -54,14 +56,16 @@ exports[`MediaBlock renders 1`] = `
 
 
 exports[`MediaBlock tooltip opens 1`] = `
 exports[`MediaBlock tooltip opens 1`] = `
 <div>
 <div>
-  <div>
+  <div
+    class="Tooltip Tooltip--top-left"
+    role="tooltip"
+    style="top: 0px; left: 0px;"
+  >
     <div
     <div
-      class="Tooltip Tooltip--top-left"
-      role="tooltip"
-      style="top: 0px; left: 0px;"
+      style="max-width: 300px;"
     >
     >
       <div
       <div
-        style="max-width: 300px;"
+        id="test"
       >
       >
         Test
         Test
       </div>
       </div>

+ 48 - 8
client/src/components/Draftail/decorators/TooltipEntity.js

@@ -22,13 +22,49 @@ class TooltipEntity extends Component {
       showTooltipAt: null,
       showTooltipAt: null,
     };
     };
 
 
+    this.onEdit = this.onEdit.bind(this);
+    this.onRemove = this.onRemove.bind(this);
     this.openTooltip = this.openTooltip.bind(this);
     this.openTooltip = this.openTooltip.bind(this);
     this.closeTooltip = this.closeTooltip.bind(this);
     this.closeTooltip = this.closeTooltip.bind(this);
   }
   }
 
 
+  onEdit(e) {
+    const { onEdit, entityKey } = this.props;
+
+    e.preventDefault();
+    e.stopPropagation();
+    onEdit(entityKey);
+  }
+
+  onRemove(e) {
+    const { onRemove, entityKey } = this.props;
+
+    e.preventDefault();
+    e.stopPropagation();
+    onRemove(entityKey);
+  }
+
   openTooltip(e) {
   openTooltip(e) {
-    const trigger = e.target;
-    this.setState({ showTooltipAt: trigger.getBoundingClientRect() });
+    const trigger = e.target.closest('[data-draftail-trigger]');
+
+    // Click is within the tooltip.
+    if (!trigger) {
+      return;
+    }
+
+    const container = trigger.closest('[data-draftail-editor-wrapper]');
+    const containerRect = container.getBoundingClientRect();
+    const rect = trigger.getBoundingClientRect();
+
+    this.setState({
+      showTooltipAt: {
+        container: container,
+        top: rect.top - containerRect.top - (document.documentElement.scrollTop || document.body.scrollTop),
+        left: rect.left - containerRect.left - (document.documentElement.scrollLeft || document.body.scrollLeft),
+        width: rect.width,
+        height: rect.height,
+      },
+    });
   }
   }
 
 
   closeTooltip() {
   closeTooltip() {
@@ -37,10 +73,7 @@ class TooltipEntity extends Component {
 
 
   render() {
   render() {
     const {
     const {
-      entityKey,
       children,
       children,
-      onEdit,
-      onRemove,
       icon,
       icon,
       label,
       label,
       url,
       url,
@@ -50,11 +83,18 @@ class TooltipEntity extends Component {
     // Contrary to what JSX A11Y says, this should be a button but it shouldn't be focusable.
     // Contrary to what JSX A11Y says, this should be a button but it shouldn't be focusable.
     /* eslint-disable springload/jsx-a11y/interactive-supports-focus */
     /* eslint-disable springload/jsx-a11y/interactive-supports-focus */
     return (
     return (
-      <a role="button" onMouseUp={this.openTooltip} className="TooltipEntity">
+      <a
+        role="button"
+        // Use onMouseUp to preserve focus in the text even after clicking.
+        onMouseUp={this.openTooltip}
+        className="TooltipEntity"
+        data-draftail-trigger
+      >
         <Icon icon={icon} className="TooltipEntity__icon" />
         <Icon icon={icon} className="TooltipEntity__icon" />
         {children}
         {children}
         {showTooltipAt && (
         {showTooltipAt && (
           <Portal
           <Portal
+            node={showTooltipAt.container}
             onClose={this.closeTooltip}
             onClose={this.closeTooltip}
             closeOnClick
             closeOnClick
             closeOnType
             closeOnType
@@ -75,14 +115,14 @@ class TooltipEntity extends Component {
 
 
               <button
               <button
                 className="button Tooltip__button"
                 className="button Tooltip__button"
-                onClick={onEdit.bind(null, entityKey)}
+                onClick={this.onEdit}
               >
               >
                 Edit
                 Edit
               </button>
               </button>
 
 
               <button
               <button
                 className="button button-secondary no Tooltip__button"
                 className="button button-secondary no Tooltip__button"
-                onClick={onRemove.bind(null, entityKey)}
+                onClick={this.onRemove}
               >
               >
                 Remove
                 Remove
               </button>
               </button>

+ 49 - 2
client/src/components/Draftail/decorators/TooltipEntity.test.js

@@ -1,5 +1,5 @@
 import React from 'react';
 import React from 'react';
-import { shallow } from 'enzyme';
+import { shallow, mount } from 'enzyme';
 
 
 import TooltipEntity from './TooltipEntity';
 import TooltipEntity from './TooltipEntity';
 
 
@@ -51,8 +51,13 @@ describe('TooltipEntity', () => {
       </TooltipEntity>
       </TooltipEntity>
     ));
     ));
 
 
+    const target = document.createElement('div');
+    target.setAttribute('data-draftail-trigger', true);
+    document.body.appendChild(target);
+    document.body.setAttribute('data-draftail-editor-wrapper', true);
+
     wrapper.find('.TooltipEntity').simulate('mouseup', {
     wrapper.find('.TooltipEntity').simulate('mouseup', {
-      target: document.createElement('div'),
+      target: target,
     });
     });
 
 
     expect(wrapper).toMatchSnapshot();
     expect(wrapper).toMatchSnapshot();
@@ -82,4 +87,46 @@ describe('TooltipEntity', () => {
       showTooltipAt: null,
       showTooltipAt: null,
     });
     });
   });
   });
+
+  it('#onEdit', () => {
+    const onEdit = jest.fn();
+
+    const wrapper = shallow((
+      <TooltipEntity
+        entityKey="1"
+        onEdit={onEdit}
+        onRemove={() => {}}
+        icon="#icon-test"
+        url="https://www.example.com/"
+        label="www.example.com"
+      >
+        test
+      </TooltipEntity>
+    ));
+
+    wrapper.instance().onEdit(new Event('click'));
+
+    expect(onEdit).toHaveBeenCalled();
+  });
+
+  it('#onRemove', () => {
+    const onRemove = jest.fn();
+
+    const wrapper = shallow((
+      <TooltipEntity
+        entityKey="1"
+        onEdit={() => {}}
+        onRemove={onRemove}
+        icon="#icon-test"
+        url="https://www.example.com/"
+        label="www.example.com"
+      >
+        test
+      </TooltipEntity>
+    ));
+
+    wrapper.instance().onRemove(new Event('click'));
+
+    expect(onRemove).toHaveBeenCalled();
+  });
 });
 });

+ 18 - 2
client/src/components/Draftail/decorators/__snapshots__/TooltipEntity.test.js.snap

@@ -3,6 +3,7 @@
 exports[`TooltipEntity #openTooltip 1`] = `
 exports[`TooltipEntity #openTooltip 1`] = `
 <a
 <a
   className="TooltipEntity"
   className="TooltipEntity"
+  data-draftail-trigger={true}
   onMouseUp={[Function]}
   onMouseUp={[Function]}
   role="button"
   role="button"
 >
 >
@@ -16,16 +17,30 @@ exports[`TooltipEntity #openTooltip 1`] = `
     closeOnClick={true}
     closeOnClick={true}
     closeOnResize={true}
     closeOnResize={true}
     closeOnType={true}
     closeOnType={true}
+    node={
+      <body
+        data-draftail-editor-wrapper="true"
+      >
+        <div
+          data-draftail-trigger="true"
+        />
+      </body>
+    }
     onClose={[Function]}
     onClose={[Function]}
   >
   >
     <Tooltip
     <Tooltip
       direction="top"
       direction="top"
       target={
       target={
         Object {
         Object {
-          "bottom": 0,
+          "container": <body
+            data-draftail-editor-wrapper="true"
+          >
+            <div
+              data-draftail-trigger="true"
+            />
+          </body>,
           "height": 0,
           "height": 0,
           "left": 0,
           "left": 0,
-          "right": 0,
           "top": 0,
           "top": 0,
           "width": 0,
           "width": 0,
         }
         }
@@ -60,6 +75,7 @@ exports[`TooltipEntity #openTooltip 1`] = `
 exports[`TooltipEntity works 1`] = `
 exports[`TooltipEntity works 1`] = `
 <a
 <a
   className="TooltipEntity"
   className="TooltipEntity"
+  data-draftail-trigger={true}
   onMouseUp={[Function]}
   onMouseUp={[Function]}
   role="button"
   role="button"
 >
 >

+ 4 - 0
client/src/components/Draftail/index.js

@@ -45,7 +45,11 @@ export const wrapWagtailIcon = type => {
  */
  */
 const initEditor = (fieldName, options) => {
 const initEditor = (fieldName, options) => {
   const field = document.querySelector(`[name="${fieldName}"]`);
   const field = document.querySelector(`[name="${fieldName}"]`);
+
   const editorWrapper = document.createElement('div');
   const editorWrapper = document.createElement('div');
+  editorWrapper.className = 'Draftail-Editor__wrapper';
+  editorWrapper.setAttribute('data-draftail-editor-wrapper', true);
+
   field.parentNode.appendChild(editorWrapper);
   field.parentNode.appendChild(editorWrapper);
 
 
   const serialiseInputValue = rawContentState => {
   const serialiseInputValue = rawContentState => {

+ 28 - 31
client/src/components/Portal/Portal.js

@@ -1,11 +1,18 @@
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import ReactDOM from 'react-dom';
-
+import { Component } from 'react';
+import { createPortal } from 'react-dom';
+
+/**
+ * A Portal component which automatically closes itself
+ * when certain events happen outside.
+ * See https://reactjs.org/docs/portals.html.
+ */
 class Portal extends Component {
 class Portal extends Component {
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
+    this.portal = document.createElement('div');
+
     this.onCloseEvent = this.onCloseEvent.bind(this);
     this.onCloseEvent = this.onCloseEvent.bind(this);
   }
   }
 
 
@@ -18,40 +25,27 @@ class Portal extends Component {
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
-    const { onClose, closeOnClick, closeOnType, closeOnResize } = this.props;
-
-    if (!this.portal) {
-      this.portal = document.createElement('div');
-      document.body.appendChild(this.portal);
+    const { node, onClose, closeOnClick, closeOnType, closeOnResize } = this.props;
 
 
-      if (onClose) {
-        if (closeOnClick) {
-          document.addEventListener('mouseup', this.onCloseEvent);
-        }
+    node.appendChild(this.portal);
 
 
-        if (closeOnType) {
-          document.addEventListener('keyup', this.onCloseEvent);
-        }
-
-        if (closeOnResize) {
-          window.addEventListener('resize', onClose);
-        }
-      }
+    if (closeOnClick) {
+      document.addEventListener('mouseup', this.onCloseEvent);
     }
     }
 
 
-    this.componentDidUpdate();
-  }
-
-  componentDidUpdate() {
-    const { children } = this.props;
+    if (closeOnType) {
+      document.addEventListener('keyup', this.onCloseEvent);
+    }
 
 
-    ReactDOM.render(<div>{children}</div>, this.portal);
+    if (closeOnResize) {
+      window.addEventListener('resize', onClose);
+    }
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
-    const { onClose } = this.props;
+    const { node, onClose } = this.props;
 
 
-    document.body.removeChild(this.portal);
+    node.removeChild(this.portal);
 
 
     document.removeEventListener('mouseup', this.onCloseEvent);
     document.removeEventListener('mouseup', this.onCloseEvent);
     document.removeEventListener('keyup', this.onCloseEvent);
     document.removeEventListener('keyup', this.onCloseEvent);
@@ -59,12 +53,15 @@ class Portal extends Component {
   }
   }
 
 
   render() {
   render() {
-    return null;
+    const { children } = this.props;
+
+    return createPortal(children, this.portal);
   }
   }
 }
 }
 
 
 Portal.propTypes = {
 Portal.propTypes = {
-  onClose: PropTypes.func,
+  onClose: PropTypes.func.isRequired,
+  node: PropTypes.instanceOf(Element),
   children: PropTypes.node,
   children: PropTypes.node,
   closeOnClick: PropTypes.bool,
   closeOnClick: PropTypes.bool,
   closeOnType: PropTypes.bool,
   closeOnType: PropTypes.bool,
@@ -72,7 +69,7 @@ Portal.propTypes = {
 };
 };
 
 
 Portal.defaultProps = {
 Portal.defaultProps = {
-  onClose: null,
+  node: document.body,
   children: null,
   children: null,
   closeOnClick: false,
   closeOnClick: false,
   closeOnType: false,
   closeOnType: false,

+ 9 - 15
client/src/components/Portal/Portal.test.js

@@ -1,5 +1,5 @@
 import React from 'react';
 import React from 'react';
-import { shallow } from 'enzyme';
+import { shallow, mount } from 'enzyme';
 import Portal from './Portal';
 import Portal from './Portal';
 
 
 const func = expect.any(Function);
 const func = expect.any(Function);
@@ -10,27 +10,21 @@ describe('Portal', () => {
   });
   });
 
 
   it('empty', () => {
   it('empty', () => {
-    expect(shallow(<Portal />)).toMatchSnapshot();
+    expect(mount(<Portal onClose={() => {}} />)).toMatchSnapshot();
   });
   });
 
 
   it('#children', () => {
   it('#children', () => {
-    expect(shallow(<Portal>Test!</Portal>)).toMatchSnapshot();
+    expect(mount(<Portal onClose={() => {}}>Test!</Portal>)).toMatchSnapshot();
   });
   });
 
 
   it('component lifecycle', () => {
   it('component lifecycle', () => {
     document.removeEventListener = jest.fn();
     document.removeEventListener = jest.fn();
     window.removeEventListener = jest.fn();
     window.removeEventListener = jest.fn();
 
 
-    const wrapper = shallow(<Portal onClose={() => {}}>Test!</Portal>);
-
-    wrapper.instance().componentDidMount();
+    const wrapper = mount(<Portal onClose={() => {}}>Test!</Portal>);
 
 
     expect(document.body.innerHTML).toMatchSnapshot();
     expect(document.body.innerHTML).toMatchSnapshot();
 
 
-    expect(wrapper.instance().portal).toBe(document.body.children[0]);
-
-    wrapper.instance().componentDidMount();
-
     wrapper.instance().componentWillUnmount();
     wrapper.instance().componentWillUnmount();
 
 
     expect(document.body.innerHTML).toBe('');
     expect(document.body.innerHTML).toBe('');
@@ -81,14 +75,14 @@ describe('Portal', () => {
           Test!
           Test!
         </Portal>
         </Portal>
       );
       );
-      expect(window.addEventListener).toHaveBeenCalledWith('error', func);
+      expect(window.addEventListener).toHaveBeenCalledWith('resize', func);
     });
     });
   });
   });
 
 
   describe('onCloseEvent', () => {
   describe('onCloseEvent', () => {
-    it('shouldClose', () => {
+    it('should close', () => {
       const onClose = jest.fn();
       const onClose = jest.fn();
-      const wrapper = shallow(<Portal onClose={onClose}>Test!</Portal>);
+      const wrapper = mount(<Portal onClose={onClose}>Test!</Portal>);
       const target = document.createElement('div');
       const target = document.createElement('div');
 
 
       wrapper.instance().onCloseEvent({ target });
       wrapper.instance().onCloseEvent({ target });
@@ -96,9 +90,9 @@ describe('Portal', () => {
       expect(onClose).toHaveBeenCalled();
       expect(onClose).toHaveBeenCalled();
     });
     });
 
 
-    it('not shouldClose', () => {
+    it('not should close', () => {
       const onClose = jest.fn();
       const onClose = jest.fn();
-      const wrapper = shallow(
+      const wrapper = mount(
         <Portal onClose={onClose}>
         <Portal onClose={onClose}>
           <div id="test">Test</div>
           <div id="test">Test</div>
         </Portal>
         </Portal>

+ 31 - 3
client/src/components/Portal/__snapshots__/Portal.test.js.snap

@@ -1,7 +1,35 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 
-exports[`Portal #children 1`] = `""`;
+exports[`Portal #children 1`] = `
+<Portal
+  closeOnClick={false}
+  closeOnResize={false}
+  closeOnType={false}
+  node={
+    <body>
+      <div>
+        Test!
+      </div>
+    </body>
+  }
+  onClose={[Function]}
+>
+  Test!
+</Portal>
+`;
 
 
-exports[`Portal component lifecycle 1`] = `"<div><div>Test!</div></div>"`;
+exports[`Portal component lifecycle 1`] = `"<div>Test!</div>"`;
 
 
-exports[`Portal empty 1`] = `""`;
+exports[`Portal empty 1`] = `
+<Portal
+  closeOnClick={false}
+  closeOnResize={false}
+  closeOnType={false}
+  node={
+    <body>
+      <div />
+    </body>
+  }
+  onClose={[Function]}
+/>
+`;

+ 4 - 0
client/src/utils/polyfills.js

@@ -2,5 +2,9 @@
  * Polyfills for Wagtail's admin.
  * Polyfills for Wagtail's admin.
  */
  */
 
 
+// IE11.
 import 'core-js/shim';
 import 'core-js/shim';
+// IE11, old iOS Safari.
 import 'whatwg-fetch';
 import 'whatwg-fetch';
+// IE11.
+import 'element-closest';

+ 1 - 0
client/tests/stubs.js

@@ -3,6 +3,7 @@
  * Those variables usually come from the back-end via templates.
  * Those variables usually come from the back-end via templates.
  * See /wagtailadmin/templates/wagtailadmin/admin_base.html.
  * See /wagtailadmin/templates/wagtailadmin/admin_base.html.
  */
  */
+import 'element-closest';
 
 
 global.wagtailConfig = {
 global.wagtailConfig = {
   ADMIN_API: {
   ADMIN_API: {

+ 5 - 0
package-lock.json

@@ -2688,6 +2688,11 @@
       "integrity": "sha1-elgja5VGjD52YAkTSFItZddzazY=",
       "integrity": "sha1-elgja5VGjD52YAkTSFItZddzazY=",
       "dev": true
       "dev": true
     },
     },
+    "element-closest": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/element-closest/-/element-closest-2.0.2.tgz",
+      "integrity": "sha1-cqdAoQdFM4LijfnOXbtajfD5Zuw="
+    },
     "elliptic": {
     "elliptic": {
       "version": "6.4.0",
       "version": "6.4.0",
       "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz",
       "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz",

+ 1 - 0
package.json

@@ -88,6 +88,7 @@
     "core-js": "^2.5.3",
     "core-js": "^2.5.3",
     "draft-js": "0.10.5",
     "draft-js": "0.10.5",
     "draftail": "^0.16.0",
     "draftail": "^0.16.0",
+    "element-closest": "^2.0.2",
     "focus-trap-react": "^3.1.0",
     "focus-trap-react": "^3.1.0",
     "prop-types": "^15.6.0",
     "prop-types": "^15.6.0",
     "react": "^16.2.0",
     "react": "^16.2.0",