From 1ffbbdd0d24485fccf079385917849aa8a5b2cf0 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Mon, 30 Jan 2017 10:02:41 -0700 Subject: [PATCH 1/6] Copy over ResizeSensor quickMount updates from web_skin_dart --- lib/src/component/resize_sensor.dart | 245 +++++++---- .../component/resize_sensor_test.dart | 385 ++++++++++++------ 2 files changed, 431 insertions(+), 199 deletions(-) diff --git a/lib/src/component/resize_sensor.dart b/lib/src/component/resize_sensor.dart index df120969f..be8c48a2f 100644 --- a/lib/src/component/resize_sensor.dart +++ b/lib/src/component/resize_sensor.dart @@ -39,7 +39,8 @@ abstract class ResizeSensorPropsMixin { static final ResizeSensorPropsMixinMapView defaultProps = new ResizeSensorPropsMixinMapView({}) ..isFlexChild = false ..isFlexContainer = false - ..shrink = false; + ..shrink = false + ..quickMount = false; Map get props; @@ -68,16 +69,31 @@ abstract class ResizeSensorPropsMixin { /// /// Default: false bool shrink; + + /// Whether quick-mount mode is enabled, which minimizes layouts caused by accessing element dimensions + /// during initialization, allowing the component to mount faster. + /// + /// When enabled: + /// + /// * The initial dimensions will not be retrieved, so the first [onResize] + /// event will contain `0` for the previous dimensions. + /// + /// * [onInitialize] will never be called. + /// + /// * The sensors will be initialized/reset in the next animation frame after mount, as opposed to synchronously, + /// helping to break up resulting layouts. + /// + /// Default: false + bool quickMount; } @Props() class ResizeSensorProps extends UiProps with ResizeSensorPropsMixin {} @Component() -class ResizeSensorComponent extends UiComponent { +class ResizeSensorComponent extends UiComponent with _SafeAnimationFrameMixin { // Refs - Element _expandSensorChildRef; Element _expandSensorRef; Element _collapseSensorRef; @@ -86,101 +102,103 @@ class ResizeSensorComponent extends UiComponent { ..addProps(ResizeSensorPropsMixin.defaultProps) ); + @override + void componentWillUnmount() { + super.componentWillUnmount(); + + cancelAnimationFrames(); + } + @override void componentDidMount() { - _reset(); + if (props.quickMount) { + assert(props.onInitialize == null || ValidationUtil.warn( + 'props.onInitialize will not be called when props.quickMount is true.' + )); + + // [1] Initialize/reset the sensor in the next animation frame after mount + // so that resulting layouts don't happen synchronously, and are better dispersed. + // + // [2] Ignore the first `2` scroll events triggered by resetting the scroll positions + // of the expand and collapse sensors. + // + // [3] Don't access the dimensions of the sensor to prevent unnecessary layouts. + + requestAnimationFrame(() { // [1] + _scrollEventsToIgnore = 2; // [2] + _reset(updateLastDimensions: false); // [3] + }); + } else { + _reset(); - if (props.onInitialize != null) { - var event = new ResizeSensorEvent(_lastWidth, _lastHeight, 0, 0); - props.onInitialize(event); + if (props.onInitialize != null) { + var event = new ResizeSensorEvent(_lastWidth, _lastHeight, 0, 0); + props.onInitialize(event); + } } } @override render() { - var expandSensorChild = (Dom.div() - ..ref = (ref) { _expandSensorChildRef = ref; } - ..style = _expandSensorChildStyle - )(); - var expandSensor = (Dom.div() ..className = 'resize-sensor-expand' ..onScroll = _handleSensorScroll ..style = props.shrink ? _shrinkBaseStyle : _baseStyle ..ref = (ref) { _expandSensorRef = ref; } - ..key = 'expandSensor' - )(expandSensorChild); - - var collapseSensorChild = (Dom.div()..style = _collapseSensorChildStyle)(); + )( + (Dom.div()..style = _expandSensorChildStyle)() + ); var collapseSensor = (Dom.div() ..className = 'resize-sensor-collapse' ..onScroll = _handleSensorScroll ..style = props.shrink ? _shrinkBaseStyle : _baseStyle ..ref = (ref) { _collapseSensorRef = ref; } - ..key = 'collapseSensor' - )(collapseSensorChild); - - var children = new List.from(props.children) - ..add( - (Dom.div() - ..className = 'resize-sensor' - ..style = props.shrink ? _shrinkBaseStyle : _baseStyle - ..key = 'resizeSensor' - )(expandSensor, collapseSensor) + )( + (Dom.div()..style = _collapseSensorChildStyle)() ); - Map wrapperStyles; + var resizeSensor = (Dom.div() + ..className = 'resize-sensor' + ..style = props.shrink ? _shrinkBaseStyle : _baseStyle + ..key = 'resizeSensor' + )(expandSensor, collapseSensor); + var wrapperStyles; if (props.isFlexChild) { - wrapperStyles = { - 'position': 'relative', - 'flex': '1 1 0%', - 'WebkitFlex': '1 1 0%', - 'msFlex': '1 1 0%', - 'display': 'block' - }; + wrapperStyles = _wrapperStylesFlexChild; } else if (props.isFlexContainer) { - wrapperStyles = { - 'position': 'relative', - 'flex': '1 1 0%', - 'WebkitFlex': '1 1 0%', - 'msFlex': '1 1 0%' - }; - - // IE 10 and Safari 8 need 'special' value prefixes for 'display:flex'. - if (browser.isInternetExplorer && browser.version.major <= 10) { - wrapperStyles['display'] = '-ms-flexbox'; - } else if (browser.isSafari && browser.version.major < 9) { - wrapperStyles['display'] = '-webkit-flex'; - } else { - wrapperStyles['display'] = 'flex'; - } - + wrapperStyles = _wrapperStylesFlexContainer; } else { - wrapperStyles = { - 'position': 'relative', - 'height': '100%', - 'width': '100%' - }; + wrapperStyles = _wrapperStyles;; } return (Dom.div() ..addProps(copyUnconsumedDomProps()) ..className = forwardingClassNameBuilder().toClassName() ..style = wrapperStyles - )(children); + )( + props.children, + resizeSensor + ); } /// When the expand or collapse sensors are resized, builds a [ResizeSensorEvent] and calls /// props.onResize with it. Then, calls through to [_reset()]. void _handleSensorScroll(react.SyntheticEvent _) { - Element sensor = findDomNode(this); + if (_scrollEventsToIgnore > 0) { + _scrollEventsToIgnore--; + return; + } + + var sensor = findDomNode(this); - if (sensor.offsetWidth != _lastWidth || sensor.offsetHeight != _lastHeight) { - var event = new ResizeSensorEvent(sensor.offsetWidth, sensor.offsetHeight, _lastWidth, _lastHeight); + var newWidth = sensor.offsetWidth; + var newHeight = sensor.offsetHeight; + if (newWidth != _lastWidth || newHeight != _lastHeight) { if (props.onResize != null) { + var event = new ResizeSensorEvent(newWidth, newHeight, _lastWidth, _lastHeight); props.onResize(event); } @@ -188,30 +206,39 @@ class ResizeSensorComponent extends UiComponent { } } - /// Update the width and height on [expandSensorChild], and the scroll position on - /// [expandSensorChild] and [collapseSensor]. + /// Reset the scroll positions on [_expandSensorRef] and [_collapseSensorRef] so that future + /// resizes will trigger scroll events. /// - /// Additionally update the state with the new [_lastWidth] and [_lastHeight]. - void _reset() { - Element expand = _expandSensorRef; - Element expandChild = _expandSensorChildRef; - Element collapse = _collapseSensorRef; - Element sensor = findDomNode(this); - - expandChild.style.width = '${expand.offsetWidth + 10}px'; - expandChild.style.height = '${expand.offsetHeight + 10}px'; - - expand.scrollLeft = expand.scrollWidth; - expand.scrollTop = expand.scrollHeight; + /// Additionally update the state with the new [_lastWidth] and [_lastHeight] when [updateLastDimensions] is true. + void _reset({bool updateLastDimensions: true}) { + if (updateLastDimensions) { + var sensor = findDomNode(this); + _lastWidth = sensor.offsetWidth; + _lastHeight = sensor.offsetHeight; + } - collapse.scrollLeft = collapse.scrollWidth; - collapse.scrollTop = collapse.scrollHeight; + // Scroll positions are clamped to their maxes; use this behavior to scroll to the end + // as opposed to scrollWidth/scrollHeight, which trigger reflows immediately. + _expandSensorRef + ..scrollLeft = _maxSensorSize + ..scrollTop = _maxSensorSize; - _lastWidth = sensor.offsetWidth; - _lastHeight = sensor.offsetHeight; + _collapseSensorRef + ..scrollLeft = _maxSensorSize + ..scrollTop = _maxSensorSize; } + /// The number of future scroll events to ignore. + /// + /// Resetting the sensors' scroll positions causes sensor scroll events to fire even though a resize didn't occur, + /// so this flag is used to ignore those scroll events on mount for performance reasons in quick-mount mode + /// (since the handler causes a layout by accessing the sensor's dimensions). + /// + /// This value is only set for the component's mount and __not__ reinitialized every time [_reset] is called + /// in order to avoid ignoring scroll events fired by actual resizes at the same time that the reset is taking place. + int _scrollEventsToIgnore = 0; + /// The most recently measured value for the height of the sensor. int _lastHeight = 0; @@ -219,6 +246,14 @@ class ResizeSensorComponent extends UiComponent { int _lastWidth = 0; } +/// The maximum size, in `px`, the sensor can be: 100,000. +/// +/// We want to use absolute values to avoid accessing element dimensions when possible, +/// and relative units like `%` don't work since they don't cause scroll events when sensor size changes. +/// +/// We could use `rem` or `vh`/`vw`, but that opens us up to more edge cases. +const int _maxSensorSize = 100 * 1000; + final Map _baseStyle = const { 'position': 'absolute', // Have this element reach "outside" its containing element in such a way to ensure its width/height are always at @@ -252,6 +287,11 @@ final Map _expandSensorChildStyle = const { 'top': '0', 'left': '0', 'visibility': 'hidden', + // Use a width/height that will always be larger than the expandSensor. + // We'd ideally want to do something like calc(100% + 10px), but that doesn't + // trigger scroll events the same way a fixed dimension does. + 'width': _maxSensorSize, + 'height': _maxSensorSize, // Set opacity in addition to visibility to work around Safari scrollbar bug. 'opacity': '0', }; @@ -267,6 +307,33 @@ final Map _collapseSensorChildStyle = const { 'opacity': '0', }; + +const Map _wrapperStyles = const { + 'position': 'relative', + 'height': '100%', + 'width': '100%', +}; + +const Map _wrapperStylesFlexChild = const { + 'position': 'relative', + 'flex': '1 1 0%', + 'msFlex': '1 1 0%', + 'display': 'block', +}; + +final Map _wrapperStylesFlexContainer = { + 'position': 'relative', + 'flex': '1 1 0%', + 'msFlex': '1 1 0%', + 'display': _displayFlex, +}; + +/// The browser-prefixed value for the CSS `display` property that enables flexbox. +final String _displayFlex = (() { + if (browser.isInternetExplorer && browser.version.major <= 10) return '-ms-flexbox'; + return 'flex'; +})(); + /// Used with [ResizeSensorHandler] to provide information about a resize. class ResizeSensorEvent { /// The new width, in pixels. @@ -291,3 +358,27 @@ class ResizeSensorPropsMixinMapView extends MapView with ResizeSensorPropsMixin @override Map get props => this; } + +/// A mixin that makes it easier to manage animation frames within a React component lifecycle. +class _SafeAnimationFrameMixin { + /// The ids of the pending animation frames. + final _animationFrameIds = []; + + /// Calls [Window.requestAnimationFrame] with the specified [callback], and keeps track of the + /// request ID so that it can be cancelled in [cancelAnimationFrames]. + void requestAnimationFrame(callback()) { + var queuedId = window.requestAnimationFrame((Object id) { + callback(); + _animationFrameIds.remove(id); + }); + + _animationFrameIds.add(queuedId); + } + + /// Cancels all pending animation frames requested by [requestAnimationFrame]. + /// + /// Should be called in [react.Component.componentWillUnmount]. + void cancelAnimationFrames() { + _animationFrameIds.forEach(window.cancelAnimationFrame); + } +} diff --git a/test/over_react/component/resize_sensor_test.dart b/test/over_react/component/resize_sensor_test.dart index 11dbbae49..7b02eb2e1 100644 --- a/test/over_react/component/resize_sensor_test.dart +++ b/test/over_react/component/resize_sensor_test.dart @@ -20,12 +20,12 @@ import 'dart:html'; 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; import 'package:test/test.dart'; import '../../test_util/test_util.dart'; import '../../wsd_test_util/common_component_tests.dart'; +import '../../wsd_test_util/validation_util_helpers.dart'; import '../../wsd_test_util/zone.dart'; void main() { @@ -34,6 +34,8 @@ void main() { const int defaultContainerHeight = 100; Element domTarget; + ResizeSensorComponent resizeSensorRef; + Element containerRef; setUp(() { domTarget = document.createElement('div'); @@ -44,53 +46,73 @@ void main() { tearDown(() { domTarget.remove(); + resizeSensorRef = null; + containerRef = null; }); - Element renderSensorIntoContainer({ResizeSensorHandler onInitialize, ResizeSensorHandler onResize, - int width: defaultContainerWidth, int height: defaultContainerHeight, Map resizeSensorProps}) { + void renderSensorIntoContainer({ + ResizeSensorHandler onInitialize, + ResizeSensorHandler onResize, + int width: defaultContainerWidth, + int height: defaultContainerHeight, + Map resizeSensorProps + }) { // Create component hierarchy. var sensor = (ResizeSensor() ..addProps(resizeSensorProps) ..onInitialize = onInitialize ..onResize = onResize + ..ref = (ref) { resizeSensorRef = ref; } )(); - var container = react.div({ - 'className': 'container', - 'style': { + var container = (Dom.div() + ..className = 'container' + ..style = { 'position': 'absolute', 'width': width, 'height': height } - }, sensor); + ..ref = (ref) { containerRef = ref; } + )(sensor); // Render into DOM and validate. - var jsContainer = react_dom.render(container, domTarget); - - // Return the container element for testing. - return findDomNode(jsContainer); + react_dom.render(container, domTarget); } - /// Expect resize sensor invokes registered `onResize` callback. + /// Expect resize sensor invokes registered [onResize] callback. /// /// Note: Test cases must await calls to this function. A boolean value is /// tracked to confirm callback invocation, instead of `expectAsync`, due to /// oddities in detecting callback invocations. - Future expectResizeAfter(void action(Element container), - {ResizeSensorHandler onResize, int width: defaultContainerWidth, int height: defaultContainerHeight, - Map resizeSensorProps}) async { - var resizeDetectedCompleter = new Completer(); - - Element containerEl; - containerEl = renderSensorIntoContainer(onResize: (event) { - if (onResize != null) { - onResize(event); - } - resizeDetectedCompleter.complete(); - }, width: width, height: height, resizeSensorProps: resizeSensorProps); - - action(containerEl); - - await resizeDetectedCompleter.future; + /// + /// The returned future will complete after [onResize] is called [resizeEventCount] times. + Future expectResizeAfter(void action(), { + ResizeSensorHandler onResize, + ResizeSensorHandler onInitialize, + int width: defaultContainerWidth, + int height: defaultContainerHeight, + Map resizeSensorProps, + int resizeEventCount: 1 + }) async { + var resizes = new StreamController(); + + var resizesDone = resizes.stream.take(resizeEventCount).drain(); + + renderSensorIntoContainer(onResize: (event) { + if (onResize != null) onResize(event); + + resizes.add(event); + }, width: width, height: height, onInitialize: onInitialize, resizeSensorProps: resizeSensorProps); + + if (resizeSensorRef.props.quickMount) { + // Quick-mount ResizeSensors can't detect changes until one animation frame and two scroll events + // after mounting. Give them some time to settle. + await window.animationFrame; + await window.animationFrame; + } + + action(); + + await resizesDone; } /// Expect resize sensor invokes registered `onInitialize` callback. @@ -130,8 +152,6 @@ void main() { 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.major < 9) { - expect(nodeStyleDecl.getPropertyValue('-webkit-flex'), '1 1 0%'); } else { expect(nodeStyleDecl.getPropertyValue('flex'), '1 1 0%'); } @@ -148,9 +168,6 @@ void main() { 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.major < 9) { - expect(renderedNode.style.display, equals('-webkit-flex')); - expect(nodeStyleDecl.getPropertyValue('-webkit-flex'), '1 1 0%'); } else { expect(renderedNode.style.display, equals('flex')); expect(nodeStyleDecl.getPropertyValue('flex'), '1 1 0%'); @@ -182,115 +199,239 @@ void main() { }); }); - group('should detect when bounding rect grows horizontally', () { - test('', () async{ - await expectResizeAfter((containerEl) { - containerEl.style.width = '${defaultContainerWidth * 2}px'; + void sharedResizeDetectTests({bool quickMount}) { + group('when props.quickMount is $quickMount: detects when the bounding rect', () { + const testResizeAmounts = const { + 'tiny': 1, + 'medium': 10, + 'large': 100, + 'huge': 1000, + 'Hugh-Mungus': 10000, + }; + + ResizeSensorProps props; + + setUp(() { + props = ResizeSensor() + ..quickMount = quickMount; }); - }); - test('even when the bounding rect is very small', () async { - await expectResizeAfter((containerEl) { - containerEl.style.width = '4px'; - }, width: 2, height: 2); - }); + group('grows horizontally', () { + testResizeAmounts.forEach((String description, int amount) { + test('by a $description amount (${amount}px)', () async { + await expectResizeAfter(() { + containerRef.style.width = '${defaultContainerWidth + amount}px'; + }, resizeSensorProps: props); + }); + }); + + test('even when the bounding rect is very small', () async { + await expectResizeAfter(() { + containerRef.style.width = '4px'; + }, width: 2, height: 2, resizeSensorProps: props); + }); + + test('and shrink is true', () async { + props.shrink = true; + + await expectResizeAfter(() { + containerRef.style.width = '${defaultContainerWidth * 2}px'; + }, resizeSensorProps: props); + }); + }); - test('and shrink is true', () async{ - await expectResizeAfter((containerEl) { - containerEl.style.width = '${defaultContainerWidth * 2}px'; - }, resizeSensorProps: ResizeSensor()..shrink = true); - }); - }); + group('grows vertically', () { + testResizeAmounts.forEach((String description, int amount) { + test('by a $description amount (${amount}px)', () async{ + await expectResizeAfter(() { + containerRef.style.height = '${defaultContainerHeight + amount}px'; + }, resizeSensorProps: props); + }); + }); + + test('even when the bounding rect is very small', () async { + await expectResizeAfter(() { + containerRef.style.height = '4px'; + }, width: 2, height: 2, resizeSensorProps: props); + }); + + test('and shrink is true', () async { + props.shrink = true; + + await expectResizeAfter(() { + containerRef.style.height = '${defaultContainerHeight * 2}px'; + }, resizeSensorProps: props); + }); + }); - group('should detect when bounding rect grows vertically', () { - test('', () async{ - await expectResizeAfter((containerEl) { - containerEl.style.height = '${defaultContainerHeight * 2}px'; + group('shrinks horizontally', () { + testResizeAmounts.forEach((String description, int amount) { + test('by a $description amount (${amount}px)', () async{ + await expectResizeAfter(() { + containerRef.style.width = '${defaultContainerWidth}px'; + }, width: defaultContainerWidth + amount, resizeSensorProps: props); + }); + }); + + test('even when the bounding rect is very small', () async { + await expectResizeAfter(() { + containerRef.style.width = '1px'; + }, width: 2, height: 2, resizeSensorProps: props); + }); + + test('and shrink is true', () async{ + props.shrink = true; + + await expectResizeAfter(() { + containerRef.style.width = '${defaultContainerWidth / 2}px'; + }, resizeSensorProps: props); + }); }); - }); - test('even when the bounding rect is very small', () async { - await expectResizeAfter((containerEl) { - containerEl.style.height = '4px'; - }, width: 2, height: 2); + group('shrinks vertically', () { + testResizeAmounts.forEach((String description, int amount) { + test('by a $description amount (${amount}px)', () async{ + await expectResizeAfter(() { + containerRef.style.height = '${defaultContainerHeight}px'; + }, height: defaultContainerHeight + amount, resizeSensorProps: props); + }); + }); + + test('even when the bounding rect is very small', () async { + await expectResizeAfter(() { + containerRef.style.height = '1px'; + }, width: 2, height: 2, resizeSensorProps: props); + }); + + test('and shrink is true', () async { + props.shrink = true; + + await expectResizeAfter(() { + containerRef.style.height = '${defaultContainerHeight / 2}px'; + }, resizeSensorProps: props); + }); + }); }); + } - test('and shrink is true', () async{ - await expectResizeAfter((containerEl) { - containerEl.style.height = '${defaultContainerHeight * 2}px'; - }, resizeSensorProps: ResizeSensor()..shrink = true); - }); - }); + sharedResizeDetectTests(quickMount: false); + sharedResizeDetectTests(quickMount: true); + + group('when quickMount is', () { + group('false:', () { + test('passes the correct event args on resize', () async { + await expectResizeAfter(() { + containerRef.style.width = '${defaultContainerWidth * 2}px'; + containerRef.style.height = '${defaultContainerHeight * 2}px'; + }, onResize: (ResizeSensorEvent event) { + zonedExpect(event.newWidth, equals(defaultContainerWidth * 2)); + zonedExpect(event.newHeight, equals(defaultContainerHeight * 2)); + zonedExpect(event.prevWidth, equals(defaultContainerWidth)); + zonedExpect(event.prevHeight, equals(defaultContainerHeight)); + }); + }); - group('should detect when bounding rect shrinks horizontally', () { - test('', () async{ - await expectResizeAfter((containerEl) { - containerEl.style.width = '${defaultContainerWidth / 2}px'; + group('passes the correct event args on initialize', () { + setUp(() { + startRecordingValidationWarnings(); + }); + + tearDown(() { + stopRecordingValidationWarnings(); + }); + + test('when initial width and height are non-zero', () async { + await expectInitialize(onInitialize: (ResizeSensorEvent event) { + zonedExpect(event.newWidth, equals(100)); + zonedExpect(event.newHeight, equals(100)); + zonedExpect(event.prevWidth, equals(0)); + zonedExpect(event.prevHeight, equals(0)); + }, width: 100, height: 100); + + // Should not warn about onInitialize when props.quickMount is false. + rejectValidationWarning(contains('onInitialize')); + }); + + test('when initial width and height are zero', () async { + await expectInitialize(onInitialize: (ResizeSensorEvent event) { + zonedExpect(event.newWidth, equals(0)); + zonedExpect(event.newHeight, equals(0)); + zonedExpect(event.prevWidth, equals(0)); + zonedExpect(event.prevHeight, equals(0)); + }, width: 0, height: 0); + + // Should not warn about onInitialize when props.quickMount is false. + rejectValidationWarning(contains('onInitialize')); + }); }); }); - test('even when the bounding rect is very small', () async { - await expectResizeAfter((containerEl) { - containerEl.style.width = '1px'; - }, width: 2, height: 2); - }); + group('true:', () { + ResizeSensorProps props; - test('and shrink is true', () async{ - await expectResizeAfter((containerEl) { - containerEl.style.width = '${defaultContainerWidth / 2}px'; - }, resizeSensorProps: ResizeSensor()..shrink = true); - }); - }); + setUp(() { + startRecordingValidationWarnings(); - group('should detect when bounding rect shrinks vertically', () { - test('', () async{ - await expectResizeAfter((containerEl) { - containerEl.style.height = '${defaultContainerHeight / 2}px'; + props = ResizeSensor() + ..quickMount = true; }); - }); - test('even when the bounding rect is very small', () async { - await expectResizeAfter((containerEl) { - containerEl.style.height = '1px'; - }, width: 2, height: 2); - }); + tearDown(() { + stopRecordingValidationWarnings(); + }); - test('and shrink is true', () async{ - await expectResizeAfter((containerEl) { - containerEl.style.height = '${defaultContainerHeight / 2}px'; - }, resizeSensorProps: ResizeSensor()..shrink = true); - }); - }); + test('warns when props.onInitialize is set and does not call it', () async { + bool onInitializeCalled = false; - test('should pass the correct event args on resize', () async { - await expectResizeAfter((containerEl) { - containerEl.style.width = '${defaultContainerWidth * 2}px'; - containerEl.style.height = '${defaultContainerHeight * 2}px'; - }, onResize: (ResizeSensorEvent event) { - zonedExpect(event.newWidth, equals(defaultContainerWidth * 2)); - zonedExpect(event.newHeight, equals(defaultContainerHeight * 2)); - zonedExpect(event.prevWidth, equals(defaultContainerWidth)); - zonedExpect(event.prevHeight, equals(defaultContainerHeight)); - }); - }); + await expectResizeAfter(() { + containerRef.style.width = '${defaultContainerWidth * 2}px'; + containerRef.style.height = '${defaultContainerHeight * 2}px'; + }, onInitialize: (_) { + onInitializeCalled = true; + }, resizeSensorProps: props); - group('should pass the correct event args on initialize', () { - test('when initial width and height are non-zero', () async { - await expectInitialize(onInitialize: (ResizeSensorEvent event) { - zonedExpect(event.newWidth, equals(100)); - zonedExpect(event.newHeight, equals(100)); - zonedExpect(event.prevWidth, equals(0)); - zonedExpect(event.prevHeight, equals(0)); - }, width: 100, height: 100); - }); + expect(onInitializeCalled, isFalse); + verifyValidationWarning(contains('props.onInitialize will not be called when props.quickMount is true')); + }, testOn: '!js'); + + test('does not warn about props.onInitialize when it is not set', () async { + await expectResizeAfter(() { + containerRef.style.width = '${defaultContainerWidth * 2}px'; + containerRef.style.height = '${defaultContainerHeight * 2}px'; + }, resizeSensorProps: props); - test('when initial width and height are zero', () async { - await expectInitialize(onInitialize: (ResizeSensorEvent event) { - zonedExpect(event.newWidth, equals(0)); - zonedExpect(event.newHeight, equals(0)); - zonedExpect(event.prevWidth, equals(0)); - zonedExpect(event.prevHeight, equals(0)); - }, width: 0, height: 0); + rejectValidationWarning(contains('onInitialize')); + }, testOn: '!js'); + + test('passes the correct event args on resize', () async { + var resizeEvents = []; + + await expectResizeAfter(() async { + containerRef.style.width = '${defaultContainerWidth + 10}px'; + containerRef.style.height = '${defaultContainerHeight + 10}px'; + + await window.animationFrame; + await window.animationFrame; + + containerRef.style.width = '${defaultContainerWidth + 20}px'; + containerRef.style.height = '${defaultContainerHeight + 20}px'; + }, onResize: resizeEvents.add, resizeEventCount: 2, resizeSensorProps: props); + + expect(resizeEvents, hasLength(2)); + + var firstEvent = resizeEvents[0]; + var secondEvent = resizeEvents[1]; + + expect(firstEvent.prevWidth, 0, reason: 'first previous dimensions should be zero when quick-mount is enabled'); + expect(firstEvent.prevHeight, 0, reason: 'first previous dimensions should be zero when quick-mount is enabled'); + expect(firstEvent.newWidth, defaultContainerWidth + 10, reason: 'should report the newSize properly'); + expect(firstEvent.newHeight, defaultContainerHeight + 10, reason: 'should report the newSize properly'); + + expect(secondEvent.prevWidth, defaultContainerWidth + 10, reason: 'should have stored previous dimensions for subsequent events'); + expect(secondEvent.prevHeight, defaultContainerHeight + 10, reason: 'should have stored previous dimensions for subsequent events'); + expect(secondEvent.newWidth, defaultContainerWidth + 20, reason: 'should report the newSize properly'); + expect(secondEvent.newHeight, defaultContainerHeight + 20, reason: 'should report the newSize properly'); + }); }); }); From c3e9d55440cb192a3f674a0ffcc55ca8b9720797 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Mon, 30 Jan 2017 10:14:05 -0700 Subject: [PATCH 2/6] Fix analysis error and bad animation frame cancelling --- lib/src/component/resize_sensor.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/component/resize_sensor.dart b/lib/src/component/resize_sensor.dart index be8c48a2f..29f6361bd 100644 --- a/lib/src/component/resize_sensor.dart +++ b/lib/src/component/resize_sensor.dart @@ -362,14 +362,15 @@ class ResizeSensorPropsMixinMapView extends MapView with ResizeSensorPropsMixin /// A mixin that makes it easier to manage animation frames within a React component lifecycle. class _SafeAnimationFrameMixin { /// The ids of the pending animation frames. - final _animationFrameIds = []; + final _animationFrameIds = []; /// Calls [Window.requestAnimationFrame] with the specified [callback], and keeps track of the /// request ID so that it can be cancelled in [cancelAnimationFrames]. void requestAnimationFrame(callback()) { - var queuedId = window.requestAnimationFrame((Object id) { + int queuedId; + queuedId = window.requestAnimationFrame((_) { callback(); - _animationFrameIds.remove(id); + _animationFrameIds.remove(queuedId); }); _animationFrameIds.add(queuedId); From 0b90b89609429ab0d9a298a2e6022f9c2aceab14 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Mon, 30 Jan 2017 10:16:26 -0700 Subject: [PATCH 3/6] Fix 1.19.1 strong mode error --- lib/src/component/resize_sensor.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/component/resize_sensor.dart b/lib/src/component/resize_sensor.dart index 29f6361bd..dacbba288 100644 --- a/lib/src/component/resize_sensor.dart +++ b/lib/src/component/resize_sensor.dart @@ -164,7 +164,7 @@ class ResizeSensorComponent extends UiComponent with _SafeAni ..key = 'resizeSensor' )(expandSensor, collapseSensor); - var wrapperStyles; + Map wrapperStyles; if (props.isFlexChild) { wrapperStyles = _wrapperStylesFlexChild; } else if (props.isFlexContainer) { From 858458a719989b818cb97d202ea97a6553e8c22e Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Mon, 30 Jan 2017 10:23:59 -0700 Subject: [PATCH 4/6] Update version info for 1.5.0 release --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1dce07dc..7bdf084dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # OverReact Changelog +## 1.5.0 +* Add `ResizeSensorProps.quickMount` flag for better performance when sensors are mounted often #46 + ## 1.4.0 > [Complete `1.4.0` Changeset](https://github.com/Workiva/over_react/compare/1.3.0...1.4.0) diff --git a/pubspec.yaml b/pubspec.yaml index 79c98e03d..2111b7c89 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: over_react -version: 1.4.0 +version: 1.5.0 description: A library for building statically-typed React UI components using Dart. homepage: https://github.com/Workiva/over_react/ authors: From 9c6a0c38209bc1cd5273495b1a605235b03218d3 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Mon, 30 Jan 2017 10:25:25 -0700 Subject: [PATCH 5/6] Add missing quiver dependency --- CHANGELOG.md | 1 + pubspec.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bdf084dd..ae47e99f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 1.5.0 * Add `ResizeSensorProps.quickMount` flag for better performance when sensors are mounted often #46 +* Add missing quiver dependency (now depends on quiver `>=0.21.4 <0.25.0`) ## 1.4.0 diff --git a/pubspec.yaml b/pubspec.yaml index 2111b7c89..31a34bc72 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: transformer_utils: "^0.1.1" w_flux: "^2.5.0" platform_detect: "^1.1.1" + quiver: ">=0.21.4 <0.25.0" dev_dependencies: matcher: ">=0.11.0 <0.13.0" coverage: "^0.7.2" From d3e9ea79ca2ee1baf481f6d152fba9bf8eed0b72 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Mon, 30 Jan 2017 11:19:29 -0700 Subject: [PATCH 6/6] Expand analyzer version constraint, remove unused markdown dependency --- CHANGELOG.md | 1 + pubspec.yaml | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae47e99f3..39d45e1b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.5.0 * Add `ResizeSensorProps.quickMount` flag for better performance when sensors are mounted often #46 * Add missing quiver dependency (now depends on quiver `>=0.21.4 <0.25.0`) +* Broaden analyzer dependency range to `>=0.26.1+3 <0.30.0` (was `>=0.26.1+3 <0.28.0`) ## 1.4.0 diff --git a/pubspec.yaml b/pubspec.yaml index 31a34bc72..a8f8c4da5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ authors: environment: sdk: ">=1.19.1" dependencies: - analyzer: ">=0.26.1+3 <0.28.0" + analyzer: ">=0.26.1+3 <0.30.0" barback: "^0.15.0" react: "^3.1.0" source_span: "^1.2.0" @@ -19,7 +19,6 @@ dev_dependencies: matcher: ">=0.11.0 <0.13.0" coverage: "^0.7.2" dart_dev: "^1.0.5" - markdown: "^0.8.0" mockito: "^0.11.0" test: "^0.12.6+2"