diff --git a/.changeset/four-carpets-wear.md b/.changeset/four-carpets-wear.md new file mode 100644 index 000000000000..7e322a469441 --- /dev/null +++ b/.changeset/four-carpets-wear.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Allows the ClientRouter to open new tabs or windows when submitting forms by clicking while holding the Cmd, Ctrl, or Shift key. diff --git a/packages/astro/components/ClientRouter.astro b/packages/astro/components/ClientRouter.astro index 8bda7b780fa0..df7a37da3d79 100644 --- a/packages/astro/components/ClientRouter.astro +++ b/packages/astro/components/ClientRouter.astro @@ -37,6 +37,7 @@ const { fallback = 'animate' } = Astro.props; import { init } from 'astro/virtual-modules/prefetch.js'; type Fallback = 'none' | 'animate' | 'swap'; + let lastClickedElementLeavingWindow: EventTarget | null = null; function getFallback(): Fallback { const el = document.querySelector('[name="astro-view-transitions-fallback"]'); @@ -50,6 +51,13 @@ const { fallback = 'animate' } = Astro.props; return el.dataset.astroReload !== undefined; } + const leavesWindow = (ev: MouseEvent) => + (ev.button && ev.button !== 0) || // left clicks only + ev.metaKey || // new tab (mac) + ev.ctrlKey || // new tab (windows) + ev.altKey || // download + ev.shiftKey; // new window + if (supportsViewTransitions || getFallback() !== 'none') { if (import.meta.env.DEV && window.matchMedia('(prefers-reduced-motion)').matches) { console.warn( @@ -58,6 +66,9 @@ const { fallback = 'animate' } = Astro.props; } document.addEventListener('click', (ev) => { let link = ev.target; + + lastClickedElementLeavingWindow = leavesWindow(ev) ? link : null; + if (ev.composed) { link = ev.composedPath()[0]; } @@ -82,11 +93,7 @@ const { fallback = 'animate' } = Astro.props; !link.href || (linkTarget && linkTarget !== '_self') || origin !== location.origin || - ev.button !== 0 || // left clicks only - ev.metaKey || // new tab (mac) - ev.ctrlKey || // new tab (windows) - ev.altKey || // download - ev.shiftKey || // new window + lastClickedElementLeavingWindow || ev.defaultPrevented ) { // No page transitions in these cases, @@ -102,11 +109,15 @@ const { fallback = 'animate' } = Astro.props; document.addEventListener('submit', (ev) => { let el = ev.target as HTMLElement; - if (el.tagName !== 'FORM' || ev.defaultPrevented || isReloadEl(el)) { + const submitter = ev.submitter; + + const clickedWithKeys = submitter && submitter === lastClickedElementLeavingWindow; + lastClickedElementLeavingWindow = null; + + if (el.tagName !== 'FORM' || ev.defaultPrevented || isReloadEl(el) || clickedWithKeys) { return; } const form = el as HTMLFormElement; - const submitter = ev.submitter; const formData = new FormData(form, submitter); // form.action and form.method can point to an or // in which case should fallback to the form attribute diff --git a/packages/astro/e2e/fixtures/view-transitions/public/favicon.ico b/packages/astro/e2e/fixtures/view-transitions/public/favicon.ico new file mode 100644 index 000000000000..578ad458b890 Binary files /dev/null and b/packages/astro/e2e/fixtures/view-transitions/public/favicon.ico differ