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(theme-classic): store selected tab in query string. #8225

Merged
merged 20 commits into from
Dec 9, 2022
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 54 additions & 17 deletions packages/docusaurus-theme-classic/src/theme/Tabs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ function isTabItem(
return 'value' in comp.props;
}

const NON_GROUP_TAB_KEY = '__noGroup__';
function getValueFromSearchParams(groupId = NON_GROUP_TAB_KEY): string | null {
if (typeof window === 'undefined') {
return null; // Ignore during SSR.
}
const searchParams = new URLSearchParams(window.location.search);
mturoci marked this conversation as resolved.
Show resolved Hide resolved
const prevSearchParams = searchParams.get('tabs');
return prevSearchParams ? JSON.parse(prevSearchParams)[groupId] : null;
mturoci marked this conversation as resolved.
Show resolved Hide resolved
}

function TabsComponent(props: Props): JSX.Element {
const {
lazy,
Expand Down Expand Up @@ -69,16 +79,15 @@ function TabsComponent(props: Props): JSX.Element {
.join(', ')}" found in <Tabs>. Every value needs to be unique.`,
);
}
// When defaultValueProp is null, don't show a default tab
const defaultValue =
defaultValueProp === null
? defaultValueProp
: defaultValueProp ??
children.find((child) => child.props.default)?.props.value ??
children[0]!.props.value;
if (defaultValue !== null && !values.some((a) => a.value === defaultValue)) {

// Warn user about passing incorrect defaultValue as prop.
if (
defaultValueProp !== null &&
defaultValueProp !== undefined &&
!values.some((a) => a.value === defaultValueProp)
) {
Comment on lines -72 to +133
Copy link
Collaborator

Choose a reason for hiding this comment

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

can you explain the intent here? Not sure to understand

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Very similar to the prev check. However, this new one only throws in case of user-error. E.g. available tab keys are ke1, key2, key3, but user supplies key4. The goal is to notify user it's their fault that the tabs are not working properly.

The prev implementation would throw even if the error was on the Docusaurus (implementation) side as it checked the derived value instead of user supplied one.

throw new Error(
`Docusaurus error: The <Tabs> has a defaultValue "${defaultValue}" but none of its children has the corresponding value. Available values are: ${values
`Docusaurus error: The <Tabs> has a defaultValue "${defaultValueProp}" but none of its children has the corresponding value. Available values are: ${values
.map((a) => a.value)
.join(
', ',
Expand All @@ -87,21 +96,37 @@ function TabsComponent(props: Props): JSX.Element {
}

const {tabGroupChoices, setTabGroupChoices} = useTabGroupChoice();
const [selectedValue, setSelectedValue] = useState(defaultValue);
const tabRefs: (HTMLLIElement | null)[] = [];
const {blockElementScrollPositionUntilNextRender} =
useScrollPositionBlocker();

if (groupId != null) {
// search params >
// local storage >
// specified defaultValue >
// first child with "default" attr >
// first tab item.
let defaultValue: string | null | undefined =
getValueFromSearchParams(groupId);
if (!defaultValue && groupId != null) {
const relevantTabGroupChoice = tabGroupChoices[groupId];
if (
relevantTabGroupChoice != null &&
relevantTabGroupChoice !== selectedValue &&
relevantTabGroupChoice !== defaultValue &&
values.some((value) => value.value === relevantTabGroupChoice)
) {
setSelectedValue(relevantTabGroupChoice);
defaultValue = relevantTabGroupChoice;
}
}
// If we didn't find the right value in search params or local storage,
// fallback to props > child with "default" specified > first tab.
if (!defaultValue || !values.some((a) => a.value === defaultValue)) {
defaultValue =
defaultValueProp !== undefined
? defaultValueProp
: children.find((child) => child.props.default)?.props.value ??
children[0]!.props.value;
}

const [selectedValue, setSelectedValue] = useState(defaultValue);
const tabRefs: (HTMLLIElement | null)[] = [];
const {blockElementScrollPositionUntilNextRender} =
useScrollPositionBlocker();

const handleTabChange = (
event:
Expand All @@ -117,6 +142,18 @@ function TabsComponent(props: Props): JSX.Element {
blockElementScrollPositionUntilNextRender(newTab);
setSelectedValue(newTabValue);

const searchParams = new URLSearchParams(window.location.search);
const prevSearchParams = searchParams.get('tabs');
const prevVal = prevSearchParams ? JSON.parse(prevSearchParams) : null;
const newVal = {[groupId || NON_GROUP_TAB_KEY]: newTabValue};
const url = new URL(window.location.origin + window.location.pathname);
searchParams.set(
'tabs',
JSON.stringify(prevVal ? {...prevVal, ...newVal} : newVal),
);
url.search = searchParams.toString();
mturoci marked this conversation as resolved.
Show resolved Hide resolved
window.history.replaceState({}, '', url);
mturoci marked this conversation as resolved.
Show resolved Hide resolved

if (groupId != null) {
setTabGroupChoices(groupId, String(newTabValue));
}
Expand Down