react-sharesheet 1.8.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/content.d.mts +3 -3
- package/dist/content.d.ts +3 -3
- package/dist/content.js +79 -15
- package/dist/content.js.map +1 -1
- package/dist/content.mjs +79 -15
- package/dist/content.mjs.map +1 -1
- package/dist/drawer.d.mts +3 -3
- package/dist/drawer.d.ts +3 -3
- package/dist/drawer.js +111 -27
- package/dist/drawer.js.map +1 -1
- package/dist/drawer.mjs +101 -27
- package/dist/drawer.mjs.map +1 -1
- package/dist/{headless-BTnsV8ZO.d.ts → headless-lxSw27f0.d.ts} +6 -2
- package/dist/{headless-DzKMJ3eW.d.mts → headless-x5iUF86E.d.mts} +6 -2
- package/dist/headless.d.mts +2 -2
- package/dist/headless.d.ts +2 -2
- package/dist/headless.js +41 -2
- package/dist/headless.js.map +1 -1
- package/dist/headless.mjs +41 -2
- package/dist/headless.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +111 -27
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +101 -27
- package/dist/index.mjs.map +1 -1
- package/dist/{platforms-BgGzSDRk.d.mts → platforms-BtmTZmgl.d.mts} +10 -2
- package/dist/{platforms-BgGzSDRk.d.ts → platforms-BtmTZmgl.d.ts} +10 -2
- package/package.json +18 -16
- package/src/ShareSheetContent.tsx +45 -15
- package/src/ShareSheetDrawer.tsx +36 -9
- package/src/__tests__/drawer-trigger.test.tsx +50 -0
- package/src/__tests__/hooks.test.ts +3 -3
- package/src/__tests__/og-fetcher.test.ts +9 -9
- package/src/__tests__/setup.ts +1 -1
- package/src/hooks.ts +60 -2
- package/src/types.ts +9 -1
|
@@ -28,9 +28,9 @@ const defaultClasses = {
|
|
|
28
28
|
header: "text-center mb-2",
|
|
29
29
|
title: "text-2xl font-black",
|
|
30
30
|
subtitle: "mt-1 text-sm",
|
|
31
|
-
preview: "flex justify-center
|
|
31
|
+
preview: "flex justify-center my-4 px-4",
|
|
32
32
|
previewSkeleton: "rounded-xl overflow-hidden",
|
|
33
|
-
previewImage: "border border-white/10",
|
|
33
|
+
previewImage: "border border-white/10 rounded-xl",
|
|
34
34
|
previewMeta: "",
|
|
35
35
|
grid: "px-2 py-6 flex flex-row items-start gap-4 gap-y-6 flex-wrap justify-center",
|
|
36
36
|
button: "flex flex-col items-center gap-0 text-xs w-[60px] outline-none cursor-pointer group",
|
|
@@ -64,12 +64,16 @@ export function ShareSheetContent({
|
|
|
64
64
|
shareText,
|
|
65
65
|
downloadUrl,
|
|
66
66
|
downloadFilename,
|
|
67
|
+
previewImage,
|
|
68
|
+
shareFile,
|
|
69
|
+
shareFilename,
|
|
67
70
|
className,
|
|
68
71
|
classNames = {},
|
|
69
72
|
buttonSize = DEFAULT_BUTTON_SIZE,
|
|
70
73
|
iconSize = DEFAULT_ICON_SIZE,
|
|
71
74
|
onNativeShare,
|
|
72
75
|
onCopy,
|
|
76
|
+
onShare,
|
|
73
77
|
onDownload,
|
|
74
78
|
hide = [],
|
|
75
79
|
show,
|
|
@@ -79,8 +83,8 @@ export function ShareSheetContent({
|
|
|
79
83
|
const [imageLoaded, setImageLoaded] = useState(false);
|
|
80
84
|
const [imageError, setImageError] = useState(false);
|
|
81
85
|
|
|
82
|
-
//
|
|
83
|
-
const { ogData, loading: ogLoading } = useOGData(shareUrl);
|
|
86
|
+
// Only fetch OG data if previewImage is not provided
|
|
87
|
+
const { ogData, loading: ogLoading } = useOGData(previewImage ? undefined : shareUrl);
|
|
84
88
|
|
|
85
89
|
const handleImageLoad = useCallback(() => {
|
|
86
90
|
setImageLoaded(true);
|
|
@@ -96,6 +100,8 @@ export function ShareSheetContent({
|
|
|
96
100
|
downloadUrl,
|
|
97
101
|
downloadFilename,
|
|
98
102
|
emailSubject: shareText,
|
|
103
|
+
shareFile,
|
|
104
|
+
shareFilename,
|
|
99
105
|
onNativeShare,
|
|
100
106
|
onCopy,
|
|
101
107
|
onDownload,
|
|
@@ -151,11 +157,14 @@ export function ShareSheetContent({
|
|
|
151
157
|
// Use CSS var with fallback to platform color
|
|
152
158
|
bgColor: cssVar(PLATFORM_CSS_VARS[id], PLATFORM_COLORS[id].bg),
|
|
153
159
|
textColor: PLATFORM_COLORS[id].text,
|
|
154
|
-
onClick:
|
|
160
|
+
onClick: () => {
|
|
161
|
+
onShare?.(id);
|
|
162
|
+
shareActions[id]();
|
|
163
|
+
},
|
|
155
164
|
condition,
|
|
156
165
|
};
|
|
157
166
|
});
|
|
158
|
-
}, [iconSize, labels, icons, dynamicLabels, shareActions, shareSheet.canNativeShare, shareSheet.platformAvailability, downloadUrl]);
|
|
167
|
+
}, [iconSize, labels, icons, dynamicLabels, shareActions, onShare, shareSheet.canNativeShare, shareSheet.platformAvailability, downloadUrl]);
|
|
159
168
|
|
|
160
169
|
const visibleButtons = useMemo(() => {
|
|
161
170
|
// If show is provided, use its order
|
|
@@ -184,11 +193,13 @@ export function ShareSheetContent({
|
|
|
184
193
|
|
|
185
194
|
// Render OG preview
|
|
186
195
|
const renderPreview = () => {
|
|
187
|
-
|
|
196
|
+
// Use previewImage if provided, otherwise use OG data
|
|
197
|
+
const ogImage = previewImage || ogData?.image;
|
|
188
198
|
const hasImage = ogImage && !imageError;
|
|
199
|
+
const isLoading = !previewImage && ogLoading;
|
|
189
200
|
|
|
190
|
-
// Loading state
|
|
191
|
-
if (
|
|
201
|
+
// Loading state (only when fetching OG data, not when previewImage is provided)
|
|
202
|
+
if (isLoading) {
|
|
192
203
|
return (
|
|
193
204
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}>
|
|
194
205
|
<div
|
|
@@ -221,8 +232,8 @@ export function ShareSheetContent({
|
|
|
221
232
|
);
|
|
222
233
|
}
|
|
223
234
|
|
|
224
|
-
// No
|
|
225
|
-
if (!
|
|
235
|
+
// No image available - show link placeholder (skip if previewImage was provided)
|
|
236
|
+
if (!hasImage) {
|
|
226
237
|
return (
|
|
227
238
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}>
|
|
228
239
|
<div
|
|
@@ -274,7 +285,27 @@ export function ShareSheetContent({
|
|
|
274
285
|
);
|
|
275
286
|
}
|
|
276
287
|
|
|
277
|
-
//
|
|
288
|
+
// If previewImage is a data URL, show it directly (no preloading needed)
|
|
289
|
+
const isDataUrl = typeof ogImage === "string" && ogImage.startsWith("data:");
|
|
290
|
+
if (isDataUrl) {
|
|
291
|
+
return (
|
|
292
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}>
|
|
293
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
294
|
+
<img
|
|
295
|
+
src={ogImage}
|
|
296
|
+
alt="Preview"
|
|
297
|
+
className={cn(defaultClasses.previewImage, classNames.previewImage)}
|
|
298
|
+
style={{
|
|
299
|
+
width: "100%",
|
|
300
|
+
maxWidth: "320px",
|
|
301
|
+
height: "auto",
|
|
302
|
+
}}
|
|
303
|
+
/>
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Image loading state (for remote URLs)
|
|
278
309
|
if (!imageLoaded) {
|
|
279
310
|
return (
|
|
280
311
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}>
|
|
@@ -308,7 +339,7 @@ export function ShareSheetContent({
|
|
|
308
339
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
309
340
|
<img
|
|
310
341
|
src={ogImage}
|
|
311
|
-
alt={ogData
|
|
342
|
+
alt={ogData?.title || "Preview"}
|
|
312
343
|
onLoad={handleImageLoad}
|
|
313
344
|
onError={handleImageError}
|
|
314
345
|
style={{ display: "none" }}
|
|
@@ -323,13 +354,12 @@ export function ShareSheetContent({
|
|
|
323
354
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
324
355
|
<img
|
|
325
356
|
src={ogImage}
|
|
326
|
-
alt={ogData
|
|
357
|
+
alt={ogData?.title || "Preview"}
|
|
327
358
|
className={cn(defaultClasses.previewImage, classNames.previewImage, "sharesheet-fadein")}
|
|
328
359
|
style={{
|
|
329
360
|
width: "100%",
|
|
330
361
|
maxWidth: "320px",
|
|
331
362
|
height: "auto",
|
|
332
|
-
borderRadius: "12px",
|
|
333
363
|
}}
|
|
334
364
|
/>
|
|
335
365
|
</div>
|
package/src/ShareSheetDrawer.tsx
CHANGED
|
@@ -16,6 +16,31 @@ const defaultDrawerClasses = {
|
|
|
16
16
|
trigger: "",
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Vaul passes the trigger's button semantics down to whatever element it receives:
|
|
21
|
+
* `type="button"`, `aria-haspopup="dialog"` and `aria-expanded`. Handing it a plain
|
|
22
|
+
* `div` puts button attributes on a non-button, which axe reports as a critical
|
|
23
|
+
* `aria-allowed-attr` violation and which screen readers cannot announce.
|
|
24
|
+
*
|
|
25
|
+
* So the trigger element is the one the caller already provided, usually their own
|
|
26
|
+
* button, with the trigger classes merged in. Only a bare string or a fragment needs a
|
|
27
|
+
* host element, and that host is a real button.
|
|
28
|
+
*/
|
|
29
|
+
function renderTrigger(children: React.ReactNode, className: string): React.ReactElement {
|
|
30
|
+
if (React.isValidElement(children)) {
|
|
31
|
+
if (!className) return children;
|
|
32
|
+
const childClassName = (children.props as { className?: string }).className;
|
|
33
|
+
return React.cloneElement(children as React.ReactElement<{ className?: string }>, {
|
|
34
|
+
className: cn(childClassName, className),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return (
|
|
38
|
+
<button type="button" className={className}>
|
|
39
|
+
{children}
|
|
40
|
+
</button>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
19
44
|
// Helper to create var() with fallback
|
|
20
45
|
function cssVar(name: string, fallback: string): string {
|
|
21
46
|
return `var(${name}, ${fallback})`;
|
|
@@ -40,6 +65,9 @@ export function ShareSheetDrawer({
|
|
|
40
65
|
shareText,
|
|
41
66
|
downloadUrl,
|
|
42
67
|
downloadFilename,
|
|
68
|
+
previewImage,
|
|
69
|
+
shareFile,
|
|
70
|
+
shareFilename,
|
|
43
71
|
disabled,
|
|
44
72
|
children,
|
|
45
73
|
open: controlledOpen,
|
|
@@ -67,15 +95,11 @@ export function ShareSheetDrawer({
|
|
|
67
95
|
return (
|
|
68
96
|
<Drawer.Root open={open} onOpenChange={setOpen} shouldScaleBackground>
|
|
69
97
|
<Drawer.Trigger asChild>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)}
|
|
76
|
-
>
|
|
77
|
-
{children}
|
|
78
|
-
</div>
|
|
98
|
+
{renderTrigger(children, cn(
|
|
99
|
+
defaultDrawerClasses.trigger,
|
|
100
|
+
classNames.trigger,
|
|
101
|
+
disabled ? "pointer-events-none opacity-50" : ""
|
|
102
|
+
))}
|
|
79
103
|
</Drawer.Trigger>
|
|
80
104
|
<Drawer.Portal>
|
|
81
105
|
<Drawer.Overlay
|
|
@@ -111,6 +135,9 @@ export function ShareSheetDrawer({
|
|
|
111
135
|
shareText={shareText}
|
|
112
136
|
downloadUrl={downloadUrl}
|
|
113
137
|
downloadFilename={downloadFilename}
|
|
138
|
+
previewImage={previewImage}
|
|
139
|
+
shareFile={shareFile}
|
|
140
|
+
shareFilename={shareFilename}
|
|
114
141
|
className={className}
|
|
115
142
|
classNames={classNames}
|
|
116
143
|
buttonSize={buttonSize}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { ShareSheetDrawer } from "../ShareSheetDrawer";
|
|
5
|
+
|
|
6
|
+
describe("ShareSheetDrawer trigger", () => {
|
|
7
|
+
it("puts the button semantics on the caller's own button, not on a wrapper div", () => {
|
|
8
|
+
render(
|
|
9
|
+
<ShareSheetDrawer shareUrl="https://example.com" shareText="hello">
|
|
10
|
+
<button type="button" data-testid="opener">Share</button>
|
|
11
|
+
</ShareSheetDrawer>,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const opener = screen.getByTestId("opener");
|
|
15
|
+
expect(opener.tagName).toBe("BUTTON");
|
|
16
|
+
expect(opener).toHaveAttribute("aria-haspopup", "dialog");
|
|
17
|
+
// Le défaut corrigé : ces attributs atterrissaient sur un div, ce qu'axe signale en
|
|
18
|
+
// critique et qu'aucun lecteur d'écran ne sait annoncer.
|
|
19
|
+
expect(document.querySelector("div[aria-haspopup]")).toBeNull();
|
|
20
|
+
expect(document.querySelector("div[aria-expanded]")).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("still gives a bare string a real button to live in", () => {
|
|
24
|
+
render(
|
|
25
|
+
<ShareSheetDrawer shareUrl="https://example.com" shareText="hello">
|
|
26
|
+
Share this
|
|
27
|
+
</ShareSheetDrawer>,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const opener = screen.getByText("Share this");
|
|
31
|
+
expect(opener.tagName).toBe("BUTTON");
|
|
32
|
+
expect(opener).toHaveAttribute("type", "button");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("merges the trigger class into the caller's element instead of wrapping it", () => {
|
|
36
|
+
render(
|
|
37
|
+
<ShareSheetDrawer
|
|
38
|
+
shareUrl="https://example.com"
|
|
39
|
+
shareText="hello"
|
|
40
|
+
classNames={{ trigger: "trigger-class" }}
|
|
41
|
+
>
|
|
42
|
+
<button type="button" className="mine" data-testid="opener">Share</button>
|
|
43
|
+
</ShareSheetDrawer>,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const opener = screen.getByTestId("opener");
|
|
47
|
+
expect(opener.className).toContain("mine");
|
|
48
|
+
expect(opener.className).toContain("trigger-class");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -115,7 +115,7 @@ describe("useOGData", () => {
|
|
|
115
115
|
});
|
|
116
116
|
|
|
117
117
|
it("should return initial loading state", () => {
|
|
118
|
-
vi.mocked(
|
|
118
|
+
vi.mocked(globalThis.fetch).mockImplementation(
|
|
119
119
|
() => new Promise(() => {}) // Never resolves
|
|
120
120
|
);
|
|
121
121
|
|
|
@@ -137,7 +137,7 @@ describe("useOGData", () => {
|
|
|
137
137
|
},
|
|
138
138
|
};
|
|
139
139
|
|
|
140
|
-
vi.mocked(
|
|
140
|
+
vi.mocked(globalThis.fetch).mockResolvedValueOnce({
|
|
141
141
|
ok: true,
|
|
142
142
|
json: () => Promise.resolve(mockResponse),
|
|
143
143
|
} as Response);
|
|
@@ -175,7 +175,7 @@ describe("useOGData", () => {
|
|
|
175
175
|
data: { title: "Page 2", url: "https://example2.com" },
|
|
176
176
|
};
|
|
177
177
|
|
|
178
|
-
vi.mocked(
|
|
178
|
+
vi.mocked(globalThis.fetch)
|
|
179
179
|
.mockResolvedValueOnce({
|
|
180
180
|
ok: true,
|
|
181
181
|
json: () => Promise.resolve(mockResponse1),
|
|
@@ -19,7 +19,7 @@ describe("og-fetcher", () => {
|
|
|
19
19
|
},
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
vi.mocked(
|
|
22
|
+
vi.mocked(globalThis.fetch).mockResolvedValueOnce({
|
|
23
23
|
ok: true,
|
|
24
24
|
json: () => Promise.resolve(mockResponse),
|
|
25
25
|
} as Response);
|
|
@@ -36,7 +36,7 @@ describe("og-fetcher", () => {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
it("should return null on failed fetch", async () => {
|
|
39
|
-
vi.mocked(
|
|
39
|
+
vi.mocked(globalThis.fetch).mockResolvedValueOnce({
|
|
40
40
|
ok: false,
|
|
41
41
|
status: 404,
|
|
42
42
|
} as Response);
|
|
@@ -52,7 +52,7 @@ describe("og-fetcher", () => {
|
|
|
52
52
|
data: null,
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
vi.mocked(
|
|
55
|
+
vi.mocked(globalThis.fetch).mockResolvedValueOnce({
|
|
56
56
|
ok: true,
|
|
57
57
|
json: () => Promise.resolve(mockResponse),
|
|
58
58
|
} as Response);
|
|
@@ -73,7 +73,7 @@ describe("og-fetcher", () => {
|
|
|
73
73
|
},
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
-
vi.mocked(
|
|
76
|
+
vi.mocked(globalThis.fetch).mockResolvedValueOnce({
|
|
77
77
|
ok: true,
|
|
78
78
|
json: () => Promise.resolve(mockResponse),
|
|
79
79
|
} as Response);
|
|
@@ -84,7 +84,7 @@ describe("og-fetcher", () => {
|
|
|
84
84
|
const result2 = await fetchOGData("https://example.com/cached");
|
|
85
85
|
|
|
86
86
|
expect(result1).toEqual(result2);
|
|
87
|
-
expect(
|
|
87
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
it("should clear cache", async () => {
|
|
@@ -96,7 +96,7 @@ describe("og-fetcher", () => {
|
|
|
96
96
|
},
|
|
97
97
|
};
|
|
98
98
|
|
|
99
|
-
vi.mocked(
|
|
99
|
+
vi.mocked(globalThis.fetch).mockResolvedValue({
|
|
100
100
|
ok: true,
|
|
101
101
|
json: () => Promise.resolve(mockResponse),
|
|
102
102
|
} as Response);
|
|
@@ -105,11 +105,11 @@ describe("og-fetcher", () => {
|
|
|
105
105
|
clearOGCache();
|
|
106
106
|
await fetchOGData("https://example.com");
|
|
107
107
|
|
|
108
|
-
expect(
|
|
108
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
it("should handle network errors gracefully", async () => {
|
|
112
|
-
vi.mocked(
|
|
112
|
+
vi.mocked(globalThis.fetch).mockRejectedValueOnce(new Error("Network error"));
|
|
113
113
|
|
|
114
114
|
const result = await fetchOGData("https://example.com");
|
|
115
115
|
|
|
@@ -126,7 +126,7 @@ describe("og-fetcher", () => {
|
|
|
126
126
|
},
|
|
127
127
|
};
|
|
128
128
|
|
|
129
|
-
vi.mocked(
|
|
129
|
+
vi.mocked(globalThis.fetch).mockResolvedValueOnce({
|
|
130
130
|
ok: true,
|
|
131
131
|
json: () => Promise.resolve(mockResponse),
|
|
132
132
|
} as Response);
|
package/src/__tests__/setup.ts
CHANGED
package/src/hooks.ts
CHANGED
|
@@ -50,6 +50,10 @@ export interface UseShareSheetOptions {
|
|
|
50
50
|
downloadFilename?: string;
|
|
51
51
|
/** Email subject (optional) */
|
|
52
52
|
emailSubject?: string;
|
|
53
|
+
/** File to share via native share (data URL or Blob) - takes priority over URL sharing */
|
|
54
|
+
shareFile?: string | Blob | null;
|
|
55
|
+
/** Filename for the shared file */
|
|
56
|
+
shareFilename?: string;
|
|
53
57
|
/** Callback after native share */
|
|
54
58
|
onNativeShare?: () => void;
|
|
55
59
|
/** Callback after copy */
|
|
@@ -68,6 +72,8 @@ export function useShareSheet({
|
|
|
68
72
|
downloadUrl,
|
|
69
73
|
downloadFilename,
|
|
70
74
|
emailSubject = "Share",
|
|
75
|
+
shareFile,
|
|
76
|
+
shareFilename = "share.png",
|
|
71
77
|
onNativeShare,
|
|
72
78
|
onCopy,
|
|
73
79
|
onDownload,
|
|
@@ -105,12 +111,64 @@ export function useShareSheet({
|
|
|
105
111
|
}, [safeUrl, onCopy]);
|
|
106
112
|
|
|
107
113
|
const nativeShare = useCallback(async () => {
|
|
108
|
-
if (!safeUrl) return;
|
|
109
114
|
const nav = navigator as Navigator & {
|
|
110
115
|
share?: (data: ShareData) => Promise<void>;
|
|
116
|
+
canShare?: (data: ShareData) => boolean;
|
|
111
117
|
};
|
|
112
118
|
if (!("share" in nav) || typeof nav.share !== "function") return;
|
|
119
|
+
|
|
113
120
|
try {
|
|
121
|
+
// If shareFile is provided, try to share as a file
|
|
122
|
+
if (shareFile) {
|
|
123
|
+
let blob: Blob;
|
|
124
|
+
|
|
125
|
+
// Convert data URL to Blob if needed
|
|
126
|
+
if (typeof shareFile === "string" && shareFile.startsWith("data:")) {
|
|
127
|
+
const response = await fetch(shareFile);
|
|
128
|
+
blob = await response.blob();
|
|
129
|
+
} else if (shareFile instanceof Blob) {
|
|
130
|
+
blob = shareFile;
|
|
131
|
+
} else {
|
|
132
|
+
// Invalid shareFile, fall back to URL sharing
|
|
133
|
+
if (!safeUrl) return;
|
|
134
|
+
await nav.share({
|
|
135
|
+
title: shareText,
|
|
136
|
+
text: shareText,
|
|
137
|
+
url: safeUrl,
|
|
138
|
+
});
|
|
139
|
+
onNativeShare?.();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Create a File from the Blob
|
|
144
|
+
const file = new File([blob], shareFilename, { type: blob.type || "image/png" });
|
|
145
|
+
|
|
146
|
+
// Check if file sharing is supported
|
|
147
|
+
const shareData: ShareData = {
|
|
148
|
+
files: [file],
|
|
149
|
+
title: shareText,
|
|
150
|
+
text: shareText,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if (nav.canShare && !nav.canShare(shareData)) {
|
|
154
|
+
// File sharing not supported, fall back to URL sharing
|
|
155
|
+
if (!safeUrl) return;
|
|
156
|
+
await nav.share({
|
|
157
|
+
title: shareText,
|
|
158
|
+
text: shareText,
|
|
159
|
+
url: safeUrl,
|
|
160
|
+
});
|
|
161
|
+
onNativeShare?.();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await nav.share(shareData);
|
|
166
|
+
onNativeShare?.();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Fall back to URL sharing
|
|
171
|
+
if (!safeUrl) return;
|
|
114
172
|
await nav.share({
|
|
115
173
|
title: shareText,
|
|
116
174
|
text: shareText,
|
|
@@ -120,7 +178,7 @@ export function useShareSheet({
|
|
|
120
178
|
} catch {
|
|
121
179
|
// user canceled or share failed -> ignore
|
|
122
180
|
}
|
|
123
|
-
}, [safeUrl, shareText, onNativeShare]);
|
|
181
|
+
}, [safeUrl, shareText, shareFile, shareFilename, onNativeShare]);
|
|
124
182
|
|
|
125
183
|
const downloadFile = useCallback(async () => {
|
|
126
184
|
const url = (downloadUrl ?? "").trim();
|
package/src/types.ts
CHANGED
|
@@ -21,7 +21,7 @@ export const CSS_VAR_UI_DEFAULTS = {
|
|
|
21
21
|
[CSS_VARS_UI.overlayBg]: "rgba(0, 0, 0, 0.7)",
|
|
22
22
|
[CSS_VARS_UI.drawerBg]: "#09090b",
|
|
23
23
|
[CSS_VARS_UI.drawerBorder]: "#27272a",
|
|
24
|
-
[CSS_VARS_UI.handleBg]: "
|
|
24
|
+
[CSS_VARS_UI.handleBg]: "rgba(255, 255, 255, 0.2)",
|
|
25
25
|
[CSS_VARS_UI.titleColor]: "#ffffff",
|
|
26
26
|
[CSS_VARS_UI.subtitleColor]: "#a1a1aa",
|
|
27
27
|
[CSS_VARS_UI.buttonLabelColor]: "#ffffff",
|
|
@@ -108,6 +108,12 @@ export interface ShareSheetContentProps {
|
|
|
108
108
|
downloadUrl?: string | null;
|
|
109
109
|
/** Filename for downloaded file */
|
|
110
110
|
downloadFilename?: string;
|
|
111
|
+
/** Custom preview image URL (skips OG fetching if provided) */
|
|
112
|
+
previewImage?: string | null;
|
|
113
|
+
/** File to share via native share (data URL or Blob) - takes priority over URL sharing */
|
|
114
|
+
shareFile?: string | Blob | null;
|
|
115
|
+
/** Filename for the shared file */
|
|
116
|
+
shareFilename?: string;
|
|
111
117
|
/** Custom class name for the container (shorthand for classNames.root) */
|
|
112
118
|
className?: string;
|
|
113
119
|
/** Override class names for sub-components */
|
|
@@ -122,6 +128,8 @@ export interface ShareSheetContentProps {
|
|
|
122
128
|
onCopy?: () => void;
|
|
123
129
|
/** Called when download starts */
|
|
124
130
|
onDownload?: () => void;
|
|
131
|
+
/** Called on every share option click, with the option id (analytics-friendly) */
|
|
132
|
+
onShare?: (option: ShareOption) => void;
|
|
125
133
|
/** Hide specific share options */
|
|
126
134
|
hide?: ShareOption[];
|
|
127
135
|
/** Show only these platforms, in this exact order */
|