Skip to content

feat(select): add scroll indicators showing when there is hidden items - #775

Open
MichaelMure wants to merge 1 commit into
charmbracelet:mainfrom
MichaelMure:select-scroll
Open

feat(select): add scroll indicators showing when there is hidden items#775
MichaelMure wants to merge 1 commit into
charmbracelet:mainfrom
MichaelMure:select-scroll

Conversation

@MichaelMure

Copy link
Copy Markdown

Disclaimer: tool assisted change, but with heavy cleanups

Select and MultiSelect have this issue that there is no visual indication showing if there is items outside of the visible range. This leads to viewer being mislead of confused, not recognising what could be selected or what is selected at the moment. Paired with pre-selecting fields that trigger pre-scrolling, extreme case can show a single selected items while there is more items available when scrolling up.

This PR adds scrolling indicators for both Select and MultiSelect, with the behavior descrived below:

Scroll indicators for Select and MultiSelect

Basic indicator placement

When the list is taller than the visible area, arrows appear inline on the first and last visible items to signal hidden content. The field height never changes.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Pick a fruit    β”‚         Items above are hidden β†’ ↑ on first visible line
β”‚ ↑ Apple         β”‚  ← scroll up indicator, on same line as item text
β”‚   Banana        β”‚
β”‚ > Cherry        β”‚  ← cursor
β”‚   Date          β”‚
β”‚ ↓ Elderberry    β”‚  ← scroll down indicator
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         Items below are hidden β†’ ↓ on last visible line

No new rows are added β€” the arrows share the line with the option text.


No hidden content β†’ no indicator

When all items fit in the viewport, or when the cursor has reached the actual end of the list, there is simply nothing to indicate.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Pick a fruit    β”‚
β”‚   Apple         β”‚  ← no ↑: nothing is hidden above
β”‚   Banana        β”‚
β”‚   Cherry        β”‚
β”‚ > Date          β”‚  ← no ↓: nothing is hidden below
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Arrow suppressed when cursor is on the edge item

If the cursor happens to sit on the first or last visible item while content is hidden in that direction, that arrow is omitted β€” it can't share the line with the cursor.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Pick a fruit    β”‚         Items above are still hidden, but ↑ is suppressed
β”‚ > Apple         β”‚  ← cursor here, so no ↑ on this line
β”‚   Banana        β”‚
β”‚   Cherry        β”‚
β”‚ ↓ Date          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Lookahead scrolling keeps the indicator alive

When navigating, the viewport scrolls one item early so the indicator stays visible. The cursor rests at second-from-edge while more content exists in that direction.

Pressing ↓ repeatedly (items 1–8, viewport shows 4 at a time):

Step 1 β€” cursor on item 2      Step 2 β€” cursor on item 3      Step 3 β€” cursor on item 4
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Item 1    β”‚                β”‚   Item 1    β”‚                β”‚   Item 2    β”‚
β”‚ > Item 2    β”‚  ← cursor      β”‚   Item 2    β”‚                β”‚   Item 3    β”‚
β”‚   Item 3    β”‚                β”‚ > Item 3    β”‚  ← cursor      β”‚ > Item 4    β”‚  ← cursor
β”‚ ↓ Item 4    β”‚  ← indicator   β”‚ ↓ Item 4    β”‚  ← indicator   β”‚ ↓ Item 5    β”‚  ← indicator
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                               viewport scrolled

The cursor never lands on the ↓ row while more items exist below β€” the viewport shifts to keep one item ahead visible.

Once the cursor reaches the actual last item, there is nothing below, so the indicator disappears and the cursor sits at the bottom naturally:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Item 5    β”‚
β”‚   Item 6    β”‚
β”‚   Item 7    β”‚
β”‚ > Item 8    β”‚  ← last item, no ↓ indicator
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Small viewport (≀ 2 items visible) β€” no indicators

With only 1 or 2 items visible there is no room to maintain the lookahead invariant, so indicators are suppressed and normal cursor-tracking scrolling is used instead.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ > Item 3    β”‚  ← no ↑, no ↓, even if items exist above and below
β”‚   Item 4    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Inline select β€” no indicators

The horizontal single-item select already has its own ← / β†’ navigation indicators and is unaffected.

  ← Cherry β†’

Disclaimer: tool assisted change.

Select and MultiSelect have this issue that there is no visual indication showing if there is items outside of the visible range. This leads to viewer being mislead of confused, not recognising what could be selected or what is selected at the moment. Paired with pre-selecting fields that trigger pre-scrolling, extreme case can show a single selected items while there is more items available when scrolling up.

This PR adds scrolling indicators for both Select and MultiSelect, with the behavior descrived below:

## Scroll indicators for Select and MultiSelect

### Basic indicator placement

When the list is taller than the visible area, arrows appear **inline** on the first and last visible items to signal hidden content. The field height never changes.

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Pick a fruit    β”‚         Items above are hidden β†’ ↑ on first visible line
β”‚ ↑ Apple         β”‚  ← scroll up indicator, on same line as item text
β”‚   Banana        β”‚
β”‚ > Cherry        β”‚  ← cursor
β”‚   Date          β”‚
β”‚ ↓ Elderberry    β”‚  ← scroll down indicator
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         Items below are hidden β†’ ↓ on last visible line
```

No new rows are added β€” the arrows share the line with the option text.

---

### No hidden content β†’ no indicator

When all items fit in the viewport, or when the cursor has reached the actual end of the list, there is simply nothing to indicate.

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Pick a fruit    β”‚
β”‚   Apple         β”‚  ← no ↑: nothing is hidden above
β”‚   Banana        β”‚
β”‚   Cherry        β”‚
β”‚ > Date          β”‚  ← no ↓: nothing is hidden below
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

---

### Arrow suppressed when cursor is on the edge item

If the cursor happens to sit on the first or last visible item while content is hidden in that direction, that arrow is omitted β€” it can't share the line with the cursor.

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Pick a fruit    β”‚         Items above are still hidden, but ↑ is suppressed
β”‚ > Apple         β”‚  ← cursor here, so no ↑ on this line
β”‚   Banana        β”‚
β”‚   Cherry        β”‚
β”‚ ↓ Date          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

---

### Lookahead scrolling keeps the indicator alive

When navigating, the viewport scrolls **one item early** so the indicator stays visible. The cursor rests at second-from-edge while more content exists in that direction.

Pressing **↓** repeatedly (items 1–8, viewport shows 4 at a time):

```
Step 1 β€” cursor on item 2      Step 2 β€” cursor on item 3      Step 3 β€” cursor on item 4
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Item 1    β”‚                β”‚   Item 1    β”‚                β”‚   Item 2    β”‚
β”‚ > Item 2    β”‚  ← cursor      β”‚   Item 2    β”‚                β”‚   Item 3    β”‚
β”‚   Item 3    β”‚                β”‚ > Item 3    β”‚  ← cursor      β”‚ > Item 4    β”‚  ← cursor
β”‚ ↓ Item 4    β”‚  ← indicator   β”‚ ↓ Item 4    β”‚  ← indicator   β”‚ ↓ Item 5    β”‚  ← indicator
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                               viewport scrolled
```

The cursor never lands on the ↓ row while more items exist below β€” the viewport shifts to keep one item ahead visible.

Once the cursor reaches the **actual last item**, there is nothing below, so the indicator disappears and the cursor sits at the bottom naturally:

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Item 5    β”‚
β”‚   Item 6    β”‚
β”‚   Item 7    β”‚
β”‚ > Item 8    β”‚  ← last item, no ↓ indicator
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

---

### Small viewport (≀ 2 items visible) β€” no indicators

With only 1 or 2 items visible there is no room to maintain the lookahead invariant, so indicators are suppressed and normal cursor-tracking scrolling is used instead.

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ > Item 3    β”‚  ← no ↑, no ↓, even if items exist above and below
β”‚   Item 4    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

---

### Inline select β€” no indicators

The horizontal single-item select already has its own `←` / `β†’` navigation indicators and is unaffected.

```
  ← Cherry β†’
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant