From 8de1f59b72fac1a15e66ab3d187e2045a75d1b88 Mon Sep 17 00:00:00 2001 From: Ang Dawa Sherpa <89925822+angsherpa456@users.noreply.github.com> Date: Sat, 13 Jul 2024 16:26:20 +0200 Subject: [PATCH] feat(ui-library): tab bar - use slots for tab bar item (#1136) Co-authored-by: Barkley --- packages/js-example-app/src/index.client.ts | 8 - packages/js-example-app/src/index.server.ts | 22 +- .../src/components/radio-group/index.ts | 5 +- .../src/components/tab-bar-item/index.css.ts | 231 +++++++++++++++++ .../src/components/tab-bar-item/index.ts | 145 +++++++++++ .../src/components/tab-bar/index.css.ts | 233 +----------------- .../src/components/tab-bar/index.stories.ts | 24 +- .../src/components/tab-bar/index.test.ts | 31 +-- .../src/components/tab-bar/index.ts | 204 ++++++--------- packages/ui-library/src/index.ts | 2 + 10 files changed, 495 insertions(+), 410 deletions(-) create mode 100644 packages/ui-library/src/components/tab-bar-item/index.css.ts create mode 100644 packages/ui-library/src/components/tab-bar-item/index.ts diff --git a/packages/js-example-app/src/index.client.ts b/packages/js-example-app/src/index.client.ts index 8487e3dfb..e4f5b25f8 100644 --- a/packages/js-example-app/src/index.client.ts +++ b/packages/js-example-app/src/index.client.ts @@ -203,14 +203,6 @@ function init() { addLog('blr-toggleswitch changed: ' + e.detail.checkedState); }); - blrTabBar.addEventListener('blrFocus', () => { - addLog('blr-tab-bar focused'); - }); - - blrTabBar.addEventListener('blrBlur', () => { - addLog('blr-tab-bar blurred'); - }); - blrTabBar.addEventListener('blrChange', (e) => { addLog('blr-tab-bar changed: ' + e.detail.changedValue); }); diff --git a/packages/js-example-app/src/index.server.ts b/packages/js-example-app/src/index.server.ts index ba62affee..f657c1340 100644 --- a/packages/js-example-app/src/index.server.ts +++ b/packages/js-example-app/src/index.server.ts @@ -214,17 +214,17 @@ export function* renderIndex() { tab-content="labelAndIcon" icon-position="leading" alignment="left" - >

Tab 1

-

Tab 2

-

Tab 3

-

Tab 4

-

Tab 5

-

Tab 6

-

Tab 7

-

Tab 8

-

Tab 9

-

Tab 10

-

Tab 11

+ >Tab 1 + Tab 2 + Tab 3 + Tab 4 + Tab 5 + Tab 6 + Tab 7 + Tab 8 + Tab 9 + Tab 10 + Tab 11 diff --git a/packages/ui-library/src/components/radio-group/index.ts b/packages/ui-library/src/components/radio-group/index.ts index 7018ca860..9742b0385 100644 --- a/packages/ui-library/src/components/radio-group/index.ts +++ b/packages/ui-library/src/components/radio-group/index.ts @@ -1,4 +1,4 @@ -import { PropertyValues, html, nothing } from 'lit'; +import { html, nothing } from 'lit'; import { classMap } from 'lit/directives/class-map.js'; import { property } from '../../utils/lit/decorators.js'; import { staticStyles as componentSpecificStaticStyles } from './index.css.js'; @@ -77,8 +77,7 @@ export class BlrRadioGroup extends LitElementCustom { } }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected firstUpdated(_changedProperties: PropertyValues): void { + protected firstUpdated() { this.handleSlotChange(); } diff --git a/packages/ui-library/src/components/tab-bar-item/index.css.ts b/packages/ui-library/src/components/tab-bar-item/index.css.ts new file mode 100644 index 000000000..5129563fb --- /dev/null +++ b/packages/ui-library/src/components/tab-bar-item/index.css.ts @@ -0,0 +1,231 @@ +import { ComponentThemeIterator } from "../../foundation/_tokens-generated/index.pseudo.generated.js"; +import { css } from "../../utils/css-in-ts/nested-typesafe-css-literals.js"; + +export const staticStyles = css` + ${ComponentThemeIterator((theme, cmp, typeSafeCss) => { + const { TabBar } = cmp; + + return typeSafeCss/* css */ ` + .nav-item-container.${theme} { + display: flex; + flex-direction: column; + justify-content: center; + + &:focus-within:not(.disabled) { + outline: 2px solid black; + outline-offset: -2px; + border-radius: 4px; + } + + .nav-item-underline { + &.selected { + background-color: ${TabBar.Tab.HighlightLine.BackgroundColor.Active.Pressed} + } + } + + .nav-item-content-wrapper { + display: flex; + justify-content: center; + + &:focus-visible { + outline: none; + } + + & > .nav-item { + all: initial; + display: flex; + text-decoration: none; + align-items: center; + flex-shrink: 0; + + &.selected { + + & > blr-icon { + color: ${TabBar.Tab.Icon.IconColor.Active.Rest}; + } + + & > label { + color: ${TabBar.Tab.Label.TextColor.Active.Rest}; + } + + &:focus { + + &:not(.disabled) { + + & > blr-icon { + color: ${TabBar.Tab.Icon.IconColor.Active.Focus}; + } + + & > label { + color: ${TabBar.Tab.Label.TextColor.Active.Focus}; + } + } + } + + &:hover { + + &:not(.disabled) { + + & > blr-icon { + color: ${TabBar.Tab.Icon.IconColor.Active.Hover}; + } + + & > label { + color: ${TabBar.Tab.Label.TextColor.Active.Hover}; + } + } + } + + &:active { + + & > blr-icon { + color: ${TabBar.Tab.Icon.IconColor.Active.Pressed}; + } + + & > label { + color: ${TabBar.Tab.Label.TextColor.Active.Pressed}; + } + } + } + &:not(.selected) { + + & > blr-icon { + color: ${TabBar.Tab.Icon.IconColor.Inactive.Rest}; + } + + & > label { + color: ${TabBar.Tab.Label.TextColor.Inactive.Rest}; + } + + &:focus { + + & > blr-icon { + color + : ${TabBar.Tab.Icon.IconColor.Inactive.Focus}; + } + & > label { + color: ${TabBar.Tab.Label.TextColor.Inactive.Focus}; + } + } + + &:hover { + + & > blr-icon { + color: ${TabBar.Tab.Icon.IconColor.Inactive.Hover}; + } + + & > label { + color: ${TabBar.Tab.Label.TextColor.Inactive.Hover}; + } + } + + &:active { + + & > blr-icon { + color: ${TabBar.Tab.Icon.IconColor.Inactive.Pressed}; + } + + & > label { + color: ${TabBar.Tab.Label.TextColor.Inactive.Pressed}; + } + } + + &.disabled { + + & > blr-icon { + color: ${TabBar.Tab.Icon.IconColor.Inactive.Disabled}; + } + + & > label { + color: ${TabBar.Tab.Label.TextColor.Inactive.Disabled}; + } + } + } + + &:focus-visible { + outline: none; + } + + &.leading { + flex-direction: row; + } + + &.trailing { + flex-direction: row-reverse; + } + } + } + + &.sm { + padding-top: ${TabBar.Tab.ContentCol.PaddingTop.SM}; + gap: ${TabBar.Tab.ContentCol.ItemSpacing.SM}; + + .nav-item-content-wrapper { + & > .nav-item { + padding-left: ${TabBar.Tab.ContentRow.Padding_H.SM}; + padding-right: ${TabBar.Tab.ContentRow.Padding_H.SM}; + gap: ${TabBar.Tab.ContentRow.ItemSpacing.SM}; + line-height: ${TabBar.Tab.Label.Typography.SM.lineHeight}; + + & > blr-icon { + width: ${TabBar.Tab.Icon.IconSize.SM}; + height: ${TabBar.Tab.Icon.IconSize.SM}; + } + } + } + + .nav-item-underline { + height: ${TabBar.Tab.HighlightLine.Height.SM}; + } + } + + &.md { + padding-top: ${TabBar.Tab.ContentCol.PaddingTop.MD}; + gap: ${TabBar.Tab.ContentCol.ItemSpacing.MD}; + + .nav-item-content-wrapper { + + & .nav-item { + padding-left: ${TabBar.Tab.ContentRow.Padding_H.MD}; + padding-right: ${TabBar.Tab.ContentRow.Padding_H.MD}; + gap: ${TabBar.Tab.ContentRow.ItemSpacing.MD}; + line-height: ${TabBar.Tab.Label.Typography.MD.lineHeight}; + + & blr-icon { + width: ${TabBar.Tab.Icon.IconSize.MD}; + height: ${TabBar.Tab.Icon.IconSize.MD}; + } + } + } + + .nav-item-underline { + height: ${TabBar.Tab.HighlightLine.Height.MD}; + } + } + + &.lg { + padding-top: ${TabBar.Tab.ContentCol.PaddingTop.LG}; + gap: ${TabBar.Tab.ContentCol.ItemSpacing.LG}; + + .nav-item-content-wrapper { + & > .nav-item { + padding-left: ${TabBar.Tab.ContentRow.Padding_H.LG}; + padding-right: ${TabBar.Tab.ContentRow.Padding_H.LG}; + gap: ${TabBar.Tab.ContentRow.ItemSpacing.LG}; + line-height: ${TabBar.Tab.Label.Typography.LG.lineHeight}; + + & > blr-icon { + width: ${TabBar.Tab.Icon.IconSize.LG}; + height: ${TabBar.Tab.Icon.IconSize.LG}; + } + } + } + + .nav-item-underline { + height: ${TabBar.Tab.HighlightLine.Height.LG}; + } + } + } + `; + })} +`; diff --git a/packages/ui-library/src/components/tab-bar-item/index.ts b/packages/ui-library/src/components/tab-bar-item/index.ts new file mode 100644 index 000000000..bd74d1188 --- /dev/null +++ b/packages/ui-library/src/components/tab-bar-item/index.ts @@ -0,0 +1,145 @@ +/* eslint-disable lit/binding-positions */ +import { html, nothing } from 'lit'; +import { classMap } from 'lit/directives/class-map.js'; +import { state } from 'lit/decorators.js'; +import { property } from '../../utils/lit/decorators.js'; +import { staticStyles } from './index.css.js'; +import { ThemeType } from '../../foundation/_tokens-generated/index.themes.js'; +import { staticActionStyles } from '../../foundation/semantic-tokens/action.css.js'; +import { IconPositionVariant, TabContentVariantType, FormSizesType, SizesType } from '../../globals/types.js'; +import { calculateIconName } from '../../utils/calculate-icon-name.js'; +import { getComponentConfigToken } from '../../utils/get-component-config-token.js'; +import { BlrIconRenderFunction } from '../icon/renderFunction.js'; +import { createBlrBlurEvent, createBlrChangeEvent, createBlrFocusEvent } from '../../globals/events.js'; +import { LitElementCustom } from '../../utils/lit/element.js'; +import { SignalHub } from '../../utils/lit/signals.js'; + +const TAG_NAME = 'blr-tab-bar-item'; + +/** + * @fires blrFocus BlrTabBarItem received focus + * @fires blrBlur BlrTabBarItem lost focus + */ + +export class BlrTabBarItem extends LitElementCustom implements PublicReactiveProperties { + public declare signals: SignalHub; + static styles = [staticStyles, staticActionStyles]; + + @property() accessor iconPosition: IconPositionVariant = 'leading'; + @property() accessor tabContent: TabContentVariantType = 'labelOnly'; + @property() accessor size: FormSizesType | undefined = 'md'; + @property() accessor disabled = false; + @property() accessor selected: boolean = false; + @property() accessor label = ''; + @property() accessor icon: string | undefined; + @property() accessor theme: ThemeType = 'Light'; + + @state() protected accessor selectedTabIndex: number | undefined; + + protected handleFocus = (event: FocusEvent, isDisabled: boolean) => { + if (!isDisabled) { + this.dispatchEvent(createBlrFocusEvent({ originalEvent: event })); + } + }; + + protected handleBlur = (event: FocusEvent, isDisabled: boolean) => { + if (!isDisabled) { + this.dispatchEvent(createBlrBlurEvent({ originalEvent: event })); + } + }; + + protected handleSelect(event: Event, isDisabled: boolean) { + if (!isDisabled) { + this.selected = true; + this.dispatchEvent(createBlrChangeEvent({ originalEvent: event, changedValue: this.label })); + } + } + + protected render() { + if (this.size) { + const iconSizeVariant = getComponentConfigToken([ + 'cmp', + 'TabBar', + 'Tab', + 'Icon', + 'SizeVariant', + this.size.toUpperCase(), + ]) as SizesType; + + const navListItemContainer = classMap({ + 'disabled': this.disabled, + 'nav-item-container': true, + [this.size || 'md']: this.size || 'md', + [this.iconPosition]: this.iconPosition, + [this.theme]: this.theme, + }); + + const navListItemClasses = classMap({ + 'disabled': this.disabled, + 'nav-item': true, + [this.size || 'md']: this.size || 'md', + [this.iconPosition]: this.iconPosition, + 'selected': this.selected, + }); + const navListItemUnderline = classMap({ + 'nav-item-underline': true, + 'selected': this.selected, + }); + + return html` `; + } + } +} + +if (!customElements.get(TAG_NAME)) { + customElements.define(TAG_NAME, BlrTabBarItem); +} + +export type BlrTabBarItemType = PublicReactiveProperties; + +export type PublicReactiveProperties = { + iconPosition: string; + tabContent: string; + size?: string; + disabled: boolean; + selected: boolean; + label: string; + icon?: string; + theme: ThemeType; +}; diff --git a/packages/ui-library/src/components/tab-bar/index.css.ts b/packages/ui-library/src/components/tab-bar/index.css.ts index f41600dd5..45eb1fbc0 100644 --- a/packages/ui-library/src/components/tab-bar/index.css.ts +++ b/packages/ui-library/src/components/tab-bar/index.css.ts @@ -2,26 +2,14 @@ import { ComponentThemeIterator } from "../../foundation/_tokens-generated/index import { css } from "../../utils/css-in-ts/nested-typesafe-css-literals.js"; export const staticStyles = css` - ${ComponentThemeIterator((theme, cmp, css) => { + ${ComponentThemeIterator((theme, cmp, typeSafeCss) => { const { ButtonIcon } = cmp; const { TabBar } = cmp; - return css` + return typeSafeCss/* css */ ` .panel-wrapper { margin-top: 2rem; - - & > slot { - display: none; - - &.active { - display: block; - } - } - } - - slot { - display: none; - } + } .wrapper-horizontal { position: relative; @@ -34,10 +22,10 @@ export const staticStyles = css` } &.browserOverflow { - padding: 0 1rem; + padding: 0px 1rem; } } - + .blr-tab-bar-group.${theme} { width: 100%; display: flex; @@ -86,6 +74,7 @@ export const staticStyles = css` &.right { margin: ${TabBar.ButtonWrapper.Padding.Trailing.MD}; } + } &.lg { @@ -141,216 +130,6 @@ export const staticStyles = css` overflow-x: visible; padding: 0; } - - .nav-item-container { - display: flex; - flex-direction: column; - justify-content: center; - - &:focus-within:not(.disabled) { - outline: 2px solid black; - outline-offset: -2px; - border-radius: 4px; - } - - .nav-item-underline { - &.selected { - background-color: ${TabBar.Tab.HighlightLine.BackgroundColor.Active.Pressed}; - } - } - - .nav-item-content-wrapper { - display: flex; - justify-content: center; - - &:focus-visible { - outline: none; - } - - & > .nav-item { - all: initial; - display: flex; - text-decoration: none; - align-items: center; - flex-shrink: 0; - - &.selected { - & > blr-icon { - color: ${TabBar.Tab.Icon.IconColor.Active.Rest}; - } - - & > label { - color: ${TabBar.Tab.Label.TextColor.Active.Rest}; - } - - &:focus { - &:not(.disabled) { - & > blr-icon { - color: ${TabBar.Tab.Icon.IconColor.Active.Focus}; - } - - & > label { - color: ${TabBar.Tab.Label.TextColor.Active.Focus}; - } - } - } - - &:hover { - &:not(.disabled) { - & > blr-icon { - color: ${TabBar.Tab.Icon.IconColor.Active.Hover}; - } - - & > label { - color: ${TabBar.Tab.Label.TextColor.Active.Hover}; - } - } - } - - &:active { - & > blr-icon { - color: ${TabBar.Tab.Icon.IconColor.Active.Pressed}; - } - - & > label { - color: ${TabBar.Tab.Label.TextColor.Active.Pressed}; - } - } - } - - &:not(.selected) { - & > blr-icon { - color: ${TabBar.Tab.Icon.IconColor.Inactive.Rest}; - } - - & > label { - color: ${TabBar.Tab.Label.TextColor.Inactive.Rest}; - } - - &:focus { - & > blr-icon { - color: ${TabBar.Tab.Icon.IconColor.Inactive.Focus}; - } - - & > label { - color: ${TabBar.Tab.Label.TextColor.Inactive.Focus}; - } - } - - &:hover { - & > blr-icon { - color: ${TabBar.Tab.Icon.IconColor.Inactive.Hover}; - } - - & > label { - color: ${TabBar.Tab.Label.TextColor.Inactive.Hover}; - } - } - - &:active { - & > blr-icon { - color: ${TabBar.Tab.Icon.IconColor.Inactive.Pressed}; - } - - & > label { - color: ${TabBar.Tab.Label.TextColor.Inactive.Pressed}; - } - } - - &.disabled { - & > blr-icon { - color: ${TabBar.Tab.Icon.IconColor.Inactive.Disabled}; - } - - & > label { - color: ${TabBar.Tab.Label.TextColor.Inactive.Disabled}; - } - } - } - - &:focus-visible { - outline: none; - } - - &.leading { - flex-direction: row; - } - - &.trailing { - flex-direction: row-reverse; - } - } - } - - &.sm { - padding-top: ${TabBar.Tab.ContentCol.PaddingTop.SM}; - gap: ${TabBar.Tab.ContentCol.ItemSpacing.SM}; - - .nav-item-content-wrapper { - & > .nav-item { - padding-left: ${TabBar.Tab.ContentRow.Padding_H.SM}; - padding-right: ${TabBar.Tab.ContentRow.Padding_H.SM}; - gap: ${TabBar.Tab.ContentRow.ItemSpacing.SM}; - line-height: ${TabBar.Tab.Label.Typography.SM.lineHeight}; - - & > blr-icon { - width: ${TabBar.Tab.Icon.IconSize.SM}; - height: ${TabBar.Tab.Icon.IconSize.SM}; - } - } - } - - .nav-item-underline { - height: ${TabBar.Tab.HighlightLine.Height.SM}; - } - } - - &.md { - padding-top: ${TabBar.Tab.ContentCol.PaddingTop.MD}; - gap: ${TabBar.Tab.ContentCol.ItemSpacing.MD}; - - .nav-item-content-wrapper { - & .nav-item { - padding-left: ${TabBar.Tab.ContentRow.Padding_H.MD}; - padding-right: ${TabBar.Tab.ContentRow.Padding_H.MD}; - gap: ${TabBar.Tab.ContentRow.ItemSpacing.MD}; - line-height: ${TabBar.Tab.Label.Typography.MD.lineHeight}; - - & blr-icon { - width: ${TabBar.Tab.Icon.IconSize.MD}; - height: ${TabBar.Tab.Icon.IconSize.MD}; - } - } - } - - .nav-item-underline { - height: ${TabBar.Tab.HighlightLine.Height.MD}; - } - } - - &.lg { - padding-top: ${TabBar.Tab.ContentCol.PaddingTop.LG}; - gap: ${TabBar.Tab.ContentCol.ItemSpacing.LG}; - - .nav-item-content-wrapper { - & > .nav-item { - padding-left: ${TabBar.Tab.ContentRow.Padding_H.LG}; - padding-right: ${TabBar.Tab.ContentRow.Padding_H.LG}; - gap: ${TabBar.Tab.ContentRow.ItemSpacing.LG}; - line-height: ${TabBar.Tab.Label.Typography.LG.lineHeight}; - - & > blr-icon { - width: ${TabBar.Tab.Icon.IconSize.LG}; - height: ${TabBar.Tab.Icon.IconSize.LG}; - } - } - } - - .nav-item-underline { - height: ${TabBar.Tab.HighlightLine.Height.LG}; - } - } - } } } } diff --git a/packages/ui-library/src/components/tab-bar/index.stories.ts b/packages/ui-library/src/components/tab-bar/index.stories.ts index 778d3db62..1cdb91438 100644 --- a/packages/ui-library/src/components/tab-bar/index.stories.ts +++ b/packages/ui-library/src/components/tab-bar/index.stories.ts @@ -14,6 +14,7 @@ import { Themes } from '../../foundation/_tokens-generated/index.themes.js'; // this loads the all components instances and registers their html tags import '../../index.js'; +import { action } from '@storybook/addon-actions'; export default { title: 'Components/TabBar', @@ -65,17 +66,17 @@ export default { }; const tabsAsChildren = html` -

Tab 1

-

Tab 2

-

Tab 3

-

Tab 4

-

Tab 5

-

Tab 6

-

Tab 7

-

Tab 8

-

Tab 9

-

Tab 10

-

Tab 11

+ Tab 1 + e} @blrBlur=${(e) => e}>Tab 2 + Tab 3 + Tab 4 + Tab 5 + Tab 6 + Tab 7 + Tab 8 + Tab 9 + Tab 10 + Tab 11 `; export const BlrTabBar = (params: BlrTabBarType) => BlrTabBarRenderFunction(params, tabsAsChildren); @@ -92,6 +93,7 @@ const args: BlrTabBarType = { tabContent: 'labelAndIcon', iconPosition: 'leading', alignment: 'left', + blrSelectedValueChange: () => action('blrSelectedValueChange'), }; BlrTabBar.args = args; diff --git a/packages/ui-library/src/components/tab-bar/index.test.ts b/packages/ui-library/src/components/tab-bar/index.test.ts index ba22677d3..478284419 100644 --- a/packages/ui-library/src/components/tab-bar/index.test.ts +++ b/packages/ui-library/src/components/tab-bar/index.test.ts @@ -3,7 +3,7 @@ import '@boiler/ui-library'; import { BlrTabBarRenderFunction } from './renderFunction.js'; import { fixture, expect } from '@open-wc/testing'; -import { querySelectorAllDeep, querySelectorDeep } from 'query-selector-shadow-dom'; +import { querySelectorDeep } from 'query-selector-shadow-dom'; import { html } from 'lit-html'; import { BlrTabBarType } from './index.js'; @@ -19,17 +19,17 @@ const sampleParams: BlrTabBarType = { }; const tabsAsChildren = html` -

Tab 1

-

Tab 2

-

Tab 3

-

Tab 4

-

Tab 5

-

Tab 6

-

Tab 7

-

Tab 8

-

Tab 9

-

Tab 10

-

Tab 11

+ Tab 1 + Tab 2 + Tab 3 + Tab 4 + Tab 5 + Tab 6 + Tab 7 + Tab 8 + Tab 9 + Tab 10 + Tab 11 `; describe('blr-tab-bar', () => { @@ -57,11 +57,4 @@ describe('blr-tab-bar', () => { const className = tabBar?.className; expect(className).to.contain('sm'); }); - - it('is rendering tabs inside slot', async () => { - const element = await fixture(BlrTabBarRenderFunction({ ...sampleParams, size: 'sm' }, tabsAsChildren)); - const tabs = querySelectorAllDeep('.nav-item-container', element?.getRootNode() as HTMLElement); - const tabsLength = tabsAsChildren.strings[0].trim().split('

').filter(Boolean).length; - expect(tabs).to.be.lengthOf(tabsLength); - }); }); diff --git a/packages/ui-library/src/components/tab-bar/index.ts b/packages/ui-library/src/components/tab-bar/index.ts index 8573f33ed..66c4335a3 100644 --- a/packages/ui-library/src/components/tab-bar/index.ts +++ b/packages/ui-library/src/components/tab-bar/index.ts @@ -1,10 +1,9 @@ /* eslint-disable lit/binding-positions */ import { html, nothing } from 'lit'; import { classMap } from 'lit/directives/class-map.js'; -import { query, queryAll, state } from 'lit/decorators.js'; +import { query, state } from 'lit/decorators.js'; import { property } from '../../utils/lit/decorators.js'; import { staticStyles } from './index.css.js'; - import { TAG_NAME } from './renderFunction.js'; import { ThemeType } from '../../foundation/_tokens-generated/index.themes.js'; import { staticActionStyles } from '../../foundation/semantic-tokens/action.css.js'; @@ -22,8 +21,14 @@ import { calculateIconName } from '../../utils/calculate-icon-name.js'; import { getComponentConfigToken } from '../../utils/get-component-config-token.js'; import { BlrDividerRenderFunction } from '../divider/renderFunction.js'; import { BlrIconRenderFunction } from '../icon/renderFunction.js'; -import { createBlrBlurEvent, createBlrChangeEvent, createBlrFocusEvent } from '../../globals/events.js'; import { LitElementCustom, ElementInterface } from '../../utils/lit/element.js'; +import { BlrTabBarItem } from '../tab-bar-item/index.js'; +import { batch, Signal } from '@lit-labs/preact-signals'; +import { createBlrSelectedValueChangeEvent } from '../../globals/events.js'; + +/** + * @fires blrSelectedValueChange TabBar selected value changed + */ export class BlrTabBar extends LitElementCustom { static styles = [staticStyles, staticActionStyles]; @@ -31,15 +36,6 @@ export class BlrTabBar extends LitElementCustom { @query('.blr-tab-bar') protected accessor _navList!: HTMLElement; - @queryAll('.nav-list li') - protected accessor _navItems!: NodeList; - - @queryAll('slot[name=tab]') - protected accessor _navItemsSlots!: NodeList; - - @queryAll('[role=tabpanel]') - protected accessor _panels!: NodeList; - @property() accessor overflowVariantStandard!: OverflowVariantTypeStandard; @property() accessor overflowVariantFullWidth!: OverflowVariantTypeFullWidth; @property() accessor iconPosition: IconPositionVariant = 'leading'; @@ -47,17 +43,12 @@ export class BlrTabBar extends LitElementCustom { @property() accessor tabContent: TabContentVariantType = 'labelOnly'; @property() accessor alignment: TabAlignmentVariantType = 'left'; @property() accessor size: FormSizesType | undefined = 'md'; - @property() accessor onChange: HTMLElement['oninput'] | undefined; - @property() accessor onBlur: HTMLElement['blur'] | undefined; - @property() accessor onFocus: HTMLElement['focus'] | undefined; @property() accessor showDivider = true; - @property() accessor onClick: HTMLButtonElement['onclick'] | undefined; - @property() accessor theme: ThemeType = 'Light'; - @state() protected accessor selectedTabIndex: number | undefined; - - protected _tabBarElements: Element[] | undefined; + @state() protected accessor _selectedTab: BlrTabBarItem | undefined; + protected _tabBarElements: BlrTabBarItem[] = []; + private _tabBarSelectedSignalSubscriptionDisposers: ReturnType[] = []; protected scrollTab = (direction: string, speed: number, distance: number) => { let scrollAmount = 0; @@ -74,38 +65,64 @@ export class BlrTabBar extends LitElementCustom { }, speed); }; - protected handleFocus = (event: FocusEvent, isDisabled: boolean) => { - if (!isDisabled) { - this.dispatchEvent(createBlrFocusEvent({ originalEvent: event })); - } - }; - - protected handleBlur = (event: FocusEvent, isDisabled: boolean) => { - if (!isDisabled) { - this.dispatchEvent(createBlrBlurEvent({ originalEvent: event })); - } - }; - - protected handleSelect(event: Event, index: number | undefined, isDisabled: boolean) { - if (!isDisabled) { - this.selectedTabIndex = index; - const changedTab = this._tabBarElements![this.selectedTabIndex!].getAttribute('label'); - this.dispatchEvent(createBlrChangeEvent({ originalEvent: event, changedValue: changedTab })); - } - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars protected firstUpdated(...args: Parameters): void { - if (!this._tabBarElements) { - this.handleSlotChange(); - } + this.handleSlotChange(); } + protected handleTabBarSelectedSignal = (target: BlrTabBarItem, value?: boolean) => { + const selectedTab: BlrTabBarItem | undefined = value + ? target + : target === this._selectedTab && !value + ? undefined + : this._selectedTab; + + batch(() => { + this._tabBarElements?.forEach((tab) => { + if (tab !== selectedTab) { + tab.selected = false; + } + }); + }); + + if (this._selectedTab !== selectedTab) { + this.dispatchEvent( + createBlrSelectedValueChangeEvent({ selectedValue: (selectedTab)?.label ?? '' }) + ); + this._selectedTab = selectedTab; + } + }; + protected handleSlotChange() { + // Cleanup signal listeners from previously slotted elements + this._tabBarSelectedSignalSubscriptionDisposers.forEach((cancelSubscription) => cancelSubscription()); const slot = this.renderRoot?.querySelector('slot'); - this._tabBarElements = slot?.assignedElements({ flatten: false }); - this.requestUpdate(); + this._tabBarElements = slot?.assignedElements({ flatten: false }) as BlrTabBarItem[]; + + // Add signal listeners to newly slotted elements + this._tabBarElements.forEach((item) => { + if (item instanceof BlrTabBarItem === false) { + throw new Error('child component of blr-tab-bar must be blr-tab-bar-item'); + } + + item.theme = this.theme; + item.size = this.size; + item.tabContent = this.tabContent; + item.iconPosition = this.iconPosition; + + this._tabBarSelectedSignalSubscriptionDisposers.push( + item.signals.selected.subscribe((value) => this.handleTabBarSelectedSignal(item, value)) + ); + }); + } + + protected handleTabSelect(currentlySelected: string) { + this._tabBarElements?.forEach((item: BlrTabBarItem) => { + if (item.label !== currentlySelected && item.selected) { + item.selected = false; + } + }); } protected render() { @@ -128,15 +145,6 @@ export class BlrTabBar extends LitElementCustom { [this.alignment]: this.alignment, }); - const iconSizeVariant = getComponentConfigToken([ - 'cmp', - 'TabBar', - 'Tab', - 'Icon', - 'SizeVariant', - this.size.toUpperCase(), - ]) as SizesType; - const buttonIconSizeVariant = getComponentConfigToken([ 'cmp', 'ButtonIcon', @@ -165,71 +173,6 @@ export class BlrTabBar extends LitElementCustom {
${this.overflowVariantStandard === 'buttons' @@ -257,18 +200,17 @@ export class BlrTabBar extends LitElementCustom { }) : nothing} - ${this._tabBarElements?.map((tab, index) => { - return index === this.selectedTabIndex - ? html`
-

${tab.getAttribute('label')}

-
` - : nothing; - })}`; + + ${this._selectedTab + ? html`
+

${this._selectedTab.getAttribute('label')}

+
` + : nothing}`; } } } diff --git a/packages/ui-library/src/index.ts b/packages/ui-library/src/index.ts index cc14f1179..81ece14ba 100644 --- a/packages/ui-library/src/index.ts +++ b/packages/ui-library/src/index.ts @@ -45,6 +45,8 @@ export { BlrFormLabel } from './components/form-label/index.js'; // Navigation export { BlrTabBar } from './components/tab-bar/index.js'; +export { BlrTabBarItem } from './components/tab-bar-item/index.js'; + // UI export { BlrButtonGroup } from './components/button-group/index.js';