diff --git a/src/demo-app/table/table-demo.html b/src/demo-app/table/table-demo.html index f25906d05ca4..f9b39b3c6814 100644 --- a/src/demo-app/table/table-demo.html +++ b/src/demo-app/table/table-demo.html @@ -172,6 +172,24 @@

MatTable Example

+

MatTable with First/Last Buttons Example

+ +
+ + + + +

{{paginatorOutput | json}}

+ + + +
+

MatTable Using 'When' Rows for Interactive Details

diff --git a/src/demo-app/table/table-demo.scss b/src/demo-app/table/table-demo.scss index 71b93a1a4c6d..4f4a2cacf01e 100644 --- a/src/demo-app/table/table-demo.scss +++ b/src/demo-app/table/table-demo.scss @@ -141,3 +141,7 @@ background: #f5f5f5; } } + +.paginator-output { + margin-left: 20px; +} diff --git a/src/demo-app/table/table-demo.ts b/src/demo-app/table/table-demo.ts index e1295ff04f79..a2d01d46a194 100644 --- a/src/demo-app/table/table-demo.ts +++ b/src/demo-app/table/table-demo.ts @@ -9,7 +9,7 @@ import {Component, ElementRef, ViewChild} from '@angular/core'; import {PeopleDatabase, UserData} from './people-database'; import {PersonDataSource} from './person-data-source'; -import {MatPaginator, MatSort, MatTableDataSource} from '@angular/material'; +import {MatPaginator, MatSort, MatTableDataSource, PageEvent} from '@angular/material'; import {DetailRow, PersonDetailDataSource} from './person-detail-data-source'; import {animate, state, style, transition, trigger} from '@angular/animations'; import {SelectionModel} from '@angular/cdk/collections'; @@ -65,6 +65,8 @@ export class TableDemo { @ViewChild('paginatorForDataSource') paginatorForDataSource: MatPaginator; @ViewChild('sortForDataSource') sortForDataSource: MatSort; + paginatorOutput: PageEvent; + constructor(public _peopleDatabase: PeopleDatabase) { this.matTableDataSource.sortingDataAccessor = (data: UserData, property: string) => { switch (property) { @@ -183,4 +185,8 @@ export class TableDemo { toggleHighlight(property: string, enable: boolean) { enable ? this.highlights.add(property) : this.highlights.delete(property); } + + handlePaginator(pageEvent: PageEvent) { + this.paginatorOutput = pageEvent; + } } diff --git a/src/lib/paginator/_paginator-theme.scss b/src/lib/paginator/_paginator-theme.scss index ff7258557f49..3082d9472bf2 100644 --- a/src/lib/paginator/_paginator-theme.scss +++ b/src/lib/paginator/_paginator-theme.scss @@ -16,15 +16,22 @@ color: mat-color($foreground, secondary-text); } - .mat-paginator-increment, - .mat-paginator-decrement { + .mat-paginator-decrement, + .mat-paginator-increment { border-top: 2px solid mat-color($foreground, 'icon'); border-right: 2px solid mat-color($foreground, 'icon'); } + .mat-paginator-first, + .mat-paginator-last { + border-top: 2px solid mat-color($foreground, 'icon'); + } + .mat-icon-button[disabled] { + .mat-paginator-decrement, .mat-paginator-increment, - .mat-paginator-decrement { + .mat-paginator-first, + .mat-paginator-last { border-color: mat-color($foreground, 'disabled'); } } diff --git a/src/lib/paginator/paginator-intl.ts b/src/lib/paginator/paginator-intl.ts index 531e3aac9d22..a235c980c080 100644 --- a/src/lib/paginator/paginator-intl.ts +++ b/src/lib/paginator/paginator-intl.ts @@ -30,6 +30,12 @@ export class MatPaginatorIntl { /** A label for the button that decrements the current page. */ previousPageLabel: string = 'Previous page'; + /** A label for the button that moves to the first page. */ + firstPageLabel: string = 'First page'; + + /** A label for the button that moves to the last page. */ + lastPageLabel: string = 'Last page'; + /** A label for the range of items within the current page and the length of the whole list. */ getRangeLabel = (page: number, pageSize: number, length: number) => { if (length == 0 || pageSize == 0) { return `0 of ${length}`; } diff --git a/src/lib/paginator/paginator.html b/src/lib/paginator/paginator.html index 11bc5d24e7e1..4d28fb721d43 100644 --- a/src/lib/paginator/paginator.html +++ b/src/lib/paginator/paginator.html @@ -25,6 +25,17 @@ {{_intl.getRangeLabel(pageIndex, pageSize, length)}} + + diff --git a/src/lib/paginator/paginator.md b/src/lib/paginator/paginator.md index 7284bb8829eb..4359b57dc7a1 100644 --- a/src/lib/paginator/paginator.md +++ b/src/lib/paginator/paginator.md @@ -26,4 +26,4 @@ This will allow you to change the following: 3. The tooltip messages on the navigation buttons. ### Accessibility -The `aria-label`s for next page and previous page buttons can be set in `MatPaginatorIntl`. +The `aria-label`s for next page, previous page, first page and last page buttons can be set in `MatPaginatorIntl`. diff --git a/src/lib/paginator/paginator.scss b/src/lib/paginator/paginator.scss index c10a51d578df..11a9bcc0556d 100644 --- a/src/lib/paginator/paginator.scss +++ b/src/lib/paginator/paginator.scss @@ -15,8 +15,18 @@ $mat-paginator-button-margin: 8px; $mat-paginator-button-icon-height: 8px; $mat-paginator-button-icon-width: 8px; -$mat-paginator-button-decrement-icon-margin: 12px; -$mat-paginator-button-increment-icon-margin: 16px; +$mat-paginator-button-increment-icon-margin: 12px; +$mat-paginator-button-decrement-icon-margin: 16px; + +$mat-paginator-button-first-last-icon-width: 14px; + +$mat-paginator-button-first-icon-margin: 3px; +$mat-paginator-button-last-icon-margin: 15px; + + +$mat-paginator-button-first-decrement-icon-margin: 21px; +$mat-paginator-button-last-increment-icon-margin: 9px; + .mat-paginator { display: block; @@ -50,7 +60,7 @@ $mat-paginator-button-increment-icon-margin: 16px; margin: $mat-paginator-range-label-margin; } -.mat-paginator-increment-button + .mat-paginator-increment-button { +.mat-paginator-decrement-button + .mat-paginator-decrement-button { margin: 0 0 0 $mat-paginator-button-margin; [dir='rtl'] & { @@ -58,21 +68,28 @@ $mat-paginator-button-increment-icon-margin: 16px; } } -.mat-paginator-increment, -.mat-paginator-decrement { +.mat-paginator-decrement, +.mat-paginator-increment { width: $mat-paginator-button-icon-width; height: $mat-paginator-button-icon-height; } -.mat-paginator-decrement, -[dir='rtl'] .mat-paginator-increment { - transform: rotate(45deg); -} .mat-paginator-increment, [dir='rtl'] .mat-paginator-decrement { + transform: rotate(45deg); +} +.mat-paginator-decrement, +[dir='rtl'] .mat-paginator-increment { transform: rotate(225deg); } +.mat-paginator-increment { + margin-left: $mat-paginator-button-increment-icon-margin; + [dir='rtl'] & { + margin-right: $mat-paginator-button-increment-icon-margin; + } +} + .mat-paginator-decrement { margin-left: $mat-paginator-button-decrement-icon-margin; [dir='rtl'] & { @@ -80,13 +97,34 @@ $mat-paginator-button-increment-icon-margin: 16px; } } -.mat-paginator-increment { - margin-left: $mat-paginator-button-increment-icon-margin; - [dir='rtl'] & { - margin-right: $mat-paginator-button-increment-icon-margin; +.mat-paginator-first { + transform: rotate(90deg); + width: $mat-paginator-button-first-last-icon-width; + height: $mat-paginator-button-icon-height; + float:left; + margin-left: $mat-paginator-button-first-icon-margin; +} + +.mat-paginator-navigation-first { + .mat-paginator-decrement { + margin-left: $mat-paginator-button-first-decrement-icon-margin; } } +.mat-paginator-navigation-last { + .mat-paginator-increment { + float: left; + margin-left: $mat-paginator-button-last-increment-icon-margin; + } +} + +.mat-paginator-last { + transform: rotate(90deg); + width: $mat-paginator-button-first-last-icon-width; + height: $mat-paginator-button-icon-height; + margin-left: $mat-paginator-button-last-icon-margin; +} + .mat-paginator-range-actions { display: flex; align-items: center; diff --git a/src/lib/paginator/paginator.spec.ts b/src/lib/paginator/paginator.spec.ts index cccd55300c26..504615e32297 100644 --- a/src/lib/paginator/paginator.spec.ts +++ b/src/lib/paginator/paginator.spec.ts @@ -104,7 +104,7 @@ describe('MatPaginator', () => { })); }); - describe('when navigating with the navigation buttons', () => { + describe('when navigating with the next and previous buttons', () => { it('should be able to go to the next page', () => { expect(paginator.pageIndex).toBe(0); @@ -125,6 +125,58 @@ describe('MatPaginator', () => { expect(component.latestPageEvent ? component.latestPageEvent.pageIndex : null).toBe(0); }); + }); + + it('should be able to show the first/last buttons', () => { + expect(getFirstButton(fixture)) + .toBeNull('Expected first button to not exist.'); + + expect(getLastButton(fixture)) + .toBeNull('Expected last button to not exist.'); + + fixture.componentInstance.showFirstLastButtons = true; + fixture.detectChanges(); + + expect(getFirstButton(fixture)) + .toBeTruthy('Expected first button to be rendered.'); + + expect(getLastButton(fixture)) + .toBeTruthy('Expected last button to be rendered.'); + + }); + + describe('when showing the first and last button', () => { + + beforeEach(() => { + component.showFirstLastButtons = true; + fixture.detectChanges(); + }); + + it('should show right aria-labels for first/last buttons', () => { + expect(getFirstButton(fixture).getAttribute('aria-label')).toBe('First page'); + expect(getLastButton(fixture).getAttribute('aria-label')).toBe('Last page'); + }); + + it('should be able to go to the last page via the last page button', () => { + expect(paginator.pageIndex).toBe(0); + + dispatchMouseEvent(getLastButton(fixture), 'click'); + + expect(paginator.pageIndex).toBe(9); + expect(component.latestPageEvent ? component.latestPageEvent.pageIndex : null).toBe(9); + }); + + it('should be able to go to the first page via the first page button', () => { + paginator.pageIndex = 3; + fixture.detectChanges(); + expect(paginator.pageIndex).toBe(3); + + dispatchMouseEvent(getFirstButton(fixture), 'click'); + + expect(paginator.pageIndex).toBe(0); + expect(component.latestPageEvent ? component.latestPageEvent.pageIndex : null).toBe(0); + }); + it('should disable navigating to the next page if at last page', () => { component.goToLastPage(); fixture.detectChanges(); @@ -148,6 +200,7 @@ describe('MatPaginator', () => { expect(component.latestPageEvent).toBe(null); expect(paginator.pageIndex).toBe(0); }); + }); it('should mark for check when inputs are changed directly', () => { @@ -253,7 +306,7 @@ describe('MatPaginator', () => { expect(fixture.nativeElement.querySelector('.mat-select')).toBeNull(); }); - it('should handle the number inputs being passed in as strings', () => { + it('should handle the number inputs being passed in as strings', () => { const withStringFixture = TestBed.createComponent(MatPaginatorWithStringValues); const withStringPaginator = withStringFixture.componentInstance.paginator; @@ -277,6 +330,7 @@ describe('MatPaginator', () => { expect(element.querySelector('.mat-paginator-page-size')) .toBeNull('Expected select to be removed.'); }); + }); function getPreviousButton(fixture: ComponentFixture) { @@ -287,12 +341,21 @@ function getNextButton(fixture: ComponentFixture) { return fixture.nativeElement.querySelector('.mat-paginator-navigation-next'); } +function getFirstButton(fixture: ComponentFixture) { + return fixture.nativeElement.querySelector('.mat-paginator-navigation-first'); +} + +function getLastButton(fixture: ComponentFixture) { + return fixture.nativeElement.querySelector('.mat-paginator-navigation-last'); +} + @Component({ template: ` @@ -303,6 +366,7 @@ class MatPaginatorApp { pageSize = 10; pageSizeOptions = [5, 10, 25, 100]; hidePageSize = false; + showFirstLastButtons = false; length = 100; latestPageEvent: PageEvent | null; diff --git a/src/lib/paginator/paginator.ts b/src/lib/paginator/paginator.ts index e4e44636ed0f..95dfebc37c5d 100644 --- a/src/lib/paginator/paginator.ts +++ b/src/lib/paginator/paginator.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {coerceNumberProperty} from '@angular/cdk/coercion'; +import {coerceNumberProperty, coerceBooleanProperty} from '@angular/cdk/coercion'; import { ChangeDetectionStrategy, ChangeDetectorRef, @@ -100,6 +100,14 @@ export class MatPaginator implements OnInit, OnDestroy { /** Whether to hide the page size selection UI from the user. */ @Input() hidePageSize = false; + /** Whether to show the first/last buttons UI to the user. */ + @Input() + get showFirstLastButtons(): boolean { return this._showFirstLastButtons; } + set showFirstLastButtons(value: boolean) { + this._showFirstLastButtons = coerceBooleanProperty(value); + } + private _showFirstLastButtons = false; + /** Event emitted when the paginator changes the page size or page index. */ @Output() readonly page = new EventEmitter(); @@ -134,6 +142,22 @@ export class MatPaginator implements OnInit, OnDestroy { this._emitPageEvent(); } + /** Move to the first page if not already there. */ + firstPage(): void { + // hasPreviousPage being false implies at the start + if (!this.hasPreviousPage()) { return; } + this.pageIndex = 0; + this._emitPageEvent(); + } + + /** Move to the last page if not already there. */ + lastPage(): void { + // hasNextPage being false implies at the end + if (!this.hasNextPage()) { return; } + this.pageIndex = this.getNumberOfPages(); + this._emitPageEvent(); + } + /** Whether there is a previous page. */ hasPreviousPage(): boolean { return this.pageIndex >= 1 && this.pageSize != 0; @@ -141,10 +165,16 @@ export class MatPaginator implements OnInit, OnDestroy { /** Whether there is a next page. */ hasNextPage(): boolean { - const numberOfPages = Math.ceil(this.length / this.pageSize) - 1; + const numberOfPages = this.getNumberOfPages(); return this.pageIndex < numberOfPages && this.pageSize != 0; } + /** Calculate the number of pages */ + getNumberOfPages(): number { + return Math.ceil(this.length / this.pageSize) - 1; + } + + /** * Changes the page size so that the first item displayed on the page will still be * displayed using the new page size. diff --git a/src/material-examples/table-pagination/table-pagination-example.html b/src/material-examples/table-pagination/table-pagination-example.html index ff362ebab4c4..cea5b5f235f7 100644 --- a/src/material-examples/table-pagination/table-pagination-example.html +++ b/src/material-examples/table-pagination/table-pagination-example.html @@ -31,6 +31,7 @@ + [pageSizeOptions]="[5, 10, 20]" + [showFirstLastButtons]="true">