xrdwb17xu: Hide native video controls until play and move the author aside while a slide video plays

This commit is contained in:
2026-10-20 08:24:45 -06:00
parent bf5e0ed475
commit adcd0cfd96
4 changed files with 38 additions and 5 deletions
+13 -3
View File
@@ -54,7 +54,7 @@ function SlideMedia({ media, author }: { media: CardMedia; author: string }) {
return (
<div className="slide-media slide-video" ref={box}>
<div className="slide-art">
<VideoPlayer mp4Url={media.mp4Url} hlsUrl={media.hlsUrl} poster={media.src} label={`Video from ${author}`} />
<VideoPlayer mp4Url={media.mp4Url} hlsUrl={media.hlsUrl} poster={media.src} label={`Video from ${author}`} controls={started} />
</div>
{started ? null : (
<>
@@ -85,9 +85,19 @@ function SlideMedia({ media, author }: { media: CardMedia; author: string }) {
function PostSlide({ post, index, current, p }: { post: CardPost; index: number; current: boolean; p: FeedViewProps }) {
const media = post.media.find((m) => m.kind === "video" || m.src) ?? null;
const own = canDelete(post, p.feedUserId) && !!p.onDelete;
const cls = `slide ${media ? "slide-on-media" : "slide-on-text"}${current ? " is-cur" : ""}`;
// While a video plays, the author and text step aside so the native controls stay clear.
const [playing, setPlaying] = useState(false);
const cls = `slide ${media ? "slide-on-media" : "slide-on-text"}${current ? " is-cur" : ""}${playing ? " slide-playing" : ""}`;
return (
<section className={cls} data-index={index} data-testid="feed-slide" aria-label={`Post by ${post.authorName}`}>
<section
className={cls}
data-index={index}
data-testid="feed-slide"
aria-label={`Post by ${post.authorName}`}
onPlayCapture={() => setPlaying(true)}
onPauseCapture={() => setPlaying(false)}
onEndedCapture={() => setPlaying(false)}
>
{media ? (
<>
<SlideMedia media={media} author={post.authorName} />
+4 -2
View File
@@ -8,6 +8,8 @@ export interface VideoPlayerProps {
hlsUrl?: string | null;
poster: string | null;
label: string;
/** Native controls; off until the caller's own play button starts the video. Default on. */
controls?: boolean;
}
const UNPLAYABLE = "Video can't be played here.";
@@ -18,7 +20,7 @@ const UNPLAYABLE = "Video can't be played here.";
* unsupported), else thumbnail only. Never
* autoplays. Any failure falls back to the thumbnail with a short note.
*/
export function VideoPlayer({ mp4Url, hlsUrl, poster, label }: VideoPlayerProps) {
export function VideoPlayer({ mp4Url, hlsUrl, poster, label, controls = true }: VideoPlayerProps) {
const ref = useRef<HTMLVideoElement>(null);
const [failed, setFailed] = useState(false);
@@ -77,7 +79,7 @@ export function VideoPlayer({ mp4Url, hlsUrl, poster, label }: VideoPlayerProps)
<video
ref={ref}
src={mp4Url ?? undefined}
controls
controls={controls}
playsInline
preload="none"
poster={poster ?? undefined}
+11
View File
@@ -61,6 +61,15 @@
.slide-scrim { bottom: 0; height: 58%; background: linear-gradient(180deg, var(--media-fade-0), var(--media-fade)); }
.slide-scrim-top { top: 0; height: 150px; background: linear-gradient(180deg, var(--media-fade-top), var(--media-fade-0)); }
/* A playing video: the author, text and fades step aside so the native controls are clear. */
.slide-meta,
.slide-scrim,
.slide-scrim-top { transition: opacity .3s var(--ease); }
.slide-playing .slide-meta,
.slide-playing .slide-scrim,
.slide-playing .slide-scrim-top { opacity: 0; pointer-events: none; }
.slide-playing .slide-meta > * { pointer-events: none; }
/* Author and text over the bottom fade. */
.slide-meta {
position: absolute;
@@ -127,6 +136,8 @@
/* The slide on screen: its content rises in when it snaps into place. */
.is-cur .slide-meta,
.is-cur .slide-quote-w { animation: slide-rise .7s var(--ease) both; }
/* The rise animation's fill would keep the meta visible; a playing video takes it off. */
.is-cur.slide-playing .slide-meta { animation: none; }
/* Own-post menu: frosted "..." at the top right, the card's delete confirm under it. */
.slide-menu { position: absolute; top: 70px; right: 12px; z-index: 6; }
+10
View File
@@ -119,3 +119,13 @@ describe("one step per gesture", () => {
expect(stepTarget(2, 0, 5)).toBe(2);
});
});
describe("video slide before play", () => {
it("shows only the frosted play button, no native controls", () => {
const html = render({});
const video = /<video[^>]*>/.exec(html)?.[0] ?? "";
expect(video).not.toBe("");
expect(video).not.toContain("controls");
expect(html).toContain('class="post-play slide-play"');
});
});