1
0
Fork 0

Initial commit.

master v1.0.0
Tomek Wójcik 2019-02-13 21:10:40 +01:00
commit 88a6747e7f
24 changed files with 8644 additions and 0 deletions

17
.babelrc Normal file
View File

@ -0,0 +1,17 @@
{
"presets": [
[
"@babel/preset-env", {
"targets": {
"chrome": 60,
"edge": 14,
"firefox": 54,
"ie": 9,
"opera": 46,
"safari": 9
}
}
],
"@babel/preset-react"
]
}

55
.eslintrc.json Normal file
View File

@ -0,0 +1,55 @@
{
"env": {
"browser": true,
"commonjs": true,
"jasmine": true,
"es6": true
},
"extends": "eslint:recommended",
"parser": "babel-eslint",
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"modules": true
}
},
"plugins": ["jasmine"],
"rules": {
"no-undef": "error",
"quotes": [
2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}
],
"no-unused-vars": [0],
"no-console": [2],
"no-empty": ["error", {"allowEmptyCatch": true}],
"array-bracket-spacing": ["error", "never"],
"block-spacing": ["error", "always"],
"brace-style": ["error", "1tbs", {"allowSingleLine": true}],
"camelcase": ["error", {"properties": "never"}],
"comma-dangle": ["error", "never"],
"comma-spacing": ["error", {"before": false, "after": true}],
"comma-style": ["error", "last"],
"computed-property-spacing": ["error", "never"],
"key-spacing": [
"error", {"beforeColon": false, "afterColon": true, "mode": "strict"}
],
"keyword-spacing": ["error", { "before": true, "after": true }],
"linebreak-style": ["error", "unix"],
"max-len": ["error", 120],
"no-multiple-empty-lines": ["error"],
"no-spaced-func": ["error"],
"no-trailing-spaces": ["error"],
"no-unreachable": [1],
"no-whitespace-before-property": ["error"],
"object-curly-spacing": ["error", "never"],
"one-var-declaration-per-line": ["error", "always"],
"one-var": ["error", "never"],
"semi-spacing": ["error", {"before": false, "after": true}],
"semi": ["error", "always"],
"space-before-function-paren": ["error", "always"],
"space-before-blocks": ["error", "always"],
"space-in-parens": ["error", "never"],
"space-infix-ops": ["error"],
"unicode-bom": ["error", "never"]
}
}

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
lib/
node_modules/
react-custom-popup-*.tgz
yarn-error.log
.sass-cache/
bthlabs-react-custom-popup-*.tgz

95
Gruntfile.js Normal file
View File

@ -0,0 +1,95 @@
var path = require('path');
var underscore = require('underscore');
var webpack = require('webpack');
var defs = require('./package.defs.js');
var distWebpackConfig = require('./webpack.dist.js');
var exampleWebpackConfig = require('./webpack.example.js');
module.exports = function (grunt) {
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-sass');
grunt.loadNpmTasks('grunt-karma');
grunt.loadNpmTasks('grunt-webpack');
grunt.loadNpmTasks('gruntify-eslint');
grunt.initConfig({
clean: {
options: {
force: true
},
all: [
defs.LIB_DIR,
path.resolve(defs.EXAMPLE_ASSETS_DIR, 'example.js'),
path.resolve(defs.EXAMPLE_ASSETS_DIR, 'example.js.map'),
path.resolve(defs.EXAMPLE_ASSETS_DIR, 'example.css'),
path.resolve(defs.EXAMPLE_ASSETS_DIR, 'example.css.map')
]
},
eslint: {
example: {
src: ['./example/example.js']
},
sources: {
src: ['./src/**/*.js']
},
tests: {
src: ['./tests/**/*.spec.js']
},
tools: {
src: [
'./Gruntfile.js', './karma.conf.js', './package.defs.js',
'./webpack.dist.js', './webpack.example.js'
]
}
},
karma: {
options: {
configFile: 'karma.conf.js'
},
dist: {
browsers: ['ChromiumHeadless'],
reporters: ['progress'],
singleRun: true,
webpackMiddleware: {
noInfo: true,
stats: 'errors-only'
}
}
},
sass: {
dist: {
src: [path.resolve(defs.SRC_DIR, 'Popup.scss')],
dest: path.resolve(defs.LIB_DIR, defs.CSS_FILENAME_DIST),
options: {
precision: 10,
sourcemap: 'auto',
style: 'compressed'
}
},
example: {
src: [path.resolve(defs.EXAMPLE_DIR, 'example.scss')],
dest: path.resolve(defs.EXAMPLE_ASSETS_DIR, 'example.css'),
options: {
precision: 10,
sourcemap: 'auto',
style: 'expanded'
}
}
},
webpack: {
dist: distWebpackConfig,
example: exampleWebpackConfig
}
});
grunt.registerTask('dist', [
'clean', 'eslint', 'karma:dist', 'sass:dist', 'webpack:dist'
]);
grunt.registerTask('example', [
'clean', 'sass:example', 'webpack:dist', 'webpack:example'
]);
};

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2019-present Tomek Wójcik <tomek@bthlabs.pl>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

28
NOTICE.txt Normal file
View File

@ -0,0 +1,28 @@
react-custom-popup
Copyright (c) 2019-present Tomek Wójcik <tomek@bthlabs.pl>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
react-custom-popup includes the following third party software
Marx
Copyright (c) 2018 Matthew Blode
Licensed under terms of the MIT License

174
README.md Normal file
View File

@ -0,0 +1,174 @@
# react-custom-popup
React Custom Popup is a React component for simply building custom popups and
modals.
## Installation
react-custom-popup requires **React 16.4.0 or later** and
**ReactDOM 16.4.0 or later**.
```
yarn add --dev react-custom-popup
```
This assumes that youre using [yarn](https://yarnpkg.com/en/) package manager
with a module bundler like [Webpack](https://webpack.js.org/).
## Usage
The following snippet shows the example usage of react-custom-popup.
```
import React from 'react';
import Popup from 'react-custom-popup';
class App extends React.Component {
constructor (props) {
super(props);
this.refButton = React.createRef();
this.state = {
popupVisible: false
};
}
onShowPopupButtonClick (event) {
event.stopPropagation();
event.preventDefault();
this.setState({popupVisible: true});
}
onHidePopup (event) {
event.stopPropagation();
event.preventDefault();
this.setState({popupVisible: false});
}
render () {
return (
<React.Fragment>
<button
ref={this.refButton}
onClick={this.onShowPopupButtonClick}
>
Show Popup
</button>
<Popup
anchor={this.refButton}
visible={this.state.popupVisible}
onOverlayClick={this.onHidePopup}
>
<div>
<h2>Hello, I'm a popup!</h2>
<p>
<button onClick={this.onHidePopup}>Close</button>
</p>
</div>
</Popup>
</React.Fragment>
);
}
}
```
### Styling
react-custom-popup includes CSS file that provides generic styles for the
popup. It's recommended to include it in your project's styles.
You can customize aspects of the popup's layout by passing a custom CSS class
using the *className* prop.
## API
### `Popup`
This is the custom popup component. It renders the popup in a React portal.
Example DOM structure rendered by the component:
```
<div className="bthlabs-react-custom-popup">
<div className="bthlabs-rcp-overlay" />
<div className="bthlabs-rcp-inner">
<!-- Children will be rendered here -->
</div>
</div>
```
The `div.bthlabs-rcp-inner` will be positioned according to the anchor. If
anchor isn't passed, the position will default to `left: 0px; top: 0px;`,
unless modified by `onLayout` prop.
**Props**
* `anchor` (*React ref*, optional) - a ref to an anchor element which will be
used to position the inner layer.
* `className` (*string*, optional) - custom CSS class.
* `hideOverlay` (*boolean*, optional) - allows the wrapping component to hide
the overlay. Defaults to `false`.
* `visible` (*boolean*, required) - specifies whether the popup is visible.
* `onLayout` (*function*, optional) - callback which allows the wrapping
component to modify the inner layer's layout. Read below for more information.
* `onOverlayClick` (*function*, optional) - *onClick* handler for the
`div.bthlabs-rcp-overlay` element.
**The onLayout callback**
The *onLayout* prop allows the wrapping component to modify the inner layer's
layout. If specified, it should accept an array of integers (`[<left>, <top>]`)
and return an array of either two or more integers
(`[<left>, <top>, <width>, <height>]`). The *width* and *height* fields can
be omitted.
Example *onLayout* callback:
```
const onLayout = (layout) => {
return [
layout[0] + 10, // left
layout[1] + 20, // top
100, // width
200 // height
];
};
```
This function would shift the inner layer by 10px to the right and 20px to the
bottom and
set its size to 100px of width and 200px of height.
## Development
To bootstrap the development environment, clone the repo and run `npm install`
from the root directory.
The `package.json` file provides the following scripts:
* `build` - builds the library modules,
* `build:example` - builds the example project,
* `lint` - performs an eslint run over the source code,
* `test` - performs a single test run,
* `test:watch` - starts karma with watcher.
**NOTE**: Tests require *Chromium* to be installed and available in the path.
Consult
[karma-chrome-launcher](https://github.com/karma-runner/karma-chrome-launcher)
docs for more info.
## Contributing
If you think you found a bug or want to send a patch, feel free to contact
me through e-mail.
If you're sending a patch, make sure it passes eslint checks and is tested.
## Author
react-custom-popup is developed by [Tomek Wójcik](https://www.bthlabs.pl/).
## License
react-custom-popup is licensed under the MIT License.

2
example/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
assets/example.css*
assets/example.js*

2
example/assets/marx.min.css vendored Normal file

File diff suppressed because one or more lines are too long

108
example/example.js Normal file
View File

@ -0,0 +1,108 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Popup from '../lib/react-custom-popup.js';
const CUSTOM_DIMENSION = 500;
class App extends React.Component {
constructor (props) {
super(props);
this.onCustomLayoutCheckboxClick = this.onCustomLayoutCheckboxClick.bind(this);
this.onHideOverlayCheckboxClick = this.onHideOverlayCheckboxClick.bind(this);
this.onShowPopupButtonClick = this.onShowPopupButtonClick.bind(this);
this.onPopupOverlayClick = this.onPopupOverlayClick.bind(this);
this.refButton = React.createRef();
this.state = {
customLayout: false,
hideOverlay: false,
popupVisible: false
};
}
onHideOverlayCheckboxClick (event) {
this.setState({hideOverlay: !this.state.hideOverlay});
}
onCustomLayoutCheckboxClick (event) {
this.setState({customLayout: !this.state.customLayout});
}
onShowPopupButtonClick (event) {
event.stopPropagation();
event.preventDefault();
this.setState({popupVisible: true});
}
onPopupOverlayClick (event) {
event.stopPropagation();
event.preventDefault();
this.setState({popupVisible: false});
}
onPopupLayout (currentLayout) {
return [
(window.innerWidth - CUSTOM_DIMENSION) / 2,
(window.innerHeight - CUSTOM_DIMENSION) / 2,
CUSTOM_DIMENSION,
CUSTOM_DIMENSION
];
}
render () {
return (
<React.Fragment>
<h1>React Custom Popup Example</h1>
<p>
<strong>Configure</strong>
<br/>
<input
id="checkboxHideOverlay"
type="checkbox"
defaultValue={this.state.hideOverlay}
onClick={this.onHideOverlayCheckboxClick}
/>
<label htmlFor="checkboxHideOverlay">Hide overlay</label>
<br/>
<input
id="checkboxCustomLayout"
type="checkbox"
defaultValue={this.state.customLayout}
onClick={this.onCustomLayoutCheckboxClick}
/>
<label htmlFor="checkboxCustomLayout">Custom layout</label>
</p>
<button
ref={this.refButton}
onClick={this.onShowPopupButtonClick}
>
Show Popup
</button>
<Popup
anchor={this.refButton}
hideOverlay={this.state.hideOverlay}
visible={this.state.popupVisible}
onLayout={
(this.state.customLayout) ? this.onPopupLayout : undefined
}
onOverlayClick={this.onPopupOverlayClick}
>
<div id="popup" className={`${this.state.customLayout ? 'custom-layout' : ''}`}>
<h2>Hello, I'm a popup!</h2>
<p>
<button onClick={this.onPopupOverlayClick}>Close</button>
</p>
</div>
</Popup>
</React.Fragment>
);
}
}
window.addEventListener('load', () => {
ReactDOM.render(
<App />, document.getElementById('main')
);
});

43
example/example.scss Normal file
View File

@ -0,0 +1,43 @@
@import "../src/Popup";
#main {
margin: 1em;
p {
input[type="checkbox"] {
margin-right: 0.5em;
}
label {
line-height: 1;
}
}
}
#popup {
background: white;
box-shadow: 0px 0px 2px #ebebeb;
border: 1px solid #ebebeb;
border-radius: 2px;
padding: 16px;
&.custom-layout {
height: 500px;
width: 500px;
}
h2 {
margin-top: 0px;
}
p {
margin-bottom: 0px;
text-align: right;
}
}
.bthlabs-react-custom-popup {
.bthlabs-rcp-overlay {
background: rgba(0, 0, 0, 0.2);
}
}

16
example/index.html Normal file
View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>React Custom Popup Example</title>
<meta name="description" content="React Custom Popup Example">
<meta name="author" content="Tomek Wójcik <contact@bthlabs.pl> (https://www.bthlabs.pl/)">
<link rel="stylesheet" href="assets/marx.min.css">
<link rel="stylesheet" href="assets/example.css">
</head>
<body>
<div id="main">
</div>
<script src="assets/example.js"></script>
</body>
</html>

45
karma.conf.js Normal file
View File

@ -0,0 +1,45 @@
var path = require('path');
var defs = require('./package.defs.js');
module.exports = function (config) {
config.set({
basePath: '.',
browsers: ['Chromium'],
frameworks: ['jasmine'],
files: [
'tests/__entry__.js'
],
mochaReporter: {
ignoreSkipped: true
},
preprocessors: {
'tests/__entry__.js': ['webpack', 'sourcemap']
},
reporters: ['mocha'],
singleRun: false,
webpack: {
devtool: 'inline-source-map',
module: {
loaders: [
{
test: /\.js?/,
include: defs.SRC_DIR,
loader: ['babel-loader']
},
{
test: /\.js?/,
include: defs.TESTS_DIR,
loader: ['babel-loader']
}
]
},
resolve: {
alias: {
'src': defs.SRC_DIR,
'tests': defs.TESTS_DIR
}
}
}
});
};

24
package.defs.js Normal file
View File

@ -0,0 +1,24 @@
/* eslint-env node */
var path = require('path');
var LIB_DIR = path.resolve(__dirname, 'lib');
var SRC_DIR = path.resolve(__dirname, 'src');
var TESTS_DIR = path.resolve(__dirname, 'tests');
var EXAMPLE_DIR = path.resolve(__dirname, 'example');
var EXAMPLE_ASSETS_DIR = path.resolve(EXAMPLE_DIR, 'assets');
var LIBRARY_NAME = 'react-custom-popup';
var FILENAME_DIST = 'react-custom-popup.js';
var CSS_FILENAME_DIST = 'react-custom-popup.css';
module.exports = {
LIB_DIR: LIB_DIR,
SRC_DIR: SRC_DIR,
LIBRARY_NAME: LIBRARY_NAME,
FILENAME_DIST: FILENAME_DIST,
EXAMPLE_DIR: EXAMPLE_DIR,
EXAMPLE_ASSETS_DIR: EXAMPLE_ASSETS_DIR,
CSS_FILENAME_DIST: CSS_FILENAME_DIST,
TESTS_DIR: TESTS_DIR
};

58
package.json Normal file
View File

@ -0,0 +1,58 @@
{
"name": "@bthlabs/react-custom-popup",
"version": "1.0.0",
"description": "",
"main": "./lib/react-custom-popup.js",
"scripts": {
"build": "./node_modules/.bin/grunt dist",
"build:example": "./node_modules/.bin/grunt example",
"lint": "./node_modules/.bin/grunt eslint",
"test": "./node_modules/.bin/grunt karma:dist",
"test:watch": "./node_modules/.bin/karma start"
},
"repository": {
"type": "git",
"url": "https://git.bthlabs.pl/tomekwojcik/react-custom-popup.git"
},
"files": [
"lib"
],
"author": "Tomek Wójcik <contact@bthlabs.pl> (https://www.bthlabs.pl/)",
"license": "MIT",
"devDependencies": {
"@babel/core": "7.2.2",
"@babel/preset-env": "7.3.1",
"@babel/preset-react": "7.0.0",
"babel-eslint": "8.2.1",
"babel-loader": "8.0.5",
"enzyme": "3.5.0",
"enzyme-adapter-react-16": "1.3.0",
"eslint-plugin-jasmine": "2.9.1",
"grunt": "1.0.1",
"grunt-contrib-clean": "1.1.0",
"grunt-contrib-sass": "1.0.0",
"grunt-karma": "2.0.0",
"grunt-webpack": "3.0.2",
"gruntify-eslint": "4.0.0",
"jasmine-core": "2.8.0",
"karma": "2.0.0",
"karma-chrome-launcher": "2.2.0",
"karma-jasmine": "1.1.1",
"karma-mocha-reporter": "2.2.5",
"karma-sourcemap-loader": "0.3.7",
"karma-webpack": "2.0.9",
"react": "16.4.0",
"react-dom": "16.4.0",
"underscore": "1.8.3",
"webpack": "3.10.0",
"webpack-uglify-js-plugin": "1.1.9"
},
"dependencies": {
"classnames": "2.2.6",
"prop-types": "15.6.2"
},
"peerDependencies": {
"react": ">=16.4.0",
"react-dom": ">=16.4.0"
}
}

91
src/Popup.js Normal file
View File

@ -0,0 +1,91 @@
/**
* Copyright (c) 2019-present Tomek Wójcik <tomek@bthlabs.pl>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import ClassName from 'classnames';
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
});
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;
}
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`;
}
}
return ReactDOM.createPortal(
<div className={className}>
{!props.hideOverlay &&
<div className="bthlabs-rcp-overlay" onClick={props.onOverlayClick} />
}
<div className="bthlabs-rcp-inner" style={innerStyle}>
{props.children}
</div>
</div>,
props.domNode || body
);
};
Popup.propTypes = {
anchor: PropTypes.object,
className: PropTypes.string,
hideOverlay: PropTypes.bool,
visible: PropTypes.bool.isRequired,
onLayout: PropTypes.func,
onOverlayClick: PropTypes.func
};

24
src/Popup.scss Normal file
View File

@ -0,0 +1,24 @@
/*!
* react-custom-popup | Copyright 2019-present Tomek Wójcik <tomek@bthlabs.pl> | MIT License
* https://git.bthlabs.pl/tomekwojcik/react-custom-popup
*/
.bthlabs-react-custom-popup {
display: none;
&.bthlabs-rcp-visible {
display: block;
}
.bthlabs-rcp-overlay {
height: 100vh;
left: 0px;
position: fixed;
top: 0px;
width: 100vw;
}
.bthlabs-rcp-inner {
position: absolute;
}
}

8
src/index.js Normal file
View File

@ -0,0 +1,8 @@
/*!
* react-custom-popup | Copyright 2019-present Tomek Wójcik <tomek@bthlabs.pl> | MIT License
* https://git.bthlabs.pl/tomekwojcik/react-custom-popup
*/
import {Popup} from './Popup';
export default Popup;

191
tests/Popup.spec.js Normal file
View File

@ -0,0 +1,191 @@
import {mount, shallow} from 'enzyme';
import React from 'react';
import {Popup} from 'src/Popup';
class Wrapper extends React.Component {
constructor (props) {
super(props);
this.anchorRef = React.createRef();
this.state = {
popupVisible: false
};
}
render () {
const buttonStyle = {
left: '10px',
position: 'relative',
top: '10px'
};
return (
<div>
<button ref={this.anchorRef} style={buttonStyle}>Anchor</button>
<Popup anchor={this.anchorRef} visible={this.state.popupVisible}>
<span>HERE POPUP CONTENT BE</span>
</Popup>
</div>
);
}
}
describe('Popup', () => {
let mountNode = null;
let onLayout = null;
let onOverlayClick = null;
beforeEach(() => {
onLayout = jasmine.createSpy('fakeOnLayout');
onOverlayClick = jasmine.createSpy('fakeOnOverlayClick');
mountNode = document.createElement('div');
document.body.appendChild(mountNode);
});
afterEach(() => {
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>
);
const popup = component.find('.bthlabs-react-custom-popup');
expect(popup.hasClass('spam')).toBe(true);
});
it('should render as invisible', () => {
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('bthlabs-rcp-visible')).toBe(false);
});
it('should render as visible', () => {
const component = shallow(
<Popup className='spam' visible={true}>
<span>HERE POPUP CONTENT BE</span>
</Popup>
);
const popup = component.find('.bthlabs-react-custom-popup');
expect(popup.hasClass('bthlabs-rcp-visible')).toBe(true);
});
it('should allow hiding the overlay', () => {
const component = shallow(
<Popup hideOverlay={true} 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 layout 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 not call onLayout when invisible', () => {
const component = shallow(
<Popup visible={false} onLayout={onLayout}>
<span>HERE POPUP CONTENT BE</span>
</Popup>
);
const inner = component.find('.bthlabs-rcp-inner');
expect(onLayout).not.toHaveBeenCalled();
});
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 inner = component.find('.bthlabs-rcp-inner');
expect(inner.prop('style')).toEqual({left: '0px', top: '0px'});
});
it('should layout the inner layer according to the anchor', () => {
const component = mount(<Wrapper />, {attachTo: mountNode});
component.setState({popupVisible: true});
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');
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>
);
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(
<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);
});
});

4
tests/__entry__.js Normal file
View File

@ -0,0 +1,4 @@
require('tests/__setup__/enzyme.setup.js');
const testsContext = require.context('.', true, /\.spec\.js$/);
testsContext.keys().forEach(testsContext);

View File

@ -0,0 +1,7 @@
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({
adapter: new Adapter(),
disableLifecycleMethods: true,
});

32
webpack.dist.js Normal file
View File

@ -0,0 +1,32 @@
var path = require('path');
var webpack = require('webpack');
var defs = require('./package.defs.js');
var config = {
devtool: 'source-map',
entry: path.resolve(defs.SRC_DIR, 'index.js'),
output: {
path: defs.LIB_DIR,
filename: defs.FILENAME_DIST,
library: defs.LIBRARY_NAME,
libraryTarget: 'umd',
umdNamedDefine: true
},
externals: {
react: 'react',
'react-dom': 'react-dom'
},
module: {
loaders: [
{
test: /\.js?/,
include: [defs.SRC_DIR],
loader: ['babel-loader']
}
]
}
};
module.exports = config;

25
webpack.example.js Normal file
View File

@ -0,0 +1,25 @@
var path = require('path');
var webpack = require('webpack');
var defs = require('./package.defs.js');
var config = {
devtool: 'source-map',
entry: path.resolve(defs.EXAMPLE_DIR, 'example.js'),
output: {
path: defs.EXAMPLE_ASSETS_DIR,
filename: 'example.js'
},
module: {
loaders: [
{
test: /\.js?/,
include: [defs.EXAMPLE_DIR, defs.SRC_DIR],
loader: ['babel-loader']
}
]
}
};
module.exports = config;

7570
yarn.lock Normal file

File diff suppressed because it is too large Load Diff