diff --git a/commitlint.config.js b/commitlint.config.js
index 0ba30cfb..b996e9dc 100644
--- a/commitlint.config.js
+++ b/commitlint.config.js
@@ -12,6 +12,7 @@ module.exports = {
'icon',
'input',
'badge',
+ 'tab',
'tooltip'
],
],
diff --git a/src/baklava.ts b/src/baklava.ts
index a4dbd08d..9fede22f 100644
--- a/src/baklava.ts
+++ b/src/baklava.ts
@@ -1,6 +1,9 @@
export { default as BlIcon } from './components/icon/bl-icon';
export { default as BlButton } from './components/button/bl-button';
-export { default as BlInput } from './components/input/bl-input';
export { default as BlBadge } from './components/badge/bl-badge';
-export { default as BlTooltip } from './components/tooltip/bl-tooltip';
+export { default as BlInput } from './components/input/bl-input';
+export { default as BlTab } from './components/tab-group/tab/bl-tab';
+export { default as BlTabGroup } from './components/tab-group/bl-tab-group';
+export { default as BlTabPanel } from './components/tab-group/tab-panel/bl-tab-panel';
export { getIconPath, setIconPath } from './utilities/asset-paths';
+export { default as BlTooltip } from './components/tooltip/bl-tooltip';
diff --git a/src/components/button/bl-button.ts b/src/components/button/bl-button.ts
index dff28dad..edc10b99 100644
--- a/src/components/button/bl-button.ts
+++ b/src/components/button/bl-button.ts
@@ -99,7 +99,7 @@ export default class BlButton extends LitElement {
}
render(): TemplateResult {
- const isAnchor = this.href ? true : false;
+ const isAnchor = !!this.href;
const icon = this.icon ? html`` : '';
const slots = html`${icon} `;
const classes = classMap({
diff --git a/src/components/tab-group/bl-tab-group.css b/src/components/tab-group/bl-tab-group.css
new file mode 100644
index 00000000..1eaedac7
--- /dev/null
+++ b/src/components/tab-group/bl-tab-group.css
@@ -0,0 +1,18 @@
+.tabs {
+ background-color: var(--bl-color-primary-background);
+ border-bottom: var(--bl-size-4xs) solid var(--bl-color-secondary-background);
+ display: flex;
+ flex-direction: row;
+}
+
+.tabs-list {
+ overflow-x: scroll;
+
+ /* FIXME: use variables */
+ border-radius: 6px 6px 0 0;
+}
+
+.panels {
+ /* FIXME: use variables */
+ border-radius: 0 0 6px 6px;
+}
diff --git a/src/components/tab-group/bl-tab-group.stories.mdx b/src/components/tab-group/bl-tab-group.stories.mdx
new file mode 100644
index 00000000..21715707
--- /dev/null
+++ b/src/components/tab-group/bl-tab-group.stories.mdx
@@ -0,0 +1,85 @@
+import { html } from 'lit';
+import { ifDefined } from 'lit/directives/if-defined.js';
+import { styleMap } from 'lit/directives/style-map.js';
+import { unsafeHTML } from 'lit/directives/unsafe-html.js';
+import { Meta, Canvas, ArgsTable, Story, Preview, Source } from '@storybook/addon-docs';
+
+
+
+export const TabGroup = (args) => html`
+
+ Disabled
+ Selected
+ Tab
+ Panel 1
+ Panel 2
+ Panel 3
+
+ `
+ export const ScrollableTabGroup = (args) => html`
+
+ Tab 1
+ Tab 2
+ Tab 3
+ Tab 4
+ Tab 5
+ Tab 6
+ Tab 7
+ Tab 8
+ Tab 9
+ Tab 10
+ Tab 11
+ Tab 12
+ Tab 13
+ Tab 14
+ Tab 15
+ Tab 16
+ Tab 17
+ Tab 18
+ Panel 1
+ Panel 2
+ Panel 3
+ Panel 4
+ Panel 5
+ Panel 6
+ Panel 7
+ Panel 8
+ Panel 9
+ Panel 10
+ Panel 11
+ Panel 12
+ Panel 13
+ Panel 14
+ Panel 15
+ Panel 16
+ Panel 17
+ Panel 18
+
+ `
+
+# Tab Group
+Tab groups organizes the content in a way that each of Tab groups displays a section at a time.
+
+
+
+## Scrolling Tabs
+The navigation will scroll if there are more tabs than the horizontal space can allow.
+
+
+
diff --git a/src/components/tab-group/bl-tab-group.test.ts b/src/components/tab-group/bl-tab-group.test.ts
new file mode 100644
index 00000000..01bc7aae
--- /dev/null
+++ b/src/components/tab-group/bl-tab-group.test.ts
@@ -0,0 +1,188 @@
+import { fixture, html, fixtureCleanup, expect, nextFrame } from '@open-wc/testing';
+import BlTabGroup from './bl-tab-group';
+
+const createTab = () => {
+ const tabName = Date.now().toString();
+ const tab = document.createElement('bl-tab');
+ tab.slot = 'tabs';
+ tab.name = tabName;
+ tab.title = 'Add Player';
+
+ return tab;
+};
+
+describe('bl-tab-group', function () {
+ afterEach(() => {
+ fixtureCleanup();
+ });
+ it('should defined', async () => {
+ const el = document.createElement('bl-tab-group');
+ expect(el).to.be.instanceof(BlTabGroup);
+ });
+
+ it('render with default values', async function () {
+ const expected = `
+
+ `;
+ const el = await fixture(html` `);
+ expect(el).to.be.shadowDom.equal(expected);
+ });
+
+ it('should render panels', async function () {
+ const el = await fixture(
+ html`
+ Test Tab
+
+ `
+ );
+
+ expect(el.tabs.length).to.be.equal(1);
+ });
+
+ it('should select correct tab if has selected attr', async function () {
+ const el = await fixture(html`
+ Test 1 Tab
+ Test 2 Tab
+
+
+ `);
+ const selectedPanel = el.panels.find(p => p.tab === 'test-2');
+
+ expect(el.selectedTabName).to.be.equal('test-2');
+ expect(selectedPanel?.visible).to.be.true;
+ });
+
+ it('should handle bl-tab-selected event', async function () {
+ const el = await fixture(html`
+ Test 1 Tab
+ Test 2 Tab
+
+
+ `);
+ el.tabs[0].select();
+ expect(el.selectedTabName).to.be.equal('test-1');
+ expect(el.tabs[0].selected).to.be.true;
+ expect(el.tabs[1].selected).to.be.false;
+ expect(el.panels.find(p => p.tab === el.tabs[0].name)?.visible).to.be.true;
+ expect(el.panels.find(p => p.tab === el.tabs[1].name)?.visible).to.be.false;
+ });
+});
+
+describe('should selected tab functionality works when add or remove tabs ', function () {
+ it('should new tab selected', async function () {
+ const el = await fixture(html`
+ Test 1 Tab
+
+ `);
+ expect(el.tabs[0].selected).to.be.true;
+
+ // add new tab with selected flag
+ const tab = createTab();
+ tab.selected = true;
+ el.appendChild(tab);
+
+ await nextFrame();
+
+ expect(el.tabs.length).to.be.equal(2);
+ expect(el.tabs[0].selected).to.be.false;
+ expect(el.tabs[1].selected).to.be.true;
+ });
+
+ it('add a tab with disabled flag', async function () {
+ const el = await fixture(html`
+ Test 1 Tab
+
+ `);
+ expect(el.tabs[0].selected).to.be.true;
+
+ // add new tab with disabled flag
+ const tab = createTab();
+ tab.disabled = true;
+ el.appendChild(tab);
+
+ await nextFrame();
+
+ expect(el.tabs.length).to.be.equal(2);
+ expect(el.tabs[0].selected).to.be.true;
+ expect(el.tabs[1].selected).to.be.false;
+ });
+
+ it('first tab is disabled', async function () {
+ const el = await fixture(html`
+ Test 1 Tab
+
+ `);
+ expect(el.tabs[0].selected).to.be.false;
+
+ // add new tab
+ const tab = createTab();
+ el.appendChild(tab);
+
+ await nextFrame();
+
+ expect(el.tabs.length).to.be.equal(2);
+ expect(el.tabs[0].selected).to.be.false;
+ expect(el.tabs[1].selected).to.be.true;
+ });
+
+ it('added two tabs that first is disabled and second is selected', async function () {
+ const el = await fixture(html`
+ Test 1 Tab
+
+ `);
+ expect(el.tabs[0].selected).to.be.false;
+
+ // add new tabs
+ const disabledTab = createTab();
+ const selectedTab = createTab();
+ disabledTab.disabled = true;
+ selectedTab.selected = true;
+ el.appendChild(disabledTab);
+ el.appendChild(selectedTab);
+
+ await nextFrame();
+
+ expect(el.tabs.length).to.be.equal(3);
+ expect(el.tabs[0].selected).to.be.false;
+ expect(el.tabs[2].selected).to.be.true;
+ });
+
+ it('add a disabled and selected tab then remove selected tab', async function () {
+ const el = await fixture(html`
+ Test 1 Tab
+
+ `);
+
+ // add new tabs
+ const disabledTab = createTab();
+ const selectedTab = createTab();
+ disabledTab.disabled = true;
+ selectedTab.selected = true;
+ el.appendChild(disabledTab);
+ await nextFrame();
+ expect(el.tabs[0].selected).to.be.true;
+ el.appendChild(selectedTab);
+ await nextFrame();
+ expect(el.tabs[0].selected).to.be.false;
+ expect(el.tabs[2].selected).to.be.true;
+
+ // remove last tab that selected
+ el.removeChild(el.tabs[el.tabs.length - 1]);
+
+ await nextFrame();
+
+ expect(el.tabs.length).to.be.equal(2);
+ expect(el.tabs[0].selected).to.be.true;
+ expect(el.tabs[1].selected).to.be.false;
+ expect(el.tabs[1].disabled).to.be.true;
+ });
+});
diff --git a/src/components/tab-group/bl-tab-group.ts b/src/components/tab-group/bl-tab-group.ts
new file mode 100644
index 00000000..9614520d
--- /dev/null
+++ b/src/components/tab-group/bl-tab-group.ts
@@ -0,0 +1,109 @@
+import { CSSResultGroup, html, LitElement, TemplateResult } from 'lit';
+import { customElement } from 'lit/decorators.js';
+import style from './bl-tab-group.css';
+import './tab-panel/bl-tab-panel';
+import './tab/bl-tab';
+import type BlTabPanel from './tab-panel/bl-tab-panel';
+import type BlTab from './tab/bl-tab';
+
+/**
+ * @tag bl-tab-group
+ * @summary Baklava Tab group component
+ */
+@customElement('bl-tab-group')
+export default class BlTabGroup extends LitElement {
+ static get styles(): CSSResultGroup {
+ return [style];
+ }
+
+ private _connectedTabs: BlTab[] = [];
+ private _connectedPanels: BlTabPanel[] = [];
+
+ get tabs() {
+ return this._connectedTabs;
+ }
+
+ get panels() {
+ return this._connectedPanels;
+ }
+
+ /**
+ * This method is used by `tab` component to register them self to the tab group.
+ * @param tab BlTab reference to be registered
+ */
+ registerTab(tab: BlTab) {
+ const isFirstAndNotDisabled =
+ this._connectedTabs.filter(t => !t.disabled).length === 0 && !tab.disabled;
+ this._connectedTabs.push(tab);
+ if ((!tab.disabled && tab.selected) || isFirstAndNotDisabled) {
+ this.selectedTabName = tab.name;
+ }
+ }
+
+ /**
+ * This method is used by `tab` component to unregister them self to the tab group.
+ * @param tab BlTab reference to be unregistered
+ */
+ unregisterTab(tab: BlTab) {
+ this._connectedTabs.splice(this._connectedTabs.indexOf(tab), 1);
+ if (tab.selected) {
+ this._connectedTabs.find(t => !t.disabled)?.select();
+ }
+ }
+
+ /**
+ * This method is used by `tab-panel` component to register them self to the tab group.
+ * @param panel BlTabPanel reference to be registered
+ */
+ registerTabPanel(panel: BlTabPanel) {
+ panel.visible = panel.tab === this.selectedTabName;
+ this._connectedPanels.push(panel);
+ }
+
+ /**
+ * This method is used by `tab-panel` component to unregister them self to the tab group.
+ * @param panel BlTabPanel reference to be unregistered
+ */
+ unregisterTabPanel(panel: BlTabPanel) {
+ this._connectedTabs.splice(this._connectedPanels.indexOf(panel), 1);
+ }
+
+ private _selectedTabName: string;
+
+ get selectedTabName() {
+ return this._selectedTabName;
+ }
+
+ set selectedTabName(name: string) {
+ this._selectedTabName = name;
+ this._connectedTabs.forEach(t => {
+ t.selected = name === t.name;
+ });
+ this._connectedPanels.forEach(p => {
+ p.visible = p.tab === this._selectedTabName;
+ });
+ }
+
+ private _handleTabSelected(e: CustomEvent) {
+ this.selectedTabName = e.detail;
+ }
+
+ render(): TemplateResult {
+ return html` `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'bl-tab-group': BlTabGroup;
+ }
+}
diff --git a/src/components/tab-group/tab-panel/bl-tab-panel.css b/src/components/tab-group/tab-panel/bl-tab-panel.css
new file mode 100644
index 00000000..f15ef1b9
--- /dev/null
+++ b/src/components/tab-group/tab-panel/bl-tab-panel.css
@@ -0,0 +1,12 @@
+:host {
+ display: none;
+
+ /* FIXME: use variables */
+ border-radius: 0 0 6px 6px;
+}
+
+:host([visible]) {
+ display: block;
+ padding: var(--bl-size-xl);
+ background-color: var(--bl-color-primary-background);
+}
diff --git a/src/components/tab-group/tab-panel/bl-tab-panel.stories.mdx b/src/components/tab-group/tab-panel/bl-tab-panel.stories.mdx
new file mode 100644
index 00000000..8fefd7e5
--- /dev/null
+++ b/src/components/tab-group/tab-panel/bl-tab-panel.stories.mdx
@@ -0,0 +1,40 @@
+import { html } from 'lit';
+import { ifDefined } from 'lit/directives/if-defined.js';
+import { styleMap } from 'lit/directives/style-map.js';
+import { unsafeHTML } from 'lit/directives/unsafe-html.js';
+import { Meta, Canvas, ArgsTable, Story, Preview, Source } from '@storybook/addon-docs';
+
+
+
+export const TabGroup = (args) => html`
+
+ Tab 1
+ Tab 2
+ Tab 3
+ This is general Tab Panel
+ Panel 2
+ Panel 3
+
+ `
+
+# Tab Panel
+Tab panels are utilized inside tab groups to show tabbed content
+
+
+
+## Reference
+
+
+
diff --git a/src/components/tab-group/tab-panel/bl-tab-panel.test.ts b/src/components/tab-group/tab-panel/bl-tab-panel.test.ts
new file mode 100644
index 00000000..7b550230
--- /dev/null
+++ b/src/components/tab-group/tab-panel/bl-tab-panel.test.ts
@@ -0,0 +1,23 @@
+import { expect, fixture, html } from '@open-wc/testing';
+import BlTabPanel from './bl-tab-panel';
+
+describe('bl-tab-panel', function () {
+ it('should defined', function () {
+ const el = document.createElement('bl-tab-panel');
+ expect(el).to.be.instanceof(BlTabPanel);
+ });
+
+ it('should render with default values', async function () {
+ const el = await fixture(html` `);
+ const expected = `
+
+ `;
+ expect(el).to.be.shadowDom.equal(expected);
+ });
+
+ it('should set name property', async function () {
+ const name = 'test-panel';
+ const el = await fixture(html` `);
+ expect(el.tab).to.be.equal(name);
+ });
+});
diff --git a/src/components/tab-group/tab-panel/bl-tab-panel.ts b/src/components/tab-group/tab-panel/bl-tab-panel.ts
new file mode 100644
index 00000000..511d25d5
--- /dev/null
+++ b/src/components/tab-group/tab-panel/bl-tab-panel.ts
@@ -0,0 +1,54 @@
+import { CSSResultGroup, html, LitElement, TemplateResult } from 'lit';
+import { customElement, property } from 'lit/decorators.js';
+import styles from './bl-tab-panel.css';
+import type BlTabGroup from '../bl-tab-group';
+
+/**
+ * @tag bl-tab-panel
+ * @summary Baklava Tab panel component
+ */
+@customElement('bl-tab-panel')
+export default class BlTabPanel extends LitElement {
+ static get styles(): CSSResultGroup {
+ return [styles];
+ }
+
+ private tabGroup: BlTabGroup | null;
+
+ connectedCallback() {
+ super.connectedCallback();
+
+ this.updateComplete.then(() => {
+ this.tabGroup = this.closest('bl-tab-group');
+ // FIXME: We need to warn if parent is not tab-group
+ this.tabGroup?.registerTabPanel(this);
+ });
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this.tabGroup?.unregisterTabPanel(this);
+ }
+
+ /**
+ * Name of the linked tab.
+ */
+ @property({ type: String, reflect: true })
+ tab: string;
+
+ /**
+ * This attribute set by `tab-group` to make panel visible or hidden.
+ */
+ @property({ type: Boolean, reflect: true })
+ visible = false;
+
+ render(): TemplateResult {
+ return html``;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'bl-tab-panel': BlTabPanel;
+ }
+}
diff --git a/src/components/tab-group/tab/bl-tab.css b/src/components/tab-group/tab/bl-tab.css
new file mode 100644
index 00000000..41f3d18c
--- /dev/null
+++ b/src/components/tab-group/tab/bl-tab.css
@@ -0,0 +1,155 @@
+:host {
+ position: relative;
+ display: flex;
+ align-items: center;
+ background-color: var(--bl-color-primary-background);
+}
+
+.container {
+ --title-padding-vertical: var(--bl-size-m);
+ --title-padding-horizontal: var(--bl-size-xl);
+ --title-color: var(--bl-color-secondary);
+ --caption-color: var(--bl-color-secondary);
+ --icon-color: var(--bl-color-secondary);
+ --border-bottom-width: var(--bl-size-4xs);
+ --border-left-space: var(--bl-size-xl);
+ --font-title: var(--bl-font-title-3-medium);
+ --font-caption: var(--bl-font-title-4-regular);
+ --tab-right-padding: var(--bl-size-xl);
+ --help-container-width: var(--bl-size-2xl);
+ --tab-height: calc(var(--bl-size-3xl) + var(--bl-size-s));
+
+ display: flex;
+ border: none;
+ cursor: pointer;
+ background-color: initial;
+ width: max-content;
+ height: var(--tab-height);
+ padding: 0 var(--tab-right-padding);
+}
+
+.container::after {
+ position: absolute;
+ content: '';
+ right: 0;
+ top: var(--bl-size-m);
+ height: calc(100% - var(--bl-size-2xl));
+ border-right: 1px solid var(--bl-color-tertiary-hover);
+}
+
+:host(:last-of-type) .container::after {
+ border-right: none;
+}
+
+:host .container::before {
+ content: '';
+ position: absolute;
+ opacity: 0;
+ bottom: calc(-1 * var(--bl-size-4xs));
+ left: var(--border-left-space);
+ width: calc(100% - var(--bl-size-4xl));
+ border-bottom: var(--border-bottom-width) solid var(--bl-color-primary);
+}
+
+:host([selected]:not([disabled])) .container::before {
+ opacity: 1;
+}
+
+:host(:hover) .container,
+:host([selected]) .container {
+ --title-color: var(--bl-color-primary);
+ --icon-color: var(--bl-color-primary);
+}
+
+:host([disabled]) .container {
+ cursor: not-allowed;
+
+ --title-color: var(--bl-color-content-passive);
+ --caption-color: var(--bl-color-content-passive);
+ --icon-color: var(--bl-color-content-passive);
+}
+
+:host(:hover) :where(.title, .icon) {
+ transition: color 120ms ease-out;
+}
+
+:host([selected]) .border-bottom {
+ display: inline-block;
+}
+
+:host([disabled]) .container:hover {
+ cursor: not-allowed;
+}
+
+:host([help-text]) button {
+ padding-right: 0;
+}
+
+.tab-button {
+ width: max-content;
+}
+
+.help-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: var(--help-container-width);
+ height: 100%;
+ font-size: var(--bl-font-size-m);
+ pointer-events: visible;
+ padding-right: var(--tab-right-padding);
+}
+
+bl-tooltip {
+ --bl-tooltip-position: fixed;
+}
+
+.title {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font: var(--font-title);
+ color: var(--title-color);
+ line-height: var(--bl-size-m);
+ white-space: nowrap;
+}
+
+.title-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ margin: auto;
+}
+
+.badge-container {
+ padding-left: var(--bl-size-3xs);
+ display: flex;
+ margin-bottom: 1px;
+}
+
+.caption {
+ font: var(--font-caption);
+ line-height: var(--bl-size-xs);
+ color: var(--caption-color);
+ text-align: center;
+ margin-top: var(--bl-size-4xs);
+}
+
+.icon {
+ display: flex;
+ color: var(--icon-color);
+ font-size: var(--bl-font-size-l);
+ margin-right: var(--bl-size-3xs);
+ margin-bottom: 1px;
+}
+
+:host([notify]) .title::after {
+ content: '';
+ height: var(--bl-size-2xs);
+ width: var(--bl-size-2xs);
+ border-radius: var(--bl-size-3xs);
+ margin-left: var(--bl-size-3xs);
+ background-color: var(--bl-color-danger);
+ margin-bottom: 1px;
+}
diff --git a/src/components/tab-group/tab/bl-tab.stories.mdx b/src/components/tab-group/tab/bl-tab.stories.mdx
new file mode 100644
index 00000000..8f6af980
--- /dev/null
+++ b/src/components/tab-group/tab/bl-tab.stories.mdx
@@ -0,0 +1,137 @@
+import { html } from 'lit';
+import { ifDefined } from 'lit/directives/if-defined.js';
+import { styleMap } from 'lit/directives/style-map.js';
+import { unsafeHTML } from 'lit/directives/unsafe-html.js';
+import { Meta, Canvas, ArgsTable, Story, Preview, Source } from '@storybook/addon-docs';
+
+
+
+export const TabsTemplate = (args) => html`
+
+ Disabled
+ Selected
+ Tab
+
+ `
+
+export const TabsAttributeTemplate = (args) => html`
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+ `
+export const NotifyTemplate = (args) => html`
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+ `
+export const IconTemplate = (args) => html`
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+ `
+export const MoreThanOneSelectedTemplate = (args) => html`
+
+ Selected Tab 1
+ Selected Tab 2
+ Tab 3
+
+ `
+
+export const SelectedDisabledTogetherTemplate = (args) => html`
+
+ Tab 1
+ Selected and Disabled Tab 2
+ Tab 3
+
+ `
+
+# Tab
+Within tab groups, tabs are used to represent and activate tab panels. A tab can be disabled by setting the disabled prop and can be selected by selected prop.
+
+
+
+
+## Tab with Caption
+Title can be clarified with caption attribute. Caption is located just below the tab title
+
+
+
+## Tab With Help Text
+You can give extra information to user with help-text attribute. help-text attribute adds a `bl-tooltip` component at right of the tab
+
+
+
+## Tab With Badge
+A Badge can be added to tab. This Badge is showed with `bl-badge` component internally
+
+
+
+## Tab With Icon
+An icon can be added to tab. This icon is showed with `bl-icon` component internally and it's color synced with tab's title color
+
+
+
+## Tab With Notification Dot
+A notification dot can be used in tab compnenet in order for user to get notified by updates in related content
+
+
+
+## Edge Cases
+
+#### More Than One Selected Tab
+If more than one selected tabs are used, the last one is picked as selected
+
+
+
+#### Using Disabled and Selected Props Together
+If disabled and selected props are used in same table, disabled prop overrides selected prop
+
+
+
+## Reference
+
+
+
+
diff --git a/src/components/tab-group/tab/bl-tab.test.ts b/src/components/tab-group/tab/bl-tab.test.ts
new file mode 100644
index 00000000..3bcadc38
--- /dev/null
+++ b/src/components/tab-group/tab/bl-tab.test.ts
@@ -0,0 +1,64 @@
+import { oneEvent, fixture, html, expect } from '@open-wc/testing';
+import BlTab from './bl-tab';
+import type BlIcon from '../../icon/bl-icon';
+
+describe('bl-tab', function () {
+ it('should defined', async function () {
+ const el = document.createElement('bl-tab');
+ expect(el).to.be.an.instanceof(BlTab);
+ });
+
+ it('renders with default values', async () => {
+ const el = await fixture(html` `);
+ const expected = `
+
+ `;
+ expect(el).to.be.shadowDom.equal(expected);
+ });
+
+ it('renders with a badge', async () => {
+ const el = await fixture(html` `);
+ const badgeEl = el.shadowRoot?.querySelector('bl-badge');
+ expect(badgeEl).is.exist;
+ });
+
+ it('renders with a help text', async () => {
+ const helpText = 'Help Me!';
+ const el = await fixture(html` `);
+ const helpContainer = el.shadowRoot?.querySelector('.help-container');
+ expect(helpContainer?.innerText).to.equal(helpText);
+ });
+
+ it('renders with icon', async () => {
+ const icon = 'heart';
+ const el = await fixture(html` `);
+ const iconEl = el.shadowRoot?.querySelector('bl-icon');
+ expect(iconEl).is.exist;
+ });
+
+ it('should create custom event on handle click function', async () => {
+ const el = await fixture(html` `);
+ const clickButton = () => el.shadowRoot?.querySelector('button')?.click();
+ setTimeout(clickButton);
+ const listener = await oneEvent(el, 'bl-tab-selected');
+ const { detail } = await listener;
+
+ expect(detail).is.equal('test');
+ });
+
+ it('should set caption', async function () {
+ const el = await fixture(html``);
+ const caption = el.shadowRoot?.querySelector('.caption');
+
+ expect(caption).is.exist;
+ });
+});
diff --git a/src/components/tab-group/tab/bl-tab.ts b/src/components/tab-group/tab/bl-tab.ts
new file mode 100644
index 00000000..fa3b5dfe
--- /dev/null
+++ b/src/components/tab-group/tab/bl-tab.ts
@@ -0,0 +1,148 @@
+import { CSSResultGroup, html, LitElement, TemplateResult } from 'lit';
+import { customElement, property } from 'lit/decorators.js';
+import { event, EventDispatcher } from '../../../utilities/event';
+
+import style from './bl-tab.css';
+import type BlTabGroup from '../bl-tab-group';
+
+/**
+ * @tag bl-tab
+ * @summary Baklava Tab component
+ */
+@customElement('bl-tab')
+export default class BlTab extends LitElement {
+ static get styles(): CSSResultGroup {
+ return [style];
+ }
+
+ private tabGroup: BlTabGroup | null;
+
+ connectedCallback() {
+ super.connectedCallback();
+
+ this.updateComplete.then(() => {
+ this.tabGroup = this.closest('bl-tab-group');
+ // FIXME: We need to warn if parent is not tab-group
+ this.tabGroup?.registerTab(this);
+ });
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this.tabGroup?.unregisterTab(this);
+ }
+
+ /**
+ * Sets the caption of tab
+ */
+ @property({ type: String })
+ caption: string;
+
+ /**
+ * Name of the tab that should match `tab-panel`'s `tab` attribute
+ */
+ @property({ type: String, reflect: true })
+ name: string;
+
+ /**
+ * Set tooltip text. Should be set to display information icon.
+ */
+ @property({ type: String, attribute: 'help-text', reflect: true })
+ helpText: string;
+
+ /**
+ * Name of the icon which display on the left side of the tab.
+ */
+ @property({ type: String })
+ icon = '';
+
+ /**
+ * Shows notification dot.
+ */
+ @property({ type: Boolean, reflect: true })
+ notify = false;
+
+ /**
+ * Sets the content of the badge.
+ */
+ @property({ type: String })
+ badge = '';
+
+ /**
+ * Set `tab` as selected.
+ */
+ @property({ type: Boolean, reflect: true })
+ selected = false;
+
+ /**
+ * Set `tab` as disabled.
+ */
+ @property({ type: Boolean, reflect: true })
+ disabled = false;
+
+ /**
+ * Fires when tab is selected.
+ */
+ @event('bl-tab-selected') private _onSelect: EventDispatcher;
+
+ /**
+ * Set tab selected.
+ */
+ select() {
+ this._onSelect(this.name);
+ }
+
+ render(): TemplateResult {
+ const title = html` `;
+
+ const helpTooltip = this.helpText
+ ? html`
+
+
+ ${this.helpText}
+
+
`
+ : null;
+
+ const icon = this.icon
+ ? html`
+
+
`
+ : null;
+
+ const badge = this.badge
+ ? html`
+ ${this.badge}
+
`
+ : null;
+
+ const caption = this.caption ? html` ${this.caption}
` : null;
+
+ return html`
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'bl-tab': BlTab;
+ }
+}