From 35424476871f54fc419f8400bc13a5d494191e76 Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 19 Feb 2024 14:48:09 +0900 Subject: [PATCH 1/3] support: location.hash --- src/atomWithLocation.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/atomWithLocation.ts b/src/atomWithLocation.ts index 1bee996..102b01e 100644 --- a/src/atomWithLocation.ts +++ b/src/atomWithLocation.ts @@ -4,6 +4,7 @@ import type { SetStateAction, WritableAtom } from 'jotai/vanilla'; type Location = { pathname?: string; searchParams?: URLSearchParams; + hash?: string; }; const getLocation = (): Location => { @@ -13,6 +14,7 @@ const getLocation = (): Location => { return { pathname: window.location.pathname, searchParams: new URLSearchParams(window.location.search), + hash: window.location.hash, }; }; @@ -21,12 +23,15 @@ const applyLocation = ( options?: { replace?: boolean }, ): void => { const url = new URL(window.location.href); - if (location.pathname) { + if ('pathname' in location) { url.pathname = location.pathname; } - if (location.searchParams) { + if ('searchParams' in location) { url.search = location.searchParams.toString(); } + if ('hash' in location) { + url.hash = location.hash; + } if (options?.replace) { window.history.replaceState(null, '', url); } else { From 6b1bc1b66aac54f498d608ec18cd1cefbbbe6322 Mon Sep 17 00:00:00 2001 From: Filip Lindahl Date: Sun, 10 Mar 2024 10:57:00 +0100 Subject: [PATCH 2/3] test(atomWithLocation) Tests for support location (#32) * first test for hash * add tests for searchparams --- __tests__/atomWithLocation_spec.tsx | 440 ++++++++++++++++++++++++++-- 1 file changed, 422 insertions(+), 18 deletions(-) diff --git a/__tests__/atomWithLocation_spec.tsx b/__tests__/atomWithLocation_spec.tsx index 3a26cb4..05864ab 100644 --- a/__tests__/atomWithLocation_spec.tsx +++ b/__tests__/atomWithLocation_spec.tsx @@ -12,41 +12,125 @@ function assertPathNameAndHistoryLength( expect(window.history.length).toEqual(expectedHistoryLength); } +function assertHashAndHistoryLength( + expectedHash: string, + expectedHistoryLength: number, +) { + expect(window.location.hash).toEqual(expectedHash); + expect(window.history.length).toEqual(expectedHistoryLength); +} + +function assertSearchParamAndHistoryLength( + expectedSearchParams: URLSearchParams, + expectedHistoryLength: number, +) { + expect(window.location.search).toEqual(`?${expectedSearchParams.toString()}`); + expect(window.history.length).toEqual(expectedHistoryLength); +} + async function assertStartState( startTestHistoryLength: number, findByText: any, + assert: 'pathname' | 'hash' | 'searchParams' | 'all', ) { - await findByText('current pathname in atomWithLocation: /'); - assertPathNameAndHistoryLength('/', startTestHistoryLength); + if (assert === 'pathname' || assert === 'all') { + await findByText('current pathname in atomWithLocation: /'); + assertPathNameAndHistoryLength('/', startTestHistoryLength); + } + if (assert === 'hash' || assert === 'all') { + await findByText('current hash in atomWithLocation: #'); + assertHashAndHistoryLength('', startTestHistoryLength); + } + if (assert === 'searchParams' || assert === 'all') { + await findByText('current searchParams in atomWithLocation:'); + expect(window.location.search).toEqual(''); + expect(window.history.length).toEqual(startTestHistoryLength); + } } function clickButtonAndAssertTemplate(localFindByText: any) { return async function clickButtonAndAssert( target: `button${number}` | 'back' | 'buttonWithReplace' | 'buttonWithPush', historyLength: number, - targetPathName?: string, + { + targetPathName, + targetHash, + targetSearchParams, + }: { + targetPathName?: string; + targetHash?: string; + targetSearchParams?: string; + } = {}, + assert: 'pathname' | 'hash' | 'search' | 'all' = 'pathname', ) { let expectedPathname: string = '/'; + let expectedHash: string = ''; + let expectedSearchParams = new URLSearchParams(); if (target === 'buttonWithReplace') { expectedPathname = '/123'; + expectedHash = '#tab=1'; + expectedSearchParams.set('tab', '1'); } else if (target === 'buttonWithPush') { expectedPathname = '/234'; + expectedHash = '#tab=2'; + expectedSearchParams.set('tab', '2'); } else if (target.startsWith('button')) { expectedPathname = `/${target.slice(-1)}`; - } else if (target === 'back' && targetPathName) { - expectedPathname = targetPathName; + expectedHash = `#tab=${target.slice(-1)}`; + expectedSearchParams.set('tab', target.slice(-1)); + } else if (target === 'back') { + expectedPathname = targetPathName ?? ''; + expectedHash = `#${targetHash ?? ''}`; + expectedSearchParams = new URLSearchParams(); + expectedSearchParams.set('tab', targetSearchParams ?? ''); } await userEvent.click(await localFindByText(target)); - await localFindByText( - `current pathname in atomWithLocation: ${expectedPathname}`, - ); - assertPathNameAndHistoryLength(expectedPathname, historyLength); + if (assert === 'pathname') { + await localFindByText( + `current pathname in atomWithLocation: ${expectedPathname}`, + ); + assertPathNameAndHistoryLength(expectedPathname, historyLength); + } + if (assert === 'hash') { + await localFindByText( + `current hash in atomWithLocation: ${expectedHash}`, + ); + assertHashAndHistoryLength( + expectedHash === '#' ? '' : expectedHash, + historyLength, + ); + } + if (assert === 'search') { + await localFindByText(`${expectedSearchParams.toString()}`); + assertSearchParamAndHistoryLength(expectedSearchParams, historyLength); + } + if (assert === 'all') { + await localFindByText( + `current pathname in atomWithLocation: ${expectedPathname}`, + ); + assertPathNameAndHistoryLength(expectedPathname, historyLength); + await localFindByText( + `current hash in atomWithLocation: ${expectedHash}`, + ); + assertHashAndHistoryLength(expectedHash, historyLength); + await localFindByText( + `current searchParams in atomWithLocation: ${expectedSearchParams}`, + ); + assertSearchParamAndHistoryLength(expectedSearchParams, historyLength); + } }; } -describe('atomWithLocation', () => { +const defaultLocation = { + pathname: '/', + search: '', + hash: '', + state: null, +}; + +describe('atomWithLocation, pathName', () => { beforeEach(() => { - window.history.pushState(null, '', '/'); + resetWindow(); }); it('can replace state', async () => { @@ -73,6 +157,12 @@ describe('atomWithLocation', () => { > button2 + ); }; @@ -85,10 +175,12 @@ describe('atomWithLocation', () => { const clickButtonAndAssert = clickButtonAndAssertTemplate(findByText); const startHistoryLength = window.history.length; - assertStartState(startHistoryLength, findByText); + await assertStartState(startHistoryLength, findByText, 'pathname'); await clickButtonAndAssert('button1', startHistoryLength); await clickButtonAndAssert('button2', startHistoryLength); + + await userEvent.click(await findByText('reset-button')); }); it('can push state', async () => { @@ -115,6 +207,12 @@ describe('atomWithLocation', () => { > button2 + ); }; @@ -127,11 +225,15 @@ describe('atomWithLocation', () => { const clickButtonAndAssert = clickButtonAndAssertTemplate(findByText); const startHistoryLength = window.history.length; - assertStartState(startHistoryLength, findByText); + assertStartState(startHistoryLength, findByText, 'pathname'); await clickButtonAndAssert('button1', startHistoryLength + 1); await clickButtonAndAssert('button2', startHistoryLength + 2); - await clickButtonAndAssert('back', startHistoryLength + 2, '/1'); + await clickButtonAndAssert('back', startHistoryLength + 2, { + targetPathName: '/1', + }); + + await userEvent.click(await findByText('reset-button')); }); it('can override atomOptions, from replace=false to replace=true', async () => { @@ -164,6 +266,12 @@ describe('atomWithLocation', () => { > buttonWithReplace + ); }; @@ -177,11 +285,13 @@ describe('atomWithLocation', () => { const clickButtonAndAssert = clickButtonAndAssertTemplate(findByText); const startHistoryLength = window.history.length; - assertStartState(startHistoryLength, findByText); + assertStartState(startHistoryLength, findByText, 'pathname'); await clickButtonAndAssert('buttonWithPush', startHistoryLength + 1); await clickButtonAndAssert('buttonWithReplace', startHistoryLength + 1); - await clickButtonAndAssert('back', startHistoryLength + 1, '/'); + await clickButtonAndAssert('back', startHistoryLength + 1, { + targetPathName: '/', + }); // This click overwrites the history entry we // went back from. The history length remains the same. @@ -189,6 +299,8 @@ describe('atomWithLocation', () => { // The second click adds a new history entry, which now increments the history length. await clickButtonAndAssert('buttonWithPush', startHistoryLength + 2); + + await userEvent.click(await findByText('reset-button')); }); it('can override atomOptions, from replace=true to replace=false', async () => { @@ -221,6 +333,12 @@ describe('atomWithLocation', () => { > buttonWithPush + ); }; @@ -233,14 +351,300 @@ describe('atomWithLocation', () => { const clickButtonAndAssert = clickButtonAndAssertTemplate(findByText); const startTestHistoryLength = window.history.length; - assertStartState(startTestHistoryLength, findByText); + assertStartState(startTestHistoryLength, findByText, 'pathname'); await clickButtonAndAssert('buttonWithReplace', startTestHistoryLength); await clickButtonAndAssert('buttonWithPush', startTestHistoryLength + 1); - await clickButtonAndAssert('back', startTestHistoryLength + 1, '/123'); + await clickButtonAndAssert('back', startTestHistoryLength + 1, { + targetPathName: '/123', + }); await clickButtonAndAssert('buttonWithReplace', startTestHistoryLength + 1); await clickButtonAndAssert('buttonWithPush', startTestHistoryLength + 1); await clickButtonAndAssert('buttonWithPush', startTestHistoryLength + 2); + + await userEvent.click(await findByText('reset-button')); + }); +}); + +describe('atomWithLocation, hash', () => { + beforeEach(() => { + resetWindow(); + }); + + it('can push state with hash', async () => { + const locationAtom = atomWithLocation({ replace: false }); + + const Navigation = () => { + const [location, setLocation] = useAtom(locationAtom); + return ( + <> +
current hash in atomWithLocation: #{location.hash}
+ + + + + + ); + }; + + const { findByText } = render( + + + , + ); + + const clickButtonAndAssert = clickButtonAndAssertTemplate(findByText); + const startHistoryLength = window.history.length; + assertStartState(startHistoryLength, findByText, 'hash'); + + await clickButtonAndAssert( + 'button1', + startHistoryLength + 1, + undefined, + 'hash', + ); + await clickButtonAndAssert( + 'button2', + startHistoryLength + 2, + undefined, + 'hash', + ); + + await userEvent.click(await findByText('reset-button')); + }); + + it('can replace state with hash', async () => { + const locationAtom = atomWithLocation({ replace: true }); + + const Navigation = () => { + const [location, setLocation] = useAtom(locationAtom); + return ( + <> +
current hash in atomWithLocation: #{location.hash}
+ + + + + + ); + }; + + const { findByText } = render( + + + , + ); + + const clickButtonAndAssert = clickButtonAndAssertTemplate(findByText); + const startHistoryLength = window.history.length; + assertStartState(startHistoryLength, findByText, 'hash'); + + await clickButtonAndAssert( + 'button1', + startHistoryLength, + undefined, + 'hash', + ); + await clickButtonAndAssert( + 'button2', + startHistoryLength, + undefined, + 'hash', + ); + + await clickButtonAndAssert( + 'back', + startHistoryLength, + { targetHash: '' }, + 'hash', + ); + + await userEvent.click(await findByText('reset-button')); + }); +}); + +function resetWindow() { + window.history.pushState(null, '', '/'); + window.location.search = ''; + window.location.hash = ''; +} + +describe('atomWithLocation, searchParams', () => { + beforeEach(() => { + resetWindow(); + }); + it('can push state with searchParams', async () => { + const locationAtom = atomWithLocation({ replace: false }); + + const Navigation = () => { + const [location, setLocation] = useAtom(locationAtom); + const tab1Params = new URLSearchParams(); + tab1Params.set('tab', '1'); + const tab2Params = new URLSearchParams(); + tab2Params.set('tab', '2'); + + return ( + <> +
+ current searchParams in atomWithLocation: +
{location.searchParams?.toString()}
+
+ + + + + + ); + }; + + const { findByText } = render( + + + , + ); + + const clickButtonAndAssert = clickButtonAndAssertTemplate(findByText); + const startHistoryLength = window.history.length; + await assertStartState(startHistoryLength, findByText, 'searchParams'); + await clickButtonAndAssert( + 'button1', + startHistoryLength + 1, + undefined, + 'search', + ); + await clickButtonAndAssert( + 'button2', + startHistoryLength + 2, + undefined, + 'search', + ); + + await userEvent.click(await findByText('reset-button')); + }); + + it('can replace state with searchParams', async () => { + const locationAtom = atomWithLocation({ replace: true }); + + const Navigation = () => { + const [location, setLocation] = useAtom(locationAtom); + const tab1Params = new URLSearchParams(); + tab1Params.set('tab', '1'); + const tab2Params = new URLSearchParams(); + tab2Params.set('tab', '2'); + + return ( + <> +
current searchParams in atomWithLocation:
+
{location.searchParams?.toString()}
+ + + + + + ); + }; + + const { findByText } = render( + + + , + ); + + const clickButtonAndAssert = clickButtonAndAssertTemplate(findByText); + const startHistoryLength = window.history.length; + assertStartState(startHistoryLength, findByText, 'searchParams'); + + await clickButtonAndAssert( + 'button1', + startHistoryLength, + undefined, + 'search', + ); + await clickButtonAndAssert( + 'button2', + startHistoryLength, + undefined, + 'search', + ); + + await clickButtonAndAssert( + 'back', + startHistoryLength, + { targetSearchParams: '2' }, + 'search', + ); + + await userEvent.click(await findByText('reset-button')); }); }); From c83f908f3786ca691a9bd4bdeb5dcc3f271fc9b1 Mon Sep 17 00:00:00 2001 From: Filip Lindahl Date: Sun, 10 Mar 2024 16:59:24 +0100 Subject: [PATCH 3/3] update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0563471..16a93aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Change Log ## [Unreleased] +- feat(atomWithLocation): support location.hash ## [0.5.4] - 2024-02-27 ### Changed