-
-
Notifications
You must be signed in to change notification settings - Fork 8.5k
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
feat(v2): support syncing tab choices #2366
Conversation
Deploy preview for docusaurus-2 ready! Built with commit c0c7a32 |
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. | ||
|
||
### Syncing Tab Choices |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Document the new feature in website/docs
. Please revert this change in versioned doc.
BTW do not use capitalization in heading.
const [selectedValue, setSelectedValue] = useState(defaultValue); | ||
|
||
if (typeof groupId !== 'undefined') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to me that we can remove the groupId
prop (this is somewhat redundant) in favor of value
prop in TabItem
component to identify each tab? Yes, this way we synchronize tabs by default and do not give users prevent this behavior. (Although I do not think that is a big problem).
const setChoiceSyncWithLocalStorage = useCallback( | ||
newChoices => { | ||
try { | ||
localStorage.setItem('tab-group', JSON.stringify(newChoices)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First, put storage value in separate const outside the hook-level. Second, add a namespace to that, for example docusaurus.tab-group
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
put storage value in separate const outside the hook-level.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lex111 Do you mean moving the literal'docusaurus.tab-choice'
outside of the hooks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like this (come up with a better name for const):
const STORAGE_KEY = ...
const setChoiceSyncWithLocalStorage = ...
|
||
import React from 'react'; | ||
|
||
const TabGroupChoiceContext = React.createContext({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use this approach?
import React, { createContext } from 'react';
const TabGroupChoiceContext = createContext(...)
Updated the PR is store only one choice |
const [selectedValue, setSelectedValue] = useState(defaultValue); | ||
|
||
console.log({values, tabGroupChoice}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logging should be removed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops. I will remove it.
const setChoiceSyncWithLocalStorage = useCallback( | ||
newChoice => { | ||
try { | ||
localStorage.setItem(TAB_CHOICE_STORAGE_KEY, newChoice); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tab-choice
is must be an array in order to maintain sync for several different tabs.
See https://deploy-preview-2366--docusaurus-2.netlify.com/docs/markdown-features/ for example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a plain array can work if all the tab choices happen to be disjoint. However, if we have something like ['win', 'mac']
and ['win', 'unix']
and we see ['win']
in the array, there is an ambiguity about which win
it is referring to.
Should we make it an object keyed by serialized values
array?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if this is necessary, so regular array should work. We assume that users will use the same tab names for similar tabs (Yarn VS npm eg) throughout the website.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is so awesome! I really like that you made it generic! Thank you @SamChou19815!
What do you think about adding a new prop to <Tabs />
called groupKey
? The presence of this prop means that that all the Tabs
instance of this groupKey
wants to be grouped and updated together. With such a groupKey
, a few problems with the current approach is solved:
- The current choices data structure doesn't know which Tabs it originally belongs to and there might be collisions with new Tabs of different purpose but have the same value (chances are slim but you never know).
- Allows saving each individual choice in its own localStorage key instead of stringifying the entire choices object (e.g.
docusaurus.tabs.operating_system
,docusaurus.tabs.package_manager
). Also eliminates the possibility of a corrupted value for one groupKey affecting the other groupKeys.
Lemme know your thoughts!
EDIT: Just read the comments above and realized my suggested approach was exactly what you did. @lex111 I believe using a groupId
/groupKey
is better for engineering stability and maintainability even if it requires more work from the user. Docusaurus tries to be explicit and avoid magic behind the scenes. Apologies for the conflicting opinions!
useEffect(() => { | ||
try { | ||
const localStorageChoice = localStorage.getItem(TAB_CHOICE_STORAGE_KEY); | ||
if (localStorageChoice !== null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can do != null
.
|
||
import {useState, useCallback, useEffect} from 'react'; | ||
|
||
const TAB_CHOICE_STORAGE_KEY = 'docusaurus.tab-choice'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
docusaurus.tabs
would be sufficient.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks so so so much @SamChou19815! I made some minor edits to the docs to add explanations and make it a little clearer. Awesome work! 🎉
Motivation
Implement feature requested in #2349, but in a more general way.
The linked issue wants auto-syncing tabs for programming languages. However, I think it can be implemented in a more general way. For example, we might also use the tabs to separate OS-specific instructions. Therefore, instead of providing hooks like
useProgrammingLanguages
, I decide to provide a more general hook calleduseTabGroupChoiceContext
.No matter whether we decide to limit the feature to programming languages or general tabs, we need to worry about conflicts. For example, inside the same project, the docs might have both tabs for JavaScript vs TypeScript, and tabs for NPM vs Yarn. We don't want these two different tabs to interfere with each other in surprising ways.
To prevent unwanted interference, I decide to separate these choices by keys. Users must explicit provide the same groupId to convey to docusaurus that those tabs are related and their choices should be synced. To preserve backward compatibility, groupId is an optional prop of Tab. Not passing the prop means that the user opts out of tab choice syncing.
The choice is stored persistently in
localStorage
, just like the choice of theme.Have you read the Contributing Guidelines on pull requests?
Yes
Test Plan
(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work. Bonus points for screenshots and videos!)
I added more examples of using tabs with
groupId
inwebsite/docs/markdown-features.mdx
to test the new feature.(Some examples are silly, and I'm open to better examples.)
Related PRs
(If this PR adds or changes functionality, please take some time to update the docs at https://github.com/facebook/docusaurus, and link to your PR here.)