From 739d25e228e8c6fc1bc7dd585405c23c78096510 Mon Sep 17 00:00:00 2001 From: Frederik Schlemmer <35023083+FrederikSchlemmer@users.noreply.github.com> Date: Tue, 17 Jul 2018 17:04:25 +0200 Subject: [PATCH 001/189] docs: adds new button examples (#11975) --- .../button-types/button-types-example.html | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/material-examples/button-types/button-types-example.html b/src/material-examples/button-types/button-types-example.html index b6da074b1d8c..710c1415831a 100644 --- a/src/material-examples/button-types/button-types-example.html +++ b/src/material-examples/button-types/button-types-example.html @@ -28,6 +28,16 @@

Stroked Buttons

Link +

Flat Buttons

+
+ + + + + + Link +
+

Icon Buttons

Link -
+ \ No newline at end of file From ead7deba8e170f399909ccd3eb3e84815572cd0b Mon Sep 17 00:00:00 2001 From: mmalerba Date: Tue, 17 Jul 2018 08:09:09 -0700 Subject: [PATCH 002/189] virtual-scroll: Avoid using bypassSecurityTrustStyle which is banned in google3 (#12181) --- .../scrolling/virtual-scroll-viewport.html | 3 +- .../scrolling/virtual-scroll-viewport.spec.ts | 12 -------- .../scrolling/virtual-scroll-viewport.ts | 29 ++++++++++--------- 3 files changed, 16 insertions(+), 28 deletions(-) diff --git a/src/cdk-experimental/scrolling/virtual-scroll-viewport.html b/src/cdk-experimental/scrolling/virtual-scroll-viewport.html index 5fc18943645a..e545f0ff4dd7 100644 --- a/src/cdk-experimental/scrolling/virtual-scroll-viewport.html +++ b/src/cdk-experimental/scrolling/virtual-scroll-viewport.html @@ -2,8 +2,7 @@ Wrap the rendered content in an element that will be used to offset it based on the scroll position. --> -
+
+ {{_control.placeholder}} @@ -46,7 +46,7 @@ - +
From 21ce45e748aa531ab25cb34b8756eda48d80bb10 Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Fri, 10 Aug 2018 08:17:02 -0700 Subject: [PATCH 081/189] chore: revert pr/11628 (#12623) --- ...exible-connected-position-strategy.spec.ts | 159 ------------------ .../flexible-connected-position-strategy.ts | 66 +++----- src/cdk/scrolling/viewport-ruler.ts | 8 +- src/lib/menu/menu-trigger.ts | 1 - 4 files changed, 21 insertions(+), 213 deletions(-) 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 d6c890931ca8..8edb456a4ff8 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts @@ -1134,165 +1134,6 @@ describe('FlexibleConnectedPositionStrategy', () => { expect(Math.floor(overlayRect.top)).toBe(15); }); - it('should not mess with the left offset when pushing from the top', () => { - originElement.style.top = `${-OVERLAY_HEIGHT * 2}px`; - originElement.style.left = '200px'; - - positionStrategy.withPositions([{ - originX: 'start', - originY: 'bottom', - overlayX: 'start', - overlayY: 'top' - }]); - - attachOverlay({positionStrategy}); - - const overlayRect = overlayRef.overlayElement.getBoundingClientRect(); - expect(Math.floor(overlayRect.left)).toBe(200); - }); - - it('should align to the trigger if the overlay is wider than the viewport, but the trigger ' + - 'is still within the viewport', () => { - originElement.style.top = '200px'; - originElement.style.left = '200px'; - - positionStrategy.withPositions([ - { - originX: 'start', - originY: 'bottom', - overlayX: 'start', - overlayY: 'top' - }, - { - originX: 'end', - originY: 'bottom', - overlayX: 'end', - overlayY: 'top' - } - ]); - - attachOverlay({ - width: viewport.getViewportRect().width + 100, - positionStrategy - }); - - const overlayRect = overlayRef.overlayElement.getBoundingClientRect(); - const originRect = originElement.getBoundingClientRect(); - - expect(Math.floor(overlayRect.left)).toBe(Math.floor(originRect.left)); - }); - - it('should push into the viewport if the overlay is wider than the viewport and the trigger' + - 'out of the viewport', () => { - originElement.style.top = '200px'; - originElement.style.left = `-${DEFAULT_WIDTH / 2}px`; - - positionStrategy.withPositions([ - { - originX: 'start', - originY: 'bottom', - overlayX: 'start', - overlayY: 'top' - }, - { - originX: 'end', - originY: 'bottom', - overlayX: 'end', - overlayY: 'top' - } - ]); - - attachOverlay({ - width: viewport.getViewportRect().width + 100, - positionStrategy - }); - - const overlayRect = overlayRef.overlayElement.getBoundingClientRect(); - expect(Math.floor(overlayRect.left)).toBe(0); - }); - - it('should keep the element inside the viewport as the user is scrolling, ' + - 'with position locking disabled', () => { - const veryLargeElement = document.createElement('div'); - - originElement.style.top = `${-OVERLAY_HEIGHT * 2}px`; - originElement.style.left = '200px'; - - veryLargeElement.style.width = '100%'; - veryLargeElement.style.height = '2000px'; - document.body.appendChild(veryLargeElement); - - positionStrategy - .withLockedPosition(false) - .withViewportMargin(0) - .withPositions([{ - overlayY: 'top', - overlayX: 'start', - originY: 'top', - originX: 'start' - }]); - - attachOverlay({positionStrategy}); - - let overlayRect = overlayRef.overlayElement.getBoundingClientRect(); - expect(Math.floor(overlayRect.top)) - .toBe(0, 'Expected overlay to be in the viewport initially.'); - - window.scroll(0, 100); - overlayRef.updatePosition(); - zone.simulateZoneExit(); - - overlayRect = overlayRef.overlayElement.getBoundingClientRect(); - expect(Math.floor(overlayRect.top)) - .toBe(0, 'Expected overlay to stay in the viewport after scrolling.'); - - window.scroll(0, 0); - document.body.removeChild(veryLargeElement); - }); - - it('should not continue pushing the overlay as the user scrolls, if position ' + - 'locking is enabled', () => { - const veryLargeElement = document.createElement('div'); - - originElement.style.top = `${-OVERLAY_HEIGHT * 2}px`; - originElement.style.left = '200px'; - - veryLargeElement.style.width = '100%'; - veryLargeElement.style.height = '2000px'; - document.body.appendChild(veryLargeElement); - - positionStrategy - .withLockedPosition() - .withViewportMargin(0) - .withPositions([{ - overlayY: 'top', - overlayX: 'start', - originY: 'top', - originX: 'start' - }]); - - attachOverlay({positionStrategy}); - - const scrollBy = 100; - let initialOverlayTop = Math.floor(overlayRef.overlayElement.getBoundingClientRect().top); - - expect(initialOverlayTop).toBe(0, 'Expected overlay to be inside the viewport initially.'); - - window.scroll(0, scrollBy); - overlayRef.updatePosition(); - zone.simulateZoneExit(); - - let currentOverlayTop = Math.floor(overlayRef.overlayElement.getBoundingClientRect().top); - - expect(currentOverlayTop).toBeLessThan(0, - 'Expected overlay to no longer be completely inside the viewport.'); - expect(currentOverlayTop).toBe(initialOverlayTop - scrollBy, - 'Expected overlay to maintain its previous position.'); - - window.scroll(0, 0); - document.body.removeChild(veryLargeElement); - }); - }); describe('with flexible dimensions', () => { diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.ts index 778e7844f124..75791a3c7c16 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.ts @@ -8,7 +8,7 @@ import {PositionStrategy} from './position-strategy'; import {ElementRef} from '@angular/core'; -import {ViewportRuler, CdkScrollable, ViewportScrollPosition} from '@angular/cdk/scrolling'; +import {ViewportRuler, CdkScrollable} from '@angular/cdk/scrolling'; import { ConnectedOverlayPositionChange, ConnectionPositionPair, @@ -112,9 +112,6 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { /** Amount of subscribers to the `positionChanges` stream. */ private _positionChangeSubscriptions = 0; - /** Amount by which the overlay was pushed in each axis during the last time it was positioned. */ - private _previousPushAmount: {x: number, y: number} | null; - /** Observable sequence of position changes. */ positionChanges: Observable = Observable.create(observer => { const subscription = this._positionChanges.subscribe(observer); @@ -285,8 +282,6 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { } detach() { - this._lastPosition = null; - this._previousPushAmount = null; this._resizeSubscription.unsubscribe(); } @@ -546,55 +541,39 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { * the viewport, the top-left corner will be pushed on-screen (with overflow occuring on the * right and bottom). * - * @param start Starting point from which the overlay is pushed. - * @param overlay Dimensions of the overlay. - * @param scrollPosition Current viewport scroll position. + * @param start The starting point from which the overlay is pushed. + * @param overlay The overlay dimensions. * @returns The point at which to position the overlay after pushing. This is effectively a new * originPoint. */ - private _pushOverlayOnScreen(start: Point, - overlay: ClientRect, - scrollPosition: ViewportScrollPosition): Point { - // If the position is locked and we've pushed the overlay already, reuse the previous push - // amount, rather than pushing it again. If we were to continue pushing, the element would - // remain in the viewport, which goes against the expectations when position locking is enabled. - if (this._previousPushAmount && this._positionLocked) { - return { - x: start.x + this._previousPushAmount.x, - y: start.y + this._previousPushAmount.y - }; - } - + private _pushOverlayOnScreen(start: Point, overlay: ClientRect): Point { const viewport = this._viewportRect; - // Determine how much the overlay goes outside the viewport on each - // side, which we'll use to decide which direction to push it. + // Determine how much the overlay goes outside the viewport on each side, which we'll use to + // decide which direction to push it. const overflowRight = Math.max(start.x + overlay.width - viewport.right, 0); const overflowBottom = Math.max(start.y + overlay.height - viewport.bottom, 0); - const overflowTop = Math.max(viewport.top - scrollPosition.top - start.y, 0); - const overflowLeft = Math.max(viewport.left - scrollPosition.left - start.x, 0); + const overflowTop = Math.max(viewport.top - start.y, 0); + const overflowLeft = Math.max(viewport.left - start.x, 0); - // Amount by which to push the overlay in each axis such that it remains on-screen. - let pushX = 0; - let pushY = 0; + // Amount by which to push the overlay in each direction such that it remains on-screen. + let pushX, pushY = 0; // If the overlay fits completely within the bounds of the viewport, push it from whichever // direction is goes off-screen. Otherwise, push the top-left corner such that its in the // viewport and allow for the trailing end of the overlay to go out of bounds. - if (overlay.width < viewport.width) { + if (overlay.width <= viewport.width) { pushX = overflowLeft || -overflowRight; } else { - pushX = start.x < this._viewportMargin ? (viewport.left - scrollPosition.left) - start.x : 0; + pushX = viewport.left - start.x; } - if (overlay.height < viewport.height) { + if (overlay.height <= viewport.height) { pushY = overflowTop || -overflowBottom; } else { - pushY = start.y < this._viewportMargin ? (viewport.top - scrollPosition.top) - start.y : 0; + pushY = viewport.top - start.y; } - this._previousPushAmount = {x: pushX, y: pushY}; - return { x: start.x + pushX, y: start.y + pushY, @@ -813,9 +792,8 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { const styles = {} as CSSStyleDeclaration; if (this._hasExactPosition()) { - const scrollPosition = this._viewportRuler.getViewportScrollPosition(); - extendStyles(styles, this._getExactOverlayY(position, originPoint, scrollPosition)); - extendStyles(styles, this._getExactOverlayX(position, originPoint, scrollPosition)); + extendStyles(styles, this._getExactOverlayY(position, originPoint)); + extendStyles(styles, this._getExactOverlayX(position, originPoint)); } else { styles.position = 'static'; } @@ -854,16 +832,14 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { } /** Gets the exact top/bottom for the overlay when not using flexible sizing or when pushing. */ - private _getExactOverlayY(position: ConnectedPosition, - originPoint: Point, - scrollPosition: ViewportScrollPosition) { + private _getExactOverlayY(position: ConnectedPosition, originPoint: Point) { // Reset any existing styles. This is necessary in case the // preferred position has changed since the last `apply`. let styles = {top: null, bottom: null} as CSSStyleDeclaration; let overlayPoint = this._getOverlayPoint(originPoint, this._overlayRect, position); if (this._isPushed) { - overlayPoint = this._pushOverlayOnScreen(overlayPoint, this._overlayRect, scrollPosition); + overlayPoint = this._pushOverlayOnScreen(overlayPoint, this._overlayRect); } // @breaking-change 7.0.0 Currently the `_overlayContainer` is optional in order to avoid a @@ -893,16 +869,14 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { } /** Gets the exact left/right for the overlay when not using flexible sizing or when pushing. */ - private _getExactOverlayX(position: ConnectedPosition, - originPoint: Point, - scrollPosition: ViewportScrollPosition) { + private _getExactOverlayX(position: ConnectedPosition, originPoint: Point) { // Reset any existing styles. This is necessary in case the preferred position has // changed since the last `apply`. let styles = {left: null, right: null} as CSSStyleDeclaration; let overlayPoint = this._getOverlayPoint(originPoint, this._overlayRect, position); if (this._isPushed) { - overlayPoint = this._pushOverlayOnScreen(overlayPoint, this._overlayRect, scrollPosition); + overlayPoint = this._pushOverlayOnScreen(overlayPoint, this._overlayRect); } // We want to set either `left` or `right` based on whether the overlay wants to appear "before" diff --git a/src/cdk/scrolling/viewport-ruler.ts b/src/cdk/scrolling/viewport-ruler.ts index cb69d1200d93..f8e8c6a166d2 100644 --- a/src/cdk/scrolling/viewport-ruler.ts +++ b/src/cdk/scrolling/viewport-ruler.ts @@ -14,12 +14,6 @@ import {auditTime} from 'rxjs/operators'; /** Time in ms to throttle the resize events by default. */ export const DEFAULT_RESIZE_TIME = 20; -/** Object that holds the scroll position of the viewport in each direction. */ -export interface ViewportScrollPosition { - top: number; - left: number; -} - /** * Simple utility for getting the bounds of the browser viewport. * @docs-private @@ -88,7 +82,7 @@ export class ViewportRuler implements OnDestroy { } /** Gets the (top, left) scroll position of the viewport. */ - getViewportScrollPosition(): ViewportScrollPosition { + getViewportScrollPosition() { // While we can get a reference to the fake document // during SSR, it doesn't have getBoundingClientRect. if (!this._platform.isBrowser) { diff --git a/src/lib/menu/menu-trigger.ts b/src/lib/menu/menu-trigger.ts index 3efa9f03aefb..8e00ee787e18 100644 --- a/src/lib/menu/menu-trigger.ts +++ b/src/lib/menu/menu-trigger.ts @@ -358,7 +358,6 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy { return new OverlayConfig({ positionStrategy: this._overlay.position() .flexibleConnectedTo(this._element) - .withLockedPosition() .withTransformOriginOn('.mat-menu-panel'), hasBackdrop: this.menu.hasBackdrop == null ? !this.triggersSubmenu() : this.menu.hasBackdrop, backdropClass: this.menu.backdropClass || 'cdk-overlay-transparent-backdrop', From 38e5eb2e3d457fc48c1cbcd88ebcf6e98fb9a5f1 Mon Sep 17 00:00:00 2001 From: Jacob Mansfield Date: Mon, 13 Aug 2018 14:53:19 +0100 Subject: [PATCH 082/189] docs(sidenav): add note about `mat-sidenav` placement (#12613) --- src/lib/sidenav/sidenav.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/sidenav/sidenav.md b/src/lib/sidenav/sidenav.md index c8aed710e66e..469fc04dd2e9 100644 --- a/src/lib/sidenav/sidenav.md +++ b/src/lib/sidenav/sidenav.md @@ -28,7 +28,7 @@ to specify which end of the main content to place the side content on. `position `start` or `end` which places the side content on the left or right respectively in left-to-right languages. If the `position` is not set, the default value of `start` will be assumed. A `` can have up to two `` elements total, but only one for any -given side. +given side. The `` must be placed as an immediate child of the ``. The main content should be wrapped in a ``. If no `` is specified for a ``, one will be created implicitly and all of the content From 7e7e87387f40e9e28bcb7f111bbde8a3fd2d2b0d Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 13 Aug 2018 15:54:16 +0200 Subject: [PATCH 083/189] fix(drag-drop): ignore self inside connectedTo (#12626) --- src/cdk-experimental/drag-drop/drop.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/cdk-experimental/drag-drop/drop.ts b/src/cdk-experimental/drag-drop/drop.ts index f055d3a44da6..e26003584af4 100644 --- a/src/cdk-experimental/drag-drop/drop.ts +++ b/src/cdk-experimental/drag-drop/drop.ts @@ -291,11 +291,9 @@ export class CdkDrop implements OnInit, OnDestroy { }) .sort((a, b) => a.clientRect.top - b.clientRect.top); - // TODO(crisbeto): add filter here that ensures that the - // current container isn't being passed to itself. this._positionCache.siblings = this.connectedTo .map(drop => typeof drop === 'string' ? this._dragDropRegistry.getDropContainer(drop)! : drop) - .filter(Boolean) + .filter(drop => drop && drop !== this) .map(drop => ({drop, clientRect: drop.element.nativeElement.getBoundingClientRect()})); } From 94e869af22965b774ad6363305a2e03f88b2f70c Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Mon, 13 Aug 2018 08:28:23 -0700 Subject: [PATCH 084/189] test(datepicker): fix test referencing a changed method name (#12653) --- src/lib/datepicker/datepicker.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index 6bb5d933b736..47c5f2563f12 100644 --- a/src/lib/datepicker/datepicker.spec.ts +++ b/src/lib/datepicker/datepicker.spec.ts @@ -482,7 +482,7 @@ describe('MatDatepicker', () => { testComponent.assignedDatepicker = testComponent.datepicker; fixture.detectChanges(); - testComponent.assignedDatepicker.select(toSelect); + testComponent.assignedDatepicker._select(toSelect); fixture.detectChanges(); flush(); fixture.detectChanges(); From 91496b1f878df34c6c2c29d4e8c773822a166678 Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Mon, 13 Aug 2018 08:36:18 -0700 Subject: [PATCH 085/189] chore: update changelog and package.json for 6.4.4 (#12652) --- CHANGELOG.md | 21 +++++++++++++++++++++ package.json | 3 ++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83fbee31180e..8fb014f9f4c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ + +## [6.4.4 mithril-magnet](https://github.com/angular/material2/compare/6.4.3...6.4.4) (2018-08-13) + + +### Bug Fixes + +* **button:** allow transition for the button focus overlay for all buttons ([#12552](https://github.com/angular/material2/issues/12552)) ([0a56cf7](https://github.com/angular/material2/commit/0a56cf7)) +* **button-toggle:** forward tabindex to underlying button ([#12538](https://github.com/angular/material2/issues/12538)) ([dcae875](https://github.com/angular/material2/commit/dcae875)) +* **breakpoint-observer:** Emit matching state of each query provided ([#12506](https://github.com/angular/material2/issues/12506)) ([cb3f760](https://github.com/angular/material2/commit/cb3f760)) +* **datepicker:** input not picking up changes if datepicker is assigned after init ([#12546](https://github.com/angular/material2/issues/12546)) ([d10a6c4](https://github.com/angular/material2/commit/d10a6c4)) +* **drag-drop:** add support for sorting animations ([#12530](https://github.com/angular/material2/issues/12530)) ([7d0e69b](https://github.com/angular/material2/commit/7d0e69b)) +* **drag-drop:** ignore self inside connectedTo ([#12626](https://github.com/angular/material2/issues/12626)) ([7e7e873](https://github.com/angular/material2/commit/7e7e873)) +* **drag-drop:** remove circular dependencies ([#12554](https://github.com/angular/material2/issues/12554)) ([fd70c07](https://github.com/angular/material2/commit/fd70c07)) +* **list:** disable hover styling on touch devices ([#12520](https://github.com/angular/material2/issues/12520)) ([6048f6f](https://github.com/angular/material2/commit/6048f6f)) +* **overlay:** flexible overlay with push not handling scroll offset and position locking ([#11628](https://github.com/angular/material2/issues/11628)) ([a192907](https://github.com/angular/material2/commit/a192907)) +* **paginator:** inconsistently disabling tooltips between browsers ([#12539](https://github.com/angular/material2/issues/12539)) ([35bdd00](https://github.com/angular/material2/commit/35bdd00)) +* **snackbar:** wrap simple snackbar text in span ([#12599](https://github.com/angular/material2/issues/12599)) ([11b97e4](https://github.com/angular/material2/commit/11b97e4)) +* **tabs:** animation running after initialization ([#12549](https://github.com/angular/material2/issues/12549)) ([2798084](https://github.com/angular/material2/commit/2798084)) +* **tree:** include constructors on MatTree classes to allow es6 builds ([#12556](https://github.com/angular/material2/issues/12556)) ([5b0eed3](https://github.com/angular/material2/commit/5b0eed3)) + + ## [6.4.3 monelite-meeple](https://github.com/angular/material2/compare/6.4.2...6.4.3) (2018-08-07) diff --git a/package.json b/package.json index 4c23731861ac..609316ba142e 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,12 @@ "docs": "gulp docs", "api": "gulp api-docs" }, - "version": "6.4.3", "license": "MIT", "engines": { "node": ">= 5.4.1" }, + "version": "6.4.4", + "requiredAngularVersion": ">=6.0.0 <7.0.0", "dependencies": { "@angular/animations": "6.0.0", "@angular/common": "6.0.0", From f575b1f05bd2a8048f0d92e2cf4ce04b4f6374e4 Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Mon, 13 Aug 2018 10:59:21 -0700 Subject: [PATCH 086/189] chore: bump to 6.4.5 (#12657) --- CHANGELOG.md | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fb014f9f4c8..55dda94314c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ - -## [6.4.4 mithril-magnet](https://github.com/angular/material2/compare/6.4.3...6.4.4) (2018-08-13) + +## [6.4.5 mithril-magnet](https://github.com/angular/material2/compare/6.4.3...6.4.4) (2018-08-13) ### Bug Fixes diff --git a/package.json b/package.json index 609316ba142e..0bb2bca02abc 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "engines": { "node": ">= 5.4.1" }, - "version": "6.4.4", + "version": "6.4.5", "requiredAngularVersion": ">=6.0.0 <7.0.0", "dependencies": { "@angular/animations": "6.0.0", From 4180e7282f1d25a9952c13671411ef9f69fcf1f1 Mon Sep 17 00:00:00 2001 From: mmalerba Date: Mon, 13 Aug 2018 09:51:03 -0700 Subject: [PATCH 087/189] fix(cdk-text-field): prevent keyframes from getting stripped by LibSass (#12567) --- src/cdk/text-field/_text-field.scss | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/cdk/text-field/_text-field.scss b/src/cdk/text-field/_text-field.scss index 82ecffcf4800..46a79b6d3180 100644 --- a/src/cdk/text-field/_text-field.scss +++ b/src/cdk/text-field/_text-field.scss @@ -1,10 +1,11 @@ // Core styles that enable monitoring autofill state of text fields. @mixin cdk-text-field { // Keyframes that apply no styles, but allow us to monitor when an text field becomes autofilled - // by watching for the animation events that are fired when they start. + // by watching for the animation events that are fired when they start. Note: the /*!*/ comment is + // needed to prevent LibSass from stripping the keyframes out. // Based on: https://medium.com/@brunn/detecting-autofilled-fields-in-javascript-aed598d25da7 - @keyframes cdk-text-field-autofill-start {} - @keyframes cdk-text-field-autofill-end {} + @keyframes cdk-text-field-autofill-start {/*!*/} + @keyframes cdk-text-field-autofill-end {/*!*/} .cdk-text-field-autofill-monitored:-webkit-autofill { animation-name: cdk-text-field-autofill-start; From 75632bd6d3834d64489bea6af6b86c7b0f9e06fd Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 13 Aug 2018 18:51:50 +0200 Subject: [PATCH 088/189] fix(tabs): changed after checked error when using isActive in view (#12206) * Fixes a "changed after checked" error being thrown if the consumer is using the `isActive` property of a tab somewhere in the view. * Reworks the `MatTab` to only have one `stateChanges` subject rather than one per property. Fixes #12197. --- src/lib/tabs/tab-group.spec.ts | 40 ++++++++++++++++++++++++++++++---- src/lib/tabs/tab-group.ts | 30 +++++++++++++++---------- src/lib/tabs/tab.ts | 18 +++++---------- 3 files changed, 59 insertions(+), 29 deletions(-) diff --git a/src/lib/tabs/tab-group.spec.ts b/src/lib/tabs/tab-group.spec.ts index cb456caa6811..dab3da381804 100644 --- a/src/lib/tabs/tab-group.spec.ts +++ b/src/lib/tabs/tab-group.spec.ts @@ -4,6 +4,7 @@ import {Component, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/co import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {CommonModule} from '@angular/common'; import {Observable} from 'rxjs'; import {MatTab, MatTabGroup, MatTabHeaderPosition, MatTabsModule} from './index'; @@ -11,7 +12,7 @@ import {MatTab, MatTabGroup, MatTabHeaderPosition, MatTabsModule} from './index' describe('MatTabGroup', () => { beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ - imports: [MatTabsModule, NoopAnimationsModule], + imports: [MatTabsModule, CommonModule, NoopAnimationsModule], declarations: [ SimpleTabsTestApp, SimpleDynamicTabsTestApp, @@ -21,6 +22,7 @@ describe('MatTabGroup', () => { TabGroupWithSimpleApi, TemplateTabs, TabGroupWithAriaInputs, + TabGroupWithIsActiveBinding, ], }); @@ -195,8 +197,9 @@ describe('MatTabGroup', () => { .toBe(0, 'Expected no ripple to show up on label mousedown.'); }); - it('should set the isActive flag on each of the tabs', () => { + it('should set the isActive flag on each of the tabs', fakeAsync(() => { fixture.detectChanges(); + tick(); const tabs = fixture.componentInstance.tabs.toArray(); @@ -206,11 +209,12 @@ describe('MatTabGroup', () => { fixture.componentInstance.selectedIndex = 2; fixture.detectChanges(); + tick(); expect(tabs[0].isActive).toBe(false); expect(tabs[1].isActive).toBe(false); expect(tabs[2].isActive).toBe(true); - }); + })); it('should fire animation done event', fakeAsync(() => { fixture.detectChanges(); @@ -567,7 +571,21 @@ describe('MatTabGroup', () => { const child = fixture.debugElement.query(By.css('.child')); expect(child.nativeElement).toBeDefined(); })); - }); + }); + + describe('special cases', () => { + it('should not throw an error when binding isActive to the view', fakeAsync(() => { + const fixture = TestBed.createComponent(TabGroupWithIsActiveBinding); + + expect(() => { + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + }).not.toThrow(); + + expect(fixture.nativeElement.textContent).toContain('pizza is active'); + })); + }); /** * Checks that the `selectedIndex` has been updated; checks that the label and body have their @@ -828,3 +846,17 @@ class TabGroupWithAriaInputs { ariaLabel: string; ariaLabelledby: string; } + + +@Component({ + template: ` + + Pizza, fries + Broccoli, spinach + + +
pizza is active
+ ` +}) +class TabGroupWithIsActiveBinding { +} diff --git a/src/lib/tabs/tab-group.ts b/src/lib/tabs/tab-group.ts index 5ef2df2273d1..5cdbbd9f50c1 100644 --- a/src/lib/tabs/tab-group.ts +++ b/src/lib/tabs/tab-group.ts @@ -165,18 +165,27 @@ export class MatTabGroup extends _MatTabGroupMixinBase implements AfterContentIn // If there is a change in selected index, emit a change event. Should not trigger if // the selected index has not yet been initialized. - if (this._selectedIndex != indexToSelect && this._selectedIndex != null) { - const tabChangeEvent = this._createChangeEvent(indexToSelect); - this.selectedTabChange.emit(tabChangeEvent); - // Emitting this value after change detection has run - // since the checked content may contain this variable' - Promise.resolve().then(() => this.selectedIndexChange.emit(indexToSelect)); + if (this._selectedIndex != indexToSelect) { + const isFirstRun = this._selectedIndex == null; + + if (!isFirstRun) { + this.selectedTabChange.emit(this._createChangeEvent(indexToSelect)); + } + + // Changing these values after change detection has run + // since the checked content may contain references to them. + Promise.resolve().then(() => { + this._tabs.forEach((tab, index) => tab.isActive = index === indexToSelect); + + if (!isFirstRun) { + this.selectedIndexChange.emit(indexToSelect); + } + }); } // Setup the position for each tab and optionally setup an origin on the next selected tab. this._tabs.forEach((tab: MatTab, index: number) => { tab.position = index - indexToSelect; - tab.isActive = index === indexToSelect; // If there is already a selected tab, then set up an origin for the next selected tab // if it doesn't have one already. @@ -256,11 +265,8 @@ export class MatTabGroup extends _MatTabGroupMixinBase implements AfterContentIn this._tabLabelSubscription.unsubscribe(); } - this._tabLabelSubscription = merge( - ...this._tabs.map(tab => tab._disableChange), - ...this._tabs.map(tab => tab._labelChange)).subscribe(() => { - this._changeDetectorRef.markForCheck(); - }); + this._tabLabelSubscription = merge(...this._tabs.map(tab => tab._stateChanges)) + .subscribe(() => this._changeDetectorRef.markForCheck()); } /** Clamps the given index to the bounds of 0 and the tabs length. */ diff --git a/src/lib/tabs/tab.ts b/src/lib/tabs/tab.ts index 79501d1fa767..25f2ee225ffa 100644 --- a/src/lib/tabs/tab.ts +++ b/src/lib/tabs/tab.ts @@ -73,11 +73,8 @@ export class MatTab extends _MatTabMixinBase implements OnInit, CanDisable, OnCh return this._contentPortal; } - /** Emits whenever the label changes. */ - readonly _labelChange = new Subject(); - - /** Emits whenever the disable changes */ - readonly _disableChange = new Subject(); + /** Emits whenever the internal state of the tab changes. */ + readonly _stateChanges = new Subject(); /** * The relatively indexed position where 0 represents the center, negative is left, and positive @@ -101,18 +98,13 @@ export class MatTab extends _MatTabMixinBase implements OnInit, CanDisable, OnCh } ngOnChanges(changes: SimpleChanges): void { - if (changes.hasOwnProperty('textLabel')) { - this._labelChange.next(); - } - - if (changes.hasOwnProperty('disabled')) { - this._disableChange.next(); + if (changes.hasOwnProperty('textLabel') || changes.hasOwnProperty('disabled')) { + this._stateChanges.next(); } } ngOnDestroy(): void { - this._disableChange.complete(); - this._labelChange.complete(); + this._stateChanges.complete(); } ngOnInit(): void { From a19c60cacb19466d83c69c85011954e0c9974b5a Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 14 Aug 2018 01:40:37 +0200 Subject: [PATCH 089/189] test(tabs): fix test failure (#12656) Fixes a test in the tab group tests due to a long-standing PR being merged. --- src/lib/tabs/tab-group.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/tabs/tab-group.spec.ts b/src/lib/tabs/tab-group.spec.ts index dab3da381804..cf23ea453a16 100644 --- a/src/lib/tabs/tab-group.spec.ts +++ b/src/lib/tabs/tab-group.spec.ts @@ -448,7 +448,7 @@ describe('MatTabGroup', () => { expect(component._tabs.toArray()[0].isActive).toBe(true); }); - it('should be able to select a new tab after creation', () => { + it('should be able to select a new tab after creation', fakeAsync(() => { fixture.detectChanges(); const component: MatTabGroup = fixture.debugElement.query(By.css('mat-tab-group')).componentInstance; @@ -457,10 +457,11 @@ describe('MatTabGroup', () => { fixture.componentInstance.selectedIndex = 3; fixture.detectChanges(); + tick(); expect(component.selectedIndex).toBe(3); expect(component._tabs.toArray()[3].isActive).toBe(true); - }); + })); it('should not fire `selectedTabChange` when the amount of tabs changes', fakeAsync(() => { fixture.detectChanges(); From c7d34be0ec86c2ff5ec767d8a90077bba813380b Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 14 Aug 2018 01:41:31 +0200 Subject: [PATCH 090/189] feat(drag-drop): add move event (#12641) This is something that came up during a discussion in #8963. Adds the `cdkDragMoved` event which will emit as an item is being dragged. Also adds some extra precautions to make sure that we're not doing extra work unless the consumer opted into the event. --- src/cdk-experimental/drag-drop/drag-events.ts | 10 +++ src/cdk-experimental/drag-drop/drag.spec.ts | 53 +++++++++++++-- src/cdk-experimental/drag-drop/drag.ts | 67 ++++++++++++++++--- 3 files changed, 115 insertions(+), 15 deletions(-) diff --git a/src/cdk-experimental/drag-drop/drag-events.ts b/src/cdk-experimental/drag-drop/drag-events.ts index 7d71eb4ca38e..4ba0db09c4ff 100644 --- a/src/cdk-experimental/drag-drop/drag-events.ts +++ b/src/cdk-experimental/drag-drop/drag-events.ts @@ -55,3 +55,13 @@ export interface CdkDragDrop { /** Container from which the item was picked up. Can be the same as the `container`. */ previousContainer: CdkDropContainer; } + +/** Event emitted as the user is dragging a draggable item. */ +export interface CdkDragMove { + /** Item that is being dragged. */ + source: CdkDrag; + /** Position of the user's pointer on the page. */ + pointerPosition: {x: number, y: number}; + /** Native event that is causing the dragging. */ + event: MouseEvent | TouchEvent; +} diff --git a/src/cdk-experimental/drag-drop/drag.spec.ts b/src/cdk-experimental/drag-drop/drag.spec.ts index bee22282faaf..fadccc09d162 100644 --- a/src/cdk-experimental/drag-drop/drag.spec.ts +++ b/src/cdk-experimental/drag-drop/drag.spec.ts @@ -1,12 +1,13 @@ import { + AfterViewInit, Component, + ElementRef, + NgZone, + Provider, + QueryList, Type, ViewChild, - ElementRef, ViewChildren, - QueryList, - AfterViewInit, - Provider, ViewEncapsulation, } from '@angular/core'; import {TestBed, ComponentFixture, fakeAsync, flush, tick} from '@angular/core/testing'; @@ -201,6 +202,50 @@ describe('CdkDrag', () => { // go into an infinite loop trying to stringify the event, if the test fails. expect(event).toEqual({source: fixture.componentInstance.dragInstance}); })); + + it('should emit when the user is moving the drag element', () => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const spy = jasmine.createSpy('move spy'); + const subscription = fixture.componentInstance.dragInstance.moved.subscribe(spy); + + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 5, 10); + expect(spy).toHaveBeenCalledTimes(1); + + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 10, 20); + expect(spy).toHaveBeenCalledTimes(2); + + subscription.unsubscribe(); + }); + + it('should emit to `moved` inside the NgZone', () => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const spy = jasmine.createSpy('move spy'); + const subscription = fixture.componentInstance.dragInstance.moved + .subscribe(() => spy(NgZone.isInAngularZone())); + + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 10, 20); + expect(spy).toHaveBeenCalledWith(true); + + subscription.unsubscribe(); + }); + + it('should complete the `moved` stream on destroy', () => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const spy = jasmine.createSpy('move spy'); + const subscription = fixture.componentInstance.dragInstance.moved + .subscribe(undefined, undefined, spy); + + fixture.destroy(); + expect(spy).toHaveBeenCalled(); + subscription.unsubscribe(); + }); + }); describe('draggable with a handle', () => { diff --git a/src/cdk-experimental/drag-drop/drag.ts b/src/cdk-experimental/drag-drop/drag.ts index 0034438b3cb5..3ad3a0d648ee 100644 --- a/src/cdk-experimental/drag-drop/drag.ts +++ b/src/cdk-experimental/drag-drop/drag.ts @@ -27,12 +27,19 @@ import {DOCUMENT} from '@angular/common'; import {Directionality} from '@angular/cdk/bidi'; import {CdkDragHandle} from './drag-handle'; import {CdkDropContainer, CDK_DROP_CONTAINER} from './drop-container'; -import {CdkDragStart, CdkDragEnd, CdkDragExit, CdkDragEnter, CdkDragDrop} from './drag-events'; +import { + CdkDragStart, + CdkDragEnd, + CdkDragExit, + CdkDragEnter, + CdkDragDrop, + CdkDragMove, +} from './drag-events'; import {CdkDragPreview} from './drag-preview'; import {CdkDragPlaceholder} from './drag-placeholder'; import {ViewportRuler} from '@angular/cdk/overlay'; import {DragDropRegistry} from './drag-drop-registry'; -import {Subject, merge} from 'rxjs'; +import {Subject, merge, Observable} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; // TODO(crisbeto): add auto-scrolling functionality. @@ -97,6 +104,15 @@ export class CdkDrag implements OnDestroy { /** Cached scroll position on the page when the element was picked up. */ private _scrollPosition: {top: number, left: number}; + /** Emits when the item is being moved. */ + private _moveEvents = new Subject>(); + + /** + * Amount of subscriptions to the move event. Used to avoid + * hitting the zone if the consumer didn't subscribe to it. + */ + private _moveEventSubscriptions = 0; + /** Elements that can be used to drag the draggable item. */ @ContentChildren(CdkDragHandle) _handles: QueryList; @@ -129,6 +145,20 @@ export class CdkDrag implements OnDestroy { @Output('cdkDragDropped') dropped: EventEmitter> = new EventEmitter>(); + /** + * Emits as the user is dragging the item. Use with caution, + * because this event will fire for every pixel that the user has dragged. + */ + @Output('cdkDragMoved') moved: Observable> = Observable.create(observer => { + const subscription = this._moveEvents.subscribe(observer); + this._moveEventSubscriptions++; + + return () => { + subscription.unsubscribe(); + this._moveEventSubscriptions--; + }; + }); + constructor( /** Element that the draggable is attached to. */ public element: ElementRef, @@ -166,6 +196,7 @@ export class CdkDrag implements OnDestroy { this._nextSibling = null; this._dragDropRegistry.removeDragItem(this); + this._moveEvents.complete(); this._destroyed.next(); this._destroyed.complete(); } @@ -245,15 +276,31 @@ export class CdkDrag implements OnDestroy { this._hasMoved = true; event.preventDefault(); + const pointerPosition = this._getPointerPositionOnPage(event); + if (this.dropContainer) { - this._updateActiveDropContainer(event); + this._updateActiveDropContainer(pointerPosition); } else { const activeTransform = this._activeTransform; - const {x: pageX, y: pageY} = this._getPointerPositionOnPage(event); - activeTransform.x = pageX - this._pickupPositionOnPage.x + this._passiveTransform.x; - activeTransform.y = pageY - this._pickupPositionOnPage.y + this._passiveTransform.y; + activeTransform.x = + pointerPosition.x - this._pickupPositionOnPage.x + this._passiveTransform.x; + activeTransform.y = + pointerPosition.y - this._pickupPositionOnPage.y + this._passiveTransform.y; this._setTransform(this.element.nativeElement, activeTransform.x, activeTransform.y); } + + // Since this event gets fired for every pixel while dragging, we only + // want to fire it if the consumer opted into it. Also we have to + // re-enter the zone becaus we run all of the events on the outside. + if (this._moveEventSubscriptions > 0) { + this._ngZone.run(() => { + this._moveEvents.next({ + source: this, + pointerPosition, + event + }); + }); + } } /** Handler that is invoked when the user lifts their pointer up, after initiating a drag. */ @@ -314,19 +361,17 @@ export class CdkDrag implements OnDestroy { * Updates the item's position in its drop container, or moves it * into a new one, depending on its current drag position. */ - private _updateActiveDropContainer(event: MouseEvent | TouchEvent) { - const {x, y} = this._getPointerPositionOnPage(event); - + private _updateActiveDropContainer({x, y}: Point) { // Drop container that draggable has been moved into. const newContainer = this.dropContainer._getSiblingContainerFromPosition(x, y); if (newContainer) { this._ngZone.run(() => { // Notify the old container that the item has left. - this.exited.emit({ item: this, container: this.dropContainer }); + this.exited.emit({item: this, container: this.dropContainer}); this.dropContainer.exit(this); // Notify the new container that the item has entered. - this.entered.emit({ item: this, container: newContainer }); + this.entered.emit({item: this, container: newContainer}); this.dropContainer = newContainer; this.dropContainer.enter(this, x, y); }); From fb6cd06bb2db7b4b86b79039c0041e9df939632a Mon Sep 17 00:00:00 2001 From: ElAndyG <457356+elAndyG@users.noreply.github.com> Date: Mon, 13 Aug 2018 20:03:39 -0400 Subject: [PATCH 091/189] add ng add as an install alternative to npm/yarn (#12471) Letting users know that the `ng add` command will simplify the install process if they are not on the latest versions of Angular. I found that by doing the stock npm install, the @angular/cdk and @angular/animations versions did not match what was already installed causing conflicts in the dependencies. --- guides/getting-started.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/guides/getting-started.md b/guides/getting-started.md index 1e333cb0913e..e8d9f65ecdbd 100644 --- a/guides/getting-started.md +++ b/guides/getting-started.md @@ -17,7 +17,7 @@ yarn add @angular/material @angular/cdk @angular/animations ``` -#### Alternative: Snapshot Build +#### Alternative 1: Snapshot Build A snapshot build with the latest changes from master is also available. Note that this snapshot build should not be considered stable and may break between releases. @@ -31,6 +31,13 @@ npm install --save angular/material2-builds angular/cdk-builds angular/animation ```bash yarn add angular/material2-builds angular/cdk-builds angular/animations-builds ``` +#### Alternative 2: Angular Devkit 6+ + +Using the Angular CLI `ng add` command will update your Angular project with the correct dependencies, perform configuration changes and execute initialization code. + +```bash +ng add @angular/material +``` ### Step 2: Configure animations From 78ac07a41517badac968f2420b74b25b39c5cded Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 15 Aug 2018 00:52:57 +0200 Subject: [PATCH 092/189] feat(drag-drop): add the ability to lock dragging along an axis (#12604) Adds inputs that allow consumers to lock dragging of a particular drag item, or all items in a drag container, along an axis. --- src/cdk-experimental/drag-drop/drag.spec.ts | 92 +++++++++++++++++++ src/cdk-experimental/drag-drop/drag.ts | 25 ++++- .../drag-drop/drop-container.ts | 3 + src/cdk-experimental/drag-drop/drop.ts | 3 + src/demo-app/drag-drop/drag-drop-demo.html | 21 ++++- src/demo-app/drag-drop/drag-drop-demo.ts | 1 + 6 files changed, 137 insertions(+), 8 deletions(-) diff --git a/src/cdk-experimental/drag-drop/drag.spec.ts b/src/cdk-experimental/drag-drop/drag.spec.ts index fadccc09d162..a8cf6b3bc2e7 100644 --- a/src/cdk-experimental/drag-drop/drag.spec.ts +++ b/src/cdk-experimental/drag-drop/drag.spec.ts @@ -246,6 +246,38 @@ describe('CdkDrag', () => { subscription.unsubscribe(); }); + it('should be able to lock dragging along the x axis', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + fixture.componentInstance.dragInstance.lockAxis = 'x'; + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 0px, 0px)'); + + dragElementViaMouse(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(150px, 0px, 0px)'); + })); + + it('should be able to lock dragging along the y axis', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + fixture.componentInstance.dragInstance.lockAxis = 'y'; + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(0px, 100px, 0px)'); + + dragElementViaMouse(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(0px, 300px, 0px)'); + })); + }); describe('draggable with a handle', () => { @@ -690,6 +722,65 @@ describe('CdkDrag', () => { expect(preview.style.transform).toBe('translate3d(50px, 50px, 0px)'); })); + it('should lock position inside a drop container along the x axis', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZoneWithCustomPreview); + fixture.detectChanges(); + + const item = fixture.componentInstance.dragItems.toArray()[1]; + const element = item.element.nativeElement; + + item.lockAxis = 'x'; + + dispatchMouseEvent(element, 'mousedown', 50, 50); + fixture.detectChanges(); + + dispatchMouseEvent(element, 'mousemove', 100, 100); + fixture.detectChanges(); + + const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; + + expect(preview.style.transform).toBe('translate3d(100px, 50px, 0px)'); + })); + + it('should lock position inside a drop container along the y axis', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZoneWithCustomPreview); + fixture.detectChanges(); + + const item = fixture.componentInstance.dragItems.toArray()[1]; + const element = item.element.nativeElement; + + item.lockAxis = 'y'; + + dispatchMouseEvent(element, 'mousedown', 50, 50); + fixture.detectChanges(); + + dispatchMouseEvent(element, 'mousemove', 100, 100); + fixture.detectChanges(); + + const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; + + expect(preview.style.transform).toBe('translate3d(50px, 100px, 0px)'); + })); + + it('should inherit the position locking from the drop container', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZoneWithCustomPreview); + fixture.detectChanges(); + + const element = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; + + fixture.componentInstance.dropInstance.lockAxis = 'x'; + + dispatchMouseEvent(element, 'mousedown', 50, 50); + fixture.detectChanges(); + + dispatchMouseEvent(element, 'mousemove', 100, 100); + fixture.detectChanges(); + + const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; + + expect(preview.style.transform).toBe('translate3d(100px, 50px, 0px)'); + })); + it('should be able to customize the placeholder', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPlaceholder); fixture.detectChanges(); @@ -1103,6 +1194,7 @@ export class DraggableInHorizontalDropZone { ` }) export class DraggableInDropZoneWithCustomPreview { + @ViewChild(CdkDrop) dropInstance: CdkDrop; @ViewChildren(CdkDrag) dragItems: QueryList; items = ['Zero', 'One', 'Two', 'Three']; } diff --git a/src/cdk-experimental/drag-drop/drag.ts b/src/cdk-experimental/drag-drop/drag.ts index 3ad3a0d648ee..2c665b866fe4 100644 --- a/src/cdk-experimental/drag-drop/drag.ts +++ b/src/cdk-experimental/drag-drop/drag.ts @@ -119,14 +119,15 @@ export class CdkDrag implements OnDestroy { /** Element that will be used as a template to create the draggable item's preview. */ @ContentChild(CdkDragPreview) _previewTemplate: CdkDragPreview; - /** - * Template for placeholder element rendered to show where a draggable would be dropped. - */ + /** Template for placeholder element rendered to show where a draggable would be dropped. */ @ContentChild(CdkDragPlaceholder) _placeholderTemplate: CdkDragPlaceholder; /** Arbitrary data to attach to this drag instance. */ @Input() data: T; + /** Locks the position of the dragged element along the specified axis. */ + @Input('cdkDragLockAxis') lockAxis: 'x' | 'y'; + /** Emits when the user starts dragging the item. */ @Output('cdkDragStarted') started: EventEmitter = new EventEmitter(); @@ -276,7 +277,7 @@ export class CdkDrag implements OnDestroy { this._hasMoved = true; event.preventDefault(); - const pointerPosition = this._getPointerPositionOnPage(event); + const pointerPosition = this._getConstrainedPointerPosition(event); if (this.dropContainer) { this._updateActiveDropContainer(pointerPosition); @@ -361,7 +362,7 @@ export class CdkDrag implements OnDestroy { * Updates the item's position in its drop container, or moves it * into a new one, depending on its current drag position. */ - private _updateActiveDropContainer({x, y}: Point) { + private _updateActiveDropContainer({x, y}) { // Drop container that draggable has been moved into. const newContainer = this.dropContainer._getSiblingContainerFromPosition(x, y); @@ -531,6 +532,20 @@ export class CdkDrag implements OnDestroy { }; } + /** Gets the pointer position on the page, accounting for any position constraints. */ + private _getConstrainedPointerPosition(event: MouseEvent | TouchEvent): Point { + const point = this._getPointerPositionOnPage(event); + const dropContainerLock = this.dropContainer ? this.dropContainer.lockAxis : null; + + if (this.lockAxis === 'x' || dropContainerLock === 'x') { + point.y = this._pickupPositionOnPage.y; + } else if (this.lockAxis === 'y' || dropContainerLock === 'y') { + point.x = this._pickupPositionOnPage.x; + } + + return point; + } + /** Determines whether an event is a touch event. */ private _isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent { return event.type.startsWith('touch'); diff --git a/src/cdk-experimental/drag-drop/drop-container.ts b/src/cdk-experimental/drag-drop/drop-container.ts index 36bd6f49df51..7030d0684c29 100644 --- a/src/cdk-experimental/drag-drop/drop-container.ts +++ b/src/cdk-experimental/drag-drop/drop-container.ts @@ -19,6 +19,9 @@ export interface CdkDropContainer { /** Direction in which the list is oriented. */ orientation: 'horizontal' | 'vertical'; + /** Locks the position of the draggable elements inside the container along the specified axis. */ + lockAxis: 'x' | 'y'; + /** Starts dragging an item. */ start(): void; diff --git a/src/cdk-experimental/drag-drop/drop.ts b/src/cdk-experimental/drag-drop/drop.ts index e26003584af4..d8fcc2c80254 100644 --- a/src/cdk-experimental/drag-drop/drop.ts +++ b/src/cdk-experimental/drag-drop/drop.ts @@ -69,6 +69,9 @@ export class CdkDrop implements OnInit, OnDestroy { */ @Input() id: string = `cdk-drop-${_uniqueIdCounter++}`; + /** Locks the position of the draggable elements inside the container along the specified axis. */ + @Input() lockAxis: 'x' | 'y'; + /** Emits when the user drops an item inside the container. */ @Output() dropped: EventEmitter> = new EventEmitter>(); diff --git a/src/demo-app/drag-drop/drag-drop-demo.html b/src/demo-app/drag-drop/drag-drop-demo.html index 1ebf05dbd83f..a82df8bd50be 100644 --- a/src/demo-app/drag-drop/drag-drop-demo.html +++ b/src/demo-app/drag-drop/drag-drop-demo.html @@ -4,6 +4,7 @@

To do

@@ -18,6 +19,7 @@

Done

@@ -34,6 +36,7 @@

Horizontal list

{{item}} @@ -43,6 +46,11 @@

Horizontal list

+
+

Free dragging

+
Drag me around
+
+

Data

{{todo.join(', ')}}
@@ -50,7 +58,14 @@

Data

{{horizontalData.join(', ')}}
-
-

Free dragging

-
Drag me around
+
+

Axis locking

+ + Lock position along axis + + None + X axis + Y axis + +
diff --git a/src/demo-app/drag-drop/drag-drop-demo.ts b/src/demo-app/drag-drop/drag-drop-demo.ts index b72cedcf9304..0d08cf4aef82 100644 --- a/src/demo-app/drag-drop/drag-drop-demo.ts +++ b/src/demo-app/drag-drop/drag-drop-demo.ts @@ -19,6 +19,7 @@ import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk-expe encapsulation: ViewEncapsulation.None, }) export class DragAndDropDemo { + axisLock: 'x' | 'y'; todo = [ 'Come up with catchy start-up name', 'Add "blockchain" to name', From 948f655355896fb4740d2eb4b6ad626121f8a2d4 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 15 Aug 2018 00:54:47 +0200 Subject: [PATCH 093/189] fix(progress-bar): query state animation not working (#11459) Currently the animation for a progress bar in the `query` state is disabled by default due to the `_noop-animation` producing the wrong selector. It looks like using the ampersand inside interpolation (e.g. in `@at-root ._mat-animation-noopable#{&}`) doesn't work as expected once we have more than one selector. What ends up happening is that SASS interpolates the first selector, but then leaves the other one as it is, which causes it to disable the animation. Fixes #11453. --- src/lib/progress-bar/progress-bar.scss | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/lib/progress-bar/progress-bar.scss b/src/lib/progress-bar/progress-bar.scss index d91971807262..29362645d0d5 100644 --- a/src/lib/progress-bar/progress-bar.scss +++ b/src/lib/progress-bar/progress-bar.scss @@ -40,7 +40,6 @@ $mat-progress-bar-piece-animation-duration: 250ms !default; // The progress bar buffer is the bar indicator showing the buffer value and is only visible // beyond the current value of the primary progress bar. .mat-progress-bar-buffer { - @include _noop-animation(); transform-origin: top left; transition: transform $mat-progress-bar-piece-animation-duration ease; @@ -58,7 +57,6 @@ $mat-progress-bar-piece-animation-duration: 250ms !default; // The progress bar fill fills the progress bar with the indicator color. .mat-progress-bar-fill { - @include _noop-animation(); animation: none; transform-origin: top left; transition: transform $mat-progress-bar-piece-animation-duration ease; @@ -70,7 +68,6 @@ $mat-progress-bar-piece-animation-duration: 250ms !default; // A pseudo element is created for each progress bar bar that fills with the indicator color. .mat-progress-bar-fill::after { - @include _noop-animation(); animation: none; content: ''; display: inline-block; @@ -95,12 +92,10 @@ $mat-progress-bar-piece-animation-duration: 250ms !default; &[mode='indeterminate'], &[mode='query'] { .mat-progress-bar-fill { - @include _noop-animation(); transition: none; } .mat-progress-bar-primary { // Avoids stacked animation tearing in Firefox >= 57. - @include _noop-animation(); @include backface-visibility(hidden); animation: mat-progress-bar-primary-indeterminate-translate $mat-progress-bar-full-animation-duration infinite linear; @@ -108,14 +103,12 @@ $mat-progress-bar-piece-animation-duration: 250ms !default; } .mat-progress-bar-primary.mat-progress-bar-fill::after { // Avoids stacked animation tearing in Firefox >= 57. - @include _noop-animation(); @include backface-visibility(hidden); animation: mat-progress-bar-primary-indeterminate-scale $mat-progress-bar-full-animation-duration infinite linear; } .mat-progress-bar-secondary { // Avoids stacked animation tearing in Firefox >= 57. - @include _noop-animation(); @include backface-visibility(hidden); animation: mat-progress-bar-secondary-indeterminate-translate $mat-progress-bar-full-animation-duration infinite linear; @@ -124,7 +117,6 @@ $mat-progress-bar-piece-animation-duration: 250ms !default; } .mat-progress-bar-secondary.mat-progress-bar-fill::after { // Avoids stacked animation tearing in Firefox >= 57. - @include _noop-animation(); @include backface-visibility(hidden); animation: mat-progress-bar-secondary-indeterminate-scale $mat-progress-bar-full-animation-duration infinite linear; @@ -134,7 +126,6 @@ $mat-progress-bar-piece-animation-duration: 250ms !default; &[mode='buffer'] { .mat-progress-bar-background { // Avoids stacked animation tearing in Firefox >= 57. - @include _noop-animation(); @include backface-visibility(hidden); animation: mat-progress-bar-background-scroll $mat-progress-bar-piece-animation-duration infinite linear; @@ -144,6 +135,21 @@ $mat-progress-bar-piece-animation-duration: 250ms !default; display: block; } } + + // Disabled animations handling. + &._mat-animation-noopable { + .mat-progress-bar-fill, + .mat-progress-bar-fill::after, + .mat-progress-bar-buffer, + .mat-progress-bar-primary, + .mat-progress-bar-primary.mat-progress-bar-fill::after, + .mat-progress-bar-secondary, + .mat-progress-bar-secondary.mat-progress-bar-fill::after, + .mat-progress-bar-background { + animation: none; + transition: none; + } + } } From 7d8c59fc849b222fc0e81ef54ef60ccb9432ade8 Mon Sep 17 00:00:00 2001 From: Erick Xavier <648239+ErickXavier@users.noreply.github.com> Date: Tue, 14 Aug 2018 21:55:15 -0300 Subject: [PATCH 094/189] Fixes MAT_MOMENT_DATE_ADAPTER_OPTIONS (#12679) Fixes MAT_MOMENT_DATE_ADAPTER_OPTIONS reference on documentation --- src/lib/datepicker/datepicker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/datepicker/datepicker.md b/src/lib/datepicker/datepicker.md index 51faa38ee845..12b9666a2caf 100644 --- a/src/lib/datepicker/datepicker.md +++ b/src/lib/datepicker/datepicker.md @@ -246,7 +246,7 @@ export class MyComponent { -By default the `MomentDateAdapter` will creates dates in your time zone specific locale. You can change the default behaviour to parse dates as UTC by providing the `MAT_MOMENT_DATA_ADAPTER_OPTIONS` and setting it to `useUtc: true`. +By default the `MomentDateAdapter` will creates dates in your time zone specific locale. You can change the default behaviour to parse dates as UTC by providing the `MAT_MOMENT_DATE_ADAPTER_OPTIONS` and setting it to `useUtc: true`. ```ts @NgModule({ From ffeb77912004409379e69f49e90904107a3c6234 Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Wed, 15 Aug 2018 14:23:02 -0700 Subject: [PATCH 095/189] fix(form-field): update label gap for outline style (#12555) --- src/lib/form-field/BUILD.bazel | 1 + src/lib/form-field/form-field-module.ts | 6 ++- src/lib/form-field/form-field.html | 9 ++-- src/lib/form-field/form-field.ts | 69 ++++++++++++------------- 4 files changed, 45 insertions(+), 40 deletions(-) diff --git a/src/lib/form-field/BUILD.bazel b/src/lib/form-field/BUILD.bazel index 406114b5ca4f..6280ca4f1b7e 100644 --- a/src/lib/form-field/BUILD.bazel +++ b/src/lib/form-field/BUILD.bazel @@ -18,6 +18,7 @@ ng_module( deps = [ "//src/lib/core", "//src/cdk/coercion", + "//src/cdk/observers", "//src/cdk/platform", ], tsconfig = "//src/lib:tsconfig-build.json", diff --git a/src/lib/form-field/form-field-module.ts b/src/lib/form-field/form-field-module.ts index 838eb6815fab..6d112db0392c 100644 --- a/src/lib/form-field/form-field-module.ts +++ b/src/lib/form-field/form-field-module.ts @@ -8,6 +8,7 @@ import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; +import {ObserversModule} from '@angular/cdk/observers'; import {MatError} from './error'; import {MatFormField} from './form-field'; import {MatHint} from './hint'; @@ -27,7 +28,10 @@ import {MatSuffix} from './suffix'; MatPrefix, MatSuffix, ], - imports: [CommonModule], + imports: [ + CommonModule, + ObserversModule, + ], exports: [ MatError, MatFormField, diff --git a/src/lib/form-field/form-field.html b/src/lib/form-field/form-field.html index 3e871bfaf4eb..8e4b28253ae6 100644 --- a/src/lib/form-field/form-field.html +++ b/src/lib/form-field/form-field.html @@ -5,13 +5,13 @@
-
-
+
+
-
-
+
+
@@ -27,6 +27,7 @@

- - + + Section 1

This is the content text that makes sense here.

- + Section 2

This is the content text that makes sense here.

- + Section 3 Reveal Buttons Below diff --git a/src/lib/expansion/accordion.spec.ts b/src/lib/expansion/accordion.spec.ts index f69022c1444d..ebb1b89143c1 100644 --- a/src/lib/expansion/accordion.spec.ts +++ b/src/lib/expansion/accordion.spec.ts @@ -13,6 +13,7 @@ describe('MatAccordion', () => { MatExpansionModule ], declarations: [ + AccordionWithHideToggle, NestedPanel, SetOfItems, ], @@ -93,6 +94,22 @@ describe('MatAccordion', () => { expect(innerPanel.accordion).not.toBe(outerPanel.accordion); }); + + it('should update the expansion panel if hideToggle changed', () => { + const fixture = TestBed.createComponent(AccordionWithHideToggle); + const panel = fixture.debugElement.query(By.directive(MatExpansionPanel)); + + fixture.detectChanges(); + + expect(panel.nativeElement.querySelector('.mat-expansion-indicator')) + .toBeTruthy('Expected the expansion indicator to be present.'); + + fixture.componentInstance.hideToggle = true; + fixture.detectChanges(); + + expect(panel.nativeElement.querySelector('.mat-expansion-indicator')) + .toBeFalsy('Expected the expansion indicator to be removed.'); + }); }); @@ -130,3 +147,15 @@ class NestedPanel { @ViewChild('outerPanel') outerPanel: MatExpansionPanel; @ViewChild('innerPanel') innerPanel: MatExpansionPanel; } + +@Component({template: ` + + + Header +

Content

+
+
` +}) +class AccordionWithHideToggle { + hideToggle = false; +} diff --git a/src/lib/expansion/accordion.ts b/src/lib/expansion/accordion.ts index 8f6cf19f9ea5..b366279e7bbc 100644 --- a/src/lib/expansion/accordion.ts +++ b/src/lib/expansion/accordion.ts @@ -19,6 +19,7 @@ export type MatAccordionDisplayMode = 'default' | 'flat'; @Directive({ selector: 'mat-accordion', exportAs: 'matAccordion', + inputs: ['multi'], host: { class: 'mat-accordion' } diff --git a/src/lib/expansion/expansion-panel-header.ts b/src/lib/expansion/expansion-panel-header.ts index 956a1e08c31f..c871cb83d9c6 100644 --- a/src/lib/expansion/expansion-panel-header.ts +++ b/src/lib/expansion/expansion-panel-header.ts @@ -19,7 +19,7 @@ import { OnDestroy, ViewEncapsulation, } from '@angular/core'; -import {merge, Subscription} from 'rxjs'; +import {merge, Subscription, EMPTY} from 'rxjs'; import {filter} from 'rxjs/operators'; import {matExpansionAnimations} from './expansion-animations'; import {MatExpansionPanel} from './expansion-panel'; @@ -65,16 +65,20 @@ export class MatExpansionPanelHeader implements OnDestroy { private _parentChangeSubscription = Subscription.EMPTY; constructor( - @Host() public panel: MatExpansionPanel, - private _element: ElementRef, - private _focusMonitor: FocusMonitor, - private _changeDetectorRef: ChangeDetectorRef) { + @Host() public panel: MatExpansionPanel, + private _element: ElementRef, + private _focusMonitor: FocusMonitor, + private _changeDetectorRef: ChangeDetectorRef) { + + const accordionHideToggleChange = panel.accordion ? + panel.accordion._stateChanges.pipe(filter(changes => !!changes.hideToggle)) : EMPTY; // Since the toggle state depends on an @Input on the panel, we - // need to subscribe and trigger change detection manually. + // need to subscribe and trigger change detection manually. this._parentChangeSubscription = merge( panel.opened, panel.closed, + accordionHideToggleChange, panel._inputChanges.pipe(filter(changes => !!(changes.hideToggle || changes.disabled))) ) .subscribe(() => this._changeDetectorRef.markForCheck()); diff --git a/src/lib/expansion/expansion-panel.ts b/src/lib/expansion/expansion-panel.ts index d805b130d067..f93c35ee3b4f 100644 --- a/src/lib/expansion/expansion-panel.ts +++ b/src/lib/expansion/expansion-panel.ts @@ -74,7 +74,9 @@ export class MatExpansionPanel extends _CdkAccordionItem implements AfterContentInit, OnChanges, OnDestroy { /** Whether the toggle indicator should be hidden. */ @Input() - get hideToggle(): boolean { return this._hideToggle; } + get hideToggle(): boolean { + return this._hideToggle || (this.accordion && this.accordion.hideToggle); + } set hideToggle(value: boolean) { this._hideToggle = coerceBooleanProperty(value); } @@ -103,17 +105,12 @@ export class MatExpansionPanel extends _CdkAccordionItem this.accordion = accordion; } - /** Whether the expansion indicator should be hidden. */ - _getHideToggle(): boolean { - if (this.accordion) { - return this.accordion.hideToggle; - } - return this.hideToggle; - } - /** Determines whether the expansion panel should have spacing between it and its siblings. */ _hasSpacing(): boolean { if (this.accordion) { + // We don't need to subscribe to the `stateChanges` of the parent accordion because each time + // the [displayMode] input changes, the change detection will also cover the host bindings + // of this expansion panel. return (this.expanded ? this.accordion.displayMode : this._getExpandedState()) === 'default'; } return false; From 3ce4e8d6a274a5858f505add2b1f5347a0554af6 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Tue, 21 Aug 2018 20:41:06 +0200 Subject: [PATCH 109/189] fix(slide-toggle): remove webkit tap highlight (#12708) * Since the slide-toggle already has the ripples as feedback indicator, the webkit tap highlight can be removed to make the slide-toggle behave more like a native (Android) switch component on touch devices. --- src/lib/slide-toggle/slide-toggle.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/slide-toggle/slide-toggle.scss b/src/lib/slide-toggle/slide-toggle.scss index 6980ba5e232c..6c66257c0886 100644 --- a/src/lib/slide-toggle/slide-toggle.scss +++ b/src/lib/slide-toggle/slide-toggle.scss @@ -21,14 +21,14 @@ $mat-slide-toggle-bar-track-width: $mat-slide-toggle-bar-width - $mat-slide-togg max-width: 100%; line-height: $mat-slide-toggle-height; - white-space: nowrap; + outline: none; // Disable user selection to ensure that dragging is smooth without grabbing // some elements accidentally. @include user-select(none); - outline: none; + -webkit-tap-highlight-color: transparent; &.mat-checked { .mat-slide-toggle-thumb-container { From f732059f4c3dbf79ffb80d574184a67676276761 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 21 Aug 2018 20:44:33 +0200 Subject: [PATCH 110/189] fix(menu): throw better error when trying to open undefined menu (#12688) Throws a better error than "Unable to read property templateRef of undefined" when clicking on a trigger with an undefined menu. We have a check for this already in one of the lifecycle hooks, but people can skip it when unit testing. Fixes #12649. --- src/lib/menu/menu-trigger.ts | 2 ++ src/lib/menu/menu.spec.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/lib/menu/menu-trigger.ts b/src/lib/menu/menu-trigger.ts index 8e00ee787e18..8cc5966b6234 100644 --- a/src/lib/menu/menu-trigger.ts +++ b/src/lib/menu/menu-trigger.ts @@ -195,6 +195,8 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy { return; } + this._checkMenu(); + const overlayRef = this._createOverlay(); this._setPosition(overlayRef.getConfig().positionStrategy as FlexibleConnectedPositionStrategy); overlayRef.attach(this._portal); diff --git a/src/lib/menu/menu.spec.ts b/src/lib/menu/menu.spec.ts index c3a10a41e981..f2daf7b6ced3 100644 --- a/src/lib/menu/menu.spec.ts +++ b/src/lib/menu/menu.spec.ts @@ -427,6 +427,19 @@ describe('MatMenu', () => { expect(triggerEl.hasAttribute('aria-expanded')).toBe(false); }); + it('should throw the correct error if the menu is not defined after init', () => { + const fixture = createComponent(SimpleMenu, [], [FakeIcon]); + fixture.detectChanges(); + + fixture.componentInstance.trigger.menu = null!; + fixture.detectChanges(); + + expect(() => { + fixture.componentInstance.trigger.openMenu(); + fixture.detectChanges(); + }).toThrowError(/must pass in an mat-menu instance/); + }); + describe('lazy rendering', () => { it('should be able to render the menu content lazily', fakeAsync(() => { const fixture = createComponent(SimpleLazyMenu); From 70cb0a24ece08febb3cb41ac070133f56a80ccb5 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 21 Aug 2018 20:46:37 +0200 Subject: [PATCH 111/189] fix(list): improved image scaling in avatar (#12660) Improves the scaling for list avatar images that aren't exact squares. Fixes #8131. --- src/lib/list/list.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/list/list.scss b/src/lib/list/list.scss index 030f6fcd6de9..302092296cad 100644 --- a/src/lib/list/list.scss +++ b/src/lib/list/list.scss @@ -143,6 +143,10 @@ $mat-list-item-inset-divider-offset: 72px; height: $avatar-size; border-radius: 50%; + // Not supported in IE11, but we're using this as a + // progressive enhancement to get better image scaling. + object-fit: cover; + ~ .mat-divider-inset { @include mat-inset-divider-offset($avatar-size, $mat-list-side-padding); } From 15e7f741911c3f13f9ac26525ceb1cc8e676556b Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 21 Aug 2018 20:47:17 +0200 Subject: [PATCH 112/189] fix(chips): incorrectly handling disabled state (#12659) * Fixes the form field displaying as disabled, but the user still being able to interact with the chip list. * Fixes the chip list still being focusable while it is disabled. * Fixes the individual chips not being disabled when the list is disabled. * Fixes the chip input not being disabled when the list is disabled. Fixes #11089. --- src/lib/chips/chip-input.spec.ts | 15 +++++++++++++- src/lib/chips/chip-input.ts | 7 +++++++ src/lib/chips/chip-list.spec.ts | 35 ++++++++++++++++++++++++++++++++ src/lib/chips/chip-list.ts | 15 ++++++++++++-- 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/lib/chips/chip-input.spec.ts b/src/lib/chips/chip-input.spec.ts index 35af7f394d31..3f6afd023220 100644 --- a/src/lib/chips/chip-input.spec.ts +++ b/src/lib/chips/chip-input.spec.ts @@ -2,7 +2,7 @@ import {Directionality} from '@angular/cdk/bidi'; import {ENTER, COMMA} from '@angular/cdk/keycodes'; import {PlatformModule} from '@angular/cdk/platform'; import {createKeyboardEvent} from '@angular/cdk/testing'; -import {Component, DebugElement} from '@angular/core'; +import {Component, DebugElement, ViewChild} from '@angular/core'; import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; @@ -10,6 +10,7 @@ import {MatFormFieldModule} from '@angular/material/form-field'; import {MatChipInput, MatChipInputEvent} from './chip-input'; import {MatChipsModule} from './index'; import {MAT_CHIPS_DEFAULT_OPTIONS, MatChipsDefaultOptions} from './chip-default-options'; +import {MatChipList} from './chip-list'; describe('MatChipInput', () => { @@ -82,6 +83,17 @@ describe('MatChipInput', () => { expect(label.textContent).toContain('or don\'t'); }); + it('should become disabled if the chip list is disabled', () => { + expect(inputNativeElement.hasAttribute('disabled')).toBe(false); + expect(chipInputDirective.disabled).toBe(false); + + fixture.componentInstance.chipListInstance.disabled = true; + fixture.detectChanges(); + + expect(inputNativeElement.getAttribute('disabled')).toBe('true'); + expect(chipInputDirective.disabled).toBe(true); + }); + }); describe('[addOnBlur]', () => { @@ -175,6 +187,7 @@ describe('MatChipInput', () => { ` }) class TestChipInput { + @ViewChild(MatChipList) chipListInstance: MatChipList; addOnBlur: boolean = false; placeholder = ''; diff --git a/src/lib/chips/chip-input.ts b/src/lib/chips/chip-input.ts index 50570fc4b091..152acaeeb33f 100644 --- a/src/lib/chips/chip-input.ts +++ b/src/lib/chips/chip-input.ts @@ -38,6 +38,7 @@ let nextUniqueId = 0; '(focus)': '_focus()', '(input)': '_onInput()', '[id]': 'id', + '[attr.disabled]': 'disabled || null', '[attr.placeholder]': 'placeholder || null', } }) @@ -82,6 +83,12 @@ export class MatChipInput implements OnChanges { /** Unique id for the input. */ @Input() id: string = `mat-chip-list-input-${nextUniqueId++}`; + /** Whether the input is disabled. */ + @Input() + get disabled(): boolean { return this._disabled || (this._chipList && this._chipList.disabled); } + set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value); } + private _disabled: boolean = false; + /** Whether the input is empty. */ get empty(): boolean { return !this._inputElement.value; } diff --git a/src/lib/chips/chip-list.spec.ts b/src/lib/chips/chip-list.spec.ts index 32dade78b090..4343f25da432 100644 --- a/src/lib/chips/chip-list.spec.ts +++ b/src/lib/chips/chip-list.spec.ts @@ -50,6 +50,20 @@ describe('MatChipList', () => { expect(chipsValid).toBe(true); }); + + it('should toggle the chips disabled state based on whether it is disabled', () => { + expect(chips.toArray().every(chip => chip.disabled)).toBe(false); + + chipListInstance.disabled = true; + fixture.detectChanges(); + + expect(chips.toArray().every(chip => chip.disabled)).toBe(true); + + chipListInstance.disabled = false; + fixture.detectChanges(); + + expect(chips.toArray().every(chip => chip.disabled)).toBe(false); + }); }); describe('with selected chips', () => { @@ -114,6 +128,27 @@ describe('MatChipList', () => { expect(manager.activeItemIndex).toBe(lastIndex); }); + it('should be able to become focused when disabled', () => { + expect(chipListInstance.focused).toBe(false, 'Expected list to not be focused.'); + + chipListInstance.disabled = true; + fixture.detectChanges(); + + chipListInstance.focus(); + fixture.detectChanges(); + + expect(chipListInstance.focused).toBe(false, 'Expected list to continue not to be focused'); + }); + + it('should remove the tabindex from the list if it is disabled', () => { + expect(chipListNativeElement.getAttribute('tabindex')).toBeTruthy(); + + chipListInstance.disabled = true; + fixture.detectChanges(); + + expect(chipListNativeElement.hasAttribute('tabindex')).toBeFalsy(); + }); + describe('on chip destroy', () => { it('should focus the next item', () => { let array = chips.toArray(); diff --git a/src/lib/chips/chip-list.ts b/src/lib/chips/chip-list.ts index caf8f90b0d07..8f418e7a20cf 100644 --- a/src/lib/chips/chip-list.ts +++ b/src/lib/chips/chip-list.ts @@ -72,7 +72,7 @@ export class MatChipListChange { template: `
`, exportAs: 'matChipList', host: { - '[attr.tabindex]': '_tabIndex', + '[attr.tabindex]': 'disabled ? null : _tabIndex', '[attr.aria-describedby]': '_ariaDescribedby || null', '[attr.aria-required]': 'required.toString()', '[attr.aria-disabled]': 'disabled.toString()', @@ -261,7 +261,13 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo */ @Input() get disabled(): boolean { return this.ngControl ? !!this.ngControl.disabled : this._disabled; } - set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value); } + set disabled(value: boolean) { + this._disabled = coerceBooleanProperty(value); + + if (this.chips) { + this.chips.forEach(chip => chip.disabled = this._disabled); + } + } protected _disabled: boolean = false; /** Orientation of the chip list. */ @@ -275,6 +281,7 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo get selectable(): boolean { return this._selectable; } set selectable(value: boolean) { this._selectable = coerceBooleanProperty(value); + if (this.chips) { this.chips.forEach(chip => chip.chipListSelectable = this._selectable); } @@ -441,6 +448,10 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo * are no eligible chips. */ focus(): void { + if (this.disabled) { + return; + } + // TODO: ARIA says this should focus the first `selected` chip if any are selected. // Focus on first element if there's no chipInput inside chip-list if (this._chipInput && this._chipInput.focused) { From 34d91c7f97139df4674b1d7ad962dc7495e214df Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Tue, 21 Aug 2018 20:48:04 +0200 Subject: [PATCH 113/189] fix(form-field): legacy ripple underline jumps in edge (#12648) The ripple underline jumps in Microsoft Edge inside legacy form-fields. Fixes #6351 --- src/lib/form-field/form-field-legacy.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/form-field/form-field-legacy.scss b/src/lib/form-field/form-field-legacy.scss index 77344f0999f6..2cb27a968329 100644 --- a/src/lib/form-field/form-field-legacy.scss +++ b/src/lib/form-field/form-field-legacy.scss @@ -49,6 +49,10 @@ $mat-form-field-legacy-underline-height: 1px !default; top: 0; height: $height; + // In some browsers like Microsoft Edge, the `scaleX` transform causes overflow that exceeds + // the desired form-field ripple height. See: angular/material2#6351 + overflow: hidden; + @include cdk-high-contrast { height: 0; border-top: solid $height; From 13f1c6eaece0448d6352c73638252aa50dc2c3a5 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 21 Aug 2018 20:48:32 +0200 Subject: [PATCH 114/189] fix(table): extra elements throwing off table alignment (#12645) Fixes extra decorative elements like badges or ripples throwing off the alignment in tables. Fixes #11165. --- src/lib/table/table.scss | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/lib/table/table.scss b/src/lib/table/table.scss index 150514aa96e3..d0c83feda3b9 100644 --- a/src/lib/table/table.scss +++ b/src/lib/table/table.scss @@ -37,7 +37,9 @@ mat-row, mat-header-row, mat-footer-row { } } -mat-cell:first-child, mat-header-cell:first-child, mat-footer-cell:first-child { +// Note: we use `first-of-type`/`last-of-type` here in order to prevent extra +// elements like ripples or badges from throwing off the layout (see #11165). +mat-cell:first-of-type, mat-header-cell:first-of-type, mat-footer-cell:first-of-type { padding-left: $mat-row-horizontal-padding; [dir='rtl'] & { @@ -46,7 +48,7 @@ mat-cell:first-child, mat-header-cell:first-child, mat-footer-cell:first-child { } } -mat-cell:last-child, mat-header-cell:last-child, mat-footer-cell:last-child { +mat-cell:last-of-type, mat-header-cell:last-of-type, mat-footer-cell:last-of-type { padding-right: $mat-row-horizontal-padding; [dir='rtl'] & { @@ -89,10 +91,12 @@ th.mat-header-cell, td.mat-cell, td.mat-footer-cell { border-bottom-style: solid; } -th.mat-header-cell:first-child, td.mat-cell:first-child, td.mat-footer-cell:first-child { +// Note: we use `first-of-type`/`last-of-type` here in order to prevent extra +// elements like ripples or badges from throwing off the layout (see #11165). +th.mat-header-cell:first-of-type, td.mat-cell:first-of-type, td.mat-footer-cell:first-of-type { padding-left: $mat-row-horizontal-padding; } -th.mat-header-cell:last-child, td.mat-cell:last-child, td.mat-footer-cell:last-child { +th.mat-header-cell:last-of-type, td.mat-cell:last-of-type, td.mat-footer-cell:last-of-type { padding-right: $mat-row-horizontal-padding; } From 70aca02600e3c894e32e37fb7b9d88db31d9e992 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 21 Aug 2018 20:49:27 +0200 Subject: [PATCH 115/189] fix(button-toggle): clickable area not stretching when custom width is set (#12642) Fixes the button toggle's clickable area not stretching, if the element is stretched beyond its initial width. Fixes #8432. --- src/lib/button-toggle/button-toggle.scss | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/lib/button-toggle/button-toggle.scss b/src/lib/button-toggle/button-toggle.scss index 0d9623b3cc0b..78208cc332f8 100644 --- a/src/lib/button-toggle/button-toggle.scss +++ b/src/lib/button-toggle/button-toggle.scss @@ -35,10 +35,6 @@ $mat-button-toggle-border-radius: 2px !default; } } -.mat-button-toggle-disabled .mat-button-toggle-label-content { - cursor: default; -} - .mat-button-toggle { white-space: nowrap; position: relative; @@ -65,7 +61,6 @@ $mat-button-toggle-border-radius: 2px !default; display: inline-block; line-height: $mat-button-toggle-height; padding: $mat-button-toggle-padding; - cursor: pointer; } .mat-button-toggle-label-content > * { @@ -110,4 +105,10 @@ $mat-button-toggle-border-radius: 2px !default; margin: 0; font: inherit; outline: none; + width: 100%; // Stretch the button in case the consumer set a custom width. + cursor: pointer; + + .mat-button-toggle-disabled & { + cursor: default; + } } From a4a79eaf7665296c2f26edf14588b9f7f799efda Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 21 Aug 2018 20:50:33 +0200 Subject: [PATCH 116/189] fix(slider): thumb label blending in with background in high contrast mode (#12606) Fixes the slider's thumb label not having an outline and blending in with the background in high contrast mode. --- src/lib/slider/slider.scss | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/lib/slider/slider.scss b/src/lib/slider/slider.scss index 0806b9e3a313..b49324917ba7 100644 --- a/src/lib/slider/slider.scss +++ b/src/lib/slider/slider.scss @@ -142,6 +142,10 @@ $mat-slider-focus-ring-size: 30px !default; transition: transform $swift-ease-out-duration $swift-ease-out-timing-function, border-radius $swift-ease-out-duration $swift-ease-out-timing-function, background-color $swift-ease-out-duration $swift-ease-out-timing-function; + + @include cdk-high-contrast { + outline: solid 1px; + } } .mat-slider-thumb-label-text { @@ -339,6 +343,13 @@ $mat-slider-focus-ring-size: 30px !default; .mat-slider-thumb-label { transform: rotate(45deg); } + + @include cdk-high-contrast { + .mat-slider-thumb-label, + .mat-slider-thumb-label-text { + transform: none; + } + } } } From 6a889f3945627204bc3a579162c50c883e25754b Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 22 Aug 2018 16:27:28 +0200 Subject: [PATCH 117/189] refactor(text-field): allow ElementRef to be passed to autofill monitor (#12783) Similarly to the other observers, allows for an `ElementRef` to be passed to the `AutofillMonitor` which is more convenient since most of the time we deal with `ElementRef`-s anyway. --- src/cdk/text-field/autofill.spec.ts | 4 +-- src/cdk/text-field/autofill.ts | 27 ++++++++++++++++--- src/lib/input/input.ts | 4 +-- .../text-field-autofill-monitor-example.ts | 8 +++--- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/cdk/text-field/autofill.spec.ts b/src/cdk/text-field/autofill.spec.ts index 624f8cca87d0..5f0e4d85571e 100644 --- a/src/cdk/text-field/autofill.spec.ts +++ b/src/cdk/text-field/autofill.spec.ts @@ -198,13 +198,13 @@ describe('cdkAutofill', () => { })); it('should monitor host element on init', () => { - expect(autofillMonitor.monitor).toHaveBeenCalledWith(testComponent.input.nativeElement); + expect(autofillMonitor.monitor).toHaveBeenCalledWith(testComponent.input); }); it('should stop monitoring host element on destroy', () => { expect(autofillMonitor.stopMonitoring).not.toHaveBeenCalled(); fixture.destroy(); - expect(autofillMonitor.stopMonitoring).toHaveBeenCalledWith(testComponent.input.nativeElement); + expect(autofillMonitor.stopMonitoring).toHaveBeenCalledWith(testComponent.input); }); }); diff --git a/src/cdk/text-field/autofill.ts b/src/cdk/text-field/autofill.ts index 814bcc2feb62..626204631c44 100644 --- a/src/cdk/text-field/autofill.ts +++ b/src/cdk/text-field/autofill.ts @@ -56,11 +56,21 @@ export class AutofillMonitor implements OnDestroy { * @param element The element to monitor. * @return A stream of autofill state changes. */ - monitor(element: Element): Observable { + monitor(element: Element): Observable; + + /** + * Monitor for changes in the autofill state of the given input element. + * @param element The element to monitor. + * @return A stream of autofill state changes. + */ + monitor(element: ElementRef): Observable; + + monitor(elementOrRef: Element | ElementRef): Observable { if (!this._platform.isBrowser) { return EMPTY; } + const element = elementOrRef instanceof ElementRef ? elementOrRef.nativeElement : elementOrRef; const info = this._monitoredElements.get(element); if (info) { @@ -103,7 +113,16 @@ export class AutofillMonitor implements OnDestroy { * Stop monitoring the autofill state of the given input element. * @param element The element to stop monitoring. */ - stopMonitoring(element: Element) { + stopMonitoring(element: Element); + + /** + * Stop monitoring the autofill state of the given input element. + * @param element The element to stop monitoring. + */ + stopMonitoring(element: ElementRef); + + stopMonitoring(elementOrRef: Element | ElementRef) { + const element = elementOrRef instanceof ElementRef ? elementOrRef.nativeElement : elementOrRef; const info = this._monitoredElements.get(element); if (info) { @@ -133,11 +152,11 @@ export class CdkAutofill implements OnDestroy, OnInit { ngOnInit() { this._autofillMonitor - .monitor(this._elementRef.nativeElement) + .monitor(this._elementRef) .subscribe(event => this.cdkAutofill.emit(event)); } ngOnDestroy() { - this._autofillMonitor.stopMonitoring(this._elementRef.nativeElement); + this._autofillMonitor.stopMonitoring(this._elementRef); } } diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts index f55cd48687a0..a42517c45bea 100644 --- a/src/lib/input/input.ts +++ b/src/lib/input/input.ts @@ -254,7 +254,7 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl< } ngOnInit() { - this._autofillMonitor.monitor(this._elementRef.nativeElement).subscribe(event => { + this._autofillMonitor.monitor(this._elementRef).subscribe(event => { this.autofilled = event.isAutofilled; this.stateChanges.next(); }); @@ -266,7 +266,7 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl< ngOnDestroy() { this.stateChanges.complete(); - this._autofillMonitor.stopMonitoring(this._elementRef.nativeElement); + this._autofillMonitor.stopMonitoring(this._elementRef); } ngDoCheck() { diff --git a/src/material-examples/text-field-autofill-monitor/text-field-autofill-monitor-example.ts b/src/material-examples/text-field-autofill-monitor/text-field-autofill-monitor-example.ts index 1dd32fd581a4..a8c640cc0fa9 100644 --- a/src/material-examples/text-field-autofill-monitor/text-field-autofill-monitor-example.ts +++ b/src/material-examples/text-field-autofill-monitor/text-field-autofill-monitor-example.ts @@ -16,14 +16,14 @@ export class TextFieldAutofillMonitorExample implements OnDestroy, OnInit { constructor(private autofill: AutofillMonitor) {} ngOnInit() { - this.autofill.monitor(this.firstName.nativeElement) + this.autofill.monitor(this.firstName) .subscribe(e => this.firstNameAutofilled = e.isAutofilled); - this.autofill.monitor(this.lastName.nativeElement) + this.autofill.monitor(this.lastName) .subscribe(e => this.lastNameAutofilled = e.isAutofilled); } ngOnDestroy() { - this.autofill.stopMonitoring(this.firstName.nativeElement); - this.autofill.stopMonitoring(this.lastName.nativeElement); + this.autofill.stopMonitoring(this.firstName); + this.autofill.stopMonitoring(this.lastName); } } From d349b9ee81ba6b12ad6103c07cb05f6b2dabe73c Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 22 Aug 2018 16:27:46 +0200 Subject: [PATCH 118/189] refactor(grid-list): remove unnecessary coercion functions (#12781) Removes a couple of functions that either duplicate functionality from `cdk/coercion` or are so simple that they can be inlined instead. This change is non-breaking, because the functions were never exported through the public API. --- src/lib/grid-list/BUILD.bazel | 1 + src/lib/grid-list/grid-list-measure.ts | 23 ----------------------- src/lib/grid-list/grid-list.ts | 11 ++++------- src/lib/grid-list/grid-tile.ts | 6 +++--- 4 files changed, 8 insertions(+), 33 deletions(-) delete mode 100644 src/lib/grid-list/grid-list-measure.ts diff --git a/src/lib/grid-list/BUILD.bazel b/src/lib/grid-list/BUILD.bazel index a88f611d22b3..d4b2ac756fc1 100644 --- a/src/lib/grid-list/BUILD.bazel +++ b/src/lib/grid-list/BUILD.bazel @@ -11,6 +11,7 @@ ng_module( deps = [ "//src/lib/core", "//src/cdk/bidi", + "//src/cdk/coercion", ], tsconfig = "//src/lib:tsconfig-build.json", ) diff --git a/src/lib/grid-list/grid-list-measure.ts b/src/lib/grid-list/grid-list-measure.ts deleted file mode 100644 index 56b8f8f98611..000000000000 --- a/src/lib/grid-list/grid-list-measure.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @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 - */ - -/** - * Converts values into strings. Falsy values become empty strings. - * @docs-private - */ -export function coerceToString(value: string | number): string { - return `${value || ''}`; -} - -/** - * Converts a value that might be a string into a number. - * @docs-private - */ -export function coerceToNumber(value: string | number): number { - return typeof value === 'string' ? parseInt(value, 10) : value; -} diff --git a/src/lib/grid-list/grid-list.ts b/src/lib/grid-list/grid-list.ts index a0f872dab54f..e2a7100f13e7 100644 --- a/src/lib/grid-list/grid-list.ts +++ b/src/lib/grid-list/grid-list.ts @@ -22,10 +22,7 @@ import {MatGridTile} from './grid-tile'; import {TileCoordinator} from './tile-coordinator'; import {TileStyler, FitTileStyler, RatioTileStyler, FixedTileStyler} from './tile-styler'; import {Directionality} from '@angular/cdk/bidi'; -import { - coerceToString, - coerceToNumber, -} from './grid-list-measure'; +import {coerceNumberProperty} from '@angular/cdk/coercion'; // TODO(kara): Conditional (responsive) column count / row size. @@ -72,17 +69,17 @@ export class MatGridList implements OnInit, AfterContentChecked { /** Amount of columns in the grid list. */ @Input() get cols(): number { return this._cols; } - set cols(value: number) { this._cols = coerceToNumber(value); } + set cols(value: number) { this._cols = Math.round(coerceNumberProperty(value)); } /** Size of the grid list's gutter in pixels. */ @Input() get gutterSize(): string { return this._gutter; } - set gutterSize(value: string) { this._gutter = coerceToString(value); } + set gutterSize(value: string) { this._gutter = `${value || ''}`; } /** Set internal representation of row height from the user-provided value. */ @Input() set rowHeight(value: string | number) { - const newValue = coerceToString(value); + const newValue = `${value || ''}`; if (newValue !== this._rowHeight) { this._rowHeight = newValue; diff --git a/src/lib/grid-list/grid-tile.ts b/src/lib/grid-list/grid-tile.ts index 87bc53abfc29..8cc34fcfde62 100644 --- a/src/lib/grid-list/grid-tile.ts +++ b/src/lib/grid-list/grid-tile.ts @@ -18,7 +18,7 @@ import { ChangeDetectionStrategy, } from '@angular/core'; import {MatLine, MatLineSetter} from '@angular/material/core'; -import {coerceToNumber} from './grid-list-measure'; +import {coerceNumberProperty} from '@angular/cdk/coercion'; @Component({ moduleId: module.id, @@ -41,12 +41,12 @@ export class MatGridTile { /** Amount of rows that the grid tile takes up. */ @Input() get rowspan(): number { return this._rowspan; } - set rowspan(value: number) { this._rowspan = coerceToNumber(value); } + set rowspan(value: number) { this._rowspan = Math.round(coerceNumberProperty(value)); } /** Amount of columns that the grid tile takes up. */ @Input() get colspan(): number { return this._colspan; } - set colspan(value: number) { this._colspan = coerceToNumber(value); } + set colspan(value: number) { this._colspan = Math.round(coerceNumberProperty(value)); } /** * Sets the style of the grid-tile element. Needs to be set manually to avoid From da3b5e0d38ee2654bbf287d5ffbf08d3dc6d4e60 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 22 Aug 2018 16:28:02 +0200 Subject: [PATCH 119/189] fix(progress-bar): incorrectly handling current path when using hash location strategy (#12713) Fixes the progress bar prefixing the references incorrectly when the consumer is using the router's hash location strategy. The issue comes from the fact that the router normalizes the `location.pathname` between the regular strategy and the hash one, whereas we need to use the exact same path as the `window.location`. Fixes #12710. --- src/lib/progress-bar/progress-bar.spec.ts | 80 +++++++++++++---------- src/lib/progress-bar/progress-bar.ts | 38 +++++++++-- 2 files changed, 79 insertions(+), 39 deletions(-) diff --git a/src/lib/progress-bar/progress-bar.spec.ts b/src/lib/progress-bar/progress-bar.spec.ts index 883cbe13dc3e..9b75b4f65900 100644 --- a/src/lib/progress-bar/progress-bar.spec.ts +++ b/src/lib/progress-bar/progress-bar.spec.ts @@ -1,52 +1,49 @@ -import {TestBed, async, ComponentFixture} from '@angular/core/testing'; -import {Component} from '@angular/core'; +import {TestBed, ComponentFixture} from '@angular/core/testing'; +import {Component, Type} from '@angular/core'; import {By} from '@angular/platform-browser'; -import {Location} from '@angular/common'; -import {MatProgressBarModule} from './index'; +import {MatProgressBarModule, MAT_PROGRESS_BAR_LOCATION} from './index'; describe('MatProgressBar', () => { let fakePath = '/fake-path'; - beforeEach(async(() => { + function createComponent(componentType: Type): ComponentFixture { TestBed.configureTestingModule({ imports: [MatProgressBarModule], - declarations: [ - BasicProgressBar, - BufferProgressBar, - ], + declarations: [componentType], providers: [{ - provide: Location, - useValue: {path: () => fakePath} + provide: MAT_PROGRESS_BAR_LOCATION, + useValue: {pathname: fakePath} }] - }); - - TestBed.compileComponents(); - })); + }).compileComponents(); + return TestBed.createComponent(componentType); + } describe('basic progress-bar', () => { - let fixture: ComponentFixture; - - beforeEach(() => { - fixture = TestBed.createComponent(BasicProgressBar); + it('should apply a mode of "determinate" if no mode is provided.', () => { + const fixture = createComponent(BasicProgressBar); fixture.detectChanges(); - }); - it('should apply a mode of "determinate" if no mode is provided.', () => { - let progressElement = fixture.debugElement.query(By.css('mat-progress-bar')); + const progressElement = fixture.debugElement.query(By.css('mat-progress-bar')); expect(progressElement.componentInstance.mode).toBe('determinate'); }); it('should define default values for value and bufferValue attributes', () => { - let progressElement = fixture.debugElement.query(By.css('mat-progress-bar')); + const fixture = createComponent(BasicProgressBar); + fixture.detectChanges(); + + const progressElement = fixture.debugElement.query(By.css('mat-progress-bar')); expect(progressElement.componentInstance.value).toBe(0); expect(progressElement.componentInstance.bufferValue).toBe(0); }); it('should clamp value and bufferValue between 0 and 100', () => { - let progressElement = fixture.debugElement.query(By.css('mat-progress-bar')); - let progressComponent = progressElement.componentInstance; + const fixture = createComponent(BasicProgressBar); + fixture.detectChanges(); + + const progressElement = fixture.debugElement.query(By.css('mat-progress-bar')); + const progressComponent = progressElement.componentInstance; progressComponent.value = 50; expect(progressComponent.value).toBe(50); @@ -68,8 +65,11 @@ describe('MatProgressBar', () => { }); it('should return the transform attribute for bufferValue and mode', () => { - let progressElement = fixture.debugElement.query(By.css('mat-progress-bar')); - let progressComponent = progressElement.componentInstance; + const fixture = createComponent(BasicProgressBar); + fixture.detectChanges(); + + const progressElement = fixture.debugElement.query(By.css('mat-progress-bar')); + const progressComponent = progressElement.componentInstance; expect(progressComponent._primaryTransform()).toEqual({transform: 'scaleX(0)'}); expect(progressComponent._bufferTransform()).toBe(undefined); @@ -95,26 +95,38 @@ describe('MatProgressBar', () => { }); it('should prefix SVG references with the current path', () => { + const fixture = createComponent(BasicProgressBar); + fixture.detectChanges(); + const rect = fixture.debugElement.query(By.css('rect')).nativeElement; expect(rect.getAttribute('fill')).toMatch(/^url\(['"]?\/fake-path#.*['"]?\)$/); }); + it('should account for location hash when prefixing the SVG references', () => { + fakePath = '/fake-path#anchor'; + + const fixture = createComponent(BasicProgressBar); + fixture.detectChanges(); + + const rect = fixture.debugElement.query(By.css('rect')).nativeElement; + expect(rect.getAttribute('fill')).not.toContain('#anchor#'); + }); + it('should not be able to tab into the underlying SVG element', () => { + const fixture = createComponent(BasicProgressBar); + fixture.detectChanges(); + const svg = fixture.debugElement.query(By.css('svg')).nativeElement; expect(svg.getAttribute('focusable')).toBe('false'); }); }); describe('buffer progress-bar', () => { - let fixture: ComponentFixture; - - beforeEach(() => { - fixture = TestBed.createComponent(BufferProgressBar); + it('should not modify the mode if a valid mode is provided.', () => { + const fixture = createComponent(BufferProgressBar); fixture.detectChanges(); - }); - it('should not modify the mode if a valid mode is provided.', () => { - let progressElement = fixture.debugElement.query(By.css('mat-progress-bar')); + const progressElement = fixture.debugElement.query(By.css('mat-progress-bar')); expect(progressElement.componentInstance.mode).toBe('buffer'); }); }); diff --git a/src/lib/progress-bar/progress-bar.ts b/src/lib/progress-bar/progress-bar.ts index 2e316531bbea..b519ac379aeb 100644 --- a/src/lib/progress-bar/progress-bar.ts +++ b/src/lib/progress-bar/progress-bar.ts @@ -12,9 +12,9 @@ import { Inject, Input, Optional, - ViewEncapsulation + ViewEncapsulation, + InjectionToken } from '@angular/core'; -import {Location} from '@angular/common'; import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; import {CanColor, mixinColor} from '@angular/material/core'; @@ -29,6 +29,30 @@ export class MatProgressBarBase { export const _MatProgressBarMixinBase = mixinColor(MatProgressBarBase, 'primary'); +/** + * Injection token used to provide the current location to `MatProgressBar`. + * Used to handle server-side rendering and to stub out during unit tests. + * @docs-private + */ +export const MAT_PROGRESS_BAR_LOCATION = new InjectionToken( + 'mat-progress-bar-location', + {providedIn: 'root', factory: MAT_PROGRESS_BAR_LOCATION_FACTORY} +); + +/** + * Stubbed out location for `MatProgressBar`. + * @docs-private + */ +export interface MatProgressBarLocation { + pathname: string; +} + +/** @docs-private */ +export function MAT_PROGRESS_BAR_LOCATION_FACTORY(): MatProgressBarLocation { + return typeof window !== 'undefined' ? window.location : {pathname: ''}; +} + + /** Counter used to generate unique IDs for progress bars. */ let progressbarId = 0; @@ -61,13 +85,17 @@ export class MatProgressBar extends _MatProgressBarMixinBase implements CanColor * @deprecated `location` parameter to be made required. * @breaking-change 8.0.0 */ - @Optional() location?: Location) { + @Optional() @Inject(MAT_PROGRESS_BAR_LOCATION) location?: MatProgressBarLocation) { super(_elementRef); // We need to prefix the SVG reference with the current path, otherwise they won't work // in Safari if the page has a `` tag. Note that we need quotes inside the `url()`, - // because named route URLs can contain parentheses (see #12338). - this._rectangleFillValue = `url('${location ? location.path() : ''}#${this.progressbarId}')`; + // because named route URLs can contain parentheses (see #12338). Also we don't use + // `Location` from `@angular/common` since we can't tell the difference between whether + // the consumer is using the hash location strategy or not, because `Location` normalizes + // both `/#/foo/bar` and `/foo/bar` to the same thing. + const path = location ? location.pathname.split('#')[0] : ''; + this._rectangleFillValue = `url('${path}#${this.progressbarId}')`; } /** Value of the progress bar. Defaults to zero. Mirrored to aria-valuenow. */ From a326ee008852b16ab6e55e7ebd9229c9f9a2d23d Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 22 Aug 2018 16:28:38 +0200 Subject: [PATCH 120/189] fix(form-field): remove outline gap for empty labels (#12637) Removes the outline gap in outlined form fields, if the label element doesn't have any content. --- src/lib/form-field/form-field.ts | 13 ++++++++----- src/lib/input/input.spec.ts | 22 +++++++++++++++++++++- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/lib/form-field/form-field.ts b/src/lib/form-field/form-field.ts index e991283b2a4a..7f0a2920967c 100644 --- a/src/lib/form-field/form-field.ts +++ b/src/lib/form-field/form-field.ts @@ -444,7 +444,10 @@ export class MatFormField extends _MatFormFieldMixinBase * appearance. */ updateOutlineGap() { - if (this.appearance !== 'outline') { + const labelEl = this._label ? this._label.nativeElement : null; + + if (this.appearance !== 'outline' || !labelEl || !labelEl.children.length || + !labelEl.textContent.trim()) { return; } @@ -465,14 +468,14 @@ export class MatFormField extends _MatFormFieldMixinBase const containerStart = this._getStartEnd( this._connectionContainerRef.nativeElement.getBoundingClientRect()); - const labelStart = this._getStartEnd( - this._label.nativeElement.children[0].getBoundingClientRect()); + const labelStart = this._getStartEnd(labelEl.children[0].getBoundingClientRect()); let labelWidth = 0; - for (const child of this._label.nativeElement.children) { + + for (const child of labelEl.children) { labelWidth += child.offsetWidth; } startWidth = labelStart - containerStart - outlineGapPadding; - gapWidth = labelWidth * floatingLabelScale + outlineGapPadding * 2; + gapWidth = labelWidth > 0 ? labelWidth * floatingLabelScale + outlineGapPadding * 2 : 0; } for (let i = 0; i < startEls.length; i++) { diff --git a/src/lib/input/input.spec.ts b/src/lib/input/input.spec.ts index bb34d05d7578..c947618ebd2f 100644 --- a/src/lib/input/input.spec.ts +++ b/src/lib/input/input.spec.ts @@ -1151,6 +1151,25 @@ describe('MatInput with appearance', () => { expect(parseInt(outlineStart.style.width)).toBeGreaterThan(0); expect(parseInt(outlineGap.style.width)).toBeGreaterThan(0); })); + + it('should not set an outline gap if the label is empty', fakeAsync(() => { + fixture.destroy(); + TestBed.resetTestingModule(); + + const outlineFixture = createComponent(MatInputWithAppearanceAndLabel); + + outlineFixture.componentInstance.labelContent = ''; + outlineFixture.detectChanges(); + outlineFixture.componentInstance.appearance = 'outline'; + outlineFixture.detectChanges(); + flush(); + outlineFixture.detectChanges(); + + const outlineGap = outlineFixture.nativeElement.querySelector('.mat-form-field-outline-gap'); + + expect(parseInt(outlineGap.style.width)).toBeFalsy(); + })); + }); describe('MatFormField default options', () => { @@ -1594,13 +1613,14 @@ class MatInputWithAppearance { @Component({ template: ` - Label + {{labelContent}} ` }) class MatInputWithAppearanceAndLabel { appearance: MatFormFieldAppearance; + labelContent = 'Label'; } @Component({ From 96cbcb25ad102aeae02dc8acae77d1d7e2cc5324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Wed, 22 Aug 2018 16:30:22 +0200 Subject: [PATCH 121/189] docs(datepicker): add 'multi-year' to union type for startView property (#12518) Adds missing literal to union type. The issue pointed out that the possible value 'multi-year' is not listed on API page. Fixes #11700 --- src/lib/datepicker/datepicker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index cf7b31acccec..1788fe39b729 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -148,7 +148,7 @@ export class MatDatepicker implements OnDestroy, CanColor { private _startAt: D | null; /** The view that the calendar should start in. */ - @Input() startView: 'month' | 'year' = 'month'; + @Input() startView: 'month' | 'year' | 'multi-year' = 'month'; /** Color palette to use on the datepicker's calendar. */ @Input() From 81e0542a6e3399eb20ca4478e36edd26286bc87c Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 22 Aug 2018 16:31:16 +0200 Subject: [PATCH 122/189] fix(sidenav): content jumping in rtl and blurry text on IE (#12726) * Fixes the content of the sidenav jumping around in RTL if it has active animations. * Fixes text inside the sidenav being blurry on IE and Edge. Relates to #10026. --- src/lib/sidenav/drawer-animations.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/sidenav/drawer-animations.ts b/src/lib/sidenav/drawer-animations.ts index e280e63b111c..d88c7c085a01 100644 --- a/src/lib/sidenav/drawer-animations.ts +++ b/src/lib/sidenav/drawer-animations.ts @@ -20,8 +20,12 @@ export const matDrawerAnimations: { } = { /** Animation that slides a drawer in and out. */ transformDrawer: trigger('transform', [ + // We remove the `transform` here completely, rather than setting it to zero, because: + // 1. Having a transform can cause elements with ripples or an animated + // transform to shift around in Chrome with an RTL layout (see #10023). + // 2. 3d transforms causes text to appear blurry on IE and Edge. state('open, open-instant', style({ - 'transform': 'translate3d(0, 0, 0)', + 'transform': 'none', 'visibility': 'visible', })), state('void', style({ From 3596e9d05b82c4612f27cb5ecae35546d7c5f43e Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 23 Aug 2018 16:41:48 +0200 Subject: [PATCH 123/189] fix(expansion-panel): focus lost if focused element is inside closing panel (#12692) Currently when an expansion panel is closed, we make the content non-focusable using `visibility: hidden`, but that means that if the focused element was inside the panel, focus will be returned back to the body. These changes add a listener that will restore focus to the panel header, if the focused element is inside the panel when it is closed. --- src/lib/expansion/expansion-panel-header.ts | 7 ++++- src/lib/expansion/expansion-panel.ts | 31 +++++++++++++++++++-- src/lib/expansion/expansion.spec.ts | 19 +++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/lib/expansion/expansion-panel-header.ts b/src/lib/expansion/expansion-panel-header.ts index c871cb83d9c6..37cf3ef4f665 100644 --- a/src/lib/expansion/expansion-panel-header.ts +++ b/src/lib/expansion/expansion-panel-header.ts @@ -83,7 +83,12 @@ export class MatExpansionPanelHeader implements OnDestroy { ) .subscribe(() => this._changeDetectorRef.markForCheck()); - _focusMonitor.monitor(_element.nativeElement); + // Avoids focus being lost if the panel contained the focused element and was closed. + panel.closed + .pipe(filter(() => panel._containsFocus())) + .subscribe(() => _focusMonitor.focusVia(_element.nativeElement, 'program')); + + _focusMonitor.monitor(_element.nativeElement); } /** Height of the header while the panel is expanded. */ diff --git a/src/lib/expansion/expansion-panel.ts b/src/lib/expansion/expansion-panel.ts index f93c35ee3b4f..289e3ab620da 100644 --- a/src/lib/expansion/expansion-panel.ts +++ b/src/lib/expansion/expansion-panel.ts @@ -11,6 +11,7 @@ import {CdkAccordionItem} from '@angular/cdk/accordion'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; import {UniqueSelectionDispatcher} from '@angular/cdk/collections'; import {TemplatePortal} from '@angular/cdk/portal'; +import {DOCUMENT} from '@angular/common'; import { AfterContentInit, ChangeDetectionStrategy, @@ -18,12 +19,15 @@ import { Component, ContentChild, Directive, + ElementRef, + Inject, Input, OnChanges, OnDestroy, Optional, SimpleChanges, SkipSelf, + ViewChild, ViewContainerRef, ViewEncapsulation, } from '@angular/core'; @@ -70,8 +74,13 @@ let uniqueId = 0; '[class.mat-expansion-panel-spacing]': '_hasSpacing()', } }) -export class MatExpansionPanel extends _CdkAccordionItem - implements AfterContentInit, OnChanges, OnDestroy { +export class MatExpansionPanel extends CdkAccordionItem implements AfterContentInit, OnChanges, + OnDestroy { + + // @breaking-change 8.0.0 Remove `| undefined` from here + // when the `_document` constructor param is required. + private _document: Document | undefined; + /** Whether the toggle indicator should be hidden. */ @Input() get hideToggle(): boolean { @@ -91,6 +100,9 @@ export class MatExpansionPanel extends _CdkAccordionItem /** Content that will be rendered lazily. */ @ContentChild(MatExpansionPanelContent) _lazyContent: MatExpansionPanelContent; + /** Element containing the panel's user-provided content. */ + @ViewChild('body') _body: ElementRef; + /** Portal holding the user's content. */ _portal: TemplatePortal; @@ -100,9 +112,11 @@ export class MatExpansionPanel extends _CdkAccordionItem constructor(@Optional() @SkipSelf() accordion: MatAccordion, _changeDetectorRef: ChangeDetectorRef, _uniqueSelectionDispatcher: UniqueSelectionDispatcher, - private _viewContainerRef: ViewContainerRef) { + private _viewContainerRef: ViewContainerRef, + @Inject(DOCUMENT) _document?: any) { super(accordion, _changeDetectorRef, _uniqueSelectionDispatcher); this.accordion = accordion; + this._document = _document; } /** Determines whether the expansion panel should have spacing between it and its siblings. */ @@ -158,6 +172,17 @@ export class MatExpansionPanel extends _CdkAccordionItem classList.remove(cssClass); } } + + /** Checks whether the expansion panel's content contains the currently-focused element. */ + _containsFocus(): boolean { + if (this._body && this._document) { + const focusedElement = this._document.activeElement; + const bodyElement = this._body.nativeElement; + return focusedElement === bodyElement || bodyElement.contains(focusedElement); + } + + return false; + } } @Directive({ diff --git a/src/lib/expansion/expansion.spec.ts b/src/lib/expansion/expansion.spec.ts index 914dd95a2a8f..1ff21293afb4 100644 --- a/src/lib/expansion/expansion.spec.ts +++ b/src/lib/expansion/expansion.spec.ts @@ -160,6 +160,25 @@ describe('MatExpansionPanel', () => { expect(document.activeElement).not.toBe(button, 'Expected button to no longer be focusable.'); })); + it('should restore focus to header if focused element is inside panel on close', fakeAsync(() => { + const fixture = TestBed.createComponent(PanelWithContent); + fixture.componentInstance.expanded = true; + fixture.detectChanges(); + tick(250); + + const button = fixture.debugElement.query(By.css('button')).nativeElement; + const header = fixture.debugElement.query(By.css('mat-expansion-panel-header')).nativeElement; + + button.focus(); + expect(document.activeElement).toBe(button, 'Expected button to start off focusable.'); + + fixture.componentInstance.expanded = false; + fixture.detectChanges(); + tick(250); + + expect(document.activeElement).toBe(header, 'Expected header to be focused.'); + })); + it('should not override the panel margin if it is not inside an accordion', fakeAsync(() => { let fixture = TestBed.createComponent(PanelWithCustomMargin); fixture.detectChanges(); From 23491661a3ae9ee96955202e2d2b1e1c0021f99d Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 23 Aug 2018 16:44:54 +0200 Subject: [PATCH 124/189] fix(select): skip disabled options when using ctrl + a (#12553) Along the same lines as #12543. Currently `mat-select` will select all options when pressing ctrl + a, no matter whether they're disabled. These changes add an extra check to ensure that the disabled ones are skipped. --- src/lib/select/select.spec.ts | 27 +++++++++++++++++++++++++++ src/lib/select/select.ts | 9 +++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index 613f41aa2559..32a040dd6d9c 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -3966,6 +3966,33 @@ describe('MatSelect', () => { ]); }); + it('should skip disabled options when using ctrl + a', () => { + const selectElement = fixture.nativeElement.querySelector('mat-select'); + const options = fixture.componentInstance.options.toArray(); + + for (let i = 0; i < 3; i++) { + options[i].disabled = true; + } + + expect(testInstance.control.value).toBeFalsy(); + + fixture.componentInstance.select.open(); + fixture.detectChanges(); + + const event = createKeyboardEvent('keydown', A, selectElement); + Object.defineProperty(event, 'ctrlKey', {get: () => true}); + dispatchEvent(selectElement, event); + fixture.detectChanges(); + + expect(testInstance.control.value).toEqual([ + 'sandwich-3', + 'chips-4', + 'eggs-5', + 'pasta-6', + 'sushi-7' + ]); + }); + it('should select all options when pressing ctrl + a when some options are selected', () => { const selectElement = fixture.nativeElement.querySelector('mat-select'); const options = fixture.componentInstance.options.toArray(); diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index a81d82ebdb05..e47c8143724b 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -708,8 +708,13 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit, manager.activeItem._selectViaInteraction(); } else if (this._multiple && keyCode === A && event.ctrlKey) { event.preventDefault(); - const hasDeselectedOptions = this.options.some(option => !option.selected); - this.options.forEach(option => hasDeselectedOptions ? option.select() : option.deselect()); + const hasDeselectedOptions = this.options.some(opt => !opt.disabled && !opt.selected); + + this.options.forEach(option => { + if (!option.disabled) { + hasDeselectedOptions ? option.select() : option.deselect(); + } + }); } else { const previouslyFocusedIndex = manager.activeItemIndex; From d08d8bc0c3ee4ffbb6d30784b34be3a74ed86521 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 23 Aug 2018 16:46:56 +0200 Subject: [PATCH 125/189] fix(chips): focus not being restored correctly on chip removal when inside component with animations (#12416) Fixes the chip list losing its focus position if a chip is deleted while it's inside a component with animations. Fixes #12374. --- src/lib/chips/chip-list.spec.ts | 80 ++++++++++++++++++++++++++++++--- src/lib/chips/chip.ts | 15 +++++-- 2 files changed, 87 insertions(+), 8 deletions(-) diff --git a/src/lib/chips/chip-list.spec.ts b/src/lib/chips/chip-list.spec.ts index 4343f25da432..1a2abe95a00f 100644 --- a/src/lib/chips/chip-list.spec.ts +++ b/src/lib/chips/chip-list.spec.ts @@ -1,7 +1,12 @@ import {FocusKeyManager} from '@angular/cdk/a11y'; import {Directionality, Direction} from '@angular/cdk/bidi'; import {BACKSPACE, DELETE, ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE, TAB} from '@angular/cdk/keycodes'; -import {createKeyboardEvent, dispatchFakeEvent, dispatchKeyboardEvent} from '@angular/cdk/testing'; +import { + createKeyboardEvent, + dispatchFakeEvent, + dispatchKeyboardEvent, + MockNgZone, +} from '@angular/cdk/testing'; import { Component, DebugElement, @@ -10,16 +15,18 @@ import { ViewChildren, Type, Provider, + NgZone, } from '@angular/core'; import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; import {FormControl, FormsModule, NgForm, ReactiveFormsModule, Validators} from '@angular/forms'; import {MatFormFieldModule} from '@angular/material/form-field'; import {By} from '@angular/platform-browser'; -import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {NoopAnimationsModule, BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {MatInputModule} from '../input/index'; import {MatChip} from './chip'; import {MatChipInputEvent} from './chip-input'; import {MatChipList, MatChipsModule} from './index'; +import {trigger, transition, style, animate} from '@angular/animations'; describe('MatChipList', () => { @@ -30,6 +37,7 @@ describe('MatChipList', () => { let testComponent: StandardChipList; let chips: QueryList; let manager: FocusKeyManager; + let zone: MockNgZone; describe('StandardChipList', () => { describe('basic behaviors', () => { @@ -189,6 +197,7 @@ describe('MatChipList', () => { // Focus and blur the middle item midItem.focus(); midItem._blur(); + zone.simulateZoneExit(); // Destroy the middle item testComponent.remove = 2; @@ -197,6 +206,32 @@ describe('MatChipList', () => { // Should not have focus expect(chipListInstance._keyManager.activeItemIndex).toEqual(-1); }); + + it('should move focus to the last chip when the focused chip was deleted inside a' + + 'component with animations', fakeAsync(() => { + fixture.destroy(); + TestBed.resetTestingModule(); + fixture = createComponent(StandardChipListWithAnimations, [], BrowserAnimationsModule); + fixture.detectChanges(); + + chipListDebugElement = fixture.debugElement.query(By.directive(MatChipList)); + chipListNativeElement = chipListDebugElement.nativeElement; + chipListInstance = chipListDebugElement.componentInstance; + testComponent = fixture.debugElement.componentInstance; + chips = chipListInstance.chips; + + chips.last.focus(); + fixture.detectChanges(); + + expect(chipListInstance._keyManager.activeItemIndex).toBe(chips.length - 1); + + dispatchKeyboardEvent(chips.last._elementRef.nativeElement, 'keydown', BACKSPACE); + fixture.detectChanges(); + tick(500); + + expect(chipListInstance._keyManager.activeItemIndex).toBe(chips.length - 1); + })); + }); }); @@ -1053,7 +1088,9 @@ describe('MatChipList', () => { }); }); - function createComponent(component: Type, providers: Provider[] = []): ComponentFixture { + function createComponent(component: Type, providers: Provider[] = [], animationsModule: + Type | Type = NoopAnimationsModule): + ComponentFixture { TestBed.configureTestingModule({ imports: [ FormsModule, @@ -1061,10 +1098,13 @@ describe('MatChipList', () => { MatChipsModule, MatFormFieldModule, MatInputModule, - NoopAnimationsModule, + animationsModule, ], declarations: [component], - providers + providers: [ + {provide: NgZone, useFactory: () => zone = new MockNgZone()}, + ...providers + ] }).compileComponents(); return TestBed.createComponent(component); @@ -1328,3 +1368,33 @@ class ChipListWithFormErrorMessages { @ViewChild('form') form: NgForm; formControl = new FormControl('', Validators.required); } + + +@Component({ + template: ` + + {{i}} + `, + animations: [ + // For the case we're testing this animation doesn't + // have to be used anywhere, it just has to be defined. + trigger('dummyAnimation', [ + transition(':leave', [ + style({opacity: 0}), + animate('500ms', style({opacity: 1})) + ]) + ]) + ] +}) +class StandardChipListWithAnimations { + numbers = [0, 1, 2, 3, 4]; + + remove(item: number): void { + const index = this.numbers.indexOf(item); + + if (index > -1) { + this.numbers.splice(index, 1); + } + } +} + diff --git a/src/lib/chips/chip.ts b/src/lib/chips/chip.ts index 4a7413d98bed..f4e41ac1e4fb 100644 --- a/src/lib/chips/chip.ts +++ b/src/lib/chips/chip.ts @@ -37,6 +37,7 @@ import { RippleTarget } from '@angular/material/core'; import {Subject} from 'rxjs'; +import {take} from 'rxjs/operators'; /** Represents an event fired on an individual `mat-chip`. */ @@ -218,14 +219,14 @@ export class MatChip extends _MatChipMixinBase implements FocusableOption, OnDes } constructor(public _elementRef: ElementRef, - ngZone: NgZone, + private _ngZone: NgZone, platform: Platform, @Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalOptions: RippleGlobalOptions) { super(_elementRef); this._addHostClassName(); - this._chipRipple = new RippleRenderer(this, ngZone, _elementRef, platform); + this._chipRipple = new RippleRenderer(this, _ngZone, _elementRef, platform); this._chipRipple.setupTriggerEvents(_elementRef.nativeElement); if (globalOptions) { @@ -359,7 +360,15 @@ export class MatChip extends _MatChipMixinBase implements FocusableOption, OnDes } _blur(): void { - this._hasFocus = false; + // When animations are enabled, Angular may end up removing the chip from the DOM a little + // earlier than usual, causing it to be blurred and throwing off the logic in the chip list + // that moves focus not the next item. To work around the issue, we defer marking the chip + // as not focused until the next time the zone stabilizes. + this._ngZone.onStable + .asObservable() + .pipe(take(1)) + .subscribe(() => this._hasFocus = false); + this._onBlur.next({chip: this}); } } From a6b8a0640d7b8850fcee1d23a089138455c52615 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 23 Aug 2018 16:48:23 +0200 Subject: [PATCH 126/189] fix(tooltip): opening after click on android (#12250) Fixes the tooltip opening on clicks on Android devices. Also does some minor cleanup. Fixes #12223. --- src/lib/tooltip/tooltip.spec.ts | 35 ++++++++++++++++++++++++++++++--- src/lib/tooltip/tooltip.ts | 17 ++++++++-------- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/lib/tooltip/tooltip.spec.ts b/src/lib/tooltip/tooltip.spec.ts index 25bea8c35488..d3a5c1879d73 100644 --- a/src/lib/tooltip/tooltip.spec.ts +++ b/src/lib/tooltip/tooltip.spec.ts @@ -22,7 +22,12 @@ import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {Direction, Directionality} from '@angular/cdk/bidi'; import {OverlayContainer, OverlayModule, CdkScrollable} from '@angular/cdk/overlay'; import {Platform} from '@angular/cdk/platform'; -import {dispatchFakeEvent, dispatchKeyboardEvent, patchElementFocus} from '@angular/cdk/testing'; +import { + dispatchFakeEvent, + dispatchKeyboardEvent, + patchElementFocus, + dispatchMouseEvent, +} from '@angular/cdk/testing'; import {ESCAPE} from '@angular/cdk/keycodes'; import {FocusMonitor} from '@angular/cdk/a11y'; import { @@ -40,12 +45,12 @@ describe('MatTooltip', () => { let overlayContainer: OverlayContainer; let overlayContainerElement: HTMLElement; let dir: {value: Direction}; - let platform: {IOS: boolean, isBrowser: boolean}; + let platform: {IOS: boolean, isBrowser: boolean, ANDROID: boolean}; let focusMonitor: FocusMonitor; beforeEach(async(() => { // Set the default Platform override that can be updated before component creation. - platform = {IOS: false, isBrowser: true}; + platform = {IOS: false, isBrowser: true, ANDROID: false}; TestBed.configureTestingModule({ imports: [MatTooltipModule, OverlayModule, NoopAnimationsModule], @@ -808,6 +813,30 @@ describe('MatTooltip', () => { expect(fixture.componentInstance.button.nativeElement.style.webkitUserDrag).toBeFalsy(); }); + it('should not open on `mouseenter` on iOS', () => { + platform.IOS = true; + + const fixture = TestBed.createComponent(BasicTooltipDemo); + + fixture.detectChanges(); + dispatchMouseEvent(fixture.componentInstance.button.nativeElement, 'mouseenter'); + fixture.detectChanges(); + + assertTooltipInstance(fixture.componentInstance.tooltip, false); + }); + + it('should not open on `mouseenter` on Android', () => { + platform.ANDROID = true; + + const fixture = TestBed.createComponent(BasicTooltipDemo); + + fixture.detectChanges(); + dispatchMouseEvent(fixture.componentInstance.button.nativeElement, 'mouseenter'); + fixture.detectChanges(); + + assertTooltipInstance(fixture.componentInstance.tooltip, false); + }); + }); }); diff --git a/src/lib/tooltip/tooltip.ts b/src/lib/tooltip/tooltip.ts index c059be30d6f0..343b7f7700fc 100644 --- a/src/lib/tooltip/tooltip.ts +++ b/src/lib/tooltip/tooltip.ts @@ -185,7 +185,7 @@ export class MatTooltip implements OnDestroy { } } - private _manualListeners = new Map(); + private _manualListeners = new Map(); /** Emits when the component is destroyed. */ private readonly _destroyed = new Subject(); @@ -206,15 +206,14 @@ export class MatTooltip implements OnDestroy { const element: HTMLElement = _elementRef.nativeElement; - // The mouse events shouldn't be bound on iOS devices, because - // they can prevent the first tap from firing its click event. - if (!_platform.IOS) { - this._manualListeners.set('mouseenter', () => this.show()); - this._manualListeners.set('mouseleave', () => this.hide()); - + // The mouse events shouldn't be bound on mobile devices, because they can prevent the + // first tap from firing its click event or can cause the tooltip to open for clicks. + if (!_platform.IOS && !_platform.ANDROID) { this._manualListeners - .forEach((listener, event) => _elementRef.nativeElement.addEventListener(event, listener)); - } else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { + .set('mouseenter', () => this.show()) + .set('mouseleave', () => this.hide()) + .forEach((listener, event) => element.addEventListener(event, listener)); + } else if (_platform.IOS && (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA')) { // When we bind a gesture event on an element (in this case `longpress`), HammerJS // will add some inline styles by default, including `user-select: none`. This is // problematic on iOS, because it will prevent users from typing in inputs. If From d3af44137ae137d39def66b441c86ca8530229c5 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 23 Aug 2018 16:48:42 +0200 Subject: [PATCH 127/189] fix(tabs): disable focus overlay for touch focus (#12249) Doesn't show the tab's focus indication if it was focused by anything, other than keyboard or programmatically. Fixes #12247. --- src/lib/tabs/_tabs-theme.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/tabs/_tabs-theme.scss b/src/lib/tabs/_tabs-theme.scss index 7b8648a02b91..65afdc75c188 100644 --- a/src/lib/tabs/_tabs-theme.scss +++ b/src/lib/tabs/_tabs-theme.scss @@ -85,8 +85,11 @@ @mixin _mat-tab-label-focus($tab-focus-color) { .mat-tab-label, .mat-tab-link { - &.cdk-focused:not(.cdk-mouse-focused):not(.mat-tab-disabled) { - background-color: mat-color($tab-focus-color, lighter, 0.3); + &.cdk-keyboard-focused, + &.cdk-program-focused { + &:not(.mat-tab-disabled) { + background-color: mat-color($tab-focus-color, lighter, 0.3); + } } } } From c807d74707c79f92a7310860403800ce928d091a Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 23 Aug 2018 16:49:24 +0200 Subject: [PATCH 128/189] fix(datepicker): multiple dialog open if the user holds down enter key (#12238) Fixes the case where the user might be holding down the enter key for a datepicker in touch mode, which could cause it to open multiple dialogs at the same time. --- src/lib/datepicker/datepicker.spec.ts | 26 ++++++++++++++++++++++++-- src/lib/datepicker/datepicker.ts | 8 ++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index 47c5f2563f12..9471394b52b8 100644 --- a/src/lib/datepicker/datepicker.spec.ts +++ b/src/lib/datepicker/datepicker.spec.ts @@ -8,7 +8,7 @@ import { dispatchMouseEvent, } from '@angular/cdk/testing'; import {Component, FactoryProvider, Type, ValueProvider, ViewChild} from '@angular/core'; -import {ComponentFixture, fakeAsync, flush, inject, TestBed} from '@angular/core/testing'; +import {ComponentFixture, fakeAsync, flush, inject, TestBed, tick} from '@angular/core/testing'; import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; import { DEC, @@ -102,7 +102,7 @@ describe('MatDatepicker', () => { expect(document.querySelector('.cdk-overlay-pane.mat-datepicker-popup')).not.toBeNull(); }); - it('open touch should open dialog', () => { + it('touch should open dialog', () => { testComponent.touch = true; fixture.detectChanges(); @@ -115,6 +115,28 @@ describe('MatDatepicker', () => { .not.toBeNull(); }); + it('should not be able to open more than one dialog', fakeAsync(() => { + testComponent.touch = true; + fixture.detectChanges(); + + expect(document.querySelectorAll('.mat-datepicker-dialog').length).toBe(0); + + testComponent.datepicker.open(); + fixture.detectChanges(); + tick(500); + fixture.detectChanges(); + + dispatchKeyboardEvent(document.querySelector('.mat-calendar-body')!, 'keydown', ENTER); + fixture.detectChanges(); + tick(100); + + testComponent.datepicker.open(); + tick(500); + fixture.detectChanges(); + + expect(document.querySelectorAll('.mat-datepicker-dialog').length).toBe(1); + })); + it('should open datepicker if opened input is set to true', fakeAsync(() => { testComponent.opened = true; fixture.detectChanges(); diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index 1788fe39b729..261de14f9cdd 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -380,6 +380,14 @@ export class MatDatepicker implements OnDestroy, CanColor { /** Open the calendar as a dialog. */ private _openAsDialog(): void { + // Usually this would be handled by `open` which ensures that we can only have one overlay + // open at a time, however since we reset the variables in async handlers some overlays + // may slip through if the user opens and closes multiple times in quick succession (e.g. + // by holding down the enter key). + if (this._dialogRef) { + this._dialogRef.close(); + } + this._dialogRef = this._dialog.open>(MatDatepickerContent, { direction: this._dir ? this._dir.value : 'ltr', viewContainerRef: this._viewContainerRef, From 22ae587cd4a8c684aa3be795c5f4751a592408a7 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Thu, 23 Aug 2018 16:50:00 +0200 Subject: [PATCH 129/189] fix(card): images in title-group overlapping content (#12205) * Fixes that `mat-card-$breakpoint-image` images overlap the card content when placed inside of the title group. Fixes #10031 --- src/demo-app/card/card-demo.html | 59 ++++++++++++++++++++++++++------ src/demo-app/card/card-demo.ts | 9 ++++- src/lib/card/card.scss | 26 ++++++++++---- 3 files changed, 76 insertions(+), 18 deletions(-) diff --git a/src/demo-app/card/card-demo.html b/src/demo-app/card/card-demo.html index bde2eee3829b..4c5c31e62053 100644 --- a/src/demo-app/card/card-demo.html +++ b/src/demo-app/card/card-demo.html @@ -3,20 +3,12 @@ Hello - - - Card with title - Subtitle - - - - Subtitle Card with title and footer

This is supporting text.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+

{{longText}}

@@ -32,7 +24,7 @@ Card with title, footer, and inset-divider

This is supporting text.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+

{{longText}}

@@ -75,4 +67,51 @@
+ +
+

Cards with media area

+ + + + Card + Small + + + + {{longText}} + + + + + + Card + Medium + + + + {{longText}} + + + + + + Card + Large + + + + {{longText}} + + + + + + Card + Extra large + + + + {{longText}} + + diff --git a/src/demo-app/card/card-demo.ts b/src/demo-app/card/card-demo.ts index 7a2bcbbf1282..58a635617488 100644 --- a/src/demo-app/card/card-demo.ts +++ b/src/demo-app/card/card-demo.ts @@ -15,4 +15,11 @@ import {Component} from '@angular/core'; templateUrl: 'card-demo.html', styleUrls: ['card-demo.css'], }) -export class CardDemo {} +export class CardDemo { + longText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor ' + + 'incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud ' + + 'exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor' + + ' in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur' + + ' sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id ' + + 'est laborum.'; +} diff --git a/src/lib/card/card.scss b/src/lib/card/card.scss index 43f9bf27b886..ebbb3e475f5a 100644 --- a/src/lib/card/card.scss +++ b/src/lib/card/card.scss @@ -74,12 +74,6 @@ $mat-card-header-size: 40px !default; margin: 0 -24px 16px -24px; } -.mat-card-xl-image { - width: 240px; - height: 240px; - margin: -8px; -} - .mat-card-footer { // The negative margins pulls out the element, countering the padding // to get the footer to be flush against the side of the card. @@ -117,7 +111,10 @@ $mat-card-header-size: 40px !default; // images grouped with title in title-group layout %mat-card-title-img { - margin: -8px 0; + // As per Material Design specifications, the images exceed the *top* content-box and take + // up some space. The margin below is necessary because otherwise space of the actual card + // content will be overlapped. + margin: -8px 0 8px 0; } .mat-card-title-group { @@ -144,6 +141,21 @@ $mat-card-header-size: 40px !default; height: 152px; } +// This should normally also extend the `%mat-card-title-img`, but in order to avoid breaking +// changes, we need to keep the horizontal margin reversion for now. +// See: https://github.com/angular/material2/issues/12203 +.mat-card-xl-image { + width: 240px; + height: 240px; + margin: -8px; + + // Special treatment inside title group in order to fix the media areas inside of a title-group. + // This can be removed once #12203 has been addressed. + .mat-card-title-group > & { + @extend %mat-card-title-img; + } +} + // MEDIA QUERIES @media ($mat-xsmall) { From bb9cfec3cd6f6544651772f78c055999edd472d4 Mon Sep 17 00:00:00 2001 From: Jeremy Elbourn Date: Thu, 23 Aug 2018 12:56:41 -0700 Subject: [PATCH 130/189] fix(progress-bar): avoid error on SSR if pathname is undefined (#12807) --- src/lib/progress-bar/progress-bar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/progress-bar/progress-bar.ts b/src/lib/progress-bar/progress-bar.ts index b519ac379aeb..043a50ace634 100644 --- a/src/lib/progress-bar/progress-bar.ts +++ b/src/lib/progress-bar/progress-bar.ts @@ -94,7 +94,7 @@ export class MatProgressBar extends _MatProgressBarMixinBase implements CanColor // `Location` from `@angular/common` since we can't tell the difference between whether // the consumer is using the hash location strategy or not, because `Location` normalizes // both `/#/foo/bar` and `/foo/bar` to the same thing. - const path = location ? location.pathname.split('#')[0] : ''; + const path = location && location.pathname ? location.pathname.split('#')[0] : ''; this._rectangleFillValue = `url('${path}#${this.progressbarId}')`; } From 8617423d32cfc5f4f6251fee5e94c1e9b85ffed6 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 23 Aug 2018 22:19:47 +0200 Subject: [PATCH 131/189] fix(autocomplete): reopening closed autocomplete when coming back to tab (#12372) Fixes a closed autocomplete being reopened, if the user moves to another tab and coming back to the current one, while the input is still focused. Fixes #12337. --- src/lib/autocomplete/autocomplete-trigger.ts | 37 ++++++++++++++++++-- src/lib/autocomplete/autocomplete.spec.ts | 30 ++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index cb33570705f1..24b16ea30015 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -135,9 +135,28 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { /** Subscription to viewport size changes. */ private _viewportSubscription = Subscription.EMPTY; + /** + * Whether the autocomplete can open the next time it is focused. Used to prevent a focused, + * closed autocomplete from being reopened if the user switches to another browser tab and then + * comes back. + */ + private _canOpenOnNextFocus = true; + /** Stream of keyboard events that can close the panel. */ private readonly _closeKeyEventStream = new Subject(); + /** + * Event handler for when the window is blurred. Needs to be an + * arrow function in order to preserve the context. + */ + private _windowBlurHandler = () => { + // If the user blurred the window while the autocomplete is focused, it means that it'll be + // refocused when they come back. In this case we want to skip the first focus event, if the + // pane was closed, in order to avoid reopening it unintentionally. + this._canOpenOnNextFocus = + document.activeElement !== this._element.nativeElement || this.panelOpen; + } + /** `View -> model callback called when value changes` */ _onChange: (value: any) => void = () => {}; @@ -178,9 +197,20 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { @Optional() @Host() private _formField: MatFormField, @Optional() @Inject(DOCUMENT) private _document: any, // @breaking-change 7.0.0 Make `_viewportRuler` required. - private _viewportRuler?: ViewportRuler) {} + private _viewportRuler?: ViewportRuler) { + + if (typeof window !== 'undefined') { + _zone.runOutsideAngular(() => { + window.addEventListener('blur', this._windowBlurHandler); + }); + } + } ngOnDestroy() { + if (typeof window !== 'undefined') { + window.removeEventListener('blur', this._windowBlurHandler); + } + this._viewportSubscription.unsubscribe(); this._componentDestroyed = true; this._destroyPanel(); @@ -375,7 +405,9 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { } _handleFocus(): void { - if (this._canOpen()) { + if (!this._canOpenOnNextFocus) { + this._canOpenOnNextFocus = true; + } else if (this._canOpen()) { this._previousValue = this._element.nativeElement.value; this._attachOverlay(); this._floatLabel(true); @@ -612,5 +644,4 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { const element: HTMLInputElement = this._element.nativeElement; return !element.readOnly && !element.disabled && !this._autocompleteDisabled; } - } diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index 1101eb98d851..395d29712e0d 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -1961,6 +1961,36 @@ describe('MatAutocomplete', () => { expect(Math.ceil(parseFloat(overlayPane.style.width as string))).toBe(500); }); + it('should not reopen a closed autocomplete when returning to a blurred tab', () => { + const fixture = createComponent(SimpleAutocomplete); + fixture.detectChanges(); + + const trigger = fixture.componentInstance.trigger; + const input = fixture.debugElement.query(By.css('input')).nativeElement; + + input.focus(); + fixture.detectChanges(); + + expect(trigger.panelOpen).toBe(true, 'Expected panel to be open.'); + + trigger.closePanel(); + fixture.detectChanges(); + + expect(trigger.panelOpen).toBe(false, 'Expected panel to be closed.'); + + // Simulate the user going to a different tab. + dispatchFakeEvent(window, 'blur'); + input.blur(); + fixture.detectChanges(); + + // Simulate the user coming back. + dispatchFakeEvent(window, 'focus'); + input.focus(); + fixture.detectChanges(); + + expect(trigger.panelOpen).toBe(false, 'Expected panel to remain closed.'); + }); + it('should update the panel width if the window is resized', fakeAsync(() => { const widthFixture = createComponent(SimpleAutocomplete); From 6bb0ffe706125701deedc4079c83add2848d57b8 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 23 Aug 2018 22:20:29 +0200 Subject: [PATCH 132/189] fix(tabs): only target direct descendants with mat-stretch-tabs (#12198) Fixes `mat-stretch-tabs` applying to all descendant tab headers, rather than direct descendants only. Fixes #12196. --- src/lib/tabs/tab-group.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/tabs/tab-group.scss b/src/lib/tabs/tab-group.scss index c5a148d777fd..320653648e91 100644 --- a/src/lib/tabs/tab-group.scss +++ b/src/lib/tabs/tab-group.scss @@ -29,7 +29,8 @@ } } -.mat-tab-group[mat-stretch-tabs] .mat-tab-label { +// Note that we only want to target direct descendant tabs. +.mat-tab-group[mat-stretch-tabs] > .mat-tab-header .mat-tab-label { flex-basis: 0; flex-grow: 1; } From 0fcdae4d949b31ce5696743fae44f89f7b412404 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 23 Aug 2018 22:26:22 +0200 Subject: [PATCH 133/189] fix(stepper): handle removing a step before the current one (#11813) Fixes an error that is thrown by the stepper if a step before the current one is removed. Fixes #11791. --- src/cdk/stepper/stepper.ts | 6 ++++++ src/lib/stepper/stepper.spec.ts | 21 ++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index ae892d376ab5..a2df89e7e0f3 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -244,6 +244,12 @@ export class CdkStepper implements AfterViewInit, OnDestroy { .subscribe(direction => this._keyManager.withHorizontalOrientation(direction)); this._keyManager.updateActiveItemIndex(this._selectedIndex); + + this._steps.changes.pipe(takeUntil(this._destroyed)).subscribe(() => { + if (!this.selected) { + this._selectedIndex = Math.max(this._selectedIndex - 1, 0); + } + }); } ngOnDestroy() { diff --git a/src/lib/stepper/stepper.spec.ts b/src/lib/stepper/stepper.spec.ts index 7aa19ef3da55..05cfe31be3a4 100644 --- a/src/lib/stepper/stepper.spec.ts +++ b/src/lib/stepper/stepper.spec.ts @@ -388,6 +388,24 @@ describe('MatStepper', () => { expect(headers.every(header => header.getAttribute('aria-setsize') === '3')).toBe(true); }); + it('should adjust the index when removing a step before the current one', () => { + const stepperComponent: MatVerticalStepper = fixture.debugElement + .query(By.css('mat-vertical-stepper')).componentInstance; + + stepperComponent.selectedIndex = 2; + fixture.detectChanges(); + + // Re-assert since the setter has some extra logic. + expect(stepperComponent.selectedIndex).toBe(2); + + expect(() => { + fixture.componentInstance.showStepTwo = false; + fixture.detectChanges(); + }).not.toThrow(); + + expect(stepperComponent.selectedIndex).toBe(1); + }); + }); describe('icon overrides', () => { @@ -1038,7 +1056,7 @@ class SimpleMatHorizontalStepperApp { - + Step 2 Content 2
@@ -1058,6 +1076,7 @@ class SimpleMatHorizontalStepperApp { }) class SimpleMatVerticalStepperApp { inputLabel = 'Step 3'; + showStepTwo = true; } @Component({ From b735e482efad20cfbfb63721061790ba1361c473 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 23 Aug 2018 22:26:52 +0200 Subject: [PATCH 134/189] fix(chips): support focusing first/last item using home/end (#11892) Based on the accessibility guidelines, grid cells should support moving focus to the first/last items via the Home and End keys. --- src/lib/chips/chip-list.spec.ts | 42 ++++++++++++++++++++++++++++++++- src/lib/chips/chip-list.ts | 13 ++++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/lib/chips/chip-list.spec.ts b/src/lib/chips/chip-list.spec.ts index 1a2abe95a00f..52f0c3b190b3 100644 --- a/src/lib/chips/chip-list.spec.ts +++ b/src/lib/chips/chip-list.spec.ts @@ -1,6 +1,16 @@ import {FocusKeyManager} from '@angular/cdk/a11y'; import {Directionality, Direction} from '@angular/cdk/bidi'; -import {BACKSPACE, DELETE, ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE, TAB} from '@angular/cdk/keycodes'; +import { + BACKSPACE, + DELETE, + ENTER, + LEFT_ARROW, + RIGHT_ARROW, + SPACE, + TAB, + HOME, + END, +} from '@angular/cdk/keycodes'; import { createKeyboardEvent, dispatchFakeEvent, @@ -296,6 +306,36 @@ describe('MatChipList', () => { .toBe(initialActiveIndex, 'Expected focused item not to have changed.'); }); + it('should focus the first item when pressing HOME', () => { + const nativeChips = chipListNativeElement.querySelectorAll('mat-chip'); + const lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement; + const HOME_EVENT = createKeyboardEvent('keydown', HOME, lastNativeChip); + const array = chips.toArray(); + const lastItem = array[array.length - 1]; + + lastItem.focus(); + expect(manager.activeItemIndex).toBe(array.length - 1); + + chipListInstance._keydown(HOME_EVENT); + fixture.detectChanges(); + + expect(manager.activeItemIndex).toBe(0); + expect(HOME_EVENT.defaultPrevented).toBe(true); + }); + + it('should focus the last item when pressing END', () => { + const nativeChips = chipListNativeElement.querySelectorAll('mat-chip'); + const END_EVENT = createKeyboardEvent('keydown', END, nativeChips[0]); + + expect(manager.activeItemIndex).toBe(-1); + + chipListInstance._keydown(END_EVENT); + fixture.detectChanges(); + + expect(manager.activeItemIndex).toBe(chips.length - 1); + expect(END_EVENT.defaultPrevented).toBe(true); + }); + }); describe('RTL', () => { diff --git a/src/lib/chips/chip-list.ts b/src/lib/chips/chip-list.ts index 8f418e7a20cf..cf4f9bbd93f8 100644 --- a/src/lib/chips/chip-list.ts +++ b/src/lib/chips/chip-list.ts @@ -10,7 +10,7 @@ import {FocusKeyManager} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; import {SelectionModel} from '@angular/cdk/collections'; -import {BACKSPACE} from '@angular/cdk/keycodes'; +import {BACKSPACE, HOME, END} from '@angular/cdk/keycodes'; import { AfterContentInit, ChangeDetectionStrategy, @@ -483,7 +483,16 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo this._keyManager.setLastItemActive(); event.preventDefault(); } else if (target && target.classList.contains('mat-chip')) { - this._keyManager.onKeydown(event); + if (event.keyCode === HOME) { + this._keyManager.setFirstItemActive(); + event.preventDefault(); + } else if (event.keyCode === END) { + this._keyManager.setLastItemActive(); + event.preventDefault(); + } else { + this._keyManager.onKeydown(event); + } + this.stateChanges.next(); } } From 4f483c289d2c631b209cf4aa796f69dbd3153ffb Mon Sep 17 00:00:00 2001 From: Edric Chan Date: Fri, 24 Aug 2018 11:40:50 +0800 Subject: [PATCH 135/189] docs: update links to Material Design spec (#12804) For color palettes, link to the archived spec since there isn't an equivalent in the 2018 revision. --- guides/theming.md | 2 +- src/lib/select/select.md | 2 +- src/lib/slider/slider.md | 4 ++-- src/lib/tooltip/tooltip.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/guides/theming.md b/guides/theming.md index 261509e83ba9..79f1f20318f0 100644 --- a/guides/theming.md +++ b/guides/theming.md @@ -16,7 +16,7 @@ a theme consists of: In Angular Material, all theme styles are generated _statically_ at build-time so that your app doesn't have to spend cycles generating theme styles on startup. -[1]: https://material.google.com/style/color.html#color-color-palette +[1]: https://material.io/archive/guidelines/style/color.html#color-color-palette ### Using a pre-built theme Angular Material comes prepackaged with several pre-built theme css files. These theme files also diff --git a/src/lib/select/select.md b/src/lib/select/select.md index bf2ce7070d98..788f61ef2ab7 100644 --- a/src/lib/select/select.md +++ b/src/lib/select/select.md @@ -1,6 +1,6 @@ `` is a form control for selecting a value from a set of options, similar to the native ` + + ` +}) +class MatInputWithLabel {} + @Component({ template: ` From 41d01969163eb91253f5389ed11711bdd5078682 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 24 Aug 2018 16:55:08 +0200 Subject: [PATCH 139/189] fix(stepper): improved alignment for step icons (#12703) Switches to using absolute positioning to center the stepper icons. This works better with the text-based icons. Fixes #12696. --- src/lib/stepper/step-header.html | 6 +++--- src/lib/stepper/step-header.scss | 12 +++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/lib/stepper/step-header.html b/src/lib/stepper/step-header.html index 2d35efdba267..67a8d381ae28 100644 --- a/src/lib/stepper/step-header.html +++ b/src/lib/stepper/step-header.html @@ -8,7 +8,7 @@ *ngSwitchCase="true" [ngTemplateOutlet]="iconOverrides.number" [ngTemplateOutletContext]="_getIconContext()"> - {{index + 1}} + {{index + 1}} @@ -16,7 +16,7 @@ *ngSwitchCase="true" [ngTemplateOutlet]="iconOverrides.edit" [ngTemplateOutletContext]="_getIconContext()"> - create + create @@ -24,7 +24,7 @@ *ngSwitchCase="true" [ngTemplateOutlet]="iconOverrides.done" [ngTemplateOutletContext]="_getIconContext()"> - done + done
Date: Fri, 24 Aug 2018 16:57:28 +0200 Subject: [PATCH 140/189] fix(datepicker-toggle): forward tabindex to underlying button (#12461) Forwards the tabindex of a `mat-button-toggle` to its underlying `button` and clears it from the host element. Fixes #12456. --- src/lib/datepicker/datepicker-toggle.html | 1 + src/lib/datepicker/datepicker-toggle.ts | 15 ++++++++++- src/lib/datepicker/datepicker.spec.ts | 33 +++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/lib/datepicker/datepicker-toggle.html b/src/lib/datepicker/datepicker-toggle.html index da7e836f7500..cd4a9c71cb46 100644 --- a/src/lib/datepicker/datepicker-toggle.html +++ b/src/lib/datepicker/datepicker-toggle.html @@ -3,6 +3,7 @@ type="button" aria-haspopup="true" [attr.aria-label]="_intl.openCalendarLabel" + [attr.tabindex]="disabled ? -1 : tabIndex" [disabled]="disabled" (click)="_open($event)"> diff --git a/src/lib/datepicker/datepicker-toggle.ts b/src/lib/datepicker/datepicker-toggle.ts index 51c2ac16f367..5cfc79555c88 100644 --- a/src/lib/datepicker/datepicker-toggle.ts +++ b/src/lib/datepicker/datepicker-toggle.ts @@ -9,6 +9,7 @@ import {coerceBooleanProperty} from '@angular/cdk/coercion'; import { AfterContentInit, + Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -39,6 +40,8 @@ export class MatDatepickerToggleIcon {} styleUrls: ['datepicker-toggle.css'], host: { 'class': 'mat-datepicker-toggle', + // Clear out the native tabindex here since we forward it to the underlying button + '[attr.tabindex]': 'null', '[class.mat-datepicker-toggle-active]': 'datepicker && datepicker.opened', '[class.mat-accent]': 'datepicker && datepicker.color === "accent"', '[class.mat-warn]': 'datepicker && datepicker.color === "warn"', @@ -53,6 +56,9 @@ export class MatDatepickerToggle implements AfterContentInit, OnChanges, OnDe /** Datepicker instance that the button will toggle. */ @Input('for') datepicker: MatDatepicker; + /** Tabindex for the toggle. */ + @Input() tabIndex: number | null; + /** Whether the toggle button is disabled. */ @Input() get disabled(): boolean { @@ -66,7 +72,14 @@ export class MatDatepickerToggle implements AfterContentInit, OnChanges, OnDe /** Custom icon set by the consumer. */ @ContentChild(MatDatepickerToggleIcon) _customIcon: MatDatepickerToggleIcon; - constructor(public _intl: MatDatepickerIntl, private _changeDetectorRef: ChangeDetectorRef) {} + constructor( + public _intl: MatDatepickerIntl, + private _changeDetectorRef: ChangeDetectorRef, + @Attribute('tabindex') defaultTabIndex: string) { + + const parsedTabIndex = Number(defaultTabIndex); + this.tabIndex = (parsedTabIndex || parsedTabIndex === 0) ? parsedTabIndex : null; + } ngOnChanges(changes: SimpleChanges) { if (changes.datepicker) { diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index 9471394b52b8..02a99f12ecad 100644 --- a/src/lib/datepicker/datepicker.spec.ts +++ b/src/lib/datepicker/datepicker.spec.ts @@ -1008,6 +1008,26 @@ describe('MatDatepicker', () => { })); }); + describe('datepicker with tabindex on mat-datepicker-toggle', () => { + it('should forward the tabindex to the underlying button', () => { + const fixture = createComponent(DatepickerWithTabindexOnToggle, [MatNativeDateModule]); + fixture.detectChanges(); + + const button = fixture.nativeElement.querySelector('.mat-datepicker-toggle button'); + + expect(button.getAttribute('tabindex')).toBe('7'); + }); + + it('should clear the tabindex from the mat-datepicker-toggle host', () => { + const fixture = createComponent(DatepickerWithTabindexOnToggle, [MatNativeDateModule]); + fixture.detectChanges(); + + const host = fixture.nativeElement.querySelector('.mat-datepicker-toggle'); + + expect(host.hasAttribute('tabindex')).toBe(false); + }); + }); + describe('datepicker inside mat-form-field', () => { let fixture: ComponentFixture; let testComponent: FormFieldDatepicker; @@ -1875,3 +1895,16 @@ class DelayedDatepicker { date: Date | null; assignedDatepicker: MatDatepicker; } + + + +@Component({ + template: ` + + +
+
+ + `, +}) +class DatepickerWithTabindexOnToggle {} From a30e909f4b48bbf77886c860930378b28eb29c4f Mon Sep 17 00:00:00 2001 From: Artur Androsovych Date: Fri, 24 Aug 2018 17:59:05 +0300 Subject: [PATCH 141/189] fix(input): only monitor focus origin on browser platform (#11604) --- src/lib/input/input.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts index a42517c45bea..1fd02d06631c 100644 --- a/src/lib/input/input.ts +++ b/src/lib/input/input.ts @@ -254,10 +254,12 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl< } ngOnInit() { - this._autofillMonitor.monitor(this._elementRef).subscribe(event => { - this.autofilled = event.isAutofilled; - this.stateChanges.next(); - }); + if (this._platform.isBrowser) { + this._autofillMonitor.monitor(this._elementRef.nativeElement).subscribe(event => { + this.autofilled = event.isAutofilled; + this.stateChanges.next(); + }); + } } ngOnChanges() { @@ -266,7 +268,10 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl< ngOnDestroy() { this.stateChanges.complete(); - this._autofillMonitor.stopMonitoring(this._elementRef); + + if (this._platform.isBrowser) { + this._autofillMonitor.stopMonitoring(this._elementRef.nativeElement); + } } ngDoCheck() { From 1f78d8af7c0d612e2b997a41dc9b90afadf8fd56 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 24 Aug 2018 16:59:45 +0200 Subject: [PATCH 142/189] fix(select,autocomplete): unable to set custom id on mat-option (#11573) Fixes consumers not being allowed to set their own id on a `mat-option`. Fixes #11572. --- src/lib/core/option/option.spec.ts | 28 ++++++++++++++++++++-------- src/lib/core/option/option.ts | 7 +++---- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/lib/core/option/option.spec.ts b/src/lib/core/option/option.spec.ts index d1b4949d3b0b..dcab92f68961 100644 --- a/src/lib/core/option/option.spec.ts +++ b/src/lib/core/option/option.spec.ts @@ -9,12 +9,12 @@ describe('MatOption component', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [MatOptionModule], - declarations: [OptionWithDisable] + declarations: [BasicOption] }).compileComponents(); })); it('should complete the `stateChanges` stream on destroy', () => { - const fixture = TestBed.createComponent(OptionWithDisable); + const fixture = TestBed.createComponent(BasicOption); fixture.detectChanges(); const optionInstance: MatOption = @@ -28,7 +28,7 @@ describe('MatOption component', () => { }); it('should not emit to `onSelectionChange` if selecting an already-selected option', () => { - const fixture = TestBed.createComponent(OptionWithDisable); + const fixture = TestBed.createComponent(BasicOption); fixture.detectChanges(); const optionInstance: MatOption = @@ -50,7 +50,7 @@ describe('MatOption component', () => { }); it('should not emit to `onSelectionChange` if deselecting an unselected option', () => { - const fixture = TestBed.createComponent(OptionWithDisable); + const fixture = TestBed.createComponent(BasicOption); fixture.detectChanges(); const optionInstance: MatOption = @@ -71,14 +71,25 @@ describe('MatOption component', () => { subscription.unsubscribe(); }); + it('should be able to set a custom id', () => { + const fixture = TestBed.createComponent(BasicOption); + + fixture.componentInstance.id = 'custom-option'; + fixture.detectChanges(); + + const optionInstance = fixture.debugElement.query(By.directive(MatOption)).componentInstance; + + expect(optionInstance.id).toBe('custom-option'); + }); + describe('ripples', () => { - let fixture: ComponentFixture; + let fixture: ComponentFixture; let optionDebugElement: DebugElement; let optionNativeElement: HTMLElement; let optionInstance: MatOption; beforeEach(() => { - fixture = TestBed.createComponent(OptionWithDisable); + fixture = TestBed.createComponent(BasicOption); fixture.detectChanges(); optionDebugElement = fixture.debugElement.query(By.directive(MatOption)); @@ -117,8 +128,9 @@ describe('MatOption component', () => { }); @Component({ - template: `` + template: `` }) -class OptionWithDisable { +class BasicOption { disabled: boolean; + id: string; } diff --git a/src/lib/core/option/option.ts b/src/lib/core/option/option.ts index 3b0339c1804d..cd6412980058 100644 --- a/src/lib/core/option/option.ts +++ b/src/lib/core/option/option.ts @@ -88,21 +88,20 @@ export class MatOption implements AfterViewChecked, OnDestroy { private _selected = false; private _active = false; private _disabled = false; - private _id = `mat-option-${_uniqueIdCounter++}`; private _mostRecentViewValue = ''; /** Whether the wrapping component is in multiple selection mode. */ get multiple() { return this._parent && this._parent.multiple; } - /** The unique ID of the option. */ - get id(): string { return this._id; } - /** Whether or not the option is currently selected. */ get selected(): boolean { return this._selected; } /** The form value of the option. */ @Input() value: any; + /** The unique ID of the option. */ + @Input() id: string = `mat-option-${_uniqueIdCounter++}`; + /** Whether the option is disabled. */ @Input() get disabled() { return (this.group && this.group.disabled) || this._disabled; } From b98954049ebdd43c2d4fdc445ed907ab06dba281 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Sat, 25 Aug 2018 16:49:36 +0200 Subject: [PATCH 143/189] build: prevent publishing from the wrong branch (#12831) * Patch releases should be published from patch publish branches with the format: `{}.{}.x` * Minor releases should be published from minor publish branches with the format: `{}.x` * Major releases should be published from the `master` branch. Closes #12655 --- tools/gulp/gulpfile.ts | 5 +- tools/gulp/tasks/publish/branch-check.ts | 61 +++++++++++++++++++ .../{publish.ts => publish/publish-task.ts} | 41 ++++++------- .../tasks/{ => publish}/validate-release.ts | 4 +- 4 files changed, 86 insertions(+), 25 deletions(-) create mode 100644 tools/gulp/tasks/publish/branch-check.ts rename tools/gulp/tasks/{publish.ts => publish/publish-task.ts} (76%) rename tools/gulp/tasks/{ => publish}/validate-release.ts (97%) diff --git a/tools/gulp/gulpfile.ts b/tools/gulp/gulpfile.ts index c7bd8048df41..2def6858c270 100644 --- a/tools/gulp/gulpfile.ts +++ b/tools/gulp/gulpfile.ts @@ -28,7 +28,8 @@ import './tasks/example-module'; import './tasks/lint'; import './tasks/material-release'; import './tasks/payload'; -import './tasks/publish'; import './tasks/unit-test'; import './tasks/universal'; -import './tasks/validate-release'; + +import './tasks/publish/publish-task'; +import './tasks/publish/validate-release'; diff --git a/tools/gulp/tasks/publish/branch-check.ts b/tools/gulp/tasks/publish/branch-check.ts new file mode 100644 index 000000000000..33ff59919744 --- /dev/null +++ b/tools/gulp/tasks/publish/branch-check.ts @@ -0,0 +1,61 @@ +import {bold} from 'chalk'; +import {spawnSync} from 'child_process'; +import {buildConfig} from 'material2-build-tools'; + +/** Regular expression that matches version names and the individual version segments. */ +export const versionNameRegex = /^(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta|rc)\.(\d)+)?/; + +/** Regular expression that matches publish branch names and their Semver digits. */ +const publishBranchNameRegex = /^([0-9]+)\.([x0-9]+)(?:\.([x0-9]+))?$/; + +/** Checks if the specified version can be released from the current Git branch. */ +export function checkPublishBranch(version: string) { + const versionType = getSemverVersionType(version); + const branchName = spawnSync('git', ['symbolic-ref', '--short', 'HEAD'], + {cwd: buildConfig.projectDir}).stdout.toString().trim(); + + if (branchName === 'master') { + if (versionType === 'major') { + return; + } + + throw `Publishing of "${versionType}" releases should not happen inside of the ` + + `${bold('master')} branch.`; + } + + const branchNameMatch = branchName.match(publishBranchNameRegex) || []; + const branchDigits = branchNameMatch.slice(1, 4); + + if (branchDigits[2] === 'x' && versionType !== 'patch') { + throw `Cannot publish a "${versionType}" release inside of a patch branch (${branchName})`; + } + + if (branchDigits[1] === 'x' && versionType !== 'minor') { + throw `Cannot publish a "${versionType}" release inside of a minor branch (${branchName})`; + } + + throw `Cannot publish a "${versionType}" release from branch: "${branchName}". Releases should ` + + `be published from "master" or the according publish branch (e.g. "6.x", "6.4.x")`; +} + +/** + * Determines the type of the specified semver version. Can be either a major, minor or + * patch version. + */ +export function getSemverVersionType(version: string): 'major' | 'minor' | 'patch' { + const versionNameMatch = version.match(versionNameRegex); + + if (!versionNameMatch) { + throw `Could not parse version: ${version}. Cannot properly determine version type.`; + } + + const versionDigits = versionNameMatch.slice(1, 4); + + if (versionDigits[1] === '0' && versionDigits[2] === '0') { + return 'major'; + } else if (versionDigits[2] === '0') { + return 'minor'; + } else { + return 'patch'; + } +} diff --git a/tools/gulp/tasks/publish.ts b/tools/gulp/tasks/publish/publish-task.ts similarity index 76% rename from tools/gulp/tasks/publish.ts rename to tools/gulp/tasks/publish/publish-task.ts index 0cf92199b6d0..9b3faa5ba6fc 100644 --- a/tools/gulp/tasks/publish.ts +++ b/tools/gulp/tasks/publish/publish-task.ts @@ -1,11 +1,12 @@ +import {green, grey, red, yellow} from 'chalk'; import {spawn} from 'child_process'; import {existsSync, statSync} from 'fs-extra'; -import {join} from 'path'; import {task} from 'gulp'; -import {execTask} from '../util/task_helpers'; import {buildConfig, sequenceTask} from 'material2-build-tools'; -import {yellow, green, red, grey} from 'chalk'; import * as minimist from 'minimist'; +import {join} from 'path'; +import {execTask} from '../../util/task_helpers'; +import {checkPublishBranch, versionNameRegex} from './branch-check'; /** Packages that will be published to NPM by the release task. */ export const releasePackages = [ @@ -16,9 +17,6 @@ export const releasePackages = [ 'material-moment-adapter' ]; -/** Regular Expression that matches valid version numbers of Angular Material. */ -export const validVersionRegex = /^\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?$/; - /** Parse command-line arguments for release task. */ const argv = minimist(process.argv.slice(3)); @@ -50,38 +48,39 @@ task(':publish', async () => { const version = buildConfig.projectVersion; const currentDir = process.cwd(); - if (!version.match(validVersionRegex)) { - console.log(red(`Error: Cannot publish due to an invalid version name. Version "${version}" ` + - `is not following our semver format.`)); - console.log(yellow(`A version should follow this format: X.X.X, X.X.X-beta.X, X.X.X-alpha.X, ` + - `X.X.X-rc.X`)); + if (!version.match(versionNameRegex)) { + console.error(red(`Error: Cannot publish due to an invalid version name. Version ` + + `"${version}" is not following our semver format.`)); + console.error(yellow(`A version should follow this format: X.X.X, X.X.X-beta.X, ` + + `X.X.X-alpha.X, X.X.X-rc.X`)); return; } - console.log(''); + console.log(); if (!tag) { console.log(grey('> You can specify the tag by passing --tag=labelName.\n')); console.log(green(`Publishing version "${version}" to the latest tag...`)); } else { console.log(yellow(`Publishing version "${version}" to the ${tag} tag...`)); } - console.log(''); - + console.log(); if (version.match(/(alpha|beta|rc)/) && (!tag || tag === 'latest')) { - console.log(red(`Publishing ${version} to the "latest" tag is not allowed.`)); - console.log(red(`Alpha, Beta or RC versions shouldn't be published to "latest".`)); - console.log(); + console.error(red(`Publishing ${version} to the "latest" tag is not allowed.`)); + console.error(red(`Alpha, Beta or RC versions shouldn't be published to "latest".`)); + console.error(); return; } if (releasePackages.length > 1) { - console.warn(red('Warning: Multiple packages will be released if proceeding.')); - console.warn(red('Warning: Packages to be released:', releasePackages.join(', '))); - console.log(); + console.warn(yellow('Warning: Multiple packages will be released.')); + console.warn(yellow('Warning: Packages to be released:', releasePackages.join(', '))); + console.warn(); } - console.log(yellow('> Make sure to check the "angularVersion" in the build config.')); + checkPublishBranch(version); + + console.log(yellow('> Make sure to check the "requiredAngularVersion" in the package.json.')); console.log(yellow('> The version in the config defines the peer dependency of Angular.')); console.log(); diff --git a/tools/gulp/tasks/validate-release.ts b/tools/gulp/tasks/publish/validate-release.ts similarity index 97% rename from tools/gulp/tasks/validate-release.ts rename to tools/gulp/tasks/publish/validate-release.ts index b6ba651401a7..2857778058d2 100644 --- a/tools/gulp/tasks/validate-release.ts +++ b/tools/gulp/tasks/publish/validate-release.ts @@ -2,7 +2,7 @@ import {task} from 'gulp'; import {readFileSync, existsSync} from 'fs'; import {join} from 'path'; import {green, red} from 'chalk'; -import {releasePackages} from './publish'; +import {releasePackages} from './publish-task'; import {sync as glob} from 'glob'; import {spawnSync} from 'child_process'; import {buildConfig, sequenceTask} from 'material2-build-tools'; @@ -10,7 +10,7 @@ import {buildConfig, sequenceTask} from 'material2-build-tools'; const {projectDir, projectVersion, outputDir} = buildConfig; /** Git repository URL that has been read out from the project package.json file. */ -const repositoryGitUrl = require('../../../package.json').repository.url; +const repositoryGitUrl = require('../../../../package.json').repository.url; /** Path to the directory where all releases are created. */ const releasesDir = join(outputDir, 'releases'); From 0e60fb89c85dda18a5644a4df708fcb8f75eefc3 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Sun, 26 Aug 2018 17:36:56 +0200 Subject: [PATCH 144/189] fix(chips): chip list removing focus from first chip when adding through the input (#12840) Fix regression in 3da390e36df3a3f63695535c4f9fdac9b137eaee. Currently when the user removes all the chips and then they add a new chip, the chip list will remove focus from the input and put it on the chip. These changes introduce the proper behavior, which is to keep focus on the input. --- src/lib/chips/chip-list.spec.ts | 36 ++++++++++++++++++++++++++++++++- src/lib/chips/chip-list.ts | 13 +++++------- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/lib/chips/chip-list.spec.ts b/src/lib/chips/chip-list.spec.ts index f39987cbfa6a..da3239147f63 100644 --- a/src/lib/chips/chip-list.spec.ts +++ b/src/lib/chips/chip-list.spec.ts @@ -17,6 +17,7 @@ import { dispatchFakeEvent, dispatchKeyboardEvent, dispatchMouseEvent, + typeInElement, MockNgZone, } from '@angular/cdk/testing'; import { @@ -993,6 +994,31 @@ describe('MatChipList', () => { .not.toBeNull(`Expected placeholder to have an asterisk, as control was required.`); }); + it('should keep focus on the input after adding the first chip', fakeAsync(() => { + const nativeInput = fixture.nativeElement.querySelector('input'); + const chipEls = Array.from(fixture.nativeElement.querySelectorAll('.mat-chip')).reverse(); + + // Remove the chips via backspace to simulate the user removing them. + chipEls.forEach((chip: HTMLElement) => { + chip.focus(); + dispatchKeyboardEvent(chip, 'keydown', BACKSPACE); + fixture.detectChanges(); + tick(); + }); + + nativeInput.focus(); + expect(fixture.componentInstance.foods).toEqual([], 'Expected all chips to be removed.'); + expect(document.activeElement).toBe(nativeInput, 'Expected input to be focused.'); + + typeInElement('123', nativeInput); + fixture.detectChanges(); + dispatchKeyboardEvent(nativeInput, 'keydown', ENTER); + fixture.detectChanges(); + tick(); + + expect(document.activeElement).toBe(nativeInput, 'Expected input to remain focused.'); + })); + describe('keyboard behavior', () => { beforeEach(() => { chipListDebugElement = fixture.debugElement.query(By.directive(MatChipList)); @@ -1322,7 +1348,7 @@ class MultiSelectionChipList { - + {{ food.viewValue }} @@ -1369,6 +1395,14 @@ class InputChipList { } } + remove(food: any): void { + const index = this.foods.indexOf(food); + + if (index > -1) { + this.foods.splice(index, 1); + } + } + @ViewChild(MatChipList) chipList: MatChipList; @ViewChildren(MatChip) chips: QueryList; } diff --git a/src/lib/chips/chip-list.ts b/src/lib/chips/chip-list.ts index 3e0701692051..1e2074a90997 100644 --- a/src/lib/chips/chip-list.ts +++ b/src/lib/chips/chip-list.ts @@ -498,18 +498,15 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo } /** - * If the amount of chips changed, we need to update the key manager state and make sure - * that to so that we can refocus the - * next closest one. + * If the amount of chips changed, we need to update the + * key manager state and focus the next closest chip. */ protected _updateFocusForDestroyedChips() { - if (this._lastDestroyedChipIndex == null || !this.chips.length) { - return; + if (this._lastDestroyedChipIndex != null && this.chips.length) { + const newChipIndex = Math.min(this._lastDestroyedChipIndex, this.chips.length - 1); + this._keyManager.setActiveItem(newChipIndex); } - const newChipIndex = Math.min(this._lastDestroyedChipIndex, this.chips.length - 1); - - this._keyManager.setActiveItem(newChipIndex); this._lastDestroyedChipIndex = null; } From 8439c91249c8f20fd86d33371afe358d856c3a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Livora?= Date: Sun, 26 Aug 2018 18:10:23 +0200 Subject: [PATCH 145/189] docs(input): fix cdkTextareaAutosize name and link (#12830) --- src/lib/input/input.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/input/input.md b/src/lib/input/input.md index 07d1eb1b6d48..25188d615704 100644 --- a/src/lib/input/input.md +++ b/src/lib/input/input.md @@ -75,7 +75,7 @@ globally cause input errors to show when the input is dirty and invalid. ### Auto-resizing `