792 lines
28 KiB
TypeScript
792 lines
28 KiB
TypeScript
import {Dir, Direction} from '@angular/cdk/bidi';
|
|
import {END, ENTER, HOME, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
|
|
import {MutationObserverFactory, ObserversModule} from '@angular/cdk/observers';
|
|
import {SharedResizeObserver} from '@angular/cdk/observers/private';
|
|
import {PortalModule} from '@angular/cdk/portal';
|
|
import {ScrollingModule, ViewportRuler} from '@angular/cdk/scrolling';
|
|
import {
|
|
createKeyboardEvent,
|
|
createMouseEvent,
|
|
dispatchEvent,
|
|
dispatchFakeEvent,
|
|
dispatchKeyboardEvent,
|
|
} from '@angular/cdk/testing/private';
|
|
import {ChangeDetectorRef, Component, ViewChild, inject} from '@angular/core';
|
|
import {
|
|
ComponentFixture,
|
|
TestBed,
|
|
discardPeriodicTasks,
|
|
fakeAsync,
|
|
flush,
|
|
flushMicrotasks,
|
|
tick,
|
|
waitForAsync,
|
|
} from '@angular/core/testing';
|
|
import {MatRippleModule} from '@angular/material/core';
|
|
import {By} from '@angular/platform-browser';
|
|
import {Subject} from 'rxjs';
|
|
import {MatTabHeader} from './tab-header';
|
|
import {MatTabLabelWrapper} from './tab-label-wrapper';
|
|
|
|
describe('MatTabHeader', () => {
|
|
let fixture: ComponentFixture<SimpleTabHeaderApp>;
|
|
let appComponent: SimpleTabHeaderApp;
|
|
let resizeEvents: Subject<ResizeObserverEntry[]>;
|
|
|
|
beforeEach(waitForAsync(() => {
|
|
TestBed.configureTestingModule({
|
|
imports: [
|
|
PortalModule,
|
|
MatRippleModule,
|
|
ScrollingModule,
|
|
ObserversModule,
|
|
MatTabHeader,
|
|
MatTabLabelWrapper,
|
|
SimpleTabHeaderApp,
|
|
],
|
|
providers: [ViewportRuler],
|
|
});
|
|
|
|
resizeEvents = new Subject();
|
|
spyOn(TestBed.inject(SharedResizeObserver), 'observe').and.returnValue(resizeEvents);
|
|
}));
|
|
|
|
describe('focusing', () => {
|
|
let tabListContainer: HTMLElement;
|
|
|
|
beforeEach(() => {
|
|
fixture = TestBed.createComponent(SimpleTabHeaderApp);
|
|
fixture.detectChanges();
|
|
|
|
appComponent = fixture.componentInstance;
|
|
tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement;
|
|
});
|
|
|
|
it('should initialize to the selected index', () => {
|
|
// Recreate the fixture so we can test that it works with a non-default selected index
|
|
fixture.destroy();
|
|
fixture = TestBed.createComponent(SimpleTabHeaderApp);
|
|
fixture.componentInstance.selectedIndex = 1;
|
|
fixture.detectChanges();
|
|
appComponent = fixture.componentInstance;
|
|
tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement;
|
|
|
|
expect(appComponent.tabHeader.focusIndex).toBe(1);
|
|
});
|
|
|
|
it('should send focus change event', () => {
|
|
appComponent.tabHeader.focusIndex = 2;
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(2);
|
|
});
|
|
|
|
it('should be able to focus a disabled tab', () => {
|
|
appComponent.tabHeader.focusIndex = 0;
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(0);
|
|
|
|
appComponent.tabHeader.focusIndex = appComponent.disabledTabIndex;
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(appComponent.disabledTabIndex);
|
|
});
|
|
|
|
it('should move focus right including over disabled tabs', () => {
|
|
appComponent.tabHeader.focusIndex = 0;
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(0);
|
|
|
|
expect(appComponent.disabledTabIndex).toBe(1);
|
|
dispatchKeyboardEvent(tabListContainer, 'keydown', RIGHT_ARROW);
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(1);
|
|
|
|
dispatchKeyboardEvent(tabListContainer, 'keydown', RIGHT_ARROW);
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(2);
|
|
});
|
|
|
|
it('should move focus left including over disabled tabs', () => {
|
|
appComponent.tabHeader.focusIndex = 3;
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(3);
|
|
|
|
// Move focus left to index 3
|
|
dispatchKeyboardEvent(tabListContainer, 'keydown', LEFT_ARROW);
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(2);
|
|
|
|
expect(appComponent.disabledTabIndex).toBe(1);
|
|
dispatchKeyboardEvent(tabListContainer, 'keydown', LEFT_ARROW);
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(1);
|
|
});
|
|
|
|
it('should support key down events to move and select focus', () => {
|
|
appComponent.tabHeader.focusIndex = 0;
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(0);
|
|
|
|
// Move focus right to 1
|
|
dispatchKeyboardEvent(tabListContainer, 'keydown', RIGHT_ARROW);
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(1);
|
|
|
|
// Try to select 1. Should not work since it's disabled.
|
|
expect(appComponent.selectedIndex).toBe(0);
|
|
const firstEnterEvent = dispatchKeyboardEvent(tabListContainer, 'keydown', ENTER);
|
|
fixture.detectChanges();
|
|
expect(appComponent.selectedIndex).toBe(0);
|
|
expect(firstEnterEvent.defaultPrevented).toBe(false);
|
|
|
|
// Move focus right to 2
|
|
dispatchKeyboardEvent(tabListContainer, 'keydown', RIGHT_ARROW);
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(2);
|
|
|
|
// Select 2 which is enabled.
|
|
expect(appComponent.selectedIndex).toBe(0);
|
|
const secondEnterEvent = dispatchKeyboardEvent(tabListContainer, 'keydown', ENTER);
|
|
fixture.detectChanges();
|
|
expect(appComponent.selectedIndex).toBe(2);
|
|
expect(secondEnterEvent.defaultPrevented).toBe(true);
|
|
|
|
// Move focus left to 1
|
|
dispatchKeyboardEvent(tabListContainer, 'keydown', LEFT_ARROW);
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(1);
|
|
|
|
// Move again to 0
|
|
dispatchKeyboardEvent(tabListContainer, 'keydown', LEFT_ARROW);
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(0);
|
|
|
|
// Select the focused 0 using space.
|
|
expect(appComponent.selectedIndex).toBe(2);
|
|
const spaceEvent = dispatchKeyboardEvent(tabListContainer, 'keydown', SPACE);
|
|
fixture.detectChanges();
|
|
expect(appComponent.selectedIndex).toBe(0);
|
|
expect(spaceEvent.defaultPrevented).toBe(true);
|
|
});
|
|
|
|
it('should not prevent the default space/enter action if the current is selected', () => {
|
|
appComponent.tabHeader.focusIndex = appComponent.tabHeader.selectedIndex = 0;
|
|
fixture.detectChanges();
|
|
|
|
const spaceEvent = dispatchKeyboardEvent(tabListContainer, 'keydown', SPACE);
|
|
fixture.detectChanges();
|
|
expect(spaceEvent.defaultPrevented).toBe(false);
|
|
|
|
const enterEvent = dispatchKeyboardEvent(tabListContainer, 'keydown', ENTER);
|
|
fixture.detectChanges();
|
|
expect(enterEvent.defaultPrevented).toBe(false);
|
|
});
|
|
|
|
it('should move focus to the first tab when pressing HOME', () => {
|
|
appComponent.tabHeader.focusIndex = 3;
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(3);
|
|
|
|
const event = dispatchKeyboardEvent(tabListContainer, 'keydown', HOME);
|
|
fixture.detectChanges();
|
|
|
|
expect(appComponent.tabHeader.focusIndex).toBe(0);
|
|
expect(event.defaultPrevented).toBe(true);
|
|
});
|
|
|
|
it('should focus disabled items when moving focus using HOME', () => {
|
|
appComponent.tabHeader.focusIndex = 3;
|
|
appComponent.tabs[0].disabled = true;
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(3);
|
|
|
|
dispatchKeyboardEvent(tabListContainer, 'keydown', HOME);
|
|
fixture.detectChanges();
|
|
|
|
expect(appComponent.tabHeader.focusIndex).toBe(0);
|
|
});
|
|
|
|
it('should move focus to the last tab when pressing END', () => {
|
|
appComponent.tabHeader.focusIndex = 0;
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(0);
|
|
|
|
const event = dispatchKeyboardEvent(tabListContainer, 'keydown', END);
|
|
fixture.detectChanges();
|
|
|
|
expect(appComponent.tabHeader.focusIndex).toBe(3);
|
|
expect(event.defaultPrevented).toBe(true);
|
|
});
|
|
|
|
it('should focus disabled items when moving focus using END', () => {
|
|
appComponent.tabHeader.focusIndex = 0;
|
|
appComponent.tabs[3].disabled = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(0);
|
|
|
|
dispatchKeyboardEvent(tabListContainer, 'keydown', END);
|
|
fixture.detectChanges();
|
|
|
|
expect(appComponent.tabHeader.focusIndex).toBe(3);
|
|
});
|
|
|
|
it('should not do anything if a modifier key is pressed', () => {
|
|
const rightArrowEvent = createKeyboardEvent('keydown', RIGHT_ARROW, undefined, {shift: true});
|
|
const enterEvent = createKeyboardEvent('keydown', ENTER, undefined, {shift: true});
|
|
|
|
appComponent.tabHeader.focusIndex = 0;
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(0);
|
|
|
|
dispatchEvent(tabListContainer, rightArrowEvent);
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.focusIndex).toBe(0);
|
|
expect(rightArrowEvent.defaultPrevented).toBe(false);
|
|
|
|
expect(appComponent.selectedIndex).toBe(0);
|
|
dispatchEvent(tabListContainer, enterEvent);
|
|
fixture.detectChanges();
|
|
expect(appComponent.selectedIndex).toBe(0);
|
|
expect(enterEvent.defaultPrevented).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('pagination', () => {
|
|
describe('ltr', () => {
|
|
beforeEach(() => {
|
|
fixture = TestBed.createComponent(SimpleTabHeaderApp);
|
|
appComponent = fixture.componentInstance;
|
|
appComponent.dir = 'ltr';
|
|
fixture.detectChanges();
|
|
});
|
|
|
|
it('should show width when tab list width exceeds container', () => {
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader._showPaginationControls).toBe(false);
|
|
|
|
// Add enough tabs that it will obviously exceed the width
|
|
appComponent.addTabsForScrolling();
|
|
fixture.detectChanges();
|
|
|
|
expect(appComponent.tabHeader._showPaginationControls).toBe(true);
|
|
});
|
|
|
|
it('should scroll to show the focused tab label', () => {
|
|
appComponent.addTabsForScrolling();
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.scrollDistance).toBe(0);
|
|
|
|
// Focus on the last tab, expect this to be the maximum scroll distance.
|
|
appComponent.tabHeader.focusIndex = appComponent.tabs.length - 1;
|
|
fixture.detectChanges();
|
|
const {offsetLeft, offsetWidth} = appComponent.getSelectedLabel(
|
|
appComponent.tabHeader.focusIndex,
|
|
);
|
|
const viewLength = appComponent.getViewLength();
|
|
expect(appComponent.tabHeader.scrollDistance).toBe(offsetLeft + offsetWidth - viewLength);
|
|
|
|
// Focus on the first tab, expect this to be the maximum scroll distance.
|
|
appComponent.tabHeader.focusIndex = 0;
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.scrollDistance).toBe(0);
|
|
});
|
|
|
|
it('should show ripples for pagination buttons', () => {
|
|
appComponent.addTabsForScrolling();
|
|
fixture.detectChanges();
|
|
|
|
expect(appComponent.tabHeader._showPaginationControls).toBe(true);
|
|
|
|
const buttonAfter = fixture.debugElement.query(
|
|
By.css('.mat-mdc-tab-header-pagination-after'),
|
|
);
|
|
|
|
expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
|
|
.withContext('Expected no ripple to show up initially.')
|
|
.toBe(0);
|
|
|
|
dispatchFakeEvent(buttonAfter.nativeElement, 'mousedown');
|
|
dispatchFakeEvent(buttonAfter.nativeElement, 'mouseup');
|
|
|
|
expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
|
|
.withContext('Expected one ripple to show up after mousedown')
|
|
.toBe(1);
|
|
});
|
|
|
|
it('should allow disabling ripples for pagination buttons', () => {
|
|
appComponent.addTabsForScrolling();
|
|
appComponent.disableRipple = true;
|
|
fixture.detectChanges();
|
|
|
|
expect(appComponent.tabHeader._showPaginationControls).toBe(true);
|
|
|
|
const buttonAfter = fixture.debugElement.query(
|
|
By.css('.mat-mdc-tab-header-pagination-after'),
|
|
);
|
|
|
|
expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
|
|
.withContext('Expected no ripple to show up initially.')
|
|
.toBe(0);
|
|
|
|
dispatchFakeEvent(buttonAfter.nativeElement, 'mousedown');
|
|
dispatchFakeEvent(buttonAfter.nativeElement, 'mouseup');
|
|
|
|
expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
|
|
.withContext('Expected no ripple to show up after mousedown')
|
|
.toBe(0);
|
|
});
|
|
|
|
it('should update the scroll distance if a tab is removed and no tabs are selected', fakeAsync(() => {
|
|
appComponent.selectedIndex = 0;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
appComponent.addTabsForScrolling();
|
|
fixture.detectChanges();
|
|
|
|
// Focus the last tab so the header scrolls to the end.
|
|
appComponent.tabHeader.focusIndex = appComponent.tabs.length - 1;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
const {offsetLeft, offsetWidth} = appComponent.getSelectedLabel(
|
|
appComponent.tabHeader.focusIndex,
|
|
);
|
|
const viewLength = appComponent.getViewLength();
|
|
expect(appComponent.tabHeader.scrollDistance).toBe(offsetLeft + offsetWidth - viewLength);
|
|
|
|
// Remove the first two tabs which includes the selected tab.
|
|
appComponent.tabs = appComponent.tabs.slice(2);
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
expect(appComponent.tabHeader.scrollDistance).toBe(
|
|
appComponent.tabHeader._getMaxScrollDistance(),
|
|
);
|
|
}));
|
|
});
|
|
|
|
describe('rtl', () => {
|
|
beforeEach(() => {
|
|
fixture = TestBed.createComponent(SimpleTabHeaderApp);
|
|
appComponent = fixture.componentInstance;
|
|
appComponent.dir = 'rtl';
|
|
fixture.detectChanges();
|
|
});
|
|
|
|
it('should scroll to show the focused tab label', () => {
|
|
appComponent.addTabsForScrolling();
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.scrollDistance).toBe(0);
|
|
|
|
// Focus on the last tab, expect this to be the maximum scroll distance.
|
|
appComponent.tabHeader.focusIndex = appComponent.tabs.length - 1;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
const {offsetLeft} = appComponent.getSelectedLabel(appComponent.tabHeader.focusIndex);
|
|
expect(offsetLeft).toBe(0);
|
|
|
|
// Focus on the first tab, expect this to be the maximum scroll distance.
|
|
appComponent.tabHeader.focusIndex = 0;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.scrollDistance).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('scrolling when holding paginator', () => {
|
|
let nextButton: HTMLElement;
|
|
let prevButton: HTMLElement;
|
|
let header: MatTabHeader;
|
|
let headerElement: HTMLElement;
|
|
|
|
beforeEach(() => {
|
|
fixture = TestBed.createComponent(SimpleTabHeaderApp);
|
|
fixture.componentInstance.disableRipple = true;
|
|
fixture.detectChanges();
|
|
|
|
fixture.componentInstance.addTabsForScrolling(50);
|
|
fixture.detectChanges();
|
|
|
|
nextButton = fixture.nativeElement.querySelector('.mat-mdc-tab-header-pagination-after');
|
|
prevButton = fixture.nativeElement.querySelector('.mat-mdc-tab-header-pagination-before');
|
|
header = fixture.componentInstance.tabHeader;
|
|
headerElement = fixture.nativeElement.querySelector('.mat-mdc-tab-header');
|
|
});
|
|
|
|
it('should scroll towards the end while holding down the next button using a mouse', fakeAsync(() => {
|
|
assertNextButtonScrolling('mousedown', 'click');
|
|
}));
|
|
|
|
it('should scroll towards the start while holding down the prev button using a mouse', fakeAsync(() => {
|
|
assertPrevButtonScrolling('mousedown', 'click');
|
|
}));
|
|
|
|
it('should scroll towards the end while holding down the next button using touch', fakeAsync(() => {
|
|
assertNextButtonScrolling('touchstart', 'touchend');
|
|
}));
|
|
|
|
it('should scroll towards the start while holding down the prev button using touch', fakeAsync(() => {
|
|
assertPrevButtonScrolling('touchstart', 'touchend');
|
|
}));
|
|
|
|
it('should not scroll if the sequence is interrupted quickly', fakeAsync(() => {
|
|
expect(header.scrollDistance).withContext('Expected to start off not scrolled.').toBe(0);
|
|
|
|
dispatchFakeEvent(nextButton, 'mousedown');
|
|
fixture.detectChanges();
|
|
|
|
tick(100);
|
|
|
|
dispatchFakeEvent(headerElement, 'mouseleave');
|
|
fixture.detectChanges();
|
|
|
|
tick(3000);
|
|
|
|
expect(header.scrollDistance)
|
|
.withContext('Expected not to have scrolled after a while.')
|
|
.toBe(0);
|
|
}));
|
|
|
|
it('should clear the timeouts on destroy', fakeAsync(() => {
|
|
dispatchFakeEvent(nextButton, 'mousedown');
|
|
fixture.detectChanges();
|
|
fixture.destroy();
|
|
|
|
// No need to assert. If fakeAsync doesn't throw, it means that the timers were cleared.
|
|
}));
|
|
|
|
it('should clear the timeouts on click', fakeAsync(() => {
|
|
dispatchFakeEvent(nextButton, 'mousedown');
|
|
fixture.detectChanges();
|
|
|
|
dispatchFakeEvent(nextButton, 'click');
|
|
fixture.detectChanges();
|
|
|
|
// No need to assert. If fakeAsync doesn't throw, it means that the timers were cleared.
|
|
}));
|
|
|
|
it('should clear the timeouts on touchend', fakeAsync(() => {
|
|
dispatchFakeEvent(nextButton, 'touchstart');
|
|
fixture.detectChanges();
|
|
|
|
dispatchFakeEvent(nextButton, 'touchend');
|
|
fixture.detectChanges();
|
|
|
|
// No need to assert. If fakeAsync doesn't throw, it means that the timers were cleared.
|
|
}));
|
|
|
|
it('should clear the timeouts when reaching the end', fakeAsync(() => {
|
|
dispatchFakeEvent(nextButton, 'mousedown');
|
|
fixture.detectChanges();
|
|
|
|
// Simulate a very long timeout.
|
|
tick(60000);
|
|
|
|
// No need to assert. If fakeAsync doesn't throw, it means that the timers were cleared.
|
|
}));
|
|
|
|
it('should clear the timeouts when reaching the start', fakeAsync(() => {
|
|
header.scrollDistance = Infinity;
|
|
fixture.detectChanges();
|
|
|
|
dispatchFakeEvent(prevButton, 'mousedown');
|
|
fixture.detectChanges();
|
|
|
|
// Simulate a very long timeout.
|
|
tick(60000);
|
|
|
|
// No need to assert. If fakeAsync doesn't throw, it means that the timers were cleared.
|
|
}));
|
|
|
|
it('should stop scrolling if the pointer leaves the header', fakeAsync(() => {
|
|
expect(header.scrollDistance).withContext('Expected to start off not scrolled.').toBe(0);
|
|
|
|
dispatchFakeEvent(nextButton, 'mousedown');
|
|
fixture.detectChanges();
|
|
tick(300);
|
|
|
|
expect(header.scrollDistance)
|
|
.withContext('Expected not to scroll after short amount of time.')
|
|
.toBe(0);
|
|
|
|
tick(1000);
|
|
|
|
expect(header.scrollDistance)
|
|
.withContext('Expected to scroll after some time.')
|
|
.toBeGreaterThan(0);
|
|
|
|
let previousDistance = header.scrollDistance;
|
|
|
|
dispatchFakeEvent(headerElement, 'mouseleave');
|
|
fixture.detectChanges();
|
|
tick(100);
|
|
|
|
expect(header.scrollDistance).toBe(previousDistance);
|
|
}));
|
|
|
|
it('should not scroll when pressing the right mouse button', fakeAsync(() => {
|
|
expect(header.scrollDistance).withContext('Expected to start off not scrolled.').toBe(0);
|
|
|
|
dispatchEvent(
|
|
nextButton,
|
|
createMouseEvent('mousedown', undefined, undefined, undefined, undefined, 2),
|
|
);
|
|
fixture.detectChanges();
|
|
tick(3000);
|
|
|
|
expect(header.scrollDistance)
|
|
.withContext('Expected not to have scrolled after a while.')
|
|
.toBe(0);
|
|
}));
|
|
|
|
/**
|
|
* Asserts that auto scrolling using the next button works.
|
|
* @param startEventName Name of the event that is supposed to start the scrolling.
|
|
* @param endEventName Name of the event that is supposed to end the scrolling.
|
|
*/
|
|
function assertNextButtonScrolling(startEventName: string, endEventName: string) {
|
|
expect(header.scrollDistance).withContext('Expected to start off not scrolled.').toBe(0);
|
|
|
|
dispatchFakeEvent(nextButton, startEventName);
|
|
fixture.detectChanges();
|
|
tick(300);
|
|
|
|
expect(header.scrollDistance)
|
|
.withContext('Expected not to scroll after short amount of time.')
|
|
.toBe(0);
|
|
|
|
tick(1000);
|
|
|
|
expect(header.scrollDistance)
|
|
.withContext('Expected to scroll after some time.')
|
|
.toBeGreaterThan(0);
|
|
|
|
let previousDistance = header.scrollDistance;
|
|
|
|
tick(100);
|
|
|
|
expect(header.scrollDistance)
|
|
.withContext('Expected to scroll again after some more time.')
|
|
.toBeGreaterThan(previousDistance);
|
|
|
|
dispatchFakeEvent(nextButton, endEventName);
|
|
flush();
|
|
}
|
|
|
|
/**
|
|
* Asserts that auto scrolling using the previous button works.
|
|
* @param startEventName Name of the event that is supposed to start the scrolling.
|
|
* @param endEventName Name of the event that is supposed to end the scrolling.
|
|
*/
|
|
function assertPrevButtonScrolling(startEventName: string, endEventName: string) {
|
|
header.scrollDistance = Infinity;
|
|
fixture.detectChanges();
|
|
|
|
let currentScroll = header.scrollDistance;
|
|
|
|
expect(currentScroll).withContext('Expected to start off scrolled.').toBeGreaterThan(0);
|
|
|
|
dispatchFakeEvent(prevButton, startEventName);
|
|
fixture.detectChanges();
|
|
tick(300);
|
|
|
|
expect(header.scrollDistance)
|
|
.withContext('Expected not to scroll after short amount of time.')
|
|
.toBe(currentScroll);
|
|
|
|
tick(1000);
|
|
|
|
expect(header.scrollDistance)
|
|
.withContext('Expected to scroll after some time.')
|
|
.toBeLessThan(currentScroll);
|
|
|
|
currentScroll = header.scrollDistance;
|
|
|
|
tick(100);
|
|
|
|
expect(header.scrollDistance)
|
|
.withContext('Expected to scroll again after some more time.')
|
|
.toBeLessThan(currentScroll);
|
|
|
|
dispatchFakeEvent(nextButton, endEventName);
|
|
flush();
|
|
}
|
|
});
|
|
|
|
describe('disabling pagination', () => {
|
|
it('should not show the pagination controls if pagination is disabled', () => {
|
|
fixture = TestBed.createComponent(SimpleTabHeaderApp);
|
|
appComponent = fixture.componentInstance;
|
|
appComponent.disablePagination = true;
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader._showPaginationControls).toBe(false);
|
|
|
|
// Add enough tabs that it will obviously exceed the width
|
|
appComponent.addTabsForScrolling();
|
|
fixture.detectChanges();
|
|
|
|
expect(appComponent.tabHeader._showPaginationControls).toBe(false);
|
|
});
|
|
|
|
it('should not change the scroll position if pagination is disabled', () => {
|
|
fixture = TestBed.createComponent(SimpleTabHeaderApp);
|
|
appComponent = fixture.componentInstance;
|
|
appComponent.disablePagination = true;
|
|
fixture.detectChanges();
|
|
appComponent.addTabsForScrolling();
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.scrollDistance).toBe(0);
|
|
|
|
appComponent.tabHeader.focusIndex = appComponent.tabs.length - 1;
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.scrollDistance).toBe(0);
|
|
|
|
appComponent.tabHeader.focusIndex = 0;
|
|
fixture.detectChanges();
|
|
expect(appComponent.tabHeader.scrollDistance).toBe(0);
|
|
});
|
|
});
|
|
|
|
it('should re-align the ink bar when the direction changes', fakeAsync(() => {
|
|
fixture = TestBed.createComponent(SimpleTabHeaderApp);
|
|
fixture.detectChanges();
|
|
|
|
const inkBar = fixture.componentInstance.tabHeader._inkBar;
|
|
spyOn(inkBar, 'alignToElement');
|
|
|
|
fixture.detectChanges();
|
|
|
|
fixture.componentInstance.dir = 'rtl';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
expect(inkBar.alignToElement).toHaveBeenCalled();
|
|
}));
|
|
|
|
it('should re-align the ink bar when the header is resized', fakeAsync(() => {
|
|
fixture = TestBed.createComponent(SimpleTabHeaderApp);
|
|
fixture.detectChanges();
|
|
|
|
const inkBar = fixture.componentInstance.tabHeader._inkBar;
|
|
|
|
spyOn(inkBar, 'alignToElement');
|
|
|
|
resizeEvents.next([]);
|
|
fixture.detectChanges();
|
|
tick(32);
|
|
|
|
expect(inkBar.alignToElement).toHaveBeenCalled();
|
|
discardPeriodicTasks();
|
|
}));
|
|
|
|
it('should update arrows when the header is resized', fakeAsync(() => {
|
|
fixture = TestBed.createComponent(SimpleTabHeaderApp);
|
|
|
|
const header = fixture.componentInstance.tabHeader;
|
|
|
|
spyOn(header, '_checkPaginationEnabled');
|
|
|
|
resizeEvents.next([]);
|
|
fixture.detectChanges();
|
|
flushMicrotasks();
|
|
|
|
expect(header._checkPaginationEnabled).toHaveBeenCalled();
|
|
discardPeriodicTasks();
|
|
}));
|
|
|
|
it('should update the pagination state if the content of the labels changes', () => {
|
|
const mutationCallbacks: Function[] = [];
|
|
spyOn(TestBed.inject(MutationObserverFactory), 'create').and.callFake(
|
|
(callback: Function) => {
|
|
mutationCallbacks.push(callback);
|
|
return {observe: () => {}, disconnect: () => {}} as any;
|
|
},
|
|
);
|
|
|
|
fixture = TestBed.createComponent(SimpleTabHeaderApp);
|
|
fixture.detectChanges();
|
|
|
|
const tabHeaderElement: HTMLElement =
|
|
fixture.nativeElement.querySelector('.mat-mdc-tab-header');
|
|
const labels = Array.from<HTMLElement>(
|
|
fixture.nativeElement.querySelectorAll('.label-content'),
|
|
);
|
|
const extraText = new Array(100).fill('w').join();
|
|
const enabledClass = 'mat-mdc-tab-header-pagination-controls-enabled';
|
|
|
|
expect(tabHeaderElement.classList).not.toContain(enabledClass);
|
|
|
|
labels.forEach(label => {
|
|
label.style.width = '';
|
|
label.textContent += extraText;
|
|
});
|
|
|
|
mutationCallbacks.forEach(callback => callback([{type: 'fake'}]));
|
|
fixture.detectChanges();
|
|
|
|
expect(tabHeaderElement.classList).toContain(enabledClass);
|
|
});
|
|
});
|
|
});
|
|
|
|
interface Tab {
|
|
label: string;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<div [dir]="dir">
|
|
<mat-tab-header [selectedIndex]="selectedIndex" [disableRipple]="disableRipple"
|
|
(indexFocused)="focusedIndex = $event"
|
|
(selectFocusedIndex)="selectedIndex = $event"
|
|
[disablePagination]="disablePagination">
|
|
@for (tab of tabs; track tab; let i = $index) {
|
|
<div matTabLabelWrapper class="label-content" style="min-width: 30px; width: 30px"
|
|
[disabled]="!!tab.disabled"
|
|
(click)="selectedIndex = i">{{tab.label}}</div>
|
|
}
|
|
</mat-tab-header>
|
|
</div>
|
|
`,
|
|
styles: `
|
|
:host {
|
|
width: 130px;
|
|
}
|
|
`,
|
|
standalone: true,
|
|
imports: [Dir, MatTabHeader, MatTabLabelWrapper],
|
|
})
|
|
class SimpleTabHeaderApp {
|
|
disableRipple: boolean = false;
|
|
selectedIndex: number = 0;
|
|
focusedIndex: number;
|
|
disabledTabIndex = 1;
|
|
tabs: Tab[] = [{label: 'tab one'}, {label: 'tab one'}, {label: 'tab one'}, {label: 'tab one'}];
|
|
dir: Direction = 'ltr';
|
|
disablePagination: boolean;
|
|
|
|
@ViewChild(MatTabHeader, {static: true}) tabHeader: MatTabHeader;
|
|
|
|
private readonly _changeDetectorRef = inject(ChangeDetectorRef);
|
|
|
|
constructor() {
|
|
this.tabs[this.disabledTabIndex].disabled = true;
|
|
}
|
|
|
|
addTabsForScrolling(amount = 4) {
|
|
for (let i = 0; i < amount; i++) {
|
|
this.tabs.push({label: 'new'});
|
|
}
|
|
this._changeDetectorRef.markForCheck();
|
|
}
|
|
|
|
getViewLength() {
|
|
return this.tabHeader._tabListContainer.nativeElement.offsetWidth;
|
|
}
|
|
|
|
getSelectedLabel(index: number) {
|
|
return this.tabHeader._items.toArray()[this.tabHeader.focusIndex].elementRef.nativeElement;
|
|
}
|
|
}
|