diff --git a/internal/design/ui/controls-design.md b/internal/design/ui/controls-design.md new file mode 100644 index 00000000..64ed15d8 --- /dev/null +++ b/internal/design/ui/controls-design.md @@ -0,0 +1,386 @@ +--- +status: draft +date: 2025-02-05 +--- + +# Controls Design + +User activity tracking and controls visibility management for Video.js 10. + +## Problem + +Video players need to: + +1. **Track user activity** — pointer movement, keyboard input, touch gestures +2. **Auto-hide controls** — fade out after inactivity while playing +3. **Keep controls visible** — when paused or interacting +4. **Expose visibility state** — for cursor hiding, overlays + +## Solution + +Split into **feature** (state management) and **component** (UI): + +| Concern | Location | Responsibility | +|---------|----------|----------------| +| Activity tracking | `controlsFeature` | Pointer/keyboard events, idle timer (internal) | +| Visibility computation | `controlsFeature` | `controlsVisible = userActive \|\| paused` | +| Auto-hide timing | `controlsFeature` | Idle timer, configurable delay | +| Layout | `` | Visual grouping of controls | + +## Quick Start + +### Feature + +```ts +const player = createPlayer({ + features: [controlsFeature], +}); + +// Read state +player.controlsVisible; // true +player.userActive; // auto-tracked +``` + +### HTML + +```ts +import '@videojs/html/ui/media-controls' +``` + +Simple (no groups): + +```html + + + + + + +``` + +With groups (time slider top, buttons bottom): + +```html + + + + + + + + + + + +``` + +### React + +```tsx +import { Controls } from '@videojs/react'; +``` + +Simple (no groups): + +```tsx + + + + + + +``` + +With groups (time slider top, buttons bottom): + +```tsx + + + + + + + + + + + +``` + +## Feature API + +### State + +```ts +interface ControlsSlice { + /** Raw activity state — true if user recently interacted */ + userActive: boolean; + + /** Computed visibility: userActive || paused */ + controlsVisible: boolean; +} +``` + +Activity tracking and input type detection are handled internally by the feature. + +## Component API + +### `` + +Container for player controls. Manages visibility based on feature state. + +#### Data Attributes + +| Attribute | Description | +|-----------|-------------| +| `[data-visible]` | Present when controls should be visible | + +### `` + +Visual grouping container. No special behavior — pure layout. + +```html + + + + + +``` + +## Behavior + +### Activity Detection + +Feature listens on the container element automatically: + +| Event | Behavior | +|-------|----------| +| `pointermove` | Set active, schedule idle | +| `pointerdown` | Record timestamp (for tap detection) | +| `pointerup` | Touch: toggle visibility. Mouse: schedule idle. | +| `keyup` | Set active, schedule idle | +| `focusin` | Set active, schedule idle | +| `mouseleave` | Immediately set inactive | + +### Touch Tap-to-Toggle + +On touch devices, tap toggles controls visibility: + +``` +pointerdown → record timestamp +pointerup → if touch && < 250ms since down: + if controlsVisible: hide + else: show + schedule idle + else (drag): no toggle +``` + +### Visibility Computation + +```ts +controlsVisible = userActive || paused +``` + +Where: +- `userActive` — True if interaction within idle timeout +- `paused` — From playback feature state + +## Styles + +Shipped CSS for common behaviors (user can override): + +```css +/* Pointer events — clicks pass through controls to video */ +media-controls { + pointer-events: none; +} + +media-controls-group { + pointer-events: auto; +} + +/* Controls visibility transition */ +media-controls { + transition: opacity 0.25s; +} + +media-controls:not([data-visible]) { + opacity: 0; +} +``` + +**Pointer events note:** Without `pointer-events: none` on controls, the overlay blocks clicks to the video. Media Chrome handles this via `::slotted()` in shadow DOM: + +```css +::slotted(:not([slot=media]):not([slot=poster])) { + pointer-events: auto; +} +``` + +## Accessibility + +### Controls Group Role + +**Decision:** Add `role="group"` only when `aria-label` or `aria-labelledby` is provided. + +```html + + + + + + + + + +``` + +**Implementation:** + +```ts +connectedCallback() { + if (this.hasAttribute('aria-label') || this.hasAttribute('aria-labelledby')) { + this.setAttribute('role', 'group'); + } +} +``` + +**Alternatives:** +- Always add `role="group"` — unlabeled groups announced as "group" (confusing) + +**Rationale:** An unlabeled `role="group"` provides no value to screen reader users and may be confusing. Conditional role based on label presence follows ARIA best practices. + +### Controls Container Role + +**Decision:** No role on `` container. + +**Rationale:** Individual controls (buttons, sliders) are the accessible elements. The controls container is a layout wrapper, not a landmark. + +**Note:** Media Chrome adds `role="region"` with `aria-label="video player"` to their **player container** (``), not the control bar. This is a player container concern — the player element (``) should handle landmark semantics, not ``. + +## Decisions + +### Activity on Container, Not Controls + +**Decision:** Track activity events on the player container, not the controls element. + +**Alternatives:** +- Track on controls — simpler, but misses activity when user moves mouse over video +- Track on document — too broad, picks up unrelated interactions + +**Rationale:** Container owns the full player area. Activity anywhere in the player should reset idle timer. Matches Media Chrome's approach. + +### Focus Resets Timer, Doesn't Prevent Hide + +**Decision:** Focus inside controls resets the idle timer but does not prevent auto-hide. + +**Alternatives:** +- Focus prevents auto-hide — more accessible, but controls never hide while focused +- Configurable via attribute — more complexity + +**Rationale:** Focus is treated like any other activity signal. Keeps behavior simple and predictable. + +### Media Type Detection (Auto-detect) + +**Decision:** Auto-detect media type from the media element. + +```ts +const mediaType = target.media?.tagName === 'AUDIO' ? 'audio' + : target.media?.videoTracks?.length === 0 ? 'audio' + : 'video'; +``` + +**Rationale:** Keep it simple. Detection from the element handles most cases. Audio-only media disables auto-hide by default. + +### Pointer Events via CSS + +**Decision:** Handle `pointer-events` passthrough in shipped CSS, not baked into components. + +**Alternatives:** +- Bake into components — always works, but awkward for audio-only or custom layouts without groups + +**Rationale:** CSS is more flexible for different layouts (with/without groups, audio-only). Default CSS targets groups, buttons, and inputs. Users can customize. Media Chrome uses a similar CSS approach with `::slotted()`. + +### Cursor Hiding via CSS + +**Decision:** Ship cursor hiding in default CSS, not baked into component. + +**Alternatives:** +- Bake into component — always works, but harder to customize + +**Rationale:** Cursor hiding is visual styling that users may want to customize or disable. CSS is more flexible and follows the cascade. + +## Descoped for Alpha + +Features reviewed but intentionally deferred: + +### Lock API (`requestControlsLock`) + +**What:** Sentinel-based API (Wake Lock pattern) for holding controls visible. + +**Use case:** Menus, popovers, and other UI that need controls to stay visible while open. + +**Why descoped:** Neither Media Chrome nor Vidstack expose a public lock API. They handle this internally. Can add if menu/popover use cases require it. + +### `reportUserActivity()` Method + +**What:** Public method to programmatically signal user activity. + +**Use case:** External controls outside the container, testing, programmatic activity. + +**Why descoped:** Activity is auto-tracked on container. Neither MC nor Vidstack expose this publicly. Add if external signaling needed. + +### `showControls()` / `hideControls()` Methods + +**What:** Imperative methods to show or hide controls. + +**Why descoped:** Neither MC nor Vidstack expose these publicly. Activity tracking is sufficient for most use cases. + +### `userInputType` in Public State + +**What:** Expose last input type (`'mouse' | 'touch' | 'pen' | 'keyboard'`) in state. + +**Why descoped:** Used internally for touch vs mouse behavior (tap-to-toggle), but neither MC nor Vidstack expose this publicly. Keep as internal implementation detail. + +### CSS Variables (`--media-controls-height`, `--media-controls-width`) + +**What:** CSS custom properties for controls dimensions. + +**Use case:** Captions positioning above controls. + +**Why descoped:** Add when captions component needs them. Neither MC nor Vidstack expose these. + +### `hideOnMouseLeave` Attribute + +**What:** Immediately hide controls when mouse leaves container. + +**Why descoped:** Vidstack has this, but it's not essential for v1. Can add later. + +### Visibility Change Event/Callback + +**What:** `controlsvisibilitychange` event or `onChange` callback. + +**Use case:** Analytics, syncing external UI. + +**Why descoped:** Store subscriptions are sufficient for internal use. Can add DOM events later for external integrations. + +### `data-input-type` on Container + +**What:** Data attribute reflecting last input type. + +**Why descoped:** Internal implementation detail. Not needed for CSS targeting in v1. + +### Toolbar Keyboard Navigation + +**What:** `role="toolbar"` with roving tabindex and arrow key navigation between controls. + +**Use case:** Reduce tab stops — one Tab to enter controls, arrow keys to navigate between buttons. + +**Why descoped:** Requires significant keyboard handling implementation. Groups work as visual containers for v1. Can upgrade to toolbar semantics later. See [WAI-ARIA Toolbar Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/). + +## References + +- [Media Chrome `media-container.ts`](https://github.com/muxinc/media-chrome) — Activity tracking implementation +- [Vidstack Controls](https://www.vidstack.io/docs/player/components/display/controls) — Similar component API +- [WAI-ARIA group role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/group_role) — Group semantics +- [WAI-ARIA Toolbar Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/) — Keyboard navigation (future) +- [Wake Lock API](https://developer.mozilla.org/en-US/docs/Web/API/WakeLock) — Sentinel pattern (descoped but documented)