Skip to content
This repository was archived by the owner on Jan 8, 2025. It is now read-only.
Merged
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
53 changes: 53 additions & 0 deletions components/Tabs/TabContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createContext, useState } from "react";
import { createHash } from "crypto";
import type { BinaryToTextEncoding } from "crypto";
import { DataTab } from "./types";
import type { ReactNode } from "react";

export interface TabContextProps {
getSelectedLabel: (tabs: Array<string>) => string;
setSelectedLabel: (tabs: Array<string>, selected: string) => void;
}

export const TabContext = createContext<TabContextProps>({
getSelectedLabel: (tabs: Array<string>): string => {
return "";
},
setSelectedLabel: (tabs: Array<string>, selected: string) => {
return;
},
});

Comment on lines +12 to +20
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess I haven't created a context with methods like this before. It looked so wrong to me but, the docs say this is how it goes so, 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We use this approach in a few places in the docs engine. I'm not super familiar with React best practices, so I'm happy to change this (or create a GitHub issue so we can change this everywhere later) if there's a better way!

// labelHashKey returns the key that TabContextProvider uses to get and set the
// selected tab label or dropdown menu item within a particular combination of
// lables/menu items.
const labelHashKey = (labels: Array<string>): string => {
const labelsCopy = [...labels];
labelsCopy.sort();
return labelsCopy.join("");
};

interface TabContextProviderProps {
children: ReactNode;
}

// TabContextProvider tracks the currently selected tab label and dropdown menu
// option in each Tabs component that has rendered.
export const TabContextProvider = ({ children }: TabContextProviderProps) => {
const [stateForAllLabels, setStateForAllLabels] = useState({});

const context = {
getSelectedLabel: (tabs: Array<string>): string => {
return stateForAllLabels[labelHashKey(tabs)];
},

setSelectedLabel: (tabs: Array<string>, selected: string): void => {
const key = labelHashKey(tabs);
setStateForAllLabels((prevState) => {
return { ...prevState, [key]: selected };
});
},
};

return <TabContext.Provider value={context}>{children}</TabContext.Provider>;
};
286 changes: 215 additions & 71 deletions components/Tabs/Tabs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@ import type { Meta, StoryObj } from "@storybook/react";
import { userEvent, within } from "@storybook/testing-library";
import { expect } from "@storybook/jest";
import { default as Pre } from "../MDX/Pre";
import { TabContextProvider } from "./TabContext";
import { DocsContextProvider } from "layouts/DocsPage/context";

import { TabItem } from "./TabItem";
import { Tabs } from "./Tabs";

export const DefaultTabsWithSelected = () => (
<Tabs>
<TabItem label="Source">Instructions for building from source.</TabItem>
<TabItem label="Helm">
Instructions for installing release using a Helm chart.
</TabItem>
<TabItem label="Shell" selected>
Instructions for installing release using shell commands.
</TabItem>
</Tabs>
<TabContextProvider>
<Tabs>
<TabItem label="Source">Instructions for building from source.</TabItem>
<TabItem label="Helm">
Instructions for installing release using a Helm chart.
</TabItem>
<TabItem label="Shell" selected>
Instructions for installing release using shell commands.
</TabItem>
</Tabs>
</TabContextProvider>
);

const meta: Meta<typeof Tabs> = {
Expand Down Expand Up @@ -62,11 +66,13 @@ export const ChangeTab: Story = {
export const TabsWithDropdownView: Story = {
render: () => {
return (
<Tabs dropdownCaption="Platform" dropdownView>
<TabItem label="Option 1">Instructions for Option 1.</TabItem>
<TabItem label="Option 2">Instructions for Option 2.</TabItem>
<TabItem label="Option 3">Instructions for Option 3.</TabItem>
</Tabs>
<TabContextProvider>
<Tabs dropdownCaption="Platform" dropdownView>
<TabItem label="Option 1">Instructions for Option 1.</TabItem>
<TabItem label="Option 2">Instructions for Option 2.</TabItem>
<TabItem label="Option 3">Instructions for Option 3.</TabItem>
</Tabs>
</TabContextProvider>
);
},
play: async ({ canvasElement, step }) => {
Expand All @@ -82,27 +88,29 @@ export const TabsWithDropdownView: Story = {
export const TabsWithDropdownAndIdenticalLabels: Story = {
render: () => {
return (
<Tabs dropdownCaption="Platform">
<TabItem options="Kubernetes Option" label="OSS">
Kubernetes/OSS
</TabItem>
<TabItem options="Linux Server Option" label="OSS">
Linux Server/OSS
</TabItem>
<TabItem options="Kubernetes Option" label="Enterprise">
Kubernetes/Enterprise
</TabItem>
<TabItem options="Linux Server Option" label="Enterprise">
Linux Server/Enterprise
</TabItem>
</Tabs>
<TabContextProvider>
<Tabs dropdownCaption="Platform">
<TabItem options="Kubernetes Option" label="OSS">
Kubernetes/OSS
</TabItem>
<TabItem options="Linux Server Option" label="OSS">
Linux Server/OSS
</TabItem>
<TabItem options="Kubernetes Option" label="Enterprise">
Kubernetes/Enterprise
</TabItem>
<TabItem options="Linux Server Option" label="Enterprise">
Linux Server/Enterprise
</TabItem>
</Tabs>
</TabContextProvider>
);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
step("Switch dropdown tabs", async () => {
userEvent.click(canvas.getByTestId("listbox-input"));
userEvent.click(await canvas.findByText("Linux Server Option"));
await userEvent.click(canvas.getByTestId("listbox-input"));
await userEvent.click(await canvas.findByText("Linux Server Option"));
const tabs = canvas.getAllByTestId("tabitem");
const visibleTabs = tabs.filter((tab) => {
return !tab.parentElement.className.includes("hidden");
Expand All @@ -115,26 +123,28 @@ export const TabsWithDropdownAndIdenticalLabels: Story = {
export const TabsWithDropdownIdenticalLabelsAndMultipleOptionValues: Story = {
render: () => {
return (
<Tabs dropdownCaption="Platform">
<TabItem options="Kubernetes Option" label="OSS">
Kubernetes/OSS
</TabItem>
<TabItem options="Linux Server Option" label="OSS">
Linux Server/OSS
</TabItem>
<TabItem options="Kubernetes Option" label="Enterprise">
Kubernetes/Enterprise
</TabItem>
<TabItem options="Linux Server Option" label="Enterprise">
Linux Server/Enterprise
</TabItem>
<TabItem
options="Linux Server Option,Kubernetes Option"
label="Cloud Label"
>
Cloud instructions
</TabItem>
</Tabs>
<TabContextProvider>
<Tabs dropdownCaption="Platform">
<TabItem options="Kubernetes Option" label="OSS">
Kubernetes/OSS
</TabItem>
<TabItem options="Linux Server Option" label="OSS">
Linux Server/OSS
</TabItem>
<TabItem options="Kubernetes Option" label="Enterprise">
Kubernetes/Enterprise
</TabItem>
<TabItem options="Linux Server Option" label="Enterprise">
Linux Server/Enterprise
</TabItem>
<TabItem
options="Linux Server Option,Kubernetes Option"
label="Cloud Label"
>
Cloud instructions
</TabItem>
</Tabs>
</TabContextProvider>
);
},
play: async ({ canvasElement, step }) => {
Expand All @@ -150,27 +160,161 @@ export const TabsWithDropdownIdenticalLabelsAndMultipleOptionValues: Story = {
export const DropdownWithCodeSnippet: Story = {
render: () => {
return (
<Tabs dropdownCaption="Platform" dropdownView>
<TabItem label="Option 1">
<Pre>
<code>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
</code>
</Pre>
</TabItem>
<TabItem label="Option 2">Instructions for the second option.</TabItem>
<TabItem label="Option 3">Instructions for the third option.</TabItem>
</Tabs>
<TabContextProvider>
<Tabs dropdownCaption="Platform" dropdownView>
<TabItem label="Option 1">
<Pre>
<code>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
</code>
</Pre>
</TabItem>
<TabItem label="Option 2">
Instructions for the second option.
</TabItem>
<TabItem label="Option 3">Instructions for the third option.</TabItem>
</Tabs>
</TabContextProvider>
);
},
};

export const TabItemsWithTheSameLabelsChangeTogether: Story = {
render: () => {
return (
<DocsContextProvider>
<TabContextProvider>
<Tabs>
<TabItem label="Kubernetes Label">
Initial Kubernetes instructions
</TabItem>
<TabItem label="Linux VM Label">
Initial Linux VM instructions.
</TabItem>
<TabItem label="Another Platform Label">
Instructions for another platform.
</TabItem>
</Tabs>
<Tabs>
<TabItem label="Self-Hosted">Self-Hosted instructions</TabItem>
<TabItem label="Linux VM Label">
These instructions should remain hidden.
</TabItem>
<TabItem label="Cloud Label">Cloud instructions</TabItem>
</Tabs>
<Tabs>
<TabItem label="Kubernetes Label">
More Kubernetes instructions
</TabItem>
<TabItem label="Another Platform Label">
Instructions for another platform.
</TabItem>
<TabItem label="Linux VM Label">More Linux VM instructions</TabItem>
</Tabs>
</TabContextProvider>
</DocsContextProvider>
);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
const linuxLabels = canvas.getAllByText("Linux VM Label");
await userEvent.click(linuxLabels[0]);
expect(
canvas.getByText("More Linux VM instructions").parentElement.className
).not.toContain("hidden");
expect(
canvas.getByText("These instructions should remain hidden.").parentElement
.className
).toContain("hidden");
},
};

export const TabsWithIdenticalDropdownsChangeTogether: Story = {
render: () => {
return (
<TabContextProvider>
<Tabs dropdownCaption="Platform">
<TabItem options="Kubernetes Option" label="OSS">
Kubernetes/OSS
</TabItem>
<TabItem options="Linux Server Option" label="OSS">
Linux Server/OSS
</TabItem>
<TabItem options="Kubernetes Option" label="Enterprise label">
Kubernetes/Enterprise
</TabItem>
<TabItem options="Linux Server Option" label="Enterprise label">
Linux Server/Enterprise
</TabItem>
</Tabs>

<Tabs dropdownCaption="Platform">
<TabItem options="Kubernetes Option" label="OSS">
Kubernetes/OSS 2
</TabItem>
<TabItem options="Linux Server Option" label="OSS">
Linux Server/OSS 2
</TabItem>
<TabItem options="Kubernetes Option" label="Enterprise label">
Kubernetes/Enterprise 2
</TabItem>
<TabItem options="Linux Server Option" label="Enterprise label">
Linux Server/Enterprise 2
</TabItem>
</Tabs>
</TabContextProvider>
);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
const ents = canvas.getAllByText("Enterprise label");
await userEvent.click(ents[0]);
const dropdowns = canvas.getAllByTestId("listbox-input");
await userEvent.click(dropdowns[0].children[0]);
const linuxOptions = await canvas.findAllByText("Linux Server Option");
await userEvent.click(linuxOptions[0]);

expect(
canvas.getByText("Linux Server/Enterprise 2").parentElement.className
).not.toContain("hidden");
},
};

export const TabsWithDropdownViewChangeTogether: Story = {
render: () => {
return (
<TabContextProvider>
<Tabs dropdownCaption="Platform" dropdownView>
<TabItem label="Option 1">First instructions for Option 1.</TabItem>
<TabItem label="Option 2">First instructions for Option 2.</TabItem>
</Tabs>
<Tabs dropdownCaption="Platform" dropdownView>
<TabItem label="Option 1">Second instructions for Option 1.</TabItem>
<TabItem label="Option 2">Second instructions for Option 2.</TabItem>
</Tabs>
</TabContextProvider>
);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
const dropdowns = canvas.getAllByTestId("listbox-input");
await userEvent.click(dropdowns[0].children[0]);
const options = await canvas.findAllByText("Option 2");
await userEvent.click(options[0]);

expect(
canvas.getByText("Second instructions for Option 2.").parentElement
.className
).not.toContain("hidden");
},
};
Loading