Reputation: 1517
I'm using Material tabs in my application (mat-tab
s inside mat-tab-group
)
When there are more tabs than can be displayed, two navigation buttons are shown to display the other tabs:
My requirement is to enable the user to scroll on the tab bar so that other tabs are shown.
I tried to do some css changes but couldn't solve it. It's highly appreciated if any solution or suggestion can be given.
Upvotes: 5
Views: 16063
Reputation: 11
Thank you @igor-kurkov for the great solution! I built upon it by adding swipping in the tab body's content area to further implement the Material Design features. Works for both mouse and touchscreens.
The scrolling.directive.ts file now looks like this:
import {
Directive,
ElementRef,
OnDestroy,
Input,
Renderer2,
} from '@angular/core';
import { fromEvent, Subscription } from 'rxjs';
import { MatTabGroup } from '@angular/material/tabs';
interface DOMRectI {
bottom: number;
height: number;
left: number; // position start of element
right: number; // position end of element
top: number;
width: number; // width of element
x?: number;
y?: number;
}
@Directive({
// tslint:disable-next-line:directive-selector
selector: '[scrollToCenter]',
})
export class MatTabScrollToCenterDirective implements OnDestroy {
@Input() scrollToCenter: number;
tabWidths: number;
tabsArrLength: number;
tabContentDiv: HTMLDivElement;
touchstartX: number;
touchendX: number;
swipeEnd: boolean;
selectText = false;
mouseStartTime: number;
subs = new Subscription();
constructor(
private element: ElementRef,
private renderer: Renderer2,
private matTabGroup: MatTabGroup
) {
this.matTabGroup.selectedIndex = 0;
this.subs.add(
fromEvent(this.element.nativeElement, 'click').subscribe(
(clickedContainer: MouseEvent) => {
this.startEvent(clickedContainer);
}
)
);
this.subs.add(
fromEvent(this.element.nativeElement, 'touchend').subscribe(
(clickedContainer: TouchEvent) => {
this.startEvent(clickedContainer);
}
)
);
}
ngAfterViewInit() {
// console.log(this.element);
const matTabLabel =
this.element.nativeElement.querySelector('.mat-tab-labels');
this.tabWidths = matTabLabel.children[0].offsetWidth;
this.tabsArrLength = matTabLabel.children.length;
this.tabContentDiv = this.element.nativeElement.querySelector(
'.mat-tab-body-wrapper'
);
this.removeBodySelect();
this.guestureListener();
}
// disable text selection in the Tab Content Body so doesn't highlight on swipe
removeBodySelect() {
this.renderer.setStyle(
this.tabContentDiv,
'-webkit-touch-callout',
'none'
); /* iOS Safari */
this.renderer.setStyle(
this.tabContentDiv,
'-webkit-user-select',
'none'
); /* Safari */
this.renderer.setStyle(
this.tabContentDiv,
'-khtml-user-select',
'none'
); /* Konqueror HTML */
this.renderer.setStyle(
this.tabContentDiv,
'-moz-user-select',
'none'
); /* Old versions of Firefox */
this.renderer.setStyle(
this.tabContentDiv,
'-ms-user-select',
'none'
); /* Internet Explorer/Edge */
this.renderer.setStyle(
this.tabContentDiv,
'user-select',
'none'
); /* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */
}
guestureListener() {
this.tabContentDiv.addEventListener(
'mousedown',
this.swipeStart.bind(this)
);
this.tabContentDiv.addEventListener('mouseup', this.swipeFinish.bind(this));
this.tabContentDiv.addEventListener(
'touchstart',
this.swipeStart.bind(this)
);
this.tabContentDiv.addEventListener(
'touchend',
this.swipeFinish.bind(this)
);
}
swipeStart($event: MouseEvent | TouchEvent) {
if ($event instanceof MouseEvent) {
this.mouseStartTime = new Date().getTime();
this.touchstartX = $event.x;
} else {
this.touchstartX = $event.changedTouches[0].screenX;
}
this.swipeEnd = false;
}
swipeFinish($event: MouseEvent | TouchEvent) {
let newTime: number;
if ($event instanceof MouseEvent) {
newTime = new Date().getTime();
const deltaTime = newTime - this.mouseStartTime;
this.touchendX = $event.x;
//if swipe is > 300 cancel to allow text selection
if (deltaTime > 300) {
return;
}
} else {
this.touchendX = $event.changedTouches[0].screenX;
}
this.checkDirection();
this.swipeEnd = true;
}
//swipe left/right detection for moving tabs
//added "+ n" so that small guestures don't activate
checkDirection() {
if (this.touchendX + 10 < this.touchstartX && !this.swipeEnd) {
// console.log('swiped left');
const isLast = this.matTabGroup.selectedIndex === this.tabsArrLength;
if (this.matTabGroup.selectedIndex < this.tabsArrLength - 1) {
this.matTabGroup.selectedIndex = isLast
? this.tabsArrLength
: this.matTabGroup.selectedIndex + 1;
}
}
if (this.touchendX > this.touchstartX + 10 && !this.swipeEnd) {
// console.log('swiped right');
const isFirst =
this.matTabGroup.selectedIndex === 0; /* starter point as 0 */
this.matTabGroup.selectedIndex = isFirst
? 0
: this.matTabGroup.selectedIndex - 1;
}
}
startEvent(clickedContainer: MouseEvent | TouchEvent) {
const scrolledButton: DOMRectI = (
clickedContainer.target as HTMLElement
).getBoundingClientRect();
const scrollContainer =
this.element.nativeElement.querySelector('.mat-tab-list');
const tabHeader =
this.element.nativeElement.querySelector('.mat-tab-header');
const containerHeight = tabHeader.offsetHeight + tabHeader.offsetTop;
let newPositionScrollTo: number;
//touch drag of the tab bar is jumpy due to touch events also init 'click' events
//so prevent double "clicks"
//If clicking/touching the tab bar else if clicking/touching tab body calcs
if (
scrolledButton.bottom <= containerHeight &&
clickedContainer.type === 'click'
) {
const leftXOffset = (window.innerWidth - scrolledButton.width) / 2;
const currentVisibleViewportLeft = scrolledButton.left;
const neededLeftOffset = currentVisibleViewportLeft - leftXOffset;
newPositionScrollTo = scrollContainer.scrollLeft + neededLeftOffset;
} else if (scrolledButton.bottom > containerHeight) {
newPositionScrollTo =
this.tabWidths * this.matTabGroup.selectedIndex +
scrollContainer.offsetLeft -
(window.innerWidth - this.tabWidths) / 2;
}
scrollContainer.scroll({
left: newPositionScrollTo,
behavior: 'smooth',
});
}
ngOnDestroy() {
this.subs.unsubscribe();
}
}
The only other change is in the app.component.html where we have to pass the selectedIndex
to the directive:
<mat-tab-group scrollToCenter="{{ selectedIndex }}">
Feel free to play with it StackBlitz
Upvotes: 1
Reputation: 156
Adding the following code to my css file worked as I expected.
::ng-deep .mat-mdc-tab-label-container {
overflow-x: auto !important;
}
Upvotes: 0
Reputation: 583
in my case, I going to hide buttons and made scroll only on touchable devices:
@media (hover: none) {
::ng-deep nav[mat-tab-nav-bar] > div {
overflow-x: scroll !important;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
::ng-deep nav[mat-tab-nav-bar] > div::-webkit-scrollbar {
display: none; /* Hide scrollbar for Chrome, Safari and Opera */
}
::ng-deep nav[mat-tab-nav-bar] .mat-mdc-tab-list {
transform: translateX(0) !important;
}
::ng-deep nav[mat-tab-nav-bar] > button {
display: none !important;
}
}
Upvotes: 0
Reputation: 1
Mat tabs are scroll-able by default. Just check if you have not overridden that property any where like this CSS below ( This CSS blocks the scroll behavior )
.mat-tab-header-pagination-controls-enabled .mat-tab-header-pagination {
display: none !important;
}
.mat-ripple-element {
display: none !important;
}
Note:
The tabs will be scroll-able when screen width is less than tabs-group width
Upvotes: 0
Reputation: 80
We can make mat tabs scrollable by adding below css code
.mat-tab-labels
{
display : inline-flex !important;
}
.mat-tab-label
{
display:inline-table !important;
padding-top:10px !important;
min-width:0px !important;
}
Upvotes: 0
Reputation: 1
adding drag event and drag start event on top of the mouse wheel event (posted above) to scroll a strd mat-tab under a mat-tab-group
onDrag(event) {
if (event.clientX > 0) {
let deltaX = this.previousX - event.clientX;
const children = this.tabGroup._tabHeader._elementRef.nativeElement.children;
// get the tabGroup pagination buttons
const back = children[0];
const forward = children[2];
console.log('dragging' + deltaX);
// depending on scroll direction click forward or back
if (deltaX > 3) {
forward.click();
} else if (deltaX < -3) {
back.click();
}
}
this.previousX = event.clientX;
event.target.style.opacity = 1;
}
onDragStart(event) {
this.previousX = event.clientX;
event.target.style.opacity = 0;
}
.noselect {
user-select: none;
/* Non-prefixed version, currently
supported by Chrome, Edge, Opera and Firefox */
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<mat-card fxflex class="card component-right" *ngIf="editMode">
<mat-card-content class="card-content">
<mat-tab-group class="noselect" draggable="true" (drag)="onDrag($event)" (dragstart)="onDragStart($event)" (wheel)="scrollTabs($event)" #tabGroup>
<mat-tab label="1st tab"> </mat-tab>
<mat-tab label="2nd tab"> </mat-tab>
<mat-tab label="3rd tab"> </mat-tab>
<mat-tab label="4th tab"> </mat-tab>
<mat-tab label="5th tab"> </mat-tab>
<mat-tab label="6th tab"> </mat-tab>
</mat-tab-group>
</mat-card-content>
</mat-card>
Upvotes: 0
Reputation: 5111
[Long text solution] My goal was to make mat-tabs scrollable by default, without controls, but with the ability to auto-scroll when clicking on partially visible mat-tab to move it to the visible center-ish viewport.
1) This behavior already partially realized in this example "from the box" - https://stackblitz.com/edit/angular-mat-tabs-scrollalble-initial-behavior. The problem is that you can't scroll backward normally - there some buggy behavior to push tabs to the left. So it does not work as I wanted.
2) The next variant was with scrolling event - take a (wheel)="event"
on mat-tab-group - but its also not work with mobile. https://stackblitz.com/edit/angular-mat-tabs-scrollable-by-wheel-event Got it from this awesome comment above.
3) My own scroll of mat-tabs with scrolling on mobile and autoscrolling clicked tab to center of screen' viewport when you click on the tab was not too simple but it works! :)
first, you need to disable "from the box" scrolling when tapping on tabs and pagination buttons:
mat-tabs-override.scss:
$black: #121212;
@mixin media-query-for-mobile {
@media (max-width: 768px) and (min-width: 1px) {
@content;
}
}
mat-tab-group {
.mat-tab-header {
.mat-tab-header-pagination {
display: none !important; // <== disable pagination
}
.mat-tab-label-container {
left: 0px; // if you need to use it on mobile - set left position to 0
width: 100%;
.mat-tab-list {
overflow-x: auto !important; // <== set horisontal scroll bar imperatively
// below rule prevents sliding of buttons' container - because it not sliding properly - to left it not slide as well
transform: none !important;
.mat-tab-labels {
// some tweaks for tabs - up to you
@include media-query-for-mobile {
justify-content: unset !important;
}
.mat-tab-label {
// min-width: 20% !important;
padding: 1.25% !important;
margin: 0px !important;
text-transform: uppercase;
color: $black;
font-weight: 600;
min-width: 140px !important;
}
}
}
}
}
}
in this case you will see that all tabs are similar by width and scrollable on mobile.
Next, you need to make auto-scroll for tabs when you clicking on them and change their position to the center of the screen, based on the current viewport - let's do it!
We can create a directive, which will listen to the main container of <mat-tabs-group>
, check width of scrollable container .mat-tab-labels
and move the tab to be visible in viewport by auto-scrolling .mat-tabs-labels
container to needed way:
directive in template:
<mat-tab-group scrollToCenter>
<mat-tab label="tab 1"></mat-tab>
<mat-tab label="tab 2"></mat-tab>
<mat-tab label="tab 3"></mat-tab>
<mat-tab label="tab 4"></mat-tab>
<mat-tab label="tab 5"></mat-tab>
<mat-tab label="tab 6"></mat-tab>
<mat-tab label="tab 7"></mat-tab>
<mat-tab label="tab 8"></mat-tab>
</mat-tab-group>
directive.ts:
import { Directive, ElementRef, OnDestroy } from '@angular/core';
import { fromEvent, Subscription } from 'rxjs';
interface DOMRectI {
bottom: number;
height: number;
left: number; // position start of element
right: number; // position end of element
top: number;
width: number; // width of element
x?: number;
y?: number;
}
@Directive({
// tslint:disable-next-line:directive-selector
selector: '[scrollToCenter]',
})
export class MatTabScrollToCenterDirective implements OnDestroy {
isMobile: boolean;
subs = new Subscription();
constructor(
private element: ElementRef
) {
this.subs.add(
fromEvent(this.element.nativeElement, 'click').subscribe((clickedContainer: MouseEvent) => {
const scrollContainer = this.element.nativeElement.querySelector('.mat-tab-list');
const currentScrolledContainerPosition: number = scrollContainer.scrollLeft;
const newPositionScrollTo = this.calcScrollToCenterValue(clickedContainer, currentScrolledContainerPosition);
})
);
}
/** calculate scroll position to center of viewport */
calcScrollToCenterValue(clickedContainer, currentScrolledContainerPosition): number {
const scrolledButton: DOMRectI = (clickedContainer.target as HTMLElement).getBoundingClientRect();
const leftXOffset = (window.innerWidth - scrolledButton.width) / 2;
const currentVisibleViewportLeft = scrolledButton.left;
const neededLeftOffset = currentVisibleViewportLeft - leftXOffset;
console.log(scrolledButton);
const newValueToSCroll = currentScrolledContainerPosition + neededLeftOffset;
return newValueToSCroll;
}
ngOnDestroy() {
this.subs.unsubscribe();
}
}
And it works! :0 But not in ios and IE... Why? Because ios and IE don't support Element.scroll()
Solution - npm i element-scroll-polyfill
and set to polyfills.ts
/** enable polufill for element.scroll() on IE and ios */
import 'element-scroll-polyfill';
Great! but now scroll is not so smooth... IE and ios not support smooth-scroll-behavior.
Solution - npm i smoothscroll-polyfill
and add to polyfills.ts
import smoothscroll from 'smoothscroll-polyfill';
// enable polyfill
smoothscroll.polyfill();
Finally it works everywhere. Hope it helps somebody to fix mat-tabs autoscrolling emptiness :)
DEMO Enjoy it :)
Upvotes: 9
Reputation: 325
Try this pure css solution StackBlitz
Put this code in the same component where mat-tab is used
::ng-deep .mat-tab-header {
overflow-x: scroll !important;
}
::ng-deep .mat-tab-label-container {
overflow: visible !important;
}
::ng-deep .mat-tab-header::-webkit-scrollbar { // TO Remove horizontal scrollbar in tabs
display: none;
}
Upvotes: 3
Reputation: 60557
Try my solution in this stackblitz demo.
<mat-tab-group #tabGroup>
^^^^^^^^^
@ViewChild('tabGroup')
tabGroup;
<mat-tab-group (wheel)="scrollTabs($event)" #tabGroup>
^^^^^^^^^^^^^^^^^^
scrollTabs(event) {
const children = this.tabGroup._tabHeader._elementRef.nativeElement.children;
// get the tabGroup pagination buttons
const back = children[0];
const forward = children[2];
// depending on scroll direction click forward or back
if (event.deltaY > 0) {
forward.click();
} else {
back.click();
}
}
Disclaimer: This solution is very brittle. The Angular Material tabs do not offer an API for doing this. The solution depends on internal references that might change without notice (e. g. this private variable this.tabGroup._tabHeader
). Also, it does not yet stop the page from scrolling and only works with vertical scrolling. (Those two can be addressed though.)
Upvotes: 3