Skip to content

Commit 788fbbb

Browse files
authored
Fix: handle event propogation and bubbling (#7)
* zero down event triggers take it down to just firing on target to simplify handling and rewriting * chore: cleanup * dom spec compliant event phases might need modifications but for now it handles the bubbling as expected * update to test with propogation stopping
1 parent 4125b04 commit 788fbbb

File tree

4 files changed

+61
-83
lines changed

4 files changed

+61
-83
lines changed

dom/src/dom.js

Lines changed: 47 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { UIManager } from 'react-native'
1+
import * as UIManager from 'react-native/Libraries/ReactNative/UIManager'
22
import getNativeComponentAttributes from 'react-native/Libraries/ReactNative/getNativeComponentAttributes'
3-
import ReactNativePrivateInterface from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'
3+
import * as ReactNativePrivateInterface from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'
44

55
let ROOT_TAG
66

@@ -14,6 +14,11 @@ const KEYBOARD_EVENTS = ['topFocus', 'topEndEditing']
1414
const FOCUS_EVENTS = ['topFocus']
1515
const BLUR_EVENTS = ['topBlur']
1616

17+
const EVENTPHASE_NONE = 0
18+
const EVENTPHASE_CAPTURE = 1
19+
const EVENTPHASE_AT_TARGET = 2
20+
const EVENTPHASE_BUBBLE = 3
21+
1722
const EVENT_TYPES = {
1823
CLICK: 'Click',
1924
CHANGE: 'Change',
@@ -409,17 +414,15 @@ class Element extends Node {
409414
}
410415
list.push({
411416
_listener: fn,
412-
_flags: getListenerFlags(options),
413417
})
414418
}
415419

416420
removeEventListener(type, listener, options) {
417421
const list = this[LISTENERS].get(type)
418422
if (!list) return false
419-
const flags = getListenerFlags(options)
420423
for (let i = 0; i < list.length; i++) {
421424
const item = list[i]
422-
if (item._listener === listener && item._flags === flags) {
425+
if (item._listener === listener) {
423426
list.splice(i, 1)
424427
return true
425428
}
@@ -429,47 +432,33 @@ class Element extends Node {
429432

430433
dispatchEvent(event) {
431434
let target = (event.target = this)
432-
const path = (event.path = [this])
433-
while ((target = target.parentNode)) path.push(target)
435+
const bubblePath = []
434436
let defaultPrevented = false
435-
for (let i = path.length; i--; ) {
436-
if (
437-
fireEvent(
438-
event,
439-
path[i],
440-
i === 0 ? EVENTPHASE_AT_TARGET : EVENTPHASE_CAPTURE
441-
)
442-
) {
443-
defaultPrevented = true
444-
}
437+
438+
while (target != null) {
439+
bubblePath.push(target)
440+
target = target.parentNode
445441
}
446-
for (let i = 1; i < path.length; i++) {
447-
if (fireEvent(event, path[i], EVENTPHASE_BUBBLE)) {
442+
443+
for (let i = bubblePath.length; --i; ) {
444+
if (fireEvent(event, bubblePath[i], EVENTPHASE_CAPTURE)) {
448445
defaultPrevented = true
449446
}
450447
}
451-
return !defaultPrevented
452-
}
453448

454-
render() {
455-
const component = TYPES[this.localName].hostComponent
456-
const _self = this
457-
const reactElement = {
458-
type: component,
459-
props: {},
460-
ref: x => {
461-
if (!VIEWS_RENDERED) {
462-
VIEWS_RENDERED = true
449+
if (fireEvent(event, this, EVENTPHASE_AT_TARGET)) {
450+
defaultPrevented = true
451+
}
452+
453+
if (!event.cancelBubble) {
454+
for (let i = 1; i < bubblePath.length; i++) {
455+
if (fireEvent(event, bubblePath[i], EVENTPHASE_BUBBLE)) {
456+
defaultPrevented = true
463457
}
464-
_self.ref = x
465-
INSTANCES.set(this[BINDING], x)
466-
},
458+
}
467459
}
468460

469-
reactElement.props.children = (this.children || []).map(x => x.render())
470-
Object.assign(reactElement.props, Object.fromEntries(this[BINDING].props))
471-
reactElement.$$typeof = REACT_ELEMENT_TYPE
472-
return reactElement
461+
return !defaultPrevented
473462
}
474463
}
475464

@@ -710,7 +699,7 @@ export function createDOM(rootTag) {
710699
}
711700

712701
class Event {
713-
constructor(type, bubbles, cancelable, timeStamp) {
702+
constructor(type, bubbles, cancelable) {
714703
Object.defineProperty(this, IS_TRUSTED, { value: false })
715704
this.type = type
716705
this.bubbles = bubbles
@@ -720,16 +709,31 @@ class Event {
720709
this.currentTarget = null
721710
this.inPassiveListener = false
722711
this.defaultPrevented = false
723-
this.cancelBubble = false
712+
this._stopPropagation = false
724713
this.immediatePropagationStopped = false
725714
this.data = undefined
715+
this.timestamp = new Date().valueOf()
726716
}
717+
727718
get isTrusted() {
728719
return this[IS_TRUSTED]
729720
}
721+
722+
get cancelBubble() {
723+
return this._stopPropagation
724+
}
725+
726+
set cancelBubble(val) {
727+
if (val) {
728+
this._stopPropagation = true
729+
}
730+
}
731+
730732
stopPropagation() {
731733
this.cancelBubble = true
734+
this._stopPropagation = true
732735
}
736+
733737
stopImmediatePropagation() {
734738
this.immediatePropagationStopped = true
735739
}
@@ -744,61 +748,30 @@ class Event {
744748
}
745749
}
746750

747-
const EVENTPHASE_NONE = 0
748-
const EVENTPHASE_BUBBLE = 1
749-
const EVENTPHASE_CAPTURE = 2
750-
const EVENTPHASE_PASSIVE = 4
751-
const EVENTPHASE_AT_TARGET = 5
752-
const EVENTOPT_ONCE = 8
753-
754-
// Flags are easier to compare for listener lookups
755-
function getListenerFlags(options) {
756-
if (typeof options === 'object' && options) {
757-
let flags = options.capture ? EVENTPHASE_CAPTURE : EVENTPHASE_BUBBLE
758-
if (options.passive) flags &= EVENTPHASE_PASSIVE
759-
if (options.once) flags &= EVENTOPT_ONCE
760-
return flags
761-
}
762-
return options ? EVENTPHASE_CAPTURE : EVENTPHASE_BUBBLE
763-
}
764-
765751
function fireEvent(event, target, phase) {
766752
const list = target[LISTENERS].get(event.type)
767753
if (!list) return
768-
// let error;
754+
769755
let defaultPrevented = false
770-
// use forEach for freezing
771-
const frozen = list.slice()
772-
for (let i = 0; i < frozen.length; i++) {
773-
const item = frozen[i]
774-
const fn = item._listener
756+
757+
for (const item of Array.from(list)) {
775758
event.eventPhase = phase
776-
// the bridge is async, so events are always passive.
777-
//event.inPassiveListener = passive;
778759
event.currentTarget = target
779760
try {
780-
let ret = fn.call(target, event)
761+
let ret = item._listener.call(target, event)
781762
if (ret === false) {
782763
event.defaultPrevented = true
783764
}
784765
} catch (e) {
785-
//error = e;
786766
setTimeout(thrower, 0, e)
787767
}
788-
// @ts-ignore
789-
// FIXME: the binary shift is always going to be true, need to
790-
// handle based on options
791-
if (item._flags & (EVENTOPT_ONCE !== 0)) {
792-
// list.splice(list.indexOf(item), 1)
793-
}
794768
if (event.defaultPrevented === true) {
795769
defaultPrevented = true
796770
}
797771
if (event.immediatePropagationStopped) {
798772
break
799773
}
800774
}
801-
// if (error !== undefined) throw error;
802775
return defaultPrevented
803776
}
804777

dom/src/index.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export * from './dom'
2-
export * from './register-native-dom'
1+
export { createDOM } from './dom'
2+
export { registerNativeDOM } from './register-native-dom'

dom/src/register-native-dom.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { registerHostElement } from './dom'
2-
import { UIManager } from 'react-native'
2+
import * as UIManager from 'react-native/Libraries/ReactNative/UIManager'
33

44
const ITEMS = []
55

example/App.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ function App({ rootTag }) {
2626
const count = signal(0)
2727

2828
function Counter() {
29-
const handleClick = () => {
29+
const handleClick = e => {
30+
e.stopPropagation()
3031
count.value += 1
3132
}
3233

@@ -36,10 +37,13 @@ function Counter() {
3637
<Text fontSize={20} margin={10} color="white">
3738
{count.value}
3839
</Text>
39-
<View onClick={handleClick}>
40-
<Text fontSize={20} margin={10} color="white">
41-
Inc
42-
</Text>
40+
{/* Additional view to force check bubbling */}
41+
<View>
42+
<View onClick={handleClick}>
43+
<Text fontSize={20} margin={10} color="white">
44+
Inc
45+
</Text>
46+
</View>
4347
</View>
4448
</SafeAreaView>
4549
</>
@@ -106,7 +110,8 @@ class TestRenderable extends Component {
106110
justifyContent="center"
107111
alignItems="center"
108112
backgroundColor="white"
109-
onClick={() => {
113+
onClick={e => {
114+
e.stopPropagation()
110115
Alert.alert(`Oh hey, ${this.state.email}`)
111116
}}
112117
>

0 commit comments

Comments
 (0)