Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions sidebarsTemplates.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ export default {
id: "frames",
label: "Frames",
},
{
type: "doc",
id: "tabs",
label: "Tabs",
},
{
type: "doc",
id: "gallery-example",
Expand Down
68 changes: 64 additions & 4 deletions src/theme/Tabs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import React, { cloneElement, type ReactElement, type ReactNode } from "react";
import React, {
cloneElement,
useEffect,
useRef,
useState,
type ReactElement,
type ReactNode,
type RefObject,
} from "react";
import clsx from "clsx";
import { useLocation } from "@docusaurus/router";
import { usePrevious } from "@docusaurus/theme-common";
import {
useScrollPositionBlocker,
useTabsContextValue,
Expand All @@ -25,9 +35,57 @@ interface TabListProps {
interface TabContentProps {
lazy: boolean;
selectedValue: string;
panelsRef: RefObject<HTMLDivElement | null>;
children: ReactNode;
}

function getHashTarget(hash: string): HTMLElement | null {
try {
return hash ? document.getElementById(decodeURIComponent(hash.slice(1))) : null;
} catch {
// Malformed percent-encoding
return null;
}
}

// Hidden panels have no layout, so open the tab holding the hash target before scrolling to it
function useOpenTabOfHashTarget(
panelsRef: RefObject<HTMLDivElement | null>,
tabValues: readonly TabValue[],
selectValue: (value: string) => void
) {
const location = useLocation();
const previousLocation = usePrevious(location);
// New object per request, so the same id scrolls again
const [scrollRequest, setScrollRequest] = useState<{ id: string } | null>(null);

// Every navigation, so a repeated link reopens the tab; search-only changes are `queryString` tab clicks
useEffect(() => {
if (previousLocation?.hash === location.hash && previousLocation.search !== location.search) {
return;
}
const target = getHashTarget(location.hash);
if (!target) {
return;
}
const panels = Array.from(panelsRef.current?.children ?? []);
const tab = tabValues[panels.findIndex((panel) => panel.contains(target))];
if (tab) {
selectValue(tab.value);
setScrollRequest({ id: target.id });
}
}, [location]);

// After the panel is unhidden and grouped Tabs have switched
useEffect(() => {
if (!scrollRequest) {
return;
}
const timeout = setTimeout(() => document.getElementById(scrollRequest.id)?.scrollIntoView());
return () => clearTimeout(timeout);
}, [scrollRequest]);
}

function TabList({ className, block, selectedValue, selectValue, tabValues }: TabListProps) {
const tabRefs: (HTMLLIElement | null)[] = [];
const { blockElementScrollPositionUntilNextRender } = useScrollPositionBlocker();
Expand Down Expand Up @@ -115,7 +173,7 @@ function TabList({ className, block, selectedValue, selectValue, tabValues }: Ta
);
}

function TabContent({ lazy, children, selectedValue }: TabContentProps) {
function TabContent({ lazy, children, selectedValue, panelsRef }: TabContentProps) {
const childTabs = (Array.isArray(children) ? children : [children]).filter(Boolean) as TabItemElement[];
if (lazy) {
const selectedTabItem = childTabs.find((tabItem) => tabItem.props.value === selectedValue);
Expand All @@ -128,7 +186,7 @@ function TabContent({ lazy, children, selectedValue }: TabContentProps) {
});
}
return (
<div className="p-4 bg-pre-background">
<div ref={panelsRef} className="p-4 bg-pre-background">
{childTabs.map((tabItem) =>
cloneElement(tabItem, {
key: tabItem.props.value,
Expand All @@ -141,6 +199,8 @@ function TabContent({ lazy, children, selectedValue }: TabContentProps) {

function TabsComponent(props: Props): ReactNode {
const tabs = useTabsContextValue(props);
const panelsRef = useRef<HTMLDivElement>(null);
useOpenTabOfHashTarget(panelsRef, tabs.tabValues, tabs.selectValue);
return (
<div
className={clsx(
Expand All @@ -155,7 +215,7 @@ function TabsComponent(props: Props): ReactNode {
selectValue={tabs.selectValue}
tabValues={tabs.tabValues}
/>
<TabContent lazy={tabs.lazy} selectedValue={tabs.selectedValue}>
<TabContent lazy={tabs.lazy} selectedValue={tabs.selectedValue} panelsRef={panelsRef}>
{props.children}
</TabContent>
</div>
Expand Down
116 changes: 116 additions & 0 deletions templates/tabs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
title: "Tabs"
unlisted: true
hide_table_of_contents: false
sidebar_label: Tabs
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Tabs

`<Tabs>` shows alternatives in one place: the same operation as C# code and as an RQL query, the sync and async
variant of a method, or the syntax of related classes. Only the selected tab is visible. The others stay in the page,
hidden.

`Tabs` and `TabItem` are not registered globally, so import them in every file that uses them, language partials
included:

```mdx
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
```

## Basic usage

````mdx
<Tabs groupId='languageSyntax'>
<TabItem value="Query" label="Query">
```csharp
var employees = session.Query<Employee>()
.Where(x => x.FirstName == "Robert")
.ToList();
```
</TabItem>
<TabItem value="RQL" label="RQL">
```sql
from "Employees" where FirstName == "Robert"
```
</TabItem>
</Tabs>
````

#### Live example

<Tabs groupId='templates-tabs-live'>
<TabItem value="Query" label="Query">
```csharp
var employees = session.Query<Employee>()
.Where(x => x.FirstName == "Robert")
.ToList();
```
</TabItem>
<TabItem value="RQL" label="RQL">
```sql
from "Employees" where FirstName == "Robert"
```
</TabItem>
</Tabs>

## Linking to content inside a tab

Put an anchor inside the `TabItem`: an `<a id="..."/>` or a Markdown heading. A link to it opens the tab and scrolls to
it, whether it comes from another page, from the same page or from the "In this article" TOC. Link to it the same way
as to any other anchor.

````mdx
<Tabs groupId='languageSyntax'>
<TabItem value="Query" label="Query">
...
</TabItem>
<TabItem value="RQL" label="RQL">
<a id="robert-rql"/>

```sql
from "Employees" where FirstName == "Robert"
```
</TabItem>
</Tabs>

[See this query in RQL](#robert-rql)
````

#### Live example

<Tabs groupId='templates-tabs-live'>
<TabItem value="Query" label="Query">
```csharp
var employees = session.Query<Employee>()
.Where(x => x.FirstName == "Robert")
.ToList();
```
</TabItem>
<TabItem value="RQL" label="RQL">
<a id="robert-rql"/>

```sql
from "Employees" where FirstName == "Robert"
```
</TabItem>
</Tabs>

[See this query in RQL](#robert-rql)

- Hidden tabs are part of the page, so every anchor needs an id that is unique on the whole page, not just in its tab.
- Opening a tab from a link works like clicking it: with a `groupId`, the other `Tabs` in the group switch too and the
choice is remembered.

## Props

| Prop | On | Description |
| --------- | --------- | ------------------------------------------------------------------------------------------------------------------- |
| `groupId` | `Tabs` | `Tabs` sharing a `groupId` switch together, and the choice is remembered across pages. API pages use `languageSyntax`. |
| `value` | `TabItem` | Required. Identifies the tab within its `Tabs`. In a group, tabs with the same `value` switch together. |
| `label` | `TabItem` | Text on the tab. Defaults to `value`. |
| `default` | `TabItem` | Selects this tab when the page opens. Without it the first tab is selected. A choice remembered through `groupId` wins. |
Loading