From 10a56cfbeda3e4befd41b69755815669af2a2f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomek=20W=C3=B3jcik?= Date: Sat, 6 Apr 2019 17:55:15 +0200 Subject: [PATCH] Layout the popup after initial render. --- example/example.js | 1 - package.json | 2 +- src/Popup.js | 117 +++++++++-------- tests/Popup.spec.js | 296 +++++++++++++++++++++++++++----------------- 4 files changed, 249 insertions(+), 167 deletions(-) diff --git a/example/example.js b/example/example.js index 3b980bd..7f73fce 100644 --- a/example/example.js +++ b/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, diff --git a/package.json b/package.json index 639036d..78395ae 100644 --- a/package.json +++ b/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", diff --git a/src/Popup.js b/src/Popup.js index e46289c..2a15090 100644 --- a/src/Popup.js +++ b/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); - } - - innerStyle.left = `${layout[0]}px`; - innerStyle.top = `${layout[1]}px`; - - if (layout[2]) { - innerStyle.width = `${layout[2]}px`; - } - - if (layout[3]) { - innerStyle.height = `${layout[3]}px`; + 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; - return ReactDOM.createPortal( -
- {!props.hideOverlay && -
+ 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; + } + + if (this.props.visible) { + if (typeof this.props.onLayout === 'function') { + layout = this.props.onLayout(layout); } -
- {props.children} -
-
, - props.domNode || body - ); -}; + + 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`; + + if (this.state.layout[2]) { + innerStyle.width = `${this.state.layout[2]}px`; + } + + if (this.state.layout[3]) { + innerStyle.height = `${this.state.layout[3]}px`; + } + } + + return ReactDOM.createPortal( +
+ {!this.props.hideOverlay && +
+ } +
+ {this.props.children} +
+
, + this.props.domNode || document.body + ); + } +} Popup.propTypes = { anchor: PropTypes.object, diff --git a/tests/Popup.spec.js b/tests/Popup.spec.js index fa3f4a0..6fb9928 100644 --- a/tests/Popup.spec.js +++ b/tests/Popup.spec.js @@ -48,144 +48,208 @@ describe('Popup', () => { document.body.removeChild(mountNode); }); - it('should render with a custom class', () => { - const component = shallow( - - HERE POPUP CONTENT BE - - ); + describe('componentDidUpdate', () => { + it('should not layout if visibility did not change', () => { + const component = shallow( + + HERE POPUP CONTENT BE + + ); + 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 not layout if it became hidden', () => { + const component = shallow( + + HERE POPUP CONTENT BE + + ); + spyOn(component.instance(), 'layout'); + + component.instance().componentDidUpdate({visible: true}); + expect(component.instance().layout).not.toHaveBeenCalled(); + }); + + it('should not layout if it became visible', () => { + const component = shallow( + + HERE POPUP CONTENT BE + + ); + spyOn(component.instance(), 'layout'); + + component.instance().componentDidUpdate({visible: false}); + expect(component.instance().layout).toHaveBeenCalled(); + }); }); - it('should render as invisible', () => { - const component = shallow( - - HERE POPUP CONTENT BE - - ); + describe('layout', () => { + it('should apply default layout if rendering without anchor', () => { + const component = shallow( + + HERE POPUP CONTENT BE + + ); + component.instance().layout(); - const popup = component.find('.bthlabs-react-custom-popup'); - expect(popup.hasClass('bthlabs-rcp-visible')).toBe(false); + expect(component.state('layout')).toEqual([0, 0]); + }); + + it('should not call onLayout when invisible', () => { + const component = shallow( + + HERE POPUP CONTENT BE + + ); + component.instance().layout(); + + expect(onLayout).not.toHaveBeenCalled(); + }); + + it('should layout according to the anchor', () => { + const component = mount(, {attachTo: mountNode}); + component.setState({popupVisible: true}); + + const popup = component.find(Popup); + expect(popup.instance().state.layout).not.toEqual([0, 0]); + + component.unmount(); + }); + + it('should call onLayout to allow for customization of the inner layer layout', () => { + onLayout = onLayout.and.returnValue([20, 20]); + + const component = shallow( + + HERE POPUP CONTENT BE + + ); + component.instance().layout(); + + expect(onLayout).toHaveBeenCalledWith([0, 0]); + expect(component.state('layout')).toEqual([20, 20]); + }); + + it('should allow the onLayout callback to specify height and width', () => { + onLayout = onLayout.and.returnValue([20, 20, 100, 100]); + + const component = shallow( + + HERE POPUP CONTENT BE + + ); + component.instance().layout(); + + expect(component.state('layout')).toEqual([20, 20, 100, 100]); + }); }); - it('should render as visible', () => { - const component = shallow( - - HERE POPUP CONTENT BE - - ); + describe('render', () => { + it('should render with a custom class', () => { + const component = shallow( + + HERE POPUP CONTENT BE + + ); - const popup = component.find('.bthlabs-react-custom-popup'); - expect(popup.hasClass('bthlabs-rcp-visible')).toBe(true); - }); + const popup = component.find('.bthlabs-react-custom-popup'); + expect(popup.hasClass('spam')).toBe(true); + }); - it('should allow hiding the overlay', () => { - const component = shallow( - - HERE POPUP CONTENT BE - - ); + it('should render as invisible', () => { + const component = shallow( + + HERE POPUP CONTENT BE + + ); - const overlay = component.find('.bthlabs-rcp-overlay'); - expect(overlay.exists()).toBe(false); - }); + const popup = component.find('.bthlabs-react-custom-popup'); + expect(popup.hasClass('bthlabs-rcp-visible')).toBe(false); + }); - it('should configure and render the overlay', () => { - const component = shallow( - - HERE POPUP CONTENT BE - - ); + it('should render as visible', () => { + const component = shallow( + + HERE POPUP CONTENT BE + + ); - const overlay = component.find('.bthlabs-rcp-overlay'); - expect(overlay.exists()).toBe(true); - expect(overlay.prop('onClick')).toBe(onOverlayClick); - }); + const popup = component.find('.bthlabs-react-custom-popup'); + expect(popup.hasClass('bthlabs-rcp-visible')).toBe(true); + }); - it('should not layout the inner layer when invisible', () => { - const component = shallow( - - HERE POPUP CONTENT BE - - ); + it('should allow hiding the overlay', () => { + const component = shallow( + + HERE POPUP CONTENT BE + + ); - const inner = component.find('.bthlabs-rcp-inner'); - expect(inner.prop('style')).toEqual({}); - }); + const overlay = component.find('.bthlabs-rcp-overlay'); + expect(overlay.exists()).toBe(false); + }); - it('should not call onLayout when invisible', () => { - const component = shallow( - - HERE POPUP CONTENT BE - - ); + it('should configure and render the overlay', () => { + const component = shallow( + + HERE POPUP CONTENT BE + + ); - const inner = component.find('.bthlabs-rcp-inner'); - expect(onLayout).not.toHaveBeenCalled(); - }); + const overlay = component.find('.bthlabs-rcp-overlay'); + expect(overlay.exists()).toBe(true); + expect(overlay.prop('onClick')).toBe(onOverlayClick); + }); - it('should apply default layout to the inner layer if rendering without anchor', () => { - const component = shallow( - - HERE POPUP CONTENT BE - - ); + it('should not apply layout style on the inner layer when invisible', () => { + const component = shallow( + + HERE POPUP CONTENT BE + + ); - const inner = component.find('.bthlabs-rcp-inner'); - expect(inner.prop('style')).toEqual({left: '0px', top: '0px'}); - }); + const inner = component.find('.bthlabs-rcp-inner'); + expect(inner.prop('style')).toEqual({}); + }); - it('should layout the inner layer according to the anchor', () => { - const component = mount(, {attachTo: mountNode}); - component.setState({popupVisible: true}); + it('should apply layout style on the inner layer when visible', () => { + const component = shallow( + + HERE POPUP CONTENT BE + + ); + component.setState({layout: [20, 20]}); - 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 inner = component.find('.bthlabs-rcp-inner'); + expect(inner.prop('style').left).toEqual('20px'); + expect(inner.prop('style').top).toEqual('20px'); + }); - component.unmount(); - }); + it('should set the inner layer height and width if onLayout specified them', () => { + const component = shallow( + + HERE POPUP CONTENT BE + + ); + component.setState({layout: [20, 20, 100, 100]}); - it('should call onLayout to allow for customization of the inner layer layout', () => { - onLayout = onLayout.and.returnValue([20, 20]); + const inner = component.find('.bthlabs-rcp-inner'); + expect(inner.prop('style').height).toEqual('100px'); + expect(inner.prop('style').width).toEqual('100px'); + }); - const component = shallow( - - HERE POPUP CONTENT BE - - ); + it('should render the children in the inner layer', () => { + const component = shallow( + + HERE POPUP CONTENT BE + + ); - const inner = component.find('.bthlabs-rcp-inner'); - expect(onLayout).toHaveBeenCalledWith([0, 0]); - expect(inner.prop('style')).toEqual({left: '20px', top: '20px'}); - }); - - it('should set the inner layour height and width if onLayout specified them', () => { - onLayout = onLayout.and.returnValue([20, 20, 100, 100]); - - const component = shallow( - - HERE POPUP CONTENT BE - - ); - - 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 render the children in the inner layer', () => { - const component = shallow( - - HERE POPUP CONTENT BE - - ); - - const inner = component.find('.bthlabs-rcp-inner'); - expect(inner.contains(HERE POPUP CONTENT BE)).toBe(true); + const inner = component.find('.bthlabs-rcp-inner'); + expect(inner.contains(HERE POPUP CONTENT BE)).toBe(true); + }); }); });