Skip to content

Commit

Permalink
feat(v2): support syncing tab choices (#2366)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
SamChou19815 and yangshun authored Mar 15, 2020
1 parent da0a61d commit 55a50d3
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 30 deletions.
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

0 comments on commit 55a50d3

Please sign in to comment.