Skip to content

Instantly share code, notes, and snippets.

@basarat
Created January 18, 2017 04:22
Show Gist options
  • Save basarat/3fef3811e502981d0bec4552e64ce78a to your computer and use it in GitHub Desktop.
Save basarat/3fef3811e502981d0bec4552e64ce78a to your computer and use it in GitHub Desktop.
import * as React from "react";
import * as ReactDOM from 'react-dom'
const Tether = require('tether');
import { PropTypes, Children } from 'react';
const renderElementToPropTypes = [
PropTypes.string,
PropTypes.shape({
appendChild: PropTypes.func.isRequired
})
]
const childrenPropType = ({ children }, propName, componentName) => {
const childCount = Children.count(children)
if (childCount <= 0) {
return new Error(`${componentName} expects at least one child to use as the target element.`)
} else if (childCount > 2) {
return new Error(`Only a max of two children allowed in ${componentName}.`)
}
}
const attachmentPositions = [
'auto auto',
'top left',
'top center',
'top right',
'middle left',
'middle center',
'middle right',
'bottom left',
'bottom center',
'bottom right'
]
export type AttachmentPosition =
'auto auto'
| 'top left'
| 'top center'
| 'top right'
| 'middle left'
| 'middle center'
| 'middle right'
| 'bottom left'
| 'bottom center'
| 'bottom right';
/**
* Thether two children.
* - The first child becomes the parent.
* - The second child if present is tetherd to the parent.
*
* TSified https://github.com/souporserious/react-tether
*/
export class TetherComponents extends React.Component<{
/**
* The tag that is used to render the Tether element, defaults to div.
*/
renderElementTag?: string,
/**
* Where in the DOM the Tether element is appended.
* Passes in any valid selector to document.querySelector. Defaults to document.body.
*/
renderElementTo?: string,
onUpdate?: () => void,
onRepositioned?: () => void,
id?: string,
className?: string,
style?: React.CSSProperties,
/**
* Which point on the child
*/
attachment: AttachmentPosition,
/**
* Which point on the parent
*/
targetAttachment: AttachmentPosition,
}, {}> {
static propTypes = {
renderElementTag: PropTypes.string,
renderElementTo: PropTypes.oneOfType(renderElementToPropTypes),
attachment: PropTypes.oneOf(attachmentPositions).isRequired,
targetAttachment: PropTypes.oneOf(attachmentPositions).isRequired,
offset: PropTypes.string,
targetOffset: PropTypes.string,
targetModifier: PropTypes.string,
enabled: PropTypes.bool,
classes: PropTypes.object,
classPrefix: PropTypes.string,
optimizations: PropTypes.object,
constraints: PropTypes.array,
id: PropTypes.string,
className: PropTypes.string,
style: PropTypes.object,
onUpdate: PropTypes.func,
onRepositioned: PropTypes.func,
children: childrenPropType
}
static defaultProps = {
renderElementTag: 'div',
renderElementTo: null
}
_targetNode = null
_elementParentNode = null
_tether: any = null;
componentDidMount() {
this._targetNode = ReactDOM.findDOMNode(this as any);
this._update();
this._registerEventListeners();
}
componentDidUpdate(prevProps) {
this._update();
}
componentWillUnmount() {
this._destroy();
}
getTetherInstance() {
return this._tether;
}
disable() {
this._tether.disable();
}
enable() {
this._tether.enable();
}
on(event, handler) {
this._tether.on(event, handler);
}
once(event, handler) {
this._tether.once(event, handler);
}
off(event, handler) {
this._tether.off(event, handler);
}
position() {
this._tether.position();
}
_registerEventListeners() {
if (this.props.onUpdate) {
this.on('update', this.props.onUpdate);
}
if (this.props.onRepositioned) {
this.on('repositioned', this.props.onRepositioned);
}
}
get _renderNode() {
const { renderElementTo } = this.props
if (typeof renderElementTo === 'string') {
return document.querySelector(renderElementTo)
} else {
return renderElementTo || document.body
}
}
_destroy() {
if (this._elementParentNode) {
ReactDOM.unmountComponentAtNode(this._elementParentNode)
this._elementParentNode.parentNode.removeChild(this._elementParentNode)
}
if (this._tether) {
this._tether.destroy()
}
this._elementParentNode = null
this._tether = null
}
_update() {
const { children, renderElementTag } = this.props
const elementComponent = Children.toArray(children)[1]
// if no element component provided, bail out
if (!elementComponent) {
// destroy Tether element if it has been created
if (this._tether) {
this._destroy()
}
return
}
// create element node container if it hasn't been yet
if (!this._elementParentNode) {
// create a node that we can stick our content Component in
this._elementParentNode = document.createElement(renderElementTag)
// append node to the render node
this._renderNode.appendChild(this._elementParentNode)
}
// render element component into the DOM
ReactDOM.unstable_renderSubtreeIntoContainer(
this as any, elementComponent as any, this._elementParentNode, () => {
// don't update Tether until the subtree has finished rendering
this._updateTether()
}
)
}
_updateTether() {
const { children, renderElementTag, renderElementTo, id, className, style, ...options } = this.props
const tetherOptions = {
target: this._targetNode,
element: this._elementParentNode,
...options
}
if (id) {
this._elementParentNode.id = id
}
if (className) {
this._elementParentNode.className = className
}
if (style) {
Object.keys(style).forEach(key => {
this._elementParentNode.style[key] = style[key]
})
}
if (!this._tether) {
this._tether = new Tether(tetherOptions)
} else {
this._tether.setOptions(tetherOptions)
}
this._tether.position()
}
render() {
return Children.toArray(this.props.children)[0] as any;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment