diff --git a/packages/atomic-react/src/components/search/components.ts b/packages/atomic-react/src/components/search/components.ts index ce22d348b39..28b1f1ab923 100644 --- a/packages/atomic-react/src/components/search/components.ts +++ b/packages/atomic-react/src/components/search/components.ts @@ -49,6 +49,7 @@ import { AtomicSortDropdown as LitAtomicSortDropdown, AtomicSortExpression as LitAtomicSortExpression, AtomicTab as LitAtomicTab, + AtomicTabManager as LitAtomicTabManager, AtomicText as LitAtomicText, } from '@coveo/atomic/components'; import {createComponent} from '@lit/react'; @@ -354,6 +355,12 @@ export const AtomicTab = createComponent({ elementClass: LitAtomicTab, }); +export const AtomicTabManager = createComponent({ + tagName: 'atomic-tab-manager', + react: React, + elementClass: LitAtomicTabManager, +}); + export const AtomicText = createComponent({ tagName: 'atomic-text', react: React, diff --git a/packages/atomic/.gitignore b/packages/atomic/.gitignore index 9af7ac35c37..5e6e0e2e46f 100644 --- a/packages/atomic/.gitignore +++ b/packages/atomic/.gitignore @@ -44,5 +44,3 @@ dist-storybook/ /dev/bueno/ /dev/headless/ -# Generated Tailwind CSS files -*.tw.css.ts diff --git a/packages/atomic/src/components.d.ts b/packages/atomic/src/components.d.ts index 896c00da29e..3953b8cd09d 100644 --- a/packages/atomic/src/components.d.ts +++ b/packages/atomic/src/components.d.ts @@ -1794,17 +1794,6 @@ export namespace Components { */ "select": () => void; } - /** - * The `atomic-tab-manager` component manages a collection of tabs, - * allowing users to switch between them. Each child `atomic-tab` represents an - * individual tab within the manager. - */ - interface AtomicTabManager { - /** - * Whether to clear the filters when the active tab changes. - */ - "clearFiltersOnTabChange"?: boolean; - } interface AtomicTabPopover { "closePopoverOnFocusOut": (event: FocusEvent) => Promise; "setButtonVisibility": (isVisible: boolean) => Promise; @@ -2911,17 +2900,6 @@ declare global { prototype: HTMLAtomicTabButtonElement; new (): HTMLAtomicTabButtonElement; }; - /** - * The `atomic-tab-manager` component manages a collection of tabs, - * allowing users to switch between them. Each child `atomic-tab` represents an - * individual tab within the manager. - */ - interface HTMLAtomicTabManagerElement extends Components.AtomicTabManager, HTMLStencilElement { - } - var HTMLAtomicTabManagerElement: { - prototype: HTMLAtomicTabManagerElement; - new (): HTMLAtomicTabManagerElement; - }; interface HTMLAtomicTabPopoverElement extends Components.AtomicTabPopover, HTMLStencilElement { } var HTMLAtomicTabPopoverElement: { @@ -3050,7 +3028,6 @@ declare global { "atomic-suggestion-renderer": HTMLAtomicSuggestionRendererElement; "atomic-tab-bar": HTMLAtomicTabBarElement; "atomic-tab-button": HTMLAtomicTabButtonElement; - "atomic-tab-manager": HTMLAtomicTabManagerElement; "atomic-tab-popover": HTMLAtomicTabPopoverElement; "atomic-table-element": HTMLAtomicTableElementElement; "atomic-timeframe": HTMLAtomicTimeframeElement; @@ -4787,17 +4764,6 @@ declare namespace LocalJSX { */ "select": () => void; } - /** - * The `atomic-tab-manager` component manages a collection of tabs, - * allowing users to switch between them. Each child `atomic-tab` represents an - * individual tab within the manager. - */ - interface AtomicTabManager { - /** - * Whether to clear the filters when the active tab changes. - */ - "clearFiltersOnTabChange"?: boolean; - } interface AtomicTabPopover { } /** @@ -4986,7 +4952,6 @@ declare namespace LocalJSX { "atomic-suggestion-renderer": AtomicSuggestionRenderer; "atomic-tab-bar": AtomicTabBar; "atomic-tab-button": AtomicTabButton; - "atomic-tab-manager": AtomicTabManager; "atomic-tab-popover": AtomicTabPopover; "atomic-table-element": AtomicTableElement; "atomic-timeframe": AtomicTimeframe; @@ -5292,12 +5257,6 @@ declare module "@stencil/core" { "atomic-suggestion-renderer": LocalJSX.AtomicSuggestionRenderer & JSXBase.HTMLAttributes; "atomic-tab-bar": LocalJSX.AtomicTabBar & JSXBase.HTMLAttributes; "atomic-tab-button": LocalJSX.AtomicTabButton & JSXBase.HTMLAttributes; - /** - * The `atomic-tab-manager` component manages a collection of tabs, - * allowing users to switch between them. Each child `atomic-tab` represents an - * individual tab within the manager. - */ - "atomic-tab-manager": LocalJSX.AtomicTabManager & JSXBase.HTMLAttributes; "atomic-tab-popover": LocalJSX.AtomicTabPopover & JSXBase.HTMLAttributes; /** * The `atomic-table-element` element defines a table column in a result list. diff --git a/packages/atomic/src/components/common/tabs/tab-popover.tsx b/packages/atomic/src/components/common/tabs/tab-popover.tsx index f9b4bf9cd30..3397ff6f87a 100644 --- a/packages/atomic/src/components/common/tabs/tab-popover.tsx +++ b/packages/atomic/src/components/common/tabs/tab-popover.tsx @@ -183,14 +183,14 @@ export class TabPopover implements InitializableComponent { title={label} part="value-label" class={ - 'group-hover:text-primary-light group-focus:text-primary mr-1.5 truncate' + 'group-hover:text-primary group-focus:text-primary mr-1.5 truncate' } > {label} + + + +The `atomic-tab-manager` component manages a collection of tabs that allow users to filter search results based on different criteria. + +This component must contain `atomic-tab` children to function correctly. + +```html + + + + + + + + + + + +``` + + diff --git a/packages/atomic/src/components/search/atomic-tab-manager/atomic-tab-manager.new.stories.tsx b/packages/atomic/src/components/search/atomic-tab-manager/atomic-tab-manager.new.stories.tsx new file mode 100644 index 00000000000..9144f9ed55b --- /dev/null +++ b/packages/atomic/src/components/search/atomic-tab-manager/atomic-tab-manager.new.stories.tsx @@ -0,0 +1,51 @@ +import type {Meta, StoryObj as Story} from '@storybook/web-components-vite'; +import {getStorybookHelpers} from '@wc-toolkit/storybook-helpers'; +import {html} from 'lit'; +import {MockSearchApi} from '@/storybook-utils/api/search/mock'; +import {parameters} from '@/storybook-utils/common/common-meta-parameters'; +import {wrapInSearchInterface} from '@/storybook-utils/search/search-interface-wrapper'; + +const mockSearchApi = new MockSearchApi(); + +const {decorator, play} = wrapInSearchInterface(); + +const {events, argTypes} = getStorybookHelpers('atomic-tab-manager', { + excludeCategories: ['methods'], +}); + +const meta: Meta = { + component: 'atomic-tab-manager', + title: 'Search/Tab Manager', + id: 'atomic-tab-manager', + render: () => html` + + + + `, + decorators: [decorator], + parameters: { + ...parameters, + actions: { + handles: events, + }, + msw: {handlers: [...mockSearchApi.handlers]}, + }, + argTypes, + play, + beforeEach: async () => { + mockSearchApi.searchEndpoint.clear(); + }, +}; + +export default meta; + +export const Default: Story = {}; diff --git a/packages/atomic/src/components/search/atomic-tab-manager/atomic-tab-manager.spec.ts b/packages/atomic/src/components/search/atomic-tab-manager/atomic-tab-manager.spec.ts new file mode 100644 index 00000000000..a562331ab40 --- /dev/null +++ b/packages/atomic/src/components/search/atomic-tab-manager/atomic-tab-manager.spec.ts @@ -0,0 +1,175 @@ +import {buildTab, buildTabManager, type TabManagerState} from '@coveo/headless'; +import {html} from 'lit'; +import {describe, expect, it, vi} from 'vitest'; +import {renderInAtomicSearchInterface} from '@/vitest-utils/testing-helpers/fixtures/atomic/search/atomic-search-interface-fixture'; +import {buildFakeTabManager} from '@/vitest-utils/testing-helpers/fixtures/headless/search/tab-manager-controller'; +import type {AtomicTabManager} from './atomic-tab-manager'; +import './atomic-tab-manager'; +import {mockConsole} from '@/vitest-utils/testing-helpers/testing-utils/mock-console'; + +vi.mock('@coveo/headless', {spy: true}); + +describe('atomic-tab-manager', () => { + const renderTabManager = async ({ + tabManagerState, + slottedContent, + clearFiltersOnTabChange = false, + }: { + tabManagerState?: Partial; + slottedContent?: ReturnType; + clearFiltersOnTabChange?: boolean; + } = {}) => { + const fakeTabManager = buildFakeTabManager({ + activeTab: 'all', + ...tabManagerState, + }); + + mockConsole(); + + vi.mocked(buildTabManager).mockReturnValue(fakeTabManager); + + vi.mocked(buildTab).mockImplementation((_engine, options) => { + return { + state: { + isActive: options?.options?.id === fakeTabManager.state.activeTab, + }, + select: vi.fn(), + subscribe: vi.fn(), + } as never; + }); + + const {element} = await renderInAtomicSearchInterface({ + template: html` + + ${ + slottedContent ?? + html` + + + + ` + } + + `, + selector: 'atomic-tab-manager', + }); + + return {element, fakeTabManager}; + }; + + it('should be defined', () => { + const el = document.createElement('atomic-tab-manager'); + expect(el).toBeInstanceOf(HTMLElement); + }); + + describe('when rendering with valid props', () => { + it('should render successfully', async () => { + const {element} = await renderTabManager(); + expect(element).toBeInTheDocument(); + }); + + it('should have tab area part', async () => { + const {element} = await renderTabManager(); + const tabArea = element.shadowRoot?.querySelector('[part="tab-area"]'); + expect(tabArea).toBeDefined(); + }); + + it('should render tab bar', async () => { + const {element} = await renderTabManager(); + const tabBar = element.shadowRoot?.querySelector('atomic-tab-bar'); + expect(tabBar).toBeDefined(); + }); + }); + + describe('when initializing', () => { + it('should call buildTabManager with the engine', async () => { + const {element} = await renderTabManager(); + + expect(buildTabManager).toHaveBeenCalledWith(element.bindings.engine); + }); + }); + + describe('when rendering tabs', () => { + it('should have the correct number of tabs', async () => { + const {element} = await renderTabManager(); + const tabElements = element.querySelectorAll('atomic-tab'); + expect(tabElements.length).toBe(3); + }); + }); + + describe('when no atomic-tab children exist', () => { + it('should set error when no atomic-tab children', async () => { + const {element} = await renderTabManager({ + slottedContent: html``, + }); + + expect(element.error).toBeDefined(); + expect(element.error.message).toBe( + 'The "atomic-tab-manager" element requires at least one "atomic-tab" child.' + ); + }); + }); + + describe('when atomic-tab child is missing name attribute', () => { + it('should set error when atomic-tab is missing name', async () => { + const consoleErrorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + + const {element} = await renderTabManager({ + slottedContent: html` + + `, + }); + + expect(element.error).toBeDefined(); + expect(element.error.message).toBe( + 'The "name" attribute must be defined on all "atomic-tab" children.' + ); + + consoleErrorSpy.mockRestore(); + }); + }); + + describe('when clicking tabs', () => { + it('should render atomic-tab elements in the component', async () => { + const {element} = await renderTabManager(); + + // Verify atomic-tab elements exist as children (not in shadow DOM) + const tabElements = element.querySelectorAll('atomic-tab'); + expect(tabElements.length).toBe(3); + + // Verify the tab manager was initialized + expect(buildTabManager).toHaveBeenCalledWith(element.bindings.engine); + }); + }); + + describe('when localizing tab labels', () => { + it('should localize tab labels using i18n.t', async () => { + const {element} = await renderTabManager(); + + // Verify i18n.t is available on the bindings + expect(element.bindings.i18n.t).toBeDefined(); + expect(typeof element.bindings.i18n.t).toBe('function'); + + // The component uses i18n.t in its render method to localize labels + // This is verified by the fact that the component renders without errors + // and has the correct structure + expect(element).toBeInTheDocument(); + }); + }); + + describe('when clearFiltersOnTabChange is set', () => { + it('should pass clearFiltersOnTabChange to buildTab options', async () => { + await renderTabManager({ + clearFiltersOnTabChange: true, + }); + + // Verify buildTab was called with clearFiltersOnTabChange option + const buildTabCalls = vi.mocked(buildTab).mock.calls; + buildTabCalls.forEach((call) => { + expect(call[1]?.options?.clearFiltersOnTabChange).toBe(true); + }); + }); + }); +}); diff --git a/packages/atomic/src/components/search/atomic-tab-manager/atomic-tab-manager.ts b/packages/atomic/src/components/search/atomic-tab-manager/atomic-tab-manager.ts new file mode 100644 index 00000000000..2face696b92 --- /dev/null +++ b/packages/atomic/src/components/search/atomic-tab-manager/atomic-tab-manager.ts @@ -0,0 +1,145 @@ +import { + buildTab, + buildTabManager, + type Tab, + type TabManager, + type TabManagerState, +} from '@coveo/headless'; +import {type CSSResultGroup, html, LitElement} from 'lit'; +import {customElement, property, state} from 'lit/decorators.js'; +import type {Bindings} from '@/src/components/search/atomic-search-interface/atomic-search-interface'; +import {booleanConverter} from '@/src/converters/boolean-converter'; +import {bindStateToController} from '@/src/decorators/bind-state'; +import {bindingGuard} from '@/src/decorators/binding-guard'; +import {bindings} from '@/src/decorators/bindings'; +import {errorGuard} from '@/src/decorators/error-guard'; +import type {InitializableComponent} from '@/src/decorators/types'; +import {withTailwindStyles} from '@/src/decorators/with-tailwind-styles'; +import styles from './atomic-tab-manager.tw.css'; + +interface TabInfo { + label: string; + name: string; + tabController: Tab; +} + +/** + * The `atomic-tab-manager` component manages a collection of tabs, + * allowing users to switch between them. Each child `atomic-tab` represents an + * individual tab within the manager. + * + * @part button-container - The container for the tab button. + * @part button-container-active - The container for the active tab button. + * @part tab-button - The tab button. + * @part tab-button-active - The container for the active tab button. + * @part dropdown-area - The dropdown area. + * @part tab-area - The tab area. + * @part popover-button - The "More" button shown when the tabs are collapsed. + * @part value-label - The label shown on the "More" button. + * @part arrow-icon - The down chevron icon shown on the "More" button. + * @part overflow-tabs - The list of tabs shown when the "More" button is clicked. + * @part popover-tab - The individual tab buttons shown when the "More" button is clicked. + * @part backdrop - The backdrop shown when the "More" button is clicked. + * @slot (default) - The `atomic-tab` elements that represent the tabs. + */ +@customElement('atomic-tab-manager') +@bindings() +@withTailwindStyles +export class AtomicTabManager + extends LitElement + implements InitializableComponent +{ + static styles: CSSResultGroup = styles; + + public bindings!: Bindings; + public tabManager!: TabManager; + + @bindStateToController('tabManager') + @state() + private tabManagerState!: TabManagerState; + + @state() public error!: Error; + + private tabs: TabInfo[] = []; + + /** + * Whether to clear the filters when the active tab changes. + */ + @property({ + type: Boolean, + converter: booleanConverter, + attribute: 'clear-filters-on-tab-change', + }) + clearFiltersOnTabChange = false; + + public initialize() { + this.tabManager = buildTabManager(this.bindings.engine); + + const tabElements = Array.from(this.querySelectorAll('atomic-tab')); + + if (tabElements.length === 0) { + this.error = new Error( + 'The "atomic-tab-manager" element requires at least one "atomic-tab" child.' + ); + return; + } + + tabElements.forEach((tabElement) => { + if (!tabElement.name) { + this.error = new Error( + 'The "name" attribute must be defined on all "atomic-tab" children.' + ); + return; + } + const tabController = buildTab(this.bindings.engine, { + options: { + expression: tabElement.expression, + id: tabElement.name, + clearFiltersOnTabChange: this.clearFiltersOnTabChange, + }, + }); + + this.tabs.push({ + label: tabElement.label, + name: tabElement.name, + tabController, + }); + }); + } + + @bindingGuard() + @errorGuard() + render() { + return html` + +
+ ${this.tabs.map((tab) => { + const isActive = this.tabManagerState.activeTab === tab.name; + return html` { + if (!tab.tabController.state.isActive) { + tab.tabController.select(); + } + }} + >`; + })} +
+
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'atomic-tab-manager': AtomicTabManager; + } +} diff --git a/packages/atomic/src/components/search/atomic-tab-manager/atomic-tab-manager.tw.css.ts b/packages/atomic/src/components/search/atomic-tab-manager/atomic-tab-manager.tw.css.ts new file mode 100644 index 00000000000..08d40045f27 --- /dev/null +++ b/packages/atomic/src/components/search/atomic-tab-manager/atomic-tab-manager.tw.css.ts @@ -0,0 +1,21 @@ +import {css} from 'lit'; + +const styles = css` + @reference '../../../utils/tailwind.global.tw.css'; + + :host { + atomic-tab-bar::part(popover-button) { + @apply m-0 px-2 pb-1 text-left text-xl font-normal text-black sm:px-6; + } + + atomic-tab-bar::part(value-label) { + @apply font-normal; + } + + ::part(popover-tab) { + @apply font-normal; + } + } +`; + +export default styles; diff --git a/packages/atomic/src/components/search/atomic-tab-manager/e2e/atomic-tab-manager.e2e.ts b/packages/atomic/src/components/search/atomic-tab-manager/e2e/atomic-tab-manager.e2e.ts new file mode 100644 index 00000000000..9f02fdefef8 --- /dev/null +++ b/packages/atomic/src/components/search/atomic-tab-manager/e2e/atomic-tab-manager.e2e.ts @@ -0,0 +1,22 @@ +import {expect, test} from './fixture'; + +test.describe('atomic-tab-manager', () => { + test.beforeEach(async ({tabManager}) => { + await tabManager.load(); + await tabManager.hydrated.waitFor(); + }); + + test('should display tabs area', async ({tabManager}) => { + await expect(tabManager.tabArea).toBeVisible(); + }); + + test('should display tab buttons for each atomic-tab element', async ({ + tabManager, + }) => { + await expect(tabManager.tabButtons()).toHaveText([ + /All/, + /Documentation/, + /Articles/, + ]); + }); +}); diff --git a/packages/atomic/src/components/search/tabs/atomic-tab-manager/e2e/fixture.ts b/packages/atomic/src/components/search/atomic-tab-manager/e2e/fixture.ts similarity index 100% rename from packages/atomic/src/components/search/tabs/atomic-tab-manager/e2e/fixture.ts rename to packages/atomic/src/components/search/atomic-tab-manager/e2e/fixture.ts diff --git a/packages/atomic/src/components/search/tabs/atomic-tab-manager/e2e/page-object.ts b/packages/atomic/src/components/search/atomic-tab-manager/e2e/page-object.ts similarity index 94% rename from packages/atomic/src/components/search/tabs/atomic-tab-manager/e2e/page-object.ts rename to packages/atomic/src/components/search/atomic-tab-manager/e2e/page-object.ts index 09d174cc197..dd6e147bbb7 100644 --- a/packages/atomic/src/components/search/tabs/atomic-tab-manager/e2e/page-object.ts +++ b/packages/atomic/src/components/search/atomic-tab-manager/e2e/page-object.ts @@ -1,7 +1,7 @@ import type {Page} from '@playwright/test'; -import {BasePageObject} from '@/playwright-utils/base-page-object'; +import {BasePageObject} from '@/playwright-utils/lit-base-page-object'; -export class TabManagerPageObject extends BasePageObject<'atomic-tab-manager'> { +export class TabManagerPageObject extends BasePageObject { constructor(page: Page) { super(page, 'atomic-tab-manager'); } diff --git a/packages/atomic/src/components/search/index.ts b/packages/atomic/src/components/search/index.ts index 05f2762ff41..3f87cacf9ed 100644 --- a/packages/atomic/src/components/search/index.ts +++ b/packages/atomic/src/components/search/index.ts @@ -43,4 +43,5 @@ export {AtomicSegmentedFacetScrollable} from './atomic-segmented-facet-scrollabl export {AtomicSortDropdown} from './atomic-sort-dropdown/atomic-sort-dropdown.js'; export {AtomicSortExpression} from './atomic-sort-expression/atomic-sort-expression.js'; export {AtomicTab} from './atomic-tab/atomic-tab.js'; +export {AtomicTabManager} from './atomic-tab-manager/atomic-tab-manager.js'; export {AtomicText} from './atomic-text/atomic-text.js'; diff --git a/packages/atomic/src/components/search/lazy-index.ts b/packages/atomic/src/components/search/lazy-index.ts index e768b562f5f..48d9a143a72 100644 --- a/packages/atomic/src/components/search/lazy-index.ts +++ b/packages/atomic/src/components/search/lazy-index.ts @@ -115,6 +115,8 @@ export default { 'atomic-sort-expression': async () => await import('./atomic-sort-expression/atomic-sort-expression.js'), 'atomic-tab': async () => await import('./atomic-tab/atomic-tab.js'), + 'atomic-tab-manager': async () => + await import('./atomic-tab-manager/atomic-tab-manager.js'), 'atomic-text': async () => await import('./atomic-text/atomic-text.js'), } as Record Promise>; diff --git a/packages/atomic/src/components/search/tabs/atomic-tab-manager/atomic-tab-manager.new.stories.tsx b/packages/atomic/src/components/search/tabs/atomic-tab-manager/atomic-tab-manager.new.stories.tsx deleted file mode 100644 index 131abb71309..00000000000 --- a/packages/atomic/src/components/search/tabs/atomic-tab-manager/atomic-tab-manager.new.stories.tsx +++ /dev/null @@ -1,299 +0,0 @@ -import type {Meta, StoryObj as Story} from '@storybook/web-components-vite'; -import {getStorybookHelpers} from '@wc-toolkit/storybook-helpers'; -import {html} from 'lit'; -import {parameters} from '@/storybook-utils/common/common-meta-parameters'; -import {wrapInSearchInterface} from '@/storybook-utils/search/search-interface-wrapper'; - -const {events, args, argTypes, template} = getStorybookHelpers( - 'atomic-tab-manager', - {excludeCategories: ['methods']} -); - -const {decorator, play} = wrapInSearchInterface({ - config: { - search: { - // eslint-disable-next-line @cspell/spellchecker - pipeline: 'genqatest', - preprocessSearchResponseMiddleware: (r) => { - const [result] = r.body.results; - result.title = 'Manage the Coveo In-Product Experiences (IPX)'; - result.clickUri = 'https://docs.coveo.com/en/3160'; - r.body.questionAnswer = { - documentId: { - contentIdKey: 'permanentid', - contentIdValue: result.raw.permanentid!, - }, - question: 'Creating an In-Product Experience (IPX)', - answerSnippet: ` -
    -
  1. On the In-Product Experiences page, click Add In-Product Experience.
  2. -
  3. In the Configuration tab, fill the Basic settings section.
  4. -
  5. (Optional) Use the Design and Content access tabs to customize your IPX interface.
  6. -
  7. Click Save.
  8. -
  9. In the Loader snippet panel that appears, you may click Copy to save the loader snippet for your IPX to your clipboard, and then click Save. You can Always retrieve the loader snippet later.
  10. -
- -

- You're now ready to embed your IPX interface. However, we recommend that you configure query pipelines for your IPX interface before. -

- `, - relatedQuestions: [], - score: 1337, - }; - return r; - }, - }, - }, -}); - -const meta: Meta = { - component: 'atomic-tab-manager', - title: 'Search/Tabs', - id: 'atomic-tab-manager', - - render: (args) => template(args), - decorators: [decorator], - parameters: { - ...parameters, - actions: { - handles: events, - }, - }, - argTypes, - - play, - args: { - ...args, - 'default-slot': ` - - - - - `, - }, -}; - -export default meta; - -export const Default: Story = { - name: 'atomic-tab-manager', - decorators: [ - (story) => html` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ${story()} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - `, - ], -}; diff --git a/packages/atomic/src/components/search/tabs/atomic-tab-manager/atomic-tab-manager.pcss b/packages/atomic/src/components/search/tabs/atomic-tab-manager/atomic-tab-manager.pcss deleted file mode 100644 index 30678266564..00000000000 --- a/packages/atomic/src/components/search/tabs/atomic-tab-manager/atomic-tab-manager.pcss +++ /dev/null @@ -1,15 +0,0 @@ -@import '../../../../global/global.pcss'; - -:host { - atomic-tab-bar::part(popover-button) { - @apply m-0 px-2 pb-1 text-left text-xl font-normal text-black sm:px-6; - } - - atomic-tab-bar::part(value-label) { - @apply font-normal; - } - - ::part(popover-tab) { - @apply font-normal; - } -} diff --git a/packages/atomic/src/components/search/tabs/atomic-tab-manager/atomic-tab-manager.spec.tsx b/packages/atomic/src/components/search/tabs/atomic-tab-manager/atomic-tab-manager.spec.tsx deleted file mode 100644 index 5320d823460..00000000000 --- a/packages/atomic/src/components/search/tabs/atomic-tab-manager/atomic-tab-manager.spec.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import {newSpecPage} from '@stencil/core/testing'; -import {Bindings} from '../../atomic-search-interface/atomic-search-interface'; -import {AtomicTabManager} from './atomic-tab-manager'; - -describe('atomic-tab-manager', () => { - let bindings: Bindings; - - beforeEach(() => { - bindings = { - i18n: { - t: jest.fn((key, options) => options?.defaultValue || key), - }, - } as unknown as Bindings; - }); - - it('should localize the label parameter using bindings.i18n.t', async () => { - const page = await newSpecPage({ - components: [AtomicTabManager], - html: '', - supportsShadowDom: false, - }); - - const component = page.rootInstance as AtomicTabManager; - component.bindings = bindings; - component.initialize(); - - expect(bindings.i18n.t).toHaveBeenCalledWith('test-label', { - defaultValue: 'test-label', - }); - }); -}); diff --git a/packages/atomic/src/components/search/tabs/atomic-tab-manager/atomic-tab-manager.tsx b/packages/atomic/src/components/search/tabs/atomic-tab-manager/atomic-tab-manager.tsx deleted file mode 100644 index 1b79af95c4b..00000000000 --- a/packages/atomic/src/components/search/tabs/atomic-tab-manager/atomic-tab-manager.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { - TabManager, - TabManagerState, - buildTabManager, - Tab, - buildTab, -} from '@coveo/headless'; -import {Component, h, Element, State, Prop, Host} from '@stencil/core'; -import { - BindStateToController, - InitializeBindings, -} from '../../../../utils/initialization-utils'; -import {Bindings} from '../../atomic-search-interface/atomic-search-interface'; - -/** - * The `atomic-tab-manager` component manages a collection of tabs, - * allowing users to switch between them. Each child `atomic-tab` represents an - * individual tab within the manager. - * - * @part button-container - The container for the tab button. - * @part button-container-active - The container for the active tab button. - * @part tab-button - The tab button. - * @part tab-button-active - The container for the active tab button. - * @part dropdown-area - The dropdown area. - * @part tab-area - The tab area. - * @part popover-button - The "More" button shown when the tabs are collapsed. - * @part value-label - The label shown on the "More" button. - * @part arrow-icon - The down chevron icon shown on the "More" button. - * @part overflow-tabs - The list of tabs shown when the "More" button is clicked. - * @part popover-tab - The individual tab buttons shown when the "More" button is clicked. - * @part backdrop - The backdrop shown when the "More" button is clicked. - * @slot default - */ -@Component({ - tag: 'atomic-tab-manager', - styleUrl: 'atomic-tab-manager.pcss', - shadow: true, -}) -export class AtomicTabManager { - @InitializeBindings() public bindings!: Bindings; - @BindStateToController('tabManager') - @State() - private tabManagerState!: TabManagerState; - @Element() - private host!: HTMLElement; - public tabManager!: TabManager; - - private tabs: {label: string; name: string; tabController: Tab}[] = []; - - /** - * Whether to clear the filters when the active tab changes. - */ - @Prop() clearFiltersOnTabChange?: boolean = false; - - @State() public error!: Error; - - public initialize() { - this.tabManager = buildTabManager(this.bindings.engine); - - const tabElements = Array.from(this.host.querySelectorAll('atomic-tab')); - - if (tabElements.length === 0) { - this.error = new Error( - 'The "atomic-tab-manager" element requires at least one "atomic-tab" child.' - ); - return; - } - - tabElements.forEach((tabElement) => { - if (!tabElement.name) { - this.error = new Error( - 'The "name" attribute must be defined on all "atomic-tab" children.' - ); - return; - } - const tabController = buildTab(this.bindings.engine, { - options: { - expression: tabElement.expression, - id: tabElement.name, - clearFiltersOnTabChange: this.clearFiltersOnTabChange, - }, - }); - - this.tabs.push({ - label: tabElement.label, - name: tabElement.name, - tabController, - }); - }); - } - - render() { - return ( - - -
- {this.tabs.map((tab) => ( - { - if (!tab.tabController.state.isActive) { - tab.tabController.select(); - } - }} - > - ))} -
-
-
- ); - } -} diff --git a/packages/atomic/src/components/search/tabs/atomic-tab-manager/e2e/atomic-tab-manager.e2e.ts b/packages/atomic/src/components/search/tabs/atomic-tab-manager/e2e/atomic-tab-manager.e2e.ts deleted file mode 100644 index 17b78a3718e..00000000000 --- a/packages/atomic/src/components/search/tabs/atomic-tab-manager/e2e/atomic-tab-manager.e2e.ts +++ /dev/null @@ -1,465 +0,0 @@ -import {expect, test} from './fixture'; - -test.describe('AtomicTabManager', () => { - test.beforeEach(async ({tabManager}) => { - await tabManager.load(); - await tabManager.hydrated.waitFor(); - }); - - test('should display tabs area', async ({tabManager}) => { - await expect(tabManager.tabArea).toBeVisible(); - }); - - test('should not display tabs popover menu button', async ({tabManager}) => { - await expect(tabManager.tabPopoverMenuButton).toBeHidden(); - }); - - test('should display tab buttons for each atomic-tab elements', async ({ - tabManager, - }) => { - await expect(tabManager.tabPopoverMenuButton).toBeHidden(); - - await expect(tabManager.tabButtons()).toHaveText([ - 'All', - 'Documentation', - 'Articles', - 'Parts and Accessories', - ]); - }); - - test.describe('when viewport is large enough to display all tabs', () => { - test('should display tabs area', async ({tabManager}) => { - await expect(tabManager.tabArea).toBeVisible(); - }); - - test('should not display tabs popover menu button', async ({ - tabManager, - }) => { - await expect(tabManager.tabPopoverMenuButton).toBeHidden(); - }); - - test.describe('should change other component visibility', async () => { - test.beforeEach(async ({facets}) => { - await facets.facetValue.first().waitFor({state: 'visible'}); - }); - test('facets', async ({tabManager}) => { - const includedFacets = await tabManager.includedFacet.all(); - for (let i = 0; i < includedFacets.length; i++) { - await expect(includedFacets[i]).toBeHidden(); - } - - const excludedFacets = await tabManager.excludedFacet.all(); - for (let i = 0; i < excludedFacets.length; i++) { - await expect(excludedFacets[i]).toBeVisible(); - } - }); - - test('smart snippet', async ({tabManager}) => { - await expect(tabManager.smartSnippet).toBeHidden(); - }); - - test('sort dropdown', async ({tabManager}) => { - await tabManager.sortDropdown.waitFor({state: 'visible'}); - - await expect(tabManager.sortDropdownOptions).toHaveText([ - 'Relevance', - 'Name descending', - 'Name ascending', - ]); - }); - - test('result list', async ({tabManager}) => { - await expect(tabManager.excludedResultList).toBeVisible(); - await expect(tabManager.includedResultList).toBeHidden(); - }); - - //TODO: fix this flaky test https://coveord.atlassian.net/browse/KIT-3816 - test.fixme('generated answer', async ({tabManager, searchBox}) => { - await searchBox.searchInput.waitFor({state: 'visible'}); - await searchBox.searchInput.fill( - // eslint-disable-next-line @cspell/spellchecker - 'how to resolve netflix connection with tivo' - ); - await searchBox.searchInput.press('Enter'); - await expect(tabManager.generatedAnswer).toBeHidden(); - }); - }); - - test('should display tab buttons for each atomic-tab elements', async ({ - tabManager, - }) => { - await expect(tabManager.tabPopoverMenuButton).toBeHidden(); - - await expect(tabManager.tabButtons()).toHaveText([ - 'All', - 'Documentation', - 'Articles', - 'Parts and Accessories', - ]); - }); - - test.describe('when clicking on tab button', () => { - test.beforeEach(async ({tabManager}) => { - await tabManager.tabButtons('Articles').click(); - }); - - test('should change active tab', async ({tabManager}) => { - await expect(tabManager.activeTab).toHaveText('Articles'); - }); - - test.describe('should change other component visibility', async () => { - test('facets', async ({tabManager}) => { - await tabManager.excludedFacet.last().waitFor({state: 'hidden'}); - const includedFacets = await tabManager.includedFacet.all(); - for (let i = 0; i < includedFacets.length; i++) { - await expect(includedFacets[i]).toBeVisible(); - } - - const excludedFacets = await tabManager.excludedFacet.all(); - for (let i = 0; i < excludedFacets.length; i++) { - await expect(excludedFacets[i]).toBeHidden(); - } - }); - - test('smart snippet', async ({tabManager}) => { - await expect(tabManager.smartSnippet).toBeVisible(); - }); - - test('sort dropdown', async ({tabManager}) => { - await tabManager.sortDropdown.waitFor({state: 'visible'}); - - await expect(tabManager.sortDropdownOptions).toHaveText([ - 'Relevance', - 'Most Recent', - 'Least Recent', - ]); - }); - - test('result list', async ({tabManager}) => { - await expect(tabManager.includedResultList).toBeVisible(); - await expect(tabManager.excludedResultList).toBeHidden(); - }); - - //TODO: fix this flaky test https://coveord.atlassian.net/browse/KIT-3816 - test.fixme('generated answer', async ({tabManager, searchBox}) => { - await searchBox.searchInput.waitFor({state: 'visible'}); - await searchBox.searchInput.fill( - // eslint-disable-next-line @cspell/spellchecker - 'how to resolve netflix connection with tivo' - ); - await searchBox.searchInput.press('Enter'); - - await tabManager.generatedAnswer.waitFor({state: 'visible'}); - await expect(tabManager.generatedAnswer).toBeVisible(); - }); - }); - - test.describe('when selecting previous tab', () => { - test.beforeEach(async ({tabManager, facets}) => { - await tabManager.tabButtons('All').click(); - await facets.facetValue.first().waitFor({state: 'visible'}); - }); - - test.describe('should change other component visibility', async () => { - test('facets', async ({tabManager}) => { - const excludedFacets = await tabManager.excludedFacet.all(); - for (let i = 0; i < excludedFacets.length; i++) { - await expect(excludedFacets[i]).toBeVisible(); - } - - const includedFacets = await tabManager.includedFacet.all(); - for (let i = 0; i < includedFacets.length; i++) { - await expect(includedFacets[i]).toBeHidden(); - } - }); - - test('smart snippet', async ({tabManager}) => { - await expect(tabManager.smartSnippet).toBeHidden(); - }); - - test('sort dropdown', async ({tabManager}) => { - await tabManager.sortDropdown.waitFor({state: 'visible'}); - - await expect(tabManager.sortDropdownOptions).toHaveText([ - 'Relevance', - 'Name descending', - 'Name ascending', - ]); - }); - - test('result list', async ({tabManager}) => { - await expect(tabManager.excludedResultList).toBeVisible(); - await expect(tabManager.includedResultList).toBeHidden(); - }); - - //TODO: fix this flaky test https://coveord.atlassian.net/browse/KIT-3816 - test.fixme('generated answer', async ({tabManager, searchBox}) => { - await searchBox.searchInput.waitFor({state: 'visible'}); - await searchBox.searchInput.fill( - // eslint-disable-next-line @cspell/spellchecker - 'how to resolve netflix connection with tivo' - ); - await searchBox.searchInput.press('Enter'); - await expect(tabManager.generatedAnswer).toBeHidden(); - }); - }); - }); - - test.describe('when resizing viewport', () => { - test.beforeEach(async ({page}) => { - await page.setViewportSize({width: 300, height: 500}); - }); - - test('should display tabs popover menu button', async ({ - tabManager, - }) => { - await expect(tabManager.tabPopoverMenuButton).toBeVisible(); - }); - - test('should move overflowed tabs to popover tabs', async ({ - tabManager, - }) => { - await tabManager.tabPopoverMenuButton.click(); - await expect(tabManager.popoverTabs()).toHaveText([ - 'Documentation', - 'Parts and Accessories', - ]); - }); - - test('should not have the active tab in the popover tabs', async ({ - tabManager, - }) => { - await tabManager.tabPopoverMenuButton.click(); - const popoverTabs = tabManager.popoverTabs(); - await expect(popoverTabs).toHaveCount(2); - for (const tab of await popoverTabs.all()) { - await expect(tab).not.toHaveText('Articles'); - } - await expect( - tabManager.tabButtons().locator('visible=true') - ).toHaveText(['All', 'Articles']); - }); - }); - }); - }); - - test.describe('when viewport is too small to display all buttons', () => { - test.beforeEach(async ({page}) => { - await page.setViewportSize({width: 300, height: 1000}); - }); - - test.describe('keyboard navigation', () => { - test('should navigate within popover menu using arrow keys', async ({ - tabManager, - page, - }) => { - await tabManager.tabPopoverMenuButton.click(); - const popoverTabs = tabManager.popoverTabs(); - await page.keyboard.press('ArrowDown'); - await expect(popoverTabs.first()).toBeFocused(); - await page.keyboard.press('ArrowDown'); - await expect(popoverTabs.nth(1)).toBeFocused(); - }); - - test('should navigate within popover menu using tab', async ({ - tabManager, - page, - }) => { - await tabManager.tabPopoverMenuButton.click(); - const popoverTabs = tabManager.popoverTabs(); - await page.keyboard.press('Tab'); - await expect(popoverTabs.first()).toBeFocused(); - await page.keyboard.press('Tab'); - await expect(popoverTabs.first()).not.toBeFocused(); - - await page.keyboard.press('Tab'); - await expect(tabManager.tabPopoverMenuButton).not.toBeFocused(); - }); - - test('should wrap around when using arrow keys', async ({ - tabManager, - page, - }) => { - await tabManager.tabPopoverMenuButton.click(); - const popoverTabs = tabManager.popoverTabs(); - await page.keyboard.press('ArrowUp'); - await expect(popoverTabs.last()).toBeFocused(); - await page.keyboard.press('ArrowDown'); - await expect(popoverTabs.first()).toBeFocused(); - }); - - test('should close popover menu when pressing escape', async ({ - tabManager, - page, - }) => { - const popoverTabs = tabManager.popoverTabs(); - await expect(popoverTabs.first()).not.toBeVisible(); - - await tabManager.tabPopoverMenuButton.click(); - const allTabs = await popoverTabs.all(); - for (const tab of allTabs) { - await expect(tab).toBeVisible(); - } - - await page.keyboard.press('Escape'); - for (const tab of allTabs) { - await expect(tab).not.toBeVisible(); - } - }); - }); - - test('should display tabs popover menu button', async ({tabManager}) => { - await expect(tabManager.tabPopoverMenuButton).toBeVisible(); - }); - - test('should move overflowed tabs to popover tabs', async ({ - tabManager, - }) => { - await tabManager.tabPopoverMenuButton.click(); - await expect(tabManager.popoverTabs()).toHaveText([ - 'Documentation', - 'Articles', - 'Parts and Accessories', - ]); - }); - - test.describe('when selecting a tab popover button', () => { - test.beforeEach(async ({tabManager}) => { - await tabManager.tabPopoverMenuButton.click(); - await tabManager.popoverTabs('Articles').click(); - }); - - test('should change active tab', async ({tabManager}) => { - await expect(tabManager.activeTab).toHaveText('Articles'); - }); - - test.describe('should change other component visibility', async () => { - test('facets', async ({tabManager}) => { - await tabManager.refineModalButton.click(); - await tabManager.refineModalHeader.waitFor({state: 'visible'}); - - const includedFacets = await tabManager.includedModalFacet.all(); - for (let i = 0; i < includedFacets.length; i++) { - await expect(includedFacets[i]).toBeVisible(); - } - - const excludedFacets = await tabManager.excludedModalFacet.all(); - for (let i = 0; i < excludedFacets.length; i++) { - await expect(excludedFacets[i]).toBeHidden(); - } - }); - - test('smart snippet', async ({tabManager}) => { - await expect(tabManager.smartSnippet).not.toBeHidden(); - }); - - test('sort dropdown', async ({tabManager}) => { - await tabManager.refineModalButton.click(); - await tabManager.refineModalHeader.waitFor({state: 'visible'}); - - await expect(tabManager.refineModalSortDropdownOptions).toHaveText([ - 'Relevance', - 'Most Recent', - 'Least Recent', - ]); - }); - - test('result list', async ({tabManager}) => { - await expect(tabManager.includedResultList).toBeVisible(); - await expect(tabManager.excludedResultList).toBeHidden(); - }); - - //TODO: fix this flaky test https://coveord.atlassian.net/browse/KIT-3816 - test.fixme('generated answer', async ({tabManager, searchBox}) => { - await searchBox.searchInput.waitFor({state: 'visible'}); - await searchBox.searchInput.fill( - // eslint-disable-next-line @cspell/spellchecker - 'how to resolve netflix connection with tivo' - ); - - await searchBox.searchInput.press('Enter'); - - await expect(tabManager.generatedAnswer).toBeVisible(); - }); - }); - - test.describe('when selecting another tab in popover buttons', () => { - test.beforeEach(async ({tabManager}) => { - await tabManager.tabPopoverMenuButton.click(); - await tabManager.popoverTabs('Parts and Accessories').click(); - }); - - test.describe('should change other component visibility', async () => { - test('facets', async ({tabManager}) => { - await tabManager.refineModalButton.click(); - await tabManager.refineModalHeader.waitFor({state: 'visible'}); - - const excludedFacets = await tabManager.excludedModalFacet.all(); - for (let i = 0; i < excludedFacets.length; i++) { - await expect(excludedFacets[i]).toBeVisible(); - } - - const includedFacets = await tabManager.includedModalFacet.all(); - for (let i = 0; i < includedFacets.length; i++) { - await expect(includedFacets[i]).toBeHidden(); - } - }); - - test('smart snippet', async ({tabManager}) => { - await expect(tabManager.smartSnippet).toBeHidden(); - }); - - test('sort dropdown', async ({tabManager}) => { - await tabManager.refineModalButton.click(); - await tabManager.refineModalHeader.waitFor({state: 'visible'}); - - await expect(tabManager.refineModalSortDropdownOptions).toHaveText([ - 'Relevance', - 'Name descending', - 'Name ascending', - ]); - }); - - test('result list', async ({tabManager}) => { - await expect(tabManager.excludedResultList).toBeVisible(); - await expect(tabManager.includedResultList).toBeHidden(); - }); - - //TODO: fix this flaky test https://coveord.atlassian.net/browse/KIT-3816 - test.fixme('generated answer', async ({tabManager, searchBox}) => { - await searchBox.searchInput.waitFor({state: 'visible'}); - await searchBox.searchInput.fill( - // eslint-disable-next-line @cspell/spellchecker - 'how to resolve netflix connection with tivo' - ); - await searchBox.searchInput.press('Enter'); - await expect(tabManager.generatedAnswer).toBeHidden(); - }); - }); - }); - - test.describe('when resizing viewport', () => { - test.beforeEach(async ({page}) => { - await page.setViewportSize({width: 1000, height: 500}); - }); - - test('should hide tab popover menu button', async ({tabManager}) => { - await expect(tabManager.tabPopoverMenuButton).toBeHidden(); - }); - - test('should display tab buttons for each atomic-tab elements', async ({ - tabManager, - }) => { - await expect(tabManager.tabPopoverMenuButton).toBeHidden(); - - await expect(tabManager.tabButtons()).toHaveText([ - 'All', - 'Documentation', - 'Articles', - 'Parts and Accessories', - ]); - }); - }); - }); - }); -}); diff --git a/packages/atomic/src/utils/custom-element-tags.ts b/packages/atomic/src/utils/custom-element-tags.ts index fa76e202f49..8e07e3bd3b0 100644 --- a/packages/atomic/src/utils/custom-element-tags.ts +++ b/packages/atomic/src/utils/custom-element-tags.ts @@ -112,6 +112,7 @@ export const ATOMIC_CUSTOM_ELEMENT_TAGS = new Set([ 'atomic-sort-dropdown', 'atomic-sort-expression', 'atomic-tab', + 'atomic-tab-manager', 'atomic-text', ]);