diff --git a/README.md b/README.md index 5ed2ead3e..aab25605b 100644 --- a/README.md +++ b/README.md @@ -495,15 +495,15 @@ as shown in the examples above. ## Component Formatting > __A note on dart_style:__ -> +> > Currently, [dart_style (dartfmt)](https://github.com/dart-lang/dart_style) decreases the readability of components > built using [OverReact's fluent-style](#fluent-style-component-consumption). > See https://github.com/dart-lang/dart_style/issues/549 for more info. -> +> > We're exploring some different ideas to improve automated formatting, but for the time being, we __do not recommend__ using dart_style with OverReact. -> +> > However, if you do choose to use dart_style, you can greatly improve its output by using trailing commas in children argument lists: -> +> > * dart_style formatting: > ```dart > return (Button() diff --git a/lib/over_react.dart b/lib/over_react.dart index 8eb1a1b41..5e2beb3ab 100644 --- a/lib/over_react.dart +++ b/lib/over_react.dart @@ -37,6 +37,7 @@ export 'src/util/key_constants.dart'; export 'src/util/map_util.dart'; export 'src/util/pretty_print.dart'; export 'src/util/prop_errors.dart'; +export 'src/util/prop_key_util.dart'; export 'src/util/react_wrappers.dart'; export 'src/util/rem_util.dart'; export 'src/util/string_util.dart'; diff --git a/lib/src/component/callback_typedefs.dart b/lib/src/component/callback_typedefs.dart index 6ff4dd95d..2ae3a0838 100644 --- a/lib/src/component/callback_typedefs.dart +++ b/lib/src/component/callback_typedefs.dart @@ -14,6 +14,9 @@ library over_react.callback_typedefs; +import 'dart:html'; + +import 'package:over_react/over_react.dart' show ResizeSensorEvent; import 'package:react/react.dart' as react; // Callbacks for React's DOM event system @@ -29,3 +32,9 @@ typedef WheelEventCallback(react.SyntheticWheelEvent event); /// A generic callback that takes no arguments. typedef Callback(); + +// Callback for DOM elements +typedef Element ElementCallback(); + +// Callback for [ResizeSensorEvent]s +typedef void ResizeSensorHandler(ResizeSensorEvent event); diff --git a/lib/src/component/resize_sensor.dart b/lib/src/component/resize_sensor.dart index d4f76edf4..df120969f 100644 --- a/lib/src/component/resize_sensor.dart +++ b/lib/src/component/resize_sensor.dart @@ -19,12 +19,9 @@ library resize_sensor; import 'dart:collection'; import 'dart:html'; -import 'package:browser_detect/browser_detect.dart'; -import 'package:over_react/over_react.dart'; +import 'package:platform_detect/platform_detect.dart'; import 'package:react/react.dart' as react; - -// Callback for [ResizeSensorEvent]s -typedef void ResizeSensorHandler(ResizeSensorEvent event); +import 'package:over_react/over_react.dart'; /// A wrapper component that detects when its parent is resized. /// @@ -152,9 +149,9 @@ class ResizeSensorComponent extends UiComponent { }; // IE 10 and Safari 8 need 'special' value prefixes for 'display:flex'. - if (browser.isIe && browser.version <= '10') { + if (browser.isInternetExplorer && browser.version.major <= 10) { wrapperStyles['display'] = '-ms-flexbox'; - } else if (browser.isSafari && browser.version < '9') { + } else if (browser.isSafari && browser.version.major < 9) { wrapperStyles['display'] = '-webkit-flex'; } else { wrapperStyles['display'] = 'flex'; diff --git a/lib/src/component_declaration/component_base.dart b/lib/src/component_declaration/component_base.dart index 2784af621..4362a06ab 100644 --- a/lib/src/component_declaration/component_base.dart +++ b/lib/src/component_declaration/component_base.dart @@ -25,6 +25,7 @@ import 'package:over_react/over_react.dart' show prettyPrintMap, unindent, PropError; + import 'package:over_react/src/component_declaration/component_type_checking.dart'; import 'package:react/react.dart' as react; import 'package:react/react_client.dart'; @@ -186,6 +187,7 @@ abstract class UiComponent extends react.Component { TProps typedPropsFactory(Map propsMap); /// Returns a typed props object backed by a new Map. + /// /// Convenient for use with [getDefaultProps]. TProps newProps() => typedPropsFactory({}); @@ -237,6 +239,7 @@ abstract class UiStatefulComponent typedStateFactory({}); diff --git a/lib/src/component_declaration/flux_component.dart b/lib/src/component_declaration/flux_component.dart index 0589379b0..09d1c938a 100644 --- a/lib/src/component_declaration/flux_component.dart +++ b/lib/src/component_declaration/flux_component.dart @@ -1,3 +1,17 @@ +// Copyright 2016 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + library over_react.component_declaration.flux_component; import 'dart:async'; diff --git a/lib/src/util/class_names.dart b/lib/src/util/class_names.dart index 4094b83dc..dfbed5be6 100644 --- a/lib/src/util/class_names.dart +++ b/lib/src/util/class_names.dart @@ -42,7 +42,6 @@ abstract class CssClassPropsMixin { String classNameBlacklist; } - /// A MapView with the typed getters/setters for all CSS-class-related props. class CssClassPropsMapView extends MapView with CssClassPropsMixin { /// Create a new instance backed by the specified map. diff --git a/lib/src/util/constants_base.dart b/lib/src/util/constants_base.dart index b5f019a7c..b33f221ce 100644 --- a/lib/src/util/constants_base.dart +++ b/lib/src/util/constants_base.dart @@ -25,7 +25,16 @@ abstract class DebugFriendlyConstant { String get debugDescription; @override - String toString() => '$runtimeType.$_name ($debugDescription)'; + String toString() { + var string = '$runtimeType.$_name'; + + var debugDescription = this.debugDescription; + if (debugDescription != null) { + string = '$string ($debugDescription)'; + } + + return string; + } } /// A named constant with a helpful string representation diff --git a/lib/src/util/css_value_util.dart b/lib/src/util/css_value_util.dart index 998979a6a..d6e1b04d2 100644 --- a/lib/src/util/css_value_util.dart +++ b/lib/src/util/css_value_util.dart @@ -57,15 +57,14 @@ class CssValue implements Comparable { unit = 'px'; } else { var unitMatch = new RegExp(r'(?:rem|em|ex|vh|vw|vmin|vmax|%|px|cm|mm|in|pt|pc|ch)?$').firstMatch(source.toString()); - try { - number = double.parse(unitMatch.input.substring(0, unitMatch.start)); unit = unitMatch.group(0); - if (unit == '') { - unit = 'px'; - } - } catch(e) { - error = new ArgumentError.value(source, 'value', 'Invalid number/unit for CSS value'); + if (unit == '') { + unit = 'px'; } + + number = double.parse(unitMatch.input.substring(0, unitMatch.start), (_) { + error = new ArgumentError.value(source, 'value', 'Invalid number/unit for CSS value'); + }); } if (number != null && !number.isFinite) { diff --git a/lib/src/util/dom_util.dart b/lib/src/util/dom_util.dart index ea96136fd..da2993977 100644 --- a/lib/src/util/dom_util.dart +++ b/lib/src/util/dom_util.dart @@ -16,6 +16,11 @@ library dom_util; import 'dart:html'; +import 'package:platform_detect/platform_detect.dart'; + +import './string_util.dart'; +import './validation_util.dart'; + /// Returns whether [root] is the same as or contains the [other] node. /// /// Returns false if either [root] or [other] is null. @@ -51,3 +56,96 @@ Element getActiveElement() { return activeElement; } + +/// A list of the `type` attribute values for an HTML `` element that implement [TextInputElementBase]. +/// +/// Necessary because of the circular inheritance hierarchy in Dart's [InputElement] class structure. +/// +/// See: +/// +/// Related: [isTextInputElementBase] +const List inputTypesWithSelectionRangeSupport = const [ + 'search', + 'text', + 'url', + 'tel', + 'email', + 'password', + 'number', +]; + +/// Returns whether the provided [element] supports `setSelectionRange`. +/// +/// Necessary in part because of the circular inheritance hierarchy in Dart's [InputElement] class structure, +/// and in part because the classes do not correspond to whether setSelectionRange is supported (e.g. number inputs). +/// +/// See: +bool supportsSelectionRange(InputElement element) { + // Uncomment once https://github.com/dart-lang/sdk/issues/22967 is fixed. + // if (element is TextInputElementBase) return true; + + final type = element.getAttribute('type'); + return inputTypesWithSelectionRangeSupport.contains(type); +} + +/// Custom implementation to prevent the error that [TextInputElementBase.setSelectionRange] throws when called +/// on an [EmailInputElement] or [NumberInputElement] since ONLY Chrome does not support it. +/// +/// A warning will be displayed in the console instead of an error. +/// +/// __Example that will throw an exception in Chrome:__ +/// InputElement inputNodeRef; +/// +/// // This will throw an exception in Chrome when the node is focused. +/// renderEmailInput() { +/// return (Dom.input() +/// ..type = 'email' +/// ..onFocus = (_) { +/// inputNodeRef.setSelectionRange(inputNodeRef.value.length, inputNodeRef.value.length); +/// } +/// ..ref = (instance) { inputNodeRef = instance; } +/// )(); +/// } +/// +/// __Example that will not throw:__ +/// InputElement inputNodeRef; +/// +/// // This will not throw an exception - and will work in all +/// // browsers except Chrome until +/// // https://bugs.chromium.org/p/chromium/issues/detail?id=324360 +/// // is fixed. +/// renderChromeSafeEmailInput() { +/// return (Dom.input() +/// ..type = 'email' +/// ..onFocus = (_) { +/// setSelectionRange(inputNodeRef, inputNodeRef.value.length, inputNodeRef.value.length); +/// } +/// ..ref = (instance) { inputNodeRef = instance; } +/// )(); +/// } +/// +/// See: +void setSelectionRange(/* TextInputElement | TextAreaElement */Element input, int start, int end, [String direction]) { + if (input is TextAreaElement) { + input.setSelectionRange(start, end, direction); + } else if (input is InputElement && supportsSelectionRange(input)) { + if (browser.isChrome) { + final inputType = input.getAttribute('type'); + + if (inputType == 'email' || inputType == 'number') { + assert(ValidationUtil.warn(unindent( + ''' + Google Chrome does not support `setSelectionRange` on email or number inputs. + See: https://bugs.chromium.org/p/chromium/issues/detail?id=324360 + ''' + ))); + + return; + } + } + + input.setSelectionRange(start, end, direction); + } else { + throw new ArgumentError.value(input, 'input', 'must be an instance of `TextInputElementBase`, `NumberInputElement` or `TextAreaElement`'); + } +} diff --git a/lib/src/util/handler_chain_util.dart b/lib/src/util/handler_chain_util.dart index 0a8da8cfc..c85f881e0 100644 --- a/lib/src/util/handler_chain_util.dart +++ b/lib/src/util/handler_chain_util.dart @@ -61,7 +61,6 @@ final CallbackUtil0Arg callbacks = co /// Provides chaining utilities for [ResizeSensorHandler]. final CallbackUtil1Arg resizeEventCallbacks = const CallbackUtil1Arg(); - typedef Callback0Arg(); typedef Callback1Arg(T1 arg1); typedef Callback2Arg(T1 arg1, T2 arg2); diff --git a/lib/src/util/map_util.dart b/lib/src/util/map_util.dart index b48df0989..583b37236 100644 --- a/lib/src/util/map_util.dart +++ b/lib/src/util/map_util.dart @@ -16,8 +16,9 @@ library over_react.map_util; import 'dart:collection'; -import 'package:over_react/src/component/prop_mixins.dart'; import 'package:over_react/src/component_declaration/transformer_helpers.dart'; +import 'package:over_react/src/component/dom_components.dart'; +import 'package:over_react/src/component/prop_mixins.dart'; /// Returns a copy of the specified props map, omitting reserved React props by default, /// in addition to any specified keys. @@ -60,6 +61,16 @@ Map getPropsToForward(Map props, {bool omitReactProps: true, bool onlyCopyDomPro return propsToForward; } +/// Returns a copy of the style map found in [props]. +/// +/// Returns an empty map if [props] or its style map are null. +Map newStyleFromProps(Map props) { + if (props == null) return {}; + + var existingStyle = domProps(props).style; + return existingStyle == null ? {} : new Map.from(existingStyle); +} + SplayTreeSet _validDomProps = new SplayTreeSet() ..addAll(const $PropKeys(DomPropsMixin)) ..addAll(const $PropKeys(SvgPropsMixin)); diff --git a/lib/src/util/prop_key_util.dart b/lib/src/util/prop_key_util.dart new file mode 100644 index 000000000..fde65d5ae --- /dev/null +++ b/lib/src/util/prop_key_util.dart @@ -0,0 +1,61 @@ +// Copyright 2016 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +library over_react.prop_key_util; + +import 'dart:collection'; + +/// Returns the string key of the [factory] prop accessed in [accessProp]. +/// +/// Example usage: +/// +/// var valuePropKey = getPropKey((props) => props.value, TextInput); +String getPropKey(void accessProp(Map keySpy), Map factory(Map props)) { + return _getKey((Map keySpy) { + return accessProp(factory(keySpy)); + }); +} + +dynamic _getKey(void accessKey(Map keySpy)) { + var keySpy = new _SingleKeyAccessMapSpy(const {}); + + accessKey(keySpy); + + return keySpy.key; +} + +/// Helper class that stores the key accessed while getting a value of a map. +class _SingleKeyAccessMapSpy extends MapView { + _SingleKeyAccessMapSpy(Map map) : super(map); + + bool _hasBeenAccessed = false; + + dynamic _key; + + dynamic get key { + if (!_hasBeenAccessed) throw new StateError('Key has not been accessed.'); + + return _key; + } + + @override + operator[](key) { + if (_hasBeenAccessed) throw new StateError('A key has already been accessed.'); + + _key = key; + _hasBeenAccessed = true; + + return null; + } +} diff --git a/lib/src/util/react_wrappers.dart b/lib/src/util/react_wrappers.dart index a6321d8a6..6d3fe162c 100644 --- a/lib/src/util/react_wrappers.dart +++ b/lib/src/util/react_wrappers.dart @@ -100,6 +100,8 @@ Map getJsProps(/* ReactElement|ReactComponent */ instance) { return props; } +Expando _elementPropsCache = new Expando('_elementPropsCache'); + /// Returns an unmodifiable Map view of props for a [ReactElement] or composite [ReactComponent] [instance]. /// /// For a native Dart component, this returns its [react.Component.props] in an unmodifiable Map view. @@ -107,9 +109,20 @@ Map getJsProps(/* ReactElement|ReactComponent */ instance) { /// /// Throws if [instance] is not a valid [ReactElement] or composite [ReactComponent] . Map getProps(/* ReactElement|ReactComponent */ instance) { - if (isValidElement(instance) || _isCompositeComponent(instance)) { + var isCompositeComponent = _isCompositeComponent(instance); + + if (isValidElement(instance) || isCompositeComponent) { + if (!isCompositeComponent) { + var cachedView = _elementPropsCache[instance]; + if (cachedView != null) return cachedView; + } + var propsMap = isDartComponent(instance) ? _getExtendedProps(instance) : getJsProps(instance); - return new UnmodifiableMapView(propsMap); + var view = new UnmodifiableMapView(propsMap); + + if (!isCompositeComponent) _elementPropsCache[instance] = view; + + return view; } throw new ArgumentError.value(instance, 'instance', 'must be a valid ReactElement or composite ReactComponent'); diff --git a/lib/src/util/rem_util.dart b/lib/src/util/rem_util.dart index 414aca9e6..10dfd8ba0 100644 --- a/lib/src/util/rem_util.dart +++ b/lib/src/util/rem_util.dart @@ -120,9 +120,7 @@ CssValue toRem(dynamic value, {bool treatNumAsRem: false, bool passThroughUnsupp } else if (parsedValue?.unit == 'px') { remValueNum = parsedValue.number / rootFontSize; } else { - if (passThroughUnsupportedUnits) { - return parsedValue; - } + if (passThroughUnsupportedUnits) return parsedValue; throw new ArgumentError.value(value, 'value', 'must be a px num or a String px/rem value'); } @@ -167,9 +165,7 @@ CssValue toPx(dynamic value, {bool treatNumAsPx: false, bool passThroughUnsuppor } else if (parsedValue?.unit == 'rem') { pxValueNum = parsedValue.number * rootFontSize; } else { - if (passThroughUnsupportedUnits) { - return parsedValue; - } + if (passThroughUnsupportedUnits) return parsedValue; throw new ArgumentError.value(value, 'value', 'must be a rem num or a String px/rem value'); } diff --git a/pubspec.yaml b/pubspec.yaml index 00006e777..73ea252f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: source_span: "^1.2.0" transformer_utils: "^0.1.1" w_flux: "^2.5.0" - browser_detect: "^1.0.4" + platform_detect: "^1.1.1" dev_dependencies: matcher: ">=0.11.0 <0.13.0" coverage: "^0.7.2" diff --git a/test/over_react/component/dom_components_test.dart b/test/over_react/component/dom_components_test.dart index 03793f301..a98305138 100644 --- a/test/over_react/component/dom_components_test.dart +++ b/test/over_react/component/dom_components_test.dart @@ -40,6 +40,7 @@ main() { String name = MirrorSystem.getName(element.simpleName); String expectedTagName = name; if (expectedTagName == 'variable') expectedTagName = 'var'; + if (expectedTagName == 'svgSet') expectedTagName = 'set'; if (expectedTagName == 'svgSwitch') expectedTagName = 'switch'; if (expectedTagName == 'colorProfile') expectedTagName = 'color-profile'; if (expectedTagName == 'fontFace') expectedTagName = 'font-face'; diff --git a/test/over_react/component/resize_sensor_test.dart b/test/over_react/component/resize_sensor_test.dart index 7539eeeb4..11dbbae49 100644 --- a/test/over_react/component/resize_sensor_test.dart +++ b/test/over_react/component/resize_sensor_test.dart @@ -18,7 +18,7 @@ library resize_sensor_test; import 'dart:async'; import 'dart:html'; -import 'package:browser_detect/browser_detect.dart'; +import 'package:platform_detect/platform_detect.dart'; import 'package:over_react/over_react.dart'; import 'package:react/react.dart' as react; import 'package:react/react_dom.dart' as react_dom; @@ -126,11 +126,11 @@ void main() { expect(renderedNode.style.display, equals('block')); var nodeStyleDecl = renderedNode.style; - if (browser.isIe && browser.version < '11') { + if (browser.isInternetExplorer && browser.version.major < 11) { expect(nodeStyleDecl.getPropertyValue('-ms-flex-positive'), '1'); expect(nodeStyleDecl.getPropertyValue('-ms-flex-negative'), '1'); expect(nodeStyleDecl.getPropertyValue('-ms-flex-preferred-size'), '0%'); - } else if (browser.isSafari && browser.version < '9') { + } else if (browser.isSafari && browser.version.major < 9) { expect(nodeStyleDecl.getPropertyValue('-webkit-flex'), '1 1 0%'); } else { expect(nodeStyleDecl.getPropertyValue('flex'), '1 1 0%'); @@ -143,12 +143,12 @@ void main() { expect(renderedNode.style.position, equals('relative')); var nodeStyleDecl = renderedNode.style; - if (browser.isIe && browser.version < '11') { + if (browser.isInternetExplorer && browser.version.major < 11) { expect(renderedNode.style.display, equals('-ms-flexbox')); expect(nodeStyleDecl.getPropertyValue('-ms-flex-positive'), '1'); expect(nodeStyleDecl.getPropertyValue('-ms-flex-negative'), '1'); expect(nodeStyleDecl.getPropertyValue('-ms-flex-preferred-size'), '0%'); - } else if (browser.isSafari && browser.version < '9') { + } else if (browser.isSafari && browser.version.major < 9) { expect(renderedNode.style.display, equals('-webkit-flex')); expect(nodeStyleDecl.getPropertyValue('-webkit-flex'), '1 1 0%'); } else { diff --git a/test/over_react/component_declaration/component_type_checking_test/type_inheritance/abstract_inheritance/extendedtype.dart b/test/over_react/component_declaration/component_type_checking_test/type_inheritance/abstract_inheritance/extendedtype.dart index bf5e95fed..56159d2af 100644 --- a/test/over_react/component_declaration/component_type_checking_test/type_inheritance/abstract_inheritance/extendedtype.dart +++ b/test/over_react/component_declaration/component_type_checking_test/type_inheritance/abstract_inheritance/extendedtype.dart @@ -15,7 +15,6 @@ library test_component.type_inheritance.extendedtype; import 'package:over_react/over_react.dart'; - import './abstract.dart'; @Factory() diff --git a/test/over_react/component_declaration/component_type_checking_test/type_inheritance/subsubtype.dart b/test/over_react/component_declaration/component_type_checking_test/type_inheritance/subsubtype.dart index 2cc2dc2bb..e579c6f0f 100644 --- a/test/over_react/component_declaration/component_type_checking_test/type_inheritance/subsubtype.dart +++ b/test/over_react/component_declaration/component_type_checking_test/type_inheritance/subsubtype.dart @@ -15,7 +15,6 @@ library test_component.type_inheritance.subsubtype; import 'package:over_react/over_react.dart'; - import './subtype.dart'; @Factory() diff --git a/test/over_react/util/dom_util_test.dart b/test/over_react/util/dom_util_test.dart index 7977caded..8d4ca0d77 100644 --- a/test/over_react/util/dom_util_test.dart +++ b/test/over_react/util/dom_util_test.dart @@ -20,6 +20,7 @@ import 'package:over_react/over_react.dart'; import 'package:test/test.dart'; import '../../test_util/test_util.dart'; +import '../../wsd_test_util/validation_util_helpers.dart'; /// Main entry point for DomUtil testing main() { @@ -152,6 +153,122 @@ main() { expect(getActiveElement(), isNull); }); }); + + group('setSelectionRange', () { + test('throws an ArgumentError if called on an unsupported Element type', () { + var invalidElement = new DivElement(); + expect(() => setSelectionRange(invalidElement, 0, 0), throwsArgumentError); + }); + + test('throws an ArgumentError if called on an unsupported InputElement type', () { + var invalidElement = new CheckboxInputElement(); + + // Note: For some unknown reason - when running the exact same expect() we use for DivElement above, + // this one fails with "Invalid Object" - the stack trace never leaves test() + // + // ¯\_(ツ)_/¯ + var error; + + try { + setSelectionRange(invalidElement, 0, 0); + } catch (err) { + error = err; + } + + expect(error, isNotNull); + }); + + group('correctly calls setSelectionRange', () { + var renderedInstance; + InputElement inputElement; + TextAreaElement textareaElement; + const String testValue = 'foo'; + + tearDown(() { + renderedInstance = null; + inputElement = null; + + tearDownAttachedNodes(); + }); + + group('on an `` of type:', () { + void sharedInputSetSelectionRangeTest(String type) { + renderedInstance = renderAttachedToDocument((Dom.input() + ..defaultValue = testValue + ..type = type + )()); + inputElement = findDomNode(renderedInstance); + setSelectionRange(inputElement, testValue.length, testValue.length); + + // setSelectionRange on number inputs shouldn't throw in other browsers, + // but it also doesn't always work. + // Don't expect that the selection actually changed. + if (type != 'number') { + expect(inputElement.selectionStart, equals(testValue.length)); + expect(inputElement.selectionEnd, equals(testValue.length)); + } + } + + for (var type in inputTypesWithSelectionRangeSupport) { + if (type == 'email' || type == 'number') { + // See: https://bugs.chromium.org/p/chromium/issues/detail?id=324360 + test(type, () { + sharedInputSetSelectionRangeTest(type); + }, testOn: 'js && !chrome'); + } else { + test(type, () { sharedInputSetSelectionRangeTest(type); }); + } + } + }); + + test('on TextAreaElement', () { + renderedInstance = renderAttachedToDocument((Dom.textarea() + ..defaultValue = testValue + )()); + textareaElement = findDomNode(renderedInstance); + setSelectionRange(textareaElement, testValue.length, testValue.length); + + expect(textareaElement.selectionStart, equals(testValue.length)); + expect(textareaElement.selectionEnd, equals(testValue.length)); + }); + + // See: https://bugs.chromium.org/p/chromium/issues/detail?id=324360 + group('without throwing an error in Google Chrome when `props.type` is', () { + void verifyLackOfException() { + expect(renderedInstance, isNotNull, reason: 'test setup sanity check'); + expect(inputElement, isNotNull, reason: 'test setup sanity check'); + + expect(() => setSelectionRange(inputElement, testValue.length, testValue.length), returnsNormally); + } + + setUp(() { + startRecordingValidationWarnings(); + }); + + tearDown(() { + stopRecordingValidationWarnings(); + }); + + test('email', () { + renderedInstance = renderAttachedToDocument((Dom.input() + ..defaultValue = testValue + ..type = 'email' + )()); + inputElement = findDomNode(renderedInstance); + verifyLackOfException(); + }); + + test('number', () { + renderedInstance = renderAttachedToDocument((Dom.input() + ..defaultValue = testValue + ..type = 'number' + )()); + inputElement = findDomNode(renderedInstance); + verifyLackOfException(); + }); + }, testOn: 'chrome'); + }); + }); } @Factory() diff --git a/test/over_react/util/map_util_test.dart b/test/over_react/util/map_util_test.dart index adce5085d..da9524467 100644 --- a/test/over_react/util/map_util_test.dart +++ b/test/over_react/util/map_util_test.dart @@ -121,5 +121,23 @@ main() { expect(actual, equals(expected)); }); }); + + group('newStyleFromProps() returns', () { + test('a copy of the style map found in the specified props', () { + var styles = {'color': 'red', 'width': '10rem'}; + var props = domProps() + ..style = styles; + + expect(newStyleFromProps(props), equals(styles)); + }); + + test('an empty map when the specified props are null', () { + expect(newStyleFromProps(null), equals({})); + }); + + test('an empty map when the specified props have a null style map', () { + expect(newStyleFromProps(domProps()), equals({})); + }); + }); }); } diff --git a/test/over_react/util/prop_key_util_test.dart b/test/over_react/util/prop_key_util_test.dart new file mode 100644 index 000000000..ac6b19ffa --- /dev/null +++ b/test/over_react/util/prop_key_util_test.dart @@ -0,0 +1,48 @@ +library prop_key_util_test; + +import 'package:test/test.dart'; +import 'package:over_react/over_react.dart'; + +main() { + group('getPropKey', () { + test('returns the expected key', () { + var testProps = Test()..foo = 'baz'; + var fooPropKey = getPropKey((Map props) { (props as TestProps).foo; }, Test); // ignore: avoid_as + expect(testProps, equals({fooPropKey: 'baz'})); + }); + + test('throws if you don\'t access the prop', () { + expect(() => getPropKey((props) => false, Test), throwsStateError); + }); + + test('throws if you access the prop multiple times', () { + expect(() => getPropKey((Map props) { + (props as TestProps).foo; // ignore: avoid_as + (props as TestProps).foo; // ignore: avoid_as + }, Test), throwsStateError); + }); + + test('throws if you access multiple props', () { + expect(() => getPropKey((Map props) { + (props as TestProps).foo; // ignore: avoid_as + (props as TestProps).bar; // ignore: avoid_as + }, Test), throwsStateError); + }); + }); +} + +@Factory() +UiFactory Test; + +@Props() +class TestProps extends UiProps { + String foo; + + String bar; +} + +@Component() +class TestComponent extends UiComponent { + @override + render() {} +} diff --git a/test/over_react/util/react_wrappers_test.dart b/test/over_react/util/react_wrappers_test.dart index 7966ad57e..2d076d23e 100644 --- a/test/over_react/util/react_wrappers_test.dart +++ b/test/over_react/util/react_wrappers_test.dart @@ -460,10 +460,10 @@ main() { const Map testStyle = const {'background': 'white'}; test('returns props for a composite JS component ReactElement', () { - ReactElement instance = render(testJsComponentFactory({ + ReactElement instance = testJsComponentFactory({ 'jsProp': 'js', 'style': testStyle, - }, testChildren)); + }, testChildren); expect(getProps(instance), equals({ 'jsProp': 'js', @@ -485,6 +485,31 @@ main() { })); }); + test('returns props for a composite JS ReactComponent, even when the props change', () { + var mountNode = new DivElement(); + ReactComponent renderedInstance = react_dom.render(testJsComponentFactory({ + 'jsProp': 'js', + 'style': testStyle, + }, testChildren), mountNode); + + expect(getProps(renderedInstance), equals({ + 'jsProp': 'js', + 'style': testStyle, + 'children': testChildren + })); + + renderedInstance = react_dom.render(testJsComponentFactory({ + 'jsProp': 'other js', + 'style': testStyle, + }, testChildren), mountNode); + + expect(getProps(renderedInstance), equals({ + 'jsProp': 'other js', + 'style': testStyle, + 'children': testChildren + })); + }); + test('returns props for a DOM component ReactElement', () { ReactElement instance = (Dom.div() ..addProp('domProp', 'dom') @@ -499,10 +524,10 @@ main() { }); test('returns props for a Dart component ReactElement', () { - ReactElement instance = render(TestComponentFactory({ + ReactElement instance = TestComponentFactory({ 'dartProp': 'dart', 'style': testStyle, - }, testChildren)); + }, testChildren); expect(getProps(instance), equals({ 'dartProp': 'dart', @@ -524,6 +549,31 @@ main() { })); }); + test('returns props for a Dart component ReactComponent, even when the props change', () { + var mountNode = new DivElement(); + ReactComponent renderedInstance = react_dom.render(TestComponentFactory({ + 'jsProp': 'js', + 'style': testStyle, + }, testChildren), mountNode); + + expect(getProps(renderedInstance), equals({ + 'jsProp': 'js', + 'style': testStyle, + 'children': testChildren + })); + + renderedInstance = react_dom.render(TestComponentFactory({ + 'jsProp': 'other js', + 'style': testStyle, + }, testChildren), mountNode); + + expect(getProps(renderedInstance), equals({ + 'jsProp': 'other js', + 'style': testStyle, + 'children': testChildren + })); + }); + test('returns props as an unmodifiable map', () { ReactComponent renderedInstance = render(TestComponentFactory({ 'dartProp': 'dart' @@ -689,7 +739,6 @@ main() { /// Helper component for testing a Dart (react-dart) React component with cloneElement. final TestComponentFactory = react.registerComponent(() => new TestComponent()) as ReactComponentFactory; // ignore: avoid_as - class TestComponent extends react.Component { @override render() => Dom.div()(); diff --git a/test/over_react/util/test_mode_test.dart b/test/over_react/util/test_mode_test.dart index 55e752b6b..d0ac17fb5 100644 --- a/test/over_react/util/test_mode_test.dart +++ b/test/over_react/util/test_mode_test.dart @@ -18,7 +18,6 @@ import 'package:over_react/src/component_declaration/component_base.dart' as com import 'package:over_react/src/util/test_mode.dart'; import 'package:test/test.dart'; - /// Main entry point for enableTestMode and disabledTestMode testing main() { test('enableTestMode and disableTestMode set UiProps.testMode as expected', () { diff --git a/test/over_react_test.dart b/test/over_react_test.dart index 72e930630..fd9d77240 100644 --- a/test/over_react_test.dart +++ b/test/over_react_test.dart @@ -46,6 +46,7 @@ import 'over_react/util/handler_chain_util_test.dart' as handler_chain_util_test import 'over_react/util/map_util_test.dart' as map_util_test; import 'over_react/util/pretty_print_test.dart' as pretty_print_test; import 'over_react/util/prop_error_test.dart' as prop_error_test; +import 'over_react/util/prop_key_util_test.dart' as prop_key_util_test; import 'over_react/util/react_wrappers_test.dart' as react_wrappers_test; import 'over_react/util/rem_util_test.dart' as rem_util_test; import 'over_react/util/string_util_test.dart' as string_util_test; @@ -79,6 +80,7 @@ main() { map_util_test.main(); pretty_print_test.main(); prop_error_test.main(); + prop_key_util_test.main(); react_wrappers_test.main(); rem_util_test.main(); string_util_test.main();