---
title: "Responsive and Accessible Tabs"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Responsive and Accessible Tabs}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(eval = FALSE)
```

Tabs often begin with three short labels and grow with the app. This article
keeps that larger version comfortable on a phone, with a keyboard, and under
system accessibility settings.

## A complete responsive app

```{r complete-app}
library(shiny)
library(glasstabs)

ui <- fluidPage(
  useGlassTabs(),
  glassTabsUI(
    "reports",
    glassTabPanel("summary", "Summary", selected = TRUE,
      h3("Summary"), p("A quick view of the current reporting period.")
    ),
    glassTabPanel("activity", "Recent activity",
      h3("Recent activity"), tableOutput("activity_table")
    ),
    glassTabPanel("quality", "Data quality",
      h3("Data quality"), plotOutput("quality_plot")
    ),
    glassTabPanel("settings", "Team settings",
      h3("Team settings"), textInput("team_name", "Team name")
    ),
    overflow = "scroll",
    theme = "auto"
  )
)

server <- function(input, output, session) {
  output$activity_table <- renderTable(head(mtcars))
  output$quality_plot <- renderPlot(plot(mtcars$wt, mtcars$mpg))
}

shinyApp(ui, server)
```

On a narrow screen, the labels remain in a touch-scrollable row. Keyboard
focus follows the selected tab, and a server update brings its destination
back into view.

## Long select labels on narrow screens

Select dropdowns match their trigger width and remain capped by the containing
layout. Start with the compact default and opt into a wider field only where
the content needs it:

```{r responsive-select}
glassSelect(
  "industry",
  long_industry_choices,
  width = "280px",
  overflow_tooltip = "auto"
)
```

For `glassSelect()`, an automatic overflow tooltip appears only when selected
trigger text is ellipsized. Inside either open dropdown, a truncated option
reveals a capped preview when hovered or reached with keyboard navigation.
The multi-select avoids a single combined tooltip for several selections. A
tap keeps its normal job—opening the dropdown—where option text wraps inside
the viewport.

## Choosing the overflow style

`"scroll"` is a natural fit for most tab bars. `"multiline"` is useful when every
label should be visible at once. In a horizontal layout, `"menu"` keeps the
header compact and uses a native select control. Vertical tabs already have a
dedicated rail, so compact menu mode is not available there.

```{r overflow-choices}
glassTabsUI(
  "wrapped",
  glassTabPanel("one", "First report", selected = TRUE, p("First")),
  glassTabPanel("two", "Second report", p("Second")),
  glassTabPanel("three", "Third report", p("Third")),
  overflow = "multiline"
)

glassTabsUI(
  "menu",
  glassTabPanel("one", "First report", selected = TRUE, p("First")),
  glassTabPanel("two", "Second report", p("Second")),
  overflow = "menu"
)
```

## Select keyboard and screen-reader behavior

Tabs have their own keyboard story; selects have a different one, and
it is worth stating plainly because the two widgets differ.

**Keyboard.** Tab reaches the trigger. Enter or Space opens the
dropdown; arrows then move through options and chrome rows — the
Select-all row, the Clear row, the style switcher — as one wrapping
cycle that skips hidden and disabled rows. Home and End jump to the
first and last rows. Enter or Space activates the focused row
(selecting and closing for a single-select, toggling for a
multi-select). Escape closes and returns focus to the trigger. Tab
while open closes the dropdown and exits: it never steps through
options one by one. The single-select clear button answers keys only
while its dropdown is open. Focus is always visible: chrome controls
carry visible focus rings, and `search_focus_ring = TRUE` strengthens
the search field's ring when the quiet default is not enough.

**Screen reader.** The trigger is a `combobox` labeled by its field
label, with `aria-expanded` tracking the open state and
`aria-haspopup="listbox"`. The dropdown is a `listbox` (multi-selects
add `aria-multiselectable="true"`); options are `option` rows with
`aria-selected`, and the active row is exposed through
`aria-activedescendant` so announcements follow arrow-key movement
without moving DOM focus. The Select-all row is a `checkbox` with
tri-state `aria-checked` (`true`, `false`, or `mixed` for a partial
selection); style switcher buttons carry `aria-pressed`; search
inputs carry an `aria-label`. The status row (`role="status"`,
`aria-live="polite"`) announces "Searching..." and no-match states.
Disabled rows carry `aria-disabled="true"`, are skipped by keyboard
navigation, and ignore clicks.

**What this does not claim.** The matrix below separates what
automated tests cover from what nobody has verified yet. In
particular, no screen-reader pass (NVDA or VoiceOver) has been run:
until one is, the screen-reader rows stay "not yet verified" no
matter how complete the ARIA markup looks.

| Environment | Status | Notes |
| ----------- | ------ | ----- |
| Chrome on Linux, macOS, Windows | Tested | Automated browser tests on CI: keyboard, pointer, positioning |
| Edge on Windows | Not yet verified | Chromium-based, but no CI run |
| Firefox | Not yet verified | Styling and keyboard behavior unconfirmed |
| Safari on macOS | Not yet verified | Focus and dropdown interaction unconfirmed |
| Android browsers | Not yet verified | Touch, scrolling, and viewport changes unconfirmed |
| iOS Safari | Not yet verified | Virtual keyboard and touch interactions unconfirmed |
| NVDA | Not yet verified | Manual screen-reader pass outstanding |
| VoiceOver | Not yet verified | Manual screen-reader pass outstanding |
| Reduced motion | Partially tested | Stylesheet rule plus automated tests for the dropdown close path; full manual pass pending |
| High contrast / forced colors | Partially tested | Stylesheet uses system colors and visible focus outlines; manual verification pending |

Supported dependency versions are exactly what `DESCRIPTION` states;
anything beyond that table is theoretically compatible, not tested.

## Touch swipes without surprises

`swipe = TRUE` adds left and right gestures to ordinary panel content. Inputs,
links, buttons, Shiny outputs, HTML widgets, and horizontal scrollers keep
their own touch behavior.

```{r touch}
glassTabsUI(
  "story",
  glassTabPanel("opening", "Opening", selected = TRUE, p("The first part.")),
  glassTabPanel("middle", "Middle", p("The middle part.")),
  glassTabPanel("ending", "Ending", p("The final part.")),
  swipe = TRUE
)
```

Add `data-gt-no-swipe` to any custom region that should always keep its own
gesture:

```{r no-swipe}
tags$div(`data-gt-no-swipe` = "", custom_touch_component)
```

## Motion and contrast

No R setting is needed for reduced motion. The browser preference removes the
transfer trace and badge pulse, shortens CSS transitions, and shows the next
panel without an animation delay.

Windows forced-colors mode uses system colors and visible focus outlines. When
backdrop blur is unsupported, the controls use solid surfaces instead of
losing contrast. Custom themes can set `focus_ring` in `glass_tab_theme()` and
`glass_select_theme()`; the colour follows select dropdowns when they move to
the document body, so search focus remains visible.

The dropdown search field uses a quiet border/background focus treatment by
default. Set `search_focus_ring = TRUE` when the stronger accent ring is useful.
All tongue and detached-panel transitions honor reduced-motion preferences.

## Experimental fillable page

`glassPage()` keeps a small full-page app pleasantly short:

```{r glass-page}
ui <- glassPage(
  title = "Operations",
  glassTabsUI(
    "main",
    glassTabPanel("overview", "Overview", selected = TRUE, overview_ui),
    glassTabPanel("alerts", "Alerts", alerts_ui),
    overflow = "scroll",
    theme = "auto"
  )
)
```

It is an experimental wrapper around `bslib::page_fillable()`. Plain
`fluidPage()`, `page_fillable()`, and dashboard layouts remain first-class
homes for every glasstabs widget.
