Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UIP-1976, UIP-1975 Release over_react 1.5.0 (HOTFIX) #46

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# OverReact Changelog

## 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

> [Complete `1.4.0` Changeset](https://github.com/Workiva/over_react/compare/1.3.0...1.4.0)
Expand Down
246 changes: 169 additions & 77 deletions lib/src/component/resize_sensor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<ResizeSensorProps> {
class ResizeSensorComponent extends UiComponent<ResizeSensorProps> with _SafeAnimationFrameMixin {
// Refs

Element _expandSensorChildRef;
Element _expandSensorRef;
Element _collapseSensorRef;

Expand All @@ -86,139 +102,158 @@ class ResizeSensorComponent extends UiComponent<ResizeSensorProps> {
..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<String, dynamic> wrapperStyles;
var resizeSensor = (Dom.div()
..className = 'resize-sensor'
..style = props.shrink ? _shrinkBaseStyle : _baseStyle
..key = 'resizeSensor'
)(expandSensor, collapseSensor);

Map<String, dynamic> 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;
}

if (sensor.offsetWidth != _lastWidth || sensor.offsetHeight != _lastHeight) {
var event = new ResizeSensorEvent(sensor.offsetWidth, sensor.offsetHeight, _lastWidth, _lastHeight);
var sensor = findDomNode(this);

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

_reset();
}
}

/// 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;

/// The most recently measured value for the width of the sensor.
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<String, dynamic> _baseStyle = const {
'position': 'absolute',
// Have this element reach "outside" its containing element in such a way to ensure its width/height are always at
Expand Down Expand Up @@ -252,6 +287,11 @@ final Map<String, dynamic> _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',
};
Expand All @@ -267,6 +307,33 @@ final Map<String, dynamic> _collapseSensorChildStyle = const {
'opacity': '0',
};


const Map<String, dynamic> _wrapperStyles = const {
'position': 'relative',
'height': '100%',
'width': '100%',
};

const Map<String, dynamic> _wrapperStylesFlexChild = const {
'position': 'relative',
'flex': '1 1 0%',
'msFlex': '1 1 0%',
'display': 'block',
};

final Map<String, dynamic> _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.
Expand All @@ -291,3 +358,28 @@ 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 = <int>[];

/// 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()) {
int queuedId;
queuedId = window.requestAnimationFrame((_) {
callback();
_animationFrameIds.remove(queuedId);
});

_animationFrameIds.add(queuedId);
}

/// Cancels all pending animation frames requested by [requestAnimationFrame].
///
/// Should be called in [react.Component.componentWillUnmount].
void cancelAnimationFrames() {
_animationFrameIds.forEach(window.cancelAnimationFrame);
}
}
6 changes: 3 additions & 3 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
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:
- Workiva UI Platform Team <uip@workiva.com>
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"
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"
dart_dev: "^1.0.5"
markdown: "^0.8.0"
mockito: "^0.11.0"
test: "^0.12.6+2"

Expand Down
Loading