Skip to content

refactor(packages)!: replace button availability with disabled and hidden state#1474

Open
mihar-22 wants to merge 12 commits intomainfrom
refactor/button-disabled-hidden
Open

refactor(packages)!: replace button availability with disabled and hidden state#1474
mihar-22 wants to merge 12 commits intomainfrom
refactor/button-disabled-hidden

Conversation

@mihar-22
Copy link
Copy Markdown
Member

@mihar-22 mihar-22 commented Apr 28, 2026

Replace the available boolean on buttons with disabled and hidden states that follow standard ARIA patterns — aria-disabled for non-interactive controls and HTML hidden for unsupported features.


Note

Medium Risk
Changes core interaction semantics for multiple control buttons (visibility/disabled behavior and error propagation), which could affect consumers’ styling and runtime behavior across browsers despite being mostly deterministic and well-tested.

Overview
Refactors the core CastButton, FullscreenButton, PiPButton, and CaptionsButton to derive two new state fieldsdisabled and hidden—from props + availability, and uses these to set aria-disabled and the native HTML hidden attribute.

Updates data-attribute mapping and styling to use data-disabled/data-hidden (instead of hiding via data-availability CSS), adds React createMediaButton support for async actions plus an isSupported gate (feature buttons render null when hidden), and adjusts HTML MediaButtonElement/button elements to safely invoke async activate() while logging (dev-only) and preventing unhandled promise rejections.

Button toggle() implementations now short-circuit via derived state and propagate underlying API errors (tests updated accordingly); docs and e2e assertions are updated for WebKit PiP being hidden when unsupported, and a design decision doc is added to codify the pattern.

Reviewed by Cursor Bugbot for commit 8bcff00. Bugbot is set up for automated code reviews on this repo. Configure here.

mihar-22 and others added 5 commits April 27, 2026 22:58
Covers the rationale for using aria-disabled over HTML disabled, HTML
hidden for unsupported features, and separate data-disabled/data-hidden
styling hooks across cast, fullscreen, and pip buttons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…dden state

Cast, fullscreen, and pip buttons now expose `disabled` (non-interactive)
and `hidden` (unsupported) state derived from `availability` and the
`disabled` prop, instead of relying on the raw availability enum at the
attribute layer.

- `getAttrs` returns `aria-disabled` from state and the native HTML
  `hidden` attribute when the feature is unsupported. New `data-disabled`
  and `data-hidden` data attribute mappings ride along.
- `toggle` short-circuits on `state.disabled` and otherwise awaits the
  underlying media call directly, propagating errors to the caller
  instead of swallowing them.
- `MediaButtonElement` and `createMediaButton` now wrap the activation
  in try/catch with a `__DEV__` console.error and rethrow so callers
  see the original failure.
- React buttons pass `isSupported: (s) => !s.hidden` so unsupported
  features render `null` rather than a hidden `<button>`.

Aligns with the WAI-ARIA APG toolbar pattern (focusable disabled
controls) documented in `internal/design/ui/disabled-hidden.md`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace `data-[availability=...]:hidden` with `data-[disabled]` styling
classes to match the new disabled/hidden button state model. Hidden
buttons use the native HTML hidden attribute; disabled buttons get
reduced opacity and grayscale via data-disabled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PiP is unsupported on WebKit so the button receives the `hidden`
attribute and is removed from the layout. Only assert
`data-availability` when the pip button is visible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tons

Update the fullscreen and pip button reference pages plus the features
concept page to describe the new `disabled`/`hidden` state model:
HTML `hidden` for unsupported environments (or `null` in React) and
`data-disabled` for non-interactive styling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 28, 2026

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit 8bcff00
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/69f05d3297e5cf00086593dd
😎 Deploy Preview https://deploy-preview-1474--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment Apr 28, 2026 7:10am

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

📦 Bundle Size Report

🎨 @videojs/html — no changes
Presets (7)
Entry Size
/video (default) 28.88 kB
/video (default + hls) 162.28 kB
/video (minimal) 26.36 kB
/video (minimal + hls) 159.80 kB
/audio (default) 26.66 kB
/audio (minimal) 24.29 kB
/background 4.16 kB
Media (8)
Entry Size
/media/background-video 1.04 kB
/media/container 1.72 kB
/media/dash-video 236.58 kB
/media/hls-video 134.69 kB
/media/mux-audio 160.91 kB
/media/mux-video 160.85 kB
/media/native-hls-video 4.62 kB
/media/simple-hls-video 15.89 kB
Players (3)
Entry Size
/video/player 7.00 kB
/audio/player 5.12 kB
/background/player 3.86 kB
Skins (29)
Entry Type Size
/video/minimal-skin.css css 3.54 kB
/video/skin.css css 3.56 kB
/video/minimal-skin js 26.34 kB
/video/minimal-skin.tailwind js 26.57 kB
/video/skin js 28.87 kB
/video/skin.tailwind js 29.01 kB
/audio/minimal-skin.css css 2.57 kB
/audio/skin.css css 2.53 kB
/audio/minimal-skin js 24.26 kB
/audio/minimal-skin.tailwind js 24.43 kB
/audio/skin js 26.68 kB
/audio/skin.tailwind js 26.85 kB
/background/skin.css css 117 B
/background/skin js 1.15 kB
/live-video/minimal-skin.css css 3.54 kB
/live-video/skin.css css 3.56 kB
/live-video/minimal-skin js 26.10 kB
/live-video/minimal-skin.tailwind js 26.20 kB
/live-video/skin js 28.57 kB
/live-video/skin.tailwind js 28.62 kB
/live-audio/minimal-skin.css css 2.57 kB
/live-audio/skin.css css 2.53 kB
/live-audio/minimal-skin js 24.09 kB
/live-audio/minimal-skin.tailwind js 24.04 kB
/live-audio/skin js 26.44 kB
/live-audio/skin.tailwind js 26.50 kB
/base.css css 157 B
/shared.css css 88 B
/skin-element js 1.36 kB
UI Components (25)
Entry Size
/ui/alert-dialog 1018 B
/ui/alert-dialog-close 504 B
/ui/alert-dialog-description 369 B
/ui/alert-dialog-title 396 B
/ui/buffering-indicator 2.48 kB
/ui/captions-button 2.64 kB
/ui/cast-button 2.64 kB
/ui/compounds 4.18 kB
/ui/controls 2.06 kB
/ui/error-dialog 3.08 kB
/ui/fullscreen-button 2.57 kB
/ui/hotkey 1.93 kB
/ui/mute-button 2.61 kB
/ui/pip-button 2.61 kB
/ui/play-button 2.59 kB
/ui/playback-rate-button 2.76 kB
/ui/popover 1.88 kB
/ui/poster 1.88 kB
/ui/seek-button 2.55 kB
/ui/slider 1.52 kB
/ui/thumbnail 2.98 kB
/ui/time 2.56 kB
/ui/time-slider 4.00 kB
/ui/tooltip 2.02 kB
/ui/volume-slider 2.72 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react — no changes
Presets (7)
Entry Size
/video (default) 23.61 kB
/video (default + hls) 155.71 kB
/video (minimal) 21.23 kB
/video (minimal + hls) 153.36 kB
/audio (default) 19.12 kB
/audio (minimal) 17.66 kB
/background 756 B
Media (7)
Entry Size
/media/background-video 575 B
/media/dash-video 235.21 kB
/media/hls-video 133.34 kB
/media/mux-audio 159.31 kB
/media/mux-video 159.49 kB
/media/native-hls-video 3.13 kB
/media/simple-hls-video 14.44 kB
Skins (26)
Entry Type Size
/video/minimal-skin.css css 3.47 kB
/video/skin.css css 3.49 kB
/video/minimal-skin js 21.13 kB
/video/minimal-skin.tailwind js 24.65 kB
/video/skin js 23.49 kB
/video/skin.tailwind js 24.79 kB
/audio/minimal-skin.css css 2.46 kB
/audio/skin.css css 2.42 kB
/audio/minimal-skin js 17.60 kB
/audio/minimal-skin.tailwind js 20.09 kB
/audio/skin js 19.04 kB
/audio/skin.tailwind js 20.10 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 3.47 kB
/live-video/skin.css css 3.49 kB
/live-video/minimal-skin js 17.85 kB
/live-video/minimal-skin.tailwind js 21.28 kB
/live-video/skin js 20.23 kB
/live-video/skin.tailwind js 21.45 kB
/live-audio/minimal-skin.css css 2.46 kB
/live-audio/skin.css css 2.42 kB
/live-audio/minimal-skin js 15.80 kB
/live-audio/minimal-skin.tailwind js 18.10 kB
/live-audio/skin js 17.27 kB
/live-audio/skin.tailwind js 18.23 kB
UI Components (20)
Entry Size
/ui/alert-dialog 860 B
/ui/buffering-indicator 2.43 kB
/ui/captions-button 2.60 kB
/ui/cast-button 2.58 kB
/ui/controls 2.33 kB
/ui/error-dialog 2.89 kB
/ui/fullscreen-button 2.61 kB
/ui/mute-button 2.60 kB
/ui/pip-button 2.63 kB
/ui/play-button 2.59 kB
/ui/playback-rate-button 2.60 kB
/ui/popover 1.48 kB
/ui/poster 2.17 kB
/ui/seek-button 2.64 kB
/ui/slider 4.03 kB
/ui/thumbnail 2.57 kB
/ui/time 1.84 kB
/ui/time-slider 3.63 kB
/ui/tooltip 1.72 kB
/ui/volume-slider 3.12 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core — no changes
Entries (9)
Entry Size
. 4.98 kB
/dom 11.93 kB
/dom/media/custom-media-element 1.90 kB
/dom/media/dash 234.36 kB
/dom/media/google-cast 4.07 kB
/dom/media/hls 132.72 kB
/dom/media/mux 158.81 kB
/dom/media/native-hls 2.52 kB
/dom/media/simple-hls 13.83 kB
🏷️ @videojs/element — no changes
Entries (2)
Entry Size
. 996 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Size
. 1.39 kB
/html 695 B
/react 360 B
🔧 @videojs/utils — no changes
Entries (10)
Entry Size
/array 104 B
/dom 1.92 kB
/events 319 B
/function 327 B
/object 275 B
/predicate 265 B
/string 148 B
/style 190 B
/time 478 B
/number 158 B
📦 @videojs/spf — no changes
Entries (3)
Entry Size
. 40 B
/dom 13.33 kB
/playback-engine 13.24 kB

ℹ️ How to interpret

All sizes are standalone totals (minified + brotli).

Icon Meaning
��� No change
🔺 Increased ≤ 10%
🔴 Increased > 10%
🔽 Decreased
🆕 New (no baseline)

Run pnpm size locally to check current sizes.

Document the two pre-existing fields alongside the newly added
disabled/hidden so the component reference table renders complete
descriptions for every cast button state property.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mihar-22 mihar-22 marked this pull request as ready for review April 28, 2026 06:19
@mihar-22 mihar-22 requested a review from luwes April 28, 2026 06:20
Comment thread packages/skins/src/default/css/components/button.css
The skin's `display: flex` (and `grid` on the icon variant) outranks the
user-agent `[hidden] { display: none }` rule on specificity, so feature
buttons stayed visible when the cast/fullscreen/pip cores set the native
`hidden` attribute.

Add a `&[hidden] { display: none }` rule under the high-specificity
skin selector in both default and minimal CSS, and a `[&[hidden]]:hidden`
class in the Tailwind variants so the same override works for the
Tailwind-compiled skins.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread packages/core/src/core/ui/cast-button/cast-button-core.ts
Comment thread packages/html/src/ui/media-button-element.ts Outdated
…omponent override

Move the native `hidden` attribute override from the button component into
the skin reset so any authored template that sets `[hidden]` stays hidden,
not just media buttons. Use the doubled `[hidden][hidden]` selector under
the skin root to outrank component-level `display: flex/grid` declarations.
The Tailwind root composition gets the equivalent `[&_[hidden][hidden]]:hidden`
class.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mihar-22 and others added 2 commits April 27, 2026 23:52
Initial `availability` is `'unavailable'` until the cast feature reports
otherwise, so the derived `disabled` flag must also start `true` to
match the invariant `disabled = props.disabled || availability !== 'available'`.
Anyone reading `core.state.current` before the first `getState()` call
now sees a consistent non-interactive state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
`createButton` / `useButton` invoke `onActivate` synchronously from click
and keyup handlers, so re-throwing from the wrapper produced unhandled
promise rejections for benign user actions like cancelling a cast prompt
or being denied fullscreen.

Log the failure in `__DEV__` for visibility and let it stop there.
Callers that await `core.toggle(...)` directly still observe the
rejection — only the synchronous UI boundary absorbs it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bdafdc6. Configure here.

Comment thread packages/skins/src/default/css/components/button.css
Comment thread packages/skins/src/default/css/components/button.css
mihar-22 and others added 2 commits April 28, 2026 00:09
The captions button previously relied on the now-removed
`data-availability="unavailable"` skin rule to hide itself when no
caption tracks were present. Extend the same `disabled`/`hidden` model
already applied to cast/fullscreen/pip buttons so it stays hidden in
that state without depending on availability-specific CSS.

- Add `disabled` and `hidden` to `CaptionsButtonState`, derived from
  the `disabled` prop and whether any caption/subtitle tracks exist.
- `getAttrs` returns `aria-disabled` from state and the native HTML
  `hidden` attribute when no tracks are available.
- `toggle` short-circuits on `state.disabled`.
- `data-disabled` and `data-hidden` data attribute mappings ride along.
- React captions button passes `isSupported: (s) => !s.hidden` so it
  renders `null` when no tracks are present.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously unavailable buttons used `display: none`, which masked the
`:active { scale: 0.98 }` rule. Now that `[data-disabled]` keeps the
button visible but non-interactive, guard the press animation with
`:not([disabled]):not([data-disabled])` (and the Tailwind equivalent
`not-disabled:not-data-disabled:active:*`) so disabled buttons no
longer give misleading press feedback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants