From 1083bc2ae10b5d34e0f58c2a93ceedf49d440527 Mon Sep 17 00:00:00 2001 From: Hunter Johnston <64506580+huntabyte@users.noreply.github.com> Date: Wed, 2 Oct 2024 21:26:51 -0400 Subject: [PATCH] fix: `Avatar.Image` crossorigin/referrerpolicy issues (#721) --- .changeset/odd-games-guess.md | 5 ++ .../src/lib/bits/avatar/avatar.svelte.ts | 56 +++++++++++++------ .../avatar/components/avatar-image.svelte | 12 +++- sites/docs/src/hooks.server.ts | 9 ++- .../docs/src/routes/(main)/sink/+page.svelte | 18 +++++- 5 files changed, 78 insertions(+), 22 deletions(-) create mode 100644 .changeset/odd-games-guess.md diff --git a/.changeset/odd-games-guess.md b/.changeset/odd-games-guess.md new file mode 100644 index 000000000..3709b021d --- /dev/null +++ b/.changeset/odd-games-guess.md @@ -0,0 +1,5 @@ +--- +"bits-ui": patch +--- + +fix bug preventing `crossorigin` and `referrerpolicy` attributes to work with `Avatar.Image` component diff --git a/packages/bits-ui/src/lib/bits/avatar/avatar.svelte.ts b/packages/bits-ui/src/lib/bits/avatar/avatar.svelte.ts index 36cbde0c1..7142ec92f 100644 --- a/packages/bits-ui/src/lib/bits/avatar/avatar.svelte.ts +++ b/packages/bits-ui/src/lib/bits/avatar/avatar.svelte.ts @@ -1,5 +1,6 @@ import { untrack } from "svelte"; import type { ReadableBox, WritableBox } from "svelte-toolbelt"; +import type { HTMLImgAttributes } from "svelte/elements"; import type { AvatarImageLoadingStatus } from "./types.js"; import { createContext } from "$lib/internal/createContext.js"; import type { ReadableBoxedValues } from "$lib/internal/box.svelte.js"; @@ -10,6 +11,9 @@ const AVATAR_ROOT_ATTR = "data-avatar-root"; const AVATAR_IMAGE_ATTR = "data-avatar-image"; const AVATAR_FALLBACK_ATTR = "data-avatar-fallback"; +type CrossOrigin = HTMLImgAttributes["crossorigin"]; +type ReferrerPolicy = HTMLImgAttributes["referrerpolicy"]; + /** * ROOT */ @@ -38,13 +42,17 @@ class AvatarRootState { }); } - loadImage(src: string) { - let imageTimerId: NodeJS.Timeout; + loadImage(src: string, crossorigin?: CrossOrigin, referrerPolicy?: ReferrerPolicy) { + let imageTimerId: number; const image = new Image(); + image.src = src; + if (crossorigin) image.crossOrigin = crossorigin; + if (referrerPolicy) image.referrerPolicy = referrerPolicy; + this.loadingStatus.current = "loading"; image.onload = () => { - imageTimerId = setTimeout(() => { + imageTimerId = window.setTimeout(() => { this.loadingStatus.current = "loaded"; }, this.delayMs.current); }; @@ -81,20 +89,26 @@ class AvatarRootState { type AvatarImageStateProps = WithRefProps< ReadableBoxedValues<{ src: AvatarImageSrc; + crossOrigin: CrossOrigin; + referrerPolicy: ReferrerPolicy; }> >; class AvatarImageState { #id: AvatarImageStateProps["id"]; #ref: AvatarImageStateProps["ref"]; - src: AvatarImageStateProps["src"]; - root: AvatarRootState; + #crossOrigin: AvatarImageStateProps["crossOrigin"]; + #referrerPolicy: AvatarImageStateProps["referrerPolicy"]; + #src: AvatarImageStateProps["src"]; + #root: AvatarRootState; constructor(props: AvatarImageStateProps, root: AvatarRootState) { - this.root = root; - this.src = props.src; + this.#root = root; + this.#src = props.src; this.#id = props.id; this.#ref = props.ref; + this.#crossOrigin = props.crossOrigin; + this.#referrerPolicy = props.referrerPolicy; useRefById({ id: this.#id, @@ -102,8 +116,16 @@ class AvatarImageState { }); $effect.pre(() => { - if (!this.src.current) return; - untrack(() => this.root.loadImage(this.src.current ?? "")); + if (!this.#src.current) return; + // dependency on crossorigin + this.#crossOrigin.current; + untrack(() => + this.#root.loadImage( + this.#src.current ?? "", + this.#crossOrigin.current, + this.#referrerPolicy.current + ) + ); }); } @@ -112,11 +134,13 @@ class AvatarImageState { ({ id: this.#id.current, style: { - display: this.root.loadingStatus.current === "loaded" ? "block" : "none", + display: this.#root.loadingStatus.current === "loaded" ? "block" : "none", }, - "data-status": this.root.loadingStatus.current, + "data-status": this.#root.loadingStatus.current, [AVATAR_IMAGE_ATTR]: "", - src: this.src.current, + src: this.#src.current, + crossorigin: this.#crossOrigin.current, + referrerpolicy: this.#referrerPolicy.current, }) as const ); } @@ -130,10 +154,10 @@ type AvatarFallbackStateProps = WithRefProps; class AvatarFallbackState { #id: AvatarFallbackStateProps["id"]; #ref: AvatarFallbackStateProps["ref"]; - root: AvatarRootState; + #root: AvatarRootState; constructor(props: AvatarFallbackStateProps, root: AvatarRootState) { - this.root = root; + this.#root = root; this.#id = props.id; this.#ref = props.ref; @@ -147,9 +171,9 @@ class AvatarFallbackState { () => ({ style: { - display: this.root.loadingStatus.current === "loaded" ? "none" : undefined, + display: this.#root.loadingStatus.current === "loaded" ? "none" : undefined, }, - "data-status": this.root.loadingStatus.current, + "data-status": this.#root.loadingStatus.current, [AVATAR_FALLBACK_ATTR]: "", }) as const ); diff --git a/packages/bits-ui/src/lib/bits/avatar/components/avatar-image.svelte b/packages/bits-ui/src/lib/bits/avatar/components/avatar-image.svelte index 038d674b3..3233f6e49 100644 --- a/packages/bits-ui/src/lib/bits/avatar/components/avatar-image.svelte +++ b/packages/bits-ui/src/lib/bits/avatar/components/avatar-image.svelte @@ -5,7 +5,15 @@ import { mergeProps } from "$lib/internal/mergeProps.js"; import { useId } from "$lib/internal/useId.js"; - let { src, child, id = useId(), ref = $bindable(null), ...restProps }: ImageProps = $props(); + let { + src, + child, + id = useId(), + ref = $bindable(null), + crossorigin = "", + referrerpolicy = undefined, + ...restProps + }: ImageProps = $props(); const imageState = useAvatarImage({ src: box.with(() => src), @@ -14,6 +22,8 @@ () => ref, (v) => (ref = v) ), + crossOrigin: box.with(() => crossorigin), + referrerPolicy: box.with(() => referrerpolicy), }); const mergedProps = $derived(mergeProps(restProps, imageState.props)); diff --git a/sites/docs/src/hooks.server.ts b/sites/docs/src/hooks.server.ts index 81ce050f0..88dc64e13 100644 --- a/sites/docs/src/hooks.server.ts +++ b/sites/docs/src/hooks.server.ts @@ -1,5 +1,10 @@ import { buildDocsIndex } from "$lib/scripts/build-search.js"; -export function handle({ event, resolve }) { - return resolve(event); +export async function handle({ event, resolve }) { + const response = await resolve(event); + + response.headers.set("Cross-Origin-Embedder-Policy", "require-corp"); + response.headers.set("Cross-Origin-Opener-Policy", "same-origin"); + + return response; } diff --git a/sites/docs/src/routes/(main)/sink/+page.svelte b/sites/docs/src/routes/(main)/sink/+page.svelte index 6e7f611c4..1c7ddef50 100644 --- a/sites/docs/src/routes/(main)/sink/+page.svelte +++ b/sites/docs/src/routes/(main)/sink/+page.svelte @@ -1,7 +1,19 @@ -