📦️(frontend) vendor GridLayout to use react-hook useSize

Replace livekit's modern-only useSize hook with react-hook implementation
that includes polyfill support for older browsers. Will update remaining
layout components in subsequent commits.

Also, it now enables us to fix the pagination controls.
This commit is contained in:
lebaudantoine
2025-06-30 21:15:50 +02:00
parent 1e7aeaf8ce
commit b452b4c437
4 changed files with 209 additions and 0 deletions
@@ -0,0 +1,55 @@
import * as React from 'react'
import { createInteractingObservable } from '@livekit/components-core'
import { usePagination } from '@livekit/components-react'
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
export interface PaginationControlProps
extends Pick<
ReturnType<typeof usePagination>,
'totalPageCount' | 'nextPage' | 'prevPage' | 'currentPage'
> {
/** Reference to an HTML element that holds the pages, while interacting (`mouseover`)
* with it, the pagination controls will appear for a while. */
pagesContainer?: React.RefObject<HTMLElement>
}
export function PaginationControl({
totalPageCount,
nextPage,
prevPage,
currentPage,
pagesContainer: connectedElement,
}: PaginationControlProps) {
const [interactive, setInteractive] = React.useState(false)
React.useEffect(() => {
let subscription:
| ReturnType<ReturnType<typeof createInteractingObservable>['subscribe']>
| undefined
if (connectedElement) {
subscription = createInteractingObservable(
connectedElement.current,
2000
).subscribe(setInteractive)
}
return () => {
if (subscription) {
subscription.unsubscribe()
}
}
}, [connectedElement])
return (
<div
className="lk-pagination-control"
data-lk-user-interaction={interactive}
>
<button className="lk-button" onClick={prevPage}>
<RiArrowLeftSLine />
</button>
<span className="lk-pagination-count">{`${currentPage} of ${totalPageCount}`}</span>
<button className="lk-button" onClick={nextPage}>
<RiArrowRightSLine />
</button>
</div>
)
}
@@ -0,0 +1,30 @@
import * as React from 'react'
export interface PaginationIndicatorProps {
totalPageCount: number
currentPage: number
}
export const PaginationIndicator: (
props: PaginationIndicatorProps & React.RefAttributes<HTMLDivElement>
) => React.ReactNode = /* @__PURE__ */ React.forwardRef<
HTMLDivElement,
PaginationIndicatorProps
>(function PaginationIndicator(
{ totalPageCount, currentPage }: PaginationIndicatorProps,
ref
) {
const bubbles = new Array(totalPageCount).fill('').map((_, index) => {
if (index + 1 === currentPage) {
return <span data-lk-active key={index} />
} else {
return <span key={index} />
}
})
return (
<div ref={ref} className="lk-pagination-indicator">
{bubbles}
</div>
)
})
@@ -0,0 +1,71 @@
import * as React from 'react'
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
import {
TrackLoop,
usePagination,
UseParticipantsOptions,
useSwipe,
} from '@livekit/components-react'
import { mergeProps } from '@/utils/mergeProps'
import { PaginationIndicator } from '../controls/PaginationIndicator'
import { useGridLayout } from '../../hooks/useGridLayout'
import { PaginationControl } from '../controls/PaginationControl'
/** @public */
export interface GridLayoutProps
extends React.HTMLAttributes<HTMLDivElement>,
Pick<UseParticipantsOptions, 'updateOnlyOn'> {
children: React.ReactNode
tracks: TrackReferenceOrPlaceholder[]
}
/**
* The `GridLayout` component displays the nested participants in a grid where every participants has the same size.
* It also supports pagination if there are more participants than the grid can display.
* @remarks
* To ensure visual stability when tiles are reordered due to track updates,
* the component uses the `useVisualStableUpdate` hook.
* @example
* ```tsx
* <LiveKitRoom>
* <GridLayout tracks={tracks}>
* <ParticipantTile />
* </GridLayout>
* <LiveKitRoom>
* ```
* @public
*/
export function GridLayout({ tracks, ...props }: GridLayoutProps) {
const gridEl = React.createRef<HTMLDivElement>()
const elementProps = React.useMemo(
() => mergeProps(props, { className: 'lk-grid-layout' }),
[props]
)
const { layout } = useGridLayout(gridEl, tracks.length)
const pagination = usePagination(layout.maxTiles, tracks)
useSwipe(gridEl, {
onLeftSwipe: pagination.nextPage,
onRightSwipe: pagination.prevPage,
})
return (
<div
ref={gridEl}
data-lk-pagination={pagination.totalPageCount > 1}
{...elementProps}
>
<TrackLoop tracks={pagination.tracks}>{props.children}</TrackLoop>
{tracks.length > layout.maxTiles && (
<>
<PaginationIndicator
totalPageCount={pagination.totalPageCount}
currentPage={pagination.currentPage}
/>
<PaginationControl pagesContainer={gridEl} {...pagination} />
</>
)}
</div>
)
}
@@ -0,0 +1,53 @@
import { GRID_LAYOUTS, selectGridLayout } from '@livekit/components-core'
import type {
GridLayoutDefinition,
GridLayoutInfo,
} from '@livekit/components-core'
import * as React from 'react'
import useSize from '@react-hook/size'
/**
* The `useGridLayout` hook tries to select the best layout to fit all tiles.
* If the available screen space is not enough, it will reduce the number of maximum visible
* tiles and select a layout that still works visually within the given limitations.
* As the order of tiles changes over time, the hook tries to keep visual updates to a minimum
* while trying to display important tiles such as speaking participants or screen shares.
*
* @example
* ```tsx
* const { layout } = useGridLayout(gridElement, trackCount);
* ```
* @public
*/
export function useGridLayout(
/** HTML element that contains the grid. */
gridElement: React.RefObject<HTMLDivElement>,
/** Count of tracks that should get layed out */
trackCount: number,
options: {
gridLayouts?: GridLayoutDefinition[]
} = {}
): { layout: GridLayoutInfo; containerWidth: number; containerHeight: number } {
const gridLayouts = options.gridLayouts ?? GRID_LAYOUTS
const [width, height] = useSize(gridElement)
const layout = selectGridLayout(gridLayouts, trackCount, width, height)
React.useEffect(() => {
if (gridElement.current && layout) {
gridElement.current.style.setProperty(
'--lk-col-count',
layout?.columns.toString()
)
gridElement.current.style.setProperty(
'--lk-row-count',
layout?.rows.toString()
)
}
}, [gridElement, layout])
return {
layout,
containerWidth: width,
containerHeight: height,
}
}