Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(v2): support syncing tab choices #2366

Merged
merged 10 commits into from
Mar 15, 2020
59 changes: 33 additions & 26 deletions packages/docusaurus-theme-classic/src/theme/Layout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
26 changes: 23 additions & 3 deletions packages/docusaurus-theme-classic/src/theme/Tabs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import React, {useState, Children} from 'react';
import useTabGroupChoiceContext from '@theme/hooks/useTabGroupChoiceContext';

import classnames from 'classnames';

Expand All @@ -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) => {
Expand Down Expand Up @@ -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>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
109 changes: 109 additions & 0 deletions website/docs/markdown-features.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
:::

Expand Down