office-ui-fabric-react
Version:
Reusable React components for building experiences for Office 365.
306 lines • 16.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var React = require("react");
var PropTypes = require("prop-types");
var Utilities_1 = require("../../Utilities");
var RESIZE_DELAY = 16;
/**
* Returns a simple object is able to store measurements with a given key.
*/
exports.getMeasurementCache = function () {
var measurementsCache = {};
return {
/**
* Checks if the provided data has a cacheKey. If it has a cacheKey and there is a
* corresponding entry in the measurementsCache, then it will return that value.
* Returns undefined otherwise.
*/
getCachedMeasurement: function (data) {
if (data && data.cacheKey && measurementsCache.hasOwnProperty(data.cacheKey)) {
return measurementsCache[data.cacheKey];
}
return undefined;
},
/**
* Should be called whenever there is a new measurement associated with a given data object.
* If the data has a cacheKey, store that measurement in the measurementsCache.
*/
addMeasurementToCache: function (data, measurement) {
if (data.cacheKey) {
measurementsCache[data.cacheKey] = measurement;
}
}
};
};
/**
* Returns a function that is able to compute the next state for the ResizeGroup given the current
* state and any measurement updates.
*/
exports.getNextResizeGroupStateProvider = function (measurementCache) {
if (measurementCache === void 0) { measurementCache = exports.getMeasurementCache(); }
var _measurementCache = measurementCache;
var _containerWidth;
/**
* Gets the width of the data rendered in a hidden div.
* @param measuredData - The data corresponding to the measurement we wish to take.
* @param getElementToMeasureWidth - A function that returns the measurement of the rendered data. Only called when the measurement
* is not in the cache.
*/
function _getMeasuredWidth(measuredData, getElementToMeasureWidth) {
var cachedWidth = _measurementCache.getCachedMeasurement(measuredData);
if (cachedWidth !== undefined) {
return cachedWidth;
}
var measuredWidth = getElementToMeasureWidth();
_measurementCache.addMeasurementToCache(measuredData, measuredWidth);
return measuredWidth;
}
/**
* Will get the next IResizeGroupState based on the current data while trying to shrink contents
* to fit in the container.
* @param data - The initial data point to start measuring.
* @param onReduceData - Function that transforms the data into something that should render with less width.
* @param getElementToMeasureWidth - A function that returns the measurement of the rendered data. Only called when the measurement
* is not in the cache.
*/
function _shrinkContentsUntilTheyFit(data, onReduceData, getElementToMeasureWidth) {
var dataToMeasure = data;
var measuredWidth = _getMeasuredWidth(data, getElementToMeasureWidth);
while (measuredWidth > _containerWidth) {
var nextMeasuredData = onReduceData(dataToMeasure);
// We don't want to get stuck in an infinite render loop when there are no more
// scaling steps, so implementations of onReduceData should return undefined when
// there are no more scaling states to apply.
if (nextMeasuredData === undefined) {
return {
renderedData: dataToMeasure,
resizeDirection: undefined,
dataToMeasure: undefined
};
}
measuredWidth = _measurementCache.getCachedMeasurement(nextMeasuredData);
// If the measurement isn't in the cache, we need to rerender with some data in a hidden div
if (measuredWidth === undefined) {
return {
dataToMeasure: nextMeasuredData,
resizeDirection: 'shrink'
};
}
dataToMeasure = nextMeasuredData;
}
return {
renderedData: dataToMeasure,
resizeDirection: undefined,
dataToMeasure: undefined
};
}
/**
* This function should be called when the state changes in a manner that might allow for more content to fit
* on the screen, such as the window width growing.
* @param data - The initial data point to start measuring.
* @param onGrowData - Function that transforms the data into something that may take up more space when rendering.
* @param getElementToMeasureWidth - A function that returns the measurement of the rendered data. Only called when the measurement
* is not in the cache.
*/
function _growDataUntilItDoesNotFit(data, onGrowData, getElementToMeasureWidth, onReduceData) {
var dataToMeasure = data;
var measuredWidth = _getMeasuredWidth(data, getElementToMeasureWidth);
while (measuredWidth < _containerWidth) {
var nextMeasuredData = onGrowData(dataToMeasure);
// We don't want to get stuck in an infinite render loop when there are no more
// scaling steps, so implementations of onGrowData should return undefined when
// there are no more scaling states to apply.
if (nextMeasuredData === undefined) {
return {
renderedData: dataToMeasure,
resizeDirection: undefined,
dataToMeasure: undefined
};
}
measuredWidth = _measurementCache.getCachedMeasurement(nextMeasuredData);
// If the measurement isn't in the cache, we need to rerender with some data in a hidden div
if (measuredWidth === undefined) {
return {
dataToMeasure: nextMeasuredData
};
}
dataToMeasure = nextMeasuredData;
}
// Once the loop is done, we should now shrink until the contents fit.
return tslib_1.__assign({ resizeDirection: 'shrink' }, _shrinkContentsUntilTheyFit(dataToMeasure, onReduceData, getElementToMeasureWidth));
}
/**
* Handles an update to the container width. Should only be called when we knew the previous container width.
* @param newWidth - The new width of the container.
* @param fullWidthData - The initial data passed in as a prop to resizeGroup.
* @param renderedData - The data that was rendered prior to the container size changing.
* @param onGrowData - Set to true if the Resize group has an onGrowData function.
*/
function _updateContainerWidth(newWidth, fullWidthData, renderedData, onGrowData) {
var nextState;
if (newWidth > _containerWidth) {
if (onGrowData) {
nextState = {
resizeDirection: 'grow',
dataToMeasure: onGrowData(renderedData)
};
}
else {
nextState = {
resizeDirection: 'shrink',
dataToMeasure: fullWidthData
};
}
}
else {
nextState = {
resizeDirection: 'shrink',
dataToMeasure: renderedData
};
}
_containerWidth = newWidth;
return tslib_1.__assign({}, nextState, { measureContainer: false });
}
function getNextState(props, currentState, getElementToMeasureWidth, newContainerWidth) {
// If there is no new container width or data to measure, there is no need for a new state update
if (newContainerWidth === undefined && currentState.dataToMeasure === undefined) {
return undefined;
}
if (newContainerWidth) {
// If we know what the last container size was and we rendered data at that width, we can do an optimized render
if (_containerWidth && currentState.renderedData && !currentState.dataToMeasure) {
return tslib_1.__assign({}, currentState, _updateContainerWidth(newContainerWidth, props.data, currentState.renderedData, props.onGrowData));
}
// If we are just setting the container width for the first time, we can't do any optimizations
_containerWidth = newContainerWidth;
}
var nextState = tslib_1.__assign({}, currentState, { measureContainer: false });
if (currentState.dataToMeasure) {
if (currentState.resizeDirection === 'grow' && props.onGrowData) {
nextState = tslib_1.__assign({}, nextState, _growDataUntilItDoesNotFit(currentState.dataToMeasure, props.onGrowData, getElementToMeasureWidth, props.onReduceData));
}
else {
nextState = tslib_1.__assign({}, nextState, _shrinkContentsUntilTheyFit(currentState.dataToMeasure, props.onReduceData, getElementToMeasureWidth));
}
}
return nextState;
}
/** Function that determines if we need to render content for measurement based on the measurement cache contents. */
function shouldRenderDataForMeasurement(dataToMeasure) {
if (!dataToMeasure || _measurementCache.getCachedMeasurement(dataToMeasure) !== undefined) {
return false;
}
return true;
}
function getInitialResizeGroupState(data) {
return {
dataToMeasure: tslib_1.__assign({}, data),
resizeDirection: 'grow',
measureContainer: true
};
}
return {
getNextState: getNextState,
shouldRenderDataForMeasurement: shouldRenderDataForMeasurement,
getInitialResizeGroupState: getInitialResizeGroupState
};
};
// Provides a context property that (if true) tells any child components that
// they are only being used for measurement purposes and will not be visible.
var MeasuredContext = Utilities_1.provideContext({
isMeasured: PropTypes.bool
}, function () {
return { isMeasured: true };
});
var getClassNames = Utilities_1.classNamesFunction();
// Styles for the hidden div used for measurement
var hiddenDivStyles = { position: 'fixed', visibility: 'hidden' };
var ResizeGroupBase = /** @class */ (function (_super) {
tslib_1.__extends(ResizeGroupBase, _super);
function ResizeGroupBase(props) {
var _this = _super.call(this, props) || this;
_this._nextResizeGroupStateProvider = exports.getNextResizeGroupStateProvider();
// The root div which is the container inside of which we are trying to fit content.
_this._root = Utilities_1.createRef();
// A div that can be used for the initial measurement so that we can avoid mounting a second instance
// of the component being measured for the initial render.
_this._initialHiddenDiv = Utilities_1.createRef();
// A hidden div that is used for mounting a new instance of the component for measurement in a hidden
// div without unmounting the currently visible content.
_this._updateHiddenDiv = Utilities_1.createRef();
// Tracks if any content has been rendered to the user. This enables us to do some performance optimizations
// for the initial render.
_this._hasRenderedContent = false;
_this.state = _this._nextResizeGroupStateProvider.getInitialResizeGroupState(_this.props.data);
return _this;
}
ResizeGroupBase.prototype.render = function () {
var _a = this.props, onRenderData = _a.onRenderData, className = _a.className, styles = _a.styles, theme = _a.theme;
var _b = this.state, dataToMeasure = _b.dataToMeasure, renderedData = _b.renderedData;
var divProps = Utilities_1.getNativeProps(this.props, Utilities_1.divProperties, ['data']);
var classNames = getClassNames(styles, { theme: theme, className: className });
var dataNeedsMeasuring = this._nextResizeGroupStateProvider.shouldRenderDataForMeasurement(dataToMeasure);
var isInitialMeasure = !this._hasRenderedContent && dataNeedsMeasuring;
// We only ever render the final content to the user. All measurements are done in a hidden div.
// For the initial render, we want this to be as fast as possible, so we need to make sure that we only mount one version of the
// component for measurement and the final render. For renders that update what is on screen, we want to make sure that
// there are no jarring effects such as the screen flashing as we apply scaling steps for meassurement. In the update case,
// we mount a second version of the component just for measurement purposes and leave the rendered content untouched until we know the
// next state sto show to the user.
return (React.createElement("div", tslib_1.__assign({}, divProps, { className: classNames.root, ref: this._root }),
dataNeedsMeasuring &&
!isInitialMeasure && (React.createElement("div", { style: hiddenDivStyles, ref: this._updateHiddenDiv },
React.createElement(MeasuredContext, null, onRenderData(dataToMeasure)))),
React.createElement("div", { ref: this._initialHiddenDiv, style: isInitialMeasure ? hiddenDivStyles : undefined, "data-automation-id": "visibleContent" }, isInitialMeasure ? onRenderData(dataToMeasure) : renderedData && onRenderData(renderedData))));
};
ResizeGroupBase.prototype.componentDidMount = function () {
this._afterComponentRendered();
this._events.on(window, 'resize', this._async.debounce(this._onResize, RESIZE_DELAY, { leading: true }));
};
ResizeGroupBase.prototype.componentWillReceiveProps = function (nextProps) {
this.setState({
dataToMeasure: tslib_1.__assign({}, nextProps.data),
resizeDirection: 'grow',
measureContainer: true // Receiving new props means the parent might rerender and the root width might change
});
};
ResizeGroupBase.prototype.componentDidUpdate = function (prevProps) {
if (this.state.renderedData) {
this._hasRenderedContent = true;
if (this.props.dataDidRender) {
this.props.dataDidRender(this.state.renderedData);
}
}
this._afterComponentRendered();
};
ResizeGroupBase.prototype.remeasure = function () {
if (this._root.current) {
this.setState({ measureContainer: true });
}
};
ResizeGroupBase.prototype._afterComponentRendered = function () {
var _this = this;
this._async.requestAnimationFrame(function () {
var containerWidth = undefined;
if (_this.state.measureContainer && _this._root.current) {
containerWidth = _this._root.current.getBoundingClientRect().width;
}
var nextState = _this._nextResizeGroupStateProvider.getNextState(_this.props, _this.state, function () {
var refToMeasure = !_this._hasRenderedContent ? _this._initialHiddenDiv : _this._updateHiddenDiv;
return refToMeasure.current ? refToMeasure.current.scrollWidth : 0;
}, containerWidth);
if (nextState) {
_this.setState(nextState);
}
});
};
ResizeGroupBase.prototype._onResize = function () {
if (this._root.current) {
this.setState({ measureContainer: true });
}
};
return ResizeGroupBase;
}(Utilities_1.BaseComponent));
exports.ResizeGroupBase = ResizeGroupBase;
//# sourceMappingURL=ResizeGroup.base.js.map