From 3e30c14bd7ab2422831622c4c3448be609d3860b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:35:42 +0000 Subject: [PATCH 1/5] Initial plan From 3d13525ad077042c27c2de88afd5385eda8c12ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:40:37 +0000 Subject: [PATCH 2/5] Address PR review comments: improve accessibility, visibility, and performance - Added TILE_DISPLAY_NAMES mapping for human-readable tile names (e.g., 'Map View' instead of 'mapView') - Added aria-label attributes to add tile buttons and select dropdown for screen reader accessibility - Fixed button text color from #2d2d2d to #f1f1f1 for better visibility on dark background - Moved Controls component outside render function and memoized it to prevent unnecessary re-renders - Removed unnecessary useMemo from ALL_TILES constant (now a static const) - Added null check validation in onChange handler before calling splitAndAdd - Implemented click-outside handler to close dropdown when user clicks elsewhere Co-authored-by: ConnorNeed <129120300+ConnorNeed@users.noreply.github.com> --- src/components/panels/MosaicDashboard.tsx | 221 +++++++++++++--------- 1 file changed, 131 insertions(+), 90 deletions(-) diff --git a/src/components/panels/MosaicDashboard.tsx b/src/components/panels/MosaicDashboard.tsx index 793e371..794ca94 100644 --- a/src/components/panels/MosaicDashboard.tsx +++ b/src/components/panels/MosaicDashboard.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useMemo, useState, ReactElement, useContext } from 'react'; +import React, { useMemo, useState, ReactElement, useContext, useRef, useEffect, memo } from 'react'; import { Mosaic, MosaicWindow, @@ -29,6 +29,126 @@ type MosaicKey = | 'goalSetter' | 'networkHealthMonitor'; +// Human-readable tile names mapping +const TILE_DISPLAY_NAMES: Record = { + mapView: 'Map View', + rosMonitor: 'System Telemetry', + waypointList: 'Waypoint List', + videoControls: 'Video Stream', + gasSensor: 'Science', + orientationDisplay: 'Rover Orientation', + goalSetter: 'Nav2', + networkHealthMonitor: 'Connection Health', +}; + +// All available tiles - no need for useMemo as this is a static array +const ALL_TILES: MosaicKey[] = [ + 'mapView', + 'rosMonitor', + 'networkHealthMonitor', + 'orientationDisplay', + 'videoControls', + 'waypointList', + 'gasSensor', + 'goalSetter', +]; + +// Move Controls component outside to prevent re-creation on every render +const Controls = memo<{ + id: MosaicKey; + path: MosaicPath; + pendingAdd: { pathKey: string; path: MosaicPath; direction: 'row' | 'column' } | null; + setPendingAdd: (value: { pathKey: string; path: MosaicPath; direction: 'row' | 'column' } | null) => void; +}>(({ id, path, pendingAdd, setPendingAdd }) => { + const { mosaicActions } = useContext(MosaicContext); + const pathKey = JSON.stringify(path); + const showDropdown = pendingAdd?.pathKey === pathKey; + const dropdownRef = useRef(null); + + const splitAndAdd = (direction: 'row' | 'column', newTile: MosaicKey) => { + const splitNode: MosaicNode = { + direction, + first: id, // keep current tile + second: newTile, + splitPercentage: 60, + }; + + mosaicActions.replaceWith(path, splitNode); + setPendingAdd(null); + }; + + // Close dropdown when clicking outside + useEffect(() => { + if (!showDropdown) return; + + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setPendingAdd(null); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [showDropdown, setPendingAdd]); + + return ( +
+ + + + + {showDropdown ? ( + + ) : null} +
+ ); +}); + +Controls.displayName = 'Controls'; + const MosaicDashboard: React.FC = () => { // TODO: parameterize layout for custom layout configs const [mosaicLayout, setMosaicLayout] = useState | null>({ @@ -66,20 +186,6 @@ const MosaicDashboard: React.FC = () => { splitPercentage: 60, }); - const ALL_TILES = useMemo( - () => [ - 'mapView', - 'rosMonitor', - 'networkHealthMonitor', - 'orientationDisplay', - 'videoControls', - 'waypointList', - 'gasSensor', - 'goalSetter', - ], - [] - ); - // Which window currently has the dropdown open? const [pendingAdd, setPendingAdd] = useState<{ pathKey: string; @@ -87,71 +193,6 @@ const MosaicDashboard: React.FC = () => { direction: 'row' | 'column'; // row => add right, column => add below } | null>(null); - const Controls: React.FC<{ id: MosaicKey; path: MosaicPath }> = ({ id, path }) => { - const { mosaicActions } = useContext(MosaicContext); - const pathKey = JSON.stringify(path); - const showDropdown = pendingAdd?.pathKey === pathKey; - - const splitAndAdd = (direction: 'row' | 'column', newTile: MosaicKey) => { - const splitNode: MosaicNode = { - direction, - first: id, // keep current tile - second: newTile, - splitPercentage: 60, - }; - - mosaicActions.replaceWith(path, splitNode); - setPendingAdd(null); - }; - - return ( -
- - - - - {showDropdown ? ( - - ) : null} -
- ); - }; - const renderTile = (id: MosaicKey, path: MosaicPath): ReactElement => { switch (id) { case 'mapView': @@ -159,7 +200,7 @@ const MosaicDashboard: React.FC = () => { title="Map View" path={path} - additionalControls={} + additionalControls={} >
@@ -172,7 +213,7 @@ const MosaicDashboard: React.FC = () => { title="Waypoint List" path={path} - additionalControls={} + additionalControls={} > @@ -183,7 +224,7 @@ const MosaicDashboard: React.FC = () => { title="Video Stream" path={path} - additionalControls={} + additionalControls={} > @@ -194,7 +235,7 @@ const MosaicDashboard: React.FC = () => { title="System Telemetry" path={path} - additionalControls={} + additionalControls={} > @@ -205,7 +246,7 @@ const MosaicDashboard: React.FC = () => { title="Connection Health" path={path} - additionalControls={} + additionalControls={} > @@ -216,7 +257,7 @@ const MosaicDashboard: React.FC = () => { title="Rover Orientation" path={path} - additionalControls={} + additionalControls={} > @@ -227,7 +268,7 @@ const MosaicDashboard: React.FC = () => { title="Science" path={path} - additionalControls={} + additionalControls={} > @@ -238,7 +279,7 @@ const MosaicDashboard: React.FC = () => { title="Nav2" path={path} - additionalControls={} + additionalControls={} > @@ -249,7 +290,7 @@ const MosaicDashboard: React.FC = () => { title="Unknown tile" path={path} - additionalControls={} + additionalControls={} >
Unknown tile
@@ -292,7 +333,7 @@ const MosaicDashboard: React.FC = () => { .tile-btn { background: transparent; border: 1px solid #444; - color: #2d2d2d; + color: #f1f1f1; border-radius: 6px; padding: 2px 6px; cursor: pointer; From 283efd3770f7d74cca2d7c00fa9adc293fac6e08 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:42:07 +0000 Subject: [PATCH 3/5] Refactor: extract PendingAdd type and improve comment clarity - Extracted PendingAdd type for better maintainability and reusability - Updated comment to more accurately describe validation logic Co-authored-by: ConnorNeed <129120300+ConnorNeed@users.noreply.github.com> --- src/components/panels/MosaicDashboard.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/panels/MosaicDashboard.tsx b/src/components/panels/MosaicDashboard.tsx index 794ca94..3b0ea4e 100644 --- a/src/components/panels/MosaicDashboard.tsx +++ b/src/components/panels/MosaicDashboard.tsx @@ -53,12 +53,18 @@ const ALL_TILES: MosaicKey[] = [ 'goalSetter', ]; +type PendingAdd = { + pathKey: string; + path: MosaicPath; + direction: 'row' | 'column'; +} | null; + // Move Controls component outside to prevent re-creation on every render const Controls = memo<{ id: MosaicKey; path: MosaicPath; - pendingAdd: { pathKey: string; path: MosaicPath; direction: 'row' | 'column' } | null; - setPendingAdd: (value: { pathKey: string; path: MosaicPath; direction: 'row' | 'column' } | null) => void; + pendingAdd: PendingAdd; + setPendingAdd: (value: PendingAdd) => void; }>(({ id, path, pendingAdd, setPendingAdd }) => { const { mosaicActions } = useContext(MosaicContext); const pathKey = JSON.stringify(path); @@ -126,7 +132,7 @@ const Controls = memo<{ onClick={(e) => e.stopPropagation()} onChange={(e) => { const value = e.target.value as MosaicKey; - // Add null check validation before calling splitAndAdd + // Validate pendingAdd state and selected value before proceeding if (pendingAdd && value) { splitAndAdd(pendingAdd.direction, value); } @@ -187,11 +193,7 @@ const MosaicDashboard: React.FC = () => { }); // Which window currently has the dropdown open? - const [pendingAdd, setPendingAdd] = useState<{ - pathKey: string; - path: MosaicPath; - direction: 'row' | 'column'; // row => add right, column => add below - } | null>(null); + const [pendingAdd, setPendingAdd] = useState(null); const renderTile = (id: MosaicKey, path: MosaicPath): ReactElement => { switch (id) { From 69d071711ae311434788556ae2a0fe71c7ed9a6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:44:39 +0000 Subject: [PATCH 4/5] Improve memoization effectiveness by stabilizing setPendingAdd - Wrapped setPendingAdd with useMemo to preserve Controls component memoization benefits - Updated all renderTile cases to use the memoized handleSetPendingAdd Co-authored-by: ConnorNeed <129120300+ConnorNeed@users.noreply.github.com> --- src/components/panels/MosaicDashboard.tsx | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/panels/MosaicDashboard.tsx b/src/components/panels/MosaicDashboard.tsx index 3b0ea4e..013a8b2 100644 --- a/src/components/panels/MosaicDashboard.tsx +++ b/src/components/panels/MosaicDashboard.tsx @@ -195,6 +195,9 @@ const MosaicDashboard: React.FC = () => { // Which window currently has the dropdown open? const [pendingAdd, setPendingAdd] = useState(null); + // Memoize setPendingAdd to preserve Controls component memoization benefits + const handleSetPendingAdd = useMemo(() => setPendingAdd, []); + const renderTile = (id: MosaicKey, path: MosaicPath): ReactElement => { switch (id) { case 'mapView': @@ -202,7 +205,7 @@ const MosaicDashboard: React.FC = () => { title="Map View" path={path} - additionalControls={} + additionalControls={} >
@@ -215,7 +218,7 @@ const MosaicDashboard: React.FC = () => { title="Waypoint List" path={path} - additionalControls={} + additionalControls={} > @@ -226,7 +229,7 @@ const MosaicDashboard: React.FC = () => { title="Video Stream" path={path} - additionalControls={} + additionalControls={} > @@ -237,7 +240,7 @@ const MosaicDashboard: React.FC = () => { title="System Telemetry" path={path} - additionalControls={} + additionalControls={} > @@ -248,7 +251,7 @@ const MosaicDashboard: React.FC = () => { title="Connection Health" path={path} - additionalControls={} + additionalControls={} > @@ -259,7 +262,7 @@ const MosaicDashboard: React.FC = () => { title="Rover Orientation" path={path} - additionalControls={} + additionalControls={} > @@ -270,7 +273,7 @@ const MosaicDashboard: React.FC = () => { title="Science" path={path} - additionalControls={} + additionalControls={} > @@ -281,7 +284,7 @@ const MosaicDashboard: React.FC = () => { title="Nav2" path={path} - additionalControls={} + additionalControls={} > @@ -292,7 +295,7 @@ const MosaicDashboard: React.FC = () => { title="Unknown tile" path={path} - additionalControls={} + additionalControls={} >
Unknown tile
From 97c2d9594ae63d620a677e2ab50d04083fc96895 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:46:00 +0000 Subject: [PATCH 5/5] Remove unnecessary useMemo wrapper for setPendingAdd - setState functions from useState are already stable and don't need memoization - Removed ineffective useMemo wrapper and pass setPendingAdd directly to Controls Co-authored-by: ConnorNeed <129120300+ConnorNeed@users.noreply.github.com> --- src/components/panels/MosaicDashboard.tsx | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/components/panels/MosaicDashboard.tsx b/src/components/panels/MosaicDashboard.tsx index 013a8b2..3b0ea4e 100644 --- a/src/components/panels/MosaicDashboard.tsx +++ b/src/components/panels/MosaicDashboard.tsx @@ -195,9 +195,6 @@ const MosaicDashboard: React.FC = () => { // Which window currently has the dropdown open? const [pendingAdd, setPendingAdd] = useState(null); - // Memoize setPendingAdd to preserve Controls component memoization benefits - const handleSetPendingAdd = useMemo(() => setPendingAdd, []); - const renderTile = (id: MosaicKey, path: MosaicPath): ReactElement => { switch (id) { case 'mapView': @@ -205,7 +202,7 @@ const MosaicDashboard: React.FC = () => { title="Map View" path={path} - additionalControls={} + additionalControls={} >
@@ -218,7 +215,7 @@ const MosaicDashboard: React.FC = () => { title="Waypoint List" path={path} - additionalControls={} + additionalControls={} > @@ -229,7 +226,7 @@ const MosaicDashboard: React.FC = () => { title="Video Stream" path={path} - additionalControls={} + additionalControls={} > @@ -240,7 +237,7 @@ const MosaicDashboard: React.FC = () => { title="System Telemetry" path={path} - additionalControls={} + additionalControls={} > @@ -251,7 +248,7 @@ const MosaicDashboard: React.FC = () => { title="Connection Health" path={path} - additionalControls={} + additionalControls={} > @@ -262,7 +259,7 @@ const MosaicDashboard: React.FC = () => { title="Rover Orientation" path={path} - additionalControls={} + additionalControls={} > @@ -273,7 +270,7 @@ const MosaicDashboard: React.FC = () => { title="Science" path={path} - additionalControls={} + additionalControls={} > @@ -284,7 +281,7 @@ const MosaicDashboard: React.FC = () => { title="Nav2" path={path} - additionalControls={} + additionalControls={} > @@ -295,7 +292,7 @@ const MosaicDashboard: React.FC = () => { title="Unknown tile" path={path} - additionalControls={} + additionalControls={} >
Unknown tile