Skip to content

Commit

Permalink
feat(onupdate): onUpdate cb that called every time visibleItems changed
Browse files Browse the repository at this point in the history
  • Loading branch information
asmyshlyaev177 committed Sep 24, 2021
1 parent 1100983 commit f4f5dd5
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 37 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ RightArrow | React component for right arrow
onWheel | (VisibilityContext, event) => void
onScroll | (VisibilityContext, event) => void
onInit | (VisibilityContext) => void
onUpdate | (VisibilityContext) => void
onMouseDown |(VisibilityContext) => (React.MouseEventHandler) => void
onMouseUp | (VisibilityContext) => (React.MouseEventHandler) => void
onMouseMove | (VisibilityContext) => (React.MouseEventHandler) => void
Expand Down
33 changes: 15 additions & 18 deletions example-nextjs/helpers/useDrag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,21 @@ export default function useDrag() {
[]
);

const dragMove = React.useCallback(
(ev: React.MouseEvent, cb: (newPos: number) => void) => {
const newDiff = position - ev.clientX;

const movedEnough = Math.abs(newDiff) > 5;

if (clicked && movedEnough) {
setDragging(true);
}

if (dragging && movedEnough) {
setPosition(ev.clientX);
setDiff(newDiff);
cb(newDiff);
}
},
[clicked, dragging, position]
);
const dragMove = (ev: React.MouseEvent, cb: (newPos: number) => void) => {
const newDiff = position - ev.clientX;

const movedEnough = Math.abs(newDiff) > 5;

if (clicked && movedEnough) {
setDragging(true);
}

if (dragging && movedEnough) {
setPosition(ev.clientX);
setDiff(newDiff);
cb(newDiff);
}
};

return {
dragStart,
Expand Down
26 changes: 13 additions & 13 deletions example-nextjs/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,22 @@ const onWheel = (
};

function App() {
const [items] = React.useState(getItems);
const [items, setItems] = React.useState(getItems);
const [selected, setSelected] = React.useState<string[]>([]);
const [position, setPosition] = React.useState(0);

// React.useEffect(() => {
// if (items.length < 25) {
// setTimeout(() => {
// const newItems = items.concat(
// Array(5)
// .fill(0)
// .map((_, ind) => ({ id: getId(items.length + ind) }))
// );
// console.log('push new items');
// setItems(newItems);
// }, 3000);
// NOTE: for add more items when last item reached
// const onUpdate = ({ isLastItemVisible }: scrollVisibilityApiType) => {
// if (isLastItemVisible) {
// const newItems = items.concat(
// Array(5)
// .fill(0)
// .map((_, ind) => ({ id: getId(items.length + ind) }))
// );
// console.log('push new items');
// setItems(newItems);
// }
// }, [items]);
// };

const isItemSelected = (id: string): boolean =>
!!selected.find((el) => el === id);
Expand Down Expand Up @@ -137,6 +136,7 @@ function App() {
LeftArrow={LeftArrow}
RightArrow={RightArrow}
onInit={restorePosition}
// onUpdate={onUpdate}
onScroll={savePosition}
onWheel={onWheel}
onMouseDown={() => (ev) => dragStart(ev)}
Expand Down
3 changes: 1 addition & 2 deletions src/hooks/useIntersectionObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ function useIntersection({ items, itemsChanged, refs, options }: Props) {

const ioCb = React.useCallback(
(entries: IntersectionObserverEntry[]) => {
const newItems = observerEntriesToItems(entries, options);
items.set(newItems);
items.set(observerEntriesToItems(entries, options));

global.clearTimeout(throttleTimer.current);
throttleTimer.current = +setTimeout(
Expand Down
48 changes: 48 additions & 0 deletions src/hooks/useOnUpdate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { renderHook } from '@testing-library/react-hooks';
import useOnUpdate from './useOnUpdate';

describe('useOnUpdate', () => {
test('should fire cb when visibleItems changed', () => {
const cb = jest.fn();
const visibleItems = ['it1', 'it2'];
const visibleItems2 = ['it2', 'it3'];
const condition = true;

const { rerender } = renderHook(useOnUpdate, {
initialProps: { cb, condition, visibleItems },
});

expect(cb).toHaveBeenCalledTimes(1);

rerender();
expect(cb).toHaveBeenCalledTimes(1);

rerender({ cb, condition, visibleItems: visibleItems2 });
expect(cb).toHaveBeenCalledTimes(2);

rerender();
expect(cb).toHaveBeenCalledTimes(2);
});

test('should fire cb only when condition is truthy', () => {
const cb = jest.fn();
const visibleItems = ['it1', 'it2'];
const visibleItems2 = ['it2', 'it3'];
const condition = false;

const { rerender } = renderHook(useOnUpdate, {
initialProps: { cb, condition, visibleItems },
});

expect(cb).toHaveBeenCalledTimes(0);

rerender();
expect(cb).toHaveBeenCalledTimes(0);

rerender({ cb, condition, visibleItems: visibleItems2 });
expect(cb).toHaveBeenCalledTimes(0);

rerender();
expect(cb).toHaveBeenCalledTimes(0);
});
});
29 changes: 29 additions & 0 deletions src/hooks/useOnUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';

import usePrevious from './usePrevious';

import { visibleItems as visibleItemsType } from '../types';

interface Props {
cb: () => void;
condition: Boolean;
visibleItems: visibleItemsType;
}

function useOnUpdate({
cb = () => void 0,
condition,
visibleItems,
}: Props): void {
const currentItemsHash = condition ? JSON.stringify(visibleItems) : '';
const prevItemsHash = usePrevious(currentItemsHash);

React.useEffect(() => {
if (condition && prevItemsHash !== currentItemsHash) {
cb();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [condition, prevItemsHash, currentItemsHash]);
}

export default useOnUpdate;
28 changes: 28 additions & 0 deletions src/hooks/usePrevious.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { renderHook } from '@testing-library/react-hooks';
import usePrevious from './usePrevious';

describe('usePrevious', () => {
test('should return previous value', () => {
const values = ['test1', 'test2', 'test3'];

const { result, rerender } = renderHook(usePrevious, {
initialProps: values[0],
});
expect(result.current).toEqual(undefined);

rerender();
expect(result.current).toEqual(values[0]);

rerender(values[1]);
expect(result.current).toEqual(values[0]);

rerender(values[2]);
expect(result.current).toEqual(values[1]);

rerender(values[2]);
expect(result.current).toEqual(values[2]);

rerender(values[2]);
expect(result.current).toEqual(values[2]);
});
});
13 changes: 13 additions & 0 deletions src/hooks/usePrevious.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';

function usePrevious<T>(value: T) {
const ref = React.useRef<T>();

React.useEffect(() => {
ref.current = value;
}, [value]);

return ref.current;
}

export default usePrevious;
62 changes: 60 additions & 2 deletions src/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,10 @@ describe('ScrollMenu', () => {
visibleItems: [],
})
.mockReturnValueOnce({
visibleItems: defaultItems,
visibleItems: defaultItemsWithSeparators,
})
.mockReturnValueOnce({
visibleItems: defaultItems,
visibleItems: defaultItemsWithSeparators,
});
const onInit = jest.fn();

Expand All @@ -148,6 +148,64 @@ describe('ScrollMenu', () => {
});
});

describe('onUpdate', () => {
beforeEach(() => {
jest.resetAllMocks();
});

test('should fire with publicApi', () => {
(useIntersectionObserver as jest.Mock).mockReturnValue({
visibleItems: defaultItemsWithSeparators,
});
const onInit = jest.fn();
const onUpdate = jest.fn();

const { container } = setup({ onInit, onUpdate });

expect(container.firstChild).toBeTruthy();

expect(onInit).toHaveBeenCalledTimes(1);
expect(onUpdate).toHaveBeenCalledTimes(1);
const call = onUpdate.mock.calls[0][0];
comparePublicApi(call);
});

test('should not fire if init not complete(when visibleItems empty)', () => {
(useIntersectionObserver as jest.Mock)
.mockReturnValueOnce({
visibleItems: [],
})
.mockReturnValueOnce({
visibleItems: [],
})
.mockReturnValueOnce({
visibleItems: defaultItemsWithSeparators,
})
.mockReturnValueOnce({
visibleItems: defaultItemsWithSeparators,
});
const onInit = jest.fn();
const onUpdate = jest.fn();

const { container, rerender } = setup({ onInit, onUpdate });

expect(onInit).not.toHaveBeenCalled();
expect(onUpdate).not.toHaveBeenCalled();
const textContent1 = container.firstChild!.textContent;
expect(textContent1!.includes('"initComplete":false')).toBeTruthy();

setup({ onInit, onUpdate, rerender });

expect(onInit).toHaveBeenCalledTimes(1);
expect(onUpdate).toHaveBeenCalledTimes(1);
const call = onUpdate.mock.calls[0][0];
comparePublicApi(call);
expect(call.initComplete).toEqual(true);
const textContent2 = container.firstChild!.textContent;
expect(textContent2!.includes('"initComplete":true')).toBeTruthy();
});
});

test('Structure - LeftArrow, ScrollContainer, MenuItems, RightArrow', () => {
(useIntersectionObserver as jest.Mock).mockReturnValue({
visibleItems: defaultItemsWithSeparators,
Expand Down
12 changes: 10 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { observerOptions as defaultObserverOptions } from './settings';
import * as constants from './constants';

import useOnInitCb from './hooks/useOnInitCb';
import useOnUpdate from './hooks/useOnUpdate';

import { VisibilityContext } from './context';

Expand All @@ -23,12 +24,12 @@ import slidingWindow from './slidingWindow';
import getItemsPos from './getItemsPos';

type ArrowType = React.FC | React.ReactNode;

export interface Props {
LeftArrow?: ArrowType;
RightArrow?: ArrowType;
children: ItemType | ItemType[];
onInit?: (api: publicApiType) => void;
onUpdate?: (api: publicApiType) => void;
onScroll?: (api: publicApiType, ev: React.UIEvent) => void;
onWheel?: (api: publicApiType, ev: React.WheelEvent) => void;
options?: Partial<typeof defaultObserverOptions>;
Expand All @@ -46,6 +47,7 @@ function ScrollMenu({
RightArrow: _RightArrow,
children,
onInit = (): void => void 0,
onUpdate = (): void => void 0,
onMouseDown,
onMouseUp,
onMouseMove,
Expand Down Expand Up @@ -90,11 +92,17 @@ function ScrollMenu({

const mounted = !!visibleItems.length;

useOnInitCb({
const onInitCbFired = useOnInitCb({
cb: () => onInit(publicApi.current),
condition: mounted,
});

useOnUpdate({
cb: () => onUpdate(publicApi.current),
condition: onInitCbFired,
visibleItems,
});

const api = React.useMemo(
() => createApi(items, visibleItems),
[items, visibleItems]
Expand Down

0 comments on commit f4f5dd5

Please sign in to comment.