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

Dynamic navbar: navbar item activation strategies #4389

Open
Yorkemartin opened this issue Mar 11, 2021 · 27 comments
Open

Dynamic navbar: navbar item activation strategies #4389

Yorkemartin opened this issue Mar 11, 2021 · 27 comments
Labels
apprentice Issues that are good candidates to be handled by a Docusaurus apprentice / trainee domain: theme Related to the default theme components feature This is not a bug or issue with Docusausus, per se. It is a feature request for the future.
Milestone

Comments

@Yorkemartin
Copy link

🚀 Feature

An example:

I have three instances of docs, each segmented into three categories with their respective sidebars: "concepts" "guides" and "reference". These docs can be called "V1" "V2" and "V3", but are fixed docs once released so versioning isn't helpful in the canonical sense. When on "V2"-"Concepts" - clicking the Guides button on the navbar will take me to "V2"-"Guides", along with the appropriate sidebar.

Have you read the Contributing Guidelines on issues?

yes

Motivation

I'm trying to make the above work and have been unsuccessful after many days of wrangling

Pitch

It seems useful to have navbar routing that can accommodate multiple doc instances

@Yorkemartin Yorkemartin added feature This is not a bug or issue with Docusausus, per se. It is a feature request for the future. status: needs triage This issue has not been triaged by maintainers labels Mar 11, 2021
@slorber
Copy link
Collaborator

slorber commented Mar 12, 2021

Hi,

I'm not totally sure to understand your usecase, a better description would be appreciated, and also if you can put a deploy preview of your doc site with 3 docs instance online on Netlify/Vercel/whatever, that would help to figure out the UX you try to achieve.

This looks kind-of related to #3930

My idea was we could build a generic system where each navbar item has an "activation strategy".

This means the navbar items could be different on a per-route basis, for both mobile and desktop.

This "strategy config" must be:

  • serializable (provided in Docusaurus config, but must be usable on client-side code)
  • composable, so that user has flexibility to create advanced rules

Here's a badly designed example:

const navbarItems = [
  {
    to: "/guides/",
    label: "Guides",
    when: { routeMatch: "/guides/.*" }
  },
  {
    to: "/api/ios",
    label: "iOS API",
    when: { plugin: { name: "@docusaurus/plugin-content-docs", id: "ios" } }
  },
  {
    to: "/api/android",
    label: "Android API",
    when: {
      type: "and",
      items: [
        { plugin: { name: "@docusaurus/plugin-content-docs", id: "ios" } },
        { routeMatch: "/guides/.*" }
      ]
    }
  }
];

We could work on defining a few useful activation conditions and the API surface, and also enable composition with AND/OR logic.

Is this what you are looking for?

@Yorkemartin
Copy link
Author

This would address my issue perfectly, I would love it if this were implemented

@slorber slorber changed the title Nav bar that routes relative to the current doc plugin Id Nav bar that routes relative to the current doc plugin Id (dynamic navbar items?) Apr 5, 2021
@slorber slorber changed the title Nav bar that routes relative to the current doc plugin Id (dynamic navbar items?) Nav bar that routes relative to the current doc plugin Id (dynamic navbar items with activation strategy?) Apr 5, 2021
@dswiecki
Copy link

@slorber Hi, thank you for a great tool ;) do you have this feature on 2.0.0 roadmap ?

@slorber
Copy link
Collaborator

slorber commented Jun 25, 2021

@dswiecki this has been requested quite often so we'll build it, however this is not the simplest one to design, and we have more important features to work on in the short term, cf the beta blog post https://docusaurus.io/blog/2021/05/12/announcing-docusaurus-two-beta#whats-next

@slorber slorber changed the title Nav bar that routes relative to the current doc plugin Id (dynamic navbar items with activation strategy?) Dynamic navbar: navbar item activation strategies Jul 8, 2021
@Josh-Cena
Copy link
Collaborator

Hey, multiple people have asked me whether it's possible to only display certain navbar items when user is logged in (by sending HTTP requests).

I like the activeWhen API, and I think we can make the type be like:

activeWhen: {strategy: string, params: any}

And then have a custom @theme/useNavbarItemIsActive hook, meant to be swizzled if the user wants additional logic so that this is accessible on the client side.

@slorber
Copy link
Collaborator

slorber commented Aug 13, 2021

We are a static site generator and not a hybrid framework like Next.js.
This means that we can only check for user authentication status once in the browser.

Having navbar items that require user authentication means that there will be navbar layout shifts happening after React hydration, and I'm not sure it's a good pattern to encourage: the strategies should rather work on the server to avoid layout shifts. But if we allow custom user-provided strategies we can't really prevent the user to do that anyway.

This post section explains the problem with gifs: https://www.joshwcomeau.com/react/the-perils-of-rehydration/#schrodingers-user:


And then have a custom @theme/useNavbarItemIsActive hook, meant to be swizzled

My plan was more to have a map of strategies in @theme/NavbarStrategies/index.tsx, similar to what we have for NavbarItem. I think having an empty map of custom strategies could also be useful so that user can swizzle a smaller part: @theme/NavbarStrategies/CustomStrategies.tsx.

We should probably add something similar to allow providing custom Navbar Items. .
I'd like the infrastructure to look similar in both cases and make it easy to extend/maintain (small swizzle API surface).

I can support you if you want to work on this

@Josh-Cena
Copy link
Collaborator

Josh-Cena commented Aug 14, 2021

My plan was more to have a map of strategies in @theme/NavbarStrategies/index.tsx,

I can't think of many use cases besides loggedIn and routeMatch 🤦‍♂️ But having a CustomStrategies.tsx is similar to what I have in mind

Agree that there will be layout shifts, but that doesn't sound like what we can help anyways (I've seen this behavior on a lot of sites using REST API for login). We can allow the user to set a default render state to minimize the impact, maybe?

the strategies should rather work on the server to avoid layout shifts

Makes sense. Strategies that we offer should conform to this, but we probably can't limit the users too much either

In any case, I might set my hands on this if no-one will be working on this soon


I imagine the API surface to be like:

export default function useNavbarItemStatus({type, params}): 'active' | 'disabled' | 'hidden' {
  return NavbarStrategies[type](params);
}

And then called in NavbarItem:

export default function NavbarItem({type, activeWhen, ...props}) {
  const status = useNavbarItemStatus(activeWhen);
  switch (status) {
    case 'hidden': return null;
    case 'disabled': return <NavbarItemComponent disabled {...props} />;
    case 'active': return <NavbarItemComponent {...props} />;
  }
}

So that you can do something like:

const navbarItems = [
  {
    to: "/guides/",
    label: "Guides",
    activeWhen: { type: 'routeMatch', params: "/guides/.*" }
  },
  {
    to: "/concepts/",
    label: "Concepts",
    activeWhen: { type: 'routeMatch', params: "/concepts/.*" }
  },
  {
    to: "/secret/",
    label: "Secrets",
    activeWhen: { type: 'loggedIn' }
  },
];

We can even have the syntactic sugar

type Strategy = { type: string, params: any} | {[type: string]: any}

to make the API close to what you have

@Josh-Cena
Copy link
Collaborator

when: { plugin: {name: "@docusaurus/plugin-content-docs", id: "ios"} }

This doesn't look like an explainable / intuitive API to me... Is this an attempt to improve SSR?

@slorber
Copy link
Collaborator

slorber commented Oct 21, 2021

Just updated this to showcase that it can cover the need of #5756, it is not at all a final/definitive API

@Josh-Cena Josh-Cena removed the status: needs triage This issue has not been triaged by maintainers label Oct 30, 2021
@Josh-Cena Josh-Cena added this to the 2.x milestone Feb 18, 2022
@Josh-Cena Josh-Cena added the domain: theme Related to the default theme components label Apr 10, 2022
@slorber
Copy link
Collaborator

slorber commented Apr 28, 2022

React-Native is using a conditional version dropdown, that is only displayed on docs pages.
https://github.com/facebook/react-native-website/blob/main/website/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.js

The implementation can likely be greatly simplified, just using swizzle --wrap should allow you to conditionally render it.

Until we have proper support for this, I suggest the following workaround:

import React from "react";
import {useActiveDocContext} from '@docusaurus/plugin-content-docs/client';
import DocsVersionDropdownNavbarItem from '@theme-original/NavbarItem/DocsVersionDropdownNavbarItem';

export default function DocsVersionDropdownNavbarItemWrapper(props) {

  // do not display this navbar item if current page is not a doc
  const activeDocContext = useActiveDocContext(props.docsPluginId);
  if (!activeDocContext) {
    return null;
  }

  return <DocsVersionDropdownNavbarItem {...props} />;
}

@al1re2a
Copy link

al1re2a commented Nov 20, 2022

I don't know if it helps, but I added some CSS in custom.css to hide irrelevant version dropdown from navbar. The only problem is the dropdown elements in navbar have no CSS class included and I had to use the sequences of the dropdown in my CSS like this:

.navbar.navbar--fixed-top .navbar__items.navbar__items--right > .navbar__item:nth-child(3),
.navbar.navbar--fixed-top .navbar__items.navbar__items--right > .navbar__item:nth-child(4) {
  display: none;
}

html.docs-wrapper.plugin-id-default .navbar.navbar--fixed-top .navbar__items.navbar__items--right > .navbar__item:nth-child(3) {
  display: block;
}

html.docs-wrapper.plugin-id-docsFormio .navbar.navbar--fixed-top .navbar__items.navbar__items--right > .navbar__item:nth-child(4) {
  display: block;
}

If only we could add some custom CSS into the dropdown elements, the problem would be solved peacefully. As a suggestion, It can be done by implementing a parameter called customCss in Navbar docs version dropdown, like this:

            type: 'docsVersionDropdown',
            position: 'right',
            docsPluginId: 'default',
            customCss: 'menu-2'
          },
          {
            type: 'docsVersionDropdown',
            position: 'right',
            docsPluginId: 'docsFormio',
            customCss: 'menu-3'
          },

image

This way, we can simply do anything to the dropdown elements by using our custom CSS classes.

@Josh-Cena

@slorber
Copy link
Collaborator

slorber commented Nov 23, 2022

@al1re2a we support passing a className: "my-css-class", which is the default class prop for React components CSS.

Maybe it's not documented well enough?

However I think it's applied to the link instead of the parent dropdown container so we should probably apply this class to the parent instead, or give a way to pass a custom class to both elements independently.

Note with the new :has() (support is improving fast!) it may be less necessary:

I'm thinking of this:

html.docs-wrapper.plugin-id-default .navbar__item:has(> a.my-custom-class) {
  display: block;
}

@al1re2a
Copy link

al1re2a commented Nov 27, 2022

@slorber Thanks for the tips. It worked for me perfectly.

Maybe it's not documented well enough?

Unfortunately yes. Although the className is supported in almost all Navbar elements, it is not mentioned in docsVersionDropdown at all. I would appreciate if it is documented as well.

@Zenahr
Copy link
Contributor

Zenahr commented Jan 10, 2023

React-Native is using a conditional version dropdown, that is only displayed on docs pages. https://github.com/facebook/react-native-website/blob/main/website/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.js

The implementation can likely be greatly simplified, just using swizzle --wrap should allow you to conditionally render it.

Until we have proper support for this, I suggest the following workaround:

import React from "react";
import {useActiveDocContext} from '@docusaurus/plugin-content-docs/client';
import DocsVersionDropdownNavbarItem from '@theme-original/NavbarItem/DocsVersionDropdownNavbarItem';

export default function DocsVersionDropdownNavbarItemWrapper(props) {

  // do not display this navbar item if current page is not a doc
  const activeDocContext = useActiveDocContext(props.docsPluginId);
  if (!activeDocContext) {
    return null;
  }

  return <DocsVersionDropdownNavbarItem {...props} />;
}

@slorber

I tried this out but nothing changes really. What's the expected result of this exactly?

I'm not sure if this could work but I was hoping I could do something like:

get the current route -> show the navbar item if the route is part of the plugin

Example:

docusaurus.config.js

      [
        '@docusaurus/plugin-content-docs',
        {
          id: 'charge-controller',
          path: 'docs/ChargeController',
          routeBasePath: 'charge-controller',
          sidebarPath: require.resolve('./docs/ChargeController/sidebars.js'),
        },
      ],

navItems:

  {
    type: 'docsVersionDropdown',
    docsPluginId: 'charge-controller',
    position: 'right',
    className: "charge-controller-version-dropdown"
  },

current URI = "http://localhost:3000/charge-controller"
--> URI contains the docsPluginId for charge-controller docs
--> show the component

otherwise, return null.

Is this possible?
Or perhaps I'm understanding your code wrong. From what I can see, it doesn't seem to do what I'd expect (hide dropdowns except for the one that corresponds to the active plugin id)

@Zenahr
Copy link
Contributor

Zenahr commented Jan 10, 2023

Nevermind my previous comment. I got something working with rather simple code.
What this does: Context-sensitive hiding/showing of version dropdowns.
For anyone interested in this:

import React from "react";
import DocsVersionDropdownNavbarItem from '@theme-original/NavbarItem/DocsVersionDropdownNavbarItem';
import { useLocation }  from '@docusaurus/router';

export default function DocsVersionDropdownNavbarItemWrapper(props) {
  const { docsPluginId, className, type } = props
  const { pathname } = useLocation()
  
  /* (Custom) check if docsPluginId contains pathname
  Given that the docsPluginId is 'charge-controller' and the routeBasePath is 'charge-controller', we can check against the current URI (pathname).
  If the pathname contains the docsPluginId, we want to show the version dropdown. Otherwise, we don't want to show it.
  This gives us one, global, context-aware version dropdown that works with multi-instance setups.

  (Example for a possible configuration)
  docusaurus.config.js:
  ****************************************************************************************************
  [
    '@docusaurus/plugin-content-docs',
    {
      id: 'charge-controller',
      path: 'docs/ChargeController',
      routeBasePath: 'charge-controller',
      sidebarPath: require.resolve('./docs/ChargeController/sidebars.js'),
    },
  ],
  ****************************************************************************************************

  navbarItems.js (or as an attribute in docusaurus.config.js):
  ****************************************************************************************************
  {
    type: 'docsVersionDropdown',
    docsPluginId: 'charge-controller',
    position: 'right',
  },
  {
    type: 'docsVersionDropdown',
    docsPluginId: 'charge-point',
    position: 'right',
  },
  ****************************************************************************************************
  */
  const doesPathnameContainDocsPluginId = pathname.includes(docsPluginId)
  if (!doesPathnameContainDocsPluginId) {
    return null
  }
  return <DocsVersionDropdownNavbarItem {...props} />;
}

@slorber
Copy link
Collaborator

slorber commented Jan 18, 2023

The solution I suggested is quite similar but more robust. The docs plugin id is not always contained in the URL.

 const activeDocContext = useActiveDocContext(props.docsPluginId);

This returns something if the current docs plugin is active.

You can also try using this undocumented hook (more low-level but core, less likely to be refactored)

import useRouteContext from '@docusaurus/useRouteContext';

const {plugin: {id, name}} = useRouteContext();

@jbltx
Copy link

jbltx commented Jan 20, 2023

I don't know for you but in 2.2.0 useActiveDocContext(props.docsPluginId) always returns an Object, I had to check if the property activeDoc is actually defined inside this object :

import React from "react";
import {useActiveDocContext} from '@docusaurus/plugin-content-docs/client';
import DocsVersionDropdownNavbarItem from '@theme-original/NavbarItem/DocsVersionDropdownNavbarItem';

export default function DocsVersionDropdownNavbarItemWrapper(props) {

  // do not display this navbar item if current page is not a doc
  const { activeDoc } = useActiveDocContext(props.docsPluginId);
  if (!activeDoc) {
    return null;
  }

  return <DocsVersionDropdownNavbarItem {...props} />;
}

@slorber
Copy link
Collaborator

slorber commented Jan 20, 2023

Ah yes, I don't always run the pseudo-code I suggest to use so take this with a grain of salt and adapt it a bit if needed 😄

@aleksimo
Copy link

I don't know for you but in 2.2.0 useActiveDocContext(props.docsPluginId) always returns an Object, I had to check if the property activeDoc is actually defined inside this object :

import React from "react";
import {useActiveDocContext} from '@docusaurus/plugin-content-docs/client';
import DocsVersionDropdownNavbarItem from '@theme-original/NavbarItem/DocsVersionDropdownNavbarItem';

export default function DocsVersionDropdownNavbarItemWrapper(props) {

  // do not display this navbar item if current page is not a doc
  const { activeDoc } = useActiveDocContext(props.docsPluginId);
  if (!activeDoc) {
    return null;
  }

  return <DocsVersionDropdownNavbarItem {...props} />;
}

Can this be applied not to the /docs folder, but on the contrary, to the versioned multi-instance docs that reside next to /docs?

@slorber
Copy link
Collaborator

slorber commented Apr 7, 2023

Can this be applied not to the /docs folder, but on the contrary, to the versioned multi-instance docs that reside next to /docs?

I don't know what you mean here, a repro would help.

You can pass a pluginId as hook arg so if you have a 2nd docs plugin instance you can use its id here: useActiveDocContext("my-android-sdk-plugin-id");

@aleksimo
Copy link

aleksimo commented Apr 7, 2023

So I have several instances of docs, each with its own sidebar and versioning. The instances are located in separate folders in the root:

  • /docs (not versioned)
  • /instance-1 (3 versions)
  • /instance-2 (2 versions)
  • etc.

To make the versions work, I added the version dropdowns to each versioned item in the navbar, thus making several version dropdowns on the UI.

With the code above, I wanted to make a single navbar dropdown that will appear only for versioned instances and contain versions depending on the instance you are currently reading.

I was able to achieve that with the following found code:

import React from "react";
import DocsVersionDropdownNavbarItem from '@theme-original/NavbarItem/DocsVersionDropdownNavbarItem';
import { useLocation }  from '@docusaurus/router';

export default function DocsVersionDropdownNavbarItemWrapper(props) {
  const { docsPluginId, className, type } = props
  const { pathname } = useLocation()
 
  const doesPathnameContainDocsPluginId = pathname.includes(docsPluginId)
  if (!doesPathnameContainDocsPluginId) {
    return null
  }
  return <DocsVersionDropdownNavbarItem {...props} />;
}

The only thing, in docusaurus.config.js, I had to create 3 separate dropdown items, but this doesn't affect the result.

items: [
          {
            label: 'Instance-0',
            to:'docs/...',
          },
          { 
            to: 'path-to-instance-1-id',
            label: 'Instance-1',
          },
          {
            to: 'path-to-instance-2-id',
            label: 'Instance-2',
          },
          {
            type: 'docsVersionDropdown',
            docsPluginId: 'instance-1',
            position: 'right',
          },
          {
            type: 'docsVersionDropdown',
            docsPluginId: 'instance-2',
            position: 'right',
          },
]

@fharper
Copy link

fharper commented Jul 13, 2023

For me the use case is being able to render some navbar items only when the user selected a specific docs version.

@luchtech
Copy link

luchtech commented Dec 30, 2023

Is this still not finished yet? An active docs-aware version dropdown is all we need.

image

Docs Multi-Instance can result into multiple docs with their own versions. I don't know how the Docs Multi-instance was released without taking into consideration the versions per instance.

@homotechsual
Copy link
Contributor

Docs Multi-Instance can result into multiple docs with their own versions. I don't know how the Docs Multi-instance was released without taking into consideration the versions per instance.

Because there are workarounds possible now to achieve this and this is an open-source project with limited volunteer maintainer time to implement things. It's generally considered that if there's a way to achieve something, even if that way is swizzling or custom components that it's not a high priority and that providing a usable base set of features for 75+% of use cases/sites is the goal not making APIs for every single advanced use case someone wants.

@pavinduLakshan
Copy link

For my site, I wanted to avoid displaying the version dropdown when the active route is a blog post. Here is how I achieved it.

I created src/theme/NavbarItem/DocsVersionDropdownNavbarItem.js and added the following code, and that alone solved the problem for me.

import React from 'react';
import DocsVersionDropdownNavbarItem from '@theme-original/NavbarItem/DocsVersionDropdownNavbarItem';
import {useLocation} from '@docusaurus/router';

export default function DocsVersionDropdownNavbarItemWrapper(props) {
  const location = useLocation();

  const unversionedRoutes = [
    // any route that starts with `/blog`
    /^\/blog(?:\/[\w-]+)?(?:\/#\w+)?$/g
  ]

  function checkPathname(pathname) {
    // Check if the provided pathname matches any of the regexes in the list
    return unversionedRoutes.some(regex => regex.test(pathname))
  }

  if (checkPathname(location.pathname)) {
    return null;
  }

  return <DocsVersionDropdownNavbarItem {...props} />;
}

Hope this would help someone else!

@slorber
Copy link
Collaborator

slorber commented Feb 25, 2024

@pavinduLakshan using useRouteContext is likely a better, more robust solution.

I didn't try but this should work:

import React from 'react';
import useRouteContext from '@docusaurus/useRouteContext';
import DocsVersionDropdownNavbarItem from '@theme-original/NavbarItem/DocsVersionDropdownNavbarItem';

export default function DocsVersionDropdownNavbarItemWrapper(props) {
  const {plugin} = useRouteContext();

  if (plugin.name === "docusaurus-plugin-content-blog") {
    return null;
  }

  return <DocsVersionDropdownNavbarItem {...props} />;
}

@AnthonyTsu1984
Copy link

React-Native is using a conditional version dropdown, that is only displayed on docs pages. https://github.com/facebook/react-native-website/blob/main/website/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.js
The implementation can likely be greatly simplified, just using swizzle --wrap should allow you to conditionally render it.
Until we have proper support for this, I suggest the following workaround:

import React from "react";
import {useActiveDocContext} from '@docusaurus/plugin-content-docs/client';
import DocsVersionDropdownNavbarItem from '@theme-original/NavbarItem/DocsVersionDropdownNavbarItem';

export default function DocsVersionDropdownNavbarItemWrapper(props) {

  // do not display this navbar item if current page is not a doc
  const activeDocContext = useActiveDocContext(props.docsPluginId);
  if (!activeDocContext) {
    return null;
  }

  return <DocsVersionDropdownNavbarItem {...props} />;
}

@slorber

I tried this out but nothing changes really. What's the expected result of this exactly?

I'm not sure if this could work but I was hoping I could do something like:

get the current route -> show the navbar item if the route is part of the plugin

Example:

docusaurus.config.js

      [
        '@docusaurus/plugin-content-docs',
        {
          id: 'charge-controller',
          path: 'docs/ChargeController',
          routeBasePath: 'charge-controller',
          sidebarPath: require.resolve('./docs/ChargeController/sidebars.js'),
        },
      ],

navItems:

  {
    type: 'docsVersionDropdown',
    docsPluginId: 'charge-controller',
    position: 'right',
    className: "charge-controller-version-dropdown"
  },

current URI = "http://localhost:3000/charge-controller" --> URI contains the docsPluginId for charge-controller docs --> show the component

otherwise, return null.

Is this possible? Or perhaps I'm understanding your code wrong. From what I can see, it doesn't seem to do what I'd expect (hide dropdowns except for the one that corresponds to the active plugin id)

This is a brilliant solution except changing

  if (!activeDocContext) {
    return null;
  }

to

  if (!activeDocContext.activeDoc) {
    return null;
  }

Because useActiveDocContext() returns an object similar to the following:

image

If the versioned doc is not the current doc, activeDoc and activeVersion will be undefined. Making use of this, you can control the display of the docsVersionDropdown item.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
apprentice Issues that are good candidates to be handled by a Docusaurus apprentice / trainee domain: theme Related to the default theme components feature This is not a bug or issue with Docusausus, per se. It is a feature request for the future.
Projects
None yet
Development

Successfully merging a pull request may close this issue.