mirror of
https://github.com/JetBrains/intellij-platform-plugin-template.git
synced 2026-01-21 16:19: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(
|
selectableItem(
|
||||||
completionItem.isSelected,
|
completionItem.isSelected,
|
||||||
onClick = {
|
onClick = {
|
||||||
popupController.onSelectCompletion()
|
popupController.onItemClicked(completionItem)
|
||||||
textFieldState.setTextAndPlaceCursorAtEnd(completionItem.item.label)
|
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 item: T,
|
||||||
val isSelected: Boolean,
|
val isSelected: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
private class CompletionPopupController<T : Searchable>(
|
internal class CompletionPopupController<T : Searchable>(
|
||||||
private val itemsProvider: SearchAutoCompletionItemProvider<T>,
|
private val itemsProvider: SearchAutoCompletionItemProvider<T>,
|
||||||
private val onSelectCompletion: (CompletionItem<T>) -> Unit = {},
|
private val onSelectCompletion: (CompletionItem<T>) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
@ -222,16 +222,22 @@ private class CompletionPopupController<T : Searchable>(
|
|||||||
clearFilteredItems()
|
clearFilteredItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSelectCompletion() {
|
fun onItemClicked(clickedItem: CompletionItem<T>) {
|
||||||
if (!isVisible) return
|
doCompleteSelection(clickedItem)
|
||||||
|
}
|
||||||
|
|
||||||
val completionPopupItem = selectedItem
|
fun onSelectionConfirmed() {
|
||||||
|
doCompleteSelection(this.selectedItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doCompleteSelection(selectedItem: CompletionItem<T>) {
|
||||||
|
if (!isVisible) return
|
||||||
|
|
||||||
skipPopupShowing = true
|
skipPopupShowing = true
|
||||||
|
|
||||||
reset()
|
reset()
|
||||||
|
|
||||||
onSelectCompletion(completionPopupItem)
|
onSelectCompletion(selectedItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFilteredItems(newItems: List<CompletionItem<T>>) {
|
private fun updateFilteredItems(newItems: List<CompletionItem<T>>) {
|
||||||
@ -279,7 +285,7 @@ private fun <T : Searchable> Modifier.handlePopupCompletionKeyEvents(
|
|||||||
|
|
||||||
return@onPreviewKeyEvent when (keyEvent.key) {
|
return@onPreviewKeyEvent when (keyEvent.key) {
|
||||||
Key.Tab, Key.Enter, Key.NumPadEnter -> {
|
Key.Tab, Key.Enter, Key.NumPadEnter -> {
|
||||||
popupController.onSelectCompletion()
|
popupController.onSelectionConfirmed()
|
||||||
true
|
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