Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(design): add link mode to tabs component #3429

Merged
merged 5 commits into from
Jan 14, 2025
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
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.
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
Loading