diff --git a/packages/dx-react-chart/src/utils/updatable-sizer.tsx b/packages/dx-react-chart/src/utils/updatable-sizer.tsx index 417262b8c5..c0fd090224 100644 --- a/packages/dx-react-chart/src/utils/updatable-sizer.tsx +++ b/packages/dx-react-chart/src/utils/updatable-sizer.tsx @@ -7,7 +7,7 @@ export class UpdatableSizer extends React.PureComponent { ref = React.createRef(); componentDidUpdate() { - this.ref.current!.setupListeners(); + this.props.onSizeChange(this.ref.current!.getSize()); } render() { diff --git a/packages/dx-react-core/src/sizer.test.tsx b/packages/dx-react-core/src/sizer.test.tsx index 75d916a19f..1e23d423bc 100644 --- a/packages/dx-react-core/src/sizer.test.tsx +++ b/packages/dx-react-core/src/sizer.test.tsx @@ -6,10 +6,10 @@ describe('Sizer', () => { const divProto = (document.createElement('div') as HTMLDivElement).constructor.prototype; let addEventListener: any; let removeEventListener: any; - const Container = ({ forwardedRef }) => ( -
+ const Container = ({ forwardedRef, ...restProps }) => ( +
); - + const onSizeChange = jest.fn(); beforeAll(() => { addEventListener = divProto.addEventListener; removeEventListener = divProto.removeEventListener; @@ -25,12 +25,36 @@ describe('Sizer', () => { afterEach(() => { divProto.addEventListener.mockClear(); divProto.removeEventListener.mockClear(); + onSizeChange.mockClear(); + }); + + it('should create component with childrens', () => { + const tree = mount( + , + ); + + const root = tree.find('.container'); + expect(root.props()).toEqual({ + className: 'container', + style: { + key: 'test style', + position: 'relative', + }, + }); + expect(root.getDOMNode().childNodes.length).toBe(1); + expect(root.getDOMNode().childNodes[0].childNodes.length).toBe(2); + expect(root.getDOMNode().childNodes[0].childNodes[0].childNodes.length).toBe(1); + expect(root.getDOMNode().childNodes[0].childNodes[1].childNodes.length).toBe(1); }); it('should add listeners on mount', () => { const tree = mount( void 0} + onSizeChange={onSizeChange} containerComponent={Container} />, ); @@ -52,7 +76,7 @@ describe('Sizer', () => { it('should remove listeners on unmount', () => { const tree = mount( void 0} + onSizeChange={onSizeChange} containerComponent={Container} />, ); @@ -73,7 +97,7 @@ describe('Sizer', () => { it('should set a 2px scroll offset to notifiers', () => { const tree = mount( void 0} + onSizeChange={onSizeChange} containerComponent={Container} />, ); @@ -88,4 +112,73 @@ describe('Sizer', () => { expect(expandNotifier.style.width).toBe('2px'); expect(expandNotifier.style.height).toBe('2px'); }); + + it('should call onSizeChange on mount', () => { + const tree = mount( + , + ); + const instance = tree.instance() as any; + instance.getSize = jest.fn().mockReturnValue({ width: 20, height: 10 }); + instance.componentDidMount(); + + expect(onSizeChange).toBeCalledWith({ width: 20, height: 10 }); + }); + + it('should update scroll offsets to rootNode', () => { + const tree = mount( + , + ); + const instance = tree.instance() as any; + instance.componentDidUpdate(); + + expect(instance.rootRef.current.scrollTop).toBe(35); + expect(instance.rootRef.current.scrollLeft).toBe(45); + }); + + // T1096930 + it('should update scroll offsets to notifiers', () => { + const resetOffsets = () => { + // after column reordering scroll offsets are resets + (root.firstChild!.childNodes[0] as any).scrollTop = 0; + (root.firstChild!.childNodes[0] as any).scrollLeft = 0; + (root.firstChild!.childNodes[1] as any).scrollTop = 0; + (root.firstChild!.childNodes[1] as any).scrollLeft = 0; + }; + const tree = mount( + , + ); + const instance = tree.instance() as any; + + const root = tree.find('.container').getDOMNode(); + resetOffsets(); + instance.getSize = jest.fn().mockReturnValue({ width: 20, height: 10 }); + instance.componentDidUpdate(); + + expect((root.firstChild!.childNodes[0] as any).scrollTop).toBe(2); + expect((root.firstChild!.childNodes[0] as any).scrollLeft).toBe(2); + + expect((root.firstChild!.childNodes[1] as any).scrollTop).toBe(10); + expect((root.firstChild!.childNodes[1] as any).scrollLeft).toBe(20); + + resetOffsets(); + instance.getSize = jest.fn().mockReturnValue({ width: 30, height: 20 }); + instance.componentDidUpdate(); + + expect((root.firstChild!.childNodes[0] as any).scrollTop).toBe(2); + expect((root.firstChild!.childNodes[0] as any).scrollLeft).toBe(2); + + expect((root.firstChild!.childNodes[1] as any).scrollTop).toBe(20); + expect((root.firstChild!.childNodes[1] as any).scrollLeft).toBe(30); + }); }); diff --git a/packages/dx-react-core/src/sizer.tsx b/packages/dx-react-core/src/sizer.tsx index 682e939432..38cde83d65 100644 --- a/packages/dx-react-core/src/sizer.tsx +++ b/packages/dx-react-core/src/sizer.tsx @@ -2,8 +2,10 @@ import * as React from 'react'; import { SizerProps, Size } from './types'; +import { shallowEqual } from '@devexpress/dx-core'; -const styles = { +const SCROLL_OFFSET = 2; +const styles: Record = { root: { position: 'relative', }, @@ -48,14 +50,13 @@ const styles = { }; /** @internal */ -export class Sizer extends React.PureComponent { +export class Sizer extends React.Component { static defaultProps = { containerComponent: 'div', }; - rootRef: React.RefObject; - // Though there properties cannot be assigned in constructor - // they will be assigned when component is mount. + rootRef: React.RefObject; + rootNode!: HTMLElement; triggersRoot!: HTMLDivElement; expandTrigger!: HTMLDivElement; @@ -67,25 +68,43 @@ export class Sizer extends React.PureComponent { super(props); this.setupListeners = this.setupListeners.bind(this); + this.updateScrolling = this.updateScrolling.bind(this); this.rootRef = React.createRef(); } componentDidMount() { + this.rootNode = this.rootRef.current!; this.createListeners(); + + this.expandTrigger.addEventListener('scroll', this.setupListeners); + this.contractTrigger.addEventListener('scroll', this.setupListeners); this.setupListeners(); } + shouldComponentUpdate(prevProps) { + if (prevProps.scrollTop !== this.props.scrollTop || + prevProps.scrollLeft !== this.props.scrollLeft || + (prevProps.style && this.props.style && + !shallowEqual(prevProps.style, this.props.style)) || + (prevProps.style && !this.props.style) || + prevProps.children !== this.props.children) { + return true; + } + return false; + } + componentDidUpdate() { // We can scroll the VirtualTable manually only by changing // containter's (rootNode) scrollTop property. // Viewport changes its own properties automatically. const { scrollTop, scrollLeft } = this.props; - if (scrollTop! > -1) { - this.rootNode.scrollTop = scrollTop!; + if (scrollTop !== undefined && scrollTop > -1) { + this.rootNode.scrollTop = scrollTop; } - if (scrollLeft! > -1) { - this.rootNode.scrollLeft = scrollLeft!; + if (scrollLeft !== undefined && scrollLeft > -1) { + this.rootNode.scrollLeft = scrollLeft; } + this.updateScrolling(this.getSize()); } // There is no need to remove listeners as divs are removed from DOM when component is unmount. @@ -96,31 +115,28 @@ export class Sizer extends React.PureComponent { this.contractTrigger.removeEventListener('scroll', this.setupListeners); } - getSize = (): Size => ({ height: this.rootNode.clientHeight, width: this.rootNode.clientWidth }); + getSize = (): Size => ({ + height: this.rootNode.clientHeight, + width: this.rootNode.clientWidth, + }) setupListeners() { const size = this.getSize(); const { width, height } = size; - this.contractTrigger.scrollTop = height; - this.contractTrigger.scrollLeft = width; + this.expandNotifier.style.width = `${width + SCROLL_OFFSET}px`; + this.expandNotifier.style.height = `${height + SCROLL_OFFSET}px`; - const scrollOffset = 2; - this.expandNotifier.style.width = `${width + scrollOffset}px`; - this.expandNotifier.style.height = `${height + scrollOffset}px`; - this.expandTrigger.scrollTop = scrollOffset; - this.expandTrigger.scrollLeft = scrollOffset; + this.updateScrolling(size); const { onSizeChange } = this.props; onSizeChange(size); } createListeners() { - this.rootNode = this.rootRef.current as HTMLElement; - this.triggersRoot = document.createElement('div'); Object.assign(this.triggersRoot.style, styles.triggersRoot); - this.rootNode.appendChild(this.triggersRoot); + this.rootNode!.appendChild(this.triggersRoot); this.expandTrigger = document.createElement('div'); Object.assign(this.expandTrigger.style, styles.expandTrigger); @@ -140,6 +156,14 @@ export class Sizer extends React.PureComponent { this.contractTrigger.appendChild(this.contractNotifier); } + updateScrolling(size) { + const { width, height } = size; + this.contractTrigger.scrollTop = height; + this.contractTrigger.scrollLeft = width; + this.expandTrigger.scrollTop = SCROLL_OFFSET; + this.expandTrigger.scrollLeft = SCROLL_OFFSET; + } + render() { const { onSizeChange,