Compare commits

...

2 Commits

Author SHA1 Message Date
Jack Wotherspoon 670d1f00ae merge: resolve conflict in BaseSelectionList and align scroll arrow implementation 2026-03-27 14:40:27 -04:00
Jack Wotherspoon e2a0aabaa5 fix: reduce height of scrollable list 2026-03-17 09:31:36 -04:00
4 changed files with 75 additions and 61 deletions
@@ -96,8 +96,14 @@ export function BaseSelectionList<
);
}
// Ensure offset is within bounds if items length changed
const maxScroll = Math.max(0, items.length - maxItemsToShow);
if (effectiveScrollOffset > maxScroll) {
effectiveScrollOffset = maxScroll;
}
// Synchronize state if it changed during derivation
if (effectiveScrollOffset !== scrollOffset) {
if (scrollOffset !== effectiveScrollOffset) {
setScrollOffset(effectiveScrollOffset);
}
@@ -107,21 +113,12 @@ export function BaseSelectionList<
);
const numberColumnWidth = String(items.length).length;
const canScrollUp = effectiveScrollOffset > 0;
const canScrollDown = effectiveScrollOffset + maxItemsToShow < items.length;
const hasScrollArrows = showScrollArrows && items.length > maxItemsToShow;
return (
<Box flexDirection="column">
{/* Use conditional coloring instead of conditional rendering */}
{showScrollArrows && items.length > maxItemsToShow && (
<Text
color={
effectiveScrollOffset > 0
? theme.text.primary
: theme.text.secondary
}
>
</Text>
)}
{visibleItems.map((item, index) => {
const itemIndex = effectiveScrollOffset + index;
const isSelected = activeIndex === itemIndex;
@@ -150,19 +147,35 @@ export function BaseSelectionList<
numberColumnWidth,
)}.`;
// Determine the indicator character for the radio column
let indicator = ' ';
let indicatorColor = theme.text.primary;
if (isSelected) {
indicator = selectedIndicator;
indicatorColor = theme.ui.focus;
} else if (hasScrollArrows && index === 0 && canScrollUp) {
indicator = '▲';
indicatorColor = theme.text.secondary;
} else if (
hasScrollArrows &&
index === visibleItems.length - 1 &&
canScrollDown
) {
indicator = '▼';
indicatorColor = theme.text.secondary;
}
return (
<Box
key={item.key}
alignItems="flex-start"
backgroundColor={isSelected ? theme.background.focus : undefined}
>
{/* Radio button indicator */}
{/* Radio button indicator (also shows scroll arrows inline) */}
<Box minWidth={2} flexShrink={0}>
<Text
color={isSelected ? theme.ui.focus : theme.text.primary}
aria-hidden
>
{isSelected ? selectedIndicator : ' '}
<Text color={indicatorColor} aria-hidden>
{indicator}
</Text>
</Box>
@@ -189,18 +202,6 @@ export function BaseSelectionList<
</Box>
);
})}
{showScrollArrows && items.length > maxItemsToShow && (
<Text
color={
effectiveScrollOffset + maxItemsToShow < items.length
? theme.text.primary
: theme.text.secondary
}
>
</Text>
)}
</Box>
);
}
@@ -172,11 +172,12 @@ export function SearchableList<T extends GenericListItem>({
item: T,
isActive: boolean,
labelWidth: number,
arrowChar?: string,
) => (
<Box flexDirection="row" alignItems="flex-start">
<Box minWidth={2} flexShrink={0}>
<Text color={isActive ? theme.status.success : theme.text.secondary}>
{isActive ? '●' : ''}
{isActive ? '●' : arrowChar || ' '}
</Text>
</Box>
<Box flexDirection="column" flexGrow={1} minWidth={0}>
@@ -226,26 +227,44 @@ export function SearchableList<T extends GenericListItem>({
</Box>
) : (
<>
{filteredItems.length > maxItemsToShow && (
<Box marginX={1}>
<Text color={theme.text.secondary}></Text>
</Box>
)}
{visibleItems.map((item, index) => {
const isSelected = activeIndex === scrollOffset + index;
const hasScrollArrows = filteredItems.length > maxItemsToShow;
const canScrollUp = scrollOffset > 0;
const canScrollDown =
scrollOffset + maxItemsToShow < filteredItems.length;
// Determine inline scroll arrow for this item position
let arrowChar = '';
if (
hasScrollArrows &&
index === 0 &&
canScrollUp &&
!isSelected
) {
arrowChar = '▲';
} else if (
hasScrollArrows &&
index === visibleItems.length - 1 &&
canScrollDown &&
!isSelected
) {
arrowChar = '▼';
}
return (
<Box key={item.key} marginBottom={1} marginX={1}>
{renderItem
? renderItem(item, isSelected, maxLabelWidth)
: defaultRenderItem(item, isSelected, maxLabelWidth)}
: defaultRenderItem(
item,
isSelected,
maxLabelWidth,
arrowChar,
)}
</Box>
);
})}
{filteredItems.length > maxItemsToShow && (
<Box marginX={1}>
<Text color={theme.text.secondary}></Text>
</Box>
)}
</>
)}
</Box>
@@ -8,28 +8,22 @@ exports[`BaseSelectionList > Scroll Arrows (showScrollArrows) > should not show
`;
exports[`BaseSelectionList > Scroll Arrows (showScrollArrows) > should show arrows and correct items when scrolled to the end 1`] = `
"▲
8. Item 8
"▲ 8. Item 8
9. Item 9
● 10. Item 10
"
`;
exports[`BaseSelectionList > Scroll Arrows (showScrollArrows) > should show arrows and correct items when scrolled to the middle 1`] = `
"▲
4. Item 4
"▲ 4. Item 4
5. Item 5
● 6. Item 6
"
`;
exports[`BaseSelectionList > Scroll Arrows (showScrollArrows) > should show arrows with correct colors when enabled (at the top) 1`] = `
"
● 1. Item 1
"● 1. Item 1
2. Item 2
3. Item 3
3. Item 3
"
`;
@@ -7,10 +7,10 @@ exports[`SearchableList > should match snapshot 1`] = `
│ Search... │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
● Item One
● Item One
Description for item one
Item Two
Item Two
Description for item two
Item Three
@@ -25,10 +25,10 @@ exports[`SearchableList > should reset selection to top when items change if res
│ Search... │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
Item One
Item One
Description for item one
● Item Two
● Item Two
Description for item two
Item Three
@@ -43,7 +43,7 @@ exports[`SearchableList > should reset selection to top when items change if res
│ One │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
● Item One
● Item One
Description for item one
"
`;
@@ -55,10 +55,10 @@ exports[`SearchableList > should reset selection to top when items change if res
│ Search... │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
● Item One
● Item One
Description for item one
Item Two
Item Two
Description for item two
Item Three