Skip to content

Commit

Permalink
chore(utils): move useEndlessScrollList to juno-ui-components, deprea…
Browse files Browse the repository at this point in the history
…cate juno-utils package (#585)

* chore(ui): include useEndlessScrollList hook into ui components

* chore(juno): change imports for useEndlessScroll

* chore(ui): modify tests and add types

* chore(ui): epoxrt useEndlessScrollList proper

* chore(utils): depreacte utils package

* chore(ui): add changeset

* chore(ui): remove comments

* chore(ui): fix regression of scroll

---------

Co-authored-by: Andreas Pfau <[email protected]>
  • Loading branch information
barsukov and andypf authored Nov 25, 2024
1 parent f3ceef4 commit 8615024
Show file tree
Hide file tree
Showing 12 changed files with 158 additions and 28 deletions.
10 changes: 10 additions & 0 deletions .changeset/popular-hairs-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@cloudoperators/juno-ui-components": minor
"@cloudoperators/juno-app-greenhouse": patch
"@cloudoperators/juno-app-supernova": patch
"@cloudoperators/juno-utils": patch
"@cloudoperators/juno-app-example": patch
"@cloudoperators/juno-app-doop": patch
---

Replace useEndlessScrollList from utils to ui-componetns and utils deprecation
10 changes: 8 additions & 2 deletions apps/doop/src/components/violations/ViolationDetailsList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@

import React from "react"
import { capitalize } from "../../lib/helpers"
import { useEndlessScrollList } from "@cloudoperators/juno-utils"
import { DataGrid, DataGridRow, DataGridCell, Box, Stack } from "@cloudoperators/juno-ui-components"
import {
DataGrid,
DataGridRow,
DataGridCell,
Box,
Stack,
useEndlessScrollList,
} from "@cloudoperators/juno-ui-components"
// import { useGlobalsDetailsViolationItems } from "../StoreProvider"
import { useDataDetailsViolationGroup } from "../StoreProvider"
import ReactMarkdown from "react-markdown"
Expand Down
2 changes: 1 addition & 1 deletion apps/example/src/components/peaks/PeaksList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import {
DataGridHeadCell,
DataGridRow,
DataGridCell,
useEndlessScrollList,
} from "@cloudoperators/juno-ui-components"
import PeaksListItem from "./PeaksListItem"
import HintNotFound from "../shared/HintNotFound"
import { useGlobalsActions } from "../StoreProvider"
import { useEndlessScrollList } from "@cloudoperators/juno-utils"

const LIST_COLUMNS = 6

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
*/

import React, { useMemo } from "react"
import { DataGrid, DataGridRow, DataGridCell, DataGridHeadCell } from "@cloudoperators/juno-ui-components"
import {
DataGrid,
DataGridRow,
DataGridCell,
DataGridHeadCell,
useEndlessScrollList,
} from "@cloudoperators/juno-ui-components"
import TeamListItem from "./TeamListItem"
import { useCurrentTeam, useDefaultTeam, useTeamMemberships } from "../StoreProvider"
import { useEndlessScrollList } from "@cloudoperators/juno-utils"

const TeamList = () => {
const currentTeam = useCurrentTeam()
Expand Down
2 changes: 1 addition & 1 deletion apps/supernova/src/components/silences/SilencesList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import {
SelectOption,
Spinner,
Icon,
useEndlessScrollList,
} from "@cloudoperators/juno-ui-components"
import constants from "../../constants"
import { useEndlessScrollList } from "@cloudoperators/juno-utils"
import {
useSilencesItems,
useSilencesActions,
Expand Down
8 changes: 7 additions & 1 deletion packages/ui-components/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ const config = [
file: `${buildDir}/lib/tailwind.config.js`,
},
},
{
input: "src/hooks/useEndlessScrollList.tsx",
output: {
file: `${buildDir}/hooks/useEndlessScrollList.js`,
},
plugins: [typescript({ tsconfig: path.resolve(__dirname, "./tsconfig.hooks.build.json") })],
},
{
input: "lib/variables.scss",
output: {
Expand All @@ -144,5 +151,4 @@ const config = [
],
},
]

module.exports = config
83 changes: 83 additions & 0 deletions packages/ui-components/src/hooks/useEndlessScrollList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { render, queryByAttribute, renderHook } from "@testing-library/react"
import { useEndlessScrollList } from "./useEndlessScrollList"

const intersectionObserverMock = () => ({
observe: () => null,
disconnect: () => null,
})
window.IntersectionObserver = vi.fn().mockImplementation(intersectionObserverMock)

describe("useEndlessScrollList", () => {
it("return no scroll items if items not all provided", () => {
const { result } = renderHook(() => useEndlessScrollList([]))
expect(result?.current?.scrollListItems?.length).toBe(0)
})

describe("scrollListItems", () => {
it("return all items if the whole amount is less then 20", () => {
const { result } = renderHook(() => useEndlessScrollList(["1", "2", "3"]))
expect(result?.current?.scrollListItems?.length).toBe(3)
})
it("return 20 items if the whole amount is more then 20", () => {
const newArray = Array.from({ length: 30 }, (_, i) => `${i + 1}`)
const { result } = renderHook(() => useEndlessScrollList(newArray))
expect(result?.current?.scrollListItems?.length).toBe(20)
})
})

describe("iterator", () => {
it("returns a map function which iterates over all scrollListItems and adds the intersection ref if nothing else specified in options", () => {
const { result } = renderHook(() => useEndlessScrollList(["1", "2", "3"]))
const mapFunction = result.current.iterator.map(
// eslint-disable-next-line no-unused-vars
(item: unknown, index: number, array: unknown[]): React.JSX.Element => item as React.JSX.Element
)
const getById = queryByAttribute.bind(null, "id")
const dom = render(mapFunction)
const intersectionRefElement = getById(dom.container, "endlessScrollListLastItemRef")
expect(intersectionRefElement).toBeTruthy()
})

it("returns a map function which iterates over all scrollListItems and do not include the intersection ref or do not call refFunction if showRef is set to false in options", () => {
const refFunction = vi.fn()
const { result } = renderHook(() =>
useEndlessScrollList(["1", "2", "3"], {
showRef: false,
refFunction: refFunction,
})
)
const mapFunction = result.current.iterator.map(
// eslint-disable-next-line no-unused-vars
(item: unknown, index: number, array: unknown[]): React.JSX.Element => item as React.JSX.Element
)
const getById = queryByAttribute.bind(null, "id")
const dom = render(mapFunction)
const intersectionRefElement = getById(dom.container, "endlessScrollListLastItemRef")
expect(intersectionRefElement).toBeFalsy()
expect(refFunction).not.toHaveBeenCalled()
})

it("returns a map function which iterates over all scrollListItems and does not add an intersection ref element", () => {
const refFunction = vi.fn()
const { result } = renderHook(() =>
useEndlessScrollList(["1", "2", "3"], {
refFunction: refFunction,
})
)
const mapFunction = result.current.iterator.map(
// eslint-disable-next-line no-unused-vars
(item: unknown, index: number, array: unknown[]): React.JSX.Element => item as React.JSX.Element
)
const getById = queryByAttribute.bind(null, "id")
const dom = render(mapFunction)
const intersectionRefElement = getById(dom.container, "endlessScrollListLastItemRef")
expect(intersectionRefElement).toBeFalsy()
expect(refFunction).toHaveBeenCalled()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useRef, useEffect, useState, useCallback, useMemo } from "react"
import React, { useRef, useEffect, useState, useMemo, useCallback, LegacyRef } from "react"

/*
This hook is used to create an endless scroll list.
@param items: the items to be displayed
@param items: complete list of items to be displayed
@param options: options for the hook
@param options.delay: the delay in ms between adding items to the list. Default is 500ms
@param options.showLoading: whether to show the loading indicator. Default is true and it renders a span with the text "Loading..."
Expand All @@ -16,19 +16,28 @@ import React, { useRef, useEffect, useState, useCallback, useMemo } from "react"
@param options.refFunction: the function to be used to render the ref element. It receives the ref as a parameter
@return: an object with the following properties:
@property scrollListItems: the items to be displayed
@property lastLisItemRef: the ref element to be used as the last item
@property isAddingItems: whether items are being added to the list
@property iterator: an iterator to be used to render the list. It has a map function that receives a function to be used to render each item
*/
const useEndlessScrollList = (items, options = {}) => {
/* eslint-disable no-unused-vars */
interface UseEndlessScrollListOptions {
delay?: number
showLoading?: boolean
loadingObject?: React.ReactNode
showRef?: boolean
refFunction?: RefFunction
}
type RefFunction = (node: Element) => void
type Timeout = ReturnType<typeof setTimeout>
export const useEndlessScrollList = (items: unknown[], options: UseEndlessScrollListOptions = {}) => {
const [visibleAmount, setVisibleAmount] = useState(20)
const [isAddingItems, setIsAddingItems] = useState(false)
const timeoutRef = useRef(null)
const observer = useRef()
const timeoutRef = useRef<Timeout | null>(null)
const observer = useRef<IntersectionObserver | undefined>()

useEffect(() => {
// clear when component is unmounted
return () => clearTimeout(timeoutRef.current)
// fix type issue
return () => clearTimeout(timeoutRef.current as Timeout)
}, [])

// recalculate if items change
Expand All @@ -39,8 +48,8 @@ const useEndlessScrollList = (items, options = {}) => {
}, [items, visibleAmount])

// recalculate if items change
const lastLisItemRef = useCallback(
(node) => {
const lastLisItemRef = useCallback<RefFunction>(
(node: Element) => {
// skip if already adding items
if (isAddingItems) return
// disconnect previous observer
Expand All @@ -49,39 +58,44 @@ const useEndlessScrollList = (items, options = {}) => {
observer.current = new IntersectionObserver((entries) => {
// if the last element is intersecting and there are still items to show
if (entries[0].isIntersecting && visibleAmount <= items.length) {
clearTimeout(timeoutRef.current)
clearTimeout(timeoutRef.current as Timeout)
setIsAddingItems(true)
timeoutRef.current = setTimeout(() => {
setIsAddingItems(false)
setVisibleAmount((prev) => prev + 10)
}, options?.delay || 500)
}
})
if (node) observer.current.observe(node)
if (node) return observer.current.observe(node)
},
[items, isAddingItems]
)

const iterator = useMemo(() => {
return {
map: (f) => {
const content = scrollListItems.map(f)
map: (elements: (value: unknown, index: number, array: unknown[]) => React.JSX.Element) => {
const content = scrollListItems?.map<React.JSX.Element>(elements)
return (
<>
{content}
{isAddingItems && options?.showLoading !== false && (
<>{options?.loadingObject ? options.loadingObject : <span>Loading...</span>}</>
<>
{options?.loadingObject ? options.loadingObject : <span id="endlessScrollListLoading">Loading...</span>}
</>
)}
{options?.showRef !== false && (
<>{options?.refFunction ? options.refFunction(lastLisItemRef) : <span ref={lastLisItemRef} />}</>
<>
{options?.refFunction ? (
options.refFunction(lastLisItemRef as unknown as Element)
) : (
<span id="endlessScrollListLastItemRef" ref={lastLisItemRef as LegacyRef<HTMLSpanElement>} />
)}
</>
)}
</>
)
},
}
}, [scrollListItems, lastLisItemRef])

return { scrollListItems, lastLisItemRef, isAddingItems, iterator }
return { scrollListItems, iterator }
}

export default useEndlessScrollList
1 change: 1 addition & 0 deletions packages/ui-components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,4 @@ export { TooltipContent } from "./components/TooltipContent/index.js"
export { TooltipTrigger } from "./components/TooltipTrigger/index.js"
export { TopNavigation } from "./components/TopNavigation/index.js"
export { TopNavigationItem } from "./components/TopNavigationItem/index.js"
export { useEndlessScrollList } from "./hooks/useEndlessScrollList"
2 changes: 1 addition & 1 deletion packages/ui-components/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"extends": "@cloudoperators/juno-config/typescript/base.json",
"compilerOptions": {
"target": "ES5",
"target": "ESNext",
"module": "ESNext",
"jsx": "react",
"strict": true,
Expand Down
4 changes: 4 additions & 0 deletions packages/ui-components/tsconfig.hooks.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.build.json",
"include": ["./src/hooks/*.ts", "./types/hooks/*.ts", "./src/hooks/*.tsx"]
}
1 change: 1 addition & 0 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "@cloudoperators/juno-utils",
"version": "1.1.14",
"description": "Description of utils",
"deprecated": "This package is deprecated and will be removed from juno core. Use @cloudoperators/juno-mock-server and @cloudoperators/juno-ui-components instad.",
"author": "UI-Team",
"contributors": [
"Andreas Pfau",
Expand Down

0 comments on commit 8615024

Please sign in to comment.