Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add synchronized, persistent tab groups (#2172)
I was working on the quickstart stuff for w3up and found myself really wishing that I had synchronized tab groups, so I can split the quickstart into broad categories (install, create space, upload, view, etc) and have the details in tabs for the various runtime envs (CLI, node, browser). That would feel super awkward with our current non-syncing tabs, but I think it could be pretty cool if we get the tabs in sync. Since I was having a bit of the old docs writer's block, I decided to just do it and ported over the feature from docusaurus, which is where I originally cribbed the tabs component in the first place. There are a few minor changes from their implementation, mostly just using `@next/router` instead of their thing and using jsdoc for types instead of proper typescript. There was one thing I copy/pasted that I didn't fully understand, namely the `useEvent` utility, which it seems is needed to make a dynamic callback usable as a dependency in another `useCallback`. It's used to refresh the tab state from local storage after the initial render. It seems to work, but I'm just calling it out since it was new to me. No tests yet, but here's a gif of it doing the thing: ![tab-groups](https://user-images.githubusercontent.com/678715/208783777-63ba932b-3ac3-4ddd-8fb3-19acef819502.gif)
- Loading branch information
1 parent
3ed273d
commit 3be2028
Showing
5 changed files
with
217 additions
and
3 deletions.
There are no files selected for viewing
105 changes: 105 additions & 0 deletions
105
packages/website/components/contexts/tabGroupChoiceContext.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import React, { createContext, useState, useCallback, useEffect, useMemo, useContext } from 'react'; | ||
|
||
const TAB_CHOICE_PREFIX = 'web3storage.tab.'; | ||
|
||
/** | ||
* @typedef {object} ContextValue | ||
* @property {boolean} ready - True if tab choices have been restored from storage | ||
* @property {Record<string, string>} tabGroupChoices - A map of `groupId` to the chosen tab `value` | ||
* @property {(groupId: string, value: string) => void} setTabGroupChoice - Set the tab `value` for a given `groupId` | ||
*/ | ||
|
||
/** @type {ContextValue} */ | ||
const initialValue = { | ||
ready: false, | ||
tabGroupChoices: {}, | ||
setTabGroupChoice: () => {}, | ||
}; | ||
|
||
const TabGroupContext = createContext(initialValue); | ||
|
||
/** | ||
* Stores the chosen `value` for a given tab `groupId` in local storage. Browser only. | ||
*/ | ||
function storeTabGroupChoice(groupId, value) { | ||
if (typeof window === 'undefined') { | ||
console.error('localStorage is not available during server rendering'); | ||
return; | ||
} | ||
const key = `${TAB_CHOICE_PREFIX}${groupId}`; | ||
window.localStorage.setItem(key, value); | ||
} | ||
|
||
/** | ||
* Loads all tab group choices from local storage. Browser only. | ||
* @returns {Record<string, string>} | ||
*/ | ||
function getAllTabGroupChoices() { | ||
if (typeof window === 'undefined') { | ||
console.error('localStorage is not available during server rendering'); | ||
return {}; | ||
} | ||
const storage = window.localStorage; | ||
|
||
/** @type {Record<string, string>} */ | ||
const choices = {}; | ||
for (let i = 0; i < storage.length; i++) { | ||
const key = storage.key(i); | ||
if (!key?.startsWith(TAB_CHOICE_PREFIX)) { | ||
continue; | ||
} | ||
const groupId = key.substring(TAB_CHOICE_PREFIX.length); | ||
choices[groupId] = storage.getItem(key) || ''; | ||
} | ||
|
||
return choices; | ||
} | ||
|
||
/** | ||
* @returns {ContextValue} | ||
*/ | ||
function useContextValue() { | ||
const [ready, setReady] = useState(false); | ||
const [tabGroupChoices, setChoices] = useState(/** @type {Record<string, string>} */ {}); | ||
const setChoiceSyncWithLocalStorage = useCallback((groupId, value) => { | ||
storeTabGroupChoice(groupId, value); | ||
}, []); | ||
|
||
useEffect(() => { | ||
try { | ||
const choices = getAllTabGroupChoices(); | ||
setChoices(choices); | ||
console.log('tab choices ready:', choices); | ||
} catch (err) { | ||
console.error('error loading tab choices from local storage:', err); | ||
} | ||
|
||
setReady(true); | ||
}, []); | ||
|
||
const setTabGroupChoice = useCallback( | ||
(groupId, value) => { | ||
setChoices(oldChoices => ({ ...oldChoices, [groupId]: value })); | ||
setChoiceSyncWithLocalStorage(groupId, value); | ||
}, | ||
[setChoiceSyncWithLocalStorage] | ||
); | ||
|
||
return useMemo(() => ({ ready, tabGroupChoices, setTabGroupChoice }), [ready, tabGroupChoices, setTabGroupChoice]); | ||
} | ||
|
||
export function TabGroupChoiceProvider({ children }) { | ||
const value = useContextValue(); | ||
return <TabGroupContext.Provider value={value}>{children}</TabGroupContext.Provider>; | ||
} | ||
|
||
/** | ||
* @returns {ContextValue} | ||
*/ | ||
export function useTabGroupChoices() { | ||
const context = useContext(TabGroupContext); | ||
if (context == null) { | ||
throw new Error(`Hook useTabGroupChoice was called outside the provider <TabGroupChoiceProvider />`); | ||
} | ||
return context; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters