diff --git a/docs/AutoSizer.md b/docs/AutoSizer.md index ff7350aad..365378cd3 100644 --- a/docs/AutoSizer.md +++ b/docs/AutoSizer.md @@ -4,17 +4,18 @@ High-order component that automatically adjusts the width and height of a single ### Prop Types -| Property | Type | Required? | Description | -| :------------ | :------- | :-------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| children | Function | ✓ | Function responsible for rendering children. This function should implement the following signature: `({ height: number, width: number }) => PropTypes.element` | -| className | String | | Optional custom CSS class name to attach to root `AutoSizer` element. This is an advanced property and is not typically necessary. | -| defaultHeight | Number | | Height passed to child for initial render; useful for server-side rendering. This value will be overridden with an accurate height after mounting. | -| defaultWidth | Number | | Width passed to child for initial render; useful for server-side rendering. This value will be overridden with an accurate width after mounting. | -| disableHeight | Boolean | | Fixed `height`; if specified, the child's `height` property will not be managed | -| disableWidth | Boolean | | Fixed `width`; if specified, the child's `width` property will not be managed | -| nonce | String | | Nonce of the inlined stylesheets for [Content Security Policy](https://www.w3.org/TR/2016/REC-CSP2-20161215/#script-src-the-nonce-attribute) | -| onResize | Function | | Callback to be invoked on-resize; it is passed the following named parameters: `({ height: number, width: number })`. | -| style | Object | | Optional custom inline style to attach to root `AutoSizer` element. This is an advanced property and is not typically necessary. | +| Property | Type | Required? | Description | +| :------------ | :------- | :-------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| children | Function | ✓ | Function responsible for rendering children. This function should implement the following signature: `({ height: number, width: number }) => PropTypes.element` | +| className | String | | Optional custom CSS class name to attach to root `AutoSizer` element. This is an advanced property and is not typically necessary. | +| defaultHeight | Number | | Height passed to child for initial render; useful for server-side rendering. This value will be overridden with an accurate height after mounting. | +| defaultWidth | Number | | Width passed to child for initial render; useful for server-side rendering. This value will be overridden with an accurate width after mounting. | +| disableHeight | Boolean | | Fixed `height`; if specified, the child's `height` property will not be managed | +| disableWidth | Boolean | | Fixed `width`; if specified, the child's `width` property will not be managed | +| nonce | String | | Nonce of the inlined stylesheets for [Content Security Policy](https://www.w3.org/TR/2016/REC-CSP2-20161215/#script-src-the-nonce-attribute) | +| onResize | Function | | Callback to be invoked on-resize; it is passed the following named parameters: `({ height: number, width: number })`. | +| roundingMode | String | | How to handle fractional (sub-pixel) measurements: `"ceil"` (default, rounds up), `"floor"` (rounds down so children never overflow the parent), or `"round"` (nearest integer). | +| style | Object | | Optional custom inline style to attach to root `AutoSizer` element. This is an advanced property and is not typically necessary. | ### Examples diff --git a/source/AutoSizer/AutoSizer.jest.js b/source/AutoSizer/AutoSizer.jest.js index 356685d60..4a32ea91a 100644 --- a/source/AutoSizer/AutoSizer.jest.js +++ b/source/AutoSizer/AutoSizer.jest.js @@ -25,6 +25,7 @@ describe('AutoSizer', () => { paddingLeft = 0, paddingRight = 0, paddingTop = 0, + roundingMode = undefined, style = undefined, width = 200, } = {}) { @@ -49,6 +50,7 @@ describe('AutoSizer', () => { disableHeight={disableHeight} disableWidth={disableWidth} onResize={onResize} + roundingMode={roundingMode} style={style}> {({height, width}) => ( { ); } - // AutoSizer uses offsetWidth and offsetHeight. - // Jest runs in JSDom which doesn't support measurements APIs. + // AutoSizer reads getBoundingClientRect; detectElementResize (resize + // simulation below) reads offsetWidth/offsetHeight. Both need mocking in JSDom. function mockOffsetSize(width, height) { Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, @@ -74,6 +76,10 @@ describe('AutoSizer', () => { configurable: true, value: width, }); + HTMLElement.prototype.getBoundingClientRect = jest.fn(() => ({ + height, + width, + })); } it('should relay properties to ChildComponent or React child', () => { @@ -88,6 +94,26 @@ describe('AutoSizer', () => { expect(rendered.textContent).toContain('width:200'); }); + describe('roundingMode', () => { + it.each` + roundingMode | height | width | expectedHeight | expectedWidth | scenario + ${undefined} | ${100.4} | ${199.1} | ${101} | ${200} | ${'round fractional measurements up by default'} + ${'ceil'} | ${100.4} | ${199.1} | ${101} | ${200} | ${'round up when roundingMode is "ceil"'} + ${'floor'} | ${100.6} | ${199.6} | ${100} | ${199} | ${'round down when roundingMode is "floor"'} + ${'round'} | ${100.4} | ${199.6} | ${100} | ${200} | ${'round to the nearest integer when roundingMode is "round"'} + ${'invalid'} | ${100.4} | ${199.1} | ${101} | ${200} | ${'fall back to rounding up when roundingMode is not a recognized value'} + `( + 'should $scenario', + ({roundingMode, height, width, expectedHeight, expectedWidth}) => { + const rendered = findDOMNode( + render(getMarkup({height, width, roundingMode})), + ); + expect(rendered.textContent).toContain(`height:${expectedHeight}`); + expect(rendered.textContent).toContain(`width:${expectedWidth}`); + }, + ); + }); + it('should account for padding when calculating the available width and height', () => { const rendered = findDOMNode( render( diff --git a/source/AutoSizer/AutoSizer.js b/source/AutoSizer/AutoSizer.js index 7b0a142cc..3192b5266 100644 --- a/source/AutoSizer/AutoSizer.js +++ b/source/AutoSizer/AutoSizer.js @@ -8,6 +8,14 @@ type Size = { width: number, }; +type RoundingMode = 'ceil' | 'floor' | 'round'; + +const ROUNDING_FNS: {[RoundingMode]: (number) => number} = { + ceil: Math.ceil, + floor: Math.floor, + round: Math.round, +}; + type Props = { /** Function responsible for rendering children.*/ children: Size => React.Element<*>, @@ -33,6 +41,9 @@ type Props = { /** Callback to be invoked on-resize */ onResize: Size => void, + /** How to round fractional pixel measurements: 'ceil' (default), 'floor', or 'round' */ + roundingMode: RoundingMode, + /** Optional inline style */ style: ?Object, }; @@ -54,6 +65,7 @@ export default class AutoSizer extends React.Component { onResize: () => {}, disableHeight: false, disableWidth: false, + roundingMode: 'ceil', style: {}, }; @@ -160,15 +172,19 @@ export default class AutoSizer extends React.Component { } _onResize = () => { - const {disableHeight, disableWidth, onResize} = this.props; + const {disableHeight, disableWidth, onResize, roundingMode} = this.props; if (this._parentNode) { // Guard against AutoSizer component being removed from the DOM immediately after being added. // This can result in invalid style values which can result in NaN values if we don't handle them. // See issue #150 for more context. - const height = this._parentNode.offsetHeight || 0; - const width = this._parentNode.offsetWidth || 0; + // getBoundingClientRect returns fractional pixel measurements; + // roundingMode controls how they're rounded to whole pixels. + const round = ROUNDING_FNS[roundingMode] || Math.ceil; + const boundingRect = this._parentNode.getBoundingClientRect(); + const height = round(boundingRect.height) || 0; + const width = round(boundingRect.width) || 0; const win = this._window || window; const style = win.getComputedStyle(this._parentNode) || {};