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`
+
+
this.handleFocus(e, this.disabled)}
+ @blur=${(e: FocusEvent) => this.handleBlur(e, this.disabled)}
+ @click=${(e: Event) => this.handleSelect(e, this.disabled)}
+ @keydown=${(event: KeyboardEvent) => {
+ if (event.code === 'Space') {
+ this.handleSelect(event, this.disabled);
+ }
+ }}
+ tabindex=${this.disabled ? '-1' : 0}
+ >
+ ${this.tabContent !== 'labelOnly' && this.icon // TODO: Make the this.tabContent !== 'labelOnly' logic work
+ ? BlrIconRenderFunction(
+ {
+ icon: calculateIconName(this.icon!, iconSizeVariant),
+ sizeVariant: iconSizeVariant,
+ },
+ {
+ 'aria-hidden': true,
+ }
+ )
+ : nothing}
+ ${this.tabContent !== 'iconOnly'
+ ? html` `
+ : nothing}
+
+
+
+ `;
+ }
+ }
+}
+
+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._tabBarElements?.map((tab: Element, index) => {
- const isDisabled = tab.hasAttribute('disabled') || tab.getAttribute('disabled') === 'true';
-
- const navListItemClasses = classMap({
- 'disabled': isDisabled,
- 'nav-item': true,
- [this.size || 'md']: this.size || 'md',
- [this.iconPosition]: this.iconPosition,
- 'selected': index === this.selectedTabIndex,
- });
-
- const navListItemContainer = classMap({
- 'disabled': tab.getAttribute('disabled') === 'true',
- 'nav-item-container': true,
- [this.size || 'md']: this.size || 'md',
- [this.iconPosition]: this.iconPosition,
- });
-
- const navListItemUnderline = classMap({
- 'nav-item-underline': true,
- 'selected': index === this.selectedTabIndex,
- });
-
- return html`
- -
-
-
this.handleFocus(e, isDisabled)}
- @blur=${(e: FocusEvent) => this.handleFocus(e, isDisabled)}
- @click=${(e: Event) => this.handleSelect(e, index, isDisabled)}
- @keydown=${(event: KeyboardEvent) => {
- if (event.code === 'Space') {
- this.handleSelect(event, index, isDisabled);
- }
- }}
- tabindex=${isDisabled ? '-1' : index}
- >
- ${this.tabContent !== 'labelOnly' && tab.hasAttribute('icon')
- ? BlrIconRenderFunction(
- {
- icon: calculateIconName(tab.getAttribute('icon')!, iconSizeVariant),
- sizeVariant: iconSizeVariant,
- },
- {
- 'aria-hidden': true,
- }
- )
- : nothing}
- ${this.tabContent !== 'iconOnly'
- ? html` `
- : nothing}
-
-
-
-
- `;
- })}
${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';