react-sharesheet 1.9.0 → 1.10.0
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 +48 -13
- package/dist/content.d.mts +3 -3
- package/dist/content.d.ts +3 -3
- package/dist/content.js +12 -5
- package/dist/content.js.map +1 -1
- package/dist/content.mjs +11 -5
- 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 +40 -17
- package/dist/drawer.js.map +1 -1
- package/dist/drawer.mjs +29 -17
- package/dist/drawer.mjs.map +1 -1
- package/dist/{headless-lxSw27f0.d.ts → headless-CEUr7QJE.d.ts} +5 -14
- package/dist/{headless-x5iUF86E.d.mts → headless-JYVQPj9Q.d.mts} +5 -14
- package/dist/headless.d.mts +2 -2
- package/dist/headless.d.ts +2 -2
- package/dist/headless.js +6 -2
- package/dist/headless.js.map +1 -1
- package/dist/headless.mjs +7 -3
- 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 +36 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +27 -16
- package/dist/index.mjs.map +1 -1
- package/dist/{platforms-BtmTZmgl.d.mts → platforms-B1VXTDkO.d.mts} +20 -2
- package/dist/{platforms-BtmTZmgl.d.ts → platforms-B1VXTDkO.d.ts} +20 -2
- package/package.json +2 -2
- package/src/ShareSheetContent.tsx +4 -2
- package/src/ShareSheetDrawer.tsx +32 -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__/preview-fetch.test.tsx +82 -0
- package/src/__tests__/setup.ts +1 -1
- package/src/content.ts +1 -0
- package/src/drawer.ts +1 -0
- package/src/headless.ts +1 -1
- package/src/hooks.ts +12 -5
- package/src/index.ts +1 -1
- package/src/og-fetcher.ts +6 -1
- package/src/types.ts +8 -1
|
@@ -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);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
2
|
+
import { render, renderHook, waitFor } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { ShareSheetContent } from "../ShareSheetContent";
|
|
5
|
+
import { ShareSheetDrawer } from "../ShareSheetDrawer";
|
|
6
|
+
import { useOGData } from "../hooks";
|
|
7
|
+
import { clearOGCache, type OGFetcher } from "../og-fetcher";
|
|
8
|
+
|
|
9
|
+
const card = { title: "A page", image: "https://example.com/card.png", url: "https://example.com/p" };
|
|
10
|
+
|
|
11
|
+
describe("link preview lookup", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
clearOGCache();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("sends the link nowhere by default: no request, a link placeholder", async () => {
|
|
18
|
+
const { container } = render(<ShareSheetContent shareUrl="https://example.com/private?seed=42" shareText="hi" />);
|
|
19
|
+
|
|
20
|
+
// Give a stray effect the chance to fire before looking
|
|
21
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
22
|
+
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
23
|
+
expect(container.querySelector("img")).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("looks the link up with the fetcher the caller passes, and shows its image", async () => {
|
|
27
|
+
const fetchPreview = vi.fn<OGFetcher>().mockResolvedValue(card);
|
|
28
|
+
const { container } = render(
|
|
29
|
+
<ShareSheetContent shareUrl="https://example.com/p" shareText="hi" fetchPreview={fetchPreview} />,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
await waitFor(() => expect(container.querySelector("img")).not.toBeNull());
|
|
33
|
+
expect(fetchPreview).toHaveBeenCalledTimes(1);
|
|
34
|
+
expect(fetchPreview).toHaveBeenCalledWith("https://example.com/p");
|
|
35
|
+
expect(container.querySelector("img")).toHaveAttribute("src", card.image);
|
|
36
|
+
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("never looks up a link that already has a preview image", async () => {
|
|
40
|
+
const fetchPreview = vi.fn<OGFetcher>().mockResolvedValue(card);
|
|
41
|
+
render(
|
|
42
|
+
<ShareSheetContent
|
|
43
|
+
shareUrl="https://example.com/p"
|
|
44
|
+
shareText="hi"
|
|
45
|
+
previewImage="data:image/png;base64,AAAA"
|
|
46
|
+
fetchPreview={fetchPreview}
|
|
47
|
+
/>,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
51
|
+
expect(fetchPreview).not.toHaveBeenCalled();
|
|
52
|
+
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("the drawer hands the fetcher to its sheet", async () => {
|
|
56
|
+
const fetchPreview = vi.fn<OGFetcher>().mockResolvedValue(card);
|
|
57
|
+
render(
|
|
58
|
+
<ShareSheetDrawer shareUrl="https://example.com/p" shareText="hi" fetchPreview={fetchPreview} open onOpenChange={() => {}}>
|
|
59
|
+
<button type="button">Share</button>
|
|
60
|
+
</ShareSheetDrawer>,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
await waitFor(() => expect(fetchPreview).toHaveBeenCalledWith("https://example.com/p"));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("useOGData does not refetch when the caller passes a new inline fetcher each render", async () => {
|
|
67
|
+
const calls: string[] = [];
|
|
68
|
+
const { result, rerender } = renderHook(() =>
|
|
69
|
+
useOGData("https://example.com/p", async (url) => {
|
|
70
|
+
calls.push(url);
|
|
71
|
+
return card;
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
await waitFor(() => expect(result.current.ogData).toEqual(card));
|
|
76
|
+
rerender();
|
|
77
|
+
rerender();
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
79
|
+
expect(calls).toEqual(["https://example.com/p"]);
|
|
80
|
+
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
});
|
package/src/__tests__/setup.ts
CHANGED
package/src/content.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { ShareSheetContent, ShareMenuContent } from "./ShareSheetContent";
|
|
|
2
2
|
export type { ShareSheetContentProps, ShareSheetContentClassNames, ShareMenuContentProps, ShareMenuContentClassNames, ShareOption } from "./types";
|
|
3
3
|
export { CSS_VARS_UI, CSS_VAR_UI_DEFAULTS, CSS_VARS, CSS_VAR_DEFAULTS } from "./types";
|
|
4
4
|
export { PLATFORM_COLORS, PLATFORM_CSS_VARS } from "./platforms";
|
|
5
|
+
export { fetchOGData, type OGData, type OGFetcher } from "./og-fetcher";
|
package/src/drawer.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { ShareSheetDrawer, ShareMenuDrawer } from "./ShareSheetDrawer";
|
|
|
2
2
|
export type { ShareSheetDrawerProps, ShareSheetDrawerClassNames, ShareMenuDrawerProps, ShareMenuDrawerClassNames, ShareOption } from "./types";
|
|
3
3
|
export { CSS_VARS_UI, CSS_VAR_UI_DEFAULTS, CSS_VARS, CSS_VAR_DEFAULTS } from "./types";
|
|
4
4
|
export { PLATFORM_COLORS, PLATFORM_CSS_VARS } from "./platforms";
|
|
5
|
+
export { fetchOGData, type OGData, type OGFetcher } from "./og-fetcher";
|
package/src/headless.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
export { useShareSheet, useShareMenu, useOGData, type UseShareSheetOptions, type UseShareMenuOptions } from "./hooks";
|
|
4
4
|
|
|
5
5
|
// OG Data fetcher
|
|
6
|
-
export { fetchOGData, clearOGCache, type OGData } from "./og-fetcher";
|
|
6
|
+
export { fetchOGData, clearOGCache, type OGData, type OGFetcher } from "./og-fetcher";
|
|
7
7
|
|
|
8
8
|
export {
|
|
9
9
|
shareToWhatsApp,
|
package/src/hooks.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useMemo, useState, useCallback, useEffect } from "react";
|
|
3
|
+
import { useMemo, useState, useCallback, useEffect, useRef } from "react";
|
|
4
4
|
import { getSafeUrl, isMobileDevice, getAllPlatformAvailability } from "./utils";
|
|
5
|
-
import { fetchOGData, type OGData } from "./og-fetcher";
|
|
5
|
+
import { fetchOGData, type OGData, type OGFetcher } from "./og-fetcher";
|
|
6
6
|
import {
|
|
7
7
|
shareToWhatsApp,
|
|
8
8
|
shareToTelegram,
|
|
@@ -279,9 +279,10 @@ export function useShareSheet({
|
|
|
279
279
|
|
|
280
280
|
/**
|
|
281
281
|
* Hook to fetch OG (Open Graph) data from a URL.
|
|
282
|
-
*
|
|
282
|
+
* Fetches with `fetcher`, the Microlink lookup (`fetchOGData`, cached) unless another is given:
|
|
283
|
+
* calling this hook is the opt-in, since the lookup sends `url` to a third party.
|
|
283
284
|
*/
|
|
284
|
-
export function useOGData(url: string | undefined): {
|
|
285
|
+
export function useOGData(url: string | undefined, fetcher: OGFetcher = fetchOGData): {
|
|
285
286
|
ogData: OGData | null;
|
|
286
287
|
loading: boolean;
|
|
287
288
|
error: string | null;
|
|
@@ -289,6 +290,12 @@ export function useOGData(url: string | undefined): {
|
|
|
289
290
|
const [ogData, setOgData] = useState<OGData | null>(null);
|
|
290
291
|
const [loading, setLoading] = useState(false);
|
|
291
292
|
const [error, setError] = useState<string | null>(null);
|
|
293
|
+
// Read at fetch time, so an inline fetcher does not refetch on every render
|
|
294
|
+
// (this effect runs before the one below in the same commit)
|
|
295
|
+
const fetcherRef = useRef(fetcher);
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
fetcherRef.current = fetcher;
|
|
298
|
+
});
|
|
292
299
|
|
|
293
300
|
useEffect(() => {
|
|
294
301
|
if (!url) {
|
|
@@ -302,7 +309,7 @@ export function useOGData(url: string | undefined): {
|
|
|
302
309
|
setLoading(true);
|
|
303
310
|
setError(null);
|
|
304
311
|
|
|
305
|
-
|
|
312
|
+
fetcherRef.current(url)
|
|
306
313
|
.then((data) => {
|
|
307
314
|
if (!cancelled) {
|
|
308
315
|
setOgData(data);
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { ShareSheetDrawer, ShareMenuDrawer } from "./ShareSheetDrawer";
|
|
|
6
6
|
export { useShareSheet, useShareMenu, useOGData, type UseShareSheetOptions, type UseShareMenuOptions } from "./hooks";
|
|
7
7
|
|
|
8
8
|
// OG Data fetcher
|
|
9
|
-
export { fetchOGData, clearOGCache, type OGData } from "./og-fetcher";
|
|
9
|
+
export { fetchOGData, clearOGCache, type OGData, type OGFetcher } from "./og-fetcher";
|
|
10
10
|
|
|
11
11
|
// Types
|
|
12
12
|
export type {
|
package/src/og-fetcher.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
// OG
|
|
1
|
+
// OG data fetcher using the Microlink API (free, no API key required).
|
|
2
|
+
// The sheet never calls it on its own: a caller opts in with `fetchPreview={fetchOGData}`,
|
|
3
|
+
// because the lookup sends the shared link to a third party.
|
|
2
4
|
|
|
3
5
|
export interface OGData {
|
|
4
6
|
title?: string;
|
|
@@ -8,6 +10,9 @@ export interface OGData {
|
|
|
8
10
|
siteName?: string;
|
|
9
11
|
}
|
|
10
12
|
|
|
13
|
+
/** Looks up a link's card. Resolves to null when there is none. */
|
|
14
|
+
export type OGFetcher = (url: string) => Promise<OGData | null>;
|
|
15
|
+
|
|
11
16
|
export interface OGFetchResult {
|
|
12
17
|
data: OGData | null;
|
|
13
18
|
loading: boolean;
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import type { OGFetcher } from "./og-fetcher";
|
|
2
3
|
|
|
3
4
|
/** CSS variable names for UI elements (non-platform specific) */
|
|
4
5
|
export const CSS_VARS_UI = {
|
|
@@ -100,7 +101,7 @@ export interface ShareSheetDrawerClassNames extends ShareSheetContentClassNames
|
|
|
100
101
|
export interface ShareSheetContentProps {
|
|
101
102
|
/** Title displayed at the top of the sheet */
|
|
102
103
|
title?: string;
|
|
103
|
-
/** URL to share
|
|
104
|
+
/** URL to share */
|
|
104
105
|
shareUrl: string;
|
|
105
106
|
/** Text to share alongside the URL */
|
|
106
107
|
shareText: string;
|
|
@@ -110,6 +111,12 @@ export interface ShareSheetContentProps {
|
|
|
110
111
|
downloadFilename?: string;
|
|
111
112
|
/** Custom preview image URL (skips OG fetching if provided) */
|
|
112
113
|
previewImage?: string | null;
|
|
114
|
+
/**
|
|
115
|
+
* Looks up the link's card when there is no `previewImage`. Unset (the default), no request
|
|
116
|
+
* leaves the page and the sheet shows a link placeholder. Pass `fetchOGData` for the Microlink
|
|
117
|
+
* lookup the sheet made by itself before 1.10.0: it sends `shareUrl` to api.microlink.io.
|
|
118
|
+
*/
|
|
119
|
+
fetchPreview?: OGFetcher;
|
|
113
120
|
/** File to share via native share (data URL or Blob) - takes priority over URL sharing */
|
|
114
121
|
shareFile?: string | Blob | null;
|
|
115
122
|
/** Filename for the shared file */
|