diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1a09e0a7502c..5d2fe3e574b6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -109,6 +109,7 @@ /src/material-experimental/mdc-list/** @mmalerba @devversion /src/material-experimental/mdc-menu/** @crisbeto /src/material-experimental/mdc-select/** @crisbeto +/src/material-experimental/mdc-paginator/** @crisbeto /src/material-experimental/mdc-progress-spinner/** @andrewseguin /src/material-experimental/mdc-progress-bar/** @andrewseguin /src/material-experimental/mdc-radio/** @mmalerba @@ -183,6 +184,7 @@ /src/dev-app/mdc-input/** @devversion @mmalerba /src/dev-app/mdc-list/** @mmalerba /src/dev-app/mdc-menu/** @crisbeto +/src/dev-app/mdc-paginator/** @crisbeto /src/dev-app/mdc-progress-bar/** @crisbeto /src/dev-app/mdc-progress-spinner/** @annieyw @mmalerba /src/dev-app/mdc-radio/** @mmalerba diff --git a/.ng-dev/commit-message.ts b/.ng-dev/commit-message.ts index 369ebda6d5ee..106a9d25779c 100644 --- a/.ng-dev/commit-message.ts +++ b/.ng-dev/commit-message.ts @@ -52,6 +52,7 @@ export const commitMessage: CommitMessageConfig = { 'material-experimental/mdc-input', 'material-experimental/mdc-list', 'material-experimental/mdc-menu', + 'material-experimental/mdc-paginator', 'material-experimental/mdc-progress-bar', 'material-experimental/mdc-progress-spinner', 'material-experimental/mdc-radio', diff --git a/src/dev-app/BUILD.bazel b/src/dev-app/BUILD.bazel index 479d55330fa5..a1e093f4d9d9 100644 --- a/src/dev-app/BUILD.bazel +++ b/src/dev-app/BUILD.bazel @@ -56,6 +56,7 @@ ng_module( "//src/dev-app/mdc-input", "//src/dev-app/mdc-list", "//src/dev-app/mdc-menu", + "//src/dev-app/mdc-paginator", "//src/dev-app/mdc-progress-bar", "//src/dev-app/mdc-progress-spinner", "//src/dev-app/mdc-radio", diff --git a/src/dev-app/dev-app/dev-app-layout.ts b/src/dev-app/dev-app/dev-app-layout.ts index 9eee7035b3a3..c945487f7e61 100644 --- a/src/dev-app/dev-app/dev-app-layout.ts +++ b/src/dev-app/dev-app/dev-app-layout.ts @@ -91,6 +91,7 @@ export class DevAppLayout { {name: 'MDC List', route: '/mdc-list'}, {name: 'MDC Menu', route: '/mdc-menu'}, {name: 'MDC Radio', route: '/mdc-radio'}, + {name: 'MDC Paginator', route: '/mdc-paginator'}, {name: 'MDC Progress Bar', route: '/mdc-progress-bar'}, {name: 'MDC Progress Spinner', route: '/mdc-progress-spinner'}, {name: 'MDC Tabs', route: '/mdc-tabs'}, diff --git a/src/dev-app/dev-app/routes.ts b/src/dev-app/dev-app/routes.ts index 93e137d54e5a..c6900d55d88c 100644 --- a/src/dev-app/dev-app/routes.ts +++ b/src/dev-app/dev-app/routes.ts @@ -89,6 +89,10 @@ export const DEV_APP_ROUTES: Routes = [ {path: 'mdc-input', loadChildren: 'mdc-input/mdc-input-demo-module#MdcInputDemoModule'}, {path: 'mdc-list', loadChildren: 'mdc-list/mdc-list-demo-module#MdcListDemoModule'}, {path: 'mdc-menu', loadChildren: 'mdc-menu/mdc-menu-demo-module#MdcMenuDemoModule'}, + { + path: 'mdc-paginator', + loadChildren: 'mdc-paginator/mdc-paginator-demo-module#MdcPaginatorDemoModule' + }, { path: 'mdc-progress-spinner', loadChildren: diff --git a/src/dev-app/mdc-paginator/BUILD.bazel b/src/dev-app/mdc-paginator/BUILD.bazel new file mode 100644 index 000000000000..c6471d0122e7 --- /dev/null +++ b/src/dev-app/mdc-paginator/BUILD.bazel @@ -0,0 +1,26 @@ +load("//tools:defaults.bzl", "ng_module", "sass_binary") + +package(default_visibility = ["//visibility:public"]) + +ng_module( + name = "mdc-paginator", + srcs = glob(["**/*.ts"]), + assets = [ + "mdc-paginator-demo.html", + ":mdc_paginator_demo_scss", + ], + deps = [ + "//src/material-experimental/mdc-card", + "//src/material-experimental/mdc-form-field", + "//src/material-experimental/mdc-input", + "//src/material-experimental/mdc-paginator", + "//src/material-experimental/mdc-slide-toggle", + "@npm//@angular/forms", + "@npm//@angular/router", + ], +) + +sass_binary( + name = "mdc_paginator_demo_scss", + src = "mdc-paginator-demo.scss", +) diff --git a/src/dev-app/mdc-paginator/mdc-paginator-demo-module.ts b/src/dev-app/mdc-paginator/mdc-paginator-demo-module.ts new file mode 100644 index 000000000000..700e95738152 --- /dev/null +++ b/src/dev-app/mdc-paginator/mdc-paginator-demo-module.ts @@ -0,0 +1,34 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {MatCardModule} from '@angular/material-experimental/mdc-card'; +import {MatFormFieldModule} from '@angular/material-experimental/mdc-form-field'; +import {MatInputModule} from '@angular/material-experimental/mdc-input'; +import {MatPaginatorModule} from '@angular/material-experimental/mdc-paginator'; +import {MatSlideToggleModule} from '@angular/material-experimental/mdc-slide-toggle'; +import {RouterModule} from '@angular/router'; +import {MdcPaginatorDemo} from './mdc-paginator-demo'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + MatCardModule, + MatFormFieldModule, + MatInputModule, + MatPaginatorModule, + MatSlideToggleModule, + RouterModule.forChild([{path: '', component: MdcPaginatorDemo}]), + ], + declarations: [MdcPaginatorDemo], +}) +export class MdcPaginatorDemoModule { +} diff --git a/src/dev-app/mdc-paginator/mdc-paginator-demo.html b/src/dev-app/mdc-paginator/mdc-paginator-demo.html new file mode 100644 index 000000000000..30b7abf00ff5 --- /dev/null +++ b/src/dev-app/mdc-paginator/mdc-paginator-demo.html @@ -0,0 +1,42 @@ + +

No inputs

+ +
+ + +
+ + Length + + + + + Page Size + + + + + Page Index + + + + Hide page size + Show multiple page size options + Show first/last buttons + Disabled +
+ + + + +
Output event: {{pageEvent | json}}
+
getNumberOfPages: {{paginator.getNumberOfPages()}}
+
diff --git a/src/dev-app/mdc-paginator/mdc-paginator-demo.scss b/src/dev-app/mdc-paginator/mdc-paginator-demo.scss new file mode 100644 index 000000000000..32628c37915e --- /dev/null +++ b/src/dev-app/mdc-paginator/mdc-paginator-demo.scss @@ -0,0 +1,20 @@ +.demo-section { + max-width: 600px; + margin-bottom: 24px; + padding: 16px; + background: #efefef !important; + + & > * { + margin: 0 0 32px; + } +} + +.demo-options { + display: flex; + flex-direction: column; + max-width: 300px; + + .mat-mdc-slide-toggle { + margin-bottom: 8px; + } +} diff --git a/src/dev-app/mdc-paginator/mdc-paginator-demo.ts b/src/dev-app/mdc-paginator/mdc-paginator-demo.ts new file mode 100644 index 000000000000..1e687b5a6501 --- /dev/null +++ b/src/dev-app/mdc-paginator/mdc-paginator-demo.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Component} from '@angular/core'; +import {PageEvent} from '@angular/material-experimental/mdc-paginator'; + +@Component({ + selector: 'mdc-paginator-demo', + templateUrl: 'mdc-paginator-demo.html', + styleUrls: ['mdc-paginator-demo.css'], +}) +export class MdcPaginatorDemo { + length = 50; + pageSize = 10; + pageIndex = 0; + pageSizeOptions = [5, 10, 25]; + + hidePageSize = false; + showPageSizeOptions = true; + showFirstLastButtons = true; + disabled = false; + + pageEvent: PageEvent; + + handlePageEvent(e: PageEvent) { + this.pageEvent = e; + this.length = e.length; + this.pageSize = e.pageSize; + this.pageIndex = e.pageIndex; + } +} diff --git a/src/dev-app/paginator/paginator-demo.scss b/src/dev-app/paginator/paginator-demo.scss index 01e52aa28fa5..f0804fe3547e 100644 --- a/src/dev-app/paginator/paginator-demo.scss +++ b/src/dev-app/paginator/paginator-demo.scss @@ -4,7 +4,7 @@ background: #efefef !important; & > * { - margin: 32px 0; + margin: 0 0 32px; } } diff --git a/src/dev-app/paginator/paginator-demo.ts b/src/dev-app/paginator/paginator-demo.ts index ad4e947dc0af..9ab6c0a07774 100644 --- a/src/dev-app/paginator/paginator-demo.ts +++ b/src/dev-app/paginator/paginator-demo.ts @@ -6,14 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, ViewEncapsulation} from '@angular/core'; +import {Component} from '@angular/core'; import {PageEvent} from '@angular/material/paginator'; @Component({ selector: 'paginator-demo', templateUrl: 'paginator-demo.html', styleUrls: ['paginator-demo.css'], - encapsulation: ViewEncapsulation.None, }) export class PaginatorDemo { length = 50; diff --git a/src/material-experimental/config.bzl b/src/material-experimental/config.bzl index bd597da02e56..13e2d979ecde 100644 --- a/src/material-experimental/config.bzl +++ b/src/material-experimental/config.bzl @@ -21,6 +21,7 @@ entryPoints = [ "mdc-list", "mdc-menu", "mdc-menu/testing", + "mdc-paginator", "mdc-progress-bar", "mdc-progress-bar/testing", "mdc-progress-spinner", diff --git a/src/material-experimental/mdc-paginator/BUILD.bazel b/src/material-experimental/mdc-paginator/BUILD.bazel new file mode 100644 index 000000000000..f8ab24a62576 --- /dev/null +++ b/src/material-experimental/mdc-paginator/BUILD.bazel @@ -0,0 +1,79 @@ +load( + "//tools:defaults.bzl", + "ng_module", + "ng_test_library", + "ng_web_test_suite", + "sass_binary", + "sass_library", +) + +package(default_visibility = ["//visibility:public"]) + +ng_module( + name = "mdc-paginator", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + assets = [":paginator.css"] + glob(["**/*.html"]), + module_name = "@angular/material-experimental/mdc-paginator", + deps = [ + "//src/material-experimental/mdc-button", + "//src/material-experimental/mdc-select", + "//src/material/paginator", + "//src/material/tooltip", + "@npm//@angular/common", + "@npm//@angular/core", + "@npm//@angular/forms", # TODO(jelbourn): transitive dep via generated code + "@npm//rxjs", + ], +) + +sass_library( + name = "mdc_paginator_scss_lib", + srcs = glob(["**/_*.scss"]), + deps = [ + "//src/material/core:core_scss_lib", + ], +) + +sass_binary( + name = "mdc_paginator_scss", + src = "paginator.scss", + include_paths = [ + "external/npm/node_modules", + ], + deps = [ + "//src/material-experimental/mdc-form-field:mdc_form_field_scss_lib", + ], +) + +ng_test_library( + name = "mdc_paginator_tests_lib", + srcs = glob( + ["**/*.spec.ts"], + exclude = ["**/*.e2e.spec.ts"], + ), + deps = [ + ":mdc-paginator", + "//src/cdk/testing/private", + "//src/material-experimental/mdc-select", + "//src/material/core", + "@npm//@angular/platform-browser", + ], +) + +ng_web_test_suite( + name = "unit_tests", + static_files = [ + "@npm//:node_modules/@material/textfield/dist/mdc.textfield.js", + "@npm//:node_modules/@material/line-ripple/dist/mdc.lineRipple.js", + "@npm//:node_modules/@material/notched-outline/dist/mdc.notchedOutline.js", + "@npm//:node_modules/@material/ripple/dist/mdc.ripple.js", + "@npm//:node_modules/@material/dom/dist/mdc.dom.js", + ], + deps = [ + ":mdc_paginator_tests_lib", + "//src/material-experimental:mdc_require_config.js", + ], +) diff --git a/src/material-experimental/mdc-paginator/README.md b/src/material-experimental/mdc-paginator/README.md new file mode 100644 index 000000000000..61d95bd14fbc --- /dev/null +++ b/src/material-experimental/mdc-paginator/README.md @@ -0,0 +1,102 @@ +This is prototype of an alternate version of `` built on top of +[MDC Web](https://github.com/material-components/material-components-web). It demonstrates how +Angular Material could use MDC Web under the hood while still exposing the same API Angular users as +the existing ``. This component is experimental and should not be used in production. + +## How to use +Assuming your application is already up and running using Angular Material, you can add this +component by following these steps: + +1. Install Angular Material Experimental & MDC WEB: + + ```bash + npm i material-components-web @angular/material-experimental + ``` + +2. In your `angular.json`, make sure `node_modules/` is listed as a Sass include path. This is + needed for the Sass compiler to be able to find the MDC Web Sass files. + + ```json + ... + "styles": [ + "src/styles.scss" + ], + "stylePreprocessorOptions": { + "includePaths": [ + "node_modules/" + ] + }, + ... + ``` + +3. Import the experimental `MatPaginatorModule` and add it to the module that declares your + component: + + ```ts + import {MatPaginatorModule} from '@angular/material-experimental/mdc-paginator'; + + @NgModule({ + declarations: [MyComponent], + imports: [MatPaginatorModule], + }) + export class MyModule {} + ``` + +4. Use `` in your component's template, just like you would the normal + ``: + + ```html + + ``` + +5. Add the theme and typography mixins to your Sass. (There is currently no pre-built CSS option for + the experimental ``): + + ```scss + @import '~@angular/material/theming'; + @import '~@angular/material-experimental/mdc-paginator'; + + $my-primary: mat-palette($mat-indigo); + $my-accent: mat-palette($mat-pink, A200, A100, A400); + $my-theme: mat-light-theme(( + color: ( + primary: $my-primary, + accent: $my-accent + ) + )); + + @include mat-mdc-paginator-theme($my-theme); + @include mat-mdc-paginator-typography(); + ``` + +## API differences +The experimental paginator API closely matches the +[API of the standard paginator](https://material.angular.io/components/paginator/api). +`@angular/material-experimental/mdc-paginator` exports symbols with the same name and public +interface as all of the symbols found under `@angular/material/paginator`, except for the following +differences: + +* The experimental paginator module has a `MatPaginatorDefaultOptions` interface that is identical +to the one from `@angular/material/paginator`, with the exception of the `formFieldAppearance` +property whose type is narrower. It allows only the `fill` and `outline` appearances, because these +are the appearances supported by the MDC-based `MatFormField`. + +## Replacing the standard paginator in an existing app +Because the experimental API mirrors the API for the standard paginator, it can easily be swapped in +by just changing the import paths. There is currently no schematic for this, but you can run the +following string replace across your TypeScript files: + +```bash +grep -lr --include="*.ts" --exclude-dir="node_modules" \ + --exclude="*.d.ts" "['\"]@angular/material/paginator['\"]" | xargs sed -i \ + "s/['\"]@angular\/material\/paginator['\"]/'@angular\/material-experimental\/mdc-paginator'/g" +``` + +CSS styles and tests that depend on implementation details of `mat-paginator` (such as getting +elements from the template by class name) will need to be manually updated. + +There are some small visual differences between this paginator and the standard `mat-paginator`. +This paginator depends on `MatFormField` and `MatButton` which are also based on MDC, putting them +closer to the Material Design specification while making them slightly wider. You may have to +account for the wider paginator in your app's layout. Furthermore, the form field inside the +paginator only supports the `outline` and `fill` appearances. diff --git a/src/material-experimental/mdc-paginator/_paginator-theme.scss b/src/material-experimental/mdc-paginator/_paginator-theme.scss new file mode 100644 index 000000000000..723cc249ad81 --- /dev/null +++ b/src/material-experimental/mdc-paginator/_paginator-theme.scss @@ -0,0 +1,84 @@ +@import '@material/theme/variables.import'; +@import '@material/typography/variables.import'; +@import '../../material/core/theming/check-duplicate-styles'; +@import './paginator-variables'; + +@mixin mat-mdc-paginator-color($config-or-theme) { + $config: mat-get-color-config($config-or-theme); + @include mat-using-mdc-theme($config) { + $icon-color: rgba(mdc-theme-prop-value(on-surface), 0.54); + $disabled-color: rgba(mdc-theme-prop-value(on-surface), 0.12); + + .mat-mdc-paginator { + background: mdc-theme-prop-value(surface); + color: rgba(mdc-theme-prop-value(on-surface), 0.87); + } + + .mat-mdc-paginator-icon { + fill: $icon-color; + } + + .mat-mdc-paginator-decrement, + .mat-mdc-paginator-increment { + border-top: 2px solid $icon-color; + border-right: 2px solid $icon-color; + } + + .mat-mdc-paginator-first, + .mat-mdc-paginator-last { + border-top: 2px solid $icon-color; + } + + .mat-mdc-icon-button[disabled] { + .mat-mdc-paginator-decrement, + .mat-mdc-paginator-increment, + .mat-mdc-paginator-first, + .mat-mdc-paginator-last { + border-color: $disabled-color; + } + + .mat-mdc-paginator-icon { + fill: $disabled-color; + } + } + } +} + +@mixin mat-mdc-paginator-typography($config-or-theme) { + $config: mat-get-typography-config($config-or-theme); + .mat-mdc-paginator { + @include mat-using-mdc-typography($config) { + @include mdc-typography(caption, $query: $mat-typography-styles-query); + } + } +} + +@mixin _mat-mdc-paginator-density($config-or-theme) { + $density-scale: mat-get-density-config($config-or-theme); + $height: _mat-density-prop-value($mat-paginator-density-config, $density-scale, height); + + @include _mat-density-legacy-compatibility() { + .mat-mdc-paginator-container { + min-height: $height; + } + } +} + +@mixin mat-mdc-paginator-theme($theme-or-color-config) { + $theme: _mat-legacy-get-theme($theme-or-color-config); + @include _mat-check-duplicate-theme-styles($theme, 'mat-mdc-paginator') { + $color: mat-get-color-config($theme); + $density: mat-get-density-config($theme); + $typography: mat-get-typography-config($theme); + + @if $color != null { + @include mat-mdc-paginator-color($color); + } + @if $density != null { + @include _mat-mdc-paginator-density($density); + } + @if $typography != null { + @include mat-mdc-paginator-typography($typography); + } + } +} diff --git a/src/material-experimental/mdc-paginator/_paginator-variables.scss b/src/material-experimental/mdc-paginator/_paginator-variables.scss new file mode 100644 index 000000000000..1625dcc5eb33 --- /dev/null +++ b/src/material-experimental/mdc-paginator/_paginator-variables.scss @@ -0,0 +1,13 @@ +$mat-paginator-height: 56px !default; +// Minimum height for paginator's in the highest density is determined based on how +// much the paginator can shrink until the content exceeds (i.e. navigation buttons). +$mat-paginator-minimum-height: 40px !default; +$mat-paginator-maximum-height: $mat-paginator-height !default; + +$mat-paginator-density-config: ( + height: ( + default: $mat-paginator-height, + maximum: $mat-paginator-maximum-height, + minimum: $mat-paginator-minimum-height, + ) +) !default; diff --git a/src/material-experimental/mdc-paginator/index.ts b/src/material-experimental/mdc-paginator/index.ts new file mode 100644 index 000000000000..676ca90f1ffa --- /dev/null +++ b/src/material-experimental/mdc-paginator/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './public-api'; diff --git a/src/material-experimental/mdc-paginator/paginator-module.ts b/src/material-experimental/mdc-paginator/paginator-module.ts new file mode 100644 index 000000000000..6c2e88834a62 --- /dev/null +++ b/src/material-experimental/mdc-paginator/paginator-module.ts @@ -0,0 +1,29 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {MAT_PAGINATOR_INTL_PROVIDER} from '@angular/material/paginator'; +import {MatButtonModule} from '@angular/material-experimental/mdc-button'; +import {MatSelectModule} from '@angular/material-experimental/mdc-select'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {MatPaginator} from './paginator'; + + +@NgModule({ + imports: [ + CommonModule, + MatButtonModule, + MatSelectModule, + MatTooltipModule, + ], + exports: [MatPaginator], + declarations: [MatPaginator], + providers: [MAT_PAGINATOR_INTL_PROVIDER], +}) +export class MatPaginatorModule {} diff --git a/src/material-experimental/mdc-paginator/paginator.html b/src/material-experimental/mdc-paginator/paginator.html new file mode 100644 index 000000000000..37cb9e21bc68 --- /dev/null +++ b/src/material-experimental/mdc-paginator/paginator.html @@ -0,0 +1,86 @@ +
+
+
+
+ {{_intl.itemsPerPageLabel}} +
+ + + + + {{pageSizeOption}} + + + + +
{{pageSize}}
+
+ +
+
+ {{_intl.getRangeLabel(pageIndex, pageSize, length)}} +
+ + + + + +
+
+
diff --git a/src/material-experimental/mdc-paginator/paginator.scss b/src/material-experimental/mdc-paginator/paginator.scss new file mode 100644 index 000000000000..7470cb2edd0d --- /dev/null +++ b/src/material-experimental/mdc-paginator/paginator.scss @@ -0,0 +1,83 @@ +@import '../mdc-form-field/form-field-theme'; + +$mat-mdc-paginator-padding: 0 8px; +$mat-mdc-paginator-page-size-margin-right: 8px; + +$mat-mdc-paginator-items-per-page-label-margin: 0 4px; +$mat-mdc-paginator-selector-margin: 0 4px; +$mat-mdc-paginator-selector-trigger-width: 84px; + +$mat-mdc-paginator-range-label-margin: 0 32px 0 24px; +$mat-mdc-paginator-button-icon-size: 28px; + +.mat-mdc-paginator { + display: block; + + // We need the form field to be as narrow as possible in order to fit + // into the paginator so we always use the densest layout available. + @include mat-mdc-form-field-density(minimum); + + // This element reserves space for hints and error messages. + // Hide it since we know that we won't need it. + .mat-mdc-form-field-subscript-wrapper { + display: none; + } + + .mat-mdc-select { + // The smaller font size inherited from the paginator throws off the centering of the select + // inside the form field. This `line-height` helps to center it relative to the other text. + line-height: 1.5; + } +} + +// Note: this wrapper element is only used to get the flexbox vertical centering to work +// with the `min-height` on IE11. It can be removed if we drop support for IE. +.mat-mdc-paginator-outer-container { + display: flex; +} + +.mat-mdc-paginator-container { + display: flex; + align-items: center; + justify-content: flex-end; + padding: $mat-mdc-paginator-padding; + flex-wrap: wrap-reverse; + width: 100%; +} + +.mat-mdc-paginator-page-size { + display: flex; + align-items: baseline; + margin-right: $mat-mdc-paginator-page-size-margin-right; + + [dir='rtl'] & { + margin-right: 0; + margin-left: $mat-mdc-paginator-page-size-margin-right; + } +} + +.mat-mdc-paginator-page-size-label { + margin: $mat-mdc-paginator-items-per-page-label-margin; +} + +.mat-mdc-paginator-page-size-select { + margin: $mat-mdc-paginator-selector-margin; + width: $mat-mdc-paginator-selector-trigger-width; +} + +.mat-mdc-paginator-range-label { + margin: $mat-mdc-paginator-range-label-margin; +} + +.mat-mdc-paginator-range-actions { + display: flex; + align-items: center; +} + +.mat-mdc-paginator-icon { + width: $mat-mdc-paginator-button-icon-size; + + [dir='rtl'] & { + transform: rotate(180deg); + } +} diff --git a/src/material-experimental/mdc-paginator/paginator.spec.ts b/src/material-experimental/mdc-paginator/paginator.spec.ts new file mode 100644 index 000000000000..8694b27e9ee9 --- /dev/null +++ b/src/material-experimental/mdc-paginator/paginator.spec.ts @@ -0,0 +1,558 @@ +import {ComponentFixture, TestBed, tick, fakeAsync} from '@angular/core/testing'; +import {Component, ViewChild, Type, Provider} from '@angular/core'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {dispatchMouseEvent} from '@angular/cdk/testing/private'; +import {ThemePalette} from '@angular/material/core'; +import {MatSelect} from '@angular/material-experimental/mdc-select'; +import {By} from '@angular/platform-browser'; +import {MatPaginatorModule, MatPaginator, MatPaginatorIntl} from './index'; +import {MAT_PAGINATOR_DEFAULT_OPTIONS, MatPaginatorDefaultOptions} from './paginator'; + + +describe('MDC-based MatPaginator', () => { + function createComponent(type: Type, providers: Provider[] = []): ComponentFixture { + TestBed.configureTestingModule({ + imports: [MatPaginatorModule, NoopAnimationsModule], + declarations: [type], + providers: [MatPaginatorIntl, ...providers] + }).compileComponents(); + + const fixture = TestBed.createComponent(type); + fixture.detectChanges(); + return fixture; + } + + describe('with the default internationalization provider', () => { + it('should show the right range text', () => { + const fixture = createComponent(MatPaginatorApp); + const component = fixture.componentInstance; + const rangeElement = fixture.nativeElement.querySelector('.mat-mdc-paginator-range-label'); + + // View second page of list of 100, each page contains 10 items. + component.length = 100; + component.pageSize = 10; + component.pageIndex = 1; + fixture.detectChanges(); + expect(rangeElement.innerText.trim()).toBe('11 – 20 of 100'); + + // View third page of list of 200, each page contains 20 items. + component.length = 200; + component.pageSize = 20; + component.pageIndex = 2; + fixture.detectChanges(); + expect(rangeElement.innerText.trim()).toBe('41 – 60 of 200'); + + // View first page of list of 0, each page contains 5 items. + component.length = 0; + component.pageSize = 5; + component.pageIndex = 2; + fixture.detectChanges(); + expect(rangeElement.innerText.trim()).toBe('0 of 0'); + + // View third page of list of 12, each page contains 5 items. + component.length = 12; + component.pageSize = 5; + component.pageIndex = 2; + fixture.detectChanges(); + expect(rangeElement.innerText.trim()).toBe('11 – 12 of 12'); + + // View third page of list of 10, each page contains 5 items. + component.length = 10; + component.pageSize = 5; + component.pageIndex = 2; + fixture.detectChanges(); + expect(rangeElement.innerText.trim()).toBe('11 – 15 of 10'); + + // View third page of list of -5, each page contains 5 items. + component.length = -5; + component.pageSize = 5; + component.pageIndex = 2; + fixture.detectChanges(); + expect(rangeElement.innerText.trim()).toBe('11 – 15 of 0'); + }); + + it('should show right aria-labels for select and buttons', () => { + const fixture = createComponent(MatPaginatorApp); + const select = fixture.nativeElement.querySelector('.mat-mdc-select'); + expect(select.getAttribute('aria-label')).toBe('Items per page:'); + + expect(getPreviousButton(fixture).getAttribute('aria-label')).toBe('Previous page'); + expect(getNextButton(fixture).getAttribute('aria-label')).toBe('Next page'); + }); + + it('should re-render when the i18n labels change', () => { + const fixture = createComponent(MatPaginatorApp); + const label = fixture.nativeElement.querySelector('.mat-mdc-paginator-page-size-label'); + const intl = TestBed.get(MatPaginatorIntl); + + intl.itemsPerPageLabel = '1337 items per page'; + intl.changes.next(); + fixture.detectChanges(); + + expect(label.textContent!.trim()).toBe('1337 items per page'); + }); + }); + + describe('when navigating with the next and previous buttons', () => { + it('should be able to go to the next page', () => { + const fixture = createComponent(MatPaginatorApp); + const component = fixture.componentInstance; + const paginator = component.paginator; + expect(paginator.pageIndex).toBe(0); + + dispatchMouseEvent(getNextButton(fixture), 'click'); + + expect(paginator.pageIndex).toBe(1); + expect(component.pageEvent).toHaveBeenCalledWith(jasmine.objectContaining({ + previousPageIndex: 0, + pageIndex: 1 + })); + }); + + it('should be able to go to the previous page', () => { + const fixture = createComponent(MatPaginatorApp); + const component = fixture.componentInstance; + const paginator = component.paginator; + paginator.pageIndex = 1; + fixture.detectChanges(); + expect(paginator.pageIndex).toBe(1); + + dispatchMouseEvent(getPreviousButton(fixture), 'click'); + + expect(paginator.pageIndex).toBe(0); + expect(component.pageEvent).toHaveBeenCalledWith(jasmine.objectContaining({ + previousPageIndex: 1, + pageIndex: 0 + })); + }); + }); + + it('should be able to show the first/last buttons', () => { + const fixture = createComponent(MatPaginatorApp); + 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.'); + }); + + it('should mark itself as initialized', fakeAsync(() => { + const fixture = createComponent(MatPaginatorApp); + const component = fixture.componentInstance; + const paginator = component.paginator; + let isMarkedInitialized = false; + paginator.initialized.subscribe(() => isMarkedInitialized = true); + + tick(); + expect(isMarkedInitialized).toBeTruthy(); + })); + + it('should not allow a negative pageSize', () => { + const fixture = createComponent(MatPaginatorApp); + const component = fixture.componentInstance; + const paginator = component.paginator; + paginator.pageSize = -1337; + expect(paginator.pageSize).toBeGreaterThanOrEqual(0); + }); + + it('should not allow a negative pageIndex', () => { + const fixture = createComponent(MatPaginatorApp); + const component = fixture.componentInstance; + const paginator = component.paginator; + paginator.pageIndex = -42; + expect(paginator.pageIndex).toBeGreaterThanOrEqual(0); + }); + + it('should be able to set the color of the form field', () => { + const fixture = createComponent(MatPaginatorApp); + const component = fixture.componentInstance; + const formField: HTMLElement = fixture.nativeElement.querySelector('.mat-mdc-form-field'); + + component.color = 'accent'; + fixture.detectChanges(); + + expect(formField.classList).not.toContain('mat-warn'); + expect(formField.classList).toContain('mat-accent'); + + component.color = 'warn'; + fixture.detectChanges(); + expect(formField.classList).toContain('mat-warn'); + expect(formField.classList).not.toContain('mat-accent'); + }); + + describe('when showing the first and last button', () => { + let fixture: ComponentFixture; + let component: MatPaginatorApp; + let paginator: MatPaginator; + + beforeEach(() => { + fixture = createComponent(MatPaginatorApp); + component = fixture.componentInstance; + paginator = component.paginator; + 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.pageEvent).toHaveBeenCalledWith(jasmine.objectContaining({ + previousPageIndex: 0, + pageIndex: 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.pageEvent).toHaveBeenCalledWith(jasmine.objectContaining({ + previousPageIndex: 3, + pageIndex: 0 + })); + }); + + it('should disable navigating to the next page if at last page', () => { + component.goToLastPage(); + fixture.detectChanges(); + expect(paginator.pageIndex).toBe(9); + expect(paginator.hasNextPage()).toBe(false); + + component.pageEvent.calls.reset(); + dispatchMouseEvent(getNextButton(fixture), 'click'); + + expect(component.pageEvent).not.toHaveBeenCalled(); + expect(paginator.pageIndex).toBe(9); + }); + + it('should disable navigating to the previous page if at first page', () => { + expect(paginator.pageIndex).toBe(0); + expect(paginator.hasPreviousPage()).toBe(false); + + component.pageEvent.calls.reset(); + dispatchMouseEvent(getPreviousButton(fixture), 'click'); + + expect(component.pageEvent).not.toHaveBeenCalled(); + expect(paginator.pageIndex).toBe(0); + }); + + }); + + it('should mark for check when inputs are changed directly', () => { + const fixture = createComponent(MatPaginatorApp); + const component = fixture.componentInstance; + const paginator = component.paginator; + const rangeElement = fixture.nativeElement.querySelector('.mat-mdc-paginator-range-label'); + + expect(rangeElement.innerText.trim()).toBe('1 – 10 of 100'); + + paginator.length = 99; + fixture.detectChanges(); + expect(rangeElement.innerText.trim()).toBe('1 – 10 of 99'); + + paginator.pageSize = 6; + fixture.detectChanges(); + expect(rangeElement.innerText.trim()).toBe('1 – 6 of 99'); + + paginator.pageIndex = 1; + fixture.detectChanges(); + expect(rangeElement.innerText.trim()).toBe('7 – 12 of 99'); + + // Having one option and the same page size should remove the select menu + expect(fixture.nativeElement.querySelector('.mat-mdc-select')).not.toBeNull(); + paginator.pageSize = 10; + paginator.pageSizeOptions = [10]; + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('.mat-mdc-select')).toBeNull(); + }); + + it('should default the page size options to the page size if no options provided', () => { + const fixture = createComponent(MatPaginatorWithoutOptionsApp); + fixture.detectChanges(); + + expect(fixture.componentInstance.paginator._displayedPageSizeOptions).toEqual([10]); + }); + + it('should default the page size to the first page size option if not provided', () => { + const fixture = createComponent(MatPaginatorWithoutPageSizeApp); + fixture.detectChanges(); + + expect(fixture.componentInstance.paginator.pageSize).toEqual(10); + }); + + it('should show a sorted list of page size options including the current page size', () => { + const fixture = createComponent(MatPaginatorApp); + const component = fixture.componentInstance; + const paginator = component.paginator; + expect(paginator._displayedPageSizeOptions).toEqual([5, 10, 25, 100]); + + component.pageSize = 30; + fixture.detectChanges(); + expect(paginator.pageSizeOptions).toEqual([5, 10, 25, 100]); + expect(paginator._displayedPageSizeOptions).toEqual([5, 10, 25, 30, 100]); + + component.pageSizeOptions = [100, 25, 10, 5]; + fixture.detectChanges(); + expect(paginator._displayedPageSizeOptions).toEqual([5, 10, 25, 30, 100]); + }); + + it('should be able to change the page size while keeping the first item present', () => { + const fixture = createComponent(MatPaginatorApp); + const component = fixture.componentInstance; + const paginator = component.paginator; + + // Start on the third page of a list of 100 with a page size of 10. + component.pageIndex = 4; + component.pageSize = 10; + component.length = 100; + fixture.detectChanges(); + + // The first item of the page should be item with index 40 + expect(paginator.pageIndex * paginator.pageSize).toBe(40); + + // The first item on the page is now 25. Change the page size to 25 so that we should now be + // on the second page where the top item is index 25. + component.pageEvent.calls.reset(); + paginator._changePageSize(25); + + expect(component.pageEvent).toHaveBeenCalledWith(jasmine.objectContaining({ + pageIndex: 1, + pageSize: 25 + })); + + // The first item on the page is still 25. Change the page size to 8 so that we should now be + // on the fourth page where the top item is index 24. + component.pageEvent.calls.reset(); + paginator._changePageSize(8); + + expect(component.pageEvent).toHaveBeenCalledWith(jasmine.objectContaining({ + pageIndex: 3, + pageSize: 8 + })); + + // The first item on the page is 24. Change the page size to 16 so that we should now be + // on the first page where the top item is index 0. + component.pageEvent.calls.reset(); + paginator._changePageSize(25); + + expect(component.pageEvent).toHaveBeenCalledWith(jasmine.objectContaining({ + pageIndex: 0, + pageSize: 25 + })); + }); + + it('should keep track of the right number of pages', () => { + const fixture = createComponent(MatPaginatorApp); + const component = fixture.componentInstance; + const paginator = component.paginator; + + component.pageSize = 10; + component.length = 100; + fixture.detectChanges(); + expect(paginator.getNumberOfPages()).toBe(10); + + component.pageSize = 10; + component.length = 0; + fixture.detectChanges(); + expect(paginator.getNumberOfPages()).toBe(0); + + component.pageSize = 10; + component.length = 10; + fixture.detectChanges(); + expect(paginator.getNumberOfPages()).toBe(1); + }); + + it('should show a select only if there are multiple options', () => { + const fixture = createComponent(MatPaginatorApp); + const component = fixture.componentInstance; + const paginator = component.paginator; + + expect(paginator._displayedPageSizeOptions).toEqual([5, 10, 25, 100]); + expect(fixture.nativeElement.querySelector('.mat-mdc-select')).not.toBeNull(); + + // Remove options so that the paginator only uses the current page size (10) as an option. + // Should no longer show the select component since there is only one option. + component.pageSizeOptions = []; + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('.mat-mdc-select')).toBeNull(); + }); + + it('should handle the number inputs being passed in as strings', () => { + const fixture = createComponent(MatPaginatorWithStringValues); + fixture.detectChanges(); + + const withStringPaginator = fixture.componentInstance.paginator; + expect(withStringPaginator.pageIndex).toEqual(0); + expect(withStringPaginator.length).toEqual(100); + expect(withStringPaginator.pageSize).toEqual(10); + expect(withStringPaginator.pageSizeOptions).toEqual([5, 10, 25, 100]); + }); + + it('should be able to hide the page size select', () => { + const fixture = createComponent(MatPaginatorApp); + const element = fixture.nativeElement; + + expect(element.querySelector('.mat-mdc-paginator-page-size')) + .toBeTruthy('Expected select to be rendered.'); + + fixture.componentInstance.hidePageSize = true; + fixture.detectChanges(); + + expect(element.querySelector('.mat-mdc-paginator-page-size')) + .toBeNull('Expected select to be removed.'); + }); + + it('should be able to disable all the controls in the paginator via the binding', () => { + const fixture = createComponent(MatPaginatorApp); + const select: MatSelect = + fixture.debugElement.query(By.directive(MatSelect))!.componentInstance; + + fixture.componentInstance.pageIndex = 1; + fixture.componentInstance.showFirstLastButtons = true; + fixture.detectChanges(); + + expect(select.disabled).toBe(false); + expect(getPreviousButton(fixture).hasAttribute('disabled')).toBe(false); + expect(getNextButton(fixture).hasAttribute('disabled')).toBe(false); + expect(getFirstButton(fixture).hasAttribute('disabled')).toBe(false); + expect(getLastButton(fixture).hasAttribute('disabled')).toBe(false); + + fixture.componentInstance.disabled = true; + fixture.detectChanges(); + + expect(select.disabled).toBe(true); + expect(getPreviousButton(fixture).hasAttribute('disabled')).toBe(true); + expect(getNextButton(fixture).hasAttribute('disabled')).toBe(true); + expect(getFirstButton(fixture).hasAttribute('disabled')).toBe(true); + expect(getLastButton(fixture).hasAttribute('disabled')).toBe(true); + }); + + + it('should be able to configure the default options via a provider', () => { + const fixture = createComponent(MatPaginatorWithoutInputsApp, [{ + provide: MAT_PAGINATOR_DEFAULT_OPTIONS, + useValue: { + pageSize: 7, + pageSizeOptions: [7, 14, 21], + hidePageSize: true, + showFirstLastButtons: true + } as MatPaginatorDefaultOptions + }]); + const paginator = fixture.componentInstance.paginator; + + expect(paginator.pageSize).toBe(7); + expect(paginator.pageSizeOptions).toEqual([7, 14, 21]); + expect(paginator.hidePageSize).toBe(true); + expect(paginator.showFirstLastButtons).toBe(true); + }); + +}); + +function getPreviousButton(fixture: ComponentFixture) { + return fixture.nativeElement.querySelector('.mat-mdc-paginator-navigation-previous'); +} + +function getNextButton(fixture: ComponentFixture) { + return fixture.nativeElement.querySelector('.mat-mdc-paginator-navigation-next'); +} + +function getFirstButton(fixture: ComponentFixture) { + return fixture.nativeElement.querySelector('.mat-mdc-paginator-navigation-first'); +} + +function getLastButton(fixture: ComponentFixture) { + return fixture.nativeElement.querySelector('.mat-mdc-paginator-navigation-last'); +} + +@Component({ + template: ` + + + `, +}) +class MatPaginatorApp { + pageIndex = 0; + pageSize = 10; + pageSizeOptions = [5, 10, 25, 100]; + hidePageSize = false; + showFirstLastButtons = false; + length = 100; + disabled: boolean; + pageEvent = jasmine.createSpy('page event'); + color: ThemePalette; + + @ViewChild(MatPaginator) paginator: MatPaginator; + + goToLastPage() { + this.pageIndex = Math.ceil(this.length / this.pageSize) - 1; + } +} + +@Component({ + template: ` + + `, +}) +class MatPaginatorWithoutInputsApp { + @ViewChild(MatPaginator) paginator: MatPaginator; +} + +@Component({ + template: ` + + `, +}) +class MatPaginatorWithoutPageSizeApp { + @ViewChild(MatPaginator) paginator: MatPaginator; +} + +@Component({ + template: ` + + `, +}) +class MatPaginatorWithoutOptionsApp { + @ViewChild(MatPaginator) paginator: MatPaginator; +} + +@Component({ + template: ` + + + ` + }) +class MatPaginatorWithStringValues { + @ViewChild(MatPaginator) paginator: MatPaginator; +} diff --git a/src/material-experimental/mdc-paginator/paginator.ts b/src/material-experimental/mdc-paginator/paginator.ts new file mode 100644 index 000000000000..355415c16ef8 --- /dev/null +++ b/src/material-experimental/mdc-paginator/paginator.ts @@ -0,0 +1,74 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Inject, + InjectionToken, + Optional, + ViewEncapsulation, +} from '@angular/core'; +import {MatPaginatorIntl, _MatPaginatorBase} from '@angular/material/paginator'; +import {MatFormFieldAppearance} from '@angular/material-experimental/mdc-form-field'; + +// Note that while `MatPaginatorDefaultOptions` and `MAT_PAGINATOR_DEFAULT_OPTIONS` are identical +// between the MDC and non-MDC versions, we have to duplicate them, because the type of +// `formFieldAppearance` is narrower in the MDC version. + +/** Object that can be used to configure the default options for the paginator module. */ +export interface MatPaginatorDefaultOptions { + /** Number of items to display on a page. By default set to 50. */ + pageSize?: number; + + /** The set of provided page size options to display to the user. */ + pageSizeOptions?: number[]; + + /** Whether to hide the page size selection UI from the user. */ + hidePageSize?: boolean; + + /** Whether to show the first/last buttons UI to the user. */ + showFirstLastButtons?: boolean; + + /** The default form-field appearance to apply to the page size options selector. */ + formFieldAppearance?: MatFormFieldAppearance; +} + +/** Injection token that can be used to provide the default options for the paginator module. */ +export const MAT_PAGINATOR_DEFAULT_OPTIONS = + new InjectionToken('MAT_PAGINATOR_DEFAULT_OPTIONS'); + +/** + * Component to provide navigation between paged information. Displays the size of the current + * page, user-selectable options to change that size, what items are being shown, and + * navigational button to go to the previous or next page. + */ +@Component({ + selector: 'mat-paginator', + exportAs: 'matPaginator', + templateUrl: 'paginator.html', + styleUrls: ['paginator.css'], + inputs: ['disabled'], + host: { + 'class': 'mat-mdc-paginator', + }, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, +}) +export class MatPaginator extends _MatPaginatorBase { + /** If set, styles the "page size" form field with the designated style. */ + _formFieldAppearance?: MatFormFieldAppearance; + + constructor(intl: MatPaginatorIntl, + changeDetectorRef: ChangeDetectorRef, + @Optional() @Inject(MAT_PAGINATOR_DEFAULT_OPTIONS) defaults?: MatPaginatorDefaultOptions) { + super(intl, changeDetectorRef, defaults); + this._formFieldAppearance = defaults?.formFieldAppearance || 'outline'; + } +} diff --git a/src/material-experimental/mdc-paginator/public-api.ts b/src/material-experimental/mdc-paginator/public-api.ts new file mode 100644 index 000000000000..04aaaeaa5f7e --- /dev/null +++ b/src/material-experimental/mdc-paginator/public-api.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './paginator-module'; +export * from './paginator'; +export { + MatPaginatorIntl, + MAT_PAGINATOR_INTL_PROVIDER_FACTORY, + MAT_PAGINATOR_INTL_PROVIDER, + PageEvent, +} from '@angular/material/paginator'; + diff --git a/src/material-experimental/mdc-theming/BUILD.bazel b/src/material-experimental/mdc-theming/BUILD.bazel index 56c1f54ba7c2..8d16bd59be25 100644 --- a/src/material-experimental/mdc-theming/BUILD.bazel +++ b/src/material-experimental/mdc-theming/BUILD.bazel @@ -27,6 +27,7 @@ sass_library( "//src/material-experimental/mdc-input:mdc_input_scss_lib", "//src/material-experimental/mdc-list:mdc_list_scss_lib", "//src/material-experimental/mdc-menu:mdc_menu_scss_lib", + "//src/material-experimental/mdc-paginator:mdc_paginator_scss_lib", "//src/material-experimental/mdc-progress-bar:mdc_progress_bar_scss_lib", "//src/material-experimental/mdc-progress-spinner:mdc_progress_spinner_scss_lib", "//src/material-experimental/mdc-radio:mdc_radio_scss_lib", diff --git a/src/material-experimental/mdc-theming/_all-theme.scss b/src/material-experimental/mdc-theming/_all-theme.scss index 20979d7edb5f..067d59c2c185 100644 --- a/src/material-experimental/mdc-theming/_all-theme.scss +++ b/src/material-experimental/mdc-theming/_all-theme.scss @@ -13,6 +13,7 @@ @import '../mdc-snack-bar/snack-bar-theme'; @import '../mdc-tabs/tabs-theme'; @import '../mdc-table/table-theme'; +@import '../mdc-paginator/paginator-theme'; @import '../mdc-progress-bar/progress-bar-theme'; @import '../mdc-progress-spinner/progress-spinner-theme'; @import '../mdc-input/input-theme'; @@ -33,6 +34,7 @@ @include mat-mdc-chips-theme($theme-or-color-config); @include mat-mdc-list-theme($theme-or-color-config); @include mat-mdc-menu-theme($theme-or-color-config); + @include mat-mdc-paginator-theme($theme-or-color-config); @include mat-mdc-progress-bar-theme($theme-or-color-config); @include mat-mdc-progress-spinner-theme($theme-or-color-config); @include mat-mdc-radio-theme($theme-or-color-config); diff --git a/src/material/paginator/paginator.spec.ts b/src/material/paginator/paginator.spec.ts index 1773ef92b23e..dcef79b6c055 100644 --- a/src/material/paginator/paginator.spec.ts +++ b/src/material/paginator/paginator.spec.ts @@ -177,13 +177,16 @@ describe('MatPaginator', () => { const component = fixture.componentInstance; const formField: HTMLElement = fixture.nativeElement.querySelector('.mat-form-field'); - expect(formField.classList).toContain('mat-primary'); - component.color = 'accent'; fixture.detectChanges(); - expect(formField.classList).not.toContain('mat-primary'); + expect(formField.classList).not.toContain('mat-warn'); expect(formField.classList).toContain('mat-accent'); + + component.color = 'warn'; + fixture.detectChanges(); + expect(formField.classList).toContain('mat-warn'); + expect(formField.classList).not.toContain('mat-accent'); }); describe('when showing the first and last button', () => { @@ -475,11 +478,11 @@ function getNextButton(fixture: ComponentFixture) { } function getFirstButton(fixture: ComponentFixture) { - return fixture.nativeElement.querySelector('.mat-paginator-navigation-first'); + return fixture.nativeElement.querySelector('.mat-paginator-navigation-first'); } function getLastButton(fixture: ComponentFixture) { - return fixture.nativeElement.querySelector('.mat-paginator-navigation-last'); + return fixture.nativeElement.querySelector('.mat-paginator-navigation-last'); } @Component({ diff --git a/src/material/paginator/paginator.ts b/src/material/paginator/paginator.ts index 40332719aa63..1dbab2c2a19f 100644 --- a/src/material/paginator/paginator.ts +++ b/src/material/paginator/paginator.ts @@ -25,6 +25,7 @@ import { InjectionToken, Inject, Optional, + Directive, } from '@angular/core'; import {Subscription} from 'rxjs'; import {MatPaginatorIntl} from './paginator-intl'; @@ -86,31 +87,24 @@ export interface MatPaginatorDefaultOptions { export const MAT_PAGINATOR_DEFAULT_OPTIONS = new InjectionToken('MAT_PAGINATOR_DEFAULT_OPTIONS'); -// Boilerplate for applying mixins to MatPaginator. +// Boilerplate for applying mixins to _MatPaginatorBase. /** @docs-private */ -class MatPaginatorBase {} -const _MatPaginatorBase: CanDisableCtor & HasInitializedCtor & typeof MatPaginatorBase = - mixinDisabled(mixinInitialized(MatPaginatorBase)); +class MatPaginatorMixinBase {} +const _MatPaginatorMixinBase: CanDisableCtor & HasInitializedCtor & typeof MatPaginatorMixinBase = + mixinDisabled(mixinInitialized(MatPaginatorMixinBase)); /** - * Component to provide navigation between paged information. Displays the size of the current - * page, user-selectable options to change that size, what items are being shown, and - * navigational button to go to the previous or next page. + * Base class with all of the `MatPaginator` functionality. + * @docs-private */ -@Component({ - selector: 'mat-paginator', - exportAs: 'matPaginator', - templateUrl: 'paginator.html', - styleUrls: ['paginator.css'], - inputs: ['disabled'], - host: { - 'class': 'mat-paginator', - }, - changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None, -}) -export class MatPaginator extends _MatPaginatorBase implements OnInit, OnDestroy, CanDisable, - HasInitialized { +@Directive() +export abstract class _MatPaginatorBase extends _MatPaginatorMixinBase implements OnInit, OnDestroy, + CanDisable, HasInitialized { private _initialized: boolean; private _intlChanges: Subscription; @@ -176,13 +170,9 @@ export class MatPaginator extends _MatPaginatorBase implements OnInit, OnDestroy /** Displayed set of page size options. Will be sorted and include current page size. */ _displayedPageSizeOptions: number[]; - /** If set, styles the "page size" form field with the designated style. */ - _formFieldAppearance?: MatFormFieldAppearance; - constructor(public _intl: MatPaginatorIntl, private _changeDetectorRef: ChangeDetectorRef, - @Optional() @Inject(MAT_PAGINATOR_DEFAULT_OPTIONS) - defaults?: MatPaginatorDefaultOptions) { + defaults?: O) { super(); this._intlChanges = _intl.changes.subscribe(() => this._changeDetectorRef.markForCheck()); @@ -192,7 +182,6 @@ export class MatPaginator extends _MatPaginatorBase implements OnInit, OnDestroy pageSizeOptions, hidePageSize, showFirstLastButtons, - formFieldAppearance, } = defaults; if (pageSize != null) { @@ -210,10 +199,6 @@ export class MatPaginator extends _MatPaginatorBase implements OnInit, OnDestroy if (showFirstLastButtons != null) { this._showFirstLastButtons = showFirstLastButtons; } - - if (formFieldAppearance != null) { - this._formFieldAppearance = formFieldAppearance; - } } } @@ -357,3 +342,36 @@ export class MatPaginator extends _MatPaginatorBase implements OnInit, OnDestroy static ngAcceptInputType_showFirstLastButtons: BooleanInput; static ngAcceptInputType_disabled: BooleanInput; } + + +/** + * Component to provide navigation between paged information. Displays the size of the current + * page, user-selectable options to change that size, what items are being shown, and + * navigational button to go to the previous or next page. + */ +@Component({ + selector: 'mat-paginator', + exportAs: 'matPaginator', + templateUrl: 'paginator.html', + styleUrls: ['paginator.css'], + inputs: ['disabled'], + host: { + 'class': 'mat-paginator', + }, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, +}) +export class MatPaginator extends _MatPaginatorBase { + /** If set, styles the "page size" form field with the designated style. */ + _formFieldAppearance?: MatFormFieldAppearance; + + constructor(intl: MatPaginatorIntl, + changeDetectorRef: ChangeDetectorRef, + @Optional() @Inject(MAT_PAGINATOR_DEFAULT_OPTIONS) defaults?: MatPaginatorDefaultOptions) { + super(intl, changeDetectorRef, defaults); + + if (defaults && defaults.formFieldAppearance != null) { + this._formFieldAppearance = defaults.formFieldAppearance; + } + } +} diff --git a/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html b/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html index c8a8dfaff664..0e698b8d6cab 100644 --- a/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html +++ b/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html @@ -132,6 +132,12 @@

MDC Tabs

Also Google +

MDC Paginator

+ + + +

MDC Table

diff --git a/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.ts b/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.ts index b19dd6a18b7c..43cde72bb7f0 100644 --- a/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.ts +++ b/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.ts @@ -19,6 +19,7 @@ import {MatIconModule} from '@angular/material/icon'; import {MatSnackBarModule, MatSnackBar} from '@angular/material-experimental/mdc-snack-bar'; import {MatProgressSpinnerModule} from '@angular/material-experimental/mdc-progress-spinner'; import {MatSelectModule} from '@angular/material-experimental/mdc-select'; +import {MatPaginatorModule} from '@angular/material-experimental/mdc-paginator'; @Component({ template: `` @@ -57,6 +58,7 @@ export class KitchenSinkMdc { MatSnackBarModule, MatProgressSpinnerModule, MatSelectModule, + MatPaginatorModule, ], declarations: [KitchenSinkMdc, TestEntryComponent], exports: [KitchenSinkMdc, TestEntryComponent], diff --git a/tools/public_api_guard/material/paginator.d.ts b/tools/public_api_guard/material/paginator.d.ts index 9f2eda95ee5f..aa321f76bc88 100644 --- a/tools/public_api_guard/material/paginator.d.ts +++ b/tools/public_api_guard/material/paginator.d.ts @@ -1,16 +1,10 @@ -export declare const MAT_PAGINATOR_DEFAULT_OPTIONS: InjectionToken; - -export declare const MAT_PAGINATOR_INTL_PROVIDER: { - provide: typeof MatPaginatorIntl; - deps: Optional[][]; - useFactory: typeof MAT_PAGINATOR_INTL_PROVIDER_FACTORY; -}; - -export declare function MAT_PAGINATOR_INTL_PROVIDER_FACTORY(parentIntl: MatPaginatorIntl): MatPaginatorIntl; - -export declare class MatPaginator extends _MatPaginatorBase implements OnInit, OnDestroy, CanDisable, HasInitialized { +export declare abstract class _MatPaginatorBase extends _MatPaginatorMixinBase implements OnInit, OnDestroy, CanDisable, HasInitialized { _displayedPageSizeOptions: number[]; - _formFieldAppearance?: MatFormFieldAppearance; _intl: MatPaginatorIntl; color: ThemePalette; get hidePageSize(): boolean; @@ -26,7 +20,7 @@ export declare class MatPaginator extends _MatPaginatorBase implements OnInit, O set pageSizeOptions(value: number[]); get showFirstLastButtons(): boolean; set showFirstLastButtons(value: boolean); - constructor(_intl: MatPaginatorIntl, _changeDetectorRef: ChangeDetectorRef, defaults?: MatPaginatorDefaultOptions); + constructor(_intl: MatPaginatorIntl, _changeDetectorRef: ChangeDetectorRef, defaults?: O); _changePageSize(pageSize: number): void; _nextButtonsDisabled(): boolean; _previousButtonsDisabled(): boolean; @@ -45,7 +39,24 @@ export declare class MatPaginator extends _MatPaginatorBase implements OnInit, O static ngAcceptInputType_pageIndex: NumberInput; static ngAcceptInputType_pageSize: NumberInput; static ngAcceptInputType_showFirstLastButtons: BooleanInput; - static ɵcmp: i0.ɵɵComponentDefWithMeta; + static ɵdir: i0.ɵɵDirectiveDefWithMeta<_MatPaginatorBase, never, never, { "color": "color"; "pageIndex": "pageIndex"; "length": "length"; "pageSize": "pageSize"; "pageSizeOptions": "pageSizeOptions"; "hidePageSize": "hidePageSize"; "showFirstLastButtons": "showFirstLastButtons"; }, { "page": "page"; }, never>; + static ɵfac: i0.ɵɵFactoryDef<_MatPaginatorBase, never>; +} + +export declare const MAT_PAGINATOR_DEFAULT_OPTIONS: InjectionToken; + +export declare const MAT_PAGINATOR_INTL_PROVIDER: { + provide: typeof MatPaginatorIntl; + deps: Optional[][]; + useFactory: typeof MAT_PAGINATOR_INTL_PROVIDER_FACTORY; +}; + +export declare function MAT_PAGINATOR_INTL_PROVIDER_FACTORY(parentIntl: MatPaginatorIntl): MatPaginatorIntl; + +export declare class MatPaginator extends _MatPaginatorBase { + _formFieldAppearance?: MatFormFieldAppearance; + constructor(intl: MatPaginatorIntl, changeDetectorRef: ChangeDetectorRef, defaults?: MatPaginatorDefaultOptions); + static ɵcmp: i0.ɵɵComponentDefWithMeta; static ɵfac: i0.ɵɵFactoryDef; }