diff --git a/.changeset/moody-swans-collect.md b/.changeset/moody-swans-collect.md new file mode 100644 index 0000000..372a5bd --- /dev/null +++ b/.changeset/moody-swans-collect.md @@ -0,0 +1,5 @@ +--- +'@envyjs/webui': patch +--- + +Update tab design diff --git a/packages/webui/src/components/ui/Tabs.stories.tsx b/packages/webui/src/components/ui/Tabs.stories.tsx new file mode 100644 index 0000000..97513b9 --- /dev/null +++ b/packages/webui/src/components/ui/Tabs.stories.tsx @@ -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 => ( + + + + ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Standard: Story = { + render: () => ( +
+ + + + + + +
+ Foo content + Bar content + Baz content + Qux content +
+
+ ), + args: { + children: [], + }, +}; diff --git a/packages/webui/src/components/ui/Tabs.test.tsx b/packages/webui/src/components/ui/Tabs.test.tsx new file mode 100644 index 0000000..46e30c9 --- /dev/null +++ b/packages/webui/src/components/ui/Tabs.test.tsx @@ -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 = () => ( +
+ + + + + + +
+ Foo content + Bar content + Baz content + Qux content +
+
+); + +describe('Tabs', () => { + afterEach(() => { + cleanup(); + }); + + it('renders a tab item as expected', () => { + const { getByTestId } = render(); + 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(); + 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(); + + // 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'); + }); +}); diff --git a/packages/webui/src/components/ui/Tabs.tsx b/packages/webui/src/components/ui/Tabs.tsx index 7b2a452..1f88c11 100644 --- a/packages/webui/src/components/ui/Tabs.tsx +++ b/packages/webui/src/components/ui/Tabs.tsx @@ -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) { return ( -
-
    {children}
-
+
    + {children} +
); } -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) { 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 (
  • { + onClick={e => { + if (disabled) { + e.preventDefault(); + return; + } setSelectedTab(id); }} > diff --git a/packages/webui/src/components/ui/TraceDetail.tsx b/packages/webui/src/components/ui/TraceDetail.tsx index 23b2c45..fb640d3 100644 --- a/packages/webui/src/components/ui/TraceDetail.tsx +++ b/packages/webui/src/components/ui/TraceDetail.tsx @@ -79,7 +79,7 @@ export default function TraceDetail() { return (
    -
    +
    @@ -116,12 +116,12 @@ export default function TraceDetail() { - {availableTabs.includes(TabMap.payload) && } - {availableTabs.includes(TabMap.response) && } + +
    -
    +