Skip to content

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

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

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

Conversation

@mihar-22

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

Copy link
Copy Markdown
Member

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
Breaking change for consumers who hid buttons via data-availability CSS; toggle errors now propagate from core (UI still swallows at click). Touches player toolbar a11y and cross-browser behavior (PiP/cast).

Overview
Toolbar feature buttons (captions, cast, fullscreen, PiP) now expose derived disabled and hidden state alongside data-availability, instead of relying on skins to hide controls with data-availability=unavailable|unsupported.

disabled is set when the disabled prop is true or availability is not available (including cast with no device, or captions with no tracks). hidden hides unsupported APIs (fullscreen/PiP/cast) or captions when there are no tracks—HTML custom elements get native hidden plus data-hidden/data-disabled; React buttons use isSupported and render null.

Accessibility and attrs: getAttrs drives aria-disabled from state.disabled (not the prop alone) and adds data-disabled / data-hidden via state attr maps. Default skins style [aria-disabled="true"] and no longer display: none on availability.

toggle() re-checks state after setMedia, returns underlying media promises, and re-throws errors (cast/fullscreen/PiP); HTML MediaButtonElement and createMediaButton catch async failures at the click boundary (dev log only).

Docs and an internal disabled/hidden ADR document the pattern; e2e only asserts PiP data-availability when the button is visible (e.g. WebKit).

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

@netlify

netlify Bot commented Apr 28, 2026

Copy link
Copy Markdown

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit 7debc81
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/6a46af3e8088190009b07733
😎 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.
🤖 Make changes Run an agent on this branch

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

@vercel

vercel Bot commented Apr 28, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment Jul 2, 2026 6:35pm

Request Review

@github-actions

github-actions Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

🎨 @videojs/html — no changes
Presets (7)
Entry Initial
/video (default) 46.32 kB
/video (default + hls) 186.02 kB
/video (minimal) 46.32 kB
/video (minimal + hls) 186.04 kB
/audio (default) 39.85 kB
/audio (minimal) 38.93 kB
/background 4.20 kB
Media (10)
Entry Initial
/media/background-video 1.14 kB
/media/container 1.72 kB
/media/dash-video 214.72 kB
/media/hlsjs-video 141.49 kB
/media/mux-audio 163.89 kB
/media/mux-video 163.96 kB
/media/native-hls-video 9.05 kB
/media/simple-hls-audio-only 17.15 kB
/media/simple-hls-video 18.72 kB
/media/vimeo-video 12.31 kB
Players (5)
Entry Initial
/video/player 8.22 kB
/audio/player 5.38 kB
/background/player 3.92 kB
/live-video/player 7.64 kB
/live-audio/player 5.39 kB
Skins (30)
Entry Type Initial
/video/minimal-skin.css css 5.66 kB
/video/skin.css css 5.61 kB
/video/minimal-skin js 46.29 kB
/video/minimal-skin.tailwind js 47.01 kB
/video/skin js 46.29 kB
/video/skin.tailwind js 47.14 kB
/audio/minimal-skin.css css 3.92 kB
/audio/skin.css css 3.81 kB
/audio/minimal-skin js 38.93 kB
/audio/minimal-skin.tailwind js 39.44 kB
/audio/skin js 39.80 kB
/audio/skin.tailwind js 40.30 kB
/background/skin.css css 133 B
/background/skin js 1.14 kB
/live-video/minimal-skin.css css 5.66 kB
/live-video/skin.css css 5.61 kB
/live-video/minimal-skin js 45.23 kB
/live-video/minimal-skin.tailwind js 45.86 kB
/live-video/skin js 44.95 kB
/live-video/skin.tailwind js 45.58 kB
/live-audio/minimal-skin.css css 3.92 kB
/live-audio/skin.css css 3.81 kB
/live-audio/minimal-skin js 31.58 kB
/live-audio/minimal-skin.tailwind js 30.98 kB
/live-audio/skin js 32.53 kB
/live-audio/skin.tailwind js 32.02 kB
/global.css css 176 B
/shared.css css 104 B
/tailwind.css css 161 B
/skin-element js 1.45 kB
UI Components (39)
Entry Initial
/ui/airplay-button 2.39 kB
/ui/alert-dialog 2.58 kB
/ui/alert-dialog-close 2.23 kB
/ui/alert-dialog-description 2.19 kB
/ui/alert-dialog-title 2.22 kB
/ui/audio-track-radio-group 2.90 kB
/ui/buffering-indicator 2.32 kB
/ui/captions-button 2.44 kB
/ui/captions-radio-group 2.88 kB
/ui/cast-button 2.44 kB
/ui/compounds 2.99 kB
/ui/controls 2.74 kB
/ui/error-dialog 2.75 kB
/ui/fullscreen-button 2.41 kB
/ui/hotkey 2.35 kB
/ui/menu 2.78 kB
/ui/mute-button 2.40 kB
/ui/pip-button 2.42 kB
/ui/play-button 2.41 kB
/ui/playback-rate-button 2.42 kB
/ui/playback-rate-radio-group 2.83 kB
/ui/popover 2.77 kB
/ui/poster 2.31 kB
/ui/quality-radio-group 2.84 kB
/ui/seek-button 2.43 kB
/ui/seek-indicator 2.40 kB
/ui/seek-indicator-value 532 B
/ui/slider 2.78 kB
/ui/status-announcer 2.35 kB
/ui/status-indicator 2.49 kB
/ui/status-indicator-value 481 B
/ui/thumbnail 2.33 kB
/ui/time 2.75 kB
/ui/time-slider 2.79 kB
/ui/tooltip 2.73 kB
/ui/volume-indicator 2.38 kB
/ui/volume-indicator-fill 519 B
/ui/volume-indicator-value 458 B
/ui/volume-slider 2.78 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react — no changes
Presets (7)
Entry Initial
/video (default) 38.38 kB
/video (default + hls) 176.74 kB
/video (minimal) 38.74 kB
/video (minimal + hls) 177.15 kB
/audio (default) 31.56 kB
/audio (minimal) 31.63 kB
/background 754 B
Media (9)
Entry Initial
/media/background-video 575 B
/media/dash-video 213.16 kB
/media/hlsjs-video 140.07 kB
/media/mux-audio 162.52 kB
/media/mux-video 162.57 kB
/media/native-hls-video 7.41 kB
/media/simple-hls-audio-only 15.56 kB
/media/simple-hls-video 17.14 kB
/media/vimeo-video 10.58 kB
Skins (27)
Entry Type Initial
/tailwind.css css 161 B
/video/minimal-skin.css css 5.56 kB
/video/skin.css css 5.51 kB
/video/minimal-skin js 38.67 kB
/video/minimal-skin.tailwind js 44.77 kB
/video/skin js 38.30 kB
/video/skin.tailwind js 44.43 kB
/audio/minimal-skin.css css 3.79 kB
/audio/skin.css css 3.67 kB
/audio/minimal-skin js 31.54 kB
/audio/minimal-skin.tailwind js 33.73 kB
/audio/skin js 31.50 kB
/audio/skin.tailwind js 35.73 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 5.56 kB
/live-video/skin.css css 5.51 kB
/live-video/minimal-skin js 33.56 kB
/live-video/minimal-skin.tailwind js 39.46 kB
/live-video/skin js 33.52 kB
/live-video/skin.tailwind js 39.51 kB
/live-audio/minimal-skin.css css 3.79 kB
/live-audio/skin.css css 3.67 kB
/live-audio/minimal-skin js 22.48 kB
/live-audio/minimal-skin.tailwind js 25.59 kB
/live-audio/skin js 22.53 kB
/live-audio/skin.tailwind js 25.66 kB
UI Components (33)
Entry Initial
/ui/airplay-button 2.17 kB
/ui/alert-dialog 2.11 kB
/ui/audio-track 2.06 kB
/ui/buffering-indicator 2.09 kB
/ui/captions-button 2.20 kB
/ui/captions-radio-group 2.08 kB
/ui/cast-button 2.14 kB
/ui/controls 2.01 kB
/ui/error-dialog 2.19 kB
/ui/fullscreen-button 2.20 kB
/ui/gesture 2.23 kB
/ui/hotkey 2.20 kB
/ui/live-button 2.12 kB
/ui/menu 2.30 kB
/ui/mute-button 2.23 kB
/ui/pip-button 2.20 kB
/ui/play-button 2.15 kB
/ui/playback-rate 2.01 kB
/ui/playback-rate-button 2.22 kB
/ui/popover 2.61 kB
/ui/poster 2.00 kB
/ui/quality 2.05 kB
/ui/seek-button 2.17 kB
/ui/seek-indicator 2.05 kB
/ui/slider 2.27 kB
/ui/status-announcer 2.11 kB
/ui/status-indicator 2.07 kB
/ui/thumbnail 2.04 kB
/ui/time 2.01 kB
/ui/time-slider 2.22 kB
/ui/tooltip 2.62 kB
/ui/volume-indicator 2.04 kB
/ui/volume-slider 2.22 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core — no changes
Entries (14)
Entry Initial
. 9.45 kB
/dom 17.58 kB
/dom/media/custom-media-element 2.09 kB
/dom/media/dash 208.96 kB
/dom/media/google-cast 4.04 kB
/dom/media/hls-js 136.00 kB
/dom/media/media-host 1.25 kB
/dom/media/media-played-ranges 576 B
/dom/media/mux 151.26 kB
/dom/media/native-hls 3.07 kB
/dom/media/simple-hls 16.49 kB
/dom/media/simple-hls-audio-only 14.92 kB
/dom/media/vimeo 9.86 kB
/media/predicate 563 B
🏷️ @videojs/element — no changes
Entries (2)
Entry Initial
. 996 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Initial
. 1.39 kB
/html 696 B
/react 360 B
🔧 @videojs/utils — no changes
Entries (10)
Entry Initial
/array 104 B
/dom 2.26 kB
/events 319 B
/function 327 B
/object 275 B
/predicate 265 B
/string 231 B
/style 190 B
/time 478 B
/number 158 B
📦 @videojs/spf — no changes
Entries (4)
Entry Initial
. 4.45 kB
/dom 6.33 kB
/hls 15.37 kB
/background-video 12.85 kB

ℹ️ How to interpret

JS sizes are initial static graph totals (minified + brotli). Lazy dynamic chunks are shown separately when present.

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

Run pnpm size locally to check current initial sizes.

@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
Comment thread packages/core/src/core/ui/cast-button/cast-button-core.ts Outdated
Comment thread packages/html/src/ui/media-button-element.ts Outdated
Comment thread packages/skins/src/default/css/components/button.css
Comment thread packages/skins/src/default/css/components/button.css Outdated
mihar-22 and others added 11 commits July 1, 2026 11:59
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>
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>
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>
…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>
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>
Promise.resolve(action(core, feature!)).catch((error) => {
if (__DEV__) console.error(`[${displayName}]`, error);
});
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Core disabled not wired to clicks

Medium Severity

createMediaButton and MediaButtonElement pass createButton an isDisabled check that only considers the disabled prop and media presence, not *Core’s derived state.disabled. After this change, controls like cast can stay visible with aria-disabled and data-disabled while pointer and keyboard activation still run (toggle no-ops). Previously data-availability="unavailable" hid them in CSS.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by learned rule: New UI feature logic belongs in *Core classes — React and HTML layers delegate

Reviewed by Cursor Bugbot for commit d5c1de0. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 using default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

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 7debc81. Configure here.

protected abstract readonly mediaState: PlayerController<any, InferMediaState<Core> | undefined>;

protected abstract activate(state: InferMediaState<Core>, event?: UIEvent): void;
protected abstract activate(state: InferMediaState<Core>, event?: UIEvent): void | Promise<void>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Interaction ignores core disabled state

Medium Severity

createButton / useButton still treat only the disabled prop (and missing media) as non-interactive, while cores now set aria-disabled and data-disabled from getState().disabled (including cast unavailable and explicit prop). Pointer and keyboard activation can still run even when the control is marked disabled.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by learned rule: New UI feature logic belongs in *Core classes — React and HTML layers delegate

Reviewed by Cursor Bugbot for commit 7debc81. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants