Skip to content

Commit 7139217

Browse files
authored
Merge pull request #81 from mshoho/importantForLayout-property
importantForLayout property to track the size changes faster.
2 parents 92bbf7d + de10ae3 commit 7139217

7 files changed

Lines changed: 157 additions & 41 deletions

File tree

docs/docs/components/view.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ ignorePointerEvents: boolean = false; // web only
4343
// Can the component accept keyboard focus?
4444
focusable: boolean = false; // web only
4545

46+
// Additional invisible DOM elements will be added inside the view
47+
// to track the size changes that are performed behind our back by
48+
// the browser's layout engine faster (ViewBase checks for the layout
49+
// updates once a second and sometimes it's not fast enough)
50+
importantForLayout?: boolean = false; // web only
51+
4652
// Mouse-specific Events
4753
onDragEnter?: (e: DragEvent) => void = undefined;
4854
onDragOver?: (e: DragEvent) => void = undefined;
@@ -97,7 +103,7 @@ disableTouchOpacityAnimation?: boolean; // iOS and Android only
97103
// Opacity value the button should animate to, on touch on views that have onPress handlers.
98104
activeOpacity?: number; // iOS and Android only
99105

100-
// Background color that will be visible on touch on views that have onPress handlers.
106+
// Background color that will be visible on touch on views that have onPress handlers.
101107
underlayColor?: string; // ßiOS and Android only
102108
```
103109

src/common/Interfaces.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,6 @@ export abstract class UserInterface {
140140

141141
// On-screen Keyboard
142142
abstract dismissKeyboard(): void;
143-
144-
// Explicit layout change indication
145-
abstract layoutChangePending(): void;
146143
}
147144

148145
export abstract class Modal {

src/common/Types.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ export type LinkStyleRuleSet = StyleRuleSet<LinkStyle>;
277277

278278
export interface ImageStyle extends ViewAndImageCommonStyle, FlexboxStyle {
279279
resizeMode?: 'contain' | 'cover' | 'stretch';
280-
280+
281281
// This is an Android only style attribute that is used to fill the gap in the case of rounded corners
282282
// in gif images.
283283
overlayColor?: string;
@@ -452,7 +452,7 @@ export interface ImagePropsShared extends CommonProps {
452452
children?: ReactNode;
453453
resizeMode?: 'stretch' | 'contain' | 'cover' | 'auto' | 'repeat';
454454

455-
resizeMethod?: 'auto' | 'resize' | 'scale'; // Android only
455+
resizeMethod?: 'auto' | 'resize' | 'scale'; // Android only
456456
title?: string;
457457

458458
onLoad?: (size: Dimensions) => void;
@@ -489,17 +489,17 @@ export interface TextPropsShared extends CommonProps {
489489

490490
// iOS and Android only
491491
ellipsizeMode?: 'head' | 'middle'| 'tail';
492-
492+
493493
// Exposing this property as temporary workaround to fix a bug.
494494
// TODO : http://skype.vso.io/865016 : remove this exposed property
495495
// Used only for Android.
496496
textBreakStrategy?: 'highQuality' | 'simple' | 'balanced';
497497

498498
importantForAccessibility?: ImportantForAccessibility;
499-
499+
500500
// Android only
501501
elevation?: number;
502-
502+
503503
onPress?: (e: SyntheticEvent) => void;
504504
}
505505

@@ -523,6 +523,8 @@ export interface ViewPropsShared extends CommonProps, CommonAccessibilityProps {
523523
children?: ReactNode;
524524
focusable?: boolean;
525525

526+
importantForLayout?: boolean; // Web-only, additional invisible DOM elements will be added to track the size changes faster
527+
526528
// There are a couple of constraints when child animations are enabled:
527529
// - Every child must have a `key`.
528530
// - String refs aren't supported on children. Only callback refs are.
@@ -547,7 +549,7 @@ export interface ViewPropsShared extends CommonProps, CommonAccessibilityProps {
547549
onBlur?: (e: FocusEvent) => void;
548550

549551
// iOS and Android only. Visual touchfeedback properties
550-
disableTouchOpacityAnimation?: boolean;
552+
disableTouchOpacityAnimation?: boolean;
551553
activeOpacity?: number;
552554
underlayColor?: string;
553555
}
@@ -762,7 +764,7 @@ export interface TextInputPropsShared extends CommonProps, CommonAccessibilityPr
762764
secureTextEntry?: boolean;
763765
value?: string;
764766
textAlign?: 'auto' | 'left' | 'right' | 'center' | 'justify';
765-
767+
766768
// Should fonts be scaled according to system setting? Defaults
767769
// to true. iOS and Android only.
768770
allowFontScaling?: boolean;
@@ -869,9 +871,9 @@ export interface PopupOptions {
869871
popupWidth: number, popupHeight: number) => ReactNode;
870872

871873
// Returns a mounted component instance that controls the triggering of the popup.
872-
// In majority of cases, "anchor" of popup has handlers to control when the popup will be seen and this function is not required.
874+
// In majority of cases, "anchor" of popup has handlers to control when the popup will be seen and this function is not required.
873875
// In a few cases, where anchor is not the same as the whole component that triggers when the popup wil be seen, this can be used.
874-
// For instance, a button combined with a chevron icon, which on click triggers a popup below the chevron icon.
876+
// For instance, a button combined with a chevron icon, which on click triggers a popup below the chevron icon.
875877
// In this example, getElementTriggeringPopup() can return the container with button and chevron icon.
876878
getElementTriggeringPopup?: () => React.Component<any, any>;
877879

@@ -893,8 +895,8 @@ export interface PopupOptions {
893895
// already unmounted as it uses a time delay to accommodate a fade-out animation.
894896
onAnchorPressed?: (e: RX.Types.SyntheticEvent) => void;
895897

896-
// Determines if the anchor invoking the popup should behave like a toggle.
897-
// Value = true => Calling Popup.show will show the popup. A subsequent call, will hide the popup, and so on.
898+
// Determines if the anchor invoking the popup should behave like a toggle.
899+
// Value = true => Calling Popup.show will show the popup. A subsequent call, will hide the popup, and so on.
898900
// Value = false or undefined (default) => Calling Popup.show will always show the popup.
899901
dismissIfShown?: boolean;
900902
}

src/native-common/UserInterface.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,6 @@ export class UserInterface extends RX.UserInterface {
121121
renderMainView() {
122122
// Nothing to do
123123
}
124-
125-
layoutChangePending() {
126-
// Nothing to do
127-
}
128124
}
129125

130126
export default new UserInterface();

src/web/UserInterface.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -107,19 +107,6 @@ export class UserInterface extends RX.UserInterface {
107107
dismissKeyboard() {
108108
// Nothing to do
109109
}
110-
111-
layoutChangePending() {
112-
if (!this._layoutChangeAnimationFrame) {
113-
// ViewBase checks for the layout changes once a second or on window resize.
114-
// To avoid laggy layout updates we can indicate that there is a change pending.
115-
this._layoutChangeAnimationFrame = window.requestAnimationFrame(() => {
116-
this._layoutChangeAnimationFrame = undefined;
117-
let event = document.createEvent('HTMLEvents');
118-
event.initEvent('resize', true, false);
119-
window.dispatchEvent(event);
120-
});
121-
}
122-
}
123110
}
124111

125112
export default new UserInterface();

src/web/View.tsx

Lines changed: 136 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
import React = require('react');
11+
import ReactDOM = require('react-dom');
1112

1213
import AccessibilityUtil from './AccessibilityUtil';
1314
import AnimateListEdits from './listAnimations/AnimateListEdits';
@@ -24,6 +25,32 @@ const _styles = {
2425
flex: '0 0 auto',
2526
overflow: 'hidden',
2627
alignItems: 'stretch'
28+
},
29+
30+
// See resize detector comments in renderResizeDetectorIfNeeded() method below.
31+
resizeDetectorContainerStyles: {
32+
position: 'absolute',
33+
left: '0',
34+
top: '0',
35+
right: '0',
36+
bottom: '0',
37+
overflow: 'scroll',
38+
zIndex: '-1',
39+
visibility: 'hidden'
40+
},
41+
42+
resizeGrowDetectorStyles: {
43+
position: 'absolute',
44+
left: '100500px',
45+
top: '100500px',
46+
width: '1px',
47+
height: '1px'
48+
},
49+
50+
resizeShrinkDetectorStyles: {
51+
position: 'absolute',
52+
width: '150%',
53+
height: '150%'
2754
}
2855
};
2956

@@ -50,6 +77,106 @@ export class View extends ViewBase<Types.ViewProps, {}> {
5077
isRxParentAText: React.PropTypes.bool.isRequired
5178
};
5279

80+
private resizeDetectorAnimationFrame: number;
81+
private resizeDetectorNodes: { grow?: HTMLElement, shrink?: HTMLElement } = {};
82+
83+
private renderResizeDetectorIfNeeded(containerStyles: any): React.ReactNode {
84+
// If needed, additional invisible DOM elements will be added inside the
85+
// view to track the size changes that are performed behind our back by
86+
// the browser's layout engine faster (ViewBase checks for the layout
87+
// updates once a second and sometimes it's not fast enough).
88+
89+
// Unfortunately <div> doesn't have `resize` event, so we're trying to
90+
// detect the fact that the view has been resized with `scroll` events.
91+
// To do that, we create two scrollable <div>s and we put them into a
92+
// state in which `scroll` event is triggered by the browser when the
93+
// container gets resized (one element triggers `scroll` when the
94+
// container gets bigger, another triggers `scroll` when the container
95+
// gets smaller).
96+
97+
if (!this.props.importantForLayout) {
98+
return null;
99+
}
100+
101+
if (containerStyles.position !== 'relative') {
102+
console.error('View: importantForLayout property is applicable only for a view with relative position');
103+
return null;
104+
}
105+
106+
let initResizer = (key: 'grow' | 'shrink', ref: React.DOMComponent<React.HTMLAttributes>) => {
107+
const cur: HTMLElement = this.resizeDetectorNodes[key];
108+
const element = ReactDOM.findDOMNode<HTMLElement>(ref);
109+
110+
if (cur) {
111+
delete this.resizeDetectorNodes[key];
112+
}
113+
114+
if (element) {
115+
this.resizeDetectorNodes[key] = element;
116+
}
117+
118+
this.resizeDetectorOnScroll();
119+
};
120+
121+
return [
122+
(
123+
<div
124+
key={ 'grow' }
125+
style={ _styles.resizeDetectorContainerStyles }
126+
ref={ (ref) => initResizer('grow', ref) }
127+
onScroll={ () => this.resizeDetectorOnScroll() }>
128+
129+
<div style={ _styles.resizeGrowDetectorStyles }></div>
130+
</div>
131+
),
132+
(
133+
<div
134+
key={ 'shrink' }
135+
style={ _styles.resizeDetectorContainerStyles }
136+
ref={ (ref) => initResizer('shrink', ref) }
137+
onScroll={ () => this.resizeDetectorOnScroll() }>
138+
139+
<div style={ _styles.resizeShrinkDetectorStyles }></div>
140+
</div>
141+
)
142+
];
143+
}
144+
145+
private resizeDetectorReset() {
146+
// Scroll the detectors to the bottom-right corner so
147+
// that `scroll` events will be triggered when the container
148+
// is resized.
149+
const scrollMax = 100500;
150+
151+
let node = this.resizeDetectorNodes.grow;
152+
153+
if (node) {
154+
node.scrollLeft = scrollMax;
155+
node.scrollTop = scrollMax;
156+
}
157+
158+
node = this.resizeDetectorNodes.shrink;
159+
160+
if (node) {
161+
node.scrollLeft = scrollMax;
162+
node.scrollTop = scrollMax;
163+
}
164+
}
165+
166+
private resizeDetectorOnScroll() {
167+
if (this.resizeDetectorAnimationFrame) {
168+
// Do not execute action more often than once per animation frame.
169+
return;
170+
}
171+
172+
this.resizeDetectorAnimationFrame = window.requestAnimationFrame(() => {
173+
this.resizeDetectorReset();
174+
this.resizeDetectorAnimationFrame = undefined;
175+
ViewBase._checkViews();
176+
});
177+
178+
}
179+
53180
getChildContext() {
54181
// Let descendant Types components know that their nearest Types ancestor is not an Types.Text.
55182
// Because they're in an Types.View, they should use their normal styling rather than their
@@ -66,7 +193,7 @@ export class View extends ViewBase<Types.ViewProps, {}> {
66193
const ariaRole = AccessibilityUtil.accessibilityTraitToString(this.props.accessibilityTraits);
67194
const ariaSelected = AccessibilityUtil.accessibilityTraitToAriaSelected(this.props.accessibilityTraits);
68195
const isAriaHidden = AccessibilityUtil.isHidden(this.props.importantForAccessibility);
69-
196+
70197
let props: Types.AccessibilityHtmlAttributes = {
71198
role: ariaRole,
72199
tabIndex: this.props.tabIndex,
@@ -100,18 +227,19 @@ export class View extends ViewBase<Types.ViewProps, {}> {
100227
if (childAnimationsEnabled) {
101228
reactElement = (
102229
<AnimateListEdits
103-
{...props}
104-
animateChildEnter={this.props.animateChildEnter}
105-
animateChildMove={this.props.animateChildMove}
106-
animateChildLeave={this.props.animateChildLeave}
230+
{ ...props }
231+
animateChildEnter={ this.props.animateChildEnter }
232+
animateChildMove={ this.props.animateChildMove }
233+
animateChildLeave={ this.props.animateChildLeave }
107234
>
108-
{this.props.children}
235+
{ this.props.children }
109236
</AnimateListEdits>
110237
);
111238
} else {
112239
reactElement = (
113-
<div {...props} >
114-
{this.props.children}
240+
<div { ...props } >
241+
{ this.renderResizeDetectorIfNeeded(combinedStyles) }
242+
{ this.props.children }
115243
</div>
116244
);
117245
}

src/web/ViewBase.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export abstract class ViewBase<P extends Types.ViewProps, S> extends RX.ViewBase
7878
}
7979
}
8080

81-
private static _checkViews() {
81+
protected static _checkViews() {
8282
_.each(ViewBase._viewCheckingList, view => {
8383
view._checkAndReportLayout();
8484
});

0 commit comments

Comments
 (0)