Skip to content

Commit

Permalink
Update tabs component in PRIME (viamrobotics#551)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrloureed authored Aug 5, 2024
1 parent 7337e19 commit 4d6c6c6
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 153 deletions.
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@viamrobotics/prime-core",
"version": "0.0.139",
"version": "0.0.140",
"publishConfig": {
"access": "public"
},
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/lib/__tests__/tabs-bar-with-slots.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import TabsBar from '$lib/tabs-bar.svelte';
import Tab from '$lib/tab.svelte';
</script>

<TabsBar aria-label="Tab example one">
<Tab
title="The first tab"
href="#first"
selected
></Tab>
<Tab
title="The second tab"
href="#second"
></Tab>
<Tab
title="The third tab"
href="#third"
></Tab>
</TabsBar>
70 changes: 33 additions & 37 deletions packages/core/src/lib/__tests__/tabs.spec.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,43 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import { Tabs } from '$lib';
import { cxTestArguments, cxTestResults } from './cx-test';
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import { TabsBar } from '$lib';
import TabsBarWithSlots from './tabs-bar-with-slots.svelte';

describe('Tabs', () => {
it('Renders tabs correctly', () => {
render(Tabs, { tabs: ['Tab 1', 'Tab 2', 'Tab 3'], selected: 'Tab 1' });
expect(screen.getByText('Tab 1')).toBeVisible();
expect(screen.getByText('Tab 2')).toBeVisible();
expect(screen.getByText('Tab 3')).toBeVisible();
describe('TabsBar renders correctly', () => {
it('Renders rest props and default variant', () => {
render(TabsBar, { 'aria-label': 'Tab example one' });
expect(screen.getByRole('navigation')).toHaveAccessibleName(
'Tab example one'
);
expect(screen.getByRole('navigation')).toHaveClass(
'h-10 bg-medium tracking-wide sm:px-2 flex items-center font-roboto-mono'
);
});

it('Marks the selected tab correctly', () => {
render(Tabs, { tabs: ['Tab 1', 'Tab 2', 'Tab 3'], selected: 'Tab 1' });
expect(screen.getByRole('button', { name: 'Tab 1' })).toHaveClass(
'bg-white border border-x-border-2 border-t-border-2 border-b-white font-semibold'
it('Renders secondary variant', () => {
render(TabsBar, { variant: 'secondary' });
expect(screen.getByRole('navigation')).toHaveClass(
'flex items-center font-roboto-mono'
);
});

it('Switches to another tab on click', async () => {
const { component } = render(Tabs, {
tabs: ['Tab 1', 'Tab 2', 'Tab 3'],
selected: 'Tab 1',
});
const onInput = vi.fn();
component.$on('input', onInput);
it('Renders TabsBar with Tab components in slots', () => {
render(TabsBarWithSlots);

await fireEvent.click(screen.getByText('Tab 2'));
expect(onInput).toHaveBeenCalledOnce();
await fireEvent.click(screen.getByText('Tab 2'));
expect(onInput).toHaveBeenCalledWith(
expect.objectContaining({ detail: { value: 'Tab 2' } })
);
});
expect(screen.getByText('The first tab')).toBeInTheDocument();
expect(screen.getByText('The second tab')).toBeInTheDocument();
expect(screen.getByText('The third tab')).toBeInTheDocument();

it('Renders with the passed cx classes', () => {
render(Tabs, {
tabs: ['Tab 1', 'Tab 2', 'Tab 3'],
selected: 'Tab 1',
cx: cxTestArguments,
});
expect(screen.getByText('Tab 1').parentElement?.parentElement).toHaveClass(
cxTestResults
);
const firstTab = screen.getByText('The first tab');
const secondTab = screen.getByText('The second tab');
const thirdTab = screen.getByText('The third tab');

expect(firstTab.closest('a')).toHaveAttribute('href', '#first');
expect(secondTab.closest('a')).toHaveAttribute('href', '#second');
expect(thirdTab.closest('a')).toHaveAttribute('href', '#third');

expect(firstTab).toHaveAttribute('aria-current', 'page');
expect(secondTab).toHaveAttribute('aria-current', 'false');
expect(thirdTab).toHaveAttribute('aria-current', 'false');
});
});
3 changes: 2 additions & 1 deletion packages/core/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ export { default as TableHeaderCell } from './table/table-header-cell.svelte';
export { default as TableBody } from './table/table-body.svelte';
export { default as TableRow } from './table/table-row.svelte';
export { default as TableCell } from './table/table-cell.svelte';
export { default as Tabs } from './tabs.svelte';
export { default as TabsBar } from './tabs-bar.svelte';
export { default as Tab } from './tab.svelte';
export { default as ToggleButtons } from './toggle-buttons.svelte';

export {
Expand Down
60 changes: 60 additions & 0 deletions packages/core/src/lib/tab.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<!--
@component
A clickable element that allows the user to navigate to another page or area.
```svelte
<Tab
title='The first tab'
href="#first"
selected
/>
```
-->
<svelte:options immutable />

<script lang="ts">
import cx from 'classnames';
import type { HTMLAttributes } from 'svelte/elements';
import { CONTEXT_KEY } from './tabs-bar.svelte';
import { getContext } from 'svelte';
import type { Writable } from 'svelte/store';
interface $$Props extends HTMLAttributes<HTMLElement> {
href: string;
title: string;
selected?: boolean;
class?: string;
}
/** The tab's href. */
export let href: $$Props['href'];
/** The tab's title. */
export let title: $$Props['title'];
//* The tab's state */
export let selected: $$Props['selected'] = false;
const variant = getContext<Writable<'primary' | 'secondary'>>(CONTEXT_KEY);
let className = '';
export { className as class };
</script>

<a
{href}
aria-current={selected ? 'page' : false}
class={cx(
'flex h-8 px-4 text-sm leading-8 text-default hover:bg-ghost-light hover:text-default focus:bg-ghost-light focus:text-default active:bg-ghost-medium',
{
'font-semibold': selected,
uppercase: $variant === 'primary',
'border-b border-gray-3 first-letter:capitalize':
$variant === 'secondary',
'border-b-black': selected && $variant === 'secondary',
},
className
)}
>
{title}
</a>
66 changes: 66 additions & 0 deletions packages/core/src/lib/tabs-bar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<!--
@component
A nav container with optional variant: 'primary' | 'secondary'
```svelte
<TabsBar variant='secondary'>
<Tab
title='The first tab'
href="#first"
isSelected='true'
/>
<Tab
title='The second tab'
href="#second"
isSelected='false'
/>
<Tab
title='The third tab'
href="#third"
isSelected='false'
/>
</TabsBar>
```
-->
<svelte:options immutable />

<script context="module">
export const CONTEXT_KEY = Symbol('tabs-bar-context');
</script>

<script lang="ts">
import { setContext } from 'svelte';
import cx from 'classnames';
import type { HTMLAttributes } from 'svelte/elements';
import { writable } from 'svelte/store';
interface $$Props extends HTMLAttributes<HTMLElement> {
variant?: 'primary' | 'secondary';
class?: string;
}
/** The tab style variant */
export let variant: $$Props['variant'] = 'primary';
const context = writable<$$Props['variant']>(variant);
$: context.set(variant);
setContext(CONTEXT_KEY, context);
let className = '';
export { className as class };
</script>

<nav
{...$$restProps}
class={cx(
{
'h-10 bg-medium tracking-wide sm:px-2': variant === 'primary',
},
className,
'flex items-center font-roboto-mono'
)}
>
<slot />
</nav>
64 changes: 0 additions & 64 deletions packages/core/src/lib/tabs.svelte

This file was deleted.

51 changes: 36 additions & 15 deletions packages/core/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
Pill,
Switch,
Radio,
Tabs,
TabsBar,
Tab,
Tooltip,
TooltipContainer,
TooltipTarget,
Expand Down Expand Up @@ -1601,21 +1602,41 @@ const onHoverDelayMsInput = (event: Event) => {

<!-- Tabs -->
<h1 class="text-2xl">Tabs</h1>
<div class="flex gap-4">
<Tabs
tabs={['Tab 1', 'Tab 2', 'Tab 3']}
selected="Tab 1"
/>

<Tabs
tabs={['Tab 1', 'Tab 2', 'Tab 3']}
selected="Tab 2"
/>
<div class="flex flex-col gap-4">
<TabsBar aria-label="Tab example one">
<Tab
title="The first tab"
href="#first"
selected
/>
<Tab
title="The second tab"
href="#second"
/>
<Tab
title="The third tab"
href="#third"
/>
</TabsBar>

<Tabs
tabs={['Tab 1', 'Tab 2']}
selected="Tab 2"
/>
<TabsBar
aria-label="Tab example two"
variant="secondary"
>
<Tab
title="The first tab"
href="#first"
selected
/>
<Tab
title="The second tab"
href="#second"
/>
<Tab
title="The third tab"
href="#third"
/>
</TabsBar>
</div>

<!-- Toast Banner -->
Expand Down
16 changes: 0 additions & 16 deletions packages/storybook/src/stories/tabs.mdx

This file was deleted.

Loading

0 comments on commit 4d6c6c6

Please sign in to comment.