Skip to content
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
File renamed without changes.
109 changes: 109 additions & 0 deletions packages/atomic/src/components/common/tabs/tab-button.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {html} from 'lit';
import {describe, expect, it, vi} from 'vitest';
import {renderFunctionFixture} from '@/vitest-utils/testing-helpers/fixture';
import {renderTabButton, type TabButtonProps} from './tab-button';

describe('#renderTabButton', () => {
const renderTab = async (
props: Partial<TabButtonProps> = {}
): Promise<{container: HTMLElement; button: HTMLButtonElement}> => {
const defaultProps: TabButtonProps = {
label: 'Test Tab',
active: false,
select: vi.fn(),
...props,
};

const element = await renderFunctionFixture(
html`${renderTabButton({props: defaultProps})}`
);

const buttonContainer = element.querySelector(
'[role="tab"]'
) as HTMLElement;
const button = element.querySelector('button') as HTMLButtonElement;

return {container: buttonContainer, button};
};

it('should render a tab button in the document', async () => {
const {button} = await renderTab();
expect(button).toBeInTheDocument();
});

it('should render the label text', async () => {
const {button} = await renderTab({label: 'Products'});
expect(button).toHaveTextContent('Products');
});

it('should render with listitem role on container', async () => {
const {container} = await renderTab();
expect(container).toHaveAttribute('role', 'tab');
});

describe('when active is false', () => {
it('should set aria-selected to false', async () => {
const {container} = await renderTab({active: false});
expect(container).toHaveAttribute('aria-selected', 'false');
});

it('should have button-container part', async () => {
const {container} = await renderTab({active: false});
expect(container).toHaveAttribute('part', 'button-container');
});

it('should have tab-button part on button', async () => {
const {button} = await renderTab({active: false});
expect(button).toHaveAttribute('part', 'tab-button');
});

it('should not have active indicator classes', async () => {
const {container} = await renderTab({active: false});
expect(container.className).not.toContain('after:block');
expect(container.className).not.toContain('after:bg-primary');
});

it('should have text-neutral-dark class on button', async () => {
const {button} = await renderTab({active: false});
expect(button.className).toContain('text-neutral-dark');
});
});

describe('when active is true', () => {
it('should set aria-selected to true', async () => {
const {container} = await renderTab({active: true});
expect(container).toHaveAttribute('aria-selected', 'true');
});

it('should have button-container-active part', async () => {
const {container} = await renderTab({active: true});
expect(container).toHaveAttribute('part', 'button-container-active');
});

it('should have tab-button-active part on button', async () => {
const {button} = await renderTab({active: true});
expect(button).toHaveAttribute('part', 'tab-button-active');
});

it('should have active indicator classes', async () => {
const {container} = await renderTab({active: true});
expect(container.className).toContain('after:block');
expect(container.className).toContain('after:bg-primary');
expect(container.className).toContain('relative');
});

it('should not have text-neutral-dark class on button', async () => {
const {button} = await renderTab({active: true});
expect(button.className).not.toContain('text-neutral-dark');
});
});

it('should call select when button is clicked', async () => {
const selectFn = vi.fn();
const {button} = await renderTab({select: selectFn});

button.click();

expect(selectFn).toHaveBeenCalledOnce();
});
});
57 changes: 57 additions & 0 deletions packages/atomic/src/components/common/tabs/tab-button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {html} from 'lit';
import {renderButton} from '@/src/components/common/button';
import {multiClassMap, tw} from '@/src/directives/multi-class-map';
import type {FunctionalComponent} from '@/src/utils/functional-component-utils';

export interface TabButtonProps {
/**
* The label to display on the tab button.
*/
label: string;
/**
* Whether the tab button is active.
*/
active?: boolean;
/**
* Click handler for the tab button.
*/
select: () => void;
}

export const renderTabButton: FunctionalComponent<TabButtonProps> = ({
props,
}) => {
const containerClasses = tw({
'relative after:block after:w-full after:h-1 after:absolute after:-bottom-0.5 after:bg-primary after:rounded':
Boolean(props.active),
});

const buttonClassNames = [
'w-full',
'truncate',
'px-2',
'pb-1',
'text-xl',
'sm:px-6',
'hover:text-primary',
!props.active && 'text-neutral-dark',
]
.filter(Boolean)
.join(' ');

return html`<div
role="tab"
class=${multiClassMap(containerClasses)}
aria-selected=${props.active ? 'true' : 'false'}
part=${props.active ? 'button-container-active' : 'button-container'}
>
${renderButton({
props: {
style: 'text-transparent',
class: buttonClassNames,
part: props.active ? 'tab-button-active' : 'tab-button',
onClick: props.select,
},
})(html`${props.label}`)}
</div>`;
};
Loading