@@ -39,7 +39,8 @@ abstract class ResizeSensorPropsMixin {
39
39
static final ResizeSensorPropsMixinMapView defaultProps = new ResizeSensorPropsMixinMapView ({})
40
40
..isFlexChild = false
41
41
..isFlexContainer = false
42
- ..shrink = false ;
42
+ ..shrink = false
43
+ ..quickMount = false ;
43
44
44
45
Map get props;
45
46
@@ -68,16 +69,31 @@ abstract class ResizeSensorPropsMixin {
68
69
///
69
70
/// Default: false
70
71
bool shrink;
72
+
73
+ /// Whether quick-mount mode is enabled, which minimizes layouts caused by accessing element dimensions
74
+ /// during initialization, allowing the component to mount faster.
75
+ ///
76
+ /// When enabled:
77
+ ///
78
+ /// * The initial dimensions will not be retrieved, so the first [onResize]
79
+ /// event will contain `0` for the previous dimensions.
80
+ ///
81
+ /// * [onInitialize] will never be called.
82
+ ///
83
+ /// * The sensors will be initialized/reset in the next animation frame after mount, as opposed to synchronously,
84
+ /// helping to break up resulting layouts.
85
+ ///
86
+ /// Default: false
87
+ bool quickMount;
71
88
}
72
89
73
90
@Props ()
74
91
class ResizeSensorProps extends UiProps with ResizeSensorPropsMixin {}
75
92
76
93
@Component ()
77
- class ResizeSensorComponent extends UiComponent <ResizeSensorProps > {
94
+ class ResizeSensorComponent extends UiComponent <ResizeSensorProps > with _SafeAnimationFrameMixin {
78
95
// Refs
79
96
80
- Element _expandSensorChildRef;
81
97
Element _expandSensorRef;
82
98
Element _collapseSensorRef;
83
99
@@ -86,139 +102,158 @@ class ResizeSensorComponent extends UiComponent<ResizeSensorProps> {
86
102
..addProps (ResizeSensorPropsMixin .defaultProps)
87
103
);
88
104
105
+ @override
106
+ void componentWillUnmount () {
107
+ super .componentWillUnmount ();
108
+
109
+ cancelAnimationFrames ();
110
+ }
111
+
89
112
@override
90
113
void componentDidMount () {
91
- _reset ();
114
+ if (props.quickMount) {
115
+ assert (props.onInitialize == null || ValidationUtil .warn (
116
+ 'props.onInitialize will not be called when props.quickMount is true.'
117
+ ));
118
+
119
+ // [1] Initialize/reset the sensor in the next animation frame after mount
120
+ // so that resulting layouts don't happen synchronously, and are better dispersed.
121
+ //
122
+ // [2] Ignore the first `2` scroll events triggered by resetting the scroll positions
123
+ // of the expand and collapse sensors.
124
+ //
125
+ // [3] Don't access the dimensions of the sensor to prevent unnecessary layouts.
126
+
127
+ requestAnimationFrame (() { // [1]
128
+ _scrollEventsToIgnore = 2 ; // [2]
129
+ _reset (updateLastDimensions: false ); // [3]
130
+ });
131
+ } else {
132
+ _reset ();
92
133
93
- if (props.onInitialize != null ) {
94
- var event = new ResizeSensorEvent (_lastWidth, _lastHeight, 0 , 0 );
95
- props.onInitialize (event);
134
+ if (props.onInitialize != null ) {
135
+ var event = new ResizeSensorEvent (_lastWidth, _lastHeight, 0 , 0 );
136
+ props.onInitialize (event);
137
+ }
96
138
}
97
139
}
98
140
99
141
@override
100
142
render () {
101
- var expandSensorChild = (Dom .div ()
102
- ..ref = (ref) { _expandSensorChildRef = ref; }
103
- ..style = _expandSensorChildStyle
104
- )();
105
-
106
143
var expandSensor = (Dom .div ()
107
144
..className = 'resize-sensor-expand'
108
145
..onScroll = _handleSensorScroll
109
146
..style = props.shrink ? _shrinkBaseStyle : _baseStyle
110
147
..ref = (ref) { _expandSensorRef = ref; }
111
- ..key = 'expandSensor'
112
- )(expandSensorChild);
113
-
114
- var collapseSensorChild = (Dom .div ()..style = _collapseSensorChildStyle)();
148
+ )(
149
+ (Dom .div ()..style = _expandSensorChildStyle)()
150
+ );
115
151
116
152
var collapseSensor = (Dom .div ()
117
153
..className = 'resize-sensor-collapse'
118
154
..onScroll = _handleSensorScroll
119
155
..style = props.shrink ? _shrinkBaseStyle : _baseStyle
120
156
..ref = (ref) { _collapseSensorRef = ref; }
121
- ..key = 'collapseSensor'
122
- )(collapseSensorChild);
123
-
124
- var children = new List .from (props.children)
125
- ..add (
126
- (Dom .div ()
127
- ..className = 'resize-sensor'
128
- ..style = props.shrink ? _shrinkBaseStyle : _baseStyle
129
- ..key = 'resizeSensor'
130
- )(expandSensor, collapseSensor)
157
+ )(
158
+ (Dom .div ()..style = _collapseSensorChildStyle)()
131
159
);
132
160
133
- Map <String , dynamic > wrapperStyles;
161
+ var resizeSensor = (Dom .div ()
162
+ ..className = 'resize-sensor'
163
+ ..style = props.shrink ? _shrinkBaseStyle : _baseStyle
164
+ ..key = 'resizeSensor'
165
+ )(expandSensor, collapseSensor);
134
166
167
+ Map <String , dynamic > wrapperStyles;
135
168
if (props.isFlexChild) {
136
- wrapperStyles = {
137
- 'position' : 'relative' ,
138
- 'flex' : '1 1 0%' ,
139
- 'WebkitFlex' : '1 1 0%' ,
140
- 'msFlex' : '1 1 0%' ,
141
- 'display' : 'block'
142
- };
169
+ wrapperStyles = _wrapperStylesFlexChild;
143
170
} else if (props.isFlexContainer) {
144
- wrapperStyles = {
145
- 'position' : 'relative' ,
146
- 'flex' : '1 1 0%' ,
147
- 'WebkitFlex' : '1 1 0%' ,
148
- 'msFlex' : '1 1 0%'
149
- };
150
-
151
- // IE 10 and Safari 8 need 'special' value prefixes for 'display:flex'.
152
- if (browser.isInternetExplorer && browser.version.major <= 10 ) {
153
- wrapperStyles['display' ] = '-ms-flexbox' ;
154
- } else if (browser.isSafari && browser.version.major < 9 ) {
155
- wrapperStyles['display' ] = '-webkit-flex' ;
156
- } else {
157
- wrapperStyles['display' ] = 'flex' ;
158
- }
159
-
171
+ wrapperStyles = _wrapperStylesFlexContainer;
160
172
} else {
161
- wrapperStyles = {
162
- 'position' : 'relative' ,
163
- 'height' : '100%' ,
164
- 'width' : '100%'
165
- };
173
+ wrapperStyles = _wrapperStyles;;
166
174
}
167
175
168
176
return (Dom .div ()
169
177
..addProps (copyUnconsumedDomProps ())
170
178
..className = forwardingClassNameBuilder ().toClassName ()
171
179
..style = wrapperStyles
172
- )(children);
180
+ )(
181
+ props.children,
182
+ resizeSensor
183
+ );
173
184
}
174
185
175
186
/// When the expand or collapse sensors are resized, builds a [ResizeSensorEvent] and calls
176
187
/// props.onResize with it. Then, calls through to [_reset()] .
177
188
void _handleSensorScroll (react.SyntheticEvent _) {
178
- Element sensor = findDomNode (this );
189
+ if (_scrollEventsToIgnore > 0 ) {
190
+ _scrollEventsToIgnore-- ;
191
+ return ;
192
+ }
179
193
180
- if (sensor.offsetWidth != _lastWidth || sensor.offsetHeight != _lastHeight) {
181
- var event = new ResizeSensorEvent (sensor.offsetWidth, sensor.offsetHeight, _lastWidth, _lastHeight);
194
+ var sensor = findDomNode (this );
182
195
196
+ var newWidth = sensor.offsetWidth;
197
+ var newHeight = sensor.offsetHeight;
198
+
199
+ if (newWidth != _lastWidth || newHeight != _lastHeight) {
183
200
if (props.onResize != null ) {
201
+ var event = new ResizeSensorEvent (newWidth, newHeight, _lastWidth, _lastHeight);
184
202
props.onResize (event);
185
203
}
186
204
187
205
_reset ();
188
206
}
189
207
}
190
208
191
- /// Update the width and height on [expandSensorChild] , and the scroll position on
192
- /// [expandSensorChild] and [collapseSensor] .
209
+ /// Reset the scroll positions on [_expandSensorRef] and [_collapseSensorRef] so that future
210
+ /// resizes will trigger scroll events .
193
211
///
194
- /// Additionally update the state with the new [_lastWidth] and [_lastHeight] .
195
- void _reset () {
196
- Element expand = _expandSensorRef;
197
- Element expandChild = _expandSensorChildRef;
198
- Element collapse = _collapseSensorRef;
199
- Element sensor = findDomNode (this );
200
-
201
- expandChild.style.width = '${expand .offsetWidth + 10 }px' ;
202
- expandChild.style.height = '${expand .offsetHeight + 10 }px' ;
203
-
204
- expand.scrollLeft = expand.scrollWidth;
205
- expand.scrollTop = expand.scrollHeight;
212
+ /// Additionally update the state with the new [_lastWidth] and [_lastHeight] when [updateLastDimensions] is true.
213
+ void _reset ({bool updateLastDimensions: true }) {
214
+ if (updateLastDimensions) {
215
+ var sensor = findDomNode (this );
216
+ _lastWidth = sensor.offsetWidth;
217
+ _lastHeight = sensor.offsetHeight;
218
+ }
206
219
207
- collapse.scrollLeft = collapse.scrollWidth;
208
- collapse.scrollTop = collapse. scrollHeight;
220
+ // Scroll positions are clamped to their maxes; use this behavior to scroll to the end
221
+ // as opposed to scrollWidth/ scrollHeight, which trigger reflows immediately.
209
222
223
+ _expandSensorRef
224
+ ..scrollLeft = _maxSensorSize
225
+ ..scrollTop = _maxSensorSize;
210
226
211
- _lastWidth = sensor.offsetWidth;
212
- _lastHeight = sensor.offsetHeight;
227
+ _collapseSensorRef
228
+ ..scrollLeft = _maxSensorSize
229
+ ..scrollTop = _maxSensorSize;
213
230
}
214
231
232
+ /// The number of future scroll events to ignore.
233
+ ///
234
+ /// Resetting the sensors' scroll positions causes sensor scroll events to fire even though a resize didn't occur,
235
+ /// so this flag is used to ignore those scroll events on mount for performance reasons in quick-mount mode
236
+ /// (since the handler causes a layout by accessing the sensor's dimensions).
237
+ ///
238
+ /// This value is only set for the component's mount and __not__ reinitialized every time [_reset] is called
239
+ /// in order to avoid ignoring scroll events fired by actual resizes at the same time that the reset is taking place.
240
+ int _scrollEventsToIgnore = 0 ;
241
+
215
242
/// The most recently measured value for the height of the sensor.
216
243
int _lastHeight = 0 ;
217
244
218
245
/// The most recently measured value for the width of the sensor.
219
246
int _lastWidth = 0 ;
220
247
}
221
248
249
+ /// The maximum size, in `px` , the sensor can be: 100,000.
250
+ ///
251
+ /// We want to use absolute values to avoid accessing element dimensions when possible,
252
+ /// and relative units like `%` don't work since they don't cause scroll events when sensor size changes.
253
+ ///
254
+ /// We could use `rem` or `vh` /`vw` , but that opens us up to more edge cases.
255
+ const int _maxSensorSize = 100 * 1000 ;
256
+
222
257
final Map <String , dynamic > _baseStyle = const {
223
258
'position' : 'absolute' ,
224
259
// 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<String, dynamic> _expandSensorChildStyle = const {
252
287
'top' : '0' ,
253
288
'left' : '0' ,
254
289
'visibility' : 'hidden' ,
290
+ // Use a width/height that will always be larger than the expandSensor.
291
+ // We'd ideally want to do something like calc(100% + 10px), but that doesn't
292
+ // trigger scroll events the same way a fixed dimension does.
293
+ 'width' : _maxSensorSize,
294
+ 'height' : _maxSensorSize,
255
295
// Set opacity in addition to visibility to work around Safari scrollbar bug.
256
296
'opacity' : '0' ,
257
297
};
@@ -267,6 +307,33 @@ final Map<String, dynamic> _collapseSensorChildStyle = const {
267
307
'opacity' : '0' ,
268
308
};
269
309
310
+
311
+ const Map <String , dynamic > _wrapperStyles = const {
312
+ 'position' : 'relative' ,
313
+ 'height' : '100%' ,
314
+ 'width' : '100%' ,
315
+ };
316
+
317
+ const Map <String , dynamic > _wrapperStylesFlexChild = const {
318
+ 'position' : 'relative' ,
319
+ 'flex' : '1 1 0%' ,
320
+ 'msFlex' : '1 1 0%' ,
321
+ 'display' : 'block' ,
322
+ };
323
+
324
+ final Map <String , dynamic > _wrapperStylesFlexContainer = {
325
+ 'position' : 'relative' ,
326
+ 'flex' : '1 1 0%' ,
327
+ 'msFlex' : '1 1 0%' ,
328
+ 'display' : _displayFlex,
329
+ };
330
+
331
+ /// The browser-prefixed value for the CSS `display` property that enables flexbox.
332
+ final String _displayFlex = (() {
333
+ if (browser.isInternetExplorer && browser.version.major <= 10 ) return '-ms-flexbox' ;
334
+ return 'flex' ;
335
+ })();
336
+
270
337
/// Used with [ResizeSensorHandler] to provide information about a resize.
271
338
class ResizeSensorEvent {
272
339
/// The new width, in pixels.
@@ -291,3 +358,28 @@ class ResizeSensorPropsMixinMapView extends MapView with ResizeSensorPropsMixin
291
358
@override
292
359
Map get props => this ;
293
360
}
361
+
362
+ /// A mixin that makes it easier to manage animation frames within a React component lifecycle.
363
+ class _SafeAnimationFrameMixin {
364
+ /// The ids of the pending animation frames.
365
+ final _animationFrameIds = < int > [];
366
+
367
+ /// Calls [Window.requestAnimationFrame] with the specified [callback] , and keeps track of the
368
+ /// request ID so that it can be cancelled in [cancelAnimationFrames] .
369
+ void requestAnimationFrame (callback ()) {
370
+ int queuedId;
371
+ queuedId = window.requestAnimationFrame ((_) {
372
+ callback ();
373
+ _animationFrameIds.remove (queuedId);
374
+ });
375
+
376
+ _animationFrameIds.add (queuedId);
377
+ }
378
+
379
+ /// Cancels all pending animation frames requested by [requestAnimationFrame] .
380
+ ///
381
+ /// Should be called in [react.Component.componentWillUnmount] .
382
+ void cancelAnimationFrames () {
383
+ _animationFrameIds.forEach (window.cancelAnimationFrame);
384
+ }
385
+ }
0 commit comments