Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions packages/components-dev/tabs/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@ import { KbqButtonModule } from '@koobiq/components/button';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqPopoverModule } from '@koobiq/components/popover';
import { KbqTabChangeEvent, KbqTabsModule } from '@koobiq/components/tabs';
import { TabsVerticalExample } from 'packages/docs-examples/components/tabs';
import {
TabsActionbarExample,
TabsUnderlinedExample,
TabsVerticalExample
} from 'packages/docs-examples/components/tabs';
import { DevThemeToggle } from '../theme-toggle';

@Component({
selector: 'dev-examples',
imports: [TabsVerticalExample],
imports: [TabsVerticalExample, TabsUnderlinedExample, TabsActionbarExample],
template: `
<tabs-vertical-example />
<hr />
<tabs-underlined-example />
<hr />
<tabs-actionbar-example />
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
Expand Down
2 changes: 1 addition & 1 deletion packages/components-dev/tabs/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ <h3>selectedTabIndex</h3>
<h3>Tabs with actionbar</h3>
<div class="dev-tabs-actionbar">
<div class="dev-tabs-actionbar_nav">
<nav kbqTabNavBar underlined [tabNavPanel]="kbqTabNavPanel77">
<nav kbqTabNavBar underlined [tabNavPanel]="kbqTabNavPanel77" [onSurface]="true">
@for (dashboard of dashboards; track dashboard) {
<a kbqTabLink [active]="activeDashboard === dashboard" (click)="activeDashboard = dashboard">
{{ dashboard }}
Expand Down
Binary file modified packages/components/tabs/__screenshots__/01-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/components/tabs/__screenshots__/01-light.png
Comment thread
NikGurev marked this conversation as resolved.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
91 changes: 60 additions & 31 deletions packages/components/tabs/_tabs-common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,6 @@

outline: none;

&.kbq-tab-label_underlined:not(.kbq-tab-label_vertical) {
padding-left: 14px;
padding-right: 14px;
}

&.kbq-tab-label_underlined:not(.kbq-tab-label_vertical).kbq-tab-label_with-icon-left {
padding-left: tokens.kbq-difference-series-css-variables(
[tabs-size-tab-item-padding-horizontal,
tabs-size-tab-item-focus-outline-width]
);
}

&.kbq-tab-label_underlined:not(.kbq-tab-label_vertical).kbq-tab-label_with-icon-right {
padding-right: tokens.kbq-difference-series-css-variables(
[tabs-size-tab-item-padding-horizontal,
tabs-size-tab-item-focus-outline-width]
);
}

&.kbq-selected {
cursor: default;
}
Expand All @@ -75,30 +56,63 @@
&.kbq-tab-label_underlined:not(.kbq-tab-label_vertical):before {
content: '';
position: absolute;
left: 0;
right: 0;
top: -10px;
bottom: -10px;
inset: calc(-1 * (var(--kbq-size-s) + var(--kbq-tabs-size-tab-item-focus-outline-width)))
calc(-1 * var(--kbq-tabs-size-tab-item-focus-outline-width));
}

&.kbq-tab-label_underlined:not(.kbq-tab-label_vertical):not(.kbq-tab-label_icon-only):after {
content: '';
position: absolute;
left: calc(
var(--kbq-tabs-size-tab-item-padding-horizontal) - var(--kbq-tabs-size-tab-item-focus-outline-width)
);
right: calc(
var(--kbq-tabs-size-tab-item-padding-horizontal) - var(--kbq-tabs-size-tab-item-focus-outline-width)
);
bottom: calc(-1 * (var(--kbq-size-s) + var(--kbq-tabs-size-tab-item-focus-outline-width)));
height: 3px;
border-radius: var(--kbq-size-3xs) var(--kbq-size-3xs) 0 0;
background: transparent;
transition: background 0.2s ease-in-out;
pointer-events: none;
}

&.kbq-tab-label_icon-only {
width: var(--kbq-size-3xl);
height: var(--kbq-size-3xl);
padding: 0 !important;
margin-inline: var(--kbq-size-xs);
margin-inline: var(--kbq-size-xxs);
box-sizing: content-box;

// Keep the space between icon-only tabs clickable after adding margins.
&.kbq-tab-label_underlined:not(.kbq-tab-label_vertical) {
width: var(--kbq-tabs-size-tab-item-underlined-icon-only-size);
height: var(--kbq-tabs-size-tab-item-underlined-icon-only-size);

&:before {
left: calc(-1 * (var(--kbq-size-xxs) + var(--kbq-tabs-size-tab-item-focus-outline-width)));
right: calc(-1 * (var(--kbq-size-xxs) + var(--kbq-tabs-size-tab-item-focus-outline-width)));
}

&:after {
top: auto;
bottom: calc(-1 * (var(--kbq-size-s) + var(--kbq-tabs-size-tab-item-focus-outline-width)));
height: 3px;
left: calc(-1 * var(--kbq-tabs-size-tab-item-focus-outline-width));
right: calc(-1 * var(--kbq-tabs-size-tab-item-focus-outline-width));
border-radius: var(--kbq-size-3xs) var(--kbq-size-3xs) 0 0;
transition: background 0.2s ease-in-out;
pointer-events: none;
}
}

&:not(.kbq-tab-label_vertical):after {
content: '';
display: block;
position: absolute;
top: 0;
bottom: 0;
// Absolute offsets are calculated from the padding box, so we add
// focus outline width to fully cover the visual margin gap.
left: calc(-1 * (var(--kbq-size-xs) + var(--kbq-tabs-size-tab-item-focus-outline-width)));
right: calc(-1 * (var(--kbq-size-xs) + var(--kbq-tabs-size-tab-item-focus-outline-width)));
left: calc(-1 * (var(--kbq-size-xxs) + var(--kbq-tabs-size-tab-item-focus-outline-width)));
right: calc(-1 * (var(--kbq-size-xxs) + var(--kbq-tabs-size-tab-item-focus-outline-width)));
background: transparent;
}
}
Expand Down Expand Up @@ -144,7 +158,18 @@
}

.kbq-tab-header_underlined:not(.kbq-tab-header_vertical) .kbq-tab-list__content {
padding: 8px 0;
padding: var(--kbq-size-s) 0;

// Negative margin shifts the first tab left so the focus frame can extend beyond the
// container edge (instead of padding-left:0 which would clip it under overflow:hidden).
> .kbq-tab-label:first-child:not(.kbq-tab-label_icon-only),
> .kbq-tab-link:first-child {
margin-inline-start: calc(-1 * var(--kbq-tabs-size-tab-item-padding-horizontal));
}

> .kbq-tab-label_icon-only:first-child {
margin-inline-start: 0;
}
}

.kbq-tab-header_underlined:not(.kbq-tab-header_vertical):after {
Expand All @@ -166,8 +191,12 @@
position: absolute;
bottom: 0;
height: 3px;
border-radius: 2px 2px 0 0;
border-radius: var(--kbq-size-3xs) var(--kbq-size-3xs) 0 0;
background: var(--kbq-line-contrast);
transition: all 0.2s ease-in-out;

&.kbq-disabled {
background: var(--kbq-states-icon-disabled);
}
}
}
12 changes: 12 additions & 0 deletions packages/components/tabs/_tabs-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@

&:is(:hover, .kbq-hover):not(.kbq-disabled) {
@include kbq-tab-item-state($type, $sub-type, states-hover);

&.kbq-tab-label_underlined:not(.kbq-tab-label_vertical) {
background: transparent;

&:not(.kbq-selected)::after {
background: var(--kbq-line-contrast-fade);
}
}
}

&.kbq-tab-label_empty {
Expand All @@ -27,6 +35,10 @@

&.kbq-tab-label_underlined:not(.kbq-tab-label_vertical) {
background: none;

&::after {
background: transparent;
}
}

&:is(:hover, .kbq-hover) {
Expand Down
63 changes: 52 additions & 11 deletions packages/components/tabs/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ import { KbqTabsModule } from '@koobiq/components/tabs';
}
</kbq-tab-group>

<!-- underlined with icons only -->
<kbq-tab-group underlined data-testid="e2eTabsUnderlinedIconsOnly">
@for (tab of tabs.slice(0, 5); track tab) {
<kbq-tab>
<ng-template kbqTabLabel iconOnly>
<i kbq-icon="kbq-bug_16"></i>
</ng-template>
</kbq-tab>
}
</kbq-tab-group>

<!-- underlined -->
<kbq-tab-group underlined data-testid="e2eTabsUnderlined">
@for (tab of tabs; track tab) {
Expand All @@ -39,24 +50,26 @@ import { KbqTabsModule } from '@koobiq/components/tabs';
}
</kbq-tab-group>

<!-- underlined with icons only -->
<kbq-tab-group underlined data-testid="e2eTabsUnderlinedIconsOnly">
@for (tab of tabs.slice(0, 5); track tab) {
<kbq-tab>
<ng-template kbqTabLabel iconOnly>
<i kbq-icon="kbq-bug_16"></i>
</ng-template>
</kbq-tab>
}
</kbq-tab-group>

<!-- stretched -->
<kbq-tab-group kbq-stretch-tabs [activeTab]="tabs[2]">
@for (tab of tabs.slice(0, 2); track tab) {
<kbq-tab [disabled]="$first" [tabId]="tab" [label]="tab">Active tab is {{ tab }}</kbq-tab>
}
</kbq-tab-group>

<!-- underlined with text and icon states -->
<kbq-tab-group underlined data-testid="e2eTabsUnderlinedTextIcon">
@for (tab of tabs.slice(0, 4); track tab) {
<kbq-tab [tabId]="tab" [disabled]="$index === 3">
<ng-template kbq-tab-label>
<i kbq-icon="kbq-bug_16"></i>
Text {{ $index }}
</ng-template>
Active tab is {{ tab }}
</kbq-tab>
}
</kbq-tab-group>

<!-- stretched with icons -->
<kbq-tab-group kbq-stretch-tabs>
@for (tab of tabs.slice(0, 2); track tab) {
Expand All @@ -83,6 +96,19 @@ import { KbqTabsModule } from '@koobiq/components/tabs';
}
</kbq-tab-group>

<!-- underlined with text and icon disabled -->
<kbq-tab-group underlined data-testid="e2eTabsUnderlinedTextIconDisabled" [onSurface]="true">
@for (tab of tabs.slice(0, 1); track tab) {
<kbq-tab [tabId]="tab" [disabled]="$first">
<ng-template kbq-tab-label>
<i kbq-icon="kbq-bug_16"></i>
Text {{ $index }}
</ng-template>
Active tab is {{ tab }}
</kbq-tab>
}
</kbq-tab-group>

<!-- vertical -->
<kbq-tab-group vertical [style.height.px]="100">
@for (tab of tabs; track tab) {
Expand Down Expand Up @@ -185,6 +211,7 @@ export class E2eTabsStates {
constructor() {
afterNextRender(() => {
this.setupTabsUnderlinedIconsOnlyStates();
this.setupTabsUnderlinedTextIconStates();
this.setupTabsVerticalIconsOnlyStates();
});
}
Expand All @@ -203,6 +230,20 @@ export class E2eTabsStates {
labels[4].classList.add('cdk-keyboard-focused');
}

private setupTabsUnderlinedTextIconStates(): void {
const labels = document.querySelectorAll('[data-testid="e2eTabsUnderlinedTextIcon"] .kbq-tab-label');

labels[0].classList.add('kbq-hover');
labels[0].classList.add('cdk-keyboard-focused');

labels[1].classList.add('kbq-hover');

labels[2].classList.add('kbq-hover');
labels[2].classList.add('cdk-keyboard-focused');

labels[4].classList.add('cdk-keyboard-focused');
}

private setupTabsVerticalIconsOnlyStates(): void {
const labels = document.querySelectorAll('[data-testid="e2eTabsVerticalIconsOnly"] .kbq-tab-label');

Expand Down
19 changes: 11 additions & 8 deletions packages/components/tabs/tab-header.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { isUndefined } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqPaginatedTabHeader } from './paginated-tab-header';
import { KbqTabLabelWrapper } from './tab-label-wrapper.directive';
Expand All @@ -23,8 +24,8 @@ import { KbqTabLabelWrapper } from './tab-label-wrapper.directive';
*/
export type ScrollDirection = 'after' | 'before';

/** Corresponds to `margin-inline: var(--kbq-size-xs)` on `.kbq-tab-label_icon-only`. */
const ICON_ONLY_TAB_MARGIN_INLINE = 6;
/** Corresponds to `--kbq-tabs-size-tab-item-padding-horizontal` on text/icon+text tabs. */
const TAB_PADDING = 12;

/**
* The header of the tab group which displays a list of all the tabs in the tab group.
Expand Down Expand Up @@ -61,28 +62,30 @@ export class KbqTabHeader extends KbqPaginatedTabHeader {

private readonly isBrowser = inject(Platform).isBrowser;

/** Width of the active tab, adjusted for icon-only tab margins. */
protected get activeTabOffsetWidth(): number | undefined {
if (!this.isBrowser) return undefined;

const item = this.items.get(this.selectedIndex);
const width = item?.elementRef?.nativeElement?.offsetWidth;

if (!width) return width;
if (!width || item?.tab?.iconOnlyLabel) return width;

return item!.tab?.iconOnlyLabel ? width + ICON_ONLY_TAB_MARGIN_INLINE * 2 : width;
return width - TAB_PADDING * 2;
}

/** Left offset of the active tab, adjusted for icon-only tab margins. */
protected get activeTabOffsetLeft(): number | undefined {
if (!this.isBrowser) return undefined;

const item = this.items.get(this.selectedIndex);
const left = item?.elementRef?.nativeElement?.offsetLeft;

if (!left) return left;
if (isUndefined(left) || item?.tab?.iconOnlyLabel) return left;

return item!.tab?.iconOnlyLabel ? left - ICON_ONLY_TAB_MARGIN_INLINE : left;
return left + TAB_PADDING;
Comment thread
NikGurev marked this conversation as resolved.
}

protected get activeTabDisabled(): boolean {
return !!this.items.get(this.selectedIndex)?.disabled;
}

protected itemSelected(event: KeyboardEvent): void {
Expand Down
1 change: 1 addition & 0 deletions packages/components/tabs/tab-header.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<ng-content />
<div
class="kbq-tab-list__active-tab-underline"
[class.kbq-disabled]="activeTabDisabled"
[style.left.px]="activeTabOffsetLeft"
[style.width.px]="activeTabOffsetWidth"
></div>
Expand Down
14 changes: 14 additions & 0 deletions packages/components/tabs/tab-header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@
position: relative;
flex-shrink: 0;
border-radius: var(--kbq-tabs-size-tab-stack-horizontal-border-radius);

&.kbq-tab-header_underlined {
.kbq-tab-group:not(.kbq-tab-group_on-surface) & {
border-radius: 0;
}

&:not(.kbq-tab-header__pagination-controls_enabled) {
overflow: visible;
}
}
}

.kbq-tab-list__content {
Expand All @@ -32,6 +42,10 @@
flex-grow: 1;
z-index: 1;
overflow: hidden;

.kbq-tab-header_underlined:not(.kbq-tab-header__pagination-controls_enabled) & {
overflow: visible;
}
}

.kbq-tab-list {
Expand Down
Loading