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

refactor(v2): render all tab panels at once #3706

Merged
merged 2 commits into from
Nov 9, 2020
Merged
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
8 changes: 6 additions & 2 deletions packages/docusaurus-theme-classic/src/theme/TabItem/index.tsx
Original file line number Diff line number Diff line change
@@ -8,8 +8,12 @@
import React from 'react';
import type {Props} from '@theme/TabItem';

function TabItem(props: Props): JSX.Element {
return <div>{props.children}</div>;
function TabItem({children, hidden, className}: Props): JSX.Element {
return (
<div role="tabpanel" {...{hidden, className}}>
{children}
</div>
);
}

export default TabItem;
39 changes: 28 additions & 11 deletions packages/docusaurus-theme-classic/src/theme/Tabs/index.tsx
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/

import React, {useState, Children, ReactElement} from 'react';
import React, {useState, cloneElement} from 'react';
import useUserPreferencesContext from '@theme/hooks/useUserPreferencesContext';
import type {Props} from '@theme/Tabs';

@@ -19,7 +19,15 @@ const keys = {
};

function Tabs(props: Props): JSX.Element {
const {block, children, defaultValue, values, groupId, className} = props;
const {
lazy,
block,
children,
defaultValue,
values,
groupId,
className,
} = props;
const {tabGroupChoices, setTabGroupChoices} = useUserPreferencesContext();
const [selectedValue, setSelectedValue] = useState(defaultValue);

@@ -109,15 +117,24 @@ function Tabs(props: Props): JSX.Element {
</li>
))}
</ul>
<div role="tabpanel" className="margin-vert--md">
{
Children.toArray(children).filter(
(child) =>
(child as ReactElement<{value: string}>).props.value ===
selectedValue,
)[0]
}
</div>

{lazy ? (
cloneElement(
Copy link
Collaborator

Choose a reason for hiding this comment

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

isn't the div wrapper missing here?

Copy link
Contributor Author

@lex111 lex111 Nov 9, 2020

Choose a reason for hiding this comment

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

We don't need div wrapper if we render only single tab panel. If multiple tab panels are rendered (!lazy), then they will be in the div wrapper. This is a kind of optimization so as not to create extra div elements and follow ARIA meanwhile.

Copy link
Collaborator

@slorber slorber Nov 9, 2020

Choose a reason for hiding this comment

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

Ah I see, didn't notice that you moved the className to the cloned element, thought that spacing was lost

children.filter(
(tabItem) => tabItem.props.value === selectedValue,
)[0],
{className: 'margin-vert--md'},
)
) : (
<div className="margin-vert--md">
{children.map((tabItem, i) =>
cloneElement(tabItem, {
key: i,
hidden: tabItem.props.value !== selectedValue,
}),
)}
</div>
)}
</div>
);
}
11 changes: 9 additions & 2 deletions packages/docusaurus-theme-classic/src/types.d.ts
Original file line number Diff line number Diff line change
@@ -374,18 +374,25 @@ declare module '@theme/NavbarItem' {
declare module '@theme/TabItem' {
import type {ReactNode} from 'react';

export type Props = {readonly children: ReactNode};
export type Props = {
readonly children: ReactNode;
readonly value: string;
readonly hidden: boolean;
readonly className: string;
};

const TabItem: () => JSX.Element;
export default TabItem;
}

declare module '@theme/Tabs' {
import type {ReactElement} from 'react';
import type {Props as TabItemProps} from '@theme/TabItem';

export type Props = {
readonly lazy?: boolean;
readonly block?: boolean;
readonly children: readonly ReactElement<{value: string}>[];
readonly children: readonly ReactElement<TabItemProps>[];
readonly defaultValue?: string;
readonly values: readonly {value: string; label: string}[];
readonly groupId?: string;
6 changes: 6 additions & 0 deletions website/docs/markdown-features.mdx
Original file line number Diff line number Diff line change
@@ -296,6 +296,12 @@ And you will get the following:
<TabItem value="banana">This is a banana 🍌</TabItem>
</Tabs>

:::info

By default, tabs are rendered eagerly, but it is possible to load them lazily by passing the `lazy` prop to the `Tabs` component.

:::

### Syncing tab choices

You may want choices of the same kind of tabs to sync with each other. For example, you might want to provide different instructions for users on Windows vs users on macOS, and you want to changing all OS-specific instructions tabs in one click. To achieve that, you can give all related tabs the same `groupId` prop. Note that doing this will persist the choice in `localStorage` and all `<Tab>` instances with the same `groupId` will update automatically when the value of one of them is changed. Not that `groupID` are globally-namespaced.