Layout the popup after initial render.
This commit is contained in:
parent
a9d2c712db
commit
10a56cfbed
|
@ -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,
|
||||||
|
|
|
@ -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",
|
||||||
|
|
117
src/Popup.js
117
src/Popup.js
|
@ -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,
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user