Skip to content

Commit

Permalink
Update Tabs component design and add tests (#159)
Browse files Browse the repository at this point in the history
  • Loading branch information
KenanYusuf authored Nov 14, 2023
1 parent b80bbdc commit e9e14ae
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/moody-swans-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@envyjs/webui': patch
---

Update tab design
45 changes: 45 additions & 0 deletions packages/webui/src/components/ui/Tabs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { Meta, StoryObj } from '@storybook/react';

import ApplicationContextProvider from '@/context/ApplicationContext';

import { TabContent, TabList, TabListItem } from './Tabs';

const meta = {
title: 'UI/Tabs',
component: TabList,
parameters: {
layout: 'centered',
},
decorators: [
Story => (
<ApplicationContextProvider>
<Story />
</ApplicationContextProvider>
),
],
} satisfies Meta<typeof TabList>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Standard: Story = {
render: () => (
<div>
<TabList>
<TabListItem id="foo" title="Foo" />
<TabListItem id="bar" title="Bar" />
<TabListItem id="baz" title="Baz" />
<TabListItem id="qux" title="Qux" disabled />
</TabList>
<div className="py-2">
<TabContent id="foo">Foo content</TabContent>
<TabContent id="bar">Bar content</TabContent>
<TabContent id="baz">Baz content</TabContent>
<TabContent id="qux">Qux content</TabContent>
</div>
</div>
),
args: {
children: [],
},
};
66 changes: 66 additions & 0 deletions packages/webui/src/components/ui/Tabs.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { cleanup, render, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { setUseApplicationData } from '@/testing/mockUseApplication';

import { TabContent, TabList, TabListItem } from './Tabs';

const Tabs = () => (
<div>
<TabList data-test-id="tab-list">
<TabListItem id="foo" title="Foo" />
<TabListItem id="bar" title="Bar" />
<TabListItem id="baz" title="Baz" />
<TabListItem id="qux" title="Qux" disabled />
</TabList>
<div className="py-2">
<TabContent id="foo">Foo content</TabContent>
<TabContent id="bar">Bar content</TabContent>
<TabContent id="baz">Baz content</TabContent>
<TabContent id="qux">Qux content</TabContent>
</div>
</div>
);

describe('Tabs', () => {
afterEach(() => {
cleanup();
});

it('renders a tab item as expected', () => {
const { getByTestId } = render(<TabListItem id="foo" title="Foo" data-test-id="tab-list-item" />);
const link = getByTestId('tab-list-item');
expect(link).toHaveAttribute('role', 'link');
expect(link).toHaveAttribute('aria-disabled', 'false');
expect(link).toHaveAttribute('href', '#foo');
});

it('renders a disabled tab item as expected', () => {
const { getByTestId } = render(<TabListItem id="foo" title="Foo" data-test-id="tab-list-item" disabled />);
const link = getByTestId('tab-list-item');
expect(link).toHaveAttribute('role', 'link');
expect(link).toHaveAttribute('aria-disabled', 'true');
expect(link).not.toHaveAttribute('href', '#foo');
});

it('should handle changing tabs as expected', async () => {
setUseApplicationData({ selectedTab: 'foo' });

const { getByTestId } = render(<Tabs />);

// TODO, check for changing content

const tabList = getByTestId('tab-list');
const tabs = within(tabList).getAllByRole('link');

await userEvent.click(tabs.at(1)!);
expect(global.window.location.hash).toBe('#bar');

await userEvent.click(tabs.at(2)!);
expect(global.window.location.hash).toBe('#baz');

// Disabled tab, should not change the hash
await userEvent.click(tabs.at(3)!);
expect(global.window.location.hash).toBe('#baz');
});
});
42 changes: 32 additions & 10 deletions packages/webui/src/components/ui/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,51 @@
import useApplication from '@/hooks/useApplication';
import { tw } from '@/utils';

export function TabList({ children }: { children: React.ReactNode }) {
export function TabList({
children,
...props
}: { children: React.ReactNode } & React.HTMLAttributes<HTMLUListElement>) {
return (
<div className="bg-secondary border-b border-primary">
<ul className="flex flex-wrap text-sm gap-1">{children}</ul>
</div>
<ul className="flex flex-wrap text-sm gap-1" {...props}>
{children}
</ul>
);
}

export function TabListItem({ id, title }: { id: string; title: string }) {
export function TabListItem({
id,
title,
disabled = false,
...props
}: { id: string; title: string; disabled?: boolean } & React.HTMLAttributes<HTMLAnchorElement>) {
const { selectedTab, setSelectedTab } = useApplication();

const href = disabled ? undefined : `#${id}`;

const allowInteractive = !(disabled || selectedTab === id);

const className = tw(
'inline-block px-4 py-3 uppercase font-semibold cursor-pointer',
'border border-b-0',
selectedTab === id ? 'border-green-400 bg-green-100' : 'border-primary bg-primary',
'inline-block px-3 py-2 rounded-[0.25rem] font-bold uppercase text-xs',
'text-manatee-800',
allowInteractive && 'hover:bg-apple-200 hover:text-apple-900',
allowInteractive && 'active:bg-apple-500 active:text-apple-950',
disabled && 'text-gray-400 cursor-not-allowed',
selectedTab === id && 'bg-apple-400 text-[#0D280B]',
);

return (
<li>
<a
href={`#${id}`}
{...props}
role="link"
aria-disabled={disabled}
href={href}
className={className}
onClick={() => {
onClick={e => {
if (disabled) {
e.preventDefault();
return;
}
setSelectedTab(id);
}}
>
Expand Down
8 changes: 4 additions & 4 deletions packages/webui/src/components/ui/TraceDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default function TraceDetail() {

return (
<div className="h-full flex flex-col p-default bg-secondary">
<div className="sticky top-0" data-test-id="summary">
<div className="sticky top-0 pb-2" data-test-id="summary">
<div className="flex flex-row gap-2 items-center">
<div className="flex-1 flex flex-row gap-2">
<div>
Expand Down Expand Up @@ -116,12 +116,12 @@ export default function TraceDetail() {

<TabList>
<TabListItem title="Details" id="default" />
{availableTabs.includes(TabMap.payload) && <TabListItem title="Payload" id={TabMap.payload} />}
{availableTabs.includes(TabMap.response) && <TabListItem title="Response" id={TabMap.response} />}
<TabListItem title="Payload" id={TabMap.payload} disabled={!availableTabs.includes(TabMap.payload)} />
<TabListItem title="Response" id={TabMap.response} disabled={!availableTabs.includes(TabMap.response)} />
</TabList>
</div>

<div className="overflow-hidden overflow-y-auto h-full" data-test-id="trace-detail">
<div className="overflow-hidden overflow-y-auto h-full border-t border-t-manatee-400" data-test-id="trace-detail">
<TabContent id="default">
<Section data-test-id="request-details" title="Request Details" className="border-t-0">
<Fields>
Expand Down

0 comments on commit e9e14ae

Please sign in to comment.