Skip to content

Commit 0461f56

Browse files
committed
refactor: Support anchor targets again
1 parent d401af0 commit 0461f56

File tree

2 files changed

+57
-41
lines changed

2 files changed

+57
-41
lines changed

src/router.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ import { useContext, useMemo, useReducer, useLayoutEffect, useRef } from 'preact
77
* @typedef {import('./internal.d.ts').VNode} VNode
88
*/
99

10+
/**
11+
* @param {NavigateEvent} e
12+
*/
13+
function isSameWindow(e) {
14+
const sourceElement = /** @type {HTMLAnchorElement | null} */ (e.sourceElement);
15+
return (
16+
!sourceElement ||
17+
!sourceElement.target ||
18+
/^(_self)?$/i.test(sourceElement.target)
19+
);
20+
}
21+
1022
/** @type {string | RegExp | undefined} */
1123
let scope;
1224

@@ -34,12 +46,11 @@ function handleNav(state, e) {
3446
!e.canIntercept ||
3547
e.hashChange ||
3648
e.downloadRequest !== null ||
37-
// Not yet implemented by Chrome, but coming?
38-
//!/^(_?self)?$/i.test(/** @type {HTMLAnchorElement} */ (e.sourceElement).target) ||
49+
!isSameWindow(e) ||
3950
!isInScope(url)
4051
) {
41-
// We only set this for our tests, it's otherwise very difficult to
42-
// determine if a navigation was intercepted or not externally.
52+
// This is set purely for our test suite so that we can check
53+
// if the event was ignored in another `navigate` handler.
4354
e['preact-iso-ignored'] = true;
4455
return state;
4556
}

test/router.test.js

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -549,20 +549,25 @@ describe('Router', () => {
549549
expect(loadEnd).not.to.have.been.called;
550550
});
551551

552-
// TODO: Relies on upcoming property being added to navigation events
553-
describe.skip('intercepted VS external links', () => {
554-
const shouldIntercept = [null, '', '_self', 'self', '_SELF'];
555-
const shouldNavigate = ['_top', '_parent', '_blank', 'custom', '_BLANK'];
552+
describe('intercepted VS external links', () => {
553+
const shouldIntercept = [null, '', '_self', '_SELF'];
554+
const shouldNavigate = ['_top', '_parent', '_blank', '_BLANK', 'custom'];
555+
556+
// Create an ID as Chrome & Safari don't support the
557+
// case-sensitivity flag for attribute selectors, meaning
558+
// we can't distinguish between `_self` and `_SELF`.
559+
const createId = (target) => {
560+
if (target === null) return 'null';
561+
if (target === '') return 'emptyString';
562+
return target;
563+
}
556564

557-
const Route = () => (
558-
<div>
559-
{[...shouldIntercept, ...shouldNavigate].map((target, i) => {
560-
const url = '/' + i + '/' + target;
561-
if (target === null) return <a href={url}>target = {target + ''}</a>;
562-
return <a href={url} target={target}>target = {target}</a>;
563-
})}
564-
</div>
565-
);
565+
const Links = () => <div>
566+
{[...shouldIntercept, ...shouldNavigate].map((target, i) => {
567+
const url = '/' + i + '/' + target;
568+
return <a href={url} target={target} id={createId(target)}>target = {target}</a>;
569+
})}
570+
</div>;
566571

567572
let triedToNavigate = false;
568573
const handler = (e) => {
@@ -572,34 +577,30 @@ describe('Router', () => {
572577
}
573578
}
574579

575-
beforeEach(async () => {
576-
const App = () => {
577-
useEffect(() => {
578-
navigation.addEventListener('navigate', handler);
579-
return () => navigation.removeEventListener('navigate', handler);
580-
}, []);
580+
const App = () => {
581+
useEffect(() => {
582+
navigation.addEventListener('navigate', handler);
583+
return () => navigation.removeEventListener('navigate', handler);
584+
}, []);
581585

582-
return (
583-
<LocationProvider>
584-
<Router>
585-
<Route default />
586-
</Router>
587-
<ShallowLocation />
588-
</LocationProvider>
589-
);
590-
}
586+
return (
587+
<LocationProvider>
588+
<Links />
589+
<ShallowLocation />
590+
</LocationProvider>
591+
);
592+
}
593+
594+
beforeEach(async () => {
591595
render(<App />, scratch);
592596
await sleep(10);
593597
});
594598

595599
const getName = target => (target == null ? 'no target attribute' : `target="${target}"`);
596600

597-
// these should all be intercepted by the router.
598601
for (const target of shouldIntercept) {
599602
it(`should intercept clicks on links with ${getName(target)}`, async () => {
600-
const sel = target == null ? `a:not([target])` : `a[target="${target}"]`;
601-
const el = scratch.querySelector(sel);
602-
if (!el) throw Error(`Unable to find link: ${sel}`);
603+
const el = scratch.querySelector(`#${createId(target)}`);
603604
const url = el.getAttribute('href');
604605
el.click();
605606
await sleep(1);
@@ -610,13 +611,17 @@ describe('Router', () => {
610611
});
611612
}
612613

613-
// these should all navigate.
614+
// TODO: These tests are rather flakey, for some reason the additional handler
615+
// occasionally isn't running prior browser actually navigates away.
614616
for (const target of shouldNavigate) {
615617
it(`should allow default browser navigation for links with ${getName(target)}`, async () => {
616-
const sel = target == null ? `a:not([target])` : `a[target="${target}"]`;
617-
const el = scratch.querySelector(sel);
618-
if (!el) throw Error(`Unable to find link: ${sel}`);
619-
el.click();
618+
// Currently cross-window navigations, (e.g., `target="_blank"`), do not trigger a
619+
// `navigate` event, which makes this difficult to observe. Per the spec, however, this
620+
// might be a bug in Chrome's implementation:
621+
// https://github.com/WICG/navigation-api?tab=readme-ov-file#restrictions-on-firing-canceling-and-responding
622+
if (target === '_blank' || target === '_BLANK' || target === 'custom') return;
623+
624+
scratch.querySelector(`#${createId(target)}`).click();
620625
await sleep(1);
621626
expect(triedToNavigate).to.be.true;
622627

0 commit comments

Comments
 (0)