+ This is the recommended use. Headers for each tab is set by the header attribute.
+
+ `,
+ play: async (context) => {
+ const tabGroupElement = within(context.canvasElement).getByTestId('test-tab-group');
+
+ // Get the tab bar element
+ const tabBar = (await querySelectorAsync(tabGroupElement.shadowRoot as ShadowRoot, '.tab-bar')) as HTMLElement;
+ await expect(tabBar).toBeTruthy();
+
+ // Get all the tab headers in the tab bar
+ const nestedTabHeaders = tabBar.querySelectorAll('omni-tab-header');
+ const tabHeadersArray = [...nestedTabHeaders];
+ // Confirm that 3 Tab headers exist.
+ await expect(tabHeadersArray.length).toBe(3);
+
+ // Get the default slot of the Tab element.
+ const tabsSlotElement = (await querySelectorAsync(tabGroupElement.shadowRoot as ShadowRoot, 'slot:not([name])')) as HTMLSlotElement;
+ // Get the all the Tab elements in the default slot.
+ const tabElements = tabsSlotElement.assignedElements();
+ // Confirm that there is 3 tab elements in the default slot.
+ await expect(tabElements.length).toBe(3);
+ }
+} as ComponentStoryFormat;
+
+export const Active = {
+ render: () => html`
+
+
+
+
+
+
+
+
+
+
+
+`,
+
+ frameworkSources: [
+ {
+ framework: 'Vue',
+ load: (args) => getSourceFromLit(Active?.render?.(args), undefined, (s) => s.replace(' active', ' :active="true"'))
+ },
+ {
+ framework: 'React',
+ load: () => `import { OmniTabGroup, OmniTab } from "@capitec/omni-components-react/tab";
+import { OmniLabel } from "@capitec/omni-components-react/label";
+
+const App = () =>
+
+
+
+
+
+
+
+
+
+
+;`
+ }
+ ],
+ args: {},
+ name: 'Active',
+ description: () => html`
+
+ Set the active attribute on an <omni-tab> to indicate its active. By default, the first slotted one is active.
+
+ `,
+ play: async (context) => {
+ const tabGroupElement = within(context.canvasElement).getByTestId('test-tab-group');
+
+ // Get the tab bar element
+ const tabBar = (await querySelectorAsync(tabGroupElement.shadowRoot as ShadowRoot, '.tab-bar')) as HTMLElement;
+ await expect(tabBar).toBeTruthy();
+
+ // Get all the tab headers in the tab bar
+ const nestedTabHeaders = tabBar.querySelectorAll('omni-tab-header');
+ const tabHeadersArray = [...nestedTabHeaders];
+
+ //Get the active tab header.
+ const activeTabHeader = tabHeadersArray.find((c) => c.hasAttribute('data-active'));
+ await expect(activeTabHeader).toBeTruthy();
+ // Confirm that the active tab header is the second one in the tab header array
+ await expect(activeTabHeader).toEqual(tabHeadersArray[1]);
+ // Click on the first tab header
+ await userEvent.click(tabHeadersArray[0]);
+
+ // Get the default slot for all the Tabs
+ const tabsSlotElement = (await querySelectorAsync(tabGroupElement.shadowRoot as ShadowRoot, 'slot:not([name])')) as HTMLSlotElement;
+ // Get the active tab
+ const tabElement = tabsSlotElement.assignedElements().find((e) => e.hasAttribute('active')) as Tab;
+ // Get the active tab component slot
+ const tabElementSlot = (await querySelectorAsync(tabElement.shadowRoot as ShadowRoot, 'slot')) as HTMLSlotElement;
+ // Get the active tab label element based on the value of the label attribute and confirm that the label exists.
+ const labelElement = tabElementSlot.assignedElements().find((e) => e.getAttribute('label') === 'Label of Tab 1') as Tab;
+ await expect(labelElement).toBeTruthy();
+ }
+} as ComponentStoryFormat;
+
+export const Disabled = {
+ render: () => html`
+
+
+
+
+
+
+
+
+
+
+
+`,
+ frameworkSources: [
+ {
+ framework: 'Vue',
+ load: (args) => getSourceFromLit(Disabled?.render?.(args), undefined, (s) => s.replace(' disabled', ' :disabled="true"'))
+ },
+ {
+ framework: 'React',
+ load: () => `import { OmniTabGroup, OmniTab } from "@capitec/omni-components-react/tab";
+import { OmniLabel } from "@capitec/omni-components-react/label";
+
+const App = () =>
+
+
+
+
+
+
+
+
+
+
+;`
+ }
+ ],
+ name: 'Disabled',
+ description: () => html`
+
+ Set the disabled attribute on an <omni-tab> to indicate its disabled.
+
+ `,
+ args: {},
+ play: async (context) => {
+ const tabGroupElement = within(context.canvasElement).getByTestId('test-tab-group');
+ const tabSelect = jest.fn();
+ tabGroupElement.addEventListener('tab-select', tabSelect);
+
+ // Get the tab bar element
+ const tabBar = (await querySelectorAsync(tabGroupElement.shadowRoot as ShadowRoot, '.tab-bar')) as HTMLElement;
+
+ // Get all the tabs in the tab bar
+ const tabHeaders = tabBar.querySelectorAll('omni-tab-header');
+ const tabsHeadersArray = [...tabHeaders];
+
+ //Get the disabled tab.
+ const disabledTabHeader = tabsHeadersArray.find((c) => c.hasAttribute('data-disabled')) as TabHeader;
+ // Confirm that the disabled tab the last tab in the tab header array.
+ await expect(disabledTabHeader).toEqual(tabsHeadersArray[2]);
+
+ //Click the disabled tab header twice
+ await userEvent.click(disabledTabHeader);
+ await userEvent.click(disabledTabHeader);
+ // Confirm that the tab select event was emitted zero times.
+ await expect(tabSelect).toBeCalledTimes(0);
+ // Click the second tab header
+ await userEvent.click(tabsHeadersArray[1]);
+ // Confirm that the tab select event was emitted once.
+ await expect(tabSelect).toBeCalledTimes(1);
+ }
+} as ComponentStoryFormat;
diff --git a/src/tab/Tab.ts b/src/tab/Tab.ts
new file mode 100644
index 00000000..bd2d7b71
--- /dev/null
+++ b/src/tab/Tab.ts
@@ -0,0 +1,72 @@
+import { css, html, TemplateResult } from 'lit';
+import { customElement, property } from 'lit/decorators.js';
+import { OmniElement } from '../core/OmniElement.js';
+
+/**
+ * Control that can be used to display slotted content, for use within an Tab Group component.
+ *
+ * @import
+ * ```js
+ * import '@capitec/omni-components/tab';
+ * ```
+ * @example
+ * html```
+ *
+ * Tab 1 Content
+ *
+ * ```
+ *
+ * @element omni-tab
+ *
+ * @slot - Content to render inside the component body.
+ *
+ */
+@customElement('omni-tab')
+export class Tab extends OmniElement {
+ /**
+ * Tab header label, use the omni-tab-header component for more complex header layouts
+ * @attr
+ */
+ @property({ type: String, reflect: true }) header?: string;
+
+ /**
+ * Indicator if the tab is disabled.
+ * @attr
+ */
+ @property({ type: Boolean, reflect: true }) disabled?: boolean;
+
+ /**
+ * Indicator if the tab is active.
+ * @attr
+ */
+ @property({ type: Boolean, reflect: true }) active?: boolean;
+
+ static override get styles() {
+ return [
+ super.styles,
+ css`
+ :host {
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ }
+
+ :host(*:not([active])) {
+ display: none !important;
+ }
+ `
+ ];
+ }
+
+ protected override render(): TemplateResult {
+ return html`
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'omni-tab': Tab;
+ }
+}
diff --git a/src/tab/TabGroup.stories.ts b/src/tab/TabGroup.stories.ts
new file mode 100644
index 00000000..f4778990
--- /dev/null
+++ b/src/tab/TabGroup.stories.ts
@@ -0,0 +1,88 @@
+import { within } from '@testing-library/dom';
+import userEvent from '@testing-library/user-event';
+import * as jest from 'jest-mock';
+import { html, nothing } from 'lit';
+import { unsafeHTML } from 'lit/directives/unsafe-html.js';
+import expect from '../utils/ExpectDOM.js';
+import { assignToSlot, ComponentStoryFormat, querySelectorAsync, raw, getSourceFromLit } from '../utils/StoryUtils.js';
+
+import { Tab } from './Tab.js';
+import { TabGroup } from './TabGroup.js';
+import '../label/Label.js';
+import './TabHeader.js';
+import './Tab.js';
+import './TabGroup.js';
+import '../icon/Icon.js';
+
+interface Args {
+ header: string;
+ '[Default Slot]': string;
+}
+
+export const Interactive: ComponentStoryFormat = {
+ render: (args: Args) => html`
+
+ ${args.header ? html`${'\r\n'}${unsafeHTML(assignToSlot('header', args.header))}` : nothing}
+ ${args['[Default Slot]'] ? html`${'\r\n'}${unsafeHTML(args['[Default Slot]'])}` : nothing}
+
+`,
+ frameworkSources: [
+ {
+ framework: 'Vue',
+ load: (args) =>
+ getSourceFromLit(Interactive!.render!(args), undefined, (s) =>
+ s.replace(' active', ' :active="true"').replace(' disabled', ' :disabled="true"')
+ )
+ }
+ ],
+ name: 'Interactive',
+ description: () => html`
+
+ The <omni-tab-group> component will display content based on the slotted <omni-tab> component(s).
+
+
+ For an advanced use case check the <omni-tab-header> example.
+
+ For slotting custom content into the header use the <omni-tab-header> component that targets the header slot of the <omni-tab-group> component by setting slot="header" and ensure you have a <omni-tab> component which has an id attribute that matches the <omni-tab-header>for attribute to display slotted content.
+