| 01-federation-mesh-hardening |
15 |
ui |
| vue |
| picture-in-picture |
| media |
| lifecycle |
| teleport |
|
| phase |
provides |
| 01-federation-mesh-hardening |
MediaLightbox.vue's existing lightbox shell (Teleported backdrop, video element, pip button) and utils/pip's browser-support helper |
|
|
| usePipSession(): a singleton PiP session that owns a body-level custodial host, so an adopted video survives the unmount of whatever view rendered it |
| A lightbox→PiP handoff that reads as the video moving into the PiP window rather than the lightbox being dismissed |
|
|
| added |
patterns |
|
|
| Custodial host pattern: to keep a media element alive across a view teardown, move it into an off-screen div under document.body before the teardown runs. Both Teleport and KeepAlive move their subtree on deactivation, which the picture-in-picture spec treats as removal — so the element must be re-parented above the view tree, not merely referenced. |
| Order-of-operations as a documented invariant: adopt → animate → emit close. Adopting first is what makes the element survive the unmount that the emit triggers; the comment in onEnterPip says so explicitly so a future refactor cannot reorder it innocently. |
|
|
| created |
modified |
| neode-ui/src/composables/usePipSession.ts |
| neode-ui/src/components/__tests__/MediaLightboxPip.test.ts |
|
| neode-ui/src/components/cloud/MediaLightbox.vue |
|
|
| PiP entry is detected via the element's own `enterpictureinpicture` event rather than inferred from the toolbar button, so PiP entered by any route — the browser's native control, a keyboard shortcut — takes exactly the same handoff path. |
| The backdrop's close is driven by `transitionend` with a bounded 350ms fallback timer, covering browsers that skip the transition and the reduced-motion path where the duration is zero and the event never fires. |
| `release()` is idempotent and is called both by the session's own `leavepictureinpicture` listener (the primary path, since the lightbox has usually unmounted by then) and by the component's handler for the rare still-mounted case. |
|
|
| id |
description |
requirement |
verification |
human_judgment |
| D1 |
Entering PiP closes the lightbox once, having adopted the video first so it survives the unmount |
UIFIX-05 |
| kind |
ref |
status |
| unit |
neode-ui/src/components/__tests__/MediaLightboxPip.test.ts#entering PiP emits close exactly once and adopts the video before doing so |
pass |
|
|
false |
|
| id |
description |
requirement |
verification |
human_judgment |
| D2 |
The close reads as a handoff animation on the PiP path, and as an ordinary dismissal otherwise |
UIFIX-05 |
| kind |
ref |
status |
| unit |
…#applies the handoff class on the PiP path; …#applies no handoff class on a button-driven close |
pass |
|
|
false |
|
| id |
description |
requirement |
verification |
human_judgment |
| D3 |
Leaving PiP releases the session and tears down playback |
UIFIX-05 |
| kind |
ref |
status |
| unit |
…#releases the session when picture-in-picture is left |
pass |
|
|
false |
|
| id |
description |
requirement |
verification |
human_judgment |
| D4 |
The component's public contract (props/emits) is unchanged by the rework |
UIFIX-05 |
| kind |
ref |
status |
| unit |
…#does not change props or emits declared by the component |
pass |
|
|
false |
|
|
85min |
2026-08-01 |
complete |