From 56878f647ea0f1066fa3e222d7aa0d83057f496d Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Mon, 13 Nov 2023 17:45:59 +0100 Subject: refactor(components): rewrite PostsList component * remove NoResults component and move logic to Search page * add a usePostsList hook * remove Pagination from PostsList (it is only used if javascript is disabled and not on every posts list) * replace `byYear` prop with `sortByYear` * replace `loadMore` prop with `onLoadMore` * remove `showLoadMoreBtn` (we can use `loadMore` prop instead to determine if we need to display the button) * replace `titleLevel` prop with `headingLvl` * add `firstNewResult` prop to handle focus on the new results when loading more article (we should not focus a useless span but the item directly) --- src/utils/helpers/index.ts | 1 + src/utils/helpers/refs.test.tsx | 28 +++++++++ src/utils/helpers/refs.ts | 16 ++++++ src/utils/hooks/index.ts | 1 + src/utils/hooks/use-posts-list/index.ts | 1 + .../hooks/use-posts-list/use-posts-list.test.ts | 24 ++++++++ src/utils/hooks/use-posts-list/use-posts-list.ts | 66 ++++++++++++++++++++++ 7 files changed, 137 insertions(+) create mode 100644 src/utils/helpers/refs.test.tsx create mode 100644 src/utils/helpers/refs.ts create mode 100644 src/utils/hooks/use-posts-list/index.ts create mode 100644 src/utils/hooks/use-posts-list/use-posts-list.test.ts create mode 100644 src/utils/hooks/use-posts-list/use-posts-list.ts (limited to 'src/utils') diff --git a/src/utils/helpers/index.ts b/src/utils/helpers/index.ts index f340a49..79077de 100644 --- a/src/utils/helpers/index.ts +++ b/src/utils/helpers/index.ts @@ -2,6 +2,7 @@ export * from './author'; export * from './images'; export * from './pages'; export * from './reading-time'; +export * from './refs'; export * from './rss'; export * from './schema-org'; export * from './strings'; diff --git a/src/utils/helpers/refs.test.tsx b/src/utils/helpers/refs.test.tsx new file mode 100644 index 0000000..93e5f89 --- /dev/null +++ b/src/utils/helpers/refs.test.tsx @@ -0,0 +1,28 @@ +import { describe, it, jest } from '@jest/globals'; +import { render } from '@testing-library/react'; +import { forwardRef, useImperativeHandle } from 'react'; +import { mergeRefs } from './refs'; + +const refValue = 'minus architecto qui'; +const TestComponentWithForwardedRef = forwardRef((_, ref) => { + useImperativeHandle(ref, () => refValue); + return null; +}); +TestComponentWithForwardedRef.displayName = 'TestComponentWithForwardedRef'; + +describe('merge-refs', () => { + it('can merge a ref function with a ref object', () => { + const refFn = jest.fn(); + const refObj = { current: null }; + + const TestComponent = () => ( + + ); + + render(); + + expect(refFn).toHaveBeenCalledTimes(1); + expect(refFn).toHaveBeenLastCalledWith(refValue); + expect(refObj.current).toBe(refValue); + }); +}); diff --git a/src/utils/helpers/refs.ts b/src/utils/helpers/refs.ts new file mode 100644 index 0000000..74a695a --- /dev/null +++ b/src/utils/helpers/refs.ts @@ -0,0 +1,16 @@ +import type { LegacyRef, MutableRefObject, RefCallback } from 'react'; +import type { Nullable } from '../../types'; + +export const mergeRefs = + ( + refs: (MutableRefObject | LegacyRef | undefined | null)[] + ): RefCallback => + (value) => { + refs.forEach((ref) => { + if (typeof ref === 'function') { + ref(value); + } else if (ref !== null) { + (ref as MutableRefObject>).current = value; + } + }); + }; diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts index 9cc2b0f..68fb7ce 100644 --- a/src/utils/hooks/index.ts +++ b/src/utils/hooks/index.ts @@ -14,6 +14,7 @@ export * from './use-match-media'; export * from './use-mutation-observer'; export * from './use-on-click-outside'; export * from './use-pagination'; +export * from './use-posts-list'; export * from './use-prism'; export * from './use-prism-theme'; export * from './use-reading-time'; diff --git a/src/utils/hooks/use-posts-list/index.ts b/src/utils/hooks/use-posts-list/index.ts new file mode 100644 index 0000000..664c142 --- /dev/null +++ b/src/utils/hooks/use-posts-list/index.ts @@ -0,0 +1 @@ +export * from './use-posts-list'; diff --git a/src/utils/hooks/use-posts-list/use-posts-list.test.ts b/src/utils/hooks/use-posts-list/use-posts-list.test.ts new file mode 100644 index 0000000..1d11111 --- /dev/null +++ b/src/utils/hooks/use-posts-list/use-posts-list.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from '@jest/globals'; +import { act, renderHook } from '@testing-library/react'; +import { getArticles } from '../../../services/graphql'; +import { usePostsList } from './use-posts-list'; + +describe('usePostsList', () => { + it('can return the first new result index when loading more posts', async () => { + const perPage = 5; + const { result } = renderHook(() => + usePostsList({ fetcher: getArticles, perPage }) + ); + + expect.assertions(2); + + expect(result.current.firstNewResultIndex).toBeUndefined(); + + await act(async () => { + await result.current.loadMore(); + }); + + // Assuming there is more than one page. + expect(result.current.firstNewResultIndex).toBe(perPage + 1); + }); +}); diff --git a/src/utils/hooks/use-posts-list/use-posts-list.ts b/src/utils/hooks/use-posts-list/use-posts-list.ts new file mode 100644 index 0000000..661727f --- /dev/null +++ b/src/utils/hooks/use-posts-list/use-posts-list.ts @@ -0,0 +1,66 @@ +import { useCallback, useState } from 'react'; +import type { PostData } from '../../../components'; +import type { Maybe, RawArticle } from '../../../types'; +import { getPostsList } from '../../helpers'; +import { + type UsePaginationConfig, + usePagination, + type UsePaginationReturn, +} from '../use-pagination'; + +export type usePostsListReturn = Omit< + UsePaginationReturn, + 'data' +> & { + /** + * The index of the first new result when loading more posts. + */ + firstNewResultIndex: Maybe; + /** + * The posts list. + */ + posts: Maybe; +}; + +export const usePostsList = ( + config: UsePaginationConfig +): usePostsListReturn => { + const { + data, + error, + hasNextPage, + isEmpty, + isError, + isLoading, + isLoadingMore, + isRefreshing, + isValidating, + loadMore, + size, + } = usePagination(config); + const [firstNewResultIndex, setFirstNewResultIndex] = + useState>(undefined); + + const posts = data ? getPostsList(data) : undefined; + + const handleLoadMore = useCallback(async () => { + setFirstNewResultIndex(size * config.perPage + 1); + + await loadMore(); + }, [config.perPage, loadMore, size]); + + return { + error, + firstNewResultIndex, + hasNextPage, + isEmpty, + isError, + isLoading, + isLoadingMore, + isRefreshing, + isValidating, + loadMore: handleLoadMore, + posts, + size, + }; +}; -- cgit v1.2.3