---
title: "Single-Select Dropdown with glasstabs"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Single-Select Dropdown with glasstabs}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
editor_options: 
  markdown: 
    wrap: 72
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(eval = FALSE)
```


## Overview

`glassSelect()` is an animated single-select dropdown for Shiny.

For filters that accept several values, see the
[multi-select guide](multiselect.html).

It behaves like a standard Shiny input:
`input$<inputId>` stores the selected value as a single character string,
or `NULL` when nothing is selected.

It supports:

- optional labels
- search and clear controls
- built-in dark and light themes
- custom theming with `glass_select_theme()`
- selectable marker styles via `check_style`
- server-side updates with `updateGlassSelect()`
- lifecycle helpers with `closeGlassSelect()`, `input$<inputId>_open`, and `input$<inputId>_ready` (TRUE once the widget has initialized)

## Basic usage

```{r basic}
library(shiny)
library(glasstabs)

fruits <- c(
  Apple  = "apple",
  Banana = "banana",
  Cherry = "cherry",
  Mango  = "mango"
)

ui <- fluidPage(
  useGlassTabs(),
  glassSelect("pick", fruits),
  verbatimTextOutput("out")
)

server <- function(input, output, session) {
  output$out <- renderPrint(input$pick)
}

if (interactive()) shinyApp(ui, server)
```


## Initial selection

```{r initial}
# No selection (default)
glassSelect("f", fruits)

# Pre-select one value
glassSelect("f", fruits, selected = "banana")
```


### Empty-string values are ordinary values

An empty string is a valid choice value, distinct from nothing
selected: `glassSelect("status", c("All statuses" = "", "Active" =
"active"), selected = "")` starts on "All statuses" and reports
`""`, while clearing the field reports `NULL`. The same holds for
`updateGlassSelect(..., selected = "")`.

## Optional label

```{r label}
glassSelect("f", fruits, label = "Choose a fruit")
```

## Selection styles

`glassSelect()` supports three selection marker styles:

- `"checkbox"` (default)
- `"check-only"`
- `"filled"`

```{r styles}
glassSelect("f1", fruits, check_style = "checkbox")
glassSelect("f2", fruits, check_style = "check-only")
glassSelect("f3", fruits, check_style = "filled")
```

## Corner shape

By default the trigger and dropdown use the signature rounded glass corners.
Pass `shape = "square"` for crisp, selectize-style corners so the widget sits
flush next to native `selectizeInput()` controls without looking out of place:

```{r shape}
# Rounded (default)
glassSelect("region_rounded", fruits, shape = "rounded")

# Square - matches selectizeInput()
glassSelect("region_square", fruits, shape = "square")
```

The shape can also be switched at runtime with
`updateGlassSelect(session, "region_square", shape = "square")`. Here is the
complete side-by-side layout with a native Shiny control:

```{r shape-comparison}
fluidRow(
  column(
    6,
    selectizeInput("native_region", "Native", choices = fruits)
  ),
  column(
    6,
    glassSelect(
      "glass_region",
      fruits,
      label = "glasstabs",
      shape = "square",
      width = "100%"
    )
  )
)
```

## Width, grouped choices, and disabled options

For parity with native `selectizeInput()`, `glassSelect()` accepts a `width`,
grouped choices as a named list, and disabled state:

The open dropdown follows the trigger width. Longer labels wrap within that
space rather than making a short field unexpectedly wider.

```{r parity}
# Fixed / fluid width
glassSelect("region_w", fruits, width = "240px")

# Grouped choices (selectInput()-style named list)
glassSelect("food", list(
  Fruit = c(Apple = "apple", Banana = "banana"),
  Veg   = c(Carrot = "carrot", Pea = "pea")
))

# Disable the whole widget, or specific options
glassSelect("region_d", fruits, disabled = TRUE)
glassSelect("region_dc", fruits, disabled_choices = "banana")
```

`disabled` and `disabled_choices` can also be toggled at runtime with
`updateGlassSelect()`.

### Long selected labels

The default `overflow_tooltip = "auto"` adds a label preview only when the
selected trigger text is actually shortened with an ellipsis. It appears on
pointer hover or keyboard focus and is capped for exceptionally large labels;
the complete accessible name remains available. Touch users can open the
dropdown to read the label wrapped inside its matching-width panel. Truncated
options in the open dropdown also reveal a preview on hover or keyboard
navigation.

```{r long-labels}
long_industries <- c(
  "Renewable energy generation, storage, and grid-scale distribution networks" = "energy",
  Healthcare = "healthcare"
)

glassSelect(
  "industry",
  long_industries,
  selected = "energy",
  width = "240px",
  overflow_tooltip = "auto"
)
```

Use `overflow_tooltip = "never"` when the visual explanation is unnecessary.
The complete accessible label remains on the trigger in either mode.

## Dropdown lifecycle

For dynamic layouts, modals, sidebars, or tab changes, you can explicitly close
an open dropdown from the server:

```{r lifecycle}
observeEvent(input$change_layout, {
  closeGlassSelect(session, "pick")
})
```

The widget also reports whether its dropdown is open:

```{r open-state}
observe({
  message("Dropdown open: ", isTRUE(input$pick_open))
})
```

`closeAllGlassSelects(session)` closes every open glasstabs select dropdown in
the current session.

## Searchable and clearable

Search is enabled by default. Use `searchable = "auto"` when short lists should
stay simple and search should appear only after the choices reach a threshold.
You can also enable a clear control.

```{r search-clear}
glassSelect(
  "f",
  fruits,
  searchable = TRUE,
  clearable = TRUE
)
```

The Clear button is keyboard-reachable too: with the dropdown open, Arrow
Down past the last option moves focus to it (Arrow Up returns to the
options), and Enter or Space clears the selection. Tab and Escape close
the panel as usual.

For example, five categories do not need a search field when the threshold is
six:

```{r automatic-search}
glassSelect(
  "category",
  c(Alpha = "alpha", Beta = "beta", Gamma = "gamma", Delta = "delta", Epsilon = "epsilon"),
  searchable = "auto",
  search_threshold = 6
)
```

The same settings can change at runtime:

```{r update-automatic-search}
updateGlassSelect(
  session,
  "category",
  searchable = "auto",
  search_threshold = 5
)
```

Field and motion styling can be adjusted without changing the input value:

```{r presentation-options}
glassSelect(
  "f",
  fruits,
  theme = "auto",
  dark_selector = "body.app-dark",
  tongue = FALSE,
  label_bold = TRUE,
  label_size = "14px",
  search_focus_ring = FALSE
)
```

`tongue = TRUE` uses the attached glass unfurl. With `FALSE`, the panel is
detached and uses a short, native-feeling fade. The quiet search focus style is
the default; set `search_focus_ring = TRUE` for the accent outline.
When an app uses a custom dark-mode class instead of Bootstrap's
`data-bs-theme`, pass that class through `dark_selector` while using
`theme = "auto"`.


To always hide the search box:

```{r no-search}
glassSelect(
  "f",
  fruits,
  searchable = FALSE
)
```


## Server-side search for large choice sets

For hundreds or thousands of choices, set `server = TRUE` in the UI and
register `glassSelectServer()` in the server function. The browser renders
only an initial slice of choices and asks Shiny for matching choices as
the user types.

```{r server-search}
many_choices <- stats::setNames(
  sprintf("value-%04d", 1:2000),
  sprintf("Choice %04d", 1:2000)
)

ui <- fluidPage(
  useGlassTabs(),
  glassSelect(
    "pick",
    many_choices,
    selected = "value-1500",
    clearable = TRUE,
    server = TRUE,
    server_limit = 30
  ),
  verbatimTextOutput("out")
)

server <- function(input, output, session) {
  glassSelectServer("pick", many_choices, session = session, limit = 30)
  output$out <- renderPrint(input$pick)
}

if (interactive()) shinyApp(ui, server)
```

This pattern also works when the control is created dynamically with
`renderUI()`. Keep the `glassSelectServer()` call in the server function
for the same `inputId`.

The two limits are independent — see the multi-select guide's
server-search section for the full table: `server_limit` bounds the
initial slice, while the helper's `limit` bounds each search's
matches. Results are bounded, not paginated: each search returns the
first N matches, with no next-page cursor. And as with the
multi-select, `server = TRUE` with search disabled is unsupported:
without a search input the user cannot reach choices outside the
initial slice. The explicit "All" option works in server mode (see
below); it sits first in the initial slice and is restored across
search responses.

## Explicit "All" option

You can prepend an explicit "All" option:

```{r include-all}
glassSelect(
  "f",
  fruits,
  include_all = TRUE,
  all_choice_label = "All fruits",
  all_choice_value = "__all__"
)
```


This is useful when you want the UI to distinguish between:
- no selection
- a deliberate "All" selection

The option is off by default (`include_all = FALSE`). When on, the
widget prepends one row with label `all_choice_label` (default
`"All categories"`) and value `all_choice_value` (default
`"__all__"`); choosing it returns that value, not the full choice
list, so the app decides what "All" means downstream. The row is a
client-side affordance, not a data row: it survives choices updates
(the row and an active "All" selection are both preserved across
`updateGlassSelect()` calls) and it works in server mode too, where it
sits first in the initial slice and is restored across search
responses. The search threshold counts real choices only — the All row
is excluded from it.

## Server-side access

Read the current value directly from `input$<inputId>`:

```{r server-direct}
server <- function(input, output, session) {
  observe({
  message("Selected: ", if (is.null(input$pick)) "NULL" else input$pick)
})
}
```


## Reactive helper

If you want a small convenience wrapper, use `glassSelectValue()`:

```{r server-helper}
server <- function(input, output, session) {
  pick <- glassSelectValue(input, "pick")

  observe({
    print(pick())
  })
}
```


## Updating choices and selection from the server

`updateGlassSelect()` can update:
- available choices
- current selection
- selection marker style

```{r style selection}
observeEvent(input$filled, {
  updateGlassSelect(
    session,
    "pick",
    check_style = "filled"
  )
})
```

It follows Shiny-style update semantics:

- `choices = NULL` leaves choices unchanged
- `selected = NULL` leaves selection unchanged
- `selected = character(0)` clears the selection

```{r updates}
ui <- fluidPage(
  useGlassTabs(),
  actionButton("subset", "Keep first 2 fruits"),
  actionButton("banana", "Select banana"),
  actionButton("clear", "Clear"),
  actionButton("filled", "Use filled style"),
  glassSelect("pick", fruits, clearable = TRUE),
  verbatimTextOutput("out")
)

server <- function(input, output, session) {
  output$out <- renderPrint(input$pick)

  observeEvent(input$subset, {
    updateGlassSelect(
      session,
      "pick",
      choices = fruits[1:2]
    )
  })

  observeEvent(input$banana, {
    updateGlassSelect(
      session,
      "pick",
      selected = "banana"
    )
  })

  observeEvent(input$clear, {
    updateGlassSelect(
      session,
      "pick",
      selected = character(0)
    )
  })
}

if (interactive()) shinyApp(ui, server)
```


When `choices` is updated without `selected`, the widget keeps the
current value if it still exists in the new set of choices. If it no
longer exists, the selection clears and the value becomes `NULL` —
there is no stale-value state, and no separate "invalid" marker. To
keep a selection that is temporarily absent instead, do not remove it
from the choice set; the multi-select updater's `drop_invalid = FALSE`
has no single-select equivalent. Two more update rules: passing more
than one value to `selected` is an error (use the multi-select
updater for multiple values), and the label cannot be updated — there
is no `label` argument on `updateGlassSelect()`, so a label change
means recreating the widget with `renderUI()`.

## Theming

### Built-in presets

```{r presets}
glassSelect("f", fruits, theme = "dark")
glassSelect("f", fruits, theme = "light")
glassSelect("f", fruits, theme = "auto")
```

Use `theme = "auto"` inside Bootstrap 5 / bslib apps to follow
`data-bs-theme` light and dark modes without server code.


### Custom theme with `glass_select_theme()`

```{r custom-theme}
glassSelect(
  "f",
  fruits,
  theme = glass_select_theme(
    mode = "dark",
    accent_color = "#f59e0b"
  )
)

glassSelect(
  "f",
  fruits,
  theme = glass_select_theme(
    bg_color = "#1a0a2e",
    border_color = "#a855f7",
    text_color = "#ede9fe",
    accent_color = "#a855f7"
  )
)


#other themes
glassSelect(
  "f",
  fruits,
  theme = glass_select_theme(
    mode = "light",
    accent_color = "#2563eb"
  )
)

#more themes
glassSelect(
  "f",
  fruits,
  theme = glass_select_theme(
    mode = "light",
    bg_color = "#ffffff",
    border_color = "rgba(0,0,0,0.15)",
    text_color = "#111111",
    accent_color = "#111111",
    label_color = "#111111"
  )
)
```


## Standalone usage

`glassSelect()` can be used without the tab widget. You only need
`useGlassTabs()` to load the package CSS and JavaScript assets:

```{r standalone}
ui <- fluidPage(
  useGlassTabs(),
  glassSelect("region", c(North = "North", South = "South", East = "East")),
  verbatimTextOutput("out")
)

server <- function(input, output, session) {
  output$out <- renderPrint(input$region)
}

if (interactive()) shinyApp(ui, server)
```


