diff --git a/.changeset/share-solid2-migration.md b/.changeset/share-solid2-migration.md new file mode 100644 index 000000000..213276e35 --- /dev/null +++ b/.changeset/share-solid2-migration.md @@ -0,0 +1,14 @@ +--- +"@solid-primitives/share": major +--- + +Migrate to Solid.js v2.0 (beta.10) + +## Breaking Changes + +**Peer dependencies**: `solid-js@^2.0.0-beta.10` and `@solidjs/web@^2.0.0-beta.10` are now required. + +### `@solid-primitives/share` + +- `isServer` is now imported from `@solidjs/web` (was `solid-js/web`) +- `createWebShare` — internal `createEffect` converted to the split compute/apply pattern required by Solid 2.0; the `on` helper (removed in Solid 2.0) is no longer used diff --git a/packages/share/README.md b/packages/share/README.md index 87a7a298f..86ba4f7a7 100644 --- a/packages/share/README.md +++ b/packages/share/README.md @@ -11,8 +11,8 @@ Primitives for supporting sharing of resources on social media and beyond. - [`createSocialShare`](#createsocialshare) - A primitive for sharing on social media and beyond. -- [`makeWebShare`](#makewebshare) - Generates a simple non-reactive WebShare primitive for sharing. -- [`createWebShare`](#createwebshare) - Creates a reactive status about web share. +- [`makeWebShare`](#makewebshare) - A simple non-reactive base primitive for the Web Share API. +- [`createWebShare`](#createwebshare) - A reactive action-based primitive for the Web Share API with status tracking. ## Installation @@ -31,11 +31,12 @@ pnpm add @solid-primitives/share ```ts import { createSocialShare, BLUESKY } from "@solid-primitives/share"; -const [share, close] = createSocialShare(() => ({ +const { share, close, isSharing } = createSocialShare(() => ({ title: "SolidJS.com", url: "https://www.solidjs.com", description: "Simple and well-behaved reactivity!", })); + share(BLUESKY); ``` @@ -56,7 +57,13 @@ function createSocialShare( popup?: SharePopupOptions; }>, controller: Window = window, -): [share: (network: Network | undefined) => void, close: () => void, isSharing: Accessor]; +): SocialShareResult; + +type SocialShareResult = { + share: (network?: Network) => void; + close: () => void; + isSharing: Accessor; +}; ``` ### Network List @@ -65,9 +72,8 @@ The following are a list of supported networks that may be imported from the sha | Network | `url` | `title` | `description` | Extras/Comments | | ------------- | ------------------ | ------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------- | -| Baidu | :heavy_check_mark: | :heavy_check_mark: | :x: | | +| Bluesky | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | Buffer | :heavy_check_mark: | :heavy_check_mark: | :x: | | -| Bluesky | :heavy_check_mark: | :heavy_check_mark: | :x: | | | Email | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | EverNote | :heavy_check_mark: | :heavy_check_mark: | :x: | | | Facebook | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | `hashtags` A list of comma-separated hashtags, only the first one will be used.
`quote` Facebook quote. | @@ -80,25 +86,28 @@ The following are a list of supported networks that may be imported from the sha | Odnoklassniki | :heavy_check_mark: | :heavy_check_mark: | :x: | | | Pinterest | :heavy_check_mark: | :heavy_check_mark: | :x: | `media` URL of an image describing the content. | | Pocket | :heavy_check_mark: | :heavy_check_mark: | :x: | | +| Quora | :heavy_check_mark: | :heavy_check_mark: | :x: | | | Reddit | :heavy_check_mark: | :heavy_check_mark: | :x: | | | Skype | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | SMS | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | -| StumbleUpon | :heavy_check_mark: | :heavy_check_mark: | :x: | | | Telegram | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | Tumblr | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | -| Twitter | :heavy_check_mark: | :heavy_check_mark: | :x: | `hashtags` A list of comma-separated hashtags.
`twitter-user` Twitter user to mention. | -| X | :heavy_check_mark: | :heavy_check_mark: | :x: | `hashtags` A list of comma-separated hashtags.
`twitter-user` X user to mention. | +| Twitter | :heavy_check_mark: | :heavy_check_mark: | :x: | `hashtags` A list of comma-separated hashtags.
`twitterUser` Twitter user to mention. | | Viber | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | VK | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | `media` URL of an image describing the content. | +| Warpcast | :heavy_check_mark: | :heavy_check_mark: | :x: | Farcaster decentralized social network. | | Weibo | :heavy_check_mark: | :heavy_check_mark: | :x: | `media` URL of an image describing the content. | | WhatsApp | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | Wordpress | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | `media` URL of an image describing the content. | +| X | :heavy_check_mark: | :heavy_check_mark: | :x: | `hashtags` A list of comma-separated hashtags.
`twitterUser` X user to mention. | | Xing | :heavy_check_mark: | :heavy_check_mark: | :x: | | | Yammer | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | -For the networks: `Bluesky`, `Line`, `Skype`, `SMS`, `Telegram`, `Viber`, `WhatsApp` and `Yammer`; the shared content is a string of the form: "`$title` `$url` `$description`" +> **Deprecated:** `STUMBLEUPON` (shut down 2018) and `MESSANGER` (typo — use `MESSENGER`) are still exported for backwards compatibility but will be removed in a future version. + +For the networks `Bluesky`, `Line`, `Skype`, `SMS`, `Telegram`, `Viber`, `WhatsApp`, and `Yammer` the shared content is a string of the form: "`$title` `$url` `$description`". -Note that you can also provide your own custom network by formatting the input string into the share function. The following is a list of properties that will be replaced by the utility: +You can also provide a custom network by formatting a URL string with the following replacement markers: - `@u`: URL - `@t`: Title @@ -106,9 +115,9 @@ Note that you can also provide your own custom network by formatting the input s - `@q`: Quote - `@h`: Hashtags - `@m`: Media -- `@tu`: X User (X specific) +- `@tu`: X/Twitter user mention -The following is an example of X's share string: +Example: ```ts const x: Network = "https://www.x.com/intent/tweet?text=@t&url=@u&hashtags=@h@tu"; @@ -124,7 +133,7 @@ A portion of this primitive was built from https://github.com/nicolasbeauvais/vu ## `makeWebShare` -Generates a simple non-reactive WebShare primitive for sharing. Uses the [WebShare API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API). +A simple non-reactive base primitive wrapping the [Web Share API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API). Returns a `share` function that rejects with a descriptive message if the browser does not support sharing or file sharing. ### How to use it @@ -136,25 +145,50 @@ const share = makeWebShare(); try { await share({ url: "https://solidjs.com" }); } catch (e) { - console.log(e); + console.error(e); } ``` ## `createWebShare` -Creates a reactive status about web share. Uses the [WebShare API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API). +A reactive, action-based primitive for the [Web Share API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API). Call `share` imperatively on a user gesture and observe the result via reactive accessors. ### How to use it ```ts import { createWebShare } from "@solid-primitives/share"; -const [data, setData] = createSignal({}); -const shareStatus = createWebShare(data); +const { share, pending, status, message } = createWebShare(); +``` -createEffect(() => { - console.log(shareStatus.status, shareStatus.message); -}); +```tsx + + + +

Share failed: {message()}

+
+``` + +### Definition + +```ts +function createWebShare(): WebShareResult; + +type WebShareResult = { + /** Imperatively trigger the Web Share API with the provided data. */ + share: (data: ShareData) => Promise; + /** True while the share dialog is open / the promise is pending. */ + pending: Accessor; + /** True on success, false on failure, undefined before first share. */ + status: Accessor; + /** The error message if the share failed, otherwise undefined. */ + message: Accessor; +}; ``` ## Changelog diff --git a/packages/share/dev/index.tsx b/packages/share/dev/index.tsx index 08edaec68..cb678bc17 100644 --- a/packages/share/dev/index.tsx +++ b/packages/share/dev/index.tsx @@ -1,4 +1,4 @@ -import { type Component, createEffect, createSignal } from "solid-js"; +import { type Component, createSignal } from "solid-js"; import { createWebShare } from "../src/index.js"; diff --git a/packages/share/package.json b/packages/share/package.json index ebef4a432..d6622384f 100644 --- a/packages/share/package.json +++ b/packages/share/package.json @@ -1,6 +1,6 @@ { "name": "@solid-primitives/share", - "version": "2.2.4", + "version": "3.0.0", "description": "Primitives to help with sharing content on social media and beyond.", "author": "David Di Biase ", "contributors": [ @@ -18,7 +18,8 @@ "stage": 3, "list": [ "createSocialShare", - "createWebShare" + "createWebShare", + "makeWebShare" ], "category": "Utilities" }, @@ -52,10 +53,13 @@ "test:ssr": "pnpm run vitest --mode ssr" }, "peerDependencies": { - "solid-js": "^1.6.12" + "@solidjs/web": "^2.0.0-beta.10", + "solid-js": "^2.0.0-beta.10" }, "typesVersions": {}, "devDependencies": { - "solid-js": "^1.9.7" + "@solid-primitives/utils": "workspace:^", + "@solidjs/web": "2.0.0-beta.10", + "solid-js": "2.0.0-beta.10" } } diff --git a/packages/share/src/networks.ts b/packages/share/src/networks.ts index d69d31e21..092b764ab 100644 --- a/packages/share/src/networks.ts +++ b/packages/share/src/networks.ts @@ -12,18 +12,18 @@ */ export type Network = string; -export const BAIDU: Network = "http://cang.baidu.com/do/add?iu=@u&it=@t"; -export const BUFFER: Network = "https://bufferapp.com/add?text=@t&url=@u"; +export const BLUESKY: Network = "https://bsky.app/intent/compose?text=@t%0D%0A@u%0D%0A@d"; +export const BUFFER: Network = "https://buffer.com/add?text=@t&url=@u"; export const EMAIL: Network = "mailto:?subject=@t&body=@u%0D%0A@d"; export const EVERNOTE: Network = "https://www.evernote.com/clip.action?url=@u&title=@t"; -export const BLUESKY: Network = "https://bsky.app/intent/compose?text=@t%0D%0A@u%0D%0A@d"; export const FACEBOOK: Network = "https://www.facebook.com/sharer/sharer.php?u=@u&title=@t&description=@d"e=@q&hashtag=@h"; export const FLIPBOARD: Network = "https://share.flipboard.com/bookmarklet/popout?v=2&url=@u&title=@t"; export const HACKERNEWS: Network = "https://news.ycombinator.com/submitlink?u=@u&t=@t"; -export const INSTAPAPER: Network = "http://www.instapaper.com/edit?url=@u&title=@t&description=@d"; -export const LINE: Network = "http://line.me/R/msg/text/?@t%0D%0A@u%0D%0A@d"; +export const INSTAPAPER: Network = + "https://www.instapaper.com/edit?url=@u&title=@t&description=@d"; +export const LINE: Network = "https://line.me/R/msg/text/?@t%0D%0A@u%0D%0A@d"; export const LINKEDIN: Network = "https://www.linkedin.com/sharing/share-offsite/?url=@u"; export const MESSENGER: Network = "fb-messenger://share/?link=@u"; /** @deprecated Use MESSENGER instead - this will be removed in a future version */ @@ -37,17 +37,19 @@ export const QUORA: Network = "https://www.quora.com/share?url=@u&title=@t"; export const REDDIT: Network = "https://www.reddit.com/submit?url=@u&title=@t"; export const SKYPE: Network = "https://web.skype.com/share?url=@t%0D%0A@u%0D%0A@d"; export const SMS: Network = "sms:?body=@t%0D%0A@u%0D%0A@d"; +/** @deprecated StumbleUpon shut down in 2018 - this will be removed in a future version */ export const STUMBLEUPON: Network = "https://www.stumbleupon.com/submit?url=@u&title=@t"; export const TELEGRAM: Network = "https://t.me/share/url?url=@u&text=@t%0D%0A@d"; export const TUMBLR: Network = "https://www.tumblr.com/share/link?url=@u&name=@t&description=@d"; export const TWITTER: Network = "https://twitter.com/intent/tweet?text=@t&url=@u&hashtags=@h@tu"; -export const X: Network = "https://www.x.com/intent/tweet?text=@t&url=@u&hashtags=@h@tu"; export const VIBER: Network = "viber://forward?text=@t%0D%0A@u%0D%0A@d"; export const VK: Network = "https://vk.com/share.php?url=@u&title=@t&description=@d&image=@m&noparse=true"; -export const WEIBO: Network = "http://service.weibo.com/share/share.php?url=@u&title=@t&pic=@m"; +export const WARPCAST: Network = "https://warpcast.com/~/compose?text=@t%0D%0A@u"; +export const WEIBO: Network = "https://service.weibo.com/share/share.php?url=@u&title=@t&pic=@m"; export const WHATSAPP: Network = "https://api.whatsapp.com/send?text=@t%0D%0A@u%0D%0A@d"; export const WORDPRESS: Network = "https://wordpress.com/press-this.php?u=@u&t=@t&s=@d&i=@m"; +export const X: Network = "https://www.x.com/intent/tweet?text=@t&url=@u&hashtags=@h@tu"; export const XING: Network = "https://www.xing.com/social/share/spi?op=share&url=@u&title=@t"; export const YAMMER: Network = "https://www.yammer.com/messages/new?login=true&status=@t%0D%0A@u%0D%0A@d"; diff --git a/packages/share/src/social-share.ts b/packages/share/src/social-share.ts index 417ea55ff..35e8f4f42 100644 --- a/packages/share/src/social-share.ts +++ b/packages/share/src/social-share.ts @@ -1,5 +1,6 @@ import { type Accessor, createSignal } from "solid-js"; -import { isServer } from "solid-js/web"; +import { isServer } from "@solidjs/web"; +import { INTERNAL_OPTIONS } from "@solid-primitives/utils"; import { type Network } from "./networks.js"; export type SharePopupOptions = { @@ -24,8 +25,18 @@ export type SharePopupOptions = { * @param {tag} string A tag to associate with the share * @param {popup} SharePopupOptions An object representing the pop-up window controls * @param {controller} Window Controller to bind the share to - * @return Returns a share, close and is sharing signal. + * @return An object with `share`, `close`, and `isSharing`. */ + +export type SocialShareResult = { + /** Open the share popup for the given network (falls back to `options().network`). */ + share: (network?: Network) => void; + /** Close the share popup. */ + close: () => void; + /** True while the share popup window is open. */ + isSharing: Accessor; +}; + export const createSocialShare = ( options: Accessor<{ network?: Network; @@ -44,23 +55,19 @@ export const createSocialShare = ( description: "", }), controller: Window = isServer ? (globalThis as any) : window, -): [ - share: (network: Network | undefined) => void, - close: () => void, - isSharing: Accessor, -] => { +): SocialShareResult => { if (isServer) { - return [ - () => { + return { + share: () => { /*noop*/ }, - () => { + close: () => { /*noop*/ }, - () => false, - ]; + isSharing: () => false, + }; } - const [isSharing, setIsSharing] = createSignal(false); + const [isSharing, setIsSharing] = createSignal(false, INTERNAL_OPTIONS); let popupInterval: null | ReturnType = null; let popupWindow: null | Window; let popup = { @@ -158,5 +165,5 @@ export const createSocialShare = ( }, 500); setIsSharing(true); }; - return [share, close, isSharing]; + return { share, close, isSharing }; }; diff --git a/packages/share/src/web-share.ts b/packages/share/src/web-share.ts index 948707f9f..8d0e32c16 100644 --- a/packages/share/src/web-share.ts +++ b/packages/share/src/web-share.ts @@ -1,5 +1,6 @@ -import { type Accessor, createEffect, createSignal, on, type OnOptions } from "solid-js"; -import { isServer } from "solid-js/web"; +import { type Accessor, action, createOptimistic, createSignal } from "solid-js"; +import { isServer } from "@solidjs/web"; +import { INTERNAL_OPTIONS } from "@solid-primitives/utils"; /** * Generates a simple non-reactive WebShare primitive for sharing. @@ -40,59 +41,60 @@ export const makeWebShare = () => { return share; }; -export type ShareStatus = { - /** The status of sharing success, failed or pending. */ - status?: boolean; - - /** The reason why sharing failed. */ - message?: string; +export type WebShareResult = { + /** Imperatively trigger the Web Share API with the provided data. */ + share: (data: ShareData) => Promise; + /** True while the share dialog is open / the promise is pending. */ + pending: Accessor; + /** True on success, false on failure, undefined before first share. */ + status: Accessor; + /** The error message if the share failed, otherwise undefined. */ + message: Accessor; }; /** - * Creates a reactive status about web share. + * Creates an action-based Web Share primitive with reactive status tracking. * - * @param data Data signal to share on web. - * @param deferInitial - Sets the value of the web share data from the signal. defaults to false. - * @return A store shows sharing status and failing message. + * @returns An object with a `share` action and reactive `pending`, `status`, and `message` accessors. * * @example * ```ts - * const [data, setData] = createSignal({}); - * const shareStatus = createWebShare(data); + * const { share, pending, status, message } = createWebShare(); * - * createEffect(() => { - * console.log(shareStatus.status, shareStatus.message) - * }) + * // Call imperatively on user gesture: + * * ``` */ -export const createWebShare = ( - data: Accessor, - deferInitial: boolean = false, -): ShareStatus => { +export const createWebShare = (): WebShareResult => { if (isServer) { - return {}; + return { + share: () => Promise.resolve(), + pending: () => false, + status: () => undefined, + message: () => undefined, + }; } - const [status, setStatus] = createSignal({}); - const share = makeWebShare(); - createEffect( - on( - data, - dataValue => { - setStatus({}); - share(dataValue) - .then(() => setStatus({ status: true })) - .catch(e => setStatus({ status: false, message: e.toString() })); - }, - { defer: deferInitial } satisfies OnOptions as any, - ), - ); - return { - get status() { - return status().status; - }, - get message() { - return status().message; - }, - }; + const [pending, setPending] = createOptimistic(false, INTERNAL_OPTIONS); + const [status, setStatus] = createSignal(undefined, INTERNAL_OPTIONS); + const [message, setMessage] = createSignal(undefined, INTERNAL_OPTIONS); + const baseShare = makeWebShare(); + + const share = action(function* (data: ShareData) { + setPending(true); + setStatus(undefined); + setMessage(undefined); + try { + yield baseShare(data); + setStatus(true); + } catch (e: unknown) { + setStatus(false); + setMessage(String(e)); + } + setPending(false); + }); + + return { share, pending, status, message }; }; diff --git a/packages/share/test/index.test.ts b/packages/share/test/index.test.ts index 35736167c..3f8335df1 100644 --- a/packages/share/test/index.test.ts +++ b/packages/share/test/index.test.ts @@ -1,19 +1,73 @@ -import { describe, test, expect, it } from "vitest"; -import { createRoot, createSignal } from "solid-js"; -import { createWebShare } from "../src/index.js"; +import { describe, test, expect, vi } from "vitest"; +import { createRoot, flush } from "solid-js"; +import { createWebShare, createSocialShare } from "../src/index.js"; describe("createWebShare", () => { - test("createWebShare initial values", () => - createRoot(async dispose => { - const [data] = createSignal({}); - const status = createWebShare(data); + test("initial values", () => + createRoot(dispose => { + const { pending, status, message } = createWebShare(); - expect(status.status, "Test starting status should be undefined.").toBe(undefined); - expect(status.message, "Test starting message should be undefined.").toBe(undefined); + expect(pending()).toBe(false); + expect(status()).toBe(undefined); + expect(message()).toBe(undefined); dispose(); })); - // todo: Asynchronous test. - it.todo("Asynchronous test to change data."); + test("share resolves and sets success status", async () => { + await createRoot(async dispose => { + Object.defineProperty(navigator, "share", { + value: vi.fn().mockResolvedValue(undefined), + configurable: true, + }); + + const { share, pending, status, message } = createWebShare(); + + const promise = share({ url: "https://solidjs.com" }); + flush(); + expect(pending()).toBe(true); + + await promise; + flush(); + expect(pending()).toBe(false); + expect(status()).toBe(true); + expect(message()).toBe(undefined); + + dispose(); + }); + }); + + test("share sets failure status on rejection", async () => { + await createRoot(async dispose => { + Object.defineProperty(navigator, "share", { + value: vi.fn().mockRejectedValue("share failed"), + configurable: true, + }); + + const { share, pending, status, message } = createWebShare(); + + await share({ url: "https://solidjs.com" }); + flush(); + expect(pending()).toBe(false); + expect(status()).toBe(false); + expect(message()).toBe("share failed"); + + dispose(); + }); + }); +}); + +describe("createSocialShare", () => { + test("initial values", () => + createRoot(dispose => { + const { share, close, isSharing } = createSocialShare( + () => ({ url: "https://solidjs.com", title: "SolidJS", description: "A reactive library" }), + ); + + expect(typeof share).toBe("function"); + expect(typeof close).toBe("function"); + expect(isSharing()).toBe(false); + + dispose(); + })); }); diff --git a/packages/share/test/server.test.ts b/packages/share/test/server.test.ts index 957099709..981e9a8e5 100644 --- a/packages/share/test/server.test.ts +++ b/packages/share/test/server.test.ts @@ -1,15 +1,32 @@ -import { createRoot, createSignal } from "solid-js"; +import { createRoot } from "solid-js"; import { describe, test, expect } from "vitest"; -import { createWebShare } from "../src/index.js"; +import { createWebShare, createSocialShare } from "../src/index.js"; describe("createWebShare", () => { test("doesn't break in SSR", () => { createRoot(dispose => { - const [data] = createSignal({}); - const status = createWebShare(data); + const { share, pending, status, message } = createWebShare(); - expect(status.status, "Server test starting status should be undefined.").toBe(undefined); - expect(status.message, "Server test starting message should be undefined.").toBe(undefined); + expect(typeof share).toBe("function"); + expect(pending()).toBe(false); + expect(status()).toBe(undefined); + expect(message()).toBe(undefined); + + dispose(); + }); + }); +}); + +describe("createSocialShare", () => { + test("doesn't break in SSR", () => { + createRoot(dispose => { + const { share, close, isSharing } = createSocialShare( + () => ({ url: "https://solidjs.com", title: "SolidJS", description: "A reactive library" }), + ); + + expect(typeof share).toBe("function"); + expect(typeof close).toBe("function"); + expect(isSharing()).toBe(false); dispose(); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee1923c5f..1fbe96caf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -942,9 +942,15 @@ importers: packages/share: devDependencies: + '@solid-primitives/utils': + specifier: workspace:^ + version: link:../utils + '@solidjs/web': + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10 packages/signal-builders: dependencies: