Skip to content

Commit 4a247a0

Browse files
feat(core): make Select selection indicator optional
Add showSelectionIndicator to hide the selected-item marker and reclaim its two-column gutter. Preserve the existing default, restore it for nullish framework updates, and document the new option. Closes #1193 Refs #1192 Co-authored-by: leonid-shutov <leonid.shutov@hey.com>
1 parent 50fd529 commit 4a247a0

4 files changed

Lines changed: 106 additions & 27 deletions

File tree

packages/core/src/renderables/Select.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,37 @@ describe("SelectRenderable", () => {
167167
})
168168
})
169169

170+
describe("Selection Indicator", () => {
171+
test("should hide the indicator and reclaim its gutter", async () => {
172+
const { select } = await createSelectRenderable(currentRenderer, {
173+
width: 20,
174+
height: 5,
175+
options: sampleOptions,
176+
showDescription: false,
177+
showSelectionIndicator: false,
178+
})
179+
180+
expect(select.showSelectionIndicator).toBe(false)
181+
expect(captureCharFrame().split("\n")[0]).toStartWith(" Option 1")
182+
})
183+
184+
test("should restore the default when reset", async () => {
185+
const { select } = await createSelectRenderable(currentRenderer, {
186+
width: 20,
187+
height: 5,
188+
options: sampleOptions,
189+
showDescription: false,
190+
showSelectionIndicator: false,
191+
})
192+
193+
select.showSelectionIndicator = undefined
194+
await renderOnce()
195+
196+
expect(select.showSelectionIndicator).toBe(true)
197+
expect(captureCharFrame().split("\n")[0]).toStartWith(" ▶ Option 1")
198+
})
199+
})
200+
170201
describe("Options Management", () => {
171202
test("should update options dynamically", async () => {
172203
const { select } = await createSelectRenderable(currentRenderer, {

packages/core/src/renderables/Select.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface SelectRenderableOptions extends RenderableOptions<SelectRendera
5050
showScrollIndicator?: boolean
5151
wrapSelection?: boolean
5252
showDescription?: boolean
53+
showSelectionIndicator?: boolean
5354
font?: keyof typeof fonts
5455
itemSpacing?: number
5556
fastScrollStep?: number
@@ -81,6 +82,7 @@ export class SelectRenderable extends Renderable {
8182
private _showScrollIndicator: boolean
8283
private _wrapSelection: boolean
8384
private _showDescription: boolean
85+
private _showSelectionIndicator: boolean
8486
private _font?: keyof typeof fonts
8587
private _itemSpacing: number
8688
private linesPerItem: number
@@ -103,6 +105,7 @@ export class SelectRenderable extends Renderable {
103105
showScrollIndicator: false,
104106
wrapSelection: false,
105107
showDescription: true,
108+
showSelectionIndicator: true,
106109
itemSpacing: 0,
107110
fastScrollStep: 5,
108111
} satisfies Partial<SelectRenderableOptions>
@@ -122,6 +125,7 @@ export class SelectRenderable extends Renderable {
122125
this._showScrollIndicator = options.showScrollIndicator ?? this._defaultOptions.showScrollIndicator
123126
this._wrapSelection = options.wrapSelection ?? this._defaultOptions.wrapSelection
124127
this._showDescription = options.showDescription ?? this._defaultOptions.showDescription
128+
this._showSelectionIndicator = options.showSelectionIndicator ?? this._defaultOptions.showSelectionIndicator
125129
this._font = options.font
126130
this._itemSpacing = options.itemSpacing || this._defaultOptions.itemSpacing
127131

@@ -191,32 +195,33 @@ export class SelectRenderable extends Renderable {
191195
this.frameBuffer.fillRect(contentX, itemY, contentWidth, contentHeight, this._selectedBackgroundColor)
192196
}
193197

194-
const nameContent = `${isSelected ? "▶ " : " "}${option.name}`
198+
const indicator = this._showSelectionIndicator ? (isSelected ? "▶ " : " ") : ""
199+
const indicatorWidth = this._showSelectionIndicator ? 2 : 0
200+
const nameContent = `${indicator}${option.name}`
195201
const baseTextColor = this._focused ? this._focusedTextColor : this._textColor
196202
const nameColor = isSelected ? this._selectedTextColor : baseTextColor
197-
let descX = contentX + 3
203+
const textX = contentX + 1 + indicatorWidth
198204

199205
if (this._font) {
200-
const indicator = isSelected ? "▶ " : " "
201-
this.frameBuffer.drawText(indicator, contentX + 1, itemY, nameColor)
206+
if (indicator) {
207+
this.frameBuffer.drawText(indicator, contentX + 1, itemY, nameColor)
208+
}
202209

203-
const indicatorWidth = 2
204210
renderFontToFrameBuffer(this.frameBuffer, {
205211
text: option.name,
206-
x: contentX + 1 + indicatorWidth,
212+
x: textX,
207213
y: itemY,
208214
color: nameColor,
209215
backgroundColor: isSelected ? this._selectedBackgroundColor : bgColor,
210216
font: this._font,
211217
})
212-
descX = contentX + 1 + indicatorWidth
213218
} else {
214219
this.frameBuffer.drawText(nameContent, contentX + 1, itemY, nameColor)
215220
}
216221

217222
if (this._showDescription && itemY + this.fontHeight < contentY + contentHeight) {
218223
const descColor = isSelected ? this._selectedDescriptionColor : this._descriptionColor
219-
this.frameBuffer.drawText(option.description, descX, itemY + this.fontHeight, descColor)
224+
this.frameBuffer.drawText(option.description, textX, itemY + this.fontHeight, descColor)
220225
}
221226
}
222227

@@ -387,6 +392,18 @@ export class SelectRenderable extends Renderable {
387392
}
388393
}
389394

395+
public get showSelectionIndicator(): boolean {
396+
return this._showSelectionIndicator
397+
}
398+
399+
public set showSelectionIndicator(show: boolean | null | undefined) {
400+
const next = show ?? this._defaultOptions.showSelectionIndicator
401+
if (this._showSelectionIndicator !== next) {
402+
this._showSelectionIndicator = next
403+
this.requestRender()
404+
}
405+
}
406+
390407
public get wrapSelection(): boolean {
391408
return this._wrapSelection
392409
}

packages/react/tests/layout.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,35 @@ describe("React Renderer | Layout Tests", () => {
101101
})
102102
})
103103

104+
describe("Select Rendering", () => {
105+
it("should restore the selection indicator when the prop resets", async () => {
106+
let resetIndicator: () => void
107+
108+
function TestComponent() {
109+
const [showSelectionIndicator, setShowSelectionIndicator] = useState<boolean | undefined>(false)
110+
resetIndicator = () => setShowSelectionIndicator(undefined)
111+
112+
return (
113+
<select
114+
width={20}
115+
height={2}
116+
showDescription={false}
117+
showSelectionIndicator={showSelectionIndicator}
118+
options={[{ name: "Option", description: "" }]}
119+
/>
120+
)
121+
}
122+
123+
testSetup = await testRender(<TestComponent />, { width: 20, height: 2 })
124+
await testSetup.renderOnce()
125+
expect(testSetup.captureCharFrame().split("\n")[0]).toStartWith(" Option")
126+
127+
act(() => resetIndicator())
128+
await testSetup.renderOnce()
129+
expect(testSetup.captureCharFrame().split("\n")[0]).toStartWith(" ▶ Option")
130+
})
131+
})
132+
104133
describe("Box Layout Rendering", () => {
105134
it("should render basic box layout correctly", async () => {
106135
testSetup = await testRender(

packages/web/src/content/docs/components/select.mdx

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -122,25 +122,26 @@ const styledMenu = new SelectRenderable(renderer, {
122122

123123
## Properties
124124

125-
| Property | Type | Default | Description |
126-
| -------------------------- | ---------------- | ------------- | --------------------------------- |
127-
| `width` | `number` | - | Component width |
128-
| `height` | `number` | - | Component height |
129-
| `options` | `SelectOption[]` | `[]` | Available options |
130-
| `selectedIndex` | `number` | `0` | Initially selected index |
131-
| `backgroundColor` | `string \| RGBA` | `transparent` | Background color |
132-
| `textColor` | `string \| RGBA` | `#FFFFFF` | Normal text color |
133-
| `focusedBackgroundColor` | `string \| RGBA` | `#1a1a1a` | Background when focused |
134-
| `focusedTextColor` | `string \| RGBA` | `#FFFFFF` | Text color when focused |
135-
| `selectedBackgroundColor` | `string \| RGBA` | `#334455` | Selected item background |
136-
| `selectedTextColor` | `string \| RGBA` | `#FFFF00` | Selected item text color |
137-
| `descriptionColor` | `string \| RGBA` | `#888888` | Description text color |
138-
| `selectedDescriptionColor` | `string \| RGBA` | `#CCCCCC` | Selected item description color |
139-
| `showDescription` | `boolean` | `true` | Show option descriptions |
140-
| `showScrollIndicator` | `boolean` | `false` | Show scroll position indicator |
141-
| `wrapSelection` | `boolean` | `false` | Wrap selection at list boundaries |
142-
| `itemSpacing` | `number` | `0` | Spacing between items |
143-
| `fastScrollStep` | `number` | `5` | Items to skip with Shift+Up/Down |
125+
| Property | Type | Default | Description |
126+
| -------------------------- | ---------------- | ------------- | ------------------------------------ |
127+
| `width` | `number` | - | Component width |
128+
| `height` | `number` | - | Component height |
129+
| `options` | `SelectOption[]` | `[]` | Available options |
130+
| `selectedIndex` | `number` | `0` | Initially selected index |
131+
| `backgroundColor` | `string \| RGBA` | `transparent` | Background color |
132+
| `textColor` | `string \| RGBA` | `#FFFFFF` | Normal text color |
133+
| `focusedBackgroundColor` | `string \| RGBA` | `#1a1a1a` | Background when focused |
134+
| `focusedTextColor` | `string \| RGBA` | `#FFFFFF` | Text color when focused |
135+
| `selectedBackgroundColor` | `string \| RGBA` | `#334455` | Selected item background |
136+
| `selectedTextColor` | `string \| RGBA` | `#FFFF00` | Selected item text color |
137+
| `descriptionColor` | `string \| RGBA` | `#888888` | Description text color |
138+
| `selectedDescriptionColor` | `string \| RGBA` | `#CCCCCC` | Selected item description color |
139+
| `showDescription` | `boolean` | `true` | Show option descriptions |
140+
| `showScrollIndicator` | `boolean` | `false` | Show scroll position indicator |
141+
| `showSelectionIndicator` | `boolean` | `true` | Show the selection marker and gutter |
142+
| `wrapSelection` | `boolean` | `false` | Wrap selection at list boundaries |
143+
| `itemSpacing` | `number` | `0` | Spacing between items |
144+
| `fastScrollStep` | `number` | `5` | Items to skip with Shift+Up/Down |
144145

145146
## Example: file menu
146147

@@ -201,5 +202,6 @@ menu.options = [
201202
// Toggle display options
202203
menu.showDescription = false
203204
menu.showScrollIndicator = true
205+
menu.showSelectionIndicator = false
204206
menu.wrapSelection = true
205207
```

0 commit comments

Comments
 (0)