Skip to content

Commit 0fe4319

Browse files
committed
BannerVideo - Bugfix - Make auto play work on route change
1 parent 1dc25d1 commit 0fe4319

4 files changed

Lines changed: 58 additions & 48 deletions

File tree

app/components/01.atoms/banner-video/BannerVideo.vue

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<component
33
:is="tag"
44
class="banner-video"
5-
:class="[elementClasses, { 'video-failed': videoFailed }]"
5+
:class="elementClasses"
66
:style="{
77
'--_max-height': maxHeight,
88
'--_max-height-tablet': maxHeightTablet,
@@ -13,16 +13,20 @@
1313
}"
1414
>
1515
<video
16+
:key="src"
17+
ref="videoEl"
1618
class="video"
1719
autoplay
1820
muted
1921
loop
2022
playsinline
23+
preload="auto"
2124
:poster="poster"
2225
:style="{ objectFit: props.objectFit }"
23-
@error="videoFailed = true"
26+
@loadeddata="handleLoadedData"
27+
@canplay="handleCanPlay"
2428
>
25-
<source :src="src" type="video/mp4" @error="videoFailed = true" />
29+
<source :src="src" type="video/mp4" />
2630
</video>
2731
<NuxtImg
2832
class="fallback"
@@ -103,7 +107,50 @@ watch(
103107
() => resetElementClasses(props.styleClassPassthrough)
104108
);
105109
106-
const videoFailed = ref(false);
110+
const videoEl = shallowRef<HTMLVideoElement | null>(null);
111+
112+
const tryPlay = async () => {
113+
const v = videoEl.value;
114+
if (!v) return;
115+
try {
116+
// Ensure muted stays true — required for programmatic autoplay in all browsers
117+
v.muted = true;
118+
await v.play();
119+
} catch {
120+
// Autoplay blocked or interrupted — fallback image will remain visible
121+
}
122+
};
123+
124+
const kickOffLoad = async () => {
125+
await nextTick();
126+
const v = videoEl.value;
127+
if (!v) return;
128+
// Force the media element to (re)read its source child and begin fetching
129+
v.load();
130+
// Attempt immediate play; loadeddata/canplay handlers will retry once data arrives
131+
void tryPlay();
132+
};
133+
134+
const handleLoadedData = () => {
135+
void tryPlay();
136+
};
137+
const handleCanPlay = () => {
138+
void tryPlay();
139+
};
140+
141+
// Runs on mount AND whenever src changes (covers route-change re-use edge cases)
142+
watch(
143+
() => props.src,
144+
() => {
145+
void kickOffLoad();
146+
},
147+
{ immediate: true, flush: "post" }
148+
);
149+
150+
// Extra safety: when the component becomes active again (e.g. returning via keep-alive)
151+
onActivated(() => {
152+
void kickOffLoad();
153+
});
107154
</script>
108155

109156
<style lang="css">
@@ -141,15 +188,6 @@ const videoFailed = ref(false);
141188
height: 100%;
142189
}
143190
144-
&.video-failed {
145-
.video {
146-
display: none;
147-
}
148-
.fallback {
149-
display: block;
150-
}
151-
}
152-
153191
@media (prefers-reduced-motion: reduce) {
154192
.video {
155193
display: none;

app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,14 @@ describe("BannerVideo", () => {
9696
expect(wrapper.find("video").attributes("poster")).toBe(defaultProps.poster);
9797
});
9898

99-
it("source element has the correct src", async () => {
99+
it("video element has the correct src", async () => {
100100
const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
101-
expect(wrapper.find("source").attributes("src")).toBe(defaultProps.src);
101+
expect(wrapper.find("video").attributes("src")).toBe(defaultProps.src);
102102
});
103103

104-
it("source element has type video/mp4", async () => {
104+
it("video element has preload=auto", async () => {
105105
const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
106-
expect(wrapper.find("source").attributes("type")).toBe("video/mp4");
106+
expect(wrapper.find("video").attributes("preload")).toBe("auto");
107107
});
108108

109109
// ─── Fallback image ──────────────────────────────────────────────────────
@@ -165,27 +165,6 @@ describe("BannerVideo", () => {
165165
expect(wrapper.find("img[data-nuxt-img]").attributes("decoding")).toBe("async");
166166
});
167167

168-
// ─── Video error handling ────────────────────────────────────────────────
169-
170-
it("does not have video-failed class initially", async () => {
171-
const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
172-
expect(wrapper.classes()).not.toContain("video-failed");
173-
});
174-
175-
it("adds video-failed class when video fires an error event", async () => {
176-
const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
177-
await wrapper.find("video").trigger("error");
178-
await nextTick();
179-
expect(wrapper.classes()).toContain("video-failed");
180-
});
181-
182-
it("adds video-failed class when source fires an error event", async () => {
183-
const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
184-
await wrapper.find("source").trigger("error");
185-
await nextTick();
186-
expect(wrapper.classes()).toContain("video-failed");
187-
});
188-
189168
// ─── CSS custom properties ───────────────────────────────────────────────
190169

191170
it("sets --_max-height to 56rem by default", async () => {
Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3-
exports[`BannerVideo > renders correct HTML structure (all props set) 1`] = `
4-
"<header class="banner-video full-bleed" style="--_max-height: 56rem; --_aspect-ratio: 21/9; --_align-self: center; --_justify-self: center;" height="40rem"><video class="video" autoplay="" muted="" loop="" playsinline="" poster="/images/banners/video/lake-banner.jpg" style="object-fit: cover;">
5-
<source src="/images/banners/video/lake-banner.mp4" type="video/mp4">
6-
</video><img width="1280" height="720" data-nuxt-img="" srcset="/_ipx/s_1280x720/images/banners/video/lake-banner.jpg 1x, /_ipx/s_2560x1440/images/banners/video/lake-banner.jpg 2x" class="fallback" alt="Lake banner" loading="eager" decoding="async" style="object-fit: cover; object-position: center center;" src="/_ipx/s_1280x720/images/banners/video/lake-banner.jpg"></header>"
7-
`;
3+
exports[`BannerVideo > renders correct HTML structure (all props set) 1`] = `"<header class="banner-video full-bleed" style="--_max-height: 56rem; --_aspect-ratio: 21/9; --_align-self: center; --_justify-self: center;" height="40rem"><video class="video" autoplay="" muted="" loop="" playsinline="" preload="auto" src="/images/banners/video/lake-banner.mp4" poster="/images/banners/video/lake-banner.jpg" style="object-fit: cover;"></video><img width="1280" height="720" data-nuxt-img="" srcset="/_ipx/s_1280x720/images/banners/video/lake-banner.jpg 1x, /_ipx/s_2560x1440/images/banners/video/lake-banner.jpg 2x" class="fallback" alt="Lake banner" loading="eager" decoding="async" style="object-fit: cover; object-position: center center;" src="/_ipx/s_1280x720/images/banners/video/lake-banner.jpg"></header>"`;
84
9-
exports[`BannerVideo > renders correct HTML structure (default props) 1`] = `
10-
"<section class="banner-video" style="--_max-height: 56rem; --_aspect-ratio: 21/9; --_align-self: center; --_justify-self: center;"><video class="video" autoplay="" muted="" loop="" playsinline="" poster="/images/banners/video/lake-banner.jpg" style="object-fit: cover;">
11-
<source src="/images/banners/video/lake-banner.mp4" type="video/mp4">
12-
</video><img width="1920" height="1080" data-nuxt-img="" srcset="/_ipx/s_1920x1080/images/banners/video/lake-banner.jpg 1x, /_ipx/s_3840x2160/images/banners/video/lake-banner.jpg 2x" class="fallback" alt="" loading="eager" decoding="async" style="object-fit: cover; object-position: center center;" src="/_ipx/s_1920x1080/images/banners/video/lake-banner.jpg"></section>"
13-
`;
5+
exports[`BannerVideo > renders correct HTML structure (default props) 1`] = `"<section class="banner-video" style="--_max-height: 56rem; --_aspect-ratio: 21/9; --_align-self: center; --_justify-self: center;"><video class="video" autoplay="" muted="" loop="" playsinline="" preload="auto" src="/images/banners/video/lake-banner.mp4" poster="/images/banners/video/lake-banner.jpg" style="object-fit: cover;"></video><img width="1920" height="1080" data-nuxt-img="" srcset="/_ipx/s_1920x1080/images/banners/video/lake-banner.jpg 1x, /_ipx/s_3840x2160/images/banners/video/lake-banner.jpg 2x" class="fallback" alt="" loading="eager" decoding="async" style="object-fit: cover; object-position: center center;" src="/_ipx/s_1920x1080/images/banners/video/lake-banner.jpg"></section>"`;

app/layouts/default.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const responsiveNavLinks = {
6666
{ name: "Tooltips", path: "/ui/tooltips" },
6767
{ name: "Marquee Scroller", path: "/ui/marquee-scroller" },
6868
{ name: "Banner", path: "/ui/display-banner" },
69+
{ name: "Banner Video", path: "/banner-video" },
6970
{ name: "Section Parallax", path: "/ui/section-parallax" },
7071
{ name: "Animated SVG Text", path: "/ui/animated-svg-text" },
7172
{ name: "Carousel (Basic)", path: "/ui/carousel-basic" },

0 commit comments

Comments
 (0)