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
Merged

feat(v2): support syncing tab choices #2366

merged 10 commits into from
Mar 15, 2020

Conversation

SamChou19815
Copy link
Contributor

@SamChou19815 SamChou19815 commented Mar 6, 2020

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 called useTabGroupChoiceContext.

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 in website/docs/markdown-features.mdx to test the new feature.

example
example 2

(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.)

@facebook-github-bot facebook-github-bot added the CLA Signed Signed Facebook CLA label Mar 6, 2020
@docusaurus-bot
Copy link
Contributor

docusaurus-bot commented Mar 6, 2020

Deploy preview for docusaurus-2 ready!

Built with commit c0c7a32

https://deploy-preview-2366--docusaurus-2.netlify.com

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
Copy link
Contributor

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') {
Copy link
Contributor

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));
Copy link
Contributor

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.

Copy link
Contributor

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.

🆙 @SamChou19815

Copy link
Contributor Author

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?

Copy link
Contributor

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({
Copy link
Contributor

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(...)

@lex111 lex111 added the pr: new feature This PR adds a new API or behavior. label Mar 6, 2020
@SamChou19815
Copy link
Contributor Author

Updated the PR is store only one choice

const [selectedValue, setSelectedValue] = useState(defaultValue);

console.log({values, tabGroupChoice});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging should be removed

Copy link
Contributor Author

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);
Copy link
Contributor

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.

image

image

Copy link
Contributor Author

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?

Copy link
Contributor

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.

@SamChou19815 SamChou19815 requested a review from lex111 March 8, 2020 23:37
Copy link
Contributor

@yangshun yangshun left a 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) {
Copy link
Contributor

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';
Copy link
Contributor

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.

@yangshun yangshun changed the title feat(v2): Support syncing tab choices feat(v2): support syncing tab choices Mar 15, 2020
Copy link
Contributor

@yangshun yangshun left a 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! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed Signed Facebook CLA pr: new feature This PR adds a new API or behavior.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants