Skip to content

Commit 6329e2a

Browse files
authored
Merge pull request #46 from greglittlefield-wf/resize_sensor_quick_mount
UIP-1976, UIP-1975 Release over_react 1.5.0 (HOTFIX)
2 parents bb39096 + d3e9ea7 commit 6329e2a

File tree

4 files changed

+440
-202
lines changed

4 files changed

+440
-202
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# OverReact Changelog
22

3+
## 1.5.0
4+
* Add `ResizeSensorProps.quickMount` flag for better performance when sensors are mounted often #46
5+
* Add missing quiver dependency (now depends on quiver `>=0.21.4 <0.25.0`)
6+
* Broaden analyzer dependency range to `>=0.26.1+3 <0.30.0` (was `>=0.26.1+3 <0.28.0`)
7+
38
## 1.4.0
49

510
> [Complete `1.4.0` Changeset](https://github.com/Workiva/over_react/compare/1.3.0...1.4.0)

lib/src/component/resize_sensor.dart

+169-77
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ abstract class ResizeSensorPropsMixin {
3939
static final ResizeSensorPropsMixinMapView defaultProps = new ResizeSensorPropsMixinMapView({})
4040
..isFlexChild = false
4141
..isFlexContainer = false
42-
..shrink = false;
42+
..shrink = false
43+
..quickMount = false;
4344

4445
Map get props;
4546

@@ -68,16 +69,31 @@ abstract class ResizeSensorPropsMixin {
6869
///
6970
/// Default: false
7071
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;
7188
}
7289

7390
@Props()
7491
class ResizeSensorProps extends UiProps with ResizeSensorPropsMixin {}
7592

7693
@Component()
77-
class ResizeSensorComponent extends UiComponent<ResizeSensorProps> {
94+
class ResizeSensorComponent extends UiComponent<ResizeSensorProps> with _SafeAnimationFrameMixin {
7895
// Refs
7996

80-
Element _expandSensorChildRef;
8197
Element _expandSensorRef;
8298
Element _collapseSensorRef;
8399

@@ -86,139 +102,158 @@ class ResizeSensorComponent extends UiComponent<ResizeSensorProps> {
86102
..addProps(ResizeSensorPropsMixin.defaultProps)
87103
);
88104

105+
@override
106+
void componentWillUnmount() {
107+
super.componentWillUnmount();
108+
109+
cancelAnimationFrames();
110+
}
111+
89112
@override
90113
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();
92133

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+
}
96138
}
97139
}
98140

99141
@override
100142
render() {
101-
var expandSensorChild = (Dom.div()
102-
..ref = (ref) { _expandSensorChildRef = ref; }
103-
..style = _expandSensorChildStyle
104-
)();
105-
106143
var expandSensor = (Dom.div()
107144
..className = 'resize-sensor-expand'
108145
..onScroll = _handleSensorScroll
109146
..style = props.shrink ? _shrinkBaseStyle : _baseStyle
110147
..ref = (ref) { _expandSensorRef = ref; }
111-
..key = 'expandSensor'
112-
)(expandSensorChild);
113-
114-
var collapseSensorChild = (Dom.div()..style = _collapseSensorChildStyle)();
148+
)(
149+
(Dom.div()..style = _expandSensorChildStyle)()
150+
);
115151

116152
var collapseSensor = (Dom.div()
117153
..className = 'resize-sensor-collapse'
118154
..onScroll = _handleSensorScroll
119155
..style = props.shrink ? _shrinkBaseStyle : _baseStyle
120156
..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)()
131159
);
132160

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);
134166

167+
Map<String, dynamic> wrapperStyles;
135168
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;
143170
} 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;
160172
} else {
161-
wrapperStyles = {
162-
'position': 'relative',
163-
'height': '100%',
164-
'width': '100%'
165-
};
173+
wrapperStyles = _wrapperStyles;;
166174
}
167175

168176
return (Dom.div()
169177
..addProps(copyUnconsumedDomProps())
170178
..className = forwardingClassNameBuilder().toClassName()
171179
..style = wrapperStyles
172-
)(children);
180+
)(
181+
props.children,
182+
resizeSensor
183+
);
173184
}
174185

175186
/// When the expand or collapse sensors are resized, builds a [ResizeSensorEvent] and calls
176187
/// props.onResize with it. Then, calls through to [_reset()].
177188
void _handleSensorScroll(react.SyntheticEvent _) {
178-
Element sensor = findDomNode(this);
189+
if (_scrollEventsToIgnore > 0) {
190+
_scrollEventsToIgnore--;
191+
return;
192+
}
179193

180-
if (sensor.offsetWidth != _lastWidth || sensor.offsetHeight != _lastHeight) {
181-
var event = new ResizeSensorEvent(sensor.offsetWidth, sensor.offsetHeight, _lastWidth, _lastHeight);
194+
var sensor = findDomNode(this);
182195

196+
var newWidth = sensor.offsetWidth;
197+
var newHeight = sensor.offsetHeight;
198+
199+
if (newWidth != _lastWidth || newHeight != _lastHeight) {
183200
if (props.onResize != null) {
201+
var event = new ResizeSensorEvent(newWidth, newHeight, _lastWidth, _lastHeight);
184202
props.onResize(event);
185203
}
186204

187205
_reset();
188206
}
189207
}
190208

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.
193211
///
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+
}
206219

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.
209222

223+
_expandSensorRef
224+
..scrollLeft = _maxSensorSize
225+
..scrollTop = _maxSensorSize;
210226

211-
_lastWidth = sensor.offsetWidth;
212-
_lastHeight = sensor.offsetHeight;
227+
_collapseSensorRef
228+
..scrollLeft = _maxSensorSize
229+
..scrollTop = _maxSensorSize;
213230
}
214231

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+
215242
/// The most recently measured value for the height of the sensor.
216243
int _lastHeight = 0;
217244

218245
/// The most recently measured value for the width of the sensor.
219246
int _lastWidth = 0;
220247
}
221248

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+
222257
final Map<String, dynamic> _baseStyle = const {
223258
'position': 'absolute',
224259
// 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 {
252287
'top': '0',
253288
'left': '0',
254289
'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,
255295
// Set opacity in addition to visibility to work around Safari scrollbar bug.
256296
'opacity': '0',
257297
};
@@ -267,6 +307,33 @@ final Map<String, dynamic> _collapseSensorChildStyle = const {
267307
'opacity': '0',
268308
};
269309

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+
270337
/// Used with [ResizeSensorHandler] to provide information about a resize.
271338
class ResizeSensorEvent {
272339
/// The new width, in pixels.
@@ -291,3 +358,28 @@ class ResizeSensorPropsMixinMapView extends MapView with ResizeSensorPropsMixin
291358
@override
292359
Map get props => this;
293360
}
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+
}

pubspec.yaml

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
name: over_react
2-
version: 1.4.0
2+
version: 1.5.0
33
description: A library for building statically-typed React UI components using Dart.
44
homepage: https://github.com/Workiva/over_react/
55
authors:
66
- Workiva UI Platform Team <[email protected]>
77
environment:
88
sdk: ">=1.19.1"
99
dependencies:
10-
analyzer: ">=0.26.1+3 <0.28.0"
10+
analyzer: ">=0.26.1+3 <0.30.0"
1111
barback: "^0.15.0"
1212
react: "^3.1.0"
1313
source_span: "^1.2.0"
1414
transformer_utils: "^0.1.1"
1515
w_flux: "^2.5.0"
1616
platform_detect: "^1.1.1"
17+
quiver: ">=0.21.4 <0.25.0"
1718
dev_dependencies:
1819
matcher: ">=0.11.0 <0.13.0"
1920
coverage: "^0.7.2"
2021
dart_dev: "^1.0.5"
21-
markdown: "^0.8.0"
2222
mockito: "^0.11.0"
2323
test: "^0.12.6+2"
2424

0 commit comments

Comments
 (0)