Skip to content

Commit

Permalink
feat: initial OS release
Browse files Browse the repository at this point in the history
* removed nyc

* trial to fix Please tell me who you are

* set config global

* fixed syntax error

* fixed flag

* post demo url as pr comment

* added onClickOutside event

* post pr comment without using circle-github-bot

* bugfix for pr demo publish

* fixed tests for onClickOutside

* finished tests for onClickOutside

* fail lint task on warning

* fixed lint problems


BREAKING CHANGE: first release version
  • Loading branch information
jfschwarz committed Jan 28, 2018
1 parent eccf680 commit 860ba17
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 68 deletions.
4 changes: 4 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ jobs:
- run:
name: Fix host authenticity for github.com
command: mkdir -p ~/.ssh/ && ssh-keyscan github.com >> ~/.ssh/known_hosts
- run:
name: Set git name & email
command: |
git config --global user.name "CircleCI" && git config --global user.email "circleci@signavio.com"
- run:
name: Build demo
command: yarn build-demo
Expand Down
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# react-stick

[![Travis][build-badge]][build]
[![npm package][npm-badge]][npm]
[![CircleCI][build-badge]][build]
[![Coveralls][coveralls-badge]][coveralls]
[![npm package][npm-badge]][npm]
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)

_Stick_ is a component that allows to attach an absolutely positioned node to a statically
positioned anchor element. Per default, the node will be rendered in a portal as a direct
Expand Down Expand Up @@ -32,10 +33,9 @@ import Stick from 'react-stick'

For `position` and `align` props string values of the form `top|middle|bottom left|center|right` are supported.

[build-badge]: https://img.shields.io/travis/user/repo/master.png?style=flat-square
[build]: https://travis-ci.org/user/repo
[npm-badge]: https://img.shields.io/npm/v/npm-package.png?style=flat-square
[npm]: https://www.npmjs.org/package/npm-package
[coveralls-badge]: https://img.shields.io/coveralls/user/repo/master.png?style=flat-square
[coveralls]: https://coveralls.io/github/user/repo
[semantic-release]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
[build-badge]: https://circleci.com/gh/signavio/react-stick/tree/master.svg?style=shield&circle-token=:circle-token
[build]: https://circleci.com/gh/signavio/react-stick/tree/master
[npm-badge]: https://img.shields.io/npm/v/react-stick.png?style=flat-square
[npm]: https://www.npmjs.org/package/react-stick
[coveralls-badge]: https://img.shields.io/coveralls/signavio/react-stick/master.png?style=flat-square
[coveralls]: https://coveralls.io/github/signavio/react-stick
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
"build-demo": "nwb build-react-component",
"clean": "nwb clean-module && nwb clean-demo",
"flow": "flow",
"format": "prettier --write --no-semi --single-quote --trailing-comma es5 \"{src,stories}/**/*.js\"",
"lint": "eslint --ext .js src tests demo/src",
"format":
"prettier --write --no-semi --single-quote --trailing-comma es5 \"{src,stories}/**/*.js\"",
"lint": "eslint --max-warnings=0 --ext .js src tests demo/src",
"publish-demo": "node scripts/gh-pages-publish.js $CIRCLE_BRANCH",
"start": "nwb serve-react-demo",
"semantic-release": "semantic-release",
Expand All @@ -22,6 +23,7 @@
"test:watch": "nwb test-react --server"
},
"dependencies": {
"lodash": "^4.17.4",
"prop-types": "^15.6.0",
"requestidlecallback": "^0.3.0",
"substyle": "^6.1.2"
Expand Down
34 changes: 31 additions & 3 deletions scripts/gh-pages-publish.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
const ghpages = require('gh-pages')
const path = require('path')
const fs = require('fs')
const { execSync } = require('child_process')

const branchName = process.argv[2]
const dir = path.resolve(__dirname, '..', 'demo', 'dist')
const url = `https://signavio.github.io/react-stick/${branchName}`
const demoUrl = `https://signavio.github.io/react-stick/${branchName}`

if (!fs.existsSync(dir)) {
throw new Error(`${dir} does not exist. Run \`yarn build\` first.`)
}

console.log(`Publishing demo/dist to ${url}...`)
// Synchronously execute command and return trimmed stdout as string
const exec = (command, options) =>
execSync(command, options)
.toString('utf8')
.trim()

// Syncronously POST to `url` with `data` content
const curl = (url, data) =>
exec(`curl --silent --data @- ${url}`, { input: data })

console.log(`Publishing demo/dist to ${demoUrl}...`)

ghpages.publish(
dir,
Expand All @@ -24,9 +35,26 @@ ghpages.publish(
process.exit(1)
}

// post PR comment with link to demo page
const {
CI_PULL_REQUEST,
GH_AUTH_TOKEN,
CIRCLE_PROJECT_USERNAME,
CIRCLE_PROJECT_REPONAME,
} = process.env
const prNumber = path.basename(CI_PULL_REQUEST)
const commitMessage = exec(
'git --no-pager log --pretty=format:"%s" -1'
).replace(/\\"/g, '\\\\"')
const body = `Demo page for commit <code>${commitMessage}</code> has been published at:<br /><strong>https://signavio.github.io/react-stick/${branchName}</strong>`
curl(
`https://${GH_AUTH_TOKEN}:x-oauth-basic@api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/issues/${prNumber}/comments`,
JSON.stringify({ body })
)

console.log(
'\x1b[32m%s\x1b[0m',
`✓ Demo page successfully published to ${url}`
`✓ Demo page successfully published at ${demoUrl}`
)
}
)
74 changes: 58 additions & 16 deletions src/Stick.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// @flow

import React, { Component } from 'react'
import { findDOMNode } from 'react-dom'
import PropTypes from 'prop-types'
import { defaultStyle } from 'substyle'
import { omit, uniqueId, compact, flatten } from 'lodash'
import { omit, uniqueId, compact, flatten, some } from 'lodash'

import getModifiers from './getModifiers'
import StickPortal from './StickPortal'
Expand All @@ -18,6 +19,7 @@ const ContextTypes = {

class Stick extends Component<PropsT> {
containerNestingKeyExtension: number
containerNode: ?HTMLElement

static contextTypes = ContextTypes
static childContextTypes = ContextTypes
Expand All @@ -27,32 +29,38 @@ class Stick extends Component<PropsT> {
this.containerNestingKeyExtension = uniqueId()
}

componentDidMount() {
document.addEventListener('click', this.handleClickOutside, true)
}

componentWillUnmount() {
document.removeEventListener('click', this.handleClickOutside, true)
}

getChildContext() {
return {
[PARENT_STICK_NESTING_KEY]: this.getNestingKey(),
}
}

render() {
const { inline, node, style, ...rest } = this.props

const wrappedNode = node && <div {...style('nodeContent')}>{node}</div>
const SpecificStick = inline ? StickInline : StickPortal

return inline ? (
<StickInline
node={wrappedNode}
{...omit(rest, 'align')}
style={style}
nestingKey={this.getNestingKey()}
/>
) : (
<StickPortal
return (
<SpecificStick
{...omit(rest, 'align', 'onClickOutside')}
node={wrappedNode}
{...omit(rest, 'align')}
style={style}
nestingKey={this.getNestingKey()}
containerRef={this.setContainerRef}
/>
)
}

getChildContext() {
return {
[PARENT_STICK_NESTING_KEY]: this.getNestingKey(),
}
setContainerRef = (ref: HTMLElement | null) => {
this.containerNode = ref
}

getNestingKey() {
Expand All @@ -61,6 +69,40 @@ class Stick extends Component<PropsT> {
this.containerNestingKeyExtension,
]).join('_')
}

handleClickOutside = (ev: Event) => {
const { onClickOutside } = this.props
if (!onClickOutside) {
return
}

const { target } = ev
if (target instanceof window.HTMLElement && this.isOutside(target)) {
onClickOutside(ev)
}
}

isOutside(target: HTMLElement) {
const anchorNode = findDOMNode(this)
if (anchorNode && anchorNode.contains(target)) {
return false
}

const nestingKey =
this.containerNode &&
this.containerNode.getAttribute('data-sticknestingkey')

if (nestingKey) {
// Find all sticked nodes nested inside our own sticked node and check if the click
// happened on any of these (our own sticked node will also be part of the query result)
const nestedStickNodes = document.querySelectorAll(
`[data-stickNestingKey^='${nestingKey}']`
)
return !some(nestedStickNodes, stickNode => stickNode.contains(target))
}

return true
}
}

const verticals = ['top', 'middle', 'bottom']
Expand Down
2 changes: 1 addition & 1 deletion src/StickPortal.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ function calculateJustifyContent(position: ?PositionT) {
function hasFixedAncestors(element: HTMLElement) {
let elem = element
do {
if (getComputedStyle(elem).position == 'fixed') return true
if (getComputedStyle(elem).position === 'fixed') return true
} while ((elem = elem.offsetParent))
return false
}
Expand Down
1 change: 1 addition & 0 deletions src/flowTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type PropsT = {
inline?: boolean,
updateOnAnimationFrame?: boolean,
nodeWidth?: number | string,
onClickOutside?: (ev: Event) => void,
containerRef: (element: HTMLElement | null) => void,
style: Substyle,
}
35 changes: 0 additions & 35 deletions tests/nesting-test.js

This file was deleted.

109 changes: 109 additions & 0 deletions tests/onClickOutside.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import expect, { createSpy } from 'expect'
import React, { cloneElement } from 'react'
import { render as renderBase, unmountComponentAtNode } from 'react-dom'

import Stick from 'src/'

describe('`onClickOutside` event', () => {
let host

const anchor = <div id="anchor" />
const node = <div id="node" />

// wrap render to invoke callback only after the node has actually been mounted
const render = (stick, host, callback) => {
let called = false
renderBase(
cloneElement(stick, {
node: cloneElement(stick.props.node, {
ref: el => !!el && !called && window.setTimeout(callback, 1),
}),
}),
host
)
}

beforeEach(() => {
host = document.createElement('div')
document.body.appendChild(host)
})

afterEach(() => {
unmountComponentAtNode(host)
document.body.removeChild(host)
})

it('should call `onClickOutside` on click on any element outside of the sticked node an anchor element', done => {
const spy = createSpy()
render(
<Stick onClickOutside={spy} node={node}>
{anchor}
</Stick>,
host,
() => {
const outsideNode = document.createElement('div')
document.body.appendChild(outsideNode)
outsideNode.click()
expect(spy).toHaveBeenCalled()
spy.reset()

document.body.click()
expect(spy).toHaveBeenCalled()
done()
}
)
})

it('should not call `onClickOutside` on click on the anchor element or sticked node', done => {
const spy = createSpy()
render(
<Stick onClickOutside={spy} node={node}>
{anchor}
</Stick>,
host,
() => {
document.getElementById('anchor').click()
expect(spy).toNotHaveBeenCalled()

document.getElementById('node').click()
expect(spy).toNotHaveBeenCalled()

done()
}
)
})

const inlineOptions = [false, true]
inlineOptions.forEach(outerInline => {
inlineOptions.forEach(innerInline => {
describe(`<Stick ${innerInline ? 'inline ' : ''}/> in node of <Stick ${
outerInline ? 'inline ' : ''
}/>`, () => {
it('should not call `onClickOutside` on click on the nested sticked node', done => {
const spy = createSpy()
render(
<Stick
inline={outerInline}
onClickOutside={spy}
node={
<div id="node">
<Stick inline={innerInline} node={<div id="nested-node" />}>
<span>foo</span>
</Stick>
</div>
}
>
<div />
</Stick>,
host,
() => {
document.getElementById('nested-node').click()
expect(spy).toNotHaveBeenCalled()
done()
}
)
})
})
})
})
})
Loading

0 comments on commit 860ba17

Please sign in to comment.