Browse Source

Fix AMS slot modal infinite scroll loop on Windows

Inline callback refs on preset buttons called scrollIntoView on every
re-render, creating a scroll → re-render → scrollIntoView loop on
Windows. Replace with a useEffect + data attribute approach that only
scrolls when the selected preset actually changes.
AneoPsy 2 months ago
parent
commit
d048d72df2
1 changed files with 16 additions and 7 deletions
  1. 16 7
      frontend/src/components/ConfigureAmsSlotModal.tsx

+ 16 - 7
frontend/src/components/ConfigureAmsSlotModal.tsx

@@ -1,4 +1,4 @@
-import { useState, useMemo, useEffect, useCallback } from 'react';
+import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
 import { useQuery, useMutation } from '@tanstack/react-query';
 import { useTranslation } from 'react-i18next';
 import { X, Loader2, Settings2, ChevronDown, CheckCircle2, RotateCcw } from 'lucide-react';
@@ -242,6 +242,7 @@ export function ConfigureAmsSlotModal({
   const [searchQuery, setSearchQuery] = useState('');
   const [showSuccess, setShowSuccess] = useState(false);
   const [showExtendedColors, setShowExtendedColors] = useState(false);
+  const scrolledToRef = useRef<string>('');
 
   // Fetch cloud settings (gracefully handle 401 when logged out)
   const { data: cloudSettings, isLoading: settingsLoading, isError: cloudError } = useQuery({
@@ -735,6 +736,7 @@ export function ConfigureAmsSlotModal({
       setColorInput('');
       setSearchQuery('');
       setShowSuccess(false);
+      scrolledToRef.current = '';
     }
   }, [isOpen, slotInfo.savedPresetId, slotInfo.trayInfoIdx, slotInfo.trayColor, cloudSettings?.filament]);
 
@@ -770,6 +772,17 @@ export function ConfigureAmsSlotModal({
     }
   }, [isOpen, handleKeyDown]);
 
+  // Scroll selected preset into view only when the selection changes (not on every render).
+  // Inline callback refs cause scrollIntoView to fire every render, which on Windows
+  // creates an infinite scroll loop due to scroll → re-render → scrollIntoView cycles.
+  useEffect(() => {
+    if (selectedPresetId && selectedPresetId !== scrolledToRef.current) {
+      scrolledToRef.current = selectedPresetId;
+      const el = document.querySelector(`[data-preset-id="${CSS.escape(selectedPresetId)}"]`);
+      el?.scrollIntoView({ block: 'nearest' });
+    }
+  }, [selectedPresetId]);
+
   if (!isOpen) return null;
 
   const isLoading = (settingsLoading && !cloudError) || localLoading || builtinLoading || kprofilesLoading;
@@ -889,9 +902,7 @@ export function ConfigureAmsSlotModal({
                     filteredPresets.map((preset) => (
                       <button
                         key={preset.id}
-                        ref={selectedPresetId === preset.id ? (el) => {
-                          el?.scrollIntoView({ block: 'nearest' });
-                        } : undefined}
+                        data-preset-id={preset.id}
                         onClick={() => setSelectedPresetId(preset.id)}
                         className={`w-full p-2 rounded-lg border text-left transition-colors ${
                           selectedPresetId === preset.id
@@ -1126,9 +1137,7 @@ export function ConfigureAmsSlotModal({
                       filteredPresets.map((preset) => (
                         <button
                           key={preset.id}
-                          ref={selectedPresetId === preset.id ? (el) => {
-                            el?.scrollIntoView({ block: 'nearest' });
-                          } : undefined}
+                          data-preset-id={preset.id}
                           onClick={() => setSelectedPresetId(preset.id)}
                           className={`w-full p-2 rounded-lg border text-left transition-colors ${
                             selectedPresetId === preset.id