From 34918050b887b8ca5bdd68f872df9762a23da2f0 Mon Sep 17 00:00:00 2001 From: Mehmet Date: Tue, 9 Aug 2022 12:40:24 +0300 Subject: [PATCH] feat: tab component (#160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(tab): init bl-tab, bl-tab-panel, bl-tab-group * feat(tab): added tab css, borders css * feat(tab): added var for css rules * feat(tab): added ::after,::before pseudo for borders * feat(tab): rename active prop to selected * feat(tab): added button for tab element * feat: tab styling * feat(tab): added help text, badge * feat: tab notify, caption, icon added * feat(tab): refactor notify * feat(tab): added hover event for help text * fix(tab): review issues * fix(tab): remove other component declaration in tab, set panel as protected prop in tab * fix(tab): set name as protected prop in tab * test(tab): added test * test(tab): added test for tab-panel * test(tab): added test for tab-panel * test(tab): added hover effect * test(tab): complete tests * test(tab): fixed disabled and selected case in css * test(tab): fixed disabled badge color * test(tab): use expect instead of assert * test(tab): fix linter * test(tab): fix linter for tooltip * fix(tab): sonar issue * fix(tab): linter after sonar issue fixed * feat(tab): storybook for tab,tab panel and tab group completed * feat(tab): tab scroll bug fixed * feat(tab): wording changes * refactor(button): reverted changes in bl-button.ts * refactor: review resolves * refactor: lint * feat(tab): added JSDoc * feat(tab): added unregister function * refactor: fix lint * test(tab): icon mock in test * fix(tab): fixed lint * feat(tab): tab component's storybook is detailed * test(tab): added add/remove tab tests * feat(tab): unnecessary code snippets removed form tab storybooks * fix(tab): set tooltip placement as default * refactor: tooltip style moved to css file * fix(tab): remove title attribute * fix(tab): css variables * fix(tab): linter format * fix(tab): remove tab title from tab-panel and tab-group stories * fix(tab): 1px margin bottom for icon notify and badge Co-authored-by: mehmet.tanas Co-authored-by: erdem.besler Co-authored-by: Murat Çorlu <127687+muratcorlu@users.noreply.github.com> Co-authored-by: Murat Çorlu --- commitlint.config.js | 1 + src/baklava.ts | 7 +- src/components/button/bl-button.ts | 2 +- src/components/tab-group/bl-tab-group.css | 18 ++ .../tab-group/bl-tab-group.stories.mdx | 85 ++++++++ src/components/tab-group/bl-tab-group.test.ts | 188 ++++++++++++++++++ src/components/tab-group/bl-tab-group.ts | 109 ++++++++++ .../tab-group/tab-panel/bl-tab-panel.css | 12 ++ .../tab-panel/bl-tab-panel.stories.mdx | 40 ++++ .../tab-group/tab-panel/bl-tab-panel.test.ts | 23 +++ .../tab-group/tab-panel/bl-tab-panel.ts | 54 +++++ src/components/tab-group/tab/bl-tab.css | 155 +++++++++++++++ .../tab-group/tab/bl-tab.stories.mdx | 137 +++++++++++++ src/components/tab-group/tab/bl-tab.test.ts | 64 ++++++ src/components/tab-group/tab/bl-tab.ts | 148 ++++++++++++++ 15 files changed, 1040 insertions(+), 3 deletions(-) create mode 100644 src/components/tab-group/bl-tab-group.css create mode 100644 src/components/tab-group/bl-tab-group.stories.mdx create mode 100644 src/components/tab-group/bl-tab-group.test.ts create mode 100644 src/components/tab-group/bl-tab-group.ts create mode 100644 src/components/tab-group/tab-panel/bl-tab-panel.css create mode 100644 src/components/tab-group/tab-panel/bl-tab-panel.stories.mdx create mode 100644 src/components/tab-group/tab-panel/bl-tab-panel.test.ts create mode 100644 src/components/tab-group/tab-panel/bl-tab-panel.ts create mode 100644 src/components/tab-group/tab/bl-tab.css create mode 100644 src/components/tab-group/tab/bl-tab.stories.mdx create mode 100644 src/components/tab-group/tab/bl-tab.test.ts create mode 100644 src/components/tab-group/tab/bl-tab.ts 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. + + + + {TabGroup.bind({})} + + + +## Scrolling Tabs +The navigation will scroll if there are more tabs than the horizontal space can allow. + + + + {ScrollableTabGroup.bind({})} + + + 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 + + + + {TabGroup.bind({})} + + + +## 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. + + + + {TabsTemplate.bind({})} + + + + +## Tab with Caption +Title can be clarified with caption attribute. Caption is located just below the tab title + + +> + {TabsAttributeTemplate.bind({})} + + + +## 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 + + +> + {TabsAttributeTemplate.bind({})} + + + +## Tab With Badge +A Badge can be added to tab. This Badge is showed with `bl-badge` component internally + + +> + {TabsAttributeTemplate.bind({})} + + + +## 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 + + +> + {IconTemplate.bind({})} + + + +## 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 + + +> + {NotifyTemplate.bind({})} + + + +## Edge Cases + +#### More Than One Selected Tab +If more than one selected tabs are used, the last one is picked as selected + + + + {MoreThanOneSelectedTemplate.bind({})} + + + +#### Using Disabled and Selected Props Together +If disabled and selected props are used in same table, disabled prop overrides selected prop + + + + {SelectedDisabledTogetherTemplate.bind({})} + + + +## 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; + } +}