diff --git a/common/changes/office-ui-fabric-react/pref-improv_2018-03-30-22-12.json b/common/changes/office-ui-fabric-react/pref-improv_2018-03-30-22-12.json new file mode 100644 index 00000000000000..248098b1549e0f --- /dev/null +++ b/common/changes/office-ui-fabric-react/pref-improv_2018-03-30-22-12.json @@ -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" +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/Callout/Callout.types.ts b/packages/office-ui-fabric-react/src/components/Callout/Callout.types.ts index 80cf8f12fca3b2..5aeb79567d3340 100644 --- a/packages/office-ui-fabric-react/src/components/Callout/Callout.types.ts +++ b/packages/office-ui-fabric-react/src/components/Callout/Callout.types.ts @@ -198,9 +198,18 @@ export interface ICalloutProps { theme?: ITheme; /** - * Optional styles for the component. - */ + * Optional styles for the component. + */ getStyles?: IStyleFunction; + + /** + * 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. + * Note: When callout is hidden its content will not be rendered. It will only render + * once the callout is visible. + */ + hidden?: boolean; } export interface ICalloutContentStyleProps { diff --git a/packages/office-ui-fabric-react/src/components/Callout/CalloutContent.base.tsx b/packages/office-ui-fabric-react/src/components/Callout/CalloutContent.base.tsx index b69501b1a3bbe9..446700070bd86c 100644 --- a/packages/office-ui-fabric-react/src/components/Callout/CalloutContent.base.tsx +++ b/packages/office-ui-fabric-react/src/components/Callout/CalloutContent.base.tsx @@ -78,6 +78,7 @@ export class CalloutContentBase extends BaseComponent
) } { beakVisible && (
) } - { children } + }
-
+ ); return content; @@ -247,11 +269,23 @@ export class CalloutContentBase extends BaseComponent { 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!)); } } 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
- { isCalloutVisible && ( - -
-

- All of your favorite people +

+

+ All of your favorite people

-
-
-
-

- Message body is optional. If help documentation is available, consider adding a link to learn more at the bottom. +

+
+
+

+ Message body is optional. If help documentation is available, consider adding a link to learn more at the bottom.

-
-
- Go to microsoft -
- - ) } +
+ Go to microsoft +
+
+
); } diff --git a/packages/office-ui-fabric-react/src/components/Dropdown/Dropdown.tsx b/packages/office-ui-fabric-react/src/components/Dropdown/Dropdown.tsx index 92c05c2aeec073..11152e58709eb7 100644 --- a/packages/office-ui-fabric-react/src/components/Dropdown/Dropdown.tsx +++ b/packages/office-ui-fabric-react/src/components/Dropdown/Dropdown.tsx @@ -549,7 +549,9 @@ export class Dropdown extends BaseComponent { if (this._focusZone.value) { - this._focusZone.value.focus(); + // Focusing an element can trigger a reflow. Making this wait until there is an animation + // frame can improve perf significantly. + this._async.requestAnimationFrame(() => this._focusZone.value!.focus()); } } diff --git a/packages/office-ui-fabric-react/src/components/Popup/Popup.tsx b/packages/office-ui-fabric-react/src/components/Popup/Popup.tsx index c50edaf3aca2e3..ec8964b5042d4e 100644 --- a/packages/office-ui-fabric-react/src/components/Popup/Popup.tsx +++ b/packages/office-ui-fabric-react/src/components/Popup/Popup.tsx @@ -10,10 +10,14 @@ import { } from '../../Utilities'; import { IPopupProps } from './Popup.types'; +export interface IPopupState { + needsVerticalScrollBar?: boolean; +} + /** * This adds accessibility to Dialog and Panel controls */ -export class Popup extends BaseComponent { +export class Popup extends BaseComponent { public static defaultProps: IPopupProps = { shouldRestoreFocus: true @@ -24,6 +28,11 @@ export class Popup extends BaseComponent { private _originalFocusedElement: HTMLElement; private _containsFocus: boolean; + public constructor(props: IPopupProps) { + super(props); + this.state = { needsVerticalScrollBar: false }; + } + public componentWillMount() { this._originalFocusedElement = getDocument()!.activeElement as HTMLElement; } @@ -39,6 +48,12 @@ export class Popup extends BaseComponent { if (doesElementContainFocus(this._root.value)) { this._containsFocus = true; } + + this._updateScrollBarAsync(); + } + + public componentDidUpdate() { + this._updateScrollBarAsync(); } public componentWillUnmount(): void { @@ -59,12 +74,6 @@ export class Popup extends BaseComponent { public render() { const { role, className, ariaLabel, ariaLabelledBy, ariaDescribedBy, style } = this.props; - let needsVerticalScrollBar = false; - if (this._root.value && this._root.value.firstElementChild) { - needsVerticalScrollBar = this._root.value.clientHeight > 0 - && this._root.value.firstElementChild.clientHeight > this._root.value.clientHeight; - } - return (
{ aria-labelledby={ ariaLabelledBy } aria-describedby={ ariaDescribedBy } onKeyDown={ this._onKeyDown } - style={ { overflowY: needsVerticalScrollBar ? 'scroll' : 'auto', ...style } } + style={ { overflowY: this.state.needsVerticalScrollBar ? 'scroll' : 'auto', ...style } } > { this.props.children }
@@ -97,6 +106,25 @@ export class Popup extends BaseComponent { } } + private _updateScrollBarAsync() { + this._async.requestAnimationFrame(() => { + this._getScrollBar(); + }); + } + + private _getScrollBar() { + let needsVerticalScrollBar = false; + if (this._root && this._root.value && this._root.value.firstElementChild) { + needsVerticalScrollBar = this._root.value.clientHeight > 0 + && this._root.value.firstElementChild.clientHeight > this._root.value.clientHeight; + } + if (this.state.needsVerticalScrollBar !== needsVerticalScrollBar) { + this.setState({ + needsVerticalScrollBar: needsVerticalScrollBar + }); + } + } + private _onFocus() { this._containsFocus = true; } diff --git a/packages/office-ui-fabric-react/src/utilities/positioning/positioning.ts b/packages/office-ui-fabric-react/src/utilities/positioning/positioning.ts index e2bd20e71a83de..b4f477965e9684 100644 --- a/packages/office-ui-fabric-react/src/utilities/positioning/positioning.ts +++ b/packages/office-ui-fabric-react/src/utilities/positioning/positioning.ts @@ -451,8 +451,16 @@ export namespace positioningFunctions { */ export function _getPositionData( directionalHint: DirectionalHint = DirectionalHint.bottomAutoEdge, - directionalHintForRTL?: DirectionalHint + directionalHintForRTL?: DirectionalHint, + previousPositions?: IPositionedData ): IPositionDirectionalHintData { + if (previousPositions) { + return { + alignmentEdge: previousPositions.alignmentEdge, + isAuto: previousPositions.isAuto, + targetEdge: previousPositions.targetEdge + }; + } const positionInformation: IPositionDirectionalHintData = { ...DirectionalDictionary[directionalHint] }; if (getRTL()) { @@ -634,14 +642,15 @@ export namespace positioningFunctions { export function _positionElementRelative( props: IPositionProps, hostElement: HTMLElement, - elementToPosition: HTMLElement): IElementPositionInfo { + elementToPosition: HTMLElement, + previousPositions?: IPositionedData): IElementPositionInfo { const gap: number = props.gapSpace ? props.gapSpace : 0; const boundingRect: Rectangle = props.bounds ? _getRectangleFromIRect(props.bounds) : new Rectangle(0, window.innerWidth - getScrollbarWidth(), 0, window.innerHeight); const targetRect: Rectangle = _getTargetRect(boundingRect, props.target); const positionData: IPositionDirectionalHintData = _getAlignmentData( - _getPositionData(props.directionalHint, props.directionalHintForRTL)!, + _getPositionData(props.directionalHint, props.directionalHintForRTL, previousPositions)!, targetRect, boundingRect, props.coverTarget); @@ -656,7 +665,7 @@ export namespace positioningFunctions { return { ...positionedElement, targetRectangle: targetRect }; } - export function _finalizePositionData(positionedElement: IElementPosition, hostElement: HTMLElement) { + export function _finalizePositionData(positionedElement: IElementPosition, hostElement: HTMLElement): IPositionedData { const finalizedElement: IPartialIRectangle = _finalizeElementPosition( positionedElement.elementRectangle, hostElement, @@ -664,25 +673,28 @@ export namespace positioningFunctions { positionedElement.alignmentEdge); return { elementPosition: finalizedElement, - targetEdge: positionedElement.targetEdge + targetEdge: positionedElement.targetEdge, + alignmentEdge: positionedElement.alignmentEdge }; } export function _positionElement( props: IPositionProps, hostElement: HTMLElement, - elementToPosition: HTMLElement): IPositionedData { - const positionedElement: IElementPosition = _positionElementRelative(props, hostElement, elementToPosition); + elementToPosition: HTMLElement, + previousPositions?: IPositionedData): IPositionedData { + const positionedElement: IElementPosition = _positionElementRelative(props, hostElement, elementToPosition, previousPositions); return _finalizePositionData(positionedElement, hostElement); } export function _positionCallout(props: ICalloutPositionProps, hostElement: HTMLElement, - callout: HTMLElement): ICalloutPositionedInfo { + callout: HTMLElement, + previousPositions?: ICalloutPositionedInfo): ICalloutPositionedInfo { const beakWidth: number = !props.isBeakVisible ? 0 : (props.beakWidth || 0); const gap: number = _calculateActualBeakWidthInPixels(beakWidth) / 2 + (props.gapSpace ? props.gapSpace : 0); const positionProps: IPositionProps = props; positionProps.gapSpace = gap; - const positionedElement: IElementPositionInfo = _positionElementRelative(positionProps, hostElement, callout); + const positionedElement: IElementPositionInfo = _positionElementRelative(positionProps, hostElement, callout, previousPositions); const beakPositioned: Rectangle = _positionBeak( beakWidth, positionedElement); @@ -737,23 +749,29 @@ export function getRelativePositions(props: IPositionProps, /** * Used to position an element relative to the given positioning props. + * If positioning has been completed before, previousPositioningData + * can be passed to ensure that the positioning element repositions based on + * its previous targets rather than starting with directionalhint. * * @export * @param {IPositionProps} props * @param {HTMLElement} hostElement * @param {HTMLElement} elementToPosition + * @param {IPositionedData} previousPositions * @returns */ export function positionElement(props: IPositionProps, hostElement: HTMLElement, - elementToPosition: HTMLElement): IPositionedData { - return positioningFunctions._positionElement(props, hostElement, elementToPosition); + elementToPosition: HTMLElement, + previousPositions?: IPositionedData): IPositionedData { + return positioningFunctions._positionElement(props, hostElement, elementToPosition, previousPositions); } export function positionCallout(props: IPositionProps, hostElement: HTMLElement, - elementToPosition: HTMLElement): ICalloutPositionedInfo { - return positioningFunctions._positionCallout(props, hostElement, elementToPosition); + elementToPosition: HTMLElement, + previousPositions?: ICalloutPositionedInfo): ICalloutPositionedInfo { + return positioningFunctions._positionCallout(props, hostElement, elementToPosition, previousPositions); } /** diff --git a/packages/office-ui-fabric-react/src/utilities/positioning/positioning.types.ts b/packages/office-ui-fabric-react/src/utilities/positioning/positioning.types.ts index 831c57988c3246..ee4249d64a33bb 100644 --- a/packages/office-ui-fabric-react/src/utilities/positioning/positioning.types.ts +++ b/packages/office-ui-fabric-react/src/utilities/positioning/positioning.types.ts @@ -73,9 +73,15 @@ export interface IPositionedData { elementPosition: IPosition; /** * The finalized target edge that element is aligning to. For instance RectangleEdge.bottom would mean - * that the bottom edge of the target is being aligned to. + * that the bottom edge of the target is being aligned to by the RectangleEdge.top of the element + * that is being positioned. */ targetEdge: RectangleEdge; + /** + * The finalized alignment edge that the element is aligning too. For instance, RectangleEdge.left means + * that the left edge of the target should be in line with the left edge of the element being positioned. + */ + alignmentEdge?: RectangleEdge; } export interface ICalloutPositionedInfo extends IPositionedData {