File selector files move around too much (vibe-kanban) (#122)

* I've simplified the dropdown positioning to be much more stable and consistent. The key changes:

1. **Removed complex cursor-based positioning** - Instead of trying to calculate exact character positions, the dropdown now appears consistently below (or above) the textarea
2. **Fixed reference point** - Always positions relative to the textarea edges rather than cursor position
3. **Simpler logic** - Just checks if there's enough space below, otherwise positions above
4. **Consistent spacing** - Uses a 4px gap between textarea and dropdown

This approach eliminates the positioning inconsistencies while keeping the dropdown visually connected to the textarea where the user is typing.

* Perfect! Now the dropdown positioning accounts for the actual content height when positioning above the textarea. The key changes:

1. **Calculate actual height** - Based on the number of filtered results (`filteredFiles.length * itemHeight + dropdownPadding`)
2. **Use actual height for positioning** - When positioning above, uses `actualHeight` instead of `maxDropdownHeight` to eliminate gaps
3. **Configurable constants** - Added `itemHeight` and `dropdownPadding` that can be adjusted based on your actual item styling

This ensures the dropdown appears right above the textarea without blank space, regardless of how many results are shown.

* Fixed! Now the dropdown positioning correctly uses `searchResults.length` instead of the undefined `filteredFiles` variable. The dropdown will now properly calculate its actual height based on the number of search results and position itself without gaps when appearing above the textarea.

* I've increased the `itemHeight` to 60px to better account for the actual rendered height of each item (which has `px-3 py-2` padding plus two lines of text), and reduced the `dropdownPadding` to 8px to match the `py-1` class. This should eliminate the gap when the dropdown is positioned above the textarea.

* Now I'm using a ref-based approach that measures the actual rendered height of the dropdown and repositions it accordingly. The key changes:

1. **Real height measurement** - Uses `dropdownRef.current?.getBoundingClientRect().height` to get the actual rendered height
2. **Dynamic repositioning** - Added a `useEffect` that runs when the dropdown content changes to reposition it
3. **Eliminated guesswork** - No more trying to predict the height with calculations

This should eliminate the gap completely since it uses the actual rendered dimensions rather than estimates.

* prettier
This commit is contained in:
Louis Knight-Webb
2025-07-10 15:09:06 +01:00
committed by GitHub
parent e4188ed949
commit 62380777c9

View File

@@ -169,97 +169,74 @@ export function FileSearchTextarea({
}, 0);
};
// Calculate dropdown position relative to viewport
// Calculate dropdown position relative to textarea (simpler, more stable approach)
const getDropdownPosition = () => {
if (!textareaRef.current || atSymbolPosition === -1)
return { top: 0, left: 0, maxHeight: 240 };
if (!textareaRef.current) return { top: 0, left: 0, maxHeight: 240 };
const textareaRect = textareaRef.current.getBoundingClientRect();
const textBeforeAt = value.slice(0, atSymbolPosition);
const lines = textBeforeAt.split('\n');
const currentLine = lines.length - 1;
const charInLine = lines[lines.length - 1].length;
// More accurate calculation using computed styles
const computedStyle = window.getComputedStyle(textareaRef.current);
const lineHeight = parseInt(computedStyle.lineHeight) || 20;
const fontSize = parseInt(computedStyle.fontSize) || 14;
const charWidth = fontSize * 0.6; // Approximate character width
const paddingLeft = parseInt(computedStyle.paddingLeft) || 12;
const paddingTop = parseInt(computedStyle.paddingTop) || 8;
// Position relative to textarea
const relativeTop = paddingTop + currentLine * lineHeight + lineHeight;
const relativeLeft = paddingLeft + charWidth * charInLine;
// Convert to viewport coordinates
const viewportTop = textareaRect.top + relativeTop;
const viewportLeft = textareaRect.left + relativeLeft;
// Dropdown dimensions
const dropdownWidth = 256; // min-w-64 = 256px
const maxDropdownHeight = 320;
const minDropdownHeight = 120;
const maxDropdownHeight = 400; // Increased to show more results without scrolling
let finalTop = viewportTop;
let finalLeft = viewportLeft;
// Position dropdown below the textarea by default
let finalTop = textareaRect.bottom + 4; // 4px gap
let finalLeft = textareaRect.left;
let maxHeight = maxDropdownHeight;
// Prevent going off the right edge
if (viewportLeft + dropdownWidth > window.innerWidth - 16) {
// Ensure dropdown doesn't go off the right edge
if (finalLeft + dropdownWidth > window.innerWidth - 16) {
finalLeft = window.innerWidth - dropdownWidth - 16;
}
// Prevent going off the left edge
// Ensure dropdown doesn't go off the left edge
if (finalLeft < 16) {
finalLeft = 16;
}
// Smart positioning: avoid clipping by positioning above when needed
const availableSpaceBelow = window.innerHeight - viewportTop - 32;
const availableSpaceAbove =
textareaRect.top + currentLine * lineHeight - 32;
// Calculate available space below and above textarea
const availableSpaceBelow = window.innerHeight - textareaRect.bottom - 32;
const availableSpaceAbove = textareaRect.top - 32;
// Check if dropdown would be clipped at bottom - if so, try positioning above
const wouldBeClippedBelow = availableSpaceBelow < maxDropdownHeight;
const hasEnoughSpaceAbove = availableSpaceAbove >= maxDropdownHeight;
if (wouldBeClippedBelow && hasEnoughSpaceAbove) {
// Position above the cursor line with full height
finalTop =
textareaRect.top +
paddingTop +
currentLine * lineHeight -
maxDropdownHeight;
maxHeight = maxDropdownHeight;
} else if (
wouldBeClippedBelow &&
// If not enough space below, position above
if (
availableSpaceBelow < minDropdownHeight &&
availableSpaceAbove > availableSpaceBelow
) {
// Position above but with reduced height if not enough space
finalTop =
textareaRect.top +
paddingTop +
currentLine * lineHeight -
availableSpaceAbove;
maxHeight = Math.max(availableSpaceAbove, minDropdownHeight);
// Get actual height from rendered dropdown
const actualHeight =
dropdownRef.current?.getBoundingClientRect().height ||
minDropdownHeight;
finalTop = textareaRect.top - actualHeight - 4;
maxHeight = Math.min(
maxDropdownHeight,
Math.max(availableSpaceAbove, minDropdownHeight)
);
} else {
// Position below the cursor line
// Position below with available space
maxHeight = Math.min(
maxDropdownHeight,
Math.max(availableSpaceBelow, minDropdownHeight)
);
}
// Ensure minimum height
if (maxHeight < minDropdownHeight) {
maxHeight = minDropdownHeight;
finalTop = Math.max(16, window.innerHeight - minDropdownHeight - 16);
}
return { top: finalTop, left: finalLeft, maxHeight };
};
// Use effect to reposition when dropdown content changes
useEffect(() => {
if (showDropdown && dropdownRef.current) {
// Small delay to ensure content is rendered
setTimeout(() => {
const newPosition = getDropdownPosition();
if (dropdownRef.current) {
dropdownRef.current.style.top = `${newPosition.top}px`;
dropdownRef.current.style.left = `${newPosition.left}px`;
dropdownRef.current.style.maxHeight = `${newPosition.maxHeight}px`;
}
}, 0);
}
}, [searchResults.length, showDropdown]);
const dropdownPosition = getDropdownPosition();
return (