From a2a3c78b64c63f5de634d810fe73ce75046c76b3 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Fri, 11 Sep 2020 13:31:25 -0700 Subject: [PATCH 1/3] Remove SyntheticEvent wrappers and use the JS objects directly --- lib/react.dart | 517 +----------------- lib/react_client.dart | 17 +- lib/react_client/component_factory.dart | 20 +- lib/src/react_client/event_factory.dart | 365 ------------- .../event_prop_key_to_event_factory.dart | 122 ++--- lib/src/react_client/factory_util.dart | 58 +- .../synthetic_event_wrappers.dart | 2 + test/factory/common_factory_tests.dart | 66 +-- test/react_client_test.dart | 20 +- 9 files changed, 95 insertions(+), 1092 deletions(-) delete mode 100644 lib/src/react_client/event_factory.dart diff --git a/lib/react.dart b/lib/react.dart index eed0bddd..915e2bae 100644 --- a/lib/react.dart +++ b/lib/react.dart @@ -22,6 +22,7 @@ import 'package:react/src/react_client/private_utils.dart' show validateJsApiThe export 'package:react/src/context.dart'; export 'package:react/src/prop_validator.dart'; export 'package:react/react_client/react_interop.dart' show forwardRef, forwardRef2, createRef, memo, memo2; +export 'package:react/src/react_client/synthetic_event_wrappers.dart'; typedef Error PropValidator(TProps props, PropValidatorInfo info); @@ -1433,522 +1434,6 @@ class NotSpecified { const NotSpecified(); } -/// A cross-browser wrapper around the browser's [nativeEvent]. -/// -/// It has the same interface as the browser's native event, including [stopPropagation] and [preventDefault], except -/// the events work identically across all browsers. -/// -/// See: -class SyntheticEvent { - /// Indicates whether the [Event] bubbles up through the DOM or not. - /// - /// See: - final bool bubbles; - - /// Indicates whether the [Event] is cancelable or not. - /// - /// See: - final bool cancelable; - - /// Identifies the current target for the event, as the [Event] traverses the DOM. - /// - /// It always refers to the [Element] the [Event] handler has been attached to as opposed to [target] which identifies - /// the [Element] on which the [Event] occurred. - /// - /// See: - final /*DOMEventTarget*/ currentTarget; - - bool _defaultPrevented; - - dynamic _preventDefault; - - /// Indicates whether or not [preventDefault] was called on the event. - /// - /// See: - bool get defaultPrevented => _defaultPrevented; - - /// Cancels the [Event] if it is [cancelable], without stopping further propagation of the event. - /// - /// See: - void preventDefault() { - _defaultPrevented = true; - _preventDefault(); - } - - /// Prevents further propagation of the current event. - /// - /// See: - final dynamic stopPropagation; - - /// For use by react-dart internals only. - @protected - void Function() $$jsPersistDoNotSetThisOrYouWillBeFired; - bool _isPersistent = false; - - /// Whether the event instance has been removed from the ReactJS event pool. - /// - /// > See: [persist] - bool get isPersistent => _isPersistent; - - /// Call this method on the current event instance if you want to access the event properties in an asynchronous way. - /// - /// This will remove the synthetic event from the event pool and allow references - /// to the event to be retained by user code. - /// - /// For example, `setState` callbacks are fired after a component updates as a result of the - /// new state being passed in - and since component updates are not guaranteed to by synchronous, any - /// reference to a `SyntheticEvent` within that callback could have been recycled by ReactJS internals. - /// - /// You can use `persist()` to ensure access to the event properties within the callback as shown in - /// the second example below. - /// - /// __Without persist()__ - /// ```dart - /// void handleClick(SyntheticMouseEvent event) { - /// print(event.type); // => "click" - /// - /// setState({}, () { - /// print(event.type); // => null - /// print(event.isPersistent); => false - /// }); - /// } - /// ``` - /// - /// __With persist()__ - /// ```dart - /// void handleClick(SyntheticMouseEvent event) { - /// print(event.type); // => "click" - /// event.persist(); - /// - /// setState({}, () { - /// print(event.type); // => "click" - /// print(event.isPersistent); => true - /// }); - /// } - /// ``` - /// - /// See: - void persist() { - if ($$jsPersistDoNotSetThisOrYouWillBeFired != null) { - _isPersistent = true; - $$jsPersistDoNotSetThisOrYouWillBeFired(); - } - } - - /// Indicates which phase of the [Event] flow is currently being evaluated. - /// - /// Possible values: - /// - /// > [Event.CAPTURING_PHASE] (1) - The [Event] is being propagated through the [target]'s ancestor objects. This - /// process starts with the Window, then [HtmlDocument], then the [HtmlHtmlElement], and so on through the [Element]s - /// until the [target]'s parent is reached. Event listeners registered for capture mode when - /// [EventTarget.addEventListener] was called are triggered during this phase. - /// - /// > [Event.AT_TARGET] (2) - The [Event] has arrived at the [target]. Event listeners registered for this phase are - /// called at this time. If [bubbles] is `false`, processing the [Event] is finished after this phase is complete. - /// - /// > [Event.BUBBLING_PHASE] (3) - The [Event] is propagating back up through the [target]'s ancestors in reverse - /// order, starting with the parent, and eventually reaching the containing Window. This is known as bubbling, and - /// occurs only if [bubbles] is `true`. [Event] listeners registered for this phase are triggered during this process. - /// - /// See: - final num eventPhase; - - /// Is `true` when the [Event] was generated by a user action, and `false` when the [Event] was created or modified - /// by a script or dispatched via [Event.dispatchEvent]. - /// - /// __Read Only__ - /// - /// See: - final bool isTrusted; - - /// Native browser event that [SyntheticEvent] wraps around. - final /*DOMEvent*/ nativeEvent; - - /// A reference to the object that dispatched the event. It is different from [currentTarget] when the [Event] - /// handler is called when [eventPhase] is [Event.BUBBLING_PHASE] or [Event.CAPTURING_PHASE]. - /// - /// See: - final /*DOMEventTarget*/ target; - - /// Returns the [Time] (in milliseconds) at which the [Event] was created. - /// - /// _Starting with Chrome 49, returns a high-resolution monotonic time instead of epoch time._ - /// - /// See: - final num timeStamp; - - /// Returns a string containing the type of event. It is set when the [Event] is constructed and is the name commonly - /// used to refer to the specific event. - /// - /// See: - final String type; - - SyntheticEvent( - this.bubbles, - this.cancelable, - this.currentTarget, - this._defaultPrevented, - this._preventDefault, - this.stopPropagation, - this.eventPhase, - this.isTrusted, - this.nativeEvent, - this.target, - this.timeStamp, - this.type) {} -} - -class SyntheticClipboardEvent extends SyntheticEvent { - final clipboardData; - - SyntheticClipboardEvent( - bool bubbles, - bool cancelable, - dynamic currentTarget, - bool defaultPrevented, - dynamic preventDefault, - dynamic stopPropagation, - num eventPhase, - bool isTrusted, - dynamic nativeEvent, - dynamic target, - num timeStamp, - String type, - this.clipboardData, - ) : super(bubbles, cancelable, currentTarget, defaultPrevented, preventDefault, stopPropagation, eventPhase, - isTrusted, nativeEvent, target, timeStamp, type) {} -} - -class SyntheticKeyboardEvent extends SyntheticEvent { - final bool altKey; - final String char; - final bool ctrlKey; - final String locale; - final num location; - final String key; - final bool metaKey; - final bool repeat; - final bool shiftKey; - final num keyCode; - final num charCode; - - SyntheticKeyboardEvent( - bool bubbles, - bool cancelable, - dynamic currentTarget, - bool defaultPrevented, - dynamic preventDefault, - dynamic stopPropagation, - num eventPhase, - bool isTrusted, - dynamic nativeEvent, - dynamic target, - num timeStamp, - String type, - this.altKey, - this.char, - this.charCode, - this.ctrlKey, - this.locale, - this.location, - this.key, - this.keyCode, - this.metaKey, - this.repeat, - this.shiftKey, - ) : super(bubbles, cancelable, currentTarget, defaultPrevented, preventDefault, stopPropagation, eventPhase, - isTrusted, nativeEvent, target, timeStamp, type) {} -} - -class SyntheticCompositionEvent extends SyntheticEvent { - final String data; - - SyntheticCompositionEvent( - bool bubbles, - bool cancelable, - dynamic currentTarget, - bool defaultPrevented, - dynamic preventDefault, - dynamic stopPropagation, - num eventPhase, - bool isTrusted, - dynamic nativeEvent, - dynamic target, - num timeStamp, - String type, - this.data, - ) : super(bubbles, cancelable, currentTarget, defaultPrevented, preventDefault, stopPropagation, eventPhase, - isTrusted, nativeEvent, target, timeStamp, type) {} -} - -class SyntheticFocusEvent extends SyntheticEvent { - final /*DOMEventTarget*/ relatedTarget; - - SyntheticFocusEvent( - bool bubbles, - bool cancelable, - dynamic currentTarget, - bool defaultPrevented, - dynamic preventDefault, - dynamic stopPropagation, - num eventPhase, - bool isTrusted, - dynamic nativeEvent, - dynamic target, - num timeStamp, - String type, - this.relatedTarget, - ) : super(bubbles, cancelable, currentTarget, defaultPrevented, preventDefault, stopPropagation, eventPhase, - isTrusted, nativeEvent, target, timeStamp, type) {} -} - -class SyntheticFormEvent extends SyntheticEvent { - SyntheticFormEvent( - bool bubbles, - bool cancelable, - dynamic currentTarget, - bool defaultPrevented, - dynamic preventDefault, - dynamic stopPropagation, - num eventPhase, - bool isTrusted, - dynamic nativeEvent, - dynamic target, - num timeStamp, - String type, - ) : super(bubbles, cancelable, currentTarget, defaultPrevented, preventDefault, stopPropagation, eventPhase, - isTrusted, nativeEvent, target, timeStamp, type) {} -} - -class SyntheticDataTransfer { - final String dropEffect; - final String effectAllowed; - final List files; - final List types; - - SyntheticDataTransfer(this.dropEffect, this.effectAllowed, this.files, this.types); -} - -class SyntheticMouseEvent extends SyntheticEvent { - final bool altKey; - final num button; - final num buttons; - final num clientX; - final num clientY; - final bool ctrlKey; - final SyntheticDataTransfer dataTransfer; - final bool metaKey; - final num pageX; - final num pageY; - final /*DOMEventTarget*/ relatedTarget; - final num screenX; - final num screenY; - final bool shiftKey; - - SyntheticMouseEvent( - bool bubbles, - bool cancelable, - dynamic currentTarget, - bool defaultPrevented, - dynamic preventDefault, - dynamic stopPropagation, - num eventPhase, - bool isTrusted, - dynamic nativeEvent, - dynamic target, - num timeStamp, - String type, - this.altKey, - this.button, - this.buttons, - this.clientX, - this.clientY, - this.ctrlKey, - this.dataTransfer, - this.metaKey, - this.pageX, - this.pageY, - this.relatedTarget, - this.screenX, - this.screenY, - this.shiftKey, - ) : super(bubbles, cancelable, currentTarget, defaultPrevented, preventDefault, stopPropagation, eventPhase, - isTrusted, nativeEvent, target, timeStamp, type) {} -} - -class SyntheticPointerEvent extends SyntheticEvent { - final num pointerId; - final num width; - final num height; - final num pressure; - final num tangentialPressure; - final num tiltX; - final num tiltY; - final num twist; - final String pointerType; - final bool isPrimary; - - SyntheticPointerEvent( - bool bubbles, - bool cancelable, - dynamic currentTarget, - bool defaultPrevented, - dynamic preventDefault, - dynamic stopPropagation, - num eventPhase, - bool isTrusted, - dynamic nativeEvent, - dynamic target, - num timeStamp, - String type, - this.pointerId, - this.width, - this.height, - this.pressure, - this.tangentialPressure, - this.tiltX, - this.tiltY, - this.twist, - this.pointerType, - this.isPrimary, - ) : super(bubbles, cancelable, currentTarget, defaultPrevented, preventDefault, stopPropagation, eventPhase, - isTrusted, nativeEvent, target, timeStamp, type) {} -} - -class SyntheticTouchEvent extends SyntheticEvent { - final bool altKey; - final /*DOMTouchList*/ changedTouches; - final bool ctrlKey; - final bool metaKey; - final bool shiftKey; - final /*DOMTouchList*/ targetTouches; - final /*DOMTouchList*/ touches; - - SyntheticTouchEvent( - bool bubbles, - bool cancelable, - dynamic currentTarget, - bool defaultPrevented, - dynamic preventDefault, - dynamic stopPropagation, - num eventPhase, - bool isTrusted, - dynamic nativeEvent, - dynamic target, - num timeStamp, - String type, - this.altKey, - this.changedTouches, - this.ctrlKey, - this.metaKey, - this.shiftKey, - this.targetTouches, - this.touches, - ) : super(bubbles, cancelable, currentTarget, defaultPrevented, preventDefault, stopPropagation, eventPhase, - isTrusted, nativeEvent, target, timeStamp, type) {} -} - -class SyntheticTransitionEvent extends SyntheticEvent { - final String propertyName; - final num elapsedTime; - final String pseudoElement; - - SyntheticTransitionEvent( - bubbles, - cancelable, - currentTarget, - _defaultPrevented, - _preventDefault, - stopPropagation, - eventPhase, - isTrusted, - nativeEvent, - target, - timeStamp, - type, - this.propertyName, - this.elapsedTime, - this.pseudoElement) - : super(bubbles, cancelable, currentTarget, _defaultPrevented, _preventDefault, stopPropagation, eventPhase, - isTrusted, nativeEvent, target, timeStamp, type) {} -} - -class SyntheticAnimationEvent extends SyntheticEvent { - final String animationName; - final num elapsedTime; - final String pseudoElement; - - SyntheticAnimationEvent( - bubbles, - cancelable, - currentTarget, - _defaultPrevented, - _preventDefault, - stopPropagation, - eventPhase, - isTrusted, - nativeEvent, - target, - timeStamp, - type, - this.animationName, - this.elapsedTime, - this.pseudoElement) - : super(bubbles, cancelable, currentTarget, _defaultPrevented, _preventDefault, stopPropagation, eventPhase, - isTrusted, nativeEvent, target, timeStamp, type) {} -} - -class SyntheticUIEvent extends SyntheticEvent { - final num detail; - final /*DOMAbstractView*/ view; - - SyntheticUIEvent( - bool bubbles, - bool cancelable, - dynamic currentTarget, - bool _defaultPrevented, - dynamic _preventDefault, - dynamic stopPropagation, - num eventPhase, - bool isTrusted, - dynamic nativeEvent, - dynamic target, - num timeStamp, - String type, - this.detail, - this.view, - ) : super(bubbles, cancelable, currentTarget, _defaultPrevented, _preventDefault, stopPropagation, eventPhase, - isTrusted, nativeEvent, target, timeStamp, type) {} -} - -class SyntheticWheelEvent extends SyntheticEvent { - final num deltaX; - final num deltaMode; - final num deltaY; - final num deltaZ; - - SyntheticWheelEvent( - bool bubbles, - bool cancelable, - dynamic currentTarget, - bool defaultPrevented, - dynamic preventDefault, - dynamic stopPropagation, - num eventPhase, - bool isTrusted, - dynamic nativeEvent, - dynamic target, - num timeStamp, - String type, - this.deltaX, - this.deltaMode, - this.deltaY, - this.deltaZ, - ) : super(bubbles, cancelable, currentTarget, defaultPrevented, preventDefault, stopPropagation, eventPhase, - isTrusted, nativeEvent, target, timeStamp, type) {} -} - /// Registers [componentFactory] on both client and server. @Deprecated('Use registerComponent2 after migrating your components from Component to Component2.') /*ComponentRegistrar*/ Function registerComponent = validateJsApiThenReturn(() => registration_utils.registerComponent); diff --git a/lib/react_client.dart b/lib/react_client.dart index b850e9fb..2787fedc 100644 --- a/lib/react_client.dart +++ b/lib/react_client.dart @@ -21,22 +21,7 @@ export 'package:react/react_client/component_factory.dart' JsBackedMapComponentFactoryMixin; export 'package:react/react_client/zone.dart' show componentZone; export 'package:react/src/react_client/chain_refs.dart' show chainRefs, chainRefList; -export 'package:react/src/react_client/event_factory.dart' - show - syntheticEventFactory, - syntheticClipboardEventFactory, - syntheticCompositionEventFactory, - syntheticKeyboardEventFactory, - syntheticFocusEventFactory, - syntheticFormEventFactory, - syntheticDataTransferFactory, - syntheticPointerEventFactory, - syntheticMouseEventFactory, - syntheticTouchEventFactory, - syntheticTransitionEventFactory, - syntheticAnimationEventFactory, - syntheticUIEventFactory, - syntheticWheelEventFactory; + export 'package:react/src/typedefs.dart' show JsFunctionComponent; /// Method used to initialize the React environment. diff --git a/lib/react_client/component_factory.dart b/lib/react_client/component_factory.dart index 4cb4fe29..c686702c 100644 --- a/lib/react_client/component_factory.dart +++ b/lib/react_client/component_factory.dart @@ -14,7 +14,6 @@ import 'package:react/src/context.dart'; import 'package:react/src/ddc_emulated_function_name_bug.dart' as ddc_emulated_function_name_bug; import 'package:react/src/js_interop_util.dart'; import 'package:react/src/typedefs.dart'; -import 'package:react/src/react_client/event_prop_key_to_event_factory.dart'; import 'package:react/src/react_client/factory_util.dart'; export 'package:react/src/react_client/factory_util.dart' show unconvertJsEventHandler; @@ -58,12 +57,6 @@ Map unconvertJsProps(/* ReactElement|ReactComponent */ instance) { throw new ArgumentError('A Dart Component cannot be passed into unconvertJsProps.'); } - eventPropKeyToEventFactory.keys.forEach((key) { - if (props.containsKey(key)) { - props[key] = unconvertJsEventHandler(props[key]) ?? props[key]; - } - }); - // Convert the nested style map so it can be read by Dart code. var style = props['style']; if (style != null) { @@ -82,8 +75,7 @@ mixin JsBackedMapComponentFactoryMixin on ReactComponentFactoryProxy { return React.createElement(type, convertedProps, children); } - static JsMap generateExtendedJsProps(Map props) => - generateJsProps(props, shouldConvertEventHandlers: false, wrapWithJsify: false); + static JsMap generateExtendedJsProps(Map props) => generateJsProps(props, wrapWithJsify: false); } /// Use [ReactDartComponentFactoryProxy2] instead by calling [registerComponent2]. @@ -199,8 +191,7 @@ class ReactDartComponentFactoryProxy2 extends Rea /// Returns a JavaScript version of the specified [props], preprocessed for consumption by ReactJS and prepared for /// consumption by the `react` library internals. - static JsMap generateExtendedJsProps(Map props) => - generateJsProps(props, shouldConvertEventHandlers: false, wrapWithJsify: false); + static JsMap generateExtendedJsProps(Map props) => generateJsProps(props, wrapWithJsify: false); } /// Creates ReactJS [ReactElement] instances for `JSContext` components. @@ -287,10 +278,8 @@ class ReactJsComponentFactoryProxy extends ReactComponentFactoryProxy { @override ReactElement build(Map props, [List childrenArgs]) { dynamic children = generateChildren(childrenArgs, shouldAlwaysBeList: alwaysReturnChildrenAsList); - JsMap convertedProps = generateJsProps(props, - shouldConvertEventHandlers: shouldConvertDomProps, - convertCallbackRefValue: false, - additionalRefPropKeys: _additionalRefPropKeys); + JsMap convertedProps = + generateJsProps(props, convertCallbackRefValue: false, additionalRefPropKeys: _additionalRefPropKeys); return React.createElement(type, convertedProps, children); } } @@ -321,7 +310,6 @@ class ReactDomComponentFactoryProxy extends ReactComponentFactoryProxy { /// Performs special handling of certain props for consumption by ReactJS DOM components. static void convertProps(Map props) { - convertEventHandlers(props); convertRefValue(props); } } diff --git a/lib/src/react_client/event_factory.dart b/lib/src/react_client/event_factory.dart deleted file mode 100644 index 2f2ffcda..00000000 --- a/lib/src/react_client/event_factory.dart +++ /dev/null @@ -1,365 +0,0 @@ -import 'dart:html'; - -import 'package:react/react.dart'; -import 'package:react/src/react_client/synthetic_event_wrappers.dart' as events; - -/// Wrapper for [SyntheticEvent]. -SyntheticEvent syntheticEventFactory(events.SyntheticEvent e) { - return new SyntheticEvent(e.bubbles, e.cancelable, e.currentTarget, e.defaultPrevented, () => e.preventDefault(), - () => e.stopPropagation(), e.eventPhase, e.isTrusted, e.nativeEvent, e.target, e.timeStamp, e.type) - // ignore: invalid_use_of_protected_member - ..$$jsPersistDoNotSetThisOrYouWillBeFired = () => e.persist(); -} - -/// Wrapper for [SyntheticClipboardEvent]. -SyntheticClipboardEvent syntheticClipboardEventFactory(events.SyntheticClipboardEvent e) { - return new SyntheticClipboardEvent( - e.bubbles, - e.cancelable, - e.currentTarget, - e.defaultPrevented, - () => e.preventDefault(), - () => e.stopPropagation(), - e.eventPhase, - e.isTrusted, - e.nativeEvent, - e.target, - e.timeStamp, - e.type, - e.clipboardData) - // ignore: invalid_use_of_protected_member - ..$$jsPersistDoNotSetThisOrYouWillBeFired = () => e.persist(); -} - -/// Wrapper for [SyntheticCompositionEvent]. -SyntheticCompositionEvent syntheticCompositionEventFactory(events.SyntheticCompositionEvent e) { - return SyntheticCompositionEvent( - e.bubbles, - e.cancelable, - e.currentTarget, - e.defaultPrevented, - () => e.preventDefault(), - () => e.stopPropagation(), - e.eventPhase, - e.isTrusted, - e.nativeEvent, - e.target, - e.timeStamp, - e.type, - e.data) - // ignore: invalid_use_of_protected_member - ..$$jsPersistDoNotSetThisOrYouWillBeFired = () => e.persist(); -} - -/// Wrapper for [SyntheticKeyboardEvent]. -SyntheticKeyboardEvent syntheticKeyboardEventFactory(events.SyntheticKeyboardEvent e) { - return new SyntheticKeyboardEvent( - e.bubbles, - e.cancelable, - e.currentTarget, - e.defaultPrevented, - () => e.preventDefault(), - () => e.stopPropagation(), - e.eventPhase, - e.isTrusted, - e.nativeEvent, - e.target, - e.timeStamp, - e.type, - e.altKey, - e.char, - e.charCode, - e.ctrlKey, - e.locale, - e.location, - e.key, - e.keyCode, - e.metaKey, - e.repeat, - e.shiftKey) - // ignore: invalid_use_of_protected_member - ..$$jsPersistDoNotSetThisOrYouWillBeFired = () => e.persist(); -} - -/// Wrapper for [SyntheticFocusEvent]. -SyntheticFocusEvent syntheticFocusEventFactory(events.SyntheticFocusEvent e) { - return new SyntheticFocusEvent( - e.bubbles, - e.cancelable, - e.currentTarget, - e.defaultPrevented, - () => e.preventDefault(), - () => e.stopPropagation(), - e.eventPhase, - e.isTrusted, - e.nativeEvent, - e.target, - e.timeStamp, - e.type, - e.relatedTarget) - // ignore: invalid_use_of_protected_member - ..$$jsPersistDoNotSetThisOrYouWillBeFired = () => e.persist(); -} - -/// Wrapper for [SyntheticFormEvent]. -SyntheticFormEvent syntheticFormEventFactory(events.SyntheticFormEvent e) { - return new SyntheticFormEvent(e.bubbles, e.cancelable, e.currentTarget, e.defaultPrevented, () => e.preventDefault(), - () => e.stopPropagation(), e.eventPhase, e.isTrusted, e.nativeEvent, e.target, e.timeStamp, e.type) - // ignore: invalid_use_of_protected_member - ..$$jsPersistDoNotSetThisOrYouWillBeFired = () => e.persist(); -} - -/// Wrapper for [SyntheticDataTransfer]. -/// -/// [dt] is typed as Object instead of [dynamic] to avoid dynamic calls in the method body, -/// ensuring the code is statically sound. -SyntheticDataTransfer syntheticDataTransferFactory(Object dt) { - if (dt == null) return null; - - List rawFiles; - List rawTypes; - - String effectAllowed; - String dropEffect; - - // Handle `dt` being either a native DOM DataTransfer object or a JS object that looks like it (events.NonNativeDataTransfer). - // Casting a JS object to DataTransfer fails intermittently in dart2js, and vice-versa fails intermittently in either DDC or dart2js. - // TODO figure out when NonNativeDataTransfer is used. - // - // Some logic here is duplicated to ensure statically-sound access of same-named members. - if (dt is DataTransfer) { - rawFiles = dt.files; - rawTypes = dt.types; - - try { - // Works around a bug in IE where dragging from outside the browser fails. - // Trying to access this property throws the error "Unexpected call to method or property access.". - effectAllowed = dt.effectAllowed; - } catch (_) { - effectAllowed = 'uninitialized'; - } - try { - // For certain types of drag events in IE (anything but ondragenter, ondragover, and ondrop), this fails. - // Trying to access this property throws the error "Unexpected call to method or property access.". - dropEffect = dt.dropEffect; - } catch (_) { - dropEffect = 'none'; - } - } else { - // Assume it's a NonNativeDataTransfer otherwise. - // Perform a cast inside `else` instead of an `else if (dt is ...)` since is-checks for - // anonymous JS objects have undefined behavior. - final castedDt = dt as events.NonNativeDataTransfer; - - rawFiles = castedDt.files; - rawTypes = castedDt.types; - - try { - // Works around a bug in IE where dragging from outside the browser fails. - // Trying to access this property throws the error "Unexpected call to method or property access.". - effectAllowed = castedDt.effectAllowed; - } catch (_) { - effectAllowed = 'uninitialized'; - } - try { - // For certain types of drag events in IE (anything but ondragenter, ondragover, and ondrop), this fails. - // Trying to access this property throws the error "Unexpected call to method or property access.". - dropEffect = castedDt.dropEffect; - } catch (_) { - dropEffect = 'none'; - } - } - - // Copy these lists and ensure they're typed properly. - // todo use .cast() in Dart 2 - final files = []; - final types = []; - rawFiles?.forEach(files.add); - rawTypes?.forEach(types.add); - - return new SyntheticDataTransfer(dropEffect, effectAllowed, files, types); -} - -/// Wrapper for [SyntheticPointerEvent]. -SyntheticPointerEvent syntheticPointerEventFactory(events.SyntheticPointerEvent e) { - return new SyntheticPointerEvent( - e.bubbles, - e.cancelable, - e.currentTarget, - e.defaultPrevented, - () => e.preventDefault(), - () => e.stopPropagation(), - e.eventPhase, - e.isTrusted, - e.nativeEvent, - e.target, - e.timeStamp, - e.type, - e.pointerId, - e.width, - e.height, - e.pressure, - e.tangentialPressure, - e.tiltX, - e.tiltY, - e.twist, - e.pointerType, - e.isPrimary, - ) - // ignore: invalid_use_of_protected_member - ..$$jsPersistDoNotSetThisOrYouWillBeFired = () => e.persist(); -} - -/// Wrapper for [SyntheticMouseEvent]. -SyntheticMouseEvent syntheticMouseEventFactory(events.SyntheticMouseEvent e) { - SyntheticDataTransfer dt = syntheticDataTransferFactory(e.dataTransfer); - return new SyntheticMouseEvent( - e.bubbles, - e.cancelable, - e.currentTarget, - e.defaultPrevented, - () => e.preventDefault(), - () => e.stopPropagation(), - e.eventPhase, - e.isTrusted, - e.nativeEvent, - e.target, - e.timeStamp, - e.type, - e.altKey, - e.button, - e.buttons, - e.clientX, - e.clientY, - e.ctrlKey, - dt, - e.metaKey, - e.pageX, - e.pageY, - e.relatedTarget, - e.screenX, - e.screenY, - e.shiftKey, - ) - // ignore: invalid_use_of_protected_member - ..$$jsPersistDoNotSetThisOrYouWillBeFired = () => e.persist(); -} - -/// Wrapper for [SyntheticTouchEvent]. -SyntheticTouchEvent syntheticTouchEventFactory(events.SyntheticTouchEvent e) { - return new SyntheticTouchEvent( - e.bubbles, - e.cancelable, - e.currentTarget, - e.defaultPrevented, - () => e.preventDefault(), - () => e.stopPropagation(), - e.eventPhase, - e.isTrusted, - e.nativeEvent, - e.target, - e.timeStamp, - e.type, - e.altKey, - e.changedTouches, - e.ctrlKey, - e.metaKey, - e.shiftKey, - e.targetTouches, - e.touches, - ) - // ignore: invalid_use_of_protected_member - ..$$jsPersistDoNotSetThisOrYouWillBeFired = () => e.persist(); -} - -/// Wrapper for [SyntheticTransitionEvent]. -SyntheticTransitionEvent syntheticTransitionEventFactory(events.SyntheticTransitionEvent e) { - return new SyntheticTransitionEvent( - e.bubbles, - e.cancelable, - e.currentTarget, - e.defaultPrevented, - () => e.preventDefault(), - () => e.stopPropagation(), - e.eventPhase, - e.isTrusted, - e.nativeEvent, - e.target, - e.timeStamp, - e.type, - e.propertyName, - e.elapsedTime, - e.pseudoElement, - ) - // ignore: invalid_use_of_protected_member - ..$$jsPersistDoNotSetThisOrYouWillBeFired = () => e.persist(); -} - -/// Wrapper for [SyntheticAnimationEvent]. -SyntheticAnimationEvent syntheticAnimationEventFactory(events.SyntheticAnimationEvent e) { - return new SyntheticAnimationEvent( - e.bubbles, - e.cancelable, - e.currentTarget, - e.defaultPrevented, - () => e.preventDefault(), - () => e.stopPropagation(), - e.eventPhase, - e.isTrusted, - e.nativeEvent, - e.target, - e.timeStamp, - e.type, - e.animationName, - e.elapsedTime, - e.pseudoElement, - ) - // ignore: invalid_use_of_protected_member - ..$$jsPersistDoNotSetThisOrYouWillBeFired = () => e.persist(); -} - -/// Wrapper for [SyntheticUIEvent]. -SyntheticUIEvent syntheticUIEventFactory(events.SyntheticUIEvent e) { - return new SyntheticUIEvent( - e.bubbles, - e.cancelable, - e.currentTarget, - e.defaultPrevented, - () => e.preventDefault(), - () => e.stopPropagation(), - e.eventPhase, - e.isTrusted, - e.nativeEvent, - e.target, - e.timeStamp, - e.type, - e.detail, - e.view, - ) - // ignore: invalid_use_of_protected_member - ..$$jsPersistDoNotSetThisOrYouWillBeFired = () => e.persist(); -} - -/// Wrapper for [SyntheticWheelEvent]. -SyntheticWheelEvent syntheticWheelEventFactory(events.SyntheticWheelEvent e) { - return new SyntheticWheelEvent( - e.bubbles, - e.cancelable, - e.currentTarget, - e.defaultPrevented, - () => e.preventDefault(), - () => e.stopPropagation(), - e.eventPhase, - e.isTrusted, - e.nativeEvent, - e.target, - e.timeStamp, - e.type, - e.deltaX, - e.deltaMode, - e.deltaY, - e.deltaZ, - ) - // ignore: invalid_use_of_protected_member - ..$$jsPersistDoNotSetThisOrYouWillBeFired = () => e.persist(); -} diff --git a/lib/src/react_client/event_prop_key_to_event_factory.dart b/lib/src/react_client/event_prop_key_to_event_factory.dart index 49473cc5..64084c85 100644 --- a/lib/src/react_client/event_prop_key_to_event_factory.dart +++ b/lib/src/react_client/event_prop_key_to_event_factory.dart @@ -1,92 +1,88 @@ -import 'package:react/react_client.dart'; - -/// A mapping from event prop keys to their respective event factories. -/// -/// Used in [_convertEventHandlers] for efficient event handler conversion. -final Map eventPropKeyToEventFactory = (() { - var _eventPropKeyToEventFactory = { +/// Keys for React DOM event handlers. +final Set knownEventKeys = (() { + final _knownEventKeys = { // SyntheticClipboardEvent - 'onCopy': syntheticClipboardEventFactory, - 'onCut': syntheticClipboardEventFactory, - 'onPaste': syntheticClipboardEventFactory, + 'onCopy', + 'onCut', + 'onPaste', // SyntheticKeyboardEvent - 'onKeyDown': syntheticKeyboardEventFactory, - 'onKeyPress': syntheticKeyboardEventFactory, - 'onKeyUp': syntheticKeyboardEventFactory, + 'onKeyDown', + 'onKeyPress', + 'onKeyUp', // SyntheticCompositionEvent - 'onCompositionStart': syntheticCompositionEventFactory, - 'onCompositionUpdate': syntheticCompositionEventFactory, - 'onCompositionEnd': syntheticCompositionEventFactory, + 'onCompositionStart', + 'onCompositionUpdate', + 'onCompositionEnd', // SyntheticFocusEvent - 'onFocus': syntheticFocusEventFactory, - 'onBlur': syntheticFocusEventFactory, + 'onFocus', + 'onBlur', // SyntheticFormEvent - 'onChange': syntheticFormEventFactory, - 'onInput': syntheticFormEventFactory, - 'onSubmit': syntheticFormEventFactory, - 'onReset': syntheticFormEventFactory, + 'onChange', + 'onInput', + 'onSubmit', + 'onReset', // SyntheticMouseEvent - 'onClick': syntheticMouseEventFactory, - 'onContextMenu': syntheticMouseEventFactory, - 'onDoubleClick': syntheticMouseEventFactory, - 'onDrag': syntheticMouseEventFactory, - 'onDragEnd': syntheticMouseEventFactory, - 'onDragEnter': syntheticMouseEventFactory, - 'onDragExit': syntheticMouseEventFactory, - 'onDragLeave': syntheticMouseEventFactory, - 'onDragOver': syntheticMouseEventFactory, - 'onDragStart': syntheticMouseEventFactory, - 'onDrop': syntheticMouseEventFactory, - 'onMouseDown': syntheticMouseEventFactory, - 'onMouseEnter': syntheticMouseEventFactory, - 'onMouseLeave': syntheticMouseEventFactory, - 'onMouseMove': syntheticMouseEventFactory, - 'onMouseOut': syntheticMouseEventFactory, - 'onMouseOver': syntheticMouseEventFactory, - 'onMouseUp': syntheticMouseEventFactory, + 'onClick', + 'onContextMenu', + 'onDoubleClick', + 'onDrag', + 'onDragEnd', + 'onDragEnter', + 'onDragExit', + 'onDragLeave', + 'onDragOver', + 'onDragStart', + 'onDrop', + 'onMouseDown', + 'onMouseEnter', + 'onMouseLeave', + 'onMouseMove', + 'onMouseOut', + 'onMouseOver', + 'onMouseUp', // SyntheticPointerEvent - 'onGotPointerCapture': syntheticPointerEventFactory, - 'onLostPointerCapture': syntheticPointerEventFactory, - 'onPointerCancel': syntheticPointerEventFactory, - 'onPointerDown': syntheticPointerEventFactory, - 'onPointerEnter': syntheticPointerEventFactory, - 'onPointerLeave': syntheticPointerEventFactory, - 'onPointerMove': syntheticPointerEventFactory, - 'onPointerOver': syntheticPointerEventFactory, - 'onPointerOut': syntheticPointerEventFactory, - 'onPointerUp': syntheticPointerEventFactory, + 'onGotPointerCapture', + 'onLostPointerCapture', + 'onPointerCancel', + 'onPointerDown', + 'onPointerEnter', + 'onPointerLeave', + 'onPointerMove', + 'onPointerOver', + 'onPointerOut', + 'onPointerUp', // SyntheticTouchEvent - 'onTouchCancel': syntheticTouchEventFactory, - 'onTouchEnd': syntheticTouchEventFactory, - 'onTouchMove': syntheticTouchEventFactory, - 'onTouchStart': syntheticTouchEventFactory, + 'onTouchCancel', + 'onTouchEnd', + 'onTouchMove', + 'onTouchStart', // SyntheticTransitionEvent - 'onTransitionEnd': syntheticTransitionEventFactory, + 'onTransitionEnd', // SyntheticAnimationEvent - 'onAnimationEnd': syntheticAnimationEventFactory, - 'onAnimationIteration': syntheticAnimationEventFactory, - 'onAnimationStart': syntheticAnimationEventFactory, + 'onAnimationEnd', + 'onAnimationIteration', + 'onAnimationStart', // SyntheticUIEvent - 'onScroll': syntheticUIEventFactory, + 'onScroll', // SyntheticWheelEvent - 'onWheel': syntheticWheelEventFactory, + 'onWheel', }; // Add support for capturing variants; e.g., onClick/onClickCapture - for (var key in _eventPropKeyToEventFactory.keys.toList()) { - _eventPropKeyToEventFactory[key + 'Capture'] = _eventPropKeyToEventFactory[key]; + for (var key in _knownEventKeys.toList()) { + _knownEventKeys.add(key + 'Capture'); } - return _eventPropKeyToEventFactory; + return _knownEventKeys; })(); diff --git a/lib/src/react_client/factory_util.dart b/lib/src/react_client/factory_util.dart index eeb63185..b461e1ee 100644 --- a/lib/src/react_client/factory_util.dart +++ b/lib/src/react_client/factory_util.dart @@ -5,17 +5,13 @@ import 'dart:js'; import 'package:js/js.dart'; -import 'package:react/react.dart'; import 'package:react/react_client/component_factory.dart'; import 'package:react/react_client/js_backed_map.dart'; import 'package:react/react_client/js_interop_helpers.dart'; import 'package:react/react_client/react_interop.dart'; -import 'package:react/src/react_client/synthetic_event_wrappers.dart' as events; import 'package:react/src/typedefs.dart'; -import 'event_prop_key_to_event_factory.dart'; - /// Converts a list of variadic children arguments to children that should be passed to ReactJS. /// /// Returns: @@ -34,53 +30,8 @@ dynamic convertArgsToChildren(List childrenArgs) { } } -/// A mapping from converted/wrapped JS handler functions (the result of [_convertEventHandlers]) -/// to the original Dart functions (the input of [_convertEventHandlers]). -final Expando originalEventHandler = new Expando(); - -/// Convert packed event handler into wrapper and pass it only the Dart [SyntheticEvent] object converted from the -/// [events.SyntheticEvent] event. -convertEventHandlers(Map args) { - args.forEach((propKey, value) { - var eventFactory = eventPropKeyToEventFactory[propKey]; - if (eventFactory != null && value != null) { - // Don't attempt to convert functions that have already been converted, or functions - // that were passed in as JS props. - final handlerHasAlreadyBeenConverted = unconvertJsEventHandler(value) != null; - if (!handlerHasAlreadyBeenConverted && !(isRawJsFunctionFromProps[value] ?? false)) { - // Apply allowInterop here so that the function we store in [_originalEventHandlers] - // is the same one we'll retrieve from the JS props. - var reactDartConvertedEventHandler = allowInterop((e, [_, __]) { - // To support Dart code calling converted handlers, - // check for Dart events and pass them through directly. - // Otherwise, convert the JS events like normal. - if (e is SyntheticEvent) { - value(e); - } else { - value(eventFactory(e as events.SyntheticEvent)); - } - }); - - args[propKey] = reactDartConvertedEventHandler; - originalEventHandler[reactDartConvertedEventHandler] = value; - } - } - }); -} - -/// Returns the original Dart handler function that, within [convertEventHandlers], -/// was converted/wrapped into the function [jsConvertedEventHandler] to be passed to the JS. -/// -/// Returns `null` if [jsConvertedEventHandler] is `null`. -/// -/// Returns `null` if [jsConvertedEventHandler] does not represent such a function -/// -/// Useful for chaining event handlers on DOM or JS composite [ReactElement]s. -Function unconvertJsEventHandler(Function jsConvertedEventHandler) { - if (jsConvertedEventHandler == null) return null; - - return originalEventHandler[jsConvertedEventHandler]; -} +@Deprecated('Event handlers are no longer converted') +Function unconvertJsEventHandler(Function jsConvertedEventHandler) => null; void convertRefValue(Map args) { var ref = args['ref']; @@ -159,14 +110,11 @@ dynamic generateChildren(List childrenArgs, {bool shouldAlwaysBeList = false}) { /// Converts [props] into a [JsMap] that can be utilized with `React.createElement()`. JsMap generateJsProps(Map props, - {bool shouldConvertEventHandlers = true, - bool convertRefValue = true, + {bool convertRefValue = true, bool convertCallbackRefValue = true, List additionalRefPropKeys = const [], bool wrapWithJsify = true}) { final propsForJs = JsBackedMap.from(props); - - if (shouldConvertEventHandlers) convertEventHandlers(propsForJs); if (convertRefValue) { convertRefValue2(propsForJs, convertCallbackRefValue: convertCallbackRefValue, additionalRefPropKeys: additionalRefPropKeys); diff --git a/lib/src/react_client/synthetic_event_wrappers.dart b/lib/src/react_client/synthetic_event_wrappers.dart index 7fd5261b..71c37492 100644 --- a/lib/src/react_client/synthetic_event_wrappers.dart +++ b/lib/src/react_client/synthetic_event_wrappers.dart @@ -25,6 +25,8 @@ class SyntheticEvent { external void preventDefault(); external void persist(); + + external bool isPersistent(); } @JS() diff --git a/test/factory/common_factory_tests.dart b/test/factory/common_factory_tests.dart index a8a42929..5db5bcc7 100644 --- a/test/factory/common_factory_tests.dart +++ b/test/factory/common_factory_tests.dart @@ -267,8 +267,7 @@ void domEventHandlerWrappingTests(ReactComponentFactoryProxy factory) { test(eventCase.description, () { eventCase.simulate(node); expect(events[eventCase], isNotNull, reason: 'handler should have been called'); - expect(events[eventCase], - eventCase.isDart ? isA() : isNot(isA())); + expect(events[eventCase], isA()); }); } }); @@ -280,8 +279,15 @@ void domEventHandlerWrappingTests(ReactComponentFactoryProxy factory) { reason: 'test setup: component must pass props into props.onDartRender'); }); - final dummyEvent = react.SyntheticMouseEvent(null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); + react.SyntheticMouseEvent event; + final divRef = react.createRef(); + render(react.div({ + 'ref': divRef, + 'onClick': (e) => event = e, + })); + rtu.Simulate.click(divRef); + + final dummyEvent = event; for (var eventCase in eventCases.where((helper) => helper.isDart)) { test(eventCase.description, () { @@ -292,39 +298,18 @@ void domEventHandlerWrappingTests(ReactComponentFactoryProxy factory) { } }); - group('wraps the handler with a function that proxies ReactJS event "persistence" as expected', () { - test('when event.persist() is called', () { - react.SyntheticMouseEvent actualEvent; - - final nodeWithClickHandler = renderAndGetRootNode(factory({ - 'onClick': (react.SyntheticMouseEvent event) { - event.persist(); - actualEvent = event; - } - })); - - rtu.Simulate.click(nodeWithClickHandler); - - // ignore: invalid_use_of_protected_member - expect(actualEvent.$$jsPersistDoNotSetThisOrYouWillBeFired, isA()); - expect(actualEvent.isPersistent, isTrue); - }); + test('event has .persist and .isPersistent methods that can be called', () { + react.SyntheticMouseEvent actualEvent; - test('when event.persist() is not called', () { - react.SyntheticMouseEvent actualEvent; - - final nodeWithClickHandler = renderAndGetRootNode(factory({ - 'onClick': (react.SyntheticMouseEvent event) { - actualEvent = event; - } - })); - - rtu.Simulate.click(nodeWithClickHandler); + final nodeWithClickHandler = renderAndGetRootNode(factory({ + 'onClick': (react.SyntheticMouseEvent event) { + expect(() => event.persist(), returnsNormally); + actualEvent = event; + } + })); - // ignore: invalid_use_of_protected_member - expect(actualEvent.$$jsPersistDoNotSetThisOrYouWillBeFired, isA()); - expect(actualEvent.isPersistent, isFalse); - }); + rtu.Simulate.click(nodeWithClickHandler); + expect(actualEvent.isPersistent(), isA()); }); test('doesn\'t wrap the handler if it is null', () { @@ -333,17 +318,6 @@ void domEventHandlerWrappingTests(ReactComponentFactoryProxy factory) { expect(() => rtu.Simulate.click(nodeWithClickHandler), returnsNormally); }); - test('stores the original function in a way that it can be retrieved from unconvertJsEventHandler', () { - final originalHandler = (event) {}; - - var instance = factory({'onClick': originalHandler}); - var instanceProps = getProps(instance); - - expect(instanceProps['onClick'], isNot(same(originalHandler)), - reason: 'test setup sanity check; should be different, converted function'); - expect(unconvertJsEventHandler(instanceProps['onClick']), same(originalHandler)); - }); - group('calls the handler in the zone the event was dispatched from', () { test('(simulated event)', () { final testZone = Zone.current; diff --git a/test/react_client_test.dart b/test/react_client_test.dart index 8d3e56df..6401e951 100644 --- a/test/react_client_test.dart +++ b/test/react_client_test.dart @@ -138,7 +138,7 @@ main() { originalHandlers = {}; props = {}; - for (final key in eventPropKeyToEventFactory.keys) { + for (final key in knownEventKeys) { props[key] = originalHandlers[key] = createHandler(key); } }); @@ -146,16 +146,17 @@ main() { test('for a DOM element', () { var component = react.div(props); var jsProps = unconvertJsProps(component); - for (final key in eventPropKeyToEventFactory.keys) { + for (final key in knownEventKeys) { expect(jsProps[key], isNotNull, reason: 'JS event handler prop should not be null'); - expect(jsProps[key], same(originalHandlers[key]), reason: 'JS event handler prop was not unconverted'); + expect(jsProps[key], anyOf(same(originalHandlers[key]), same(allowInterop(originalHandlers[key]))), + reason: 'JS event handler prop was not unconverted'); } }); test(', except for a JS composite component (handlers should already be unconverted)', () { var component = testJsComponentFactory(props); var jsProps = unconvertJsProps(component); - for (final key in eventPropKeyToEventFactory.keys) { + for (final key in knownEventKeys) { expect(jsProps[key], isNotNull, reason: 'JS event handler prop should not be null'); expect(jsProps[key], same(allowInterop(originalHandlers[key])), reason: 'JS event handler prop was unexpectedly modified'); @@ -164,17 +165,6 @@ main() { }); }); - group('unconvertJsEventHandler', () { - test('returns null when the input is null', () { - var result; - expect(() { - result = unconvertJsEventHandler(null); - }, returnsNormally); - - expect(result, isNull); - }); - }); - group('registerComponent', () { test('throws with printed error', () { expect(() => react.registerComponent(() => ThrowsInDefaultPropsComponent()), throwsStateError); From 0c7557d12798ad83d57a67d271549be272037bff Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Tue, 27 Oct 2020 10:28:40 -0700 Subject: [PATCH 2/3] Remove anonymous annotations from event wrappers --- lib/src/react_client/synthetic_event_wrappers.dart | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/lib/src/react_client/synthetic_event_wrappers.dart b/lib/src/react_client/synthetic_event_wrappers.dart index 71c37492..739523b5 100644 --- a/lib/src/react_client/synthetic_event_wrappers.dart +++ b/lib/src/react_client/synthetic_event_wrappers.dart @@ -8,7 +8,6 @@ import 'dart:html'; import 'package:js/js.dart'; @JS() -@anonymous class SyntheticEvent { external bool get bubbles; external bool get cancelable; @@ -30,13 +29,11 @@ class SyntheticEvent { } @JS() -@anonymous class SyntheticClipboardEvent extends SyntheticEvent { external get clipboardData; } @JS() -@anonymous class SyntheticKeyboardEvent extends SyntheticEvent { external bool get altKey; external String get char; @@ -52,26 +49,22 @@ class SyntheticKeyboardEvent extends SyntheticEvent { } @JS() -@anonymous class SyntheticCompositionEvent extends SyntheticEvent { external String get data; } @JS() -@anonymous class SyntheticFocusEvent extends SyntheticEvent { external EventTarget get relatedTarget; } @JS() -@anonymous class SyntheticFormEvent extends SyntheticEvent {} /// A JS object that looks like a [DataTransfer] but isn't one. /// /// See `syntheticDataTransferFactory` for more info. @JS() -@anonymous class NonNativeDataTransfer { external String get dropEffect; external String get effectAllowed; @@ -80,7 +73,6 @@ class NonNativeDataTransfer { } @JS() -@anonymous class SyntheticMouseEvent extends SyntheticEvent { external bool get altKey; external num get button; @@ -99,7 +91,6 @@ class SyntheticMouseEvent extends SyntheticEvent { } @JS() -@anonymous class SyntheticPointerEvent extends SyntheticEvent { external num get pointerId; external num get width; @@ -114,7 +105,6 @@ class SyntheticPointerEvent extends SyntheticEvent { } @JS() -@anonymous class SyntheticTouchEvent extends SyntheticEvent { external bool get altKey; external TouchList get changedTouches; @@ -126,7 +116,6 @@ class SyntheticTouchEvent extends SyntheticEvent { } @JS() -@anonymous class SyntheticTransitionEvent extends SyntheticEvent { external String get propertyName; external num get elapsedTime; @@ -134,7 +123,6 @@ class SyntheticTransitionEvent extends SyntheticEvent { } @JS() -@anonymous class SyntheticAnimationEvent extends SyntheticEvent { external String get animationName; external num get elapsedTime; @@ -142,14 +130,12 @@ class SyntheticAnimationEvent extends SyntheticEvent { } @JS() -@anonymous class SyntheticUIEvent extends SyntheticEvent { external num get detail; external get view; } @JS() -@anonymous class SyntheticWheelEvent extends SyntheticEvent { external num get deltaX; external num get deltaMode; From c312cf1df17cf0e3e4dbf6546395fbf0d74a32b1 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Mon, 2 Nov 2020 16:01:53 -0700 Subject: [PATCH 3/3] Add event helper utils from over_react --- lib/react.dart | 1 + lib/src/react_client/event_helpers.dart | 107 +++++++++++++++ pubspec.yaml | 1 + test/react_client/event_helpers_test.dart | 156 ++++++++++++++++++++++ test/react_client/event_helpers_test.html | 18 +++ 5 files changed, 283 insertions(+) create mode 100644 lib/src/react_client/event_helpers.dart create mode 100644 test/react_client/event_helpers_test.dart create mode 100644 test/react_client/event_helpers_test.html diff --git a/lib/react.dart b/lib/react.dart index 915e2bae..2b25c4de 100644 --- a/lib/react.dart +++ b/lib/react.dart @@ -23,6 +23,7 @@ export 'package:react/src/context.dart'; export 'package:react/src/prop_validator.dart'; export 'package:react/react_client/react_interop.dart' show forwardRef, forwardRef2, createRef, memo, memo2; export 'package:react/src/react_client/synthetic_event_wrappers.dart'; +export 'package:react/src/react_client/event_helpers.dart'; typedef Error PropValidator(TProps props, PropValidatorInfo info); diff --git a/lib/src/react_client/event_helpers.dart b/lib/src/react_client/event_helpers.dart new file mode 100644 index 00000000..c83ea59d --- /dev/null +++ b/lib/src/react_client/event_helpers.dart @@ -0,0 +1,107 @@ +library react_client.event_helpers; + +import 'dart:html'; + +import 'package:react/react.dart'; +import 'package:react/react_client/js_interop_helpers.dart'; + +/// Helper util that wraps a native [KeyboardEvent] in a [SyntheticKeyboardEvent]. +/// +/// Used where a native [KeyboardEvent] is given and a [SyntheticKeyboardEvent] is needed. +SyntheticKeyboardEvent wrapNativeKeyboardEvent(KeyboardEvent nativeEvent) { + return jsifyAndAllowInterop({ + // SyntheticEvent fields + 'bubbles': nativeEvent.bubbles, + 'cancelable': nativeEvent.cancelable, + 'currentTarget': nativeEvent.currentTarget, + 'defaultPrevented': nativeEvent.defaultPrevented, + 'eventPhase': nativeEvent.eventPhase, + 'isTrusted': nativeEvent.isTrusted, + 'nativeEvent': nativeEvent, + 'target': nativeEvent.target, + 'timeStamp': nativeEvent.timeStamp, + 'type': nativeEvent.type, + // SyntheticEvent methods + 'stopPropagation': nativeEvent.stopPropagation, + 'preventDefault': nativeEvent.preventDefault, + 'persist': () {}, + 'isPersistent': () => true, + // SyntheticKeyboardEvent fields + 'altKey': nativeEvent.altKey, + 'char': nativeEvent.charCode == null ? null : String.fromCharCode(nativeEvent.charCode), + 'ctrlKey': nativeEvent.ctrlKey, + 'locale': null, + 'location': nativeEvent.location, + 'key': nativeEvent.key, + 'metaKey': nativeEvent.metaKey, + 'repeat': nativeEvent.repeat, + 'shiftKey': nativeEvent.shiftKey, + 'keyCode': nativeEvent.keyCode, + 'charCode': nativeEvent.charCode, + }) as SyntheticKeyboardEvent; +} + +/// Helper util that wraps a native [MouseEvent] in a [SyntheticMouseEvent]. +/// +/// Used where a native [MouseEvent] is given and a [SyntheticMouseEvent] is needed. +SyntheticMouseEvent wrapNativeMouseEvent(MouseEvent nativeEvent) { + return jsifyAndAllowInterop({ + // SyntheticEvent fields + 'bubbles': nativeEvent.bubbles, + 'cancelable': nativeEvent.cancelable, + 'currentTarget': nativeEvent.currentTarget, + 'defaultPrevented': nativeEvent.defaultPrevented, + 'eventPhase': nativeEvent.eventPhase, + 'isTrusted': nativeEvent.isTrusted, + 'nativeEvent': nativeEvent, + 'target': nativeEvent.target, + 'timeStamp': nativeEvent.timeStamp, + 'type': nativeEvent.type, + // SyntheticEvent methods + 'stopPropagation': nativeEvent.stopPropagation, + 'preventDefault': nativeEvent.preventDefault, + 'persist': () {}, + 'isPersistent': () => true, + // SyntheticMouseEvent fields + 'altKey': nativeEvent.altKey, + 'button': nativeEvent.button, + 'buttons': nativeEvent.buttons, + 'clientX': nativeEvent.client.x, + 'clientY': nativeEvent.client.y, + 'ctrlKey': nativeEvent.ctrlKey, + 'dataTransfer': nativeEvent.dataTransfer, + 'metaKey': nativeEvent.metaKey, + 'pageX': nativeEvent.page.x, + 'pageY': nativeEvent.page.y, + 'relatedTarget': nativeEvent.relatedTarget, + 'screenX': nativeEvent.screen.x, + 'screenY': nativeEvent.screen.y, + 'shiftKey': nativeEvent.shiftKey, + }) as SyntheticMouseEvent; +} + +/// If the consumer specifies a callback like `onChange` on one of our custom form components that are not *actually* +/// form elements - we still need a valid [SyntheticFormEvent] to pass as the expected parameter to that callback. +/// +/// This helper method generates a "fake" [SyntheticFormEvent], with nothing but the `target` set to [element], +/// `type` set to [type] and `timeStamp` set to the current time. All other arguments are `noop`, `false` or `null`. +SyntheticFormEvent fakeSyntheticFormEvent(Element element, String type) { + return jsifyAndAllowInterop({ + // SyntheticEvent fields + 'bubbles': false, + 'cancelable': false, + 'currentTarget': element, + 'defaultPrevented': false, + 'eventPhase': Event.AT_TARGET, + 'isTrusted': false, + 'nativeEvent': null, + 'target': element, + 'timeStamp': DateTime.now().millisecondsSinceEpoch, + 'type': type, + // SyntheticEvent methods + 'stopPropagation': () {}, + 'preventDefault': () {}, + 'persist': () {}, + 'isPersistent': () => true, + }) as SyntheticFormEvent; +} diff --git a/pubspec.yaml b/pubspec.yaml index 008ffd9d..97e740c6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,4 +23,5 @@ dev_dependencies: build_web_compilers: ^2.1.4 dependency_validator: ^1.2.0 matcher: ^0.12.5 + mockito: ^4.1.1 test: ^1.6.5 diff --git a/test/react_client/event_helpers_test.dart b/test/react_client/event_helpers_test.dart new file mode 100644 index 00000000..aa7c2b2e --- /dev/null +++ b/test/react_client/event_helpers_test.dart @@ -0,0 +1,156 @@ +@TestOn('browser') +library react.event_helpers_test; + +import 'dart:html'; + +import 'package:mockito/mockito.dart'; +import 'package:react/react.dart'; +import 'package:test/test.dart'; + +/// Main entry point for Event Helpers testing +main() { + test('wrapNativeKeyboardEvent', () { + var nativeKeyboardEvent = MockKeyboardEvent(); + var currentTarget = DivElement(); + var target = DivElement(); + var calls = []; + + when(nativeKeyboardEvent.bubbles).thenReturn(true); + when(nativeKeyboardEvent.cancelable).thenReturn(true); + when(nativeKeyboardEvent.currentTarget).thenReturn(currentTarget); + when(nativeKeyboardEvent.defaultPrevented).thenReturn(false); + when(nativeKeyboardEvent.preventDefault()).thenAnswer((_) => calls.add('preventDefault')); + when(nativeKeyboardEvent.stopPropagation()).thenAnswer((_) => calls.add('stopPropagation')); + when(nativeKeyboardEvent.eventPhase).thenReturn(0); + when(nativeKeyboardEvent.target).thenReturn(target); + when(nativeKeyboardEvent.timeStamp).thenReturn(0); + when(nativeKeyboardEvent.type).thenReturn('type'); + when(nativeKeyboardEvent.altKey).thenReturn(false); + when(nativeKeyboardEvent.charCode).thenReturn(0); + when(nativeKeyboardEvent.ctrlKey).thenReturn(false); + when(nativeKeyboardEvent.location).thenReturn(0); + when(nativeKeyboardEvent.keyCode).thenReturn(0); + when(nativeKeyboardEvent.metaKey).thenReturn(false); + when(nativeKeyboardEvent.repeat).thenReturn(false); + when(nativeKeyboardEvent.shiftKey).thenReturn(false); + + expect(nativeKeyboardEvent.defaultPrevented, isFalse); + + var syntheticKeyboardEvent = wrapNativeKeyboardEvent(nativeKeyboardEvent); + + expect(syntheticKeyboardEvent, isA()); + + expect(syntheticKeyboardEvent.bubbles, isTrue); + expect(syntheticKeyboardEvent.cancelable, isTrue); + expect(syntheticKeyboardEvent.currentTarget, currentTarget); + expect(syntheticKeyboardEvent.defaultPrevented, isFalse); + expect(() => syntheticKeyboardEvent.preventDefault(), returnsNormally); + expect(calls, contains('preventDefault')); + expect(() => syntheticKeyboardEvent.stopPropagation(), returnsNormally); + expect(calls, contains('stopPropagation')); + expect(syntheticKeyboardEvent.eventPhase, 0); + expect(syntheticKeyboardEvent.isTrusted, isNull); + expect(syntheticKeyboardEvent.nativeEvent, nativeKeyboardEvent); + expect(syntheticKeyboardEvent.target, target); + expect(syntheticKeyboardEvent.timeStamp, 0); + expect(syntheticKeyboardEvent.type, 'type'); + expect(syntheticKeyboardEvent.altKey, isFalse); + expect(syntheticKeyboardEvent.char, String.fromCharCode(0)); + expect(syntheticKeyboardEvent.charCode, 0); + expect(syntheticKeyboardEvent.ctrlKey, isFalse); + expect(syntheticKeyboardEvent.locale, isNull); + expect(syntheticKeyboardEvent.location, 0); + expect(syntheticKeyboardEvent.key, isNull); + expect(syntheticKeyboardEvent.keyCode, 0); + expect(syntheticKeyboardEvent.metaKey, isFalse); + expect(syntheticKeyboardEvent.repeat, isFalse); + expect(syntheticKeyboardEvent.shiftKey, isFalse); + }); + + test('wrapNativeMouseEvent', () { + var nativeMouseEvent = MockMouseEvent(); + var currentTarget = DivElement(); + var target = DivElement(); + var relatedTarget = DivElement(); + var calls = []; + + when(nativeMouseEvent.bubbles).thenReturn(true); + when(nativeMouseEvent.cancelable).thenReturn(true); + when(nativeMouseEvent.currentTarget).thenReturn(currentTarget); + when(nativeMouseEvent.defaultPrevented).thenReturn(false); + when(nativeMouseEvent.preventDefault()).thenAnswer((_) => calls.add('preventDefault')); + when(nativeMouseEvent.stopPropagation()).thenAnswer((_) => calls.add('stopPropagation')); + when(nativeMouseEvent.eventPhase).thenReturn(0); + when(nativeMouseEvent.target).thenReturn(target); + when(nativeMouseEvent.timeStamp).thenReturn(0); + when(nativeMouseEvent.type).thenReturn('type'); + when(nativeMouseEvent.altKey).thenReturn(false); + when(nativeMouseEvent.button).thenReturn(0); + when(nativeMouseEvent.ctrlKey).thenReturn(false); + when(nativeMouseEvent.metaKey).thenReturn(false); + when(nativeMouseEvent.relatedTarget).thenReturn(relatedTarget); + when(nativeMouseEvent.shiftKey).thenReturn(false); + when(nativeMouseEvent.client).thenReturn(Point(1, 2)); + when(nativeMouseEvent.page).thenReturn(Point(3, 4)); + when(nativeMouseEvent.screen).thenReturn(Point(5, 6)); + + var syntheticMouseEvent = wrapNativeMouseEvent(nativeMouseEvent); + + expect(syntheticMouseEvent, isA()); + + expect(syntheticMouseEvent.bubbles, isTrue); + expect(syntheticMouseEvent.cancelable, isTrue); + expect(syntheticMouseEvent.currentTarget, currentTarget); + expect(syntheticMouseEvent.defaultPrevented, isFalse); + expect(() => syntheticMouseEvent.preventDefault(), returnsNormally); + expect(calls, contains('preventDefault')); + expect(() => syntheticMouseEvent.stopPropagation(), returnsNormally); + expect(calls, contains('stopPropagation')); + expect(syntheticMouseEvent.eventPhase, 0); + expect(syntheticMouseEvent.isTrusted, isNull); + expect(syntheticMouseEvent.nativeEvent, nativeMouseEvent); + expect(syntheticMouseEvent.target, target); + expect(syntheticMouseEvent.timeStamp, 0); + expect(syntheticMouseEvent.type, 'type'); + expect(syntheticMouseEvent.altKey, isFalse); + expect(syntheticMouseEvent.button, 0); + expect(syntheticMouseEvent.buttons, isNull); + expect(syntheticMouseEvent.clientX, 1); + expect(syntheticMouseEvent.clientY, 2); + expect(syntheticMouseEvent.ctrlKey, isFalse); + expect(syntheticMouseEvent.dataTransfer, isNull); + expect(syntheticMouseEvent.metaKey, isFalse); + expect(syntheticMouseEvent.pageX, 3); + expect(syntheticMouseEvent.pageY, 4); + expect(syntheticMouseEvent.relatedTarget, relatedTarget); + expect(syntheticMouseEvent.screenX, 5); + expect(syntheticMouseEvent.screenY, 6); + expect(syntheticMouseEvent.shiftKey, isFalse); + }); + + test('fakeSyntheticFormEvent', () { + var element = DivElement(); + var fakeEvent = fakeSyntheticFormEvent(element, 'change'); + + expect(fakeEvent, isA()); + + expect(fakeEvent.bubbles, isFalse); + expect(fakeEvent.cancelable, isFalse); + expect(fakeEvent.currentTarget, element); + expect(fakeEvent.defaultPrevented, false); + expect(() => fakeEvent.preventDefault(), returnsNormally); + expect(() => fakeEvent.stopPropagation(), returnsNormally); + expect(fakeEvent.eventPhase, Event.AT_TARGET); + expect(fakeEvent.isTrusted, isFalse); + expect(fakeEvent.nativeEvent, isNull); + expect(fakeEvent.target, element); + expect(fakeEvent.timeStamp, isA()); + expect(fakeEvent.type, 'change'); + }); +} + +// ignore: avoid_implementing_value_types +class MockKeyboardEvent extends Mock implements KeyboardEvent {} + +// ignore: avoid_implementing_value_types +class MockMouseEvent extends Mock implements MouseEvent {} diff --git a/test/react_client/event_helpers_test.html b/test/react_client/event_helpers_test.html new file mode 100644 index 00000000..d54b2568 --- /dev/null +++ b/test/react_client/event_helpers_test.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + +