-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Callout/Positioning: perf improvement with hidden flag and improved repositioning logic #4419
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
Changes from 11 commits
2ea4e38
a1df02e
2cec49d
796b8f9
2c210b8
703d4d8
6846fb7
9795ccd
474a789
32335bc
e1af7b5
84158f8
d5e4716
a82e34e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "changes": [ | ||
| { | ||
| "packageName": "office-ui-fabric-react", | ||
| "comment": "Callout/Positioning: Improve callout perf with hidden flag and improve repositioning logic", | ||
| "type": "minor" | ||
| } | ||
| ], | ||
| "packageName": "office-ui-fabric-react", | ||
| "email": "joschect@microsoft.com" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,6 +78,7 @@ export class CalloutContentBase extends BaseComponent<ICalloutProps, ICalloutSta | |
| private _positionAttempts: number; | ||
| private _target: Element | MouseEvent | IPoint | null; | ||
| private _setHeightOffsetTimer: number; | ||
| private _hasListeners = false; | ||
|
|
||
| constructor(props: ICalloutProps) { | ||
| super(props); | ||
|
|
@@ -97,7 +98,16 @@ export class CalloutContentBase extends BaseComponent<ICalloutProps, ICalloutSta | |
|
|
||
| public componentDidUpdate() { | ||
| this._setInitialFocus(); | ||
| this._updateAsyncPosition(); | ||
| if (!this.props.hidden) { | ||
| if (!this._hasListeners) { | ||
| this._addListeners(); | ||
| } | ||
| this._updateAsyncPosition(); | ||
| } else { | ||
| if (this._hasListeners) { | ||
| this._removeListeners(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public componentWillMount() { | ||
|
|
@@ -119,10 +129,19 @@ export class CalloutContentBase extends BaseComponent<ICalloutProps, ICalloutSta | |
| if (newProps.finalHeight !== this.props.finalHeight) { | ||
| this._setHeightOffsetEveryFrame(); | ||
| } | ||
|
|
||
| // if the callout becomes hidden, then remove any positions that were placed on it. | ||
| if (newProps.hidden && newProps.hidden !== this.props.hidden) { | ||
| this.setState({ | ||
| positions: undefined | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| public componentDidMount() { | ||
| this._onComponentDidMount(); | ||
| if (!this.props.hidden) { | ||
| this._onComponentDidMount(); | ||
| } | ||
| } | ||
|
|
||
| public render() { | ||
|
|
@@ -174,11 +193,13 @@ export class CalloutContentBase extends BaseComponent<ICalloutProps, ICalloutSta | |
| ); | ||
|
|
||
| const overflowStyle: React.CSSProperties = overflowYHidden ? { overflowY: 'hidden' } : {}; | ||
| const visibilityStyle: React.CSSProperties | undefined = this.props.hidden ? { visibility: 'hidden' } : undefined; | ||
| // React.CSSProperties does not understand IRawStyle, so the inline animations will need to be cast as any for now. | ||
| const content = ( | ||
| <div | ||
| ref={ this._hostElement } | ||
| className={ this._classNames.container } | ||
| style={ visibilityStyle } | ||
| > | ||
| <div | ||
| className={ css(this._classNames.root, positions && positions.targetEdge && ANIMATIONS[positions.targetEdge!]) } | ||
|
|
@@ -195,7 +216,7 @@ export class CalloutContentBase extends BaseComponent<ICalloutProps, ICalloutSta | |
| />) } | ||
| { beakVisible && | ||
| (<div className={ this._classNames.beakCurtain } />) } | ||
| <Popup | ||
| { !this.props.hidden && <Popup | ||
| role={ role } | ||
| ariaLabel={ ariaLabel } | ||
| ariaDescribedBy={ ariaDescribedBy } | ||
|
|
@@ -208,8 +229,9 @@ export class CalloutContentBase extends BaseComponent<ICalloutProps, ICalloutSta | |
| > | ||
| { children } | ||
| </Popup> | ||
| } | ||
| </div> | ||
| </div> | ||
| </div > | ||
| ); | ||
|
|
||
| return content; | ||
|
|
@@ -247,11 +269,23 @@ export class CalloutContentBase extends BaseComponent<ICalloutProps, ICalloutSta | |
| protected _setInitialFocus = (): void => { | ||
| if (this.props.setInitialFocus && !this._didSetInitialFocus && this.state.positions && this._calloutElement.value) { | ||
| this._didSetInitialFocus = true; | ||
| focusFirstChild(this._calloutElement.value); | ||
| this._async.requestAnimationFrame(() => focusFirstChild(this._calloutElement.value!)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a new helper focusAsync, which does this. I wonder if focusFirstChild should just call focusAsync.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would make a lot of sense, especially since focus triggers reflows. |
||
| } | ||
| } | ||
|
|
||
| protected _onComponentDidMount = (): void => { | ||
|
|
||
| this._addListeners(); | ||
|
|
||
| if (this.props.onLayerMounted) { | ||
| this.props.onLayerMounted(); | ||
| } | ||
|
|
||
| this._updateAsyncPosition(); | ||
| this._setHeightOffsetEveryFrame(); | ||
| } | ||
|
|
||
| private _addListeners() { | ||
| // This is added so the callout will dismiss when the window is scrolled | ||
| // but not when something inside the callout is scrolled. The delay seems | ||
| // to be required to avoid React firing an async focus event in IE from | ||
|
|
@@ -261,14 +295,16 @@ export class CalloutContentBase extends BaseComponent<ICalloutProps, ICalloutSta | |
| this._events.on(this._targetWindow, 'resize', this.dismiss, true); | ||
| this._events.on(this._targetWindow.document.body, 'focus', this._dismissOnLostFocus, true); | ||
| this._events.on(this._targetWindow.document.body, 'click', this._dismissOnLostFocus, true); | ||
| this._hasListeners = true; | ||
| }, 0); | ||
| } | ||
|
|
||
| if (this.props.onLayerMounted) { | ||
| this.props.onLayerMounted(); | ||
| } | ||
|
|
||
| this._updateAsyncPosition(); | ||
| this._setHeightOffsetEveryFrame(); | ||
| private _removeListeners() { | ||
| this._events.off(this._targetWindow, 'scroll', this._dismissOnScroll, true); | ||
| this._events.off(this._targetWindow, 'resize', this.dismiss, true); | ||
| this._events.off(this._targetWindow.document.body, 'focus', this._dismissOnLostFocus, true); | ||
| this._events.off(this._targetWindow.document.body, 'click', this._dismissOnLostFocus, true); | ||
| this._hasListeners = false; | ||
| } | ||
|
|
||
| private _updateAsyncPosition() { | ||
|
|
@@ -299,7 +335,7 @@ export class CalloutContentBase extends BaseComponent<ICalloutProps, ICalloutSta | |
| currentProps = assign(currentProps, this.props); | ||
| currentProps!.bounds = this._getBounds(); | ||
| currentProps!.target = this._target!; | ||
| const newPositions: ICalloutPositionedInfo = positionCallout(currentProps!, hostElement, calloutElement); | ||
| const newPositions: ICalloutPositionedInfo = positionCallout(currentProps!, hostElement, calloutElement, positions); | ||
|
|
||
| // Set the new position only when the positions are not exists or one of the new callout positions are different. | ||
| // The position should not change if the position is within 2 decimal places. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better text:
If specified, renders the Callout in a hidden state. Use this flag, rather than rendering a callout conditionally based on visibility, to improve rendering performance when it becomes visible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also blank line between props