Skip to content

feat(packages): add flip functionality to popovers/tooltips/menus - #1857

Merged
sampotts merged 1 commit into
mainfrom
feat/popover-flip
Jul 28, 2026
Merged

feat(packages): add flip functionality to popovers/tooltips/menus#1857
sampotts merged 1 commit into
mainfrom
feat/popover-flip

Conversation

@sampotts

@sampotts sampotts commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds logic to flip the side based on the boundary element. The data-side attribute now reflects the resolved side rather than the preferred side.

This paves the way for the responsive skins PR where we've added controls with tooltips at the top of the player on small displays resulting in the tooltips overflowing the container.


Note

Medium Risk
Changes shared positioning for menus, popovers, and tooltips across HTML and React; behavior is well covered by tests but affects visible placement at viewport edges.

Overview
Adds collision-aware side flipping for root menus, popovers, and tooltips via new getPositionedSide in @videojs/utils/dom. The side prop stays the preferred placement; when space on that side is insufficient, the popup uses the opposite side if it has more room, and data-side is set to that resolved side (not the prop) for styling.

Positioning engine tweaks support stable flip/measure: manual top/left placement uses viewport bottom/right for those sides so side-axis size does not shift the anchor; getPopupPositionRect takes side and uses scroll width/height on the side axis for clipped content; cross-axis --media-*-available-* vars use full boundary span instead of align-trimmed width.

React adds usePositionedState so open UI exposes the measured side while closed renders reset to the preferred side. HTML and React popup components run the same measure → flip → style flow and re-measure on the JS fallback path when needed.

Docs note that data-side may differ from side after collision handling.

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

@sampotts
sampotts requested a review from mihar-22 July 21, 2026 17:22
@vercel

vercel Bot commented Jul 21, 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 22, 2026 9:26pm

Request Review

@netlify

netlify Bot commented Jul 21, 2026

Copy link
Copy Markdown

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit 2495b71
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/6a61354bfc9b0d00095cc5f0
😎 Deploy Preview https://deploy-preview-1857--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.

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

🎨 @videojs/html

Path Base initial PR initial Diff % Lazy
/audio (minimal) 41.47 kB 41.77 kB +305 B +0.7% 0 B 🔺
Presets (7)
Entry Initial Lazy
/video (default) 51.24 kB 54.24 kB
/video (default + hls) 190.51 kB 54.24 kB
/video (minimal) 51.13 kB 54.24 kB
/video (minimal + hls) 190.43 kB 54.24 kB
/audio (default) 44.47 kB 54.24 kB
/audio (minimal) 41.77 kB 54.24 kB
/background 4.20 kB
Media (10)
Entry Initial
/media/background-video 1.14 kB
/media/container 1.71 kB
/media/dash-video 214.79 kB
/media/hlsjs-video 141.52 kB
/media/mux-audio 164.00 kB
/media/mux-video 163.70 kB
/media/native-hls-video 9.06 kB
/media/simple-hls-audio-only 17.19 kB
/media/simple-hls-video 18.69 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.63 kB
/live-audio/player 5.39 kB
Skins (30)
Entry Type Initial Lazy
/video/minimal-skin.css css 5.70 kB
/video/skin.css css 5.65 kB
/video/minimal-skin js 51.14 kB 54.24 kB
/video/minimal-skin.tailwind js 51.85 kB 54.24 kB
/video/skin js 51.21 kB 54.24 kB
/video/skin.tailwind js 51.91 kB 54.24 kB
/audio/minimal-skin.css css 3.97 kB
/audio/skin.css css 3.86 kB
/audio/minimal-skin js 41.67 kB 54.24 kB
/audio/minimal-skin.tailwind js 42.21 kB 54.24 kB
/audio/skin js 44.43 kB 54.24 kB
/audio/skin.tailwind js 44.97 kB 54.24 kB
/background/skin.css css 133 B
/background/skin js 1.14 kB
/live-video/minimal-skin.css css 5.70 kB
/live-video/skin.css css 5.65 kB
/live-video/minimal-skin js 48.06 kB 54.24 kB
/live-video/minimal-skin.tailwind js 48.59 kB 54.24 kB
/live-video/skin js 49.69 kB 54.24 kB
/live-video/skin.tailwind js 50.26 kB 54.24 kB
/live-audio/minimal-skin.css css 3.97 kB
/live-audio/skin.css css 3.86 kB
/live-audio/minimal-skin js 34.02 kB 54.24 kB
/live-audio/minimal-skin.tailwind js 33.39 kB 54.24 kB
/live-audio/skin js 36.93 kB 54.24 kB
/live-audio/skin.tailwind js 36.46 kB 54.24 kB
/global.css css 183 B
/shared.css css 153 B
/tailwind.css css 161 B
/skin-element js 1.51 kB
UI Components (39)
Entry Initial
/ui/airplay-button 2.52 kB
/ui/alert-dialog 2.91 kB
/ui/alert-dialog-close 2.40 kB
/ui/alert-dialog-description 2.37 kB
/ui/alert-dialog-title 2.33 kB
/ui/audio-track-radio-group 3.05 kB
/ui/buffering-indicator 2.65 kB
/ui/captions-button 2.62 kB
/ui/captions-radio-group 3.06 kB
/ui/cast-button 2.61 kB
/ui/compounds 3.17 kB
/ui/controls 2.89 kB
/ui/error-dialog 2.90 kB
/ui/fullscreen-button 2.54 kB
/ui/hotkey 2.53 kB
/ui/menu 2.93 kB
/ui/mute-button 2.52 kB
/ui/pip-button 2.57 kB
/ui/play-button 2.51 kB
/ui/playback-rate-button 2.58 kB
/ui/playback-rate-radio-group 2.83 kB
/ui/popover 3.15 kB
/ui/poster 2.53 kB
/ui/quality-radio-group 3.00 kB
/ui/seek-button 2.51 kB
/ui/seek-indicator 2.71 kB
/ui/seek-indicator-value 431 B
/ui/slider 2.92 kB
/ui/status-announcer 2.47 kB
/ui/status-indicator 2.59 kB
/ui/status-indicator-value 361 B
/ui/thumbnail 2.57 kB
/ui/time 2.90 kB
/ui/time-slider 2.95 kB
/ui/tooltip 2.94 kB
/ui/volume-indicator 2.59 kB
/ui/volume-indicator-fill 400 B
/ui/volume-indicator-value 389 B
/ui/volume-slider 2.92 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react

Path Base initial PR initial Diff % Lazy
/video/minimal-skin 40.88 kB 41.28 kB +409 B +1.0% 0 B 🔺
/video/minimal-skin.tailwind 46.97 kB 47.43 kB +472 B +1.0% 0 B 🔺
/video/skin 40.47 kB 40.89 kB +425 B +1.0% 0 B 🔺
/video/skin.tailwind 46.63 kB 46.99 kB +365 B +0.8% 0 B 🔺
/audio/minimal-skin 33.80 kB 34.16 kB +368 B +1.1% 0 B 🔺
/audio/minimal-skin.tailwind 35.90 kB 36.39 kB +495 B +1.3% 0 B 🔺
/audio/skin 33.68 kB 34.12 kB +454 B +1.3% 0 B 🔺
/audio/skin.tailwind 37.92 kB 38.30 kB +391 B +1.0% 0 B 🔺
/live-video/minimal-skin 35.43 kB 35.88 kB +462 B +1.3% 0 B 🔺
/live-video/minimal-skin.tailwind 41.46 kB 41.80 kB +346 B +0.8% 0 B 🔺
/live-video/skin 35.44 kB 35.86 kB +429 B +1.2% 0 B 🔺
/live-audio/minimal-skin 24.25 kB 24.70 kB +453 B +1.8% 0 B 🔺
/live-audio/minimal-skin.tailwind 27.42 kB 27.81 kB +399 B +1.4% 0 B 🔺
/live-audio/skin 24.31 kB 24.71 kB +416 B +1.7% 0 B 🔺
/live-audio/skin.tailwind 27.45 kB 27.85 kB +407 B +1.4% 0 B 🔺
/ui/popover 6.25 kB 6.58 kB +345 B +5.4% 🔺
/ui/tooltip 6.89 kB 7.21 kB +325 B +4.6% 🔺
/video (default) 40.56 kB 41.01 kB +457 B +1.1% 0 B 🔺
/video (minimal) 40.94 kB 41.36 kB +429 B +1.0% 0 B 🔺
/video (minimal + hls) 179.07 kB 179.49 kB +431 B +0.2% 0 B 🔺
/audio (default) 33.76 kB 34.22 kB +477 B +1.4% 0 B 🔺
/audio (minimal) 33.80 kB 34.25 kB +463 B +1.3% 0 B 🔺
Presets (7)
Entry Initial Lazy
/video (default) 41.01 kB 54.24 kB
/video (default + hls) 179.01 kB 54.24 kB
/video (minimal) 41.36 kB 54.24 kB
/video (minimal + hls) 179.49 kB 54.24 kB
/audio (default) 34.22 kB 54.24 kB
/audio (minimal) 34.25 kB 54.24 kB
/background 579 B
Media (9)
Entry Initial
/media/background-video 403 B
/media/dash-video 212.98 kB
/media/hlsjs-video 139.79 kB
/media/mux-audio 162.38 kB
/media/mux-video 162.41 kB
/media/native-hls-video 7.26 kB
/media/simple-hls-audio-only 15.39 kB
/media/simple-hls-video 16.96 kB
/media/vimeo-video 10.44 kB
Skins (27)
Entry Type Initial Lazy
/tailwind.css css 161 B
/video/minimal-skin.css css 5.55 kB
/video/skin.css css 5.52 kB
/video/minimal-skin js 41.28 kB 54.24 kB
/video/minimal-skin.tailwind js 47.43 kB 54.24 kB
/video/skin js 40.89 kB 54.24 kB
/video/skin.tailwind js 46.99 kB 54.24 kB
/audio/minimal-skin.css css 3.79 kB
/audio/skin.css css 3.68 kB
/audio/minimal-skin js 34.16 kB 54.24 kB
/audio/minimal-skin.tailwind js 36.39 kB 54.24 kB
/audio/skin js 34.12 kB 54.24 kB
/audio/skin.tailwind js 38.30 kB 54.24 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 5.55 kB
/live-video/skin.css css 5.52 kB
/live-video/minimal-skin js 35.88 kB 54.24 kB
/live-video/minimal-skin.tailwind js 41.80 kB 54.24 kB
/live-video/skin js 35.86 kB 54.24 kB
/live-video/skin.tailwind js 41.83 kB 54.24 kB
/live-audio/minimal-skin.css css 3.79 kB
/live-audio/skin.css css 3.68 kB
/live-audio/minimal-skin js 24.70 kB 54.24 kB
/live-audio/minimal-skin.tailwind js 27.81 kB 54.24 kB
/live-audio/skin js 24.71 kB 54.24 kB
/live-audio/skin.tailwind js 27.85 kB 54.24 kB
UI Components (33)
Entry Initial
/ui/airplay-button 2.48 kB
/ui/alert-dialog 2.61 kB
/ui/audio-track 2.23 kB
/ui/buffering-indicator 2.50 kB
/ui/captions-button 2.44 kB
/ui/captions-radio-group 2.29 kB
/ui/cast-button 2.47 kB
/ui/controls 2.46 kB
/ui/error-dialog 2.48 kB
/ui/fullscreen-button 2.54 kB
/ui/gesture 2.29 kB
/ui/hotkey 2.34 kB
/ui/live-button 2.37 kB
/ui/menu 2.60 kB
/ui/mute-button 2.53 kB
/ui/pip-button 2.46 kB
/ui/play-button 2.48 kB
/ui/playback-rate 2.45 kB
/ui/playback-rate-button 2.52 kB
/ui/popover 3.06 kB
/ui/poster 2.52 kB
/ui/quality 2.25 kB
/ui/seek-button 2.48 kB
/ui/seek-indicator 2.59 kB
/ui/slider 2.67 kB
/ui/status-announcer 2.27 kB
/ui/status-indicator 2.32 kB
/ui/thumbnail 2.43 kB
/ui/time 2.19 kB
/ui/time-slider 2.50 kB
/ui/tooltip 3.08 kB
/ui/volume-indicator 2.39 kB
/ui/volume-slider 2.57 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core — no changes
Entries (68)
Entry Initial Lazy
. 10.39 kB
/dom 17.80 kB
/dom/media/custom-media-element 2.09 kB
/dom/media/dash 208.97 kB
/dom/media/google-cast 4.03 kB
/dom/media/hls-js 135.87 kB
/dom/media/media-host 1.25 kB
/dom/media/media-played-ranges 576 B
/dom/media/mux 151.37 kB
/dom/media/native-hls 3.07 kB
/dom/media/simple-hls 16.48 kB
/dom/media/simple-hls-audio-only 14.93 kB
/dom/media/vimeo 9.87 kB
/media/predicate 563 B
/i18n 2.66 kB 54.24 kB
/i18n/locales/all 30.58 kB
/i18n/locales/ar 1.21 kB
/i18n/locales/az 1.08 kB
/i18n/locales/bg 1.25 kB
/i18n/locales/bn 1.27 kB
/i18n/locales/bs 1014 B
/i18n/locales/ca 1.05 kB
/i18n/locales/cs 1.04 kB
/i18n/locales/cy 1.00 kB
/i18n/locales/da 996 B
/i18n/locales/de 1.08 kB
/i18n/locales/el 1.45 kB
/i18n/locales/en 621 B
/i18n/locales/es 1010 B
/i18n/locales/et 1.05 kB
/i18n/locales/eu 1.01 kB
/i18n/locales/fa 1.20 kB
/i18n/locales/fi 1.03 kB
/i18n/locales/fr 1.07 kB
/i18n/locales/gd 1.10 kB
/i18n/locales/gl 1014 B
/i18n/locales/he 1.12 kB
/i18n/locales/hi 1.28 kB
/i18n/locales/hr 1.02 kB
/i18n/locales/hu 1.09 kB
/i18n/locales/it 1023 B
/i18n/locales/ja 1.17 kB
/i18n/locales/ko 1.11 kB
/i18n/locales/lv 1.08 kB
/i18n/locales/mr 1.28 kB
/i18n/locales/nb 1000 B
/i18n/locales/ne 1.28 kB
/i18n/locales/nl 1013 B
/i18n/locales/nn 996 B
/i18n/locales/oc 1.05 kB
/i18n/locales/pl 1.12 kB
/i18n/locales/pt 1.01 kB
/i18n/locales/pt-BR 1.01 kB
/i18n/locales/pt-PT 1009 B
/i18n/locales/ro 1.05 kB
/i18n/locales/ru 1.33 kB
/i18n/locales/sk 1.10 kB
/i18n/locales/sl 1.02 kB
/i18n/locales/sr 1.03 kB
/i18n/locales/sv 1022 B
/i18n/locales/te 1.31 kB
/i18n/locales/th 1.29 kB
/i18n/locales/tr 1.07 kB
/i18n/locales/uk 1.36 kB
/i18n/locales/vi 1.08 kB
/i18n/locales/zh 1.01 kB
/i18n/locales/zh-CN 1.01 kB
/i18n/locales/zh-TW 1.01 kB
🏷️ @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 (11)
Entry Initial
/array 104 B
/dom 2.93 kB
/events 319 B
/function 327 B
/object 275 B
/predicate 265 B
/percent 281 B
/string 231 B
/style 190 B
/time 629 B
/number 158 B
📦 @videojs/spf — no changes
Entries (4)
Entry Initial
. 4.45 kB
/dom 6.32 kB
/hls 15.40 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.

Comment thread packages/html/src/ui/menu/menu-element.ts
Comment thread packages/core/src/dom/ui/popover/popover-positioning.ts Outdated
Comment thread packages/core/src/dom/ui/popover/popover-positioning.ts Outdated
Comment thread packages/core/src/dom/ui/popover/popover-positioning.ts Outdated
Comment thread packages/react/src/ui/popover/popover-popup.tsx Outdated
Comment thread packages/react/src/ui/menu/menu-content.tsx Outdated
Comment thread packages/react/src/ui/hooks/use-positioned-state.ts

@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.

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

Comment thread packages/react/src/ui/hooks/use-positioned-state.ts Outdated
@sampotts
sampotts merged commit be89470 into main Jul 28, 2026
26 checks passed
@sampotts
sampotts deleted the feat/popover-flip branch July 28, 2026 06:30
@luwes luwes mentioned this pull request Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants