📦️(frontend) vendor GridLayout components
Vendor LiveKit layout components to internationalize them
This commit is contained in:
committed by
aleb_the_flash
parent
e86dc12bf9
commit
73a9fb3a72
@@ -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 '@/features/rooms/livekit/hooks/useResizeObserver'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,6 @@ import { useState } from 'react'
|
|||||||
import {
|
import {
|
||||||
ConnectionStateToast,
|
ConnectionStateToast,
|
||||||
FocusLayoutContainer,
|
FocusLayoutContainer,
|
||||||
GridLayout,
|
|
||||||
LayoutContextProvider,
|
LayoutContextProvider,
|
||||||
RoomAudioRenderer,
|
RoomAudioRenderer,
|
||||||
usePinnedTracks,
|
usePinnedTracks,
|
||||||
@@ -36,6 +35,7 @@ import { SettingsDialogProvider } from '@/features/settings/components/SettingsD
|
|||||||
import { useSubtitles } from '@/features/subtitle/hooks/useSubtitles'
|
import { useSubtitles } from '@/features/subtitle/hooks/useSubtitles'
|
||||||
import { Subtitles } from '@/features/subtitle/component/Subtitles'
|
import { Subtitles } from '@/features/subtitle/component/Subtitles'
|
||||||
import { CarouselLayout } from '../components/layout/CarouselLayout'
|
import { CarouselLayout } from '../components/layout/CarouselLayout'
|
||||||
|
import { GridLayout } from '../components/layout/GridLayout'
|
||||||
|
|
||||||
const LayoutWrapper = styled(
|
const LayoutWrapper = styled(
|
||||||
'div',
|
'div',
|
||||||
|
|||||||
Reference in New Issue
Block a user