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
37 changes: 0 additions & 37 deletions packages/atomic/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1102,20 +1102,6 @@ export namespace Components {
"side": 'left' | 'right';
"suggestion": SearchBoxSuggestionElement;
}
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;
}
/**
* A facet is a list of values for a certain field occurring in the results.
* An `atomic-timeframe-facet` displays a facet of the results for the current query as date intervals.
Expand Down Expand Up @@ -1887,12 +1873,6 @@ declare global {
prototype: HTMLAtomicSuggestionRendererElement;
new (): HTMLAtomicSuggestionRendererElement;
};
interface HTMLAtomicTabButtonElement extends Components.AtomicTabButton, HTMLStencilElement {
}
var HTMLAtomicTabButtonElement: {
prototype: HTMLAtomicTabButtonElement;
new (): HTMLAtomicTabButtonElement;
};
/**
* A facet is a list of values for a certain field occurring in the results.
* An `atomic-timeframe-facet` displays a facet of the results for the current query as date intervals.
Expand Down Expand Up @@ -1965,7 +1945,6 @@ declare global {
"atomic-smart-snippet-suggestions": HTMLAtomicSmartSnippetSuggestionsElement;
"atomic-stencil-facet-date-input": HTMLAtomicStencilFacetDateInputElement;
"atomic-suggestion-renderer": HTMLAtomicSuggestionRendererElement;
"atomic-tab-button": HTMLAtomicTabButtonElement;
"atomic-timeframe-facet": HTMLAtomicTimeframeFacetElement;
}
}
Expand Down Expand Up @@ -3015,20 +2994,6 @@ declare namespace LocalJSX {
"side": 'left' | 'right';
"suggestion": SearchBoxSuggestionElement;
}
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;
}
/**
* A facet is a list of values for a certain field occurring in the results.
* An `atomic-timeframe-facet` displays a facet of the results for the current query as date intervals.
Expand Down Expand Up @@ -3153,7 +3118,6 @@ declare namespace LocalJSX {
"atomic-smart-snippet-suggestions": AtomicSmartSnippetSuggestions;
"atomic-stencil-facet-date-input": AtomicStencilFacetDateInput;
"atomic-suggestion-renderer": AtomicSuggestionRenderer;
"atomic-tab-button": AtomicTabButton;
"atomic-timeframe-facet": AtomicTimeframeFacet;
}
}
Expand Down Expand Up @@ -3331,7 +3295,6 @@ declare module "@stencil/core" {
* use native Elements.
*/
"atomic-suggestion-renderer": LocalJSX.AtomicSuggestionRenderer & JSXBase.HTMLAttributes<HTMLAtomicSuggestionRendererElement>;
"atomic-tab-button": LocalJSX.AtomicTabButton & JSXBase.HTMLAttributes<HTMLAtomicTabButtonElement>;
/**
* A facet is a list of values for a certain field occurring in the results.
* An `atomic-timeframe-facet` displays a facet of the results for the current query as date intervals.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {html} from 'lit';
import {describe, expect, it, vi} from 'vitest';
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>`
);

return {
element,
button: element.querySelector('button'),
};
};

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

it('should render the label text', async () => {
const {button} = await renderTabButton({label: 'Products'});
expect(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,96 @@
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';
import {LightDomMixin} from '@/src/mixins/light-dom';

/**
* 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 LightDomMixin(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 @@ -10,5 +10,6 @@ export {AtomicModal} from './atomic-modal/atomic-modal.js';
export {AtomicNumericRange} from './atomic-numeric-range/atomic-numeric-range.js';
export {AtomicSmartSnippetExpandableAnswer} from './atomic-smart-snippet-expandable-answer/atomic-smart-snippet-expandable-answer.js';
export {AtomicTabBar} from './atomic-tab-bar/atomic-tab-bar.js';
export {AtomicTabButton} from './atomic-tab-button/atomic-tab-button.js';
export {AtomicTabPopover} from './atomic-tab-popover/atomic-tab-popover.js';
export {AtomicTimeframe} from './atomic-timeframe/atomic-timeframe.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 @@ -22,6 +22,8 @@ export default {
),
'atomic-tab-bar': async () =>
await import('./atomic-tab-bar/atomic-tab-bar.js'),
'atomic-tab-button': async () =>
await import('./atomic-tab-button/atomic-tab-button.js'),
'atomic-tab-popover': async () =>
await import('./atomic-tab-popover/atomic-tab-popover.js'),
'atomic-timeframe': async () =>
Expand Down

This file was deleted.

This file was deleted.

Loading
Loading