mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-09 14:22:27 +02:00
feat(v2): support syncing tab choices (#2366)
* feat(v2): Support syncing tab choices * Move docs changes to website/docs * Do not import entire React in TabGroupChoiceContext * Store only one tab choice according to discussion in PR * Remove leftover logging code during debugging * Put storage value in separate const outside the hook-level * Use an array to keep track of different tab groups * Revert back to using `groupId` * Update markdown-features.mdx * Update markdown-features.mdx Co-authored-by: Yangshun Tay <tay.yang.shun@gmail.com>
This commit is contained in:
parent
da0a61d1f0
commit
55a50d37dc
8 changed files with 268 additions and 30 deletions
|
@ -11,6 +11,7 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
import ThemeProvider from '@theme/ThemeProvider';
|
||||
import TabGroupChoiceProvider from '@theme/TabGroupChoiceProvider';
|
||||
import Navbar from '@theme/Navbar';
|
||||
import Footer from '@theme/Footer';
|
||||
|
||||
|
@ -41,33 +42,39 @@ function Layout(props) {
|
|||
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<Head>
|
||||
{/* TODO: Do not assume that it is in english language */}
|
||||
<html lang="en" />
|
||||
<TabGroupChoiceProvider>
|
||||
<Head>
|
||||
{/* TODO: Do not assume that it is in english language */}
|
||||
<html lang="en" />
|
||||
|
||||
<meta httpEquiv="x-ua-compatible" content="ie=edge" />
|
||||
{metaTitle && <title>{metaTitle}</title>}
|
||||
{metaTitle && <meta property="og:title" content={metaTitle} />}
|
||||
{favicon && <link rel="shortcut icon" href={faviconUrl} />}
|
||||
{description && <meta name="description" content={description} />}
|
||||
{description && (
|
||||
<meta property="og:description" content={description} />
|
||||
)}
|
||||
{version && <meta name="docsearch:version" content={version} />}
|
||||
{keywords && keywords.length && (
|
||||
<meta name="keywords" content={keywords.join(',')} />
|
||||
)}
|
||||
{metaImage && <meta property="og:image" content={metaImageUrl} />}
|
||||
{metaImage && <meta property="twitter:image" content={metaImageUrl} />}
|
||||
{metaImage && (
|
||||
<meta name="twitter:image:alt" content={`Image for ${metaTitle}`} />
|
||||
)}
|
||||
{permalink && <meta property="og:url" content={siteUrl + permalink} />}
|
||||
<meta name="twitter:card" content="summary" />
|
||||
</Head>
|
||||
<Navbar />
|
||||
<div className="main-wrapper">{children}</div>
|
||||
{!noFooter && <Footer />}
|
||||
<meta httpEquiv="x-ua-compatible" content="ie=edge" />
|
||||
{metaTitle && <title>{metaTitle}</title>}
|
||||
{metaTitle && <meta property="og:title" content={metaTitle} />}
|
||||
{favicon && <link rel="shortcut icon" href={faviconUrl} />}
|
||||
{description && <meta name="description" content={description} />}
|
||||
{description && (
|
||||
<meta property="og:description" content={description} />
|
||||
)}
|
||||
{version && <meta name="docsearch:version" content={version} />}
|
||||
{keywords && keywords.length && (
|
||||
<meta name="keywords" content={keywords.join(',')} />
|
||||
)}
|
||||
{metaImage && <meta property="og:image" content={metaImageUrl} />}
|
||||
{metaImage && (
|
||||
<meta property="twitter:image" content={metaImageUrl} />
|
||||
)}
|
||||
{metaImage && (
|
||||
<meta name="twitter:image:alt" content={`Image for ${metaTitle}`} />
|
||||
)}
|
||||
{permalink && (
|
||||
<meta property="og:url" content={siteUrl + permalink} />
|
||||
)}
|
||||
<meta name="twitter:card" content="summary" />
|
||||
</Head>
|
||||
<Navbar />
|
||||
<div className="main-wrapper">{children}</div>
|
||||
{!noFooter && <Footer />}
|
||||
</TabGroupChoiceProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {createContext} from 'react';
|
||||
|
||||
const TabGroupChoiceContext = createContext({
|
||||
tabGroupChoices: {},
|
||||
setTabGroupChoices: () => {},
|
||||
});
|
||||
|
||||
export default TabGroupChoiceContext;
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import useTabGroupChoice from '@theme/hooks/useTabGroupChoice';
|
||||
import TabGroupChoiceContext from '@theme/TabGroupChoiceContext';
|
||||
|
||||
function TabGroupChoiceProvider(props) {
|
||||
const {tabGroupChoices, setTabGroupChoices} = useTabGroupChoice();
|
||||
|
||||
return (
|
||||
<TabGroupChoiceContext.Provider
|
||||
value={{tabGroupChoices, setTabGroupChoices}}>
|
||||
{props.children}
|
||||
</TabGroupChoiceContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default TabGroupChoiceProvider;
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, {useState, Children} from 'react';
|
||||
import useTabGroupChoiceContext from '@theme/hooks/useTabGroupChoiceContext';
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
|
@ -17,8 +18,27 @@ const keys = {
|
|||
};
|
||||
|
||||
function Tabs(props) {
|
||||
const {block, children, defaultValue, values} = props;
|
||||
const {block, children, defaultValue, values, groupId} = props;
|
||||
const {tabGroupChoices, setTabGroupChoices} = useTabGroupChoiceContext();
|
||||
const [selectedValue, setSelectedValue] = useState(defaultValue);
|
||||
|
||||
if (groupId != null) {
|
||||
const relevantTabGroupChoice = tabGroupChoices[groupId];
|
||||
if (
|
||||
relevantTabGroupChoice != null &&
|
||||
relevantTabGroupChoice !== selectedValue
|
||||
) {
|
||||
setSelectedValue(relevantTabGroupChoice);
|
||||
}
|
||||
}
|
||||
|
||||
const changeSelectedValue = newValue => {
|
||||
setSelectedValue(newValue);
|
||||
if (groupId != null) {
|
||||
setTabGroupChoices(groupId, newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const tabRefs = [];
|
||||
|
||||
const focusNextTab = (tabs, target) => {
|
||||
|
@ -73,8 +93,8 @@ function Tabs(props) {
|
|||
key={value}
|
||||
ref={tabControl => tabRefs.push(tabControl)}
|
||||
onKeyDown={event => handleKeydown(tabRefs, event.target, event)}
|
||||
onFocus={() => setSelectedValue(value)}
|
||||
onClick={() => setSelectedValue(value)}>
|
||||
onFocus={() => changeSelectedValue(value)}
|
||||
onClick={() => changeSelectedValue(value)}>
|
||||
{label}
|
||||
</li>
|
||||
))}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {useState, useCallback, useEffect} from 'react';
|
||||
|
||||
const TAB_CHOICE_PREFIX = 'docusaurus.tab.';
|
||||
|
||||
const useTabGroupChoice = () => {
|
||||
const [tabGroupChoices, setChoices] = useState({});
|
||||
const setChoiceSyncWithLocalStorage = useCallback((groupId, newChoice) => {
|
||||
try {
|
||||
localStorage.setItem(`${TAB_CHOICE_PREFIX}${groupId}`, newChoice);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const localStorageChoices = {};
|
||||
for (let i = 0; i < localStorage.length; i += 1) {
|
||||
const storageKey = localStorage.key(i);
|
||||
if (storageKey.startsWith(TAB_CHOICE_PREFIX)) {
|
||||
const groupId = storageKey.substring(TAB_CHOICE_PREFIX.length);
|
||||
localStorageChoices[groupId] = localStorage.getItem(storageKey);
|
||||
}
|
||||
}
|
||||
setChoices(localStorageChoices);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
tabGroupChoices,
|
||||
setTabGroupChoices: (groupId, newChoice) => {
|
||||
setChoices(oldChoices => ({...oldChoices, [groupId]: newChoice}));
|
||||
setChoiceSyncWithLocalStorage(groupId, newChoice);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default useTabGroupChoice;
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {useContext} from 'react';
|
||||
|
||||
import TabGroupChoiceContext from '@theme/TabGroupChoiceContext';
|
||||
|
||||
function useTabGroupChoiceContext() {
|
||||
return useContext(TabGroupChoiceContext);
|
||||
}
|
||||
|
||||
export default useTabGroupChoiceContext;
|
|
@ -477,6 +477,115 @@ class HelloWorld {
|
|||
|
||||
You may want to implement your own `<MultiLanguageCode />` abstraction if you find the above approach too verbose. We might just implement one in future for convenience.
|
||||
|
||||
If you have multiple of these multi-language code tabs, and you want to sync the selection across the tab instances, read on for the [Syncing tab choices section](#syncing-tab-choices).
|
||||
|
||||
### Syncing tab choices
|
||||
|
||||
You may want choices of the same kind of tabs to sync with each other. For example, you might want to provide different instructions for users on Windows vs users on macOS, and you want to changing all OS-specific instructions tabs in one click. To achieve that, you can give all related tabs the same `groupId`. Note that doing this will persist the choice in `localStorage` and all `<Tab>` instances with the same `groupId` will update automatically when the value of one of them is changed.
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
<Tabs
|
||||
groupId="operating-systems"
|
||||
defaultValue="win"
|
||||
values={[
|
||||
{ label: 'Windows', value: 'win', },
|
||||
{ label: 'macOS', value: 'mac', },
|
||||
]
|
||||
}>
|
||||
<TabItem value="win">Use Ctrl + C to copy.</TabItem>
|
||||
<TabItem value="mac">Use Command + C to copy.</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<Tabs
|
||||
groupId="operating-systems"
|
||||
defaultValue="win"
|
||||
values={[
|
||||
{ label: 'Windows', value: 'win', },
|
||||
{ label: 'macOS', value: 'mac', },
|
||||
]
|
||||
}>
|
||||
<TabItem value="win">Use Ctrl + V to paste.</TabItem>
|
||||
<TabItem value="mac">Use Command + V to paste.</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
<Tabs
|
||||
groupId="operating-systems"
|
||||
defaultValue="win"
|
||||
values={[
|
||||
{ label: 'Windows', value: 'win', },
|
||||
{ label: 'macOS', value: 'mac', },
|
||||
]
|
||||
}>
|
||||
<TabItem value="win">Use Ctrl + C to copy.</TabItem>
|
||||
<TabItem value="mac">Use Command + C to copy.</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<Tabs
|
||||
groupId="operating-systems"
|
||||
defaultValue="win"
|
||||
values={[
|
||||
{ label: 'Windows', value: 'win', },
|
||||
{ label: 'macOS', value: 'mac', },
|
||||
]
|
||||
}>
|
||||
<TabItem value="win">Use Ctrl + V to paste.</TabItem>
|
||||
<TabItem value="mac">Use Command + V to paste.</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Tab choices with different `groupId`s will not interfere with each other:
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
<Tabs
|
||||
groupId="non-mac-operating-systems"
|
||||
defaultValue="win"
|
||||
values={[
|
||||
{ label: 'Windows', value: 'win', },
|
||||
{ label: 'Unix', value: 'unix', },
|
||||
]
|
||||
}>
|
||||
<TabItem value="win">Windows is windows.</TabItem>
|
||||
<TabItem value="unix">Unix is unix.</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<Tabs
|
||||
groupId="non-mac-operating-systems"
|
||||
defaultValue="win"
|
||||
values={[
|
||||
{ label: 'Windows', value: 'win', },
|
||||
{ label: 'Unix', value: 'unix', },
|
||||
]
|
||||
}>
|
||||
<TabItem value="win">Windows is not unix.</TabItem>
|
||||
<TabItem value="unix">Unix is not windows.</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<Tabs
|
||||
groupId="non-mac-operating-systems"
|
||||
defaultValue="win"
|
||||
values={[
|
||||
{ label: 'Windows', value: 'win', },
|
||||
{ label: 'Unix', value: 'unix', },
|
||||
]
|
||||
}>
|
||||
<TabItem value="win">Windows is windows.</TabItem>
|
||||
<TabItem value="unix">Unix is unix.</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<Tabs
|
||||
groupId="non-mac-operating-systems"
|
||||
defaultValue="win"
|
||||
values={[
|
||||
{ label: 'Windows', value: 'win', },
|
||||
{ label: 'Unix', value: 'unix', },
|
||||
]
|
||||
}>
|
||||
<TabItem value="win">Windows is not unix.</TabItem>
|
||||
<TabItem value="unix">Unix is not windows.</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Callouts/admonitions
|
||||
|
||||
In addition to the basic Markdown syntax, we use [remark-admonitions](https://github.com/elviswolcott/remark-admonitions) alongside MDX to add support for admonitions.
|
||||
|
|
|
@ -16,7 +16,7 @@ Docusaurus 2 uses modern tooling to help you compose your interactive documentat
|
|||
|
||||
In this section, we'd like to introduce you to the tools we've picked that we believe will help you build powerful documentation. Let us walk you through with an example.
|
||||
|
||||
:::important
|
||||
:::important
|
||||
All the following content assumes you are using `@docusaurus/preset-classic`.
|
||||
:::
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue