diff --git a/lib/test-setup.js b/lib/test-setup.js index 142070b..f221ded 100644 --- a/lib/test-setup.js +++ b/lib/test-setup.js @@ -1,10 +1,8 @@ 'use strict' -var chai = require('chai') -var jsdom = require('jsdom').jsdom -var sinonChai = require('sinon-chai') - -var exposedProperties = ['window', 'navigator', 'document'] +const chai = require('chai') +const jsdom = require('jsdom').jsdom +const sinonChai = require('sinon-chai') // http://airbnb.io/enzyme/docs/guides/jsdom.html global.document = jsdom('') @@ -16,7 +14,6 @@ global.document.createRange = () => ({ global.window = document.defaultView Object.keys(document.defaultView).forEach((property) => { if (typeof global[property] === 'undefined') { - exposedProperties.push(property) global[property] = document.defaultView[property] } }) @@ -26,11 +23,15 @@ global.navigator = { } global.Node = {} global.requestAnimationFrame = (fn) => fn() +global.cancelAnimationFrame = () => {} // enzyme, and therefore chai-enzyme, needs to be required after // global.navigator is set up (https://github.com/airbnb/enzyme/issues/395) -var chaiEnzyme = require('chai-enzyme') +const enzyme = require('enzyme') +const EnzymeAdapter = require('enzyme-adapter-react-16') +const chaiEnzyme = require('chai-enzyme') +enzyme.configure({ adapter: new EnzymeAdapter() }) chai.use(chaiEnzyme()) chai.use(sinonChai) global.expect = chai.expect diff --git a/package.json b/package.json index 9dee12e..9142418 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,12 @@ "scripts": { "build": "node ./scripts/build-dev.js", "build-prod": "node ./scripts/build-prod.js", - "lint": "eslint --fix src/*.js src/*.jsx", + "lint": "eslint --fix src/*.jsx", "precommit": "npm run lint", "prepublish": "npm run build-prod", - "pretest": "npm run lint", "start": "node ./scripts/watch.js --port 8888", "test": "node ./scripts/test.js", - "semantic-release": - "semantic-release pre && npm publish --access public && semantic-release post" + "semantic-release": "semantic-release pre && npm publish --access public && semantic-release post" }, "repository": { "type": "git", @@ -28,36 +26,38 @@ "dist" ], "dependencies": { - "popper.js": "^1.12.9", - "prop-types": "15.6.0" + "popper.js": "^1.14.4" }, "devDependencies": { - "babel-eslint": "^7.2.3", - "chai": "^3.5.0", - "chai-enzyme": "0.5.1", + "babel-eslint": "^10.0.1", + "chai": "^4.2.0", + "chai-enzyme": "1.0.0-beta.1", "condition-circle": "1.5.0", "dont-crack": "1.2.1", - "enzyme": "2.4.1", - "eslint": "^4.12.1", - "eslint-plugin-cypress-dev": "^1.1.1", - "eslint-plugin-mocha": "^4.11.0", - "eslint-plugin-react": "^7.5.1", + "enzyme": "3.7.0", + "enzyme-adapter-react-16": "1.6.0", + "eslint": "^5.6.1", + "eslint-plugin-cypress-dev": "^1.1.2", + "eslint-plugin-mocha": "^5.2.0", + "eslint-plugin-react": "^7.11.1", + "fs-extra": "7.0.0", "github-post-release": "1.13.1", "jsdom": "^9.5.0", - "lodash": "^4.0.0", - "react": "15.6.2", - "react-addons-test-utils": "15.6.2", - "react-dom": "15.6.2", + "lodash": "^4.17.11", + "prop-types": "^15.6.2", + "react": "16.5.2", + "react-dom": "16.5.2", "semantic-release": "8.2.0", "simple-commit-message": "3.3.1", - "sinon": "^1.17.6", - "sinon-chai": "^2.8.0", - "zunder": "5.5.1" + "sinon": "^6.3.5", + "sinon-chai": "^3.2.0", + "zunder": "6.2.0" }, "peerDependencies": { - "lodash": "^4.0.0", - "react": "^15.3.2", - "react-dom": "^15.3.2" + "lodash": "^4.17.11", + "prop-types": "^15.6.2", + "react": "^16.5.2", + "react-dom": "^16.5.2" }, "release": { "verifyConditions": "condition-circle", diff --git a/scripts/build-dev.js b/scripts/build-dev.js index 33b8bf5..ac4cafb 100644 --- a/scripts/build-dev.js +++ b/scripts/build-dev.js @@ -1,14 +1,14 @@ -var z = require('zunder') -var u = z.undertaker -var setZunderConfig = require('./set-zunder-config') +const z = require('zunder') +const u = z.undertaker +const setZunderConfig = require('./set-zunder-config') +const copyScss = require('./copy-scss') setZunderConfig(z) u.series( z.applyDevEnv, z.cleanDev, - u.parallel( - z.copyDevScripts, - z.buildDevStylesheets - ) + z.copyDevScripts, + z.buildDevStylesheets, + copyScss(z.config.devDir) )() diff --git a/scripts/build-prod.js b/scripts/build-prod.js index da73d36..295e82e 100644 --- a/scripts/build-prod.js +++ b/scripts/build-prod.js @@ -1,23 +1,14 @@ -var fs = require('fs') -var path = require('path') -var z = require('zunder') -var u = z.undertaker -var setZunderConfig = require('./set-zunder-config') +const z = require('zunder') +const u = z.undertaker +const setZunderConfig = require('./set-zunder-config') +const copyScss = require('./copy-scss') setZunderConfig(z) -var copyScss = () => { - var from = path.join(__dirname, '..', 'src', 'main.scss') - var to = path.join(z.config.prodDir, 'tooltip.scss') - - return fs.createReadStream(from) - .pipe(fs.createWriteStream(to)) -} - u.series( z.applyProdEnv, z.cleanProd, z.copyProdScripts, z.buildProdStylesheets, - copyScss + copyScss(z.config.prodDir) )() diff --git a/scripts/copy-scss.js b/scripts/copy-scss.js new file mode 100644 index 0000000..c529fd1 --- /dev/null +++ b/scripts/copy-scss.js @@ -0,0 +1,10 @@ +const fs = require('fs') +const path = require('path') + +module.exports = (dir) => () => { + const from = path.join(__dirname, '..', 'src', 'main.scss') + const to = path.join(dir, 'tooltip.scss') + + return fs.createReadStream(from) + .pipe(fs.createWriteStream(to)) +} diff --git a/scripts/set-zunder-config.js b/scripts/set-zunder-config.js index 73277cd..4b7e34b 100644 --- a/scripts/set-zunder-config.js +++ b/scripts/set-zunder-config.js @@ -2,7 +2,12 @@ module.exports = function setZunderConfig (zunder) { zunder.setConfig({ cacheBust: false, prodDir: 'dist', - stylesheetName: 'tooltip.css', + stylesheets: { + 'src/main.scss': { + watch: ['src/**/*.scss'], + output: 'tooltip.css', + }, + }, testDir: 'dist', }) } diff --git a/scripts/watch.js b/scripts/watch.js index 27e0e08..ccecfd5 100644 --- a/scripts/watch.js +++ b/scripts/watch.js @@ -1,17 +1,27 @@ -var z = require('zunder') -var u = z.undertaker -var setZunderConfig = require('./set-zunder-config') +const fs = require('fs-extra') +const z = require('zunder') +const u = z.undertaker +const setZunderConfig = require('./set-zunder-config') +const copyScss = require('./copy-scss') + +const ensureDevDir = () => fs.ensureDir(z.config.devDir) setZunderConfig(z) +// TODO: watch scss, so it's copied on change + u.series( z.applyDevEnv, z.cleanDev, + ensureDevDir, + copyScss(z.config.devDir), u.parallel( - z.watchServer, - z.watchStaticAssets, + // need these for 'design' mode, but z.watchScripts conflicts with z.watchTests + // because testDir and devDir are the same. need to fix in zunder + // z.watchServer, + // z.watchStaticAssets, + // z.watchScripts, z.watchTests, - z.watchScripts, z.watchStylesheets ) )() diff --git a/src/portal-popper.jsx b/src/portal-popper.jsx index eb8fc73..40bf535 100644 --- a/src/portal-popper.jsx +++ b/src/portal-popper.jsx @@ -29,14 +29,10 @@ class PortalPopper extends Component { className: '', } - constructor (...props) { - super(...props) - - this.state = { - arrowProps: initialArrowProps, - popperProps: initialPopperProps, - flipped: false, - } + state = { + arrowProps: initialArrowProps, + popperProps: initialPopperProps, + flipped: false, } render () { @@ -45,32 +41,33 @@ class PortalPopper extends Component { const flippedClass = this.state.flipped ? ` ${prefix}-flipped` : '' return ( - - {title} +
this.portalNode = node} + className={`${className} ${prefix}-${placement}${flippedClass}`} + style={this._getPopperStyle()} > - - - + {title} +
this.arrowNode = node} + className={`${prefix}-arrow`} + style={this._getArrowStyle()} + > + + + +
) } componentDidMount () { - this.popper = new this.props.Popper(this.props.getTargetNode(), this.refs.portal.domNode, { + this.popper = new this.props.Popper(this.props.getTargetNode(), this.portalNode, { content: this.props.title, placement: this.props.placement, modifiers: { - arrow: { element: this.refs.arrow }, + arrow: { element: this.arrowNode }, preventOverflow: { boundariesElement: this.props.boundary, }, diff --git a/src/portal-popper.spec.jsx b/src/portal-popper.spec.jsx index a867c2b..631e081 100644 --- a/src/portal-popper.spec.jsx +++ b/src/portal-popper.spec.jsx @@ -3,15 +3,19 @@ import React from 'react' import { mount, shallow } from 'enzyme' import sinon from 'sinon' -import Portal from './portal' - import PortalPopper from './portal-popper' const getProps = (props) => { return _.extend({ + className: 'tooltip', placement: 'top', title: 'tooltip title', getTargetNode: sinon.stub().returns('target node'), + Popper () { + return { + scheduleUpdate () {}, + } + }, }, props) } @@ -26,15 +30,16 @@ const popperStub = (popperInstance) => sinon.stub().returns(popperInstance) describe('', () => { it('renders a with a placement class', () => { - const component = shallow() - expect(component.find(Portal)).to.have.className('tooltip-top') + const component = shallow() + expect(component.find('.tooltip-top')).to.exist }) it('renders a with default styles', () => { const component = shallow() - expect(component.find(Portal).prop('style').position).to.equal('absolute') - expect(component.find(Portal).prop('style').transform).to.equal('translate3d(0px, 0px, 0)') - expect(component.find(Portal).prop('style').WebkitTransform).to.equal('translate3d(0px, 0px, 0)') + const style = component.find('.tooltip-top').prop('style') + expect(style.position).to.equal('absolute') + expect(style.transform).to.equal('translate3d(0px, 0px, 0)') + expect(style.WebkitTransform).to.equal('translate3d(0px, 0px, 0)') }) it('renders the title specified', () => { @@ -44,21 +49,22 @@ describe('', () => { it('renders the tooltip arrow with default styles', () => { const component = shallow() - expect(component.find('div').prop('style').left).to.equal(0) - expect(component.find('div').prop('style').top).to.equal(0) + const style = component.find('.tooltip-arrow').prop('style') + expect(style.left).to.equal(0) + expect(style.top).to.equal(0) }) it('renders with className specified', () => { - const component = shallow() - expect(component.find(Portal)).to.have.className('the-tooltip') - expect(component.find(Portal)).to.have.className('the-tooltip-top') - expect(component.find('div')).to.have.className('the-tooltip-arrow') + const component = shallow() + expect(component.find('.custom-class')).to.exist + expect(component.find('.custom-class-top')).to.exist + expect(component.find('.custom-class-arrow')).to.exist }) it('uses last className as prefix if multiple', () => { - const component = shallow() - expect(component.find(Portal).prop('className')).to.equal('custom-class the-tooltip the-tooltip-top') - expect(component.find('div').prop('className')).to.equal('the-tooltip-arrow') + const component = shallow() + expect(component.find('.custom-class').prop('className')).to.equal('custom-class tooltip tooltip-top') + expect(component.find('.tooltip-arrow')).to.exist }) it('creates Popper instance with the right props', () => { @@ -94,8 +100,10 @@ describe('', () => { const Popper = popperStub(popperInstance) const component = mount() Popper.firstCall.args[2].onUpdate({ offsets: { arrow: { left: 5, top: 10 } } }) - expect(component.ref('arrow').prop('style').left).to.equal(5) - expect(component.ref('arrow').prop('style').top).to.equal(10) + component.update() + const style = component.find('.tooltip-arrow').prop('style') + expect(style.left).to.equal(5) + expect(style.top).to.equal(10) }) it('does not update the arrow props if not specified', () => { @@ -103,8 +111,10 @@ describe('', () => { const Popper = popperStub(popperInstance) const component = mount() Popper.firstCall.args[2].onUpdate({ offsets: {} }) - expect(component.ref('arrow').prop('style').left).to.equal(0) - expect(component.ref('arrow').prop('style').top).to.equal(0) + component.update() + const style = component.find('.tooltip-arrow').prop('style') + expect(style.left).to.equal(0) + expect(style.top).to.equal(0) }) it('only updates the arrow props that are specified', () => { @@ -112,8 +122,10 @@ describe('', () => { const Popper = popperStub(popperInstance) const component = mount() Popper.firstCall.args[2].onUpdate({ offsets: { arrow: { top: 20 } } }) - expect(component.ref('arrow').prop('style').left).to.equal(null) - expect(component.ref('arrow').prop('style').top).to.equal(20) + component.update() + const style = component.find('.tooltip-arrow').prop('style') + expect(style.left).to.equal(null) + expect(style.top).to.equal(20) }) it('rounds the arrow props', () => { @@ -121,8 +133,10 @@ describe('', () => { const Popper = popperStub(popperInstance) const component = mount() Popper.firstCall.args[2].onUpdate({ offsets: { arrow: { left: 7.2, top: 20.8 } } }) - expect(component.ref('arrow').prop('style').left).to.equal(7) - expect(component.ref('arrow').prop('style').top).to.equal(21) + component.update() + const style = component.find('.tooltip-arrow').prop('style') + expect(style.left).to.equal(7) + expect(style.top).to.equal(21) }) it('updates the popper props if specified', () => { @@ -130,9 +144,11 @@ describe('', () => { const Popper = popperStub(popperInstance) const component = mount() Popper.firstCall.args[2].onUpdate({ offsets: { popper: { position: 'relative', left: 2, top: 4 } } }) - expect(component.find(Portal).prop('style').position).to.equal('relative') - expect(component.find(Portal).prop('style').transform).to.equal('translate3d(2px, 4px, 0)') - expect(component.find(Portal).prop('style').WebkitTransform).to.equal('translate3d(2px, 4px, 0)') + component.update() + const style = component.find('.tooltip-top').prop('style') + expect(style.position).to.equal('relative') + expect(style.transform).to.equal('translate3d(2px, 4px, 0)') + expect(style.WebkitTransform).to.equal('translate3d(2px, 4px, 0)') }) it('does not update the popper props if not specified', () => { @@ -140,9 +156,11 @@ describe('', () => { const Popper = popperStub(popperInstance) const component = mount() Popper.firstCall.args[2].onUpdate({ offsets: {} }) - expect(component.find(Portal).prop('style').position).to.equal('absolute') - expect(component.find(Portal).prop('style').transform).to.equal('translate3d(0px, 0px, 0)') - expect(component.find(Portal).prop('style').WebkitTransform).to.equal('translate3d(0px, 0px, 0)') + component.update() + const style = component.find('.tooltip-top').prop('style') + expect(style.position).to.equal('absolute') + expect(style.transform).to.equal('translate3d(0px, 0px, 0)') + expect(style.WebkitTransform).to.equal('translate3d(0px, 0px, 0)') }) it('rounds the popper props', () => { @@ -150,8 +168,10 @@ describe('', () => { const Popper = popperStub(popperInstance) const component = mount() Popper.firstCall.args[2].onUpdate({ offsets: { popper: { left: 15.2, top: 2.8 } } }) - expect(component.find(Portal).prop('style').transform).to.equal('translate3d(15px, 3px, 0)') - expect(component.find(Portal).prop('style').WebkitTransform).to.equal('translate3d(15px, 3px, 0)') + component.update() + const style = component.find('.tooltip-top').prop('style') + expect(style.transform).to.equal('translate3d(15px, 3px, 0)') + expect(style.WebkitTransform).to.equal('translate3d(15px, 3px, 0)') }) }) }) diff --git a/src/portal.jsx b/src/portal.jsx index b2597d8..a89cb21 100644 --- a/src/portal.jsx +++ b/src/portal.jsx @@ -1,6 +1,5 @@ -import _ from 'lodash' -import React, { Component } from 'react' -import { render } from 'react-dom' +import { Component } from 'react' +import { createPortal } from 'react-dom' class Portal extends Component { static idNum = 0 @@ -9,7 +8,7 @@ class Portal extends Component { appendTo: document.body, } - componentDidMount () { + componentWillMount () { const appendTo = this.props.appendTo const id = `portal-${Portal.idNum++}` let element = appendTo.ownerDocument.getElementById(id) @@ -19,7 +18,6 @@ class Portal extends Component { appendTo.appendChild(element) } this._element = element - this.componentDidUpdate() } componentWillUnmount () { @@ -27,16 +25,11 @@ class Portal extends Component { appendTo.removeChild(this._element) } - componentDidUpdate () { - render(( -
this.domNode = node} {..._.omit(this.props, 'children', 'appendTo')}> - {this.props.children} -
- ), this._element) - } - render () { - return null + return createPortal( + this.props.children, + this._element, + ) } } diff --git a/src/portal.spec.jsx b/src/portal.spec.jsx index 097dd61..c596ec5 100644 --- a/src/portal.spec.jsx +++ b/src/portal.spec.jsx @@ -4,10 +4,6 @@ import React from 'react' import Portal from './portal' describe('', () => { - it('renders nothing', () => { - expect(mount()).to.be.empty - }) - it('creates a div with a unique id', () => { Portal.idNum = 0 mount() @@ -18,29 +14,13 @@ describe('', () => { document.getElementById('portal-1').remove() }) - it('renders a div within the portal div with the properties passed in', () => { + it('renders children', () => { Portal.idNum = 0 - mount() + mount(
) expect(document.querySelector('.foo')).to.exist document.getElementById('portal-0').remove() }) - it('renders children within the rendered div', () => { - Portal.idNum = 0 - mount(
) - expect(document.querySelector('.bar')).to.exist - document.getElementById('portal-0').remove() - }) - - it('responds to updates', () => { - Portal.idNum = 0 - const component = mount() - component.setProps({ className: 'foo-new' }) - expect(document.querySelector('.foo')).not.to.exist - expect(document.querySelector('.foo-new')).to.exist - document.getElementById('portal-0').remove() - }) - it('removes the portal div on unmount', () => { Portal.idNum = 0 const component = mount() diff --git a/src/tooltip.jsx b/src/tooltip.jsx index a3b22ca..5cc8567 100644 --- a/src/tooltip.jsx +++ b/src/tooltip.jsx @@ -19,12 +19,8 @@ class Tooltip extends Component { wrapperClassName: '', } - constructor (...props) { - super(...props) - - this.state = { - shouldShow: false, - } + state = { + shouldShow: false, } render () {