Skip to content

Commit

Permalink
feat: getItemElementById and getItemElementByIndex helpers
Browse files Browse the repository at this point in the history
getItemElementById can be used to get dom element directly and scroll to it, e.g. with apiRef object

#167
  • Loading branch information
asmyshlyaev177 committed Oct 16, 2021
1 parent f6448a9 commit 5bdc115
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 51 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,9 @@ wrapperClassname | ClassName of the outer-most div
Prop | Signature
-----|----------
getItemById | itemId => IOItem \| undefined
getItemElementById | itemId => DOM Element \| null
getItemByIndex | index => IOItem \| undefined
getItemElementByIndex | index => DOM Element \| null
getNextItem | () => IOItem \| undefined)
getPrevItem | () => IOItem \| undefined
initComplete | boolean
Expand Down Expand Up @@ -229,8 +231,10 @@ Check out [examples](#examples)

### apiRef
Can pass Ref object to Menu, current value will assigned as VisibilityContext. But `visibleItems` and some other values can be staled, so better use it only for firing functions like `scrollToItem`.

For scrolling use `apiRef.scrollToItem(apiRef.getItemElementById)` instead of `apiRef.scrollToItem(apiRef.getItemById)`.

Can get item outside of context directly via ```document.querySelector(`[data-key='${itemId}']`)```.
Can get item outside of context via `apiRef.getItemElementById(id)` or directly via ```document.querySelector(`[data-key='${itemId}']`)```.
See [`apiRef` example and `Add item and scroll to it`](#examples)

## Browser support
Expand Down
7 changes: 6 additions & 1 deletion src/components/Item/Item.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';

import type { Refs } from '../../types';
import { dataKeyAttribute, dataIndexAttribute } from '../../constants';

export type Props = {
id: string;
Expand All @@ -15,7 +16,11 @@ function Item({ children, className, id, index, refs }: Props) {
refs[String(index)] = ref;

return (
<div className={className} data-key={id} data-index={index} ref={ref}>
<div
className={className}
{...{ [dataKeyAttribute]: id, [dataIndexAttribute]: index }}
ref={ref}
>
{children}
</div>
);
Expand Down
8 changes: 7 additions & 1 deletion src/components/Separator/Separator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from 'react';

import type { Refs } from '../../types';

import { dataKeyAttribute, dataIndexAttribute } from '../../constants';

export type Props = {
id: string;
index: number;
Expand All @@ -14,7 +16,11 @@ function Separator({ className, id, index, refs }: Props) {
refs[index] = ref;

return (
<div className={className} data-key={id} data-index={index} ref={ref} />
<div
className={className}
{...{ [dataKeyAttribute]: id, [dataIndexAttribute]: index }}
ref={ref}
/>
);
}

Expand Down
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ export const scrollContainerClassName = `${rootClassName}--scroll-container`;
export const wrapperClassName = `${rootClassName}--wrapper`;

export const id = 'itemId';

export const dataKeyAttribute = 'data-key';
export const dataIndexAttribute = 'data-index';
76 changes: 30 additions & 46 deletions src/createApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import createApi from './createApi';
import ItemsMap from './ItemsMap';
import { observerEntriesToItems } from './helpers';
import { observerOptions } from './settings';
import { IOItem } from './types';

import {
getItemElementById,
getItemElementByIndex,
scrollToItem,
} from './helpers';

const setup = (ratio = [0.3, 1, 0.7]) => {
const items = new ItemsMap();
Expand Down Expand Up @@ -64,6 +69,30 @@ describe('createApi', () => {
expect(createApi(items, []).visibleItemsWithoutSeparators).toEqual([]);
});

describe('helpers', () => {
test('scrollToItem', () => {
const { items, visibleItems } = setup([0.7, 0, 0]);

expect(createApi(items, visibleItems).scrollToItem).toEqual(scrollToItem);
});

test('getItemElementById', () => {
const { items, visibleItems } = setup([0.7, 0, 0]);

expect(createApi(items, visibleItems).getItemElementById).toEqual(
getItemElementById
);
});

test('getItemElementByIndex', () => {
const { items, visibleItems } = setup([0.7, 0, 0]);

expect(createApi(items, visibleItems).getItemElementByIndex).toEqual(
getItemElementByIndex
);
});
});

describe('isFirstItemVisible', () => {
test('first item visible', () => {
const { items, visibleItems } = setup([0.7, 0, 0]);
Expand Down Expand Up @@ -198,51 +227,6 @@ describe('createApi', () => {
});
});

describe('scrollToItem', () => {
test('item exists', async () => {
const { items, visibleItems } = setup([1, 1, 0.3]);

const item = {
entry: { target: document.createElement('div') },
} as unknown as IOItem;
const scrollIntoView = jest.fn();
item.entry.target.scrollIntoView = scrollIntoView;

createApi(items, visibleItems).scrollToItem(item);

await new Promise((res) => setTimeout(res, 500));
expect(scrollIntoView).toHaveBeenCalledTimes(1);
expect(scrollIntoView).toHaveBeenNthCalledWith(1, {
behavior: 'smooth',
block: 'nearest',
inline: 'end',
});

createApi(items, visibleItems).scrollToItem(
item,
'auto',
'start',
'start'
);

await new Promise((res) => setTimeout(res, 500));
expect(scrollIntoView).toHaveBeenCalledTimes(2);
expect(scrollIntoView).toHaveBeenNthCalledWith(2, {
behavior: 'auto',
block: 'start',
inline: 'start',
});
});

test('item not exists', () => {
const { items, visibleItems } = setup([1, 1, 0.3]);

expect(() =>
createApi(items, visibleItems).scrollToItem(undefined)
).not.toThrow();
});
});

describe('scrollPrev', () => {
test('have prev item', async () => {
const { items, nodes, visibleItems } = setup([0, 1, 1]);
Expand Down
11 changes: 9 additions & 2 deletions src/createApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { filterSeparators, scrollToItem } from './helpers';
import {
filterSeparators,
scrollToItem,
getItemElementById,
getItemElementByIndex,
} from './helpers';
import ItemsMap from './ItemsMap';

import type { visibleItems } from './types';
Expand All @@ -15,7 +20,7 @@ export default function createApi(
const getItemById = (id: string) =>
items.find((value) => value[1].key === String(id))?.[1];

const getItemByIndex = (index: number) =>
const getItemByIndex = (index: number | string) =>
items.find((el) => String(el[1].index) === String(index))?.[1];

const isItemVisible = (id: string) => visibleItems.includes(id);
Expand All @@ -41,7 +46,9 @@ export default function createApi(

return {
getItemById,
getItemElementById,
getItemByIndex,
getItemElementByIndex,
getNextItem,
getPrevItem,
isFirstItemVisible,
Expand Down
56 changes: 56 additions & 0 deletions src/helpers.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from 'react';
import {
filterSeparators,
getElementOrConstructor,
getItemElementById,
getItemElementByIndex,
getNodesFromRefs,
observerEntriesToItems,
scrollToItem,
Expand Down Expand Up @@ -138,6 +140,60 @@ describe('scrollToItem', () => {
});
});

describe('getItemElementById', () => {
test('should return element node when exists', () => {
const id = 'test123';
document.body.innerHTML = `
<div data-key=${id}>${id}</div>
<div>other node</div>
<div data-key=123 />other2</div>`;

const result = getItemElementById(id);

expect(result instanceof HTMLDivElement).toBeTruthy();
expect(result?.textContent).toEqual(id);
});

test('should return null when element does not exists', () => {
const id = 'test123';
document.body.innerHTML = `
<div data-key=${id}>${id}</div>
<div>other node</div>
<div data-key=123 />other2</div>`;

expect(getItemElementById('test456')).toEqual(null);
expect(getItemElementById(456)).toEqual(null);
expect(getItemElementById('')).toEqual(null);
});
});

describe('getItemElementByIndex', () => {
test('should return element node when exists', () => {
const index = '123';
document.body.innerHTML = `
<div data-index=${index}>${index}</div>
<div>other node</div>
<div data-key=123 />other2</div>`;

const result = getItemElementByIndex(index);

expect(result instanceof HTMLDivElement).toBeTruthy();
expect(result?.textContent).toEqual(index);
});

test('should return null when element does not exists', () => {
const index = '123';
document.body.innerHTML = `
<div data-index=${index}>${index}</div>
<div>other node</div>
<div data-key=123 />other2</div>`;

expect(getItemElementByIndex('456')).toEqual(null);
expect(getItemElementByIndex(456)).toEqual(null);
expect(getItemElementByIndex('')).toEqual(null);
});
});

describe('getElementOrConstructor', () => {
const JsxElem = <div>jsx_elem</div>;
const JsxElemConstructor = () => JsxElem;
Expand Down
7 changes: 7 additions & 0 deletions src/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
} from './types';
import { separatorString } from './constants';
import { observerOptions } from './settings';
import { dataKeyAttribute, dataIndexAttribute } from './constants';

export const getNodesFromRefs = (refs: Refs): HTMLElement[] => {
const result = Object.values(refs)
Expand Down Expand Up @@ -57,6 +58,12 @@ export function scrollToItem(
}
}

export const getItemElementById = (id: string | number) =>
document.querySelector(`[${dataKeyAttribute}='${id}']`);

export const getItemElementByIndex = (id: string | number) =>
document.querySelector(`[${dataIndexAttribute}='${id}']`);

export function getElementOrConstructor(
Elem: React.FC | React.ReactNode
): JSX.Element | null {
Expand Down

0 comments on commit 5bdc115

Please sign in to comment.