Skip to content

Commit

Permalink
Improved paged list loader
Browse files Browse the repository at this point in the history
  • Loading branch information
pookmish committed Jul 3, 2024
1 parent 1079212 commit 6ae13de
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 150 deletions.
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@formkit/auto-animate": "^0.8.2",
"@heroicons/react": "^2.1.4",
"@js-temporal/polyfill": "^0.4.4",
"@mui/base": "^5.0.0-dev.20240529-082515-213b5e33ab",
"@mui/base": "5.0.0-beta.40",
"@next/third-parties": "^14.2.4",
"@tailwindcss/container-queries": "^0.1.1",
"@types/node": "^20.14.9",
Expand All @@ -38,27 +38,27 @@
"html-react-parser": "^5.1.10",
"next": "^14.2.4",
"next-drupal": "^1.6.0",
"postcss": "^8.4.38",
"qs": "^6.12.1",
"postcss": "^8.4.39",
"qs": "^6.12.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-focus-lock": "^2.12.1",
"react-instantsearch": "^7.11.4",
"react-instantsearch-nextjs": "^0.3.4",
"react-instantsearch": "^7.12.0",
"react-instantsearch-nextjs": "^0.3.5",
"react-slick": "^0.30.2",
"react-tiny-oembed": "^1.1.0",
"sharp": "^0.33.4",
"tailwind-merge": "^2.3.0",
"tailwindcss": "^3.4.4",
"typescript": "^5.5.2",
"typescript": "^5.5.3",
"usehooks-ts": "^3.1.0"
},
"devDependencies": {
"@graphql-codegen/add": "^5.0.3",
"@graphql-codegen/cli": "^5.0.2",
"@graphql-codegen/import-types-preset": "^3.0.0",
"@graphql-codegen/typescript-graphql-request": "^6.2.0",
"@graphql-codegen/typescript-operations": "^4.2.1",
"@graphql-codegen/typescript-operations": "^4.2.3",
"@next/bundle-analyzer": "^14.2.4",
"@storybook/addon-essentials": "^8.1.11",
"@storybook/addon-interactions": "^8.1.11",
Expand Down
90 changes: 55 additions & 35 deletions src/components/elements/paged-list.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"use client"

import {useLayoutEffect, useRef, HtmlHTMLAttributes, useEffect, useId, JSX, useState} from "react"
import {useLayoutEffect, useRef, HtmlHTMLAttributes, useEffect, JSX, useState, useCallback} from "react"
import {useAutoAnimate} from "@formkit/auto-animate/react"
import {useBoolean, useCounter} from "usehooks-ts"
import {useRouter, useSearchParams} from "next/navigation"
import usePagination from "@lib/hooks/usePagination"
import useFocusOnRender from "@lib/hooks/useFocusOnRender"
import {ArrowLongLeftIcon, ArrowLongRightIcon} from "@heroicons/react/20/solid"

type Props = HtmlHTMLAttributes<HTMLDivElement> & {
/**
Expand Down Expand Up @@ -35,29 +36,35 @@ type Props = HtmlHTMLAttributes<HTMLDivElement> & {
}

const PagedList = ({children, ulProps, liProps, pageKey = "page", totalPages, pagerSiblingCount = 2, loadPage, ...props}: Props) => {
const id = useId()
const ref = useRef(false)
const [items, setItems] = useState<JSX.Element[]>(Array.isArray(children) ? children : [children])

const router = useRouter()
const searchParams = useSearchParams()

// Use the GET param for page, but make sure that it is between 1 and the last page. If it's a string or a number
// outside the range, fix the value, so it works as expected.
const {count: currentPage, setCount: setPage} = useCounter(Math.max(1, parseInt(searchParams.get(pageKey || "") || "") || 1))

const {value: focusOnElement, setTrue: enableFocusElement, setFalse: disableFocusElement} = useBoolean(false)

const focusItemRef = useRef<HTMLLIElement>(null)
const [animationParent] = useAutoAnimate<HTMLUListElement>()

const goToPage = async (page: number) => {
if (loadPage) {
const newView = await loadPage(page - 1)
setItems(newView.props.children)
}

enableFocusElement()
setPage(page)
}
const goToPage = useCallback(
async (page: number, doNotFocusOnResults?: boolean) => {
if (loadPage) {
loadPage(page - 1)
.then(response => {
setItems(response.props.fallback ? response.props.children.props.children : response.props.children)

if (!doNotFocusOnResults) enableFocusElement()
setPage(page)
})
.catch(() => console.warn("An error occurred fetching more results."))
}
},
[enableFocusElement, setPage, loadPage]
)

const setFocusOnItem = useFocusOnRender(focusItemRef, false)

Expand All @@ -80,16 +87,10 @@ const PagedList = ({children, ulProps, liProps, pageKey = "page", totalPages, pa
}, [loadPage, router, currentPage, pageKey, searchParams])

useEffect(() => {
const updateInitialContents = async (initialPage: number) => {
if (loadPage) {
const newView = await loadPage(initialPage - 1)
setItems(newView.props.children)
}
}

const initialPage = parseInt(searchParams.get(pageKey || "") || "")
if (initialPage > 1) updateInitialContents(initialPage)
}, [searchParams, pageKey, loadPage])
if (initialPage > 1 && !ref.current) goToPage(initialPage, true)
ref.current = true
}, [searchParams, pageKey, loadPage, goToPage])

const paginationButtons = usePagination(totalPages * items.length, currentPage, items.length, pagerSiblingCount)

Expand All @@ -101,7 +102,7 @@ const PagedList = ({children, ulProps, liProps, pageKey = "page", totalPages, pa
>
{items.map((item, i) => (
<li
key={`pager-${id}-${i}`}
key={`pager-${currentPage}-${i}`}
ref={i === 0 ? focusItemRef : null}
tabIndex={i === 0 && focusOnElement ? 0 : undefined}
onBlur={disableFocusElement}
Expand All @@ -115,16 +116,17 @@ const PagedList = ({children, ulProps, liProps, pageKey = "page", totalPages, pa
{loadPage && paginationButtons.length > 1 && (
<nav
aria-label="Pager"
className="mx-auto w-fit"
className="rs-mt-4 mx-auto w-fit"
>
<ul className="list-unstyled flex gap-5">
<ul className="list-unstyled flex items-center gap-5">
{paginationButtons.map((pageNum, i) => (
<PaginationButton
key={`page-button-${pageNum}--${i}`}
page={pageNum}
currentPage={currentPage}
total={items.length * totalPages}
onClick={() => goToPage(pageNum)}
total={totalPages}
onPageClick={goToPage}
pagerSiblingCount={pagerSiblingCount}
/>
))}
</ul>
Expand All @@ -134,7 +136,7 @@ const PagedList = ({children, ulProps, liProps, pageKey = "page", totalPages, pa
)
}

const PaginationButton = ({page, currentPage, total, onClick}: {page: number | string; currentPage: number; total: number; onClick: () => void}) => {
const PaginationButton = ({page, currentPage, total, onPageClick, pagerSiblingCount}: {page: number | string; currentPage: number; total: number; onPageClick: (_page: number) => void; pagerSiblingCount: number}) => {
if (page === 0) {
return (
<li className="mt-auto h-full">
Expand All @@ -143,22 +145,40 @@ const PaginationButton = ({page, currentPage, total, onClick}: {page: number | s
</li>
)
}
const isCurrent = page == currentPage

const handleClick = () => {
if (page === "leftArrow") return onPageClick(1)
if (page === "rightArrow") return onPageClick(total)
onPageClick(page as number)
}

// Conditionally render left arrow and right arrow based on currentPage
if (page === 1 && currentPage >= pagerSiblingCount + 3) return null
if (page === "leftArrow" && currentPage < pagerSiblingCount + 3) return null

if (page === total && currentPage <= total - (pagerSiblingCount + 3)) return null
if (page === "rightArrow" && currentPage > total - (pagerSiblingCount + 3)) return null

const isCurrent = page === currentPage
return (
<li>
<li className="m-0 flex items-center">
<button
className="text-m2 font-medium hocus:underline"
onClick={onClick}
aria-current={isCurrent}
className="group text-m2 font-medium hocus:underline"
onClick={handleClick}
aria-current={isCurrent ? "page" : undefined}
>
<span className="sr-only">
Go to page {page} of {total}
{page === "leftArrow" && "Go to first page"}
{page === "rightArrow" && "Go to last page"}
{page !== "leftArrow" && page !== "rightArrow" && `Go to page ${page} of ${total}`}
</span>
<span
aria-hidden
className={(isCurrent ? "border-digital-red text-digital-red" : "border-transparent text-cardinal-red") + " border-b-2 px-4"}
className={(isCurrent ? "border-stone-dark text-stone-dark" : "border-transparent text-cardinal-red") + " block h-fit border-b-2 px-4"}
>
{page}
{page === "leftArrow" && <ArrowLongLeftIcon width={30} />}
{page === "rightArrow" && <ArrowLongRightIcon width={30} />}
{page !== "leftArrow" && page !== "rightArrow" && page}
</span>
</button>
</li>
Expand Down
32 changes: 16 additions & 16 deletions src/components/paragraphs/stanford-lists/list-paragraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ type Props = HtmlHTMLAttributes<HTMLDivElement> & {
paragraph: ParagraphStanfordList
}

const loadPage = async (viewId: string, displayId: string, contextualFilter: string[], hasHeadline: boolean, page: number): Promise<JSX.Element> => {
"use server"

const {items, totalItems} = await getViewItems(viewId, displayId, contextualFilter, page)
return (
<View
viewId={viewId}
displayId={displayId}
items={items}
headingLevel={hasHeadline ? "h3" : "h2"}
totalItems={totalItems}
/>
)
}

const ListParagraph = async ({paragraph, ...props}: Props) => {
const behaviors = getParagraphBehaviors<ListParagraphBehaviors>(paragraph)
const viewId = paragraph.suListView?.view || ""
Expand All @@ -24,21 +39,6 @@ const ListParagraph = async ({paragraph, ...props}: Props) => {

const ListWrapper: ElementType = paragraph.suListHeadline && behaviors.list_paragraph?.heading_behavior !== "remove" ? "section" : "div"

const loadPage = async (page: number): Promise<JSX.Element> => {
"use server"

const {items, totalItems} = await getViewItems(viewId, displayId, paragraph.suListView?.contextualFilter, page)
return (
<View
viewId={viewId}
displayId={displayId}
items={items}
headingLevel={paragraph.suListHeadline ? "h3" : "h2"}
totalItems={totalItems}
/>
)
}

return (
<ListWrapper
{...props}
Expand All @@ -62,7 +62,7 @@ const ListParagraph = async ({paragraph, ...props}: Props) => {
displayId={displayId}
items={viewItems}
headingLevel={paragraph.suListHeadline ? "h3" : "h2"}
loadPage={addLoadMore ? loadPage : undefined}
loadPage={addLoadMore ? loadPage.bind(null, viewId, displayId, paragraph.suListView?.contextualFilter || [], !!paragraph.suListHeadline) : undefined}
totalItems={totalItems}
/>
)}
Expand Down
21 changes: 13 additions & 8 deletions src/lib/hooks/usePagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,21 @@ import {useMemo} from "react"
* @param siblingCount
* How many page buttons to display left and right of the current page.
*/
const usePagination = (totalCount: number, currentPage = 1, pageSize = 5, siblingCount = 2): number[] => {
const usePagination = (totalCount: number, currentPage = 1, pageSize = 5, siblingCount = 2): (number | string)[] => {
return useMemo(() => {
const totalPageCount = Math.ceil(totalCount / pageSize)

// Pages count is determined as siblingCount + firstPage + lastPage + currentPage + 2 * DOTS
const totalPageNumbers = siblingCount + 5
// Pages count is determined as siblingCount + firstPage + lastPage + currentPage + 2 * DOTS + 2 * ARROWS
const totalPageNumbers = siblingCount + 7

// Arrow constants
const leftArrow = "leftArrow"
const rightArrow = "rightArrow"

// Case 1: If the number of pages is less than the page numbers we want to show in our paginationComponent, we
// return the range [1..totalPageCount]
if (totalPageNumbers >= totalPageCount) {
return range(1, totalPageCount)
return [leftArrow, ...range(1, totalPageCount), rightArrow]
}

// Calculate left and right sibling index and make sure they are within range 1 and totalPageCount
Expand All @@ -31,7 +35,7 @@ const usePagination = (totalCount: number, currentPage = 1, pageSize = 5, siblin

// We do not show dots just when there is just one page number to be inserted between the extremes of sibling and the page limits i.e 1 and totalPageCount. Hence we are using leftSiblingIndex > 2 and rightSiblingIndex < totalPageCount - 2
const shouldShowLeftDots = leftSiblingIndex > 2
const shouldShowRightDots = rightSiblingIndex < totalPageCount - 1
const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2

const firstPageIndex = 1
const lastPageIndex = totalPageCount
Expand All @@ -41,20 +45,21 @@ const usePagination = (totalCount: number, currentPage = 1, pageSize = 5, siblin
const leftItemCount = 3 + 2 * siblingCount
const leftRange = range(1, leftItemCount)

return [...leftRange, 0, totalPageCount]
return [leftArrow, ...leftRange, 0, totalPageCount, rightArrow]
}

// Case 3: No right dots to show, but left dots to be shown
if (shouldShowLeftDots && !shouldShowRightDots) {
const rightItemCount = 3 + 2 * siblingCount
const rightRange = range(totalPageCount - rightItemCount + 1, totalPageCount)
return [firstPageIndex, 0, ...rightRange]

return [leftArrow, firstPageIndex, 0, ...rightRange, rightArrow]
}

// Case 4: Both left and right dots to be shown
if (shouldShowLeftDots && shouldShowRightDots) {
const middleRange = range(leftSiblingIndex, rightSiblingIndex)
return [firstPageIndex, 0, ...middleRange, 0, lastPageIndex]
return [leftArrow, firstPageIndex, 0, ...middleRange, 0, lastPageIndex, rightArrow]
}
return []
}, [totalCount, pageSize, siblingCount, currentPage])
Expand Down
Loading

0 comments on commit 6ae13de

Please sign in to comment.