Skip to content

Commit

Permalink
perf(region): Refactor creator to modify rect directly during draw
Browse files Browse the repository at this point in the history
  • Loading branch information
jstoffan committed May 19, 2020
1 parent bbed3f4 commit 76614cd
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 217 deletions.
62 changes: 23 additions & 39 deletions src/components/AutoScroller/AutoScroller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,23 @@ import * as React from 'react';
import noop from 'lodash/noop';
import { getScrollParent } from './util';

export type Props = Partial<React.Attributes> & {
as?: React.ElementType;
children?: React.ReactNode;
className?: string;
export type Options = {
enabled?: boolean;
intensity?: number; // Intensity factor to ramp up scroll speed as the cursor moves deeper into the scroll gutter
onScroll?: (x: number, y: number) => void;
reference: Element | null;
size?: number; // Size of the invisible zone within the component where mouse events will trigger auto-scroll
};

export function AutoScroller(props: Props, ref: React.Ref<Element>): JSX.Element {
const { as: Element = 'div', children, enabled, intensity = 0.2, onScroll = noop, size = 50, ...rest } = props;
export default function useAutoScroll(options: Options): void {
const { enabled, intensity = 0.2, onScroll = noop, reference, size = 50 } = options;

// Create refs to store un-rendered data, references, and state
const handleRef = React.useRef<number | null>(null);
const parentRef = React.useRef<Element | null>(null);
const parentRectRef = React.useRef<DOMRect | null>(null);
const positionXRef = React.useRef<number | null>(null);
const positionYRef = React.useRef<number | null>(null);
const referenceRef = React.useRef<Element | null>(null);

// Create handlers to be called when autoscroll is enabled
const checkStep = (callback: () => void): void => {
Expand Down Expand Up @@ -78,27 +75,34 @@ export function AutoScroller(props: Props, ref: React.Ref<Element>): JSX.Element
return checkStep(checkScroll);
};

// Event Handlers
const handleMouseMove = ({ clientX, clientY }: MouseEvent): void => {
positionXRef.current = clientX;
positionYRef.current = clientY;
};
const handleTouchMove = ({ targetTouches }: TouchEvent): void => {
positionXRef.current = targetTouches[0].clientX;
positionYRef.current = targetTouches[0].clientY;
};

// Create reference to scroll parent on mount
React.useEffect(() => {
const { current: reference } = referenceRef;

if (reference) {
parentRef.current = getScrollParent(reference);
parentRectRef.current = parentRef.current.getBoundingClientRect(); // Cache to improve performance
}
}, [referenceRef]);
}, [reference]);

// Create document-level event handlers for mouse/touch move
React.useEffect(() => {
const handleMouseMove = ({ clientX, clientY }: MouseEvent): void => {
positionXRef.current = clientX;
positionYRef.current = clientY;
};
const handleTouchMove = ({ targetTouches }: TouchEvent): void => {
positionXRef.current = targetTouches[0].clientX;
positionYRef.current = targetTouches[0].clientY;
};
const cleanup = (): void => {
if (enabled) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('touchmove', handleTouchMove);

checkStep(checkScroll);
}

return (): void => {
const { current: handle } = handleRef;

// Cancel the scroll check loop
Expand All @@ -115,25 +119,5 @@ export function AutoScroller(props: Props, ref: React.Ref<Element>): JSX.Element
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('touchmove', handleTouchMove);
};

if (enabled) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('touchmove', handleTouchMove);

checkStep(checkScroll);
}

return cleanup;
}, [enabled]); // eslint-disable-line react-hooks/exhaustive-deps

// Synchronize the outer ref with the inner ref
React.useImperativeHandle(ref, () => referenceRef.current as Element, [referenceRef]);

return (
<Element ref={referenceRef} {...rest}>
{children}
</Element>
);
}

export default React.memo(React.forwardRef(AutoScroller));
35 changes: 13 additions & 22 deletions src/components/AutoScroller/__tests__/AutoScroller-test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import * as React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import AutoScroller from '../AutoScroller';
import useAutoScroll from '../AutoScroller';

describe('AutoScroller', () => {
const defaults = {
enabled: true,
id: 'auto-scroller',
const TestComponent = (props = {}): JSX.Element => {
const reference = document.querySelector('#child');

useAutoScroll({
enabled: true,
reference,
...props,
});

return <></>;
};
const DefaultChild = (): JSX.Element => <div id="child">Test</div>;
const getDOMRect = (x: number, y: number, height: number, width: number): DOMRect => ({
bottom: x + height,
top: y,
Expand All @@ -31,13 +37,7 @@ describe('AutoScroller', () => {

return parent;
};
const getWrapper = (props = {}): ReactWrapper =>
mount(
<AutoScroller {...defaults} {...props}>
<DefaultChild />
</AutoScroller>,
{ attachTo: getParent() },
);
const getWrapper = (props = {}): ReactWrapper => mount(<TestComponent {...props} />);

beforeEach(() => {
jest.useFakeTimers();
Expand All @@ -47,7 +47,7 @@ describe('AutoScroller', () => {
jest.spyOn(window, 'cancelAnimationFrame');
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => setTimeout(cb, 100)); // 10 fps

document.body.innerHTML = '<div id="parent" />';
document.body.innerHTML = '<div id="parent"><div id="child"/></div>';
});

describe('cleanup', () => {
Expand Down Expand Up @@ -132,13 +132,4 @@ describe('AutoScroller', () => {
expect(wrapper).toBeTruthy();
});
});

describe('render', () => {
test('should render any children that are provided', () => {
const wrapper = getWrapper();

expect(wrapper.exists(DefaultChild)).toBe(true);
expect(document.addEventListener).toHaveBeenCalledTimes(2);
});
});
});
2 changes: 1 addition & 1 deletion src/region/RegionAnnotation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const RegionAnnotation = (props: Props, ref: React.Ref<HTMLAnchorElement>): JSX.

return (
<AnnotationTarget ref={ref} className={className} isActive={isActive} {...rest}>
<RegionRect shape={shape} />
<RegionRect {...shape} />
</AnnotationTarget>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/region/RegionAnnotations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export default class RegionAnnotations extends React.PureComponent<Props, State>
{/* Layer 3a: Staged (unsaved) annotation target, if any */}
{isCreating && staged && (
<svg className="ba-RegionAnnotations-target">
<RegionRect ref={this.setRectRef} shape={scaleShape(staged.shape, scale)} />
<RegionRect ref={this.setRectRef} {...scaleShape(staged.shape, scale)} />
</svg>
)}

Expand Down
Loading

0 comments on commit 76614cd

Please sign in to comment.