diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 3b7a137..b861fd2 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -1,8 +1,8 @@ { "{{count}} stars|one": "{{count}} star", "{{count}} stars|other": "{{count}} stars", + "{{displayName}} is presenting": "{{displayName}} is presenting", "{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.", - "{{name}} is presenting": "{{name}} is presenting", "{{names}}, {{name}}": "{{names}}, {{name}}", "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.", "<0>Already have an account?<1><0>Log in Or <2>Access as a guest": "<0>Already have an account?<1><0>Log in Or <2>Access as a guest", @@ -102,7 +102,6 @@ "Turn off camera": "Turn off camera", "Turn on camera": "Turn on camera", "Unmute microphone": "Unmute microphone", - "Use the upcoming grid system": "Use the upcoming grid system", "User menu": "User menu", "Username": "Username", "Version: {{version}}": "Version: {{version}}", diff --git a/src/room/InCallView.module.css b/src/room/InCallView.module.css index 0e510a7..0ed59e6 100644 --- a/src/room/InCallView.module.css +++ b/src/room/InCallView.module.css @@ -82,19 +82,6 @@ limitations under the License. bottom: 0; } -.avatar { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - /* To make avatars scale smoothly with their tiles during animations, we - override the styles set on the element */ - --avatarSize: calc(min(var(--tileWidth), var(--tileHeight)) / 2); - width: var(--avatarSize) !important; - height: var(--avatarSize) !important; - border-radius: 10000px !important; -} - @media (min-height: 300px) { .inRoom { --footerPadding: 24px; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 4d7093f..2ec28e6 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -28,7 +28,7 @@ import { Room, Track } from "livekit-client"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; -import React, { useCallback, useEffect, useMemo, useRef } from "react"; +import React, { Ref, useCallback, useEffect, useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; import useMeasure from "react-use-measure"; import { OverlayTriggerState } from "@react-stately/overlays"; @@ -55,29 +55,29 @@ import { useVideoGridLayout, TileDescriptor, } from "../video-grid/VideoGrid"; -import { useNewGrid, useShowInspector } from "../settings/useSetting"; +import { useShowInspector } from "../settings/useSetting"; import { useModalTriggerState } from "../Modal"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { useUrlParams } from "../UrlParams"; import { MediaDevicesState } from "../settings/mediaDevices"; -import { useRageshakeRequestModal } from "../settings/submit-rageshake"; import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; import { usePrefersReducedMotion } from "../usePrefersReducedMotion"; -import { ItemData, VideoTileContainer } from "../video-grid/VideoTileContainer"; import { ElementWidgetActions, widget } from "../widget"; import { GridLayoutMenu } from "./GridLayoutMenu"; import { GroupCallInspector } from "./GroupCallInspector"; import styles from "./InCallView.module.css"; -import { RageshakeRequestModal } from "./RageshakeRequestModal"; import { MatrixInfo } from "./VideoPreview"; import { useJoinRule } from "./useJoinRule"; import { ParticipantInfo } from "./useGroupCall"; -import { TileContent } from "../video-grid/VideoTile"; +import { ItemData, TileContent } from "../video-grid/VideoTile"; import { Config } from "../config/Config"; import { NewVideoGrid } from "../video-grid/NewVideoGrid"; import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; import { SettingsModal } from "../settings/SettingsModal"; import { InviteModal } from "./InviteModal"; +import { useRageshakeRequestModal } from "../settings/submit-rageshake"; +import { RageshakeRequestModal } from "./RageshakeRequestModal"; +import { VideoTile } from "../video-grid/VideoTile"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); // There is currently a bug in Safari our our code with cloning and sending MediaStreams @@ -258,8 +258,9 @@ export function InCallView({ [noControls, items] ); - const [newGrid] = useNewGrid(); - const Grid = newGrid ? NewVideoGrid : VideoGrid; + const Grid = + items.length > 12 && layout === "freedom" ? NewVideoGrid : VideoGrid; + const prefersReducedMotion = usePrefersReducedMotion(); const renderContent = (): JSX.Element => { @@ -272,12 +273,11 @@ export function InCallView({ } if (maximisedParticipant) { return ( - ); } @@ -288,7 +288,9 @@ export function InCallView({ layout={layout} disableAnimations={prefersReducedMotion || isSafari} > - {(child) => } + {(props) => ( + } /> + )} ); }; @@ -462,7 +464,6 @@ function useParticipantTiles( focused: false, local: sfuParticipant.isLocal, data: { - id, member, sfuParticipant, content: TileContent.UserMedia, diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 2ac546f..4dd9170 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -33,7 +33,6 @@ import { MediaDevicesState } from "./mediaDevices"; import { useShowInspector, useOptInAnalytics, - useNewGrid, useDeveloperSettingsTab, } from "./useSetting"; import { FieldRow, InputField } from "../input/Input"; @@ -60,7 +59,6 @@ export const SettingsModal = (props: Props) => { const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics(); const [developerSettingsTab, setDeveloperSettingsTab] = useDeveloperSettingsTab(); - const [newGrid, setNewGrid] = useNewGrid(); const downloadDebugLog = useDownloadDebugLog(); @@ -235,17 +233,6 @@ export const SettingsModal = (props: Props) => { } /> - - ) => - setNewGrid(e.target.checked) - } - /> - - {participantCount < 12 && ( - - )} - {participantCount > 0 && ( - - )} - - - ); -}; - -ParticipantsTest.args = { - layout: "freedom", - participantCount: 1, -}; diff --git a/src/video-grid/VideoGrid.tsx b/src/video-grid/VideoGrid.tsx index 15f4180..63c0c62 100644 --- a/src/video-grid/VideoGrid.tsx +++ b/src/video-grid/VideoGrid.tsx @@ -1,5 +1,5 @@ /* -Copyright 2022 New Vector Ltd +Copyright 2022-2023 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,8 +15,10 @@ limitations under the License. */ import React, { + ComponentProps, Key, - RefObject, + ReactNode, + Ref, useCallback, useEffect, useRef, @@ -26,21 +28,20 @@ import { EventTypes, FullGestureState, Handler, - useDrag, useGesture, } from "@use-gesture/react"; import { + animated, SpringRef, - SpringValue, SpringValues, useSprings, } from "@react-spring/web"; import useMeasure from "react-use-measure"; import { ResizeObserver as JuggleResizeObserver } from "@juggle/resize-observer"; -import { ReactDOMAttributes } from "@use-gesture/react/dist/declarations/src/types"; import styles from "./VideoGrid.module.css"; import { Layout } from "../room/GridLayoutMenu"; +import { TileWrapper } from "./TileWrapper"; interface TilePosition { x: number; @@ -51,7 +52,7 @@ interface TilePosition { } interface Tile { - key: Key; + key: string; order: number; item: TileDescriptor; remove: boolean; @@ -727,25 +728,18 @@ interface DragTileData { y: number; } -export interface ChildrenProperties extends ReactDOMAttributes { - key: Key; - data: T; +export interface ChildrenProperties { + ref: Ref; + style: ComponentProps["style"]; + /** + * The width this tile will have once its animations have settled. + */ targetWidth: number; + /** + * The height this tile will have once its animations have settled. + */ targetHeight: number; - opacity: SpringValue; - scale: SpringValue; - shadow: SpringValue; - zIndex: SpringValue; - x: SpringValue; - y: SpringValue; - width: SpringValue; - height: SpringValue; - onDragRef?: RefObject< - ( - tileId: string, - state: Parameters>[0] - ) => void - >; + data: T; } export interface VideoGridProps { @@ -768,7 +762,7 @@ export function VideoGrid({ items, layout, disableAnimations, - children: createChild, + children, }: VideoGridProps) { // Place the PiP in the bottom right corner by default const [pipXRatio, setPipXRatio] = useState(1); @@ -1082,117 +1076,132 @@ export function VideoGrid({ [tiles, layout, gridBounds.width, gridBounds.height, pipXRatio, pipYRatio] ); - const bindTile = useDrag( - ({ args: [key], active, xy, movement, tap, last, event }) => { - event.preventDefault(); + // Callback for useDrag. We could call useDrag here, but the default + // pattern of spreading {...bind()} across the children to bind the gesture + // ends up breaking memoization and ruining this component's performance. + // Instead, we pass this callback to each tile via a ref, to let them bind the + // gesture using the much more sensible ref-based method. + const onTileDrag = ( + tileId: string, + { + active, + xy, + movement, + tap, + last, + event, + }: Parameters>[0] + ) => { + event.preventDefault(); - if (tap) { - onTap(key); - return; - } + if (tap) { + onTap(tileId); + return; + } - if (layout !== "freedom") return; + if (layout !== "freedom") return; - const dragTileIndex = tiles.findIndex((tile) => tile.key === key); - const dragTile = tiles[dragTileIndex]; - const dragTilePosition = tilePositions[dragTile.order]; + const dragTileIndex = tiles.findIndex((tile) => tile.key === tileId); + const dragTile = tiles[dragTileIndex]; + const dragTilePosition = tilePositions[dragTile.order]; - const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top]; + const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top]; - let newTiles = tiles; + let newTiles = tiles; - if (tiles.length === 2 && !tiles.some((t) => t.focused)) { - // We're in 1:1 mode, so only the local tile should be draggable - if (!dragTile.item.local) return; + if (tiles.length === 2 && !tiles.some((t) => t.focused)) { + // We're in 1:1 mode, so only the local tile should be draggable + if (!dragTile.item.local) return; - // Position should only update on the very last event, to avoid - // compounding the offset on every drag event - if (last) { - const remotePosition = tilePositions[1]; + // Position should only update on the very last event, to avoid + // compounding the offset on every drag event + if (last) { + const remotePosition = tilePositions[1]; - const pipGap = getPipGap( - gridBounds.width / gridBounds.height, - gridBounds.width - ); - const pipMinX = remotePosition.x + pipGap; - const pipMinY = remotePosition.y + pipGap; - const pipMaxX = - remotePosition.x + - remotePosition.width - - dragTilePosition.width - - pipGap; - const pipMaxY = - remotePosition.y + - remotePosition.height - - dragTilePosition.height - - pipGap; - - const newPipXRatio = - (dragTilePosition.x + movement[0] - pipMinX) / (pipMaxX - pipMinX); - const newPipYRatio = - (dragTilePosition.y + movement[1] - pipMinY) / (pipMaxY - pipMinY); - - setPipXRatio(Math.max(0, Math.min(1, newPipXRatio))); - setPipYRatio(Math.max(0, Math.min(1, newPipYRatio))); - } - } else { - const hoverTile = tiles.find( - (tile) => - tile.key !== key && - isInside(cursorPosition, tilePositions[tile.order]) + const pipGap = getPipGap( + gridBounds.width / gridBounds.height, + gridBounds.width ); + const pipMinX = remotePosition.x + pipGap; + const pipMinY = remotePosition.y + pipGap; + const pipMaxX = + remotePosition.x + + remotePosition.width - + dragTilePosition.width - + pipGap; + const pipMaxY = + remotePosition.y + + remotePosition.height - + dragTilePosition.height - + pipGap; - if (hoverTile) { - // Shift the tiles into their new order - newTiles = newTiles.map((tile) => { - let order = tile.order; - if (order < dragTile.order) { - if (order >= hoverTile.order) order++; - } else if (order > dragTile.order) { - if (order <= hoverTile.order) order--; - } else { - order = hoverTile.order; - } + const newPipXRatio = + (dragTilePosition.x + movement[0] - pipMinX) / (pipMaxX - pipMinX); + const newPipYRatio = + (dragTilePosition.y + movement[1] - pipMinY) / (pipMaxY - pipMinY); - let focused; - if (tile === hoverTile) { - focused = dragTile.focused; - } else if (tile === dragTile) { - focused = hoverTile.focused; - } else { - focused = tile.focused; - } - - return { ...tile, order, focused }; - }); - - reorderTiles(newTiles, layout); - - setTileState((state) => ({ ...state, tiles: newTiles })); - } + setPipXRatio(Math.max(0, Math.min(1, newPipXRatio))); + setPipYRatio(Math.max(0, Math.min(1, newPipYRatio))); } + } else { + const hoverTile = tiles.find( + (tile) => + tile.key !== tileId && + isInside(cursorPosition, tilePositions[tile.order]) + ); - if (active) { - if (!draggingTileRef.current) { - draggingTileRef.current = { - key: dragTile.key, - offsetX: dragTilePosition.x, - offsetY: dragTilePosition.y, - x: movement[0], - y: movement[1], - }; - } else { - draggingTileRef.current.x = movement[0]; - draggingTileRef.current.y = movement[1]; - } + if (hoverTile) { + // Shift the tiles into their new order + newTiles = newTiles.map((tile) => { + let order = tile.order; + if (order < dragTile.order) { + if (order >= hoverTile.order) order++; + } else if (order > dragTile.order) { + if (order <= hoverTile.order) order--; + } else { + order = hoverTile.order; + } + + let focused; + if (tile === hoverTile) { + focused = dragTile.focused; + } else if (tile === dragTile) { + focused = hoverTile.focused; + } else { + focused = tile.focused; + } + + return { ...tile, order, focused }; + }); + + reorderTiles(newTiles, layout); + + setTileState((state) => ({ ...state, tiles: newTiles })); + } + } + + if (active) { + if (!draggingTileRef.current) { + draggingTileRef.current = { + key: dragTile.key, + offsetX: dragTilePosition.x, + offsetY: dragTilePosition.y, + x: movement[0], + y: movement[1], + }; } else { - draggingTileRef.current = null; + draggingTileRef.current.x = movement[0]; + draggingTileRef.current.y = movement[1]; } + } else { + draggingTileRef.current = null; + } - api.start(animate(newTiles)); - }, - { filterTaps: true, pointer: { buttons: [1] } } - ); + api.start(animate(newTiles)); + }; + + const onTileDragRef = useRef(onTileDrag); + onTileDragRef.current = onTileDrag; const onGridGesture = useCallback( ( @@ -1239,18 +1248,23 @@ export function VideoGrid({ return (
- {springs.map((style, i) => { + {springs.map((spring, i) => { const tile = tiles[i]; const tilePosition = tilePositions[tile.order]; - return createChild({ - ...bindTile(tile.key), - ...style, - key: tile.key, - data: tile.item.data, - targetWidth: tilePosition.width, - targetHeight: tilePosition.height, - }); + return ( + + {children as (props: ChildrenProperties) => ReactNode} + + ); })}
); diff --git a/src/video-grid/VideoTile.module.css b/src/video-grid/VideoTile.module.css index adcc57a..20849a9 100644 --- a/src/video-grid/VideoTile.module.css +++ b/src/video-grid/VideoTile.module.css @@ -18,12 +18,10 @@ limitations under the License. position: absolute; contain: strict; top: 0; - width: var(--tileWidth); - height: var(--tileHeight); + container-name: videoTile; + container-type: size; --tileRadius: 8px; border-radius: var(--tileRadius); - box-shadow: rgba(0, 0, 0, 0.5) 0px var(--tileShadow) - calc(2 * var(--tileShadow)) var(--tileShadowSpread); overflow: hidden; cursor: pointer; @@ -195,3 +193,20 @@ limitations under the License. --tileRadius: 20px; } } + +/* CSS makes us put a condition here, even though all we want to do is +unconditionally select the container so we can use cqmin units */ +@container videoTile (width > 0) { + .avatar { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + /* To make avatars scale smoothly with their tiles during animations, we + override the styles set on the element */ + --avatarSize: 50cqmin; /* Half of the smallest dimension of the tile */ + width: var(--avatarSize) !important; + height: var(--avatarSize) !important; + border-radius: 10000px !important; + } +} diff --git a/src/video-grid/VideoTile.tsx b/src/video-grid/VideoTile.tsx index a9ec49a..703d3da 100644 --- a/src/video-grid/VideoTile.tsx +++ b/src/video-grid/VideoTile.tsx @@ -1,5 +1,5 @@ /* -Copyright 2022 New Vector Ltd +Copyright 2022-2023 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ForwardedRef, forwardRef } from "react"; -import { animated, SpringValue } from "@react-spring/web"; +import React from "react"; +import { animated } from "@react-spring/web"; import classNames from "classnames"; import { useTranslation } from "react-i18next"; import { LocalParticipant, RemoteParticipant, Track } from "livekit-client"; @@ -24,59 +24,60 @@ import { VideoTrack, useMediaTrack, } from "@livekit/components-react"; +import { + RoomMember, + RoomMemberEvent, +} from "matrix-js-sdk/src/models/room-member"; +import { Avatar } from "../Avatar"; import styles from "./VideoTile.module.css"; import { ReactComponent as MicIcon } from "../icons/Mic.svg"; import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg"; +export interface ItemData { + member?: RoomMember; + sfuParticipant: LocalParticipant | RemoteParticipant; + content: TileContent; +} + export enum TileContent { UserMedia = "user-media", ScreenShare = "screen-share", } interface Props { - name: string; - sfuParticipant: LocalParticipant | RemoteParticipant; - content: TileContent; + data: ItemData; - // TODO: Refactor this set of props. - // See https://github.com/vector-im/element-call/pull/1099#discussion_r1226863404 - avatar?: JSX.Element; + // TODO: Refactor these props. + targetWidth: number; + targetHeight: number; className?: string; - opacity?: SpringValue; - scale?: SpringValue; - shadow?: SpringValue; - shadowSpread?: SpringValue; - zIndex?: SpringValue; - x?: SpringValue; - y?: SpringValue; - width?: SpringValue; - height?: SpringValue; + style?: React.ComponentProps["style"]; } -export const VideoTile = forwardRef( - ( - { - name, - sfuParticipant, - content, - avatar, - className, - opacity, - scale, - shadow, - shadowSpread, - zIndex, - x, - y, - width, - height, - ...rest - }, - ref - ) => { +export const VideoTile = React.forwardRef( + ({ data, className, style, targetWidth, targetHeight }, tileRef) => { const { t } = useTranslation(); + const { content, sfuParticipant, member } = data; + + // Handle display name changes. + const [displayName, setDisplayName] = React.useState("[👻]"); + React.useEffect(() => { + if (member) { + setDisplayName(member.rawDisplayName); + + const updateName = () => { + setDisplayName(member.rawDisplayName); + }; + + member!.on(RoomMemberEvent.Name, updateName); + return () => { + member!.removeListener(RoomMemberEvent.Name, updateName); + }; + } + }, [member]); + const audioEl = React.useRef(null); const { isMuted: microphoneMuted } = useMediaTrack( content === TileContent.UserMedia @@ -88,6 +89,9 @@ export const VideoTile = forwardRef( } ); + // Firefox doesn't respect the disablePictureInPicture attribute + // https://bugzilla.mozilla.org/show_bug.cgi?id=1611831 + return ( ( [styles.muted]: microphoneMuted, [styles.screenshare]: content === TileContent.ScreenShare, })} - style={{ - opacity, - scale, - zIndex, - x, - y, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore React does in fact support assigning custom properties, - // but React's types say no - "--tileWidth": width?.to((w) => `${w}px`), - "--tileHeight": height?.to((h) => `${h}px`), - "--tileShadow": shadow?.to((s) => `${s}px`), - "--tileShadowSpread": shadowSpread?.to((s) => `${s}px`), - }} - ref={ref as ForwardedRef} + style={style} + ref={tileRef} data-testid="videoTile" - {...rest} > - {!sfuParticipant.isCameraEnabled && ( + {content === TileContent.UserMedia && !sfuParticipant.isCameraEnabled && ( <>
- {avatar} + )} - {sfuParticipant.isScreenShareEnabled ? ( + {content == TileContent.ScreenShare ? (
- {t("{{name}} is presenting", { name })} + {t("{{displayName}} is presenting", { displayName })}
) : (
{microphoneMuted ? : } - {name} + {displayName}
)} diff --git a/src/video-grid/VideoTileContainer.tsx b/src/video-grid/VideoTileContainer.tsx deleted file mode 100644 index 215535f..0000000 --- a/src/video-grid/VideoTileContainer.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/* -Copyright 2022 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import { - RoomMember, - RoomMemberEvent, -} from "matrix-js-sdk/src/models/room-member"; -import { LocalParticipant, RemoteParticipant } from "livekit-client"; -import { SpringValue } from "@react-spring/web"; -import { EventTypes, Handler, useDrag } from "@use-gesture/react"; - -import { TileContent, VideoTile } from "./VideoTile"; -import { Avatar } from "../Avatar"; -import Styles from "../room/InCallView.module.css"; - -export interface ItemData { - member?: RoomMember; - sfuParticipant: LocalParticipant | RemoteParticipant; - content: TileContent; -} - -interface Props { - item: ItemData; - - // TODO: Refactor this set of props. - // See https://github.com/vector-im/element-call/pull/1099#discussion_r1226863404 - id: string; - targetWidth: number; - targetHeight: number; - opacity?: SpringValue; - scale?: SpringValue; - shadow?: SpringValue; - shadowSpread?: SpringValue; - zIndex?: SpringValue; - x?: SpringValue; - y?: SpringValue; - width?: SpringValue; - height?: SpringValue; - onDragRef?: React.RefObject< - ( - tileId: string, - state: Parameters>[0] - ) => void - >; -} - -export const VideoTileContainer: React.FC = React.memo( - ({ item, id, targetWidth, targetHeight, onDragRef, ...rest }) => { - // Handle display name changes. - const [displayName, setDisplayName] = React.useState("[👻]"); - React.useEffect(() => { - const member = item.member; - - if (member) { - setDisplayName(member.rawDisplayName); - - const updateName = () => { - setDisplayName(member.rawDisplayName); - }; - - member!.on(RoomMemberEvent.Name, updateName); - return () => { - member!.removeListener(RoomMemberEvent.Name, updateName); - }; - } - }, [item.member]); - - // Create an avatar. - const avatar = ( - - ); - - // Make sure that the tile is draggable and work well within video grid layout. - // - // Firefox doesn't respect the disablePictureInPicture attribute - // https://bugzilla.mozilla.org/show_bug.cgi?id=1611831 - const tileRef = React.useRef(null); - useDrag((state) => onDragRef?.current!(id, state), { - target: tileRef, - filterTaps: true, - preventScroll: true, - }); - - return ( - - ); - } -);