-
-
Save pankajpatel/b3040a754317d3c5dd551cafef3c3b8f to your computer and use it in GitHub Desktop.
Non-bubbling React Portals
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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