Diam maecenas ultricies mi eget mauris pharetra et ultrices neque ornare aenean euismod
@@ -143,16 +143,25 @@ const amount: InputType = {
},
};
+const size: InputType = {
+ control: {
+ type: 'inline-radio',
+ },
+ options: ['l', 'xl'],
+};
+
const basicArgTypes: ArgTypes = {
label,
'icon-name': iconName,
amount: amount,
+ size: size,
};
const basicArgs: Args = {
label: 'Tab label one',
'icon-name': undefined,
amount: undefined,
+ size: size.options[0],
};
const templateRes = [
@@ -164,20 +173,34 @@ const templateRes = [
withActions as Decorator,
];
-export const defaultTabs: StoryObj = {
+export const defaultTabsSizeL: StoryObj = {
render: DefaultTemplate,
argTypes: basicArgTypes,
args: { ...basicArgs },
decorators: templateRes,
};
-export const numbersAndIcons: StoryObj = {
+export const numbersAndIconsSizeL: StoryObj = {
render: IconsAndNumbersTemplate,
argTypes: basicArgTypes,
args: { ...basicArgs, amount: 16, 'icon-name': iconName.options[0] },
decorators: templateRes,
};
+export const defaultTabsSizeXL: StoryObj = {
+ render: DefaultTemplate,
+ argTypes: basicArgTypes,
+ args: { ...basicArgs, size: size.options[1] },
+ decorators: templateRes,
+};
+
+export const numbersAndIconsSizeXL: StoryObj = {
+ render: IconsAndNumbersTemplate,
+ argTypes: basicArgTypes,
+ args: { ...basicArgs, amount: 16, 'icon-name': iconName.options[0], size: size.options[1] },
+ decorators: templateRes,
+};
+
export const nestedTabGroups: StoryObj = {
render: NestedTemplate,
argTypes: basicArgTypes,
diff --git a/src/components/sbb-tab-group/sbb-tab-group.tsx b/src/components/sbb-tab-group/sbb-tab-group.tsx
index 027cf7e505..e7e4cffaa2 100644
--- a/src/components/sbb-tab-group/sbb-tab-group.tsx
+++ b/src/components/sbb-tab-group/sbb-tab-group.tsx
@@ -10,10 +10,11 @@ import {
Listen,
Method,
Prop,
+ Watch,
} from '@stencil/core';
import { InterfaceSbbTabGroupTab } from './sbb-tab-group.custom';
import { isArrowKeyPressed, getNextElementIndex, interactivityChecker } from '../../global/a11y';
-import { isValidAttribute, hostContext } from '../../global/dom';
+import { isValidAttribute, hostContext, toggleDatasetEntry } from '../../global/dom';
import { throttle } from '../../global/eventing';
import { AgnosticMutationObserver, AgnosticResizeObserver } from '../../global/observers';
@@ -40,25 +41,41 @@ let nextId = 0;
tag: 'sbb-tab-group',
})
export class SbbTabGroup implements ComponentInterface {
- public tabs: InterfaceSbbTabGroupTab[] = [];
+ private _tabs: InterfaceSbbTabGroupTab[] = [];
private _selectedTab: InterfaceSbbTabGroupTab;
private _isNested: boolean;
+ private _tabGroupElement: HTMLElement;
private _tabContentElement: HTMLElement;
- private _tabAttributeObserver = new AgnosticMutationObserver(
- this._onTabAttributesChange.bind(this),
+ private _tabAttributeObserver = new AgnosticMutationObserver((mutationsList) =>
+ this._onTabAttributesChange(mutationsList),
);
- private _tabContentResizeObserver = new AgnosticResizeObserver(
- this._onTabContentElementResize.bind(this),
+ private _tabGroupResizeObserver = new AgnosticResizeObserver((entries) =>
+ this._onTabGroupElementResize(entries),
+ );
+ private _tabContentResizeObserver = new AgnosticResizeObserver((entries) =>
+ this._onTabContentElementResize(entries),
);
@Element() private _element: HTMLElement;
+ /**
+ * Size variant, either l or xl.
+ */
+ @Prop() public size: InterfaceSbbTabGroupTab['size'] = 'l';
+
/**
* Sets the initial tab. If it matches a disabled tab or exceeds the length of
* the tab group, the first enabled tab will be selected.
*/
@Prop() public initialSelectedIndex = 0;
+ @Watch('size')
+ public updateSize(): void {
+ for (const tab of this._tabs) {
+ tab.setAttribute('data-size', this.size);
+ }
+ }
+
/**
* Emits an event on selected tab change
*/
@@ -73,7 +90,7 @@ export class SbbTabGroup implements ComponentInterface {
*/
@Method()
public async disableTab(tabIndex: number): Promise
{
- this.tabs[tabIndex]?.tabGroupActions.disable();
+ this._tabs[tabIndex]?.tabGroupActions.disable();
}
/**
@@ -82,7 +99,7 @@ export class SbbTabGroup implements ComponentInterface {
*/
@Method()
public async enableTab(tabIndex: number): Promise {
- this.tabs[tabIndex]?.tabGroupActions.enable();
+ this._tabs[tabIndex]?.tabGroupActions.enable();
}
/**
@@ -91,7 +108,7 @@ export class SbbTabGroup implements ComponentInterface {
*/
@Method()
public async activateTab(tabIndex: number): Promise {
- this.tabs[tabIndex]?.tabGroupActions.select();
+ this._tabs[tabIndex]?.tabGroupActions.select();
}
private _getTabs(): InterfaceSbbTabGroupTab[] {
@@ -101,7 +118,7 @@ export class SbbTabGroup implements ComponentInterface {
}
private get _enabledTabs(): InterfaceSbbTabGroupTab[] {
- return this.tabs.filter(
+ return this._tabs.filter(
(t) => !isValidAttribute(t, 'disabled') && interactivityChecker.isVisible(t),
);
}
@@ -111,24 +128,26 @@ export class SbbTabGroup implements ComponentInterface {
}
public componentDidLoad(): void {
- this.tabs = this._getTabs();
- this.tabs.forEach((tab) => this._configure(tab));
+ this._tabs = this._getTabs();
+ this._tabs.forEach((tab) => this._configure(tab));
this._initSelection();
+ this._tabGroupResizeObserver.observe(this._tabGroupElement);
}
public disconnectedCallback(): void {
this._tabAttributeObserver.disconnect();
this._tabContentResizeObserver.disconnect();
+ this._tabGroupResizeObserver.disconnect();
}
private _onContentSlotChange = (): void => {
this._tabContentElement = this._element.shadowRoot.querySelector('div.tab-content');
- const loadedTabs = this._getTabs().filter((tab) => !this.tabs.includes(tab));
+ const loadedTabs = this._getTabs().filter((tab) => !this._tabs.includes(tab));
// if a new tab/content is added to the tab group
if (loadedTabs.length) {
loadedTabs.forEach((tab) => this._configure(tab));
- this.tabs = this.tabs.concat(loadedTabs);
+ this._tabs = this._tabs.concat(loadedTabs);
}
};
@@ -136,14 +155,15 @@ export class SbbTabGroup implements ComponentInterface {
const tabs = this._getTabs();
// if a tab is removed from the tab group
- if (tabs.length < this.tabs.length) {
- const removedTabs = this.tabs.filter((tab) => !tabs.includes(tab));
+ if (tabs.length < this._tabs.length) {
+ const removedTabs = this._tabs.filter((tab) => !tabs.includes(tab));
removedTabs.forEach((removedTab) => {
removedTab.relatedContent?.remove();
});
- this.tabs = tabs;
+ this._tabs = tabs;
}
+ this._tabs.forEach((tab: HTMLSbbTabTitleElement) => tab.setAttribute('data-size', this.size));
};
private _assignId(): string {
@@ -153,10 +173,10 @@ export class SbbTabGroup implements ComponentInterface {
private _initSelection(): void {
if (
this.initialSelectedIndex >= 0 &&
- this.initialSelectedIndex < this.tabs.length &&
- !this.tabs[this.initialSelectedIndex].disabled
+ this.initialSelectedIndex < this._tabs.length &&
+ !this._tabs[this.initialSelectedIndex].disabled
) {
- this.tabs[this.initialSelectedIndex].tabGroupActions.select();
+ this._tabs[this.initialSelectedIndex].tabGroupActions.select();
} else {
this._enabledTabs[0]?.tabGroupActions.select();
}
@@ -186,6 +206,26 @@ export class SbbTabGroup implements ComponentInterface {
}
}
+ private _onTabGroupElementResize(entries: ResizeObserverEntry[]): void {
+ for (const entry of entries) {
+ const tabTitles = (
+ entry.target.firstElementChild as HTMLSlotElement
+ ).assignedElements() as HTMLSbbTabTitleElement[];
+
+ for (const tab of tabTitles) {
+ toggleDatasetEntry(
+ tab,
+ 'hasDivider',
+ tab === tabTitles[0] || tab.offsetLeft === tabTitles[0].offsetLeft,
+ );
+ this._element.style.setProperty(
+ '--sbb-tab-group-width',
+ `${this._tabGroupElement.clientWidth}px`,
+ );
+ }
+ }
+ }
+
private _onTabContentElementResize(entries: ResizeObserverEntry[]): void {
for (const entry of entries) {
const contentHeight = Math.floor(entry.contentRect.height);
@@ -309,7 +349,7 @@ export class SbbTabGroup implements ComponentInterface {
public render(): JSX.Element {
return (
-
+
(this._tabGroupElement = el)}>
diff --git a/src/components/sbb-tab-title/sbb-tab-title.scss b/src/components/sbb-tab-title/sbb-tab-title.scss
index 10d8d400e2..5a9272a44d 100644
--- a/src/components/sbb-tab-title/sbb-tab-title.scss
+++ b/src/components/sbb-tab-title/sbb-tab-title.scss
@@ -5,17 +5,14 @@
@include sbb.host-component-properties;
:host {
- --sbb-tab-title-height: #{sbb.px-to-rem-build(40)};
- --sbb-tab-title-border-radius: var(--sbb-border-radius-infinity);
- --sbb-tab-title-color: var(--sbb-color-charcoal-default);
+ --sbb-tab-title-height: var(--sbb-spacing-fixed-12x);
+ --sbb-tab-title-color: var(--sbb-color-granite-default);
--sbb-tab-title-icon-color: var(--sbb-color-black-default);
--sbb-tab-title-background-color: var(--sbb-color-white-default);
- --sbb-tab-title-border-color: var(--sbb-color-cloud-default);
--sbb-tab-title-cursor: pointer;
--sbb-tab-title-pointer-events: unset;
- --sbb-tab-title-shift: translateY(0);
--sbb-tab-title-inset: 0;
- --sbb-tab-title-gap: var(--sbb-spacing-fixed-2x);
+ --sbb-tab-title-marker-transform: scale(0);
--sbb-tab-title-text-decoration: none;
--sbb-tab-title-animation-duration: var(--sbb-animation-duration-2x);
--sbb-tab-title-animation-easing: var(--sbb-animation-easing);
@@ -23,83 +20,79 @@
display: inline-block;
max-width: 100%;
+ pointer-events: var(--sbb-tab-title-pointer-events);
-webkit-tap-highlight-color: transparent;
// Use !important here to not interfere with Firefox focus ring definition
// which appears in normalize css of several frameworks.
outline: none !important;
+ @include sbb.mq($from: medium) {
+ --sbb-tab-title-height: var(--sbb-spacing-fixed-14x);
+ }
+
@include sbb.if-forced-colors {
--sbb-tab-title-color: ButtonText;
--sbb-tab-title-icon-color: ButtonText;
--sbb-tab-title-amount-color: ButtonText;
- --sbb-tab-title-border-color: CanvasText;
}
}
:host([disabled]:not([disabled='false'])) {
- --sbb-tab-title-color: var(--sbb-color-granite-default);
--sbb-tab-title-icon-color: var(--sbb-color-granite-default);
--sbb-tab-title-background-color: var(--sbb-color-milk-default);
- --sbb-tab-title-border-color: var(--sbb-color-cloud-default);
--sbb-tab-title-cursor: unset;
--sbb-tab-title-pointer-events: none;
- --sbb-tab-title-amount-color: var(--sbb-color-granite-default);
--sbb-tab-title-text-decoration: line-through;
@include sbb.if-forced-colors {
--sbb-tab-title-color: GrayText;
--sbb-tab-title-icon-color: GrayText;
--sbb-tab-title-amount-color: GrayText;
- --sbb-tab-title-border-color: GrayText;
}
}
// If active and not disabled
:host([active]:not([active='false'], [disabled]:not([disabled='false']))) {
- --sbb-tab-title-color: var(--sbb-color-white-default);
+ --sbb-tab-title-color: var(--sbb-color-charcoal-default);
--sbb-tab-title-icon-color: var(--sbb-tab-title-color);
--sbb-tab-title-background-color: var(--sbb-color-black-default);
- --sbb-tab-title-border-color: var(--sbb-color-black-default);
- --sbb-tab-title-amount-color: var(--sbb-color-cement-default);
--sbb-tab-title-cursor: unset;
--sbb-tab-title-pointer-events: none;
+ --sbb-tab-title-marker-transform: scale(1);
@include sbb.if-forced-colors {
--sbb-tab-title-color: ButtonText;
--sbb-tab-title-icon-color: ButtonText;
--sbb-tab-title-amount-color: ButtonText;
- --sbb-tab-title-border-color: Highlight;
}
}
-// Hover if not disabled, not active and not pressed
-:host(
- :hover:not(
- [disabled]:not([disabled='false']),
- [data-active],
- :active,
- [active]:not([active='false'])
- )
- ) {
+:host(:hover:not([disabled]:not([disabled='false']))) {
@include sbb.hover-mq($hover: true) {
- --sbb-tab-title-shift: translateY(calc(-1 * #{sbb.px-to-rem-build(1)}));
- --sbb-tab-title-inset: calc(-1 * #{sbb.px-to-rem-build(2)});
- --sbb-tab-title-background-color: var(--sbb-color-milk-default);
-
- @include sbb.if-forced-colors {
- --sbb-tab-title-border-color: Highlight;
- }
+ --sbb-tab-title-marker-transform: scale(1);
}
}
// Pressed/active state
:host(:is([data-active], :active)) {
- --sbb-tab-title-border-color: var(--sbb-color-black-default);
- --sbb-tab-title-background-color: var(--sbb-color-white-default);
+ --sbb-tab-title-color: var(--sbb-color-charcoal-default);
+}
- @include sbb.if-forced-colors {
- --sbb-tab-title-border-color: Highlight;
+.sbb-tab-title__wrapper {
+ position: relative;
+
+ // Hide focus outline when focus origin is mouse or touch. This is being used in tooltip as a workaround.
+ :host(:focus-visible:not([data-focus-origin='mouse'], [data-focus-origin='touch'])) & {
+ &::before {
+ content: '';
+ position: absolute;
+ display: block;
+ inset: calc((var(--sbb-focus-outline-width) + var(--sbb-focus-outline-offset)) * -1);
+ border: var(--sbb-focus-outline-width) solid var(--sbb-focus-outline-color);
+ border-radius: var(--sbb-border-radius-2x);
+ z-index: 1;
+ }
}
}
@@ -109,66 +102,79 @@
min-height: var(--sbb-tab-title-height);
display: flex;
align-items: center;
- padding-block: var(--sbb-spacing-fixed-2x);
- padding-inline: var(--sbb-spacing-fixed-5x);
- gap: var(--sbb-tab-title-gap);
+ padding-inline: var(--sbb-spacing-responsive-xs);
+ gap: var(--sbb-spacing-fixed-2x);
user-select: none;
cursor: var(--sbb-tab-title-cursor);
transition: color var(--sbb-tab-title-animation-duration) var(--sbb-tab-title-animation-easing);
color: var(--sbb-tab-title-icon-color);
- pointer-events: var(--sbb-tab-title-pointer-events);
+
+ // Show a border under the tab-group and between flex rows when the tab titles wrap to a new line
+ :host([data-has-divider]) & {
+ &::after {
+ content: '';
+ position: absolute;
+ inset-inline-start: 0;
+ inset-block-end: 0;
+ width: var(--sbb-tab-group-width);
+ height: var(--sbb-border-width-1x);
+ background-color: var(--sbb-color-cloud-default);
+ }
+ }
&::before {
position: absolute;
content: '';
- inset: var(--sbb-tab-title-inset);
- border: var(--sbb-border-width-1x) solid var(--sbb-tab-title-border-color);
- border-radius: var(--sbb-tab-title-border-radius);
- background-color: var(--sbb-tab-title-background-color);
- transition-duration: var(--sbb-tab-title-animation-duration);
- transition-timing-function: var(--sbb-tab-title-animation-easing);
- transition-property: inset, background-color, border-color, box-shadow;
-
- // Hide focus outline when focus origin is mouse or touch. This is being used in tooltip as a workaround.
- :host(:focus-visible:not([data-focus-origin='mouse'], [data-focus-origin='touch'])) & {
- @include sbb.focus-outline;
+ inset-inline: 0;
+ inset-block-end: 0;
+ height: #{sbb.px-to-rem-build(3)};
+ background-color: var(--sbb-tab-title-color);
+ transform: var(--sbb-tab-title-marker-transform);
+ transition: {
+ duration: var(--sbb-tab-title-animation-duration);
+ timing-function: var(--sbb-tab-title-animation-easing);
+ property: transform, background-color;
}
- @include sbb.if-forced-colors {
- border-width: var(--sbb-border-width-2x);
- }
+ z-index: 1;
}
}
+.sbb-tab-title__icon,
+.sbb-tab-title__text,
+.sbb-tab-title__amount {
+ text-decoration: var(--sbb-tab-title-text-decoration);
+}
+
.sbb-tab-title__icon {
display: flex;
flex-shrink: 0;
+ color: var(--sbb-tab-title-color);
+ transition: color var(--sbb-tab-title-animation-duration) var(--sbb-tab-title-animation-easing);
}
.sbb-tab-title__text {
- @include sbb.text-xs--bold;
+ @include sbb.text-m--bold;
@include sbb.font-smoothing;
@include sbb.ellipsis;
+ :host([data-size='xl']) & {
+ @include sbb.text-xl--bold;
+ }
+
color: var(--sbb-tab-title-color);
transition: color var(--sbb-tab-title-animation-duration) var(--sbb-tab-title-animation-easing);
}
.sbb-tab-title__amount {
- @include sbb.text-xs--regular;
+ @include sbb.text-m--regular;
@include sbb.font-smoothing;
+ :host([data-size='xl']) & {
+ @include sbb.text-xl--regular;
+ }
+
display: flex;
color: var(--sbb-tab-title-amount-color);
transition: color var(--sbb-tab-title-animation-duration) var(--sbb-tab-title-animation-easing);
}
-
-.sbb-tab-title__icon,
-.sbb-tab-title__text,
-.sbb-tab-title__amount {
- transition: transform var(--sbb-tab-title-animation-duration)
- var(--sbb-tab-title-animation-easing);
- transform: var(--sbb-tab-title-shift);
- will-change: transform;
- text-decoration: var(--sbb-tab-title-text-decoration);
-}
diff --git a/src/components/sbb-tab-title/sbb-tab-title.spec.ts b/src/components/sbb-tab-title/sbb-tab-title.spec.ts
index 620cae0755..b68745c04b 100644
--- a/src/components/sbb-tab-title/sbb-tab-title.spec.ts
+++ b/src/components/sbb-tab-title/sbb-tab-title.spec.ts
@@ -11,11 +11,13 @@ describe('sbb-tab-title', () => {
expect(root).toEqualHtml(`
-
-
-
-
-
+
+
+
+
+
+
+
`);
@@ -28,18 +30,20 @@ describe('sbb-tab-title', () => {
});
expect(root).toEqualHtml(`
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`);
});
@@ -52,14 +56,16 @@ describe('sbb-tab-title', () => {
expect(root).toEqualHtml(`
-
-
-
-
-
- 78
-
-
+
+
+
+
+
+
+ 78
+
+
+
`);
diff --git a/src/components/sbb-tab-title/sbb-tab-title.tsx b/src/components/sbb-tab-title/sbb-tab-title.tsx
index c3e1e613ca..c6b98cbc55 100644
--- a/src/components/sbb-tab-title/sbb-tab-title.tsx
+++ b/src/components/sbb-tab-title/sbb-tab-title.tsx
@@ -62,21 +62,23 @@ export class SbbTabTitle {
const TAGNAME = `h${Number(this.level) < 7 ? this.level : '1'}`;
return (
-
- {(this.iconName || this._namedSlots['icon']) && (
-
- {this.iconName && }
+
+
+ {(this.iconName || this._namedSlots['icon']) && (
+
+ {this.iconName && }
+
+ )}
+
+
- )}
-
-
-
- {(this.amount || this._namedSlots['amount']) && (
-
- {this.amount}
-
- )}
-
+ {(this.amount || this._namedSlots['amount']) && (
+
+ {this.amount}
+
+ )}
+
+
);
}
}