diff --git a/.gitignore b/.gitignore index ce0991dfb3e0..2a71b14567d2 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,8 @@ node_modules /.vs *.swo *.swp +.vimrc +.nvimrc # misc .DS_Store diff --git a/LICENSE b/LICENSE index 40cf2459805b..c24af42e203d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2021 Google LLC. +Copyright (c) 2022 Google LLC. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/goldens/BUILD.bazel b/goldens/BUILD.bazel index e8a75f0d5692..16bac4c0e639 100644 --- a/goldens/BUILD.bazel +++ b/goldens/BUILD.bazel @@ -1,3 +1,4 @@ exports_files([ "size-test.yaml", + "tsec-exemption.json", ]) diff --git a/goldens/tsec-exemption.json b/goldens/tsec-exemption.json new file mode 100644 index 000000000000..17bc2283c356 --- /dev/null +++ b/goldens/tsec-exemption.json @@ -0,0 +1,17 @@ +{ + "ban-trustedtypes-createpolicy": [ + "../src/material/icon/trusted-types.ts" + ], + "ban-element-innerhtml-assignments": [ + "../src/material/icon/icon-registry.ts" + ], + "ban-element-setattribute": [ + "../src/cdk/a11y/aria-describer/aria-reference.ts", + "../src/material-experimental/mdc-checkbox/checkbox.ts", + "../src/material-experimental/mdc-list/interactive-list-base.ts", + "../src/material-experimental/mdc-progress-spinner/progress-spinner.ts", + "../src/material-experimental/mdc-slide-toggle/slide-toggle.ts", + "../src/material/icon/icon-registry.ts", + "../src/material/icon/icon.ts" + ] +} diff --git a/package.json b/package.json index dab9f845c8b3..cb0d40011277 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "test": "node ./scripts/run-component-tests.js", "test-local": "yarn -s test --local", "test-firefox": "yarn -s test --firefox", + "test-tsec": "yarn bazelisk test //... --build_tag_filters=tsec --test_tag_filters=tsec", "lint": "yarn -s tslint && yarn -s stylelint && yarn -s ownerslint && yarn -s ng-dev format changed --check", "e2e": "bazel test //src/... --build_tag_filters=e2e --test_tag_filters=e2e --build_tests_only", "deploy-dev-app": "node ./scripts/deploy-dev-app.js", @@ -60,7 +61,7 @@ "@types/google.maps": "^3.45.6", "@types/youtube": "^0.0.42", "core-js-bundle": "^3.8.2", - "material-components-web": "14.0.0-canary.7d8ea4624.0", + "material-components-web": "14.0.0-canary.c047f7c19.0", "rxjs": "^6.6.7", "rxjs-tslint-rules": "^4.33.1", "tslib": "^2.3.0", @@ -92,53 +93,53 @@ "@bazel/terser": "4.4.5", "@bazel/typescript": "4.4.5", "@firebase/app-types": "^0.6.1", - "@material/animation": "14.0.0-canary.7d8ea4624.0", - "@material/auto-init": "14.0.0-canary.7d8ea4624.0", - "@material/banner": "14.0.0-canary.7d8ea4624.0", - "@material/base": "14.0.0-canary.7d8ea4624.0", - "@material/button": "14.0.0-canary.7d8ea4624.0", - "@material/card": "14.0.0-canary.7d8ea4624.0", - "@material/checkbox": "14.0.0-canary.7d8ea4624.0", - "@material/chips": "14.0.0-canary.7d8ea4624.0", - "@material/circular-progress": "14.0.0-canary.7d8ea4624.0", - "@material/data-table": "14.0.0-canary.7d8ea4624.0", - "@material/density": "14.0.0-canary.7d8ea4624.0", - "@material/dialog": "14.0.0-canary.7d8ea4624.0", - "@material/dom": "14.0.0-canary.7d8ea4624.0", - "@material/drawer": "14.0.0-canary.7d8ea4624.0", - "@material/elevation": "14.0.0-canary.7d8ea4624.0", - "@material/fab": "14.0.0-canary.7d8ea4624.0", - "@material/feature-targeting": "14.0.0-canary.7d8ea4624.0", - "@material/floating-label": "14.0.0-canary.7d8ea4624.0", - "@material/form-field": "14.0.0-canary.7d8ea4624.0", - "@material/icon-button": "14.0.0-canary.7d8ea4624.0", - "@material/image-list": "14.0.0-canary.7d8ea4624.0", - "@material/layout-grid": "14.0.0-canary.7d8ea4624.0", - "@material/line-ripple": "14.0.0-canary.7d8ea4624.0", - "@material/linear-progress": "14.0.0-canary.7d8ea4624.0", - "@material/list": "14.0.0-canary.7d8ea4624.0", - "@material/menu": "14.0.0-canary.7d8ea4624.0", - "@material/menu-surface": "14.0.0-canary.7d8ea4624.0", - "@material/notched-outline": "14.0.0-canary.7d8ea4624.0", - "@material/radio": "14.0.0-canary.7d8ea4624.0", - "@material/ripple": "14.0.0-canary.7d8ea4624.0", - "@material/rtl": "14.0.0-canary.7d8ea4624.0", - "@material/segmented-button": "14.0.0-canary.7d8ea4624.0", - "@material/select": "14.0.0-canary.7d8ea4624.0", - "@material/shape": "14.0.0-canary.7d8ea4624.0", - "@material/slider": "14.0.0-canary.7d8ea4624.0", - "@material/snackbar": "14.0.0-canary.7d8ea4624.0", - "@material/switch": "14.0.0-canary.7d8ea4624.0", - "@material/tab": "14.0.0-canary.7d8ea4624.0", - "@material/tab-bar": "14.0.0-canary.7d8ea4624.0", - "@material/tab-indicator": "14.0.0-canary.7d8ea4624.0", - "@material/tab-scroller": "14.0.0-canary.7d8ea4624.0", - "@material/textfield": "14.0.0-canary.7d8ea4624.0", - "@material/theme": "14.0.0-canary.7d8ea4624.0", - "@material/tooltip": "14.0.0-canary.7d8ea4624.0", - "@material/top-app-bar": "14.0.0-canary.7d8ea4624.0", - "@material/touch-target": "14.0.0-canary.7d8ea4624.0", - "@material/typography": "14.0.0-canary.7d8ea4624.0", + "@material/animation": "14.0.0-canary.c047f7c19.0", + "@material/auto-init": "14.0.0-canary.c047f7c19.0", + "@material/banner": "14.0.0-canary.c047f7c19.0", + "@material/base": "14.0.0-canary.c047f7c19.0", + "@material/button": "14.0.0-canary.c047f7c19.0", + "@material/card": "14.0.0-canary.c047f7c19.0", + "@material/checkbox": "14.0.0-canary.c047f7c19.0", + "@material/chips": "14.0.0-canary.c047f7c19.0", + "@material/circular-progress": "14.0.0-canary.c047f7c19.0", + "@material/data-table": "14.0.0-canary.c047f7c19.0", + "@material/density": "14.0.0-canary.c047f7c19.0", + "@material/dialog": "14.0.0-canary.c047f7c19.0", + "@material/dom": "14.0.0-canary.c047f7c19.0", + "@material/drawer": "14.0.0-canary.c047f7c19.0", + "@material/elevation": "14.0.0-canary.c047f7c19.0", + "@material/fab": "14.0.0-canary.c047f7c19.0", + "@material/feature-targeting": "14.0.0-canary.c047f7c19.0", + "@material/floating-label": "14.0.0-canary.c047f7c19.0", + "@material/form-field": "14.0.0-canary.c047f7c19.0", + "@material/icon-button": "14.0.0-canary.c047f7c19.0", + "@material/image-list": "14.0.0-canary.c047f7c19.0", + "@material/layout-grid": "14.0.0-canary.c047f7c19.0", + "@material/line-ripple": "14.0.0-canary.c047f7c19.0", + "@material/linear-progress": "14.0.0-canary.c047f7c19.0", + "@material/list": "14.0.0-canary.c047f7c19.0", + "@material/menu": "14.0.0-canary.c047f7c19.0", + "@material/menu-surface": "14.0.0-canary.c047f7c19.0", + "@material/notched-outline": "14.0.0-canary.c047f7c19.0", + "@material/radio": "14.0.0-canary.c047f7c19.0", + "@material/ripple": "14.0.0-canary.c047f7c19.0", + "@material/rtl": "14.0.0-canary.c047f7c19.0", + "@material/segmented-button": "14.0.0-canary.c047f7c19.0", + "@material/select": "14.0.0-canary.c047f7c19.0", + "@material/shape": "14.0.0-canary.c047f7c19.0", + "@material/slider": "14.0.0-canary.c047f7c19.0", + "@material/snackbar": "14.0.0-canary.c047f7c19.0", + "@material/switch": "14.0.0-canary.c047f7c19.0", + "@material/tab": "14.0.0-canary.c047f7c19.0", + "@material/tab-bar": "14.0.0-canary.c047f7c19.0", + "@material/tab-indicator": "14.0.0-canary.c047f7c19.0", + "@material/tab-scroller": "14.0.0-canary.c047f7c19.0", + "@material/textfield": "14.0.0-canary.c047f7c19.0", + "@material/theme": "14.0.0-canary.c047f7c19.0", + "@material/tooltip": "14.0.0-canary.c047f7c19.0", + "@material/top-app-bar": "14.0.0-canary.c047f7c19.0", + "@material/touch-target": "14.0.0-canary.c047f7c19.0", + "@material/typography": "14.0.0-canary.c047f7c19.0", "@octokit/rest": "18.3.5", "@rollup/plugin-commonjs": "^21.0.0", "@rollup/plugin-node-resolve": "^13.0.5", @@ -211,6 +212,7 @@ "stylelint": "^14.0.1", "terser": "^5.9.0", "ts-node": "^10.2.1", + "tsec": "0.2.1", "tsickle": "0.39.1", "tslint": "^6.1.3", "tsutils": "^3.21.0", diff --git a/src/BUILD.bazel b/src/BUILD.bazel index d84776afa83c..5bd9ad3bf60e 100644 --- a/src/BUILD.bazel +++ b/src/BUILD.bazel @@ -49,3 +49,12 @@ ts_library( name = "dev_mode_types", srcs = ["dev-mode-types.d.ts"], ) + +ts_config( + name = "tsec_config", + src = "tsconfig-tsec.json", + deps = [ + ":bazel-tsconfig-build.json", + "//goldens:tsec-exemption.json", + ], +) diff --git a/src/cdk/a11y/focus-monitor/focus-monitor.ts b/src/cdk/a11y/focus-monitor/focus-monitor.ts index 4b827bf0e731..fe7b50acaf54 100644 --- a/src/cdk/a11y/focus-monitor/focus-monitor.ts +++ b/src/cdk/a11y/focus-monitor/focus-monitor.ts @@ -134,7 +134,7 @@ export class FocusMonitor implements OnDestroy { // Make a note of when the window regains focus, so we can // restore the origin info for the focused element. this._windowFocused = true; - this._windowFocusTimeoutId = setTimeout(() => (this._windowFocused = false)); + this._windowFocusTimeoutId = window.setTimeout(() => (this._windowFocused = false)); }; /** Used to reference correct document/window */ diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts index c13f6502583d..d9014316a338 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts @@ -164,6 +164,46 @@ describe('FlexibleConnectedPositionStrategy', () => { originElement.remove(); }); + it('should calculate position with simulated zoom in Safari', () => { + let containerElement = overlayContainer.getContainerElement(); + spyOn(containerElement, 'getBoundingClientRect').and.returnValue({ + top: -200, + bottom: 900, + left: -200, + right: 100, + width: 100, + height: 100, + } as DOMRect); + + const originElement = createPositionedBlockElement(); + document.body.appendChild(originElement); + + // Position the element so it would have enough space to fit. + originElement.style.top = '200px'; + originElement.style.left = '70px'; + + attachOverlay({ + positionStrategy: overlay + .position() + .flexibleConnectedTo(originElement) + .withFlexibleDimensions(false) + .withPush(false) + .withPositions([ + { + originX: 'start', + originY: 'top', + overlayX: 'start', + overlayY: 'top', + }, + ]), + }); + + expect(getComputedStyle(overlayRef.overlayElement).left).toBe('270px'); + expect(getComputedStyle(overlayRef.overlayElement).top).toBe('400px'); + + originElement.remove(); + }); + it('should clean up after itself when disposed', () => { const origin = document.createElement('div'); const positionStrategy = overlay diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.ts index 636efcc08756..df943e0d39cf 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.ts @@ -85,6 +85,9 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { /** Cached viewport dimensions */ private _viewportRect: Dimensions; + /** Cached container dimensions */ + private _containerRect: Dimensions; + /** Amount of space that must be maintained between the overlay and the edge of the viewport. */ private _viewportMargin = 0; @@ -213,16 +216,18 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { this._resetOverlayElementStyles(); this._resetBoundingBoxStyles(); - // We need the bounding rects for the origin and the overlay to determine how to position + // We need the bounding rects for the origin, the overlay and the container to determine how to position // the overlay relative to the origin. // We use the viewport rect to determine whether a position would go off-screen. this._viewportRect = this._getNarrowedViewportRect(); this._originRect = this._getOriginRect(); this._overlayRect = this._pane.getBoundingClientRect(); + this._containerRect = this._overlayContainer.getContainerElement().getBoundingClientRect(); const originRect = this._originRect; const overlayRect = this._overlayRect; const viewportRect = this._viewportRect; + const containerRect = this._containerRect; // Positions where the overlay will fit with flexible dimensions. const flexibleFits: FlexibleFit[] = []; @@ -234,7 +239,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { // If a good fit is found, it will be applied immediately. for (let pos of this._preferredPositions) { // Get the exact (x, y) coordinate for the point-of-origin on the origin element. - let originPoint = this._getOriginPoint(originRect, pos); + let originPoint = this._getOriginPoint(originRect, containerRect, pos); // From that point-of-origin, get the exact (x, y) coordinate for the top-left corner of the // overlay in this position. We use the top-left corner for calculations and later translate @@ -359,9 +364,10 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { this._originRect = this._getOriginRect(); this._overlayRect = this._pane.getBoundingClientRect(); this._viewportRect = this._getNarrowedViewportRect(); + this._containerRect = this._overlayContainer.getContainerElement().getBoundingClientRect(); const lastPosition = this._lastPosition || this._preferredPositions[0]; - const originPoint = this._getOriginPoint(this._originRect, lastPosition); + const originPoint = this._getOriginPoint(this._originRect, this._containerRect, lastPosition); this._applyPosition(lastPosition, originPoint); } @@ -479,7 +485,11 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { /** * Gets the (x, y) coordinate of a connection point on the origin based on a relative position. */ - private _getOriginPoint(originRect: Dimensions, pos: ConnectedPosition): Point { + private _getOriginPoint( + originRect: Dimensions, + containerRect: Dimensions, + pos: ConnectedPosition, + ): Point { let x: number; if (pos.originX == 'center') { // Note: when centering we should always use the `left` @@ -491,6 +501,12 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { x = pos.originX == 'start' ? startX : endX; } + // When zooming in Safari the container rectangle contains negative values for the position + // and we need to re-add them to the calculated coordinates. + if (containerRect.left < 0) { + x -= containerRect.left; + } + let y: number; if (pos.originY == 'center') { y = originRect.top + originRect.height / 2; @@ -498,6 +514,15 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { y = pos.originY == 'top' ? originRect.top : originRect.bottom; } + // Normally the containerRect's top value would be zero, however when the overlay is attached to an input + // (e.g. in an autocomplete), mobile browsers will shift everything in order to put the input in the middle + // of the screen and to make space for the virtual keyboard. We need to account for this offset, + // otherwise our positioning will be thrown off. + // Additionally, when zooming in Safari this fixes the vertical position. + if (containerRect.top < 0) { + y -= containerRect.top; + } + return {x, y}; } @@ -580,7 +605,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { /** * Whether the overlay can fit within the viewport when it may resize either its width or height. * @param fit How well the overlay fits in the viewport at some position. - * @param point The (x, y) coordinates of the overlat at some position. + * @param point The (x, y) coordinates of the overlay at some position. * @param viewport The geometry of the viewport. */ private _canFitWithFlexibleDimensions(fit: OverlayFit, point: Point, viewport: Dimensions) { @@ -606,7 +631,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { * right and bottom). * * @param start Starting point from which the overlay is pushed. - * @param overlay Dimensions of the overlay. + * @param rawOverlayRect Dimensions of the overlay. * @param scrollPosition Current viewport scroll position. * @returns The point at which to position the overlay after pushing. This is effectively a new * originPoint. @@ -958,16 +983,6 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { overlayPoint = this._pushOverlayOnScreen(overlayPoint, this._overlayRect, scrollPosition); } - let virtualKeyboardOffset = this._overlayContainer - .getContainerElement() - .getBoundingClientRect().top; - - // Normally this would be zero, however when the overlay is attached to an input (e.g. in an - // autocomplete), mobile browsers will shift everything in order to put the input in the middle - // of the screen and to make space for the virtual keyboard. We need to account for this offset, - // otherwise our positioning will be thrown off. - overlayPoint.y -= virtualKeyboardOffset; - // We want to set either `top` or `bottom` based on whether the overlay wants to appear // above or below the origin and the direction in which the element will expand. if (position.overlayY === 'bottom') { @@ -1183,7 +1198,7 @@ interface OverlayFit { visibleArea: number; } -/** Record of the measurments determining whether an overlay will fit in a specific position. */ +/** Record of the measurements determining whether an overlay will fit in a specific position. */ interface FallbackPosition { position: ConnectedPosition; originPoint: Point; diff --git a/src/cdk/schematics/ng-update/migrations/tilde-import-v13/tilde-import-migration.ts b/src/cdk/schematics/ng-update/migrations/tilde-import-v13/tilde-import-migration.ts index 671a1a8d08bd..3a6fe74917d4 100644 --- a/src/cdk/schematics/ng-update/migrations/tilde-import-v13/tilde-import-migration.ts +++ b/src/cdk/schematics/ng-update/migrations/tilde-import-v13/tilde-import-migration.ts @@ -21,10 +21,11 @@ export class TildeImportMigration extends DevkitMigration { if (extension === '.scss' || extension === '.css') { const content = stylesheet.content; const migratedContent = content.replace( - /@(?:import|use) +['"]~@angular\/.*['"].*;?/g, - match => { - const index = match.indexOf('~@angular'); - return match.slice(0, index) + match.slice(index + 1); + /@(?:import|use) +['"](~@angular\/.*)['"].*;?/g, + (match, importPath) => { + const index = match.indexOf(importPath); + const newImportPath = importPath.replace(/^~|\.scss$/g, ''); + return match.slice(0, index) + newImportPath + match.slice(index + importPath.length); }, ); diff --git a/src/cdk/schematics/ng-update/test-cases/v13/misc/tilde-import-v13.spec.ts b/src/cdk/schematics/ng-update/test-cases/v13/misc/tilde-import-v13.spec.ts index d831eaf148ff..360dd588a877 100644 --- a/src/cdk/schematics/ng-update/test-cases/v13/misc/tilde-import-v13.spec.ts +++ b/src/cdk/schematics/ng-update/test-cases/v13/misc/tilde-import-v13.spec.ts @@ -124,4 +124,26 @@ describe('v13 tilde import migration', () => { `@include mat-core();`, ]); }); + + it('should remove remove .scss file extension', async () => { + writeLines(TEST_PATH, [ + `@use '~@angular/material.scss' as mat;`, + `@import '~@angular/material/theming.scss';`, + `@import '~@angular/cdk/overlay-prebuilt.css';`, + + `@include mat.button-theme();`, + `@include mat-core();`, + ]); + + await runMigration(); + + expect(splitFile(TEST_PATH)).toEqual([ + `@use '@angular/material' as mat;`, + `@import '@angular/material/theming';`, + `@import '@angular/cdk/overlay-prebuilt.css';`, + + `@include mat.button-theme();`, + `@include mat-core();`, + ]); + }); }); diff --git a/src/cdk/table/cell.ts b/src/cdk/table/cell.ts index 218d9c5ab709..4aabe6e48985 100644 --- a/src/cdk/table/cell.ts +++ b/src/cdk/table/cell.ts @@ -150,12 +150,7 @@ export class CdkColumnDef extends _CdkColumnDefBase implements CanStick { /** Base class for the cells. Adds a CSS classname that identifies the column it renders in. */ export class BaseCdkCell { constructor(columnDef: CdkColumnDef, elementRef: ElementRef) { - // If IE 11 is dropped before we switch to setting a single class name, change to multi param - // with destructuring. - const classList = elementRef.nativeElement.classList; - for (const className of columnDef._columnCssClassName) { - classList.add(className); - } + elementRef.nativeElement.classList.add(...columnDef._columnCssClassName); } } diff --git a/src/cdk/testing/harness-environment.ts b/src/cdk/testing/harness-environment.ts index e744da638ae7..cb2ea81521aa 100644 --- a/src/cdk/testing/harness-environment.ts +++ b/src/cdk/testing/harness-environment.ts @@ -45,11 +45,16 @@ type ParsedQueries = { */ export abstract class HarnessEnvironment implements HarnessLoader, LocatorFactory { // Implemented as part of the `LocatorFactory` interface. - rootElement: TestElement; - - protected constructor(protected rawRootElement: E) { - this.rootElement = this.createTestElement(rawRootElement); + get rootElement(): TestElement { + this._rootElement = this._rootElement || this.createTestElement(this.rawRootElement); + return this._rootElement; + } + set rootElement(element: TestElement) { + this._rootElement = element; } + private _rootElement: TestElement | undefined; + + protected constructor(protected rawRootElement: E) {} // Implemented as part of the `LocatorFactory` interface. documentRootLocatorFactory(): LocatorFactory { diff --git a/src/cdk/testing/selenium-webdriver/selenium-web-driver-harness-environment.ts b/src/cdk/testing/selenium-webdriver/selenium-web-driver-harness-environment.ts index 4c50f14ab00d..28f62408fbec 100644 --- a/src/cdk/testing/selenium-webdriver/selenium-web-driver-harness-environment.ts +++ b/src/cdk/testing/selenium-webdriver/selenium-web-driver-harness-environment.ts @@ -74,12 +74,16 @@ export class SeleniumWebDriverHarnessEnvironment extends HarnessEnvironment< /** The options for this environment. */ private _options: WebDriverHarnessEnvironmentOptions; + /** Environment stabilization callback passed to the created test elements. */ + private _stabilizeCallback: () => Promise; + protected constructor( rawRootElement: () => webdriver.WebElement, options?: WebDriverHarnessEnvironmentOptions, ) { super(rawRootElement); this._options = {...defaultEnvironmentOptions, ...options}; + this._stabilizeCallback = () => this.forceStabilize(); } /** Gets the ElementFinder corresponding to the given TestElement. */ @@ -123,7 +127,7 @@ export class SeleniumWebDriverHarnessEnvironment extends HarnessEnvironment< /** Creates a `TestElement` from a raw element. */ protected createTestElement(element: () => webdriver.WebElement): TestElement { - return new SeleniumWebDriverElement(element, () => this.forceStabilize()); + return new SeleniumWebDriverElement(element, this._stabilizeCallback); } /** Creates a `HarnessLoader` rooted at the given raw element. */ diff --git a/src/cdk/testing/testbed/testbed-harness-environment.ts b/src/cdk/testing/testbed/testbed-harness-environment.ts index 5378a2949a71..60f061853dc4 100644 --- a/src/cdk/testing/testbed/testbed-harness-environment.ts +++ b/src/cdk/testing/testbed/testbed-harness-environment.ts @@ -96,6 +96,9 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment { /** The options for this environment. */ private _options: TestbedHarnessEnvironmentOptions; + /** Environment stabilization callback passed to the created test elements. */ + private _stabilizeCallback: () => Promise; + protected constructor( rawRootElement: Element, private _fixture: ComponentFixture, @@ -104,6 +107,7 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment { super(rawRootElement); this._options = {...defaultEnvironmentOptions, ...options}; this._taskState = TaskStateZoneInterceptor.setup(); + this._stabilizeCallback = () => this.forceStabilize(); installAutoChangeDetectionStatusHandler(_fixture); _fixture.componentRef.onDestroy(() => { uninstallAutoChangeDetectionStatusHandler(_fixture); @@ -198,7 +202,7 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment { /** Creates a `TestElement` from a raw element. */ protected createTestElement(element: Element): TestElement { - return new UnitTestElement(element, () => this.forceStabilize()); + return new UnitTestElement(element, this._stabilizeCallback); } /** Creates a `HarnessLoader` rooted at the given raw element. */ diff --git a/src/cdk/text-field/autosize.spec.ts b/src/cdk/text-field/autosize.spec.ts index 66877cf9be9a..eabb88228f97 100644 --- a/src/cdk/text-field/autosize.spec.ts +++ b/src/cdk/text-field/autosize.spec.ts @@ -373,6 +373,13 @@ describe('CdkTextareaAutosize', () => { .withContext('Expected textarea to have a scrollbar.') .toBeLessThan(textarea.scrollHeight); })); + + it('should handle an undefined placeholder', () => { + fixture.componentInstance.placeholder = undefined!; + fixture.detectChanges(); + + expect(textarea.hasAttribute('placeholder')).toBe(false); + }); }); // Styles to reset padding and border to make measurement comparisons easier. diff --git a/src/cdk/text-field/autosize.ts b/src/cdk/text-field/autosize.ts index 3170259f2d64..f07bf93bbddd 100644 --- a/src/cdk/text-field/autosize.ts +++ b/src/cdk/text-field/autosize.ts @@ -100,7 +100,13 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy { } set placeholder(value: string) { this._cachedPlaceholderHeight = undefined; - this._textareaElement.placeholder = value; + + if (value) { + this._textareaElement.setAttribute('placeholder', value); + } else { + this._textareaElement.removeAttribute('placeholder'); + } + this._cacheTextareaPlaceholderHeight(); } diff --git a/src/components-examples/material/tabs/index.ts b/src/components-examples/material/tabs/index.ts index 6190a8240f2a..cb5279b3487b 100644 --- a/src/components-examples/material/tabs/index.ts +++ b/src/components-examples/material/tabs/index.ts @@ -20,6 +20,7 @@ import {TabGroupLazyLoadedExample} from './tab-group-lazy-loaded/tab-group-lazy- import {TabGroupStretchedExample} from './tab-group-stretched/tab-group-stretched-example'; import {TabGroupThemeExample} from './tab-group-theme/tab-group-theme-example'; import {TabNavBarBasicExample} from './tab-nav-bar-basic/tab-nav-bar-basic-example'; +import {TabNavBarWithPanelExample} from './tab-nav-bar-with-panel/tab-nav-bar-with-panel-example'; export { TabGroupAlignExample, @@ -35,6 +36,7 @@ export { TabGroupStretchedExample, TabGroupThemeExample, TabNavBarBasicExample, + TabNavBarWithPanelExample, }; const EXAMPLES = [ @@ -51,6 +53,7 @@ const EXAMPLES = [ TabGroupStretchedExample, TabGroupThemeExample, TabNavBarBasicExample, + TabNavBarWithPanelExample, ]; @NgModule({ diff --git a/src/components-examples/material/tabs/tab-nav-bar-with-panel/tab-nav-bar-with-panel-example.css b/src/components-examples/material/tabs/tab-nav-bar-with-panel/tab-nav-bar-with-panel-example.css new file mode 100644 index 000000000000..e7f8daa5cd3b --- /dev/null +++ b/src/components-examples/material/tabs/tab-nav-bar-with-panel/tab-nav-bar-with-panel-example.css @@ -0,0 +1,4 @@ +.example-action-button { + margin-top: 8px; + margin-right: 8px; +} diff --git a/src/components-examples/material/tabs/tab-nav-bar-with-panel/tab-nav-bar-with-panel-example.html b/src/components-examples/material/tabs/tab-nav-bar-with-panel/tab-nav-bar-with-panel-example.html new file mode 100644 index 000000000000..1ab8a3c4da0f --- /dev/null +++ b/src/components-examples/material/tabs/tab-nav-bar-with-panel/tab-nav-bar-with-panel-example.html @@ -0,0 +1,9 @@ + + + + diff --git a/src/components-examples/material/tabs/tab-nav-bar-with-panel/tab-nav-bar-with-panel-example.ts b/src/components-examples/material/tabs/tab-nav-bar-with-panel/tab-nav-bar-with-panel-example.ts new file mode 100644 index 000000000000..b7beb7ae2e2f --- /dev/null +++ b/src/components-examples/material/tabs/tab-nav-bar-with-panel/tab-nav-bar-with-panel-example.ts @@ -0,0 +1,14 @@ +import {Component} from '@angular/core'; + +/** + * @title Use of the tab nav bar with the dedicated panel component. + */ +@Component({ + selector: 'tab-nav-bar-with-panel-example', + templateUrl: 'tab-nav-bar-with-panel-example.html', + styleUrls: ['tab-nav-bar-with-panel-example.css'], +}) +export class TabNavBarWithPanelExample { + links = ['First', 'Second', 'Third']; + activeLink = this.links[0]; +} diff --git a/src/dev-app/mdc-tabs/mdc-tabs-demo.html b/src/dev-app/mdc-tabs/mdc-tabs-demo.html index 70156eec4d1c..daddf891f4f1 100644 --- a/src/dev-app/mdc-tabs/mdc-tabs-demo.html +++ b/src/dev-app/mdc-tabs/mdc-tabs-demo.html @@ -127,4 +127,13 @@

Tab nav bar

[active]="activeLink == link">{{link}} Disabled Link + +

Tab nav bar with panel

+ + diff --git a/src/dev-app/tabs/tabs-demo.html b/src/dev-app/tabs/tabs-demo.html index 52d62976e2a8..9531a56be77a 100644 --- a/src/dev-app/tabs/tabs-demo.html +++ b/src/dev-app/tabs/tabs-demo.html @@ -18,5 +18,7 @@

Tab group stretched

Tab group theming

-

Tab Navigation Bar basic

+

Tab navigation bar basic

+

Tab navigation bar with panel

+ diff --git a/src/material-experimental/mdc-checkbox/_checkbox-theme.scss b/src/material-experimental/mdc-checkbox/_checkbox-theme.scss index aeb8729ce661..c65c217b1733 100644 --- a/src/material-experimental/mdc-checkbox/_checkbox-theme.scss +++ b/src/material-experimental/mdc-checkbox/_checkbox-theme.scss @@ -4,6 +4,7 @@ @use '@material/theme/theme-color' as mdc-theme-color; @use '@material/theme/theme'; @use 'sass:map'; +@use 'sass:color'; @use '../mdc-helpers/mdc-helpers'; @use '../../material/core/typography/typography'; @use '../../material/core/theming/theming'; @@ -12,15 +13,34 @@ // Mixin that includes the checkbox theme styles with a given palette. // By default, the MDC checkbox always uses the `secondary` palette. -@mixin private-checkbox-styles-with-color($color) { - @include mdc-checkbox-theme.theme-deprecated( - ( - checkmark-color: mdc-theme-color.prop-value(on-#{$color}), - container-checked-color: $color, - container-disabled-color: rgba(mdc-theme-color.prop-value(on-surface), 0.38), - outline-color: rgba(mdc-theme-color.prop-value(on-surface), 0.54), - ) - ); +@mixin private-checkbox-styles-with-color($color, $mdcColor) { + $on-surface: mdc-theme-color.prop-value(on-surface); + $border-color: rgba($on-surface, color.opacity(mdc-checkbox-theme.$border-color)); + $disabled-color: rgba($on-surface, color.opacity(mdc-checkbox-theme.$disabled-color)); + + @include mdc-checkbox-theme.theme(( + selected-checkmark-color: mdc-theme-color.prop-value(on-#{$mdcColor}), + + selected-focus-icon-color: $color, + selected-hover-icon-color: $color, + selected-hover-state-layer-color: $color, + selected-icon-color: $color, + selected-pressed-icon-color: $color, + unselected-focus-icon-color: $color, + unselected-hover-icon-color: $color, + + selected-focus-state-layer-color: $on-surface, + selected-pressed-state-layer-color: $on-surface, + unselected-focus-state-layer-color: $on-surface, + unselected-hover-state-layer-color: $on-surface, + unselected-pressed-state-layer-color: $on-surface, + + disabled-selected-icon-color: $disabled-color, + disabled-unselected-icon-color: $disabled-color, + + unselected-icon-color: $border-color, + unselected-pressed-icon-color: $border-color, + )); } // Apply ripple colors to the MatRipple element and the MDC ripple element when the @@ -47,21 +67,7 @@ $accent: theming.get-color-from-palette(map.get($config, accent)); $warn: theming.get-color-from-palette(map.get($config, warn)); - // Save original values of MDC global variables. We need to save these so we can restore the - // variables to their original values and prevent unintended side effects from using this mixin. - $orig-border-color: mdc-checkbox-theme.$border-color; - $orig-disabled-color: mdc-checkbox-theme.$disabled-color; - @include mdc-helpers.mat-using-mdc-theme($config) { - mdc-checkbox-theme.$border-color: rgba( - mdc-theme-color.prop-value(on-surface), - 0.54 - ); - mdc-checkbox-theme.$disabled-color: rgba( - mdc-theme-color.prop-value(on-surface), - 0.26 - ); - .mat-mdc-checkbox { @include mdc-form-field.core-styles($query: mdc-helpers.$mat-theme-styles-query); @include ripple-theme.color(( @@ -78,25 +84,21 @@ // class for accent and warn style, and applying the appropriate overrides below. Since we // don't use MDC's ripple, we also need to set the color for our replacement ripple. &.mat-primary { - @include private-checkbox-styles-with-color(primary); + @include private-checkbox-styles-with-color($primary, primary); @include _selected-ripple-colors($primary, primary); } &.mat-accent { - @include private-checkbox-styles-with-color(secondary); + @include private-checkbox-styles-with-color($accent, secondary); @include _selected-ripple-colors($accent, secondary); } &.mat-warn { - @include private-checkbox-styles-with-color(error); + @include private-checkbox-styles-with-color($warn, error); @include _selected-ripple-colors($warn, error); } } } - - // Restore original values of MDC global variables. - mdc-checkbox-theme.$border-color: $orig-border-color; - mdc-checkbox-theme.$disabled-color: $orig-disabled-color; } @mixin typography($config-or-theme) { diff --git a/src/material-experimental/mdc-checkbox/checkbox.scss b/src/material-experimental/mdc-checkbox/checkbox.scss index 12434dfd44c4..66dddc6dcd25 100644 --- a/src/material-experimental/mdc-checkbox/checkbox.scss +++ b/src/material-experimental/mdc-checkbox/checkbox.scss @@ -1,4 +1,5 @@ @use '@material/checkbox' as mdc-checkbox; +@use '@material/checkbox/checkbox-theme' as mdc-checkbox-theme; @use '@material/form-field' as mdc-form-field; @use '@material/ripple' as mdc-ripple; @use '@material/touch-target' as mdc-touch-target; @@ -23,25 +24,31 @@ // we have to change it in order for margins to work. display: inline-block; - // The MDC checkbox styles related to the hover state are intertwined with the MDC ripple styles. - // We currently don't use the MDC ripple due to size concerns, therefore we need to add some - // additional styles to restore the hover state. - .mdc-checkbox:hover .mdc-checkbox__native-control:not([disabled]) ~ .mdc-checkbox__ripple { - opacity: map.get(mdc-ripple.$dark-ink-opacities, hover); - transform: scale(1); - transition: mdc-checkbox-transition-enter(opacity, 0, 80ms), - mdc-checkbox-transition-enter(transform, 0, 80ms); - } + .mdc-checkbox { + // MDC theme styles also include structural styles so we have to include the theme at least + // once here. The values will be overwritten by our own theme file afterwards. + @include mdc-checkbox-theme.theme-styles(mdc-checkbox-theme.$light-theme); + + // The MDC checkbox styles related to the hover state are intertwined with the MDC ripple + // styles. We currently don't use the MDC ripple due to size concerns, therefore we need to + // add some additional styles to restore the hover state. + &:hover .mdc-checkbox__native-control:not([disabled]) ~ .mdc-checkbox__ripple { + opacity: map.get(mdc-ripple.$dark-ink-opacities, hover); + transform: scale(1); + transition: mdc-checkbox-transition-enter(opacity, 0, 80ms), + mdc-checkbox-transition-enter(transform, 0, 80ms); + } - // Note that the :not([disabled]) here isn't necessary, but we need it for the - // extra specificity so that the hover styles don't override the focus styles. - .mdc-checkbox .mdc-checkbox__native-control:not([disabled]):focus ~ .mdc-checkbox__ripple { - opacity: map.get(mdc-ripple.$dark-ink-opacities, hover) + - map.get(mdc-ripple.$dark-ink-opacities, focus); + // Note that the :not([disabled]) here isn't necessary, but we need it for the + // extra specificity so that the hover styles don't override the focus styles. + .mdc-checkbox__native-control:not([disabled]):focus ~ .mdc-checkbox__ripple { + opacity: map.get(mdc-ripple.$dark-ink-opacities, hover) + + map.get(mdc-ripple.$dark-ink-opacities, focus); - @include a11y.high-contrast(active, off) { - outline: solid 3px; - opacity: 1; + @include a11y.high-contrast(active, off) { + outline: solid 3px; + opacity: 1; + } } } diff --git a/src/material-experimental/mdc-chips/chip-row.ts b/src/material-experimental/mdc-chips/chip-row.ts index b811863c02b4..08c57d940136 100644 --- a/src/material-experimental/mdc-chips/chip-row.ts +++ b/src/material-experimental/mdc-chips/chip-row.ts @@ -167,7 +167,7 @@ export class MatChipRow } // Wait to see if focus moves to the other gridcell - this._focusoutTimeout = setTimeout(() => { + this._focusoutTimeout = window.setTimeout(() => { this._hasFocusInternal = false; this._onBlur.next({chip: this}); this._handleInteraction(event); diff --git a/src/material-experimental/mdc-dialog/dialog.scss b/src/material-experimental/mdc-dialog/dialog.scss index 65d79daaee4b..ba994299dfd2 100644 --- a/src/material-experimental/mdc-dialog/dialog.scss +++ b/src/material-experimental/mdc-dialog/dialog.scss @@ -1,7 +1,6 @@ @use '@material/dialog' as mdc-dialog; @use '../mdc-helpers/mdc-helpers'; @use './mdc-dialog-structure-overrides'; -@use '../../cdk/a11y'; // Dialog content max height. This has been copied from the standard dialog // and is needed to make the dialog content scrollable. @@ -17,10 +16,6 @@ $mat-dialog-button-horizontal-margin: 8px !default; // The dialog container is focusable. We remove the default outline shown in browsers. .mat-mdc-dialog-container { outline: 0; - - @include a11y.high-contrast(active, off) { - outline: solid 1px; - } } // MDC sets the display behavior for title and actions, but not for content. Since we support diff --git a/src/material-experimental/mdc-list/_list-option-theme.scss b/src/material-experimental/mdc-list/_list-option-theme.scss index 031af6384ace..cef9b98d85fc 100644 --- a/src/material-experimental/mdc-list/_list-option-theme.scss +++ b/src/material-experimental/mdc-list/_list-option-theme.scss @@ -6,9 +6,9 @@ // Mixin that overrides the selected item and checkbox colors for list options. By // default, the MDC list uses the `primary` color for list items. The MDC checkbox // inside list options by default uses the `primary` color too. -@mixin private-list-option-color-override($color) { +@mixin private-list-option-color-override($color, $mdcColor) { & .mdc-list-item__start, & .mdc-list-item__end { - @include checkbox-theme.private-checkbox-styles-with-color($color); + @include checkbox-theme.private-checkbox-styles-with-color($color, $mdcColor); } } diff --git a/src/material-experimental/mdc-list/_list-theme.scss b/src/material-experimental/mdc-list/_list-theme.scss index ac1eea9c964b..b8d6048d8650 100644 --- a/src/material-experimental/mdc-list/_list-theme.scss +++ b/src/material-experimental/mdc-list/_list-theme.scss @@ -1,3 +1,4 @@ +@use 'sass:map'; @use '@material/list/evolution-mixins' as mdc-list; @use './interactive-list-theme'; @use './list-option-theme'; @@ -11,6 +12,9 @@ @mixin color($config-or-theme) { $config: theming.get-color-config($config-or-theme); + $primary: theming.get-color-from-palette(map.get($config, primary)); + $accent: theming.get-color-from-palette(map.get($config, accent)); + $warn: theming.get-color-from-palette(map.get($config, warn)); // MDC's state styles are tied in with their ripple. Since we don't use the MDC // ripple, we need to add the hover, focus and selected states manually. @@ -20,13 +24,13 @@ @include mdc-list.without-ripple($query: mdc-helpers.$mat-theme-styles-query); .mat-mdc-list-option { - @include list-option-theme.private-list-option-color-override(primary); + @include list-option-theme.private-list-option-color-override($primary, primary); } .mat-mdc-list-option.mat-accent { - @include list-option-theme.private-list-option-color-override(secondary); + @include list-option-theme.private-list-option-color-override($accent, secondary); } .mat-mdc-list-option.mat-warn { - @include list-option-theme.private-list-option-color-override(error); + @include list-option-theme.private-list-option-color-override($warn, error); } } } diff --git a/src/material-experimental/mdc-list/list-option.scss b/src/material-experimental/mdc-list/list-option.scss index 05cee79f3949..1a3f93841125 100644 --- a/src/material-experimental/mdc-list/list-option.scss +++ b/src/material-experimental/mdc-list/list-option.scss @@ -1,5 +1,6 @@ @use '@material/checkbox' as mdc-checkbox; @use '@material/list/evolution-variables' as mdc-list-variables; +@use '@material/checkbox/checkbox-theme' as mdc-checkbox-theme; @use '../mdc-helpers/mdc-helpers'; @use '../../cdk/a11y'; @use './list-option-trailing-avatar-compat'; @@ -18,6 +19,14 @@ @include mdc-checkbox.without-ripple($query: animation); } + // We can't use the MDC checkbox here directly, because this checkbox is purely + // decorative and including the MDC one will bring in unnecessary JS. + .mdc-checkbox { + // MDC theme styles also include structural styles so we have to include the theme at least + // once here. The values will be overwritten by our own theme file afterwards. + @include mdc-checkbox-theme.theme-styles(mdc-checkbox-theme.$light-theme); + } + // The internal checkbox is purely decorative, but because it's an `input`, the user can still // focus it by tabbing or clicking. Furthermore, `mat-list-option` has the `option` role which // doesn't allow a nested `input`. We use `display: none` both to remove it from the tab order diff --git a/src/material-experimental/mdc-select/_select-theme.scss b/src/material-experimental/mdc-select/_select-theme.scss index 1964be8b2c6b..47ced15c462e 100644 --- a/src/material-experimental/mdc-select/_select-theme.scss +++ b/src/material-experimental/mdc-select/_select-theme.scss @@ -1,7 +1,6 @@ @use '@material/theme/theme-color' as mdc-theme-color; @use '@material/menu-surface' as mdc-menu-surface; @use '@material/list/evolution-mixins' as mdc-list; -@use '@material/select' as mdc-select; @use '@material/typography' as mdc-typography; @use '../mdc-helpers/mdc-helpers'; @use '../../material/core/typography/typography'; @@ -15,38 +14,25 @@ @mixin color($config-or-theme) { $config: theming.get-color-config($config-or-theme); - // Save original values of MDC global variables. We need to save these so we can restore the - // variables to their original values and prevent unintended side effects from using this mixin. - $orig-ink-color: mdc-select.$ink-color; - $orig-label-color: mdc-select.$label-color; - $orig-disabled-label-color: mdc-select.$disabled-label-color; - $orig-dropdown-icon-color: mdc-select.$dropdown-icon-color; - $orig-disabled-dropdown-icon-color: mdc-select.$disabled-dropdown-icon-color; - @include mdc-helpers.mat-using-mdc-theme($config) { - mdc-select.$ink-color: rgba(mdc-theme-color.prop-value(on-surface), 0.87); - mdc-select.$label-color: rgba(mdc-theme-color.prop-value(on-surface), 0.6); - mdc-select.$disabled-label-color: rgba(mdc-theme-color.prop-value(on-surface), 0.38); - mdc-select.$dropdown-icon-color: rgba(mdc-theme-color.prop-value(on-surface), 0.54); - mdc-select.$disabled-dropdown-icon-color: rgba(mdc-theme-color.prop-value(on-surface), 0.38); - + $disabled-color: rgba(mdc-theme-color.prop-value(on-surface), 0.38); @include mdc-menu-surface.core-styles(mdc-helpers.$mat-theme-styles-query); @include mdc-list.without-ripple(mdc-helpers.$mat-theme-styles-query); .mat-mdc-select-value { - color: mdc-select.$ink-color; + color: rgba(mdc-theme-color.prop-value(on-surface), 0.87); } .mat-mdc-select-placeholder { - color: mdc-select.$label-color; + color: rgba(mdc-theme-color.prop-value(on-surface), 0.6); } .mat-mdc-select-disabled .mat-mdc-select-value { - color: mdc-select.$disabled-label-color; + color: $disabled-color; } .mat-mdc-select-arrow { - color: mdc-select.$dropdown-icon-color; + color: rgba(mdc-theme-color.prop-value(on-surface), 0.54); } .mat-mdc-form-field { @@ -69,17 +55,10 @@ } .mat-mdc-select.mat-mdc-select-disabled .mat-mdc-select-arrow { - color: mdc-select.$disabled-dropdown-icon-color; + color: $disabled-color; } } } - - // Restore original values of MDC global variables. - mdc-select.$ink-color: $orig-ink-color; - mdc-select.$label-color: $orig-label-color; - mdc-select.$disabled-label-color: $orig-disabled-label-color; - mdc-select.$dropdown-icon-color: $orig-dropdown-icon-color; - mdc-select.$disabled-dropdown-icon-color: $orig-disabled-dropdown-icon-color; } @mixin typography($config-or-theme) { diff --git a/src/material-experimental/mdc-select/select.scss b/src/material-experimental/mdc-select/select.scss index c201f400dd2e..7defc9a0bf9d 100644 --- a/src/material-experimental/mdc-select/select.scss +++ b/src/material-experimental/mdc-select/select.scss @@ -7,7 +7,7 @@ $mat-select-arrow-size: 5px !default; $mat-select-arrow-margin: 4px !default; -$mat-select-panel-max-height: 256px !default; +$mat-select-panel-max-height: 275px !default; $mat-select-placeholder-arrow-space: 2 * ($mat-select-arrow-size + $mat-select-arrow-margin); $leading-width: 12px !default; diff --git a/src/material-experimental/mdc-select/select.spec.ts b/src/material-experimental/mdc-select/select.spec.ts index d14495631d09..fec21af6b1ba 100644 --- a/src/material-experimental/mdc-select/select.spec.ts +++ b/src/material-experimental/mdc-select/select.spec.ts @@ -2237,9 +2237,8 @@ describe('MDC-based MatSelect', () => { dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW); } - // +