Skip to content

Commit

Permalink
feat(design): add link mode to tabs component (#3429)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: xelaint <[email protected]>
  • Loading branch information
griest024 and xelaint authored Jan 14, 2025
1 parent ef5254b commit 69d2859
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 16 deletions.
3 changes: 3 additions & 0 deletions libs/design/tabs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ A meaningful `aria-label` should be set on `<daff-tabs>` by using the `aria-labe
| Right Arrow | Moves focus and activates next tab. If focus is on the last tab, moves focus to the first tab. |
| Home | Moves focus and activates first tab. |
| End | Moves focus and activates last tab. |

### Link Mode
Tabs can operate in "link mode" which replaces the tab buttons with anchors. This allows the selected tab to be connected to the URL. To use this mode, set `linkMode` to `true` on the tabs component. By default, the current URL and `tab` query param will be used. These can be overriden with the `url` and `queryParam` inputs respectively.
55 changes: 55 additions & 0 deletions libs/design/tabs/examples/src/link-tabs/link-tabs.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<daff-tabs
aria-label="List of tabs"
[linkMode]="true"
>
<daff-tab>
<daff-tab-label>
<fa-icon [icon]="faInfoCircle" daffPrefix></fa-icon>
Tab 1
</daff-tab-label>
<daff-tab-panel>
Tab 1 Panel
</daff-tab-panel>
</daff-tab>

<daff-tab>
<daff-tab-label>
Tab 2
<fa-icon [icon]="faInfoCircle" daffSuffix></fa-icon>
</daff-tab-label>
<daff-tab-panel>
Tab 2 Panel
</daff-tab-panel>
</daff-tab>

<daff-tab>
<daff-tab-label>
<fa-icon [icon]="faInfoCircle" daffPrefix></fa-icon>
Tab 3
<fa-icon [icon]="faInfoCircle" daffSuffix></fa-icon>
</daff-tab-label>
<daff-tab-panel>
Tab 3 Panel
</daff-tab-panel>
</daff-tab>

<daff-tab>
<daff-tab-label>
Tab 4
<fa-icon [icon]="faInfoCircle" daffSuffix></fa-icon>
</daff-tab-label>
<daff-tab-panel>
Tab 4 Panel
</daff-tab-panel>
</daff-tab>

<daff-tab>
<daff-tab-label>
Tab 5
<fa-icon [icon]="faInfoCircle" daffSuffix></fa-icon>
</daff-tab-label>
<daff-tab-panel>
Tab 5 Panel
</daff-tab-panel>
</daff-tab>
</daff-tabs>
23 changes: 23 additions & 0 deletions libs/design/tabs/examples/src/link-tabs/link-tabs.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {
ChangeDetectionStrategy,
Component,
} from '@angular/core';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';

import { DAFF_TABS_COMPONENTS } from '@daffodil/design/tabs';

@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'link-tabs',
templateUrl: './link-tabs.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
DAFF_TABS_COMPONENTS,
FaIconComponent,
],
})
export class LinkTabsComponent {
faInfoCircle = faInfoCircle;
}
2 changes: 2 additions & 0 deletions libs/design/tabs/examples/src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { BasicTabsComponent } from './basic-tabs/basic-tabs.component';
import { CustomSelectTabsComponent } from './custom-select-tabs/custom-select-tabs.component';
import { DisabledTabsComponent } from './disabled-tabs/disabled-tabs.component';
import { InitiallySelectTabComponent } from './initially-select-tab/initially-select-tab.component';
import { LinkTabsComponent } from './link-tabs/link-tabs.component';

export const TABS_EXAMPLES = [
BasicTabsComponent,
DisabledTabsComponent,
InitiallySelectTabComponent,
CustomSelectTabsComponent,
LinkTabsComponent,
];
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
color: currentColor;
font-size: 1rem;
line-height: 1.5rem;
font-weight: 400;
font-weight: 500;
height: 3rem;
margin: 0;
min-width: 8rem;
padding: 0.5rem 1.5rem;
text-decoration: none;
z-index: 2;

&[disabled] {
Expand Down
46 changes: 36 additions & 10 deletions libs/design/tabs/src/tabs/tabs.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,42 @@
(keydown.home)="selectFirst($event)"
(keydown.end)="selectLast($event)">
@for (tab of _tabs; track tab) {
<button daff-tab-activator
[selected]="tab.id === selectedTab"
(click)="select(tab.id)"
[panelId]="tab.panelId"
[disabled]="tab.disabled"
[tabActivatorId]="tab.id"
(keydown.arrowright)="next()"
(keydown.arrowleft)="previous()">
<ng-container *ngTemplateOutlet="tab.labelRef"></ng-container>
</button>
@if (linkMode && tab.disabled) {
<button daff-tab-activator routerLinkActive
[selected]="tab.id === selectedTab"
[panelId]="tab.panelId"
[disabled]="tab.disabled"
[tabActivatorId]="tab.id"
(keydown.arrowright)="next()"
(keydown.arrowleft)="previous()">
<ng-container *ngTemplateOutlet="tab.labelRef"></ng-container>
</button>
} @else if (linkMode) {
<a daff-tab-activator routerLinkActive
class="daff-ae daff_tabs__link"
[selected]="tab.id === selectedTab"
[routerLink]="url || currentPath"
queryParamsHandling="merge"
[queryParams]="_buildQueryParams(tab.id)"
[panelId]="tab.panelId"
[tabActivatorId]="tab.id"
(keydown.arrowright)="next()"
(keydown.arrowleft)="previous()"
(isActiveChange)="$event && select(tab.id)">
<ng-container *ngTemplateOutlet="tab.labelRef"></ng-container>
</a>
} @else {
<button daff-tab-activator
[selected]="tab.id === selectedTab"
(click)="select(tab.id)"
[panelId]="tab.panelId"
[disabled]="tab.disabled"
[tabActivatorId]="tab.id"
(keydown.arrowright)="next()"
(keydown.arrowleft)="previous()">
<ng-container *ngTemplateOutlet="tab.labelRef"></ng-container>
</button>
}
}
</div>

Expand Down
70 changes: 69 additions & 1 deletion libs/design/tabs/src/tabs/tabs.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Location } from '@angular/common';
import {
ChangeDetectorRef,
Component,
Expand All @@ -10,6 +11,10 @@ import {
TestBed,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import {
provideRouter,
RouterModule,
} from '@angular/router';

import { DaffTabComponent } from './tab/tab.component';
import { DaffTabActivatorComponent } from './tab-activator/tab-activator.component';
Expand All @@ -19,7 +24,11 @@ import { DAFF_TABS_COMPONENTS } from '../tabs';

@Component({
template: `
<daff-tabs (tabChange)="onTabChange($event)">
<daff-tabs
[linkMode]="linkModeValue"
[url]="urlValue"
(tabChange)="onTabChange($event)"
>
<daff-tab>
<daff-tab-label>
Tab 1
Expand Down Expand Up @@ -55,6 +64,8 @@ import { DAFF_TABS_COMPONENTS } from '../tabs';
})
class WrapperComponent {
changed: string | null = null;
linkModeValue: boolean;
urlValue: string;

onTabChange(val: string) {
this.changed = val;
Expand All @@ -66,20 +77,37 @@ describe('@daffodil/design/tabs | DaffTabsComponent', () => {
let fixture: ComponentFixture<WrapperComponent>;
let component: DaffTabsComponent;
let de: DebugElement;
let locationSpy: jasmine.SpyObj<Location>;
let path: string;
let onUrlChangeCb: (url: string, state: unknown) => void;

beforeEach(waitForAsync(() => {
locationSpy = jasmine.createSpyObj('Location', ['path', 'onUrlChange']);
locationSpy.onUrlChange.and.callFake((cb) => {
onUrlChangeCb = cb;
return () => {};
});

TestBed.configureTestingModule({
imports: [
WrapperComponent,
],
providers: [
ChangeDetectorRef,
{
provide: Location,
useValue: locationSpy,
},
provideRouter([]),
],
})
.compileComponents();
}));

beforeEach(() => {
path = 'path';
locationSpy.path.and.returnValue(path);

fixture = TestBed.createComponent(WrapperComponent);
wrapper = fixture.componentInstance;

Expand All @@ -92,6 +120,46 @@ describe('@daffodil/design/tabs | DaffTabsComponent', () => {
expect(wrapper).toBeTruthy();
});

describe('in link mode', () => {
beforeEach(() => {
wrapper.linkModeValue = true;
fixture.detectChanges();
});

it('should render the tabs as anchors with the current path', () => {
fixture.debugElement.queryAll(By.directive(DaffTabActivatorComponent)).forEach((tab) => {
expect((<HTMLAnchorElement>tab.nativeElement).tagName).toEqual('A');
expect((<HTMLAnchorElement>tab.nativeElement).attributes.getNamedItem('ng-reflect-router-link').value).toEqual(path);
});
});

describe('when the url changes', () => {
beforeEach(() => {
component.select('tab-2');
fixture.detectChanges();
onUrlChangeCb('newurl', {});
fixture.detectChanges();
});

it('should reset the selected tab', () => {
expect(component.selectedTab).not.toEqual('tab-2');
});
});

describe('when a url is specified', () => {
beforeEach(() => {
wrapper.urlValue = 'url';
fixture.detectChanges();
});

it('should use that value as the router link', () => {
fixture.debugElement.queryAll(By.directive(DaffTabActivatorComponent)).forEach((tab) => {
expect((<HTMLAnchorElement>tab.nativeElement).attributes.getNamedItem('ng-reflect-router-link').value).toEqual(wrapper.urlValue);
});
});
});
});

it('should add a class of "daff-tabs" to the host element', () => {
expect(de.classes).toEqual(jasmine.objectContaining({
'daff-tabs': true,
Expand Down
Loading

0 comments on commit 69d2859

Please sign in to comment.