Skip to content
Draft
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
37 changes: 0 additions & 37 deletions packages/atomic/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1664,20 +1664,6 @@ export namespace Components {
}
interface AtomicTabBar {
}
interface AtomicTabButton {
/**
* Whether the tab button is active.
*/
"active": boolean;
/**
* The label to display on the tab button.
*/
"label": string;
/**
* Click handler for the tab button.
*/
"select": () => void;
}
interface AtomicTabPopover {
"closePopoverOnFocusOut": (event: FocusEvent) => Promise<void>;
"setButtonVisibility": (isVisible: boolean) => Promise<void>;
Expand Down Expand Up @@ -2738,12 +2724,6 @@ declare global {
prototype: HTMLAtomicTabBarElement;
new (): HTMLAtomicTabBarElement;
};
interface HTMLAtomicTabButtonElement extends Components.AtomicTabButton, HTMLStencilElement {
}
var HTMLAtomicTabButtonElement: {
prototype: HTMLAtomicTabButtonElement;
new (): HTMLAtomicTabButtonElement;
};
interface HTMLAtomicTabPopoverElement extends Components.AtomicTabPopover, HTMLStencilElement {
}
var HTMLAtomicTabPopoverElement: {
Expand Down Expand Up @@ -2867,7 +2847,6 @@ declare global {
"atomic-stencil-facet-date-input": HTMLAtomicStencilFacetDateInputElement;
"atomic-suggestion-renderer": HTMLAtomicSuggestionRendererElement;
"atomic-tab-bar": HTMLAtomicTabBarElement;
"atomic-tab-button": HTMLAtomicTabButtonElement;
"atomic-tab-popover": HTMLAtomicTabPopoverElement;
"atomic-table-element": HTMLAtomicTableElementElement;
"atomic-timeframe": HTMLAtomicTimeframeElement;
Expand Down Expand Up @@ -4474,20 +4453,6 @@ declare namespace LocalJSX {
}
interface AtomicTabBar {
}
interface AtomicTabButton {
/**
* Whether the tab button is active.
*/
"active"?: boolean;
/**
* The label to display on the tab button.
*/
"label": string;
/**
* Click handler for the tab button.
*/
"select": () => void;
}
interface AtomicTabPopover {
}
/**
Expand Down Expand Up @@ -4671,7 +4636,6 @@ declare namespace LocalJSX {
"atomic-stencil-facet-date-input": AtomicStencilFacetDateInput;
"atomic-suggestion-renderer": AtomicSuggestionRenderer;
"atomic-tab-bar": AtomicTabBar;
"atomic-tab-button": AtomicTabButton;
"atomic-tab-popover": AtomicTabPopover;
"atomic-table-element": AtomicTableElement;
"atomic-timeframe": AtomicTimeframe;
Expand Down Expand Up @@ -4956,7 +4920,6 @@ declare module "@stencil/core" {
*/
"atomic-suggestion-renderer": LocalJSX.AtomicSuggestionRenderer & JSXBase.HTMLAttributes<HTMLAtomicSuggestionRendererElement>;
"atomic-tab-bar": LocalJSX.AtomicTabBar & JSXBase.HTMLAttributes<HTMLAtomicTabBarElement>;
"atomic-tab-button": LocalJSX.AtomicTabButton & JSXBase.HTMLAttributes<HTMLAtomicTabButtonElement>;
"atomic-tab-popover": LocalJSX.AtomicTabPopover & JSXBase.HTMLAttributes<HTMLAtomicTabPopoverElement>;
/**
* The `atomic-table-element` element defines a table column in a result list.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {html} from 'lit';
import {describe, expect, it, vi} from 'vitest';
import {page} from 'vitest/browser';
import {fixture} from '@/vitest-utils/testing-helpers/fixture';
import './atomic-tab-button';
import type {AtomicTabButton} from './atomic-tab-button';

describe('atomic-tab-button', () => {
const renderTabButton = async (
props: Partial<{
label: string;
active: boolean;
select: () => void;
}> = {}
) => {
const element = await fixture<AtomicTabButton>(
html`<atomic-tab-button
.label=${props.label ?? 'Test Tab'}
.active=${props.active ?? false}
.select=${props.select ?? vi.fn()}
></atomic-tab-button>`
);

const getButton = () =>
element.shadowRoot?.querySelector('button') as HTMLButtonElement;

return {
element,
button: getButton(),
locators: {
button: page.getByRole('button'),
},
};
};

it('should render in the document', async () => {
const {element} = await renderTabButton();
await expect.element(element).toBeInTheDocument();
});

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

it('should render with listitem role on host element', async () => {
const {element} = await renderTabButton();
expect(element).toHaveAttribute('role', 'listitem');
});

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

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

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

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

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

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

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

it('should not have text-neutral-dark class on button', async () => {
const {button} = await renderTabButton({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 renderTabButton({select: selectFn});

button.click();

expect(selectFn).toHaveBeenCalledOnce();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {html, LitElement, type PropertyValues} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {renderButton} from '@/src/components/common/button';
import {errorGuard} from '@/src/decorators/error-guard';
import type {LitElementWithError} from '@/src/decorators/types';
import {withTailwindStyles} from '@/src/decorators/with-tailwind-styles';

/**
* The `atomic-tab-button` component renders a tab button for use in tab interfaces.
*
* @internal
* @part button-container - The container for the tab button when inactive.
* @part button-container-active - The container for the tab button when active.
* @part tab-button - The tab button itself when inactive.
* @part tab-button-active - The tab button itself when active.
*/
@customElement('atomic-tab-button')
@withTailwindStyles
export class AtomicTabButton extends LitElement implements LitElementWithError {
error!: Error;
/**
* The label to display on the tab button.
*/
@property({type: String}) label!: string;

/**
* Whether the tab button is active.
*/
@property({type: Boolean}) active = false;

/**
* A click handler for the tab button.
*/
@property({attribute: false}) select!: () => void;

connectedCallback() {
super.connectedCallback();
this.setAttribute('role', 'listitem');
this.updateHostClasses();
}

updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('active')) {
this.setAttribute('aria-current', this.active ? 'true' : 'false');
this.updateHostClasses();
}
}

private updateHostClasses() {
this.classList.toggle('relative', this.active);
this.classList.toggle('after:block', this.active);
this.classList.toggle('after:w-full', this.active);
this.classList.toggle('after:h-1', this.active);
this.classList.toggle('after:absolute', this.active);
this.classList.toggle('after:-bottom-0.5', this.active);
this.classList.toggle('after:bg-primary', this.active);
this.classList.toggle('after:rounded', this.active);
}

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

return html`
${renderButton({
props: {
style: 'text-transparent',
class: buttonClasses,
part: this.active ? 'tab-button-active' : 'tab-button',
onClick: this.select,
},
})(html`${this.label}`)}
`;
}
}

declare global {
interface HTMLElementTagNameMap {
'atomic-tab-button': AtomicTabButton;
}
}
1 change: 1 addition & 0 deletions packages/atomic/src/components/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export {AtomicIcon} from './atomic-icon/atomic-icon.js';
export {AtomicLayoutSection} from './atomic-layout-section/atomic-layout-section.js';
export {AtomicModal} from './atomic-modal/atomic-modal.js';
export {AtomicNumericRange} from './atomic-numeric-range/atomic-numeric-range.js';
export {AtomicTabButton} from './atomic-tab-button/atomic-tab-button.js';
2 changes: 2 additions & 0 deletions packages/atomic/src/components/common/lazy-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export default {
'atomic-modal': async () => await import('./atomic-modal/atomic-modal.js'),
'atomic-numeric-range': async () =>
await import('./atomic-numeric-range/atomic-numeric-range.js'),
'atomic-tab-button': async () =>
await import('./atomic-tab-button/atomic-tab-button.js'),
} as Record<string, () => Promise<unknown>>;

export type * from './index.js';

This file was deleted.

This file was deleted.

Loading
Loading