From fa07f1423ecdd5f787e49434415e0fbde2b4e7c4 Mon Sep 17 00:00:00 2001 From: lharries Date: Fri, 26 Jan 2018 10:57:49 +0000 Subject: [PATCH] feat(paginator): Add functionality to jump to first and last page Add two buttons which allow you to jump to the first and last page. Hidden by default and can be toggled with showHideFirstButtons Fixes #9278 --- src/demo-app/table/table-demo.html | 18 +++++ src/demo-app/table/table-demo.scss | 4 ++ src/demo-app/table/table-demo.ts | 8 ++- src/lib/paginator/_paginator-theme.scss | 13 +++- src/lib/paginator/paginator-intl.ts | 6 ++ src/lib/paginator/paginator.html | 26 ++++++- src/lib/paginator/paginator.md | 2 +- src/lib/paginator/paginator.scss | 64 +++++++++++++---- src/lib/paginator/paginator.spec.ts | 68 ++++++++++++++++++- src/lib/paginator/paginator.ts | 34 +++++++++- .../table-pagination-example.html | 3 +- 11 files changed, 221 insertions(+), 25 deletions(-) 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">