diff --git a/src/org/openstreetmap/josm/gui/dialogs/NotesDialog.java b/src/org/openstreetmap/josm/gui/dialogs/NotesDialog.java
index 3dedcdfd1b..cf5ddb9f3c 100644
--- a/src/org/openstreetmap/josm/gui/dialogs/NotesDialog.java
+++ b/src/org/openstreetmap/josm/gui/dialogs/NotesDialog.java
@@ -407,7 +407,12 @@ public class NotesDialog extends ToggleDialog implements LayerChangeListener, No
             if (note != null) {
                 int selectedIndex = displayList.getSelectedIndex();
                 noteData.closeNote(note, dialog.getInputText());
-                noteData.setSelectedNote(model.getElementAt(selectedIndex));
+                // This is required since filtering may cause the model to not have any visible elements
+                if (model.getSize() > 0) {
+                    noteData.setSelectedNote(model.getElementAt(selectedIndex));
+                } else {
+                    noteData.setSelectedNote(null);
+                }
             }
         }
     }
diff --git a/test/unit/org/openstreetmap/josm/gui/dialogs/NotesDialogTest.java b/test/unit/org/openstreetmap/josm/gui/dialogs/NotesDialogTest.java
index a8ec189fc8..c2fe4f2a64 100644
--- a/test/unit/org/openstreetmap/josm/gui/dialogs/NotesDialogTest.java
+++ b/test/unit/org/openstreetmap/josm/gui/dialogs/NotesDialogTest.java
@@ -1,29 +1,43 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.dialogs;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.openstreetmap.josm.tools.I18n.tr;
 
 import java.time.Instant;
+import java.util.Collections;
 
 import javax.swing.JLabel;
 import javax.swing.JList;
 
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.platform.commons.util.ReflectionUtils;
+import org.openstreetmap.josm.TestUtils;
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.notes.Note;
 import org.openstreetmap.josm.data.notes.NoteComment;
 import org.openstreetmap.josm.data.osm.User;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.dialogs.NotesDialog.NoteRenderer;
+import org.openstreetmap.josm.gui.layer.NoteLayer;
+import org.openstreetmap.josm.gui.widgets.JosmTextField;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
 import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
-
-import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.testutils.mockers.ExtendedDialogMocker;
 
 /**
  * Unit tests of {@link NotesDialog}
  */
 @BasicPreferences
 class NotesDialogTest {
+    /** Only needed for {@link #testTicket21558} */
+    @RegisterExtension
+    JOSMTestRules rules = new JOSMTestRules().main().projection();
     private Note createMultiLineNote() {
         Note note = new Note(LatLon.ZERO);
         note.setCreatedAt(Instant.now());
@@ -55,4 +69,31 @@ class NotesDialogTest {
         assertFalse(NotesDialog.matchesNote("new", note));
         assertFalse(NotesDialog.matchesNote("reopened", note));
     }
+
+    /**
+     * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/21558>#21558</a>
+     */
+    @Test
+    void testTicket21558() throws Exception {
+        TestUtils.assumeWorkingJMockit();
+        new ExtendedDialogMocker(Collections.singletonMap(tr("Close note"), tr("Close note"))) {
+            @Override
+            protected String getString(ExtendedDialog instance) {
+                return instance.getTitle();
+            }
+        };
+        final NotesDialog notesDialog = new NotesDialog();
+        final NotesDialog.CloseAction closeAction = (NotesDialog.CloseAction) ReflectionUtils
+                .tryToReadFieldValue(NotesDialog.class, "closeAction", notesDialog).get();
+        final JosmTextField filter = (JosmTextField) ReflectionUtils
+                .tryToReadFieldValue(NotesDialog.class, "filter", notesDialog).get();
+        final NoteLayer noteLayer = new NoteLayer();
+        MainApplication.getLayerManager().addLayer(noteLayer);
+        final Note note = createMultiLineNote();
+        note.setState(Note.State.OPEN);
+        noteLayer.getNoteData().addNotes(Collections.singleton(note));
+        noteLayer.getNoteData().setSelectedNote(note);
+        filter.setText("open");
+        assertDoesNotThrow(() -> closeAction.actionPerformed(null));
+    }
 }
diff --git a/test/unit/org/openstreetmap/josm/testutils/mockers/ExtendedDialogMocker.java b/test/unit/org/openstreetmap/josm/testutils/mockers/ExtendedDialogMocker.java
index f71e581dfc..2e77130c43 100644
--- a/test/unit/org/openstreetmap/josm/testutils/mockers/ExtendedDialogMocker.java
+++ b/test/unit/org/openstreetmap/josm/testutils/mockers/ExtendedDialogMocker.java
@@ -142,6 +142,37 @@ public class ExtendedDialogMocker extends BaseDialogMockUp<ExtendedDialog> {
         }
     }
 
+    /**
+     * Get the result field for an extended dialog instance
+     * @param instance The instance to get the result field for
+     * @return The result field. May be private.
+     * @throws NoSuchFieldException If the field cannot be found. Should never be thrown.
+     */
+    protected Field getResultField(ExtendedDialog instance) throws NoSuchFieldException {
+        // Note that subclasses of ExtendedDialogMocker will not have "result" as a declared field.
+        // Iterate up the chain until we get to a field that has "result" as a declared field.
+        // Only reason for this is just in case someone overrides the logic in ExtendedDialog.
+        Class<?> clazz = instance.getClass();
+        Field resultField = null;
+        // Store the exception, if any
+        NoSuchFieldException noSuchFieldException = null;
+        while (!Object.class.equals(clazz) && resultField == null) {
+            try {
+                resultField = clazz.getDeclaredField("result");
+            } catch (NoSuchFieldException e) {
+                clazz = instance.getClass().getSuperclass();
+                // Only save the first exception
+                if (noSuchFieldException == null) {
+                    noSuchFieldException = e;
+                }
+            }
+        }
+        if (resultField == null) {
+            throw noSuchFieldException;
+        }
+        return resultField;
+    }
+
     @Mock
     private void setupDialog(final Invocation invocation) {
         if (!GraphicsEnvironment.isHeadless()) {
@@ -159,7 +190,7 @@ public class ExtendedDialogMocker extends BaseDialogMockUp<ExtendedDialog> {
                 this.act(instance);
                 final int mockResult = this.getMockResult(instance);
                 // TODO check validity of mockResult?
-                Field resultField = instance.getClass().getDeclaredField("result");
+                final Field resultField = this.getResultField(instance);
                 resultField.setAccessible(true);
                 resultField.set(instance, mockResult);
                 Logging.info(
