use availableWidth instead of useContainerWidth for grid layout

The useContainerWidth hook from react-grid-layout v2 returns raw
container width without accounting for scrollbar width, causing the
grid to not fill the full available space. Use the existing
availableWidth value from useResizeObserver which already compensates
for scrollbar width, matching the working implementation.
This commit is contained in:
Josh Hawkins 2026-03-04 20:15:18 -06:00
parent 1c7f1095d8
commit b48647b967

View File

@ -16,8 +16,7 @@ import React, {
import { import {
Layout, Layout,
LayoutItem, LayoutItem,
Responsive, ResponsiveGridLayout as Responsive,
useContainerWidth,
} from "react-grid-layout"; } from "react-grid-layout";
import "react-grid-layout/css/styles.css"; import "react-grid-layout/css/styles.css";
import "react-resizable/css/styles.css"; import "react-resizable/css/styles.css";
@ -322,24 +321,6 @@ export default function DraggableGridLayout({
const gridContainerRef = useRef<HTMLDivElement>(null); const gridContainerRef = useRef<HTMLDivElement>(null);
const {
width: gridWidth,
containerRef: gridWidthRef,
mounted: gridMounted,
} = useContainerWidth();
// Combine gridContainerRef and gridWidthRef into a single callback ref
const combinedGridRef = useCallback(
(node: HTMLDivElement | null) => {
(
gridContainerRef as React.MutableRefObject<HTMLDivElement | null>
).current = node;
(gridWidthRef as React.MutableRefObject<HTMLDivElement | null>).current =
node;
},
[gridWidthRef],
);
const [{ width: containerWidth, height: containerHeight }] = const [{ width: containerWidth, height: containerHeight }] =
useResizeObserver(gridContainerRef); useResizeObserver(gridContainerRef);
@ -546,7 +527,7 @@ export default function DraggableGridLayout({
) : ( ) : (
<div <div
className="no-scrollbar my-2 select-none overflow-x-hidden px-2 pb-8" className="no-scrollbar my-2 select-none overflow-x-hidden px-2 pb-8"
ref={combinedGridRef} ref={gridContainerRef}
> >
<EditGroupDialog <EditGroupDialog
open={editGroup} open={editGroup}
@ -554,170 +535,160 @@ export default function DraggableGridLayout({
currentGroups={groups} currentGroups={groups}
activeGroup={group} activeGroup={group}
/> />
{gridMounted && ( <Responsive
<Responsive className="grid-layout"
className="grid-layout" width={availableWidth ?? window.innerWidth}
width={gridWidth} layouts={{
layouts={{ lg: currentGridLayout,
lg: currentGridLayout, md: currentGridLayout,
md: currentGridLayout, sm: currentGridLayout,
sm: currentGridLayout, xs: currentGridLayout,
xs: currentGridLayout, xxs: currentGridLayout,
xxs: currentGridLayout, }}
}} rowHeight={cellHeight}
rowHeight={cellHeight} breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }} cols={{ lg: 12, md: 12, sm: 12, xs: 12, xxs: 12 }}
cols={{ lg: 12, md: 12, sm: 12, xs: 12, xxs: 12 }} margin={[marginValue, marginValue]}
margin={[marginValue, marginValue]} containerPadding={[0, isEditMode ? 6 : 3]}
containerPadding={[0, isEditMode ? 6 : 3]} resizeConfig={{
resizeConfig={{ enabled: isEditMode,
enabled: isEditMode, handles: isEditMode ? ["sw", "nw", "se", "ne"] : [],
handles: isEditMode ? ["sw", "nw", "se", "ne"] : [], }}
}} dragConfig={{
dragConfig={{ enabled: isEditMode,
enabled: isEditMode, }}
}} onDragStop={handleLayoutChange}
onDragStop={handleLayoutChange} onResize={handleResize}
onResize={handleResize} onResizeStart={() => setShowCircles(false)}
onResizeStart={() => setShowCircles(false)} onResizeStop={handleLayoutChange}
onResizeStop={handleLayoutChange} >
> {includeBirdseye && birdseyeConfig?.enabled && (
{includeBirdseye && birdseyeConfig?.enabled && ( <BirdseyeLivePlayerGridItem
<BirdseyeLivePlayerGridItem key="birdseye"
key="birdseye" className={cn(
className={cn( isEditMode &&
isEditMode && showCircles &&
showCircles && "outline outline-2 outline-muted-foreground hover:cursor-grab hover:outline-4 active:cursor-grabbing",
"outline outline-2 outline-muted-foreground hover:cursor-grab hover:outline-4 active:cursor-grabbing", )}
)} birdseyeConfig={birdseyeConfig}
birdseyeConfig={birdseyeConfig} liveMode={birdseyeConfig.restream ? "mse" : "jsmpeg"}
liveMode={birdseyeConfig.restream ? "mse" : "jsmpeg"} onClick={() => onSelectCamera("birdseye")}
onClick={() => onSelectCamera("birdseye")} >
> {isEditMode && showCircles && <CornerCircles />}
{isEditMode && showCircles && <CornerCircles />} </BirdseyeLivePlayerGridItem>
</BirdseyeLivePlayerGridItem> )}
)} {cameras.map((camera) => {
{cameras.map((camera) => { let grow;
let grow; const aspectRatio = camera.detect.width / camera.detect.height;
const aspectRatio = camera.detect.width / camera.detect.height; if (aspectRatio > ASPECT_WIDE_LAYOUT) {
if (aspectRatio > ASPECT_WIDE_LAYOUT) { grow = `aspect-wide w-full`;
grow = `aspect-wide w-full`; } else if (aspectRatio < ASPECT_VERTICAL_LAYOUT) {
} else if (aspectRatio < ASPECT_VERTICAL_LAYOUT) { grow = `aspect-tall h-full`;
grow = `aspect-tall h-full`; } else {
} else { grow = "aspect-video";
grow = "aspect-video"; }
} const availableStreams = camera.live.streams || {};
const availableStreams = camera.live.streams || {}; const firstStreamEntry = Object.values(availableStreams)[0] || "";
const firstStreamEntry =
Object.values(availableStreams)[0] || "";
const streamNameFromSettings = const streamNameFromSettings =
currentGroupStreamingSettings?.[camera.name]?.streamName || currentGroupStreamingSettings?.[camera.name]?.streamName || "";
""; const streamExists =
const streamExists = streamNameFromSettings &&
streamNameFromSettings && Object.values(availableStreams).includes(
Object.values(availableStreams).includes( streamNameFromSettings,
streamNameFromSettings,
);
const streamName = streamExists
? streamNameFromSettings
: firstStreamEntry;
const streamType =
currentGroupStreamingSettings?.[camera.name]?.streamType;
const autoLive =
streamType !== undefined
? streamType !== "no-streaming"
: undefined;
const showStillWithoutActivity =
currentGroupStreamingSettings?.[camera.name]?.streamType !==
"continuous";
const useWebGL =
currentGroupStreamingSettings?.[camera.name]
?.compatibilityMode || false;
return (
<GridLiveContextMenu
className={grow}
key={camera.name}
camera={camera.name}
streamName={streamName}
cameraGroup={cameraGroup}
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
isRestreamed={isRestreamedStates[camera.name]}
supportsAudio={
supportsAudioOutputStates[streamName]?.supportsAudio ??
false
}
audioState={audioStates[camera.name]}
toggleAudio={() => toggleAudio(camera.name)}
statsState={statsStates[camera.name]}
toggleStats={() => toggleStats(camera.name)}
volumeState={volumeStates[camera.name]}
setVolumeState={(value) =>
setVolumeStates({
[camera.name]: value,
})
}
muteAll={muteAll}
unmuteAll={unmuteAll}
resetPreferredLiveMode={() =>
resetPreferredLiveMode(camera.name)
}
config={config}
streamMetadata={streamMetadata}
>
<LivePlayer
key={camera.name}
streamName={streamName}
autoLive={autoLive ?? globalAutoLive}
showStillWithoutActivity={
showStillWithoutActivity ?? true
}
alwaysShowCameraName={displayCameraNames}
useWebGL={useWebGL}
cameraRef={cameraRef}
className={cn(
"rounded-lg bg-black md:rounded-2xl",
grow,
isEditMode &&
showCircles &&
"outline-2 outline-muted-foreground hover:cursor-grab hover:outline-4 active:cursor-grabbing",
)}
windowVisible={
windowVisible && visibleCameras.includes(camera.name)
}
cameraConfig={camera}
preferredLiveMode={
preferredLiveModes[camera.name] ?? "mse"
}
playInBackground={false}
showStats={statsStates[camera.name]}
onClick={() => {
!isEditMode && onSelectCamera(camera.name);
}}
onError={(e) => {
setPreferredLiveModes((prevModes) => {
const newModes = { ...prevModes };
if (e === "mse-decode") {
newModes[camera.name] = "webrtc";
} else {
newModes[camera.name] = "jsmpeg";
}
return newModes;
});
}}
onResetLiveMode={() =>
resetPreferredLiveMode(camera.name)
}
playAudio={audioStates[camera.name]}
volume={volumeStates[camera.name]}
/>
{isEditMode && showCircles && <CornerCircles />}
</GridLiveContextMenu>
); );
})}
</Responsive> const streamName = streamExists
)} ? streamNameFromSettings
: firstStreamEntry;
const streamType =
currentGroupStreamingSettings?.[camera.name]?.streamType;
const autoLive =
streamType !== undefined
? streamType !== "no-streaming"
: undefined;
const showStillWithoutActivity =
currentGroupStreamingSettings?.[camera.name]?.streamType !==
"continuous";
const useWebGL =
currentGroupStreamingSettings?.[camera.name]
?.compatibilityMode || false;
return (
<GridLiveContextMenu
className={grow}
key={camera.name}
camera={camera.name}
streamName={streamName}
cameraGroup={cameraGroup}
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
isRestreamed={isRestreamedStates[camera.name]}
supportsAudio={
supportsAudioOutputStates[streamName]?.supportsAudio ??
false
}
audioState={audioStates[camera.name]}
toggleAudio={() => toggleAudio(camera.name)}
statsState={statsStates[camera.name]}
toggleStats={() => toggleStats(camera.name)}
volumeState={volumeStates[camera.name]}
setVolumeState={(value) =>
setVolumeStates({
[camera.name]: value,
})
}
muteAll={muteAll}
unmuteAll={unmuteAll}
resetPreferredLiveMode={() =>
resetPreferredLiveMode(camera.name)
}
config={config}
streamMetadata={streamMetadata}
>
<LivePlayer
key={camera.name}
streamName={streamName}
autoLive={autoLive ?? globalAutoLive}
showStillWithoutActivity={showStillWithoutActivity ?? true}
alwaysShowCameraName={displayCameraNames}
useWebGL={useWebGL}
cameraRef={cameraRef}
className={cn(
"rounded-lg bg-black md:rounded-2xl",
grow,
isEditMode &&
showCircles &&
"outline-2 outline-muted-foreground hover:cursor-grab hover:outline-4 active:cursor-grabbing",
)}
windowVisible={
windowVisible && visibleCameras.includes(camera.name)
}
cameraConfig={camera}
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
playInBackground={false}
showStats={statsStates[camera.name]}
onClick={() => {
!isEditMode && onSelectCamera(camera.name);
}}
onError={(e) => {
setPreferredLiveModes((prevModes) => {
const newModes = { ...prevModes };
if (e === "mse-decode") {
newModes[camera.name] = "webrtc";
} else {
newModes[camera.name] = "jsmpeg";
}
return newModes;
});
}}
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
playAudio={audioStates[camera.name]}
volume={volumeStates[camera.name]}
/>
{isEditMode && showCircles && <CornerCircles />}
</GridLiveContextMenu>
);
})}
</Responsive>
{isDesktop && ( {isDesktop && (
<div <div
className={cn( className={cn(