From 5bdc115e75d7484539a23ae44ba6687f35e6d1ad Mon Sep 17 00:00:00 2001 From: Aleksandr Smyshlyaev Date: Sat, 16 Oct 2021 12:32:00 +0400 Subject: [PATCH] feat: getItemElementById and getItemElementByIndex helpers getItemElementById can be used to get dom element directly and scroll to it, e.g. with apiRef object #167 --- README.md | 6 +- src/components/Item/Item.tsx | 7 ++- src/components/Separator/Separator.tsx | 8 ++- src/constants.ts | 3 + src/createApi.test.ts | 76 ++++++++++---------------- src/createApi.ts | 11 +++- src/helpers.test.tsx | 56 +++++++++++++++++++ src/helpers.tsx | 7 +++ 8 files changed, 123 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 5eaadd76..3ae95a4d 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/src/components/Item/Item.tsx b/src/components/Item/Item.tsx index 3d9ae954..e1bcffcf 100644 --- a/src/components/Item/Item.tsx +++ b/src/components/Item/Item.tsx @@ -1,6 +1,7 @@ import React from 'react'; import type { Refs } from '../../types'; +import { dataKeyAttribute, dataIndexAttribute } from '../../constants'; export type Props = { id: string; @@ -15,7 +16,11 @@ function Item({ children, className, id, index, refs }: Props) { refs[String(index)] = ref; return ( -
+
{children}
); diff --git a/src/components/Separator/Separator.tsx b/src/components/Separator/Separator.tsx index ddf281e5..b5481668 100644 --- a/src/components/Separator/Separator.tsx +++ b/src/components/Separator/Separator.tsx @@ -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; @@ -14,7 +16,11 @@ function Separator({ className, id, index, refs }: Props) { refs[index] = ref; return ( -
+
); } diff --git a/src/constants.ts b/src/constants.ts index db285f22..cabb2ab9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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'; diff --git a/src/createApi.test.ts b/src/createApi.test.ts index 519c7f0c..c0777e87 100644 --- a/src/createApi.test.ts +++ b/src/createApi.test.ts @@ -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(); @@ -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]); @@ -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]); diff --git a/src/createApi.ts b/src/createApi.ts index 4d268788..18fd3c96 100644 --- a/src/createApi.ts +++ b/src/createApi.ts @@ -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'; @@ -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); @@ -41,7 +46,9 @@ export default function createApi( return { getItemById, + getItemElementById, getItemByIndex, + getItemElementByIndex, getNextItem, getPrevItem, isFirstItemVisible, diff --git a/src/helpers.test.tsx b/src/helpers.test.tsx index a633f045..9416a010 100644 --- a/src/helpers.test.tsx +++ b/src/helpers.test.tsx @@ -2,6 +2,8 @@ import React from 'react'; import { filterSeparators, getElementOrConstructor, + getItemElementById, + getItemElementByIndex, getNodesFromRefs, observerEntriesToItems, scrollToItem, @@ -138,6 +140,60 @@ describe('scrollToItem', () => { }); }); +describe('getItemElementById', () => { + test('should return element node when exists', () => { + const id = 'test123'; + document.body.innerHTML = ` +
${id}
+
other node
+
other2
`; + + 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 = ` +
${id}
+
other node
+
other2
`; + + 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 = ` +
${index}
+
other node
+
other2
`; + + 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 = ` +
${index}
+
other node
+
other2
`; + + expect(getItemElementByIndex('456')).toEqual(null); + expect(getItemElementByIndex(456)).toEqual(null); + expect(getItemElementByIndex('')).toEqual(null); + }); +}); + describe('getElementOrConstructor', () => { const JsxElem =
jsx_elem
; const JsxElemConstructor = () => JsxElem; diff --git a/src/helpers.tsx b/src/helpers.tsx index 24878873..8134a95c 100644 --- a/src/helpers.tsx +++ b/src/helpers.tsx @@ -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) @@ -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 {