Skip to content

Instantly share code, notes, and snippets.

@pankajpatel
Forked from mogelbrod/portal.js
Created August 27, 2019 14:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pankajpatel/b3040a754317d3c5dd551cafef3c3b8f to your computer and use it in GitHub Desktop.
Save pankajpatel/b3040a754317d3c5dd551cafef3c3b8f to your computer and use it in GitHub Desktop.
Non-bubbling React Portals
import PropTypes from 'prop-types'
import React from 'react'
import ReactDOM from 'react-dom'
function portalContainer() {
return document.getElementById('portal-container')
}
// Taken from https://reactjs.org/docs/events.html
const DEFAULT_EVENT_LISTENERS = [
// Clipboard
'onCopy', 'onCut', 'onPaste',
// Composition - seldom used?
// 'onCompositionEnd', 'onCompositionStart', 'onCompositionUpdate',
// Keyboard
'onKeyDown', 'onKeyPress', 'onKeyUp',
// Focus - doesn't bubble, blocking them will prevent native listeners from working
// 'onFocus', 'onBlur',
// Form
'onChange', 'onInput', 'onInvalid', 'onSubmit',
// Mouse
'onClick', 'onContextMenu', 'onDoubleClick',
'onMouseDown', 'onMouseUp',
'onMouseEnter', 'onMouseLeave',
'onMouseMove', 'onMouseOut', 'onMouseOver',
// Drag & drop
'onDrag', 'onDragEnd', 'onDragEnter', 'onDragExit',
'onDragLeave', 'onDragOver', 'onDragStart', 'onDrop',
// Pointer
'onPointerDown', 'onPointerMove', 'onPointerUp', 'onPointerCancel',
'onPointerEnter', 'onPointerLeave', 'onPointerOver', 'onPointerOut',
// Selection
'onSelect',
// Touch
'onTouchCancel', 'onTouchEnd', 'onTouchMove', 'onTouchStart',
// UI
'onScroll',
// Wheel
'onWheel',
// Media
'onAbort', 'onCanPlay', 'onCanPlayThrough', 'onDurationChange', 'onEmptied',
'onEncrypted', 'onEnded', 'onError', 'onLoadedData', 'onLoadedMetadata',
'onLoadStart', 'onPause', 'onPlay', 'onPlaying', 'onProgress',
'onRateChange', 'onSeeked', 'onSeeking', 'onStalled', 'onSuspend',
'onTimeUpdate', 'onVolumeChange', 'onWaiting',
// Image
'onLoad', 'onError',
// Animation
'onAnimationStart', 'onAnimationEnd', 'onAnimationIteration',
// Transition
'onTransitionEnd',
// Other
'onToggle',
]
/**
* Portal which by default doesn't bubble events triggered within it to React
* parent components.
*
* Adds blocking event handlers for most SyntheticEvents until
* native support for event bubbling prevention is added.
*
* See https://github.com/facebook/react/issues/11387
*/
export default class Portal extends React.Component {
static propTypes = {
/** List of event names (such as `onClick`) to avoid bubbling up React component hierarchy */
blockedEvents: PropTypes.arrayOf(PropTypes.string).isRequired,
/** Custom DOM node to render portal inside */
container: PropTypes.object,
}
static defaultProps = {
blockedEvents: DEFAULT_EVENT_LISTENERS,
}
static DEFAULT_EVENT_LISTENERS = DEFAULT_EVENT_LISTENERS
render() {
const { blockedEvents, container, ...props } = this.props
for (let eventName of blockedEvents) {
props[eventName] = stopAndRedispatchEventOnWindow
}
return ReactDOM.createPortal(
React.createElement('div', props, this.props.children),
container || portalContainer()
)
}
}
const EVENT_TYPE_WHITELIST_AND_ARGUMENT_ORDER = {
CustomEvent: 'bubbles cancelable detail',
Event: 'bubbles cancelable detail',
FocusEvent: 'bubbles cancelable view detail relatedTarget',
KeyboardEvent: 'bubbles cancelable view key location ctrlKey altKey shiftKey metaKey repeat locale',
MouseEvent: 'bubbles cancelable view detail screenX screenY clientX clientY ctrlKey altKey shiftKey metaKey button relatedTarget',
PointerEvent: 'bubbles cancelable view detail screenX screenY clientX clientY ctrlKey altKey shiftKey metaKey button relatedTarget offsetX offsetY width height pressure rotation tiltX tiltY pointerId pointerType hwTimestamp isPrimary',
// For some reason WheelEvents use a space separated string for 'modifiers' instead of individual arguments, so we'll just ignore it
WheelEvent: 'bubbles cancelable view detail screenX screenY clientX clientY button relatedTarget modifiers deltaX deltaY deltaZ deltaMode',
}
const EVENT_PROP_OVERRIDES = {
eventPhase: Event.BUBBLING_PHASE,
currentTarget: window,
}
/**
* Stops the propagation of a React event within its component hierarchy while
* also dispatching a clone of it to `window`.
*
* Event types not present in EVENT_TYPE_WHITELIST can't be cloned, and will
* instead be allowed through.
*
* @param {React.SyntheticEvent} event - React event
* @return {Event|null} Cloned native event dispatched to `window`
*/
/* export */ function stopAndRedispatchEventOnWindow(event) {
const { nativeEvent } = event
const eventType = nativeEvent.constructor.name ||
{}.toString.call(nativeEvent.constructor).slice(8, -1)
if (!EVENT_TYPE_WHITELIST_AND_ARGUMENT_ORDER[eventType]) {
// console.warn(`Ignored ${eventType}(${event.type}) event`)
return null
}
event.stopPropagation()
// Clone original native event
let windowEvent
try {
windowEvent = new (nativeEvent.constructor)(nativeEvent.type, nativeEvent)
} catch (error) {
// Legacy initialization of events for IE
// FIXME: IE prevents the read-only property `target` from being set via
// `defineProperty`. This makes it seemingly impossible to clone events in IE11.
/*
windowEvent = document.createEvent(eventType)
const args = EVENT_TYPE_WHITELIST_AND_ARGUMENT_ORDER[eventType].split(' ')
.map(prop => nativeEvent[prop])
windowEvent['init' + eventType](nativeEvent.type, ...args)
*/
console.error(`Unable to clone ${eventType}(${event.type})`, error)
}
for (let prop in nativeEvent) {
if (prop !== 'isTrusted' && typeof nativeEvent[prop] !== 'function') {
Object.defineProperty(windowEvent, prop, {
writable: false,
value: EVENT_PROP_OVERRIDES[prop] || nativeEvent[prop]
})
}
}
// TODO: Should this be scheduled differently?
requestAnimationFrame(() => {
window.dispatchEvent(windowEvent)
})
return windowEvent
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment