Remove usages of forwardRef

It has been deprecated in React 19, which allows functional components to receive refs just like any other prop.
This commit is contained in:
Robin
2025-06-23 22:48:37 -04:00
parent f86c9fe0a0
commit 0c27610119
16 changed files with 712 additions and 725 deletions

View File

@@ -6,12 +6,7 @@ Please see LICENSE in the repository root for full details.
*/
import classNames from "classnames";
import {
type FC,
type HTMLAttributes,
type ReactNode,
forwardRef,
} from "react";
import { type Ref, type FC, type HTMLAttributes, type ReactNode } from "react";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Heading, Text } from "@vector-im/compound-web";
@@ -24,12 +19,17 @@ import { EncryptionLock } from "./room/EncryptionLock";
import { useMediaQuery } from "./useMediaQuery";
interface HeaderProps extends HTMLAttributes<HTMLElement> {
ref?: Ref<HTMLElement>;
children: ReactNode;
className?: string;
}
export const Header = forwardRef<HTMLElement, HeaderProps>(
({ children, className, ...rest }, ref) => {
export const Header: FC<HeaderProps> = ({
ref,
children,
className,
...rest
}) => {
return (
<header
ref={ref}
@@ -39,8 +39,7 @@ export const Header = forwardRef<HTMLElement, HeaderProps>(
{children}
</header>
);
},
);
};
Header.displayName = "Header";

View File

@@ -5,11 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import {
type ComponentPropsWithoutRef,
forwardRef,
type MouseEvent,
} from "react";
import { type ComponentProps, type FC, type MouseEvent } from "react";
import { Link as CpdLink } from "@vector-im/compound-web";
import { type LinkProps, useHref, useLinkClickHandler } from "react-router-dom";
import classNames from "classnames";
@@ -26,31 +22,30 @@ export function useLink(
return [href, onClick];
}
type Props = Omit<
ComponentPropsWithoutRef<typeof CpdLink>,
"href" | "onClick"
> & { to: LinkProps["to"]; state?: unknown };
type Props = Omit<ComponentProps<typeof CpdLink>, "href" | "onClick"> & {
to: LinkProps["to"];
state?: unknown;
};
/**
* A version of Compound's link component that integrates with our router setup.
* This is only for app-internal links.
*/
export const Link = forwardRef<HTMLAnchorElement, Props>(function Link(
{ to, state, ...props },
ref,
) {
export const Link: FC<Props> = ({ ref, to, state, ...props }) => {
const [path, onClick] = useLink(to, state);
return <CpdLink ref={ref} {...props} href={path} onClick={onClick} />;
});
};
/**
* A link to an external web page, made to fit into blocks of text more subtly
* than the normal Compound link component.
*/
export const ExternalLink = forwardRef<
HTMLAnchorElement,
ComponentPropsWithoutRef<"a">
>(function ExternalLink({ className, children, ...props }, ref) {
export const ExternalLink: FC<ComponentProps<"a">> = ({
ref,
className,
children,
...props
}) => {
return (
<a
ref={ref}
@@ -62,4 +57,4 @@ export const ExternalLink = forwardRef<
{children}
</a>
);
});
};

View File

@@ -5,24 +5,22 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { type ComponentPropsWithoutRef, forwardRef } from "react";
import { type ComponentProps, type FC } from "react";
import { Button } from "@vector-im/compound-web";
import type { LinkProps } from "react-router-dom";
import { useLink } from "./Link";
type Props = Omit<
ComponentPropsWithoutRef<typeof Button<"a">>,
"as" | "href"
> & { to: LinkProps["to"]; state?: unknown };
type Props = Omit<ComponentProps<typeof Button<"a">>, "as" | "href"> & {
to: LinkProps["to"];
state?: unknown;
};
/**
* A version of Compound's button component that acts as a link and integrates
* with our router setup.
*/
export const LinkButton = forwardRef<HTMLAnchorElement, Props>(
function LinkButton({ to, state, ...props }, ref) {
export const LinkButton: FC<Props> = ({ ref, to, state, ...props }) => {
const [path, onClick] = useLink(to, state);
return <Button as="a" ref={ref} {...props} href={path} onClick={onClick} />;
},
);
};

View File

@@ -6,18 +6,23 @@ Please see LICENSE in the repository root for full details.
*/
import classNames from "classnames";
import { type FormEventHandler, forwardRef, type ReactNode } from "react";
import {
type FC,
type Ref,
type FormEventHandler,
type ReactNode,
} from "react";
import styles from "./Form.module.css";
interface FormProps {
ref?: Ref<HTMLFormElement>;
className: string;
onSubmit: FormEventHandler<HTMLFormElement>;
children: ReactNode[];
}
export const Form = forwardRef<HTMLFormElement, FormProps>(
({ children, className, onSubmit }, ref) => {
export const Form: FC<FormProps> = ({ ref, children, className, onSubmit }) => {
return (
<form
onSubmit={onSubmit}
@@ -27,7 +32,6 @@ export const Form = forwardRef<HTMLFormElement, FormProps>(
{children}
</form>
);
},
);
};
Form.displayName = "Form";

View File

@@ -18,11 +18,10 @@ import {
type ComponentType,
type Dispatch,
type FC,
type LegacyRef,
type ReactNode,
type Ref,
type SetStateAction,
createContext,
forwardRef,
memo,
useContext,
useEffect,
@@ -162,7 +161,7 @@ const windowHeightObservable$ = fromEvent(window, "resize").pipe(
);
export interface LayoutProps<LayoutModel, TileModel, R extends HTMLElement> {
ref: LegacyRef<R>;
ref?: Ref<R>;
model: LayoutModel;
/**
* Component creating an invisible "slot" for a tile to go in.
@@ -171,7 +170,7 @@ export interface LayoutProps<LayoutModel, TileModel, R extends HTMLElement> {
}
export interface TileProps<Model, R extends HTMLElement> {
ref: LegacyRef<R>;
ref?: Ref<R>;
className?: string;
style?: ComponentProps<typeof animated.div>["style"];
/**
@@ -297,14 +296,13 @@ export function Grid<
// render of Grid causes a re-render of Layout, which in turn re-renders Grid
const LayoutMemo = useMemo(
() =>
memo(
forwardRef<
LayoutRef,
LayoutMemoProps<LayoutModel, TileModel, LayoutRef>
>(function LayoutMemo({ Layout, ...props }, ref): ReactNode {
memo(function LayoutMemo({
ref,
Layout,
...props
}: LayoutMemoProps<LayoutModel, TileModel, LayoutRef>): ReactNode {
return <Layout {...props} ref={ref} />;
}),
),
[],
);

View File

@@ -5,7 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { type CSSProperties, forwardRef, useCallback, useMemo } from "react";
import {
type CSSProperties,
type ReactNode,
useCallback,
useMemo,
} from "react";
import { distinctUntilChanged } from "rxjs";
import { useObservableEagerState } from "observable-hooks";
@@ -33,7 +38,7 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
// The "fixed" (non-scrolling) part of the layout is where the spotlight tile
// lives
fixed: forwardRef(function GridLayoutFixed({ model, Slot }, ref) {
fixed: function GridLayoutFixed({ ref, model, Slot }): ReactNode {
useUpdateLayout();
const alignment = useObservableEagerState(
useInitial(() =>
@@ -68,10 +73,10 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
)}
</div>
);
}),
},
// The scrolling part of the layout is where all the grid tiles live
scrolling: forwardRef(function GridLayout({ model, Slot }, ref) {
scrolling: function GridLayout({ ref, model, Slot }): ReactNode {
useUpdateLayout();
useVisibleTiles(model.setVisibleTiles);
const { width, height: minHeight } = useObservableEagerState(minBounds$);
@@ -98,5 +103,5 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
))}
</div>
);
}),
},
});

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { forwardRef, useCallback, useMemo } from "react";
import { type ReactNode, useCallback, useMemo } from "react";
import { useObservableEagerState } from "observable-hooks";
import classNames from "classnames";
@@ -24,12 +24,12 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
}) => ({
scrollingOnTop: false,
fixed: forwardRef(function OneOnOneLayoutFixed(_props, ref) {
fixed: function OneOnOneLayoutFixed({ ref }): ReactNode {
useUpdateLayout();
return <div ref={ref} />;
}),
},
scrolling: forwardRef(function OneOnOneLayoutScrolling({ model, Slot }, ref) {
scrolling: function OneOnOneLayoutScrolling({ ref, model, Slot }): ReactNode {
useUpdateLayout();
const { width, height } = useObservableEagerState(minBounds$);
const pipAlignmentValue = useObservableEagerState(pipAlignment$);
@@ -66,5 +66,5 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
</Slot>
</div>
);
}),
},
});

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { forwardRef, useCallback } from "react";
import { type ReactNode, useCallback } from "react";
import { useObservableEagerState } from "observable-hooks";
import { type SpotlightExpandedLayout as SpotlightExpandedLayoutModel } from "../state/CallViewModel";
@@ -22,10 +22,11 @@ export const makeSpotlightExpandedLayout: CallLayout<
> = ({ pipAlignment$ }) => ({
scrollingOnTop: true,
fixed: forwardRef(function SpotlightExpandedLayoutFixed(
{ model, Slot },
fixed: function SpotlightExpandedLayoutFixed({
ref,
) {
model,
Slot,
}): ReactNode {
useUpdateLayout();
return (
@@ -37,12 +38,13 @@ export const makeSpotlightExpandedLayout: CallLayout<
/>
</div>
);
}),
},
scrolling: forwardRef(function SpotlightExpandedLayoutScrolling(
{ model, Slot },
scrolling: function SpotlightExpandedLayoutScrolling({
ref,
) {
model,
Slot,
}): ReactNode {
useUpdateLayout();
const pipAlignmentValue = useObservableEagerState(pipAlignment$);
@@ -69,5 +71,5 @@ export const makeSpotlightExpandedLayout: CallLayout<
)}
</div>
);
}),
},
});

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { forwardRef } from "react";
import { type ReactNode } from "react";
import { useObservableEagerState } from "observable-hooks";
import classNames from "classnames";
@@ -24,10 +24,11 @@ export const makeSpotlightLandscapeLayout: CallLayout<
> = ({ minBounds$ }) => ({
scrollingOnTop: false,
fixed: forwardRef(function SpotlightLandscapeLayoutFixed(
{ model, Slot },
fixed: function SpotlightLandscapeLayoutFixed({
ref,
) {
model,
Slot,
}): ReactNode {
useUpdateLayout();
useObservableEagerState(minBounds$);
@@ -43,12 +44,13 @@ export const makeSpotlightLandscapeLayout: CallLayout<
<div className={styles.grid} />
</div>
);
}),
},
scrolling: forwardRef(function SpotlightLandscapeLayoutScrolling(
{ model, Slot },
scrolling: function SpotlightLandscapeLayoutScrolling({
ref,
) {
model,
Slot,
}): ReactNode {
useUpdateLayout();
useVisibleTiles(model.setVisibleTiles);
useObservableEagerState(minBounds$);
@@ -69,5 +71,5 @@ export const makeSpotlightLandscapeLayout: CallLayout<
</div>
</div>
);
}),
},
});

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { type CSSProperties, forwardRef } from "react";
import { type ReactNode, type CSSProperties } from "react";
import { useObservableEagerState } from "observable-hooks";
import classNames from "classnames";
@@ -30,10 +30,11 @@ export const makeSpotlightPortraitLayout: CallLayout<
> = ({ minBounds$ }) => ({
scrollingOnTop: false,
fixed: forwardRef(function SpotlightPortraitLayoutFixed(
{ model, Slot },
fixed: function SpotlightPortraitLayoutFixed({
ref,
) {
model,
Slot,
}): ReactNode {
useUpdateLayout();
return (
@@ -47,12 +48,13 @@ export const makeSpotlightPortraitLayout: CallLayout<
</div>
</div>
);
}),
},
scrolling: forwardRef(function SpotlightPortraitLayoutScrolling(
{ model, Slot },
scrolling: function SpotlightPortraitLayoutScrolling({
ref,
) {
model,
Slot,
}): ReactNode {
useUpdateLayout();
useVisibleTiles(model.setVisibleTiles);
const { width } = useObservableEagerState(minBounds$);
@@ -90,5 +92,5 @@ export const makeSpotlightPortraitLayout: CallLayout<
</div>
</div>
);
}),
},
});

View File

@@ -9,10 +9,10 @@ import {
type ChangeEvent,
type FC,
type ForwardedRef,
forwardRef,
type ReactNode,
useId,
type JSX,
type Ref,
} from "react";
import classNames from "classnames";
@@ -54,6 +54,7 @@ function Field({ children, className }: FieldProps): JSX.Element {
}
interface InputFieldProps {
ref?: Ref<HTMLInputElement | HTMLTextAreaElement>;
label?: string;
type: string;
prefix?: string;
@@ -78,12 +79,8 @@ interface InputFieldProps {
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
}
export const InputField = forwardRef<
HTMLInputElement | HTMLTextAreaElement,
InputFieldProps
>(
(
{
export const InputField: FC<InputFieldProps> = ({
ref,
id,
label,
className,
@@ -95,9 +92,7 @@ export const InputField = forwardRef<
disabled,
min,
...rest
},
ref,
) => {
}) => {
const descriptionId = useId();
return (
@@ -158,8 +153,7 @@ export const InputField = forwardRef<
)}
</Field>
);
},
);
};
InputField.displayName = "InputField";

View File

@@ -12,15 +12,14 @@ import { type MatrixClient } from "matrix-js-sdk";
import {
type FC,
type PointerEvent,
type PropsWithoutRef,
type TouchEvent,
forwardRef,
useCallback,
useEffect,
useMemo,
useRef,
useState,
type JSX,
type ReactNode,
} from "react";
import useMeasure from "react-use-measure";
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
@@ -437,13 +436,14 @@ export const InCallView: FC<InCallViewProps> = ({
const Tile = useMemo(
() =>
forwardRef<
HTMLDivElement,
PropsWithoutRef<TileProps<TileViewModel, HTMLDivElement>>
>(function Tile(
{ className, style, targetWidth, targetHeight, model },
function Tile({
ref,
) {
className,
style,
targetWidth,
targetHeight,
model,
}: TileProps<TileViewModel, HTMLDivElement>): ReactNode {
const spotlightExpanded = useObservableEagerState(
vm.spotlightExpanded$,
);
@@ -481,7 +481,7 @@ export const InCallView: FC<InCallViewProps> = ({
style={style}
/>
);
}),
},
[vm, openProfile],
);

View File

@@ -7,8 +7,9 @@ Please see LICENSE in the repository root for full details.
import {
type ComponentProps,
type FC,
type ReactNode,
forwardRef,
type Ref,
useCallback,
useRef,
useState,
@@ -50,6 +51,7 @@ import { useMergedRefs } from "../useMergedRefs";
import { useReactionsSender } from "../reactions/useReactionsSender";
interface TileProps {
ref?: Ref<HTMLDivElement>;
className?: string;
style?: ComponentProps<typeof animated.div>["style"];
targetWidth: number;
@@ -66,9 +68,8 @@ interface UserMediaTileProps extends TileProps {
menuEnd?: ReactNode;
}
const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
(
{
const UserMediaTile: FC<UserMediaTileProps> = ({
ref,
vm,
showSpeakingIndicators,
locallyMuted,
@@ -77,9 +78,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
className,
displayName,
...props
},
ref,
) => {
}) => {
const { toggleRaisedHand } = useReactionsSender();
const { t } = useTranslation();
const video = useObservableEagerState(vm.video$);
@@ -190,8 +189,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
{menu}
</ContextMenu>
);
},
);
};
UserMediaTile.displayName = "UserMediaTile";
@@ -200,8 +198,12 @@ interface LocalUserMediaTileProps extends TileProps {
onOpenProfile: (() => void) | null;
}
const LocalUserMediaTile = forwardRef<HTMLDivElement, LocalUserMediaTileProps>(
({ vm, onOpenProfile, ...props }, ref) => {
const LocalUserMediaTile: FC<LocalUserMediaTileProps> = ({
ref,
vm,
onOpenProfile,
...props
}) => {
const { t } = useTranslation();
const mirror = useObservableEagerState(vm.mirror$);
const alwaysShow = useObservableEagerState(vm.alwaysShow$);
@@ -240,8 +242,7 @@ const LocalUserMediaTile = forwardRef<HTMLDivElement, LocalUserMediaTileProps>(
{...props}
/>
);
},
);
};
LocalUserMediaTile.displayName = "LocalUserMediaTile";
@@ -249,10 +250,11 @@ interface RemoteUserMediaTileProps extends TileProps {
vm: RemoteUserMediaViewModel;
}
const RemoteUserMediaTile = forwardRef<
HTMLDivElement,
RemoteUserMediaTileProps
>(({ vm, ...props }, ref) => {
const RemoteUserMediaTile: FC<RemoteUserMediaTileProps> = ({
ref,
vm,
...props
}) => {
const { t } = useTranslation();
const locallyMuted = useObservableEagerState(vm.locallyMuted$);
const localVolume = useObservableEagerState(vm.localVolume$);
@@ -303,11 +305,12 @@ const RemoteUserMediaTile = forwardRef<
{...props}
/>
);
});
};
RemoteUserMediaTile.displayName = "RemoteUserMediaTile";
interface GridTileProps {
ref?: Ref<HTMLDivElement>;
vm: GridTileViewModel;
onOpenProfile: (() => void) | null;
targetWidth: number;
@@ -317,8 +320,12 @@ interface GridTileProps {
showSpeakingIndicators: boolean;
}
export const GridTile = forwardRef<HTMLDivElement, GridTileProps>(
({ vm, onOpenProfile, ...props }, theirRef) => {
export const GridTile: FC<GridTileProps> = ({
ref: theirRef,
vm,
onOpenProfile,
...props
}) => {
const ourRef = useRef<HTMLDivElement | null>(null);
const ref = useMergedRefs(ourRef, theirRef);
const media = useObservableEagerState(vm.media$);
@@ -344,7 +351,6 @@ export const GridTile = forwardRef<HTMLDivElement, GridTileProps>(
/>
);
}
},
);
};
GridTile.displayName = "GridTile";

View File

@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
import { type TrackReferenceOrPlaceholder } from "@livekit/components-core";
import { animated } from "@react-spring/web";
import { type RoomMember } from "matrix-js-sdk";
import { type ComponentProps, type ReactNode, forwardRef } from "react";
import { type FC, type ComponentProps, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import classNames from "classnames";
import { VideoTrack } from "@livekit/components-react";
@@ -47,9 +47,8 @@ interface Props extends ComponentProps<typeof animated.div> {
videoStreamStats?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats;
}
export const MediaView = forwardRef<HTMLDivElement, Props>(
(
{
export const MediaView: FC<Props> = ({
ref,
className,
style,
targetWidth,
@@ -71,9 +70,7 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
audioStreamStats,
videoStreamStats,
...props
},
ref,
) => {
}) => {
const { t } = useTranslation();
const [handRaiseTimerVisible] = useSetting(showHandRaisedTimer);
@@ -182,7 +179,6 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
</div>
</animated.div>
);
},
);
};
MediaView.displayName = "MediaView";

View File

@@ -7,8 +7,9 @@ Please see LICENSE in the repository root for full details.
import {
type ComponentProps,
type FC,
type Ref,
type RefAttributes,
forwardRef,
useCallback,
useEffect,
useRef,
@@ -44,6 +45,7 @@ import { useLatest } from "../useLatest";
import { type SpotlightTileViewModel } from "../state/TileViewModel";
interface SpotlightItemBaseProps {
ref?: Ref<HTMLDivElement>;
className?: string;
"data-id": string;
targetWidth: number;
@@ -67,13 +69,13 @@ interface SpotlightLocalUserMediaItemProps
vm: LocalUserMediaViewModel;
}
const SpotlightLocalUserMediaItem = forwardRef<
HTMLDivElement,
SpotlightLocalUserMediaItemProps
>(({ vm, ...props }, ref) => {
const SpotlightLocalUserMediaItem: FC<SpotlightLocalUserMediaItemProps> = ({
vm,
...props
}) => {
const mirror = useObservableEagerState(vm.mirror$);
return <MediaView ref={ref} mirror={mirror} {...props} />;
});
return <MediaView mirror={mirror} {...props} />;
};
SpotlightLocalUserMediaItem.displayName = "SpotlightLocalUserMediaItem";
@@ -81,16 +83,15 @@ interface SpotlightUserMediaItemProps extends SpotlightItemBaseProps {
vm: UserMediaViewModel;
}
const SpotlightUserMediaItem = forwardRef<
HTMLDivElement,
SpotlightUserMediaItemProps
>(({ vm, ...props }, ref) => {
const SpotlightUserMediaItem: FC<SpotlightUserMediaItemProps> = ({
vm,
...props
}) => {
const videoEnabled = useObservableEagerState(vm.videoEnabled$);
const cropVideo = useObservableEagerState(vm.cropVideo$);
const baseProps: SpotlightUserMediaItemBaseProps &
RefAttributes<HTMLDivElement> = {
ref,
videoEnabled,
videoFit: cropVideo ? "cover" : "contain",
...props,
@@ -101,11 +102,12 @@ const SpotlightUserMediaItem = forwardRef<
) : (
<MediaView mirror={false} {...baseProps} />
);
});
};
SpotlightUserMediaItem.displayName = "SpotlightUserMediaItem";
interface SpotlightItemProps {
ref?: Ref<HTMLDivElement>;
vm: MediaViewModel;
targetWidth: number;
targetHeight: number;
@@ -117,18 +119,15 @@ interface SpotlightItemProps {
"aria-hidden"?: boolean;
}
const SpotlightItem = forwardRef<HTMLDivElement, SpotlightItemProps>(
(
{
const SpotlightItem: FC<SpotlightItemProps> = ({
ref: theirRef,
vm,
targetWidth,
targetHeight,
intersectionObserver$,
snap,
"aria-hidden": ariaHidden,
},
theirRef,
) => {
}) => {
const ourRef = useRef<HTMLDivElement | null>(null);
const ref = useMergedRefs(ourRef, theirRef);
const displayName = useObservableEagerState(vm.displayname$);
@@ -167,21 +166,16 @@ const SpotlightItem = forwardRef<HTMLDivElement, SpotlightItemProps>(
};
return vm instanceof ScreenShareViewModel ? (
<MediaView
videoEnabled
videoFit="contain"
mirror={false}
{...baseProps}
/>
<MediaView videoEnabled videoFit="contain" mirror={false} {...baseProps} />
) : (
<SpotlightUserMediaItem vm={vm} {...baseProps} />
);
},
);
};
SpotlightItem.displayName = "SpotlightItem";
interface Props {
ref?: Ref<HTMLDivElement>;
vm: SpotlightTileViewModel;
expanded: boolean;
onToggleExpanded: (() => void) | null;
@@ -192,9 +186,8 @@ interface Props {
style?: ComponentProps<typeof animated.div>["style"];
}
export const SpotlightTile = forwardRef<HTMLDivElement, Props>(
(
{
export const SpotlightTile: FC<Props> = ({
ref: theirRef,
vm,
expanded,
onToggleExpanded,
@@ -203,17 +196,13 @@ export const SpotlightTile = forwardRef<HTMLDivElement, Props>(
showIndicators,
className,
style,
},
theirRef,
) => {
}) => {
const { t } = useTranslation();
const [ourRef, root$] = useObservableRef<HTMLDivElement | null>(null);
const ref = useMergedRefs(ourRef, theirRef);
const maximised = useObservableEagerState(vm.maximised$);
const media = useObservableEagerState(vm.media$);
const [visibleId, setVisibleId] = useState<string | undefined>(
media[0]?.id,
);
const [visibleId, setVisibleId] = useState<string | undefined>(media[0]?.id);
const latestMedia = useLatest(media);
const latestVisibleId = useLatest(visibleId);
const visibleIndex = media.findIndex((vm) => vm.id === visibleId);
@@ -243,9 +232,7 @@ export const SpotlightTile = forwardRef<HTMLDivElement, Props>(
const [scrollToId, setScrollToId] = useReactiveState<string | null>(
(prev) =>
prev == null ||
prev === visibleId ||
media.every((vm) => vm.id !== prev)
prev == null || prev === visibleId || media.every((vm) => vm.id !== prev)
? null
: prev,
[visibleId],
@@ -341,7 +328,6 @@ export const SpotlightTile = forwardRef<HTMLDivElement, Props>(
)}
</animated.div>
);
},
);
};
SpotlightTile.displayName = "SpotlightTile";

View File

@@ -5,21 +5,21 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { type MutableRefObject, type RefCallback, useCallback } from "react";
import { type RefCallback, type RefObject, useCallback } from "react";
/**
* Combines multiple refs into one, useful for attaching multiple refs to the
* same DOM node.
*/
export const useMergedRefs = <T>(
...refs: (MutableRefObject<T | null> | RefCallback<T | null> | null)[]
...refs: (RefObject<T | null> | RefCallback<T | null> | null | undefined)[]
): RefCallback<T | null> =>
useCallback(
(value) =>
refs.forEach((ref) => {
if (typeof ref === "function") {
ref(value);
} else if (ref !== null) {
} else if (ref != null) {
ref.current = value;
}
}),