Skip to content

Commit 291e15e

Browse files
CopilotalexprudhommeCopilotdeveloper-experience-bot[bot]
authored
refactor(atomic): migrate atomic-tab to Lit (#6603)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: alexprudhomme <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: developer-experience-bot[bot] <91079284+developer-experience-bot[bot]@users.noreply.github.com>
1 parent 7918ede commit 291e15e

File tree

11 files changed

+238
-81
lines changed

11 files changed

+238
-81
lines changed

packages/atomic-react/src/components/search/components.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
AtomicSearchBoxRecentQueries as LitAtomicSearchBoxRecentQueries,
4040
AtomicSearchInterface as LitAtomicSearchInterface,
4141
AtomicSearchLayout as LitAtomicSearchLayout,
42+
AtomicTab as LitAtomicTab,
4243
AtomicText as LitAtomicText,
4344
} from '@coveo/atomic/components';
4445
import {createComponent} from '@lit/react';
@@ -284,6 +285,12 @@ export const AtomicSearchLayout = createComponent({
284285
elementClass: LitAtomicSearchLayout,
285286
});
286287

288+
export const AtomicTab = createComponent({
289+
tagName: 'atomic-tab',
290+
react: React,
291+
elementClass: LitAtomicTab,
292+
});
293+
287294
export const AtomicText = createComponent({
288295
tagName: 'atomic-text',
289296
react: React,

packages/atomic/src/components.d.ts

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1918,24 +1918,6 @@ export namespace Components {
19181918
"side": 'left' | 'right';
19191919
"suggestion": SearchBoxSuggestionElement;
19201920
}
1921-
/**
1922-
* The `atomic-tab` component represents an individual tab within the `atomic-tab-manager` component.
1923-
* It must be used as a child of the `atomic-tab-manager` component to function correctly.
1924-
*/
1925-
interface AtomicTab {
1926-
/**
1927-
* The [constant query expression (`cq`)](https://docs.coveo.com/en/2830/searching-with-coveo/about-the-query-expression#constant-query-expression-cq) to apply when the tab is the active one.
1928-
*/
1929-
"expression": string;
1930-
/**
1931-
* The label to display on the tab.
1932-
*/
1933-
"label": string;
1934-
/**
1935-
* The internal name of the atomic tab.
1936-
*/
1937-
"name": string;
1938-
}
19391921
interface AtomicTabBar {
19401922
}
19411923
interface AtomicTabButton {
@@ -3152,16 +3134,6 @@ declare global {
31523134
prototype: HTMLAtomicSuggestionRendererElement;
31533135
new (): HTMLAtomicSuggestionRendererElement;
31543136
};
3155-
/**
3156-
* The `atomic-tab` component represents an individual tab within the `atomic-tab-manager` component.
3157-
* It must be used as a child of the `atomic-tab-manager` component to function correctly.
3158-
*/
3159-
interface HTMLAtomicTabElement extends Components.AtomicTab, HTMLStencilElement {
3160-
}
3161-
var HTMLAtomicTabElement: {
3162-
prototype: HTMLAtomicTabElement;
3163-
new (): HTMLAtomicTabElement;
3164-
};
31653137
interface HTMLAtomicTabBarElement extends Components.AtomicTabBar, HTMLStencilElement {
31663138
}
31673139
var HTMLAtomicTabBarElement: {
@@ -3320,7 +3292,6 @@ declare global {
33203292
"atomic-sort-expression": HTMLAtomicSortExpressionElement;
33213293
"atomic-stencil-facet-date-input": HTMLAtomicStencilFacetDateInputElement;
33223294
"atomic-suggestion-renderer": HTMLAtomicSuggestionRendererElement;
3323-
"atomic-tab": HTMLAtomicTabElement;
33243295
"atomic-tab-bar": HTMLAtomicTabBarElement;
33253296
"atomic-tab-button": HTMLAtomicTabButtonElement;
33263297
"atomic-tab-manager": HTMLAtomicTabManagerElement;
@@ -5184,24 +5155,6 @@ declare namespace LocalJSX {
51845155
"side": 'left' | 'right';
51855156
"suggestion": SearchBoxSuggestionElement;
51865157
}
5187-
/**
5188-
* The `atomic-tab` component represents an individual tab within the `atomic-tab-manager` component.
5189-
* It must be used as a child of the `atomic-tab-manager` component to function correctly.
5190-
*/
5191-
interface AtomicTab {
5192-
/**
5193-
* The [constant query expression (`cq`)](https://docs.coveo.com/en/2830/searching-with-coveo/about-the-query-expression#constant-query-expression-cq) to apply when the tab is the active one.
5194-
*/
5195-
"expression"?: string;
5196-
/**
5197-
* The label to display on the tab.
5198-
*/
5199-
"label": string;
5200-
/**
5201-
* The internal name of the atomic tab.
5202-
*/
5203-
"name": string;
5204-
}
52055158
interface AtomicTabBar {
52065159
}
52075160
interface AtomicTabButton {
@@ -5424,7 +5377,6 @@ declare namespace LocalJSX {
54245377
"atomic-sort-expression": AtomicSortExpression;
54255378
"atomic-stencil-facet-date-input": AtomicStencilFacetDateInput;
54265379
"atomic-suggestion-renderer": AtomicSuggestionRenderer;
5427-
"atomic-tab": AtomicTab;
54285380
"atomic-tab-bar": AtomicTabBar;
54295381
"atomic-tab-button": AtomicTabButton;
54305382
"atomic-tab-manager": AtomicTabManager;
@@ -5781,11 +5733,6 @@ declare module "@stencil/core" {
57815733
* use native Elements.
57825734
*/
57835735
"atomic-suggestion-renderer": LocalJSX.AtomicSuggestionRenderer & JSXBase.HTMLAttributes<HTMLAtomicSuggestionRendererElement>;
5784-
/**
5785-
* The `atomic-tab` component represents an individual tab within the `atomic-tab-manager` component.
5786-
* It must be used as a child of the `atomic-tab-manager` component to function correctly.
5787-
*/
5788-
"atomic-tab": LocalJSX.AtomicTab & JSXBase.HTMLAttributes<HTMLAtomicTabElement>;
57895736
"atomic-tab-bar": LocalJSX.AtomicTabBar & JSXBase.HTMLAttributes<HTMLAtomicTabBarElement>;
57905737
"atomic-tab-button": LocalJSX.AtomicTabButton & JSXBase.HTMLAttributes<HTMLAtomicTabButtonElement>;
57915738
/**

packages/atomic/src/components/common/tabs/tab-button.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class AtomicTabButton {
3131
}
3232

3333
private get activeTabTextClass() {
34-
return this.active ? '' : 'text-neutral-dark hover:text-primary-light';
34+
return this.active ? '' : 'text-neutral-dark';
3535
}
3636

3737
render() {
@@ -44,7 +44,7 @@ export class AtomicTabButton {
4444
part={'button-container' + (this.active ? '-active' : '')}
4545
>
4646
<Button
47-
class={`w-full truncate px-2 pb-1 text-xl sm:px-6 ${this.activeTabTextClass}`}
47+
class={`w-full truncate px-2 pb-1 text-xl sm:px-6 hover:text-primary ${this.activeTabTextClass}`}
4848
part={'tab-button' + (this.active ? '-active' : '')}
4949
onClick={this.select}
5050
style="text-transparent"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Meta } from '@storybook/addon-docs/blocks';
2+
import * as AtomicTabStories from './atomic-tab.new.stories';
3+
import { AtomicDocTemplate } from '../../../../storybook-utils/documentation/atomic-doc-template';
4+
5+
6+
<Meta of={AtomicTabStories} />
7+
8+
<AtomicDocTemplate
9+
stories={AtomicTabStories}
10+
githubPath="search/atomic-tab/atomic-tab.ts"
11+
tagName="atomic-tab"
12+
className="AtomicTab"
13+
>
14+
15+
The `atomic-tab` component must be used as a child of the `atomic-tab-manager` component to function correctly.
16+
17+
This component is not used standalone. It must be a child of `atomic-tab-manager`.
18+
19+
See the [atomic-tab-manager documentation](?path=/docs/atomic-tab-manager--docs) for additional usage examples.
20+
21+
```html
22+
<atomic-search-interface>
23+
<!-- other components -->
24+
25+
<atomic-tab-manager>
26+
<atomic-tab name="all" label="All"></atomic-tab>
27+
<atomic-tab name="images" label="Images" expression="@objecttype==Image"></atomic-tab>
28+
<atomic-tab name="articles" label="Articles" expression="@objecttype==Article"></atomic-tab>
29+
</atomic-tab-manager>
30+
31+
<!-- other components -->
32+
</atomic-search-interface>
33+
```
34+
35+
## Related Components
36+
37+
- [atomic-tab-manager](?path=/docs/atomic-tab-manager--docs): Manages a collection of tabs and allows users to switch between them
38+
39+
</AtomicDocTemplate>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type {Meta, StoryObj as Story} from '@storybook/web-components-vite';
2+
import {getStorybookHelpers} from '@wc-toolkit/storybook-helpers';
3+
import {html} from 'lit';
4+
import {parameters} from '@/storybook-utils/common/common-meta-parameters';
5+
import {wrapInSearchInterface} from '@/storybook-utils/search/search-interface-wrapper';
6+
7+
const {decorator, play} = wrapInSearchInterface();
8+
9+
const {events, argTypes} = getStorybookHelpers('atomic-tab', {
10+
excludeCategories: ['methods'],
11+
});
12+
13+
const meta: Meta = {
14+
component: 'atomic-tab',
15+
title: 'Search/Tab',
16+
id: 'atomic-tab',
17+
render: () => html`<atomic-tab-manager>
18+
<atomic-tab
19+
label="All"
20+
name="all"
21+
></atomic-tab>
22+
<atomic-tab
23+
label="Images"
24+
name="images"
25+
></atomic-tab>
26+
<atomic-tab
27+
label="Articles"
28+
name="articles"
29+
></atomic-tab>
30+
</atomic-tab-manager>`,
31+
decorators: [decorator],
32+
parameters: {
33+
...parameters,
34+
actions: {
35+
handles: events,
36+
},
37+
},
38+
argTypes,
39+
play,
40+
};
41+
42+
export default meta;
43+
44+
export const Default: Story = {};
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import {html} from 'lit';
2+
import {describe, expect, it} from 'vitest';
3+
import {fixture} from '@/vitest-utils/testing-helpers/fixture';
4+
import type {AtomicTab} from './atomic-tab';
5+
import './atomic-tab';
6+
7+
describe('atomic-tab', () => {
8+
const renderTab = async ({
9+
label = 'Test Tab',
10+
name = 'test-tab',
11+
expression = '',
12+
} = {}) => {
13+
const element = await fixture<AtomicTab>(html`
14+
<atomic-tab
15+
label="${label}"
16+
name="${name}"
17+
expression="${expression}"
18+
></atomic-tab>
19+
`);
20+
21+
return {element};
22+
};
23+
24+
describe('when rendering with valid props', () => {
25+
it('should render successfully with required props', async () => {
26+
const {element} = await renderTab();
27+
expect(element).toBeInTheDocument();
28+
});
29+
30+
it('should render successfully with all props', async () => {
31+
const {element} = await renderTab({
32+
label: 'My Tab',
33+
name: 'my-tab',
34+
expression: '@source==MySource',
35+
});
36+
expect(element).toBeInTheDocument();
37+
});
38+
39+
it('should reflect label property to attribute', async () => {
40+
const {element} = await renderTab({label: 'Custom Label'});
41+
expect(element.getAttribute('label')).toBe('Custom Label');
42+
});
43+
44+
it('should reflect name property to attribute', async () => {
45+
const {element} = await renderTab({name: 'custom-name'});
46+
expect(element.getAttribute('name')).toBe('custom-name');
47+
});
48+
49+
it('should reflect expression property to attribute', async () => {
50+
const {element} = await renderTab({expression: '@source==Test'});
51+
expect(element.getAttribute('expression')).toBe('@source==Test');
52+
});
53+
54+
it('should have empty expression by default', async () => {
55+
const {element} = await renderTab();
56+
expect(element.expression).toBe('');
57+
});
58+
59+
it('should allow updating properties', async () => {
60+
const {element} = await renderTab();
61+
62+
element.label = 'Updated Label';
63+
element.name = 'updated-name';
64+
element.expression = '@updated==true';
65+
await element.updateComplete;
66+
67+
expect(element.label).toBe('Updated Label');
68+
expect(element.name).toBe('updated-name');
69+
expect(element.expression).toBe('@updated==true');
70+
});
71+
});
72+
73+
describe('when rendering slots', () => {
74+
it('should render default slot content', async () => {
75+
const element = await fixture<AtomicTab>(html`
76+
<atomic-tab label="Test" name="test">
77+
<div id="slot-content">Slot Content</div>
78+
</atomic-tab>
79+
`);
80+
81+
const slotContent = element.querySelector('#slot-content');
82+
expect(slotContent).toBeInTheDocument();
83+
expect(slotContent?.textContent).toBe('Slot Content');
84+
});
85+
});
86+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {Schema, StringValue} from '@coveo/bueno';
2+
import {html, LitElement} from 'lit';
3+
import {customElement, property, state} from 'lit/decorators.js';
4+
import {ValidatePropsController} from '@/src/components/common/validate-props-controller/validate-props-controller';
5+
import {LightDomMixin} from '@/src/mixins/light-dom';
6+
7+
/**
8+
* The `atomic-tab` component represents an individual tab within the `atomic-tab-manager` component.
9+
* It must be used as a child of the `atomic-tab-manager` component to function correctly.
10+
*
11+
* @slot default - The default slot for any additional content within the tab (rarely used).
12+
*/
13+
@customElement('atomic-tab')
14+
export class AtomicTab extends LightDomMixin(LitElement) {
15+
/**
16+
* The label to display on the tab.
17+
*/
18+
@property({type: String, reflect: true}) label!: string;
19+
20+
/**
21+
* The internal name of the atomic tab.
22+
*/
23+
@property({type: String, reflect: true}) name!: string;
24+
25+
/**
26+
* The [constant query expression (`cq`)](https://docs.coveo.com/en/2830/searching-with-coveo/about-the-query-expression#constant-query-expression-cq) to apply when the tab is the active one.
27+
*/
28+
@property({type: String, reflect: true}) public expression: string = '';
29+
30+
@state() public error!: Error;
31+
32+
constructor() {
33+
super();
34+
35+
new ValidatePropsController(
36+
this,
37+
() => ({
38+
label: this.label,
39+
name: this.name,
40+
}),
41+
new Schema({
42+
label: new StringValue({required: true, emptyAllowed: false}),
43+
name: new StringValue({required: true, emptyAllowed: false}),
44+
})
45+
);
46+
}
47+
48+
render() {
49+
return html`<slot></slot>`;
50+
}
51+
}
52+
53+
declare global {
54+
interface HTMLElementTagNameMap {
55+
'atomic-tab': AtomicTab;
56+
}
57+
}

packages/atomic/src/components/search/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ export {AtomicSearchBoxQuerySuggestions} from './atomic-search-box-query-suggest
3333
export {AtomicSearchBoxRecentQueries} from './atomic-search-box-recent-queries/atomic-search-box-recent-queries.js';
3434
export {AtomicSearchInterface} from './atomic-search-interface/atomic-search-interface.js';
3535
export {AtomicSearchLayout} from './atomic-search-layout/atomic-search-layout.js';
36+
export {AtomicTab} from './atomic-tab/atomic-tab.js';
3637
export {AtomicText} from './atomic-text/atomic-text.js';

packages/atomic/src/components/search/lazy-index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export default {
9090
await import('./atomic-search-interface/atomic-search-interface.js'),
9191
'atomic-search-layout': async () =>
9292
await import('./atomic-search-layout/atomic-search-layout.js'),
93+
'atomic-tab': async () => await import('./atomic-tab/atomic-tab.js'),
9394
'atomic-text': async () => await import('./atomic-text/atomic-text.js'),
9495
} as Record<string, () => Promise<unknown>>;
9596

0 commit comments

Comments
 (0)