Skip to content

Commit f753d1a

Browse files
authored
Merge pull request #10684 from IgniteUI/vkombov/feat-5904-master
feat(tabs): add full rtl support
2 parents 269ea7c + f90d502 commit f753d1a

File tree

8 files changed

+185
-96
lines changed

8 files changed

+185
-96
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
All notable changes for each version of this project will be documented in this file.
44

55
## 13.1.0
6+
67
### New Features
78
- `igxTooltipTarget` directive now allows specifying a plain text tooltip without adding an additional DOM element decorated with the `igxTooltip` directive. This is achieved via the newly introduced `tooltip` string input.
89
```html
910
<button igxTooltipTarget [tooltip]="'Infragistics Inc. HQ'">
1011
info
1112
</button>
1213
```
14+
- `IgxTabs` have full right-to-left (RTL) support.
15+
1316
### General
1417

1518
- `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid`

projects/igniteui-angular/src/lib/core/styles/components/tabs/_tabs-theme.scss

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,30 +58,23 @@
5858
@function igx-tabs-theme(
5959
$palette: null,
6060
$schema: $light-schema,
61-
6261
$item-text-color: null,
6362
$item-background: null,
64-
6563
$item-hover-background: null,
6664
$item-hover-color: null,
67-
6865
$item-active-color: null,
6966
$item-active-icon-color: null,
7067
$item-active-background: null,
7168
$indicator-color: null,
72-
7369
$button-color: null,
7470
$button-background: null,
7571
$button-hover-background: null,
7672
$button-hover-color: null,
77-
7873
$tab-ripple-color: null,
7974
$button-ripple-color: null,
8075
$border-radius: null,
81-
8276
$border-color: null,
8377
$border-color--hover: null,
84-
8578
$disable-shadow: true
8679
) {
8780
$name: 'igx-tabs';
@@ -347,6 +340,10 @@
347340
display: none;
348341
}
349342

343+
@include if-rtl() {
344+
transform: scaleX(-1);
345+
}
346+
350347
@include igx-ripple($button-ripple-theme);
351348
@include igx-css-vars($button-ripple-theme);
352349
}
@@ -594,5 +591,3 @@
594591
}
595592
}
596593
}
597-
598-

projects/igniteui-angular/src/lib/tabs/tabs.directive.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import { Subscription } from 'rxjs';
77
import { Direction, IgxCarouselComponentBase } from '../carousel/carousel-base';
88
import { IBaseEventArgs } from '../core/utils';
9+
import { IgxDirectionality } from '../services/direction/directionality';
910
import { IgxTabItemDirective } from './tab-item.directive';
1011
import { IgxTabContentBase, IgxTabsBase } from './tabs.base';
1112

@@ -119,7 +120,7 @@ export abstract class IgxTabsDirective extends IgxCarouselComponentBase implemen
119120
private _itemChanges$: Subscription;
120121

121122
/** @hidden */
122-
constructor(builder: AnimationBuilder, cdr: ChangeDetectorRef) {
123+
constructor(builder: AnimationBuilder, cdr: ChangeDetectorRef, public dir: IgxDirectionality) {
123124
super(builder, cdr);
124125
}
125126

@@ -293,7 +294,9 @@ export abstract class IgxTabsDirective extends IgxCarouselComponentBase implemen
293294
this.hasPanels &&
294295
this.currentItem &&
295296
!this.currentItem.selected) {
296-
item.direction = this._selectedIndex > oldSelectedIndex ? Direction.NEXT : Direction.PREV;
297+
item.direction = (!this.dir.rtl && this._selectedIndex > oldSelectedIndex) ||
298+
(this.dir.rtl && this._selectedIndex < oldSelectedIndex)
299+
? Direction.NEXT : Direction.PREV;
297300

298301
if (this.previousItem && this.previousItem.previous) {
299302
this.previousItem.previous = false;

projects/igniteui-angular/src/lib/tabs/tabs/tab-header.component.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { IgxTabHeaderBase } from '../tabs.base';
55
import { IgxTabsComponent } from './tabs.component';
66
import { getResizeObserver } from '../../core/utils';
77
import { PlatformUtil } from '../../core/utils';
8+
import { IgxDirectionality } from '../../services/direction/directionality';
89

910
@Component({
1011
selector: 'igx-tab-header',
@@ -37,7 +38,8 @@ export class IgxTabHeaderComponent extends IgxTabHeaderDirective implements Afte
3738
tab: IgxTabItemDirective,
3839
elementRef: ElementRef<HTMLElement>,
3940
protected platform: PlatformUtil,
40-
private ngZone: NgZone
41+
private ngZone: NgZone,
42+
private dir: IgxDirectionality
4143
) {
4244
super(tabs, tab, elementRef, platform);
4345
}
@@ -52,16 +54,10 @@ export class IgxTabHeaderComponent extends IgxTabHeaderDirective implements Afte
5254
const hasDisabledItems = itemsArray.some((item) => item.disabled);
5355
switch (event.key) {
5456
case this.platform.KEYMAP.ARROW_RIGHT:
55-
newIndex = newIndex === itemsArray.length - 1 ? 0 : newIndex + 1;
56-
while (hasDisabledItems && itemsArray[newIndex].disabled && newIndex < itemsArray.length) {
57-
newIndex = newIndex === itemsArray.length - 1 ? 0 : newIndex + 1;
58-
}
57+
newIndex = this.getNewSelectionIndex(newIndex, itemsArray, event.key, hasDisabledItems);
5958
break;
6059
case this.platform.KEYMAP.ARROW_LEFT:
61-
newIndex = newIndex === 0 ? itemsArray.length - 1 : newIndex - 1;
62-
while (hasDisabledItems && itemsArray[newIndex].disabled && newIndex >= 0) {
63-
newIndex = newIndex === 0 ? itemsArray.length - 1 : newIndex - 1;
64-
}
60+
newIndex = this.getNewSelectionIndex(newIndex, itemsArray, event.key, hasDisabledItems);
6561
break;
6662
case this.platform.KEYMAP.HOME:
6763
event.preventDefault();
@@ -119,5 +115,20 @@ export class IgxTabHeaderComponent extends IgxTabHeaderDirective implements Afte
119115
this._resizeObserver?.disconnect();
120116
});
121117
}
118+
119+
private getNewSelectionIndex(newIndex: number, itemsArray: any[], key: string, hasDisabledItems: boolean): number {
120+
if ((key === this.platform.KEYMAP.ARROW_RIGHT && !this.dir.rtl) || (key === this.platform.KEYMAP.ARROW_LEFT && this.dir.rtl)) {
121+
newIndex = newIndex === itemsArray.length - 1 ? 0 : newIndex + 1;
122+
while (hasDisabledItems && itemsArray[newIndex].disabled && newIndex < itemsArray.length) {
123+
newIndex = newIndex === itemsArray.length - 1 ? 0 : newIndex + 1;
124+
}
125+
} else {
126+
newIndex = newIndex === 0 ? itemsArray.length - 1 : newIndex - 1;
127+
while (hasDisabledItems && itemsArray[newIndex].disabled && newIndex >= 0) {
128+
newIndex = newIndex === 0 ? itemsArray.length - 1 : newIndex - 1;
129+
}
130+
}
131+
return newIndex;
132+
}
122133
}
123134

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,24 @@
11
<div #headerContainer class="igx-tabs__header">
2-
<button
3-
#leftButton
4-
igxButton="icon"
5-
igxRipple
6-
class="igx-tabs__header-button"
7-
(click)="scrollLeft()"
8-
>
2+
<button #scrollPrevButton igxButton="icon" igxRipple class="igx-tabs__header-button" (click)="scrollPrev()">
93
<igx-icon>navigate_before</igx-icon>
104
</button>
115
<div #viewPort class="igx-tabs__header-content">
12-
<div
13-
#itemsWrapper
14-
class="igx-tabs__header-wrapper"
15-
role="tablist"
16-
>
17-
<div
18-
#itemsContainer
19-
class="igx-tabs__header-scroll"
20-
[ngClass]="resolveHeaderScrollClasses()"
21-
>
6+
<div #itemsWrapper class="igx-tabs__header-wrapper" role="tablist">
7+
<div #itemsContainer class="igx-tabs__header-scroll" [ngClass]="resolveHeaderScrollClasses()">
228
<ng-container *ngFor="let tab of items; let i = index">
239
<ng-container *ngTemplateOutlet="tab.headerTemplate"></ng-container>
2410
</ng-container>
2511
</div>
26-
<div
27-
#selectedIndicator
28-
*ngIf="items.length > 0"
29-
class="igx-tabs__header-active-indicator"
30-
>
12+
<div #selectedIndicator *ngIf="items.length > 0" class="igx-tabs__header-active-indicator">
3113
</div>
3214
</div>
3315
</div>
34-
<button
35-
#rightButton
36-
igxButton="icon"
37-
igxRipple
38-
class="igx-tabs__header-button"
39-
(click)="scrollRight()"
40-
>
16+
<button #scrollNextButton igxButton="icon" igxRipple class="igx-tabs__header-button" (click)="scrollNext()">
4117
<igx-icon>navigate_next</igx-icon>
4218
</button>
4319
</div>
4420
<div class="igx-tabs__panels">
4521
<ng-container *ngFor="let tab of items; let i = index">
4622
<ng-container *ngTemplateOutlet="tab.panelTemplate"></ng-container>
4723
</ng-container>
48-
</div>
24+
</div>

projects/igniteui-angular/src/lib/tabs/tabs/tabs.component.spec.ts

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,26 @@ import { By } from '@angular/platform-browser';
88
import { RouterTestingModule } from '@angular/router/testing';
99
import { Router } from '@angular/router';
1010
import { Location } from '@angular/common';
11-
import { AddingSelectedTabComponent, TabsContactsComponent, TabsDisabledTestComponent, TabsRoutingDisabledTestComponent,
12-
TabsRoutingGuardTestComponent, TabsRoutingTestComponent, TabsTabsOnlyModeTest1Component, TabsTabsOnlyModeTest2Component,
11+
import {
12+
AddingSelectedTabComponent, TabsContactsComponent, TabsDisabledTestComponent, TabsRoutingDisabledTestComponent,
13+
TabsRoutingGuardTestComponent, TabsRoutingTestComponent, TabsRtlComponent, TabsTabsOnlyModeTest1Component, TabsTabsOnlyModeTest2Component,
1314
TabsTest2Component, TabsTestBug4420Component, TabsTestComponent, TabsTestCustomStylesComponent,
1415
TabsTestHtmlAttributesComponent, TabsTestSelectedTabComponent, TabsWithPrefixSuffixTestComponent,
15-
TemplatedTabsTestComponent } from '../../test-utils/tabs-components.spec';
16+
TemplatedTabsTestComponent
17+
} from '../../test-utils/tabs-components.spec';
1618
import { IgxTabsModule } from './tabs.module';
1719
import { configureTestSuite } from '../../test-utils/configure-suite';
1820
import { UIInteractions, wait } from '../../test-utils/ui-interactions.spec';
1921
import { IgxTabContentComponent } from './tab-content.component';
2022
import { RoutingTestGuard } from '../../test-utils/routing-test-guard.spec';
21-
import { RoutingView1Component,
23+
import {
24+
RoutingView1Component,
2225
RoutingView2Component,
2326
RoutingView3Component,
2427
RoutingView4Component,
2528
RoutingView5Component,
26-
RoutingViewComponentsModule } from '../../test-utils/routing-view-components.spec';
29+
RoutingViewComponentsModule
30+
} from '../../test-utils/routing-view-components.spec';
2731
import { IgxButtonModule } from '../../directives/button/button.directive';
2832
import { IgxDropDownModule } from '../../drop-down/public_api';
2933
import { IgxToggleModule } from '../../directives/toggle/toggle.directive';
@@ -58,7 +62,7 @@ describe('IgxTabs', () => {
5862
declarations: [TabsTestHtmlAttributesComponent, TabsTestComponent, TabsTest2Component, TemplatedTabsTestComponent,
5963
TabsRoutingDisabledTestComponent, TabsTestSelectedTabComponent, TabsTestCustomStylesComponent, TabsTestBug4420Component,
6064
TabsRoutingTestComponent, TabsTabsOnlyModeTest1Component, TabsTabsOnlyModeTest2Component, TabsDisabledTestComponent,
61-
TabsRoutingGuardTestComponent, TabsWithPrefixSuffixTestComponent, TabsContactsComponent, AddingSelectedTabComponent],
65+
TabsRoutingGuardTestComponent, TabsWithPrefixSuffixTestComponent, TabsContactsComponent, AddingSelectedTabComponent, TabsRtlComponent],
6266
imports: [IgxTabsModule, BrowserAnimationsModule, IgxButtonModule, IgxIconModule, IgxDropDownModule, IgxToggleModule,
6367
RoutingViewComponentsModule, IgxPrefixModule, IgxSuffixModule, RouterTestingModule.withRoutes(testRoutes)],
6468
providers: [RoutingTestGuard, PlatformUtil]
@@ -280,7 +284,7 @@ describe('IgxTabs', () => {
280284
fixture.detectChanges();
281285
expect(tabs.offset).toBeGreaterThan(0);
282286

283-
tabs.scrollLeft(null);
287+
tabs.scrollPrev(null);
284288

285289
tick(100);
286290
fixture.detectChanges();
@@ -700,14 +704,14 @@ describe('IgxTabs', () => {
700704
headerElements = tabItems.map(item => item.headerComponent.nativeElement);
701705

702706
fixture.ngZone.run(() => {
703-
router.initialNavigation();
704-
});
707+
router.initialNavigation();
708+
});
705709
tick();
706710
expect(location.path()).toBe('/');
707711

708712
fixture.ngZone.run(() => {
709-
UIInteractions.simulateClickAndSelectEvent(headerElements[0]);
710-
});
713+
UIInteractions.simulateClickAndSelectEvent(headerElements[0]);
714+
});
711715
tick();
712716
expect(location.path()).toBe('/view1');
713717
fixture.detectChanges();
@@ -716,8 +720,8 @@ describe('IgxTabs', () => {
716720
expect(tabItems[1].selected).toBe(false);
717721

718722
fixture.ngZone.run(() => {
719-
UIInteractions.simulateClickAndSelectEvent(headerElements[1]);
720-
});
723+
UIInteractions.simulateClickAndSelectEvent(headerElements[1]);
724+
});
721725
tick();
722726
expect(location.path()).toBe('/view1');
723727
fixture.detectChanges();
@@ -1048,7 +1052,7 @@ describe('IgxTabs', () => {
10481052
expect(indexChangingSpy).toHaveBeenCalledTimes(2);
10491053
expect(indexChangeSpy).toHaveBeenCalledTimes(2);
10501054
expect(itemChangeSpy).toHaveBeenCalledTimes(2);
1051-
}));
1055+
}));
10521056

10531057
it('Validate the events are not fired when navigating between tabs with home/end before pressing enter/space key.',
10541058
fakeAsync(() => {
@@ -1112,7 +1116,7 @@ describe('IgxTabs', () => {
11121116
expect(indexChangingSpy).toHaveBeenCalledTimes(2);
11131117
expect(indexChangeSpy).toHaveBeenCalledTimes(2);
11141118
expect(itemChangeSpy).toHaveBeenCalledTimes(2);
1115-
}));
1119+
}));
11161120

11171121
});
11181122
});
@@ -1290,4 +1294,63 @@ describe('IgxTabs', () => {
12901294

12911295
expect(rightScrollButton.clientWidth).toBeFalsy();
12921296
});
1297+
1298+
describe('IgxTabs RTL', () => {
1299+
let fix;
1300+
let tabs;
1301+
let tabItems;
1302+
let headers;
1303+
1304+
beforeEach(() => {
1305+
document.body.dir = 'rtl';
1306+
fix = TestBed.createComponent(TabsRtlComponent);
1307+
tabs = fix.componentInstance.tabs;
1308+
fix.detectChanges();
1309+
tabItems = tabs.items.toArray();
1310+
headers = tabItems.map(item => item.headerComponent.nativeElement);
1311+
});
1312+
1313+
it('should position scroll buttons properly', () => {
1314+
fix.componentInstance.wrapperDiv.nativeElement.style.width = '300px';
1315+
fix.detectChanges();
1316+
1317+
const scrollNextButton = fix.componentInstance.tabs.scrollNextButton;
1318+
const scrollPrevButton = fix.componentInstance.tabs.scrollPrevButton;
1319+
expect(scrollNextButton.nativeElement.offsetLeft).toBeLessThan(scrollPrevButton.nativeElement.offsetLeft);
1320+
});
1321+
1322+
it('should select next tab when left arrow is pressed and previous tab when right arrow is pressed', fakeAsync(() => {
1323+
tick(100);
1324+
fix.detectChanges();
1325+
headers = tabs.items.map(item => item.headerComponent.nativeElement);
1326+
1327+
headers[0].focus();
1328+
headers[0].dispatchEvent(KEY_LEFT_EVENT);
1329+
tick(200);
1330+
fix.detectChanges();
1331+
expect(tabs.selectedIndex).toBe(1);
1332+
1333+
headers[1].dispatchEvent(KEY_LEFT_EVENT);
1334+
tick(200);
1335+
fix.detectChanges();
1336+
expect(tabs.selectedIndex).toBe(2);
1337+
1338+
headers[2].dispatchEvent(KEY_LEFT_EVENT);
1339+
tick(200);
1340+
fix.detectChanges();
1341+
expect(tabs.selectedIndex).toBe(3);
1342+
1343+
headers[0].dispatchEvent(KEY_RIGHT_EVENT);
1344+
tick(200);
1345+
fix.detectChanges();
1346+
expect(tabs.selectedIndex).toBe(8);
1347+
1348+
headers[8].dispatchEvent(KEY_RIGHT_EVENT);
1349+
tick(200);
1350+
fix.detectChanges();
1351+
expect(tabs.selectedIndex).toBe(7);
1352+
}));
1353+
});
12931354
});
1355+
1356+

0 commit comments

Comments
 (0)