Browse Source

Layout the popup after initial render.

Tomek Wójcik 4 years ago
parent
commit
10a56cfbed
4 changed files with 239 additions and 157 deletions
  1. 0 1
      example/example.js
  2. 1 1
      package.json
  3. 63 44
      src/Popup.js
  4. 175 111
      tests/Popup.spec.js

+ 0 - 1
example/example.js

@@ -40,7 +40,6 @@ class App extends React.Component {
     this.setState({popupVisible: false});
   }
   onPopupLayout (currentLayout) {
-
     return [
       (window.innerWidth - CUSTOM_DIMENSION) / 2,
       (window.innerHeight - CUSTOM_DIMENSION) / 2,

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "@bthlabs/react-custom-popup",
-  "version": "1.0.2",
+  "version": "1.0.3",
   "private": false,
   "description": "React component for simply building custom popups and modals.",
   "main": "./lib/react-custom-popup.js",

+ 63 - 44
src/Popup.js

@@ -25,61 +25,80 @@ import PropTypes from 'prop-types';
 import React from 'react';
 import ReactDOM from 'react-dom';
 
-export const Popup = (props) => {
-  const className = ClassName('bthlabs-react-custom-popup', props.className, {
-    'bthlabs-rcp-visible': props.visible
-  });
+export class Popup extends React.Component {
+  constructor (props) {
+    super(props);
 
-  let layout = [0, 0];
-  const body = document.body;
-  const innerStyle = {};
-  if (props.visible && props.anchor && props.anchor.current) {
-    const box = props.anchor.current.getBoundingClientRect();
-    const document_ = document.documentElement;
-
-    const scrollTop = (
-      window.pageYOffset || document_.scrollTop || body.scrollTop
-    );
-    const scrollLeft = (
-      window.pageXOffset || document_.scrollLeft || body.scrollLeft
-    );
-
-    const clientTop = document_.clientTop || body.clientTop || 0;
-    const clientLeft = document_.clientLeft || body.clientLeft || 0;
-
-    layout[0] = box.left + scrollLeft - clientLeft;
-    layout[1] = box.top +  scrollTop - clientTop;
+    this.state = {
+      layout: [0, 0]
+    };
   }
-
-  if (props.visible) {
-    if (typeof props.onLayout === 'function') {
-      layout = props.onLayout(layout);
+  componentDidUpdate (prevProps, prevState) {
+    if (prevProps.visible !== this.props.visible && this.props.visible) {
+      this.layout();
     }
+  }
+  layout () {
+    let layout = [0, 0];
+    const body = document.body;
+    if (this.props.visible && this.props.anchor && this.props.anchor.current) {
+      const box = this.props.anchor.current.getBoundingClientRect();
+      const document_ = document.documentElement;
 
-    innerStyle.left = `${layout[0]}px`;
-    innerStyle.top = `${layout[1]}px`;
+      const scrollTop = (
+        window.pageYOffset || document_.scrollTop || body.scrollTop
+      );
+      const scrollLeft = (
+        window.pageXOffset || document_.scrollLeft || body.scrollLeft
+      );
 
-    if (layout[2]) {
-      innerStyle.width = `${layout[2]}px`;
+      const clientTop = document_.clientTop || body.clientTop || 0;
+      const clientLeft = document_.clientLeft || body.clientLeft || 0;
+
+      layout[0] = box.left + scrollLeft - clientLeft;
+      layout[1] = box.top +  scrollTop - clientTop;
     }
 
-    if (layout[3]) {
-      innerStyle.height = `${layout[3]}px`;
+    if (this.props.visible) {
+      if (typeof this.props.onLayout === 'function') {
+        layout = this.props.onLayout(layout);
+      }
+
+      this.setState({layout: layout});
     }
   }
+  render () {
+    const className = ClassName('bthlabs-react-custom-popup', this.props.className, {
+      'bthlabs-rcp-visible': this.props.visible
+    });
+
+    const innerStyle = {};
+    if (this.props.visible) {
+      innerStyle.left = `${this.state.layout[0]}px`;
+      innerStyle.top = `${this.state.layout[1]}px`;
 
-  return ReactDOM.createPortal(
-    <div className={className}>
-      {!props.hideOverlay &&
-        <div className="bthlabs-rcp-overlay" onClick={props.onOverlayClick} />
+      if (this.state.layout[2]) {
+        innerStyle.width = `${this.state.layout[2]}px`;
       }
-      <div className="bthlabs-rcp-inner" style={innerStyle}>
-        {props.children}
-      </div>
-    </div>,
-    props.domNode || body
-  );
-};
+
+      if (this.state.layout[3]) {
+        innerStyle.height = `${this.state.layout[3]}px`;
+      }
+    }
+
+    return ReactDOM.createPortal(
+      <div className={className}>
+        {!this.props.hideOverlay &&
+          <div className="bthlabs-rcp-overlay" onClick={this.props.onOverlayClick} />
+        }
+        <div className="bthlabs-rcp-inner" style={innerStyle}>
+          {this.props.children}
+        </div>
+      </div>,
+      this.props.domNode || document.body
+    );
+  }
+}
 
 Popup.propTypes = {
   anchor: PropTypes.object,

+ 175 - 111
tests/Popup.spec.js

@@ -48,144 +48,208 @@ describe('Popup', () => {
     document.body.removeChild(mountNode);
   });
 
-  it('should render with a custom class', () => {
-    const component = shallow(
-      <Popup className='spam' visible={false}>
-        <span>HERE POPUP CONTENT BE</span>
-      </Popup>
-    );
+  describe('componentDidUpdate', () => {
+    it('should not layout if visibility did not change', () => {
+      const component = shallow(
+        <Popup className='spam' visible={false}>
+          <span>HERE POPUP CONTENT BE</span>
+        </Popup>
+      );
+      spyOn(component.instance(), 'layout');
 
-    const popup = component.find('.bthlabs-react-custom-popup');
-    expect(popup.hasClass('spam')).toBe(true);
-  });
+      component.instance().componentDidUpdate({visible: false});
+      expect(component.instance().layout).not.toHaveBeenCalled();
+    });
 
-  it('should render as invisible', () => {
-    const component = shallow(
-      <Popup className='spam' visible={false}>
-        <span>HERE POPUP CONTENT BE</span>
-      </Popup>
-    );
+    it('should not layout if it became hidden', () => {
+      const component = shallow(
+        <Popup className='spam' visible={false}>
+          <span>HERE POPUP CONTENT BE</span>
+        </Popup>
+      );
+      spyOn(component.instance(), 'layout');
 
-    const popup = component.find('.bthlabs-react-custom-popup');
-    expect(popup.hasClass('bthlabs-rcp-visible')).toBe(false);
-  });
+      component.instance().componentDidUpdate({visible: true});
+      expect(component.instance().layout).not.toHaveBeenCalled();
+    });
 
-  it('should render as visible', () => {
-    const component = shallow(
-      <Popup className='spam' visible={true}>
-        <span>HERE POPUP CONTENT BE</span>
-      </Popup>
-    );
+    it('should not layout if it became visible', () => {
+      const component = shallow(
+        <Popup className='spam' visible={true}>
+          <span>HERE POPUP CONTENT BE</span>
+        </Popup>
+      );
+      spyOn(component.instance(), 'layout');
 
-    const popup = component.find('.bthlabs-react-custom-popup');
-    expect(popup.hasClass('bthlabs-rcp-visible')).toBe(true);
+      component.instance().componentDidUpdate({visible: false});
+      expect(component.instance().layout).toHaveBeenCalled();
+    });
   });
 
-  it('should allow hiding the overlay', () => {
-    const component = shallow(
-      <Popup hideOverlay={true} visible={false}>
-        <span>HERE POPUP CONTENT BE</span>
-      </Popup>
-    );
+  describe('layout', () => {
+    it('should apply default layout if rendering without anchor', () => {
+      const component = shallow(
+        <Popup visible={true}>
+          <span>HERE POPUP CONTENT BE</span>
+        </Popup>
+      );
+      component.instance().layout();
 
-    const overlay = component.find('.bthlabs-rcp-overlay');
-    expect(overlay.exists()).toBe(false);
-  });
+      expect(component.state('layout')).toEqual([0, 0]);
+    });
 
-  it('should configure and render the overlay', () => {
-    const component = shallow(
-      <Popup visible={false} onOverlayClick={onOverlayClick}>
-        <span>HERE POPUP CONTENT BE</span>
-      </Popup>
-    );
+    it('should not call onLayout when invisible', () => {
+      const component = shallow(
+        <Popup visible={false} onLayout={onLayout}>
+          <span>HERE POPUP CONTENT BE</span>
+        </Popup>
+      );
+      component.instance().layout();
 
-    const overlay = component.find('.bthlabs-rcp-overlay');
-    expect(overlay.exists()).toBe(true);
-    expect(overlay.prop('onClick')).toBe(onOverlayClick);
-  });
+      expect(onLayout).not.toHaveBeenCalled();
+    });
 
-  it('should not layout the inner layer when invisible', () => {
-    const component = shallow(
-      <Popup visible={false}>
-        <span>HERE POPUP CONTENT BE</span>
-      </Popup>
-    );
+    it('should layout according to the anchor', () => {
+      const component = mount(<Wrapper />, {attachTo: mountNode});
+      component.setState({popupVisible: true});
 
-    const inner = component.find('.bthlabs-rcp-inner');
-    expect(inner.prop('style')).toEqual({});
-  });
+      const popup = component.find(Popup);
+      expect(popup.instance().state.layout).not.toEqual([0, 0]);
 
-  it('should not call onLayout when invisible', () => {
-    const component = shallow(
-      <Popup visible={false} onLayout={onLayout}>
-        <span>HERE POPUP CONTENT BE</span>
-      </Popup>
-    );
+      component.unmount();
+    });
 
-    const inner = component.find('.bthlabs-rcp-inner');
-    expect(onLayout).not.toHaveBeenCalled();
-  });
+    it('should call onLayout to allow for customization of the inner layer layout', () => {
+      onLayout = onLayout.and.returnValue([20, 20]);
 
-  it('should apply default layout to the inner layer if rendering without anchor', () => {
-    const component = shallow(
-      <Popup visible={true}>
-        <span>HERE POPUP CONTENT BE</span>
-      </Popup>
-    );
+      const component = shallow(
+        <Popup visible={true} onLayout={onLayout}>
+          <span>HERE POPUP CONTENT BE</span>
+        </Popup>
+      );
+      component.instance().layout();
 
-    const inner = component.find('.bthlabs-rcp-inner');
-    expect(inner.prop('style')).toEqual({left: '0px', top: '0px'});
-  });
+      expect(onLayout).toHaveBeenCalledWith([0, 0]);
+      expect(component.state('layout')).toEqual([20, 20]);
+    });
 
-  it('should layout the inner layer according to the anchor', () => {
-    const component = mount(<Wrapper />, {attachTo: mountNode});
-    component.setState({popupVisible: true});
+    it('should allow the onLayout callback to specify height and width', () => {
+      onLayout = onLayout.and.returnValue([20, 20, 100, 100]);
 
-    const popup = component.find(Popup);
-    const inner = popup.find('.bthlabs-rcp-inner');
-    expect(inner.prop('style').left).not.toEqual('0px');
-    expect(inner.prop('style').top).not.toEqual('0px');
+      const component = shallow(
+        <Popup visible={true} onLayout={onLayout}>
+          <span>HERE POPUP CONTENT BE</span>
+        </Popup>
+      );
+      component.instance().layout();
 
-    component.unmount();
+      expect(component.state('layout')).toEqual([20, 20, 100, 100]);
+    });
   });
 
-  it('should call onLayout to allow for customization of the inner layer layout', () => {
-    onLayout = onLayout.and.returnValue([20, 20]);
+  describe('render', () => {
+    it('should render with a custom class', () => {
+      const component = shallow(
+        <Popup className='spam' visible={false}>
+          <span>HERE POPUP CONTENT BE</span>
+        </Popup>
+      );
+
+      const popup = component.find('.bthlabs-react-custom-popup');
+      expect(popup.hasClass('spam')).toBe(true);
+    });
 
-    const component = shallow(
-      <Popup visible={true} onLayout={onLayout}>
-        <span>HERE POPUP CONTENT BE</span>
-      </Popup>
-    );
+    it('should render as invisible', () => {
+      const component = shallow(
+        <Popup className='spam' visible={false}>
+          <span>HERE POPUP CONTENT BE</span>
+        </Popup>
+      );
 
-    const inner = component.find('.bthlabs-rcp-inner');
-    expect(onLayout).toHaveBeenCalledWith([0, 0]);
-    expect(inner.prop('style')).toEqual({left: '20px', top: '20px'});
-  });
+      const popup = component.find('.bthlabs-react-custom-popup');
+      expect(popup.hasClass('bthlabs-rcp-visible')).toBe(false);
+    });
 
-  it('should set the inner layour height and width if onLayout specified them', () => {
-    onLayout = onLayout.and.returnValue([20, 20, 100, 100]);
+    it('should render as visible', () => {
+      const component = shallow(
+        <Popup className='spam' visible={true}>
+          <span>HERE POPUP CONTENT BE</span>
+        </Popup>
+      );
 
-    const component = shallow(
-      <Popup visible={true} onLayout={onLayout}>
-        <span>HERE POPUP CONTENT BE</span>
-      </Popup>
-    );
+      const popup = component.find('.bthlabs-react-custom-popup');
+      expect(popup.hasClass('bthlabs-rcp-visible')).toBe(true);
+    });
 
-    const inner = component.find('.bthlabs-rcp-inner');
-    expect(onLayout).toHaveBeenCalledWith([0, 0]);
-    expect(inner.prop('style').height).toEqual('100px');
-    expect(inner.prop('style').width).toEqual('100px');
-  });
+    it('should allow hiding the overlay', () => {
+      const component = shallow(
+        <Popup hideOverlay={true} visible={false}>
+          <span>HERE POPUP CONTENT BE</span>
+        </Popup>
+      );
 
-  it('should render the children in the inner layer', () => {
-    const component = shallow(
-      <Popup visible={false}>
-        <span>HERE POPUP CONTENT BE</span>
-      </Popup>
-    );
+      const overlay = component.find('.bthlabs-rcp-overlay');
+      expect(overlay.exists()).toBe(false);
+    });
+
+    it('should configure and render the overlay', () => {
+      const component = shallow(
+        <Popup visible={false} onOverlayClick={onOverlayClick}>
+          <span>HERE POPUP CONTENT BE</span>
+        </Popup>
+      );
+
+      const overlay = component.find('.bthlabs-rcp-overlay');
+      expect(overlay.exists()).toBe(true);
+      expect(overlay.prop('onClick')).toBe(onOverlayClick);
+    });
+
+    it('should not apply layout style on the inner layer when invisible', () => {
+      const component = shallow(
+        <Popup visible={false}>
+          <span>HERE POPUP CONTENT BE</span>
+        </Popup>
+      );
+
+      const inner = component.find('.bthlabs-rcp-inner');
+      expect(inner.prop('style')).toEqual({});
+    });
+
+    it('should apply layout style on the inner layer when visible', () => {
+      const component = shallow(
+        <Popup visible={true}>
+          <span>HERE POPUP CONTENT BE</span>
+        </Popup>
+      );
+      component.setState({layout: [20, 20]});
+
+      const inner = component.find('.bthlabs-rcp-inner');
+      expect(inner.prop('style').left).toEqual('20px');
+      expect(inner.prop('style').top).toEqual('20px');
+    });
+
+    it('should set the inner layer height and width if onLayout specified them', () => {
+      const component = shallow(
+        <Popup visible={true} onLayout={onLayout}>
+          <span>HERE POPUP CONTENT BE</span>
+        </Popup>
+      );
+      component.setState({layout: [20, 20, 100, 100]});
+
+      const inner = component.find('.bthlabs-rcp-inner');
+      expect(inner.prop('style').height).toEqual('100px');
+      expect(inner.prop('style').width).toEqual('100px');
+    });
+
+    it('should render the children in the inner layer', () => {
+      const component = shallow(
+        <Popup visible={false}>
+          <span>HERE POPUP CONTENT BE</span>
+        </Popup>
+      );
 
-    const inner = component.find('.bthlabs-rcp-inner');
-    expect(inner.contains(<span>HERE POPUP CONTENT BE</span>)).toBe(true);
+      const inner = component.find('.bthlabs-rcp-inner');
+      expect(inner.contains(<span>HERE POPUP CONTENT BE</span>)).toBe(true);
+    });
   });
 });