diff --git a/components/__tests__/util/domHook.js b/components/__tests__/util/domHook.js index 1c920fb223..dddc3ffc27 100644 --- a/components/__tests__/util/domHook.js +++ b/components/__tests__/util/domHook.js @@ -1,10 +1,12 @@ +const __NULL__ = { notExist: true }; + export function spyElementPrototypes(Element, properties) { const propNames = Object.keys(properties); const originDescriptors = {}; propNames.forEach(propName => { const originDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, propName); - originDescriptors[propName] = originDescriptor; + originDescriptors[propName] = originDescriptor || __NULL__; const spyProp = properties[propName]; @@ -37,7 +39,9 @@ export function spyElementPrototypes(Element, properties) { mockRestore() { propNames.forEach(propName => { const originDescriptor = originDescriptors[propName]; - if (typeof originDescriptor === 'function') { + if (originDescriptor === __NULL__) { + delete Element.prototype[propName]; + } else if (typeof originDescriptor === 'function') { Element.prototype[propName] = originDescriptor; } else { Object.defineProperty(Element.prototype, propName, originDescriptor); diff --git a/components/_util/__tests__/util.test.js b/components/_util/__tests__/util.test.js index 6f35f47be1..2a86219337 100644 --- a/components/_util/__tests__/util.test.js +++ b/components/_util/__tests__/util.test.js @@ -9,6 +9,8 @@ import triggerEvent from '../triggerEvent'; import Wave from '../wave'; import TransButton from '../transButton'; import openAnimation from '../openAnimation'; +import ResizeObserver from '../resizeObserver'; +import { spyElementPrototype } from '../../__tests__/util/domHook'; describe('Test utils function', () => { beforeAll(() => { @@ -143,7 +145,9 @@ describe('Test utils function', () => { it('bindAnimationEvent should return when node is null', () => { const wrapper = mount( - + , ).instance(); expect(wrapper.bindAnimationEvent()).toBe(undefined); @@ -152,7 +156,9 @@ describe('Test utils function', () => { it('bindAnimationEvent.onClick should return when children is hidden', () => { const wrapper = mount( - + , ).instance(); expect(wrapper.bindAnimationEvent()).toBe(undefined); @@ -220,4 +226,47 @@ describe('Test utils function', () => { expect(done).toHaveBeenCalled(); }); }); + + describe('ResizeObserver', () => { + let domMock; + + beforeAll(() => { + domMock = spyElementPrototype(HTMLDivElement, 'getBoundingClientRect', () => { + return { + width: 1128 + Math.random(), + height: 903 + Math.random(), + }; + }); + }); + + afterAll(() => { + domMock.mockRestore(); + }); + + it('should not trigger `onResize` if size shaking', () => { + const onResize = jest.fn(); + let divNode; + + const wrapper = mount( + +
{ + divNode = node; + }} + /> + , + ); + + // First trigger + wrapper.instance().onResize([{ target: divNode }]); + onResize.mockReset(); + + // Repeat trigger should not trigger outer `onResize` with shaking + for (let i = 0; i < 10; i += 1) { + wrapper.instance().onResize([{ target: divNode }]); + } + + expect(onResize).not.toHaveBeenCalled(); + }); + }); }); diff --git a/components/_util/resizeObserver.tsx b/components/_util/resizeObserver.tsx index 3fd815c7cd..8efec93e18 100644 --- a/components/_util/resizeObserver.tsx +++ b/components/_util/resizeObserver.tsx @@ -10,9 +10,19 @@ interface ResizeObserverProps { onResize?: () => void; } -class ReactResizeObserver extends React.Component { +interface ResizeObserverState { + height: number; + width: number; +} + +class ReactResizeObserver extends React.Component { resizeObserver: ResizeObserver | null = null; + state = { + width: 0, + height: 0, + }; + componentDidMount() { this.onComponentUpdated(); } @@ -38,10 +48,30 @@ class ReactResizeObserver extends React.Component { } } - onResize = () => { + onResize: ResizeObserverCallback = (entries: ResizeObserverEntry[]) => { const { onResize } = this.props; - if (onResize) { - onResize(); + + const { target } = entries[0]; + + const { width, height } = target.getBoundingClientRect(); + + /** + * Resize observer trigger when content size changed. + * In most case we just care about element size, + * let's use `boundary` instead of `contentRect` here to avoid shaking. + */ + const fixedWidth = Math.floor(width); + const fixedHeight = Math.floor(height); + + if (this.state.width !== fixedWidth || this.state.height !== fixedHeight) { + this.setState({ + width: fixedWidth, + height: fixedHeight, + }); + + if (onResize) { + onResize(); + } } }; diff --git a/components/affix/__tests__/Affix.test.js b/components/affix/__tests__/Affix.test.js index f5051dcfb3..f9531e81b8 100644 --- a/components/affix/__tests__/Affix.test.js +++ b/components/affix/__tests__/Affix.test.js @@ -3,6 +3,7 @@ import { mount } from 'enzyme'; import Affix from '..'; import { getObserverEntities } from '../utils'; import Button from '../../button'; +import { spyElementPrototype } from '../../__tests__/util/domHook'; const events = {}; @@ -40,6 +41,7 @@ class AffixMounter extends React.Component { describe('Affix Render', () => { let wrapper; + let domMock; const classRect = { container: { @@ -48,23 +50,21 @@ describe('Affix Render', () => { }, }; - const originGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect; - HTMLElement.prototype.getBoundingClientRect = function getBoundingClientRect() { - return ( - classRect[this.className] || { - top: 0, - bottom: 0, - } - ); - }; - beforeAll(() => { jest.useFakeTimers(); + domMock = spyElementPrototype(HTMLElement, 'getBoundingClientRect', function mockBounding() { + return ( + classRect[this.className] || { + top: 0, + bottom: 0, + } + ); + }); }); afterAll(() => { jest.useRealTimers(); - HTMLElement.prototype.getBoundingClientRect = originGetBoundingClientRect; + domMock.mockRestore(); }); const movePlaceholder = top => { classRect.fixed = { @@ -185,7 +185,7 @@ describe('Affix Render', () => { .find('ReactResizeObserver') .at(index) .instance() - .onResize(); + .onResize([{ target: { getBoundingClientRect: () => ({ width: 99, height: 99 }) } }]); jest.runAllTimers(); expect(updateCalled).toHaveBeenCalled();