opentui icon indicating copy to clipboard operation
opentui copied to clipboard

Allow for <select> to have no currently selected option, or allow to hide that the option is selected when the <select> is not focused

Open pizzaisdavid opened this issue 1 month ago • 6 comments

I have four <select> elements side-by-side:

Image

In the screenshot, the second <select> is focused.

I would like the possibility to have no selected option when an select isn't focused.

Meaning: no triangle, no yellow text, no blue background.

An alternative could be that the selected option looks different when the <select> is not focused compared to when it is focused.

Below is my sample code. You can change the focused <select> using the left and right arrow keys.

import { render, useKeyboard } from "@opentui/react"
import type { SelectOption } from "@opentui/core"
import { useState } from "react"

function App() {
  const [focused, setFocused] = useState<0 | 1 | 2 | 3>(0)

  useKeyboard((key) => {
    if (key.name === "left") {
      if (focused === 0) {
        // none
      } else if (focused === 1) {
        setFocused(0)
      } else if (focused === 2) {
        setFocused(1)
      } else if (focused === 3) {
        setFocused(2)
      }
    } else if (key.name === "right") {
      if (focused === 0) {
        setFocused(1)
      } else if (focused === 1) {
        setFocused(2)
      } else if (focused === 2) {
        setFocused(3)
      } else if (focused === 3) {
        // none
      }
    }
  })

  const options: SelectOption[] = [
    { name: "Option 1", description: "Option 1 description", value: "opt1" },
    { name: "Option 2", description: "Option 2 description", value: "opt2" },
    { name: "Option 3", description: "Option 3 description", value: "opt3" },
  ]

  return (
    <box style={{ flexDirection: 'row', width: '100%' }}>
      <box style={{ border: true, height: '100%', width: '25%' }}>
        <select
          style={{ height: '100%', width: '100%' }}
          options={options}
          focused={focused === 0}
        />
      </box>

      <box style={{ border: true, height: '100%', width: '25%' }}>
        <select
          style={{ height: '100%', width: '100%' }}
          options={options}
          focused={focused === 1}
        />
      </box>

      <box style={{ border: true, height: '100%', width: '25%' }}>
        <select
          style={{ height: '100%', width: '100%' }}
          options={options}
          focused={focused === 2}
        />
      </box>

      <box style={{ border: true, height: '100%', width: '25%' }}>
        <select
          style={{ height: '100%', width: '100%' }}
          options={options}
          focused={focused === 3}
        />
      </box>
    </box>
  )
}

render(<App />)

pizzaisdavid avatar Oct 23 '25 13:10 pizzaisdavid

I am trying to make a kanban board I only want one option that looks currently selected (at most) because is where the "cursor" is located. I hope that makes some sense.

pizzaisdavid avatar Oct 23 '25 13:10 pizzaisdavid

Yeah that makes sense.

kommander avatar Oct 23 '25 16:10 kommander

A bit more research:

The behavior that Textual does by default: when a <select> has a selected option, and the <select> becomes unfocused, the color of the selected option changes.

Demo:

Image

pizzaisdavid avatar Oct 23 '25 22:10 pizzaisdavid

@pizzaisdavid this can also be solved as a consumer by setting the selectedTextColor, selectedBackgroundColor and selectedDescriptionColor properties.

Example 👇
import type { SelectOption, SelectRenderableOptions } from "@opentui/core"
import { render, useKeyboard } from "@opentui/react"
import { useState } from "react"

function App() {
  const [focused, setFocused] = useState<"left" | "right">("left")

  useKeyboard((key) => {
    if (key.name === "right" || key.name === "left") {
      setFocused((prev) => (prev === "left" ? "right" : "left"))
    }
  })

  const options: SelectOption[] = [
    { name: "Option 1", description: "Description #1", value: "opt_1" },
    { name: "Option 2", description: "Description #2", value: "opt_2" },
    { name: "Option 3", description: "Description #3", value: "opt_3" },
  ]

  const focusedStyle = {
    selectedBackgroundColor: "#334455",
    selectedTextColor: "#FFFF00",
    selectedDescriptionColor: "#CCCCCC",
  } satisfies SelectRenderableOptions

  const unfocusedStyle = {
    selectedBackgroundColor: "gray",
    selectedTextColor: "white",
    selectedDescriptionColor: "white",
  } satisfies SelectRenderableOptions

  return (
    <box style={{ flexDirection: "row", width: "100%" }}>
      <box style={{ border: true, flexGrow: 1 }}>
        <select
          style={{
            height: "100%",
            width: "100%",
            ...(focused !== "left" ? unfocusedStyle : focusedStyle),
          }}
          options={options}
          focused={focused === "left"}
        />
      </box>

      <box style={{ border: true, flexGrow: 1 }}>
        <select
          style={{
            height: "100%",
            width: "100%",
            ...(focused !== "right" ? unfocusedStyle : focusedStyle),
          }}
          options={options}
          focused={focused === "right"}
        />
      </box>
    </box>
  )
}

render(<App />)
Image

msmps avatar Oct 30 '25 17:10 msmps

Can I take it?

aprogramq avatar Nov 04 '25 19:11 aprogramq

Image

import type { SelectOption } from "@opentui/core"
import { useState } from "react"

function App() {
  const [focused, setFocused] = useState<0 | 1 | 2 | 3>(0)

  useKeyboard((key) => {
    if (key.name === "left") {
      if (focused === 0) {
        // none
      } else if (focused === 1) {
        setFocused(0)
      } else if (focused === 2) {
        setFocused(1)
      } else if (focused === 3) {
        setFocused(2)
      }
    } else if (key.name === "right") {
      if (focused === 0) {
        setFocused(1)
      } else if (focused === 1) {
        setFocused(2)
      } else if (focused === 2) {
        setFocused(3)
      } else if (focused === 3) {
        // none
      }
    }
  })

  const options: SelectOption[] = [
    { name: "Option 1", description: "Option 1 description", value: "opt1" },
    { name: "Option 2", description: "Option 2 description", value: "opt2" },
    { name: "Option 3", description: "Option 3 description", value: "opt3" },
  ]

  return (
    <box style={{ flexDirection: "row", width: "100%" }}>
      <box style={{ border: true, height: "100%", width: "25%" }}>
        <select
          style={{ height: "100%", width: "100%" }}
          options={options}
          focused={focused === 0}
          selectedIndex={focused !== 0 ? -1 : 0}
        />
      </box>

      <box style={{ border: true, height: "100%", width: "25%" }}>
        <select
          style={{ height: "100%", width: "100%" }}
          options={options}
          focused={focused === 1}
          selectedIndex={focused !== 1 ? -1 : 0}
        />
      </box>

      <box style={{ border: true, height: "100%", width: "25%" }}>
        <select
          style={{ height: "100%", width: "100%" }}
          options={options}
          focused={focused === 2}
          selectedIndex={focused !== 2 ? -1 : 0}
        />
      </box>

      <box style={{ border: true, height: "100%", width: "25%" }}>
        <select
          style={{ height: "100%", width: "100%" }}
          options={options}
          focused={focused === 3}
          selectedIndex={focused !== 3 ? -1: 0}
        />
      </box>
    </box>
  );
}

render(<App />);

aprogramq avatar Nov 04 '25 23:11 aprogramq