Skip to content

Tabs: Styling #2545

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

Merged
merged 14 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from 13 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
13 changes: 13 additions & 0 deletions .changeset/dirty-pens-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@khanacademy/wonder-blocks-tabs": minor
---

Tabs:

- Add styling
- Add support for `styles` prop
- Add `animated` prop for enabling transition animations for the selected tab

NavigationTabs:

- Refactored current tab indicator logic to work with Tabs
29 changes: 29 additions & 0 deletions __docs__/components/placeholder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from "react";
import {View} from "@khanacademy/wonder-blocks-core";
import {border, semanticColor, sizing} from "@khanacademy/wonder-blocks-tokens";

type Props = {
children: React.ReactNode;
};

/**
* Component that renders a placeholder block. It is useful for visualizing
* layouts.
*/
export const Placeholder = (props: Props) => {
const {children} = props;
return (
<View
style={{
backgroundColor: semanticColor.surface.secondary,
padding: sizing.size_120,
margin: sizing.size_010,
border: `${border.width.thin}px dashed ${semanticColor.border.subtle}`,
width: "100%",
alignItems: "center",
}}
>
{children}
</View>
);
};
123 changes: 123 additions & 0 deletions __docs__/wonder-blocks-tabs/tab-variants.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import type {Meta, StoryObj} from "@storybook/react";
import * as React from "react";

import {Tab} from "@khanacademy/wonder-blocks-tabs";
import {AllVariants} from "../components/all-variants";
import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon";
import {IconMappings} from "../wonder-blocks-icon/phosphor-icon.argtypes";
import {addStyle} from "@khanacademy/wonder-blocks-core";
import {sizing} from "@khanacademy/wonder-blocks-tokens";
import {rtlText} from "../components/text-for-testing";

const StyledDiv = addStyle("div");

const generateRows = (rtl: boolean = false) => [
{
name: "Default",
props: {
children: rtl ? rtlText : "Tab",
},
},
{
name: "With Icons",
props: {
children: (
<StyledDiv
style={{
display: "flex",
alignItems: "center",
gap: sizing.size_040,
}}
>
<PhosphorIcon icon={IconMappings.cookie} />
{rtl ? rtlText : "Tab"}
<PhosphorIcon icon={IconMappings.iceCream} />
</StyledDiv>
),
ariaLabel: "Tab with icons",
},
},
{
name: "Icon Only",
props: {
children: (
<PhosphorIcon icon={IconMappings.iceCream} size="medium" />
),
ariaLabel: "Tab with icon",
},
},
];

const rows = generateRows();
const rtlRows = generateRows(true);

const columns = [
{
name: "Default",
props: {},
},
{
name: "Selected",
props: {
selected: true,
},
},
];

type Story = StoryObj<typeof Tab>;

/**
* The following stories are used to generate the pseudo states for the Switch
* component. This is only used for visual testing in Chromatic.
*/
const meta = {
title: "Packages / Tabs / Tabs / Subcomponents /Tab - All Variants",
component: Tab,
render: (args) => (
<>
<AllVariants rows={rows} columns={columns}>
{(props) => (
<div role="tablist">
<Tab {...args} {...props} />
</div>
)}
</AllVariants>
<div dir="rtl">
<AllVariants rows={rtlRows} columns={columns}>
{(props) => (
<div role="tablist">
<Tab {...args} {...props} />
</div>
)}
</AllVariants>
</div>
</>
),
args: {},
tags: ["!autodocs"],
} satisfies Meta<typeof Tab>;

export default meta;

export const Default: Story = {};

export const Hover: Story = {
parameters: {pseudo: {hover: true}},
};

export const Focus: Story = {
parameters: {pseudo: {focusVisible: true}},
};

export const HoverFocus: Story = {
name: "Hover + Focus",
parameters: {pseudo: {hover: true, focusVisible: true}},
};

export const Press: Story = {
parameters: {pseudo: {hover: true, active: true}},
};

export const PressFocus: Story = {
parameters: {pseudo: {focusVisible: true, active: true}},
};
219 changes: 219 additions & 0 deletions __docs__/wonder-blocks-tabs/tabs-variants.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import type {Meta, StoryObj} from "@storybook/react";
import * as React from "react";

import {Tabs} from "@khanacademy/wonder-blocks-tabs";
import {AllVariants} from "../components/all-variants";
import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon";
import {IconMappings} from "../wonder-blocks-icon/phosphor-icon.argtypes";
import {addStyle, View} from "@khanacademy/wonder-blocks-core";
import {rtlText} from "../components/text-for-testing";

const StyledDiv = addStyle("div");

const generateRows = (rtl: boolean = false) => [
{
name: "Default",
props: {
tabs: [
{
label: rtl ? rtlText : "Tab 1",
id: "tab-1",
panel: rtl ? rtlText : "Tab 1 Contents",
},
{
label: rtl ? rtlText : "Tab 2",
id: "tab-2",
panel: rtl ? rtlText : "Tab 2 Contents",
},
{
label: rtl ? rtlText : "Tab 3",
id: "tab-3",
panel: rtl ? rtlText : "Tab 3 Contents",
},
],
selectedTabId: "tab-1",
},
},
{
name: "With Icons",
props: {
tabs: [
{
label: (
<StyledDiv
style={{
display: "flex",
alignItems: "center",
// TODO: Update to use spacing tokens
gap: "4px",
}}
>
<PhosphorIcon icon={IconMappings.cookie} />
{rtl ? rtlText : "Tab 1"}
<PhosphorIcon icon={IconMappings.iceCream} />
</StyledDiv>
),
id: "tab-1",
panel: rtl ? rtlText : "Tab 1 Contents",
},
{
label: (
<StyledDiv
style={{
display: "flex",
alignItems: "center",
// TODO: Update to use spacing tokens
gap: "4px",
}}
>
<PhosphorIcon icon={IconMappings.cookie} />
{rtl ? rtlText : "Tab 2"}
<PhosphorIcon icon={IconMappings.iceCream} />
</StyledDiv>
),
id: "tab-2",
panel: rtl ? rtlText : "Tab 2 Contents",
},
{
label: (
<StyledDiv
style={{
display: "flex",
alignItems: "center",
// TODO: Update to use spacing tokens
gap: "4px",
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: I guess this is possible now that Marcy has landed the new spacing tokens :).

}}
>
<PhosphorIcon icon={IconMappings.cookie} />
{rtl ? rtlText : "Tab 3"}
<PhosphorIcon icon={IconMappings.iceCream} />
</StyledDiv>
),
id: "tab-3",
panel: rtl ? rtlText : "Tab 3 Contents",
},
],
selectedTabId: "tab-1",
},
},
{
name: "Icon Only",
props: {
tabs: [
{
label: (
<PhosphorIcon
icon={IconMappings.iceCream}
size="medium"
/>
),
id: "tab-1",
panel: rtl ? rtlText : "Tab 1 Contents",
"aria-label": "Tab 1",
},
{
label: (
<PhosphorIcon
icon={IconMappings.cookie}
size="medium"
/>
),
id: "tab-2",
panel: rtl ? rtlText : "Tab 2 Contents",
"aria-label": "Tab 2",
},
],
selectedTabId: "tab-1",
},
},
];

const rows = generateRows();
const rtlRows = generateRows(true);

const columns = [
{
name: "Default",
props: {},
},
];

type Story = StoryObj<typeof Tabs>;

/**
* The following stories are used to generate the pseudo states for the Switch
* component. This is only used for visual testing in Chromatic.
*/
const meta = {
title: "Packages / Tabs / Tabs / Tabs - All Variants",
component: Tabs,
render: (args) => (
<>
<AllVariants rows={rows} columns={columns}>
{(props) => (
<View>
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: This View could be removed if it is not necessary.

Copy link
Member Author

Choose a reason for hiding this comment

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

I added the <View> so that the tabs would show as expected in the tables for rtl! Here's what it looks like without the <View>:
Screenshot 2025-04-15 at 10 50 41 AM

And with the <View> wrapper (the tabs in rtl are right aligned in the table!):
image

<Tabs {...args} {...props} />
</View>
)}
</AllVariants>
<div dir="rtl">
<AllVariants rows={rtlRows} columns={columns}>
{(props) => (
<View>
<Tabs {...args} {...props} />
</View>
)}
</AllVariants>
</div>
</>
),
args: {},
tags: ["!autodocs"],
} satisfies Meta<typeof Tabs>;

export default meta;

export const Default: Story = {};

export const Hover: Story = {
parameters: {pseudo: {hover: true}},
};

export const Focus: Story = {
parameters: {pseudo: {focusVisible: true}},
};

export const HoverFocus: Story = {
name: "Hover + Focus",
parameters: {pseudo: {hover: true, focusVisible: true}},
};

export const Press: Story = {
parameters: {pseudo: {hover: true, active: true}},
};

export const Zoom: Story = {
render: (args) => (
<>
<AllVariants rows={rows} columns={columns} layout="list">
{(props) => (
<View>
<Tabs {...args} {...props} />
</View>
)}
</AllVariants>
<div dir="rtl">
<AllVariants rows={rtlRows} columns={columns} layout="list">
{(props) => (
<View>
<Tabs {...args} {...props} />
</View>
)}
</AllVariants>
</div>
</>
),
globals: {
zoom: "400%",
Copy link
Member Author

Choose a reason for hiding this comment

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

This is working as expected in Chromatic now! I need to follow up with the Chromatic support team still on the other flaky tests we were seeing before

},
};
5 changes: 5 additions & 0 deletions __docs__/wonder-blocks-tabs/tabs.accessibility.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import {Meta} from "@storybook/blocks";
the id of the label. If there isn't, set the `aria-label` prop
- Make sure the ids for the tabs are unique. They are used to associate the tabs
and tab panels together
- When enabling animations in the component using the `animated` prop, consider
setting it to `false` for users who have their prefered reduced motion setting
on. By default, animations are disabled.
- Note: We have an `animated` prop to enable animations so that this can be
configured based on both the OS and user's site settings

## About the Implementation

Expand Down
Loading
Loading