mirror of
https://github.com/JetBrains/intellij-platform-plugin-template.git
synced 2026-01-20 07:39:22 +00:00
Fix: Autocomplete popup confirms the last selected item instead of clicked one
This commit is contained in:
parent
a205391e0a
commit
4673a445bf
@ -107,7 +107,7 @@ fun <T> SearchBarWithAutoCompletion(
|
||||
selectableItem(
|
||||
completionItem.isSelected,
|
||||
onClick = {
|
||||
popupController.onSelectCompletion()
|
||||
popupController.onItemClicked(completionItem)
|
||||
textFieldState.setTextAndPlaceCursorAtEnd(completionItem.item.label)
|
||||
},
|
||||
) {
|
||||
@ -147,12 +147,12 @@ fun CloseIconButton(onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
|
||||
private data class CompletionItem<T : Searchable>(
|
||||
internal data class CompletionItem<T : Searchable>(
|
||||
val item: T,
|
||||
val isSelected: Boolean,
|
||||
)
|
||||
|
||||
private class CompletionPopupController<T : Searchable>(
|
||||
internal class CompletionPopupController<T : Searchable>(
|
||||
private val itemsProvider: SearchAutoCompletionItemProvider<T>,
|
||||
private val onSelectCompletion: (CompletionItem<T>) -> Unit = {},
|
||||
) {
|
||||
@ -222,16 +222,22 @@ private class CompletionPopupController<T : Searchable>(
|
||||
clearFilteredItems()
|
||||
}
|
||||
|
||||
fun onSelectCompletion() {
|
||||
if (!isVisible) return
|
||||
fun onItemClicked(clickedItem: CompletionItem<T>) {
|
||||
doCompleteSelection(clickedItem)
|
||||
}
|
||||
|
||||
val completionPopupItem = selectedItem
|
||||
fun onSelectionConfirmed() {
|
||||
doCompleteSelection(this.selectedItem)
|
||||
}
|
||||
|
||||
private fun doCompleteSelection(selectedItem: CompletionItem<T>) {
|
||||
if (!isVisible) return
|
||||
|
||||
skipPopupShowing = true
|
||||
|
||||
reset()
|
||||
|
||||
onSelectCompletion(completionPopupItem)
|
||||
onSelectCompletion(selectedItem)
|
||||
}
|
||||
|
||||
private fun updateFilteredItems(newItems: List<CompletionItem<T>>) {
|
||||
@ -279,7 +285,7 @@ private fun <T : Searchable> Modifier.handlePopupCompletionKeyEvents(
|
||||
|
||||
return@onPreviewKeyEvent when (keyEvent.key) {
|
||||
Key.Tab, Key.Enter, Key.NumPadEnter -> {
|
||||
popupController.onSelectCompletion()
|
||||
popupController.onSelectionConfirmed()
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,211 @@
|
||||
package org.jetbrains.plugins.template.components
|
||||
|
||||
import org.jetbrains.plugins.template.weatherApp.model.PreviewableItem
|
||||
import org.jetbrains.plugins.template.weatherApp.model.Searchable
|
||||
import org.jetbrains.plugins.template.weatherApp.services.SearchAutoCompletionItemProvider
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
internal class CompletionPopupControllerTest {
|
||||
|
||||
private lateinit var mockProvider: MockSearchProvider
|
||||
private lateinit var controller: CompletionPopupController<TestItem>
|
||||
|
||||
// Autocompleted items
|
||||
private val selectedItems = mutableListOf<CompletionItem<TestItem>>()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val testItems = listOf(
|
||||
TestItem("Paris, France"),
|
||||
TestItem("Berlin, Germany"),
|
||||
TestItem("Chicago, USA"),
|
||||
TestItem("Rome, Italy")
|
||||
)
|
||||
mockProvider = MockSearchProvider(testItems)
|
||||
|
||||
controller = CompletionPopupController(mockProvider) { item ->
|
||||
selectedItems.add(item)
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
selectedItems.clear()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test query changes updates completion items`() {
|
||||
// When
|
||||
controller.onQueryChanged("a")
|
||||
|
||||
// Then
|
||||
assertTrue(controller.isVisible)
|
||||
assertItemCount(4)
|
||||
assertOnlyItemAtIndexIsSelected(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test keyboard selection with onSelectMovedDown`() {
|
||||
// Given
|
||||
controller.onQueryChanged("a")
|
||||
assertEquals(4, controller.completionItems.size)
|
||||
assertOnlyItemAtIndexIsSelected(0) // First item is initially selected
|
||||
|
||||
// When
|
||||
moveSelectionDown(1)
|
||||
|
||||
// Then
|
||||
assertOnlyItemAtIndexIsSelected(1) // Second item should now be selected
|
||||
|
||||
// When moving down again
|
||||
moveSelectionDown(1)
|
||||
|
||||
// Then
|
||||
assertOnlyItemAtIndexIsSelected(2) // Third item should now be selected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test keyboard selection with onSelectMovedUp`() {
|
||||
// Given - Initialize with query and move to the last item
|
||||
controller.onQueryChanged("a")
|
||||
assertOnlyItemAtIndexIsSelected(0)
|
||||
assertItemCount(4)
|
||||
moveSelectionDown(3) // Move to the last item (index 3)
|
||||
|
||||
// Test case 1: Move up from last item (index 3) to third item (index 2)
|
||||
moveSelectionUp()
|
||||
assertOnlyItemAtIndexIsSelected(2)
|
||||
|
||||
// Test case 2: Move up from third item (index 2) to second item (index 1)
|
||||
moveSelectionUp()
|
||||
assertOnlyItemAtIndexIsSelected(1)
|
||||
|
||||
// Test case 3: Move up from second item (index 1) to first item (index 0)
|
||||
moveSelectionUp()
|
||||
assertOnlyItemAtIndexIsSelected(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test keyboard selection stays within boundaries`() {
|
||||
// Given
|
||||
controller.onQueryChanged("a")
|
||||
assertEquals(4, controller.completionItems.size)
|
||||
assertOnlyItemAtIndexIsSelected(0) // First item is initially selected
|
||||
|
||||
// When moving up from the first item
|
||||
moveSelectionUp(1)
|
||||
|
||||
// Then it should stay at the first item (no wrapping)
|
||||
assertOnlyItemAtIndexIsSelected(0) // First item should still be selected
|
||||
|
||||
// Move to the last item
|
||||
moveSelectionDown(3)
|
||||
assertOnlyItemAtIndexIsSelected(3) // Last item is selected
|
||||
|
||||
// When moving down from the last item
|
||||
moveSelectionDown(1)
|
||||
|
||||
// Then it should stay at the last item (no wrapping)
|
||||
assertOnlyItemAtIndexIsSelected(3) // Last item should still be selected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test selection confirmation with keyboard navigation`() {
|
||||
// Given
|
||||
controller.onQueryChanged("a")
|
||||
moveSelectionDown(1)
|
||||
|
||||
// When
|
||||
controller.onSelectionConfirmed()
|
||||
|
||||
// Then
|
||||
assertItemSelected("Berlin, Germany")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test selection confirmation with explicit item (mouse click)`() {
|
||||
// Given
|
||||
controller.onQueryChanged("a")
|
||||
val itemToSelect = controller.completionItems[2] // "Chicago, USA"
|
||||
|
||||
controller.onItemClicked(itemToSelect)
|
||||
|
||||
// Then
|
||||
assertItemSelected("Chicago, USA")
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the given selected item label matches the expected label and verifies the state
|
||||
* of the selected items list and popup visibility after a selection operation.
|
||||
*
|
||||
* @param selectedItemLabel The expected label of the selected item.
|
||||
*/
|
||||
private fun assertItemSelected(selectedItemLabel: String) {
|
||||
assertEquals(1, selectedItems.size)
|
||||
assertEquals(selectedItemLabel, selectedItems[0].item.label)
|
||||
assertFalse(controller.isVisible) // Popup should be hidden after selection
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to move selection down by the specified number of steps
|
||||
*
|
||||
* @param step Number of steps to move down
|
||||
*/
|
||||
private fun moveSelectionDown(step: Int) {
|
||||
repeat(step) {
|
||||
controller.onSelectionMovedDown()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to move selection up by the specified number of steps
|
||||
*
|
||||
* @param step Number of steps to move up
|
||||
*/
|
||||
private fun moveSelectionUp(step: Int = 1) {
|
||||
repeat(step) {
|
||||
controller.onSelectionMovedUp()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the current number of completion items matches the expected count.
|
||||
*
|
||||
* @param count The expected number of completion items.
|
||||
*/
|
||||
private fun assertItemCount(count: Int) {
|
||||
assertEquals(count, controller.completionItems.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to assert that only the item at the specified index is selected
|
||||
*/
|
||||
private fun assertOnlyItemAtIndexIsSelected(selectedIndex: Int) {
|
||||
for (i in controller.completionItems.indices) {
|
||||
if (i == selectedIndex) {
|
||||
assertTrue(
|
||||
"Item at index $selectedIndex should be selected",
|
||||
controller.completionItems[i].isSelected
|
||||
)
|
||||
} else {
|
||||
assertFalse(
|
||||
"Item at index $i should not be selected",
|
||||
controller.completionItems[i].isSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class TestItem(override val label: String) : Searchable, PreviewableItem {
|
||||
override fun matches(query: String): Boolean = label.contains(query, ignoreCase = true)
|
||||
}
|
||||
|
||||
private class MockSearchProvider(private val items: List<TestItem>) : SearchAutoCompletionItemProvider<TestItem> {
|
||||
override fun provideSearchableItems(searchTerm: String): List<TestItem> {
|
||||
return items.filter { it.matches(searchTerm) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user