1
0
Fork 0

Layout the popup after initial render.

master v1.0.3
Tomek Wójcik 2019-04-06 17:55:15 +02:00
parent a9d2c712db
commit 10a56cfbed
4 changed files with 249 additions and 167 deletions

View File

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

View File

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

View File

@ -25,61 +25,80 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
export const Popup = (props) => { export class Popup extends React.Component {
const className = ClassName('bthlabs-react-custom-popup', props.className, { constructor (props) {
'bthlabs-rcp-visible': props.visible super(props);
});
let layout = [0, 0]; this.state = {
const body = document.body; layout: [0, 0]
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;
} }
componentDidUpdate (prevProps, prevState) {
if (props.visible) { if (prevProps.visible !== this.props.visible && this.props.visible) {
if (typeof props.onLayout === 'function') { this.layout();
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`;
} }
} }
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( const scrollTop = (
<div className={className}> window.pageYOffset || document_.scrollTop || body.scrollTop
{!props.hideOverlay && );
<div className="bthlabs-rcp-overlay" onClick={props.onOverlayClick} /> 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);
} }
<div className="bthlabs-rcp-inner" style={innerStyle}>
{props.children} this.setState({layout: layout});
</div> }
</div>, }
props.domNode || body 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(
<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 = { Popup.propTypes = {
anchor: PropTypes.object, anchor: PropTypes.object,

View File

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