From 32c53088d171b949477df8198c26b51ebb8caa74 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 29 May 2024 09:06:41 -0700 Subject: [PATCH 01/61] test: fix overzealous calls to markForCheck from #29083 (#29131) --- .../live-announcer/live-announcer.spec.ts | 4 - src/cdk/clipboard/copy-to-clipboard.spec.ts | 4 - src/cdk/drag-drop/directives/drag.spec.ts | 759 +++--------------- .../drag-drop/directives/test-utils.spec.ts | 12 - src/cdk/drag-drop/drag-drop-registry.ts | 55 +- src/cdk/drag-drop/drag-drop.spec.ts | 2 - src/cdk/drag-drop/drag-ref.ts | 46 +- src/cdk/listbox/listbox.spec.ts | 90 +-- src/cdk/listbox/listbox.ts | 37 +- src/cdk/observers/observe-content.spec.ts | 6 - src/cdk/overlay/overlay-directives.spec.ts | 10 - 11 files changed, 208 insertions(+), 817 deletions(-) diff --git a/src/cdk/a11y/live-announcer/live-announcer.spec.ts b/src/cdk/a11y/live-announcer/live-announcer.spec.ts index 9c3efcbc4063..b1509f5e7edc 100644 --- a/src/cdk/a11y/live-announcer/live-announcer.spec.ts +++ b/src/cdk/a11y/live-announcer/live-announcer.spec.ts @@ -178,7 +178,6 @@ describe('LiveAnnouncer', () => { const overlayRef = overlay.create(); const componentRef = overlayRef.attach(portal); const modal = componentRef.location.nativeElement; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(ariaLiveElement.id).toBeTruthy(); @@ -199,7 +198,6 @@ describe('LiveAnnouncer', () => { const overlayRef = overlay.create(); const componentRef = overlayRef.attach(portal); const modal = componentRef.location.nativeElement; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); componentRef.instance.ariaOwns = 'foo bar'; @@ -320,7 +318,6 @@ describe('CdkAriaLive', () => { announcer = la; announcerSpy = spyOn(la, 'announce').and.callThrough(); fixture = TestBed.createComponent(DivWithCdkAriaLive); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); }), @@ -375,7 +372,6 @@ describe('CdkAriaLive', () => { expect(announcer.announce).toHaveBeenCalledTimes(1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); invokeMutationCallbacks(); flush(); diff --git a/src/cdk/clipboard/copy-to-clipboard.spec.ts b/src/cdk/clipboard/copy-to-clipboard.spec.ts index 1662553b8046..337dcaa01fc5 100644 --- a/src/cdk/clipboard/copy-to-clipboard.spec.ts +++ b/src/cdk/clipboard/copy-to-clipboard.spec.ts @@ -78,7 +78,6 @@ describe('CdkCopyToClipboard', () => { fixture.detectChanges(); fixture.nativeElement.querySelector('button')!.click(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(3); @@ -102,7 +101,6 @@ describe('CdkCopyToClipboard', () => { fixture.detectChanges(); fixture.nativeElement.querySelector('button')!.click(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(3); @@ -122,11 +120,9 @@ describe('CdkCopyToClipboard', () => { fixture.detectChanges(); spyOn(clipboard, 'beginCopy').and.returnValue(fakeCopy); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.nativeElement.querySelector('button')!.click(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(1); diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index a675c51dd214..fd07ecb51b25 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -12,6 +12,7 @@ import { import { AfterViewInit, ChangeDetectionStrategy, + ChangeDetectorRef, Component, ElementRef, ErrorHandler, @@ -22,6 +23,8 @@ import { ViewChild, ViewChildren, ViewEncapsulation, + inject, + signal, } from '@angular/core'; import {ComponentFixture, TestBed, fakeAsync, flush, tick} from '@angular/core/testing'; import {of as observableOf} from 'rxjs'; @@ -32,7 +35,7 @@ import {CdkDragDrop, CdkDragEnter, CdkDragStart} from '../drag-events'; import {DragRef, Point, PreviewContainer} from '../drag-ref'; import {moveItemInArray} from '../drag-utils'; -import {CDK_DRAG_CONFIG, DragDropConfig} from './config'; +import {CDK_DRAG_CONFIG, DragAxis, DragDropConfig} from './config'; import {CdkDrag} from './drag'; import {CdkDragHandle} from './drag-handle'; import {CdkDropList} from './drop-list'; @@ -95,7 +98,6 @@ describe('CdkDrag', () => { describe('mouse dragging', () => { it('should drag an element freely to a particular position', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -106,7 +108,6 @@ describe('CdkDrag', () => { it('should drag an element freely to a particular position when the page is scrolled', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const cleanup = makeScrollable(); @@ -121,7 +122,6 @@ describe('CdkDrag', () => { it('should continue dragging the element from where it was left off', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -136,7 +136,6 @@ describe('CdkDrag', () => { it('should continue dragging from where it was left off when the page is scrolled', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -156,7 +155,6 @@ describe('CdkDrag', () => { it('should not drag an element with the right mouse button', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const event = createMouseEvent('mousedown', 50, 100, 2); @@ -164,15 +162,12 @@ describe('CdkDrag', () => { expect(dragElement.style.transform).toBeFalsy(); dispatchEvent(dragElement, event); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dragElement.style.transform).toBeFalsy(); @@ -180,7 +175,6 @@ describe('CdkDrag', () => { it('should not drag the element if it was not moved more than the minimum distance', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable, [], 5); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -191,28 +185,22 @@ describe('CdkDrag', () => { it('should be able to stop dragging after a double click', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable, [], 5); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; expect(dragElement.style.transform).toBeFalsy(); dispatchMouseEvent(dragElement, 'mousedown'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(dragElement, 'mousedown'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dragElementViaMouse(fixture, dragElement, 50, 50); dispatchMouseEvent(document, 'mousemove', 100, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dragElement.style.transform).toBeFalsy(); @@ -220,7 +208,6 @@ describe('CdkDrag', () => { it('should preserve the previous `transform` value', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -231,7 +218,6 @@ describe('CdkDrag', () => { it('should not generate multiple own `translate3d` values', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -246,7 +232,6 @@ describe('CdkDrag', () => { it('should prevent the `mousedown` action for native draggable elements', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -256,11 +241,9 @@ describe('CdkDrag', () => { Object.defineProperty(mousedownEvent, 'target', {get: () => dragElement}); spyOn(mousedownEvent, 'preventDefault').and.callThrough(); dispatchEvent(dragElement, mousedownEvent); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mousemove', 50, 50); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(mousedownEvent.preventDefault).toHaveBeenCalled(); @@ -268,7 +251,6 @@ describe('CdkDrag', () => { it('should not start dragging an element with a fake mousedown event', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const event = createMouseEvent('mousedown', 0, 0); @@ -281,18 +263,14 @@ describe('CdkDrag', () => { expect(dragElement.style.transform).toBeFalsy(); dispatchEvent(dragElement, event); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mousemove', 20, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dragElement.style.transform).toBeFalsy(); @@ -300,13 +278,11 @@ describe('CdkDrag', () => { it('should prevent the default dragstart action', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const event = dispatchFakeEvent( fixture.componentInstance.dragElement.nativeElement, 'dragstart', ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(event.defaultPrevented).toBe(true); @@ -314,14 +290,12 @@ describe('CdkDrag', () => { it('should not prevent the default dragstart action when dragging is disabled', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); + fixture.componentInstance.dragDisabled.set(true); fixture.detectChanges(); - fixture.componentInstance.dragInstance.disabled = true; const event = dispatchFakeEvent( fixture.componentInstance.dragElement.nativeElement, 'dragstart', ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(event.defaultPrevented).toBe(false); @@ -331,7 +305,6 @@ describe('CdkDrag', () => { describe('touch dragging', () => { it('should drag an element freely to a particular position', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -342,7 +315,6 @@ describe('CdkDrag', () => { it('should drag an element freely to a particular position when the page is scrolled', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -357,7 +329,6 @@ describe('CdkDrag', () => { it('should continue dragging the element from where it was left off', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -372,7 +343,6 @@ describe('CdkDrag', () => { it('should continue dragging from where it was left off when the page is scrolled', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -392,11 +362,9 @@ describe('CdkDrag', () => { it('should prevent the default `touchmove` action on the page while dragging', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchTouchEvent(fixture.componentInstance.dragElement.nativeElement, 'touchstart'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dispatchTouchEvent(document, 'touchmove').defaultPrevented) @@ -407,13 +375,11 @@ describe('CdkDrag', () => { .toBe(true); dispatchTouchEvent(document, 'touchend'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); })); it('should not prevent `touchstart` action for native draggable elements', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -423,11 +389,9 @@ describe('CdkDrag', () => { Object.defineProperty(touchstartEvent, 'target', {get: () => dragElement}); spyOn(touchstartEvent, 'preventDefault').and.callThrough(); dispatchEvent(dragElement, touchstartEvent); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchTouchEvent(document, 'touchmove'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(touchstartEvent.preventDefault).not.toHaveBeenCalled(); @@ -435,7 +399,6 @@ describe('CdkDrag', () => { it('should not start dragging an element with a fake touchstart event', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const event = createTouchEvent('touchstart', 50, 50) as TouchEvent; @@ -449,18 +412,14 @@ describe('CdkDrag', () => { expect(dragElement.style.transform).toBeFalsy(); dispatchEvent(dragElement, event); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchTouchEvent(document, 'touchmove', 20, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchTouchEvent(document, 'touchmove', 50, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchTouchEvent(document, 'touchend'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dragElement.style.transform).toBeFalsy(); @@ -470,7 +429,6 @@ describe('CdkDrag', () => { describe('mouse dragging when initial transform is none', () => { it('should drag an element freely to a particular position', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; dragElement.style.transform = 'none'; @@ -482,7 +440,6 @@ describe('CdkDrag', () => { it('should dispatch an event when the user has started dragging', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); startDraggingViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement); @@ -501,7 +458,6 @@ describe('CdkDrag', () => { it('should dispatch an event when the user has stopped dragging', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 5, 10); @@ -522,7 +478,6 @@ describe('CdkDrag', () => { it('should include the drag distance in the ended event', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 25, 30); @@ -548,7 +503,6 @@ describe('CdkDrag', () => { it('should emit when the user is moving the drag element', () => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const spy = jasmine.createSpy('move spy'); @@ -565,7 +519,6 @@ describe('CdkDrag', () => { it('should not emit events if it was not moved more than the minimum distance', () => { const fixture = createComponent(StandaloneDraggable, [], 5); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const moveSpy = jasmine.createSpy('move spy'); @@ -582,7 +535,6 @@ describe('CdkDrag', () => { it('should complete the `moved` stream on destroy', () => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const spy = jasmine.createSpy('move spy'); @@ -595,9 +547,8 @@ describe('CdkDrag', () => { it('should be able to lock dragging along the x axis', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); + fixture.componentInstance.dragLockAxis.set('x'); fixture.detectChanges(); - fixture.componentInstance.dragInstance.lockAxis = 'x'; const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -612,10 +563,8 @@ describe('CdkDrag', () => { it('should be able to lock dragging along the x axis while using constrainPosition', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - fixture.componentInstance.dragInstance.lockAxis = 'x'; - fixture.componentInstance.dragInstance.constrainPosition = ( + fixture.componentInstance.dragLockAxis.set('x'); + fixture.componentInstance.constrainPosition = ( {x, y}: Point, _dragRef: DragRef, _dimensions: DOMRect, @@ -625,6 +574,8 @@ describe('CdkDrag', () => { y -= pickup.y; return {x, y}; }; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -639,9 +590,8 @@ describe('CdkDrag', () => { it('should be able to lock dragging along the y axis', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); + fixture.componentInstance.dragLockAxis.set('y'); fixture.detectChanges(); - fixture.componentInstance.dragInstance.lockAxis = 'y'; const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -656,11 +606,8 @@ describe('CdkDrag', () => { it('should be able to lock dragging along the y axis while using constrainPosition', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - - fixture.componentInstance.dragInstance.lockAxis = 'y'; - fixture.componentInstance.dragInstance.constrainPosition = ( + fixture.componentInstance.dragLockAxis.set('y'); + fixture.componentInstance.constrainPosition = ( {x, y}: Point, _dragRef: DragRef, _dimensions: DOMRect, @@ -670,6 +617,8 @@ describe('CdkDrag', () => { y -= pickup.y; return {x, y}; }; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -684,7 +633,6 @@ describe('CdkDrag', () => { it('should add a class while an element is being dragged', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const element = fixture.componentInstance.dragElement.nativeElement; @@ -696,7 +644,6 @@ describe('CdkDrag', () => { expect(element.classList).toContain('cdk-drag-dragging'); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(element.classList).not.toContain('cdk-drag-dragging'); @@ -704,7 +651,6 @@ describe('CdkDrag', () => { it('should add a class while an element is being dragged with OnPush change detection', fakeAsync(() => { const fixture = createComponent(StandaloneDraggableWithOnPush); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const element = fixture.componentInstance.dragElement.nativeElement; @@ -716,7 +662,6 @@ describe('CdkDrag', () => { expect(element.classList).toContain('cdk-drag-dragging'); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(element.classList).not.toContain('cdk-drag-dragging'); @@ -724,7 +669,6 @@ describe('CdkDrag', () => { it('should not add a class if item was not dragged more than the threshold', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable, [], 5); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const element = fixture.componentInstance.dragElement.nativeElement; @@ -756,7 +700,6 @@ describe('CdkDrag', () => { it('should be able to set the cdkDrag element as handle if it has a different root element', fakeAsync(() => { const fixture = createComponent(DraggableWithAlternateRootAndSelfHandle); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragRoot = fixture.componentInstance.dragRoot.nativeElement; @@ -780,7 +723,6 @@ describe('CdkDrag', () => { it('should be able to set an alternate drag root element for ng-container', fakeAsync(() => { const fixture = createComponent(DraggableNgContainerWithAlternateRoot); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragRoot = fixture.componentInstance.dragRoot.nativeElement; @@ -794,7 +736,6 @@ describe('CdkDrag', () => { it('should preserve the initial transform if the root element changes', fakeAsync(() => { const fixture = createComponent(DraggableWithAlternateRoot); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const alternateRoot = fixture.componentInstance.dragRoot.nativeElement; @@ -816,7 +757,6 @@ describe('CdkDrag', () => { it('should handle the root element selector changing after init', fakeAsync(() => { const fixture = createComponent(DraggableWithAlternateRoot); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(); @@ -838,14 +778,12 @@ describe('CdkDrag', () => { it('should not be able to drag the element if dragging is disabled', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; expect(dragElement.classList).not.toContain('cdk-drag-disabled'); - fixture.componentInstance.dragInstance.disabled = true; - fixture.changeDetectorRef.markForCheck(); + fixture.componentInstance.dragDisabled.set(true); fixture.detectChanges(); expect(dragElement.classList).toContain('cdk-drag-disabled'); @@ -856,15 +794,13 @@ describe('CdkDrag', () => { it('should enable native drag interactions if dragging is disabled', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const styles = dragElement.style; expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - fixture.componentInstance.dragInstance.disabled = true; - fixture.changeDetectorRef.markForCheck(); + fixture.componentInstance.dragDisabled.set(true); fixture.detectChanges(); expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); @@ -872,7 +808,6 @@ describe('CdkDrag', () => { it('should enable native drag interactions if not dragging', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const styles = dragElement.style; @@ -882,7 +817,6 @@ describe('CdkDrag', () => { it('should disable native drag interactions if dragging', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const styles = dragElement.style; @@ -891,7 +825,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, dragElement); dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); @@ -899,20 +832,17 @@ describe('CdkDrag', () => { it('should re-enable drag interactions once dragging is over', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const styles = dragElement.style; startDraggingViaMouse(fixture, dragElement); dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); dispatchMouseEvent(document, 'mouseup', 50, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); @@ -920,7 +850,6 @@ describe('CdkDrag', () => { it('should not stop propagation for the drag sequence start event by default', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -928,7 +857,6 @@ describe('CdkDrag', () => { spyOn(event, 'stopPropagation').and.callThrough(); dispatchEvent(dragElement, event); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(event.stopPropagation).not.toHaveBeenCalled(); @@ -944,7 +872,6 @@ describe('CdkDrag', () => { it('should enable native drag interactions on the drag item when there is a handle', () => { const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; expect(dragElement.style.touchAction).not.toBe('none'); @@ -952,7 +879,6 @@ describe('CdkDrag', () => { it('should disable native drag interactions on the drag handle', () => { const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const styles = fixture.componentInstance.handleElement.nativeElement.style; expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); @@ -960,7 +886,6 @@ describe('CdkDrag', () => { it('should enable native drag interactions on the drag handle if dragging is disabled', () => { const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.componentInstance.draggingDisabled = true; fixture.changeDetectorRef.markForCheck(); @@ -984,23 +909,19 @@ describe('CdkDrag', () => { it('should toggle native drag interactions based on whether the handle is disabled', () => { const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.componentInstance.handleInstance.disabled = true; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const styles = fixture.componentInstance.handleElement.nativeElement.style; expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); fixture.componentInstance.handleInstance.disabled = false; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); }); it('should be able to reset a freely-dragged item to its initial position', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1014,7 +935,6 @@ describe('CdkDrag', () => { it('should preserve initial transform after resetting', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1029,7 +949,6 @@ describe('CdkDrag', () => { it('should start dragging an item from its initial position after a reset', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1044,26 +963,21 @@ describe('CdkDrag', () => { it('should not dispatch multiple events for a mouse event right after a touch event', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; // Dispatch a touch sequence. dispatchTouchEvent(dragElement, 'touchstart'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchTouchEvent(dragElement, 'touchend'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(); // Immediately dispatch a mouse sequence to simulate a fake event. startDraggingViaMouse(fixture, dragElement); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(dragElement, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(); @@ -1073,7 +987,6 @@ describe('CdkDrag', () => { it('should round the transform value', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1085,7 +998,6 @@ describe('CdkDrag', () => { it('should allow for dragging to be constrained to an element', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); fixture.componentInstance.boundary = '.wrapper'; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1097,7 +1009,6 @@ describe('CdkDrag', () => { it('should allow for dragging to be constrained to an element while using constrainPosition', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); fixture.componentInstance.boundary = '.wrapper'; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.componentInstance.dragInstance.constrainPosition = ( @@ -1121,7 +1032,6 @@ describe('CdkDrag', () => { it('should be able to pass in a DOM node as the boundary', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); fixture.componentInstance.boundary = fixture.nativeElement.querySelector('.wrapper'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1134,7 +1044,6 @@ describe('CdkDrag', () => { const fixture = createComponent(StandaloneDraggable); const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); fixture.componentInstance.boundary = boundary; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1152,7 +1061,6 @@ describe('CdkDrag', () => { const fixture = createComponent(StandaloneDraggable); const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); fixture.componentInstance.boundary = boundary; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1170,7 +1078,6 @@ describe('CdkDrag', () => { const fixture = createComponent(StandaloneDraggable); const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); fixture.componentInstance.boundary = boundary; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1189,7 +1096,6 @@ describe('CdkDrag', () => { const fixture = createComponent(StandaloneDraggable); const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); fixture.componentInstance.boundary = boundary; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1210,7 +1116,6 @@ describe('CdkDrag', () => { const fixture = createComponent(StandaloneDraggable); const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); fixture.componentInstance.boundary = boundary; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1229,7 +1134,6 @@ describe('CdkDrag', () => { const fixture = createComponent(StandaloneDraggable); const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); fixture.componentInstance.boundary = boundary; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1251,7 +1155,6 @@ describe('CdkDrag', () => { } as Point); fixture.componentInstance.constrainPosition = spy; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1290,7 +1193,6 @@ describe('CdkDrag', () => { const fixture = createComponent(StandaloneDraggable); fixture.componentInstance.dragStartDelay = 1000; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1302,7 +1204,6 @@ describe('CdkDrag', () => { currentTime += 750; dispatchMouseEvent(document, 'mousemove', 50, 100); currentTime += 500; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dragElement.style.transform) @@ -1317,7 +1218,6 @@ describe('CdkDrag', () => { const fixture = createComponent(StandaloneDraggable); fixture.componentInstance.dragStartDelay = 1000; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const styles = dragElement.style; @@ -1330,7 +1230,6 @@ describe('CdkDrag', () => { currentTime += 750; dispatchMouseEvent(document, 'mousemove', 50, 100); currentTime += 500; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); @@ -1343,7 +1242,6 @@ describe('CdkDrag', () => { const fixture = createComponent(StandaloneDraggable); fixture.componentInstance.dragStartDelay = 500; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1352,14 +1250,12 @@ describe('CdkDrag', () => { .toBeFalsy(); dispatchMouseEvent(dragElement, 'mousedown'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); currentTime += 750; // The first `mousemove` here starts the sequence and the second one moves the element. dispatchMouseEvent(document, 'mousemove', 50, 100); dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dragElement.style.transform) @@ -1373,7 +1269,6 @@ describe('CdkDrag', () => { const fixture = createComponent(StandaloneDraggable); fixture.componentInstance.dragStartDelay = 500; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1382,7 +1277,6 @@ describe('CdkDrag', () => { .toBeFalsy(); dispatchTouchEvent(dragElement, 'touchstart'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); currentTime += 250; @@ -1396,7 +1290,6 @@ describe('CdkDrag', () => { const fixture = createComponent(StandaloneDraggable); fixture.componentInstance.dragStartDelay = '500'; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1405,14 +1298,12 @@ describe('CdkDrag', () => { .toBeFalsy(); dispatchMouseEvent(dragElement, 'mousedown'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); currentTime += 750; // The first `mousemove` here starts the sequence and the second one moves the element. dispatchMouseEvent(document, 'mousemove', 50, 100); dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dragElement.style.transform) @@ -1427,7 +1318,6 @@ describe('CdkDrag', () => { const fixture = createComponent(StandaloneDraggable); fixture.componentInstance.dragStartDelay = {touch: 500, mouse: 0}; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1448,7 +1338,6 @@ describe('CdkDrag', () => { it('should be able to get the current position', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1465,7 +1354,6 @@ describe('CdkDrag', () => { it('should be able to set the current position programmatically', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1492,7 +1380,6 @@ describe('CdkDrag', () => { it('should be able to get the up-to-date position as the user is dragging', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1502,13 +1389,11 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, dragElement); dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); dispatchMouseEvent(document, 'mousemove', 100, 200); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dragInstance.getFreeDragPosition()).toEqual({x: 100, y: 200}); @@ -1547,7 +1432,6 @@ describe('CdkDrag', () => { it('should include the dragged distance as the user is dragging', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const spy = jasmine.createSpy('moved spy'); @@ -1556,14 +1440,12 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, dragElement); dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); let event = spy.calls.mostRecent().args[0]; expect(event.distance).toEqual({x: 50, y: 100}); dispatchMouseEvent(document, 'mousemove', 75, 50); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); event = spy.calls.mostRecent().args[0]; @@ -1590,7 +1472,6 @@ describe('CdkDrag', () => { useValue: config, }, ]); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const drag = fixture.componentInstance.dragInstance; expect(drag.disabled).toBe(true); @@ -1605,7 +1486,6 @@ describe('CdkDrag', () => { it('should not throw if touches and changedTouches are empty', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1620,7 +1500,6 @@ describe('CdkDrag', () => { expect(() => { dispatchEvent(document, event); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(); }).not.toThrow(); @@ -1628,7 +1507,6 @@ describe('CdkDrag', () => { it('should update the free drag position if the page is scrolled', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const cleanup = makeScrollable(); @@ -1637,14 +1515,12 @@ describe('CdkDrag', () => { expect(dragElement.style.transform).toBeFalsy(); startDraggingViaMouse(fixture, dragElement, 0, 0); dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); scrollTo(0, 500); dispatchFakeEvent(document, 'scroll'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dragElement.style.transform).toBe('translate3d(50px, 600px, 0px)'); @@ -1656,7 +1532,6 @@ describe('CdkDrag', () => { 'is scrolled', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const cleanup = makeScrollable(); @@ -1665,17 +1540,14 @@ describe('CdkDrag', () => { expect(dragElement.style.transform).toBeFalsy(); startDraggingViaMouse(fixture, dragElement, 0, 0); dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); scrollTo(0, 500); dispatchFakeEvent(document, 'scroll'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mousemove', 50, 200); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dragElement.style.transform).toBe('translate3d(50px, 700px, 0px)'); @@ -1688,7 +1560,6 @@ describe('CdkDrag', () => { describe('draggable with a handle', () => { it('should not be able to drag the entire element if it has a handle', fakeAsync(() => { const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1699,7 +1570,6 @@ describe('CdkDrag', () => { it('should be able to drag an element using its handle', fakeAsync(() => { const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const handle = fixture.componentInstance.handleElement.nativeElement; @@ -1711,7 +1581,6 @@ describe('CdkDrag', () => { it('should not be able to drag the element if the handle is disabled', fakeAsync(() => { const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const handle = fixture.componentInstance.handleElement.nativeElement; @@ -1725,7 +1594,6 @@ describe('CdkDrag', () => { it('should not be able to drag the element if the handle is disabled before init', fakeAsync(() => { const fixture = createComponent(StandaloneDraggableWithPreDisabledHandle); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const handle = fixture.componentInstance.handleElement.nativeElement; @@ -1737,12 +1605,13 @@ describe('CdkDrag', () => { it('should not be able to drag using the handle if the element is disabled', fakeAsync(() => { const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const handle = fixture.componentInstance.handleElement.nativeElement; - fixture.componentInstance.dragInstance.disabled = true; + fixture.componentInstance.draggingDisabled = true; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); expect(dragElement.style.transform).toBeFalsy(); dragElementViaMouse(fixture, handle, 50, 100); @@ -1752,7 +1621,6 @@ describe('CdkDrag', () => { it('should be able to use a handle that was added after init', fakeAsync(() => { const fixture = createComponent(StandaloneDraggableWithDelayedHandle); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.componentInstance.showHandle = true; fixture.changeDetectorRef.markForCheck(); @@ -1766,9 +1634,8 @@ describe('CdkDrag', () => { expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); })); - it('should be able to use more than one handle to drag the element', fakeAsync(() => { + it('should be able to use more than one handle to drag the element', fakeAsync(async () => { const fixture = createComponent(StandaloneDraggableWithMultipleHandles); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; @@ -1784,7 +1651,6 @@ describe('CdkDrag', () => { it('should be able to drag with a handle that is not a direct descendant', fakeAsync(() => { const fixture = createComponent(StandaloneDraggableWithIndirectHandle); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const handle = fixture.componentInstance.handleElement.nativeElement; @@ -1809,7 +1675,6 @@ describe('CdkDrag', () => { } const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const handle = fixture.componentInstance.handleElement.nativeElement; @@ -1821,11 +1686,9 @@ describe('CdkDrag', () => { expect((dragElement.style as any).webkitTapHighlightColor).toBe('transparent'); dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mouseup', 50, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect((dragElement.style as any).webkitTapHighlightColor).toBeFalsy(); @@ -1838,7 +1701,6 @@ describe('CdkDrag', () => { } const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const handle = fixture.componentInstance.handleElement.nativeElement; @@ -1850,11 +1712,9 @@ describe('CdkDrag', () => { expect((dragElement.style as any).webkitTapHighlightColor).toBe('transparent'); dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mouseup', 50, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect((dragElement.style as any).webkitTapHighlightColor).toBe('purple'); @@ -1878,7 +1738,6 @@ describe('CdkDrag', () => { undefined, [ShadowWrapper], ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const handleChild = fixture.componentInstance.handleChild.nativeElement; @@ -1890,14 +1749,12 @@ describe('CdkDrag', () => { it('should prevent default dragStart on handle, not on entire draggable', fakeAsync(() => { const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const draggableEvent = dispatchFakeEvent( fixture.componentInstance.dragElement.nativeElement, 'dragstart', ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const handleEvent = dispatchFakeEvent( @@ -1905,7 +1762,6 @@ describe('CdkDrag', () => { 'dragstart', true, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(draggableEvent.defaultPrevented).toBe(false); @@ -1916,7 +1772,6 @@ describe('CdkDrag', () => { describe('in a drop container', () => { it('should be able to attach data to the drop container', () => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.dropInstance.data).toBe(fixture.componentInstance.items); @@ -1924,7 +1779,6 @@ describe('CdkDrag', () => { it('should register an item with the drop container', () => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const list = fixture.componentInstance.dropInstance; @@ -1939,7 +1793,6 @@ describe('CdkDrag', () => { it('should remove an item from the drop container', () => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const list = fixture.componentInstance.dropInstance; @@ -1955,7 +1808,6 @@ describe('CdkDrag', () => { it('should return the items sorted by their position in the DOM', () => { const fixture = createComponent(DraggableInDropZone); const items = fixture.componentInstance.items; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Insert a couple of items in the start and the middle so the list gets shifted around. @@ -1973,7 +1825,6 @@ describe('CdkDrag', () => { it('should sync the drop list inputs with the drop list ref', () => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dropInstance = fixture.componentInstance.dropInstance; @@ -1982,8 +1833,9 @@ describe('CdkDrag', () => { expect(dropListRef.lockAxis).toBeFalsy(); expect(dropListRef.disabled).toBe(false); - dropInstance.lockAxis = 'x'; - dropInstance.disabled = true; + fixture.componentInstance.dropLockAxis.set('x'); + fixture.componentInstance.dropDisabled.set(true); + fixture.detectChanges(); dropListRef.beforeStarted.next(); @@ -1993,7 +1845,6 @@ describe('CdkDrag', () => { it('should be able to attach data to a drag item', () => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.dragItems.first.data).toBe( @@ -2005,7 +1856,6 @@ describe('CdkDrag', () => { const fixture = createComponent(DraggableInDropZone); fixture.componentInstance.dropZoneId = 'custom-id'; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const drop = fixture.componentInstance.dropInstance; @@ -2016,7 +1866,6 @@ describe('CdkDrag', () => { it('should toggle a class when the user starts dragging an item', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const dropZone = fixture.componentInstance.dropInstance; @@ -2028,10 +1877,8 @@ describe('CdkDrag', () => { expect(dropZone.element.nativeElement.classList).toContain('cdk-drop-list-dragging'); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZone.element.nativeElement.classList).not.toContain('cdk-drop-dragging'); @@ -2039,7 +1886,6 @@ describe('CdkDrag', () => { it('should toggle the drop dragging classes if there is nothing to trigger change detection', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithoutEvents); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const dropZone = fixture.componentInstance.dropInstance; @@ -2053,10 +1899,8 @@ describe('CdkDrag', () => { expect(item.classList).toContain('cdk-drag-dragging'); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZone.element.nativeElement.classList).not.toContain('cdk-drop-dragging'); @@ -2065,7 +1909,6 @@ describe('CdkDrag', () => { it('should toggle a class when the user starts dragging an item with OnPush change detection', fakeAsync(() => { const fixture = createComponent(DraggableInOnPushDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const dropZone = fixture.componentInstance.dropInstance; @@ -2077,10 +1920,8 @@ describe('CdkDrag', () => { expect(dropZone.element.nativeElement.classList).toContain('cdk-drop-list-dragging'); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZone.element.nativeElement.classList).not.toContain('cdk-drop-dragging'); @@ -2088,7 +1929,6 @@ describe('CdkDrag', () => { it('should not toggle dragging class if the element was not dragged more than the threshold', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone, [], 5); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const dropZone = fixture.componentInstance.dropInstance; @@ -2102,7 +1942,6 @@ describe('CdkDrag', () => { it('should dispatch the `dropped` event when an item has been dropped', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; @@ -2123,7 +1962,6 @@ describe('CdkDrag', () => { thirdItemRect.top + 1, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -2154,7 +1992,6 @@ describe('CdkDrag', () => { it('should expose whether an item was dropped over a container', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; const firstItem = dragItems.first; @@ -2167,7 +2004,6 @@ describe('CdkDrag', () => { thirdItemRect.top + 1, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -2180,14 +2016,12 @@ describe('CdkDrag', () => { it('should expose the drag distance when an item is dropped', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; const firstItem = dragItems.first; dragElementViaMouse(fixture, firstItem.element.nativeElement, 50, 60); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -2201,7 +2035,6 @@ describe('CdkDrag', () => { it('should expose whether an item was dropped outside of a container', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; const firstItem = dragItems.first; @@ -2215,7 +2048,6 @@ describe('CdkDrag', () => { containerRect.bottom + 10, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -2228,7 +2060,6 @@ describe('CdkDrag', () => { it('should dispatch the `sorted` event as an item is being sorted', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.map(item => item.element.nativeElement); @@ -2242,7 +2073,6 @@ describe('CdkDrag', () => { const elementRect = items[i].getBoundingClientRect(); dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.top + 5); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.sortedSpy.calls.mostRecent().args[0]).toEqual({ @@ -2254,7 +2084,6 @@ describe('CdkDrag', () => { } dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); })); @@ -2265,7 +2094,6 @@ describe('CdkDrag', () => { fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.componentInstance.items = [fixture.componentInstance.items[0]]; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const draggedItem = fixture.componentInstance.dragItems.first.element.nativeElement; @@ -2275,14 +2103,12 @@ describe('CdkDrag', () => { for (let i = 0; i < 5; i++) { dispatchMouseEvent(document, 'mousemove', left, top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.sortedSpy).not.toHaveBeenCalled(); } dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); }), @@ -2290,7 +2116,6 @@ describe('CdkDrag', () => { it('should not move items in a vertical list if the pointer is too far away', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; @@ -2312,7 +2137,6 @@ describe('CdkDrag', () => { thirdItemRect.top + 1, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -2343,7 +2167,6 @@ describe('CdkDrag', () => { it('should not move the original element from its initial DOM position', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const root = fixture.nativeElement as HTMLElement; let dragElements = Array.from(root.querySelectorAll('.cdk-drag')); @@ -2363,7 +2186,6 @@ describe('CdkDrag', () => { thirdItemRect.top + 1, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dragElements = Array.from(root.querySelectorAll('.cdk-drag')); @@ -2372,7 +2194,6 @@ describe('CdkDrag', () => { it('should dispatch the `dropped` event in a horizontal drop zone', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; @@ -2393,7 +2214,6 @@ describe('CdkDrag', () => { thirdItemRect.top + 1, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -2431,7 +2251,6 @@ describe('CdkDrag', () => { ]); fixture.nativeElement.setAttribute('dir', 'rtl'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; @@ -2452,7 +2271,6 @@ describe('CdkDrag', () => { thirdItemRect.top + 1, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -2483,7 +2301,6 @@ describe('CdkDrag', () => { it('should not move items in a horizontal list if pointer is too far away', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; @@ -2505,7 +2322,6 @@ describe('CdkDrag', () => { thirdItemRect.bottom + 1000, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -2536,7 +2352,6 @@ describe('CdkDrag', () => { it('should calculate the index if the list is scrolled while dragging', fakeAsync(() => { const fixture = createComponent(DraggableInScrollableVerticalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; const firstItem = dragItems.first; @@ -2544,23 +2359,18 @@ describe('CdkDrag', () => { const list = fixture.componentInstance.dropInstance.element.nativeElement; startDraggingViaMouse(fixture, firstItem.element.nativeElement); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mousemove', thirdItemRect.left + 1, thirdItemRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); list.scrollTop = ITEM_HEIGHT * 10; dispatchFakeEvent(list, 'scroll'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -2595,7 +2405,6 @@ describe('CdkDrag', () => { [], ViewEncapsulation.ShadowDom, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; const firstItem = dragItems.first; @@ -2603,23 +2412,18 @@ describe('CdkDrag', () => { const list = fixture.componentInstance.dropInstance.element.nativeElement; startDraggingViaMouse(fixture, firstItem.element.nativeElement); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mousemove', thirdItemRect.left + 1, thirdItemRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); list.scrollTop = ITEM_HEIGHT * 10; dispatchFakeEvent(list, 'scroll'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -2659,23 +2463,18 @@ describe('CdkDrag', () => { const thirdItemRect = dragItems.toArray()[2].element.nativeElement.getBoundingClientRect(); startDraggingViaMouse(fixture, firstItem.element.nativeElement); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mousemove', thirdItemRect.left + 1, thirdItemRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); scrollTo(0, ITEM_HEIGHT * 10); dispatchFakeEvent(document, 'scroll'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -2701,7 +2500,6 @@ describe('CdkDrag', () => { it('should create a preview element while the item is dragged', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const itemRect = item.getBoundingClientRect(); @@ -2763,7 +2561,6 @@ describe('CdkDrag', () => { .toMatch(zeroPxRegex); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -2791,7 +2588,6 @@ describe('CdkDrag', () => { }, }, ]); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; startDraggingViaMouse(fixture, item); @@ -2803,7 +2599,6 @@ describe('CdkDrag', () => { it('should be able to constrain the preview position', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.componentInstance.boundarySelector = '.cdk-drop-list'; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const listRect = @@ -2816,7 +2611,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item, listRect.right + 50, listRect.bottom + 50); flush(); dispatchMouseEvent(document, 'mousemove', listRect.right + 50, listRect.bottom + 50); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const previewRect = preview.getBoundingClientRect(); @@ -2828,7 +2622,6 @@ describe('CdkDrag', () => { it('should update the boundary if the page is scrolled while dragging', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.componentInstance.boundarySelector = '.cdk-drop-list'; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -2841,7 +2634,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item, listRect.right, listRect.bottom); flush(); dispatchMouseEvent(document, 'mousemove', listRect.right, listRect.bottom); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; @@ -2850,11 +2642,9 @@ describe('CdkDrag', () => { scrollTo(0, 0); dispatchFakeEvent(document, 'scroll'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); listRect = list.getBoundingClientRect(); // We need to update these since we've scrolled. dispatchMouseEvent(document, 'mousemove', listRect.right, listRect.bottom); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); previewRect = preview.getBoundingClientRect(); @@ -2865,7 +2655,6 @@ describe('CdkDrag', () => { it('should update the boundary if a parent is scrolled while dragging', fakeAsync(() => { const fixture = createComponent(DraggableInScrollableParentContainer); fixture.componentInstance.boundarySelector = '.cdk-drop-list'; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const container: HTMLElement = fixture.nativeElement.querySelector('.scroll-container'); @@ -2879,7 +2668,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item, listRect.right, listRect.bottom); flush(); dispatchMouseEvent(document, 'mousemove', listRect.right, listRect.bottom); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; @@ -2891,11 +2679,9 @@ describe('CdkDrag', () => { container.scrollTop = 0; dispatchFakeEvent(container, 'scroll'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); listRect = list.getBoundingClientRect(); // We need to update these since we've scrolled. dispatchMouseEvent(document, 'mousemove', listRect.right, listRect.bottom); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); previewRect = preview.getBoundingClientRect(); @@ -2917,7 +2703,6 @@ describe('CdkDrag', () => { ViewEncapsulation.ShadowDom, ); fixture.componentInstance.boundarySelector = '.cdk-drop-list'; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const container: HTMLElement = @@ -2934,7 +2719,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item, listRect.right, listRect.bottom); flush(); dispatchMouseEvent(document, 'mousemove', listRect.right, listRect.bottom); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const preview = fixture.nativeElement.shadowRoot.querySelector( @@ -2948,11 +2732,9 @@ describe('CdkDrag', () => { container.scrollTop = 0; dispatchFakeEvent(container, 'scroll'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); listRect = list.getBoundingClientRect(); // We need to update these since we've scrolled. dispatchMouseEvent(document, 'mousemove', listRect.right, listRect.bottom); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); previewRect = preview.getBoundingClientRect(); @@ -2962,7 +2744,6 @@ describe('CdkDrag', () => { it('should clear the id from the preview', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; item.id = 'custom-id'; @@ -2976,7 +2757,6 @@ describe('CdkDrag', () => { it('should clone the content of descendant canvas elements', fakeAsync(() => { const fixture = createComponent(DraggableWithCanvasInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const sourceCanvas = item.querySelector('canvas') as HTMLCanvasElement; @@ -3004,7 +2784,6 @@ describe('CdkDrag', () => { it('should not throw when cloning an invalid canvas', fakeAsync(() => { const fixture = createComponent(DraggableWithInvalidCanvasInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -3018,7 +2797,6 @@ describe('CdkDrag', () => { it('should clone the content of descendant input elements', fakeAsync(() => { const fixture = createComponent(DraggableWithInputsInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const sourceInput = item.querySelector('input')!; @@ -3044,7 +2822,6 @@ describe('CdkDrag', () => { it('should preserve checked state for radio inputs in the content', fakeAsync(() => { const fixture = createComponent(DraggableWithRadioInputsInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[2].element.nativeElement; const sourceRadioInput = item.querySelector('input[type="radio"]')!; @@ -3078,7 +2855,6 @@ describe('CdkDrag', () => { it('should clear the ids from descendants of the preview', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const extraChild = document.createElement('div'); @@ -3094,7 +2870,6 @@ describe('CdkDrag', () => { it('should not create a preview if the element was not dragged far enough', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone, [], 5); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -3111,7 +2886,6 @@ describe('CdkDrag', () => { }, ]); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -3124,7 +2898,6 @@ describe('CdkDrag', () => { it('should remove the preview if its `transitionend` event timed out', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -3137,11 +2910,9 @@ describe('CdkDrag', () => { // Move somewhere so the draggable doesn't exit immediately. dispatchMouseEvent(document, 'mousemove', 50, 50); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(250); @@ -3159,7 +2930,6 @@ describe('CdkDrag', () => { it('should be able to set a single class on a preview', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.componentInstance.previewClass = 'custom-class'; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -3172,7 +2942,6 @@ describe('CdkDrag', () => { it('should be able to set multiple classes on a preview', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.componentInstance.previewClass = ['custom-class-1', 'custom-class-2']; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -3185,7 +2954,6 @@ describe('CdkDrag', () => { it('should emit the released event as soon as the item is released', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1]; const endedSpy = jasmine.createSpy('ended spy'); @@ -3202,11 +2970,9 @@ describe('CdkDrag', () => { // Move somewhere so the draggable doesn't exit immediately. dispatchMouseEvent(document, 'mousemove', 50, 50); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expected the released event to fire immediately upon release. @@ -3222,7 +2988,6 @@ describe('CdkDrag', () => { it('should reset immediately when failed drag happens after a successful one', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const itemInstance = fixture.componentInstance.dragItems.toArray()[1]; @@ -3245,7 +3010,6 @@ describe('CdkDrag', () => { // Dispatch the mouseup immediately to simulate the user not moving the element. dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(0); // Important to tick with 0 since we don't want to flush any pending timeouts. @@ -3256,7 +3020,6 @@ describe('CdkDrag', () => { it('should not wait for transition that are not on the `transform` property', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -3266,11 +3029,9 @@ describe('CdkDrag', () => { preview.style.transition = 'opacity 500ms ease'; dispatchMouseEvent(document, 'mousemove', 50, 50); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(0); @@ -3281,7 +3042,6 @@ describe('CdkDrag', () => { it('should pick out the `transform` duration if multiple properties are being transitioned', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -3291,11 +3051,9 @@ describe('CdkDrag', () => { preview.style.transition = 'opacity 500ms ease, transform 1000ms ease'; dispatchMouseEvent(document, 'mousemove', 50, 50); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(500); @@ -3314,7 +3072,6 @@ describe('CdkDrag', () => { it('should create a placeholder element while the item is dragged', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const initialParent = item.parentNode; @@ -3335,7 +3092,6 @@ describe('CdkDrag', () => { .toBe('none'); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -3372,7 +3128,6 @@ describe('CdkDrag', () => { it('should insert the preview into a particular element, if specified', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const previewContainer = fixture.componentInstance.alternatePreviewContainer; @@ -3389,7 +3144,6 @@ describe('CdkDrag', () => { it('should remove the id from the placeholder', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -3404,7 +3158,6 @@ describe('CdkDrag', () => { it('should clear the ids from descendants of the placeholder', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const extraChild = document.createElement('div'); @@ -3420,7 +3173,6 @@ describe('CdkDrag', () => { it('should not create placeholder if the element was not dragged far enough', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone, [], 5); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -3431,7 +3183,6 @@ describe('CdkDrag', () => { it('should move the placeholder as an item is being sorted down', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); assertDownwardSorting( fixture, @@ -3443,7 +3194,6 @@ describe('CdkDrag', () => { it('should move the placeholder as an item is being sorted down on a scrolled page', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const cleanup = makeScrollable(); @@ -3459,7 +3209,6 @@ describe('CdkDrag', () => { it('should move the placeholder as an item is being sorted up', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); assertUpwardSorting( fixture, @@ -3471,7 +3220,6 @@ describe('CdkDrag', () => { it('should move the placeholder as an item is being sorted up on a scrolled page', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const cleanup = makeScrollable(); @@ -3487,7 +3235,6 @@ describe('CdkDrag', () => { it('should move the placeholder as an item is being sorted to the right', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.toArray(); @@ -3504,20 +3251,17 @@ describe('CdkDrag', () => { // Add a few pixels to the left offset so we get some overlap. dispatchMouseEvent(document, 'mousemove', elementRect.left + 5, elementRect.top); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(getElementIndexByPosition(placeholder, 'left')).toBe(i); } dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); })); it('should move the placeholder as an item is being sorted to the left', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.toArray(); @@ -3534,13 +3278,11 @@ describe('CdkDrag', () => { // Remove a few pixels from the right offset so we get some overlap. dispatchMouseEvent(document, 'mousemove', elementRect.right - 5, elementRect.top); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(getElementIndexByPosition(placeholder, 'left')).toBe(i); } dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); })); @@ -3550,7 +3292,6 @@ describe('CdkDrag', () => { 'sorting vertically', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); @@ -3564,7 +3305,6 @@ describe('CdkDrag', () => { // Add a few pixels to the top offset so we get some overlap. dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top + 5); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect( @@ -3572,7 +3312,6 @@ describe('CdkDrag', () => { ).toEqual(['One', 'Two', 'Three', 'Zero']); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); }), @@ -3580,7 +3319,6 @@ describe('CdkDrag', () => { it('should lay out the elements correctly, when swapping down with a taller element', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); @@ -3598,21 +3336,18 @@ describe('CdkDrag', () => { // Add a few pixels to the top offset so we get some overlap. dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top + 5); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(placeholder.style.transform).toBe(`translate3d(0px, ${ITEM_HEIGHT}px, 0px)`); expect(target.style.transform).toBe(`translate3d(0px, ${-ITEM_HEIGHT * 2}px, 0px)`); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); })); it('should lay out the elements correctly, when swapping up with a taller element', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); @@ -3630,21 +3365,18 @@ describe('CdkDrag', () => { // Add a few pixels to the top offset so we get some overlap. dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom - 5); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(placeholder.style.transform).toBe(`translate3d(0px, ${-ITEM_HEIGHT}px, 0px)`); expect(target.style.transform).toBe(`translate3d(0px, ${ITEM_HEIGHT * 2}px, 0px)`); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); })); it('should lay out elements correctly, when swapping an item with margin', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); @@ -3662,14 +3394,12 @@ describe('CdkDrag', () => { // Add a few pixels to the top offset so we get some overlap. dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top + 5); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(placeholder.style.transform).toBe(`translate3d(0px, ${ITEM_HEIGHT + 12}px, 0px)`); expect(target.style.transform).toBe(`translate3d(0px, ${-ITEM_HEIGHT - 12}px, 0px)`); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); })); @@ -3679,7 +3409,6 @@ describe('CdkDrag', () => { 'sorting horizontally', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); @@ -3693,7 +3422,6 @@ describe('CdkDrag', () => { // Add a few pixels to the left offset so we get some overlap. dispatchMouseEvent(document, 'mousemove', targetRect.right - 5, targetRect.top); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect( @@ -3701,7 +3429,6 @@ describe('CdkDrag', () => { ).toEqual(['One', 'Two', 'Three', 'Zero']); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); }), @@ -3709,7 +3436,6 @@ describe('CdkDrag', () => { it('should lay out the elements correctly, when swapping to the right with a wider element', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); @@ -3726,21 +3452,18 @@ describe('CdkDrag', () => { const targetRect = target.getBoundingClientRect(); dispatchMouseEvent(document, 'mousemove', targetRect.right - 5, targetRect.top); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(placeholder.style.transform).toBe(`translate3d(${ITEM_WIDTH}px, 0px, 0px)`); expect(target.style.transform).toBe(`translate3d(${-ITEM_WIDTH * 2}px, 0px, 0px)`); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); })); it('should lay out the elements correctly, when swapping left with a wider element', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); @@ -3757,21 +3480,18 @@ describe('CdkDrag', () => { const targetRect = target.getBoundingClientRect(); dispatchMouseEvent(document, 'mousemove', targetRect.right - 5, targetRect.top); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(placeholder.style.transform).toBe(`translate3d(${-ITEM_WIDTH}px, 0px, 0px)`); expect(target.style.transform).toBe(`translate3d(${ITEM_WIDTH * 2}px, 0px, 0px)`); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); })); it('should lay out elements correctly, when horizontally swapping an item with margin', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); @@ -3788,21 +3508,18 @@ describe('CdkDrag', () => { const targetRect = target.getBoundingClientRect(); dispatchMouseEvent(document, 'mousemove', targetRect.right - 5, targetRect.top); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(placeholder.style.transform).toBe(`translate3d(${ITEM_WIDTH + 12}px, 0px, 0px)`); expect(target.style.transform).toBe(`translate3d(${-ITEM_WIDTH - 12}px, 0px, 0px)`); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); })); it('should not swap position for tiny pointer movements', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); @@ -3826,7 +3543,6 @@ describe('CdkDrag', () => { // Move over the target so there's a 20px overlap. dispatchMouseEvent(document, 'mousemove', targetRect.left, pointerTop); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) .withContext('Expected position to swap.') @@ -3834,21 +3550,18 @@ describe('CdkDrag', () => { // Move down a further 1px. dispatchMouseEvent(document, 'mousemove', targetRect.left, pointerTop + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) .withContext('Expected positions not to swap.') .toEqual(['One', 'Zero', 'Two', 'Three']); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); })); it('should swap position for pointer movements in the opposite direction', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); @@ -3872,7 +3585,6 @@ describe('CdkDrag', () => { // Move over the target so there's a 20px overlap. dispatchMouseEvent(document, 'mousemove', targetRect.left, pointerTop); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) .withContext('Expected position to swap.') @@ -3880,14 +3592,12 @@ describe('CdkDrag', () => { // Move up 10px. dispatchMouseEvent(document, 'mousemove', targetRect.left, pointerTop - 10); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) .withContext('Expected positions to swap again.') .toEqual(['Zero', 'One', 'Two', 'Three']); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); })); @@ -3897,7 +3607,6 @@ describe('CdkDrag', () => { 'overlap with the sibling item after the previous swap', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); @@ -3918,10 +3627,8 @@ describe('CdkDrag', () => { // Trigger a mouse move coming from the bottom so that the list thinks that we're // sorting upwards. This usually how a user would behave with a mouse pointer. dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom + 50); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom - 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect( @@ -3931,7 +3638,6 @@ describe('CdkDrag', () => { // Refresh the rect since the element position has changed. targetRect = target.getBoundingClientRect(); dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom - 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect( @@ -3939,7 +3645,6 @@ describe('CdkDrag', () => { ).toEqual(['One', 'Two', 'Zero', 'Three']); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); }), @@ -3947,7 +3652,6 @@ describe('CdkDrag', () => { it('should clean up the preview element if the item is destroyed mid-drag', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -3970,7 +3674,6 @@ describe('CdkDrag', () => { it('should be able to customize the preview element', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPreview); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -3985,7 +3688,6 @@ describe('CdkDrag', () => { it('should handle the custom preview being removed', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPreview); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -4005,7 +3707,6 @@ describe('CdkDrag', () => { it('should be able to constrain the position of a custom preview', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPreview); fixture.componentInstance.boundarySelector = '.cdk-drop-list'; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const listRect = @@ -4018,7 +3719,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item, listRect.right + 50, listRect.bottom + 50); flush(); dispatchMouseEvent(document, 'mousemove', listRect.right + 50, listRect.bottom + 50); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const previewRect = preview.getBoundingClientRect(); @@ -4035,7 +3735,6 @@ describe('CdkDrag', () => { } as Point); fixture.componentInstance.constrainPosition = spy; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -4046,7 +3745,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item, 200, 200); flush(); dispatchMouseEvent(document, 'mousemove', 200, 200); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const previewRect = preview.getBoundingClientRect(); @@ -4066,7 +3764,6 @@ describe('CdkDrag', () => { 'preview has stopped', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPreview); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragContainer = fixture.componentInstance.dropInstance.element.nativeElement; @@ -4079,7 +3776,6 @@ describe('CdkDrag', () => { // The coordinates don't matter. dragElementViaMouse(fixture, item, 10, 10); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dragContainer.contains(item)) @@ -4090,7 +3786,6 @@ describe('CdkDrag', () => { it('should position custom previews next to the pointer', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPreview); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -4103,7 +3798,6 @@ describe('CdkDrag', () => { it('should keep the preview next to the trigger if the page was scrolled', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPreview); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const cleanup = makeScrollable(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -4114,12 +3808,10 @@ describe('CdkDrag', () => { expect(preview.style.transform).toBe('translate3d(50px, 50px, 0px)'); scrollTo(0, 500); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Move the pointer a bit so the preview has to reposition. dispatchMouseEvent(document, 'mousemove', 55, 55); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(preview.style.transform).toBe('translate3d(55px, 555px, 0px)'); @@ -4129,18 +3821,17 @@ describe('CdkDrag', () => { it('should lock position inside a drop container along the x axis', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPreview); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - const item = fixture.componentInstance.dragItems.toArray()[1]; - const element = item.element.nativeElement; + const element = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; - item.lockAxis = 'x'; + fixture.componentInstance.items[1].lockAxis = 'x'; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); startDraggingViaMouse(fixture, element, 50, 50); dispatchMouseEvent(element, 'mousemove', 100, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; @@ -4150,18 +3841,17 @@ describe('CdkDrag', () => { it('should lock position inside a drop container along the y axis', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPreview); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - const item = fixture.componentInstance.dragItems.toArray()[1]; - const element = item.element.nativeElement; + const element = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; - item.lockAxis = 'y'; + fixture.componentInstance.items[1].lockAxis = 'y'; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); startDraggingViaMouse(fixture, element, 50, 50); dispatchMouseEvent(element, 'mousemove', 100, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; @@ -4171,17 +3861,16 @@ describe('CdkDrag', () => { it('should inherit the position locking from the drop container', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPreview); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const element = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; - fixture.componentInstance.dropInstance.lockAxis = 'x'; + fixture.componentInstance.dropLockAxis.set('x'); + fixture.detectChanges(); startDraggingViaMouse(fixture, element, 50, 50); dispatchMouseEvent(element, 'mousemove', 100, 100); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; @@ -4192,7 +3881,6 @@ describe('CdkDrag', () => { it('should be able to set a class on a custom preview', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPreview); fixture.componentInstance.previewClass = 'custom-class'; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -4206,7 +3894,6 @@ describe('CdkDrag', () => { it('should be able to apply the size of the dragged element to a custom preview', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPreview); fixture.componentInstance.matchPreviewSize = true; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const itemRect = item.getBoundingClientRect(); @@ -4226,7 +3913,6 @@ describe('CdkDrag', () => { fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPreview); fixture.componentInstance.matchPreviewSize = true; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -4240,13 +3926,11 @@ describe('CdkDrag', () => { it('should not have the size of the inserted preview affect the size applied via matchSize', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalFlexDropZoneWithMatchSizePreview); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const itemRect = item.getBoundingClientRect(); startDraggingViaMouse(fixture, item); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; @@ -4258,7 +3942,6 @@ describe('CdkDrag', () => { it('should not throw when custom preview only has text', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomTextOnlyPreview); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -4274,7 +3957,6 @@ describe('CdkDrag', () => { it('should handle custom preview with multiple root nodes', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomMultiNodePreview); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -4290,7 +3972,6 @@ describe('CdkDrag', () => { it('should be able to customize the placeholder', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPlaceholder); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -4305,7 +3986,6 @@ describe('CdkDrag', () => { it('should handle the custom placeholder being removed', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPlaceholder); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -4327,7 +4007,6 @@ describe('CdkDrag', () => { it('should measure the custom placeholder after the first change detection', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPlaceholder); fixture.componentInstance.extraPlaceholderClass = 'tall-placeholder'; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; const item = dragItems.toArray()[0].element.nativeElement; @@ -4338,14 +4017,11 @@ describe('CdkDrag', () => { const thirdItemRect = thirdItem.getBoundingClientRect(); dispatchMouseEvent(document, 'mousemove', thirdItemRect.left + 1, thirdItemRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; @@ -4354,7 +4030,6 @@ describe('CdkDrag', () => { it('should not throw when custom placeholder only has text', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomTextOnlyPlaceholder); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -4370,7 +4045,6 @@ describe('CdkDrag', () => { it('should handle custom placeholder with multiple root nodes', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomMultiNodePlaceholder); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -4386,7 +4060,6 @@ describe('CdkDrag', () => { it('should clear the `transform` value from siblings when item is dropped`', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; @@ -4397,16 +4070,13 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, firstItem.element.nativeElement); dispatchMouseEvent(document, 'mousemove', thirdItemRect.left + 1, thirdItemRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(thirdItem.style.transform).toBeTruthy(); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(thirdItem.style.transform).toBeFalsy(); @@ -4414,15 +4084,13 @@ describe('CdkDrag', () => { it('should not move the item if the list is disabled', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; const dropElement = fixture.componentInstance.dropInstance.element.nativeElement; expect(dropElement.classList).not.toContain('cdk-drop-list-disabled'); - fixture.componentInstance.dropInstance.disabled = true; - fixture.changeDetectorRef.markForCheck(); + fixture.componentInstance.dropDisabled.set(true); fixture.detectChanges(); expect(dropElement.classList).toContain('cdk-drop-list-disabled'); @@ -4443,7 +4111,6 @@ describe('CdkDrag', () => { thirdItemRect.top + 1, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); @@ -4458,20 +4125,16 @@ describe('CdkDrag', () => { it('should not throw if the `touches` array is empty', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; dispatchTouchEvent(item, 'touchstart'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchTouchEvent(document, 'touchmove'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchTouchEvent(document, 'touchmove', 50, 50); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(() => { @@ -4479,14 +4142,12 @@ describe('CdkDrag', () => { Object.defineProperty(endEvent, 'touches', {get: () => []}); dispatchEvent(document, endEvent); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }).not.toThrow(); })); it('should not move the item if the group is disabled', fakeAsync(() => { const fixture = createComponent(ConnectedDropZonesViaGroupDirective); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.groupedDragItems[0]; @@ -4511,7 +4172,6 @@ describe('CdkDrag', () => { thirdItemRect.top + 1, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); @@ -4526,7 +4186,6 @@ describe('CdkDrag', () => { it('should not sort an item if sorting the list is disabled', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dropInstance = fixture.componentInstance.dropInstance; @@ -4551,7 +4210,6 @@ describe('CdkDrag', () => { const placeholder = document.querySelector('.cdk-drag-placeholder') as HTMLElement; dispatchMouseEvent(document, 'mousemove', targetX, targetY); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(getElementIndexByPosition(placeholder, 'top')) @@ -4559,11 +4217,9 @@ describe('CdkDrag', () => { .toBe(0); dispatchMouseEvent(document, 'mouseup', targetX, targetY); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -4594,7 +4250,6 @@ describe('CdkDrag', () => { it('should not throw if an item is removed after dragging has started', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; const firstElement = dragItems.first.element.nativeElement; @@ -4611,7 +4266,6 @@ describe('CdkDrag', () => { expect(() => { // Move the dragged item over where the remove item would've been. dispatchMouseEvent(document, 'mousemove', lastItemRect.left + 1, lastItemRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); }).not.toThrow(); @@ -4619,7 +4273,6 @@ describe('CdkDrag', () => { it('should not be able to start a drag sequence while another one is still active', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const [item, otherItem] = fixture.componentInstance.dragItems.toArray(); @@ -4638,14 +4291,12 @@ describe('CdkDrag', () => { it('should should be able to disable auto-scrolling', fakeAsync(() => { const fixture = createComponent(DraggableInScrollableVerticalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.first.element.nativeElement; const list = fixture.componentInstance.dropInstance.element.nativeElement; const listRect = list.getBoundingClientRect(); fixture.componentInstance.dropInstance.autoScrollDisabled = true; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(list.scrollTop).toBe(0); @@ -4657,7 +4308,6 @@ describe('CdkDrag', () => { listRect.left + listRect.width / 2, listRect.top + listRect.height, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -4666,7 +4316,6 @@ describe('CdkDrag', () => { it('should auto-scroll down if the user holds their pointer at bottom edge', fakeAsync(() => { const fixture = createComponent(DraggableInScrollableVerticalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.first.element.nativeElement; const list = fixture.componentInstance.dropInstance.element.nativeElement; @@ -4681,7 +4330,6 @@ describe('CdkDrag', () => { listRect.left + listRect.width / 2, listRect.top + listRect.height, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -4690,7 +4338,6 @@ describe('CdkDrag', () => { it('should auto-scroll up if the user holds their pointer at top edge', fakeAsync(() => { const fixture = createComponent(DraggableInScrollableVerticalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.first.element.nativeElement; const list = fixture.componentInstance.dropInstance.element.nativeElement; @@ -4699,7 +4346,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item); dispatchMouseEvent(document, 'mousemove', listRect.left + listRect.width / 2, listRect.top); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -4708,7 +4354,6 @@ describe('CdkDrag', () => { it('should auto-scroll right if the user holds their pointer at right edge in ltr', fakeAsync(() => { const fixture = createComponent(DraggableInScrollableHorizontalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.first.element.nativeElement; const list = fixture.componentInstance.dropInstance.element.nativeElement; @@ -4723,7 +4368,6 @@ describe('CdkDrag', () => { listRect.left + listRect.width, listRect.top + listRect.height / 2, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -4732,7 +4376,6 @@ describe('CdkDrag', () => { it('should auto-scroll left if the user holds their pointer at left edge in ltr', fakeAsync(() => { const fixture = createComponent(DraggableInScrollableHorizontalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.first.element.nativeElement; const list = fixture.componentInstance.dropInstance.element.nativeElement; @@ -4741,7 +4384,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item); dispatchMouseEvent(document, 'mousemove', listRect.left, listRect.top + listRect.height / 2); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -4756,7 +4398,6 @@ describe('CdkDrag', () => { }, ]); fixture.nativeElement.setAttribute('dir', 'rtl'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.first.element.nativeElement; const list = fixture.componentInstance.dropInstance.element.nativeElement; @@ -4770,7 +4411,6 @@ describe('CdkDrag', () => { listRect.left + listRect.width, listRect.top + listRect.height / 2, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -4785,7 +4425,6 @@ describe('CdkDrag', () => { }, ]); fixture.nativeElement.setAttribute('dir', 'rtl'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.first.element.nativeElement; const list = fixture.componentInstance.dropInstance.element.nativeElement; @@ -4795,7 +4434,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item); dispatchMouseEvent(document, 'mousemove', listRect.left, listRect.top + listRect.height / 2); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -4805,7 +4443,6 @@ describe('CdkDrag', () => { it('should be able to start auto scrolling with a drag boundary', fakeAsync(() => { const fixture = createComponent(DraggableInScrollableHorizontalDropZone); fixture.componentInstance.boundarySelector = '.drop-list'; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.first.element.nativeElement; const list = fixture.componentInstance.dropInstance.element.nativeElement; @@ -4820,7 +4457,6 @@ describe('CdkDrag', () => { listRect.left + listRect.width, listRect.top + listRect.height / 2, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -4829,7 +4465,6 @@ describe('CdkDrag', () => { it('should stop scrolling if the user moves their pointer away', fakeAsync(() => { const fixture = createComponent(DraggableInScrollableVerticalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.first.element.nativeElement; const list = fixture.componentInstance.dropInstance.element.nativeElement; @@ -4844,7 +4479,6 @@ describe('CdkDrag', () => { listRect.left + listRect.width / 2, listRect.top + listRect.height, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -4858,7 +4492,6 @@ describe('CdkDrag', () => { listRect.left + listRect.width / 2, listRect.top + listRect.height / 2, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -4867,7 +4500,6 @@ describe('CdkDrag', () => { it('should stop scrolling if the user stops dragging', fakeAsync(() => { const fixture = createComponent(DraggableInScrollableVerticalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.first.element.nativeElement; const list = fixture.componentInstance.dropInstance.element.nativeElement; @@ -4882,7 +4514,6 @@ describe('CdkDrag', () => { listRect.left + listRect.width / 2, listRect.top + listRect.height, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -4890,7 +4521,6 @@ describe('CdkDrag', () => { expect(previousScrollTop).toBeGreaterThan(0); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -4899,7 +4529,6 @@ describe('CdkDrag', () => { it('should auto-scroll viewport down if the pointer is close to bottom edge', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const cleanup = makeScrollable(); @@ -4911,7 +4540,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item); dispatchMouseEvent(document, 'mousemove', viewportSize.width / 2, viewportSize.height); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -4922,7 +4550,6 @@ describe('CdkDrag', () => { it('should auto-scroll viewport up if the pointer is close to top edge', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const cleanup = makeScrollable(); @@ -4936,7 +4563,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item); dispatchMouseEvent(document, 'mousemove', viewportSize.width / 2, 0); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -4947,7 +4573,6 @@ describe('CdkDrag', () => { it('should auto-scroll viewport right if the pointer is near right edge', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const cleanup = makeScrollable('horizontal'); @@ -4959,7 +4584,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item); dispatchMouseEvent(document, 'mousemove', viewportSize.width, viewportSize.height / 2); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -4970,7 +4594,6 @@ describe('CdkDrag', () => { it('should auto-scroll viewport left if the pointer is close to left edge', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const cleanup = makeScrollable('horizontal'); @@ -4984,7 +4607,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item); dispatchMouseEvent(document, 'mousemove', 0, viewportSize.height / 2); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -4998,7 +4620,6 @@ describe('CdkDrag', () => { 'both the list and the viewport', fakeAsync(() => { const fixture = createComponent(DraggableInScrollableVerticalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const list = fixture.componentInstance.dropInstance.element.nativeElement; @@ -5025,7 +4646,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item); dispatchMouseEvent(document, 'mousemove', listRect.left + listRect.width / 2, 0); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -5041,7 +4661,6 @@ describe('CdkDrag', () => { 'and the viewport, if the list cannot be scrolled in that direction', fakeAsync(() => { const fixture = createComponent(DraggableInScrollableVerticalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const list = fixture.componentInstance.dropInstance.element.nativeElement; @@ -5068,7 +4687,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item); dispatchMouseEvent(document, 'mousemove', listRect.left + listRect.width / 2, 0); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -5081,7 +4699,6 @@ describe('CdkDrag', () => { it('should be able to auto-scroll a parent container', fakeAsync(() => { const fixture = createComponent(DraggableInScrollableParentContainer); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.first.element.nativeElement; const container = fixture.nativeElement.querySelector('.scroll-container'); @@ -5096,7 +4713,6 @@ describe('CdkDrag', () => { containerRect.left + containerRect.width / 2, containerRect.top + containerRect.height, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(20); @@ -5105,7 +4721,6 @@ describe('CdkDrag', () => { it('should be able to configure the auto-scroll speed', fakeAsync(() => { const fixture = createComponent(DraggableInScrollableVerticalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.componentInstance.dropInstance.autoScrollStep = 20; const item = fixture.componentInstance.dragItems.first.element.nativeElement; @@ -5121,7 +4736,6 @@ describe('CdkDrag', () => { listRect.left + listRect.width / 2, listRect.top + listRect.height, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tickAnimationFrames(10); @@ -5130,7 +4744,6 @@ describe('CdkDrag', () => { it('should pick up descendants inside of containers', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithContainer); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; const firstItem = dragItems.first; @@ -5143,7 +4756,6 @@ describe('CdkDrag', () => { thirdItemRect.top + 1, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -5167,7 +4779,6 @@ describe('CdkDrag', () => { it('should not pick up items from descendant drop lists', fakeAsync(() => { const fixture = createComponent(NestedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const {dragItems, innerList, outerList} = fixture.componentInstance; const innerClasses = innerList.nativeElement.classList; @@ -5184,7 +4795,6 @@ describe('CdkDrag', () => { ); startDraggingViaMouse(fixture, dragItems.first.element.nativeElement); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(innerClasses) @@ -5195,7 +4805,6 @@ describe('CdkDrag', () => { it('should be able to re-enable a disabled drop list', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; const tryDrag = () => { @@ -5208,7 +4817,6 @@ describe('CdkDrag', () => { thirdItemRect.top + 1, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }; @@ -5219,8 +4827,7 @@ describe('CdkDrag', () => { 'Three', ]); - fixture.componentInstance.dropInstance.disabled = true; - fixture.changeDetectorRef.markForCheck(); + fixture.componentInstance.dropDisabled.set(true); fixture.detectChanges(); tryDrag(); @@ -5231,8 +4838,7 @@ describe('CdkDrag', () => { 'Three', ]); - fixture.componentInstance.dropInstance.disabled = false; - fixture.changeDetectorRef.markForCheck(); + fixture.componentInstance.dropDisabled.set(false); fixture.detectChanges(); tryDrag(); @@ -5259,7 +4865,6 @@ describe('CdkDrag', () => { useValue: config, }, ]); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const list = fixture.componentInstance.dropList; expect(list.disabled).toBe(true); @@ -5271,7 +4876,6 @@ describe('CdkDrag', () => { it('should disable scroll snapping while the user is dragging', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const styles: any = fixture.componentInstance.dropInstance.element.nativeElement.style; @@ -5288,10 +4892,8 @@ describe('CdkDrag', () => { expect(styles.scrollSnapType || styles.msScrollSnapType).toBe('none'); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(styles.scrollSnapType || styles.msScrollSnapType).toBeFalsy(); @@ -5299,7 +4901,6 @@ describe('CdkDrag', () => { it('should restore the previous inline scroll snap value', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const styles: any = fixture.componentInstance.dropInstance.element.nativeElement.style; @@ -5317,10 +4918,8 @@ describe('CdkDrag', () => { expect(styles.scrollSnapType || styles.msScrollSnapType).toBe('none'); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(styles.scrollSnapType || styles.msScrollSnapType).toBe('block'); @@ -5328,7 +4927,6 @@ describe('CdkDrag', () => { it('should be able to start dragging again if the dragged item is destroyed', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); let item = fixture.componentInstance.dragItems.first; @@ -5357,7 +4955,6 @@ describe('CdkDrag', () => { .toBeTruthy(); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -5366,7 +4963,6 @@ describe('CdkDrag', () => { it('should make the placeholder available in the start event', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; let placeholder: HTMLElement | undefined; @@ -5381,7 +4977,6 @@ describe('CdkDrag', () => { it('should not move item into position not allowed by the sort predicate', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; const spy = jasmine.createSpy('sort predicate spy').and.returnValue(false); @@ -5399,7 +4994,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, firstItem.element.nativeElement); dispatchMouseEvent(document, 'mousemove', thirdItemRect.left + 1, thirdItemRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(spy).toHaveBeenCalledWith(2, firstItem, fixture.componentInstance.dropInstance); @@ -5411,7 +5005,6 @@ describe('CdkDrag', () => { ]); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -5434,7 +5027,6 @@ describe('CdkDrag', () => { it('should not call the sort predicate for the same index', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const spy = jasmine.createSpy('sort predicate spy').and.returnValue(true); fixture.componentInstance.dropInstance.sortPredicate = spy; @@ -5444,7 +5036,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item); dispatchMouseEvent(document, 'mousemove', itemRect.left + 10, itemRect.top + 10); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(spy).not.toHaveBeenCalled(); @@ -5459,7 +5050,6 @@ describe('CdkDrag', () => { it('should preserve the original `transform` of items in the list', fakeAsync(() => { const fixture = createComponent(DraggableInScrollableVerticalDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.map(item => item.element.nativeElement); items.forEach(element => (element.style.transform = 'rotate(180deg)')); @@ -5468,7 +5058,6 @@ describe('CdkDrag', () => { element.style.transform.indexOf('rotate(180deg)') > -1; startDraggingViaMouse(fixture, items[0]); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const preview = document.querySelector('.cdk-drag-preview') as HTMLElement; const placeholder = fixture.nativeElement.querySelector('.cdk-drag-placeholder'); @@ -5484,7 +5073,6 @@ describe('CdkDrag', () => { .toBe(true); dispatchMouseEvent(document, 'mousemove', thirdItemRect.left + 1, thirdItemRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(items.every(hasInitialTransform)) .withContext('Expected items to preserve transform while dragging.') @@ -5497,10 +5085,8 @@ describe('CdkDrag', () => { .toBe(true); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(items.every(hasInitialTransform)) .withContext('Expected items to preserve transform when dragging stops.') @@ -5516,7 +5102,6 @@ describe('CdkDrag', () => { it('should sort correctly if the node has been offset', fakeAsync(() => { const documentElement = document.documentElement!; const fixture = createComponent(DraggableInDropZone); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); documentElement.style.position = 'absolute'; @@ -5537,7 +5122,6 @@ describe('CdkDrag', () => { describe('in a connected drop container', () => { it('should dispatch the `dropped` event when an item has been dropped into a new container', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -5551,7 +5135,6 @@ describe('CdkDrag', () => { targetRect.top + 1, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -5573,7 +5156,6 @@ describe('CdkDrag', () => { it('should be able to move the element over a new container and return it', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -5592,7 +5174,6 @@ describe('CdkDrag', () => { .toBe(true); dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[1].contains(placeholder)) @@ -5600,7 +5181,6 @@ describe('CdkDrag', () => { .toBe(true); dispatchMouseEvent(document, 'mousemove', initialRect.left + 1, initialRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[0].contains(placeholder)) @@ -5608,7 +5188,6 @@ describe('CdkDrag', () => { .toBe(true); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); @@ -5619,7 +5198,6 @@ describe('CdkDrag', () => { 'one, even if it no longer matches the enterPredicate', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -5629,7 +5207,6 @@ describe('CdkDrag', () => { const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); fixture.componentInstance.dropInstances.first.enterPredicate = () => false; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); startDraggingViaMouse(fixture, item.element.nativeElement); @@ -5642,7 +5219,6 @@ describe('CdkDrag', () => { .toBe(true); dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[1].contains(placeholder)) @@ -5650,7 +5226,6 @@ describe('CdkDrag', () => { .toBe(true); dispatchMouseEvent(document, 'mousemove', initialRect.left + 1, initialRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[0].contains(placeholder)) @@ -5658,7 +5233,6 @@ describe('CdkDrag', () => { .toBe(true); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); @@ -5667,7 +5241,6 @@ describe('CdkDrag', () => { it('should transfer the DOM element from one drop zone to another', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems.slice(); @@ -5679,7 +5252,6 @@ describe('CdkDrag', () => { // after dragged item is removed from first container dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; @@ -5700,11 +5272,9 @@ describe('CdkDrag', () => { it('should not be able to transfer an item into a container that is not in `connectedTo`', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - fixture.componentInstance.dropInstances.forEach(d => (d.connectedTo = [])); - fixture.changeDetectorRef.markForCheck(); + fixture.componentInstance.todoConnectedTo.set([]); + fixture.componentInstance.doneConnectedTo.set([]); + fixture.componentInstance.extraConnectedTo.set([]); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems.slice(); @@ -5714,7 +5284,6 @@ describe('CdkDrag', () => { dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top + 1); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; @@ -5736,10 +5305,8 @@ describe('CdkDrag', () => { it('should not be able to transfer an item that does not match the `enterPredicate`', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.componentInstance.dropInstances.forEach(d => (d.enterPredicate = () => false)); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems.slice(); @@ -5749,7 +5316,6 @@ describe('CdkDrag', () => { dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top + 1); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; @@ -5770,7 +5336,6 @@ describe('CdkDrag', () => { it('should call the `enterPredicate` with the item and the container it is entering', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dropInstances = fixture.componentInstance.dropInstances.toArray(); @@ -5780,7 +5345,6 @@ describe('CdkDrag', () => { const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); dropInstances[1].enterPredicate = spy; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dragElementViaMouse( @@ -5790,7 +5354,6 @@ describe('CdkDrag', () => { targetRect.top + 1, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(spy).toHaveBeenCalledWith(dragItem, dropInstances[1]); @@ -5798,7 +5361,6 @@ describe('CdkDrag', () => { it('should be able to start dragging after an item has been transferred', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -5810,7 +5372,6 @@ describe('CdkDrag', () => { [1, -1].forEach(offset => { dragElementViaMouse(fixture, element, targetRect.left + offset, targetRect.top + offset); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }); @@ -5822,7 +5383,6 @@ describe('CdkDrag', () => { // Make sure there's only one item in the first list. fixture.componentInstance.todo = ['things']; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -5842,7 +5402,6 @@ describe('CdkDrag', () => { .toBe(true); dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[1].contains(placeholder)) @@ -5850,7 +5409,6 @@ describe('CdkDrag', () => { .toBe(true); dispatchMouseEvent(document, 'mousemove', initialRect.left + 1, initialRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[0].contains(placeholder)) @@ -5858,7 +5416,6 @@ describe('CdkDrag', () => { .toBe(true); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); @@ -5869,7 +5426,6 @@ describe('CdkDrag', () => { // Make sure there's only one item in the first list. fixture.componentInstance.todo = ['things']; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dropInstances = fixture.componentInstance.dropInstances.toArray(); @@ -5887,7 +5443,6 @@ describe('CdkDrag', () => { expect(placeholder).toBeTruthy(); dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(targetElement.previousSibling === placeholder) @@ -5902,13 +5457,11 @@ describe('CdkDrag', () => { // Swap with target dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.bottom - 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Drop and verify item drop positon and coontainer dispatchMouseEvent(document, 'mouseup', targetRect.left + 1, targetRect.bottom - 1); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; @@ -5932,7 +5485,6 @@ describe('CdkDrag', () => { // Make sure there's only one item in the first list. fixture.componentInstance.todo = ['things']; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -5955,7 +5507,6 @@ describe('CdkDrag', () => { .toBe(true); dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[1].firstElementChild === placeholder) @@ -5970,7 +5521,6 @@ describe('CdkDrag', () => { // Make sure there's only one item in the first list. fixture.componentInstance.todo = ['things']; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -5996,7 +5546,6 @@ describe('CdkDrag', () => { .toBe(true); dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[1].lastChild === placeholder) @@ -6011,7 +5560,6 @@ describe('CdkDrag', () => { // Make sure there's only one item in the first list. fixture.componentInstance.todo = ['things']; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -6025,14 +5573,12 @@ describe('CdkDrag', () => { expect(() => { dragElementViaMouse(fixture, item.element.nativeElement, targetRect.left, targetRect.top); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }).not.toThrow(); })); it('should assign a default id on each drop zone', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect( @@ -6044,16 +5590,13 @@ describe('CdkDrag', () => { it('should be able to connect two drop zones by id', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - const dropInstances = fixture.componentInstance.dropInstances.toArray(); + const [todoDropInstance, doneDropInstance] = + fixture.componentInstance.dropInstances.toArray(); - dropInstances[0].id = 'todo'; - dropInstances[1].id = 'done'; - dropInstances[0].connectedTo = ['done']; - dropInstances[1].connectedTo = ['todo']; - fixture.changeDetectorRef.markForCheck(); + fixture.componentInstance.todoConnectedTo.set([doneDropInstance.id]); + fixture.componentInstance.doneConnectedTo.set([todoDropInstance.id]); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -6062,7 +5605,6 @@ describe('CdkDrag', () => { dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top + 1); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; @@ -6072,8 +5614,8 @@ describe('CdkDrag', () => { previousIndex: 1, currentIndex: 3, item: groups[0][1], - container: dropInstances[1], - previousContainer: dropInstances[0], + container: doneDropInstance, + previousContainer: todoDropInstance, isPointerOverContainer: true, distance: {x: jasmine.any(Number), y: jasmine.any(Number)}, dropPoint: {x: jasmine.any(Number), y: jasmine.any(Number)}, @@ -6083,7 +5625,6 @@ describe('CdkDrag', () => { it('should be able to connect two drop zones using the drop list group', fakeAsync(() => { const fixture = createComponent(ConnectedDropZonesViaGroupDirective); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dropInstances = fixture.componentInstance.dropInstances.toArray(); @@ -6093,7 +5634,6 @@ describe('CdkDrag', () => { dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top + 1); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; @@ -6114,14 +5654,12 @@ describe('CdkDrag', () => { it('should be able to pass a single id to `connectedTo`', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - const dropInstances = fixture.componentInstance.dropInstances.toArray(); + const [todoDropInstance, doneDropInstance] = + fixture.componentInstance.dropInstances.toArray(); - dropInstances[1].id = 'done'; - dropInstances[0].connectedTo = ['done']; - fixture.changeDetectorRef.markForCheck(); + fixture.componentInstance.todoConnectedTo.set([doneDropInstance.id]); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -6130,7 +5668,6 @@ describe('CdkDrag', () => { dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top + 1); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; @@ -6140,8 +5677,8 @@ describe('CdkDrag', () => { previousIndex: 1, currentIndex: 3, item: groups[0][1], - container: dropInstances[1], - previousContainer: dropInstances[0], + container: doneDropInstance, + previousContainer: todoDropInstance, isPointerOverContainer: true, distance: {x: jasmine.any(Number), y: jasmine.any(Number)}, dropPoint: {x: jasmine.any(Number), y: jasmine.any(Number)}, @@ -6154,7 +5691,6 @@ describe('CdkDrag', () => { 'with one draggable item', fakeAsync(() => { const fixture = createComponent(ConnectedDropZonesWithSingleItems); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.toArray(); @@ -6178,7 +5714,6 @@ describe('CdkDrag', () => { targetRect.top + 1, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -6211,49 +5746,47 @@ describe('CdkDrag', () => { 'even if it is not connected to the current container', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; - const dropInstances = fixture.componentInstance.dropInstances.toArray(); - const dropZones = dropInstances.map(d => d.element.nativeElement); + const [todoDropInstance, doneDropInstance] = + fixture.componentInstance.dropInstances.toArray(); + const todoZone = todoDropInstance.element.nativeElement; + const doneZone = doneDropInstance.element.nativeElement; const item = groups[0][1]; const initialRect = item.element.nativeElement.getBoundingClientRect(); const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); // Change the `connectedTo` so the containers are only connected one-way. - dropInstances[0].connectedTo = dropInstances[1]; - dropInstances[1].connectedTo = []; + fixture.componentInstance.todoConnectedTo.set([doneDropInstance]); + fixture.componentInstance.doneConnectedTo.set([]); + fixture.detectChanges(); startDraggingViaMouse(fixture, item.element.nativeElement); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!; + const placeholder = todoZone.querySelector('.cdk-drag-placeholder')!; expect(placeholder).toBeTruthy(); - expect(dropZones[0].contains(placeholder)) + expect(todoZone.contains(placeholder)) .withContext('Expected placeholder to be inside the first container.') .toBe(true); dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - expect(dropZones[1].contains(placeholder)) + expect(doneZone.contains(placeholder)) .withContext('Expected placeholder to be inside second container.') .toBe(true); dispatchMouseEvent(document, 'mousemove', initialRect.left + 1, initialRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - expect(dropZones[0].contains(placeholder)) + expect(todoZone.contains(placeholder)) .withContext('Expected placeholder to be back inside first container.') .toBe(true); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); @@ -6263,7 +5796,6 @@ describe('CdkDrag', () => { it('should not add child drop lists to the same group as their parents', fakeAsync(() => { const fixture = createComponent(NestedDropListGroups); const component = fixture.componentInstance; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(Array.from(component.group._items)).toEqual([component.listOne, component.listTwo]); @@ -6271,7 +5803,6 @@ describe('CdkDrag', () => { it('should not be able to drop an element into a container that is under another element', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems.slice(); @@ -6294,7 +5825,6 @@ describe('CdkDrag', () => { dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top + 1); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; @@ -6315,7 +5845,6 @@ describe('CdkDrag', () => { it('should set a class when a container can receive an item', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement); @@ -6326,7 +5855,6 @@ describe('CdkDrag', () => { .toBe(true); startDraggingViaMouse(fixture, item.element.nativeElement); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[0].classList).not.toContain( @@ -6341,7 +5869,6 @@ describe('CdkDrag', () => { it('should toggle the `receiving` class when the item enters a new list', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -6365,7 +5892,6 @@ describe('CdkDrag', () => { .toContain('cdk-drop-list-receiving'); dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[0].classList) @@ -6379,7 +5905,6 @@ describe('CdkDrag', () => { it('should not set the receiving class if the item does not match the enter predicate', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.componentInstance.dropInstances.toArray()[1].enterPredicate = () => false; @@ -6391,7 +5916,6 @@ describe('CdkDrag', () => { .toBe(true); startDraggingViaMouse(fixture, item.element.nativeElement); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones.every(c => !c.classList.contains('cdk-drop-list-receiving'))) @@ -6404,7 +5928,6 @@ describe('CdkDrag', () => { 'does not match', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.componentInstance.dropInstances.toArray()[0].enterPredicate = () => false; @@ -6428,7 +5951,6 @@ describe('CdkDrag', () => { .toContain('cdk-drop-list-receiving'); dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[0].classList) @@ -6446,7 +5968,6 @@ describe('CdkDrag', () => { const fixture = createComponent(ConnectedDropListsInOnPush, undefined, undefined, [ DraggableInOnPushDropZone, ]); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dropZones = Array.from( @@ -6459,7 +5980,6 @@ describe('CdkDrag', () => { .toBe(true); startDraggingViaMouse(fixture, item); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[0].classList) @@ -6476,28 +5996,29 @@ describe('CdkDrag', () => { 'dropping it into the final one', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - const dropInstances = fixture.componentInstance.dropInstances.toArray(); - dropInstances[0].connectedTo = [dropInstances[1], dropInstances[2]]; - dropInstances[1].connectedTo = []; - dropInstances[2].connectedTo = []; - fixture.changeDetectorRef.markForCheck(); + const [todoDropInstance, doneDropInstance, extraDropInstance] = + fixture.componentInstance.dropInstances.toArray(); + fixture.componentInstance.todoConnectedTo.set([doneDropInstance, extraDropInstance]); + fixture.componentInstance.doneConnectedTo.set([]); + fixture.componentInstance.extraConnectedTo.set([]); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; - const dropZones = dropInstances.map(d => d.element.nativeElement); + const todoZone = todoDropInstance.element.nativeElement; + const doneZone = doneDropInstance.element.nativeElement; + const extraZone = extraDropInstance.element.nativeElement; const item = groups[0][1]; - const intermediateRect = dropZones[1].getBoundingClientRect(); - const finalRect = dropZones[2].getBoundingClientRect(); + const intermediateRect = doneZone.getBoundingClientRect(); + const finalRect = extraZone.getBoundingClientRect(); startDraggingViaMouse(fixture, item.element.nativeElement); - const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!; + const placeholder = todoZone.querySelector('.cdk-drag-placeholder')!; expect(placeholder).toBeTruthy(); - expect(dropZones[0].contains(placeholder)) + expect(todoZone.contains(placeholder)) .withContext('Expected placeholder to be inside the first container.') .toBe(true); @@ -6507,26 +6028,22 @@ describe('CdkDrag', () => { intermediateRect.left + 1, intermediateRect.top + 1, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - expect(dropZones[1].contains(placeholder)) + expect(doneZone.contains(placeholder)) .withContext('Expected placeholder to be inside second container.') .toBe(true); dispatchMouseEvent(document, 'mousemove', finalRect.left + 1, finalRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - expect(dropZones[2].contains(placeholder)) + expect(extraZone.contains(placeholder)) .withContext('Expected placeholder to be inside third container.') .toBe(true); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; @@ -6537,8 +6054,8 @@ describe('CdkDrag', () => { previousIndex: 1, currentIndex: 0, item: groups[0][1], - container: dropInstances[2], - previousContainer: dropInstances[0], + container: extraDropInstance, + previousContainer: todoDropInstance, isPointerOverContainer: false, distance: {x: jasmine.any(Number), y: jasmine.any(Number)}, dropPoint: {x: jasmine.any(Number), y: jasmine.any(Number)}, @@ -6553,28 +6070,29 @@ describe('CdkDrag', () => { 'not connected to by passing it over an intermediate one that is', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - const dropInstances = fixture.componentInstance.dropInstances.toArray(); - dropInstances[0].connectedTo = [dropInstances[1]]; - dropInstances[1].connectedTo = [dropInstances[0], dropInstances[2]]; - dropInstances[2].connectedTo = [dropInstances[1]]; - fixture.changeDetectorRef.markForCheck(); + const [todoDropInstance, doneDropInstance, extraDropInstance] = + fixture.componentInstance.dropInstances.toArray(); + fixture.componentInstance.todoConnectedTo.set([doneDropInstance]); + fixture.componentInstance.doneConnectedTo.set([todoDropInstance, extraDropInstance]); + fixture.componentInstance.extraConnectedTo.set([doneDropInstance]); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; - const dropZones = dropInstances.map(d => d.element.nativeElement); + const todoZone = todoDropInstance.element.nativeElement; + const doneZone = doneDropInstance.element.nativeElement; + const extraZone = extraDropInstance.element.nativeElement; const item = groups[0][1]; - const intermediateRect = dropZones[1].getBoundingClientRect(); - const finalRect = dropZones[2].getBoundingClientRect(); + const intermediateRect = doneZone.getBoundingClientRect(); + const finalRect = extraZone.getBoundingClientRect(); startDraggingViaMouse(fixture, item.element.nativeElement); - const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!; + const placeholder = todoZone.querySelector('.cdk-drag-placeholder')!; expect(placeholder).toBeTruthy(); - expect(dropZones[0].contains(placeholder)) + expect(todoZone.contains(placeholder)) .withContext('Expected placeholder to be inside the first container.') .toBe(true); @@ -6584,26 +6102,22 @@ describe('CdkDrag', () => { intermediateRect.left + 1, intermediateRect.top + 1, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - expect(dropZones[1].contains(placeholder)) + expect(doneZone.contains(placeholder)) .withContext('Expected placeholder to be inside second container.') .toBe(true); dispatchMouseEvent(document, 'mousemove', finalRect.left + 1, finalRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - expect(dropZones[1].contains(placeholder)) + expect(doneZone.contains(placeholder)) .withContext('Expected placeholder to remain in the second container.') .toBe(true); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; @@ -6614,8 +6128,8 @@ describe('CdkDrag', () => { previousIndex: 1, currentIndex: 1, item: groups[0][1], - container: dropInstances[1], - previousContainer: dropInstances[0], + container: doneDropInstance, + previousContainer: todoDropInstance, isPointerOverContainer: false, }), ); @@ -6627,7 +6141,6 @@ describe('CdkDrag', () => { 'was disabled', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -6649,7 +6162,6 @@ describe('CdkDrag', () => { .toBe(1); dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[1].contains(placeholder)) @@ -6668,7 +6180,6 @@ describe('CdkDrag', () => { firstInitialSiblingRect.left + 1, firstInitialSiblingRect.top + 1, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[0].contains(placeholder)) @@ -6679,7 +6190,6 @@ describe('CdkDrag', () => { .toBe(1); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); @@ -6691,7 +6201,6 @@ describe('CdkDrag', () => { 'sorting is enabled', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -6714,7 +6223,6 @@ describe('CdkDrag', () => { .toBe(1); dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[1].contains(placeholder)) @@ -6728,7 +6236,6 @@ describe('CdkDrag', () => { // Return the item to an index that is different from the initial one. dispatchMouseEvent(document, 'mousemove', nextTargetRect.left + 1, nextTargetRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[0].contains(placeholder)) @@ -6742,7 +6249,6 @@ describe('CdkDrag', () => { it('should return the last item to initial position when dragging back into a container with disabled sorting', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -6765,7 +6271,6 @@ describe('CdkDrag', () => { .toBe(lastIndex); dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[1].contains(placeholder)) @@ -6784,7 +6289,6 @@ describe('CdkDrag', () => { firstInitialSiblingRect.left, firstInitialSiblingRect.top, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dropZones[0].contains(placeholder)) @@ -6795,7 +6299,6 @@ describe('CdkDrag', () => { .toBe(lastIndex); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); @@ -6808,7 +6311,6 @@ describe('CdkDrag', () => { const fixture = createComponent(ConnectedWrappedDropZones, [], 0, [ WrappedDropContainerComponent, ]); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const [startZone, targetZone] = fixture.nativeElement.querySelectorAll('.cdk-drop-list'); @@ -6834,7 +6336,6 @@ describe('CdkDrag', () => { .toContain('cdk-drop-list-dragging'); dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(startZone.classList) @@ -6850,7 +6351,6 @@ describe('CdkDrag', () => { it('should dispatch an event when an item enters a new container', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -6860,7 +6360,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item.element.nativeElement); dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const containerEnterEvent = fixture.componentInstance.enteredSpy.calls.mostRecent().args[0]; @@ -6877,7 +6376,6 @@ describe('CdkDrag', () => { it('should not throw if dragging was interrupted as a result of the entered event', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -6886,7 +6384,6 @@ describe('CdkDrag', () => { fixture.componentInstance.enteredSpy.and.callFake(() => { fixture.componentInstance.todo = []; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }); @@ -6898,24 +6395,20 @@ describe('CdkDrag', () => { targetRect.top + 1, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }).not.toThrow(); })); it('should be able to drop into a new container after scrolling into view', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Make the page scrollable and scroll the items out of view. const cleanup = makeScrollable(); scrollTo(0, 4000); dispatchFakeEvent(document, 'scroll'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -6929,10 +6422,8 @@ describe('CdkDrag', () => { const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); dispatchMouseEvent(document, 'mouseup', targetRect.left + 1, targetRect.top + 1); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -6967,7 +6458,6 @@ describe('CdkDrag', () => { [], ViewEncapsulation.ShadowDom, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -6981,7 +6471,6 @@ describe('CdkDrag', () => { targetRect.top + 1, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -7008,7 +6497,6 @@ describe('CdkDrag', () => { } const fixture = createComponent(ConnectedDropZonesInsideShadowRootWithNgIf); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -7022,7 +6510,6 @@ describe('CdkDrag', () => { targetRect.top + 1, ); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); @@ -7055,35 +6542,29 @@ describe('CdkDrag', () => { [], ViewEncapsulation.ShadowDom, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const shadowRoot = fixture.nativeElement.shadowRoot; const item = fixture.componentInstance.groupedDragItems[0][1]; startDraggingViaMouse(fixture, item.element.nativeElement); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const initialSelectStart = dispatchFakeEvent(shadowRoot, 'selectstart'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(initialSelectStart.defaultPrevented).toBe(true); dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); const afterDropSelectStart = dispatchFakeEvent(shadowRoot, 'selectstart'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(afterDropSelectStart.defaultPrevented).toBe(false); })); it('should not throw if its next sibling is removed while dragging', fakeAsync(() => { const fixture = createComponent(ConnectedDropZonesWithSingleItems); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.toArray(); @@ -7107,18 +6588,15 @@ describe('CdkDrag', () => { expect(() => { flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }).not.toThrow(); })); it('should warn when the connected container ID does not exist', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - fixture.componentInstance.dropInstances.first.connectedTo = 'does-not-exist'; - fixture.changeDetectorRef.markForCheck(); + fixture.componentInstance.todoConnectedTo.set(['does-not-exist']); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -7127,7 +6605,6 @@ describe('CdkDrag', () => { spyOn(console, 'warn'); dragElementViaMouse(fixture, element, 0, 0); flush(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(console.warn).toHaveBeenCalledWith( @@ -7137,7 +6614,6 @@ describe('CdkDrag', () => { it('should not be able to start a drag sequence while a connected container is active', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.groupedDragItems[0][0]; const itemInOtherList = fixture.componentInstance.groupedDragItems[1][0]; @@ -7168,12 +6644,10 @@ describe('CdkDrag', () => { [], ViewEncapsulation.ShadowDom, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const item = fixture.componentInstance.groupedDragItems[0][1]; startDraggingViaMouse(fixture, item.element.nativeElement); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // `querySelector` doesn't descend into the shadow DOM so we can assert that the preview @@ -7185,7 +6659,6 @@ describe('CdkDrag', () => { describe('with nested drags', () => { it('should not move draggable container when dragging child (multitouch)', fakeAsync(() => { const fixture = createComponent(NestedDragsComponent, [], 10); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // First finger drags container (less then threshold) @@ -7238,7 +6711,6 @@ describe('CdkDrag', () => { it('should stop event propagation when dragging a nested item', fakeAsync(() => { const fixture = createComponent(NestedDragsComponent); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.item.nativeElement; @@ -7246,7 +6718,6 @@ describe('CdkDrag', () => { spyOn(event, 'stopPropagation').and.callThrough(); dispatchEvent(dragElement, event); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(event.stopPropagation).toHaveBeenCalled(); @@ -7254,7 +6725,6 @@ describe('CdkDrag', () => { it('should stop event propagation when dragging item nested via ng-template', fakeAsync(() => { const fixture = createComponent(NestedDragsThroughTemplate); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.item.nativeElement; @@ -7262,7 +6732,6 @@ describe('CdkDrag', () => { spyOn(event, 'stopPropagation').and.callThrough(); dispatchEvent(dragElement, event); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(event.stopPropagation).toHaveBeenCalled(); @@ -7279,6 +6748,8 @@ describe('CdkDrag', () => { [cdkDragStartDelay]="dragStartDelay" [cdkDragConstrainPosition]="constrainPosition" [cdkDragFreeDragPosition]="freeDragPosition" + [cdkDragDisabled]="dragDisabled()" + [cdkDragLockAxis]="dragLockAxis()" (cdkDragStarted)="startedSpy($event)" (cdkDragReleased)="releasedSpy($event)" (cdkDragEnded)="endedSpy($event)" @@ -7295,8 +6766,15 @@ class StandaloneDraggable { releasedSpy = jasmine.createSpy('released spy'); boundary: string | HTMLElement; dragStartDelay: number | string | {touch: number; mouse: number}; - constrainPosition: (point: Point) => Point; + constrainPosition: ( + userPointerPosition: Point, + dragRef: DragRef, + dimensions: DOMRect, + pickupPositionInElement: Point, + ) => Point; freeDragPosition?: {x: number; y: number}; + dragDisabled = signal(false); + dragLockAxis = signal(undefined); } @Component({ @@ -7437,6 +6915,8 @@ const DROP_ZONE_FIXTURE_TEMPLATE = ` style="width: 100px; background: pink;" [id]="dropZoneId" [cdkDropListData]="items" + [cdkDropListDisabled]="dropDisabled()" + [cdkDropListLockAxis]="dropLockAxis()" (cdkDropListSorted)="sortedSpy($event)" (cdkDropListDropped)="droppedSpy($event)">
(undefined); constructor(protected _elementRef: ElementRef) {} @@ -7684,15 +7166,16 @@ class DraggableInScrollableHorizontalDropZone extends DraggableInHorizontalDropZ // https://github.com/angular/angular/pull/52515 @Component({ template: ` -
+
@for (item of items; track item) {
- {{item}} + {{item.label}} @@ -7709,12 +7192,18 @@ class DraggableInScrollableHorizontalDropZone extends DraggableInHorizontalDropZ class DraggableInDropZoneWithCustomPreview { @ViewChild(CdkDropList) dropInstance: CdkDropList; @ViewChildren(CdkDrag) dragItems: QueryList; - items = ['Zero', 'One', 'Two', 'Three']; + items: {label: string; lockAxis?: DragAxis}[] = [ + {label: 'Zero'}, + {label: 'One'}, + {label: 'Two'}, + {label: 'Three'}, + ]; boundarySelector: string; renderCustomPreview = true; matchPreviewSize = false; previewClass: string | string[]; constrainPosition: (point: Point) => Point; + dropLockAxis = signal(undefined); } @Component({ @@ -7852,7 +7341,7 @@ const CONNECTED_DROP_ZONES_TEMPLATE = ` cdkDropList #todoZone="cdkDropList" [cdkDropListData]="todo" - [cdkDropListConnectedTo]="[doneZone]" + [cdkDropListConnectedTo]="todoConnectedTo() || [doneZone]" (cdkDropListDropped)="droppedSpy($event)" (cdkDropListEntered)="enteredSpy($event)"> @for (item of todo; track item) { @@ -7867,7 +7356,7 @@ const CONNECTED_DROP_ZONES_TEMPLATE = ` cdkDropList #doneZone="cdkDropList" [cdkDropListData]="done" - [cdkDropListConnectedTo]="[todoZone]" + [cdkDropListConnectedTo]="doneConnectedTo() || [todoZone]" (cdkDropListDropped)="droppedSpy($event)" (cdkDropListEntered)="enteredSpy($event)"> @for (item of done; track item) { @@ -7882,6 +7371,7 @@ const CONNECTED_DROP_ZONES_TEMPLATE = ` cdkDropList #extraZone="cdkDropList" [cdkDropListData]="extra" + [cdkDropListConnectedTo]="extraConnectedTo()" (cdkDropListDropped)="droppedSpy($event)" (cdkDropListEntered)="enteredSpy($event)"> @for (item of extra; track item) { @@ -7901,6 +7391,7 @@ const CONNECTED_DROP_ZONES_TEMPLATE = ` class ConnectedDropZones implements AfterViewInit { @ViewChildren(CdkDrag) rawDragItems: QueryList; @ViewChildren(CdkDropList) dropInstances: QueryList; + changeDetectorRef = inject(ChangeDetectorRef); groupedDragItems: CdkDrag[][] = []; todo = ['Zero', 'One', 'Two', 'Three']; @@ -7909,6 +7400,9 @@ class ConnectedDropZones implements AfterViewInit { droppedSpy = jasmine.createSpy('dropped spy'); enteredSpy = jasmine.createSpy('entered spy'); itemEnteredSpy = jasmine.createSpy('item entered spy'); + todoConnectedTo = signal<(CdkDropList | string)[] | undefined>(undefined); + doneConnectedTo = signal<(CdkDropList | string)[] | undefined>(undefined); + extraConnectedTo = signal<(CdkDropList | string)[] | undefined>(undefined); ngAfterViewInit() { this.dropInstances.forEach((dropZone, index) => { @@ -7918,6 +7412,7 @@ class ConnectedDropZones implements AfterViewInit { this.groupedDragItems[index].push(...dropZone.getSortedItems()); }); + this.changeDetectorRef.markForCheck(); } } diff --git a/src/cdk/drag-drop/directives/test-utils.spec.ts b/src/cdk/drag-drop/directives/test-utils.spec.ts index 2096c9dd6c70..451a390a5e9c 100644 --- a/src/cdk/drag-drop/directives/test-utils.spec.ts +++ b/src/cdk/drag-drop/directives/test-utils.spec.ts @@ -17,11 +17,9 @@ export function dragElementViaMouse( startDraggingViaMouse(fixture, element); dispatchMouseEvent(document, 'mousemove', x, y); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mouseup', x, y); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); } @@ -39,11 +37,9 @@ export function startDraggingViaMouse( y?: number, ) { dispatchMouseEvent(element, 'mousedown', x, y); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchMouseEvent(document, 'mousemove', x, y); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); } @@ -71,11 +67,9 @@ export function dragElementViaTouch( */ export function startDraggingViaTouch(fixture: ComponentFixture, element: Element) { dispatchTouchEvent(element, 'touchstart'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchTouchEvent(document, 'touchmove'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); } @@ -86,7 +80,6 @@ export function startDraggingViaTouch(fixture: ComponentFixture, element: E */ export function continueDraggingViaTouch(fixture: ComponentFixture, x: number, y: number) { dispatchTouchEvent(document, 'touchmove', x, y); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); } @@ -97,7 +90,6 @@ export function continueDraggingViaTouch(fixture: ComponentFixture, x: numb */ export function stopDraggingViaTouch(fixture: ComponentFixture, x: number, y: number) { dispatchTouchEvent(document, 'touchend', x, y); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); } @@ -153,13 +145,11 @@ export function assertDownwardSorting(fixture: ComponentFixture, items: Ele // Add a few pixels to the top offset so we get some overlap. dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.top + 5); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(getElementIndexByPosition(placeholder, 'top')).toBe(i); } dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); } @@ -183,13 +173,11 @@ export function assertUpwardSorting(fixture: ComponentFixture, items: Eleme // Remove a few pixels from the bottom offset so we get some overlap. dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.bottom - 5); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(getElementIndexByPosition(placeholder, 'top')).toBe(i); } dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); } diff --git a/src/cdk/drag-drop/drag-drop-registry.ts b/src/cdk/drag-drop/drag-drop-registry.ts index a09fcaab495c..53655ef1dcea 100644 --- a/src/cdk/drag-drop/drag-drop-registry.ts +++ b/src/cdk/drag-drop/drag-drop-registry.ts @@ -6,22 +6,24 @@ * found in the LICENSE file at https://angular.io/license */ +import {normalizePassiveListenerOptions} from '@angular/cdk/platform'; +import {DOCUMENT} from '@angular/common'; import { + ApplicationRef, + ChangeDetectionStrategy, + Component, + EnvironmentInjector, + Inject, Injectable, NgZone, OnDestroy, - Inject, - inject, - ApplicationRef, - EnvironmentInjector, - Component, ViewEncapsulation, - ChangeDetectionStrategy, + WritableSignal, createComponent, + inject, + signal, } from '@angular/core'; -import {DOCUMENT} from '@angular/common'; -import {normalizePassiveListenerOptions} from '@angular/cdk/platform'; -import {merge, Observable, Observer, Subject} from 'rxjs'; +import {Observable, Observer, Subject, merge} from 'rxjs'; /** Event options that can be used to bind an active, capturing event. */ const activeCapturingEventOptions = normalizePassiveListenerOptions({ @@ -67,7 +69,7 @@ export class DragDropRegistry implements O private _dragInstances = new Set(); /** Drag item instances that are currently being dragged. */ - private _activeDragInstances: I[] = []; + private _activeDragInstances: WritableSignal = signal([]); /** Keeps track of the event listeners that we've bound to the `document`. */ private _globalListeners = new Map< @@ -163,14 +165,14 @@ export class DragDropRegistry implements O */ startDragging(drag: I, event: TouchEvent | MouseEvent) { // Do not process the same drag twice to avoid memory leaks and redundant listeners - if (this._activeDragInstances.indexOf(drag) > -1) { + if (this._activeDragInstances().indexOf(drag) > -1) { return; } this._loadResets(); - this._activeDragInstances.push(drag); + this._activeDragInstances.update(instances => [...instances, drag]); - if (this._activeDragInstances.length === 1) { + if (this._activeDragInstances().length === 1) { const isTouchEvent = event.type.startsWith('touch'); // We explicitly bind __active__ listeners here, because newer browsers will default to @@ -215,20 +217,23 @@ export class DragDropRegistry implements O /** Stops dragging a drag item instance. */ stopDragging(drag: I) { - const index = this._activeDragInstances.indexOf(drag); - - if (index > -1) { - this._activeDragInstances.splice(index, 1); - - if (this._activeDragInstances.length === 0) { - this._clearGlobalListeners(); + this._activeDragInstances.update(instances => { + const index = instances.indexOf(drag); + if (index > -1) { + instances.splice(index, 1); + return [...instances]; } + return instances; + }); + + if (this._activeDragInstances().length === 0) { + this._clearGlobalListeners(); } } /** Gets whether a drag item instance is currently being dragged. */ isDragging(drag: I) { - return this._activeDragInstances.indexOf(drag) > -1; + return this._activeDragInstances().indexOf(drag) > -1; } /** @@ -250,7 +255,7 @@ export class DragDropRegistry implements O return this._ngZone.runOutsideAngular(() => { const eventOptions = true; const callback = (event: Event) => { - if (this._activeDragInstances.length) { + if (this._activeDragInstances().length) { observer.next(event); } }; @@ -281,18 +286,18 @@ export class DragDropRegistry implements O * @param event Event whose default action should be prevented. */ private _preventDefaultWhileDragging = (event: Event) => { - if (this._activeDragInstances.length > 0) { + if (this._activeDragInstances().length > 0) { event.preventDefault(); } }; /** Event listener for `touchmove` that is bound even if no dragging is happening. */ private _persistentTouchmoveListener = (event: TouchEvent) => { - if (this._activeDragInstances.length > 0) { + if (this._activeDragInstances().length > 0) { // Note that we only want to prevent the default action after dragging has actually started. // Usually this is the same time at which the item is added to the `_activeDragInstances`, // but it could be pushed back if the user has set up a drag delay or threshold. - if (this._activeDragInstances.some(this._draggingPredicate)) { + if (this._activeDragInstances().some(this._draggingPredicate)) { event.preventDefault(); } diff --git a/src/cdk/drag-drop/drag-drop.spec.ts b/src/cdk/drag-drop/drag-drop.spec.ts index 5c6c0ef26ceb..bdff7576c197 100644 --- a/src/cdk/drag-drop/drag-drop.spec.ts +++ b/src/cdk/drag-drop/drag-drop.spec.ts @@ -22,7 +22,6 @@ describe('DragDrop', () => { it('should be able to attach a DragRef to a DOM node', () => { const fixture = TestBed.createComponent(TestComponent); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const ref = service.createDrag(fixture.componentInstance.elementRef); @@ -31,7 +30,6 @@ describe('DragDrop', () => { it('should be able to attach a DropListRef to a DOM node', () => { const fixture = TestBed.createComponent(TestComponent); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const ref = service.createDropList(fixture.componentInstance.elementRef); diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index 860f52993983..b961326e84be 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -6,31 +6,38 @@ * found in the LICENSE file at https://angular.io/license */ -import {EmbeddedViewRef, ElementRef, NgZone, ViewContainerRef, TemplateRef} from '@angular/core'; -import {ViewportRuler} from '@angular/cdk/scrolling'; +import {isFakeMousedownFromScreenReader, isFakeTouchstartFromScreenReader} from '@angular/cdk/a11y'; import {Direction} from '@angular/cdk/bidi'; +import {coerceElement} from '@angular/cdk/coercion'; import { - normalizePassiveListenerOptions, _getEventTarget, _getShadowRoot, + normalizePassiveListenerOptions, } from '@angular/cdk/platform'; -import {coerceElement} from '@angular/cdk/coercion'; -import {isFakeMousedownFromScreenReader, isFakeTouchstartFromScreenReader} from '@angular/cdk/a11y'; -import {Subscription, Subject, Observable} from 'rxjs'; -import type {DropListRef} from './drop-list-ref'; -import {DragDropRegistry} from './drag-drop-registry'; +import {ViewportRuler} from '@angular/cdk/scrolling'; +import { + ElementRef, + EmbeddedViewRef, + NgZone, + TemplateRef, + ViewContainerRef, + signal, +} from '@angular/core'; +import {Observable, Subject, Subscription} from 'rxjs'; +import {deepCloneNode} from './dom/clone-node'; +import {adjustDomRect, getMutableClientRect} from './dom/dom-rect'; +import {ParentPositionTracker} from './dom/parent-position-tracker'; +import {getRootNode} from './dom/root-node'; import { - combineTransforms, DragCSSStyleDeclaration, + combineTransforms, getTransform, toggleNativeDragInteractions, toggleVisibility, } from './dom/styling'; -import {getMutableClientRect, adjustDomRect} from './dom/dom-rect'; -import {ParentPositionTracker} from './dom/parent-position-tracker'; -import {deepCloneNode} from './dom/clone-node'; +import {DragDropRegistry} from './drag-drop-registry'; +import type {DropListRef} from './drop-list-ref'; import {DragPreviewTemplate, PreviewRef} from './preview-ref'; -import {getRootNode} from './dom/root-node'; /** Object that can be used to configure the behavior of DragRef. */ export interface DragRefConfig { @@ -155,7 +162,7 @@ export class DragRef { * Whether the dragging sequence has been started. Doesn't * necessarily mean that the element has been moved. */ - private _hasStartedDragging = false; + private _hasStartedDragging = signal(false); /** Whether the element has moved since the user started dragging it. */ private _hasMoved: boolean; @@ -521,7 +528,7 @@ export class DragRef { /** Checks whether the element is currently being dragged. */ isDragging(): boolean { - return this._hasStartedDragging && this._dragDropRegistry.isDragging(this); + return this._hasStartedDragging() && this._dragDropRegistry.isDragging(this); } /** Resets a standalone drag item to its initial position. */ @@ -651,7 +658,7 @@ export class DragRef { private _pointerMove = (event: MouseEvent | TouchEvent) => { const pointerPosition = this._getPointerPositionOnPage(event); - if (!this._hasStartedDragging) { + if (!this._hasStartedDragging()) { const distanceX = Math.abs(pointerPosition.x - this._pickupPositionOnPage.x); const distanceY = Math.abs(pointerPosition.y - this._pickupPositionOnPage.y); const isOverThreshold = distanceX + distanceY >= this._config.dragStartThreshold; @@ -678,7 +685,7 @@ export class DragRef { if (event.cancelable) { event.preventDefault(); } - this._hasStartedDragging = true; + this._hasStartedDragging.set(true); this._ngZone.run(() => this._startDragSequence(event)); } } @@ -753,7 +760,7 @@ export class DragRef { this._rootElementTapHighlight; } - if (!this._hasStartedDragging) { + if (!this._hasStartedDragging()) { return; } @@ -908,7 +915,8 @@ export class DragRef { rootStyles.webkitTapHighlightColor = 'transparent'; } - this._hasStartedDragging = this._hasMoved = false; + this._hasMoved = false; + this._hasStartedDragging.set(this._hasMoved); // Avoid multiple subscriptions and memory leaks when multi touch // (isDragging check above isn't enough because of possible temporal and/or dimensional delays) diff --git a/src/cdk/listbox/listbox.spec.ts b/src/cdk/listbox/listbox.spec.ts index 4448e872fb62..e49c7951d14a 100644 --- a/src/cdk/listbox/listbox.spec.ts +++ b/src/cdk/listbox/listbox.spec.ts @@ -23,7 +23,6 @@ function setupComponent(component: Type, imports: any[] = []) declarations: [component], }).compileComponents(); const fixture = TestBed.createComponent(component); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const listboxDebugEl = fixture.debugElement.query(By.directive(CdkListbox)); @@ -71,7 +70,6 @@ describe('CdkOption and CdkListbox', () => { expect(optionEls[0].getAttribute('tabindex')).toBe('-1'); listbox.focus(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listboxEl.getAttribute('tabindex')).toBe('-1'); @@ -90,7 +88,6 @@ describe('CdkOption and CdkListbox', () => { expect(optionEls[0].getAttribute('tabindex')).toBe('-1'); listbox.focus(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listboxEl.getAttribute('tabindex')).toBe('-1'); @@ -106,7 +103,6 @@ describe('CdkOption and CdkListbox', () => { expect(optionEls[0].getAttribute('tabindex')).toBe('-1'); listbox.focus(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(optionEls[0].getAttribute('tabindex')).toBe('10'); @@ -119,7 +115,6 @@ describe('CdkOption and CdkListbox', () => { expect(options[0].getAttribute('tabindex')).toBe('-1'); listbox.focus(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listboxEl.getAttribute('tabindex')).toBe('-1'); @@ -149,7 +144,6 @@ describe('CdkOption and CdkListbox', () => { it('should update when selection is changed programmatically', () => { const {fixture, listbox, options, optionEls} = setupComponent(ListboxWithOptions); options[1].select(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual(['orange']); @@ -161,7 +155,6 @@ describe('CdkOption and CdkListbox', () => { it('should update on option clicked', () => { const {fixture, listbox, options, optionEls} = setupComponent(ListboxWithOptions); optionEls[0].click(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual(['apple']); @@ -173,7 +166,6 @@ describe('CdkOption and CdkListbox', () => { it('should prevent the default click action', () => { const {fixture, optionEls} = setupComponent(ListboxWithOptions); const event = dispatchFakeEvent(optionEls[1], 'click'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(event.defaultPrevented).toBe(true); @@ -195,7 +187,6 @@ describe('CdkOption and CdkListbox', () => { undefined, {shift: true}, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual(['orange']); @@ -210,7 +201,6 @@ describe('CdkOption and CdkListbox', () => { undefined, {shift: true}, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual(['orange', 'banana', 'peach']); @@ -225,7 +215,6 @@ describe('CdkOption and CdkListbox', () => { undefined, {shift: true}, ); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual(['orange']); @@ -235,7 +224,6 @@ describe('CdkOption and CdkListbox', () => { const {fixture, listbox, listboxEl, options, optionEls} = setupComponent(ListboxWithOptions); listbox.focus(); dispatchKeyboardEvent(listboxEl, 'keydown', SPACE); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual(['apple']); @@ -247,14 +235,12 @@ describe('CdkOption and CdkListbox', () => { it('should deselect previously selected option in single-select listbox', () => { const {fixture, listbox, options, optionEls} = setupComponent(ListboxWithOptions); dispatchMouseEvent(optionEls[0], 'click'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual(['apple']); expect(options[0].isSelected()).toBeTrue(); dispatchMouseEvent(optionEls[2], 'click'); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual(['banana']); @@ -268,7 +254,6 @@ describe('CdkOption and CdkListbox', () => { fixture.detectChanges(); listbox.setAllSelected(true); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual(['apple', 'orange', 'banana', 'peach']); @@ -278,15 +263,14 @@ describe('CdkOption and CdkListbox', () => { const {testComponent, fixture, listbox, options, optionEls} = setupComponent(ListboxWithOptions); testComponent.isMultiselectable = true; - optionEls[0].click(); fixture.changeDetectorRef.markForCheck(); + optionEls[0].click(); fixture.detectChanges(); expect(listbox.value).toEqual(['apple']); expect(options[0].isSelected()).toBeTrue(); optionEls[2].click(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual(['apple', 'banana']); @@ -299,7 +283,6 @@ describe('CdkOption and CdkListbox', () => { fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); listbox.setAllSelected(true); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual(['apple', 'orange', 'banana', 'peach']); @@ -317,7 +300,6 @@ describe('CdkOption and CdkListbox', () => { fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); optionEls[0].click(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual(['apple']); @@ -337,7 +319,6 @@ describe('CdkOption and CdkListbox', () => { options[0].toggle(); listbox.toggle(options[1]); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[0].isSelected()).toBeTrue(); @@ -345,7 +326,6 @@ describe('CdkOption and CdkListbox', () => { options[0].toggle(); listbox.toggle(options[1]); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[0].isSelected()).toBeFalse(); @@ -360,7 +340,6 @@ describe('CdkOption and CdkListbox', () => { options[0].select(); listbox.select(options[1]); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[0].isSelected()).toBeTrue(); @@ -368,7 +347,6 @@ describe('CdkOption and CdkListbox', () => { options[0].deselect(); listbox.deselect(options[1]); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[0].isSelected()).toBeFalse(); @@ -409,7 +387,6 @@ describe('CdkOption and CdkListbox', () => { const {fixture, testComponent, listbox, listboxEl, options, optionEls} = setupComponent(ListboxWithOptions); testComponent.isListboxDisabled.set(true); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.disabled).toBeTrue(); @@ -438,7 +415,6 @@ describe('CdkOption and CdkListbox', () => { fixture.detectChanges(); optionEls[0].click(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual([]); @@ -448,11 +424,9 @@ describe('CdkOption and CdkListbox', () => { it('should not change selection on click in a disabled listbox', () => { const {fixture, testComponent, listbox, optionEls} = setupComponent(ListboxWithOptions); testComponent.isListboxDisabled.set(true); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); optionEls[0].click(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual([]); @@ -462,15 +436,12 @@ describe('CdkOption and CdkListbox', () => { it('should not change selection on keyboard activation in a disabled listbox', () => { const {fixture, testComponent, listbox, listboxEl} = setupComponent(ListboxWithOptions); listbox.focus(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); testComponent.isListboxDisabled.set(true); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchKeyboardEvent(listboxEl, 'keydown', SPACE); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual([]); @@ -480,7 +451,6 @@ describe('CdkOption and CdkListbox', () => { it('should not change selection on click of a disabled option', () => { const {fixture, testComponent, listbox, listboxEl} = setupComponent(ListboxWithOptions); listbox.focus(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); testComponent.isAppleDisabled = true; @@ -488,7 +458,6 @@ describe('CdkOption and CdkListbox', () => { fixture.detectChanges(); dispatchKeyboardEvent(listboxEl, 'keydown', SPACE); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual([]); @@ -499,11 +468,9 @@ describe('CdkOption and CdkListbox', () => { const {fixture, testComponent, listboxEl, options} = setupComponent(ListboxWithOptions); await fakeAsync(() => { testComponent.isListboxDisabled.set(true); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchKeyboardEvent(listboxEl, 'keydown', B); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(200); @@ -517,14 +484,13 @@ describe('CdkOption and CdkListbox', () => { const {testComponent, fixture, listbox, listboxEl, options} = setupComponent(ListboxWithOptions); testComponent.isOrangeDisabled = true; - listbox.focus(); fixture.changeDetectorRef.markForCheck(); + listbox.focus(); fixture.detectChanges(); expect(options[0].isActive()).toBeTrue(); dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[2].isActive()).toBeTrue(); @@ -535,14 +501,13 @@ describe('CdkOption and CdkListbox', () => { setupComponent(ListboxWithOptions); testComponent.navigationSkipsDisabled = false; testComponent.isOrangeDisabled = true; - listbox.focus(); fixture.changeDetectorRef.markForCheck(); + listbox.focus(); fixture.detectChanges(); expect(options[0].isActive()).toBeTrue(); dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[1].isActive()).toBeTrue(); @@ -557,7 +522,6 @@ describe('CdkOption and CdkListbox', () => { listbox.focus(); dispatchKeyboardEvent(listboxEl, 'keydown', A, undefined, {control: true}); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual(['apple', 'banana', 'peach']); @@ -571,13 +535,11 @@ describe('CdkOption and CdkListbox', () => { [CommonModule], ); listbox.value = [{name: 'Banana'}]; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[2].isSelected()).toBeTrue(); listbox.value = [{name: 'Orange', extraStuff: true} as any]; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[1].isSelected()).toBeTrue(); @@ -589,13 +551,11 @@ describe('CdkOption and CdkListbox', () => { const {fixture, listbox, listboxEl, options} = setupComponent(ListboxWithOptions); listbox.focus(); dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[1].isActive()).toBeTrue(); dispatchKeyboardEvent(listboxEl, 'keydown', UP_ARROW); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[0].isActive()).toBeTrue(); @@ -605,14 +565,12 @@ describe('CdkOption and CdkListbox', () => { const {fixture, listbox, listboxEl, options, optionEls} = setupComponent(ListboxWithOptions); listbox.focus(); dispatchKeyboardEvent(listboxEl, 'keydown', END); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[options.length - 1].isActive()).toBeTrue(); expect(optionEls[options.length - 1].classList).toContain('cdk-option-active'); dispatchKeyboardEvent(listboxEl, 'keydown', HOME); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[0].isActive()).toBeTrue(); @@ -623,11 +581,9 @@ describe('CdkOption and CdkListbox', () => { const {fixture, listbox, listboxEl, options} = setupComponent(ListboxWithOptions); await fakeAsync(() => { listbox.focus(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchKeyboardEvent(listboxEl, 'keydown', B); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(200); @@ -639,11 +595,9 @@ describe('CdkOption and CdkListbox', () => { const {fixture, listbox, listboxEl, options} = setupComponent(ListboxWithCustomTypeahead); await fakeAsync(() => { listbox.focus(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchKeyboardEvent(listboxEl, 'keydown', B); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(200); @@ -654,11 +608,9 @@ describe('CdkOption and CdkListbox', () => { it('should focus and toggle the next item when pressing SHIFT + DOWN_ARROW', () => { const {fixture, listbox, listboxEl, options} = setupComponent(ListboxWithOptions); listbox.focus(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW, undefined, {shift: true}); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual(['orange']); @@ -676,13 +628,11 @@ describe('CdkOption and CdkListbox', () => { listbox.focus(); dispatchKeyboardEvent(listboxEl, 'keydown', RIGHT_ARROW); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[1].isActive()).toBeTrue(); dispatchKeyboardEvent(listboxEl, 'keydown', LEFT_ARROW); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[0].isActive()).toBeTrue(); @@ -696,13 +646,11 @@ describe('CdkOption and CdkListbox', () => { listbox.focus(); dispatchKeyboardEvent(listboxEl, 'keydown', A, undefined, {control: true}); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual(['apple', 'orange', 'banana', 'peach']); dispatchKeyboardEvent(listboxEl, 'keydown', A, undefined, {control: true}); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual([]); @@ -717,7 +665,6 @@ describe('CdkOption and CdkListbox', () => { listbox.focus(); dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW); dispatchKeyboardEvent(listboxEl, 'keydown', SPACE, undefined, {shift: true}); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual(['orange']); @@ -725,7 +672,6 @@ describe('CdkOption and CdkListbox', () => { dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW); dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW); dispatchKeyboardEvent(listboxEl, 'keydown', SPACE, undefined, {shift: true}); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.value).toEqual(['orange', 'banana', 'peach']); @@ -739,8 +685,8 @@ describe('CdkOption and CdkListbox', () => { it('should select and deselect range with CONTROL + SHIFT + HOME', () => { const {testComponent, fixture, listbox, listboxEl} = setupComponent(ListboxWithOptions); testComponent.isMultiselectable = true; - listbox.focus(); fixture.changeDetectorRef.markForCheck(); + listbox.focus(); fixture.detectChanges(); dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW); @@ -759,8 +705,8 @@ describe('CdkOption and CdkListbox', () => { it('should select and deselect range with CONTROL + SHIFT + END', () => { const {testComponent, fixture, listbox, listboxEl} = setupComponent(ListboxWithOptions); testComponent.isMultiselectable = true; - listbox.focus(); fixture.changeDetectorRef.markForCheck(); + listbox.focus(); fixture.detectChanges(); dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW); @@ -779,13 +725,11 @@ describe('CdkOption and CdkListbox', () => { const {fixture, listbox, listboxEl, options} = setupComponent(ListboxWithOptions); listbox.focus(); dispatchKeyboardEvent(listboxEl, 'keydown', END); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[options.length - 1].isActive()).toBeTrue(); dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[0].isActive()).toBeTrue(); @@ -800,13 +744,11 @@ describe('CdkOption and CdkListbox', () => { listbox.focus(); dispatchKeyboardEvent(listboxEl, 'keydown', END); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[options.length - 1].isActive()).toBeTrue(); dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[options.length - 1].isActive()).toBeTrue(); @@ -819,13 +761,11 @@ describe('CdkOption and CdkListbox', () => { fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); listbox.focus(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[3].isActive()).toBeTrue(); dispatchKeyboardEvent(listboxEl, 'keydown', UP_ARROW); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[2].isActive()).toBeTrue(); @@ -835,12 +775,10 @@ describe('CdkOption and CdkListbox', () => { const {testComponent, fixture, listbox, listboxEl, options} = setupComponent(ListboxWithOptions); listbox.focus(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[0].isActive()).toBeTrue(); dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[1].isActive()).toBeTrue(); @@ -855,14 +793,12 @@ describe('CdkOption and CdkListbox', () => { it('should shift focus on keyboard navigation', () => { const {fixture, listbox, listboxEl, optionEls} = setupComponent(ListboxWithOptions); listbox.focus(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(document.activeElement).toBe(optionEls[0]); expect(listboxEl.hasAttribute('aria-activedescendant')).toBeFalse(); dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(document.activeElement).toBe(optionEls[1]); @@ -872,7 +808,6 @@ describe('CdkOption and CdkListbox', () => { it('should focus first option on listbox focus', () => { const {fixture, listbox, optionEls} = setupComponent(ListboxWithOptions); listbox.focus(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(document.activeElement).toBe(optionEls[0]); @@ -882,7 +817,6 @@ describe('CdkOption and CdkListbox', () => { const {fixture, listbox, listboxEl} = setupComponent(ListboxWithNoOptions); listbox.focus(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(document.activeElement).toBe(listboxEl); @@ -898,14 +832,12 @@ describe('CdkOption and CdkListbox', () => { fixture.detectChanges(); listbox.focus(); dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listboxEl.getAttribute('aria-activedescendant')).toBe(optionEls[0].id); expect(document.activeElement).toBe(listboxEl); dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listboxEl.getAttribute('aria-activedescendant')).toBe(optionEls[1].id); @@ -918,7 +850,6 @@ describe('CdkOption and CdkListbox', () => { fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); listbox.focus(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); for (let option of options) { @@ -933,7 +864,6 @@ describe('CdkOption and CdkListbox', () => { fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); optionEls[2].focus(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(document.activeElement).toBe(listboxEl); @@ -947,7 +877,6 @@ describe('CdkOption and CdkListbox', () => { ReactiveFormsModule, ]); testComponent.formControl.disable(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(listbox.disabled).toBeTrue(); @@ -958,7 +887,6 @@ describe('CdkOption and CdkListbox', () => { ReactiveFormsModule, ]); testComponent.formControl.setValue(['banana']); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[2].isSelected()).toBeTrue(); @@ -970,13 +898,11 @@ describe('CdkOption and CdkListbox', () => { ]); const spy = jasmine.createSpy(); const subscription = testComponent.formControl.valueChanges.subscribe(spy); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(spy).not.toHaveBeenCalled(); optionEls[1].click(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(spy).toHaveBeenCalledWith(['orange']); @@ -991,7 +917,6 @@ describe('CdkOption and CdkListbox', () => { fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); testComponent.formControl.setValue(['orange', 'banana']); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options[1].isSelected()).toBeTrue(); @@ -1007,18 +932,15 @@ describe('CdkOption and CdkListbox', () => { fixture.detectChanges(); const spy = jasmine.createSpy(); const subscription = testComponent.formControl.valueChanges.subscribe(spy); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(spy).not.toHaveBeenCalled(); optionEls[1].click(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(spy).toHaveBeenCalledWith(['orange']); optionEls[2].click(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(spy).toHaveBeenCalledWith(['orange', 'banana']); subscription.unsubscribe(); @@ -1031,7 +953,6 @@ describe('CdkOption and CdkListbox', () => { expect(() => { testComponent.formControl.setValue(['orange', 'banana']); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }).toThrowError('Listbox cannot have more than one selected value in multi-selection mode.'); }); @@ -1046,7 +967,6 @@ describe('CdkOption and CdkListbox', () => { expect(() => { testComponent.formControl.setValue(['orange', 'dragonfruit', 'mango']); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }).toThrowError('Listbox has selected values that do not match any of its options.'); }); diff --git a/src/cdk/listbox/listbox.ts b/src/cdk/listbox/listbox.ts index 3b7b81076aef..213d1f31adef 100644 --- a/src/cdk/listbox/listbox.ts +++ b/src/cdk/listbox/listbox.ts @@ -6,6 +6,23 @@ * found in the LICENSE file at https://angular.io/license */ +import {ActiveDescendantKeyManager, Highlightable, ListKeyManagerOption} from '@angular/cdk/a11y'; +import {Directionality} from '@angular/cdk/bidi'; +import {coerceArray} from '@angular/cdk/coercion'; +import {SelectionModel} from '@angular/cdk/collections'; +import { + A, + DOWN_ARROW, + END, + ENTER, + hasModifierKey, + HOME, + LEFT_ARROW, + RIGHT_ARROW, + SPACE, + UP_ARROW, +} from '@angular/cdk/keycodes'; +import {Platform} from '@angular/cdk/platform'; import { AfterContentInit, booleanAttribute, @@ -21,26 +38,9 @@ import { Output, QueryList, } from '@angular/core'; -import {ActiveDescendantKeyManager, Highlightable, ListKeyManagerOption} from '@angular/cdk/a11y'; -import { - A, - DOWN_ARROW, - END, - ENTER, - hasModifierKey, - HOME, - LEFT_ARROW, - RIGHT_ARROW, - SPACE, - UP_ARROW, -} from '@angular/cdk/keycodes'; -import {coerceArray} from '@angular/cdk/coercion'; -import {SelectionModel} from '@angular/cdk/collections'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import {defer, fromEvent, merge, Observable, Subject} from 'rxjs'; import {filter, map, startWith, switchMap, takeUntil} from 'rxjs/operators'; -import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; -import {Directionality} from '@angular/cdk/bidi'; -import {Platform} from '@angular/cdk/platform'; /** The next id to use for creating unique DOM IDs. */ let nextId = 0; @@ -585,6 +585,7 @@ export class CdkListbox implements AfterContentInit, OnDestroy, Con */ setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; + this.changeDetectorRef.markForCheck(); } /** Focus the listbox's host element. */ diff --git a/src/cdk/observers/observe-content.spec.ts b/src/cdk/observers/observe-content.spec.ts index f893280ec803..05d3c483f6c5 100644 --- a/src/cdk/observers/observe-content.spec.ts +++ b/src/cdk/observers/observe-content.spec.ts @@ -21,7 +21,6 @@ describe('Observe content directive', () => { it('should trigger the callback when the content of the element changes', done => { let fixture = TestBed.createComponent(ComponentWithTextContent); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // If the hint label is empty, expect no label. @@ -39,7 +38,6 @@ describe('Observe content directive', () => { it('should trigger the callback when the content of the children changes', done => { let fixture = TestBed.createComponent(ComponentWithChildTextContent); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // If the hint label is empty, expect no label. @@ -70,7 +68,6 @@ describe('Observe content directive', () => { }); const fixture = TestBed.createComponent(ComponentWithTextContent); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(observeSpy).toHaveBeenCalledTimes(1); @@ -114,7 +111,6 @@ describe('Observe content directive', () => { TestBed.compileComponents(); fixture = TestBed.createComponent(ComponentWithDebouncedListener); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); })); @@ -166,7 +162,6 @@ describe('ContentObserver injectable', () => { it('should trigger the callback when the content of the element changes', fakeAsync(() => { const spy = jasmine.createSpy('content observer'); const fixture = TestBed.createComponent(UnobservedComponentWithTextContent); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); contentObserver.observe(fixture.componentInstance.contentEl).subscribe(() => spy()); @@ -184,7 +179,6 @@ describe('ContentObserver injectable', () => { const spy = jasmine.createSpy('content observer'); spyOn(mof, 'create').and.callThrough(); const fixture = TestBed.createComponent(UnobservedComponentWithTextContent); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const sub1 = contentObserver diff --git a/src/cdk/overlay/overlay-directives.spec.ts b/src/cdk/overlay/overlay-directives.spec.ts index dd36f38e5fcf..e562e01c3fdb 100644 --- a/src/cdk/overlay/overlay-directives.spec.ts +++ b/src/cdk/overlay/overlay-directives.spec.ts @@ -54,7 +54,6 @@ describe('Overlay directives', () => { beforeEach(() => { fixture = TestBed.createComponent(ConnectedOverlayDirectiveTest); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }); @@ -161,7 +160,6 @@ describe('Overlay directives', () => { fixture.detectChanges(); const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(overlayContainerElement.textContent!.trim()) @@ -177,7 +175,6 @@ describe('Overlay directives', () => { const event = createKeyboardEvent('keydown', ESCAPE, undefined, {alt: true}); dispatchEvent(document.body, event); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(overlayContainerElement.textContent!.trim()).toBeTruthy(); @@ -192,7 +189,6 @@ describe('Overlay directives', () => { const backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement; backdrop.click(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(overlayContainerElement.textContent!.trim()).toBeTruthy(); @@ -205,7 +201,6 @@ describe('Overlay directives', () => { fixture.detectChanges(); const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(overlayContainerElement.textContent!.trim()).toBeTruthy(); @@ -216,7 +211,6 @@ describe('Overlay directives', () => { fixture.destroy(); const propOrderFixture = TestBed.createComponent(ConnectedOverlayPropertyInitOrder); - propOrderFixture.changeDetectorRef.markForCheck(); propOrderFixture.detectChanges(); const overlayDirective = propOrderFixture.componentInstance.connectedOverlayDirective; @@ -224,7 +218,6 @@ describe('Overlay directives', () => { expect(() => { overlayDirective.open = true; overlayDirective.origin = propOrderFixture.componentInstance.trigger; - propOrderFixture.changeDetectorRef.markForCheck(); propOrderFixture.detectChanges(); }).not.toThrow(); })); @@ -657,7 +650,6 @@ describe('Overlay directives', () => { '.cdk-overlay-backdrop', ) as HTMLElement; backdrop.click(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.backdropClickHandler).toHaveBeenCalledWith( @@ -718,7 +710,6 @@ describe('Overlay directives', () => { fixture.detectChanges(); const event = dispatchKeyboardEvent(document.body, 'keydown', A); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.keydownHandler).toHaveBeenCalledWith(event); @@ -734,7 +725,6 @@ describe('Overlay directives', () => { expect(fixture.componentInstance.detachHandler).not.toHaveBeenCalled(); scrolledSubject.next(); - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.detachHandler).toHaveBeenCalled(); From 205c0a4dcb8368356109084f8c68303650b16a4a Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 29 May 2024 10:00:11 -0700 Subject: [PATCH 02/61] test: move some CDK tests to zoneless (#29103) * test: move some CDK tests to zoneless * test: Fix flakiness in virtual scroll tests * test: remove overzealous markForCheck calls --- src/cdk/overlay/overlay.spec.ts | 53 +++--- .../scroll/close-scroll-strategy.spec.ts | 20 +- .../scroll/close-scroll-strategy.zone.spec.ts | 72 ++++++++ src/cdk/portal/portal.spec.ts | 36 +++- src/cdk/scrolling/scrollable.spec.ts | 16 +- .../scrolling/virtual-scroll-viewport.spec.ts | 85 ++++----- .../virtual-scroll-viewport.zone.spec.ts | 173 ++++++++++++++++++ 7 files changed, 344 insertions(+), 111 deletions(-) create mode 100644 src/cdk/overlay/scroll/close-scroll-strategy.zone.spec.ts create mode 100644 src/cdk/scrolling/virtual-scroll-viewport.zone.spec.ts diff --git a/src/cdk/overlay/overlay.spec.ts b/src/cdk/overlay/overlay.spec.ts index dee7c159babe..47a700d22740 100644 --- a/src/cdk/overlay/overlay.spec.ts +++ b/src/cdk/overlay/overlay.spec.ts @@ -1,37 +1,35 @@ -import { - waitForAsync, - fakeAsync, - tick, - ComponentFixture, - TestBed, - flush, -} from '@angular/core/testing'; +import {Direction, Directionality} from '@angular/cdk/bidi'; +import {CdkPortal, ComponentPortal, TemplatePortal} from '@angular/cdk/portal'; +import {Location} from '@angular/common'; +import {SpyLocation} from '@angular/common/testing'; import { Component, - ViewChild, - ViewContainerRef, ErrorHandler, - Injectable, EventEmitter, - NgZone, + Injectable, Type, - provideZoneChangeDetection, + ViewChild, + ViewContainerRef, } from '@angular/core'; -import {Direction, Directionality} from '@angular/cdk/bidi'; +import { + ComponentFixture, + TestBed, + fakeAsync, + flush, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {dispatchFakeEvent} from '../testing/private'; -import {ComponentPortal, TemplatePortal, CdkPortal} from '@angular/cdk/portal'; -import {Location} from '@angular/common'; -import {SpyLocation} from '@angular/common/testing'; import { Overlay, + OverlayConfig, OverlayContainer, OverlayModule, OverlayRef, - OverlayConfig, PositionStrategy, ScrollStrategy, } from './index'; -import {NoopAnimationsModule} from '@angular/platform-browser/animations'; describe('Overlay', () => { let overlay: Overlay; @@ -48,8 +46,6 @@ describe('Overlay', () => { TestBed.configureTestingModule({ imports: [OverlayModule, ...imports], providers: [ - provideZoneChangeDetection(), - provideZoneChangeDetection(), { provide: Directionality, useFactory: () => { @@ -949,12 +945,12 @@ describe('Overlay', () => { .toContain('custom-panel-class'); }); - it('should wait for the overlay to be detached before removing the panelClass', () => { + it('should wait for the overlay to be detached before removing the panelClass', async () => { const config = new OverlayConfig({panelClass: 'custom-panel-class'}); const overlayRef = overlay.create(config); overlayRef.attach(componentPortal); - viewContainerFixture.detectChanges(); + await viewContainerFixture.whenStable(); const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; expect(pane.classList) @@ -962,13 +958,10 @@ describe('Overlay', () => { .toContain('custom-panel-class'); overlayRef.detach(); - // Stable emits after zone.run - TestBed.inject(NgZone).run(() => { - viewContainerFixture.detectChanges(); - expect(pane.classList) - .withContext('Expected class not to be removed immediately') - .toContain('custom-panel-class'); - }); + expect(pane.classList) + .withContext('Expected class not to be removed immediately') + .toContain('custom-panel-class'); + await viewContainerFixture.whenStable(); expect(pane.classList) .not.withContext('Expected class to be removed on stable') diff --git a/src/cdk/overlay/scroll/close-scroll-strategy.spec.ts b/src/cdk/overlay/scroll/close-scroll-strategy.spec.ts index eab3daf2c83e..54c404ff8f11 100644 --- a/src/cdk/overlay/scroll/close-scroll-strategy.spec.ts +++ b/src/cdk/overlay/scroll/close-scroll-strategy.spec.ts @@ -1,9 +1,9 @@ -import {inject, TestBed, fakeAsync} from '@angular/core/testing'; -import {Component, ElementRef, NgZone, provideZoneChangeDetection} from '@angular/core'; -import {Subject} from 'rxjs'; import {ComponentPortal, PortalModule} from '@angular/cdk/portal'; import {CdkScrollable, ScrollDispatcher, ViewportRuler} from '@angular/cdk/scrolling'; -import {Overlay, OverlayConfig, OverlayRef, OverlayModule, OverlayContainer} from '../index'; +import {Component, ElementRef} from '@angular/core'; +import {TestBed, fakeAsync, inject} from '@angular/core/testing'; +import {Subject} from 'rxjs'; +import {Overlay, OverlayConfig, OverlayContainer, OverlayModule, OverlayRef} from '../index'; describe('CloseScrollStrategy', () => { let overlayRef: OverlayRef; @@ -17,7 +17,6 @@ describe('CloseScrollStrategy', () => { TestBed.configureTestingModule({ imports: [OverlayModule, PortalModule, MozarellaMsg], providers: [ - provideZoneChangeDetection(), { provide: ScrollDispatcher, useFactory: () => ({ @@ -75,17 +74,6 @@ describe('CloseScrollStrategy', () => { expect(overlayRef.detach).not.toHaveBeenCalled(); }); - it('should detach inside the NgZone', () => { - const spy = jasmine.createSpy('detachment spy'); - const subscription = overlayRef.detachments().subscribe(() => spy(NgZone.isInAngularZone())); - - overlayRef.attach(componentPortal); - scrolledSubject.next(); - - expect(spy).toHaveBeenCalledWith(true); - subscription.unsubscribe(); - }); - it('should be able to reposition the overlay up to a certain threshold before closing', inject( [Overlay], (overlay: Overlay) => { diff --git a/src/cdk/overlay/scroll/close-scroll-strategy.zone.spec.ts b/src/cdk/overlay/scroll/close-scroll-strategy.zone.spec.ts new file mode 100644 index 000000000000..85c85a5dd92c --- /dev/null +++ b/src/cdk/overlay/scroll/close-scroll-strategy.zone.spec.ts @@ -0,0 +1,72 @@ +import {ComponentPortal, PortalModule} from '@angular/cdk/portal'; +import {Component, NgZone, provideZoneChangeDetection} from '@angular/core'; +import {TestBed, fakeAsync, inject} from '@angular/core/testing'; +import {Subject} from 'rxjs'; +import {Overlay} from '../overlay'; +import {OverlayConfig} from '../overlay-config'; +import {OverlayContainer} from '../overlay-container'; +import {OverlayModule} from '../overlay-module'; +import {OverlayRef} from '../overlay-ref'; +import {CdkScrollable, ScrollDispatcher, ViewportRuler} from '../public-api'; + +describe('CloseScrollStrategy Zone.js integration', () => { + let overlayRef: OverlayRef; + let componentPortal: ComponentPortal; + let scrolledSubject = new Subject(); + let scrollPosition: number; + + beforeEach(fakeAsync(() => { + scrollPosition = 0; + + TestBed.configureTestingModule({ + imports: [OverlayModule, PortalModule, MozarellaMsg], + providers: [ + provideZoneChangeDetection(), + { + provide: ScrollDispatcher, + useFactory: () => ({ + scrolled: () => scrolledSubject, + }), + }, + { + provide: ViewportRuler, + useFactory: () => ({ + getViewportScrollPosition: () => ({top: scrollPosition}), + }), + }, + ], + }); + + TestBed.compileComponents(); + })); + + beforeEach(inject([Overlay], (overlay: Overlay) => { + let overlayConfig = new OverlayConfig({scrollStrategy: overlay.scrollStrategies.close()}); + overlayRef = overlay.create(overlayConfig); + componentPortal = new ComponentPortal(MozarellaMsg); + })); + + afterEach(inject([OverlayContainer], (container: OverlayContainer) => { + overlayRef.dispose(); + container.getContainerElement().remove(); + })); + + it('should detach inside the NgZone', () => { + const spy = jasmine.createSpy('detachment spy'); + const subscription = overlayRef.detachments().subscribe(() => spy(NgZone.isInAngularZone())); + + overlayRef.attach(componentPortal); + scrolledSubject.next(); + + expect(spy).toHaveBeenCalledWith(true); + subscription.unsubscribe(); + }); +}); + +/** Simple component that we can attach to the overlay. */ +@Component({ + template: '

Mozarella

', + standalone: true, + imports: [OverlayModule, PortalModule], +}) +class MozarellaMsg {} diff --git a/src/cdk/portal/portal.spec.ts b/src/cdk/portal/portal.spec.ts index 12f3f7a86079..e7d4fe54d1b0 100644 --- a/src/cdk/portal/portal.spec.ts +++ b/src/cdk/portal/portal.spec.ts @@ -1,9 +1,11 @@ import {CommonModule} from '@angular/common'; import { + AfterViewInit, ApplicationRef, Component, ComponentFactoryResolver, ComponentRef, + Directive, ElementRef, Injector, Optional, @@ -13,11 +15,8 @@ import { ViewChild, ViewChildren, ViewContainerRef, - Directive, - AfterViewInit, - provideZoneChangeDetection, } from '@angular/core'; -import {ComponentFixture, inject, TestBed} from '@angular/core/testing'; +import {ComponentFixture, TestBed, inject} from '@angular/core/testing'; import {DomPortalOutlet} from './dom-portal-outlet'; import {ComponentPortal, DomPortal, Portal, TemplatePortal} from './portal'; import {CdkPortal, CdkPortalOutlet, PortalModule} from './portal-directives'; @@ -25,7 +24,6 @@ import {CdkPortal, CdkPortalOutlet, PortalModule} from './portal-directives'; describe('Portals', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [ PortalModule, CommonModule, @@ -58,6 +56,7 @@ describe('Portals', () => { let hostContainer = fixture.nativeElement.querySelector('.portal-container'); testAppComponent.selectedPortal = componentPortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present. @@ -75,6 +74,7 @@ describe('Portals', () => { let templatePortal = new TemplatePortal(testAppComponent.templateRef, null!); testAppComponent.selectedPortal = templatePortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present and no context is projected @@ -103,6 +103,7 @@ describe('Portals', () => { ); testAppComponent.selectedPortal = templatePortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present and no context is projected @@ -136,6 +137,7 @@ describe('Portals', () => { .toBe(false); testAppComponent.selectedPortal = domPortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(domPortal.element.parentNode).not.toBe( @@ -148,6 +150,7 @@ describe('Portals', () => { expect(testAppComponent.portalOutlet.hasAttached()).toBe(true); testAppComponent.selectedPortal = undefined; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(domPortal.element.parentNode) @@ -166,6 +169,7 @@ describe('Portals', () => { expect(() => { testAppComponent.selectedPortal = domPortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }).toThrowError('DOM portal content must be attached to a parent node.'); }); @@ -176,12 +180,14 @@ describe('Portals', () => { const domPortal = new DomPortal(testAppComponent.domPortalContent); testAppComponent.selectedPortal = domPortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); parent.innerHTML = ''; expect(() => { testAppComponent.selectedPortal = undefined; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }).not.toThrow(); }); @@ -193,12 +199,14 @@ describe('Portals', () => { // TemplatePortal without context: let templatePortal = new TemplatePortal(testAppComponent.templateRef, null!); testAppComponent.selectedPortal = templatePortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present and NO context is projected expect(hostContainer.textContent).toContain('Banana - !'); // using TemplatePortal.attach method to set context testAppComponent.selectedPortal = undefined; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); templatePortal.attach(testAppComponent.portalOutlet, {$implicit: {status: 'rotten'}}); fixture.detectChanges(); @@ -211,6 +219,7 @@ describe('Portals', () => { $implicit: {status: 'fresh'}, }); testAppComponent.selectedPortal = templatePortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present and context given via the // constructor is projected @@ -219,6 +228,7 @@ describe('Portals', () => { // using TemplatePortal constructor to set the context but also calling attach method with // context, the latter should take precedence: testAppComponent.selectedPortal = undefined; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); templatePortal.attach(testAppComponent.portalOutlet, {$implicit: {status: 'rotten'}}); fixture.detectChanges(); @@ -231,6 +241,7 @@ describe('Portals', () => { // Set the selectedHost to be a ComponentPortal. let testAppComponent = fixture.componentInstance; testAppComponent.selectedPortal = new ComponentPortal(PizzaMsg); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(testAppComponent.selectedPortal.isAttached).toBe(true); @@ -246,6 +257,7 @@ describe('Portals', () => { // Set the selectedHost to be a ComponentPortal. let testAppComponent = fixture.componentInstance; testAppComponent.selectedPortal = new ComponentPortal(PizzaMsg, undefined, chocolateInjector); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present. @@ -262,6 +274,7 @@ describe('Portals', () => { // Set the selectedHost to be a TemplatePortal. testAppComponent.selectedPortal = testAppComponent.cakePortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present. @@ -277,6 +290,7 @@ describe('Portals', () => { // Set the selectedHost to be a TemplatePortal (with the `*` syntax). testAppComponent.selectedPortal = testAppComponent.piePortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present. @@ -292,6 +306,7 @@ describe('Portals', () => { // Set the selectedHost to be a TemplatePortal. testAppComponent.selectedPortal = testAppComponent.portalWithBinding; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present. @@ -302,6 +317,7 @@ describe('Portals', () => { // When updating the binding value. testAppComponent.fruit = 'Mango'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect the new value to be reflected in the rendered output. @@ -316,6 +332,7 @@ describe('Portals', () => { // Set the selectedHost to be a TemplatePortal. testAppComponent.selectedPortal = testAppComponent.portalWithTemplate; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present. @@ -324,6 +341,7 @@ describe('Portals', () => { // When updating the binding value. testAppComponent.fruits = ['Mangosteen']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect the new value to be reflected in the rendered output. @@ -338,6 +356,7 @@ describe('Portals', () => { // Set the selectedHost to be a ComponentPortal. testAppComponent.selectedPortal = testAppComponent.piePortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present. @@ -345,6 +364,7 @@ describe('Portals', () => { expect(hostContainer.textContent).toContain('Pie'); testAppComponent.selectedPortal = new ComponentPortal(PizzaMsg); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(hostContainer.textContent).toContain('Pizza'); @@ -353,12 +373,14 @@ describe('Portals', () => { it('should detach the portal when it is set to null', () => { let testAppComponent = fixture.componentInstance; testAppComponent.selectedPortal = new ComponentPortal(PizzaMsg); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(testAppComponent.portalOutlet.hasAttached()).toBe(true); expect(testAppComponent.portalOutlet.portal).toBe(testAppComponent.selectedPortal); testAppComponent.selectedPortal = null!; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(testAppComponent.portalOutlet.hasAttached()).toBe(false); @@ -387,6 +409,7 @@ describe('Portals', () => { let testAppComponent = fixture.componentInstance; testAppComponent.selectedPortal = new ComponentPortal(PizzaMsg); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(testAppComponent.portalOutlet.portal).toBeTruthy(); @@ -449,6 +472,7 @@ describe('Portals', () => { const portal = new ComponentPortal(PizzaMsg, fixture.componentInstance.alternateContainer); fixture.componentInstance.selectedPortal = portal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(hostContainer.textContent).toContain('Pizza'); @@ -462,6 +486,7 @@ describe('Portals', () => { ]); testAppComponent.selectedPortal = componentPortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.nativeElement.textContent).toContain('Projectable node'); @@ -586,6 +611,7 @@ describe('Portals', () => { // When updating the binding value. testAppComponent.fruit = 'Mango'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect the new value to be reflected in the rendered output. diff --git a/src/cdk/scrolling/scrollable.spec.ts b/src/cdk/scrolling/scrollable.spec.ts index 495e3804e5a1..2a09d7863ef7 100644 --- a/src/cdk/scrolling/scrollable.spec.ts +++ b/src/cdk/scrolling/scrollable.spec.ts @@ -1,14 +1,7 @@ import {Direction} from '@angular/cdk/bidi'; import {CdkScrollable, ScrollingModule} from '@angular/cdk/scrolling'; -import { - Component, - ElementRef, - Input, - ViewChild, - NgZone, - provideZoneChangeDetection, -} from '@angular/core'; -import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing'; +import {Component, ElementRef, Input, ViewChild} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; function expectOverlapping(el1: ElementRef, el2: ElementRef, expected = true) { const r1 = el1.nativeElement.getBoundingClientRect(); @@ -33,10 +26,6 @@ describe('CdkScrollable', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - providers: [ - provideZoneChangeDetection(), - {provide: NgZone, useFactory: () => new NgZone({})}, - ], imports: [ScrollingModule, ScrollableViewport], }).compileComponents(); })); @@ -140,6 +129,7 @@ describe('CdkScrollable', () => { describe('in RTL context', () => { beforeEach(() => { testComponent.dir = 'rtl'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); maxOffset = testComponent.scrollContainer.nativeElement.scrollHeight - diff --git a/src/cdk/scrolling/virtual-scroll-viewport.spec.ts b/src/cdk/scrolling/virtual-scroll-viewport.spec.ts index 27c373979b2b..fca855affe4b 100644 --- a/src/cdk/scrolling/virtual-scroll-viewport.spec.ts +++ b/src/cdk/scrolling/virtual-scroll-viewport.spec.ts @@ -7,28 +7,26 @@ import { ScrollingModule, } from '@angular/cdk/scrolling'; import {CommonModule} from '@angular/common'; -import {dispatchFakeEvent} from '../testing/private'; import { + ApplicationRef, Component, - NgZone, + Directive, TrackByFunction, ViewChild, - ViewEncapsulation, - Directive, ViewContainerRef, - ApplicationRef, - provideZoneChangeDetection, + ViewEncapsulation, } from '@angular/core'; import { - waitForAsync, ComponentFixture, + TestBed, fakeAsync, flush, inject, - TestBed, tick, + waitForAsync, } from '@angular/core/testing'; -import {animationFrameScheduler, Subject} from 'rxjs'; +import {Subject} from 'rxjs'; +import {dispatchFakeEvent} from '../testing/private'; describe('CdkVirtualScrollViewport', () => { describe('with FixedSizeVirtualScrollStrategy', () => { @@ -38,7 +36,6 @@ describe('CdkVirtualScrollViewport', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [ScrollingModule, FixedSizeVirtualScroll], }).compileComponents(); })); @@ -74,12 +71,14 @@ describe('CdkVirtualScrollViewport', () => { it('should update viewport size', fakeAsync(() => { testComponent.viewportSize = 300; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); viewport.checkViewportSize(); expect(viewport.getViewportSize()).toBe(300); testComponent.viewportSize = 500; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); viewport.checkViewportSize(); @@ -111,11 +110,13 @@ describe('CdkVirtualScrollViewport', () => { expect(viewport.getRenderedRange()).toEqual({start: 0, end: 4}); fixture.componentInstance.items = [0, 1]; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(viewport.getRenderedRange()).toEqual({start: 0, end: 2}); fixture.componentInstance.items = []; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(viewport.getRenderedRange()).toEqual({start: 0, end: 0}); @@ -196,6 +197,7 @@ describe('CdkVirtualScrollViewport', () => { expect(viewportElement.classList).toContain('cdk-virtual-scroll-orientation-vertical'); testComponent.orientation = 'horizontal'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(viewportElement.classList).toContain('cdk-virtual-scroll-orientation-horizontal'); @@ -214,8 +216,8 @@ describe('CdkVirtualScrollViewport', () => { it('should set rendered range', fakeAsync(() => { finishInit(fixture); viewport.setRenderedRange({start: 2, end: 3}); - fixture.detectChanges(); flush(); + fixture.detectChanges(); const items = fixture.elementRef.nativeElement.querySelectorAll('.item'); expect(items.length).withContext('Expected 1 item to be rendered').toBe(1); @@ -233,15 +235,15 @@ describe('CdkVirtualScrollViewport', () => { expect(viewport.getOffsetToRenderedContentStart()).toBe(10); })); - it('should set content offset to bottom of content', fakeAsync(() => { + it('should set content offset to bottom of content', fakeAsync(async () => { finishInit(fixture); const contentSize = viewport.measureRenderedContentSize(); expect(contentSize).toBeGreaterThan(0); viewport.setRenderedContentOffset(contentSize + 10, 'to-end'); - fixture.detectChanges(); flush(); + await fixture.whenStable(); expect(viewport.getOffsetToRenderedContentStart()).toBe(10); })); @@ -423,6 +425,7 @@ describe('CdkVirtualScrollViewport', () => { .toEqual({start: 2, end: 6}); testComponent.itemSize *= 2; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -443,6 +446,7 @@ describe('CdkVirtualScrollViewport', () => { testComponent.minBufferPx = testComponent.itemSize; testComponent.maxBufferPx = testComponent.itemSize; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -462,6 +466,7 @@ describe('CdkVirtualScrollViewport', () => { .toBe(testComponent.itemSize * 6); testComponent.items = Array(5).fill(0); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -484,6 +489,7 @@ describe('CdkVirtualScrollViewport', () => { testComponent.minBufferPx = testComponent.itemSize; testComponent.maxBufferPx = testComponent.itemSize; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -504,6 +510,7 @@ describe('CdkVirtualScrollViewport', () => { .toBe(testComponent.itemSize * 50); testComponent.items = Array(54).fill(0); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -639,12 +646,14 @@ describe('CdkVirtualScrollViewport', () => { finishInit(fixture); testComponent.items = [0]; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); expect(testComponent.virtualForOf._viewContainerRef.detach).not.toHaveBeenCalled(); testComponent.items = [1]; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -658,12 +667,14 @@ describe('CdkVirtualScrollViewport', () => { finishInit(fixture); testComponent.items = [0]; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); expect(testComponent.virtualForOf._viewContainerRef.detach).not.toHaveBeenCalled(); testComponent.items = [1]; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -764,9 +775,13 @@ describe('CdkVirtualScrollViewport', () => { })); it('should throw if maxBufferPx is less than minBufferPx', fakeAsync(() => { - testComponent.minBufferPx = 100; - testComponent.maxBufferPx = 99; - expect(() => finishInit(fixture)).toThrow(); + expect(() => { + testComponent.minBufferPx = 100; + testComponent.maxBufferPx = 99; + finishInit(fixture); + }).toThrowError( + 'CDK virtual scroll: maxBufferPx must be greater than or equal to minBufferPx', + ); })); it('should register and degregister with ScrollDispatcher', fakeAsync( @@ -780,17 +795,11 @@ describe('CdkVirtualScrollViewport', () => { }), )); - it('should emit on viewChange inside the Angular zone', fakeAsync(() => { - const zoneTest = jasmine.createSpy('zone test'); - testComponent.virtualForOf.viewChange.subscribe(() => zoneTest(NgZone.isInAngularZone())); - finishInit(fixture); - expect(zoneTest).toHaveBeenCalledWith(true); - })); - it('should not throw when disposing of a view that will not fit in the cache', fakeAsync(() => { finishInit(fixture); testComponent.items = new Array(200).fill(0); testComponent.templateCacheSize = 1; // Reduce the cache size to something we can easily hit. + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -814,6 +823,7 @@ describe('CdkVirtualScrollViewport', () => { it('should not run change detection if there are no viewChange listeners', fakeAsync(() => { finishInit(fixture); testComponent.items = Array(10).fill(0); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -824,21 +834,6 @@ describe('CdkVirtualScrollViewport', () => { expect(appRef.tick).not.toHaveBeenCalled(); })); - - it('should run change detection if there are any viewChange listeners', fakeAsync(() => { - testComponent.virtualForOf.viewChange.subscribe(); - finishInit(fixture); - testComponent.items = Array(10).fill(0); - fixture.detectChanges(); - flush(); - - spyOn(appRef, 'tick'); - - viewport.scrollToIndex(5); - triggerScroll(viewport); - - expect(appRef.tick).toHaveBeenCalledTimes(1); - })); }); }); @@ -851,7 +846,6 @@ describe('CdkVirtualScrollViewport', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [ScrollingModule, FixedSizeVirtualScrollWithRtlDirection], }).compileComponents(); @@ -952,7 +946,6 @@ describe('CdkVirtualScrollViewport', () => { describe('with no VirtualScrollStrategy', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [ScrollingModule, VirtualScrollWithNoStrategy], }).compileComponents(); }); @@ -971,7 +964,6 @@ describe('CdkVirtualScrollViewport', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [ ScrollingModule, VirtualScrollWithItemInjectingViewContainer, @@ -1010,7 +1002,6 @@ describe('CdkVirtualScrollViewport', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [ScrollingModule, CommonModule, DelayedInitializationVirtualScroll], }).compileComponents(); fixture = TestBed.createComponent(DelayedInitializationVirtualScroll); @@ -1023,6 +1014,7 @@ describe('CdkVirtualScrollViewport', () => { expect(testComponent.trackBy).not.toHaveBeenCalled(); testComponent.renderVirtualFor = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); triggerScroll(viewport, testComponent.itemSize * 5); fixture.detectChanges(); @@ -1040,7 +1032,6 @@ describe('CdkVirtualScrollViewport', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [ScrollingModule, CommonModule, VirtualScrollWithAppendOnly], }).compileComponents(); fixture = TestBed.createComponent(VirtualScrollWithAppendOnly); @@ -1074,15 +1065,15 @@ describe('CdkVirtualScrollViewport', () => { .toBe(0); })); - it('should set content offset to bottom of content', fakeAsync(() => { + it('should set content offset to bottom of content', fakeAsync(async () => { finishInit(fixture); const contentSize = viewport.measureRenderedContentSize(); expect(contentSize).toBeGreaterThan(0); viewport.setRenderedContentOffset(contentSize + 10, 'to-end'); - fixture.detectChanges(); flush(); + await fixture.whenStable(); expect(viewport.getOffsetToRenderedContentStart()).toBe(0); })); @@ -1203,7 +1194,7 @@ function finishInit(fixture: ComponentFixture) { flush(); // Flush the initial fake scroll event. - animationFrameScheduler.flush(); + tick(16); // flush animation frame flush(); fixture.detectChanges(); } @@ -1214,7 +1205,7 @@ function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) { viewport.scrollToOffset(offset); } dispatchFakeEvent(viewport.scrollable.getElementRef().nativeElement, 'scroll'); - animationFrameScheduler.flush(); + tick(16); // flush animation frame } @Component({ diff --git a/src/cdk/scrolling/virtual-scroll-viewport.zone.spec.ts b/src/cdk/scrolling/virtual-scroll-viewport.zone.spec.ts new file mode 100644 index 000000000000..a1d9dfbb055d --- /dev/null +++ b/src/cdk/scrolling/virtual-scroll-viewport.zone.spec.ts @@ -0,0 +1,173 @@ +import { + ApplicationRef, + Component, + NgZone, + TrackByFunction, + ViewChild, + ViewEncapsulation, + provideZoneChangeDetection, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + fakeAsync, + flush, + inject, + waitForAsync, +} from '@angular/core/testing'; +import {animationFrameScheduler} from 'rxjs'; +import {dispatchFakeEvent} from '../testing/private'; +import {ScrollingModule} from './scrolling-module'; +import {CdkVirtualForOf} from './virtual-for-of'; +import {CdkVirtualScrollViewport} from './virtual-scroll-viewport'; + +describe('CdkVirtualScrollViewport Zone.js intergation', () => { + describe('with FixedSizeVirtualScrollStrategy', () => { + let fixture: ComponentFixture; + let testComponent: FixedSizeVirtualScroll; + let viewport: CdkVirtualScrollViewport; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + providers: [provideZoneChangeDetection()], + imports: [ScrollingModule, FixedSizeVirtualScroll], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FixedSizeVirtualScroll); + testComponent = fixture.componentInstance; + viewport = testComponent.viewport; + }); + + it('should emit on viewChange inside the Angular zone', fakeAsync(() => { + const zoneTest = jasmine.createSpy('zone test'); + testComponent.virtualForOf.viewChange.subscribe(() => zoneTest(NgZone.isInAngularZone())); + finishInit(fixture); + expect(zoneTest).toHaveBeenCalledWith(true); + })); + + describe('viewChange change detection behavior', () => { + let appRef: ApplicationRef; + + beforeEach(inject([ApplicationRef], (ar: ApplicationRef) => { + appRef = ar; + })); + + it('should run change detection if there are any viewChange listeners', fakeAsync(() => { + testComponent.virtualForOf.viewChange.subscribe(); + finishInit(fixture); + testComponent.items = Array(10).fill(0); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + flush(); + + spyOn(appRef, 'tick'); + + viewport.scrollToIndex(5); + triggerScroll(viewport); + + expect(appRef.tick).toHaveBeenCalledTimes(1); + })); + }); + }); +}); + +@Component({ + template: ` + +
+ {{i}} - {{item}} +
+
+ `, + styles: ` + .cdk-virtual-scroll-content-wrapper { + display: flex; + flex-direction: column; + } + + .cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper { + flex-direction: row; + } + + .cdk-virtual-scroll-viewport { + background-color: #f5f5f5; + } + + .item { + box-sizing: border-box; + border: 1px dashed #ccc; + } + + .has-margin .item { + margin-bottom: 10px; + } + `, + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [ScrollingModule], +}) +class FixedSizeVirtualScroll { + @ViewChild(CdkVirtualScrollViewport, {static: true}) viewport: CdkVirtualScrollViewport; + // Casting virtualForOf as any so we can spy on private methods + @ViewChild(CdkVirtualForOf, {static: true}) virtualForOf: any; + + orientation = 'vertical'; + viewportSize = 200; + viewportCrossSize = 100; + itemSize = 50; + minBufferPx = 0; + maxBufferPx = 0; + items = Array(10) + .fill(0) + .map((_, i) => i); + trackBy: TrackByFunction; + templateCacheSize = 20; + + scrolledToIndex = 0; + hasMargin = false; + + get viewportWidth() { + return this.orientation == 'horizontal' ? this.viewportSize : this.viewportCrossSize; + } + + get viewportHeight() { + return this.orientation == 'horizontal' ? this.viewportCrossSize : this.viewportSize; + } +} + +/** Finish initializing the virtual scroll component at the beginning of a test. */ +function finishInit(fixture: ComponentFixture) { + // On the first cycle we render and measure the viewport. + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + flush(); + + // On the second cycle we render the items. + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + flush(); + + // Flush the initial fake scroll event. + animationFrameScheduler.flush(); + flush(); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); +} + +/** Trigger a scroll event on the viewport (optionally setting a new scroll offset). */ +function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) { + if (offset !== undefined) { + viewport.scrollToOffset(offset); + } + dispatchFakeEvent(viewport.scrollable.getElementRef().nativeElement, 'scroll'); + animationFrameScheduler.flush(); +} From f00d2969f4144d58a69660fc0ca2d4d628d28ebb Mon Sep 17 00:00:00 2001 From: Andrew Seguin Date: Wed, 29 May 2024 16:40:55 -0600 Subject: [PATCH 03/61] docs: release notes for the v18.0.1 release --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1c884e34211..199c8c694c3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ + +# 18.0.1 "plastic-baby" (2024-05-29) +### material +| Commit | Type | Description | +| -- | -- | -- | +| [d96b5e39e0](https://github.com/angular/components/commit/d96b5e39e08945b5b4ec92dbc89a7ef44dec1baa) | fix | **core:** M3 themes not inserting loaded marker | +| [b7c0a6ef56](https://github.com/angular/components/commit/b7c0a6ef56ade6d99e9b097e0d616e9e3bb5a9f5) | fix | **form-field:** outline label position ([#29123](https://github.com/angular/components/pull/29123)) | +| [24de3d4884](https://github.com/angular/components/commit/24de3d4884677c427e036258eb2e999a89da03e5) | fix | **menu:** prevent divider styles from bleeding out ([#29111](https://github.com/angular/components/pull/29111)) | +| [2110f2c97e](https://github.com/angular/components/commit/2110f2c97ec8d9b84ee4f8bcd47ca7b95d398879) | fix | **tabs:** avoid pagination infinite loop in safari ([#29121](https://github.com/angular/components/pull/29121)) | +### youtube-player +| Commit | Type | Description | +| -- | -- | -- | +| [466e249cd1](https://github.com/angular/components/commit/466e249cd1eb4b8ce9dd2f9f74c3f4c3cb33cf65) | fix | error when interacting with the player before the API has been loaded ([#29127](https://github.com/angular/components/pull/29127)) | + + + # 18.0.0 "satin-sasquatch" (2024-05-22) ## Breaking Changes From 5f933166fd063d81859970cbecf8c778c0992f7e Mon Sep 17 00:00:00 2001 From: Andrew Seguin Date: Wed, 29 May 2024 19:24:57 -0600 Subject: [PATCH 04/61] release: cut the v18.1.0-next.0 release --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 199c8c694c3a..d7ad4bda1d39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ + +# 18.1.0-next.0 "plastic-moose" (2024-05-29) + + + # 18.0.1 "plastic-baby" (2024-05-29) ### material From 3314414e174a3fda90d18c8d00cf284a77f04e9c Mon Sep 17 00:00:00 2001 From: lsamboretrorabbit <160831207+lsamboretrorabbit@users.noreply.github.com> Date: Thu, 30 May 2024 06:53:17 +0200 Subject: [PATCH 05/61] fix(material/slider): Tick marks changes position as the slider is changed (for a step that is decimal number) (#29108) Fixes the bug in the Angular Material 'slider' component. Changed the function in the calculation from .floor to .round Due to floating-point precision in JavaScript. (1 - 0.9) / 0.1 evaluates to 0.9999999999999999 Even though mathematically it should be 1 The calculation in the code resulted in slightly smaller value. Math.floor(0.9999999999999999) evaluates to 0. Math.round(0.9999999999999999) evaluates to 1. Fixes #29084 --- src/material/slider/slider.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/material/slider/slider.ts b/src/material/slider/slider.ts index 7d2219cc4c7b..f11be294f70c 100644 --- a/src/material/slider/slider.ts +++ b/src/material/slider/slider.ts @@ -870,8 +870,8 @@ export class MatSlider implements AfterViewInit, OnDestroy, _MatSlider { private _updateTickMarkUINonRange(step: number): void { const value = this._getValue(); - let numActive = Math.max(Math.floor((value - this.min) / step), 0); - let numInactive = Math.max(Math.floor((this.max - value) / step), 0); + let numActive = Math.max(Math.round((value - this.min) / step), 0); + let numInactive = Math.max(Math.round((this.max - value) / step), 0); this._isRtl ? numActive++ : numInactive++; this._tickMarks = Array(numActive) @@ -883,9 +883,9 @@ export class MatSlider implements AfterViewInit, OnDestroy, _MatSlider { const endValue = this._getValue(); const startValue = this._getValue(_MatThumb.START); - const numInactiveBeforeStartThumb = Math.max(Math.floor((startValue - this.min) / step), 0); - const numActive = Math.max(Math.floor((endValue - startValue) / step) + 1, 0); - const numInactiveAfterEndThumb = Math.max(Math.floor((this.max - endValue) / step), 0); + const numInactiveBeforeStartThumb = Math.max(Math.round((startValue - this.min) / step), 0); + const numActive = Math.max(Math.round((endValue - startValue) / step) + 1, 0); + const numInactiveAfterEndThumb = Math.max(Math.round((this.max - endValue) / step), 0); this._tickMarks = Array(numInactiveBeforeStartThumb) .fill(_MatTickMark.INACTIVE) .concat( From 9a48e1692d22203368b8e837fe1adf0385070440 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 30 May 2024 09:13:59 +0200 Subject: [PATCH 06/61] test(cdk/drag-drop): split standalone drag tests out into a separate file Separates out the standalone dragging tests into a separate file to make them easier to navigate. --- src/cdk/drag-drop/directives/drag.spec.ts | 1931 +--------------- .../directives/standalone-drag.spec.ts | 1993 +++++++++++++++++ ...e.spec.ts => standalone-drag.zone.spec.ts} | 46 +- 3 files changed, 2007 insertions(+), 1963 deletions(-) create mode 100644 src/cdk/drag-drop/directives/standalone-drag.spec.ts rename src/cdk/drag-drop/directives/{drag.zone.spec.ts => standalone-drag.zone.spec.ts} (67%) diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index fd07ecb51b25..c63c7aba6918 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -15,7 +15,6 @@ import { ChangeDetectorRef, Component, ElementRef, - ErrorHandler, Input, Provider, QueryList, @@ -37,7 +36,6 @@ import {moveItemInArray} from '../drag-utils'; import {CDK_DRAG_CONFIG, DragAxis, DragDropConfig} from './config'; import {CdkDrag} from './drag'; -import {CdkDragHandle} from './drag-handle'; import {CdkDropList} from './drop-list'; import {CdkDropListGroup} from './drop-list-group'; import { @@ -45,7 +43,6 @@ import { assertUpwardSorting, continueDraggingViaTouch, dragElementViaMouse, - dragElementViaTouch, getElementIndexByPosition, getElementSibligsByPosition, makeScrollable, @@ -81,7 +78,7 @@ describe('CdkDrag', () => { }, ...providers, ], - declarations: [PassthroughComponent, componentType, ...extraDeclarations], + declarations: [componentType, ...extraDeclarations], }); if (encapsulation != null) { @@ -94,1681 +91,6 @@ describe('CdkDrag', () => { return TestBed.createComponent(componentType); } - describe('standalone draggable', () => { - describe('mouse dragging', () => { - it('should drag an element freely to a particular position', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - })); - - it('should drag an element freely to a particular position when the page is scrolled', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - const cleanup = makeScrollable(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - scrollTo(0, 500); - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - cleanup(); - })); - - it('should continue dragging the element from where it was left off', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - - dragElementViaMouse(fixture, dragElement, 100, 200); - expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); - })); - - it('should continue dragging from where it was left off when the page is scrolled', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const cleanup = makeScrollable(); - - scrollTo(0, 500); - expect(dragElement.style.transform).toBeFalsy(); - - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - - dragElementViaMouse(fixture, dragElement, 100, 200); - expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); - - cleanup(); - })); - - it('should not drag an element with the right mouse button', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const event = createMouseEvent('mousedown', 50, 100, 2); - - expect(dragElement.style.transform).toBeFalsy(); - - dispatchEvent(dragElement, event); - fixture.detectChanges(); - - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - - expect(dragElement.style.transform).toBeFalsy(); - })); - - it('should not drag the element if it was not moved more than the minimum distance', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable, [], 5); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 2, 2); - expect(dragElement.style.transform).toBeFalsy(); - })); - - it('should be able to stop dragging after a double click', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable, [], 5); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - - dispatchMouseEvent(dragElement, 'mousedown'); - fixture.detectChanges(); - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - dispatchMouseEvent(dragElement, 'mousedown'); - fixture.detectChanges(); - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - - dragElementViaMouse(fixture, dragElement, 50, 50); - dispatchMouseEvent(document, 'mousemove', 100, 100); - fixture.detectChanges(); - - expect(dragElement.style.transform).toBeFalsy(); - })); - - it('should preserve the previous `transform` value', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - dragElement.style.transform = 'translateX(-50%)'; - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px) translateX(-50%)'); - })); - - it('should not generate multiple own `translate3d` values', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - dragElement.style.transform = 'translateY(-50%)'; - - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px) translateY(-50%)'); - - dragElementViaMouse(fixture, dragElement, 100, 200); - expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px) translateY(-50%)'); - })); - - it('should prevent the `mousedown` action for native draggable elements', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - dragElement.draggable = true; - - const mousedownEvent = createMouseEvent('mousedown', 50, 50); - Object.defineProperty(mousedownEvent, 'target', {get: () => dragElement}); - spyOn(mousedownEvent, 'preventDefault').and.callThrough(); - dispatchEvent(dragElement, mousedownEvent); - fixture.detectChanges(); - - dispatchMouseEvent(document, 'mousemove', 50, 50); - fixture.detectChanges(); - - expect(mousedownEvent.preventDefault).toHaveBeenCalled(); - })); - - it('should not start dragging an element with a fake mousedown event', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const event = createMouseEvent('mousedown', 0, 0); - - Object.defineProperties(event, { - buttons: {get: () => 0}, - detail: {get: () => 0}, - }); - - expect(dragElement.style.transform).toBeFalsy(); - - dispatchEvent(dragElement, event); - fixture.detectChanges(); - - dispatchMouseEvent(document, 'mousemove', 20, 100); - fixture.detectChanges(); - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - - expect(dragElement.style.transform).toBeFalsy(); - })); - - it('should prevent the default dragstart action', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const event = dispatchFakeEvent( - fixture.componentInstance.dragElement.nativeElement, - 'dragstart', - ); - fixture.detectChanges(); - - expect(event.defaultPrevented).toBe(true); - })); - - it('should not prevent the default dragstart action when dragging is disabled', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragDisabled.set(true); - fixture.detectChanges(); - const event = dispatchFakeEvent( - fixture.componentInstance.dragElement.nativeElement, - 'dragstart', - ); - fixture.detectChanges(); - - expect(event.defaultPrevented).toBe(false); - })); - }); - - describe('touch dragging', () => { - it('should drag an element freely to a particular position', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaTouch(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - })); - - it('should drag an element freely to a particular position when the page is scrolled', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const cleanup = makeScrollable(); - - scrollTo(0, 500); - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaTouch(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - cleanup(); - })); - - it('should continue dragging the element from where it was left off', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - - dragElementViaTouch(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - - dragElementViaTouch(fixture, dragElement, 100, 200); - expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); - })); - - it('should continue dragging from where it was left off when the page is scrolled', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const cleanup = makeScrollable(); - - scrollTo(0, 500); - expect(dragElement.style.transform).toBeFalsy(); - - dragElementViaTouch(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - - dragElementViaTouch(fixture, dragElement, 100, 200); - expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); - - cleanup(); - })); - - it('should prevent the default `touchmove` action on the page while dragging', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - dispatchTouchEvent(fixture.componentInstance.dragElement.nativeElement, 'touchstart'); - fixture.detectChanges(); - - expect(dispatchTouchEvent(document, 'touchmove').defaultPrevented) - .withContext('Expected initial touchmove to be prevented.') - .toBe(true); - expect(dispatchTouchEvent(document, 'touchmove').defaultPrevented) - .withContext('Expected subsequent touchmose to be prevented.') - .toBe(true); - - dispatchTouchEvent(document, 'touchend'); - fixture.detectChanges(); - })); - - it('should not prevent `touchstart` action for native draggable elements', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - dragElement.draggable = true; - - const touchstartEvent = createTouchEvent('touchstart', 50, 50); - Object.defineProperty(touchstartEvent, 'target', {get: () => dragElement}); - spyOn(touchstartEvent, 'preventDefault').and.callThrough(); - dispatchEvent(dragElement, touchstartEvent); - fixture.detectChanges(); - - dispatchTouchEvent(document, 'touchmove'); - fixture.detectChanges(); - - expect(touchstartEvent.preventDefault).not.toHaveBeenCalled(); - })); - - it('should not start dragging an element with a fake touchstart event', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const event = createTouchEvent('touchstart', 50, 50) as TouchEvent; - - Object.defineProperties(event.touches[0], { - identifier: {get: () => -1}, - radiusX: {get: () => null}, - radiusY: {get: () => null}, - }); - - expect(dragElement.style.transform).toBeFalsy(); - - dispatchEvent(dragElement, event); - fixture.detectChanges(); - - dispatchTouchEvent(document, 'touchmove', 20, 100); - fixture.detectChanges(); - dispatchTouchEvent(document, 'touchmove', 50, 100); - fixture.detectChanges(); - - dispatchTouchEvent(document, 'touchend'); - fixture.detectChanges(); - - expect(dragElement.style.transform).toBeFalsy(); - })); - }); - - describe('mouse dragging when initial transform is none', () => { - it('should drag an element freely to a particular position', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - dragElement.style.transform = 'none'; - - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - })); - }); - - it('should dispatch an event when the user has started dragging', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - startDraggingViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement); - - expect(fixture.componentInstance.startedSpy).toHaveBeenCalled(); - - const event = fixture.componentInstance.startedSpy.calls.mostRecent().args[0]; - - // Assert the event like this, rather than `toHaveBeenCalledWith`, because Jasmine will - // go into an infinite loop trying to stringify the event, if the test fails. - expect(event).toEqual({ - source: fixture.componentInstance.dragInstance, - event: jasmine.anything(), - }); - })); - - it('should dispatch an event when the user has stopped dragging', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 5, 10); - - expect(fixture.componentInstance.endedSpy).toHaveBeenCalled(); - - const event = fixture.componentInstance.endedSpy.calls.mostRecent().args[0]; - - // Assert the event like this, rather than `toHaveBeenCalledWith`, because Jasmine will - // go into an infinite loop trying to stringify the event, if the test fails. - expect(event).toEqual({ - source: fixture.componentInstance.dragInstance, - distance: {x: jasmine.any(Number), y: jasmine.any(Number)}, - dropPoint: {x: jasmine.any(Number), y: jasmine.any(Number)}, - event: jasmine.anything(), - }); - })); - - it('should include the drag distance in the ended event', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 25, 30); - let event = fixture.componentInstance.endedSpy.calls.mostRecent().args[0]; - - expect(event).toEqual({ - source: jasmine.anything(), - distance: {x: 25, y: 30}, - dropPoint: {x: 25, y: 30}, - event: jasmine.anything(), - }); - - dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 40, 50); - event = fixture.componentInstance.endedSpy.calls.mostRecent().args[0]; - - expect(event).toEqual({ - source: jasmine.anything(), - distance: {x: 40, y: 50}, - dropPoint: {x: 40, y: 50}, - event: jasmine.anything(), - }); - })); - - 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 not emit events if it was not moved more than the minimum distance', () => { - const fixture = createComponent(StandaloneDraggable, [], 5); - fixture.detectChanges(); - - const moveSpy = jasmine.createSpy('move spy'); - const subscription = fixture.componentInstance.dragInstance.moved.subscribe(moveSpy); - - dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 2, 2); - - expect(fixture.componentInstance.startedSpy).not.toHaveBeenCalled(); - expect(fixture.componentInstance.releasedSpy).not.toHaveBeenCalled(); - expect(fixture.componentInstance.endedSpy).not.toHaveBeenCalled(); - expect(moveSpy).not.toHaveBeenCalled(); - 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({complete: spy}); - - fixture.destroy(); - expect(spy).toHaveBeenCalled(); - subscription.unsubscribe(); - }); - - it('should be able to lock dragging along the x axis', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragLockAxis.set('x'); - fixture.detectChanges(); - - 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 x axis while using constrainPosition', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragLockAxis.set('x'); - fixture.componentInstance.constrainPosition = ( - {x, y}: Point, - _dragRef: DragRef, - _dimensions: DOMRect, - pickup: Point, - ) => { - x -= pickup.x; - y -= pickup.y; - return {x, y}; - }; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - - 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.componentInstance.dragLockAxis.set('y'); - fixture.detectChanges(); - - 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)'); - })); - - it('should be able to lock dragging along the y axis while using constrainPosition', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragLockAxis.set('y'); - fixture.componentInstance.constrainPosition = ( - {x, y}: Point, - _dragRef: DragRef, - _dimensions: DOMRect, - pickup: Point, - ) => { - x -= pickup.x; - y -= pickup.y; - return {x, y}; - }; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - - 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)'); - })); - - it('should add a class while an element is being dragged', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - const element = fixture.componentInstance.dragElement.nativeElement; - - expect(element.classList).not.toContain('cdk-drag-dragging'); - - startDraggingViaMouse(fixture, element); - - expect(element.classList).toContain('cdk-drag-dragging'); - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - - expect(element.classList).not.toContain('cdk-drag-dragging'); - })); - - it('should add a class while an element is being dragged with OnPush change detection', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggableWithOnPush); - fixture.detectChanges(); - - const element = fixture.componentInstance.dragElement.nativeElement; - - expect(element.classList).not.toContain('cdk-drag-dragging'); - - startDraggingViaMouse(fixture, element); - - expect(element.classList).toContain('cdk-drag-dragging'); - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - - expect(element.classList).not.toContain('cdk-drag-dragging'); - })); - - it('should not add a class if item was not dragged more than the threshold', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable, [], 5); - fixture.detectChanges(); - - const element = fixture.componentInstance.dragElement.nativeElement; - - expect(element.classList).not.toContain('cdk-drag-dragging'); - - startDraggingViaMouse(fixture, element); - - expect(element.classList).not.toContain('cdk-drag-dragging'); - })); - - it('should be able to set an alternate drag root element', fakeAsync(() => { - const fixture = createComponent(DraggableWithAlternateRoot); - fixture.componentInstance.rootElementSelector = '.alternate-root'; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - - const dragRoot = fixture.componentInstance.dragRoot.nativeElement; - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragRoot.style.transform).toBeFalsy(); - expect(dragElement.style.transform).toBeFalsy(); - - dragElementViaMouse(fixture, dragRoot, 50, 100); - - expect(dragRoot.style.transform).toBe('translate3d(50px, 100px, 0px)'); - expect(dragElement.style.transform).toBeFalsy(); - })); - - it('should be able to set the cdkDrag element as handle if it has a different root element', fakeAsync(() => { - const fixture = createComponent(DraggableWithAlternateRootAndSelfHandle); - fixture.detectChanges(); - - const dragRoot = fixture.componentInstance.dragRoot.nativeElement; - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragRoot.style.transform).toBeFalsy(); - expect(dragElement.style.transform).toBeFalsy(); - - // Try dragging the root. This should be possible since the drag element is the handle. - dragElementViaMouse(fixture, dragRoot, 50, 100); - - expect(dragRoot.style.transform).toBeFalsy(); - expect(dragElement.style.transform).toBeFalsy(); - - // Drag via the drag element which acts as the handle. - dragElementViaMouse(fixture, dragElement, 50, 100); - - expect(dragRoot.style.transform).toBe('translate3d(50px, 100px, 0px)'); - expect(dragElement.style.transform).toBeFalsy(); - })); - - it('should be able to set an alternate drag root element for ng-container', fakeAsync(() => { - const fixture = createComponent(DraggableNgContainerWithAlternateRoot); - fixture.detectChanges(); - - const dragRoot = fixture.componentInstance.dragRoot.nativeElement; - - expect(dragRoot.style.transform).toBeFalsy(); - - dragElementViaMouse(fixture, dragRoot, 50, 100); - - expect(dragRoot.style.transform).toBe('translate3d(50px, 100px, 0px)'); - })); - - it('should preserve the initial transform if the root element changes', fakeAsync(() => { - const fixture = createComponent(DraggableWithAlternateRoot); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const alternateRoot = fixture.componentInstance.dragRoot.nativeElement; - - dragElement.style.transform = 'translateX(-50%)'; - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toContain('translateX(-50%)'); - - alternateRoot.style.transform = 'scale(2)'; - fixture.componentInstance.rootElementSelector = '.alternate-root'; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - - dragElementViaMouse(fixture, alternateRoot, 50, 100); - - expect(alternateRoot.style.transform).not.toContain('translateX(-50%)'); - expect(alternateRoot.style.transform).toContain('scale(2)'); - })); - - it('should handle the root element selector changing after init', fakeAsync(() => { - const fixture = createComponent(DraggableWithAlternateRoot); - fixture.detectChanges(); - tick(); - - fixture.componentInstance.rootElementSelector = '.alternate-root'; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - - const dragRoot = fixture.componentInstance.dragRoot.nativeElement; - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragRoot.style.transform).toBeFalsy(); - expect(dragElement.style.transform).toBeFalsy(); - - dragElementViaMouse(fixture, dragRoot, 50, 100); - - expect(dragRoot.style.transform).toBe('translate3d(50px, 100px, 0px)'); - expect(dragElement.style.transform).toBeFalsy(); - })); - - it('should not be able to drag the element if dragging is disabled', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.classList).not.toContain('cdk-drag-disabled'); - - fixture.componentInstance.dragDisabled.set(true); - fixture.detectChanges(); - - expect(dragElement.classList).toContain('cdk-drag-disabled'); - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBeFalsy(); - })); - - it('should enable native drag interactions if dragging is disabled', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const styles = dragElement.style; - - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - - fixture.componentInstance.dragDisabled.set(true); - fixture.detectChanges(); - - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - })); - - it('should enable native drag interactions if not dragging', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const styles = dragElement.style; - - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - })); - - it('should disable native drag interactions if dragging', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const styles = dragElement.style; - - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - - startDraggingViaMouse(fixture, dragElement); - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); - - expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); - })); - - it('should re-enable drag interactions once dragging is over', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const styles = dragElement.style; - - startDraggingViaMouse(fixture, dragElement); - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); - - expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); - - dispatchMouseEvent(document, 'mouseup', 50, 100); - fixture.detectChanges(); - - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - })); - - it('should not stop propagation for the drag sequence start event by default', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - const event = createMouseEvent('mousedown'); - spyOn(event, 'stopPropagation').and.callThrough(); - - dispatchEvent(dragElement, event); - fixture.detectChanges(); - - expect(event.stopPropagation).not.toHaveBeenCalled(); - })); - - it('should not throw if destroyed before the first change detection run', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - - expect(() => { - fixture.destroy(); - }).not.toThrow(); - })); - - it('should enable native drag interactions on the drag item when there is a handle', () => { - const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - expect(dragElement.style.touchAction).not.toBe('none'); - }); - - it('should disable native drag interactions on the drag handle', () => { - const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.detectChanges(); - const styles = fixture.componentInstance.handleElement.nativeElement.style; - expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); - }); - - it('should enable native drag interactions on the drag handle if dragging is disabled', () => { - const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.detectChanges(); - fixture.componentInstance.draggingDisabled = true; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - const styles = fixture.componentInstance.handleElement.nativeElement.style; - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - }); - - it( - 'should enable native drag interactions on the drag handle if dragging is disabled ' + - 'on init', - () => { - const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.componentInstance.draggingDisabled = true; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - const styles = fixture.componentInstance.handleElement.nativeElement.style; - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - }, - ); - - it('should toggle native drag interactions based on whether the handle is disabled', () => { - const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.detectChanges(); - fixture.componentInstance.handleInstance.disabled = true; - fixture.detectChanges(); - const styles = fixture.componentInstance.handleElement.nativeElement.style; - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - - fixture.componentInstance.handleInstance.disabled = false; - fixture.detectChanges(); - expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); - }); - - it('should be able to reset a freely-dragged item to its initial position', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - - fixture.componentInstance.dragInstance.reset(); - expect(dragElement.style.transform).toBeFalsy(); - })); - - it('should preserve initial transform after resetting', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - dragElement.style.transform = 'scale(2)'; - - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px) scale(2)'); - - fixture.componentInstance.dragInstance.reset(); - expect(dragElement.style.transform).toBe('scale(2)'); - })); - - it('should start dragging an item from its initial position after a reset', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - fixture.componentInstance.dragInstance.reset(); - - dragElementViaMouse(fixture, dragElement, 25, 50); - expect(dragElement.style.transform).toBe('translate3d(25px, 50px, 0px)'); - })); - - it('should not dispatch multiple events for a mouse event right after a touch event', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - // Dispatch a touch sequence. - dispatchTouchEvent(dragElement, 'touchstart'); - fixture.detectChanges(); - dispatchTouchEvent(dragElement, 'touchend'); - fixture.detectChanges(); - tick(); - - // Immediately dispatch a mouse sequence to simulate a fake event. - startDraggingViaMouse(fixture, dragElement); - fixture.detectChanges(); - dispatchMouseEvent(dragElement, 'mouseup'); - fixture.detectChanges(); - tick(); - - expect(fixture.componentInstance.startedSpy).toHaveBeenCalledTimes(1); - expect(fixture.componentInstance.endedSpy).toHaveBeenCalledTimes(1); - })); - - it('should round the transform value', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 13.37, 37); - expect(dragElement.style.transform).toBe('translate3d(13px, 37px, 0px)'); - })); - - it('should allow for dragging to be constrained to an element', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.boundary = '.wrapper'; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); - })); - - it('should allow for dragging to be constrained to an element while using constrainPosition', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.boundary = '.wrapper'; - fixture.detectChanges(); - - fixture.componentInstance.dragInstance.constrainPosition = ( - {x, y}: Point, - _dragRef: DragRef, - _dimensions: DOMRect, - pickup: Point, - ) => { - x -= pickup.x; - y -= pickup.y; - return {x, y}; - }; - - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); - })); - - it('should be able to pass in a DOM node as the boundary', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.boundary = fixture.nativeElement.querySelector('.wrapper'); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); - })); - - it('should adjust the x offset if the boundary becomes narrower after a viewport resize', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); - fixture.componentInstance.boundary = boundary; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); - - boundary.style.width = '150px'; - dispatchFakeEvent(window, 'resize'); - tick(20); - - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - })); - - it('should keep the old position if the boundary is invisible after a resize', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); - fixture.componentInstance.boundary = boundary; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); - - boundary.style.display = 'none'; - dispatchFakeEvent(window, 'resize'); - tick(20); - - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); - })); - - it('should handle the element and boundary dimensions changing between drag sequences', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); - fixture.componentInstance.boundary = boundary; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); - - // Bump the width and height of both the boundary and the drag element. - boundary.style.width = boundary.style.height = '300px'; - dragElement.style.width = dragElement.style.height = '150px'; - - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(150px, 150px, 0px)'); - })); - - it('should adjust the y offset if the boundary becomes shorter after a viewport resize', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); - fixture.componentInstance.boundary = boundary; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); - - boundary.style.height = '150px'; - dispatchFakeEvent(window, 'resize'); - tick(20); - - expect(dragElement.style.transform).toBe('translate3d(100px, 50px, 0px)'); - })); - - it( - 'should reset the x offset if the boundary becomes narrower than the element ' + - 'after a resize', - fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); - fixture.componentInstance.boundary = boundary; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); - - boundary.style.width = '50px'; - dispatchFakeEvent(window, 'resize'); - tick(20); - - expect(dragElement.style.transform).toBe('translate3d(0px, 100px, 0px)'); - }), - ); - - it('should reset the y offset if the boundary becomes shorter than the element after a resize', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); - fixture.componentInstance.boundary = boundary; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); - - boundary.style.height = '50px'; - dispatchFakeEvent(window, 'resize'); - tick(20); - - expect(dragElement.style.transform).toBe('translate3d(100px, 0px, 0px)'); - })); - - it('should allow for the position constrain logic to be customized', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - const spy = jasmine.createSpy('constrain position spy').and.returnValue({ - x: 50, - y: 50, - } as Point); - - fixture.componentInstance.constrainPosition = spy; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 300, 300); - - expect(spy).toHaveBeenCalledWith( - jasmine.objectContaining({x: 300, y: 300}), - jasmine.any(DragRef), - jasmine.anything(), - jasmine.objectContaining({x: jasmine.any(Number), y: jasmine.any(Number)}), - ); - - const elementRect = dragElement.getBoundingClientRect(); - expect(Math.floor(elementRect.top)).toBe(50); - expect(Math.floor(elementRect.left)).toBe(50); - })); - - it('should throw if drag item is attached to an ng-container', () => { - const errorHandler = jasmine.createSpyObj(['handleError']); - createComponent(DraggableOnNgContainer, [ - { - provide: ErrorHandler, - useValue: errorHandler, - }, - ]).detectChanges(); - expect(errorHandler.handleError.calls.mostRecent().args[0].message).toMatch( - /^cdkDrag must be attached to an element node/, - ); - }); - - it('should cancel drag if the mouse moves before the delay is elapsed', fakeAsync(() => { - // We can't use Jasmine's `clock` because Zone.js interferes with it. - spyOn(Date, 'now').and.callFake(() => currentTime); - let currentTime = 0; - - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragStartDelay = 1000; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform) - .withContext('Expected element not to be moved by default.') - .toBeFalsy(); - - startDraggingViaMouse(fixture, dragElement); - currentTime += 750; - dispatchMouseEvent(document, 'mousemove', 50, 100); - currentTime += 500; - fixture.detectChanges(); - - expect(dragElement.style.transform) - .withContext('Expected element not to be moved if the mouse moved before the delay.') - .toBeFalsy(); - })); - - it('should enable native drag interactions if mouse moves before the delay', fakeAsync(() => { - // We can't use Jasmine's `clock` because Zone.js interferes with it. - spyOn(Date, 'now').and.callFake(() => currentTime); - let currentTime = 0; - - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragStartDelay = 1000; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const styles = dragElement.style; - - expect(dragElement.style.transform) - .withContext('Expected element not to be moved by default.') - .toBeFalsy(); - - startDraggingViaMouse(fixture, dragElement); - currentTime += 750; - dispatchMouseEvent(document, 'mousemove', 50, 100); - currentTime += 500; - fixture.detectChanges(); - - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - })); - - it('should allow dragging after the drag start delay is elapsed', fakeAsync(() => { - // We can't use Jasmine's `clock` because Zone.js interferes with it. - spyOn(Date, 'now').and.callFake(() => currentTime); - let currentTime = 0; - - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragStartDelay = 500; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform) - .withContext('Expected element not to be moved by default.') - .toBeFalsy(); - - dispatchMouseEvent(dragElement, 'mousedown'); - fixture.detectChanges(); - currentTime += 750; - - // The first `mousemove` here starts the sequence and the second one moves the element. - dispatchMouseEvent(document, 'mousemove', 50, 100); - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); - - expect(dragElement.style.transform) - .withContext('Expected element to be dragged after all the time has passed.') - .toBe('translate3d(50px, 100px, 0px)'); - })); - - it('should not prevent the default touch action before the delay has elapsed', fakeAsync(() => { - spyOn(Date, 'now').and.callFake(() => currentTime); - let currentTime = 0; - - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragStartDelay = 500; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform) - .withContext('Expected element not to be moved by default.') - .toBeFalsy(); - - dispatchTouchEvent(dragElement, 'touchstart'); - fixture.detectChanges(); - currentTime += 250; - - expect(dispatchTouchEvent(document, 'touchmove', 50, 100).defaultPrevented).toBe(false); - })); - - it('should handle the drag delay as a string', fakeAsync(() => { - // We can't use Jasmine's `clock` because Zone.js interferes with it. - spyOn(Date, 'now').and.callFake(() => currentTime); - let currentTime = 0; - - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragStartDelay = '500'; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform) - .withContext('Expected element not to be moved by default.') - .toBeFalsy(); - - dispatchMouseEvent(dragElement, 'mousedown'); - fixture.detectChanges(); - currentTime += 750; - - // The first `mousemove` here starts the sequence and the second one moves the element. - dispatchMouseEvent(document, 'mousemove', 50, 100); - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); - - expect(dragElement.style.transform) - .withContext('Expected element to be dragged after all the time has passed.') - .toBe('translate3d(50px, 100px, 0px)'); - })); - - it('should be able to configure the drag start delay based on the event type', fakeAsync(() => { - // We can't use Jasmine's `clock` because Zone.js interferes with it. - spyOn(Date, 'now').and.callFake(() => currentTime); - let currentTime = 0; - - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragStartDelay = {touch: 500, mouse: 0}; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform) - .withContext('Expected element not to be moved by default.') - .toBeFalsy(); - - dragElementViaTouch(fixture, dragElement, 50, 100); - expect(dragElement.style.transform) - .withContext('Expected element not to be moved via touch because it has a delay.') - .toBeFalsy(); - - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform) - .withContext('Expected element to be moved via mouse because it has no delay.') - .toBe('translate3d(50px, 100px, 0px)'); - })); - - it('should be able to get the current position', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const dragInstance = fixture.componentInstance.dragInstance; - - expect(dragInstance.getFreeDragPosition()).toEqual({x: 0, y: 0}); - - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); - - dragElementViaMouse(fixture, dragElement, 100, 200); - expect(dragInstance.getFreeDragPosition()).toEqual({x: 150, y: 300}); - })); - - it('should be able to set the current position programmatically', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const dragInstance = fixture.componentInstance.dragInstance; - - dragInstance.setFreeDragPosition({x: 50, y: 100}); - - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); - })); - - it('should be able to set the current position', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.freeDragPosition = {x: 50, y: 100}; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const dragInstance = fixture.componentInstance.dragInstance; - - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); - })); - - it('should be able to get the up-to-date position as the user is dragging', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const dragInstance = fixture.componentInstance.dragInstance; - - expect(dragInstance.getFreeDragPosition()).toEqual({x: 0, y: 0}); - - startDraggingViaMouse(fixture, dragElement); - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); - - expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); - - dispatchMouseEvent(document, 'mousemove', 100, 200); - fixture.detectChanges(); - - expect(dragInstance.getFreeDragPosition()).toEqual({x: 100, y: 200}); - })); - - it('should react to changes in the free drag position', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.freeDragPosition = {x: 50, y: 100}; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - - fixture.componentInstance.freeDragPosition = {x: 100, y: 200}; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - - expect(dragElement.style.transform).toBe('translate3d(100px, 200px, 0px)'); - })); - - it('should be able to continue dragging after the current position was set', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.freeDragPosition = {x: 50, y: 100}; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - - dragElementViaMouse(fixture, dragElement, 100, 200); - - expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); - })); - - it('should include the dragged distance as the user is dragging', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const spy = jasmine.createSpy('moved spy'); - const subscription = fixture.componentInstance.dragInstance.moved.subscribe(spy); - - startDraggingViaMouse(fixture, dragElement); - - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); - - let event = spy.calls.mostRecent().args[0]; - expect(event.distance).toEqual({x: 50, y: 100}); - - dispatchMouseEvent(document, 'mousemove', 75, 50); - fixture.detectChanges(); - - event = spy.calls.mostRecent().args[0]; - expect(event.distance).toEqual({x: 75, y: 50}); - - subscription.unsubscribe(); - })); - - it('should be able to configure the drag input defaults through a provider', fakeAsync(() => { - const config: DragDropConfig = { - draggingDisabled: true, - dragStartDelay: 1337, - lockAxis: 'y', - constrainPosition: () => ({x: 1337, y: 42}), - previewClass: 'custom-preview-class', - boundaryElement: '.boundary', - rootElementSelector: '.root', - previewContainer: 'parent', - }; - - const fixture = createComponent(PlainStandaloneDraggable, [ - { - provide: CDK_DRAG_CONFIG, - useValue: config, - }, - ]); - fixture.detectChanges(); - const drag = fixture.componentInstance.dragInstance; - expect(drag.disabled).toBe(true); - expect(drag.dragStartDelay).toBe(1337); - expect(drag.lockAxis).toBe('y'); - expect(drag.constrainPosition).toBe(config.constrainPosition); - expect(drag.previewClass).toBe('custom-preview-class'); - expect(drag.boundaryElement).toBe('.boundary'); - expect(drag.rootElementSelector).toBe('.root'); - expect(drag.previewContainer).toBe('parent'); - })); - - it('should not throw if touches and changedTouches are empty', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - startDraggingViaTouch(fixture, dragElement); - continueDraggingViaTouch(fixture, 50, 100); - - const event = createTouchEvent('touchend', 50, 100); - Object.defineProperties(event, { - touches: {get: () => []}, - changedTouches: {get: () => []}, - }); - - expect(() => { - dispatchEvent(document, event); - fixture.detectChanges(); - tick(); - }).not.toThrow(); - })); - - it('should update the free drag position if the page is scrolled', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - const cleanup = makeScrollable(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - startDraggingViaMouse(fixture, dragElement, 0, 0); - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); - - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - - scrollTo(0, 500); - dispatchFakeEvent(document, 'scroll'); - fixture.detectChanges(); - expect(dragElement.style.transform).toBe('translate3d(50px, 600px, 0px)'); - - cleanup(); - })); - - it( - 'should update the free drag position if the user moves their pointer after the page ' + - 'is scrolled', - fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - const cleanup = makeScrollable(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - startDraggingViaMouse(fixture, dragElement, 0, 0); - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); - - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - - scrollTo(0, 500); - dispatchFakeEvent(document, 'scroll'); - fixture.detectChanges(); - dispatchMouseEvent(document, 'mousemove', 50, 200); - fixture.detectChanges(); - - expect(dragElement.style.transform).toBe('translate3d(50px, 700px, 0px)'); - - cleanup(); - }), - ); - }); - - describe('draggable with a handle', () => { - it('should not be able to drag the entire element if it has a handle', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBeFalsy(); - })); - - it('should be able to drag an element using its handle', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const handle = fixture.componentInstance.handleElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, handle, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - })); - - it('should not be able to drag the element if the handle is disabled', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const handle = fixture.componentInstance.handleElement.nativeElement; - - fixture.componentInstance.handleInstance.disabled = true; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, handle, 50, 100); - expect(dragElement.style.transform).toBeFalsy(); - })); - - it('should not be able to drag the element if the handle is disabled before init', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggableWithPreDisabledHandle); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const handle = fixture.componentInstance.handleElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, handle, 50, 100); - expect(dragElement.style.transform).toBeFalsy(); - })); - - it('should not be able to drag using the handle if the element is disabled', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const handle = fixture.componentInstance.handleElement.nativeElement; - - fixture.componentInstance.draggingDisabled = true; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, handle, 50, 100); - expect(dragElement.style.transform).toBeFalsy(); - })); - - it('should be able to use a handle that was added after init', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggableWithDelayedHandle); - - fixture.detectChanges(); - fixture.componentInstance.showHandle = true; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const handle = fixture.componentInstance.handleElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, handle, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - })); - - it('should be able to use more than one handle to drag the element', fakeAsync(async () => { - const fixture = createComponent(StandaloneDraggableWithMultipleHandles); - fixture.detectChanges(); - - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const handles = fixture.componentInstance.handles.map(handle => handle.element.nativeElement); - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, handles[1], 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - - dragElementViaMouse(fixture, handles[0], 100, 200); - expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); - })); - - it('should be able to drag with a handle that is not a direct descendant', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggableWithIndirectHandle); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const handle = fixture.componentInstance.handleElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 50, 100); - - expect(dragElement.style.transform) - .withContext('Expected not to be able to drag the element by itself.') - .toBeFalsy(); - - dragElementViaMouse(fixture, handle, 50, 100); - expect(dragElement.style.transform) - .withContext('Expected to drag the element by its handle.') - .toBe('translate3d(50px, 100px, 0px)'); - })); - - it('should disable the tap highlight while dragging via the handle', fakeAsync(() => { - // This test is irrelevant if the browser doesn't support styling the tap highlight color. - if (!('webkitTapHighlightColor' in document.body.style)) { - return; - } - - const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const handle = fixture.componentInstance.handleElement.nativeElement; - - expect((dragElement.style as any).webkitTapHighlightColor).toBeFalsy(); - - startDraggingViaMouse(fixture, handle); - - expect((dragElement.style as any).webkitTapHighlightColor).toBe('transparent'); - - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); - - dispatchMouseEvent(document, 'mouseup', 50, 100); - fixture.detectChanges(); - - expect((dragElement.style as any).webkitTapHighlightColor).toBeFalsy(); - })); - - it('should preserve any existing `webkitTapHighlightColor`', fakeAsync(() => { - // This test is irrelevant if the browser doesn't support styling the tap highlight color. - if (!('webkitTapHighlightColor' in document.body.style)) { - return; - } - - const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const handle = fixture.componentInstance.handleElement.nativeElement; - - (dragElement.style as any).webkitTapHighlightColor = 'purple'; - - startDraggingViaMouse(fixture, handle); - - expect((dragElement.style as any).webkitTapHighlightColor).toBe('transparent'); - - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); - - dispatchMouseEvent(document, 'mouseup', 50, 100); - fixture.detectChanges(); - - expect((dragElement.style as any).webkitTapHighlightColor).toBe('purple'); - })); - - it('should throw if drag handle is attached to an ng-container', fakeAsync(() => { - expect(() => { - createComponent(DragHandleOnNgContainer).detectChanges(); - flush(); - }).toThrowError(/^cdkDragHandle must be attached to an element node/); - })); - - it('should be able to drag an element using a handle with a shadow DOM child', fakeAsync(() => { - if (!_supportsShadowDom()) { - return; - } - - const fixture = createComponent( - StandaloneDraggableWithShadowInsideHandle, - undefined, - undefined, - [ShadowWrapper], - ); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const handleChild = fixture.componentInstance.handleChild.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, handleChild, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - })); - - it('should prevent default dragStart on handle, not on entire draggable', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.detectChanges(); - - const draggableEvent = dispatchFakeEvent( - fixture.componentInstance.dragElement.nativeElement, - 'dragstart', - ); - fixture.detectChanges(); - - const handleEvent = dispatchFakeEvent( - fixture.componentInstance.handleElement.nativeElement, - 'dragstart', - true, - ); - fixture.detectChanges(); - - expect(draggableEvent.defaultPrevented).toBe(false); - expect(handleEvent.defaultPrevented).toBe(true); - })); - }); - describe('in a drop container', () => { it('should be able to attach data to the drop container', () => { const fixture = createComponent(DraggableInDropZone); @@ -6739,174 +5061,6 @@ describe('CdkDrag', () => { }); }); -@Component({ - template: ` -
-
-
- `, -}) -class StandaloneDraggable { - @ViewChild('dragElement') dragElement: ElementRef; - @ViewChild(CdkDrag) dragInstance: CdkDrag; - startedSpy = jasmine.createSpy('started spy'); - endedSpy = jasmine.createSpy('ended spy'); - releasedSpy = jasmine.createSpy('released spy'); - boundary: string | HTMLElement; - dragStartDelay: number | string | {touch: number; mouse: number}; - constrainPosition: ( - userPointerPosition: Point, - dragRef: DragRef, - dimensions: DOMRect, - pickupPositionInElement: Point, - ) => Point; - freeDragPosition?: {x: number; y: number}; - dragDisabled = signal(false); - dragLockAxis = signal(undefined); -} - -@Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - template: ` -
- `, -}) -class StandaloneDraggableWithOnPush { - @ViewChild('dragElement') dragElement: ElementRef; - @ViewChild(CdkDrag) dragInstance: CdkDrag; -} - -@Component({ - template: ` -
-
-
- `, -}) -class StandaloneDraggableWithHandle { - @ViewChild('dragElement') dragElement: ElementRef; - @ViewChild('handleElement') handleElement: ElementRef; - @ViewChild(CdkDrag) dragInstance: CdkDrag; - @ViewChild(CdkDragHandle) handleInstance: CdkDragHandle; - draggingDisabled = false; -} - -@Component({ - template: ` -
-
-
- `, -}) -class StandaloneDraggableWithPreDisabledHandle { - @ViewChild('dragElement') dragElement: ElementRef; - @ViewChild('handleElement') handleElement: ElementRef; - @ViewChild(CdkDrag) dragInstance: CdkDrag; - disableHandle = true; -} - -@Component({ - template: ` -
- @if (showHandle) { -
- } -
- `, -}) -class StandaloneDraggableWithDelayedHandle { - @ViewChild('dragElement') dragElement: ElementRef; - @ViewChild('handleElement') handleElement: ElementRef; - showHandle = false; -} - -@Component({ - template: ` -
- - -
-
-
- `, -}) -class StandaloneDraggableWithIndirectHandle { - @ViewChild('dragElement') dragElement: ElementRef; - @ViewChild('handleElement') handleElement: ElementRef; -} - -@Component({ - selector: 'shadow-wrapper', - template: '', - encapsulation: ViewEncapsulation.ShadowDom, -}) -class ShadowWrapper {} - -@Component({ - template: ` -
-
- -
-
-
-
- `, -}) -class StandaloneDraggableWithShadowInsideHandle { - @ViewChild('dragElement') dragElement: ElementRef; - @ViewChild('handleChild') handleChild: ElementRef; -} - -@Component({ - encapsulation: ViewEncapsulation.None, - styles: ` - .cdk-drag-handle { - position: absolute; - top: 0; - background: green; - width: 10px; - height: 10px; - } - `, - template: ` -
-
-
-
- `, -}) -class StandaloneDraggableWithMultipleHandles { - @ViewChild('dragElement') dragElement: ElementRef; - @ViewChildren(CdkDragHandle) handles: QueryList; -} - // TODO(crisbeto): figure out why switch `*ngFor` with `@for` here causes a test failure. const DROP_ZONE_FIXTURE_TEMPLATE = `
-
-
- `, -}) -class DraggableWithAlternateRoot { - @ViewChild('dragElement') dragElement: ElementRef; - @ViewChild('dragRoot') dragRoot: ElementRef; - @ViewChild(CdkDrag) dragInstance: CdkDrag; - rootElementSelector: string; -} - @Component({ encapsulation: ViewEncapsulation.None, styles: ` @@ -7542,22 +5678,6 @@ class NestedDropListGroups { @ViewChild('listTwo') listTwo: CdkDropList; } -@Component({ - template: ` - - `, -}) -class DraggableOnNgContainer {} - -@Component({ - template: ` -
- -
- `, -}) -class DragHandleOnNgContainer {} - @Component({ template: ` @@ -7687,16 +5807,6 @@ class DraggableWithCanvasInDropZone extends DraggableInDropZone implements After }) class DraggableWithInvalidCanvasInDropZone extends DraggableInDropZone {} -/** - * Component that passes through whatever content is projected into it. - * Used to test having drag elements being projected into a component. - */ -@Component({ - selector: 'passthrough-component', - template: '', -}) -class PassthroughComponent {} - /** Component that wraps a drop container and uses OnPush change detection. */ @Component({ selector: 'wrapped-drop-container', @@ -7833,13 +5943,6 @@ class NestedDropZones { items = ['Zero', 'One', 'Two', 'Three']; } -@Component({ - template: `
`, -}) -class PlainStandaloneDraggable { - @ViewChild(CdkDrag) dragInstance: CdkDrag; -} - @Component({ template: `
`, }) @@ -7920,24 +6023,6 @@ class DraggableInHorizontalFlexDropZoneWithMatchSizePreview { }) class ConnectedDropZonesWithIntermediateSibling extends ConnectedDropZones {} -@Component({ - template: ` -
-
-
- `, -}) -class DraggableWithAlternateRootAndSelfHandle { - @ViewChild('dragElement') dragElement: ElementRef; - @ViewChild('dragRoot') dragRoot: ElementRef; - @ViewChild(CdkDrag) dragInstance: CdkDrag; -} - @Component({ template: `
- -
-
-
- `, -}) -class DraggableNgContainerWithAlternateRoot { - @ViewChild('dragRoot') dragRoot: ElementRef; - @ViewChild(CdkDrag) dragInstance: CdkDrag; -} diff --git a/src/cdk/drag-drop/directives/standalone-drag.spec.ts b/src/cdk/drag-drop/directives/standalone-drag.spec.ts new file mode 100644 index 000000000000..68c612bf64fb --- /dev/null +++ b/src/cdk/drag-drop/directives/standalone-drag.spec.ts @@ -0,0 +1,1993 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + ErrorHandler, + Provider, + QueryList, + Type, + ViewChild, + ViewChildren, + ViewEncapsulation, + signal, +} from '@angular/core'; +import {ComponentFixture, TestBed, fakeAsync, flush, tick} from '@angular/core/testing'; +import {CdkScrollableModule} from '@angular/cdk/scrolling'; +import {DragDropModule} from '../drag-drop-module'; +import { + dispatchEvent, + createMouseEvent, + createTouchEvent, + dispatchFakeEvent, + dispatchMouseEvent, + dispatchTouchEvent, +} from '@angular/cdk/testing/private'; +import {_supportsShadowDom} from '@angular/cdk/platform'; +import {CdkDragHandle} from './drag-handle'; +import {CdkDrag} from './drag'; +import {CDK_DRAG_CONFIG, DragAxis, DragDropConfig} from './config'; +import {DragRef, Point} from '../drag-ref'; +import { + continueDraggingViaTouch, + dragElementViaMouse, + dragElementViaTouch, + makeScrollable, + startDraggingViaMouse, + startDraggingViaTouch, +} from './test-utils.spec'; + +describe('Standalone CdkDrag', () => { + function createComponent( + componentType: Type, + providers: Provider[] = [], + dragDistance = 0, + extraDeclarations: Type[] = [], + ): ComponentFixture { + TestBed.configureTestingModule({ + imports: [DragDropModule, CdkScrollableModule], + providers: [ + { + provide: CDK_DRAG_CONFIG, + useValue: { + // We default the `dragDistance` to zero, because the majority of the tests + // don't care about it and drags are a lot easier to simulate when we don't + // have to deal with thresholds. + dragStartThreshold: dragDistance, + pointerDirectionChangeThreshold: 5, + } as DragDropConfig, + }, + ...providers, + ], + declarations: [componentType, ...extraDeclarations], + }); + + TestBed.compileComponents(); + return TestBed.createComponent(componentType); + } + + describe('standalone draggable', () => { + describe('mouse dragging', () => { + it('should drag an element freely to a particular position', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + })); + + it('should drag an element freely to a particular position when the page is scrolled', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const cleanup = makeScrollable(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + scrollTo(0, 500); + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + cleanup(); + })); + + it('should continue dragging the element from where it was left off', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + + dragElementViaMouse(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); + })); + + it('should continue dragging from where it was left off when the page is scrolled', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const cleanup = makeScrollable(); + + scrollTo(0, 500); + expect(dragElement.style.transform).toBeFalsy(); + + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + + dragElementViaMouse(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); + + cleanup(); + })); + + it('should not drag an element with the right mouse button', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const event = createMouseEvent('mousedown', 50, 100, 2); + + expect(dragElement.style.transform).toBeFalsy(); + + dispatchEvent(dragElement, event); + fixture.detectChanges(); + + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + + expect(dragElement.style.transform).toBeFalsy(); + })); + + it('should not drag the element if it was not moved more than the minimum distance', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable, [], 5); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 2, 2); + expect(dragElement.style.transform).toBeFalsy(); + })); + + it('should be able to stop dragging after a double click', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable, [], 5); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + + dispatchMouseEvent(dragElement, 'mousedown'); + fixture.detectChanges(); + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + dispatchMouseEvent(dragElement, 'mousedown'); + fixture.detectChanges(); + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + + dragElementViaMouse(fixture, dragElement, 50, 50); + dispatchMouseEvent(document, 'mousemove', 100, 100); + fixture.detectChanges(); + + expect(dragElement.style.transform).toBeFalsy(); + })); + + it('should preserve the previous `transform` value', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElement.style.transform = 'translateX(-50%)'; + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px) translateX(-50%)'); + })); + + it('should not generate multiple own `translate3d` values', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElement.style.transform = 'translateY(-50%)'; + + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px) translateY(-50%)'); + + dragElementViaMouse(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px) translateY(-50%)'); + })); + + it('should prevent the `mousedown` action for native draggable elements', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElement.draggable = true; + + const mousedownEvent = createMouseEvent('mousedown', 50, 50); + Object.defineProperty(mousedownEvent, 'target', {get: () => dragElement}); + spyOn(mousedownEvent, 'preventDefault').and.callThrough(); + dispatchEvent(dragElement, mousedownEvent); + fixture.detectChanges(); + + dispatchMouseEvent(document, 'mousemove', 50, 50); + fixture.detectChanges(); + + expect(mousedownEvent.preventDefault).toHaveBeenCalled(); + })); + + it('should not start dragging an element with a fake mousedown event', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const event = createMouseEvent('mousedown', 0, 0); + + Object.defineProperties(event, { + buttons: {get: () => 0}, + detail: {get: () => 0}, + }); + + expect(dragElement.style.transform).toBeFalsy(); + + dispatchEvent(dragElement, event); + fixture.detectChanges(); + + dispatchMouseEvent(document, 'mousemove', 20, 100); + fixture.detectChanges(); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + + expect(dragElement.style.transform).toBeFalsy(); + })); + + it('should prevent the default dragstart action', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const event = dispatchFakeEvent( + fixture.componentInstance.dragElement.nativeElement, + 'dragstart', + ); + fixture.detectChanges(); + + expect(event.defaultPrevented).toBe(true); + })); + + it('should not prevent the default dragstart action when dragging is disabled', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragDisabled.set(true); + fixture.detectChanges(); + const event = dispatchFakeEvent( + fixture.componentInstance.dragElement.nativeElement, + 'dragstart', + ); + fixture.detectChanges(); + + expect(event.defaultPrevented).toBe(false); + })); + }); + + describe('touch dragging', () => { + it('should drag an element freely to a particular position', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaTouch(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + })); + + it('should drag an element freely to a particular position when the page is scrolled', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const cleanup = makeScrollable(); + + scrollTo(0, 500); + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaTouch(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + cleanup(); + })); + + it('should continue dragging the element from where it was left off', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + + dragElementViaTouch(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + + dragElementViaTouch(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); + })); + + it('should continue dragging from where it was left off when the page is scrolled', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const cleanup = makeScrollable(); + + scrollTo(0, 500); + expect(dragElement.style.transform).toBeFalsy(); + + dragElementViaTouch(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + + dragElementViaTouch(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); + + cleanup(); + })); + + it('should prevent the default `touchmove` action on the page while dragging', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + dispatchTouchEvent(fixture.componentInstance.dragElement.nativeElement, 'touchstart'); + fixture.detectChanges(); + + expect(dispatchTouchEvent(document, 'touchmove').defaultPrevented) + .withContext('Expected initial touchmove to be prevented.') + .toBe(true); + expect(dispatchTouchEvent(document, 'touchmove').defaultPrevented) + .withContext('Expected subsequent touchmose to be prevented.') + .toBe(true); + + dispatchTouchEvent(document, 'touchend'); + fixture.detectChanges(); + })); + + it('should not prevent `touchstart` action for native draggable elements', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElement.draggable = true; + + const touchstartEvent = createTouchEvent('touchstart', 50, 50); + Object.defineProperty(touchstartEvent, 'target', {get: () => dragElement}); + spyOn(touchstartEvent, 'preventDefault').and.callThrough(); + dispatchEvent(dragElement, touchstartEvent); + fixture.detectChanges(); + + dispatchTouchEvent(document, 'touchmove'); + fixture.detectChanges(); + + expect(touchstartEvent.preventDefault).not.toHaveBeenCalled(); + })); + + it('should not start dragging an element with a fake touchstart event', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const event = createTouchEvent('touchstart', 50, 50) as TouchEvent; + + Object.defineProperties(event.touches[0], { + identifier: {get: () => -1}, + radiusX: {get: () => null}, + radiusY: {get: () => null}, + }); + + expect(dragElement.style.transform).toBeFalsy(); + + dispatchEvent(dragElement, event); + fixture.detectChanges(); + + dispatchTouchEvent(document, 'touchmove', 20, 100); + fixture.detectChanges(); + dispatchTouchEvent(document, 'touchmove', 50, 100); + fixture.detectChanges(); + + dispatchTouchEvent(document, 'touchend'); + fixture.detectChanges(); + + expect(dragElement.style.transform).toBeFalsy(); + })); + }); + + describe('mouse dragging when initial transform is none', () => { + it('should drag an element freely to a particular position', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + dragElement.style.transform = 'none'; + + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + })); + }); + + it('should dispatch an event when the user has started dragging', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + startDraggingViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement); + + expect(fixture.componentInstance.startedSpy).toHaveBeenCalled(); + + const event = fixture.componentInstance.startedSpy.calls.mostRecent().args[0]; + + // Assert the event like this, rather than `toHaveBeenCalledWith`, because Jasmine will + // go into an infinite loop trying to stringify the event, if the test fails. + expect(event).toEqual({ + source: fixture.componentInstance.dragInstance, + event: jasmine.anything(), + }); + })); + + it('should dispatch an event when the user has stopped dragging', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 5, 10); + + expect(fixture.componentInstance.endedSpy).toHaveBeenCalled(); + + const event = fixture.componentInstance.endedSpy.calls.mostRecent().args[0]; + + // Assert the event like this, rather than `toHaveBeenCalledWith`, because Jasmine will + // go into an infinite loop trying to stringify the event, if the test fails. + expect(event).toEqual({ + source: fixture.componentInstance.dragInstance, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)}, + dropPoint: {x: jasmine.any(Number), y: jasmine.any(Number)}, + event: jasmine.anything(), + }); + })); + + it('should include the drag distance in the ended event', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 25, 30); + let event = fixture.componentInstance.endedSpy.calls.mostRecent().args[0]; + + expect(event).toEqual({ + source: jasmine.anything(), + distance: {x: 25, y: 30}, + dropPoint: {x: 25, y: 30}, + event: jasmine.anything(), + }); + + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 40, 50); + event = fixture.componentInstance.endedSpy.calls.mostRecent().args[0]; + + expect(event).toEqual({ + source: jasmine.anything(), + distance: {x: 40, y: 50}, + dropPoint: {x: 40, y: 50}, + event: jasmine.anything(), + }); + })); + + 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 not emit events if it was not moved more than the minimum distance', () => { + const fixture = createComponent(StandaloneDraggable, [], 5); + fixture.detectChanges(); + + const moveSpy = jasmine.createSpy('move spy'); + const subscription = fixture.componentInstance.dragInstance.moved.subscribe(moveSpy); + + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 2, 2); + + expect(fixture.componentInstance.startedSpy).not.toHaveBeenCalled(); + expect(fixture.componentInstance.releasedSpy).not.toHaveBeenCalled(); + expect(fixture.componentInstance.endedSpy).not.toHaveBeenCalled(); + expect(moveSpy).not.toHaveBeenCalled(); + 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({complete: spy}); + + fixture.destroy(); + expect(spy).toHaveBeenCalled(); + subscription.unsubscribe(); + }); + + it('should be able to lock dragging along the x axis', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragLockAxis.set('x'); + fixture.detectChanges(); + + 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 x axis while using constrainPosition', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragLockAxis.set('x'); + fixture.componentInstance.constrainPosition = ( + {x, y}: Point, + _dragRef: DragRef, + _dimensions: DOMRect, + pickup: Point, + ) => { + x -= pickup.x; + y -= pickup.y; + return {x, y}; + }; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + 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.componentInstance.dragLockAxis.set('y'); + fixture.detectChanges(); + + 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)'); + })); + + it('should be able to lock dragging along the y axis while using constrainPosition', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragLockAxis.set('y'); + fixture.componentInstance.constrainPosition = ( + {x, y}: Point, + _dragRef: DragRef, + _dimensions: DOMRect, + pickup: Point, + ) => { + x -= pickup.x; + y -= pickup.y; + return {x, y}; + }; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + 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)'); + })); + + it('should add a class while an element is being dragged', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const element = fixture.componentInstance.dragElement.nativeElement; + + expect(element.classList).not.toContain('cdk-drag-dragging'); + + startDraggingViaMouse(fixture, element); + + expect(element.classList).toContain('cdk-drag-dragging'); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + + expect(element.classList).not.toContain('cdk-drag-dragging'); + })); + + it('should add a class while an element is being dragged with OnPush change detection', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggableWithOnPush); + fixture.detectChanges(); + + const element = fixture.componentInstance.dragElement.nativeElement; + + expect(element.classList).not.toContain('cdk-drag-dragging'); + + startDraggingViaMouse(fixture, element); + + expect(element.classList).toContain('cdk-drag-dragging'); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + + expect(element.classList).not.toContain('cdk-drag-dragging'); + })); + + it('should not add a class if item was not dragged more than the threshold', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable, [], 5); + fixture.detectChanges(); + + const element = fixture.componentInstance.dragElement.nativeElement; + + expect(element.classList).not.toContain('cdk-drag-dragging'); + + startDraggingViaMouse(fixture, element); + + expect(element.classList).not.toContain('cdk-drag-dragging'); + })); + + it('should be able to set an alternate drag root element', fakeAsync(() => { + const fixture = createComponent(DraggableWithAlternateRoot); + fixture.componentInstance.rootElementSelector = '.alternate-root'; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + const dragRoot = fixture.componentInstance.dragRoot.nativeElement; + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragRoot.style.transform).toBeFalsy(); + expect(dragElement.style.transform).toBeFalsy(); + + dragElementViaMouse(fixture, dragRoot, 50, 100); + + expect(dragRoot.style.transform).toBe('translate3d(50px, 100px, 0px)'); + expect(dragElement.style.transform).toBeFalsy(); + })); + + it('should be able to set the cdkDrag element as handle if it has a different root element', fakeAsync(() => { + const fixture = createComponent(DraggableWithAlternateRootAndSelfHandle); + fixture.detectChanges(); + + const dragRoot = fixture.componentInstance.dragRoot.nativeElement; + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragRoot.style.transform).toBeFalsy(); + expect(dragElement.style.transform).toBeFalsy(); + + // Try dragging the root. This should be possible since the drag element is the handle. + dragElementViaMouse(fixture, dragRoot, 50, 100); + + expect(dragRoot.style.transform).toBeFalsy(); + expect(dragElement.style.transform).toBeFalsy(); + + // Drag via the drag element which acts as the handle. + dragElementViaMouse(fixture, dragElement, 50, 100); + + expect(dragRoot.style.transform).toBe('translate3d(50px, 100px, 0px)'); + expect(dragElement.style.transform).toBeFalsy(); + })); + + it('should be able to set an alternate drag root element for ng-container', fakeAsync(() => { + const fixture = createComponent(DraggableNgContainerWithAlternateRoot); + fixture.detectChanges(); + + const dragRoot = fixture.componentInstance.dragRoot.nativeElement; + + expect(dragRoot.style.transform).toBeFalsy(); + + dragElementViaMouse(fixture, dragRoot, 50, 100); + + expect(dragRoot.style.transform).toBe('translate3d(50px, 100px, 0px)'); + })); + + it('should preserve the initial transform if the root element changes', fakeAsync(() => { + const fixture = createComponent(DraggableWithAlternateRoot); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const alternateRoot = fixture.componentInstance.dragRoot.nativeElement; + + dragElement.style.transform = 'translateX(-50%)'; + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toContain('translateX(-50%)'); + + alternateRoot.style.transform = 'scale(2)'; + fixture.componentInstance.rootElementSelector = '.alternate-root'; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + dragElementViaMouse(fixture, alternateRoot, 50, 100); + + expect(alternateRoot.style.transform).not.toContain('translateX(-50%)'); + expect(alternateRoot.style.transform).toContain('scale(2)'); + })); + + it('should handle the root element selector changing after init', fakeAsync(() => { + const fixture = createComponent(DraggableWithAlternateRoot); + fixture.detectChanges(); + tick(); + + fixture.componentInstance.rootElementSelector = '.alternate-root'; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + const dragRoot = fixture.componentInstance.dragRoot.nativeElement; + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragRoot.style.transform).toBeFalsy(); + expect(dragElement.style.transform).toBeFalsy(); + + dragElementViaMouse(fixture, dragRoot, 50, 100); + + expect(dragRoot.style.transform).toBe('translate3d(50px, 100px, 0px)'); + expect(dragElement.style.transform).toBeFalsy(); + })); + + it('should not be able to drag the element if dragging is disabled', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.classList).not.toContain('cdk-drag-disabled'); + + fixture.componentInstance.dragDisabled.set(true); + fixture.detectChanges(); + + expect(dragElement.classList).toContain('cdk-drag-disabled'); + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBeFalsy(); + })); + + it('should enable native drag interactions if dragging is disabled', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const styles = dragElement.style; + + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + + fixture.componentInstance.dragDisabled.set(true); + fixture.detectChanges(); + + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + })); + + it('should enable native drag interactions if not dragging', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const styles = dragElement.style; + + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + })); + + it('should disable native drag interactions if dragging', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const styles = dragElement.style; + + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + + startDraggingViaMouse(fixture, dragElement); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); + + expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); + })); + + it('should re-enable drag interactions once dragging is over', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const styles = dragElement.style; + + startDraggingViaMouse(fixture, dragElement); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); + + expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); + + dispatchMouseEvent(document, 'mouseup', 50, 100); + fixture.detectChanges(); + + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + })); + + it('should not stop propagation for the drag sequence start event by default', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + const event = createMouseEvent('mousedown'); + spyOn(event, 'stopPropagation').and.callThrough(); + + dispatchEvent(dragElement, event); + fixture.detectChanges(); + + expect(event.stopPropagation).not.toHaveBeenCalled(); + })); + + it('should not throw if destroyed before the first change detection run', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + + expect(() => { + fixture.destroy(); + }).not.toThrow(); + })); + + it('should enable native drag interactions on the drag item when there is a handle', () => { + const fixture = createComponent(StandaloneDraggableWithHandle); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + expect(dragElement.style.touchAction).not.toBe('none'); + }); + + it('should disable native drag interactions on the drag handle', () => { + const fixture = createComponent(StandaloneDraggableWithHandle); + fixture.detectChanges(); + const styles = fixture.componentInstance.handleElement.nativeElement.style; + expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); + }); + + it('should enable native drag interactions on the drag handle if dragging is disabled', () => { + const fixture = createComponent(StandaloneDraggableWithHandle); + fixture.detectChanges(); + fixture.componentInstance.draggingDisabled = true; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + const styles = fixture.componentInstance.handleElement.nativeElement.style; + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + }); + + it( + 'should enable native drag interactions on the drag handle if dragging is disabled ' + + 'on init', + () => { + const fixture = createComponent(StandaloneDraggableWithHandle); + fixture.componentInstance.draggingDisabled = true; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + const styles = fixture.componentInstance.handleElement.nativeElement.style; + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + }, + ); + + it('should toggle native drag interactions based on whether the handle is disabled', () => { + const fixture = createComponent(StandaloneDraggableWithHandle); + fixture.detectChanges(); + fixture.componentInstance.handleInstance.disabled = true; + fixture.detectChanges(); + const styles = fixture.componentInstance.handleElement.nativeElement.style; + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + + fixture.componentInstance.handleInstance.disabled = false; + fixture.detectChanges(); + expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); + }); + + it('should be able to reset a freely-dragged item to its initial position', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + + fixture.componentInstance.dragInstance.reset(); + expect(dragElement.style.transform).toBeFalsy(); + })); + + it('should preserve initial transform after resetting', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElement.style.transform = 'scale(2)'; + + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px) scale(2)'); + + fixture.componentInstance.dragInstance.reset(); + expect(dragElement.style.transform).toBe('scale(2)'); + })); + + it('should start dragging an item from its initial position after a reset', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + fixture.componentInstance.dragInstance.reset(); + + dragElementViaMouse(fixture, dragElement, 25, 50); + expect(dragElement.style.transform).toBe('translate3d(25px, 50px, 0px)'); + })); + + it('should not dispatch multiple events for a mouse event right after a touch event', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + // Dispatch a touch sequence. + dispatchTouchEvent(dragElement, 'touchstart'); + fixture.detectChanges(); + dispatchTouchEvent(dragElement, 'touchend'); + fixture.detectChanges(); + tick(); + + // Immediately dispatch a mouse sequence to simulate a fake event. + startDraggingViaMouse(fixture, dragElement); + fixture.detectChanges(); + dispatchMouseEvent(dragElement, 'mouseup'); + fixture.detectChanges(); + tick(); + + expect(fixture.componentInstance.startedSpy).toHaveBeenCalledTimes(1); + expect(fixture.componentInstance.endedSpy).toHaveBeenCalledTimes(1); + })); + + it('should round the transform value', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 13.37, 37); + expect(dragElement.style.transform).toBe('translate3d(13px, 37px, 0px)'); + })); + + it('should allow for dragging to be constrained to an element', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.boundary = '.wrapper'; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + })); + + it('should allow for dragging to be constrained to an element while using constrainPosition', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.boundary = '.wrapper'; + fixture.detectChanges(); + + fixture.componentInstance.dragInstance.constrainPosition = ( + {x, y}: Point, + _dragRef: DragRef, + _dimensions: DOMRect, + pickup: Point, + ) => { + x -= pickup.x; + y -= pickup.y; + return {x, y}; + }; + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + })); + + it('should be able to pass in a DOM node as the boundary', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.boundary = fixture.nativeElement.querySelector('.wrapper'); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + })); + + it('should adjust the x offset if the boundary becomes narrower after a viewport resize', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); + fixture.componentInstance.boundary = boundary; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + + boundary.style.width = '150px'; + dispatchFakeEvent(window, 'resize'); + tick(20); + + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + })); + + it('should keep the old position if the boundary is invisible after a resize', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); + fixture.componentInstance.boundary = boundary; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + + boundary.style.display = 'none'; + dispatchFakeEvent(window, 'resize'); + tick(20); + + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + })); + + it('should handle the element and boundary dimensions changing between drag sequences', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); + fixture.componentInstance.boundary = boundary; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + + // Bump the width and height of both the boundary and the drag element. + boundary.style.width = boundary.style.height = '300px'; + dragElement.style.width = dragElement.style.height = '150px'; + + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(150px, 150px, 0px)'); + })); + + it('should adjust the y offset if the boundary becomes shorter after a viewport resize', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); + fixture.componentInstance.boundary = boundary; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + + boundary.style.height = '150px'; + dispatchFakeEvent(window, 'resize'); + tick(20); + + expect(dragElement.style.transform).toBe('translate3d(100px, 50px, 0px)'); + })); + + it( + 'should reset the x offset if the boundary becomes narrower than the element ' + + 'after a resize', + fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); + fixture.componentInstance.boundary = boundary; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + + boundary.style.width = '50px'; + dispatchFakeEvent(window, 'resize'); + tick(20); + + expect(dragElement.style.transform).toBe('translate3d(0px, 100px, 0px)'); + }), + ); + + it('should reset the y offset if the boundary becomes shorter than the element after a resize', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); + fixture.componentInstance.boundary = boundary; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + + boundary.style.height = '50px'; + dispatchFakeEvent(window, 'resize'); + tick(20); + + expect(dragElement.style.transform).toBe('translate3d(100px, 0px, 0px)'); + })); + + it('should allow for the position constrain logic to be customized', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const spy = jasmine.createSpy('constrain position spy').and.returnValue({ + x: 50, + y: 50, + } as Point); + + fixture.componentInstance.constrainPosition = spy; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 300, 300); + + expect(spy).toHaveBeenCalledWith( + jasmine.objectContaining({x: 300, y: 300}), + jasmine.any(DragRef), + jasmine.anything(), + jasmine.objectContaining({x: jasmine.any(Number), y: jasmine.any(Number)}), + ); + + const elementRect = dragElement.getBoundingClientRect(); + expect(Math.floor(elementRect.top)).toBe(50); + expect(Math.floor(elementRect.left)).toBe(50); + })); + + it('should throw if drag item is attached to an ng-container', () => { + const errorHandler = jasmine.createSpyObj(['handleError']); + createComponent(DraggableOnNgContainer, [ + { + provide: ErrorHandler, + useValue: errorHandler, + }, + ]).detectChanges(); + expect(errorHandler.handleError.calls.mostRecent().args[0].message).toMatch( + /^cdkDrag must be attached to an element node/, + ); + }); + + it('should cancel drag if the mouse moves before the delay is elapsed', fakeAsync(() => { + // We can't use Jasmine's `clock` because Zone.js interferes with it. + spyOn(Date, 'now').and.callFake(() => currentTime); + let currentTime = 0; + + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragStartDelay = 1000; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform) + .withContext('Expected element not to be moved by default.') + .toBeFalsy(); + + startDraggingViaMouse(fixture, dragElement); + currentTime += 750; + dispatchMouseEvent(document, 'mousemove', 50, 100); + currentTime += 500; + fixture.detectChanges(); + + expect(dragElement.style.transform) + .withContext('Expected element not to be moved if the mouse moved before the delay.') + .toBeFalsy(); + })); + + it('should enable native drag interactions if mouse moves before the delay', fakeAsync(() => { + // We can't use Jasmine's `clock` because Zone.js interferes with it. + spyOn(Date, 'now').and.callFake(() => currentTime); + let currentTime = 0; + + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragStartDelay = 1000; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const styles = dragElement.style; + + expect(dragElement.style.transform) + .withContext('Expected element not to be moved by default.') + .toBeFalsy(); + + startDraggingViaMouse(fixture, dragElement); + currentTime += 750; + dispatchMouseEvent(document, 'mousemove', 50, 100); + currentTime += 500; + fixture.detectChanges(); + + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + })); + + it('should allow dragging after the drag start delay is elapsed', fakeAsync(() => { + // We can't use Jasmine's `clock` because Zone.js interferes with it. + spyOn(Date, 'now').and.callFake(() => currentTime); + let currentTime = 0; + + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragStartDelay = 500; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform) + .withContext('Expected element not to be moved by default.') + .toBeFalsy(); + + dispatchMouseEvent(dragElement, 'mousedown'); + fixture.detectChanges(); + currentTime += 750; + + // The first `mousemove` here starts the sequence and the second one moves the element. + dispatchMouseEvent(document, 'mousemove', 50, 100); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); + + expect(dragElement.style.transform) + .withContext('Expected element to be dragged after all the time has passed.') + .toBe('translate3d(50px, 100px, 0px)'); + })); + + it('should not prevent the default touch action before the delay has elapsed', fakeAsync(() => { + spyOn(Date, 'now').and.callFake(() => currentTime); + let currentTime = 0; + + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragStartDelay = 500; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform) + .withContext('Expected element not to be moved by default.') + .toBeFalsy(); + + dispatchTouchEvent(dragElement, 'touchstart'); + fixture.detectChanges(); + currentTime += 250; + + expect(dispatchTouchEvent(document, 'touchmove', 50, 100).defaultPrevented).toBe(false); + })); + + it('should handle the drag delay as a string', fakeAsync(() => { + // We can't use Jasmine's `clock` because Zone.js interferes with it. + spyOn(Date, 'now').and.callFake(() => currentTime); + let currentTime = 0; + + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragStartDelay = '500'; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform) + .withContext('Expected element not to be moved by default.') + .toBeFalsy(); + + dispatchMouseEvent(dragElement, 'mousedown'); + fixture.detectChanges(); + currentTime += 750; + + // The first `mousemove` here starts the sequence and the second one moves the element. + dispatchMouseEvent(document, 'mousemove', 50, 100); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); + + expect(dragElement.style.transform) + .withContext('Expected element to be dragged after all the time has passed.') + .toBe('translate3d(50px, 100px, 0px)'); + })); + + it('should be able to configure the drag start delay based on the event type', fakeAsync(() => { + // We can't use Jasmine's `clock` because Zone.js interferes with it. + spyOn(Date, 'now').and.callFake(() => currentTime); + let currentTime = 0; + + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragStartDelay = {touch: 500, mouse: 0}; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform) + .withContext('Expected element not to be moved by default.') + .toBeFalsy(); + + dragElementViaTouch(fixture, dragElement, 50, 100); + expect(dragElement.style.transform) + .withContext('Expected element not to be moved via touch because it has a delay.') + .toBeFalsy(); + + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform) + .withContext('Expected element to be moved via mouse because it has no delay.') + .toBe('translate3d(50px, 100px, 0px)'); + })); + + it('should be able to get the current position', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const dragInstance = fixture.componentInstance.dragInstance; + + expect(dragInstance.getFreeDragPosition()).toEqual({x: 0, y: 0}); + + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); + + dragElementViaMouse(fixture, dragElement, 100, 200); + expect(dragInstance.getFreeDragPosition()).toEqual({x: 150, y: 300}); + })); + + it('should be able to set the current position programmatically', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const dragInstance = fixture.componentInstance.dragInstance; + + dragInstance.setFreeDragPosition({x: 50, y: 100}); + + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); + })); + + it('should be able to set the current position', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.freeDragPosition = {x: 50, y: 100}; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const dragInstance = fixture.componentInstance.dragInstance; + + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); + })); + + it('should be able to get the up-to-date position as the user is dragging', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const dragInstance = fixture.componentInstance.dragInstance; + + expect(dragInstance.getFreeDragPosition()).toEqual({x: 0, y: 0}); + + startDraggingViaMouse(fixture, dragElement); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); + + expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); + + dispatchMouseEvent(document, 'mousemove', 100, 200); + fixture.detectChanges(); + + expect(dragInstance.getFreeDragPosition()).toEqual({x: 100, y: 200}); + })); + + it('should react to changes in the free drag position', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.freeDragPosition = {x: 50, y: 100}; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + + fixture.componentInstance.freeDragPosition = {x: 100, y: 200}; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + expect(dragElement.style.transform).toBe('translate3d(100px, 200px, 0px)'); + })); + + it('should be able to continue dragging after the current position was set', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.freeDragPosition = {x: 50, y: 100}; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + + dragElementViaMouse(fixture, dragElement, 100, 200); + + expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); + })); + + it('should include the dragged distance as the user is dragging', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const spy = jasmine.createSpy('moved spy'); + const subscription = fixture.componentInstance.dragInstance.moved.subscribe(spy); + + startDraggingViaMouse(fixture, dragElement); + + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); + + let event = spy.calls.mostRecent().args[0]; + expect(event.distance).toEqual({x: 50, y: 100}); + + dispatchMouseEvent(document, 'mousemove', 75, 50); + fixture.detectChanges(); + + event = spy.calls.mostRecent().args[0]; + expect(event.distance).toEqual({x: 75, y: 50}); + + subscription.unsubscribe(); + })); + + it('should be able to configure the drag input defaults through a provider', fakeAsync(() => { + const config: DragDropConfig = { + draggingDisabled: true, + dragStartDelay: 1337, + lockAxis: 'y', + constrainPosition: () => ({x: 1337, y: 42}), + previewClass: 'custom-preview-class', + boundaryElement: '.boundary', + rootElementSelector: '.root', + previewContainer: 'parent', + }; + + const fixture = createComponent(PlainStandaloneDraggable, [ + { + provide: CDK_DRAG_CONFIG, + useValue: config, + }, + ]); + fixture.detectChanges(); + const drag = fixture.componentInstance.dragInstance; + expect(drag.disabled).toBe(true); + expect(drag.dragStartDelay).toBe(1337); + expect(drag.lockAxis).toBe('y'); + expect(drag.constrainPosition).toBe(config.constrainPosition); + expect(drag.previewClass).toBe('custom-preview-class'); + expect(drag.boundaryElement).toBe('.boundary'); + expect(drag.rootElementSelector).toBe('.root'); + expect(drag.previewContainer).toBe('parent'); + })); + + it('should not throw if touches and changedTouches are empty', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + startDraggingViaTouch(fixture, dragElement); + continueDraggingViaTouch(fixture, 50, 100); + + const event = createTouchEvent('touchend', 50, 100); + Object.defineProperties(event, { + touches: {get: () => []}, + changedTouches: {get: () => []}, + }); + + expect(() => { + dispatchEvent(document, event); + fixture.detectChanges(); + tick(); + }).not.toThrow(); + })); + + it('should update the free drag position if the page is scrolled', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const cleanup = makeScrollable(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + startDraggingViaMouse(fixture, dragElement, 0, 0); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); + + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + + scrollTo(0, 500); + dispatchFakeEvent(document, 'scroll'); + fixture.detectChanges(); + expect(dragElement.style.transform).toBe('translate3d(50px, 600px, 0px)'); + + cleanup(); + })); + + it( + 'should update the free drag position if the user moves their pointer after the page ' + + 'is scrolled', + fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const cleanup = makeScrollable(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + startDraggingViaMouse(fixture, dragElement, 0, 0); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); + + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + + scrollTo(0, 500); + dispatchFakeEvent(document, 'scroll'); + fixture.detectChanges(); + dispatchMouseEvent(document, 'mousemove', 50, 200); + fixture.detectChanges(); + + expect(dragElement.style.transform).toBe('translate3d(50px, 700px, 0px)'); + + cleanup(); + }), + ); + }); + + describe('draggable with a handle', () => { + it('should not be able to drag the entire element if it has a handle', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggableWithHandle); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBeFalsy(); + })); + + it('should be able to drag an element using its handle', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggableWithHandle); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const handle = fixture.componentInstance.handleElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, handle, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + })); + + it('should not be able to drag the element if the handle is disabled', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggableWithHandle); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const handle = fixture.componentInstance.handleElement.nativeElement; + + fixture.componentInstance.handleInstance.disabled = true; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, handle, 50, 100); + expect(dragElement.style.transform).toBeFalsy(); + })); + + it('should not be able to drag the element if the handle is disabled before init', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggableWithPreDisabledHandle); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const handle = fixture.componentInstance.handleElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, handle, 50, 100); + expect(dragElement.style.transform).toBeFalsy(); + })); + + it('should not be able to drag using the handle if the element is disabled', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggableWithHandle); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const handle = fixture.componentInstance.handleElement.nativeElement; + + fixture.componentInstance.draggingDisabled = true; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, handle, 50, 100); + expect(dragElement.style.transform).toBeFalsy(); + })); + + it('should be able to use a handle that was added after init', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggableWithDelayedHandle); + + fixture.detectChanges(); + fixture.componentInstance.showHandle = true; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const handle = fixture.componentInstance.handleElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, handle, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + })); + + it('should be able to use more than one handle to drag the element', fakeAsync(async () => { + const fixture = createComponent(StandaloneDraggableWithMultipleHandles); + fixture.detectChanges(); + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const handles = fixture.componentInstance.handles.map(handle => handle.element.nativeElement); + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, handles[1], 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + + dragElementViaMouse(fixture, handles[0], 100, 200); + expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); + })); + + it('should be able to drag with a handle that is not a direct descendant', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggableWithIndirectHandle); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const handle = fixture.componentInstance.handleElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 50, 100); + + expect(dragElement.style.transform) + .withContext('Expected not to be able to drag the element by itself.') + .toBeFalsy(); + + dragElementViaMouse(fixture, handle, 50, 100); + expect(dragElement.style.transform) + .withContext('Expected to drag the element by its handle.') + .toBe('translate3d(50px, 100px, 0px)'); + })); + + it('should disable the tap highlight while dragging via the handle', fakeAsync(() => { + // This test is irrelevant if the browser doesn't support styling the tap highlight color. + if (!('webkitTapHighlightColor' in document.body.style)) { + return; + } + + const fixture = createComponent(StandaloneDraggableWithHandle); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const handle = fixture.componentInstance.handleElement.nativeElement; + + expect((dragElement.style as any).webkitTapHighlightColor).toBeFalsy(); + + startDraggingViaMouse(fixture, handle); + + expect((dragElement.style as any).webkitTapHighlightColor).toBe('transparent'); + + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); + + dispatchMouseEvent(document, 'mouseup', 50, 100); + fixture.detectChanges(); + + expect((dragElement.style as any).webkitTapHighlightColor).toBeFalsy(); + })); + + it('should preserve any existing `webkitTapHighlightColor`', fakeAsync(() => { + // This test is irrelevant if the browser doesn't support styling the tap highlight color. + if (!('webkitTapHighlightColor' in document.body.style)) { + return; + } + + const fixture = createComponent(StandaloneDraggableWithHandle); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const handle = fixture.componentInstance.handleElement.nativeElement; + + (dragElement.style as any).webkitTapHighlightColor = 'purple'; + + startDraggingViaMouse(fixture, handle); + + expect((dragElement.style as any).webkitTapHighlightColor).toBe('transparent'); + + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); + + dispatchMouseEvent(document, 'mouseup', 50, 100); + fixture.detectChanges(); + + expect((dragElement.style as any).webkitTapHighlightColor).toBe('purple'); + })); + + it('should throw if drag handle is attached to an ng-container', fakeAsync(() => { + expect(() => { + createComponent(DragHandleOnNgContainer).detectChanges(); + flush(); + }).toThrowError(/^cdkDragHandle must be attached to an element node/); + })); + + it('should be able to drag an element using a handle with a shadow DOM child', fakeAsync(() => { + if (!_supportsShadowDom()) { + return; + } + + const fixture = createComponent( + StandaloneDraggableWithShadowInsideHandle, + undefined, + undefined, + [ShadowWrapper], + ); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const handleChild = fixture.componentInstance.handleChild.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, handleChild, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + })); + + it('should prevent default dragStart on handle, not on entire draggable', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggableWithHandle); + fixture.detectChanges(); + + const draggableEvent = dispatchFakeEvent( + fixture.componentInstance.dragElement.nativeElement, + 'dragstart', + ); + fixture.detectChanges(); + + const handleEvent = dispatchFakeEvent( + fixture.componentInstance.handleElement.nativeElement, + 'dragstart', + true, + ); + fixture.detectChanges(); + + expect(draggableEvent.defaultPrevented).toBe(false); + expect(handleEvent.defaultPrevented).toBe(true); + })); + }); +}); + +@Component({ + template: ` +
+
+
+ `, +}) +class StandaloneDraggable { + @ViewChild('dragElement') dragElement: ElementRef; + @ViewChild(CdkDrag) dragInstance: CdkDrag; + startedSpy = jasmine.createSpy('started spy'); + endedSpy = jasmine.createSpy('ended spy'); + releasedSpy = jasmine.createSpy('released spy'); + boundary: string | HTMLElement; + dragStartDelay: number | string | {touch: number; mouse: number}; + constrainPosition: ( + userPointerPosition: Point, + dragRef: DragRef, + dimensions: DOMRect, + pickupPositionInElement: Point, + ) => Point; + freeDragPosition?: {x: number; y: number}; + dragDisabled = signal(false); + dragLockAxis = signal(undefined); +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+ `, +}) +class StandaloneDraggableWithOnPush { + @ViewChild('dragElement') dragElement: ElementRef; + @ViewChild(CdkDrag) dragInstance: CdkDrag; +} + +@Component({ + template: ` +
+
+
+ `, +}) +class StandaloneDraggableWithHandle { + @ViewChild('dragElement') dragElement: ElementRef; + @ViewChild('handleElement') handleElement: ElementRef; + @ViewChild(CdkDrag) dragInstance: CdkDrag; + @ViewChild(CdkDragHandle) handleInstance: CdkDragHandle; + draggingDisabled = false; +} + +@Component({ + template: ` +
+
+
+ `, +}) +class StandaloneDraggableWithPreDisabledHandle { + @ViewChild('dragElement') dragElement: ElementRef; + @ViewChild('handleElement') handleElement: ElementRef; + @ViewChild(CdkDrag) dragInstance: CdkDrag; + disableHandle = true; +} + +@Component({ + template: ` +
+ @if (showHandle) { +
+ } +
+ `, +}) +class StandaloneDraggableWithDelayedHandle { + @ViewChild('dragElement') dragElement: ElementRef; + @ViewChild('handleElement') handleElement: ElementRef; + showHandle = false; +} + +@Component({ + template: ` +
+ + +
+
+
+ `, +}) +class StandaloneDraggableWithIndirectHandle { + @ViewChild('dragElement') dragElement: ElementRef; + @ViewChild('handleElement') handleElement: ElementRef; +} + +@Component({ + selector: 'shadow-wrapper', + template: '', + encapsulation: ViewEncapsulation.ShadowDom, +}) +class ShadowWrapper {} + +@Component({ + template: ` +
+
+ +
+
+
+
+ `, +}) +class StandaloneDraggableWithShadowInsideHandle { + @ViewChild('dragElement') dragElement: ElementRef; + @ViewChild('handleChild') handleChild: ElementRef; +} + +@Component({ + encapsulation: ViewEncapsulation.None, + styles: ` + .cdk-drag-handle { + position: absolute; + top: 0; + background: green; + width: 10px; + height: 10px; + } + `, + template: ` +
+
+
+
+ `, +}) +class StandaloneDraggableWithMultipleHandles { + @ViewChild('dragElement') dragElement: ElementRef; + @ViewChildren(CdkDragHandle) handles: QueryList; +} + +@Component({ + template: ` +
+
+
+ `, +}) +class DraggableWithAlternateRoot { + @ViewChild('dragElement') dragElement: ElementRef; + @ViewChild('dragRoot') dragRoot: ElementRef; + @ViewChild(CdkDrag) dragInstance: CdkDrag; + rootElementSelector: string; +} + +@Component({ + template: ` + + `, +}) +class DraggableOnNgContainer {} + +@Component({ + template: ` +
+ +
+ `, +}) +class DragHandleOnNgContainer {} + +@Component({ + template: ` +
+
+
+ `, +}) +class DraggableWithAlternateRootAndSelfHandle { + @ViewChild('dragElement') dragElement: ElementRef; + @ViewChild('dragRoot') dragRoot: ElementRef; + @ViewChild(CdkDrag) dragInstance: CdkDrag; +} + +@Component({ + template: ` +
+ +
+
+
+ `, +}) +class DraggableNgContainerWithAlternateRoot { + @ViewChild('dragRoot') dragRoot: ElementRef; + @ViewChild(CdkDrag) dragInstance: CdkDrag; +} + +/** + * Component that passes through whatever content is projected into it. + * Used to test having drag elements being projected into a component. + */ +@Component({ + selector: 'passthrough-component', + template: '', +}) +class PassthroughComponent {} + +@Component({ + template: `
`, +}) +class PlainStandaloneDraggable { + @ViewChild(CdkDrag) dragInstance: CdkDrag; +} diff --git a/src/cdk/drag-drop/directives/drag.zone.spec.ts b/src/cdk/drag-drop/directives/standalone-drag.zone.spec.ts similarity index 67% rename from src/cdk/drag-drop/directives/drag.zone.spec.ts rename to src/cdk/drag-drop/directives/standalone-drag.zone.spec.ts index f3ff46139f0e..0acb83681139 100644 --- a/src/cdk/drag-drop/directives/drag.zone.spec.ts +++ b/src/cdk/drag-drop/directives/standalone-drag.zone.spec.ts @@ -6,7 +6,6 @@ import { Provider, Type, ViewChild, - ViewEncapsulation, provideZoneChangeDetection, } from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; @@ -16,13 +15,12 @@ import {CDK_DRAG_CONFIG, DragDropConfig} from './config'; import {CdkDrag} from './drag'; import {dragElementViaMouse} from './test-utils.spec'; -describe('CdkDrag Zone.js integration', () => { +describe('Standalone CdkDrag Zone.js integration', () => { function createComponent( componentType: Type, providers: Provider[] = [], dragDistance = 0, extraDeclarations: Type[] = [], - encapsulation?: ViewEncapsulation, ): ComponentFixture { TestBed.configureTestingModule({ imports: [DragDropModule, CdkScrollableModule], @@ -40,48 +38,30 @@ describe('CdkDrag Zone.js integration', () => { }, ...providers, ], - declarations: [PassthroughComponent, componentType, ...extraDeclarations], + declarations: [componentType, ...extraDeclarations], }); - if (encapsulation != null) { - TestBed.overrideComponent(componentType, { - set: {encapsulation}, - }); - } - TestBed.compileComponents(); return TestBed.createComponent(componentType); } - describe('standalone draggable', () => { - it('should emit to `moved` inside the NgZone', () => { - const fixture = createComponent(StandaloneDraggable); - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + it('should emit to `moved` inside the NgZone', () => { + const fixture = createComponent(StandaloneDraggable); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); - const spy = jasmine.createSpy('move spy'); - const subscription = fixture.componentInstance.dragInstance.moved.subscribe(() => - spy(NgZone.isInAngularZone()), - ); + 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); + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 10, 20); + expect(spy).toHaveBeenCalledWith(true); - subscription.unsubscribe(); - }); + subscription.unsubscribe(); }); }); -/** - * Component that passes through whatever content is projected into it. - * Used to test having drag elements being projected into a component. - */ -@Component({ - selector: 'passthrough-component', - template: '', -}) -class PassthroughComponent {} - @Component({ template: `
From 2d258df35bf8bd6a39f538f35cdef271233f97b3 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 30 May 2024 09:31:58 +0200 Subject: [PATCH 07/61] test(cdk/drag-drop): reuse component creation function Reuses the `createComponent` function instead of having to duplicate it in all the tests. Also makes it more ergonomic and reduces some test nesting. --- src/cdk/drag-drop/directives/drag.spec.ts | 199 +- .../directives/standalone-drag.spec.ts | 2320 ++++++++--------- .../directives/standalone-drag.zone.spec.ts | 37 +- .../drag-drop/directives/test-utils.spec.ts | 48 +- 4 files changed, 1274 insertions(+), 1330 deletions(-) diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index c63c7aba6918..03ba6a04cebe 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -16,20 +16,17 @@ import { Component, ElementRef, Input, - Provider, QueryList, - Type, ViewChild, ViewChildren, ViewEncapsulation, inject, signal, } from '@angular/core'; -import {ComponentFixture, TestBed, fakeAsync, flush, tick} from '@angular/core/testing'; +import {TestBed, fakeAsync, flush, tick} from '@angular/core/testing'; import {of as observableOf} from 'rxjs'; import {extendStyles} from '../dom/styling'; -import {DragDropModule} from '../drag-drop-module'; import {CdkDragDrop, CdkDragEnter, CdkDragStart} from '../drag-events'; import {DragRef, Point, PreviewContainer} from '../drag-ref'; import {moveItemInArray} from '../drag-utils'; @@ -39,6 +36,7 @@ import {CdkDrag} from './drag'; import {CdkDropList} from './drop-list'; import {CdkDropListGroup} from './drop-list-group'; import { + createComponent, assertDownwardSorting, assertUpwardSorting, continueDraggingViaTouch, @@ -56,41 +54,6 @@ const ITEM_HEIGHT = 25; const ITEM_WIDTH = 75; describe('CdkDrag', () => { - function createComponent( - componentType: Type, - providers: Provider[] = [], - dragDistance = 0, - extraDeclarations: Type[] = [], - encapsulation?: ViewEncapsulation, - ): ComponentFixture { - TestBed.configureTestingModule({ - imports: [DragDropModule, CdkScrollableModule], - providers: [ - { - provide: CDK_DRAG_CONFIG, - useValue: { - // We default the `dragDistance` to zero, because the majority of the tests - // don't care about it and drags are a lot easier to simulate when we don't - // have to deal with thresholds. - dragStartThreshold: dragDistance, - pointerDirectionChangeThreshold: 5, - } as DragDropConfig, - }, - ...providers, - ], - declarations: [componentType, ...extraDeclarations], - }); - - if (encapsulation != null) { - TestBed.overrideComponent(componentType, { - set: {encapsulation}, - }); - } - - TestBed.compileComponents(); - return TestBed.createComponent(componentType); - } - describe('in a drop container', () => { it('should be able to attach data to the drop container', () => { const fixture = createComponent(DraggableInDropZone); @@ -250,7 +213,7 @@ describe('CdkDrag', () => { })); it('should not toggle dragging class if the element was not dragged more than the threshold', fakeAsync(() => { - const fixture = createComponent(DraggableInDropZone, [], 5); + const fixture = createComponent(DraggableInDropZone, {dragDistance: 5}); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const dropZone = fixture.componentInstance.dropInstance; @@ -565,12 +528,14 @@ describe('CdkDrag', () => { })); it('should dispatch the correct `dropped` event in RTL horizontal drop zone', fakeAsync(() => { - const fixture = createComponent(DraggableInHorizontalDropZone, [ - { - provide: Directionality, - useValue: {value: 'rtl', change: observableOf()}, - }, - ]); + const fixture = createComponent(DraggableInHorizontalDropZone, { + providers: [ + { + provide: Directionality, + useValue: {value: 'rtl', change: observableOf()}, + }, + ], + }); fixture.nativeElement.setAttribute('dir', 'rtl'); fixture.detectChanges(); @@ -720,13 +685,9 @@ describe('CdkDrag', () => { return; } - const fixture = createComponent( - DraggableInScrollableVerticalDropZone, - [], - undefined, - [], - ViewEncapsulation.ShadowDom, - ); + const fixture = createComponent(DraggableInScrollableVerticalDropZone, { + encapsulation: ViewEncapsulation.ShadowDom, + }); fixture.detectChanges(); const dragItems = fixture.componentInstance.dragItems; const firstItem = dragItems.first; @@ -901,15 +862,17 @@ describe('CdkDrag', () => { })); it('should be able to configure the preview z-index', fakeAsync(() => { - const fixture = createComponent(DraggableInDropZone, [ - { - provide: CDK_DRAG_CONFIG, - useValue: { - dragStartThreshold: 0, - zIndex: 3000, + const fixture = createComponent(DraggableInDropZone, { + providers: [ + { + provide: CDK_DRAG_CONFIG, + useValue: { + dragStartThreshold: 0, + zIndex: 3000, + }, }, - }, - ]); + ], + }); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; startDraggingViaMouse(fixture, item); @@ -1017,13 +980,9 @@ describe('CdkDrag', () => { return; } - const fixture = createComponent( - DraggableInScrollableParentContainer, - [], - undefined, - [], - ViewEncapsulation.ShadowDom, - ); + const fixture = createComponent(DraggableInScrollableParentContainer, { + encapsulation: ViewEncapsulation.ShadowDom, + }); fixture.componentInstance.boundarySelector = '.cdk-drop-list'; fixture.detectChanges(); @@ -1191,7 +1150,7 @@ describe('CdkDrag', () => { })); it('should not create a preview if the element was not dragged far enough', fakeAsync(() => { - const fixture = createComponent(DraggableInDropZone, [], 5); + const fixture = createComponent(DraggableInDropZone, {dragDistance: 5}); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -1201,12 +1160,14 @@ describe('CdkDrag', () => { })); it('should pass the proper direction to the preview in rtl', fakeAsync(() => { - const fixture = createComponent(DraggableInDropZone, [ - { - provide: Directionality, - useValue: {value: 'rtl', change: observableOf()}, - }, - ]); + const fixture = createComponent(DraggableInDropZone, { + providers: [ + { + provide: Directionality, + useValue: {value: 'rtl', change: observableOf()}, + }, + ], + }); fixture.detectChanges(); @@ -1494,7 +1455,7 @@ describe('CdkDrag', () => { })); it('should not create placeholder if the element was not dragged far enough', fakeAsync(() => { - const fixture = createComponent(DraggableInDropZone, [], 5); + const fixture = createComponent(DraggableInDropZone, {dragDistance: 5}); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; @@ -2713,12 +2674,14 @@ describe('CdkDrag', () => { })); it('should auto-scroll right if the user holds their pointer at right edge in rtl', fakeAsync(() => { - const fixture = createComponent(DraggableInScrollableHorizontalDropZone, [ - { - provide: Directionality, - useValue: {value: 'rtl', change: observableOf()}, - }, - ]); + const fixture = createComponent(DraggableInScrollableHorizontalDropZone, { + providers: [ + { + provide: Directionality, + useValue: {value: 'rtl', change: observableOf()}, + }, + ], + }); fixture.nativeElement.setAttribute('dir', 'rtl'); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.first.element.nativeElement; @@ -2740,12 +2703,14 @@ describe('CdkDrag', () => { })); it('should auto-scroll left if the user holds their pointer at left edge in rtl', fakeAsync(() => { - const fixture = createComponent(DraggableInScrollableHorizontalDropZone, [ - { - provide: Directionality, - useValue: {value: 'rtl', change: observableOf()}, - }, - ]); + const fixture = createComponent(DraggableInScrollableHorizontalDropZone, { + providers: [ + { + provide: Directionality, + useValue: {value: 'rtl', change: observableOf()}, + }, + ], + }); fixture.nativeElement.setAttribute('dir', 'rtl'); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.first.element.nativeElement; @@ -3181,12 +3146,14 @@ describe('CdkDrag', () => { lockAxis: 'y', }; - const fixture = createComponent(PlainStandaloneDropList, [ - { - provide: CDK_DRAG_CONFIG, - useValue: config, - }, - ]); + const fixture = createComponent(PlainStandaloneDropList, { + providers: [ + { + provide: CDK_DRAG_CONFIG, + useValue: config, + }, + ], + }); fixture.detectChanges(); const list = fixture.componentInstance.dropList; expect(list.disabled).toBe(true); @@ -4287,9 +4254,9 @@ describe('CdkDrag', () => { ); it('should set the receiving class when the list is wrapped in an OnPush component', fakeAsync(() => { - const fixture = createComponent(ConnectedDropListsInOnPush, undefined, undefined, [ - DraggableInOnPushDropZone, - ]); + const fixture = createComponent(ConnectedDropListsInOnPush, { + extraDeclarations: [DraggableInOnPushDropZone], + }); fixture.detectChanges(); const dropZones = Array.from( @@ -4630,9 +4597,9 @@ describe('CdkDrag', () => { 'should toggle a class when dragging an item inside a wrapper component component ' + 'with OnPush change detection', fakeAsync(() => { - const fixture = createComponent(ConnectedWrappedDropZones, [], 0, [ - WrappedDropContainerComponent, - ]); + const fixture = createComponent(ConnectedWrappedDropZones, { + extraDeclarations: [WrappedDropContainerComponent], + }); fixture.detectChanges(); const [startZone, targetZone] = fixture.nativeElement.querySelectorAll('.cdk-drop-list'); @@ -4773,13 +4740,9 @@ describe('CdkDrag', () => { return; } - const fixture = createComponent( - ConnectedDropZones, - [], - undefined, - [], - ViewEncapsulation.ShadowDom, - ); + const fixture = createComponent(ConnectedDropZones, { + encapsulation: ViewEncapsulation.ShadowDom, + }); fixture.detectChanges(); const groups = fixture.componentInstance.groupedDragItems; @@ -4857,13 +4820,9 @@ describe('CdkDrag', () => { return; } - const fixture = createComponent( - ConnectedDropZones, - [], - undefined, - [], - ViewEncapsulation.ShadowDom, - ); + const fixture = createComponent(ConnectedDropZones, { + encapsulation: ViewEncapsulation.ShadowDom, + }); fixture.detectChanges(); const shadowRoot = fixture.nativeElement.shadowRoot; @@ -4959,13 +4918,9 @@ describe('CdkDrag', () => { return; } - const fixture = createComponent( - ConnectedDropZones, - [], - undefined, - [], - ViewEncapsulation.ShadowDom, - ); + const fixture = createComponent(ConnectedDropZones, { + encapsulation: ViewEncapsulation.ShadowDom, + }); fixture.detectChanges(); const item = fixture.componentInstance.groupedDragItems[0][1]; @@ -4980,7 +4935,7 @@ describe('CdkDrag', () => { describe('with nested drags', () => { it('should not move draggable container when dragging child (multitouch)', fakeAsync(() => { - const fixture = createComponent(NestedDragsComponent, [], 10); + const fixture = createComponent(NestedDragsComponent, {dragDistance: 10}); fixture.detectChanges(); // First finger drags container (less then threshold) diff --git a/src/cdk/drag-drop/directives/standalone-drag.spec.ts b/src/cdk/drag-drop/directives/standalone-drag.spec.ts index 68c612bf64fb..a6ce3aa16255 100644 --- a/src/cdk/drag-drop/directives/standalone-drag.spec.ts +++ b/src/cdk/drag-drop/directives/standalone-drag.spec.ts @@ -3,17 +3,13 @@ import { Component, ElementRef, ErrorHandler, - Provider, QueryList, - Type, ViewChild, ViewChildren, ViewEncapsulation, signal, } from '@angular/core'; -import {ComponentFixture, TestBed, fakeAsync, flush, tick} from '@angular/core/testing'; -import {CdkScrollableModule} from '@angular/cdk/scrolling'; -import {DragDropModule} from '../drag-drop-module'; +import {fakeAsync, flush, tick} from '@angular/core/testing'; import { dispatchEvent, createMouseEvent, @@ -28,6 +24,7 @@ import {CdkDrag} from './drag'; import {CDK_DRAG_CONFIG, DragAxis, DragDropConfig} from './config'; import {DragRef, Point} from '../drag-ref'; import { + createComponent, continueDraggingViaTouch, dragElementViaMouse, dragElementViaTouch, @@ -37,1446 +34,1446 @@ import { } from './test-utils.spec'; describe('Standalone CdkDrag', () => { - function createComponent( - componentType: Type, - providers: Provider[] = [], - dragDistance = 0, - extraDeclarations: Type[] = [], - ): ComponentFixture { - TestBed.configureTestingModule({ - imports: [DragDropModule, CdkScrollableModule], - providers: [ - { - provide: CDK_DRAG_CONFIG, - useValue: { - // We default the `dragDistance` to zero, because the majority of the tests - // don't care about it and drags are a lot easier to simulate when we don't - // have to deal with thresholds. - dragStartThreshold: dragDistance, - pointerDirectionChangeThreshold: 5, - } as DragDropConfig, - }, - ...providers, - ], - declarations: [componentType, ...extraDeclarations], - }); - - TestBed.compileComponents(); - return TestBed.createComponent(componentType); - } - - describe('standalone draggable', () => { - describe('mouse dragging', () => { - it('should drag an element freely to a particular position', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - })); - - it('should drag an element freely to a particular position when the page is scrolled', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - const cleanup = makeScrollable(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - scrollTo(0, 500); - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - cleanup(); - })); - - it('should continue dragging the element from where it was left off', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - - dragElementViaMouse(fixture, dragElement, 100, 200); - expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); - })); - - it('should continue dragging from where it was left off when the page is scrolled', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const cleanup = makeScrollable(); - - scrollTo(0, 500); - expect(dragElement.style.transform).toBeFalsy(); - - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - - dragElementViaMouse(fixture, dragElement, 100, 200); - expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); - - cleanup(); - })); - - it('should not drag an element with the right mouse button', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const event = createMouseEvent('mousedown', 50, 100, 2); - - expect(dragElement.style.transform).toBeFalsy(); - - dispatchEvent(dragElement, event); - fixture.detectChanges(); - - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - - expect(dragElement.style.transform).toBeFalsy(); - })); - - it('should not drag the element if it was not moved more than the minimum distance', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable, [], 5); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 2, 2); - expect(dragElement.style.transform).toBeFalsy(); - })); - - it('should be able to stop dragging after a double click', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable, [], 5); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - - dispatchMouseEvent(dragElement, 'mousedown'); - fixture.detectChanges(); - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - dispatchMouseEvent(dragElement, 'mousedown'); - fixture.detectChanges(); - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - - dragElementViaMouse(fixture, dragElement, 50, 50); - dispatchMouseEvent(document, 'mousemove', 100, 100); - fixture.detectChanges(); - - expect(dragElement.style.transform).toBeFalsy(); - })); - - it('should preserve the previous `transform` value', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - dragElement.style.transform = 'translateX(-50%)'; - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px) translateX(-50%)'); - })); - - it('should not generate multiple own `translate3d` values', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - dragElement.style.transform = 'translateY(-50%)'; - - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px) translateY(-50%)'); - - dragElementViaMouse(fixture, dragElement, 100, 200); - expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px) translateY(-50%)'); - })); - - it('should prevent the `mousedown` action for native draggable elements', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - dragElement.draggable = true; - - const mousedownEvent = createMouseEvent('mousedown', 50, 50); - Object.defineProperty(mousedownEvent, 'target', {get: () => dragElement}); - spyOn(mousedownEvent, 'preventDefault').and.callThrough(); - dispatchEvent(dragElement, mousedownEvent); - fixture.detectChanges(); - - dispatchMouseEvent(document, 'mousemove', 50, 50); - fixture.detectChanges(); - - expect(mousedownEvent.preventDefault).toHaveBeenCalled(); - })); - - it('should not start dragging an element with a fake mousedown event', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const event = createMouseEvent('mousedown', 0, 0); - - Object.defineProperties(event, { - buttons: {get: () => 0}, - detail: {get: () => 0}, - }); - - expect(dragElement.style.transform).toBeFalsy(); - - dispatchEvent(dragElement, event); - fixture.detectChanges(); - - dispatchMouseEvent(document, 'mousemove', 20, 100); - fixture.detectChanges(); - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - - expect(dragElement.style.transform).toBeFalsy(); - })); - - it('should prevent the default dragstart action', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const event = dispatchFakeEvent( - fixture.componentInstance.dragElement.nativeElement, - 'dragstart', - ); - fixture.detectChanges(); - - expect(event.defaultPrevented).toBe(true); - })); - - it('should not prevent the default dragstart action when dragging is disabled', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragDisabled.set(true); - fixture.detectChanges(); - const event = dispatchFakeEvent( - fixture.componentInstance.dragElement.nativeElement, - 'dragstart', - ); - fixture.detectChanges(); - - expect(event.defaultPrevented).toBe(false); - })); - }); - - describe('touch dragging', () => { - it('should drag an element freely to a particular position', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaTouch(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - })); - - it('should drag an element freely to a particular position when the page is scrolled', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const cleanup = makeScrollable(); - - scrollTo(0, 500); - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaTouch(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - cleanup(); - })); - - it('should continue dragging the element from where it was left off', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + describe('mouse dragging', () => { + it('should drag an element freely to a particular position', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - expect(dragElement.style.transform).toBeFalsy(); + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + })); - dragElementViaTouch(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + it('should drag an element freely to a particular position when the page is scrolled', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); - dragElementViaTouch(fixture, dragElement, 100, 200); - expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); - })); + const cleanup = makeScrollable(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - it('should continue dragging from where it was left off when the page is scrolled', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); + scrollTo(0, 500); + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + cleanup(); + })); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const cleanup = makeScrollable(); + it('should continue dragging the element from where it was left off', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - scrollTo(0, 500); - expect(dragElement.style.transform).toBeFalsy(); + expect(dragElement.style.transform).toBeFalsy(); - dragElementViaTouch(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - dragElementViaTouch(fixture, dragElement, 100, 200); - expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); + dragElementViaMouse(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); + })); - cleanup(); - })); + it('should continue dragging from where it was left off when the page is scrolled', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); - it('should prevent the default `touchmove` action on the page while dragging', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const cleanup = makeScrollable(); - dispatchTouchEvent(fixture.componentInstance.dragElement.nativeElement, 'touchstart'); - fixture.detectChanges(); + scrollTo(0, 500); + expect(dragElement.style.transform).toBeFalsy(); - expect(dispatchTouchEvent(document, 'touchmove').defaultPrevented) - .withContext('Expected initial touchmove to be prevented.') - .toBe(true); - expect(dispatchTouchEvent(document, 'touchmove').defaultPrevented) - .withContext('Expected subsequent touchmose to be prevented.') - .toBe(true); + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - dispatchTouchEvent(document, 'touchend'); - fixture.detectChanges(); - })); + dragElementViaMouse(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); - it('should not prevent `touchstart` action for native draggable elements', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + cleanup(); + })); - dragElement.draggable = true; + it('should not drag an element with the right mouse button', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const event = createMouseEvent('mousedown', 50, 100, 2); - const touchstartEvent = createTouchEvent('touchstart', 50, 50); - Object.defineProperty(touchstartEvent, 'target', {get: () => dragElement}); - spyOn(touchstartEvent, 'preventDefault').and.callThrough(); - dispatchEvent(dragElement, touchstartEvent); - fixture.detectChanges(); + expect(dragElement.style.transform).toBeFalsy(); - dispatchTouchEvent(document, 'touchmove'); - fixture.detectChanges(); + dispatchEvent(dragElement, event); + fixture.detectChanges(); - expect(touchstartEvent.preventDefault).not.toHaveBeenCalled(); - })); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); - it('should not start dragging an element with a fake touchstart event', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const event = createTouchEvent('touchstart', 50, 50) as TouchEvent; + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); - Object.defineProperties(event.touches[0], { - identifier: {get: () => -1}, - radiusX: {get: () => null}, - radiusY: {get: () => null}, - }); + expect(dragElement.style.transform).toBeFalsy(); + })); - expect(dragElement.style.transform).toBeFalsy(); + it('should not drag the element if it was not moved more than the minimum distance', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable, {dragDistance: 5}); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - dispatchEvent(dragElement, event); - fixture.detectChanges(); + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 2, 2); + expect(dragElement.style.transform).toBeFalsy(); + })); - dispatchTouchEvent(document, 'touchmove', 20, 100); - fixture.detectChanges(); - dispatchTouchEvent(document, 'touchmove', 50, 100); - fixture.detectChanges(); + it('should be able to stop dragging after a double click', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable, {dragDistance: 5}); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - dispatchTouchEvent(document, 'touchend'); - fixture.detectChanges(); + expect(dragElement.style.transform).toBeFalsy(); - expect(dragElement.style.transform).toBeFalsy(); - })); - }); + dispatchMouseEvent(dragElement, 'mousedown'); + fixture.detectChanges(); + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + dispatchMouseEvent(dragElement, 'mousedown'); + fixture.detectChanges(); + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); - describe('mouse dragging when initial transform is none', () => { - it('should drag an element freely to a particular position', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - dragElement.style.transform = 'none'; + dragElementViaMouse(fixture, dragElement, 50, 50); + dispatchMouseEvent(document, 'mousemove', 100, 100); + fixture.detectChanges(); - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - })); - }); + expect(dragElement.style.transform).toBeFalsy(); + })); - it('should dispatch an event when the user has started dragging', fakeAsync(() => { + it('should preserve the previous `transform` value', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - startDraggingViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement); - - expect(fixture.componentInstance.startedSpy).toHaveBeenCalled(); - - const event = fixture.componentInstance.startedSpy.calls.mostRecent().args[0]; - - // Assert the event like this, rather than `toHaveBeenCalledWith`, because Jasmine will - // go into an infinite loop trying to stringify the event, if the test fails. - expect(event).toEqual({ - source: fixture.componentInstance.dragInstance, - event: jasmine.anything(), - }); + dragElement.style.transform = 'translateX(-50%)'; + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px) translateX(-50%)'); })); - it('should dispatch an event when the user has stopped dragging', fakeAsync(() => { + it('should not generate multiple own `translate3d` values', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 5, 10); - - expect(fixture.componentInstance.endedSpy).toHaveBeenCalled(); + dragElement.style.transform = 'translateY(-50%)'; - const event = fixture.componentInstance.endedSpy.calls.mostRecent().args[0]; + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px) translateY(-50%)'); - // Assert the event like this, rather than `toHaveBeenCalledWith`, because Jasmine will - // go into an infinite loop trying to stringify the event, if the test fails. - expect(event).toEqual({ - source: fixture.componentInstance.dragInstance, - distance: {x: jasmine.any(Number), y: jasmine.any(Number)}, - dropPoint: {x: jasmine.any(Number), y: jasmine.any(Number)}, - event: jasmine.anything(), - }); + dragElementViaMouse(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px) translateY(-50%)'); })); - it('should include the drag distance in the ended event', fakeAsync(() => { + it('should prevent the `mousedown` action for native draggable elements', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 25, 30); - let event = fixture.componentInstance.endedSpy.calls.mostRecent().args[0]; + dragElement.draggable = true; - expect(event).toEqual({ - source: jasmine.anything(), - distance: {x: 25, y: 30}, - dropPoint: {x: 25, y: 30}, - event: jasmine.anything(), - }); + const mousedownEvent = createMouseEvent('mousedown', 50, 50); + Object.defineProperty(mousedownEvent, 'target', {get: () => dragElement}); + spyOn(mousedownEvent, 'preventDefault').and.callThrough(); + dispatchEvent(dragElement, mousedownEvent); + fixture.detectChanges(); - dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 40, 50); - event = fixture.componentInstance.endedSpy.calls.mostRecent().args[0]; + dispatchMouseEvent(document, 'mousemove', 50, 50); + fixture.detectChanges(); - expect(event).toEqual({ - source: jasmine.anything(), - distance: {x: 40, y: 50}, - dropPoint: {x: 40, y: 50}, - event: jasmine.anything(), - }); + expect(mousedownEvent.preventDefault).toHaveBeenCalled(); })); - it('should emit when the user is moving the drag element', () => { + it('should not start dragging an element with a fake mousedown event', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const event = createMouseEvent('mousedown', 0, 0); - const spy = jasmine.createSpy('move spy'); - const subscription = fixture.componentInstance.dragInstance.moved.subscribe(spy); + Object.defineProperties(event, { + buttons: {get: () => 0}, + detail: {get: () => 0}, + }); - dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 5, 10); - expect(spy).toHaveBeenCalledTimes(1); + expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 10, 20); - expect(spy).toHaveBeenCalledTimes(2); + dispatchEvent(dragElement, event); + fixture.detectChanges(); - subscription.unsubscribe(); - }); + dispatchMouseEvent(document, 'mousemove', 20, 100); + fixture.detectChanges(); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); - it('should not emit events if it was not moved more than the minimum distance', () => { - const fixture = createComponent(StandaloneDraggable, [], 5); + dispatchMouseEvent(document, 'mouseup'); fixture.detectChanges(); - const moveSpy = jasmine.createSpy('move spy'); - const subscription = fixture.componentInstance.dragInstance.moved.subscribe(moveSpy); + expect(dragElement.style.transform).toBeFalsy(); + })); - dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 2, 2); + it('should prevent the default dragstart action', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const event = dispatchFakeEvent( + fixture.componentInstance.dragElement.nativeElement, + 'dragstart', + ); + fixture.detectChanges(); - expect(fixture.componentInstance.startedSpy).not.toHaveBeenCalled(); - expect(fixture.componentInstance.releasedSpy).not.toHaveBeenCalled(); - expect(fixture.componentInstance.endedSpy).not.toHaveBeenCalled(); - expect(moveSpy).not.toHaveBeenCalled(); - subscription.unsubscribe(); - }); + expect(event.defaultPrevented).toBe(true); + })); - it('should complete the `moved` stream on destroy', () => { + it('should not prevent the default dragstart action when dragging is disabled', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragDisabled.set(true); + fixture.detectChanges(); + const event = dispatchFakeEvent( + fixture.componentInstance.dragElement.nativeElement, + 'dragstart', + ); fixture.detectChanges(); - const spy = jasmine.createSpy('move spy'); - const subscription = fixture.componentInstance.dragInstance.moved.subscribe({complete: spy}); - - fixture.destroy(); - expect(spy).toHaveBeenCalled(); - subscription.unsubscribe(); - }); + expect(event.defaultPrevented).toBe(false); + })); + }); - it('should be able to lock dragging along the x axis', fakeAsync(() => { + describe('touch dragging', () => { + it('should drag an element freely to a particular position', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragLockAxis.set('x'); fixture.detectChanges(); - 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)'); + dragElementViaTouch(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); })); - it('should be able to lock dragging along the x axis while using constrainPosition', fakeAsync(() => { + it('should drag an element freely to a particular position when the page is scrolled', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragLockAxis.set('x'); - fixture.componentInstance.constrainPosition = ( - {x, y}: Point, - _dragRef: DragRef, - _dimensions: DOMRect, - pickup: Point, - ) => { - x -= pickup.x; - y -= pickup.y; - return {x, y}; - }; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; + const cleanup = makeScrollable(); + scrollTo(0, 500); 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)'); + dragElementViaTouch(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + cleanup(); })); - it('should be able to lock dragging along the y axis', fakeAsync(() => { + it('should continue dragging the element from where it was left off', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragLockAxis.set('y'); fixture.detectChanges(); - 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)'); + dragElementViaTouch(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - dragElementViaMouse(fixture, dragElement, 100, 200); - expect(dragElement.style.transform).toBe('translate3d(0px, 300px, 0px)'); + dragElementViaTouch(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); })); - it('should be able to lock dragging along the y axis while using constrainPosition', fakeAsync(() => { + it('should continue dragging from where it was left off when the page is scrolled', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragLockAxis.set('y'); - fixture.componentInstance.constrainPosition = ( - {x, y}: Point, - _dragRef: DragRef, - _dimensions: DOMRect, - pickup: Point, - ) => { - x -= pickup.x; - y -= pickup.y; - return {x, y}; - }; - fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; + const cleanup = makeScrollable(); + scrollTo(0, 500); expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(0px, 100px, 0px)'); + dragElementViaTouch(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - dragElementViaMouse(fixture, dragElement, 100, 200); - expect(dragElement.style.transform).toBe('translate3d(0px, 300px, 0px)'); + dragElementViaTouch(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); + + cleanup(); })); - it('should add a class while an element is being dragged', fakeAsync(() => { + it('should prevent the default `touchmove` action on the page while dragging', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); fixture.detectChanges(); - const element = fixture.componentInstance.dragElement.nativeElement; - - expect(element.classList).not.toContain('cdk-drag-dragging'); - - startDraggingViaMouse(fixture, element); + dispatchTouchEvent(fixture.componentInstance.dragElement.nativeElement, 'touchstart'); + fixture.detectChanges(); - expect(element.classList).toContain('cdk-drag-dragging'); + expect(dispatchTouchEvent(document, 'touchmove').defaultPrevented) + .withContext('Expected initial touchmove to be prevented.') + .toBe(true); + expect(dispatchTouchEvent(document, 'touchmove').defaultPrevented) + .withContext('Expected subsequent touchmose to be prevented.') + .toBe(true); - dispatchMouseEvent(document, 'mouseup'); + dispatchTouchEvent(document, 'touchend'); fixture.detectChanges(); - - expect(element.classList).not.toContain('cdk-drag-dragging'); })); - it('should add a class while an element is being dragged with OnPush change detection', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggableWithOnPush); + it('should not prevent `touchstart` action for native draggable elements', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - const element = fixture.componentInstance.dragElement.nativeElement; - - expect(element.classList).not.toContain('cdk-drag-dragging'); - - startDraggingViaMouse(fixture, element); + dragElement.draggable = true; - expect(element.classList).toContain('cdk-drag-dragging'); + const touchstartEvent = createTouchEvent('touchstart', 50, 50); + Object.defineProperty(touchstartEvent, 'target', {get: () => dragElement}); + spyOn(touchstartEvent, 'preventDefault').and.callThrough(); + dispatchEvent(dragElement, touchstartEvent); + fixture.detectChanges(); - dispatchMouseEvent(document, 'mouseup'); + dispatchTouchEvent(document, 'touchmove'); fixture.detectChanges(); - expect(element.classList).not.toContain('cdk-drag-dragging'); + expect(touchstartEvent.preventDefault).not.toHaveBeenCalled(); })); - it('should not add a class if item was not dragged more than the threshold', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable, [], 5); + it('should not start dragging an element with a fake touchstart event', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const event = createTouchEvent('touchstart', 50, 50) as TouchEvent; - const element = fixture.componentInstance.dragElement.nativeElement; - - expect(element.classList).not.toContain('cdk-drag-dragging'); - - startDraggingViaMouse(fixture, element); + Object.defineProperties(event.touches[0], { + identifier: {get: () => -1}, + radiusX: {get: () => null}, + radiusY: {get: () => null}, + }); - expect(element.classList).not.toContain('cdk-drag-dragging'); - })); + expect(dragElement.style.transform).toBeFalsy(); - it('should be able to set an alternate drag root element', fakeAsync(() => { - const fixture = createComponent(DraggableWithAlternateRoot); - fixture.componentInstance.rootElementSelector = '.alternate-root'; - fixture.changeDetectorRef.markForCheck(); + dispatchEvent(dragElement, event); fixture.detectChanges(); - const dragRoot = fixture.componentInstance.dragRoot.nativeElement; - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragRoot.style.transform).toBeFalsy(); - expect(dragElement.style.transform).toBeFalsy(); + dispatchTouchEvent(document, 'touchmove', 20, 100); + fixture.detectChanges(); + dispatchTouchEvent(document, 'touchmove', 50, 100); + fixture.detectChanges(); - dragElementViaMouse(fixture, dragRoot, 50, 100); + dispatchTouchEvent(document, 'touchend'); + fixture.detectChanges(); - expect(dragRoot.style.transform).toBe('translate3d(50px, 100px, 0px)'); expect(dragElement.style.transform).toBeFalsy(); })); + }); - it('should be able to set the cdkDrag element as handle if it has a different root element', fakeAsync(() => { - const fixture = createComponent(DraggableWithAlternateRootAndSelfHandle); + describe('mouse dragging when initial transform is none', () => { + it('should drag an element freely to a particular position', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); fixture.detectChanges(); - - const dragRoot = fixture.componentInstance.dragRoot.nativeElement; const dragElement = fixture.componentInstance.dragElement.nativeElement; + dragElement.style.transform = 'none'; - expect(dragRoot.style.transform).toBeFalsy(); - expect(dragElement.style.transform).toBeFalsy(); - - // Try dragging the root. This should be possible since the drag element is the handle. - dragElementViaMouse(fixture, dragRoot, 50, 100); - - expect(dragRoot.style.transform).toBeFalsy(); - expect(dragElement.style.transform).toBeFalsy(); - - // Drag via the drag element which acts as the handle. dragElementViaMouse(fixture, dragElement, 50, 100); - - expect(dragRoot.style.transform).toBe('translate3d(50px, 100px, 0px)'); - expect(dragElement.style.transform).toBeFalsy(); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); })); + }); - it('should be able to set an alternate drag root element for ng-container', fakeAsync(() => { - const fixture = createComponent(DraggableNgContainerWithAlternateRoot); - fixture.detectChanges(); + it('should dispatch an event when the user has started dragging', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); - const dragRoot = fixture.componentInstance.dragRoot.nativeElement; + startDraggingViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement); - expect(dragRoot.style.transform).toBeFalsy(); + expect(fixture.componentInstance.startedSpy).toHaveBeenCalled(); - dragElementViaMouse(fixture, dragRoot, 50, 100); + const event = fixture.componentInstance.startedSpy.calls.mostRecent().args[0]; - expect(dragRoot.style.transform).toBe('translate3d(50px, 100px, 0px)'); - })); + // Assert the event like this, rather than `toHaveBeenCalledWith`, because Jasmine will + // go into an infinite loop trying to stringify the event, if the test fails. + expect(event).toEqual({ + source: fixture.componentInstance.dragInstance, + event: jasmine.anything(), + }); + })); - it('should preserve the initial transform if the root element changes', fakeAsync(() => { - const fixture = createComponent(DraggableWithAlternateRoot); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const alternateRoot = fixture.componentInstance.dragRoot.nativeElement; + it('should dispatch an event when the user has stopped dragging', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); - dragElement.style.transform = 'translateX(-50%)'; - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toContain('translateX(-50%)'); + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 5, 10); - alternateRoot.style.transform = 'scale(2)'; - fixture.componentInstance.rootElementSelector = '.alternate-root'; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + expect(fixture.componentInstance.endedSpy).toHaveBeenCalled(); - dragElementViaMouse(fixture, alternateRoot, 50, 100); + const event = fixture.componentInstance.endedSpy.calls.mostRecent().args[0]; - expect(alternateRoot.style.transform).not.toContain('translateX(-50%)'); - expect(alternateRoot.style.transform).toContain('scale(2)'); - })); + // Assert the event like this, rather than `toHaveBeenCalledWith`, because Jasmine will + // go into an infinite loop trying to stringify the event, if the test fails. + expect(event).toEqual({ + source: fixture.componentInstance.dragInstance, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)}, + dropPoint: {x: jasmine.any(Number), y: jasmine.any(Number)}, + event: jasmine.anything(), + }); + })); - it('should handle the root element selector changing after init', fakeAsync(() => { - const fixture = createComponent(DraggableWithAlternateRoot); - fixture.detectChanges(); - tick(); + it('should include the drag distance in the ended event', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); - fixture.componentInstance.rootElementSelector = '.alternate-root'; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 25, 30); + let event = fixture.componentInstance.endedSpy.calls.mostRecent().args[0]; - const dragRoot = fixture.componentInstance.dragRoot.nativeElement; - const dragElement = fixture.componentInstance.dragElement.nativeElement; + expect(event).toEqual({ + source: jasmine.anything(), + distance: {x: 25, y: 30}, + dropPoint: {x: 25, y: 30}, + event: jasmine.anything(), + }); - expect(dragRoot.style.transform).toBeFalsy(); - expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 40, 50); + event = fixture.componentInstance.endedSpy.calls.mostRecent().args[0]; - dragElementViaMouse(fixture, dragRoot, 50, 100); + expect(event).toEqual({ + source: jasmine.anything(), + distance: {x: 40, y: 50}, + dropPoint: {x: 40, y: 50}, + event: jasmine.anything(), + }); + })); - expect(dragRoot.style.transform).toBe('translate3d(50px, 100px, 0px)'); - expect(dragElement.style.transform).toBeFalsy(); - })); + it('should emit when the user is moving the drag element', () => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); - it('should not be able to drag the element if dragging is disabled', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + const spy = jasmine.createSpy('move spy'); + const subscription = fixture.componentInstance.dragInstance.moved.subscribe(spy); - expect(dragElement.classList).not.toContain('cdk-drag-disabled'); + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 5, 10); + expect(spy).toHaveBeenCalledTimes(1); - fixture.componentInstance.dragDisabled.set(true); - fixture.detectChanges(); + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 10, 20); + expect(spy).toHaveBeenCalledTimes(2); - expect(dragElement.classList).toContain('cdk-drag-disabled'); - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBeFalsy(); - })); + subscription.unsubscribe(); + }); - it('should enable native drag interactions if dragging is disabled', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const styles = dragElement.style; + it('should not emit events if it was not moved more than the minimum distance', () => { + const fixture = createComponent(StandaloneDraggable, {dragDistance: 5}); + fixture.detectChanges(); - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + const moveSpy = jasmine.createSpy('move spy'); + const subscription = fixture.componentInstance.dragInstance.moved.subscribe(moveSpy); - fixture.componentInstance.dragDisabled.set(true); - fixture.detectChanges(); + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 2, 2); - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - })); + expect(fixture.componentInstance.startedSpy).not.toHaveBeenCalled(); + expect(fixture.componentInstance.releasedSpy).not.toHaveBeenCalled(); + expect(fixture.componentInstance.endedSpy).not.toHaveBeenCalled(); + expect(moveSpy).not.toHaveBeenCalled(); + subscription.unsubscribe(); + }); - it('should enable native drag interactions if not dragging', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const styles = dragElement.style; + it('should complete the `moved` stream on destroy', () => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - })); + const spy = jasmine.createSpy('move spy'); + const subscription = fixture.componentInstance.dragInstance.moved.subscribe({complete: spy}); - it('should disable native drag interactions if dragging', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const styles = dragElement.style; + fixture.destroy(); + expect(spy).toHaveBeenCalled(); + subscription.unsubscribe(); + }); - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + it('should be able to lock dragging along the x axis', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragLockAxis.set('x'); + fixture.detectChanges(); - startDraggingViaMouse(fixture, dragElement); - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); - })); + expect(dragElement.style.transform).toBeFalsy(); - it('should re-enable drag interactions once dragging is over', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const styles = dragElement.style; + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 0px, 0px)'); - startDraggingViaMouse(fixture, dragElement); - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); + dragElementViaMouse(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(150px, 0px, 0px)'); + })); - expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); + it('should be able to lock dragging along the x axis while using constrainPosition', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragLockAxis.set('x'); + fixture.componentInstance.constrainPosition = ( + {x, y}: Point, + _dragRef: DragRef, + _dimensions: DOMRect, + pickup: Point, + ) => { + x -= pickup.x; + y -= pickup.y; + return {x, y}; + }; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); - dispatchMouseEvent(document, 'mouseup', 50, 100); - fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - })); + expect(dragElement.style.transform).toBeFalsy(); - it('should not stop propagation for the drag sequence start event by default', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 0px, 0px)'); - const event = createMouseEvent('mousedown'); - spyOn(event, 'stopPropagation').and.callThrough(); + dragElementViaMouse(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(150px, 0px, 0px)'); + })); - dispatchEvent(dragElement, event); - fixture.detectChanges(); + it('should be able to lock dragging along the y axis', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragLockAxis.set('y'); + fixture.detectChanges(); - expect(event.stopPropagation).not.toHaveBeenCalled(); - })); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - it('should not throw if destroyed before the first change detection run', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); + expect(dragElement.style.transform).toBeFalsy(); - expect(() => { - fixture.destroy(); - }).not.toThrow(); - })); + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(0px, 100px, 0px)'); - it('should enable native drag interactions on the drag item when there is a handle', () => { - const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - expect(dragElement.style.touchAction).not.toBe('none'); - }); + dragElementViaMouse(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(0px, 300px, 0px)'); + })); - it('should disable native drag interactions on the drag handle', () => { - const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.detectChanges(); - const styles = fixture.componentInstance.handleElement.nativeElement.style; - expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); - }); + it('should be able to lock dragging along the y axis while using constrainPosition', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragLockAxis.set('y'); + fixture.componentInstance.constrainPosition = ( + {x, y}: Point, + _dragRef: DragRef, + _dimensions: DOMRect, + pickup: Point, + ) => { + x -= pickup.x; + y -= pickup.y; + return {x, y}; + }; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); - it('should enable native drag interactions on the drag handle if dragging is disabled', () => { - const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.detectChanges(); - fixture.componentInstance.draggingDisabled = true; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - const styles = fixture.componentInstance.handleElement.nativeElement.style; - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - }); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - it( - 'should enable native drag interactions on the drag handle if dragging is disabled ' + - 'on init', - () => { - const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.componentInstance.draggingDisabled = true; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - const styles = fixture.componentInstance.handleElement.nativeElement.style; - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - }, - ); + expect(dragElement.style.transform).toBeFalsy(); - it('should toggle native drag interactions based on whether the handle is disabled', () => { - const fixture = createComponent(StandaloneDraggableWithHandle); - fixture.detectChanges(); - fixture.componentInstance.handleInstance.disabled = true; - fixture.detectChanges(); - const styles = fixture.componentInstance.handleElement.nativeElement.style; - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(0px, 100px, 0px)'); - fixture.componentInstance.handleInstance.disabled = false; - fixture.detectChanges(); - expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); - }); + dragElementViaMouse(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(0px, 300px, 0px)'); + })); - it('should be able to reset a freely-dragged item to its initial position', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + it('should add a class while an element is being dragged', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + const element = fixture.componentInstance.dragElement.nativeElement; - fixture.componentInstance.dragInstance.reset(); - expect(dragElement.style.transform).toBeFalsy(); - })); + expect(element.classList).not.toContain('cdk-drag-dragging'); - it('should preserve initial transform after resetting', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + startDraggingViaMouse(fixture, element); - dragElement.style.transform = 'scale(2)'; + expect(element.classList).toContain('cdk-drag-dragging'); - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px) scale(2)'); + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); - fixture.componentInstance.dragInstance.reset(); - expect(dragElement.style.transform).toBe('scale(2)'); - })); + expect(element.classList).not.toContain('cdk-drag-dragging'); + })); - it('should start dragging an item from its initial position after a reset', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + it('should add a class while an element is being dragged with OnPush change detection', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggableWithOnPush); + fixture.detectChanges(); - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - fixture.componentInstance.dragInstance.reset(); + const element = fixture.componentInstance.dragElement.nativeElement; - dragElementViaMouse(fixture, dragElement, 25, 50); - expect(dragElement.style.transform).toBe('translate3d(25px, 50px, 0px)'); - })); + expect(element.classList).not.toContain('cdk-drag-dragging'); - it('should not dispatch multiple events for a mouse event right after a touch event', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); + startDraggingViaMouse(fixture, element); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + expect(element.classList).toContain('cdk-drag-dragging'); - // Dispatch a touch sequence. - dispatchTouchEvent(dragElement, 'touchstart'); - fixture.detectChanges(); - dispatchTouchEvent(dragElement, 'touchend'); - fixture.detectChanges(); - tick(); + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); - // Immediately dispatch a mouse sequence to simulate a fake event. - startDraggingViaMouse(fixture, dragElement); - fixture.detectChanges(); - dispatchMouseEvent(dragElement, 'mouseup'); - fixture.detectChanges(); - tick(); + expect(element.classList).not.toContain('cdk-drag-dragging'); + })); - expect(fixture.componentInstance.startedSpy).toHaveBeenCalledTimes(1); - expect(fixture.componentInstance.endedSpy).toHaveBeenCalledTimes(1); - })); + it('should not add a class if item was not dragged more than the threshold', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable, {dragDistance: 5}); + fixture.detectChanges(); - it('should round the transform value', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + const element = fixture.componentInstance.dragElement.nativeElement; + + expect(element.classList).not.toContain('cdk-drag-dragging'); - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 13.37, 37); - expect(dragElement.style.transform).toBe('translate3d(13px, 37px, 0px)'); - })); + startDraggingViaMouse(fixture, element); - it('should allow for dragging to be constrained to an element', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.boundary = '.wrapper'; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + expect(element.classList).not.toContain('cdk-drag-dragging'); + })); - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); - })); + it('should be able to set an alternate drag root element', fakeAsync(() => { + const fixture = createComponent(DraggableWithAlternateRoot); + fixture.componentInstance.rootElementSelector = '.alternate-root'; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); - it('should allow for dragging to be constrained to an element while using constrainPosition', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.boundary = '.wrapper'; - fixture.detectChanges(); + const dragRoot = fixture.componentInstance.dragRoot.nativeElement; + const dragElement = fixture.componentInstance.dragElement.nativeElement; - fixture.componentInstance.dragInstance.constrainPosition = ( - {x, y}: Point, - _dragRef: DragRef, - _dimensions: DOMRect, - pickup: Point, - ) => { - x -= pickup.x; - y -= pickup.y; - return {x, y}; - }; + expect(dragRoot.style.transform).toBeFalsy(); + expect(dragElement.style.transform).toBeFalsy(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + dragElementViaMouse(fixture, dragRoot, 50, 100); - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); - })); + expect(dragRoot.style.transform).toBe('translate3d(50px, 100px, 0px)'); + expect(dragElement.style.transform).toBeFalsy(); + })); - it('should be able to pass in a DOM node as the boundary', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.boundary = fixture.nativeElement.querySelector('.wrapper'); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + it('should be able to set the cdkDrag element as handle if it has a different root element', fakeAsync(() => { + const fixture = createComponent(DraggableWithAlternateRootAndSelfHandle); + fixture.detectChanges(); - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); - })); + const dragRoot = fixture.componentInstance.dragRoot.nativeElement; + const dragElement = fixture.componentInstance.dragElement.nativeElement; - it('should adjust the x offset if the boundary becomes narrower after a viewport resize', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); - fixture.componentInstance.boundary = boundary; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + expect(dragRoot.style.transform).toBeFalsy(); + expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + // Try dragging the root. This should be possible since the drag element is the handle. + dragElementViaMouse(fixture, dragRoot, 50, 100); - boundary.style.width = '150px'; - dispatchFakeEvent(window, 'resize'); - tick(20); + expect(dragRoot.style.transform).toBeFalsy(); + expect(dragElement.style.transform).toBeFalsy(); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - })); + // Drag via the drag element which acts as the handle. + dragElementViaMouse(fixture, dragElement, 50, 100); - it('should keep the old position if the boundary is invisible after a resize', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); - fixture.componentInstance.boundary = boundary; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + expect(dragRoot.style.transform).toBe('translate3d(50px, 100px, 0px)'); + expect(dragElement.style.transform).toBeFalsy(); + })); - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + it('should be able to set an alternate drag root element for ng-container', fakeAsync(() => { + const fixture = createComponent(DraggableNgContainerWithAlternateRoot); + fixture.detectChanges(); - boundary.style.display = 'none'; - dispatchFakeEvent(window, 'resize'); - tick(20); + const dragRoot = fixture.componentInstance.dragRoot.nativeElement; - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); - })); + expect(dragRoot.style.transform).toBeFalsy(); - it('should handle the element and boundary dimensions changing between drag sequences', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); - fixture.componentInstance.boundary = boundary; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + dragElementViaMouse(fixture, dragRoot, 50, 100); - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + expect(dragRoot.style.transform).toBe('translate3d(50px, 100px, 0px)'); + })); - // Bump the width and height of both the boundary and the drag element. - boundary.style.width = boundary.style.height = '300px'; - dragElement.style.width = dragElement.style.height = '150px'; + it('should preserve the initial transform if the root element changes', fakeAsync(() => { + const fixture = createComponent(DraggableWithAlternateRoot); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const alternateRoot = fixture.componentInstance.dragRoot.nativeElement; - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(150px, 150px, 0px)'); - })); + dragElement.style.transform = 'translateX(-50%)'; + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toContain('translateX(-50%)'); - it('should adjust the y offset if the boundary becomes shorter after a viewport resize', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); - fixture.componentInstance.boundary = boundary; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + alternateRoot.style.transform = 'scale(2)'; + fixture.componentInstance.rootElementSelector = '.alternate-root'; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + dragElementViaMouse(fixture, alternateRoot, 50, 100); - boundary.style.height = '150px'; - dispatchFakeEvent(window, 'resize'); - tick(20); + expect(alternateRoot.style.transform).not.toContain('translateX(-50%)'); + expect(alternateRoot.style.transform).toContain('scale(2)'); + })); - expect(dragElement.style.transform).toBe('translate3d(100px, 50px, 0px)'); - })); + it('should handle the root element selector changing after init', fakeAsync(() => { + const fixture = createComponent(DraggableWithAlternateRoot); + fixture.detectChanges(); + tick(); - it( - 'should reset the x offset if the boundary becomes narrower than the element ' + - 'after a resize', - fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); - fixture.componentInstance.boundary = boundary; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); - - boundary.style.width = '50px'; - dispatchFakeEvent(window, 'resize'); - tick(20); - - expect(dragElement.style.transform).toBe('translate3d(0px, 100px, 0px)'); - }), - ); + fixture.componentInstance.rootElementSelector = '.alternate-root'; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); - it('should reset the y offset if the boundary becomes shorter than the element after a resize', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); - fixture.componentInstance.boundary = boundary; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + const dragRoot = fixture.componentInstance.dragRoot.nativeElement; + const dragElement = fixture.componentInstance.dragElement.nativeElement; - dragElementViaMouse(fixture, dragElement, 300, 300); - expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + expect(dragRoot.style.transform).toBeFalsy(); + expect(dragElement.style.transform).toBeFalsy(); - boundary.style.height = '50px'; - dispatchFakeEvent(window, 'resize'); - tick(20); + dragElementViaMouse(fixture, dragRoot, 50, 100); - expect(dragElement.style.transform).toBe('translate3d(100px, 0px, 0px)'); - })); + expect(dragRoot.style.transform).toBe('translate3d(50px, 100px, 0px)'); + expect(dragElement.style.transform).toBeFalsy(); + })); - it('should allow for the position constrain logic to be customized', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - const spy = jasmine.createSpy('constrain position spy').and.returnValue({ - x: 50, - y: 50, - } as Point); + it('should not be able to drag the element if dragging is disabled', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - fixture.componentInstance.constrainPosition = spy; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + expect(dragElement.classList).not.toContain('cdk-drag-disabled'); - expect(dragElement.style.transform).toBeFalsy(); - dragElementViaMouse(fixture, dragElement, 300, 300); + fixture.componentInstance.dragDisabled.set(true); + fixture.detectChanges(); - expect(spy).toHaveBeenCalledWith( - jasmine.objectContaining({x: 300, y: 300}), - jasmine.any(DragRef), - jasmine.anything(), - jasmine.objectContaining({x: jasmine.any(Number), y: jasmine.any(Number)}), - ); + expect(dragElement.classList).toContain('cdk-drag-disabled'); + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBeFalsy(); + })); - const elementRect = dragElement.getBoundingClientRect(); - expect(Math.floor(elementRect.top)).toBe(50); - expect(Math.floor(elementRect.left)).toBe(50); - })); + it('should enable native drag interactions if dragging is disabled', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const styles = dragElement.style; - it('should throw if drag item is attached to an ng-container', () => { - const errorHandler = jasmine.createSpyObj(['handleError']); - createComponent(DraggableOnNgContainer, [ - { - provide: ErrorHandler, - useValue: errorHandler, - }, - ]).detectChanges(); - expect(errorHandler.handleError.calls.mostRecent().args[0].message).toMatch( - /^cdkDrag must be attached to an element node/, - ); - }); + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - it('should cancel drag if the mouse moves before the delay is elapsed', fakeAsync(() => { - // We can't use Jasmine's `clock` because Zone.js interferes with it. - spyOn(Date, 'now').and.callFake(() => currentTime); - let currentTime = 0; + fixture.componentInstance.dragDisabled.set(true); + fixture.detectChanges(); - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragStartDelay = 1000; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + })); - expect(dragElement.style.transform) - .withContext('Expected element not to be moved by default.') - .toBeFalsy(); + it('should enable native drag interactions if not dragging', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const styles = dragElement.style; - startDraggingViaMouse(fixture, dragElement); - currentTime += 750; - dispatchMouseEvent(document, 'mousemove', 50, 100); - currentTime += 500; - fixture.detectChanges(); + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + })); - expect(dragElement.style.transform) - .withContext('Expected element not to be moved if the mouse moved before the delay.') - .toBeFalsy(); - })); + it('should disable native drag interactions if dragging', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const styles = dragElement.style; - it('should enable native drag interactions if mouse moves before the delay', fakeAsync(() => { - // We can't use Jasmine's `clock` because Zone.js interferes with it. - spyOn(Date, 'now').and.callFake(() => currentTime); - let currentTime = 0; + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragStartDelay = 1000; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const styles = dragElement.style; + startDraggingViaMouse(fixture, dragElement); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); - expect(dragElement.style.transform) - .withContext('Expected element not to be moved by default.') - .toBeFalsy(); + expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); + })); - startDraggingViaMouse(fixture, dragElement); - currentTime += 750; - dispatchMouseEvent(document, 'mousemove', 50, 100); - currentTime += 500; - fixture.detectChanges(); + it('should re-enable drag interactions once dragging is over', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const styles = dragElement.style; - expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); - })); + startDraggingViaMouse(fixture, dragElement); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); - it('should allow dragging after the drag start delay is elapsed', fakeAsync(() => { - // We can't use Jasmine's `clock` because Zone.js interferes with it. - spyOn(Date, 'now').and.callFake(() => currentTime); - let currentTime = 0; + expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragStartDelay = 500; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + dispatchMouseEvent(document, 'mouseup', 50, 100); + fixture.detectChanges(); - expect(dragElement.style.transform) - .withContext('Expected element not to be moved by default.') - .toBeFalsy(); + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + })); - dispatchMouseEvent(dragElement, 'mousedown'); - fixture.detectChanges(); - currentTime += 750; + it('should not stop propagation for the drag sequence start event by default', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - // The first `mousemove` here starts the sequence and the second one moves the element. - dispatchMouseEvent(document, 'mousemove', 50, 100); - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); + const event = createMouseEvent('mousedown'); + spyOn(event, 'stopPropagation').and.callThrough(); - expect(dragElement.style.transform) - .withContext('Expected element to be dragged after all the time has passed.') - .toBe('translate3d(50px, 100px, 0px)'); - })); + dispatchEvent(dragElement, event); + fixture.detectChanges(); - it('should not prevent the default touch action before the delay has elapsed', fakeAsync(() => { - spyOn(Date, 'now').and.callFake(() => currentTime); - let currentTime = 0; + expect(event.stopPropagation).not.toHaveBeenCalled(); + })); - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragStartDelay = 500; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + it('should not throw if destroyed before the first change detection run', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); - expect(dragElement.style.transform) - .withContext('Expected element not to be moved by default.') - .toBeFalsy(); + expect(() => { + fixture.destroy(); + }).not.toThrow(); + })); + + it('should enable native drag interactions on the drag item when there is a handle', () => { + const fixture = createComponent(StandaloneDraggableWithHandle); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + expect(dragElement.style.touchAction).not.toBe('none'); + }); - dispatchTouchEvent(dragElement, 'touchstart'); - fixture.detectChanges(); - currentTime += 250; + it('should disable native drag interactions on the drag handle', () => { + const fixture = createComponent(StandaloneDraggableWithHandle); + fixture.detectChanges(); + const styles = fixture.componentInstance.handleElement.nativeElement.style; + expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); + }); - expect(dispatchTouchEvent(document, 'touchmove', 50, 100).defaultPrevented).toBe(false); - })); + it('should enable native drag interactions on the drag handle if dragging is disabled', () => { + const fixture = createComponent(StandaloneDraggableWithHandle); + fixture.detectChanges(); + fixture.componentInstance.draggingDisabled = true; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + const styles = fixture.componentInstance.handleElement.nativeElement.style; + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + }); - it('should handle the drag delay as a string', fakeAsync(() => { - // We can't use Jasmine's `clock` because Zone.js interferes with it. - spyOn(Date, 'now').and.callFake(() => currentTime); - let currentTime = 0; + it( + 'should enable native drag interactions on the drag handle if dragging is disabled ' + + 'on init', + () => { + const fixture = createComponent(StandaloneDraggableWithHandle); + fixture.componentInstance.draggingDisabled = true; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + const styles = fixture.componentInstance.handleElement.nativeElement.style; + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + }, + ); + + it('should toggle native drag interactions based on whether the handle is disabled', () => { + const fixture = createComponent(StandaloneDraggableWithHandle); + fixture.detectChanges(); + fixture.componentInstance.handleInstance.disabled = true; + fixture.detectChanges(); + const styles = fixture.componentInstance.handleElement.nativeElement.style; + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + + fixture.componentInstance.handleInstance.disabled = false; + fixture.detectChanges(); + expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none'); + }); + it('should be able to reset a freely-dragged item to its initial position', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + + fixture.componentInstance.dragInstance.reset(); + expect(dragElement.style.transform).toBeFalsy(); + })); + + it('should preserve initial transform after resetting', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElement.style.transform = 'scale(2)'; + + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px) scale(2)'); + + fixture.componentInstance.dragInstance.reset(); + expect(dragElement.style.transform).toBe('scale(2)'); + })); + + it('should start dragging an item from its initial position after a reset', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + fixture.componentInstance.dragInstance.reset(); + + dragElementViaMouse(fixture, dragElement, 25, 50); + expect(dragElement.style.transform).toBe('translate3d(25px, 50px, 0px)'); + })); + + it('should not dispatch multiple events for a mouse event right after a touch event', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + // Dispatch a touch sequence. + dispatchTouchEvent(dragElement, 'touchstart'); + fixture.detectChanges(); + dispatchTouchEvent(dragElement, 'touchend'); + fixture.detectChanges(); + tick(); + + // Immediately dispatch a mouse sequence to simulate a fake event. + startDraggingViaMouse(fixture, dragElement); + fixture.detectChanges(); + dispatchMouseEvent(dragElement, 'mouseup'); + fixture.detectChanges(); + tick(); + + expect(fixture.componentInstance.startedSpy).toHaveBeenCalledTimes(1); + expect(fixture.componentInstance.endedSpy).toHaveBeenCalledTimes(1); + })); + + it('should round the transform value', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 13.37, 37); + expect(dragElement.style.transform).toBe('translate3d(13px, 37px, 0px)'); + })); + + it('should allow for dragging to be constrained to an element', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.boundary = '.wrapper'; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + })); + + it('should allow for dragging to be constrained to an element while using constrainPosition', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.boundary = '.wrapper'; + fixture.detectChanges(); + + fixture.componentInstance.dragInstance.constrainPosition = ( + {x, y}: Point, + _dragRef: DragRef, + _dimensions: DOMRect, + pickup: Point, + ) => { + x -= pickup.x; + y -= pickup.y; + return {x, y}; + }; + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + })); + + it('should be able to pass in a DOM node as the boundary', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.boundary = fixture.nativeElement.querySelector('.wrapper'); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + })); + + it('should adjust the x offset if the boundary becomes narrower after a viewport resize', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); + fixture.componentInstance.boundary = boundary; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + + boundary.style.width = '150px'; + dispatchFakeEvent(window, 'resize'); + tick(20); + + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + })); + + it('should keep the old position if the boundary is invisible after a resize', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); + fixture.componentInstance.boundary = boundary; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + + boundary.style.display = 'none'; + dispatchFakeEvent(window, 'resize'); + tick(20); + + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + })); + + it('should handle the element and boundary dimensions changing between drag sequences', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); + fixture.componentInstance.boundary = boundary; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + + // Bump the width and height of both the boundary and the drag element. + boundary.style.width = boundary.style.height = '300px'; + dragElement.style.width = dragElement.style.height = '150px'; + + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(150px, 150px, 0px)'); + })); + + it('should adjust the y offset if the boundary becomes shorter after a viewport resize', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); + fixture.componentInstance.boundary = boundary; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + + boundary.style.height = '150px'; + dispatchFakeEvent(window, 'resize'); + tick(20); + + expect(dragElement.style.transform).toBe('translate3d(100px, 50px, 0px)'); + })); + + it( + 'should reset the x offset if the boundary becomes narrower than the element ' + + 'after a resize', + fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragStartDelay = '500'; + const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); + fixture.componentInstance.boundary = boundary; fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; - expect(dragElement.style.transform) - .withContext('Expected element not to be moved by default.') - .toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); - dispatchMouseEvent(dragElement, 'mousedown'); - fixture.detectChanges(); - currentTime += 750; + boundary.style.width = '50px'; + dispatchFakeEvent(window, 'resize'); + tick(20); - // The first `mousemove` here starts the sequence and the second one moves the element. - dispatchMouseEvent(document, 'mousemove', 50, 100); - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); + expect(dragElement.style.transform).toBe('translate3d(0px, 100px, 0px)'); + }), + ); + + it('should reset the y offset if the boundary becomes shorter than the element after a resize', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); + fixture.componentInstance.boundary = boundary; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + + boundary.style.height = '50px'; + dispatchFakeEvent(window, 'resize'); + tick(20); + + expect(dragElement.style.transform).toBe('translate3d(100px, 0px, 0px)'); + })); + + it('should allow for the position constrain logic to be customized', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const spy = jasmine.createSpy('constrain position spy').and.returnValue({ + x: 50, + y: 50, + } as Point); + + fixture.componentInstance.constrainPosition = spy; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 300, 300); + + expect(spy).toHaveBeenCalledWith( + jasmine.objectContaining({x: 300, y: 300}), + jasmine.any(DragRef), + jasmine.anything(), + jasmine.objectContaining({x: jasmine.any(Number), y: jasmine.any(Number)}), + ); - expect(dragElement.style.transform) - .withContext('Expected element to be dragged after all the time has passed.') - .toBe('translate3d(50px, 100px, 0px)'); - })); + const elementRect = dragElement.getBoundingClientRect(); + expect(Math.floor(elementRect.top)).toBe(50); + expect(Math.floor(elementRect.left)).toBe(50); + })); - it('should be able to configure the drag start delay based on the event type', fakeAsync(() => { - // We can't use Jasmine's `clock` because Zone.js interferes with it. - spyOn(Date, 'now').and.callFake(() => currentTime); - let currentTime = 0; + it('should throw if drag item is attached to an ng-container', () => { + const errorHandler = jasmine.createSpyObj(['handleError']); + createComponent(DraggableOnNgContainer, { + providers: [ + { + provide: ErrorHandler, + useValue: errorHandler, + }, + ], + }).detectChanges(); + expect(errorHandler.handleError.calls.mostRecent().args[0].message).toMatch( + /^cdkDrag must be attached to an element node/, + ); + }); - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.dragStartDelay = {touch: 500, mouse: 0}; - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + it('should cancel drag if the mouse moves before the delay is elapsed', fakeAsync(() => { + // We can't use Jasmine's `clock` because Zone.js interferes with it. + spyOn(Date, 'now').and.callFake(() => currentTime); + let currentTime = 0; + + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragStartDelay = 1000; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform) + .withContext('Expected element not to be moved by default.') + .toBeFalsy(); + + startDraggingViaMouse(fixture, dragElement); + currentTime += 750; + dispatchMouseEvent(document, 'mousemove', 50, 100); + currentTime += 500; + fixture.detectChanges(); + + expect(dragElement.style.transform) + .withContext('Expected element not to be moved if the mouse moved before the delay.') + .toBeFalsy(); + })); + + it('should enable native drag interactions if mouse moves before the delay', fakeAsync(() => { + // We can't use Jasmine's `clock` because Zone.js interferes with it. + spyOn(Date, 'now').and.callFake(() => currentTime); + let currentTime = 0; + + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragStartDelay = 1000; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const styles = dragElement.style; + + expect(dragElement.style.transform) + .withContext('Expected element not to be moved by default.') + .toBeFalsy(); + + startDraggingViaMouse(fixture, dragElement); + currentTime += 750; + dispatchMouseEvent(document, 'mousemove', 50, 100); + currentTime += 500; + fixture.detectChanges(); + + expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy(); + })); + + it('should allow dragging after the drag start delay is elapsed', fakeAsync(() => { + // We can't use Jasmine's `clock` because Zone.js interferes with it. + spyOn(Date, 'now').and.callFake(() => currentTime); + let currentTime = 0; + + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragStartDelay = 500; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform) + .withContext('Expected element not to be moved by default.') + .toBeFalsy(); + + dispatchMouseEvent(dragElement, 'mousedown'); + fixture.detectChanges(); + currentTime += 750; + + // The first `mousemove` here starts the sequence and the second one moves the element. + dispatchMouseEvent(document, 'mousemove', 50, 100); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); + + expect(dragElement.style.transform) + .withContext('Expected element to be dragged after all the time has passed.') + .toBe('translate3d(50px, 100px, 0px)'); + })); + + it('should not prevent the default touch action before the delay has elapsed', fakeAsync(() => { + spyOn(Date, 'now').and.callFake(() => currentTime); + let currentTime = 0; + + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragStartDelay = 500; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform) + .withContext('Expected element not to be moved by default.') + .toBeFalsy(); + + dispatchTouchEvent(dragElement, 'touchstart'); + fixture.detectChanges(); + currentTime += 250; + + expect(dispatchTouchEvent(document, 'touchmove', 50, 100).defaultPrevented).toBe(false); + })); + + it('should handle the drag delay as a string', fakeAsync(() => { + // We can't use Jasmine's `clock` because Zone.js interferes with it. + spyOn(Date, 'now').and.callFake(() => currentTime); + let currentTime = 0; + + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragStartDelay = '500'; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform) + .withContext('Expected element not to be moved by default.') + .toBeFalsy(); + + dispatchMouseEvent(dragElement, 'mousedown'); + fixture.detectChanges(); + currentTime += 750; + + // The first `mousemove` here starts the sequence and the second one moves the element. + dispatchMouseEvent(document, 'mousemove', 50, 100); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); + + expect(dragElement.style.transform) + .withContext('Expected element to be dragged after all the time has passed.') + .toBe('translate3d(50px, 100px, 0px)'); + })); - expect(dragElement.style.transform) - .withContext('Expected element not to be moved by default.') - .toBeFalsy(); + it('should be able to configure the drag start delay based on the event type', fakeAsync(() => { + // We can't use Jasmine's `clock` because Zone.js interferes with it. + spyOn(Date, 'now').and.callFake(() => currentTime); + let currentTime = 0; - dragElementViaTouch(fixture, dragElement, 50, 100); - expect(dragElement.style.transform) - .withContext('Expected element not to be moved via touch because it has a delay.') - .toBeFalsy(); + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.dragStartDelay = {touch: 500, mouse: 0}; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragElement.style.transform) - .withContext('Expected element to be moved via mouse because it has no delay.') - .toBe('translate3d(50px, 100px, 0px)'); - })); + expect(dragElement.style.transform) + .withContext('Expected element not to be moved by default.') + .toBeFalsy(); - it('should be able to get the current position', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); + dragElementViaTouch(fixture, dragElement, 50, 100); + expect(dragElement.style.transform) + .withContext('Expected element not to be moved via touch because it has a delay.') + .toBeFalsy(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const dragInstance = fixture.componentInstance.dragInstance; + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform) + .withContext('Expected element to be moved via mouse because it has no delay.') + .toBe('translate3d(50px, 100px, 0px)'); + })); - expect(dragInstance.getFreeDragPosition()).toEqual({x: 0, y: 0}); + it('should be able to get the current position', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); - dragElementViaMouse(fixture, dragElement, 50, 100); - expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const dragInstance = fixture.componentInstance.dragInstance; - dragElementViaMouse(fixture, dragElement, 100, 200); - expect(dragInstance.getFreeDragPosition()).toEqual({x: 150, y: 300}); - })); + expect(dragInstance.getFreeDragPosition()).toEqual({x: 0, y: 0}); - it('should be able to set the current position programmatically', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const dragInstance = fixture.componentInstance.dragInstance; + dragElementViaMouse(fixture, dragElement, 100, 200); + expect(dragInstance.getFreeDragPosition()).toEqual({x: 150, y: 300}); + })); - dragInstance.setFreeDragPosition({x: 50, y: 100}); + it('should be able to set the current position programmatically', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); - })); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const dragInstance = fixture.componentInstance.dragInstance; - it('should be able to set the current position', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.freeDragPosition = {x: 50, y: 100}; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + dragInstance.setFreeDragPosition({x: 50, y: 100}); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const dragInstance = fixture.componentInstance.dragInstance; + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); + })); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); - })); + it('should be able to set the current position', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.freeDragPosition = {x: 50, y: 100}; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); - it('should be able to get the up-to-date position as the user is dragging', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const dragInstance = fixture.componentInstance.dragInstance; - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const dragInstance = fixture.componentInstance.dragInstance; + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); + })); - expect(dragInstance.getFreeDragPosition()).toEqual({x: 0, y: 0}); + it('should be able to get the up-to-date position as the user is dragging', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); - startDraggingViaMouse(fixture, dragElement); - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const dragInstance = fixture.componentInstance.dragInstance; - expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); + expect(dragInstance.getFreeDragPosition()).toEqual({x: 0, y: 0}); - dispatchMouseEvent(document, 'mousemove', 100, 200); - fixture.detectChanges(); + startDraggingViaMouse(fixture, dragElement); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); - expect(dragInstance.getFreeDragPosition()).toEqual({x: 100, y: 200}); - })); + expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); - it('should react to changes in the free drag position', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.freeDragPosition = {x: 50, y: 100}; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + dispatchMouseEvent(document, 'mousemove', 100, 200); + fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + expect(dragInstance.getFreeDragPosition()).toEqual({x: 100, y: 200}); + })); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + it('should react to changes in the free drag position', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.freeDragPosition = {x: 50, y: 100}; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); - fixture.componentInstance.freeDragPosition = {x: 100, y: 200}; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - expect(dragElement.style.transform).toBe('translate3d(100px, 200px, 0px)'); - })); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - it('should be able to continue dragging after the current position was set', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.componentInstance.freeDragPosition = {x: 50, y: 100}; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + fixture.componentInstance.freeDragPosition = {x: 100, y: 200}; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + expect(dragElement.style.transform).toBe('translate3d(100px, 200px, 0px)'); + })); - dragElementViaMouse(fixture, dragElement, 100, 200); + it('should be able to continue dragging after the current position was set', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.freeDragPosition = {x: 50, y: 100}; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); - })); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - it('should include the dragged distance as the user is dragging', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - const spy = jasmine.createSpy('moved spy'); - const subscription = fixture.componentInstance.dragInstance.moved.subscribe(spy); + dragElementViaMouse(fixture, dragElement, 100, 200); - startDraggingViaMouse(fixture, dragElement); + expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); + })); - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); + it('should include the dragged distance as the user is dragging', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const spy = jasmine.createSpy('moved spy'); + const subscription = fixture.componentInstance.dragInstance.moved.subscribe(spy); - let event = spy.calls.mostRecent().args[0]; - expect(event.distance).toEqual({x: 50, y: 100}); + startDraggingViaMouse(fixture, dragElement); - dispatchMouseEvent(document, 'mousemove', 75, 50); - fixture.detectChanges(); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); - event = spy.calls.mostRecent().args[0]; - expect(event.distance).toEqual({x: 75, y: 50}); + let event = spy.calls.mostRecent().args[0]; + expect(event.distance).toEqual({x: 50, y: 100}); - subscription.unsubscribe(); - })); + dispatchMouseEvent(document, 'mousemove', 75, 50); + fixture.detectChanges(); + + event = spy.calls.mostRecent().args[0]; + expect(event.distance).toEqual({x: 75, y: 50}); - it('should be able to configure the drag input defaults through a provider', fakeAsync(() => { - const config: DragDropConfig = { - draggingDisabled: true, - dragStartDelay: 1337, - lockAxis: 'y', - constrainPosition: () => ({x: 1337, y: 42}), - previewClass: 'custom-preview-class', - boundaryElement: '.boundary', - rootElementSelector: '.root', - previewContainer: 'parent', - }; - - const fixture = createComponent(PlainStandaloneDraggable, [ + subscription.unsubscribe(); + })); + + it('should be able to configure the drag input defaults through a provider', fakeAsync(() => { + const config: DragDropConfig = { + draggingDisabled: true, + dragStartDelay: 1337, + lockAxis: 'y', + constrainPosition: () => ({x: 1337, y: 42}), + previewClass: 'custom-preview-class', + boundaryElement: '.boundary', + rootElementSelector: '.root', + previewContainer: 'parent', + }; + + const fixture = createComponent(PlainStandaloneDraggable, { + providers: [ { provide: CDK_DRAG_CONFIG, useValue: config, }, - ]); - fixture.detectChanges(); - const drag = fixture.componentInstance.dragInstance; - expect(drag.disabled).toBe(true); - expect(drag.dragStartDelay).toBe(1337); - expect(drag.lockAxis).toBe('y'); - expect(drag.constrainPosition).toBe(config.constrainPosition); - expect(drag.previewClass).toBe('custom-preview-class'); - expect(drag.boundaryElement).toBe('.boundary'); - expect(drag.rootElementSelector).toBe('.root'); - expect(drag.previewContainer).toBe('parent'); - })); + ], + }); + fixture.detectChanges(); + const drag = fixture.componentInstance.dragInstance; + expect(drag.disabled).toBe(true); + expect(drag.dragStartDelay).toBe(1337); + expect(drag.lockAxis).toBe('y'); + expect(drag.constrainPosition).toBe(config.constrainPosition); + expect(drag.previewClass).toBe('custom-preview-class'); + expect(drag.boundaryElement).toBe('.boundary'); + expect(drag.rootElementSelector).toBe('.root'); + expect(drag.previewContainer).toBe('parent'); + })); + + it('should not throw if touches and changedTouches are empty', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + startDraggingViaTouch(fixture, dragElement); + continueDraggingViaTouch(fixture, 50, 100); + + const event = createTouchEvent('touchend', 50, 100); + Object.defineProperties(event, { + touches: {get: () => []}, + changedTouches: {get: () => []}, + }); - it('should not throw if touches and changedTouches are empty', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); + expect(() => { + dispatchEvent(document, event); fixture.detectChanges(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + tick(); + }).not.toThrow(); + })); - startDraggingViaTouch(fixture, dragElement); - continueDraggingViaTouch(fixture, 50, 100); + it('should update the free drag position if the page is scrolled', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); - const event = createTouchEvent('touchend', 50, 100); - Object.defineProperties(event, { - touches: {get: () => []}, - changedTouches: {get: () => []}, - }); + const cleanup = makeScrollable(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - expect(() => { - dispatchEvent(document, event); - fixture.detectChanges(); - tick(); - }).not.toThrow(); - })); + expect(dragElement.style.transform).toBeFalsy(); + startDraggingViaMouse(fixture, dragElement, 0, 0); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); + + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + + scrollTo(0, 500); + dispatchFakeEvent(document, 'scroll'); + fixture.detectChanges(); + expect(dragElement.style.transform).toBe('translate3d(50px, 600px, 0px)'); - it('should update the free drag position if the page is scrolled', fakeAsync(() => { + cleanup(); + })); + + it( + 'should update the free drag position if the user moves their pointer after the page ' + + 'is scrolled', + fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); fixture.detectChanges(); @@ -1493,42 +1490,16 @@ describe('Standalone CdkDrag', () => { scrollTo(0, 500); dispatchFakeEvent(document, 'scroll'); fixture.detectChanges(); - expect(dragElement.style.transform).toBe('translate3d(50px, 600px, 0px)'); - - cleanup(); - })); - - it( - 'should update the free drag position if the user moves their pointer after the page ' + - 'is scrolled', - fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); - - const cleanup = makeScrollable(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; - - expect(dragElement.style.transform).toBeFalsy(); - startDraggingViaMouse(fixture, dragElement, 0, 0); - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); - - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - - scrollTo(0, 500); - dispatchFakeEvent(document, 'scroll'); - fixture.detectChanges(); - dispatchMouseEvent(document, 'mousemove', 50, 200); - fixture.detectChanges(); + dispatchMouseEvent(document, 'mousemove', 50, 200); + fixture.detectChanges(); - expect(dragElement.style.transform).toBe('translate3d(50px, 700px, 0px)'); + expect(dragElement.style.transform).toBe('translate3d(50px, 700px, 0px)'); - cleanup(); - }), - ); - }); + cleanup(); + }), + ); - describe('draggable with a handle', () => { + describe('with a handle', () => { it('should not be able to drag the entire element if it has a handle', fakeAsync(() => { const fixture = createComponent(StandaloneDraggableWithHandle); fixture.detectChanges(); @@ -1621,7 +1592,9 @@ describe('Standalone CdkDrag', () => { })); it('should be able to drag with a handle that is not a direct descendant', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggableWithIndirectHandle); + const fixture = createComponent(StandaloneDraggableWithIndirectHandle, { + extraDeclarations: [PassthroughComponent], + }); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const handle = fixture.componentInstance.handleElement.nativeElement; @@ -1703,12 +1676,9 @@ describe('Standalone CdkDrag', () => { return; } - const fixture = createComponent( - StandaloneDraggableWithShadowInsideHandle, - undefined, - undefined, - [ShadowWrapper], - ); + const fixture = createComponent(StandaloneDraggableWithShadowInsideHandle, { + extraDeclarations: [ShadowWrapper], + }); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const handleChild = fixture.componentInstance.handleChild.nativeElement; diff --git a/src/cdk/drag-drop/directives/standalone-drag.zone.spec.ts b/src/cdk/drag-drop/directives/standalone-drag.zone.spec.ts index 0acb83681139..45bb38fb1fd0 100644 --- a/src/cdk/drag-drop/directives/standalone-drag.zone.spec.ts +++ b/src/cdk/drag-drop/directives/standalone-drag.zone.spec.ts @@ -1,48 +1,21 @@ -import {CdkScrollableModule} from '@angular/cdk/scrolling'; import { Component, ElementRef, NgZone, - Provider, Type, ViewChild, provideZoneChangeDetection, } from '@angular/core'; -import {ComponentFixture, TestBed} from '@angular/core/testing'; -import {DragDropModule} from '../drag-drop-module'; import {Point} from '../drag-ref'; -import {CDK_DRAG_CONFIG, DragDropConfig} from './config'; import {CdkDrag} from './drag'; -import {dragElementViaMouse} from './test-utils.spec'; +import {createComponent as _createComponent, dragElementViaMouse} from './test-utils.spec'; +import {ComponentFixture} from '@angular/core/testing'; describe('Standalone CdkDrag Zone.js integration', () => { - function createComponent( - componentType: Type, - providers: Provider[] = [], - dragDistance = 0, - extraDeclarations: Type[] = [], - ): ComponentFixture { - TestBed.configureTestingModule({ - imports: [DragDropModule, CdkScrollableModule], - providers: [ - provideZoneChangeDetection(), - { - provide: CDK_DRAG_CONFIG, - useValue: { - // We default the `dragDistance` to zero, because the majority of the tests - // don't care about it and drags are a lot easier to simulate when we don't - // have to deal with thresholds. - dragStartThreshold: dragDistance, - pointerDirectionChangeThreshold: 5, - } as DragDropConfig, - }, - ...providers, - ], - declarations: [componentType, ...extraDeclarations], + function createComponent(type: Type): ComponentFixture { + return _createComponent(type, { + providers: [provideZoneChangeDetection()], }); - - TestBed.compileComponents(); - return TestBed.createComponent(componentType); } it('should emit to `moved` inside the NgZone', () => { diff --git a/src/cdk/drag-drop/directives/test-utils.spec.ts b/src/cdk/drag-drop/directives/test-utils.spec.ts index 451a390a5e9c..16e7eb5426b3 100644 --- a/src/cdk/drag-drop/directives/test-utils.spec.ts +++ b/src/cdk/drag-drop/directives/test-utils.spec.ts @@ -1,5 +1,51 @@ +import {EnvironmentProviders, Provider, Type, ViewEncapsulation} from '@angular/core'; +import {ComponentFixture, TestBed, flush, tick} from '@angular/core/testing'; import {dispatchMouseEvent, dispatchTouchEvent} from '@angular/cdk/testing/private'; -import {ComponentFixture, flush, tick} from '@angular/core/testing'; +import {CdkScrollableModule} from '@angular/cdk/scrolling'; +import {DragDropModule} from '../drag-drop-module'; +import {CDK_DRAG_CONFIG, DragDropConfig} from './config'; + +/** + * Creates a component fixture that can be used in a test. + * @param componentType Component for which to create the fixture. + * @param config Object that can be used to further configure the test. + */ +export function createComponent( + componentType: Type, + config: { + providers?: (Provider | EnvironmentProviders)[]; + dragDistance?: number; + extraDeclarations?: Type[]; + encapsulation?: ViewEncapsulation; + } = {}, +): ComponentFixture { + TestBed.configureTestingModule({ + imports: [DragDropModule, CdkScrollableModule], + providers: [ + { + provide: CDK_DRAG_CONFIG, + useValue: { + // We default the `dragDistance` to zero, because the majority of the tests + // don't care about it and drags are a lot easier to simulate when we don't + // have to deal with thresholds. + dragStartThreshold: config?.dragDistance ?? 0, + pointerDirectionChangeThreshold: 5, + } as DragDropConfig, + }, + ...(config.providers || []), + ], + declarations: [componentType, ...(config.extraDeclarations || [])], + }); + + if (config.encapsulation != null) { + TestBed.overrideComponent(componentType, { + set: {encapsulation: config.encapsulation}, + }); + } + + TestBed.compileComponents(); + return TestBed.createComponent(componentType); +} /** * Drags an element to a position on the page using the mouse. From 4bc1ea077c94830b8397aacc17fa2a4b38534789 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 30 May 2024 11:15:54 +0200 Subject: [PATCH 08/61] test(cdk/drag-drop): allow drop list tests to be reused Reworks the tests for drop lists so that they can be reused for some future features. --- ...{drag.spec.ts => drop-list-shared.spec.ts} | 348 ++++++++++-------- .../directives/single-axis-drop-list.spec.ts | 79 ++++ .../drag-drop/directives/test-utils.spec.ts | 107 ++---- 3 files changed, 287 insertions(+), 247 deletions(-) rename src/cdk/drag-drop/directives/{drag.spec.ts => drop-list-shared.spec.ts} (96%) create mode 100644 src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts similarity index 96% rename from src/cdk/drag-drop/directives/drag.spec.ts rename to src/cdk/drag-drop/directives/drop-list-shared.spec.ts index 03ba6a04cebe..b82c9ce56195 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts @@ -1,6 +1,6 @@ import {Directionality} from '@angular/cdk/bidi'; import {_supportsShadowDom} from '@angular/cdk/platform'; -import {CdkScrollableModule, ViewportRuler} from '@angular/cdk/scrolling'; +import {ViewportRuler} from '@angular/cdk/scrolling'; import { createMouseEvent, createTouchEvent, @@ -17,13 +17,14 @@ import { ElementRef, Input, QueryList, + Type, ViewChild, ViewChildren, ViewEncapsulation, inject, signal, } from '@angular/core'; -import {TestBed, fakeAsync, flush, tick} from '@angular/core/testing'; +import {ComponentFixture, TestBed, fakeAsync, flush, tick} from '@angular/core/testing'; import {of as observableOf} from 'rxjs'; import {extendStyles} from '../dom/styling'; @@ -31,18 +32,15 @@ import {CdkDragDrop, CdkDragEnter, CdkDragStart} from '../drag-events'; import {DragRef, Point, PreviewContainer} from '../drag-ref'; import {moveItemInArray} from '../drag-utils'; -import {CDK_DRAG_CONFIG, DragAxis, DragDropConfig} from './config'; +import {CDK_DRAG_CONFIG, DragAxis, DragDropConfig, DropListOrientation} from './config'; import {CdkDrag} from './drag'; import {CdkDropList} from './drop-list'; import {CdkDropListGroup} from './drop-list-group'; import { - createComponent, - assertDownwardSorting, - assertUpwardSorting, + createComponent as _createComponent, + DragDropTestConfig, continueDraggingViaTouch, dragElementViaMouse, - getElementIndexByPosition, - getElementSibligsByPosition, makeScrollable, startDraggingViaMouse, startDraggingViaTouch, @@ -53,7 +51,32 @@ import { const ITEM_HEIGHT = 25; const ITEM_WIDTH = 75; -describe('CdkDrag', () => { +export function defineCommonDropListTests(config: { + /** Orientation value that will be passed to tests checking vertical orientation. */ + verticalListOrientation: DropListOrientation; + + /** Orientation value that will be passed to tests checking horizontal orientation. */ + horizontalListOrientation: DropListOrientation; + + /** Asserts that sorting an element up works correctly. */ + assertUpwardSorting: (fixture: ComponentFixture, items: Element[]) => void; + + /** Asserts that sorting an element down works correctly. */ + assertDownwardSorting: (fixture: ComponentFixture, items: Element[]) => void; + + /** Gets the index of an element among its siblings, based on their visible position. */ + getElementIndexByPosition: (element: Element, direction: 'top' | 'left') => number; + + /** Gets the siblings of an element, sorted by their visible position. */ + getElementSibligsByPosition: (element: Element, direction: 'top' | 'left') => Element[]; +}) { + function createComponent( + type: Type, + testConfig: DragDropTestConfig = {}, + ): ComponentFixture { + return _createComponent(type, {...testConfig, listOrientation: config.verticalListOrientation}); + } + describe('in a drop container', () => { it('should be able to attach data to the drop container', () => { const fixture = createComponent(DraggableInDropZone); @@ -1467,7 +1490,7 @@ describe('CdkDrag', () => { it('should move the placeholder as an item is being sorted down', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.detectChanges(); - assertDownwardSorting( + config.assertDownwardSorting( fixture, fixture.componentInstance.dragItems.map(item => { return item.element.nativeElement; @@ -1481,7 +1504,7 @@ describe('CdkDrag', () => { const cleanup = makeScrollable(); scrollTo(0, 500); - assertDownwardSorting( + config.assertDownwardSorting( fixture, fixture.componentInstance.dragItems.map(item => { return item.element.nativeElement; @@ -1493,7 +1516,7 @@ describe('CdkDrag', () => { it('should move the placeholder as an item is being sorted up', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.detectChanges(); - assertUpwardSorting( + config.assertUpwardSorting( fixture, fixture.componentInstance.dragItems.map(item => { return item.element.nativeElement; @@ -1507,7 +1530,7 @@ describe('CdkDrag', () => { const cleanup = makeScrollable(); scrollTo(0, 500); - assertUpwardSorting( + config.assertUpwardSorting( fixture, fixture.componentInstance.dragItems.map(item => { return item.element.nativeElement; @@ -1535,7 +1558,7 @@ describe('CdkDrag', () => { // Add a few pixels to the left offset so we get some overlap. dispatchMouseEvent(document, 'mousemove', elementRect.left + 5, elementRect.top); fixture.detectChanges(); - expect(getElementIndexByPosition(placeholder, 'left')).toBe(i); + expect(config.getElementIndexByPosition(placeholder, 'left')).toBe(i); } dispatchMouseEvent(document, 'mouseup'); @@ -1562,7 +1585,7 @@ describe('CdkDrag', () => { // Remove a few pixels from the right offset so we get some overlap. dispatchMouseEvent(document, 'mousemove', elementRect.right - 5, elementRect.top); fixture.detectChanges(); - expect(getElementIndexByPosition(placeholder, 'left')).toBe(i); + expect(config.getElementIndexByPosition(placeholder, 'left')).toBe(i); } dispatchMouseEvent(document, 'mouseup'); @@ -1591,7 +1614,7 @@ describe('CdkDrag', () => { fixture.detectChanges(); expect( - getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), + config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), ).toEqual(['One', 'Two', 'Three', 'Zero']); dispatchMouseEvent(document, 'mouseup'); @@ -1708,7 +1731,7 @@ describe('CdkDrag', () => { fixture.detectChanges(); expect( - getElementSibligsByPosition(placeholder, 'left').map(e => e.textContent!.trim()), + config.getElementSibligsByPosition(placeholder, 'left').map(e => e.textContent!.trim()), ).toEqual(['One', 'Two', 'Three', 'Zero']); dispatchMouseEvent(document, 'mouseup'); @@ -1818,7 +1841,7 @@ describe('CdkDrag', () => { const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; expect( - getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), + config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), ).toEqual(['Zero', 'One', 'Two', 'Three']); const targetRect = target.getBoundingClientRect(); @@ -1827,14 +1850,14 @@ describe('CdkDrag', () => { // Move over the target so there's a 20px overlap. dispatchMouseEvent(document, 'mousemove', targetRect.left, pointerTop); fixture.detectChanges(); - expect(getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) + expect(config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) .withContext('Expected position to swap.') .toEqual(['One', 'Zero', 'Two', 'Three']); // Move down a further 1px. dispatchMouseEvent(document, 'mousemove', targetRect.left, pointerTop + 1); fixture.detectChanges(); - expect(getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) + expect(config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) .withContext('Expected positions not to swap.') .toEqual(['One', 'Zero', 'Two', 'Three']); @@ -1860,7 +1883,7 @@ describe('CdkDrag', () => { const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; expect( - getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), + config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), ).toEqual(['Zero', 'One', 'Two', 'Three']); const targetRect = target.getBoundingClientRect(); @@ -1869,14 +1892,14 @@ describe('CdkDrag', () => { // Move over the target so there's a 20px overlap. dispatchMouseEvent(document, 'mousemove', targetRect.left, pointerTop); fixture.detectChanges(); - expect(getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) + expect(config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) .withContext('Expected position to swap.') .toEqual(['One', 'Zero', 'Two', 'Three']); // Move up 10px. dispatchMouseEvent(document, 'mousemove', targetRect.left, pointerTop - 10); fixture.detectChanges(); - expect(getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) + expect(config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) .withContext('Expected positions to swap again.') .toEqual(['Zero', 'One', 'Two', 'Three']); @@ -1902,7 +1925,7 @@ describe('CdkDrag', () => { const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; expect( - getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), + config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), ).toEqual(['Zero', 'One', 'Two', 'Three']); let targetRect = target.getBoundingClientRect(); @@ -1915,7 +1938,7 @@ describe('CdkDrag', () => { fixture.detectChanges(); expect( - getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), + config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), ).toEqual(['One', 'Two', 'Three', 'Zero']); // Refresh the rect since the element position has changed. @@ -1924,7 +1947,7 @@ describe('CdkDrag', () => { fixture.detectChanges(); expect( - getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), + config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), ).toEqual(['One', 'Two', 'Zero', 'Three']); dispatchMouseEvent(document, 'mouseup'); @@ -2495,7 +2518,7 @@ describe('CdkDrag', () => { dispatchMouseEvent(document, 'mousemove', targetX, targetY); fixture.detectChanges(); - expect(getElementIndexByPosition(placeholder, 'top')) + expect(config.getElementIndexByPosition(placeholder, 'top')) .withContext('Expected placeholder to stay in place.') .toBe(0); @@ -3396,7 +3419,7 @@ describe('CdkDrag', () => { documentElement.style.position = 'absolute'; documentElement.style.top = '-100px'; - assertDownwardSorting( + config.assertDownwardSorting( fixture, fixture.componentInstance.dragItems.map(item => { return item.element.nativeElement; @@ -3664,7 +3687,7 @@ describe('CdkDrag', () => { fixture.detectChanges(); }); - assertDownwardSorting(fixture, Array.from(dropZone.querySelectorAll('.cdk-drag'))); + config.assertDownwardSorting(fixture, Array.from(dropZone.querySelectorAll('.cdk-drag'))); })); it('should be able to return the last item inside its initial container', fakeAsync(() => { @@ -4446,7 +4469,7 @@ describe('CdkDrag', () => { expect(dropZones[0].contains(placeholder)) .withContext('Expected placeholder to be inside the first container.') .toBe(true); - expect(getElementIndexByPosition(placeholder, 'top')) + expect(config.getElementIndexByPosition(placeholder, 'top')) .withContext('Expected placeholder to be at item index.') .toBe(1); @@ -4456,7 +4479,7 @@ describe('CdkDrag', () => { expect(dropZones[1].contains(placeholder)) .withContext('Expected placeholder to be inside second container.') .toBe(true); - expect(getElementIndexByPosition(placeholder, 'top')) + expect(config.getElementIndexByPosition(placeholder, 'top')) .withContext('Expected placeholder to be at the target index.') .toBe(3); @@ -4474,7 +4497,7 @@ describe('CdkDrag', () => { expect(dropZones[0].contains(placeholder)) .withContext('Expected placeholder to be back inside first container.') .toBe(true); - expect(getElementIndexByPosition(placeholder, 'top')) + expect(config.getElementIndexByPosition(placeholder, 'top')) .withContext('Expected placeholder to be back at the initial index.') .toBe(1); @@ -4507,7 +4530,7 @@ describe('CdkDrag', () => { expect(dropZones[0].contains(placeholder)) .withContext('Expected placeholder to be inside the first container.') .toBe(true); - expect(getElementIndexByPosition(placeholder, 'top')) + expect(config.getElementIndexByPosition(placeholder, 'top')) .withContext('Expected placeholder to be at item index.') .toBe(1); @@ -4517,7 +4540,7 @@ describe('CdkDrag', () => { expect(dropZones[1].contains(placeholder)) .withContext('Expected placeholder to be inside second container.') .toBe(true); - expect(getElementIndexByPosition(placeholder, 'top')) + expect(config.getElementIndexByPosition(placeholder, 'top')) .withContext('Expected placeholder to be at the target index.') .toBe(3); @@ -4530,7 +4553,7 @@ describe('CdkDrag', () => { expect(dropZones[0].contains(placeholder)) .withContext('Expected placeholder to be back inside first container.') .toBe(true); - expect(getElementIndexByPosition(placeholder, 'top')) + expect(config.getElementIndexByPosition(placeholder, 'top')) .withContext('Expected placeholder to be at the index at which it entered.') .toBe(2); }), @@ -4555,7 +4578,7 @@ describe('CdkDrag', () => { expect(dropZones[0].contains(placeholder)) .withContext('Expected placeholder to be inside the first container.') .toBe(true); - expect(getElementIndexByPosition(placeholder, 'top')) + expect(config.getElementIndexByPosition(placeholder, 'top')) .withContext('Expected placeholder to be at item index.') .toBe(lastIndex); @@ -4565,7 +4588,7 @@ describe('CdkDrag', () => { expect(dropZones[1].contains(placeholder)) .withContext('Expected placeholder to be inside second container.') .toBe(true); - expect(getElementIndexByPosition(placeholder, 'top')) + expect(config.getElementIndexByPosition(placeholder, 'top')) .withContext('Expected placeholder to be at the target index.') .toBe(3); @@ -4583,7 +4606,7 @@ describe('CdkDrag', () => { expect(dropZones[0].contains(placeholder)) .withContext('Expected placeholder to be back inside first container.') .toBe(true); - expect(getElementIndexByPosition(placeholder, 'top')) + expect(config.getElementIndexByPosition(placeholder, 'top')) .withContext('Expected placeholder to be back at the initial index.') .toBe(lastIndex); @@ -5014,7 +5037,131 @@ describe('CdkDrag', () => { expect(event.stopPropagation).toHaveBeenCalled(); })); }); -}); + + // Horizontal fixtures need to be defined here, because the + // `horizontalListOrientation` value can change between tests. + // Use inline blocks here to avoid flexbox issues and not to have to flip floats in rtl. + const HORIZONTAL_FIXTURE_STYLES = ` + .cdk-drop-list { + display: block; + width: 500px; + background: pink; + font-size: 0; + } + + .cdk-drag { + height: ${ITEM_HEIGHT * 2}px; + background: red; + display: inline-block; + } + `; + + const HORIZONTAL_FIXTURE_TEMPLATE = ` +
+ @for (item of items; track item) { +
{{item.value}}
+ } +
+ `; + + @Component({ + encapsulation: ViewEncapsulation.None, + styles: HORIZONTAL_FIXTURE_STYLES, + template: HORIZONTAL_FIXTURE_TEMPLATE, + }) + class DraggableInHorizontalDropZone implements AfterViewInit { + @ViewChildren(CdkDrag) dragItems: QueryList; + @ViewChild(CdkDropList) dropInstance: CdkDropList; + items = [ + {value: 'Zero', width: ITEM_WIDTH, margin: 0}, + {value: 'One', width: ITEM_WIDTH, margin: 0}, + {value: 'Two', width: ITEM_WIDTH, margin: 0}, + {value: 'Three', width: ITEM_WIDTH, margin: 0}, + ]; + boundarySelector: string; + droppedSpy = jasmine.createSpy('dropped spy').and.callFake((event: CdkDragDrop) => { + moveItemInArray(this.items, event.previousIndex, event.currentIndex); + }); + + constructor(protected _elementRef: ElementRef) {} + + ngAfterViewInit() { + // Firefox preserves the `scrollLeft` value from previous similar containers. This + // could throw off test assertions and result in flaky results. + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=959812. + this._elementRef.nativeElement.querySelector('.scroll-container').scrollLeft = 0; + } + } + + @Component({ + template: HORIZONTAL_FIXTURE_TEMPLATE, + + // Note that it needs a margin to ensure that it's not flush against the viewport + // edge which will cause the viewport to scroll, rather than the list. + styles: [ + HORIZONTAL_FIXTURE_STYLES, + ` + .drop-list { + max-width: 300px; + margin: 10vw 0 0 10vw; + overflow: auto; + white-space: nowrap; + } + `, + ], + }) + class DraggableInScrollableHorizontalDropZone extends DraggableInHorizontalDropZone { + constructor(elementRef: ElementRef) { + super(elementRef); + + for (let i = 0; i < 60; i++) { + this.items.push({value: `Extra item ${i}`, width: ITEM_WIDTH, margin: 0}); + } + } + } + + @Component({ + styles: ` + .list { + display: flex; + width: 100px; + flex-direction: row; + } + + .item { + display: flex; + flex-grow: 1; + flex-basis: 0; + min-height: 50px; + } + `, + template: ` +
+ @for (item of items; track item) { +
+ {{item}} + +
{{item}}
+
+
+ } +
+ `, + }) + class DraggableInHorizontalFlexDropZoneWithMatchSizePreview { + @ViewChildren(CdkDrag) dragItems: QueryList; + items = ['Zero', 'One', 'Two']; + } +} // TODO(crisbeto): figure out why switch `*ngFor` with `@for` here causes a test failure. const DROP_ZONE_FIXTURE_TEMPLATE = ` @@ -5182,95 +5329,6 @@ class DraggableInScrollableParentContainer extends DraggableInDropZone implement }) class DraggableInDropZoneWithContainer extends DraggableInDropZone {} -// Use inline blocks here to avoid flexbox issues and not to have to flip floats in rtl. -const HORIZONTAL_FIXTURE_STYLES = ` - .cdk-drop-list { - display: block; - width: 500px; - background: pink; - font-size: 0; - } - - .cdk-drag { - height: ${ITEM_HEIGHT * 2}px; - background: red; - display: inline-block; - } -`; - -const HORIZONTAL_FIXTURE_TEMPLATE = ` -
- @for (item of items; track item) { -
{{item.value}}
- } -
-`; - -@Component({ - encapsulation: ViewEncapsulation.None, - styles: HORIZONTAL_FIXTURE_STYLES, - template: HORIZONTAL_FIXTURE_TEMPLATE, -}) -class DraggableInHorizontalDropZone implements AfterViewInit { - @ViewChildren(CdkDrag) dragItems: QueryList; - @ViewChild(CdkDropList) dropInstance: CdkDropList; - items = [ - {value: 'Zero', width: ITEM_WIDTH, margin: 0}, - {value: 'One', width: ITEM_WIDTH, margin: 0}, - {value: 'Two', width: ITEM_WIDTH, margin: 0}, - {value: 'Three', width: ITEM_WIDTH, margin: 0}, - ]; - boundarySelector: string; - droppedSpy = jasmine.createSpy('dropped spy').and.callFake((event: CdkDragDrop) => { - moveItemInArray(this.items, event.previousIndex, event.currentIndex); - }); - - constructor(protected _elementRef: ElementRef) {} - - ngAfterViewInit() { - // Firefox preserves the `scrollLeft` value from previous similar containers. This - // could throw off test assertions and result in flaky results. - // See: https://bugzilla.mozilla.org/show_bug.cgi?id=959812. - this._elementRef.nativeElement.querySelector('.scroll-container').scrollLeft = 0; - } -} - -@Component({ - template: HORIZONTAL_FIXTURE_TEMPLATE, - - // Note that it needs a margin to ensure that it's not flush against the viewport - // edge which will cause the viewport to scroll, rather than the list. - styles: [ - HORIZONTAL_FIXTURE_STYLES, - ` - .drop-list { - max-width: 300px; - margin: 10vw 0 0 10vw; - overflow: auto; - white-space: nowrap; - } - `, - ], -}) -class DraggableInScrollableHorizontalDropZone extends DraggableInHorizontalDropZone { - constructor(elementRef: ElementRef) { - super(elementRef); - - for (let i = 0; i < 60; i++) { - this.items.push({value: `Extra item ${i}`, width: ITEM_WIDTH, margin: 0}); - } - } -} - // TODO(crisbeto): `*ngIf` here can be removed after updating to a version of Angular that includes // https://github.com/angular/angular/pull/52515 @Component({ @@ -5904,40 +5962,6 @@ class NestedDropZones { class PlainStandaloneDropList { @ViewChild(CdkDropList) dropList: CdkDropList; } - -@Component({ - styles: ` - .list { - display: flex; - width: 100px; - flex-direction: row; - } - - .item { - display: flex; - flex-grow: 1; - flex-basis: 0; - min-height: 50px; - } - `, - template: ` -
- @for (item of items; track item) { -
- {{item}} - -
{{item}}
-
-
- } -
- `, -}) -class DraggableInHorizontalFlexDropZoneWithMatchSizePreview { - @ViewChildren(CdkDrag) dragItems: QueryList; - items = ['Zero', 'One', 'Two']; -} - @Component({ styles: CONNECTED_DROP_ZONES_STYLES, template: ` diff --git a/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts b/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts new file mode 100644 index 000000000000..05f338fd16ef --- /dev/null +++ b/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts @@ -0,0 +1,79 @@ +import {ComponentFixture, flush} from '@angular/core/testing'; +import {dispatchMouseEvent} from '@angular/cdk/testing/private'; +import {_supportsShadowDom} from '@angular/cdk/platform'; + +import {startDraggingViaMouse} from './test-utils.spec'; +import {defineCommonDropListTests} from './drop-list-shared.spec'; + +describe('Single-axis drop list', () => { + defineCommonDropListTests({ + verticalListOrientation: 'vertical', + horizontalListOrientation: 'horizontal', + getElementIndexByPosition, + getElementSibligsByPosition, + assertUpwardSorting, + assertDownwardSorting, + }); + + function getElementIndexByPosition(element: Element, direction: 'top' | 'left') { + return getElementSibligsByPosition(element, direction).indexOf(element); + } + + function getElementSibligsByPosition(element: Element, direction: 'top' | 'left') { + return element.parentElement + ? Array.from(element.parentElement.children).sort((a, b) => { + return a.getBoundingClientRect()[direction] - b.getBoundingClientRect()[direction]; + }) + : []; + } + + function assertDownwardSorting(fixture: ComponentFixture, items: Element[]) { + const draggedItem = items[0]; + const {top, left} = draggedItem.getBoundingClientRect(); + + startDraggingViaMouse(fixture, draggedItem, left, top); + + const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; + + // Drag over each item one-by-one going downwards. + for (let i = 0; i < items.length; i++) { + const elementRect = items[i].getBoundingClientRect(); + + // Add a few pixels to the top offset so we get some overlap. + dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.top + 5); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + expect(getElementIndexByPosition(placeholder, 'top')).toBe(i); + } + + dispatchMouseEvent(document, 'mouseup'); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + flush(); + } + + function assertUpwardSorting(fixture: ComponentFixture, items: Element[]) { + const draggedItem = items[items.length - 1]; + const {top, left} = draggedItem.getBoundingClientRect(); + + startDraggingViaMouse(fixture, draggedItem, left, top); + + const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; + + // Drag over each item one-by-one going upwards. + for (let i = items.length - 1; i > -1; i--) { + const elementRect = items[i].getBoundingClientRect(); + + // Remove a few pixels from the bottom offset so we get some overlap. + dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.bottom - 5); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + expect(getElementIndexByPosition(placeholder, 'top')).toBe(i); + } + + dispatchMouseEvent(document, 'mouseup'); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + flush(); + } +}); diff --git a/src/cdk/drag-drop/directives/test-utils.spec.ts b/src/cdk/drag-drop/directives/test-utils.spec.ts index 16e7eb5426b3..7a490cd22ed4 100644 --- a/src/cdk/drag-drop/directives/test-utils.spec.ts +++ b/src/cdk/drag-drop/directives/test-utils.spec.ts @@ -1,9 +1,18 @@ import {EnvironmentProviders, Provider, Type, ViewEncapsulation} from '@angular/core'; -import {ComponentFixture, TestBed, flush, tick} from '@angular/core/testing'; +import {ComponentFixture, TestBed, tick} from '@angular/core/testing'; import {dispatchMouseEvent, dispatchTouchEvent} from '@angular/cdk/testing/private'; import {CdkScrollableModule} from '@angular/cdk/scrolling'; import {DragDropModule} from '../drag-drop-module'; -import {CDK_DRAG_CONFIG, DragDropConfig} from './config'; +import {CDK_DRAG_CONFIG, DragDropConfig, DropListOrientation} from './config'; + +/** Options that can be used to configure a test. */ +export interface DragDropTestConfig { + providers?: (Provider | EnvironmentProviders)[]; + dragDistance?: number; + extraDeclarations?: Type[]; + encapsulation?: ViewEncapsulation; + listOrientation?: DropListOrientation; +} /** * Creates a component fixture that can be used in a test. @@ -12,25 +21,23 @@ import {CDK_DRAG_CONFIG, DragDropConfig} from './config'; */ export function createComponent( componentType: Type, - config: { - providers?: (Provider | EnvironmentProviders)[]; - dragDistance?: number; - extraDeclarations?: Type[]; - encapsulation?: ViewEncapsulation; - } = {}, + config: DragDropTestConfig = {}, ): ComponentFixture { + const dragConfig: DragDropConfig = { + // We default the `dragDistance` to zero, because the majority of the tests + // don't care about it and drags are a lot easier to simulate when we don't + // have to deal with thresholds. + dragStartThreshold: config?.dragDistance ?? 0, + pointerDirectionChangeThreshold: 5, + listOrientation: config.listOrientation, + }; + TestBed.configureTestingModule({ imports: [DragDropModule, CdkScrollableModule], providers: [ { provide: CDK_DRAG_CONFIG, - useValue: { - // We default the `dragDistance` to zero, because the majority of the tests - // don't care about it and drags are a lot easier to simulate when we don't - // have to deal with thresholds. - dragStartThreshold: config?.dragDistance ?? 0, - pointerDirectionChangeThreshold: 5, - } as DragDropConfig, + useValue: dragConfig, }, ...(config.providers || []), ], @@ -139,20 +146,6 @@ export function stopDraggingViaTouch(fixture: ComponentFixture, x: number, fixture.detectChanges(); } -/** Gets the index of an element among its siblings, based on their position on the page. */ -export function getElementIndexByPosition(element: Element, direction: 'top' | 'left') { - return getElementSibligsByPosition(element, direction).indexOf(element); -} - -/** Gets the siblings of an element, sorted by their position on the page. */ -export function getElementSibligsByPosition(element: Element, direction: 'top' | 'left') { - return element.parentElement - ? Array.from(element.parentElement.children).sort((a, b) => { - return a.getBoundingClientRect()[direction] - b.getBoundingClientRect()[direction]; - }) - : []; -} - /** * Adds a large element to the page in order to make it scrollable. * @returns Function that should be used to clean up after the test is done. @@ -172,62 +165,6 @@ export function makeScrollable( }; } -/** - * Asserts that sorting an element down works correctly. - * @param fixture Fixture against which to run the assertions. - * @param items Array of items against which to test sorting. - */ -export function assertDownwardSorting(fixture: ComponentFixture, items: Element[]) { - const draggedItem = items[0]; - const {top, left} = draggedItem.getBoundingClientRect(); - - startDraggingViaMouse(fixture, draggedItem, left, top); - - const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - - // Drag over each item one-by-one going downwards. - for (let i = 0; i < items.length; i++) { - const elementRect = items[i].getBoundingClientRect(); - - // Add a few pixels to the top offset so we get some overlap. - dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.top + 5); - fixture.detectChanges(); - expect(getElementIndexByPosition(placeholder, 'top')).toBe(i); - } - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - flush(); -} - -/** - * Asserts that sorting an element up works correctly. - * @param fixture Fixture against which to run the assertions. - * @param items Array of items against which to test sorting. - */ -export function assertUpwardSorting(fixture: ComponentFixture, items: Element[]) { - const draggedItem = items[items.length - 1]; - const {top, left} = draggedItem.getBoundingClientRect(); - - startDraggingViaMouse(fixture, draggedItem, left, top); - - const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - - // Drag over each item one-by-one going upwards. - for (let i = items.length - 1; i > -1; i--) { - const elementRect = items[i].getBoundingClientRect(); - - // Remove a few pixels from the bottom offset so we get some overlap. - dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.bottom - 5); - fixture.detectChanges(); - expect(getElementIndexByPosition(placeholder, 'top')).toBe(i); - } - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - flush(); -} - /** Ticks the specified amount of `requestAnimationFrame`-s. */ export function tickAnimationFrames(amount: number) { tick(16.6 * amount); // Angular turns rAF calls into 16.6ms timeouts in tests. From 664ab79e09e39360a0120683a4a186a5b1d5b314 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 30 May 2024 13:12:17 +0200 Subject: [PATCH 09/61] test(cdk/drag-drop): move transform layout tests out of the common ones Moves the tests that use `transform` to lay out the list out of the common ones since they're specific to the `SingleAxisSortStrategy`. --- .../directives/drop-list-shared.spec.ts | 373 ++++-------------- .../directives/single-axis-drop-list.spec.ts | 338 ++++++++++++++-- .../drag-drop/directives/test-utils.spec.ts | 17 +- 3 files changed, 380 insertions(+), 348 deletions(-) diff --git a/src/cdk/drag-drop/directives/drop-list-shared.spec.ts b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts index b82c9ce56195..8b009fe6cda8 100644 --- a/src/cdk/drag-drop/directives/drop-list-shared.spec.ts +++ b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts @@ -47,16 +47,17 @@ import { stopDraggingViaTouch, tickAnimationFrames, } from './test-utils.spec'; +import {NgFor} from '@angular/common'; -const ITEM_HEIGHT = 25; -const ITEM_WIDTH = 75; +export const ITEM_HEIGHT = 25; +export const ITEM_WIDTH = 75; export function defineCommonDropListTests(config: { /** Orientation value that will be passed to tests checking vertical orientation. */ - verticalListOrientation: DropListOrientation; + verticalListOrientation: Exclude; /** Orientation value that will be passed to tests checking horizontal orientation. */ - horizontalListOrientation: DropListOrientation; + horizontalListOrientation: Exclude; /** Asserts that sorting an element up works correctly. */ assertUpwardSorting: (fixture: ComponentFixture, items: Element[]) => void; @@ -70,6 +71,12 @@ export function defineCommonDropListTests(config: { /** Gets the siblings of an element, sorted by their visible position. */ getElementSibligsByPosition: (element: Element, direction: 'top' | 'left') => Element[]; }) { + const { + DraggableInHorizontalDropZone, + DraggableInScrollableHorizontalDropZone, + DraggableInHorizontalFlexDropZoneWithMatchSizePreview, + } = getHorizontalFixtures(config.horizontalListOrientation); + function createComponent( type: Type, testConfig: DragDropTestConfig = {}, @@ -1623,201 +1630,26 @@ export function defineCommonDropListTests(config: { }), ); - it('should lay out the elements correctly, when swapping down with a taller element', fakeAsync(() => { - const fixture = createComponent(DraggableInDropZone); - fixture.detectChanges(); - - const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); - const {top, left} = items[0].getBoundingClientRect(); - - fixture.componentInstance.items[0].height = ITEM_HEIGHT * 2; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - - startDraggingViaMouse(fixture, items[0], left, top); - - const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - const target = items[1]; - const targetRect = target.getBoundingClientRect(); - - // Add a few pixels to the top offset so we get some overlap. - dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top + 5); - fixture.detectChanges(); - - expect(placeholder.style.transform).toBe(`translate3d(0px, ${ITEM_HEIGHT}px, 0px)`); - expect(target.style.transform).toBe(`translate3d(0px, ${-ITEM_HEIGHT * 2}px, 0px)`); - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - flush(); - })); - - it('should lay out the elements correctly, when swapping up with a taller element', fakeAsync(() => { - const fixture = createComponent(DraggableInDropZone); - fixture.detectChanges(); - - const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); - const {top, left} = items[1].getBoundingClientRect(); - - fixture.componentInstance.items[1].height = ITEM_HEIGHT * 2; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - - startDraggingViaMouse(fixture, items[1], left, top); - - const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - const target = items[0]; - const targetRect = target.getBoundingClientRect(); - - // Add a few pixels to the top offset so we get some overlap. - dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom - 5); - fixture.detectChanges(); - - expect(placeholder.style.transform).toBe(`translate3d(0px, ${-ITEM_HEIGHT}px, 0px)`); - expect(target.style.transform).toBe(`translate3d(0px, ${ITEM_HEIGHT * 2}px, 0px)`); - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - flush(); - })); - - it('should lay out elements correctly, when swapping an item with margin', fakeAsync(() => { - const fixture = createComponent(DraggableInDropZone); - fixture.detectChanges(); - - const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); - const {top, left} = items[0].getBoundingClientRect(); - - fixture.componentInstance.items[0].margin = 12; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - - startDraggingViaMouse(fixture, items[0], left, top); - - const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - const target = items[1]; - const targetRect = target.getBoundingClientRect(); - - // Add a few pixels to the top offset so we get some overlap. - dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top + 5); - fixture.detectChanges(); - - expect(placeholder.style.transform).toBe(`translate3d(0px, ${ITEM_HEIGHT + 12}px, 0px)`); - expect(target.style.transform).toBe(`translate3d(0px, ${-ITEM_HEIGHT - 12}px, 0px)`); - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - flush(); - })); - - it( - 'should lay out the elements correctly, if an element skips multiple positions when ' + - 'sorting horizontally', - fakeAsync(() => { - const fixture = createComponent(DraggableInHorizontalDropZone); - fixture.detectChanges(); - - const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); - const draggedItem = items[0]; - const {top, left} = draggedItem.getBoundingClientRect(); - - startDraggingViaMouse(fixture, draggedItem, left, top); - - const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - const targetRect = items[items.length - 1].getBoundingClientRect(); - - // Add a few pixels to the left offset so we get some overlap. - dispatchMouseEvent(document, 'mousemove', targetRect.right - 5, targetRect.top); - fixture.detectChanges(); - - expect( - config.getElementSibligsByPosition(placeholder, 'left').map(e => e.textContent!.trim()), - ).toEqual(['One', 'Two', 'Three', 'Zero']); - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - flush(); - }), - ); - - it('should lay out the elements correctly, when swapping to the right with a wider element', fakeAsync(() => { - const fixture = createComponent(DraggableInHorizontalDropZone); - fixture.detectChanges(); - - const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); - - fixture.componentInstance.items[0].width = ITEM_WIDTH * 2; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - - const {top, left} = items[0].getBoundingClientRect(); - startDraggingViaMouse(fixture, items[0], left, top); - - const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - const target = items[1]; - const targetRect = target.getBoundingClientRect(); - - dispatchMouseEvent(document, 'mousemove', targetRect.right - 5, targetRect.top); - fixture.detectChanges(); - - expect(placeholder.style.transform).toBe(`translate3d(${ITEM_WIDTH}px, 0px, 0px)`); - expect(target.style.transform).toBe(`translate3d(${-ITEM_WIDTH * 2}px, 0px, 0px)`); - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - flush(); - })); - - it('should lay out the elements correctly, when swapping left with a wider element', fakeAsync(() => { + it('should lay out the elements correctly, if an element skips multiple positions when sorting horizontally', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalDropZone); fixture.detectChanges(); const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); - const {top, left} = items[1].getBoundingClientRect(); - - fixture.componentInstance.items[1].width = ITEM_WIDTH * 2; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - - startDraggingViaMouse(fixture, items[1], left, top); - - const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - const target = items[0]; - const targetRect = target.getBoundingClientRect(); - - dispatchMouseEvent(document, 'mousemove', targetRect.right - 5, targetRect.top); - fixture.detectChanges(); - - expect(placeholder.style.transform).toBe(`translate3d(${-ITEM_WIDTH}px, 0px, 0px)`); - expect(target.style.transform).toBe(`translate3d(${ITEM_WIDTH * 2}px, 0px, 0px)`); - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - flush(); - })); - - it('should lay out elements correctly, when horizontally swapping an item with margin', fakeAsync(() => { - const fixture = createComponent(DraggableInHorizontalDropZone); - fixture.detectChanges(); - - const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); - const {top, left} = items[0].getBoundingClientRect(); - - fixture.componentInstance.items[0].margin = 12; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + const draggedItem = items[0]; + const {top, left} = draggedItem.getBoundingClientRect(); - startDraggingViaMouse(fixture, items[0], left, top); + startDraggingViaMouse(fixture, draggedItem, left, top); const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - const target = items[1]; - const targetRect = target.getBoundingClientRect(); + const targetRect = items[items.length - 1].getBoundingClientRect(); + // Add a few pixels to the left offset so we get some overlap. dispatchMouseEvent(document, 'mousemove', targetRect.right - 5, targetRect.top); fixture.detectChanges(); - expect(placeholder.style.transform).toBe(`translate3d(${ITEM_WIDTH + 12}px, 0px, 0px)`); - expect(target.style.transform).toBe(`translate3d(${-ITEM_WIDTH - 12}px, 0px, 0px)`); + expect( + config.getElementSibligsByPosition(placeholder, 'left').map(e => e.textContent!.trim()), + ).toEqual(['One', 'Two', 'Three', 'Zero']); dispatchMouseEvent(document, 'mouseup'); fixture.detectChanges(); @@ -2065,30 +1897,26 @@ export function defineCommonDropListTests(config: { expect(Math.floor(previewRect.left)).toBe(50); })); - it( - 'should revert the element back to its parent after dragging with a custom ' + - 'preview has stopped', - fakeAsync(() => { - const fixture = createComponent(DraggableInDropZoneWithCustomPreview); - fixture.detectChanges(); + it('should revert the element back to its parent after dragging with a custom preview has stopped', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZoneWithCustomPreview); + fixture.detectChanges(); - const dragContainer = fixture.componentInstance.dropInstance.element.nativeElement; - const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; + const dragContainer = fixture.componentInstance.dropInstance.element.nativeElement; + const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; - expect(dragContainer.contains(item)) - .withContext('Expected item to be in container.') - .toBe(true); + expect(dragContainer.contains(item)) + .withContext('Expected item to be in container.') + .toBe(true); - // The coordinates don't matter. - dragElementViaMouse(fixture, item, 10, 10); - flush(); - fixture.detectChanges(); + // The coordinates don't matter. + dragElementViaMouse(fixture, item, 10, 10); + flush(); + fixture.detectChanges(); - expect(dragContainer.contains(item)) - .withContext('Expected item to be returned to container.') - .toBe(true); - }), - ); + expect(dragContainer.contains(item)) + .withContext('Expected item to be returned to container.') + .toBe(true); + })); it('should position custom previews next to the pointer', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPreview); @@ -2213,22 +2041,18 @@ export function defineCommonDropListTests(config: { expect(preview.style.height).toBe(`${itemRect.height}px`); })); - it( - 'should preserve the pickup position if the custom preview inherits the size of the ' + - 'dragged element', - fakeAsync(() => { - const fixture = createComponent(DraggableInDropZoneWithCustomPreview); - fixture.componentInstance.matchPreviewSize = true; - fixture.detectChanges(); - const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; + it('should preserve the pickup position if the custom preview inherits the size of the dragged element', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZoneWithCustomPreview); + fixture.componentInstance.matchPreviewSize = true; + fixture.detectChanges(); + const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; - startDraggingViaMouse(fixture, item, 50, 50); + startDraggingViaMouse(fixture, item, 50, 50); - const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; + const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; - expect(preview.style.transform).toBe('translate3d(8px, 33px, 0px)'); - }), - ); + expect(preview.style.transform).toBe('translate3d(8px, 33px, 0px)'); + })); it('should not have the size of the inserted preview affect the size applied via matchSize', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalFlexDropZoneWithMatchSizePreview); @@ -2364,30 +2188,6 @@ export function defineCommonDropListTests(config: { expect(placeholder.textContent!.trim()).toContain('HelloOne'); })); - it('should clear the `transform` value from siblings when item is dropped`', fakeAsync(() => { - const fixture = createComponent(DraggableInDropZone); - fixture.detectChanges(); - - const dragItems = fixture.componentInstance.dragItems; - const firstItem = dragItems.first; - const thirdItem = dragItems.toArray()[2].element.nativeElement; - const thirdItemRect = thirdItem.getBoundingClientRect(); - - startDraggingViaMouse(fixture, firstItem.element.nativeElement); - - dispatchMouseEvent(document, 'mousemove', thirdItemRect.left + 1, thirdItemRect.top + 1); - fixture.detectChanges(); - - expect(thirdItem.style.transform).toBeTruthy(); - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - - expect(thirdItem.style.transform).toBeFalsy(); - })); - it('should not move the item if the list is disabled', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.detectChanges(); @@ -3360,57 +3160,6 @@ export function defineCommonDropListTests(config: { }).toThrowError(/^cdkDropList must be attached to an element node/); })); - it('should preserve the original `transform` of items in the list', fakeAsync(() => { - const fixture = createComponent(DraggableInScrollableVerticalDropZone); - fixture.detectChanges(); - const items = fixture.componentInstance.dragItems.map(item => item.element.nativeElement); - items.forEach(element => (element.style.transform = 'rotate(180deg)')); - const thirdItemRect = items[2].getBoundingClientRect(); - const hasInitialTransform = (element: HTMLElement) => - element.style.transform.indexOf('rotate(180deg)') > -1; - - startDraggingViaMouse(fixture, items[0]); - fixture.detectChanges(); - const preview = document.querySelector('.cdk-drag-preview') as HTMLElement; - const placeholder = fixture.nativeElement.querySelector('.cdk-drag-placeholder'); - - expect(items.every(hasInitialTransform)) - .withContext('Expected items to preserve transform when dragging starts.') - .toBe(true); - expect(hasInitialTransform(preview)) - .withContext('Expected preview to preserve transform when dragging starts.') - .toBe(true); - expect(hasInitialTransform(placeholder)) - .withContext('Expected placeholder to preserve transform when dragging starts.') - .toBe(true); - - dispatchMouseEvent(document, 'mousemove', thirdItemRect.left + 1, thirdItemRect.top + 1); - fixture.detectChanges(); - expect(items.every(hasInitialTransform)) - .withContext('Expected items to preserve transform while dragging.') - .toBe(true); - expect(hasInitialTransform(preview)) - .withContext('Expected preview to preserve transform while dragging.') - .toBe(true); - expect(hasInitialTransform(placeholder)) - .withContext('Expected placeholder to preserve transform while dragging.') - .toBe(true); - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(items.every(hasInitialTransform)) - .withContext('Expected items to preserve transform when dragging stops.') - .toBe(true); - expect(hasInitialTransform(preview)) - .withContext('Expected preview to preserve transform when dragging stops.') - .toBe(true); - expect(hasInitialTransform(placeholder)) - .withContext('Expected placeholder to preserve transform when dragging stops.') - .toBe(true); - })); - it('should sort correctly if the node has been offset', fakeAsync(() => { const documentElement = document.documentElement!; const fixture = createComponent(DraggableInDropZone); @@ -5037,9 +4786,15 @@ export function defineCommonDropListTests(config: { expect(event.stopPropagation).toHaveBeenCalled(); })); }); +} - // Horizontal fixtures need to be defined here, because the - // `horizontalListOrientation` value can change between tests. +/** + * Dynamically creates the horizontal list fixtures. They need to be + * generated so that the list orientation can be changed between tests. + * @param listOrientation Orientation value to be assigned to the list. + * Does not affect the actual styles. + */ +export function getHorizontalFixtures(listOrientation: Exclude) { // Use inline blocks here to avoid flexbox issues and not to have to flip floats in rtl. const HORIZONTAL_FIXTURE_STYLES = ` .cdk-drop-list { @@ -5060,7 +4815,7 @@ export function defineCommonDropListTests(config: {
@for (item of items; track item) { @@ -5092,7 +4847,7 @@ export function defineCommonDropListTests(config: { moveItemInArray(this.items, event.previousIndex, event.currentIndex); }); - constructor(protected _elementRef: ElementRef) {} + constructor(readonly _elementRef: ElementRef) {} ngAfterViewInit() { // Firefox preserves the `scrollLeft` value from previous similar containers. This @@ -5145,7 +4900,7 @@ export function defineCommonDropListTests(config: { } `, template: ` -
+
@for (item of items; track item) {
{{item}} @@ -5161,6 +4916,12 @@ export function defineCommonDropListTests(config: { @ViewChildren(CdkDrag) dragItems: QueryList; items = ['Zero', 'One', 'Two']; } + + return { + DraggableInHorizontalDropZone, + DraggableInScrollableHorizontalDropZone, + DraggableInHorizontalFlexDropZoneWithMatchSizePreview, + }; } // TODO(crisbeto): figure out why switch `*ngFor` with `@for` here causes a test failure. @@ -5191,8 +4952,12 @@ const DROP_ZONE_FIXTURE_TEMPLATE = `
`; -@Component({template: DROP_ZONE_FIXTURE_TEMPLATE}) -class DraggableInDropZone implements AfterViewInit { +@Component({ + template: DROP_ZONE_FIXTURE_TEMPLATE, + standalone: true, + imports: [CdkDropList, CdkDrag, NgFor], +}) +export class DraggableInDropZone implements AfterViewInit { @ViewChildren(CdkDrag) dragItems: QueryList; @ViewChild(CdkDropList) dropInstance: CdkDropList; @ViewChild('alternatePreviewContainer') alternatePreviewContainer: ElementRef; @@ -5254,7 +5019,7 @@ class ConnectedDropListsInOnPush {} } `, }) -class DraggableInScrollableVerticalDropZone extends DraggableInDropZone { +export class DraggableInScrollableVerticalDropZone extends DraggableInDropZone { constructor(elementRef: ElementRef) { super(elementRef); diff --git a/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts b/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts index 05f338fd16ef..ecaf013d33c5 100644 --- a/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts +++ b/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts @@ -1,11 +1,19 @@ -import {ComponentFixture, flush} from '@angular/core/testing'; +import {ComponentFixture, fakeAsync, flush} from '@angular/core/testing'; import {dispatchMouseEvent} from '@angular/cdk/testing/private'; import {_supportsShadowDom} from '@angular/cdk/platform'; - -import {startDraggingViaMouse} from './test-utils.spec'; -import {defineCommonDropListTests} from './drop-list-shared.spec'; +import {createComponent, startDraggingViaMouse} from './test-utils.spec'; +import { + DraggableInDropZone, + DraggableInScrollableVerticalDropZone, + ITEM_HEIGHT, + ITEM_WIDTH, + defineCommonDropListTests, + getHorizontalFixtures, +} from './drop-list-shared.spec'; describe('Single-axis drop list', () => { + const {DraggableInHorizontalDropZone} = getHorizontalFixtures('horizontal'); + defineCommonDropListTests({ verticalListOrientation: 'vertical', horizontalListOrientation: 'horizontal', @@ -15,65 +23,311 @@ describe('Single-axis drop list', () => { assertDownwardSorting, }); - function getElementIndexByPosition(element: Element, direction: 'top' | 'left') { - return getElementSibligsByPosition(element, direction).indexOf(element); - } + it('should lay out the elements correctly, when swapping down with a taller element', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); - function getElementSibligsByPosition(element: Element, direction: 'top' | 'left') { - return element.parentElement - ? Array.from(element.parentElement.children).sort((a, b) => { - return a.getBoundingClientRect()[direction] - b.getBoundingClientRect()[direction]; - }) - : []; - } + const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); + const {top, left} = items[0].getBoundingClientRect(); - function assertDownwardSorting(fixture: ComponentFixture, items: Element[]) { - const draggedItem = items[0]; - const {top, left} = draggedItem.getBoundingClientRect(); + fixture.componentInstance.items[0].height = ITEM_HEIGHT * 2; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); - startDraggingViaMouse(fixture, draggedItem, left, top); + startDraggingViaMouse(fixture, items[0], left, top); const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; + const target = items[1]; + const targetRect = target.getBoundingClientRect(); - // Drag over each item one-by-one going downwards. - for (let i = 0; i < items.length; i++) { - const elementRect = items[i].getBoundingClientRect(); + // Add a few pixels to the top offset so we get some overlap. + dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top + 5); + fixture.detectChanges(); - // Add a few pixels to the top offset so we get some overlap. - dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.top + 5); - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - expect(getElementIndexByPosition(placeholder, 'top')).toBe(i); - } + expect(placeholder.style.transform).toBe(`translate3d(0px, ${ITEM_HEIGHT}px, 0px)`); + expect(target.style.transform).toBe(`translate3d(0px, ${-ITEM_HEIGHT * 2}px, 0px)`); dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + })); + + it('should lay out the elements correctly, when swapping up with a taller element', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); + + const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); + const {top, left} = items[1].getBoundingClientRect(); + + fixture.componentInstance.items[1].height = ITEM_HEIGHT * 2; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); + + startDraggingViaMouse(fixture, items[1], left, top); + + const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; + const target = items[0]; + const targetRect = target.getBoundingClientRect(); + + // Add a few pixels to the top offset so we get some overlap. + dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom - 5); + fixture.detectChanges(); + + expect(placeholder.style.transform).toBe(`translate3d(0px, ${-ITEM_HEIGHT}px, 0px)`); + expect(target.style.transform).toBe(`translate3d(0px, ${ITEM_HEIGHT * 2}px, 0px)`); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); flush(); - } + })); + + it('should lay out elements correctly, when swapping an item with margin', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); + + const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); + const {top, left} = items[0].getBoundingClientRect(); + + fixture.componentInstance.items[0].margin = 12; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + startDraggingViaMouse(fixture, items[0], left, top); - function assertUpwardSorting(fixture: ComponentFixture, items: Element[]) { - const draggedItem = items[items.length - 1]; - const {top, left} = draggedItem.getBoundingClientRect(); + const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; + const target = items[1]; + const targetRect = target.getBoundingClientRect(); + + // Add a few pixels to the top offset so we get some overlap. + dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top + 5); + fixture.detectChanges(); + + expect(placeholder.style.transform).toBe(`translate3d(0px, ${ITEM_HEIGHT + 12}px, 0px)`); + expect(target.style.transform).toBe(`translate3d(0px, ${-ITEM_HEIGHT - 12}px, 0px)`); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + })); - startDraggingViaMouse(fixture, draggedItem, left, top); + it('should lay out the elements correctly, when swapping to the right with a wider element', fakeAsync(() => { + const fixture = createComponent(DraggableInHorizontalDropZone); + fixture.detectChanges(); + + const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); + + fixture.componentInstance.items[0].width = ITEM_WIDTH * 2; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + const {top, left} = items[0].getBoundingClientRect(); + startDraggingViaMouse(fixture, items[0], left, top); const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; + const target = items[1]; + const targetRect = target.getBoundingClientRect(); - // Drag over each item one-by-one going upwards. - for (let i = items.length - 1; i > -1; i--) { - const elementRect = items[i].getBoundingClientRect(); + dispatchMouseEvent(document, 'mousemove', targetRect.right - 5, targetRect.top); + fixture.detectChanges(); - // Remove a few pixels from the bottom offset so we get some overlap. - dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.bottom - 5); - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - expect(getElementIndexByPosition(placeholder, 'top')).toBe(i); - } + expect(placeholder.style.transform).toBe(`translate3d(${ITEM_WIDTH}px, 0px, 0px)`); + expect(target.style.transform).toBe(`translate3d(${-ITEM_WIDTH * 2}px, 0px, 0px)`); dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + })); + + it('should lay out the elements correctly, when swapping left with a wider element', fakeAsync(() => { + const fixture = createComponent(DraggableInHorizontalDropZone); + fixture.detectChanges(); + + const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); + const {top, left} = items[1].getBoundingClientRect(); + + fixture.componentInstance.items[1].width = ITEM_WIDTH * 2; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); + + startDraggingViaMouse(fixture, items[1], left, top); + + const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; + const target = items[0]; + const targetRect = target.getBoundingClientRect(); + + dispatchMouseEvent(document, 'mousemove', targetRect.right - 5, targetRect.top); + fixture.detectChanges(); + + expect(placeholder.style.transform).toBe(`translate3d(${-ITEM_WIDTH}px, 0px, 0px)`); + expect(target.style.transform).toBe(`translate3d(${ITEM_WIDTH * 2}px, 0px, 0px)`); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); flush(); - } + })); + + it('should clear the `transform` value from siblings when item is dropped', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); + + const dragItems = fixture.componentInstance.dragItems; + const firstItem = dragItems.first; + const thirdItem = dragItems.toArray()[2].element.nativeElement; + const thirdItemRect = thirdItem.getBoundingClientRect(); + + startDraggingViaMouse(fixture, firstItem.element.nativeElement); + + dispatchMouseEvent(document, 'mousemove', thirdItemRect.left + 1, thirdItemRect.top + 1); + fixture.detectChanges(); + + expect(thirdItem.style.transform).toBeTruthy(); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + + expect(thirdItem.style.transform).toBeFalsy(); + })); + + it('should lay out elements correctly, when horizontally swapping an item with margin', fakeAsync(() => { + const fixture = createComponent(DraggableInHorizontalDropZone); + fixture.detectChanges(); + + const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); + const {top, left} = items[0].getBoundingClientRect(); + + fixture.componentInstance.items[0].margin = 12; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + startDraggingViaMouse(fixture, items[0], left, top); + + const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; + const target = items[1]; + const targetRect = target.getBoundingClientRect(); + + dispatchMouseEvent(document, 'mousemove', targetRect.right - 5, targetRect.top); + fixture.detectChanges(); + + expect(placeholder.style.transform).toBe(`translate3d(${ITEM_WIDTH + 12}px, 0px, 0px)`); + expect(target.style.transform).toBe(`translate3d(${-ITEM_WIDTH - 12}px, 0px, 0px)`); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + })); + + it('should preserve the original `transform` of items in the list', fakeAsync(() => { + const fixture = createComponent(DraggableInScrollableVerticalDropZone); + fixture.detectChanges(); + const items = fixture.componentInstance.dragItems.map(item => item.element.nativeElement); + items.forEach(element => (element.style.transform = 'rotate(180deg)')); + const thirdItemRect = items[2].getBoundingClientRect(); + const hasInitialTransform = (element: HTMLElement) => + element.style.transform.indexOf('rotate(180deg)') > -1; + + startDraggingViaMouse(fixture, items[0]); + fixture.detectChanges(); + const preview = document.querySelector('.cdk-drag-preview') as HTMLElement; + const placeholder = fixture.nativeElement.querySelector('.cdk-drag-placeholder'); + + expect(items.every(hasInitialTransform)) + .withContext('Expected items to preserve transform when dragging starts.') + .toBe(true); + expect(hasInitialTransform(preview)) + .withContext('Expected preview to preserve transform when dragging starts.') + .toBe(true); + expect(hasInitialTransform(placeholder)) + .withContext('Expected placeholder to preserve transform when dragging starts.') + .toBe(true); + + dispatchMouseEvent(document, 'mousemove', thirdItemRect.left + 1, thirdItemRect.top + 1); + fixture.detectChanges(); + expect(items.every(hasInitialTransform)) + .withContext('Expected items to preserve transform while dragging.') + .toBe(true); + expect(hasInitialTransform(preview)) + .withContext('Expected preview to preserve transform while dragging.') + .toBe(true); + expect(hasInitialTransform(placeholder)) + .withContext('Expected placeholder to preserve transform while dragging.') + .toBe(true); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + expect(items.every(hasInitialTransform)) + .withContext('Expected items to preserve transform when dragging stops.') + .toBe(true); + expect(hasInitialTransform(preview)) + .withContext('Expected preview to preserve transform when dragging stops.') + .toBe(true); + expect(hasInitialTransform(placeholder)) + .withContext('Expected placeholder to preserve transform when dragging stops.') + .toBe(true); + })); }); + +function getElementIndexByPosition(element: Element, direction: 'top' | 'left') { + return getElementSibligsByPosition(element, direction).indexOf(element); +} + +function getElementSibligsByPosition(element: Element, direction: 'top' | 'left') { + return element.parentElement + ? Array.from(element.parentElement.children).sort((a, b) => { + return a.getBoundingClientRect()[direction] - b.getBoundingClientRect()[direction]; + }) + : []; +} + +function assertDownwardSorting(fixture: ComponentFixture, items: Element[]) { + const draggedItem = items[0]; + const {top, left} = draggedItem.getBoundingClientRect(); + + startDraggingViaMouse(fixture, draggedItem, left, top); + + const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; + + // Drag over each item one-by-one going downwards. + for (let i = 0; i < items.length; i++) { + const elementRect = items[i].getBoundingClientRect(); + + // Add a few pixels to the top offset so we get some overlap. + dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.top + 5); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + expect(getElementIndexByPosition(placeholder, 'top')).toBe(i); + } + + dispatchMouseEvent(document, 'mouseup'); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + flush(); +} + +function assertUpwardSorting(fixture: ComponentFixture, items: Element[]) { + const draggedItem = items[items.length - 1]; + const {top, left} = draggedItem.getBoundingClientRect(); + + startDraggingViaMouse(fixture, draggedItem, left, top); + + const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; + + // Drag over each item one-by-one going upwards. + for (let i = items.length - 1; i > -1; i--) { + const elementRect = items[i].getBoundingClientRect(); + + // Remove a few pixels from the bottom offset so we get some overlap. + dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.bottom - 5); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + expect(getElementIndexByPosition(placeholder, 'top')).toBe(i); + } + + dispatchMouseEvent(document, 'mouseup'); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + flush(); +} diff --git a/src/cdk/drag-drop/directives/test-utils.spec.ts b/src/cdk/drag-drop/directives/test-utils.spec.ts index 7a490cd22ed4..bcbb564a5b82 100644 --- a/src/cdk/drag-drop/directives/test-utils.spec.ts +++ b/src/cdk/drag-drop/directives/test-utils.spec.ts @@ -1,4 +1,10 @@ -import {EnvironmentProviders, Provider, Type, ViewEncapsulation} from '@angular/core'; +import { + EnvironmentProviders, + Provider, + Type, + ViewEncapsulation, + reflectComponentType, +} from '@angular/core'; import {ComponentFixture, TestBed, tick} from '@angular/core/testing'; import {dispatchMouseEvent, dispatchTouchEvent} from '@angular/cdk/testing/private'; import {CdkScrollableModule} from '@angular/cdk/scrolling'; @@ -23,6 +29,8 @@ export function createComponent( componentType: Type, config: DragDropTestConfig = {}, ): ComponentFixture { + // TODO(crisbeto): drop this logic once all the fixtures are converted to standalone. + const isStandalone = reflectComponentType(componentType)?.isStandalone; const dragConfig: DragDropConfig = { // We default the `dragDistance` to zero, because the majority of the tests // don't care about it and drags are a lot easier to simulate when we don't @@ -31,6 +39,11 @@ export function createComponent( pointerDirectionChangeThreshold: 5, listOrientation: config.listOrientation, }; + const declarations = [...(config.extraDeclarations || [])]; + + if (!isStandalone) { + declarations.push(componentType); + } TestBed.configureTestingModule({ imports: [DragDropModule, CdkScrollableModule], @@ -41,7 +54,7 @@ export function createComponent( }, ...(config.providers || []), ], - declarations: [componentType, ...(config.extraDeclarations || [])], + declarations, }); if (config.encapsulation != null) { From 3b96730f321dd831db5ac49a85ce7c5f72e63fbb Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 30 May 2024 13:42:46 +0200 Subject: [PATCH 10/61] test(cdk/drag-drop): convert tests to standalone Converts the `drag-drop` tests to standalone to avoid some AoT compilation issues on the CI. --- .../directives/drop-list-shared.spec.ts | 114 ++++++++++++++---- .../directives/standalone-drag.spec.ts | 58 ++++++--- .../directives/standalone-drag.zone.spec.ts | 30 ++--- .../drag-drop/directives/test-utils.spec.ts | 21 +--- src/cdk/drag-drop/drag-drop.spec.ts | 8 +- 5 files changed, 150 insertions(+), 81 deletions(-) diff --git a/src/cdk/drag-drop/directives/drop-list-shared.spec.ts b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts index 8b009fe6cda8..10d1808caaa4 100644 --- a/src/cdk/drag-drop/directives/drop-list-shared.spec.ts +++ b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts @@ -1,6 +1,6 @@ import {Directionality} from '@angular/cdk/bidi'; import {_supportsShadowDom} from '@angular/cdk/platform'; -import {ViewportRuler} from '@angular/cdk/scrolling'; +import {CdkScrollable, ViewportRuler} from '@angular/cdk/scrolling'; import { createMouseEvent, createTouchEvent, @@ -47,7 +47,9 @@ import { stopDraggingViaTouch, tickAnimationFrames, } from './test-utils.spec'; -import {NgFor} from '@angular/common'; +import {NgClass, NgFor, NgIf, NgTemplateOutlet} from '@angular/common'; +import {CdkDragPreview} from './drag-preview'; +import {CdkDragPlaceholder} from './drag-placeholder'; export const ITEM_HEIGHT = 25; export const ITEM_WIDTH = 75; @@ -4026,9 +4028,7 @@ export function defineCommonDropListTests(config: { ); it('should set the receiving class when the list is wrapped in an OnPush component', fakeAsync(() => { - const fixture = createComponent(ConnectedDropListsInOnPush, { - extraDeclarations: [DraggableInOnPushDropZone], - }); + const fixture = createComponent(ConnectedDropListsInOnPush); fixture.detectChanges(); const dropZones = Array.from( @@ -4369,9 +4369,7 @@ export function defineCommonDropListTests(config: { 'should toggle a class when dragging an item inside a wrapper component component ' + 'with OnPush change detection', fakeAsync(() => { - const fixture = createComponent(ConnectedWrappedDropZones, { - extraDeclarations: [WrappedDropContainerComponent], - }); + const fixture = createComponent(ConnectedWrappedDropZones); fixture.detectChanges(); const [startZone, targetZone] = fixture.nativeElement.querySelectorAll('.cdk-drop-list'); @@ -4832,6 +4830,8 @@ export function getHorizontalFixtures(listOrientation: Exclude; @@ -4858,6 +4858,8 @@ export function getHorizontalFixtures(listOrientation: Exclude `, + standalone: true, + imports: [CdkDropList, CdkDrag, CdkDragPreview], }) class DraggableInHorizontalFlexDropZoneWithMatchSizePreview { @ViewChildren(CdkDrag) dragItems: QueryList; @@ -4993,6 +4997,8 @@ export class DraggableInDropZone implements AfterViewInit { selector: 'draggable-in-on-push-zone', template: DROP_ZONE_FIXTURE_TEMPLATE, changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CdkDropList, CdkDrag, NgFor], }) class DraggableInOnPushDropZone extends DraggableInDropZone {} @@ -5003,6 +5009,8 @@ class DraggableInOnPushDropZone extends DraggableInDropZone {}
`, + standalone: true, + imports: [CdkDropListGroup, DraggableInOnPushDropZone], }) class ConnectedDropListsInOnPush {} @@ -5018,6 +5026,8 @@ class ConnectedDropListsInOnPush {} margin: 10vw 0 0 10vw; } `, + standalone: true, + imports: [CdkDropList, CdkDrag, NgFor], }) export class DraggableInScrollableVerticalDropZone extends DraggableInDropZone { constructor(elementRef: ElementRef) { @@ -5045,6 +5055,8 @@ export class DraggableInScrollableVerticalDropZone extends DraggableInDropZone { margin: 10vw 0 0 10vw; } `, + standalone: true, + imports: [CdkDropList, CdkDrag, NgFor, CdkScrollable], }) class DraggableInScrollableParentContainer extends DraggableInDropZone implements AfterViewInit { @ViewChild('scrollContainer') scrollContainer: ElementRef; @@ -5091,6 +5103,8 @@ class DraggableInScrollableParentContainer extends DraggableInDropZone implement }
`, + standalone: true, + imports: [CdkDropList, CdkDrag], }) class DraggableInDropZoneWithContainer extends DraggableInDropZone {} @@ -5120,6 +5134,8 @@ class DraggableInDropZoneWithContainer extends DraggableInDropZone {} }
`, + standalone: true, + imports: [CdkDropList, CdkDrag, CdkDragPreview, NgIf], }) class DraggableInDropZoneWithCustomPreview { @ViewChild(CdkDropList) dropInstance: CdkDropList; @@ -5153,6 +5169,8 @@ class DraggableInDropZoneWithCustomPreview { }
`, + standalone: true, + imports: [CdkDropList, CdkDrag, CdkDragPreview], }) class DraggableInDropZoneWithCustomTextOnlyPreview { @ViewChild(CdkDropList) dropInstance: CdkDropList; @@ -5174,6 +5192,8 @@ class DraggableInDropZoneWithCustomTextOnlyPreview { }
`, + standalone: true, + imports: [CdkDropList, CdkDrag, CdkDragPreview], }) class DraggableInDropZoneWithCustomMultiNodePreview { @ViewChild(CdkDropList) dropInstance: CdkDropList; @@ -5205,6 +5225,8 @@ class DraggableInDropZoneWithCustomMultiNodePreview { height: ${ITEM_HEIGHT * 3}px; } `, + standalone: true, + imports: [CdkDropList, CdkDrag, CdkDragPlaceholder, NgClass], }) class DraggableInDropZoneWithCustomPlaceholder { @ViewChildren(CdkDrag) dragItems: QueryList; @@ -5225,6 +5247,8 @@ class DraggableInDropZoneWithCustomPlaceholder { }
`, + standalone: true, + imports: [CdkDropList, CdkDrag, CdkDragPlaceholder], }) class DraggableInDropZoneWithCustomTextOnlyPlaceholder { @ViewChildren(CdkDrag) dragItems: QueryList; @@ -5245,6 +5269,8 @@ class DraggableInDropZoneWithCustomTextOnlyPlaceholder { }
`, + standalone: true, + imports: [CdkDropList, CdkDrag, CdkDragPlaceholder], }) class DraggableInDropZoneWithCustomMultiNodePlaceholder { @ViewChildren(CdkDrag) dragItems: QueryList; @@ -5319,6 +5345,8 @@ const CONNECTED_DROP_ZONES_TEMPLATE = ` encapsulation: ViewEncapsulation.None, styles: CONNECTED_DROP_ZONES_STYLES, template: CONNECTED_DROP_ZONES_TEMPLATE, + standalone: true, + imports: [CdkDropList, CdkDrag], }) class ConnectedDropZones implements AfterViewInit { @ViewChildren(CdkDrag) rawDragItems: QueryList; @@ -5352,6 +5380,8 @@ class ConnectedDropZones implements AfterViewInit { encapsulation: ViewEncapsulation.ShadowDom, styles: CONNECTED_DROP_ZONES_STYLES, template: `@if (true) {${CONNECTED_DROP_ZONES_TEMPLATE}}`, + standalone: true, + imports: [CdkDropList, CdkDrag], }) class ConnectedDropZonesInsideShadowRootWithNgIf extends ConnectedDropZones {} @@ -5378,8 +5408,8 @@ class ConnectedDropZonesInsideShadowRootWithNgIf extends ConnectedDropZones {} [cdkDropListData]="todo" (cdkDropListDropped)="droppedSpy($event)"> @for (item of todo; track item) { -
{{item}}
-} +
{{item}}
+ }
`, + standalone: true, + imports: [CdkDropList, CdkDrag, CdkDropListGroup], }) class ConnectedDropZonesViaGroupDirective extends ConnectedDropZones { groupDisabled = false; @@ -5430,6 +5462,8 @@ class ConnectedDropZonesViaGroupDirective extends ConnectedDropZones {
Two
`, + standalone: true, + imports: [CdkDropList, CdkDrag], }) class ConnectedDropZonesWithSingleItems { @ViewChildren(CdkDrag) dragItems: QueryList; @@ -5449,6 +5483,8 @@ class ConnectedDropZonesWithSingleItems {
`, + standalone: true, + imports: [CdkDropList, CdkDropListGroup], }) class NestedDropListGroups { @ViewChild('group') group: CdkDropListGroup; @@ -5460,11 +5496,15 @@ class NestedDropListGroups { template: ` `, + standalone: true, + imports: [CdkDropList], }) class DropListOnNgContainer {} @Component({ changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CdkDropList, CdkDrag], template: `
@for (item of items; track item) { @@ -5487,6 +5527,24 @@ class DraggableInDropZoneWithoutEvents { ]; } +/** Component that wraps a drop container and uses OnPush change detection. */ +@Component({ + selector: 'wrapped-drop-container', + template: ` +
+ @for (item of items; track item) { +
{{item}}
+ } +
+ `, + standalone: true, + imports: [CdkDropList, CdkDrag], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class WrappedDropContainerComponent { + @Input() items: string[]; +} + @Component({ encapsulation: ViewEncapsulation.None, styles: ` @@ -5509,6 +5567,8 @@ class DraggableInDropZoneWithoutEvents {
`, + standalone: true, + imports: [CdkDropListGroup, WrappedDropContainerComponent], }) class ConnectedWrappedDropZones { todo = ['Zero', 'One', 'Two', 'Three']; @@ -5538,6 +5598,8 @@ class ConnectedWrappedDropZones { } `, + standalone: true, + imports: [CdkDropList, CdkDrag], }) class DraggableWithCanvasInDropZone extends DraggableInDropZone implements AfterViewInit { constructor(elementRef: ElementRef) { @@ -5582,25 +5644,11 @@ class DraggableWithCanvasInDropZone extends DraggableInDropZone implements After } `, + standalone: true, + imports: [CdkDropList, CdkDrag], }) class DraggableWithInvalidCanvasInDropZone extends DraggableInDropZone {} -/** Component that wraps a drop container and uses OnPush change detection. */ -@Component({ - selector: 'wrapped-drop-container', - template: ` -
- @for (item of items; track item) { -
{{item}}
- } -
- `, - changeDetection: ChangeDetectionStrategy.OnPush, -}) -class WrappedDropContainerComponent { - @Input() items: string[]; -} - @Component({ styles: ` :host { @@ -5636,6 +5684,8 @@ class WrappedDropContainerComponent { (cdkDragReleased)="itemDragReleasedSpy($event)"> `, + standalone: true, + imports: [CdkDrag], }) class NestedDragsComponent { @ViewChild('container') container: ElementRef; @@ -5689,6 +5739,8 @@ class NestedDragsComponent { `, + standalone: true, + imports: [CdkDrag, NgTemplateOutlet], }) class NestedDragsThroughTemplate { @ViewChild('container') container: ElementRef; @@ -5713,6 +5765,8 @@ class NestedDragsThroughTemplate { `, + standalone: true, + imports: [CdkDropList, CdkDrag], }) class NestedDropZones { @ViewChildren(CdkDrag) dragItems: QueryList; @@ -5723,6 +5777,8 @@ class NestedDropZones { @Component({ template: `
`, + standalone: true, + imports: [CdkDropList], }) class PlainStandaloneDropList { @ViewChild(CdkDropList) dropList: CdkDropList; @@ -5764,6 +5820,8 @@ class PlainStandaloneDropList { `, + standalone: true, + imports: [CdkDropList, CdkDrag], }) class ConnectedDropZonesWithIntermediateSibling extends ConnectedDropZones {} @@ -5795,6 +5853,8 @@ class ConnectedDropZonesWithIntermediateSibling extends ConnectedDropZones {} } `, + standalone: true, + imports: [CdkDropList, CdkDrag], }) class DraggableWithInputsInDropZone extends DraggableInDropZone { inputValue = 'hello'; @@ -5816,6 +5876,8 @@ class DraggableWithInputsInDropZone extends DraggableInDropZone { } `, + standalone: true, + imports: [CdkDropList, CdkDrag], }) class DraggableWithRadioInputsInDropZone { @ViewChildren(CdkDrag) dragItems: QueryList; diff --git a/src/cdk/drag-drop/directives/standalone-drag.spec.ts b/src/cdk/drag-drop/directives/standalone-drag.spec.ts index a6ce3aa16255..4a7e14a8d9a8 100644 --- a/src/cdk/drag-drop/directives/standalone-drag.spec.ts +++ b/src/cdk/drag-drop/directives/standalone-drag.spec.ts @@ -1592,9 +1592,7 @@ describe('Standalone CdkDrag', () => { })); it('should be able to drag with a handle that is not a direct descendant', fakeAsync(() => { - const fixture = createComponent(StandaloneDraggableWithIndirectHandle, { - extraDeclarations: [PassthroughComponent], - }); + const fixture = createComponent(StandaloneDraggableWithIndirectHandle); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const handle = fixture.componentInstance.handleElement.nativeElement; @@ -1676,9 +1674,7 @@ describe('Standalone CdkDrag', () => { return; } - const fixture = createComponent(StandaloneDraggableWithShadowInsideHandle, { - extraDeclarations: [ShadowWrapper], - }); + const fixture = createComponent(StandaloneDraggableWithShadowInsideHandle); fixture.detectChanges(); const dragElement = fixture.componentInstance.dragElement.nativeElement; const handleChild = fixture.componentInstance.handleChild.nativeElement; @@ -1729,6 +1725,8 @@ describe('Standalone CdkDrag', () => { style="width: 100px; height: 100px; background: red;"> `, + standalone: true, + imports: [CdkDrag], }) class StandaloneDraggable { @ViewChild('dragElement') dragElement: ElementRef; @@ -1754,6 +1752,8 @@ class StandaloneDraggable { template: `
`, + standalone: true, + imports: [CdkDrag], }) class StandaloneDraggableWithOnPush { @ViewChild('dragElement') dragElement: ElementRef; @@ -1767,6 +1767,8 @@ class StandaloneDraggableWithOnPush {
`, + standalone: true, + imports: [CdkDrag, CdkDragHandle], }) class StandaloneDraggableWithHandle { @ViewChild('dragElement') dragElement: ElementRef; @@ -1787,6 +1789,8 @@ class StandaloneDraggableWithHandle { style="width: 10px; height: 10px; background: green;"> `, + standalone: true, + imports: [CdkDrag, CdkDragHandle], }) class StandaloneDraggableWithPreDisabledHandle { @ViewChild('dragElement') dragElement: ElementRef; @@ -1806,6 +1810,8 @@ class StandaloneDraggableWithPreDisabledHandle { } `, + standalone: true, + imports: [CdkDrag, CdkDragHandle], }) class StandaloneDraggableWithDelayedHandle { @ViewChild('dragElement') dragElement: ElementRef; @@ -1813,6 +1819,17 @@ class StandaloneDraggableWithDelayedHandle { showHandle = false; } +/** + * Component that passes through whatever content is projected into it. + * Used to test having drag elements being projected into a component. + */ +@Component({ + selector: 'passthrough-component', + template: '', + standalone: true, +}) +class PassthroughComponent {} + @Component({ template: `
`, + standalone: true, + imports: [CdkDrag, CdkDragHandle, PassthroughComponent], }) class StandaloneDraggableWithIndirectHandle { @ViewChild('dragElement') dragElement: ElementRef; @@ -1836,6 +1855,7 @@ class StandaloneDraggableWithIndirectHandle { selector: 'shadow-wrapper', template: '', encapsulation: ViewEncapsulation.ShadowDom, + standalone: true, }) class ShadowWrapper {} @@ -1849,6 +1869,8 @@ class ShadowWrapper {} `, + standalone: true, + imports: [CdkDrag, CdkDragHandle, ShadowWrapper], }) class StandaloneDraggableWithShadowInsideHandle { @ViewChild('dragElement') dragElement: ElementRef; @@ -1873,6 +1895,8 @@ class StandaloneDraggableWithShadowInsideHandle {
`, + standalone: true, + imports: [CdkDrag, CdkDragHandle], }) class StandaloneDraggableWithMultipleHandles { @ViewChild('dragElement') dragElement: ElementRef; @@ -1889,6 +1913,8 @@ class StandaloneDraggableWithMultipleHandles { style="width: 100px; height: 100px; background: red;"> `, + standalone: true, + imports: [CdkDrag], }) class DraggableWithAlternateRoot { @ViewChild('dragElement') dragElement: ElementRef; @@ -1901,6 +1927,8 @@ class DraggableWithAlternateRoot { template: ` `, + standalone: true, + imports: [CdkDrag], }) class DraggableOnNgContainer {} @@ -1910,6 +1938,8 @@ class DraggableOnNgContainer {} `, + standalone: true, + imports: [CdkDrag, CdkDragHandle], }) class DragHandleOnNgContainer {} @@ -1924,6 +1954,8 @@ class DragHandleOnNgContainer {} style="width: 100px; height: 100px; background: red;"> `, + standalone: true, + imports: [CdkDrag, CdkDragHandle], }) class DraggableWithAlternateRootAndSelfHandle { @ViewChild('dragElement') dragElement: ElementRef; @@ -1939,24 +1971,18 @@ class DraggableWithAlternateRootAndSelfHandle { `, + standalone: true, + imports: [CdkDrag], }) class DraggableNgContainerWithAlternateRoot { @ViewChild('dragRoot') dragRoot: ElementRef; @ViewChild(CdkDrag) dragInstance: CdkDrag; } -/** - * Component that passes through whatever content is projected into it. - * Used to test having drag elements being projected into a component. - */ -@Component({ - selector: 'passthrough-component', - template: '', -}) -class PassthroughComponent {} - @Component({ template: `
`, + standalone: true, + imports: [CdkDrag], }) class PlainStandaloneDraggable { @ViewChild(CdkDrag) dragInstance: CdkDrag; diff --git a/src/cdk/drag-drop/directives/standalone-drag.zone.spec.ts b/src/cdk/drag-drop/directives/standalone-drag.zone.spec.ts index 45bb38fb1fd0..50e9ba0eb46f 100644 --- a/src/cdk/drag-drop/directives/standalone-drag.zone.spec.ts +++ b/src/cdk/drag-drop/directives/standalone-drag.zone.spec.ts @@ -37,20 +37,22 @@ describe('Standalone CdkDrag Zone.js integration', () => { @Component({ template: ` -
-
-
- `, +
+
+
+ `, + standalone: true, + imports: [CdkDrag], }) class StandaloneDraggable { @ViewChild('dragElement') dragElement: ElementRef; diff --git a/src/cdk/drag-drop/directives/test-utils.spec.ts b/src/cdk/drag-drop/directives/test-utils.spec.ts index bcbb564a5b82..267f4c06817f 100644 --- a/src/cdk/drag-drop/directives/test-utils.spec.ts +++ b/src/cdk/drag-drop/directives/test-utils.spec.ts @@ -1,21 +1,12 @@ -import { - EnvironmentProviders, - Provider, - Type, - ViewEncapsulation, - reflectComponentType, -} from '@angular/core'; +import {EnvironmentProviders, Provider, Type, ViewEncapsulation} from '@angular/core'; import {ComponentFixture, TestBed, tick} from '@angular/core/testing'; import {dispatchMouseEvent, dispatchTouchEvent} from '@angular/cdk/testing/private'; -import {CdkScrollableModule} from '@angular/cdk/scrolling'; -import {DragDropModule} from '../drag-drop-module'; import {CDK_DRAG_CONFIG, DragDropConfig, DropListOrientation} from './config'; /** Options that can be used to configure a test. */ export interface DragDropTestConfig { providers?: (Provider | EnvironmentProviders)[]; dragDistance?: number; - extraDeclarations?: Type[]; encapsulation?: ViewEncapsulation; listOrientation?: DropListOrientation; } @@ -29,8 +20,6 @@ export function createComponent( componentType: Type, config: DragDropTestConfig = {}, ): ComponentFixture { - // TODO(crisbeto): drop this logic once all the fixtures are converted to standalone. - const isStandalone = reflectComponentType(componentType)?.isStandalone; const dragConfig: DragDropConfig = { // We default the `dragDistance` to zero, because the majority of the tests // don't care about it and drags are a lot easier to simulate when we don't @@ -39,14 +28,9 @@ export function createComponent( pointerDirectionChangeThreshold: 5, listOrientation: config.listOrientation, }; - const declarations = [...(config.extraDeclarations || [])]; - - if (!isStandalone) { - declarations.push(componentType); - } TestBed.configureTestingModule({ - imports: [DragDropModule, CdkScrollableModule], + imports: [componentType], providers: [ { provide: CDK_DRAG_CONFIG, @@ -54,7 +38,6 @@ export function createComponent( }, ...(config.providers || []), ], - declarations, }); if (config.encapsulation != null) { diff --git a/src/cdk/drag-drop/drag-drop.spec.ts b/src/cdk/drag-drop/drag-drop.spec.ts index bdff7576c197..0755c0388b17 100644 --- a/src/cdk/drag-drop/drag-drop.spec.ts +++ b/src/cdk/drag-drop/drag-drop.spec.ts @@ -1,5 +1,5 @@ import {Component, ElementRef} from '@angular/core'; -import {TestBed, fakeAsync, inject} from '@angular/core/testing'; +import {TestBed, fakeAsync} from '@angular/core/testing'; import {DragDrop} from './drag-drop'; import {DragDropModule} from './drag-drop-module'; import {DragRef} from './drag-ref'; @@ -14,10 +14,7 @@ describe('DragDrop', () => { }); TestBed.compileComponents(); - })); - - beforeEach(inject([DragDrop], (d: DragDrop) => { - service = d; + service = TestBed.inject(DragDrop); })); it('should be able to attach a DragRef to a DOM node', () => { @@ -40,7 +37,6 @@ describe('DragDrop', () => { @Component({ template: '
', standalone: true, - imports: [DragDropModule], }) class TestComponent { constructor(public elementRef: ElementRef) {} From dbcf35d638287037ef0881db9f374d563d7fb502 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 31 May 2024 08:30:37 +0200 Subject: [PATCH 11/61] refactor(material/checkbox): simplify structural styles (#29136) Simplifies the styles for the checkbox to make them smaller and easier to follow. --- src/material/checkbox/_checkbox-common.scss | 523 ++++++++++++++++++ src/material/checkbox/_checkbox-theme.scss | 31 +- src/material/checkbox/checkbox.scss | 188 +------ .../core/tokens/m2/mdc/_checkbox.scss | 7 +- src/material/list/BUILD.bazel | 1 + .../list/_list-item-hcm-indicator.scss | 38 +- src/material/list/_list-theme.scss | 10 +- src/material/list/list-option.scss | 14 +- 8 files changed, 588 insertions(+), 224 deletions(-) create mode 100644 src/material/checkbox/_checkbox-common.scss diff --git a/src/material/checkbox/_checkbox-common.scss b/src/material/checkbox/_checkbox-common.scss new file mode 100644 index 000000000000..0a713417a551 --- /dev/null +++ b/src/material/checkbox/_checkbox-common.scss @@ -0,0 +1,523 @@ +@use 'sass:math'; +@use '@angular/cdk'; +@use '../core/tokens/m2/mdc/checkbox' as tokens-mdc-checkbox; +@use '../core/tokens/token-utils'; + +$_path-length: 29.7833385; +$_transition-duration: 90ms; +$_icon-size: 18px; +$_mark-stroke-size: math.div(2, 15) * $_icon-size; +$_indeterminate-checked-curve: cubic-bezier(0.14, 0, 0, 1); +$_indeterminate-change-duration: 500ms; +$_enter-curve: cubic-bezier(0, 0, 0.2, 1); +$_exit-curve: cubic-bezier(0.4, 0, 0.6, 1); +$_fallback-size: 40px; + +// Structural styles for a checkbox. Shared with the selection list. +@mixin checkbox-structure($include-state-layer-styles) { + $prefix: tokens-mdc-checkbox.$prefix; + $slots: tokens-mdc-checkbox.get-token-slots(); + + .mdc-checkbox { + display: inline-block; + position: relative; + flex: 0 0 $_icon-size; + box-sizing: content-box; + width: $_icon-size; + height: $_icon-size; + line-height: 0; + white-space: nowrap; + cursor: pointer; + vertical-align: bottom; + + @include token-utils.use-tokens($prefix, $slots) { + $layer-size: token-utils.get-token-variable(state-layer-size); + padding: calc((var(#{$layer-size}, #{$_fallback-size}) - #{$_icon-size}) / 2); + margin: calc((var(#{$layer-size}, #{$_fallback-size}) - + var(#{$layer-size}, #{$_fallback-size})) / 2); + + @if ($include-state-layer-styles) { + @include _state-layer-styles; + } + } + + // These styles have to be nested in order to override overly-broad + // user selectors like `input[type='checkbox']`. + .mdc-checkbox__native-control { + position: absolute; + margin: 0; + padding: 0; + opacity: 0; + cursor: inherit; + + @include token-utils.use-tokens($prefix, $slots) { + $layer-size: token-utils.get-token-variable(state-layer-size); + $offset: calc((var(#{$layer-size}, #{$_fallback-size}) - + var(#{$layer-size}, #{$_fallback-size})) / 2); + width: var(#{$layer-size}, #{$_fallback-size}); + height: var(#{$layer-size}, #{$_fallback-size}); + top: $offset; + right: $offset; + left: $offset; + } + } + } + + .mdc-checkbox--disabled { + cursor: default; + pointer-events: none; + } + + .mdc-checkbox__background { + display: inline-flex; + position: absolute; + align-items: center; + justify-content: center; + box-sizing: border-box; + width: $_icon-size; + height: $_icon-size; + border: 2px solid currentColor; + border-radius: 2px; + background-color: transparent; + pointer-events: none; + will-change: background-color, border-color; + transition: background-color $_transition-duration $_exit-curve, + border-color $_transition-duration $_exit-curve; + + @include token-utils.use-tokens($prefix, $slots) { + $layer-size: token-utils.get-token-variable(state-layer-size); + $offset: calc((var(#{$layer-size}, $_fallback-size) - #{$_icon-size}) / 2); + + @include token-utils.create-token-slot(border-color, unselected-icon-color); + top: $offset; + left: $offset; + } + } + + // These can't be under `.mdc-checkbox__background` because + // the selectors will break when the mixin is nested. + @include token-utils.use-tokens($prefix, $slots) { + .mdc-checkbox__native-control:enabled:checked ~ .mdc-checkbox__background, + .mdc-checkbox__native-control:enabled:indeterminate ~ .mdc-checkbox__background { + @include token-utils.create-token-slot(border-color, selected-icon-color); + @include token-utils.create-token-slot(background-color, selected-icon-color); + } + + .mdc-checkbox--disabled .mdc-checkbox__background { + @include token-utils.create-token-slot(border-color, disabled-unselected-icon-color); + } + + .mdc-checkbox__native-control:disabled:checked ~ .mdc-checkbox__background, + .mdc-checkbox__native-control:disabled:indeterminate ~ .mdc-checkbox__background { + @include token-utils.create-token-slot(background-color, disabled-selected-icon-color); + border-color: transparent; + } + + .mdc-checkbox:hover .mdc-checkbox__native-control:not(:checked) ~ .mdc-checkbox__background, + .mdc-checkbox:hover + .mdc-checkbox__native-control:not(:indeterminate) ~ .mdc-checkbox__background { + @include token-utils.create-token-slot(border-color, unselected-hover-icon-color); + background-color: transparent; + } + + .mdc-checkbox:hover .mdc-checkbox__native-control:checked ~ .mdc-checkbox__background, + .mdc-checkbox:hover .mdc-checkbox__native-control:indeterminate ~ .mdc-checkbox__background { + @include token-utils.create-token-slot(border-color, selected-hover-icon-color); + @include token-utils.create-token-slot(background-color, selected-hover-icon-color); + } + + // Note: this must be more specific than the hover styles above. + // Double :focus is added for increased specificity. + .mdc-checkbox__native-control:focus:focus:not(:checked) ~ .mdc-checkbox__background, + .mdc-checkbox__native-control:focus:focus:not(:indeterminate) ~ .mdc-checkbox__background { + @include token-utils.create-token-slot(border-color, unselected-focus-icon-color); + } + + .mdc-checkbox__native-control:focus:focus:checked ~ .mdc-checkbox__background, + .mdc-checkbox__native-control:focus:focus:indeterminate ~ .mdc-checkbox__background { + @include token-utils.create-token-slot(border-color, selected-focus-icon-color); + @include token-utils.create-token-slot(background-color, selected-focus-icon-color); + } + } + + .mdc-checkbox__checkmark { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + width: 100%; + opacity: 0; + transition: opacity $_transition-duration * 2 $_exit-curve; + + @include token-utils.use-tokens($prefix, $slots) { + // Always apply the color since the element becomes `opacity: 0` + // when unchecked. This makes the animation look better. + @include token-utils.create-token-slot(color, selected-checkmark-color); + } + } + + @include token-utils.use-tokens($prefix, $slots) { + .mdc-checkbox--disabled .mdc-checkbox__checkmark { + @include token-utils.create-token-slot(color, disabled-selected-checkmark-color); + } + } + + .mdc-checkbox__checkmark-path { + transition: stroke-dashoffset $_transition-duration * 2 $_exit-curve; + stroke: currentColor; + stroke-width: $_mark-stroke-size * 1.3; + stroke-dashoffset: $_path-length; + stroke-dasharray: $_path-length; + } + + .mdc-checkbox__mixedmark { + width: 100%; + height: 0; + transform: scaleX(0) rotate(0deg); + border-width: math.div(math.floor($_mark-stroke-size), 2); + border-style: solid; + opacity: 0; + transition: opacity $_transition-duration $_exit-curve, + transform $_transition-duration $_exit-curve; + + @include cdk.high-contrast(active, off) { + margin: 0 1px; + } + + @include token-utils.use-tokens($prefix, $slots) { + // Always apply the color since the element becomes `opacity: 0` + // when unchecked. This makes the animation look better. + @include token-utils.create-token-slot(border-color, selected-checkmark-color); + } + } + + @include token-utils.use-tokens($prefix, $slots) { + .mdc-checkbox--disabled .mdc-checkbox__mixedmark { + @include token-utils.create-token-slot(border-color, disabled-selected-checkmark-color); + } + } + + .mdc-checkbox--anim-unchecked-checked, + .mdc-checkbox--anim-unchecked-indeterminate, + .mdc-checkbox--anim-checked-unchecked, + .mdc-checkbox--anim-indeterminate-unchecked { + .mdc-checkbox__background { + animation-duration: $_transition-duration * 2; + animation-timing-function: linear; + } + } + + .mdc-checkbox--anim-unchecked-checked { + .mdc-checkbox__checkmark-path { + animation: mdc-checkbox-unchecked-checked-checkmark-path + $_transition-duration * 2 linear; + transition: none; + } + } + + .mdc-checkbox--anim-unchecked-indeterminate { + .mdc-checkbox__mixedmark { + animation: mdc-checkbox-unchecked-indeterminate-mixedmark $_transition-duration linear; + transition: none; + } + } + + .mdc-checkbox--anim-checked-unchecked { + .mdc-checkbox__checkmark-path { + animation: mdc-checkbox-checked-unchecked-checkmark-path $_transition-duration linear; + transition: none; + } + } + + .mdc-checkbox--anim-checked-indeterminate { + .mdc-checkbox__checkmark { + animation: mdc-checkbox-checked-indeterminate-checkmark $_transition-duration linear; + transition: none; + } + + .mdc-checkbox__mixedmark { + animation: mdc-checkbox-checked-indeterminate-mixedmark $_transition-duration linear; + transition: none; + } + } + + .mdc-checkbox--anim-indeterminate-checked { + .mdc-checkbox__checkmark { + animation: mdc-checkbox-indeterminate-checked-checkmark + $_indeterminate-change-duration linear; + transition: none; + } + + .mdc-checkbox__mixedmark { + animation: mdc-checkbox-indeterminate-checked-mixedmark + $_indeterminate-change-duration linear; + transition: none; + } + } + + .mdc-checkbox--anim-indeterminate-unchecked { + .mdc-checkbox__mixedmark { + animation: mdc-checkbox-indeterminate-unchecked-mixedmark + $_indeterminate-change-duration * 0.6 linear; + transition: none; + } + } + + .mdc-checkbox__native-control:checked ~ .mdc-checkbox__background, + .mdc-checkbox__native-control:indeterminate ~ .mdc-checkbox__background { + transition: border-color $_transition-duration $_enter-curve, + background-color $_transition-duration $_enter-curve; + + .mdc-checkbox__checkmark-path { + stroke-dashoffset: 0; + } + } + + .mdc-checkbox__native-control:checked ~ .mdc-checkbox__background { + .mdc-checkbox__checkmark { + transition: opacity $_transition-duration * 2 $_enter-curve, + transform $_transition-duration * 2 $_enter-curve; + opacity: 1; + } + + .mdc-checkbox__mixedmark { + transform: scaleX(1) rotate(-45deg); + } + } + .mdc-checkbox__native-control:indeterminate ~ .mdc-checkbox__background { + .mdc-checkbox__checkmark { + transform: rotate(45deg); + opacity: 0; + transition: opacity $_transition-duration $_exit-curve, + transform $_transition-duration $_exit-curve; + } + + .mdc-checkbox__mixedmark { + transform: scaleX(1) rotate(0deg); + opacity: 1; + } + } + + @keyframes mdc-checkbox-unchecked-checked-checkmark-path { + 0%, 50% { + stroke-dashoffset: $_path-length; + } + + 50% { + animation-timing-function: $_enter-curve; + } + + 100% { + stroke-dashoffset: 0; + } + } + + @keyframes mdc-checkbox-unchecked-indeterminate-mixedmark { + 0%, 68.2% { + transform: scaleX(0); + } + + 68.2% { + animation-timing-function: cubic-bezier(0, 0, 0, 1); + } + + 100% { + transform: scaleX(1); + } + } + + @keyframes mdc-checkbox-checked-unchecked-checkmark-path { + from { + animation-timing-function: cubic-bezier(0.4, 0, 1, 1); + opacity: 1; + stroke-dashoffset: 0; + } + + to { + opacity: 0; + stroke-dashoffset: $_path-length * -1; + } + } + + @keyframes mdc-checkbox-checked-indeterminate-checkmark { + from { + animation-timing-function: $_enter-curve; + transform: rotate(0deg); + opacity: 1; + } + + to { + transform: rotate(45deg); + opacity: 0; + } + } + + @keyframes mdc-checkbox-indeterminate-checked-checkmark { + from { + animation-timing-function: $_indeterminate-checked-curve; + transform: rotate(45deg); + opacity: 0; + } + + to { + transform: rotate(360deg); + opacity: 1; + } + } + + @keyframes mdc-checkbox-checked-indeterminate-mixedmark { + from { + animation-timing-function: $_enter-curve; + transform: rotate(-45deg); + opacity: 0; + } + + to { + transform: rotate(0deg); + opacity: 1; + } + } + + @keyframes mdc-checkbox-indeterminate-checked-mixedmark { + from { + animation-timing-function: $_indeterminate-checked-curve; + transform: rotate(0deg); + opacity: 1; + } + + to { + transform: rotate(315deg); + opacity: 0; + } + } + + @keyframes mdc-checkbox-indeterminate-unchecked-mixedmark { + 0% { + animation-timing-function: linear; + transform: scaleX(1); + opacity: 1; + } + + 32.8%, 100% { + transform: scaleX(0); + opacity: 0; + } + } +} + +// Conditionally disables the animations of the checkbox. +@mixin checkbox-noop-animations() { + &._mat-animation-noopable .mdc-checkbox { + *, *::before { + transition: none !important; + animation: none !important; + } + } +} + +@mixin _state-layer-styles() { + // MDC expects `.mdc-checkbox__ripple::before` to be the state layer, but we use + // `.mdc-checkbox__ripple` instead, so we emit the state layer slots ourselves. + &:hover { + .mdc-checkbox__ripple { + @include token-utils.create-token-slot(opacity, unselected-hover-state-layer-opacity); + @include token-utils.create-token-slot( + background-color, + unselected-hover-state-layer-color + ); + } + + .mat-mdc-checkbox-ripple .mat-ripple-element { + @include token-utils.create-token-slot( + background-color, + unselected-hover-state-layer-color + ); + } + } + + .mdc-checkbox__native-control:focus { + & ~ .mdc-checkbox__ripple { + @include token-utils.create-token-slot(opacity, unselected-focus-state-layer-opacity); + @include token-utils.create-token-slot( + background-color, + unselected-focus-state-layer-color + ); + } + + & ~ .mat-mdc-checkbox-ripple .mat-ripple-element { + @include token-utils.create-token-slot( + background-color, + unselected-focus-state-layer-color + ); + } + } + + &:active .mdc-checkbox__native-control { + & ~ .mdc-checkbox__ripple { + @include token-utils.create-token-slot(opacity, unselected-pressed-state-layer-opacity); + @include token-utils.create-token-slot( + background-color, + unselected-pressed-state-layer-color + ); + } + + & ~ .mat-mdc-checkbox-ripple .mat-ripple-element { + @include token-utils.create-token-slot( + background-color, + unselected-pressed-state-layer-color + ); + } + } + + &:hover .mdc-checkbox__native-control:checked { + & ~ .mdc-checkbox__ripple { + @include token-utils.create-token-slot(opacity, selected-hover-state-layer-opacity); + @include token-utils.create-token-slot( + background-color, + selected-hover-state-layer-color + ); + } + + & ~ .mat-mdc-checkbox-ripple .mat-ripple-element { + @include token-utils.create-token-slot( + background-color, + selected-hover-state-layer-color + ); + } + } + + .mdc-checkbox__native-control:focus:checked { + & ~ .mdc-checkbox__ripple { + @include token-utils.create-token-slot(opacity, selected-focus-state-layer-opacity); + @include token-utils.create-token-slot( + background-color, + selected-focus-state-layer-color + ); + } + + & ~ .mat-mdc-checkbox-ripple .mat-ripple-element { + @include token-utils.create-token-slot( + background-color, + selected-focus-state-layer-color + ); + } + } + + &:active .mdc-checkbox__native-control:checked { + & ~ .mdc-checkbox__ripple { + @include token-utils.create-token-slot(opacity, selected-pressed-state-layer-opacity); + @include token-utils.create-token-slot( + background-color, + selected-pressed-state-layer-color + ); + } + + & ~ .mat-mdc-checkbox-ripple .mat-ripple-element { + @include token-utils.create-token-slot( + background-color, + selected-pressed-state-layer-color + ); + } + } +} diff --git a/src/material/checkbox/_checkbox-theme.scss b/src/material/checkbox/_checkbox-theme.scss index 6835ef515eae..13d106a71781 100644 --- a/src/material/checkbox/_checkbox-theme.scss +++ b/src/material/checkbox/_checkbox-theme.scss @@ -1,4 +1,3 @@ -@use '@material/checkbox/checkbox-theme' as mdc-checkbox-theme; @use '../core/style/sass-utils'; @use '../core/theming/theming'; @use '../core/theming/inspection'; @@ -16,7 +15,10 @@ @include _theme-from-tokens(inspection.get-theme-tokens($theme, base)); } @else { @include sass-utils.current-selector-or-root() { - @include mdc-checkbox-theme.theme(tokens-mdc-checkbox.get-unthemable-tokens()); + @include token-utils.create-token-values( + tokens-mdc-checkbox.$prefix, + tokens-mdc-checkbox.get-unthemable-tokens() + ); @include token-utils.create-token-values( tokens-mat-checkbox.$prefix, tokens-mat-checkbox.get-unthemable-tokens() @@ -35,7 +37,10 @@ @include _theme-from-tokens(inspection.get-theme-tokens($theme, color), $options...); } @else { @include sass-utils.current-selector-or-root() { - @include mdc-checkbox-theme.theme(tokens-mdc-checkbox.get-color-tokens($theme)); + @include token-utils.create-token-values( + tokens-mdc-checkbox.$prefix, + tokens-mdc-checkbox.get-color-tokens($theme) + ); @include token-utils.create-token-values( tokens-mat-checkbox.$prefix, tokens-mat-checkbox.get-color-tokens($theme) @@ -44,11 +49,15 @@ .mat-mdc-checkbox { &.mat-primary { - @include mdc-checkbox-theme.theme(tokens-mdc-checkbox.get-color-tokens($theme, primary)); + @include token-utils.create-token-values( + tokens-mdc-checkbox.$prefix, + tokens-mdc-checkbox.get-color-tokens($theme, primary)); } &.mat-warn { - @include mdc-checkbox-theme.theme(tokens-mdc-checkbox.get-color-tokens($theme, warn)); + @include token-utils.create-token-values( + tokens-mdc-checkbox.$prefix, + tokens-mdc-checkbox.get-color-tokens($theme, warn)); } } } @@ -61,7 +70,10 @@ @include _theme-from-tokens(inspection.get-theme-tokens($theme, typography)); } @else { @include sass-utils.current-selector-or-root() { - @include mdc-checkbox-theme.theme(tokens-mdc-checkbox.get-typography-tokens($theme)); + @include token-utils.create-token-values( + tokens-mdc-checkbox.$prefix, + tokens-mdc-checkbox.get-typography-tokens($theme) + ); @include token-utils.create-token-values( tokens-mat-checkbox.$prefix, tokens-mat-checkbox.get-typography-tokens($theme) @@ -79,7 +91,10 @@ @include _theme-from-tokens(inspection.get-theme-tokens($theme, density)); } @else { @include sass-utils.current-selector-or-root() { - @include mdc-checkbox-theme.theme(tokens-mdc-checkbox.get-density-tokens($theme)); + @include token-utils.create-token-values( + tokens-mdc-checkbox.$prefix, + tokens-mdc-checkbox.get-density-tokens($theme) + ); @include token-utils.create-token-values( tokens-mat-checkbox.$prefix, tokens-mat-checkbox.get-density-tokens($theme) @@ -140,6 +155,6 @@ // Don't pass $options here, since the mdc-checkbox doesn't support color options, // only the mdc-checkbox does. $mat-checkbox-tokens: token-utils.get-tokens-for($tokens, tokens-mat-checkbox.$prefix); - @include mdc-checkbox-theme.theme($mdc-checkbox-tokens); + @include token-utils.create-token-values(tokens-mdc-checkbox.$prefix, $mdc-checkbox-tokens); @include token-utils.create-token-values(tokens-mat-checkbox.$prefix, $mat-checkbox-tokens); } diff --git a/src/material/checkbox/checkbox.scss b/src/material/checkbox/checkbox.scss index eba4223a959f..e63f95417e5a 100644 --- a/src/material/checkbox/checkbox.scss +++ b/src/material/checkbox/checkbox.scss @@ -1,176 +1,15 @@ -@use 'sass:map'; @use '@angular/cdk'; -@use '@material/checkbox/checkbox' as mdc-checkbox; -@use '@material/checkbox/checkbox-theme' as mdc-checkbox-theme; -@use '@material/touch-target' as mdc-touch-target; -@use '@material/theme/custom-properties' as mdc-custom-properties; -@use '../core/mdc-helpers/mdc-helpers'; @use '../core/style/layout-common'; @use '../core/style/vendor-prefixes'; -@use '../core/tokens/m2/mdc/checkbox' as tokens-mdc-checkbox; @use '../core/tokens/m2/mat/checkbox' as tokens-mat-checkbox; @use '../core/tokens/token-utils'; +@use './checkbox-common'; -@include mdc-custom-properties.configure($emit-fallback-values: false, $emit-fallback-vars: false) { - // Add the checkbox static styles. - @include mdc-checkbox.static-styles(); - - $mdc-checkbox-slots: tokens-mdc-checkbox.get-token-slots(); - - .mdc-checkbox { - // Add the slots for MDC checkbox. - @include mdc-checkbox-theme.theme-styles( - map.merge( - $mdc-checkbox-slots, - ( - // Angular Material focuses the native input. rather than the element MDC expects, - // so we create this slot ourselves. - selected-focus-icon-color: null, - unselected-focus-icon-color: null, - // MDC expects `.mdc-checkbox__ripple::before` to be the state layer, but we use - // `.mdc-checkbox__ripple` instead, so we emit the state layer slots ourselves. - unselected-hover-state-layer-opacity: null, - unselected-hover-state-layer-color: null, - unselected-focus-state-layer-opacity: null, - unselected-focus-state-layer-color: null, - unselected-pressed-state-layer-opacity: null, - unselected-pressed-state-layer-color: null, - selected-hover-state-layer-opacity: null, - selected-hover-state-layer-color: null, - selected-focus-state-layer-opacity: null, - selected-focus-state-layer-color: null, - selected-pressed-state-layer-opacity: null, - selected-pressed-state-layer-color: null - ) - ) - ); - - @include token-utils.use-tokens(tokens-mdc-checkbox.$prefix, $mdc-checkbox-slots) { - // MDC expects focus on .mdc-checkbox, but we focus the native element instead, so we need to - // emit a our own slot for the focus styles. - .mdc-checkbox__native-control:enabled:focus { - // Extra `:focus` included to achieve higher specificity than MDC's `:hover` style. - &:focus:not(:checked):not(:indeterminate) ~ .mdc-checkbox__background { - @include token-utils.create-token-slot(border-color, unselected-focus-icon-color); - } - - &:checked, - &:indeterminate { - & ~ .mdc-checkbox__background { - @include token-utils.create-token-slot(border-color, selected-focus-icon-color); - @include token-utils.create-token-slot(background-color, selected-focus-icon-color); - } - } - } - - // MDC expects `.mdc-checkbox__ripple::before` to be the state layer, but we use - // `.mdc-checkbox__ripple` instead, so we emit the state layer slots ourselves. - &:hover { - .mdc-checkbox__ripple { - @include token-utils.create-token-slot(opacity, unselected-hover-state-layer-opacity); - @include token-utils.create-token-slot( - background-color, - unselected-hover-state-layer-color - ); - } - - .mat-mdc-checkbox-ripple .mat-ripple-element { - @include token-utils.create-token-slot( - background-color, - unselected-hover-state-layer-color - ); - } - } - - .mdc-checkbox__native-control:focus { - & ~ .mdc-checkbox__ripple { - @include token-utils.create-token-slot(opacity, unselected-focus-state-layer-opacity); - @include token-utils.create-token-slot( - background-color, - unselected-focus-state-layer-color - ); - } - - & ~ .mat-mdc-checkbox-ripple .mat-ripple-element { - @include token-utils.create-token-slot( - background-color, - unselected-focus-state-layer-color - ); - } - } - - &:active .mdc-checkbox__native-control { - & ~ .mdc-checkbox__ripple { - @include token-utils.create-token-slot(opacity, unselected-pressed-state-layer-opacity); - @include token-utils.create-token-slot( - background-color, - unselected-pressed-state-layer-color - ); - } - - & ~ .mat-mdc-checkbox-ripple .mat-ripple-element { - @include token-utils.create-token-slot( - background-color, - unselected-pressed-state-layer-color - ); - } - } - - &:hover .mdc-checkbox__native-control:checked { - & ~ .mdc-checkbox__ripple { - @include token-utils.create-token-slot(opacity, selected-hover-state-layer-opacity); - @include token-utils.create-token-slot( - background-color, - selected-hover-state-layer-color - ); - } - - & ~ .mat-mdc-checkbox-ripple .mat-ripple-element { - @include token-utils.create-token-slot( - background-color, - selected-hover-state-layer-color - ); - } - } - - .mdc-checkbox__native-control:focus:checked { - & ~ .mdc-checkbox__ripple { - @include token-utils.create-token-slot(opacity, selected-focus-state-layer-opacity); - @include token-utils.create-token-slot( - background-color, - selected-focus-state-layer-color - ); - } - - & ~ .mat-mdc-checkbox-ripple .mat-ripple-element { - @include token-utils.create-token-slot( - background-color, - selected-focus-state-layer-color - ); - } - } - - &:active .mdc-checkbox__native-control:checked { - & ~ .mdc-checkbox__ripple { - @include token-utils.create-token-slot(opacity, selected-pressed-state-layer-opacity); - @include token-utils.create-token-slot( - background-color, - selected-pressed-state-layer-color - ); - } - - & ~ .mat-mdc-checkbox-ripple .mat-ripple-element { - @include token-utils.create-token-slot( - background-color, - selected-pressed-state-layer-color - ); - } - } - } - } -} +@include checkbox-common.checkbox-structure(true); .mat-mdc-checkbox { + @include checkbox-common.checkbox-noop-animations; + // The host node defaults to `display: inline`, we have to change it in order for margins to work. display: inline-block; // Avoids issues in some CSS grid layouts (see #25153). @@ -183,15 +22,6 @@ @include vendor-prefixes.color-adjust(exact); } - // Angular Material supports disabling all animations when NoopAnimationsModule is imported. - &._mat-animation-noopable { - *, - *::before { - transition: none !important; - animation: none !important; - } - } - // Clicking the label toggles the checkbox, but MDC does not include any styles that inform the // user of this. Therefore we add the pointer cursor on top of MDC's styles. label { @@ -269,10 +99,12 @@ // Element used to provide a larger tap target for users on touch devices. .mat-mdc-checkbox-touch-target { - @include mdc-touch-target.touch-target( - $set-width: true, - $query: mdc-helpers.$mdc-base-styles-query - ); + position: absolute; + top: 50%; + left: 50%; + height: 48px; + width: 48px; + transform: translate(-50%, -50%); @include token-utils.use-tokens( tokens-mat-checkbox.$prefix, diff --git a/src/material/core/tokens/m2/mdc/_checkbox.scss b/src/material/core/tokens/m2/mdc/_checkbox.scss index b461fea1605b..5a230ebe7c1f 100644 --- a/src/material/core/tokens/m2/mdc/_checkbox.scss +++ b/src/material/core/tokens/m2/mdc/_checkbox.scss @@ -35,6 +35,9 @@ $prefix: (mdc, checkbox); // ============================================================================================= // = TOKENS NOT USED IN ANGULAR MATERIAL = // ============================================================================================= + selected-pressed-icon-color: null, + unselected-pressed-icon-color: null, + // MDC currently doesn't output a slot for these tokens. disabled-selected-icon-opacity: null, disabled-unselected-icon-opacity: null, @@ -79,16 +82,12 @@ $prefix: (mdc, checkbox); selected-hover-icon-color: $palette-selected, // The color of the checkbox fill when the checkbox is selected. selected-icon-color: $palette-selected, - // The color of the checkbox fill when the checkbox is selected an pressed. - selected-pressed-icon-color: $palette-selected, // The color of the checkbox border when the checkbox is unselected and focused. unselected-focus-icon-color: $active-border-color, // The color of the checkbox border when the checkbox is unselected and hovered. unselected-hover-icon-color: $active-border-color, // The color of the checkbox border when the checkbox is unselected. unselected-icon-color: $border-color, - // The color of the checkbox border when the checkbox is unselected and pressed. - unselected-pressed-icon-color: $border-color, // The color of the ripple when the checkbox is selected and focused. selected-focus-state-layer-color: $palette-default, // The color of the ripple when the checkbox is selected and hovered. diff --git a/src/material/list/BUILD.bazel b/src/material/list/BUILD.bazel index 950518a63151..5bd7a26b1467 100644 --- a/src/material/list/BUILD.bazel +++ b/src/material/list/BUILD.bazel @@ -71,6 +71,7 @@ sass_binary( ":list_scss_lib", "//:mdc_sass_lib", "//src/cdk:sass_lib", + "//src/material/checkbox:checkbox_scss_lib", "//src/material/core:core_scss_lib", ], ) diff --git a/src/material/list/_list-item-hcm-indicator.scss b/src/material/list/_list-item-hcm-indicator.scss index cc88f0539693..488064bb58a2 100644 --- a/src/material/list/_list-item-hcm-indicator.scss +++ b/src/material/list/_list-item-hcm-indicator.scss @@ -6,25 +6,25 @@ // its background color. Since that doesn't work in HCM, this mixin provides an alternative by // rendering a circle. @mixin private-high-contrast-list-item-indicator() { - @include cdk.high-contrast(active, off) { - &::after { - $size: 10px; - content: ''; - position: absolute; - top: 50%; - right: mdc-list-variables.$side-padding; - transform: translateY(-50%); - width: $size; - height: 0; - border-bottom: solid $size; - border-radius: $size; - } + @include cdk.high-contrast(active, off) { + &::after { + $size: 10px; + content: ''; + position: absolute; + top: 50%; + right: mdc-list-variables.$side-padding; + transform: translateY(-50%); + width: $size; + height: 0; + border-bottom: solid $size; + border-radius: $size; + } - [dir='rtl'] { - &::after { - right: auto; - left: mdc-list-variables.$side-padding; - } - } + [dir='rtl'] { + &::after { + right: auto; + left: mdc-list-variables.$side-padding; + } } + } } diff --git a/src/material/list/_list-theme.scss b/src/material/list/_list-theme.scss index b88620f0743e..cbd129154e94 100644 --- a/src/material/list/_list-theme.scss +++ b/src/material/list/_list-theme.scss @@ -1,6 +1,5 @@ @use 'sass:map'; @use '@material/list/evolution-mixins'; -@use '@material/checkbox/checkbox-theme' as mdc-checkbox-theme; @use '@material/radio/radio-theme' as mdc-radio-theme; @use '@material/list/list-theme' as mdc-list-theme; @@ -63,13 +62,16 @@ } .mat-mdc-list-option { - @include mdc-checkbox-theme.theme(tokens-mdc-checkbox.get-color-tokens($theme, primary)); + @include token-utils.create-token-values( + tokens-mdc-checkbox.$prefix, tokens-mdc-checkbox.get-color-tokens($theme, primary)); } .mat-mdc-list-option.mat-accent { - @include mdc-checkbox-theme.theme(tokens-mdc-checkbox.get-color-tokens($theme, accent)); + @include token-utils.create-token-values( + tokens-mdc-checkbox.$prefix, tokens-mdc-checkbox.get-color-tokens($theme, accent)); } .mat-mdc-list-option.mat-warn { - @include mdc-checkbox-theme.theme(tokens-mdc-checkbox.get-color-tokens($theme, warn)); + @include token-utils.create-token-values( + tokens-mdc-checkbox.$prefix, tokens-mdc-checkbox.get-color-tokens($theme, warn)); } // There is no token for activated color on nav list. diff --git a/src/material/list/list-option.scss b/src/material/list/list-option.scss index 90c1eb876f9d..09388c58fe9b 100644 --- a/src/material/list/list-option.scss +++ b/src/material/list/list-option.scss @@ -1,10 +1,8 @@ -@use '@material/checkbox/checkbox' as mdc-checkbox; -@use '@material/checkbox/checkbox-theme' as mdc-checkbox-theme; @use '@material/radio/radio' as mdc-radio; @use '@material/radio/radio-theme' as mdc-radio-theme; +@use '../checkbox/checkbox-common'; @use '../core/mdc-helpers/mdc-helpers'; -@use '../core/tokens/m2/mdc/checkbox' as tokens-mdc-checkbox; @use '../core/tokens/m2/mdc/radio' as tokens-mdc-radio; @use './list-option-trailing-avatar-compat'; @use './list-item-hcm-indicator'; @@ -17,24 +15,18 @@ // The MDC-based list-option uses the MDC checkbox/radio for the selection indicators. // We need to ensure that the checkbox and radio styles are not included for the list-option. @include mdc-helpers.disable-mdc-fallback-declarations { - @include mdc-checkbox.static-styles( - $query: mdc-helpers.$mdc-base-styles-without-animation-query); @include mdc-radio.static-styles( $query: mdc-helpers.$mdc-base-styles-without-animation-query); &:not(._mat-animation-noopable) { - @include mdc-checkbox.static-styles($query: animation); @include mdc-radio.static-styles($query: animation); } } // We can't use the MDC checkbox here directly, because this checkbox is purely // decorative and including the MDC one will bring in unnecessary JS. - .mdc-checkbox { - // MDC theme styles also include structural styles so we have to include the theme at least - // once here. The values will be overwritten by our own theme file afterwards. - @include mdc-checkbox-theme.theme-styles(tokens-mdc-checkbox.get-token-slots()); - } + @include checkbox-common.checkbox-structure(false); + @include checkbox-common.checkbox-noop-animations; // We can't use the MDC radio here directly, because this radio is purely // decorative and including the MDC one will bring in unnecessary JS. From 04d3b63de2fb843beb55836e39e930c3342d26f1 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 31 May 2024 10:15:10 -0700 Subject: [PATCH 12/61] test: Move more CDK tests to zoneless (#29144) --- src/cdk/a11y/focus-trap/focus-trap.spec.ts | 38 ++++++++--- src/cdk/menu/menu-base.ts | 30 +++++---- src/cdk/menu/menu-trigger.spec.ts | 12 +--- src/cdk/menu/menu.spec.ts | 11 +--- src/cdk/table/table.spec.ts | 74 ++++++++++++++++++---- src/cdk/table/text-column.spec.ts | 11 ++-- src/cdk/text-field/autofill.spec.ts | 21 +----- src/cdk/text-field/autofill.zone.spec.ts | 61 ++++++++++++++++++ src/cdk/text-field/autosize.spec.ts | 25 ++++++-- src/cdk/tree/tree.spec.ts | 43 ++++++++----- src/cdk/tree/tree.ts | 19 +++++- 11 files changed, 241 insertions(+), 104 deletions(-) create mode 100644 src/cdk/text-field/autofill.zone.spec.ts diff --git a/src/cdk/a11y/focus-trap/focus-trap.spec.ts b/src/cdk/a11y/focus-trap/focus-trap.spec.ts index 8af57b940bd1..a0d3b07df439 100644 --- a/src/cdk/a11y/focus-trap/focus-trap.spec.ts +++ b/src/cdk/a11y/focus-trap/focus-trap.spec.ts @@ -1,21 +1,19 @@ import {Platform, _supportsShadowDom} from '@angular/cdk/platform'; +import {CdkPortalOutlet, PortalModule, TemplatePortal} from '@angular/cdk/portal'; import { Component, - ViewChild, TemplateRef, + ViewChild, ViewContainerRef, ViewEncapsulation, - provideZoneChangeDetection, } from '@angular/core'; -import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing'; -import {PortalModule, CdkPortalOutlet, TemplatePortal} from '@angular/cdk/portal'; -import {A11yModule, FocusTrap, CdkTrapFocus} from '../index'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; +import {A11yModule, CdkTrapFocus, FocusTrap} from '../index'; describe('FocusTrap', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [ A11yModule, PortalModule, @@ -106,6 +104,7 @@ describe('FocusTrap', () => { expect(rootElement.querySelectorAll('div.cdk-visually-hidden').length).toBe(2); fixture.componentInstance.renderFocusTrap = false; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(rootElement.querySelectorAll('div.cdk-visually-hidden').length).toBe(0); @@ -120,6 +119,7 @@ describe('FocusTrap', () => { expect(anchors.every(current => current.getAttribute('aria-hidden') === 'true')).toBe(true); fixture.componentInstance._isFocusTrapEnabled = false; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(anchors.every(current => !current.hasAttribute('tabindex'))).toBe(true); @@ -216,12 +216,16 @@ describe('FocusTrap', () => { expect(getActiveElement()).toBe(buttonOutsideTrappedRegion); fixture.componentInstance.showTrappedRegion = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.whenStable().then(() => { expect(getActiveElement().id).toBe('auto-capture-target'); - fixture.destroy(); + fixture.componentInstance.showTrappedRegion = false; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + expect(getActiveElement()).toBe(buttonOutsideTrappedRegion); }); })); @@ -230,6 +234,7 @@ describe('FocusTrap', () => { const fixture = TestBed.createComponent(FocusTrapWithAutoCapture); fixture.componentInstance.autoCaptureEnabled = false; fixture.componentInstance.showTrappedRegion = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const buttonOutsideTrappedRegion = fixture.nativeElement.querySelector('button'); @@ -237,12 +242,16 @@ describe('FocusTrap', () => { expect(getActiveElement()).toBe(buttonOutsideTrappedRegion); fixture.componentInstance.autoCaptureEnabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.whenStable().then(() => { expect(getActiveElement().id).toBe('auto-capture-target'); - fixture.destroy(); + fixture.componentInstance.showTrappedRegion = false; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + expect(getActiveElement()).toBe(buttonOutsideTrappedRegion); }); })); @@ -260,12 +269,16 @@ describe('FocusTrap', () => { expect(getActiveElement()).toBe(buttonOutsideTrappedRegion); fixture.componentInstance.showTrappedRegion = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.whenStable().then(() => { expect(getActiveElement().id).toBe('auto-capture-target'); - fixture.destroy(); + fixture.componentInstance.showTrappedRegion = false; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + expect(getActiveElement()).toBe(buttonOutsideTrappedRegion); }); })); @@ -278,6 +291,7 @@ describe('FocusTrap', () => { const fixture = TestBed.createComponent(FocusTrapWithAutoCaptureInShadowDom); fixture.componentInstance.autoCaptureEnabled = false; fixture.componentInstance.showTrappedRegion = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const buttonOutsideTrappedRegion = fixture.debugElement.query(By.css('button')).nativeElement; @@ -285,12 +299,16 @@ describe('FocusTrap', () => { expect(getActiveElement()).toBe(buttonOutsideTrappedRegion); fixture.componentInstance.autoCaptureEnabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.whenStable().then(() => { expect(getActiveElement().id).toBe('auto-capture-target'); - fixture.destroy(); + fixture.componentInstance.showTrappedRegion = false; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + expect(getActiveElement()).toBe(buttonOutsideTrappedRegion); }); })); diff --git a/src/cdk/menu/menu-base.ts b/src/cdk/menu/menu-base.ts index 4af909003931..031e323abec3 100644 --- a/src/cdk/menu/menu-base.ts +++ b/src/cdk/menu/menu-base.ts @@ -6,27 +6,29 @@ * found in the LICENSE file at https://angular.io/license */ -import {CdkMenuGroup} from './menu-group'; +import {FocusKeyManager, FocusOrigin} from '@angular/cdk/a11y'; +import {Directionality} from '@angular/cdk/bidi'; import { AfterContentInit, ContentChildren, Directive, ElementRef, - inject, Input, NgZone, OnDestroy, QueryList, + computed, + inject, + signal, } from '@angular/core'; -import {FocusKeyManager, FocusOrigin} from '@angular/cdk/a11y'; -import {CdkMenuItem} from './menu-item'; -import {merge, Subject} from 'rxjs'; -import {Directionality} from '@angular/cdk/bidi'; +import {Subject, merge} from 'rxjs'; import {mapTo, mergeAll, mergeMap, startWith, switchMap, takeUntil} from 'rxjs/operators'; -import {MENU_STACK, MenuStack, MenuStackItem} from './menu-stack'; +import {MENU_AIM} from './menu-aim'; +import {CdkMenuGroup} from './menu-group'; import {Menu} from './menu-interface'; +import {CdkMenuItem} from './menu-item'; +import {MENU_STACK, MenuStack, MenuStackItem} from './menu-stack'; import {PointerFocusTracker} from './pointer-focus-tracker'; -import {MENU_AIM} from './menu-aim'; /** Counter used to create unique IDs for menus. */ let nextId = 0; @@ -97,7 +99,12 @@ export abstract class CdkMenuBase protected pointerTracker?: PointerFocusTracker; /** Whether this menu's menu stack has focus. */ - private _menuStackHasFocus = false; + private _menuStackHasFocus = signal(false); + + private _tabIndexSignal = computed(() => { + const tabindexIfInline = this._menuStackHasFocus() ? -1 : 0; + return this.isInline ? tabindexIfInline : null; + }); ngAfterContentInit() { if (!this.isInline) { @@ -137,8 +144,7 @@ export abstract class CdkMenuBase /** Gets the tabindex for this menu. */ _getTabIndex() { - const tabindexIfInline = this._menuStackHasFocus ? -1 : 0; - return this.isInline ? tabindexIfInline : null; + return this._tabIndexSignal(); } /** @@ -211,7 +217,7 @@ export abstract class CdkMenuBase private _subscribeToMenuStackHasFocus() { if (this.isInline) { this.menuStack.hasFocus.pipe(takeUntil(this.destroyed)).subscribe(hasFocus => { - this._menuStackHasFocus = hasFocus; + this._menuStackHasFocus.set(hasFocus); }); } } diff --git a/src/cdk/menu/menu-trigger.spec.ts b/src/cdk/menu/menu-trigger.spec.ts index 00f0a902171a..a7a681d236e3 100644 --- a/src/cdk/menu/menu-trigger.spec.ts +++ b/src/cdk/menu/menu-trigger.spec.ts @@ -1,13 +1,5 @@ import {ENTER, SPACE, TAB} from '@angular/cdk/keycodes'; -import { - Component, - ElementRef, - QueryList, - Type, - ViewChild, - ViewChildren, - provideZoneChangeDetection, -} from '@angular/core'; +import {Component, ElementRef, QueryList, Type, ViewChild, ViewChildren} from '@angular/core'; import {ComponentFixture, TestBed, fakeAsync, tick, waitForAsync} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {dispatchKeyboardEvent} from '../../cdk/testing/private'; @@ -122,7 +114,6 @@ describe('MenuTrigger', () => { TestBed.configureTestingModule({ imports: [CdkMenuModule], declarations: [MenuBarWithNestedSubMenus], - providers: [provideZoneChangeDetection()], }).compileComponents(); })); @@ -161,6 +152,7 @@ describe('MenuTrigger', () => { it('should not open the menu when menu item disabled', () => { menuItems[0].disabled = true; + fixture.changeDetectorRef.markForCheck(); menuItems[0].trigger(); detectChanges(); diff --git a/src/cdk/menu/menu.spec.ts b/src/cdk/menu/menu.spec.ts index 122d8bef7795..ce117ca6bf62 100644 --- a/src/cdk/menu/menu.spec.ts +++ b/src/cdk/menu/menu.spec.ts @@ -1,12 +1,5 @@ import {TAB} from '@angular/cdk/keycodes'; -import { - Component, - ElementRef, - QueryList, - ViewChild, - ViewChildren, - provideZoneChangeDetection, -} from '@angular/core'; +import {Component, ElementRef, QueryList, ViewChild, ViewChildren} from '@angular/core'; import { ComponentFixture, TestBed, @@ -145,7 +138,6 @@ describe('Menu', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [CdkMenuModule, WithComplexNestedMenus], - providers: [provideZoneChangeDetection()], }).compileComponents(); })); @@ -337,7 +329,6 @@ describe('Menu', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [CdkMenuModule, WithComplexNestedMenusOnBottom], - providers: [provideZoneChangeDetection()], }).compileComponents(); })); diff --git a/src/cdk/table/table.spec.ts b/src/cdk/table/table.spec.ts index 247163a324e1..41b3cb86254f 100644 --- a/src/cdk/table/table.spec.ts +++ b/src/cdk/table/table.spec.ts @@ -1,6 +1,10 @@ +import {BidiModule} from '@angular/cdk/bidi'; import {CollectionViewer, DataSource} from '@angular/cdk/collections'; import { AfterContentInit, + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, Component, ContentChild, ContentChildren, @@ -8,12 +12,10 @@ import { QueryList, Type, ViewChild, - AfterViewInit, - ChangeDetectionStrategy, - provideZoneChangeDetection, + inject, } from '@angular/core'; -import {ComponentFixture, fakeAsync, flush, TestBed, waitForAsync} from '@angular/core/testing'; -import {BehaviorSubject, combineLatest, Observable, of as observableOf} from 'rxjs'; +import {ComponentFixture, TestBed, fakeAsync, flush, waitForAsync} from '@angular/core/testing'; +import {BehaviorSubject, Observable, combineLatest, of as observableOf} from 'rxjs'; import {map} from 'rxjs/operators'; import {CdkColumnDef} from './cell'; import { @@ -22,7 +24,7 @@ import { StickyPositioningListener, StickyUpdate, } from './index'; -import {CdkHeaderRowDef, CdkRowDef, CdkCellOutlet, CdkNoDataRow} from './row'; +import {CdkCellOutlet, CdkHeaderRowDef, CdkNoDataRow, CdkRowDef} from './row'; import {CdkTable} from './table'; import { getTableDuplicateColumnNameError, @@ -32,7 +34,6 @@ import { getTableUnknownColumnError, getTableUnknownDataSourceError, } from './table-errors'; -import {BidiModule} from '@angular/cdk/bidi'; describe('CdkTable', () => { let fixture: ComponentFixture; @@ -44,7 +45,6 @@ describe('CdkTable', () => { declarations: any[] = [], ): ComponentFixture { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [CdkTableModule, BidiModule], declarations: [componentType, ...declarations], }).compileComponents(); @@ -285,6 +285,7 @@ describe('CdkTable', () => { // Remove column_a and swap column_b/column_c. component.columnsToRender = ['column_c', 'column_b']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); let changedTableContent = [['Column C', 'Column B']]; @@ -389,6 +390,7 @@ describe('CdkTable', () => { it('should render with data array input', () => { const data = baseData.slice(); component.dataSource = data; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const expectedRender = [ @@ -420,12 +422,14 @@ describe('CdkTable', () => { // Remove the data input entirely and expect no rows - just header. component.dataSource = null; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expectTableToMatchContent(tableElement, [expectedRender[0]]); // Add back the data to verify that it renders rows component.dataSource = data; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expectTableToMatchContent(tableElement, expectedRender); @@ -435,6 +439,7 @@ describe('CdkTable', () => { const data = baseData.slice(); const stream = new BehaviorSubject(data); component.dataSource = stream; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const expectedRender = [ @@ -470,12 +475,14 @@ describe('CdkTable', () => { // Remove the data input entirely and expect no rows - just header. component.dataSource = null; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expectTableToMatchContent(tableElement, [expectedRender[0]]); // Add back the data to verify that it renders rows component.dataSource = stream; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expectTableToMatchContent(tableElement, expectedRender); @@ -483,12 +490,14 @@ describe('CdkTable', () => { it('should throw an error if the data source is not valid', () => { component.dataSource = {invalid: 'dataSource'}; + fixture.changeDetectorRef.markForCheck(); expect(() => fixture.detectChanges()).toThrowError(getTableUnknownDataSourceError().message); }); it('should throw an error if the data source is not valid', () => { component.dataSource = undefined; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect the table to render just the header, no rows @@ -720,6 +729,7 @@ describe('CdkTable', () => { // Add a new column and expect it to show up in the table let columnA = 'columnA'; component.dynamicColumns.push(columnA); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expectTableToMatchContent(tableElement, [ [columnA], // Header row @@ -731,6 +741,7 @@ describe('CdkTable', () => { // Add another new column and expect it to show up in the table let columnB = 'columnB'; component.dynamicColumns.push(columnB); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expectTableToMatchContent(tableElement, [ [columnA, columnB], // Header row @@ -741,6 +752,7 @@ describe('CdkTable', () => { // Remove column A expect only column B to be rendered component.dynamicColumns.shift(); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expectTableToMatchContent(tableElement, [ [columnB], // Header row @@ -791,12 +803,9 @@ describe('CdkTable', () => { const whenRowWithoutDefaultFixture = createComponent(WhenRowWithoutDefaultCdkTableApp); const data = whenRowWithoutDefaultFixture.componentInstance.dataSource.data; expect(() => { - try { - whenRowWithoutDefaultFixture.detectChanges(); - flush(); - } catch { - flush(); - } + whenRowWithoutDefaultFixture.detectChanges(); + flush(); + fixture.detectChanges(); }).toThrowError(getTableMissingMatchingRowDefError(data[0]).message); })); @@ -812,6 +821,7 @@ describe('CdkTable', () => { it('should be able to render multiple rows per data object', () => { setupTableTestApp(WhenRowCdkTableApp); component.multiTemplateDataRows = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const data = component.dataSource.data; @@ -829,6 +839,7 @@ describe('CdkTable', () => { it('should have the correct data and row indicies', () => { setupTableTestApp(WhenRowCdkTableApp); component.multiTemplateDataRows = true; + fixture.changeDetectorRef.markForCheck(); component.showIndexColumns(); fixture.detectChanges(); @@ -849,6 +860,7 @@ describe('CdkTable', () => { () => { setupTableTestApp(WhenRowCdkTableApp); component.multiTemplateDataRows = true; + fixture.changeDetectorRef.markForCheck(); component.showIndexColumns(); const obj = {value: true}; @@ -970,6 +982,7 @@ describe('CdkTable', () => { it('should stick and unstick headers', waitForAsync(async () => { component.stickyHeaders = ['header-1', 'header-3']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); @@ -998,6 +1011,7 @@ describe('CdkTable', () => { expect(component.mostRecentStickyEndColumnsUpdate).toEqual({sizes: []}); component.stickyHeaders = []; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); expectNoStickyStyles(headerRows); @@ -1017,6 +1031,7 @@ describe('CdkTable', () => { it('should stick and unstick footers', waitForAsync(async () => { component.stickyFooters = ['footer-1', 'footer-3']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); @@ -1045,6 +1060,7 @@ describe('CdkTable', () => { expect(component.mostRecentStickyEndColumnsUpdate).toEqual({sizes: []}); component.stickyFooters = []; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); expectNoStickyStyles(footerRows); @@ -1064,6 +1080,7 @@ describe('CdkTable', () => { it('should stick the correct footer row', waitForAsync(async () => { component.stickyFooters = ['footer-3']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); @@ -1074,6 +1091,7 @@ describe('CdkTable', () => { it('should stick and unstick left columns', waitForAsync(async () => { component.stickyStartColumns = ['column-1', 'column-3']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); @@ -1121,6 +1139,7 @@ describe('CdkTable', () => { expect(component.mostRecentStickyEndColumnsUpdate).toEqual({sizes: []}); component.stickyStartColumns = []; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); headerRows.forEach(row => expectNoStickyStyles(getHeaderCells(row))); @@ -1142,6 +1161,7 @@ describe('CdkTable', () => { it('should stick and unstick right columns', waitForAsync(async () => { component.stickyEndColumns = ['column-4', 'column-6']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); @@ -1189,6 +1209,7 @@ describe('CdkTable', () => { }); component.stickyEndColumns = []; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); headerRows.forEach(row => expectNoStickyStyles(getHeaderCells(row))); @@ -1212,6 +1233,7 @@ describe('CdkTable', () => { component.dir = 'rtl'; component.stickyStartColumns = ['column-1', 'column-2']; component.stickyEndColumns = ['column-5', 'column-6']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); @@ -1256,6 +1278,7 @@ describe('CdkTable', () => { component.stickyFooters = ['footer-3']; component.stickyStartColumns = ['column-1']; component.stickyEndColumns = ['column-6']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); @@ -1309,6 +1332,7 @@ describe('CdkTable', () => { component.stickyFooters = []; component.stickyStartColumns = []; component.stickyEndColumns = []; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); @@ -1346,6 +1370,7 @@ describe('CdkTable', () => { it('should stick and unstick headers', waitForAsync(async () => { component.stickyHeaders = ['header-1', 'header-3']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); @@ -1378,6 +1403,7 @@ describe('CdkTable', () => { expect(component.mostRecentStickyEndColumnsUpdate).toEqual({sizes: []}); component.stickyHeaders = []; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); expectNoStickyStyles(headerRows); // No sticky styles on rows for native table @@ -1398,6 +1424,7 @@ describe('CdkTable', () => { it('should stick and unstick footers', waitForAsync(async () => { component.stickyFooters = ['footer-1', 'footer-3']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); @@ -1430,6 +1457,7 @@ describe('CdkTable', () => { expect(component.mostRecentStickyEndColumnsUpdate).toEqual({sizes: []}); component.stickyFooters = []; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); expectNoStickyStyles(footerRows); // No sticky styles on rows for native table @@ -1451,17 +1479,20 @@ describe('CdkTable', () => { it('should stick tfoot when all rows are stuck', waitForAsync(async () => { const tfoot = tableElement.querySelector('tfoot'); component.stickyFooters = ['footer-1']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); expectNoStickyStyles([tfoot]); component.stickyFooters = ['footer-1', 'footer-2', 'footer-3']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); expectStickyStyles(tfoot, '10', {bottom: '0px'}); expectStickyBorderClass(tfoot); component.stickyFooters = ['footer-1', 'footer-2']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); expectNoStickyStyles([tfoot]); @@ -1469,6 +1500,7 @@ describe('CdkTable', () => { it('should stick and unstick left columns', waitForAsync(async () => { component.stickyStartColumns = ['column-1', 'column-3']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); @@ -1516,6 +1548,7 @@ describe('CdkTable', () => { expect(component.mostRecentStickyEndColumnsUpdate).toEqual({sizes: []}); component.stickyStartColumns = []; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); headerRows.forEach(row => expectNoStickyStyles(getHeaderCells(row))); @@ -1537,6 +1570,7 @@ describe('CdkTable', () => { it('should stick and unstick right columns', waitForAsync(async () => { component.stickyEndColumns = ['column-4', 'column-6']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); @@ -1584,6 +1618,7 @@ describe('CdkTable', () => { }); component.stickyEndColumns = []; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); headerRows.forEach(row => expectNoStickyStyles(getHeaderCells(row))); @@ -1608,6 +1643,7 @@ describe('CdkTable', () => { component.stickyFooters = ['footer-3']; component.stickyStartColumns = ['column-1']; component.stickyEndColumns = ['column-6']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); @@ -1671,6 +1707,7 @@ describe('CdkTable', () => { component.stickyFooters = []; component.stickyStartColumns = []; component.stickyEndColumns = []; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); @@ -1837,6 +1874,7 @@ describe('CdkTable', () => { // Add a data source that has initialized data. Expect that the table shows this data. const dynamicDataSource = new FakeDataSource(); component.dataSource = dynamicDataSource; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(dynamicDataSource.isConnected).toBe(true); @@ -1845,6 +1883,7 @@ describe('CdkTable', () => { // Remove the data source and check to make sure the table is empty again. component.dataSource = undefined; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the old data source has been disconnected. @@ -1854,6 +1893,7 @@ describe('CdkTable', () => { // Reconnect a data source and check that the table is populated const newDynamicDataSource = new FakeDataSource(); component.dataSource = newDynamicDataSource; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(newDynamicDataSource.isConnected).toBe(true); @@ -1881,6 +1921,7 @@ describe('CdkTable', () => { // Enable all the context classes component.enableRowContextClasses = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(rowElements[0].classList.contains('custom-row-class-first')).toBe(true); @@ -1917,6 +1958,7 @@ describe('CdkTable', () => { // Enable the context classes component.enableCellContextClasses = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); let cellElement = rowElements[0].querySelectorAll('cdk-cell')[0]; @@ -2205,6 +2247,7 @@ class WhenRowCdkTableApp { columnsForHasC3Row = ['c3Column']; isIndex1 = (index: number, _rowData: TestData) => index == 1; hasC3 = (_index: number, rowData: TestData) => rowData.c == 'c_3'; + cdr = inject(ChangeDetectorRef); constructor() { this.dataSource.addData(); @@ -2217,6 +2260,7 @@ class WhenRowCdkTableApp { this.columnsToRender = indexColumns; this.columnsForIsIndex1Row = indexColumns; this.columnsForHasC3Row = indexColumns; + this.cdr.markForCheck(); } } @@ -2669,10 +2713,12 @@ class MissingColumnDefCdkTableApp { class MissingColumnDefAfterRenderCdkTableApp implements AfterViewInit { dataSource: FakeDataSource | null = null; displayedColumns: string[] = []; + cdr = inject(ChangeDetectorRef); ngAfterViewInit() { setTimeout(() => { this.displayedColumns = ['column_a']; + this.cdr.markForCheck(); }, 0); } } diff --git a/src/cdk/table/text-column.spec.ts b/src/cdk/table/text-column.spec.ts index 9d0f06f89ae7..3bdd9616204f 100644 --- a/src/cdk/table/text-column.spec.ts +++ b/src/cdk/table/text-column.spec.ts @@ -1,9 +1,9 @@ -import {Component, provideZoneChangeDetection} from '@angular/core'; -import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing'; +import {Component} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import { - getTableTextColumnMissingParentTableError, getTableTextColumnMissingNameError, + getTableTextColumnMissingParentTableError, } from './table-errors'; import {CdkTableModule} from './table-module'; import {expectTableToMatchContent} from './table.spec'; @@ -16,7 +16,6 @@ describe('CdkTextColumn', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [CdkTableModule, BasicTextColumnApp, MissingTableApp, TextColumnWithoutNameApp], }).compileComponents(); })); @@ -51,6 +50,7 @@ describe('CdkTextColumn', () => { it('should allow for alternate header text', () => { component.headerTextB = 'column-b'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expectTableToMatchContent(tableElement, [ @@ -62,6 +62,7 @@ describe('CdkTextColumn', () => { it('should allow for custom data accessor', () => { component.dataAccessorA = (data: TestData) => data.propertyA + '!'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expectTableToMatchContent(tableElement, [ @@ -73,6 +74,7 @@ describe('CdkTextColumn', () => { it('should allow for custom data accessor', () => { component.dataAccessorA = (data: TestData) => data.propertyA + '!'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expectTableToMatchContent(tableElement, [ @@ -87,6 +89,7 @@ describe('CdkTextColumn', () => { {propertyA: 'changed-a_1', propertyB: 'b_1', propertyC: 'c_1'}, {propertyA: 'changed-a_2', propertyB: 'b_2', propertyC: 'c_2'}, ]; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expectTableToMatchContent(tableElement, [ diff --git a/src/cdk/text-field/autofill.spec.ts b/src/cdk/text-field/autofill.spec.ts index 2268559e1ac4..6f14e907430c 100644 --- a/src/cdk/text-field/autofill.spec.ts +++ b/src/cdk/text-field/autofill.spec.ts @@ -7,8 +7,8 @@ */ import {normalizePassiveListenerOptions} from '@angular/cdk/platform'; -import {Component, ElementRef, NgZone, ViewChild, provideZoneChangeDetection} from '@angular/core'; -import {ComponentFixture, inject, TestBed} from '@angular/core/testing'; +import {Component, ElementRef, ViewChild} from '@angular/core'; +import {ComponentFixture, TestBed, inject} from '@angular/core/testing'; import {EMPTY} from 'rxjs'; import {AutofillEvent, AutofillMonitor} from './autofill'; import {TextFieldModule} from './text-field-module'; @@ -22,7 +22,6 @@ describe('AutofillMonitor', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [TextFieldModule, Inputs], }).compileComponents(); }); @@ -154,22 +153,6 @@ describe('AutofillMonitor', () => { expect(spy).toHaveBeenCalled(); }); - it('should emit on stream inside the NgZone', () => { - const inputEl = testComponent.input1.nativeElement; - let animationStartCallback: Function = () => {}; - inputEl.addEventListener.and.callFake( - (_: string, cb: Function) => (animationStartCallback = cb), - ); - const autofillStream = autofillMonitor.monitor(inputEl); - const spy = jasmine.createSpy('autofill spy'); - - autofillStream.subscribe(() => spy(NgZone.isInAngularZone())); - expect(spy).not.toHaveBeenCalled(); - - animationStartCallback({animationName: 'cdk-text-field-autofill-start', target: inputEl}); - expect(spy).toHaveBeenCalledWith(true); - }); - it('should not emit on init if input is unfilled', () => { const inputEl = testComponent.input1.nativeElement; let animationStartCallback: Function = () => {}; diff --git a/src/cdk/text-field/autofill.zone.spec.ts b/src/cdk/text-field/autofill.zone.spec.ts new file mode 100644 index 000000000000..5efa4704cca5 --- /dev/null +++ b/src/cdk/text-field/autofill.zone.spec.ts @@ -0,0 +1,61 @@ +import {Component, ElementRef, NgZone, ViewChild, provideZoneChangeDetection} from '@angular/core'; +import {ComponentFixture, TestBed, inject} from '@angular/core/testing'; +import {AutofillMonitor} from './autofill'; +import {TextFieldModule} from './text-field-module'; + +describe('AutofillMonitor', () => { + let autofillMonitor: AutofillMonitor; + let fixture: ComponentFixture; + let testComponent: Inputs; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideZoneChangeDetection()], + imports: [TextFieldModule, Inputs], + }).compileComponents(); + }); + + beforeEach(inject([AutofillMonitor], (afm: AutofillMonitor) => { + autofillMonitor = afm; + fixture = TestBed.createComponent(Inputs); + testComponent = fixture.componentInstance; + fixture.detectChanges(); + + for (const input of [testComponent.input1, testComponent.input2, testComponent.input3]) { + spyOn(input.nativeElement, 'addEventListener'); + spyOn(input.nativeElement, 'removeEventListener'); + } + })); + + it('should emit on stream inside the NgZone', () => { + const inputEl = testComponent.input1.nativeElement; + let animationStartCallback: Function = () => {}; + inputEl.addEventListener.and.callFake( + (_: string, cb: Function) => (animationStartCallback = cb), + ); + const autofillStream = autofillMonitor.monitor(inputEl); + const spy = jasmine.createSpy('autofill spy'); + + autofillStream.subscribe(() => spy(NgZone.isInAngularZone())); + expect(spy).not.toHaveBeenCalled(); + + animationStartCallback({animationName: 'cdk-text-field-autofill-start', target: inputEl}); + expect(spy).toHaveBeenCalledWith(true); + }); +}); + +@Component({ + template: ` + + + + `, + standalone: true, + imports: [TextFieldModule], +}) +class Inputs { + // Cast to `any` so we can stub out some methods in the tests. + @ViewChild('input1') input1: ElementRef; + @ViewChild('input2') input2: ElementRef; + @ViewChild('input3') input3: ElementRef; +} diff --git a/src/cdk/text-field/autosize.spec.ts b/src/cdk/text-field/autosize.spec.ts index 7338b5dc5faa..ad3b159484d9 100644 --- a/src/cdk/text-field/autosize.spec.ts +++ b/src/cdk/text-field/autosize.spec.ts @@ -1,16 +1,16 @@ -import {dispatchFakeEvent} from '../testing/private'; -import {Component, ViewChild, provideZoneChangeDetection} from '@angular/core'; +import {Component, ViewChild} from '@angular/core'; import { - waitForAsync, ComponentFixture, + TestBed, fakeAsync, flush, - TestBed, tick, + waitForAsync, } from '@angular/core/testing'; import {FormsModule} from '@angular/forms'; import {By} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {dispatchFakeEvent} from '../testing/private'; import {CdkTextareaAutosize} from './autosize'; import {TextFieldModule} from './text-field-module'; @@ -21,7 +21,6 @@ describe('CdkTextareaAutosize', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [ FormsModule, TextFieldModule, @@ -103,6 +102,7 @@ describe('CdkTextareaAutosize', () => { As of some one gently rapping, rapping at my chamber door. “’Tis some visitor,” I muttered, “tapping at my chamber door— Only this and nothing more.”`; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); @@ -127,6 +127,7 @@ describe('CdkTextareaAutosize', () => { expect(textarea.style.minHeight).toBeFalsy(); fixture.componentInstance.minRows = 4; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(textarea.style.minHeight) @@ -135,6 +136,7 @@ describe('CdkTextareaAutosize', () => { let previousMinHeight = parseInt(textarea.style.minHeight as string); fixture.componentInstance.minRows = 6; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(parseInt(textarea.style.minHeight as string)) @@ -146,6 +148,7 @@ describe('CdkTextareaAutosize', () => { expect(textarea.style.maxHeight).toBeFalsy(); fixture.componentInstance.maxRows = 4; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(textarea.style.maxHeight) @@ -154,6 +157,7 @@ describe('CdkTextareaAutosize', () => { let previousMaxHeight = parseInt(textarea.style.maxHeight as string); fixture.componentInstance.maxRows = 6; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(parseInt(textarea.style.maxHeight as string)) @@ -165,6 +169,7 @@ describe('CdkTextareaAutosize', () => { expect(textarea.style.minHeight).toBeFalsy(); fixture.componentInstance.minRows = 6; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(textarea.style.minHeight) @@ -173,6 +178,7 @@ describe('CdkTextareaAutosize', () => { let previousHeight = parseInt(textarea.style.height!); fixture.componentInstance.minRows = 3; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(parseInt(textarea.style.height!)) @@ -191,6 +197,7 @@ describe('CdkTextareaAutosize', () => { .toBe(1); fixture.componentInstance.minRows = 1; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(textarea.rows) @@ -200,6 +207,7 @@ describe('CdkTextareaAutosize', () => { const previousMinHeight = parseInt(textarea.style.minHeight as string); fixture.componentInstance.minRows = 2; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(textarea.rows) @@ -224,6 +232,7 @@ describe('CdkTextareaAutosize', () => { .toBe(textarea.scrollHeight); fixture.componentInstance.maxRows = 5; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(textarea.clientHeight) @@ -246,6 +255,7 @@ describe('CdkTextareaAutosize', () => { Line Line Line`; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); @@ -269,6 +279,7 @@ describe('CdkTextareaAutosize', () => { Line Line Line`; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); @@ -291,6 +302,7 @@ describe('CdkTextareaAutosize', () => { “’Tis some visitor entreating entrance at my chamber door— Some late visitor entreating entrance at my chamber door;— This it is and nothing more.” `; + fixtureWithForms.changeDetectorRef.markForCheck(); fixtureWithForms.detectChanges(); flush(); fixtureWithForms.detectChanges(); @@ -308,6 +320,7 @@ describe('CdkTextareaAutosize', () => { if a woodchuck could chuck wood? `; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); fixture.detectChanges(); @@ -343,6 +356,7 @@ describe('CdkTextareaAutosize', () => { Line Line Line`; + fixtureWithoutAutosize.changeDetectorRef.markForCheck(); // Manually call resizeToFitContent instead of faking an `input` event. fixtureWithoutAutosize.detectChanges(); @@ -377,6 +391,7 @@ describe('CdkTextareaAutosize', () => { it('should handle an undefined placeholder', () => { fixture.componentInstance.placeholder = undefined!; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(textarea.hasAttribute('placeholder')).toBe(false); diff --git a/src/cdk/tree/tree.spec.ts b/src/cdk/tree/tree.spec.ts index 2e21b1bb1a62..db8d9eedbb15 100644 --- a/src/cdk/tree/tree.spec.ts +++ b/src/cdk/tree/tree.spec.ts @@ -5,28 +5,29 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing'; import { + ChangeDetectorRef, Component, ErrorHandler, - ViewChild, + EventEmitter, + QueryList, TrackByFunction, Type, - EventEmitter, + ViewChild, ViewChildren, - QueryList, - provideZoneChangeDetection, + inject, } from '@angular/core'; +import {ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing'; +import {Direction, Directionality} from '@angular/cdk/bidi'; import {CollectionViewer, DataSource} from '@angular/cdk/collections'; -import {Directionality, Direction} from '@angular/cdk/bidi'; -import {combineLatest, BehaviorSubject, Observable} from 'rxjs'; +import {BehaviorSubject, Observable, combineLatest} from 'rxjs'; import {map} from 'rxjs/operators'; import {BaseTreeControl} from './control/base-tree-control'; -import {TreeControl} from './control/tree-control'; import {FlatTreeControl} from './control/flat-tree-control'; import {NestedTreeControl} from './control/nested-tree-control'; +import {TreeControl} from './control/tree-control'; import {CdkTreeModule, CdkTreeNodePadding} from './index'; import {CdkTree, CdkTreeNode} from './tree'; import {getTreeControlFunctionsMissingError} from './tree-errors'; @@ -43,7 +44,6 @@ describe('CdkTree', () => { TestBed.configureTestingModule({ imports: [CdkTreeModule], providers: [ - provideZoneChangeDetection(), { provide: Directionality, useFactory: () => (dir = {value: 'ltr', change: new EventEmitter()}), @@ -134,6 +134,7 @@ describe('CdkTree', () => { // add a child to the first node let data = dataSource.data; dataSource.addChild(data[0], true); + fixture.detectChanges(); const ariaLevels = getNodes(treeElement).map(n => n.getAttribute('aria-level')); expect(ariaLevels).toEqual(['2', '3', '2', '2']); @@ -191,6 +192,7 @@ describe('CdkTree', () => { it('should be able to use units different from px for the indentation', () => { component.indent = '15rem'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const data = dataSource.data; @@ -207,6 +209,7 @@ describe('CdkTree', () => { it('should default to px if no unit is set for string value indentation', () => { component.indent = '17'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const data = dataSource.data; @@ -241,6 +244,7 @@ describe('CdkTree', () => { const node = getNodes(treeElement)[0]; component.indent = 10; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(node.style.paddingLeft).toBe('10px'); @@ -279,6 +283,7 @@ describe('CdkTree', () => { .toBe(0); component.toggleRecursively = false; + fixture.changeDetectorRef.markForCheck(); let data = dataSource.data; dataSource.addChild(data[2]); fixture.detectChanges(); @@ -807,6 +812,7 @@ describe('CdkTree', () => { ).toBe(true); component.toggleRecursively = false; + fixture.changeDetectorRef.markForCheck(); let data = dataSource.data; const child = dataSource.addChild(data[1], false); dataSource.addChild(child, false); @@ -821,6 +827,7 @@ describe('CdkTree', () => { it('should expand/collapse the node multiple times', () => { component.toggleRecursively = false; + fixture.changeDetectorRef.markForCheck(); let data = dataSource.data; const child = dataSource.addChild(data[1], false); dataSource.addChild(child, false); @@ -1136,8 +1143,6 @@ describe('CdkTree', () => { try { TestBed.createComponent(NestedCdkErrorTreeApp).detectChanges(); flush(); - } catch { - flush(); } finally { flush(); } @@ -1147,12 +1152,8 @@ describe('CdkTree', () => { it('should throw an error when missing function in flat tree', fakeAsync(() => { configureCdkTreeTestingModule([FlatCdkErrorTreeApp]); expect(() => { - try { - TestBed.createComponent(FlatCdkErrorTreeApp).detectChanges(); - flush(); - } catch { - flush(); - } + TestBed.createComponent(FlatCdkErrorTreeApp).detectChanges(); + flush(); }).toThrowError(getTreeControlFunctionsMissingError().message); })); }); @@ -1574,6 +1575,14 @@ class ArrayDataSourceCdkTreeApp { } @ViewChild(CdkTree) tree: CdkTree; + + cdr = inject(ChangeDetectorRef); + + constructor() { + this.dataSource._dataChange.subscribe(() => { + this.cdr.markForCheck(); + }); + } } @Component({ diff --git a/src/cdk/tree/tree.ts b/src/cdk/tree/tree.ts index 2b1ee04a1772..9746981bdf4f 100644 --- a/src/cdk/tree/tree.ts +++ b/src/cdk/tree/tree.ts @@ -26,17 +26,18 @@ import { ViewChild, ViewContainerRef, ViewEncapsulation, + inject, numberAttribute, } from '@angular/core'; import { BehaviorSubject, - isObservable, Observable, - of as observableOf, Subject, Subscription, + isObservable, + of as observableOf, } from 'rxjs'; -import {takeUntil} from 'rxjs/operators'; +import {distinctUntilChanged, map, takeUntil} from 'rxjs/operators'; import {TreeControl} from './control/tree-control'; import {CdkTreeNodeDef, CdkTreeNodeOutletContext} from './node'; import {CdkTreeNodeOutlet} from './outlet'; @@ -255,6 +256,8 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, }, ); + // TODO: change to `this._changeDetectorRef.markForCheck()`, or just switch this component to + // use signals. this._changeDetectorRef.detectChanges(); } @@ -381,6 +384,8 @@ export class CdkTreeNode implements FocusableOption, OnDestroy, OnInit : this._parentNodeAriaLevel; } + private _changeDetectorRef = inject(ChangeDetectorRef); + constructor( protected _elementRef: ElementRef, protected _tree: CdkTree, @@ -392,6 +397,14 @@ export class CdkTreeNode implements FocusableOption, OnDestroy, OnInit ngOnInit(): void { this._parentNodeAriaLevel = getParentNodeAriaLevel(this._elementRef.nativeElement); this._elementRef.nativeElement.setAttribute('aria-level', `${this.level + 1}`); + this._tree.treeControl.expansionModel.changed + .pipe( + map(() => this.isExpanded), + distinctUntilChanged(), + ) + .subscribe(() => { + this._changeDetectorRef.markForCheck(); + }); } ngOnDestroy() { From 9c53b7a7ef4473be719cbb0a80fcc1698e769ada Mon Sep 17 00:00:00 2001 From: Naji <54370141+naaajii@users.noreply.github.com> Date: Mon, 3 Jun 2024 14:30:42 +0500 Subject: [PATCH 13/61] refactor(material/schematics): add project name for theme styles & backwards compatibility (#29169) adds project name to commented theme styles & backwards compatibility mixins on generating custom theme --- src/material/schematics/ng-add/theming/create-custom-theme.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/material/schematics/ng-add/theming/create-custom-theme.ts b/src/material/schematics/ng-add/theming/create-custom-theme.ts index f5cd62cefaed..364a65457eee 100644 --- a/src/material/schematics/ng-add/theming/create-custom-theme.ts +++ b/src/material/schematics/ng-add/theming/create-custom-theme.ts @@ -40,9 +40,9 @@ $${name}-theme: mat.define-theme(( // Comment out the line below if you want to use the pre-defined typography utility classes. // For more information: https://material.angular.io/guide/typography#using-typography-styles-in-your-application. -// @include mat.typography-hierarchy($theme); +// @include mat.typography-hierarchy($${name}-theme); // Comment out the line below if you want to use the deprecated \`color\` inputs. -// @include mat.color-variants-backwards-compatibility($theme); +// @include mat.color-variants-backwards-compatibility($${name}-theme); `; } From 7b48b54cc13c7c7804db82e74603e815c2ac0b9c Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 3 Jun 2024 18:24:27 +0200 Subject: [PATCH 14/61] refactor(material/table): simplify structural styles (#29168) Simplifies the table's structural styles to make them smaller and easier to maintain. Also fixes an error in the table demo. --- src/dev-app/main.ts | 2 + src/material/core/style/_vendor-prefixes.scss | 5 ++ src/material/table/table.scss | 55 +++++++++++-------- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/dev-app/main.ts b/src/dev-app/main.ts index 55dc303a8408..19e8d61e4d6f 100644 --- a/src/dev-app/main.ts +++ b/src/dev-app/main.ts @@ -16,6 +16,7 @@ import { } from '@angular/core'; import {bootstrapApplication} from '@angular/platform-browser'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {provideHttpClient} from '@angular/common/http'; import {RouterModule} from '@angular/router'; import {Directionality} from '@angular/cdk/bidi'; @@ -51,6 +52,7 @@ function bootstrap(): void { RouterModule.forRoot(DEV_APP_ROUTES), ), provideNativeDateAdapter(), + provideHttpClient(), {provide: OverlayContainer, useClass: FullscreenOverlayContainer}, {provide: MAT_RIPPLE_GLOBAL_OPTIONS, useExisting: DevAppRippleOptions}, {provide: Directionality, useClass: DevAppDirectionality}, diff --git a/src/material/core/style/_vendor-prefixes.scss b/src/material/core/style/_vendor-prefixes.scss index 298f582a52a0..ee0d67e3e6a1 100644 --- a/src/material/core/style/_vendor-prefixes.scss +++ b/src/material/core/style/_vendor-prefixes.scss @@ -43,4 +43,9 @@ -webkit-clip-path: $value; clip-path: $value; } + +@mixin smooth-font { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; +} // stylelint-enable diff --git a/src/material/table/table.scss b/src/material/table/table.scss index e45134a8dae3..42e05e907c3f 100644 --- a/src/material/table/table.scss +++ b/src/material/table/table.scss @@ -1,11 +1,7 @@ -@use '@material/data-table/data-table' as mdc-data-table; -@use '@material/data-table/data-table-cell' as mdc-data-table-cell; -@use '@material/data-table/data-table-header-cell' as mdc-data-table-header-cell; -@use '@material/data-table' as mdc-data-table-theme; -@use '@material/typography/typography' as mdc-typography; @use '../core/mdc-helpers/mdc-helpers'; @use '../core/tokens/token-utils'; @use '../core/tokens/m2/mat/table' as tokens-mat-table; +@use '../core/style/vendor-prefixes'; @use './table-flex-styles'; .mat-mdc-table-sticky { @@ -21,25 +17,13 @@ border-bottom-style: solid; } -@include mdc-data-table.static-styles($query: mdc-helpers.$mdc-base-styles-query); -@include mdc-data-table-cell.static-styles($query: mdc-helpers.$mdc-base-styles-query); -@include mdc-data-table-header-cell.static-styles($query: mdc-helpers.$mdc-base-styles-query); -@include mdc-data-table-theme.cell-padding( - $leading-padding: mdc-data-table-theme.$cell-leading-padding, - $trailing-padding: mdc-data-table-theme.$cell-trailing-padding, - $query: mdc-helpers.$mdc-base-styles-query -); @include table-flex-styles.private-table-flex-styles(); .mat-mdc-table { - // MDC Table applies `table-layout: fixed`, but this is a backwards incompatible - // change since the table did not previously apply it. - // TODO: Add a mixin to MDC to set the layout instead of including this override, - // see this issue: https://github.com/material-components/material-components-web/issues/6412 + min-width: 100%; + border: 0; + border-spacing: 0; table-layout: auto; - - // The MDC table does not allow text to wrap within the cell. This allows for text to - // wrap when the cell reaches its maximum width. white-space: normal; @include token-utils.use-tokens(tokens-mat-table.$prefix, tokens-mat-table.get-token-slots()) { @@ -47,12 +31,28 @@ } } +.mdc-data-table__cell { + box-sizing: border-box; + overflow: hidden; + text-align: left; + text-overflow: ellipsis; + + [dir='rtl'] & { + text-align: right; + } +} + +.mdc-data-table__cell, +.mdc-data-table__header-cell { + padding: 0 16px; +} + @include mdc-helpers.disable-mdc-fallback-declarations { @include token-utils.use-tokens(tokens-mat-table.$prefix, tokens-mat-table.get-token-slots()) { // TODO(crisbeto): these tokens have default values in order to make the initial token // work easier to land in g3. Eventually we should remove them. .mat-mdc-header-row { - @include mdc-typography.smooth-font(); + @include vendor-prefixes.smooth-font; @include token-utils.create-token-slot(height, header-container-height, 56px); @include token-utils.create-token-slot(color, header-headline-color, true); @include token-utils.create-token-slot(font-family, header-headline-font, true); @@ -72,7 +72,7 @@ // letter spacing which leads to a lot of internal screenshot diffs. .mat-mdc-row, .mdc-data-table__content { - @include mdc-typography.smooth-font(); + @include vendor-prefixes.smooth-font; @include token-utils.create-token-slot(font-family, row-item-label-text-font, true); @include token-utils.create-token-slot(line-height, row-item-label-text-line-height); @include token-utils.create-token-slot(font-size, row-item-label-text-size, 14px); @@ -80,7 +80,7 @@ } .mat-mdc-footer-row { - @include mdc-typography.smooth-font(); + @include vendor-prefixes.smooth-font; @include token-utils.create-token-slot(height, footer-container-height, 52px); @include token-utils.create-token-slot(color, row-item-label-text-color, true); @include token-utils.create-token-slot(font-family, footer-supporting-text-font, true); @@ -95,6 +95,15 @@ @include token-utils.create-token-slot(letter-spacing, header-headline-tracking); font-weight: inherit; line-height: inherit; + box-sizing: border-box; + text-overflow: ellipsis; + overflow: hidden; + outline: none; + text-align: left; + + [dir='rtl'] & { + text-align: right; + } } .mat-mdc-cell { From c9e1d4aedd929b611b908633afac28a2bfb474d2 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 3 Jun 2024 18:24:58 +0200 Subject: [PATCH 15/61] fix(material/schematics): theming API migration not working with CRLF line endings (#29171) Fixes that the migration didn't account for CRLF line endings when figuring out the Sass namespace. Fixes #29147. --- .../migrations/m2-theming-v18/migration.ts | 2 +- .../ng-update/test-cases/m2-theming.spec.ts | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/material/schematics/ng-update/migrations/m2-theming-v18/migration.ts b/src/material/schematics/ng-update/migrations/m2-theming-v18/migration.ts index 9733303b654f..b548cdf56d4b 100644 --- a/src/material/schematics/ng-update/migrations/m2-theming-v18/migration.ts +++ b/src/material/schematics/ng-update/migrations/m2-theming-v18/migration.ts @@ -271,7 +271,7 @@ function extractNamespaceFromUseStatement(fullImport: string): string { function getNamespaces(moduleName: string, content: string): string[] { const namespaces = new Set(); const escapedName = moduleName.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); - const pattern = new RegExp(`@use +['"]${escapedName}['"].*;?\n`, 'g'); + const pattern = new RegExp(`@use +['"]${escapedName}['"].*;?\\r?\\n`, 'g'); let match: RegExpExecArray | null = null; while ((match = pattern.exec(content))) { diff --git a/src/material/schematics/ng-update/test-cases/m2-theming.spec.ts b/src/material/schematics/ng-update/test-cases/m2-theming.spec.ts index 41990ef8aaf4..d34716714a42 100644 --- a/src/material/schematics/ng-update/test-cases/m2-theming.spec.ts +++ b/src/material/schematics/ng-update/test-cases/m2-theming.spec.ts @@ -309,4 +309,46 @@ describe('M2 theming migration', () => { `@include matx.something-not-theming-related();`, ]); }); + + it('should migrate usages of the M2 theming APIs in a file with CRLF endings', async () => { + const result = await setup( + [ + `@use '@angular/material' as mat;`, + + `$my-primary: mat.define-palette(mat.$indigo-palette, 500);`, + `$my-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);`, + `$my-warn: mat.define-palette(mat.$red-palette);`, + + `$my-theme: mat.define-light-theme((`, + ` color: (`, + ` primary: $my-primary,`, + ` accent: $my-accent,`, + ` warn: $my-warn,`, + ` ),`, + ` typography: mat.define-typography-config(),`, + ` density: 0,`, + `));`, + `@include mat.all-component-themes($my-theme);`, + ].join('\r\n'), + ); + + expect(result.split('\r\n')).toEqual([ + `@use '@angular/material' as mat;`, + + `$my-primary: mat.m2-define-palette(mat.$m2-indigo-palette, 500);`, + `$my-accent: mat.m2-define-palette(mat.$m2-pink-palette, A200, A100, A400);`, + `$my-warn: mat.m2-define-palette(mat.$m2-red-palette);`, + + `$my-theme: mat.m2-define-light-theme((`, + ` color: (`, + ` primary: $my-primary,`, + ` accent: $my-accent,`, + ` warn: $my-warn,`, + ` ),`, + ` typography: mat.m2-define-typography-config(),`, + ` density: 0,`, + `));`, + `@include mat.all-component-themes($my-theme);`, + ]); + }); }); From b9fedfe9f900111d749262b2e25cc2fff8aac01f Mon Sep 17 00:00:00 2001 From: Amy Sorto <8575252+amysorto@users.noreply.github.com> Date: Mon, 3 Jun 2024 12:13:24 -0700 Subject: [PATCH 16/61] fix(material/datepicker): Move aria-live attribute so month can also be announced when using previous and next month buttons (#29137) --- src/material/datepicker/calendar-header.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/material/datepicker/calendar-header.html b/src/material/datepicker/calendar-header.html index 8302fc166bef..d6cc038e68ed 100644 --- a/src/material/datepicker/calendar-header.html +++ b/src/material/datepicker/calendar-header.html @@ -1,12 +1,12 @@
- - + @@ -28,7 +28,7 @@
Icon with a badge - home + home diff --git a/src/material/badge/badge.md b/src/material/badge/badge.md index 9a636d75d053..c3263b2e94df 100644 --- a/src/material/badge/badge.md +++ b/src/material/badge/badge.md @@ -1,4 +1,4 @@ -Badges are small status descriptors for UI elements. A badge consists of a small circle, +Badges are small status descriptors for UI elements. A badge consists of a small circle, typically containing a number or other short set of characters, that appears in proximity to another object. @@ -13,7 +13,7 @@ By default, the badge will be placed `above after`. The direction can be changed the attribute `matBadgePosition` follow by `above|below` and `before|after`. The overlap of the badge in relation to its inner contents can also be defined @@ -21,32 +21,27 @@ using the `matBadgeOverlap` tag. Typically, you want the badge to overlap an ico a text phrase. By default it will overlap. ### Badge sizing The badge has 3 sizes: `small`, `medium` and `large`. By default, the badge is set to `medium`. + +Badges that are `small` do not show the label text. This can be useful in contexts such as showing there are unread notifications but not needing to show the exact amount. + You can change the size by adding `matBadgeSize` to the host element. ### Badge visibility The badge visibility can be toggled programmatically by defining `matBadgeHidden`. -### Theming -Badges can be colored in terms of the current theme using the `matBadgeColor` property to set the -background color to `primary`, `accent`, or `warn`. - - - ### Accessibility You must provide a meaningful description via `matBadgeDescription`. When attached to an interactive element, `MatBadge` applies this description to its host via `aria-describedby`. When attached to diff --git a/src/material/badge/badge.ts b/src/material/badge/badge.ts index 14f8a9daf616..4b8676981409 100644 --- a/src/material/badge/badge.ts +++ b/src/material/badge/badge.ts @@ -83,7 +83,10 @@ export class _MatBadgeStyleLoader {} standalone: true, }) export class MatBadge implements OnInit, OnDestroy { - /** The color of the badge. Can be `primary`, `accent`, or `warn`. */ + /** + * The color of the badge. Can be `primary`, `accent`, or `warn`. + * Not recommended in M3, for more information see https://material.angular.io/guide/material-2-theming#optional-add-backwards-compatibility-styles-for-color-variants. + */ @Input('matBadgeColor') get color(): ThemePalette { return this._color; From 569603e71a668790745449bf5e0973030bc26932 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 7 Jun 2024 12:50:55 +0200 Subject: [PATCH 40/61] test(cdk/drag-drop): fix up flawed test cases Fixes some test cases that don't behave like a user would do, like hovering over an item that is out of the screen. They worked because currently the drag&drop is almost purely coordinate-based, but that won't be the case with the `mixed` orientation. Also fixes some concatenated test names that were difficult to search for. --- .../directives/drop-list-shared.spec.ts | 1097 ++++++++--------- .../directives/single-axis-drop-list.spec.ts | 40 + .../drag-drop/directives/test-utils.spec.ts | 2 +- 3 files changed, 541 insertions(+), 598 deletions(-) diff --git a/src/cdk/drag-drop/directives/drop-list-shared.spec.ts b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts index 10d1808caaa4..c989d60e6d56 100644 --- a/src/cdk/drag-drop/directives/drop-list-shared.spec.ts +++ b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts @@ -405,31 +405,27 @@ export function defineCommonDropListTests(config: { flush(); })); - it( - 'should not dispatch the `sorted` event when an item is dragged inside ' + - 'a single-item list', - fakeAsync(() => { - const fixture = createComponent(DraggableInDropZone); - fixture.componentInstance.items = [fixture.componentInstance.items[0]]; - fixture.detectChanges(); + it('should not dispatch the `sorted` event when an item is dragged inside a single-item list', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.componentInstance.items = [fixture.componentInstance.items[0]]; + fixture.detectChanges(); - const draggedItem = fixture.componentInstance.dragItems.first.element.nativeElement; - const {top, left} = draggedItem.getBoundingClientRect(); + const draggedItem = fixture.componentInstance.dragItems.first.element.nativeElement; + const {top, left} = draggedItem.getBoundingClientRect(); - startDraggingViaMouse(fixture, draggedItem, left, top); + startDraggingViaMouse(fixture, draggedItem, left, top); - for (let i = 0; i < 5; i++) { - dispatchMouseEvent(document, 'mousemove', left, top + 1); - fixture.detectChanges(); + for (let i = 0; i < 5; i++) { + dispatchMouseEvent(document, 'mousemove', left, top + 1); + fixture.detectChanges(); - expect(fixture.componentInstance.sortedSpy).not.toHaveBeenCalled(); - } + expect(fixture.componentInstance.sortedSpy).not.toHaveBeenCalled(); + } - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - flush(); - }), - ); + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + })); it('should not move items in a vertical list if the pointer is too far away', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); @@ -1512,7 +1508,7 @@ export function defineCommonDropListTests(config: { fixture.detectChanges(); const cleanup = makeScrollable(); - scrollTo(0, 500); + scrollTo(0, 5000); config.assertDownwardSorting( fixture, fixture.componentInstance.dragItems.map(item => { @@ -1538,7 +1534,7 @@ export function defineCommonDropListTests(config: { fixture.detectChanges(); const cleanup = makeScrollable(); - scrollTo(0, 500); + scrollTo(0, 5000); config.assertUpwardSorting( fixture, fixture.componentInstance.dragItems.map(item => { @@ -1602,35 +1598,31 @@ export function defineCommonDropListTests(config: { flush(); })); - it( - 'should lay out the elements correctly, if an element skips multiple positions when ' + - 'sorting vertically', - fakeAsync(() => { - const fixture = createComponent(DraggableInDropZone); - fixture.detectChanges(); + it('should lay out the elements correctly, if an element skips multiple positions when sorting vertically', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); - const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); - const draggedItem = items[0]; - const {top, left} = draggedItem.getBoundingClientRect(); + const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); + const draggedItem = items[0]; + const {top, left} = draggedItem.getBoundingClientRect(); - startDraggingViaMouse(fixture, draggedItem, left, top); + startDraggingViaMouse(fixture, draggedItem, left, top); - const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - const targetRect = items[items.length - 1].getBoundingClientRect(); + const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; + const targetRect = items[items.length - 1].getBoundingClientRect(); - // Add a few pixels to the top offset so we get some overlap. - dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top + 5); - fixture.detectChanges(); + // Add a few pixels to the top offset so we get some overlap. + dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top + 5); + fixture.detectChanges(); - expect( - config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), - ).toEqual(['One', 'Two', 'Three', 'Zero']); + expect( + config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), + ).toEqual(['One', 'Two', 'Three', 'Zero']); - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - flush(); - }), - ); + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + })); it('should lay out the elements correctly, if an element skips multiple positions when sorting horizontally', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalDropZone); @@ -1742,53 +1734,49 @@ export function defineCommonDropListTests(config: { flush(); })); - it( - 'it should allow item swaps in the same drag direction, if the pointer did not ' + - 'overlap with the sibling item after the previous swap', - fakeAsync(() => { - const fixture = createComponent(DraggableInDropZone); - fixture.detectChanges(); + it('it should allow item swaps in the same drag direction, if the pointer did not overlap with the sibling item after the previous swap', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); - const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); - const draggedItem = items[0]; - const target = items[items.length - 1]; - const itemRect = draggedItem.getBoundingClientRect(); + const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); + const draggedItem = items[0]; + const target = items[items.length - 1]; + const itemRect = draggedItem.getBoundingClientRect(); - startDraggingViaMouse(fixture, draggedItem, itemRect.left, itemRect.top); + startDraggingViaMouse(fixture, draggedItem, itemRect.left, itemRect.top); - const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; + const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - expect( - config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), - ).toEqual(['Zero', 'One', 'Two', 'Three']); + expect( + config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), + ).toEqual(['Zero', 'One', 'Two', 'Three']); - let targetRect = target.getBoundingClientRect(); + let targetRect = target.getBoundingClientRect(); - // Trigger a mouse move coming from the bottom so that the list thinks that we're - // sorting upwards. This usually how a user would behave with a mouse pointer. - dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom + 50); - fixture.detectChanges(); - dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom - 1); - fixture.detectChanges(); + // Trigger a mouse move coming from the bottom so that the list thinks that we're + // sorting upwards. This usually how a user would behave with a mouse pointer. + dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom + 50); + fixture.detectChanges(); + dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom - 1); + fixture.detectChanges(); - expect( - config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), - ).toEqual(['One', 'Two', 'Three', 'Zero']); + expect( + config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), + ).toEqual(['One', 'Two', 'Three', 'Zero']); - // Refresh the rect since the element position has changed. - targetRect = target.getBoundingClientRect(); - dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom - 1); - fixture.detectChanges(); + // Refresh the rect since the element position has changed. + targetRect = target.getBoundingClientRect(); + dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom - 1); + fixture.detectChanges(); - expect( - config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), - ).toEqual(['One', 'Two', 'Zero', 'Three']); + expect( + config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), + ).toEqual(['One', 'Two', 'Zero', 'Three']); - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - flush(); - }), - ); + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + })); it('should clean up the preview element if the item is destroyed mid-drag', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); @@ -1943,14 +1931,14 @@ export function defineCommonDropListTests(config: { const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; expect(preview.style.transform).toBe('translate3d(50px, 50px, 0px)'); - scrollTo(0, 500); + scrollTo(0, 5000); fixture.detectChanges(); // Move the pointer a bit so the preview has to reposition. dispatchMouseEvent(document, 'mousemove', 55, 55); fixture.detectChanges(); - expect(preview.style.transform).toBe('translate3d(55px, 555px, 0px)'); + expect(preview.style.transform).toBe('translate3d(55px, 1571px, 0px)'); cleanup(); })); @@ -2602,7 +2590,7 @@ export function defineCommonDropListTests(config: { document, 'mousemove', listRect.left + listRect.width / 2, - listRect.top + listRect.height / 2, + listRect.bottom + listRect.height / 2, ); fixture.detectChanges(); tickAnimationFrames(20); @@ -2727,87 +2715,79 @@ export function defineCommonDropListTests(config: { cleanup(); })); - it( - 'should auto-scroll the list, not the viewport, when the pointer is over the edge of ' + - 'both the list and the viewport', - fakeAsync(() => { - const fixture = createComponent(DraggableInScrollableVerticalDropZone); - fixture.detectChanges(); + it('should auto-scroll the list, not the viewport, when the pointer is over the edge of both the list and the viewport', fakeAsync(() => { + const fixture = createComponent(DraggableInScrollableVerticalDropZone); + fixture.detectChanges(); - const list = fixture.componentInstance.dropInstance.element.nativeElement; - const viewportRuler = TestBed.inject(ViewportRuler); - const item = fixture.componentInstance.dragItems.first.element.nativeElement; + const list = fixture.componentInstance.dropInstance.element.nativeElement; + const viewportRuler = TestBed.inject(ViewportRuler); + const item = fixture.componentInstance.dragItems.first.element.nativeElement; - // Position the list so that its top aligns with the viewport top. That way the pointer - // will both over its top edge and the viewport's. We use top instead of bottom, because - // bottom behaves weirdly when we run tests on mobile devices. - list.style.position = 'fixed'; - list.style.left = '50%'; - list.style.top = '0'; - list.style.margin = '0'; + // Position the list so that its top aligns with the viewport top. That way the pointer + // will both over its top edge and the viewport's. We use top instead of bottom, because + // bottom behaves weirdly when we run tests on mobile devices. + list.style.position = 'fixed'; + list.style.left = '50%'; + list.style.top = '0'; + list.style.margin = '0'; - const listRect = list.getBoundingClientRect(); - const cleanup = makeScrollable(); + const listRect = list.getBoundingClientRect(); + const cleanup = makeScrollable(); - scrollTo(0, viewportRuler.getViewportSize().height * 5); - list.scrollTop = 50; + scrollTo(0, viewportRuler.getViewportSize().height * 5); + list.scrollTop = 50; - const initialScrollDistance = viewportRuler.getViewportScrollPosition().top; - expect(initialScrollDistance).toBeGreaterThan(0); - expect(list.scrollTop).toBe(50); + const initialScrollDistance = viewportRuler.getViewportScrollPosition().top; + expect(initialScrollDistance).toBeGreaterThan(0); + expect(list.scrollTop).toBe(50); - startDraggingViaMouse(fixture, item); - dispatchMouseEvent(document, 'mousemove', listRect.left + listRect.width / 2, 0); - fixture.detectChanges(); - tickAnimationFrames(20); + startDraggingViaMouse(fixture, item); + dispatchMouseEvent(document, 'mousemove', listRect.left + listRect.width / 2, 0); + fixture.detectChanges(); + tickAnimationFrames(20); - expect(viewportRuler.getViewportScrollPosition().top).toBe(initialScrollDistance); - expect(list.scrollTop).toBeLessThan(50); + expect(viewportRuler.getViewportScrollPosition().top).toBe(initialScrollDistance); + expect(list.scrollTop).toBeLessThan(50); - cleanup(); - }), - ); + cleanup(); + })); - it( - 'should auto-scroll the viewport, when the pointer is over the edge of both the list ' + - 'and the viewport, if the list cannot be scrolled in that direction', - fakeAsync(() => { - const fixture = createComponent(DraggableInScrollableVerticalDropZone); - fixture.detectChanges(); + it('should auto-scroll the viewport, when the pointer is over the edge of both the list and the viewport, if the list cannot be scrolled in that direction', fakeAsync(() => { + const fixture = createComponent(DraggableInScrollableVerticalDropZone); + fixture.detectChanges(); - const list = fixture.componentInstance.dropInstance.element.nativeElement; - const viewportRuler = TestBed.inject(ViewportRuler); - const item = fixture.componentInstance.dragItems.first.element.nativeElement; + const list = fixture.componentInstance.dropInstance.element.nativeElement; + const viewportRuler = TestBed.inject(ViewportRuler); + const item = fixture.componentInstance.dragItems.first.element.nativeElement; - // Position the list so that its top aligns with the viewport top. That way the pointer - // will both over its top edge and the viewport's. We use top instead of bottom, because - // bottom behaves weirdly when we run tests on mobile devices. - list.style.position = 'fixed'; - list.style.left = '50%'; - list.style.top = '0'; - list.style.margin = '0'; + // Position the list so that its top aligns with the viewport top. That way the pointer + // will both over its top edge and the viewport's. We use top instead of bottom, because + // bottom behaves weirdly when we run tests on mobile devices. + list.style.position = 'fixed'; + list.style.left = '50%'; + list.style.top = '0'; + list.style.margin = '0'; - const listRect = list.getBoundingClientRect(); - const cleanup = makeScrollable(); + const listRect = list.getBoundingClientRect(); + const cleanup = makeScrollable(); - scrollTo(0, viewportRuler.getViewportSize().height * 5); - list.scrollTop = 0; + scrollTo(0, viewportRuler.getViewportSize().height * 5); + list.scrollTop = 0; - const initialScrollDistance = viewportRuler.getViewportScrollPosition().top; - expect(initialScrollDistance).toBeGreaterThan(0); - expect(list.scrollTop).toBe(0); + const initialScrollDistance = viewportRuler.getViewportScrollPosition().top; + expect(initialScrollDistance).toBeGreaterThan(0); + expect(list.scrollTop).toBe(0); - startDraggingViaMouse(fixture, item); - dispatchMouseEvent(document, 'mousemove', listRect.left + listRect.width / 2, 0); - fixture.detectChanges(); - tickAnimationFrames(20); + startDraggingViaMouse(fixture, item); + dispatchMouseEvent(document, 'mousemove', listRect.left + listRect.width / 2, 0); + fixture.detectChanges(); + tickAnimationFrames(20); - expect(viewportRuler.getViewportScrollPosition().top).toBeLessThan(initialScrollDistance); - expect(list.scrollTop).toBe(0); + expect(viewportRuler.getViewportScrollPosition().top).toBeLessThan(initialScrollDistance); + expect(list.scrollTop).toBe(0); - cleanup(); - }), - ); + cleanup(); + })); it('should be able to auto-scroll a parent container', fakeAsync(() => { const fixture = createComponent(DraggableInScrollableParentContainer); @@ -3168,7 +3148,7 @@ export function defineCommonDropListTests(config: { fixture.detectChanges(); documentElement.style.position = 'absolute'; - documentElement.style.top = '-100px'; + documentElement.style.top = '100px'; config.assertDownwardSorting( fixture, @@ -3256,51 +3236,47 @@ export function defineCommonDropListTests(config: { expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); })); - it( - 'should be able to move the element over a new container and return it to the initial ' + - 'one, even if it no longer matches the enterPredicate', - fakeAsync(() => { - const fixture = createComponent(ConnectedDropZones); - fixture.detectChanges(); + it('should be able to move the element over a new container and return it to the initial one, even if it no longer matches the enterPredicate', fakeAsync(() => { + const fixture = createComponent(ConnectedDropZones); + fixture.detectChanges(); - const groups = fixture.componentInstance.groupedDragItems; - const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement); - const item = groups[0][1]; - const initialRect = item.element.nativeElement.getBoundingClientRect(); - const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); + const groups = fixture.componentInstance.groupedDragItems; + const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement); + const item = groups[0][1]; + const initialRect = item.element.nativeElement.getBoundingClientRect(); + const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); - fixture.componentInstance.dropInstances.first.enterPredicate = () => false; - fixture.detectChanges(); + fixture.componentInstance.dropInstances.first.enterPredicate = () => false; + fixture.detectChanges(); - startDraggingViaMouse(fixture, item.element.nativeElement); + startDraggingViaMouse(fixture, item.element.nativeElement); - const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!; + const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!; - expect(placeholder).toBeTruthy(); - expect(dropZones[0].contains(placeholder)) - .withContext('Expected placeholder to be inside the first container.') - .toBe(true); + expect(placeholder).toBeTruthy(); + expect(dropZones[0].contains(placeholder)) + .withContext('Expected placeholder to be inside the first container.') + .toBe(true); - dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.detectChanges(); + dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); + fixture.detectChanges(); - expect(dropZones[1].contains(placeholder)) - .withContext('Expected placeholder to be inside second container.') - .toBe(true); + expect(dropZones[1].contains(placeholder)) + .withContext('Expected placeholder to be inside second container.') + .toBe(true); - dispatchMouseEvent(document, 'mousemove', initialRect.left + 1, initialRect.top + 1); - fixture.detectChanges(); + dispatchMouseEvent(document, 'mousemove', initialRect.left + 1, initialRect.top + 1); + fixture.detectChanges(); - expect(dropZones[0].contains(placeholder)) - .withContext('Expected placeholder to be back inside first container.') - .toBe(true); + expect(dropZones[0].contains(placeholder)) + .withContext('Expected placeholder to be back inside first container.') + .toBe(true); - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); - expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); - }), - ); + expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); + })); it('should transfer the DOM element from one drop zone to another', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); @@ -3579,45 +3555,6 @@ export function defineCommonDropListTests(config: { dispatchMouseEvent(document, 'mouseup'); })); - it('should enter as last child if entering from top in reversed container', fakeAsync(() => { - const fixture = createComponent(ConnectedDropZones); - - // Make sure there's only one item in the first list. - fixture.componentInstance.todo = ['things']; - fixture.detectChanges(); - - const groups = fixture.componentInstance.groupedDragItems; - const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement); - const item = groups[0][0]; - - // Add some initial padding as the target drop zone - const targetDropZoneStyle = dropZones[1].style; - targetDropZoneStyle.paddingTop = '10px'; - targetDropZoneStyle.display = 'flex'; - targetDropZoneStyle.flexDirection = 'column-reverse'; - - const targetRect = dropZones[1].getBoundingClientRect(); - - startDraggingViaMouse(fixture, item.element.nativeElement); - - const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!; - - expect(placeholder).toBeTruthy(); - - expect(dropZones[0].contains(placeholder)) - .withContext('Expected placeholder to be inside the first container.') - .toBe(true); - - dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top); - fixture.detectChanges(); - - expect(dropZones[1].lastChild === placeholder) - .withContext('Expected placeholder to be last child inside second container.') - .toBe(true); - - dispatchMouseEvent(document, 'mouseup'); - })); - it('should not throw when entering from the top with an intermediate sibling present', fakeAsync(() => { const fixture = createComponent(ConnectedDropZonesWithIntermediateSibling); @@ -3749,112 +3686,104 @@ export function defineCommonDropListTests(config: { }); })); - it( - 'should return DOM element to its initial container after it is dropped, in a container ' + - 'with one draggable item', - fakeAsync(() => { - const fixture = createComponent(ConnectedDropZonesWithSingleItems); - fixture.detectChanges(); + it('should return DOM element to its initial container after it is dropped, in a container with one draggable item', fakeAsync(() => { + const fixture = createComponent(ConnectedDropZonesWithSingleItems); + fixture.detectChanges(); - const items = fixture.componentInstance.dragItems.toArray(); - const item = items[0]; - const targetRect = items[1].element.nativeElement.getBoundingClientRect(); - const dropContainers = fixture.componentInstance.dropInstances.map( - drop => drop.element.nativeElement, - ); + const items = fixture.componentInstance.dragItems.toArray(); + const item = items[0]; + const targetRect = items[1].element.nativeElement.getBoundingClientRect(); + const dropContainers = fixture.componentInstance.dropInstances.map( + drop => drop.element.nativeElement, + ); - expect(dropContainers[0].contains(item.element.nativeElement)) - .withContext('Expected DOM element to be in first container') - .toBe(true); - expect(item.dropContainer) - .withContext('Expected CdkDrag to be in first container in memory') - .toBe(fixture.componentInstance.dropInstances.first); + expect(dropContainers[0].contains(item.element.nativeElement)) + .withContext('Expected DOM element to be in first container') + .toBe(true); + expect(item.dropContainer) + .withContext('Expected CdkDrag to be in first container in memory') + .toBe(fixture.componentInstance.dropInstances.first); - dragElementViaMouse( - fixture, - item.element.nativeElement, - targetRect.left + 1, - targetRect.top + 1, - ); - flush(); - fixture.detectChanges(); + dragElementViaMouse( + fixture, + item.element.nativeElement, + targetRect.left + 1, + targetRect.top + 1, + ); + flush(); + fixture.detectChanges(); - expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); + expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); - const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; + const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; - expect(event).toEqual({ - previousIndex: 0, - currentIndex: 0, - item, - container: fixture.componentInstance.dropInstances.toArray()[1], - previousContainer: fixture.componentInstance.dropInstances.first, - isPointerOverContainer: true, - distance: {x: jasmine.any(Number), y: jasmine.any(Number)}, - dropPoint: {x: jasmine.any(Number), y: jasmine.any(Number)}, - event: jasmine.anything(), - }); + expect(event).toEqual({ + previousIndex: 0, + currentIndex: 0, + item, + container: fixture.componentInstance.dropInstances.toArray()[1], + previousContainer: fixture.componentInstance.dropInstances.first, + isPointerOverContainer: true, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)}, + dropPoint: {x: jasmine.any(Number), y: jasmine.any(Number)}, + event: jasmine.anything(), + }); - expect(dropContainers[0].contains(item.element.nativeElement)) - .withContext('Expected DOM element to be returned to first container') - .toBe(true); - expect(item.dropContainer) - .withContext('Expected CdkDrag to be returned to first container in memory') - .toBe(fixture.componentInstance.dropInstances.first); - }), - ); - - it( - 'should be able to return an element to its initial container in the same sequence, ' + - 'even if it is not connected to the current container', - fakeAsync(() => { - const fixture = createComponent(ConnectedDropZones); - fixture.detectChanges(); + expect(dropContainers[0].contains(item.element.nativeElement)) + .withContext('Expected DOM element to be returned to first container') + .toBe(true); + expect(item.dropContainer) + .withContext('Expected CdkDrag to be returned to first container in memory') + .toBe(fixture.componentInstance.dropInstances.first); + })); - const groups = fixture.componentInstance.groupedDragItems; - const [todoDropInstance, doneDropInstance] = - fixture.componentInstance.dropInstances.toArray(); - const todoZone = todoDropInstance.element.nativeElement; - const doneZone = doneDropInstance.element.nativeElement; - const item = groups[0][1]; - const initialRect = item.element.nativeElement.getBoundingClientRect(); - const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); - - // Change the `connectedTo` so the containers are only connected one-way. - fixture.componentInstance.todoConnectedTo.set([doneDropInstance]); - fixture.componentInstance.doneConnectedTo.set([]); - fixture.detectChanges(); + it('should be able to return an element to its initial container in the same sequence, even if it is not connected to the current container', fakeAsync(() => { + const fixture = createComponent(ConnectedDropZones); + fixture.detectChanges(); - startDraggingViaMouse(fixture, item.element.nativeElement); - fixture.detectChanges(); + const groups = fixture.componentInstance.groupedDragItems; + const [todoDropInstance, doneDropInstance] = + fixture.componentInstance.dropInstances.toArray(); + const todoZone = todoDropInstance.element.nativeElement; + const doneZone = doneDropInstance.element.nativeElement; + const item = groups[0][1]; + const initialRect = item.element.nativeElement.getBoundingClientRect(); + const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); - const placeholder = todoZone.querySelector('.cdk-drag-placeholder')!; + // Change the `connectedTo` so the containers are only connected one-way. + fixture.componentInstance.todoConnectedTo.set([doneDropInstance]); + fixture.componentInstance.doneConnectedTo.set([]); + fixture.detectChanges(); - expect(placeholder).toBeTruthy(); - expect(todoZone.contains(placeholder)) - .withContext('Expected placeholder to be inside the first container.') - .toBe(true); + startDraggingViaMouse(fixture, item.element.nativeElement); + fixture.detectChanges(); - dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.detectChanges(); + const placeholder = todoZone.querySelector('.cdk-drag-placeholder')!; - expect(doneZone.contains(placeholder)) - .withContext('Expected placeholder to be inside second container.') - .toBe(true); + expect(placeholder).toBeTruthy(); + expect(todoZone.contains(placeholder)) + .withContext('Expected placeholder to be inside the first container.') + .toBe(true); - dispatchMouseEvent(document, 'mousemove', initialRect.left + 1, initialRect.top + 1); - fixture.detectChanges(); + dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); + fixture.detectChanges(); + + expect(doneZone.contains(placeholder)) + .withContext('Expected placeholder to be inside second container.') + .toBe(true); - expect(todoZone.contains(placeholder)) - .withContext('Expected placeholder to be back inside first container.') - .toBe(true); + dispatchMouseEvent(document, 'mousemove', initialRect.left + 1, initialRect.top + 1); + fixture.detectChanges(); - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); + expect(todoZone.contains(placeholder)) + .withContext('Expected placeholder to be back inside first container.') + .toBe(true); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); - expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); - }), - ); + expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); + })); it('should not add child drop lists to the same group as their parents', fakeAsync(() => { const fixture = createComponent(NestedDropListGroups); @@ -3986,46 +3915,42 @@ export function defineCommonDropListTests(config: { .toBe(true); })); - it( - 'should set the receiving class on the source container, even if the enter predicate ' + - 'does not match', - fakeAsync(() => { - const fixture = createComponent(ConnectedDropZones); - fixture.detectChanges(); - fixture.componentInstance.dropInstances.toArray()[0].enterPredicate = () => false; + it('should set the receiving class on the source container, even if the enter predicate does not match', fakeAsync(() => { + const fixture = createComponent(ConnectedDropZones); + fixture.detectChanges(); + fixture.componentInstance.dropInstances.toArray()[0].enterPredicate = () => false; - const groups = fixture.componentInstance.groupedDragItems; - const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement); - const item = groups[0][1]; - const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); + const groups = fixture.componentInstance.groupedDragItems; + const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement); + const item = groups[0][1]; + const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); - expect(dropZones.every(c => !c.classList.contains('cdk-drop-list-receiving'))) - .withContext('Expected neither of the containers to have the class.') - .toBe(true); + expect(dropZones.every(c => !c.classList.contains('cdk-drop-list-receiving'))) + .withContext('Expected neither of the containers to have the class.') + .toBe(true); - startDraggingViaMouse(fixture, item.element.nativeElement); + startDraggingViaMouse(fixture, item.element.nativeElement); - expect(dropZones[0].classList) - .not.withContext('Expected source container not to have the receiving class.') - .toContain('cdk-drop-list-receiving'); + expect(dropZones[0].classList) + .not.withContext('Expected source container not to have the receiving class.') + .toContain('cdk-drop-list-receiving'); - expect(dropZones[1].classList) - .withContext('Expected target container to have the receiving class.') - .toContain('cdk-drop-list-receiving'); + expect(dropZones[1].classList) + .withContext('Expected target container to have the receiving class.') + .toContain('cdk-drop-list-receiving'); - dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.detectChanges(); + dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); + fixture.detectChanges(); - expect(dropZones[0].classList) - .withContext('Expected old container to have the receiving class after exiting.') - .toContain('cdk-drop-list-receiving'); + expect(dropZones[0].classList) + .withContext('Expected old container to have the receiving class after exiting.') + .toContain('cdk-drop-list-receiving'); - expect(dropZones[1].classList).not.toContain( - 'cdk-drop-list-receiving', - 'Expected new container not to have the receiving class after exiting.', - ); - }), - ); + expect(dropZones[1].classList).not.toContain( + 'cdk-drop-list-receiving', + 'Expected new container not to have the receiving class after exiting.', + ); + })); it('should set the receiving class when the list is wrapped in an OnPush component', fakeAsync(() => { const fixture = createComponent(ConnectedDropListsInOnPush); @@ -4052,261 +3977,245 @@ export function defineCommonDropListTests(config: { .toContain('cdk-drop-list-receiving'); })); - it( - 'should be able to move the item over an intermediate container before ' + - 'dropping it into the final one', - fakeAsync(() => { - const fixture = createComponent(ConnectedDropZones); - fixture.detectChanges(); + it('should be able to move the item over an intermediate container before dropping it into the final one', fakeAsync(() => { + const fixture = createComponent(ConnectedDropZones); + fixture.detectChanges(); - const [todoDropInstance, doneDropInstance, extraDropInstance] = - fixture.componentInstance.dropInstances.toArray(); - fixture.componentInstance.todoConnectedTo.set([doneDropInstance, extraDropInstance]); - fixture.componentInstance.doneConnectedTo.set([]); - fixture.componentInstance.extraConnectedTo.set([]); - fixture.detectChanges(); + const [todoDropInstance, doneDropInstance, extraDropInstance] = + fixture.componentInstance.dropInstances.toArray(); + fixture.componentInstance.todoConnectedTo.set([doneDropInstance, extraDropInstance]); + fixture.componentInstance.doneConnectedTo.set([]); + fixture.componentInstance.extraConnectedTo.set([]); + fixture.detectChanges(); - const groups = fixture.componentInstance.groupedDragItems; - const todoZone = todoDropInstance.element.nativeElement; - const doneZone = doneDropInstance.element.nativeElement; - const extraZone = extraDropInstance.element.nativeElement; - const item = groups[0][1]; - const intermediateRect = doneZone.getBoundingClientRect(); - const finalRect = extraZone.getBoundingClientRect(); + const groups = fixture.componentInstance.groupedDragItems; + const todoZone = todoDropInstance.element.nativeElement; + const doneZone = doneDropInstance.element.nativeElement; + const extraZone = extraDropInstance.element.nativeElement; + const item = groups[0][1]; + const intermediateRect = doneZone.getBoundingClientRect(); + const finalRect = extraZone.getBoundingClientRect(); - startDraggingViaMouse(fixture, item.element.nativeElement); + startDraggingViaMouse(fixture, item.element.nativeElement); - const placeholder = todoZone.querySelector('.cdk-drag-placeholder')!; + const placeholder = todoZone.querySelector('.cdk-drag-placeholder')!; - expect(placeholder).toBeTruthy(); - expect(todoZone.contains(placeholder)) - .withContext('Expected placeholder to be inside the first container.') - .toBe(true); + expect(placeholder).toBeTruthy(); + expect(todoZone.contains(placeholder)) + .withContext('Expected placeholder to be inside the first container.') + .toBe(true); - dispatchMouseEvent( - document, - 'mousemove', - intermediateRect.left + 1, - intermediateRect.top + 1, - ); - fixture.detectChanges(); + dispatchMouseEvent( + document, + 'mousemove', + intermediateRect.left + 1, + intermediateRect.top + 1, + ); + fixture.detectChanges(); - expect(doneZone.contains(placeholder)) - .withContext('Expected placeholder to be inside second container.') - .toBe(true); + expect(doneZone.contains(placeholder)) + .withContext('Expected placeholder to be inside second container.') + .toBe(true); - dispatchMouseEvent(document, 'mousemove', finalRect.left + 1, finalRect.top + 1); - fixture.detectChanges(); + dispatchMouseEvent(document, 'mousemove', finalRect.left + 1, finalRect.top + 1); + fixture.detectChanges(); - expect(extraZone.contains(placeholder)) - .withContext('Expected placeholder to be inside third container.') - .toBe(true); + expect(extraZone.contains(placeholder)) + .withContext('Expected placeholder to be inside third container.') + .toBe(true); - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); - const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; - - expect(event).toBeTruthy(); - expect(event).toEqual( - jasmine.objectContaining({ - previousIndex: 1, - currentIndex: 0, - item: groups[0][1], - container: extraDropInstance, - previousContainer: todoDropInstance, - isPointerOverContainer: false, - distance: {x: jasmine.any(Number), y: jasmine.any(Number)}, - dropPoint: {x: jasmine.any(Number), y: jasmine.any(Number)}, - event: jasmine.anything(), - }), - ); - }), - ); - - it( - 'should not be able to move an item into a drop container that the initial container is ' + - 'not connected to by passing it over an intermediate one that is', - fakeAsync(() => { - const fixture = createComponent(ConnectedDropZones); - fixture.detectChanges(); + const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; - const [todoDropInstance, doneDropInstance, extraDropInstance] = - fixture.componentInstance.dropInstances.toArray(); - fixture.componentInstance.todoConnectedTo.set([doneDropInstance]); - fixture.componentInstance.doneConnectedTo.set([todoDropInstance, extraDropInstance]); - fixture.componentInstance.extraConnectedTo.set([doneDropInstance]); - fixture.detectChanges(); + expect(event).toBeTruthy(); + expect(event).toEqual( + jasmine.objectContaining({ + previousIndex: 1, + currentIndex: 0, + item: groups[0][1], + container: extraDropInstance, + previousContainer: todoDropInstance, + isPointerOverContainer: false, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)}, + dropPoint: {x: jasmine.any(Number), y: jasmine.any(Number)}, + event: jasmine.anything(), + }), + ); + })); - const groups = fixture.componentInstance.groupedDragItems; - const todoZone = todoDropInstance.element.nativeElement; - const doneZone = doneDropInstance.element.nativeElement; - const extraZone = extraDropInstance.element.nativeElement; - const item = groups[0][1]; - const intermediateRect = doneZone.getBoundingClientRect(); - const finalRect = extraZone.getBoundingClientRect(); + it('should not be able to move an item into a drop container that the initial container is not connected to by passing it over an intermediate one that is', fakeAsync(() => { + const fixture = createComponent(ConnectedDropZones); + fixture.detectChanges(); - startDraggingViaMouse(fixture, item.element.nativeElement); + const [todoDropInstance, doneDropInstance, extraDropInstance] = + fixture.componentInstance.dropInstances.toArray(); + fixture.componentInstance.todoConnectedTo.set([doneDropInstance]); + fixture.componentInstance.doneConnectedTo.set([todoDropInstance, extraDropInstance]); + fixture.componentInstance.extraConnectedTo.set([doneDropInstance]); + fixture.detectChanges(); - const placeholder = todoZone.querySelector('.cdk-drag-placeholder')!; + const groups = fixture.componentInstance.groupedDragItems; + const todoZone = todoDropInstance.element.nativeElement; + const doneZone = doneDropInstance.element.nativeElement; + const extraZone = extraDropInstance.element.nativeElement; + const item = groups[0][1]; + const intermediateRect = doneZone.getBoundingClientRect(); + const finalRect = extraZone.getBoundingClientRect(); - expect(placeholder).toBeTruthy(); - expect(todoZone.contains(placeholder)) - .withContext('Expected placeholder to be inside the first container.') - .toBe(true); + startDraggingViaMouse(fixture, item.element.nativeElement); - dispatchMouseEvent( - document, - 'mousemove', - intermediateRect.left + 1, - intermediateRect.top + 1, - ); - fixture.detectChanges(); + const placeholder = todoZone.querySelector('.cdk-drag-placeholder')!; - expect(doneZone.contains(placeholder)) - .withContext('Expected placeholder to be inside second container.') - .toBe(true); + expect(placeholder).toBeTruthy(); + expect(todoZone.contains(placeholder)) + .withContext('Expected placeholder to be inside the first container.') + .toBe(true); - dispatchMouseEvent(document, 'mousemove', finalRect.left + 1, finalRect.top + 1); - fixture.detectChanges(); + dispatchMouseEvent( + document, + 'mousemove', + intermediateRect.left + 1, + intermediateRect.top + 1, + ); + fixture.detectChanges(); - expect(doneZone.contains(placeholder)) - .withContext('Expected placeholder to remain in the second container.') - .toBe(true); + expect(doneZone.contains(placeholder)) + .withContext('Expected placeholder to be inside second container.') + .toBe(true); - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); + dispatchMouseEvent(document, 'mousemove', finalRect.left + 1, finalRect.top + 1); + fixture.detectChanges(); - const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; - - expect(event).toBeTruthy(); - expect(event).toEqual( - jasmine.objectContaining({ - previousIndex: 1, - currentIndex: 1, - item: groups[0][1], - container: doneDropInstance, - previousContainer: todoDropInstance, - isPointerOverContainer: false, - }), - ); - }), - ); - - it( - 'should return the item to its initial position, if sorting in the source container ' + - 'was disabled', - fakeAsync(() => { - const fixture = createComponent(ConnectedDropZones); - fixture.detectChanges(); + expect(doneZone.contains(placeholder)) + .withContext('Expected placeholder to remain in the second container.') + .toBe(true); - const groups = fixture.componentInstance.groupedDragItems; - const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement); - const item = groups[0][1]; - const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); - fixture.componentInstance.dropInstances.first.sortingDisabled = true; - startDraggingViaMouse(fixture, item.element.nativeElement); + const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; - const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!; + expect(event).toBeTruthy(); + expect(event).toEqual( + jasmine.objectContaining({ + previousIndex: 1, + currentIndex: 1, + item: groups[0][1], + container: doneDropInstance, + previousContainer: todoDropInstance, + isPointerOverContainer: false, + }), + ); + })); - expect(placeholder).toBeTruthy(); - expect(dropZones[0].contains(placeholder)) - .withContext('Expected placeholder to be inside the first container.') - .toBe(true); - expect(config.getElementIndexByPosition(placeholder, 'top')) - .withContext('Expected placeholder to be at item index.') - .toBe(1); + it('should return the item to its initial position, if sorting in the source container was disabled', fakeAsync(() => { + const fixture = createComponent(ConnectedDropZones); + fixture.detectChanges(); - dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.detectChanges(); + const groups = fixture.componentInstance.groupedDragItems; + const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement); + const item = groups[0][1]; + const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); - expect(dropZones[1].contains(placeholder)) - .withContext('Expected placeholder to be inside second container.') - .toBe(true); - expect(config.getElementIndexByPosition(placeholder, 'top')) - .withContext('Expected placeholder to be at the target index.') - .toBe(3); - - const firstInitialSiblingRect = groups[0][0].element.nativeElement.getBoundingClientRect(); - - // Return the item to an index that is different from the initial one. - dispatchMouseEvent( - document, - 'mousemove', - firstInitialSiblingRect.left + 1, - firstInitialSiblingRect.top + 1, - ); - fixture.detectChanges(); + fixture.componentInstance.dropInstances.first.sortingDisabled = true; + startDraggingViaMouse(fixture, item.element.nativeElement); - expect(dropZones[0].contains(placeholder)) - .withContext('Expected placeholder to be back inside first container.') - .toBe(true); - expect(config.getElementIndexByPosition(placeholder, 'top')) - .withContext('Expected placeholder to be back at the initial index.') - .toBe(1); + const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!; - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); + expect(placeholder).toBeTruthy(); + expect(dropZones[0].contains(placeholder)) + .withContext('Expected placeholder to be inside the first container.') + .toBe(true); + expect(config.getElementIndexByPosition(placeholder, 'top')) + .withContext('Expected placeholder to be at item index.') + .toBe(1); - expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); - }), - ); + dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); + fixture.detectChanges(); - it( - 'should enter an item into the correct index when returning to the initial container, if ' + - 'sorting is enabled', - fakeAsync(() => { - const fixture = createComponent(ConnectedDropZones); - fixture.detectChanges(); + expect(dropZones[1].contains(placeholder)) + .withContext('Expected placeholder to be inside second container.') + .toBe(true); + expect(config.getElementIndexByPosition(placeholder, 'top')) + .withContext('Expected placeholder to be at the target index.') + .toBe(3); - const groups = fixture.componentInstance.groupedDragItems; - const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement); - const item = groups[0][1]; - const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); + const firstInitialSiblingRect = groups[0][0].element.nativeElement.getBoundingClientRect(); - // Explicitly enable just in case. - fixture.componentInstance.dropInstances.first.sortingDisabled = false; - startDraggingViaMouse(fixture, item.element.nativeElement); + // Return the item to an index that is different from the initial one. + dispatchMouseEvent( + document, + 'mousemove', + firstInitialSiblingRect.left + 1, + firstInitialSiblingRect.top + 1, + ); + fixture.detectChanges(); - const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!; + expect(dropZones[0].contains(placeholder)) + .withContext('Expected placeholder to be back inside first container.') + .toBe(true); + expect(config.getElementIndexByPosition(placeholder, 'top')) + .withContext('Expected placeholder to be back at the initial index.') + .toBe(1); - expect(placeholder).toBeTruthy(); - expect(dropZones[0].contains(placeholder)) - .withContext('Expected placeholder to be inside the first container.') - .toBe(true); - expect(config.getElementIndexByPosition(placeholder, 'top')) - .withContext('Expected placeholder to be at item index.') - .toBe(1); + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); - dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.detectChanges(); + expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); + })); + + it('should enter an item into the correct index when returning to the initial container, if sorting is enabled', fakeAsync(() => { + const fixture = createComponent(ConnectedDropZones); + fixture.detectChanges(); - expect(dropZones[1].contains(placeholder)) - .withContext('Expected placeholder to be inside second container.') - .toBe(true); - expect(config.getElementIndexByPosition(placeholder, 'top')) - .withContext('Expected placeholder to be at the target index.') - .toBe(3); + const groups = fixture.componentInstance.groupedDragItems; + const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement); + const item = groups[0][1]; + const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); - const nextTargetRect = groups[0][3].element.nativeElement.getBoundingClientRect(); + // Explicitly enable just in case. + fixture.componentInstance.dropInstances.first.sortingDisabled = false; + startDraggingViaMouse(fixture, item.element.nativeElement); - // Return the item to an index that is different from the initial one. - dispatchMouseEvent(document, 'mousemove', nextTargetRect.left + 1, nextTargetRect.top + 1); - fixture.detectChanges(); + const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!; - expect(dropZones[0].contains(placeholder)) - .withContext('Expected placeholder to be back inside first container.') - .toBe(true); - expect(config.getElementIndexByPosition(placeholder, 'top')) - .withContext('Expected placeholder to be at the index at which it entered.') - .toBe(2); - }), - ); + expect(placeholder).toBeTruthy(); + expect(dropZones[0].contains(placeholder)) + .withContext('Expected placeholder to be inside the first container.') + .toBe(true); + expect(config.getElementIndexByPosition(placeholder, 'top')) + .withContext('Expected placeholder to be at item index.') + .toBe(1); + + dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); + fixture.detectChanges(); + + expect(dropZones[1].contains(placeholder)) + .withContext('Expected placeholder to be inside second container.') + .toBe(true); + expect(config.getElementIndexByPosition(placeholder, 'top')) + .withContext('Expected placeholder to be at the target index.') + .toBe(3); + + const nextTargetRect = groups[0][3].element.nativeElement.getBoundingClientRect(); + + // Return the item to an index that is different from the initial one. + dispatchMouseEvent(document, 'mousemove', nextTargetRect.left + 1, nextTargetRect.top + 1); + fixture.detectChanges(); + + expect(dropZones[0].contains(placeholder)) + .withContext('Expected placeholder to be back inside first container.') + .toBe(true); + expect(config.getElementIndexByPosition(placeholder, 'top')) + .withContext('Expected placeholder to be at the index at which it entered.') + .toBe(2); + })); it('should return the last item to initial position when dragging back into a container with disabled sorting', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); @@ -4365,48 +4274,42 @@ export function defineCommonDropListTests(config: { expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); })); - it( - 'should toggle a class when dragging an item inside a wrapper component component ' + - 'with OnPush change detection', - fakeAsync(() => { - const fixture = createComponent(ConnectedWrappedDropZones); - fixture.detectChanges(); + it('should toggle a class when dragging an item inside a wrapper component component with OnPush change detection', fakeAsync(() => { + const fixture = createComponent(ConnectedWrappedDropZones); + fixture.detectChanges(); - const [startZone, targetZone] = fixture.nativeElement.querySelectorAll('.cdk-drop-list'); - const item = startZone.querySelector('.cdk-drag'); - const targetRect = targetZone.getBoundingClientRect(); + const [startZone, targetZone] = fixture.nativeElement.querySelectorAll('.cdk-drop-list'); + const item = startZone.querySelector('.cdk-drag'); + const targetRect = targetZone.getBoundingClientRect(); - expect(startZone.classList).not.toContain( - 'cdk-drop-list-dragging', - 'Expected start not to have dragging class on init.', - ); - expect(targetZone.classList).not.toContain( - 'cdk-drop-list-dragging', - 'Expected target not to have dragging class on init.', - ); + expect(startZone.classList).not.toContain( + 'cdk-drop-list-dragging', + 'Expected start not to have dragging class on init.', + ); + expect(targetZone.classList).not.toContain( + 'cdk-drop-list-dragging', + 'Expected target not to have dragging class on init.', + ); - startDraggingViaMouse(fixture, item); + startDraggingViaMouse(fixture, item); - expect(startZone.classList) - .withContext('Expected start to have dragging class after dragging has started.') - .toContain('cdk-drop-list-dragging'); - expect(targetZone.classList) - .not.withContext('Expected target not to have dragging class after dragging has started.') - .toContain('cdk-drop-list-dragging'); + expect(startZone.classList) + .withContext('Expected start to have dragging class after dragging has started.') + .toContain('cdk-drop-list-dragging'); + expect(targetZone.classList) + .not.withContext('Expected target not to have dragging class after dragging has started.') + .toContain('cdk-drop-list-dragging'); - dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); - fixture.detectChanges(); + dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); + fixture.detectChanges(); - expect(startZone.classList) - .not.withContext( - 'Expected start not to have dragging class once item has been moved over.', - ) - .toContain('cdk-drop-list-dragging'); - expect(targetZone.classList) - .withContext('Expected target to have dragging class once item has been moved over.') - .toContain('cdk-drop-list-dragging'); - }), - ); + expect(startZone.classList) + .not.withContext('Expected start not to have dragging class once item has been moved over.') + .toContain('cdk-drop-list-dragging'); + expect(targetZone.classList) + .withContext('Expected target to have dragging class once item has been moved over.') + .toContain('cdk-drop-list-dragging'); + })); it('should dispatch an event when an item enters a new container', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); @@ -4464,7 +4367,7 @@ export function defineCommonDropListTests(config: { // Make the page scrollable and scroll the items out of view. const cleanup = makeScrollable(); - scrollTo(0, 4000); + scrollTo(0, 0); dispatchFakeEvent(document, 'scroll'); fixture.detectChanges(); flush(); @@ -4475,7 +4378,7 @@ export function defineCommonDropListTests(config: { // Start dragging and then scroll the elements back into view. startDraggingViaMouse(fixture, item.element.nativeElement); - scrollTo(0, 0); + scrollTo(0, 5000); dispatchFakeEvent(document, 'scroll'); const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); @@ -5348,7 +5251,7 @@ const CONNECTED_DROP_ZONES_TEMPLATE = ` standalone: true, imports: [CdkDropList, CdkDrag], }) -class ConnectedDropZones implements AfterViewInit { +export class ConnectedDropZones implements AfterViewInit { @ViewChildren(CdkDrag) rawDragItems: QueryList; @ViewChildren(CdkDropList) dropInstances: QueryList; changeDetectorRef = inject(ChangeDetectorRef); diff --git a/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts b/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts index ecaf013d33c5..ca353cd5ce13 100644 --- a/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts +++ b/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts @@ -3,6 +3,7 @@ import {dispatchMouseEvent} from '@angular/cdk/testing/private'; import {_supportsShadowDom} from '@angular/cdk/platform'; import {createComponent, startDraggingViaMouse} from './test-utils.spec'; import { + ConnectedDropZones, DraggableInDropZone, DraggableInScrollableVerticalDropZone, ITEM_HEIGHT, @@ -268,6 +269,45 @@ describe('Single-axis drop list', () => { .withContext('Expected placeholder to preserve transform when dragging stops.') .toBe(true); })); + + it('should enter as last child if entering from top in reversed container', fakeAsync(() => { + const fixture = createComponent(ConnectedDropZones); + + // Make sure there's only one item in the first list. + fixture.componentInstance.todo = ['things']; + fixture.detectChanges(); + + const groups = fixture.componentInstance.groupedDragItems; + const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement); + const item = groups[0][0]; + + // Add some initial padding as the target drop zone + const targetDropZoneStyle = dropZones[1].style; + targetDropZoneStyle.paddingTop = '10px'; + targetDropZoneStyle.display = 'flex'; + targetDropZoneStyle.flexDirection = 'column-reverse'; + + const targetRect = dropZones[1].getBoundingClientRect(); + + startDraggingViaMouse(fixture, item.element.nativeElement); + + const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!; + + expect(placeholder).toBeTruthy(); + + expect(dropZones[0].contains(placeholder)) + .withContext('Expected placeholder to be inside the first container.') + .toBe(true); + + dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top); + fixture.detectChanges(); + + expect(dropZones[1].lastChild === placeholder) + .withContext('Expected placeholder to be last child inside second container.') + .toBe(true); + + dispatchMouseEvent(document, 'mouseup'); + })); }); function getElementIndexByPosition(element: Element, direction: 'top' | 'left') { diff --git a/src/cdk/drag-drop/directives/test-utils.spec.ts b/src/cdk/drag-drop/directives/test-utils.spec.ts index 267f4c06817f..2c0c12027eac 100644 --- a/src/cdk/drag-drop/directives/test-utils.spec.ts +++ b/src/cdk/drag-drop/directives/test-utils.spec.ts @@ -153,7 +153,7 @@ export function makeScrollable( const veryTallElement = document.createElement('div'); veryTallElement.style.width = direction === 'vertical' ? '100%' : '4000px'; veryTallElement.style.height = direction === 'vertical' ? '2000px' : '5px'; - element.appendChild(veryTallElement); + element.prepend(veryTallElement); return () => { scrollTo(0, 0); From b9ef78630c36fc4ac537539f81a59a25706809fd Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 7 Jun 2024 12:52:16 +0200 Subject: [PATCH 41/61] test(cdk/drag-drop): simplify shared tests Reduces the amount of information that needs to be passed in for the shared drag&drop tests. --- .../directives/drop-list-shared.spec.ts | 284 +++++++++++------- .../directives/single-axis-drop-list.spec.ts | 75 +---- 2 files changed, 182 insertions(+), 177 deletions(-) diff --git a/src/cdk/drag-drop/directives/drop-list-shared.spec.ts b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts index c989d60e6d56..e45102078fea 100644 --- a/src/cdk/drag-drop/directives/drop-list-shared.spec.ts +++ b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts @@ -54,6 +54,9 @@ import {CdkDragPlaceholder} from './drag-placeholder'; export const ITEM_HEIGHT = 25; export const ITEM_WIDTH = 75; +/** Function that can be used to get the sorted siblings of an element. */ +type SortedSiblingsFunction = (element: Element, direction: 'top' | 'left') => Element[]; + export function defineCommonDropListTests(config: { /** Orientation value that will be passed to tests checking vertical orientation. */ verticalListOrientation: Exclude; @@ -61,17 +64,8 @@ export function defineCommonDropListTests(config: { /** Orientation value that will be passed to tests checking horizontal orientation. */ horizontalListOrientation: Exclude; - /** Asserts that sorting an element up works correctly. */ - assertUpwardSorting: (fixture: ComponentFixture, items: Element[]) => void; - - /** Asserts that sorting an element down works correctly. */ - assertDownwardSorting: (fixture: ComponentFixture, items: Element[]) => void; - - /** Gets the index of an element among its siblings, based on their visible position. */ - getElementIndexByPosition: (element: Element, direction: 'top' | 'left') => number; - /** Gets the siblings of an element, sorted by their visible position. */ - getElementSibligsByPosition: (element: Element, direction: 'top' | 'left') => Element[]; + getSortedSiblings: SortedSiblingsFunction; }) { const { DraggableInHorizontalDropZone, @@ -1495,11 +1489,11 @@ export function defineCommonDropListTests(config: { it('should move the placeholder as an item is being sorted down', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.detectChanges(); - config.assertDownwardSorting( + assertDownwardSorting( + 'vertical', fixture, - fixture.componentInstance.dragItems.map(item => { - return item.element.nativeElement; - }), + config.getSortedSiblings, + fixture.componentInstance.dragItems.map(item => item.element.nativeElement), ); })); @@ -1509,11 +1503,11 @@ export function defineCommonDropListTests(config: { const cleanup = makeScrollable(); scrollTo(0, 5000); - config.assertDownwardSorting( + assertDownwardSorting( + 'vertical', fixture, - fixture.componentInstance.dragItems.map(item => { - return item.element.nativeElement; - }), + config.getSortedSiblings, + fixture.componentInstance.dragItems.map(item => item.element.nativeElement), ); cleanup(); })); @@ -1521,11 +1515,11 @@ export function defineCommonDropListTests(config: { it('should move the placeholder as an item is being sorted up', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.detectChanges(); - config.assertUpwardSorting( + assertUpwardSorting( + 'vertical', fixture, - fixture.componentInstance.dragItems.map(item => { - return item.element.nativeElement; - }), + config.getSortedSiblings, + fixture.componentInstance.dragItems.map(item => item.element.nativeElement), ); })); @@ -1535,11 +1529,11 @@ export function defineCommonDropListTests(config: { const cleanup = makeScrollable(); scrollTo(0, 5000); - config.assertUpwardSorting( + assertUpwardSorting( + 'vertical', fixture, - fixture.componentInstance.dragItems.map(item => { - return item.element.nativeElement; - }), + config.getSortedSiblings, + fixture.componentInstance.dragItems.map(item => item.element.nativeElement), ); cleanup(); })); @@ -1547,55 +1541,23 @@ export function defineCommonDropListTests(config: { it('should move the placeholder as an item is being sorted to the right', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalDropZone); fixture.detectChanges(); - - const items = fixture.componentInstance.dragItems.toArray(); - const draggedItem = items[0].element.nativeElement; - const {top, left} = draggedItem.getBoundingClientRect(); - - startDraggingViaMouse(fixture, draggedItem, left, top); - - const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - - // Drag over each item one-by-one going to the right. - for (let i = 0; i < items.length; i++) { - const elementRect = items[i].element.nativeElement.getBoundingClientRect(); - - // Add a few pixels to the left offset so we get some overlap. - dispatchMouseEvent(document, 'mousemove', elementRect.left + 5, elementRect.top); - fixture.detectChanges(); - expect(config.getElementIndexByPosition(placeholder, 'left')).toBe(i); - } - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - flush(); + assertDownwardSorting( + 'horizontal', + fixture, + config.getSortedSiblings, + fixture.componentInstance.dragItems.map(item => item.element.nativeElement), + ); })); it('should move the placeholder as an item is being sorted to the left', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalDropZone); fixture.detectChanges(); - - const items = fixture.componentInstance.dragItems.toArray(); - const draggedItem = items[items.length - 1].element.nativeElement; - const {top, left} = draggedItem.getBoundingClientRect(); - - startDraggingViaMouse(fixture, draggedItem, left, top); - - const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - - // Drag over each item one-by-one going to the left. - for (let i = items.length - 1; i > -1; i--) { - const elementRect = items[i].element.nativeElement.getBoundingClientRect(); - - // Remove a few pixels from the right offset so we get some overlap. - dispatchMouseEvent(document, 'mousemove', elementRect.right - 5, elementRect.top); - fixture.detectChanges(); - expect(config.getElementIndexByPosition(placeholder, 'left')).toBe(i); - } - - dispatchMouseEvent(document, 'mouseup'); - fixture.detectChanges(); - flush(); + assertUpwardSorting( + 'horizontal', + fixture, + config.getSortedSiblings, + fixture.componentInstance.dragItems.map(item => item.element.nativeElement), + ); })); it('should lay out the elements correctly, if an element skips multiple positions when sorting vertically', fakeAsync(() => { @@ -1615,9 +1577,12 @@ export function defineCommonDropListTests(config: { dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top + 5); fixture.detectChanges(); - expect( - config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), - ).toEqual(['One', 'Two', 'Three', 'Zero']); + expect(config.getSortedSiblings(placeholder, 'top').map(e => e.textContent!.trim())).toEqual([ + 'One', + 'Two', + 'Three', + 'Zero', + ]); dispatchMouseEvent(document, 'mouseup'); fixture.detectChanges(); @@ -1641,9 +1606,9 @@ export function defineCommonDropListTests(config: { dispatchMouseEvent(document, 'mousemove', targetRect.right - 5, targetRect.top); fixture.detectChanges(); - expect( - config.getElementSibligsByPosition(placeholder, 'left').map(e => e.textContent!.trim()), - ).toEqual(['One', 'Two', 'Three', 'Zero']); + expect(config.getSortedSiblings(placeholder, 'left').map(e => e.textContent!.trim())).toEqual( + ['One', 'Two', 'Three', 'Zero'], + ); dispatchMouseEvent(document, 'mouseup'); fixture.detectChanges(); @@ -1666,9 +1631,12 @@ export function defineCommonDropListTests(config: { const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - expect( - config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), - ).toEqual(['Zero', 'One', 'Two', 'Three']); + expect(config.getSortedSiblings(placeholder, 'top').map(e => e.textContent!.trim())).toEqual([ + 'Zero', + 'One', + 'Two', + 'Three', + ]); const targetRect = target.getBoundingClientRect(); const pointerTop = targetRect.top + 20; @@ -1676,14 +1644,14 @@ export function defineCommonDropListTests(config: { // Move over the target so there's a 20px overlap. dispatchMouseEvent(document, 'mousemove', targetRect.left, pointerTop); fixture.detectChanges(); - expect(config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) + expect(config.getSortedSiblings(placeholder, 'top').map(e => e.textContent!.trim())) .withContext('Expected position to swap.') .toEqual(['One', 'Zero', 'Two', 'Three']); // Move down a further 1px. dispatchMouseEvent(document, 'mousemove', targetRect.left, pointerTop + 1); fixture.detectChanges(); - expect(config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) + expect(config.getSortedSiblings(placeholder, 'top').map(e => e.textContent!.trim())) .withContext('Expected positions not to swap.') .toEqual(['One', 'Zero', 'Two', 'Three']); @@ -1708,9 +1676,12 @@ export function defineCommonDropListTests(config: { const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - expect( - config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), - ).toEqual(['Zero', 'One', 'Two', 'Three']); + expect(config.getSortedSiblings(placeholder, 'top').map(e => e.textContent!.trim())).toEqual([ + 'Zero', + 'One', + 'Two', + 'Three', + ]); const targetRect = target.getBoundingClientRect(); const pointerTop = targetRect.top + 20; @@ -1718,14 +1689,14 @@ export function defineCommonDropListTests(config: { // Move over the target so there's a 20px overlap. dispatchMouseEvent(document, 'mousemove', targetRect.left, pointerTop); fixture.detectChanges(); - expect(config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) + expect(config.getSortedSiblings(placeholder, 'top').map(e => e.textContent!.trim())) .withContext('Expected position to swap.') .toEqual(['One', 'Zero', 'Two', 'Three']); // Move up 10px. dispatchMouseEvent(document, 'mousemove', targetRect.left, pointerTop - 10); fixture.detectChanges(); - expect(config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) + expect(config.getSortedSiblings(placeholder, 'top').map(e => e.textContent!.trim())) .withContext('Expected positions to swap again.') .toEqual(['Zero', 'One', 'Two', 'Three']); @@ -1747,9 +1718,12 @@ export function defineCommonDropListTests(config: { const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - expect( - config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), - ).toEqual(['Zero', 'One', 'Two', 'Three']); + expect(config.getSortedSiblings(placeholder, 'top').map(e => e.textContent!.trim())).toEqual([ + 'Zero', + 'One', + 'Two', + 'Three', + ]); let targetRect = target.getBoundingClientRect(); @@ -1760,18 +1734,24 @@ export function defineCommonDropListTests(config: { dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom - 1); fixture.detectChanges(); - expect( - config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), - ).toEqual(['One', 'Two', 'Three', 'Zero']); + expect(config.getSortedSiblings(placeholder, 'top').map(e => e.textContent!.trim())).toEqual([ + 'One', + 'Two', + 'Three', + 'Zero', + ]); // Refresh the rect since the element position has changed. targetRect = target.getBoundingClientRect(); dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom - 1); fixture.detectChanges(); - expect( - config.getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()), - ).toEqual(['One', 'Two', 'Zero', 'Three']); + expect(config.getSortedSiblings(placeholder, 'top').map(e => e.textContent!.trim())).toEqual([ + 'One', + 'Two', + 'Zero', + 'Three', + ]); dispatchMouseEvent(document, 'mouseup'); fixture.detectChanges(); @@ -2308,7 +2288,7 @@ export function defineCommonDropListTests(config: { dispatchMouseEvent(document, 'mousemove', targetX, targetY); fixture.detectChanges(); - expect(config.getElementIndexByPosition(placeholder, 'top')) + expect(config.getSortedSiblings(placeholder, 'top').indexOf(placeholder)) .withContext('Expected placeholder to stay in place.') .toBe(0); @@ -3150,11 +3130,11 @@ export function defineCommonDropListTests(config: { documentElement.style.position = 'absolute'; documentElement.style.top = '100px'; - config.assertDownwardSorting( + assertDownwardSorting( + 'vertical', fixture, - fixture.componentInstance.dragItems.map(item => { - return item.element.nativeElement; - }), + config.getSortedSiblings, + fixture.componentInstance.dragItems.map(item => item.element.nativeElement), ); documentElement.style.position = ''; @@ -3414,7 +3394,12 @@ export function defineCommonDropListTests(config: { fixture.detectChanges(); }); - config.assertDownwardSorting(fixture, Array.from(dropZone.querySelectorAll('.cdk-drag'))); + assertDownwardSorting( + 'vertical', + fixture, + config.getSortedSiblings, + Array.from(dropZone.querySelectorAll('.cdk-drag')), + ); })); it('should be able to return the last item inside its initial container', fakeAsync(() => { @@ -4132,7 +4117,7 @@ export function defineCommonDropListTests(config: { expect(dropZones[0].contains(placeholder)) .withContext('Expected placeholder to be inside the first container.') .toBe(true); - expect(config.getElementIndexByPosition(placeholder, 'top')) + expect(config.getSortedSiblings(placeholder, 'top').indexOf(placeholder)) .withContext('Expected placeholder to be at item index.') .toBe(1); @@ -4142,7 +4127,7 @@ export function defineCommonDropListTests(config: { expect(dropZones[1].contains(placeholder)) .withContext('Expected placeholder to be inside second container.') .toBe(true); - expect(config.getElementIndexByPosition(placeholder, 'top')) + expect(config.getSortedSiblings(placeholder, 'top').indexOf(placeholder)) .withContext('Expected placeholder to be at the target index.') .toBe(3); @@ -4160,7 +4145,7 @@ export function defineCommonDropListTests(config: { expect(dropZones[0].contains(placeholder)) .withContext('Expected placeholder to be back inside first container.') .toBe(true); - expect(config.getElementIndexByPosition(placeholder, 'top')) + expect(config.getSortedSiblings(placeholder, 'top').indexOf(placeholder)) .withContext('Expected placeholder to be back at the initial index.') .toBe(1); @@ -4189,7 +4174,7 @@ export function defineCommonDropListTests(config: { expect(dropZones[0].contains(placeholder)) .withContext('Expected placeholder to be inside the first container.') .toBe(true); - expect(config.getElementIndexByPosition(placeholder, 'top')) + expect(config.getSortedSiblings(placeholder, 'top').indexOf(placeholder)) .withContext('Expected placeholder to be at item index.') .toBe(1); @@ -4199,7 +4184,7 @@ export function defineCommonDropListTests(config: { expect(dropZones[1].contains(placeholder)) .withContext('Expected placeholder to be inside second container.') .toBe(true); - expect(config.getElementIndexByPosition(placeholder, 'top')) + expect(config.getSortedSiblings(placeholder, 'top').indexOf(placeholder)) .withContext('Expected placeholder to be at the target index.') .toBe(3); @@ -4212,7 +4197,7 @@ export function defineCommonDropListTests(config: { expect(dropZones[0].contains(placeholder)) .withContext('Expected placeholder to be back inside first container.') .toBe(true); - expect(config.getElementIndexByPosition(placeholder, 'top')) + expect(config.getSortedSiblings(placeholder, 'top').indexOf(placeholder)) .withContext('Expected placeholder to be at the index at which it entered.') .toBe(2); })); @@ -4236,7 +4221,7 @@ export function defineCommonDropListTests(config: { expect(dropZones[0].contains(placeholder)) .withContext('Expected placeholder to be inside the first container.') .toBe(true); - expect(config.getElementIndexByPosition(placeholder, 'top')) + expect(config.getSortedSiblings(placeholder, 'top').indexOf(placeholder)) .withContext('Expected placeholder to be at item index.') .toBe(lastIndex); @@ -4246,7 +4231,7 @@ export function defineCommonDropListTests(config: { expect(dropZones[1].contains(placeholder)) .withContext('Expected placeholder to be inside second container.') .toBe(true); - expect(config.getElementIndexByPosition(placeholder, 'top')) + expect(config.getSortedSiblings(placeholder, 'top').indexOf(placeholder)) .withContext('Expected placeholder to be at the target index.') .toBe(3); @@ -4264,7 +4249,7 @@ export function defineCommonDropListTests(config: { expect(dropZones[0].contains(placeholder)) .withContext('Expected placeholder to be back inside first container.') .toBe(true); - expect(config.getElementIndexByPosition(placeholder, 'top')) + expect(config.getSortedSiblings(placeholder, 'top').indexOf(placeholder)) .withContext('Expected placeholder to be back at the initial index.') .toBe(lastIndex); @@ -4689,6 +4674,85 @@ export function defineCommonDropListTests(config: { }); } +function assertDownwardSorting( + listOrientation: 'vertical' | 'horizontal', + fixture: ComponentFixture, + getSortedSiblings: SortedSiblingsFunction, + items: Element[], +) { + const draggedItem = items[0]; + const {top, left} = draggedItem.getBoundingClientRect(); + + startDraggingViaMouse(fixture, draggedItem, left, top); + + const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; + + // Drag over each item one-by-one going downwards. + for (let i = 0; i < items.length; i++) { + const elementRect = items[i].getBoundingClientRect(); + + // Add a few pixels to the top offset so we get some overlap. + if (listOrientation === 'vertical') { + dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.top + 5); + } else { + dispatchMouseEvent(document, 'mousemove', elementRect.left + 5, elementRect.top); + } + + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + const sortedSiblings = getSortedSiblings( + placeholder, + listOrientation === 'vertical' ? 'top' : 'left', + ); + expect(sortedSiblings.indexOf(placeholder)).toBe(i); + } + + dispatchMouseEvent(document, 'mouseup'); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + flush(); +} + +function assertUpwardSorting( + listOrientation: 'vertical' | 'horizontal', + fixture: ComponentFixture, + getSortedSiblings: SortedSiblingsFunction, + items: Element[], +) { + const draggedItem = items[items.length - 1]; + const {top, left} = draggedItem.getBoundingClientRect(); + + startDraggingViaMouse(fixture, draggedItem, left, top); + + const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; + + // Drag over each item one-by-one going upwards. + for (let i = items.length - 1; i > -1; i--) { + const elementRect = items[i].getBoundingClientRect(); + + // Remove a few pixels from the bottom offset so we get some overlap. + if (listOrientation === 'vertical') { + dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.bottom - 5); + } else { + dispatchMouseEvent(document, 'mousemove', elementRect.right - 5, elementRect.top); + } + + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + expect( + getSortedSiblings(placeholder, listOrientation === 'vertical' ? 'top' : 'left').indexOf( + placeholder, + ), + ).toBe(i); + } + + dispatchMouseEvent(document, 'mouseup'); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + flush(); +} + /** * Dynamically creates the horizontal list fixtures. They need to be * generated so that the list orientation can be changed between tests. diff --git a/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts b/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts index ca353cd5ce13..324ccc8b8776 100644 --- a/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts +++ b/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts @@ -1,4 +1,4 @@ -import {ComponentFixture, fakeAsync, flush} from '@angular/core/testing'; +import {fakeAsync, flush} from '@angular/core/testing'; import {dispatchMouseEvent} from '@angular/cdk/testing/private'; import {_supportsShadowDom} from '@angular/cdk/platform'; import {createComponent, startDraggingViaMouse} from './test-utils.spec'; @@ -18,10 +18,13 @@ describe('Single-axis drop list', () => { defineCommonDropListTests({ verticalListOrientation: 'vertical', horizontalListOrientation: 'horizontal', - getElementIndexByPosition, - getElementSibligsByPosition, - assertUpwardSorting, - assertDownwardSorting, + getSortedSiblings: (element, direction) => { + return element.parentElement + ? Array.from(element.parentElement.children).sort((a, b) => { + return a.getBoundingClientRect()[direction] - b.getBoundingClientRect()[direction]; + }) + : []; + }, }); it('should lay out the elements correctly, when swapping down with a taller element', fakeAsync(() => { @@ -309,65 +312,3 @@ describe('Single-axis drop list', () => { dispatchMouseEvent(document, 'mouseup'); })); }); - -function getElementIndexByPosition(element: Element, direction: 'top' | 'left') { - return getElementSibligsByPosition(element, direction).indexOf(element); -} - -function getElementSibligsByPosition(element: Element, direction: 'top' | 'left') { - return element.parentElement - ? Array.from(element.parentElement.children).sort((a, b) => { - return a.getBoundingClientRect()[direction] - b.getBoundingClientRect()[direction]; - }) - : []; -} - -function assertDownwardSorting(fixture: ComponentFixture, items: Element[]) { - const draggedItem = items[0]; - const {top, left} = draggedItem.getBoundingClientRect(); - - startDraggingViaMouse(fixture, draggedItem, left, top); - - const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - - // Drag over each item one-by-one going downwards. - for (let i = 0; i < items.length; i++) { - const elementRect = items[i].getBoundingClientRect(); - - // Add a few pixels to the top offset so we get some overlap. - dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.top + 5); - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - expect(getElementIndexByPosition(placeholder, 'top')).toBe(i); - } - - dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - flush(); -} - -function assertUpwardSorting(fixture: ComponentFixture, items: Element[]) { - const draggedItem = items[items.length - 1]; - const {top, left} = draggedItem.getBoundingClientRect(); - - startDraggingViaMouse(fixture, draggedItem, left, top); - - const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; - - // Drag over each item one-by-one going upwards. - for (let i = items.length - 1; i > -1; i--) { - const elementRect = items[i].getBoundingClientRect(); - - // Remove a few pixels from the bottom offset so we get some overlap. - dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.bottom - 5); - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - expect(getElementIndexByPosition(placeholder, 'top')).toBe(i); - } - - dispatchMouseEvent(document, 'mouseup'); - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - flush(); -} From d0ca10bf46661a170feb0a9acf27ebea7c961689 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 7 Jun 2024 13:02:12 +0200 Subject: [PATCH 42/61] refactor(cdk/drag-drop): simplify sort strategy interface Simplifies the interface for sort strategies to avoid the unnecessary generics that were put in place to avoid circular imports. --- src/cdk/drag-drop/drop-list-ref.ts | 14 +++++-- .../sorting/drop-list-sort-strategy.ts | 33 ++++----------- .../sorting/single-axis-sort-strategy.ts | 41 ++++++++----------- 3 files changed, 37 insertions(+), 51 deletions(-) diff --git a/src/cdk/drag-drop/drop-list-ref.ts b/src/cdk/drag-drop/drop-list-ref.ts index e648c40a93db..eaf77b8967ae 100644 --- a/src/cdk/drag-drop/drop-list-ref.ts +++ b/src/cdk/drag-drop/drop-list-ref.ts @@ -146,7 +146,7 @@ export class DropListRef { private _parentPositions: ParentPositionTracker; /** Strategy being used to sort items within the list. */ - private _sortStrategy: DropListSortStrategy; + private _sortStrategy: DropListSortStrategy; /** Cached `DOMRect` of the drop list. */ private _domRect: DOMRect | undefined; @@ -187,6 +187,9 @@ export class DropListRef { /** Initial value for the element's `scroll-snap-type` style. */ private _initialScrollSnap: string; + /** Direction of the list's layout. */ + private _direction: Direction = 'ltr'; + constructor( element: ElementRef | HTMLElement, private _dragDropRegistry: DragDropRegistry, @@ -332,7 +335,10 @@ export class DropListRef { /** Sets the layout direction of the drop list. */ withDirection(direction: Direction): this { - this._sortStrategy.direction = direction; + this._direction = direction; + if (this._sortStrategy instanceof SingleAxisSortStrategy) { + this._sortStrategy.direction = direction; + } return this; } @@ -353,7 +359,7 @@ export class DropListRef { withOrientation(orientation: 'vertical' | 'horizontal'): this { // TODO(crisbeto): eventually we should be constructing the new sort strategy here based on // the new orientation. For now we can assume that it'll always be `SingleAxisSortStrategy`. - (this._sortStrategy as SingleAxisSortStrategy).orientation = orientation; + (this._sortStrategy as SingleAxisSortStrategy).orientation = orientation; return this; } @@ -455,7 +461,7 @@ export class DropListRef { [verticalScrollDirection, horizontalScrollDirection] = getElementScrollDirections( element as HTMLElement, position.clientRect, - this._sortStrategy.direction, + this._direction, pointerX, pointerY, ); diff --git a/src/cdk/drag-drop/sorting/drop-list-sort-strategy.ts b/src/cdk/drag-drop/sorting/drop-list-sort-strategy.ts index 68046ffadfc4..f6574811bfe6 100644 --- a/src/cdk/drag-drop/sorting/drop-list-sort-strategy.ts +++ b/src/cdk/drag-drop/sorting/drop-list-sort-strategy.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Direction} from '@angular/cdk/bidi'; +import type {DragRef} from '../drag-ref'; /** * Function that is used to determine whether an item can be sorted into a particular index. @@ -14,38 +14,23 @@ import {Direction} from '@angular/cdk/bidi'; */ export type SortPredicate = (index: number, item: T) => boolean; -/** - * Item that can be sorted within `DropListSortStrategy`. This is a limited representation of - * `DragRef` used to avoid circular dependencies. It is intended to only be used within - * `DropListSortStrategy`. - * @docs-private - */ -export interface DropListSortStrategyItem { - isDragging(): boolean; - getPlaceholderElement(): HTMLElement; - getRootElement(): HTMLElement; - _sortFromLastPointerPosition(): void; - getVisibleElement(): HTMLElement; -} - /** * Strategy used to sort and position items within a drop list. * @docs-private */ -export interface DropListSortStrategy { - direction: Direction; - start(items: readonly T[]): void; +export interface DropListSortStrategy { + start(items: readonly DragRef[]): void; sort( - item: T, + item: DragRef, pointerX: number, pointerY: number, pointerDelta: {x: number; y: number}, ): {previousIndex: number; currentIndex: number} | null; - enter(item: T, pointerX: number, pointerY: number, index?: number): void; - withItems(items: readonly T[]): void; - withSortPredicate(predicate: SortPredicate): void; + enter(item: DragRef, pointerX: number, pointerY: number, index?: number): void; + withItems(items: readonly DragRef[]): void; + withSortPredicate(predicate: SortPredicate): void; reset(): void; - getActiveItemsSnapshot(): readonly T[]; - getItemIndex(item: T): number; + getActiveItemsSnapshot(): readonly DragRef[]; + getItemIndex(item: DragRef): number; updateOnScroll(topDifference: number, leftDifference: number): void; } diff --git a/src/cdk/drag-drop/sorting/single-axis-sort-strategy.ts b/src/cdk/drag-drop/sorting/single-axis-sort-strategy.ts index f7177bbf5052..7ce70419deda 100644 --- a/src/cdk/drag-drop/sorting/single-axis-sort-strategy.ts +++ b/src/cdk/drag-drop/sorting/single-axis-sort-strategy.ts @@ -13,11 +13,8 @@ import {DragDropRegistry} from '../drag-drop-registry'; import {moveItemInArray} from '../drag-utils'; import {combineTransforms} from '../dom/styling'; import {adjustDomRect, getMutableClientRect, isInsideClientRect} from '../dom/dom-rect'; -import { - DropListSortStrategy, - DropListSortStrategyItem, - SortPredicate, -} from './drop-list-sort-strategy'; +import {DropListSortStrategy, SortPredicate} from './drop-list-sort-strategy'; +import type {DragRef} from '../drag-ref'; /** * Entry in the position cache for draggable items. @@ -39,21 +36,19 @@ interface CachedItemPosition { * Items are reordered using CSS transforms which allows for sorting to be animated. * @docs-private */ -export class SingleAxisSortStrategy - implements DropListSortStrategy -{ +export class SingleAxisSortStrategy implements DropListSortStrategy { /** Function used to determine if an item can be sorted into a specific index. */ - private _sortPredicate: SortPredicate; + private _sortPredicate: SortPredicate; /** Cache of the dimensions of all the items inside the container. */ - private _itemPositions: CachedItemPosition[] = []; + private _itemPositions: CachedItemPosition[] = []; /** * Draggable items that are currently active inside the container. Includes the items * that were there at the start of the sequence, as well as any items that have been dragged * in, but haven't been dropped yet. */ - private _activeDraggables: T[]; + private _activeDraggables: DragRef[]; /** Direction in which the list is oriented. */ orientation: 'vertical' | 'horizontal' = 'vertical'; @@ -63,7 +58,7 @@ export class SingleAxisSortStrategy constructor( private _element: HTMLElement | ElementRef, - private _dragDropRegistry: DragDropRegistry, + private _dragDropRegistry: DragDropRegistry, ) {} /** @@ -72,7 +67,7 @@ export class SingleAxisSortStrategy * overlap with the swapped item after the swapping occurred. */ private _previousSwap = { - drag: null as T | null, + drag: null as DragRef | null, delta: 0, overlaps: false, }; @@ -81,7 +76,7 @@ export class SingleAxisSortStrategy * To be called when the drag sequence starts. * @param items Items that are currently in the list. */ - start(items: readonly T[]) { + start(items: readonly DragRef[]) { this.withItems(items); } @@ -92,7 +87,7 @@ export class SingleAxisSortStrategy * @param pointerY Position of the item along the Y axis. * @param pointerDelta Direction in which the pointer is moving along each axis. */ - sort(item: T, pointerX: number, pointerY: number, pointerDelta: {x: number; y: number}) { + sort(item: DragRef, pointerX: number, pointerY: number, pointerDelta: {x: number; y: number}) { const siblings = this._itemPositions; const newIndex = this._getItemIndexFromPointerPosition(item, pointerX, pointerY, pointerDelta); @@ -172,7 +167,7 @@ export class SingleAxisSortStrategy * @param index Index at which the item entered. If omitted, the container will try to figure it * out automatically. */ - enter(item: T, pointerX: number, pointerY: number, index?: number): void { + enter(item: DragRef, pointerX: number, pointerY: number, index?: number): void { const newIndex = index == null || index < 0 ? // We use the coordinates of where the item entered the drop @@ -183,7 +178,7 @@ export class SingleAxisSortStrategy const activeDraggables = this._activeDraggables; const currentIndex = activeDraggables.indexOf(item); const placeholder = item.getPlaceholderElement(); - let newPositionReference: T | undefined = activeDraggables[newIndex]; + let newPositionReference: DragRef | undefined = activeDraggables[newIndex]; // If the item at the new position is the same as the item that is being dragged, // it means that we're trying to restore the item to its initial position. In this @@ -229,13 +224,13 @@ export class SingleAxisSortStrategy } /** Sets the items that are currently part of the list. */ - withItems(items: readonly T[]): void { + withItems(items: readonly DragRef[]): void { this._activeDraggables = items.slice(); this._cacheItemPositions(); } /** Assigns a sort predicate to the strategy. */ - withSortPredicate(predicate: SortPredicate): void { + withSortPredicate(predicate: SortPredicate): void { this._sortPredicate = predicate; } @@ -262,12 +257,12 @@ export class SingleAxisSortStrategy * Gets a snapshot of items currently in the list. * Can include items that we dragged in from another list. */ - getActiveItemsSnapshot(): readonly T[] { + getActiveItemsSnapshot(): readonly DragRef[] { return this._activeDraggables; } /** Gets the index of a specific item. */ - getItemIndex(item: T): number { + getItemIndex(item: DragRef): number { // Items are sorted always by top/left in the cache, however they flow differently in RTL. // The rest of the logic still stands no matter what orientation we're in, however // we need to invert the array when determining the index. @@ -351,7 +346,7 @@ export class SingleAxisSortStrategy */ private _getSiblingOffsetPx( currentIndex: number, - siblings: CachedItemPosition[], + siblings: CachedItemPosition[], delta: 1 | -1, ) { const isHorizontal = this.orientation === 'horizontal'; @@ -410,7 +405,7 @@ export class SingleAxisSortStrategy * @param delta Direction in which the user is moving their pointer. */ private _getItemIndexFromPointerPosition( - item: T, + item: DragRef, pointerX: number, pointerY: number, delta?: {x: number; y: number}, From 0bc65838926e88723bfc677fc3e4de81826cfe5b Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 10 Jun 2024 10:21:49 +0200 Subject: [PATCH 43/61] feat(cdk/drag-drop): add mixed orientation support Currently the drop list sorts items by moving them using a `transform` which keeps the DOM stable and allows for the sorting to be animated, but has the drawback of only allowing sorting in one direction. These changes implement a new `DropListSortStrategy` that allows sorting of lists that can wrap by moving the DOM nodes around directly, rather than via a `transform`. It has the caveat that it can't animate the sorting action. The new strategy can be enabled by setting `cdkDropListOrientation="mixed"`. Fixes #13372. --- src/cdk/drag-drop/directives/config.ts | 2 +- .../directives/drop-list-shared.spec.ts | 63 +++- .../directives/mixed-drop-list.spec.ts | 144 +++++++++ src/cdk/drag-drop/drag-drop.md | 14 +- src/cdk/drag-drop/drag-ref.ts | 3 +- src/cdk/drag-drop/drop-list-ref.ts | 27 +- .../drag-drop/sorting/mixed-sort-strategy.ts | 305 ++++++++++++++++++ .../sorting/single-axis-sort-strategy.ts | 6 +- .../cdk-drag-drop-mixed-sorting-example.css | 42 +++ .../cdk-drag-drop-mixed-sorting-example.html | 5 + .../cdk-drag-drop-mixed-sorting-example.ts | 20 ++ .../cdk/drag-drop/index.ts | 1 + src/dev-app/drag-drop/BUILD.bazel | 1 + src/dev-app/drag-drop/drag-drop-demo.html | 46 +++ src/dev-app/drag-drop/drag-drop-demo.scss | 36 ++- src/dev-app/drag-drop/drag-drop-demo.ts | 5 + tools/public_api_guard/cdk/drag-drop.md | 4 +- 17 files changed, 683 insertions(+), 41 deletions(-) create mode 100644 src/cdk/drag-drop/directives/mixed-drop-list.spec.ts create mode 100644 src/cdk/drag-drop/sorting/mixed-sort-strategy.ts create mode 100644 src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.css create mode 100644 src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.html create mode 100644 src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.ts diff --git a/src/cdk/drag-drop/directives/config.ts b/src/cdk/drag-drop/directives/config.ts index 06a05c9c72cf..3734137ef226 100644 --- a/src/cdk/drag-drop/directives/config.ts +++ b/src/cdk/drag-drop/directives/config.ts @@ -19,7 +19,7 @@ export type DragAxis = 'x' | 'y'; export type DragConstrainPosition = (point: Point, dragRef: DragRef) => Point; /** Possible orientations for a drop list. */ -export type DropListOrientation = 'horizontal' | 'vertical'; +export type DropListOrientation = 'horizontal' | 'vertical' | 'mixed'; /** * Injection token that can be used to configure the diff --git a/src/cdk/drag-drop/directives/drop-list-shared.spec.ts b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts index e45102078fea..ad3f25577d00 100644 --- a/src/cdk/drag-drop/directives/drop-list-shared.spec.ts +++ b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts @@ -1,5 +1,5 @@ import {Directionality} from '@angular/cdk/bidi'; -import {_supportsShadowDom} from '@angular/cdk/platform'; +import {Platform, _supportsShadowDom} from '@angular/cdk/platform'; import {CdkScrollable, ViewportRuler} from '@angular/cdk/scrolling'; import { createMouseEvent, @@ -803,6 +803,26 @@ export function defineCommonDropListTests(config: { scrollTo(0, 0); })); + it('should remove the anchor node once dragging stops', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); + const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; + const list = fixture.componentInstance.dropInstance.element.nativeElement; + + startDraggingViaMouse(fixture, item); + + const anchor = Array.from(list.childNodes).find( + node => node.textContent === 'cdk-drag-anchor', + ); + expect(anchor).toBeTruthy(); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + + expect(anchor!.parentNode).toBeFalsy(); + })); + it('should create a preview element while the item is dragged', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.detectChanges(); @@ -1489,7 +1509,7 @@ export function defineCommonDropListTests(config: { it('should move the placeholder as an item is being sorted down', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.detectChanges(); - assertDownwardSorting( + assertStartToEndSorting( 'vertical', fixture, config.getSortedSiblings, @@ -1503,7 +1523,7 @@ export function defineCommonDropListTests(config: { const cleanup = makeScrollable(); scrollTo(0, 5000); - assertDownwardSorting( + assertStartToEndSorting( 'vertical', fixture, config.getSortedSiblings, @@ -1515,7 +1535,7 @@ export function defineCommonDropListTests(config: { it('should move the placeholder as an item is being sorted up', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.detectChanges(); - assertUpwardSorting( + assertEndToStartSorting( 'vertical', fixture, config.getSortedSiblings, @@ -1529,7 +1549,7 @@ export function defineCommonDropListTests(config: { const cleanup = makeScrollable(); scrollTo(0, 5000); - assertUpwardSorting( + assertEndToStartSorting( 'vertical', fixture, config.getSortedSiblings, @@ -1541,7 +1561,7 @@ export function defineCommonDropListTests(config: { it('should move the placeholder as an item is being sorted to the right', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalDropZone); fixture.detectChanges(); - assertDownwardSorting( + assertStartToEndSorting( 'horizontal', fixture, config.getSortedSiblings, @@ -1552,7 +1572,7 @@ export function defineCommonDropListTests(config: { it('should move the placeholder as an item is being sorted to the left', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalDropZone); fixture.detectChanges(); - assertUpwardSorting( + assertEndToStartSorting( 'horizontal', fixture, config.getSortedSiblings, @@ -1901,15 +1921,28 @@ export function defineCommonDropListTests(config: { })); it('should keep the preview next to the trigger if the page was scrolled', fakeAsync(() => { + const extractTransform = (element: HTMLElement) => { + const match = element.style.transform.match(/translate3d\(\d+px, (\d+)px, \d+px\)/); + return match ? parseInt(match[1]) : 0; + }; + const fixture = createComponent(DraggableInDropZoneWithCustomPreview); fixture.detectChanges(); + const platform = TestBed.inject(Platform); + + // The programmatic scrolling inside the Karma iframe doesn't seem to work on iOS in the CI. + // Skip the test since the logic is the same for all other browsers which are covered. + if (platform.IOS) { + return; + } + const cleanup = makeScrollable(); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; startDraggingViaMouse(fixture, item, 50, 50); const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; - expect(preview.style.transform).toBe('translate3d(50px, 50px, 0px)'); + expect(extractTransform(preview)).toBe(50); scrollTo(0, 5000); fixture.detectChanges(); @@ -1918,7 +1951,9 @@ export function defineCommonDropListTests(config: { dispatchMouseEvent(document, 'mousemove', 55, 55); fixture.detectChanges(); - expect(preview.style.transform).toBe('translate3d(55px, 1571px, 0px)'); + // Note that here we just check that the value is greater, because on the + // CI the values end up being inconsistent between browsers. + expect(extractTransform(preview)).toBeGreaterThan(1000); cleanup(); })); @@ -2603,6 +2638,8 @@ export function defineCommonDropListTests(config: { dispatchMouseEvent(document, 'mouseup'); fixture.detectChanges(); tickAnimationFrames(20); + flush(); + fixture.detectChanges(); expect(list.scrollTop).toBe(previousScrollTop); })); @@ -3130,7 +3167,7 @@ export function defineCommonDropListTests(config: { documentElement.style.position = 'absolute'; documentElement.style.top = '100px'; - assertDownwardSorting( + assertStartToEndSorting( 'vertical', fixture, config.getSortedSiblings, @@ -3394,7 +3431,7 @@ export function defineCommonDropListTests(config: { fixture.detectChanges(); }); - assertDownwardSorting( + assertStartToEndSorting( 'vertical', fixture, config.getSortedSiblings, @@ -4674,7 +4711,7 @@ export function defineCommonDropListTests(config: { }); } -function assertDownwardSorting( +export function assertStartToEndSorting( listOrientation: 'vertical' | 'horizontal', fixture: ComponentFixture, getSortedSiblings: SortedSiblingsFunction, @@ -4714,7 +4751,7 @@ function assertDownwardSorting( flush(); } -function assertUpwardSorting( +export function assertEndToStartSorting( listOrientation: 'vertical' | 'horizontal', fixture: ComponentFixture, getSortedSiblings: SortedSiblingsFunction, diff --git a/src/cdk/drag-drop/directives/mixed-drop-list.spec.ts b/src/cdk/drag-drop/directives/mixed-drop-list.spec.ts new file mode 100644 index 000000000000..8deaae351dd5 --- /dev/null +++ b/src/cdk/drag-drop/directives/mixed-drop-list.spec.ts @@ -0,0 +1,144 @@ +import {Component, QueryList, ViewChild, ViewChildren} from '@angular/core'; +import {fakeAsync, flush} from '@angular/core/testing'; +import {CdkDropList} from './drop-list'; +import {CdkDrag} from './drag'; +import {moveItemInArray} from '../drag-utils'; +import {CdkDragDrop} from '../drag-events'; +import { + ITEM_HEIGHT, + ITEM_WIDTH, + assertStartToEndSorting, + assertEndToStartSorting, + defineCommonDropListTests, +} from './drop-list-shared.spec'; +import {createComponent, dragElementViaMouse} from './test-utils.spec'; + +describe('mixed drop list', () => { + defineCommonDropListTests({ + verticalListOrientation: 'mixed', + horizontalListOrientation: 'mixed', + getSortedSiblings, + }); + + it('should dispatch the `dropped` event in a wrapping drop zone', fakeAsync(() => { + const fixture = createComponent(DraggableInHorizontalWrappingDropZone); + fixture.detectChanges(); + const dragItems = fixture.componentInstance.dragItems; + + expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim())).toEqual([ + 'Zero', + 'One', + 'Two', + 'Three', + 'Four', + 'Five', + 'Six', + 'Seven', + ]); + + const firstItem = dragItems.first; + const seventhItemRect = dragItems.toArray()[6].element.nativeElement.getBoundingClientRect(); + + dragElementViaMouse( + fixture, + firstItem.element.nativeElement, + seventhItemRect.left + 1, + seventhItemRect.top + 1, + ); + flush(); + fixture.detectChanges(); + + expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); + const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; + + // Assert the event like this, rather than `toHaveBeenCalledWith`, because Jasmine will + // go into an infinite loop trying to stringify the event, if the test fails. + expect(event).toEqual({ + previousIndex: 0, + currentIndex: 6, + item: firstItem, + container: fixture.componentInstance.dropInstance, + previousContainer: fixture.componentInstance.dropInstance, + isPointerOverContainer: true, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)}, + dropPoint: {x: jasmine.any(Number), y: jasmine.any(Number)}, + event: jasmine.anything(), + }); + + expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim())).toEqual([ + 'One', + 'Two', + 'Three', + 'Four', + 'Five', + 'Six', + 'Zero', + 'Seven', + ]); + })); + + it('should move the placeholder as an item is being sorted to the right in a wrapping drop zone', fakeAsync(() => { + const fixture = createComponent(DraggableInHorizontalWrappingDropZone); + fixture.detectChanges(); + assertStartToEndSorting( + 'horizontal', + fixture, + getSortedSiblings, + fixture.componentInstance.dragItems.map(item => item.element.nativeElement), + ); + })); + + it('should move the placeholder as an item is being sorted to the left in a wrapping drop zone', fakeAsync(() => { + const fixture = createComponent(DraggableInHorizontalWrappingDropZone); + fixture.detectChanges(); + assertEndToStartSorting( + 'horizontal', + fixture, + getSortedSiblings, + fixture.componentInstance.dragItems.map(item => item.element.nativeElement), + ); + })); +}); + +function getSortedSiblings(item: Element) { + return Array.from(item.parentElement?.children || []); +} + +@Component({ + styles: ` + .cdk-drop-list { + display: block; + width: ${ITEM_WIDTH * 3}px; + background: pink; + font-size: 0; + } + + .cdk-drag { + height: ${ITEM_HEIGHT * 2}px; + width: ${ITEM_WIDTH}px; + background: red; + display: inline-block; + } + `, + template: ` +
+ @for (item of items; track item) { +
{{item}}
+ } +
+ `, + standalone: true, + imports: [CdkDropList, CdkDrag], +}) +class DraggableInHorizontalWrappingDropZone { + @ViewChildren(CdkDrag) dragItems: QueryList; + @ViewChild(CdkDropList) dropInstance: CdkDropList; + items = ['Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven']; + droppedSpy = jasmine.createSpy('dropped spy').and.callFake((event: CdkDragDrop) => { + moveItemInArray(this.items, event.previousIndex, event.currentIndex); + }); +} diff --git a/src/cdk/drag-drop/drag-drop.md b/src/cdk/drag-drop/drag-drop.md index 0d90da0800fc..f9f7ad1b02e8 100644 --- a/src/cdk/drag-drop/drag-drop.md +++ b/src/cdk/drag-drop/drag-drop.md @@ -157,10 +157,22 @@ directive: ### List orientation The `cdkDropList` directive assumes that lists are vertical by default. This can be -changed by setting the `orientation` property to `"horizontal". +changed by setting the `cdkDropListOrientation` property to `horizontal`. +### List wrapping +By default the `cdkDropList` sorts the items by moving them around using a CSS `transform`. This +allows for the sorting to be animated which provides a better user experience, but comes with the +drawback that it works only one direction: vertically or horizontally. + +If you have a sortable list that needs to wrap, you can set `cdkDropListOrientation="mixed"` which +will use a different strategy of sorting the elements that works by moving them in the DOM. It has +the advantage of allowing the items to wrap to the next line, but it **cannot** animate the +sorting action. + + + ### Restricting movement within an element If you want to stop the user from being able to drag a `cdkDrag` element outside of another element, diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index b961326e84be..2b751e373171 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -822,7 +822,8 @@ export class DragRef { const element = this._rootElement; const parent = element.parentNode as HTMLElement; const placeholder = (this._placeholder = this._createPlaceholderElement()); - const anchor = (this._anchor = this._anchor || this._document.createComment('')); + const anchor = (this._anchor = + this._anchor || this._document.createComment(ngDevMode ? 'cdk-drag-anchor' : '')); // Insert an anchor node so that we can restore the element's position in the DOM. parent.insertBefore(anchor, element); diff --git a/src/cdk/drag-drop/drop-list-ref.ts b/src/cdk/drag-drop/drop-list-ref.ts index eaf77b8967ae..a1bf214aeb42 100644 --- a/src/cdk/drag-drop/drop-list-ref.ts +++ b/src/cdk/drag-drop/drop-list-ref.ts @@ -20,6 +20,8 @@ import {ParentPositionTracker} from './dom/parent-position-tracker'; import {DragCSSStyleDeclaration} from './dom/styling'; import {DropListSortStrategy} from './sorting/drop-list-sort-strategy'; import {SingleAxisSortStrategy} from './sorting/single-axis-sort-strategy'; +import {MixedSortStrategy} from './sorting/mixed-sort-strategy'; +import {DropListOrientation} from './directives/config'; /** * Proximity, as a ratio to width/height, at which a @@ -199,11 +201,9 @@ export class DropListRef { ) { this.element = coerceElement(element); this._document = _document; - this.withScrollableParents([this.element]); + this.withScrollableParents([this.element]).withOrientation('vertical'); _dragDropRegistry.registerDropContainer(this); this._parentPositions = new ParentPositionTracker(_document); - this._sortStrategy = new SingleAxisSortStrategy(this.element, _dragDropRegistry); - this._sortStrategy.withSortPredicate((index, item) => this.sortPredicate(index, item, this)); } /** Removes the drop list functionality from the DOM element. */ @@ -356,10 +356,23 @@ export class DropListRef { * Sets the orientation of the container. * @param orientation New orientation for the container. */ - withOrientation(orientation: 'vertical' | 'horizontal'): this { - // TODO(crisbeto): eventually we should be constructing the new sort strategy here based on - // the new orientation. For now we can assume that it'll always be `SingleAxisSortStrategy`. - (this._sortStrategy as SingleAxisSortStrategy).orientation = orientation; + withOrientation(orientation: DropListOrientation): this { + if (orientation === 'mixed') { + this._sortStrategy = new MixedSortStrategy( + coerceElement(this.element), + this._document, + this._dragDropRegistry, + ); + } else { + const strategy = new SingleAxisSortStrategy( + coerceElement(this.element), + this._dragDropRegistry, + ); + strategy.direction = this._direction; + strategy.orientation = orientation; + this._sortStrategy = strategy; + } + this._sortStrategy.withSortPredicate((index, item) => this.sortPredicate(index, item, this)); return this; } diff --git a/src/cdk/drag-drop/sorting/mixed-sort-strategy.ts b/src/cdk/drag-drop/sorting/mixed-sort-strategy.ts new file mode 100644 index 000000000000..ab728b64aed1 --- /dev/null +++ b/src/cdk/drag-drop/sorting/mixed-sort-strategy.ts @@ -0,0 +1,305 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {_getShadowRoot} from '@angular/cdk/platform'; +import {moveItemInArray} from '../drag-utils'; +import {DropListSortStrategy, SortPredicate} from './drop-list-sort-strategy'; +import {DragDropRegistry} from '../drag-drop-registry'; +import type {DragRef} from '../drag-ref'; + +/** + * Strategy that only supports sorting on a list that might wrap. + * Items are reordered by moving their DOM nodes around. + * @docs-private + */ +export class MixedSortStrategy implements DropListSortStrategy { + /** Function used to determine if an item can be sorted into a specific index. */ + private _sortPredicate: SortPredicate; + + /** Lazily-resolved root node containing the list. Use `_getRootNode` to read this. */ + private _rootNode: DocumentOrShadowRoot | undefined; + + /** + * Draggable items that are currently active inside the container. Includes the items + * that were there at the start of the sequence, as well as any items that have been dragged + * in, but haven't been dropped yet. + */ + private _activeItems: DragRef[]; + + /** + * Keeps track of the item that was last swapped with the dragged item, as well as what direction + * the pointer was moving in when the swap occurred and whether the user's pointer continued to + * overlap with the swapped item after the swapping occurred. + */ + private _previousSwap = { + drag: null as DragRef | null, + deltaX: 0, + deltaY: 0, + overlaps: false, + }; + + /** + * Keeps track of the relationship between a node and its next sibling. This information + * is used to restore the DOM to the order it was in before dragging started. + */ + private _relatedNodes: [node: Node, nextSibling: Node | null][] = []; + + constructor( + private _element: HTMLElement, + private _document: Document, + private _dragDropRegistry: DragDropRegistry, + ) {} + + /** + * To be called when the drag sequence starts. + * @param items Items that are currently in the list. + */ + start(items: readonly DragRef[]): void { + const childNodes = this._element.childNodes; + this._relatedNodes = []; + + for (let i = 0; i < childNodes.length; i++) { + const node = childNodes[i]; + this._relatedNodes.push([node, node.nextSibling]); + } + + this.withItems(items); + } + + /** + * To be called when an item is being sorted. + * @param item Item to be sorted. + * @param pointerX Position of the item along the X axis. + * @param pointerY Position of the item along the Y axis. + * @param pointerDelta Direction in which the pointer is moving along each axis. + */ + sort( + item: DragRef, + pointerX: number, + pointerY: number, + pointerDelta: {x: number; y: number}, + ): {previousIndex: number; currentIndex: number} | null { + const newIndex = this._getItemIndexFromPointerPosition(item, pointerX, pointerY); + const previousSwap = this._previousSwap; + + if (newIndex === -1 || this._activeItems[newIndex] === item) { + return null; + } + + const toSwapWith = this._activeItems[newIndex]; + + // Prevent too many swaps over the same item. + if ( + previousSwap.drag === toSwapWith && + previousSwap.overlaps && + previousSwap.deltaX === pointerDelta.x && + previousSwap.deltaY === pointerDelta.y + ) { + return null; + } + + const previousIndex = this.getItemIndex(item); + const current = item.getPlaceholderElement(); + const overlapElement = toSwapWith.getRootElement(); + + if (newIndex > previousIndex) { + overlapElement.after(current); + } else { + overlapElement.before(current); + } + + moveItemInArray(this._activeItems, previousIndex, newIndex); + + const newOverlapElement = this._getRootNode().elementFromPoint(pointerX, pointerY); + // Note: it's tempting to save the entire `pointerDelta` object here, however that'll + // break this functionality, because the same object is passed for all `sort` calls. + previousSwap.deltaX = pointerDelta.x; + previousSwap.deltaY = pointerDelta.y; + previousSwap.drag = toSwapWith; + previousSwap.overlaps = + overlapElement === newOverlapElement || overlapElement.contains(newOverlapElement); + + return { + previousIndex, + currentIndex: newIndex, + }; + } + + /** + * Called when an item is being moved into the container. + * @param item Item that was moved into the container. + * @param pointerX Position of the item along the X axis. + * @param pointerY Position of the item along the Y axis. + * @param index Index at which the item entered. If omitted, the container will try to figure it + * out automatically. + */ + enter(item: DragRef, pointerX: number, pointerY: number, index?: number): void { + let enterIndex = + index == null || index < 0 + ? this._getItemIndexFromPointerPosition(item, pointerX, pointerY) + : index; + + // In some cases (e.g. when the container has padding) we might not be able to figure + // out which item to insert the dragged item next to, because the pointer didn't overlap + // with anything. In that case we find the item that's closest to the pointer. + if (enterIndex === -1) { + enterIndex = this._getClosestItemIndexToPointer(item, pointerX, pointerY); + } + + const targetItem = this._activeItems[enterIndex] as DragRef | undefined; + const currentIndex = this._activeItems.indexOf(item); + + if (currentIndex > -1) { + this._activeItems.splice(currentIndex, 1); + } + + if (targetItem && !this._dragDropRegistry.isDragging(targetItem)) { + this._activeItems.splice(enterIndex, 0, item); + targetItem.getRootElement().before(item.getPlaceholderElement()); + } else { + this._activeItems.push(item); + this._element.appendChild(item.getPlaceholderElement()); + } + } + + /** Sets the items that are currently part of the list. */ + withItems(items: readonly DragRef[]): void { + this._activeItems = items.slice(); + } + + /** Assigns a sort predicate to the strategy. */ + withSortPredicate(predicate: SortPredicate): void { + this._sortPredicate = predicate; + } + + /** Resets the strategy to its initial state before dragging was started. */ + reset(): void { + const root = this._element; + const previousSwap = this._previousSwap; + + // Moving elements around in the DOM can break things like the `@for` loop, because it + // uses comment nodes to know where to insert elements. To avoid such issues, we restore + // the DOM nodes in the list to their original order when the list is reset. + // Note that this could be simpler if we just saved all the nodes, cleared the root + // and then appended them in the original order. We don't do it, because it can break + // down depending on when the snapshot was taken. E.g. we may end up snapshotting the + // placeholder element which is removed after dragging. + for (let i = this._relatedNodes.length - 1; i > -1; i--) { + const [node, nextSibling] = this._relatedNodes[i]; + if (node.parentNode === root && node.nextSibling !== nextSibling) { + if (nextSibling === null) { + root.appendChild(node); + } else if (nextSibling.parentNode === root) { + root.insertBefore(node, nextSibling); + } + } + } + + this._relatedNodes = []; + this._activeItems = []; + previousSwap.drag = null; + previousSwap.deltaX = previousSwap.deltaY = 0; + previousSwap.overlaps = false; + } + + /** + * Gets a snapshot of items currently in the list. + * Can include items that we dragged in from another list. + */ + getActiveItemsSnapshot(): readonly DragRef[] { + return this._activeItems; + } + + /** Gets the index of a specific item. */ + getItemIndex(item: DragRef): number { + return this._activeItems.indexOf(item); + } + + /** Used to notify the strategy that the scroll position has changed. */ + updateOnScroll(): void { + this._activeItems.forEach(item => { + if (this._dragDropRegistry.isDragging(item)) { + // We need to re-sort the item manually, because the pointer move + // events won't be dispatched while the user is scrolling. + item._sortFromLastPointerPosition(); + } + }); + } + + /** + * Gets the index of an item in the drop container, based on the position of the user's pointer. + * @param item Item that is being sorted. + * @param pointerX Position of the user's pointer along the X axis. + * @param pointerY Position of the user's pointer along the Y axis. + * @param delta Direction in which the user is moving their pointer. + */ + private _getItemIndexFromPointerPosition( + item: DragRef, + pointerX: number, + pointerY: number, + ): number { + const elementAtPoint = this._getRootNode().elementFromPoint( + Math.floor(pointerX), + Math.floor(pointerY), + ); + const index = elementAtPoint + ? this._activeItems.findIndex(item => { + const root = item.getRootElement(); + return elementAtPoint === root || root.contains(elementAtPoint); + }) + : -1; + return index === -1 || !this._sortPredicate(index, item) ? -1 : index; + } + + /** Lazily resolves the list's root node. */ + private _getRootNode(): DocumentOrShadowRoot { + // Resolve the root node lazily to ensure that the drop list is in its final place in the DOM. + if (!this._rootNode) { + this._rootNode = _getShadowRoot(this._element) || this._document; + } + return this._rootNode; + } + + /** + * Finds the index of the item that's closest to the item being dragged. + * @param item Item being dragged. + * @param pointerX Position of the user's pointer along the X axis. + * @param pointerY Position of the user's pointer along the Y axis. + */ + private _getClosestItemIndexToPointer(item: DragRef, pointerX: number, pointerY: number): number { + if (this._activeItems.length === 0) { + return -1; + } + + if (this._activeItems.length === 1) { + return 0; + } + + let minDistance = Infinity; + let minIndex = -1; + + // Find the Euclidean distance (https://en.wikipedia.org/wiki/Euclidean_distance) between each + // item and the pointer, and return the smallest one. Note that this is a bit flawed in that DOM + // nodes are rectangles, not points, so we use the top/left coordinates. It should be enough + // for our purposes. + for (let i = 0; i < this._activeItems.length; i++) { + const current = this._activeItems[i]; + if (current !== item) { + const {x, y} = current.getRootElement().getBoundingClientRect(); + const distance = Math.hypot(pointerX - x, pointerY - y); + + if (distance < minDistance) { + minDistance = distance; + minIndex = i; + } + } + } + + return minIndex; + } +} diff --git a/src/cdk/drag-drop/sorting/single-axis-sort-strategy.ts b/src/cdk/drag-drop/sorting/single-axis-sort-strategy.ts index 7ce70419deda..24cb859bf97c 100644 --- a/src/cdk/drag-drop/sorting/single-axis-sort-strategy.ts +++ b/src/cdk/drag-drop/sorting/single-axis-sort-strategy.ts @@ -7,8 +7,6 @@ */ import {Direction} from '@angular/cdk/bidi'; -import {ElementRef} from '@angular/core'; -import {coerceElement} from '@angular/cdk/coercion'; import {DragDropRegistry} from '../drag-drop-registry'; import {moveItemInArray} from '../drag-utils'; import {combineTransforms} from '../dom/styling'; @@ -57,7 +55,7 @@ export class SingleAxisSortStrategy implements DropListSortStrategy { direction: Direction; constructor( - private _element: HTMLElement | ElementRef, + private _element: HTMLElement, private _dragDropRegistry: DragDropRegistry, ) {} @@ -210,7 +208,7 @@ export class SingleAxisSortStrategy implements DropListSortStrategy { element.parentElement!.insertBefore(placeholder, element); activeDraggables.splice(newIndex, 0, item); } else { - coerceElement(this._element).appendChild(placeholder); + this._element.appendChild(placeholder); activeDraggables.push(item); } diff --git a/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.css b/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.css new file mode 100644 index 000000000000..eec65dd76c5c --- /dev/null +++ b/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.css @@ -0,0 +1,42 @@ +.example-list { + display: flex; + flex-wrap: wrap; + width: 505px; + max-width: 100%; + gap: 15px; + padding: 15px; + border: solid 1px #ccc; + min-height: 60px; + border-radius: 4px; + overflow: hidden; +} + +.example-box { + padding: 20px 10px; + border: solid 1px #ccc; + border-radius: 4px; + color: rgba(0, 0, 0, 0.87); + display: inline-block; + box-sizing: border-box; + cursor: move; + background: white; + text-align: center; + font-size: 14px; + min-width: 115px; +} + +.cdk-drag-preview { + box-sizing: border-box; + border-radius: 4px; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12); +} + +.cdk-drag-placeholder { + opacity: 0; +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} diff --git a/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.html b/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.html new file mode 100644 index 000000000000..e081bbe4b11b --- /dev/null +++ b/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.html @@ -0,0 +1,5 @@ +
+ @for (item of items; track item) { +
{{item}}
+ } +
diff --git a/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.ts b/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.ts new file mode 100644 index 000000000000..f2fd955665ad --- /dev/null +++ b/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.ts @@ -0,0 +1,20 @@ +import {Component} from '@angular/core'; +import {CdkDragDrop, CdkDrag, CdkDropList, moveItemInArray} from '@angular/cdk/drag-drop'; + +/** + * @title Drag&Drop horizontal wrapping list + */ +@Component({ + selector: 'cdk-drag-drop-mixed-sorting-example', + templateUrl: 'cdk-drag-drop-mixed-sorting-example.html', + styleUrl: 'cdk-drag-drop-mixed-sorting-example.css', + standalone: true, + imports: [CdkDropList, CdkDrag], +}) +export class CdkDragDropMixedSortingExample { + items = ['Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine']; + + drop(event: CdkDragDrop) { + moveItemInArray(this.items, event.previousIndex, event.currentIndex); + } +} diff --git a/src/components-examples/cdk/drag-drop/index.ts b/src/components-examples/cdk/drag-drop/index.ts index 7c41155fbf58..712fe39ac20f 100644 --- a/src/components-examples/cdk/drag-drop/index.ts +++ b/src/components-examples/cdk/drag-drop/index.ts @@ -16,3 +16,4 @@ export {CdkDragDropRootElementExample} from './cdk-drag-drop-root-element/cdk-dr export {CdkDragDropSortingExample} from './cdk-drag-drop-sorting/cdk-drag-drop-sorting-example'; export {CdkDragDropSortPredicateExample} from './cdk-drag-drop-sort-predicate/cdk-drag-drop-sort-predicate-example'; export {CdkDragDropTableExample} from './cdk-drag-drop-table/cdk-drag-drop-table-example'; +export {CdkDragDropMixedSortingExample} from './cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example'; diff --git a/src/dev-app/drag-drop/BUILD.bazel b/src/dev-app/drag-drop/BUILD.bazel index 416ba4b592d8..d41ec8215880 100644 --- a/src/dev-app/drag-drop/BUILD.bazel +++ b/src/dev-app/drag-drop/BUILD.bazel @@ -11,6 +11,7 @@ ng_module( ], deps = [ "//src/cdk/drag-drop", + "//src/material/checkbox", "//src/material/form-field", "//src/material/icon", "//src/material/input", diff --git a/src/dev-app/drag-drop/drag-drop-demo.html b/src/dev-app/drag-drop/drag-drop-demo.html index 5c011e0a1042..104101ae2496 100644 --- a/src/dev-app/drag-drop/drag-drop-demo.html +++ b/src/dev-app/drag-drop/drag-drop-demo.html @@ -68,6 +68,52 @@

Preferred Ages

+

Mixed orientation

+ +

+ Wrap list +

+ +
+
+
+ @for (item of mixedTodo; track item) { +
+ {{item}} + +
+ } +
+
+ +
+
+ @for (item of mixedDone; track item) { +
+ {{item}} + +
+ } +
+
+
+

Free dragging

{ @@ -544,7 +544,7 @@ export class DropListRef { _stopScrolling(): void; withDirection(direction: Direction): this; withItems(items: DragRef[]): this; - withOrientation(orientation: 'vertical' | 'horizontal'): this; + withOrientation(orientation: DropListOrientation): this; withScrollableParents(elements: HTMLElement[]): this; } From 70780a65d898558b3b2046a19cbe97aa40ef6832 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 12 Jun 2024 11:04:04 -0700 Subject: [PATCH 44/61] refactor(cdk/dialog): Consolidate afterNextRender calls (#29237) --- src/cdk/dialog/dialog-container.ts | 68 +++++++++++------------------- 1 file changed, 25 insertions(+), 43 deletions(-) diff --git a/src/cdk/dialog/dialog-container.ts b/src/cdk/dialog/dialog-container.ts index ab77ef8e9554..4e393db87011 100644 --- a/src/cdk/dialog/dialog-container.ts +++ b/src/cdk/dialog/dialog-container.ts @@ -257,61 +257,43 @@ export class CdkDialogContainer return; } - const element = this._elementRef.nativeElement; // If were to attempt to focus immediately, then the content of the dialog would not yet be // ready in instances where change detection has to run first. To deal with this, we simply - // wait for the microtask queue to be empty when setting focus when autoFocus isn't set to - // dialog. If the element inside the dialog can't be focused, then the container is focused - // so the user can't tab into other elements behind it. - const autoFocus = this._config.autoFocus; - switch (autoFocus) { - case false: - case 'dialog': - // Ensure that focus is on the dialog container. It's possible that a different - // component tried to move focus while the open animation was running. See: - // https://github.com/angular/components/issues/16215. Note that we only want to do this - // if the focus isn't inside the dialog already, because it's possible that the consumer - // turned off `autoFocus` in order to move focus themselves. - afterNextRender( - () => { + // wait until after the next render. + afterNextRender( + () => { + const element = this._elementRef.nativeElement; + switch (this._config.autoFocus) { + case false: + case 'dialog': + // Ensure that focus is on the dialog container. It's possible that a different + // component tried to move focus while the open animation was running. See: + // https://github.com/angular/components/issues/16215. Note that we only want to do this + // if the focus isn't inside the dialog already, because it's possible that the consumer + // turned off `autoFocus` in order to move focus themselves. if (!this._containsFocus()) { element.focus(); } - }, - {injector: this._injector}, - ); - break; - case true: - case 'first-tabbable': - afterNextRender( - () => { + break; + case true: + case 'first-tabbable': const focusedSuccessfully = this._focusTrap?.focusInitialElement(); // If we weren't able to find a focusable element in the dialog, then focus the dialog // container instead. if (!focusedSuccessfully) { this._focusDialogContainer(); } - }, - {injector: this._injector}, - ); - break; - case 'first-heading': - afterNextRender( - () => { + break; + case 'first-heading': this._focusByCssSelector('h1, h2, h3, h4, h5, h6, [role="heading"]'); - }, - {injector: this._injector}, - ); - break; - default: - afterNextRender( - () => { - this._focusByCssSelector(autoFocus!); - }, - {injector: this._injector}, - ); - break; - } + break; + default: + this._focusByCssSelector(this._config.autoFocus!); + break; + } + }, + {injector: this._injector}, + ); } /** Restores focus to the element that was focused before the dialog opened. */ From de4b13afe80e02ec04a73d39ca57bd2f9e0d362c Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 12 Jun 2024 15:27:35 -0700 Subject: [PATCH 45/61] docs(material/datepicker): Update datepicker docs & examples (#29236) --- .../date-range-picker-comparison-example.ts | 9 +-- .../date-range-picker-forms-example.ts | 9 +-- .../date-range-picker-overview-example.ts | 3 +- ...range-picker-selection-strategy-example.ts | 5 +- .../datepicker-actions-example.html | 18 +++--- .../datepicker-actions-example.ts | 3 +- .../datepicker-api/datepicker-api-example.ts | 7 ++- .../datepicker-color-example.css | 3 - .../datepicker-color-example.html | 15 ----- .../datepicker-color-example.ts | 16 ----- .../datepicker-custom-header-example.ts | 43 ++++++------- .../datepicker-custom-icon-example.ts | 9 +-- .../datepicker-date-class-example.ts | 7 ++- .../datepicker-disabled-example.ts | 7 ++- .../datepicker-events-example.html | 10 ++- .../datepicker-events-example.ts | 11 ++-- .../datepicker-filter-example.ts | 7 ++- .../datepicker-formats-example.ts | 12 ++-- .../datepicker-harness-example.html | 6 +- .../datepicker-harness-example.ts | 5 +- .../datepicker-inline-calendar-example.html | 2 +- .../datepicker-inline-calendar-example.ts | 7 ++- .../datepicker-locale-example.html | 4 +- .../datepicker-locale-example.ts | 40 ++++++------ .../datepicker-min-max-example.html | 2 +- .../datepicker-min-max-example.ts | 20 +++--- .../datepicker-moment-example.ts | 12 ++-- .../datepicker-overview-example.ts | 3 +- .../datepicker-start-view-example.ts | 9 +-- .../datepicker-touch-example.ts | 3 +- .../datepicker-value-example.ts | 11 ++-- .../datepicker-views-selection-example.ts | 5 +- .../material/datepicker/index.ts | 1 - src/material/datepicker/datepicker-base.ts | 62 +++++++++++-------- src/material/datepicker/datepicker.md | 57 ++++++++--------- 35 files changed, 210 insertions(+), 233 deletions(-) delete mode 100644 src/components-examples/material/datepicker/datepicker-color/datepicker-color-example.css delete mode 100644 src/components-examples/material/datepicker/datepicker-color/datepicker-color-example.html delete mode 100644 src/components-examples/material/datepicker/datepicker-color/datepicker-color-example.ts diff --git a/src/components-examples/material/datepicker/date-range-picker-comparison/date-range-picker-comparison-example.ts b/src/components-examples/material/datepicker/date-range-picker-comparison/date-range-picker-comparison-example.ts index b11f3ef380e0..07ce8e6a5ac0 100644 --- a/src/components-examples/material/datepicker/date-range-picker-comparison/date-range-picker-comparison-example.ts +++ b/src/components-examples/material/datepicker/date-range-picker-comparison/date-range-picker-comparison-example.ts @@ -1,5 +1,5 @@ -import {Component} from '@angular/core'; -import {FormGroup, FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {provideNativeDateAdapter} from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; import {MatFormFieldModule} from '@angular/material/form-field'; @@ -16,13 +16,14 @@ const year = today.getFullYear(); standalone: true, providers: [provideNativeDateAdapter()], imports: [MatFormFieldModule, MatDatepickerModule, FormsModule, ReactiveFormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DateRangePickerComparisonExample { - campaignOne = new FormGroup({ + readonly campaignOne = new FormGroup({ start: new FormControl(new Date(year, month, 13)), end: new FormControl(new Date(year, month, 16)), }); - campaignTwo = new FormGroup({ + readonly campaignTwo = new FormGroup({ start: new FormControl(new Date(year, month, 15)), end: new FormControl(new Date(year, month, 19)), }); diff --git a/src/components-examples/material/datepicker/date-range-picker-forms/date-range-picker-forms-example.ts b/src/components-examples/material/datepicker/date-range-picker-forms/date-range-picker-forms-example.ts index 93f7559f0757..907abf773ad0 100644 --- a/src/components-examples/material/datepicker/date-range-picker-forms/date-range-picker-forms-example.ts +++ b/src/components-examples/material/datepicker/date-range-picker-forms/date-range-picker-forms-example.ts @@ -1,9 +1,9 @@ -import {Component} from '@angular/core'; -import {FormGroup, FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {JsonPipe} from '@angular/common'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {provideNativeDateAdapter} from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; import {MatFormFieldModule} from '@angular/material/form-field'; -import {provideNativeDateAdapter} from '@angular/material/core'; /** @title Date range picker forms integration */ @Component({ @@ -12,9 +12,10 @@ import {provideNativeDateAdapter} from '@angular/material/core'; standalone: true, providers: [provideNativeDateAdapter()], imports: [MatFormFieldModule, MatDatepickerModule, FormsModule, ReactiveFormsModule, JsonPipe], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DateRangePickerFormsExample { - range = new FormGroup({ + readonly range = new FormGroup({ start: new FormControl(null), end: new FormControl(null), }); diff --git a/src/components-examples/material/datepicker/date-range-picker-overview/date-range-picker-overview-example.ts b/src/components-examples/material/datepicker/date-range-picker-overview/date-range-picker-overview-example.ts index 2b59e0a15da3..32e17bdc1cdb 100644 --- a/src/components-examples/material/datepicker/date-range-picker-overview/date-range-picker-overview-example.ts +++ b/src/components-examples/material/datepicker/date-range-picker-overview/date-range-picker-overview-example.ts @@ -1,4 +1,4 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; import {provideNativeDateAdapter} from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; import {MatFormFieldModule} from '@angular/material/form-field'; @@ -10,5 +10,6 @@ import {MatFormFieldModule} from '@angular/material/form-field'; standalone: true, imports: [MatFormFieldModule, MatDatepickerModule], providers: [provideNativeDateAdapter()], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DateRangePickerOverviewExample {} diff --git a/src/components-examples/material/datepicker/date-range-picker-selection-strategy/date-range-picker-selection-strategy-example.ts b/src/components-examples/material/datepicker/date-range-picker-selection-strategy/date-range-picker-selection-strategy-example.ts index 075be05b1183..7f18ade38e3f 100644 --- a/src/components-examples/material/datepicker/date-range-picker-selection-strategy/date-range-picker-selection-strategy-example.ts +++ b/src/components-examples/material/datepicker/date-range-picker-selection-strategy/date-range-picker-selection-strategy-example.ts @@ -1,9 +1,9 @@ -import {Component, Injectable} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Injectable} from '@angular/core'; import {DateAdapter, provideNativeDateAdapter} from '@angular/material/core'; import { - MatDateRangeSelectionStrategy, DateRange, MAT_DATE_RANGE_SELECTION_STRATEGY, + MatDateRangeSelectionStrategy, MatDatepickerModule, } from '@angular/material/datepicker'; import {MatFormFieldModule} from '@angular/material/form-field'; @@ -44,5 +44,6 @@ export class FiveDayRangeSelectionStrategy implements MatDateRangeSelectionSt ], standalone: true, imports: [MatFormFieldModule, MatDatepickerModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DateRangePickerSelectionStrategyExample {} diff --git a/src/components-examples/material/datepicker/datepicker-actions/datepicker-actions-example.html b/src/components-examples/material/datepicker/datepicker-actions/datepicker-actions-example.html index 460a271dd2d5..29ae1ae580f0 100644 --- a/src/components-examples/material/datepicker/datepicker-actions/datepicker-actions-example.html +++ b/src/components-examples/material/datepicker/datepicker-actions/datepicker-actions-example.html @@ -1,32 +1,32 @@ Choose a date - + MM/DD/YYYY - + - + - + Enter a date range - - + + MM/DD/YYYY – MM/DD/YYYY - + - + - + diff --git a/src/components-examples/material/datepicker/datepicker-actions/datepicker-actions-example.ts b/src/components-examples/material/datepicker/datepicker-actions/datepicker-actions-example.ts index aa9db2362cd4..6cd6ee931bd2 100644 --- a/src/components-examples/material/datepicker/datepicker-actions/datepicker-actions-example.ts +++ b/src/components-examples/material/datepicker/datepicker-actions/datepicker-actions-example.ts @@ -1,4 +1,4 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; import {MatButtonModule} from '@angular/material/button'; import {MatDatepickerModule} from '@angular/material/datepicker'; import {MatInputModule} from '@angular/material/input'; @@ -13,5 +13,6 @@ import {provideNativeDateAdapter} from '@angular/material/core'; standalone: true, providers: [provideNativeDateAdapter()], imports: [MatFormFieldModule, MatInputModule, MatDatepickerModule, MatButtonModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerActionsExample {} diff --git a/src/components-examples/material/datepicker/datepicker-api/datepicker-api-example.ts b/src/components-examples/material/datepicker/datepicker-api/datepicker-api-example.ts index 0560293266ff..5368a236f301 100644 --- a/src/components-examples/material/datepicker/datepicker-api/datepicker-api-example.ts +++ b/src/components-examples/material/datepicker/datepicker-api/datepicker-api-example.ts @@ -1,9 +1,9 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; import {MatButtonModule} from '@angular/material/button'; +import {provideNativeDateAdapter} from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; -import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; -import {provideNativeDateAdapter} from '@angular/material/core'; +import {MatInputModule} from '@angular/material/input'; /** @title Datepicker open method */ @Component({ @@ -13,5 +13,6 @@ import {provideNativeDateAdapter} from '@angular/material/core'; standalone: true, providers: [provideNativeDateAdapter()], imports: [MatFormFieldModule, MatInputModule, MatDatepickerModule, MatButtonModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerApiExample {} diff --git a/src/components-examples/material/datepicker/datepicker-color/datepicker-color-example.css b/src/components-examples/material/datepicker/datepicker-color/datepicker-color-example.css deleted file mode 100644 index ece1d0db513c..000000000000 --- a/src/components-examples/material/datepicker/datepicker-color/datepicker-color-example.css +++ /dev/null @@ -1,3 +0,0 @@ -mat-form-field { - margin-right: 12px; -} diff --git a/src/components-examples/material/datepicker/datepicker-color/datepicker-color-example.html b/src/components-examples/material/datepicker/datepicker-color/datepicker-color-example.html deleted file mode 100644 index 9e69871270ce..000000000000 --- a/src/components-examples/material/datepicker/datepicker-color/datepicker-color-example.html +++ /dev/null @@ -1,15 +0,0 @@ - - Inherited calendar color - - MM/DD/YYYY - - - - - - Custom calendar color - - MM/DD/YYYY - - - diff --git a/src/components-examples/material/datepicker/datepicker-color/datepicker-color-example.ts b/src/components-examples/material/datepicker/datepicker-color/datepicker-color-example.ts deleted file mode 100644 index 1c7ce411d875..000000000000 --- a/src/components-examples/material/datepicker/datepicker-color/datepicker-color-example.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {Component} from '@angular/core'; -import {MatDatepickerModule} from '@angular/material/datepicker'; -import {MatInputModule} from '@angular/material/input'; -import {MatFormFieldModule} from '@angular/material/form-field'; -import {provideNativeDateAdapter} from '@angular/material/core'; - -/** @title Datepicker palette colors */ -@Component({ - selector: 'datepicker-color-example', - templateUrl: 'datepicker-color-example.html', - styleUrl: 'datepicker-color-example.css', - standalone: true, - providers: [provideNativeDateAdapter()], - imports: [MatFormFieldModule, MatInputModule, MatDatepickerModule], -}) -export class DatepickerColorExample {} diff --git a/src/components-examples/material/datepicker/datepicker-custom-header/datepicker-custom-header-example.ts b/src/components-examples/material/datepicker/datepicker-custom-header/datepicker-custom-header-example.ts index d2b2c019b4ef..b3dc89f5d8a2 100644 --- a/src/components-examples/material/datepicker/datepicker-custom-header/datepicker-custom-header-example.ts +++ b/src/components-examples/material/datepicker/datepicker-custom-header/datepicker-custom-header-example.ts @@ -1,35 +1,29 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - Inject, - OnDestroy, -} from '@angular/core'; -import {MatCalendar, MatDatepickerModule} from '@angular/material/datepicker'; +import {ChangeDetectionStrategy, Component, Inject, OnDestroy, signal} from '@angular/core'; +import {MatButtonModule} from '@angular/material/button'; import { DateAdapter, MAT_DATE_FORMATS, MatDateFormats, provideNativeDateAdapter, } from '@angular/material/core'; -import {Subject} from 'rxjs'; -import {takeUntil} from 'rxjs/operators'; +import {MatCalendar, MatDatepickerModule} from '@angular/material/datepicker'; +import {MatFormFieldModule} from '@angular/material/form-field'; import {MatIconModule} from '@angular/material/icon'; -import {MatButtonModule} from '@angular/material/button'; import {MatInputModule} from '@angular/material/input'; -import {MatFormFieldModule} from '@angular/material/form-field'; +import {Subject} from 'rxjs'; +import {startWith, takeUntil} from 'rxjs/operators'; /** @title Datepicker with custom calendar header */ @Component({ selector: 'datepicker-custom-header-example', templateUrl: 'datepicker-custom-header-example.html', - changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, providers: [provideNativeDateAdapter()], imports: [MatFormFieldModule, MatInputModule, MatDatepickerModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerCustomHeaderExample { - exampleHeader = ExampleHeader; + readonly exampleHeader = ExampleHeader; } /** Custom header component for datepicker. */ @@ -57,7 +51,7 @@ export class DatepickerCustomHeaderExample { - {{periodLabel}} + {{periodLabel()}} @@ -66,20 +60,27 @@ export class DatepickerCustomHeaderExample {
`, - changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [MatButtonModule, MatIconModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class ExampleHeader implements OnDestroy { private _destroyed = new Subject(); + readonly periodLabel = signal(''); + constructor( private _calendar: MatCalendar, private _dateAdapter: DateAdapter, @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats, - cdr: ChangeDetectorRef, ) { - _calendar.stateChanges.pipe(takeUntil(this._destroyed)).subscribe(() => cdr.markForCheck()); + _calendar.stateChanges.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { + this.periodLabel.set( + this._dateAdapter + .format(this._calendar.activeDate, this._dateFormats.display.monthYearLabel) + .toLocaleUpperCase(), + ); + }); } ngOnDestroy() { @@ -87,12 +88,6 @@ export class ExampleHeader implements OnDestroy { this._destroyed.complete(); } - get periodLabel() { - return this._dateAdapter - .format(this._calendar.activeDate, this._dateFormats.display.monthYearLabel) - .toLocaleUpperCase(); - } - previousClicked(mode: 'month' | 'year') { this._calendar.activeDate = mode === 'month' diff --git a/src/components-examples/material/datepicker/datepicker-custom-icon/datepicker-custom-icon-example.ts b/src/components-examples/material/datepicker/datepicker-custom-icon/datepicker-custom-icon-example.ts index 752846e38c56..9e6489d466d3 100644 --- a/src/components-examples/material/datepicker/datepicker-custom-icon/datepicker-custom-icon-example.ts +++ b/src/components-examples/material/datepicker/datepicker-custom-icon/datepicker-custom-icon-example.ts @@ -1,9 +1,9 @@ -import {Component} from '@angular/core'; -import {MatIconModule} from '@angular/material/icon'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {provideNativeDateAdapter} from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; -import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; -import {provideNativeDateAdapter} from '@angular/material/core'; +import {MatIconModule} from '@angular/material/icon'; +import {MatInputModule} from '@angular/material/input'; /** @title Datepicker with custom icon */ @Component({ @@ -12,5 +12,6 @@ import {provideNativeDateAdapter} from '@angular/material/core'; standalone: true, providers: [provideNativeDateAdapter()], imports: [MatFormFieldModule, MatInputModule, MatDatepickerModule, MatIconModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerCustomIconExample {} diff --git a/src/components-examples/material/datepicker/datepicker-date-class/datepicker-date-class-example.ts b/src/components-examples/material/datepicker/datepicker-date-class/datepicker-date-class-example.ts index 769bf00b02a8..fb831f7e145b 100644 --- a/src/components-examples/material/datepicker/datepicker-date-class/datepicker-date-class-example.ts +++ b/src/components-examples/material/datepicker/datepicker-date-class/datepicker-date-class-example.ts @@ -1,8 +1,8 @@ -import {Component, ViewEncapsulation} from '@angular/core'; +import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; +import {provideNativeDateAdapter} from '@angular/material/core'; import {MatCalendarCellClassFunction, MatDatepickerModule} from '@angular/material/datepicker'; -import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; -import {provideNativeDateAdapter} from '@angular/material/core'; +import {MatInputModule} from '@angular/material/input'; /** @title Datepicker with custom date classes */ @Component({ @@ -13,6 +13,7 @@ import {provideNativeDateAdapter} from '@angular/material/core'; standalone: true, providers: [provideNativeDateAdapter()], imports: [MatFormFieldModule, MatInputModule, MatDatepickerModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerDateClassExample { dateClass: MatCalendarCellClassFunction = (cellDate, view) => { diff --git a/src/components-examples/material/datepicker/datepicker-disabled/datepicker-disabled-example.ts b/src/components-examples/material/datepicker/datepicker-disabled/datepicker-disabled-example.ts index bc49b2d75cff..2948d4cadebe 100644 --- a/src/components-examples/material/datepicker/datepicker-disabled/datepicker-disabled-example.ts +++ b/src/components-examples/material/datepicker/datepicker-disabled/datepicker-disabled-example.ts @@ -1,8 +1,8 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {provideNativeDateAdapter} from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; -import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; -import {provideNativeDateAdapter} from '@angular/material/core'; +import {MatInputModule} from '@angular/material/input'; /** @title Disabled datepicker */ @Component({ @@ -11,5 +11,6 @@ import {provideNativeDateAdapter} from '@angular/material/core'; standalone: true, providers: [provideNativeDateAdapter()], imports: [MatFormFieldModule, MatInputModule, MatDatepickerModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerDisabledExample {} diff --git a/src/components-examples/material/datepicker/datepicker-events/datepicker-events-example.html b/src/components-examples/material/datepicker/datepicker-events/datepicker-events-example.html index 1dc59fb2450d..6973491dec91 100644 --- a/src/components-examples/material/datepicker/datepicker-events/datepicker-events-example.html +++ b/src/components-examples/material/datepicker/datepicker-events/datepicker-events-example.html @@ -1,14 +1,18 @@ Input & change events - + MM/DD/YYYY
- @for (e of events; track e) { + @for (e of events(); track e) {
{{e}}
}
diff --git a/src/components-examples/material/datepicker/datepicker-events/datepicker-events-example.ts b/src/components-examples/material/datepicker/datepicker-events/datepicker-events-example.ts index e8d0d1776e15..354fabe38a80 100644 --- a/src/components-examples/material/datepicker/datepicker-events/datepicker-events-example.ts +++ b/src/components-examples/material/datepicker/datepicker-events/datepicker-events-example.ts @@ -1,8 +1,8 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component, signal} from '@angular/core'; +import {provideNativeDateAdapter} from '@angular/material/core'; import {MatDatepickerInputEvent, MatDatepickerModule} from '@angular/material/datepicker'; -import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; -import {provideNativeDateAdapter} from '@angular/material/core'; +import {MatInputModule} from '@angular/material/input'; /** @title Datepicker input and change events */ @Component({ @@ -12,11 +12,12 @@ import {provideNativeDateAdapter} from '@angular/material/core'; standalone: true, providers: [provideNativeDateAdapter()], imports: [MatFormFieldModule, MatInputModule, MatDatepickerModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerEventsExample { - events: string[] = []; + events = signal([]); addEvent(type: string, event: MatDatepickerInputEvent) { - this.events.push(`${type}: ${event.value}`); + this.events.update(events => [...events, `${type}: ${event.value}`]); } } diff --git a/src/components-examples/material/datepicker/datepicker-filter/datepicker-filter-example.ts b/src/components-examples/material/datepicker/datepicker-filter/datepicker-filter-example.ts index 48deeac155d2..18051587c147 100644 --- a/src/components-examples/material/datepicker/datepicker-filter/datepicker-filter-example.ts +++ b/src/components-examples/material/datepicker/datepicker-filter/datepicker-filter-example.ts @@ -1,8 +1,8 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {provideNativeDateAdapter} from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; -import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; -import {provideNativeDateAdapter} from '@angular/material/core'; +import {MatInputModule} from '@angular/material/input'; /** @title Datepicker with filter validation */ @Component({ @@ -11,6 +11,7 @@ import {provideNativeDateAdapter} from '@angular/material/core'; standalone: true, providers: [provideNativeDateAdapter()], imports: [MatFormFieldModule, MatInputModule, MatDatepickerModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerFilterExample { myFilter = (d: Date | null): boolean => { diff --git a/src/components-examples/material/datepicker/datepicker-formats/datepicker-formats-example.ts b/src/components-examples/material/datepicker/datepicker-formats/datepicker-formats-example.ts index b2291812c3a0..ade7995ea8ee 100644 --- a/src/components-examples/material/datepicker/datepicker-formats/datepicker-formats-example.ts +++ b/src/components-examples/material/datepicker/datepicker-formats/datepicker-formats-example.ts @@ -1,7 +1,9 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {provideMomentDateAdapter} from '@angular/material-moment-adapter'; - +import {MatDatepickerModule} from '@angular/material/datepicker'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; // Depending on whether rollup is used, moment needs to be imported differently. // Since Moment.js doesn't have a default export, we normally need to import using the `* as` // syntax. However, rollup creates a synthetic default module and we thus need to import it using @@ -9,9 +11,6 @@ import {provideMomentDateAdapter} from '@angular/material-moment-adapter'; import * as _moment from 'moment'; // tslint:disable-next-line:no-duplicate-imports import {default as _rollupMoment} from 'moment'; -import {MatDatepickerModule} from '@angular/material/datepicker'; -import {MatInputModule} from '@angular/material/input'; -import {MatFormFieldModule} from '@angular/material/form-field'; const moment = _rollupMoment || _moment; @@ -47,7 +46,8 @@ export const MY_FORMATS = { FormsModule, ReactiveFormsModule, ], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerFormatsExample { - date = new FormControl(moment()); + readonly date = new FormControl(moment()); } diff --git a/src/components-examples/material/datepicker/datepicker-harness/datepicker-harness-example.html b/src/components-examples/material/datepicker/datepicker-harness/datepicker-harness-example.html index daef3ea96968..58228373eceb 100644 --- a/src/components-examples/material/datepicker/datepicker-harness/datepicker-harness-example.html +++ b/src/components-examples/material/datepicker/datepicker-harness/datepicker-harness-example.html @@ -1,6 +1,2 @@ - + diff --git a/src/components-examples/material/datepicker/datepicker-harness/datepicker-harness-example.ts b/src/components-examples/material/datepicker/datepicker-harness/datepicker-harness-example.ts index 500d0d5b8612..7ddb30fc4e22 100644 --- a/src/components-examples/material/datepicker/datepicker-harness/datepicker-harness-example.ts +++ b/src/components-examples/material/datepicker/datepicker-harness/datepicker-harness-example.ts @@ -1,4 +1,4 @@ -import {Component, signal} from '@angular/core'; +import {ChangeDetectionStrategy, Component, model, signal} from '@angular/core'; import {FormsModule} from '@angular/forms'; import {provideNativeDateAdapter} from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; @@ -13,8 +13,9 @@ import {MatInputModule} from '@angular/material/input'; standalone: true, providers: [provideNativeDateAdapter()], imports: [MatInputModule, MatDatepickerModule, FormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerHarnessExample { - date: Date | null = null; + date = model(null); minDate = signal(null); } diff --git a/src/components-examples/material/datepicker/datepicker-inline-calendar/datepicker-inline-calendar-example.html b/src/components-examples/material/datepicker/datepicker-inline-calendar/datepicker-inline-calendar-example.html index 8ffcf05863ac..3fb2d5348f78 100644 --- a/src/components-examples/material/datepicker/datepicker-inline-calendar/datepicker-inline-calendar-example.html +++ b/src/components-examples/material/datepicker/datepicker-inline-calendar/datepicker-inline-calendar-example.html @@ -1,4 +1,4 @@ -

Selected date: {{selected}}

+

Selected date: {{selected()}}

diff --git a/src/components-examples/material/datepicker/datepicker-inline-calendar/datepicker-inline-calendar-example.ts b/src/components-examples/material/datepicker/datepicker-inline-calendar/datepicker-inline-calendar-example.ts index f7acfa610c81..b31d24d1c544 100644 --- a/src/components-examples/material/datepicker/datepicker-inline-calendar/datepicker-inline-calendar-example.ts +++ b/src/components-examples/material/datepicker/datepicker-inline-calendar/datepicker-inline-calendar-example.ts @@ -1,7 +1,7 @@ -import {Component} from '@angular/core'; -import {MatDatepickerModule} from '@angular/material/datepicker'; +import {ChangeDetectionStrategy, Component, model} from '@angular/core'; import {MatCardModule} from '@angular/material/card'; import {provideNativeDateAdapter} from '@angular/material/core'; +import {MatDatepickerModule} from '@angular/material/datepicker'; /** @title Datepicker inline calendar example */ @Component({ @@ -11,7 +11,8 @@ import {provideNativeDateAdapter} from '@angular/material/core'; standalone: true, providers: [provideNativeDateAdapter()], imports: [MatCardModule, MatDatepickerModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerInlineCalendarExample { - selected: Date | null; + selected = model(null); } diff --git a/src/components-examples/material/datepicker/datepicker-locale/datepicker-locale-example.html b/src/components-examples/material/datepicker/datepicker-locale/datepicker-locale-example.html index a0d2983857f2..aa9889acc61b 100644 --- a/src/components-examples/material/datepicker/datepicker-locale/datepicker-locale-example.html +++ b/src/components-examples/material/datepicker/datepicker-locale/datepicker-locale-example.html @@ -1,7 +1,7 @@ Different locale - - {{getDateFormatString()}} + + {{dateFormatString()}} diff --git a/src/components-examples/material/datepicker/datepicker-locale/datepicker-locale-example.ts b/src/components-examples/material/datepicker/datepicker-locale/datepicker-locale-example.ts index 17f4e061c545..383712032b7a 100644 --- a/src/components-examples/material/datepicker/datepicker-locale/datepicker-locale-example.ts +++ b/src/components-examples/material/datepicker/datepicker-locale/datepicker-locale-example.ts @@ -1,12 +1,12 @@ -import {Component, Inject, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, Component, OnInit, computed, inject, signal} from '@angular/core'; import {provideMomentDateAdapter} from '@angular/material-moment-adapter'; -import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material/core'; import {MatButtonModule} from '@angular/material/button'; -import {MatDatepickerModule, MatDatepickerIntl} from '@angular/material/datepicker'; -import {MatInputModule} from '@angular/material/input'; +import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material/core'; +import {MatDatepickerIntl, MatDatepickerModule} from '@angular/material/datepicker'; import {MatFormFieldModule} from '@angular/material/form-field'; -import 'moment/locale/ja'; +import {MatInputModule} from '@angular/material/input'; import 'moment/locale/fr'; +import 'moment/locale/ja'; /** @title Datepicker with different locale */ @Component({ @@ -25,21 +25,28 @@ import 'moment/locale/fr'; ], standalone: true, imports: [MatFormFieldModule, MatInputModule, MatDatepickerModule, MatButtonModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerLocaleExample implements OnInit { - constructor( - private _adapter: DateAdapter, - private _intl: MatDatepickerIntl, - @Inject(MAT_DATE_LOCALE) private _locale: string, - ) {} + private readonly _adapter = inject>(DateAdapter); + private readonly _intl = inject(MatDatepickerIntl); + private readonly _locale = signal(inject(MAT_DATE_LOCALE)); + readonly dateFormatString = computed(() => { + if (this._locale() === 'ja-JP') { + return 'YYYY/MM/DD'; + } else if (this._locale() === 'fr') { + return 'DD/MM/YYYY'; + } + return ''; + }); ngOnInit() { this.updateCloseButtonLabel('カレンダーを閉じる'); } french() { - this._locale = 'fr'; - this._adapter.setLocale(this._locale); + this._locale.set('fr'); + this._adapter.setLocale(this._locale()); this.updateCloseButtonLabel('Fermer le calendrier'); } @@ -47,13 +54,4 @@ export class DatepickerLocaleExample implements OnInit { this._intl.closeCalendarLabel = label; this._intl.changes.next(); } - - getDateFormatString(): string { - if (this._locale === 'ja-JP') { - return 'YYYY/MM/DD'; - } else if (this._locale === 'fr') { - return 'DD/MM/YYYY'; - } - return ''; - } } diff --git a/src/components-examples/material/datepicker/datepicker-min-max/datepicker-min-max-example.html b/src/components-examples/material/datepicker/datepicker-min-max/datepicker-min-max-example.html index a33e3f8338ce..0c0ba98aefcf 100644 --- a/src/components-examples/material/datepicker/datepicker-min-max/datepicker-min-max-example.html +++ b/src/components-examples/material/datepicker/datepicker-min-max/datepicker-min-max-example.html @@ -1,6 +1,6 @@ Choose a date - + MM/DD/YYYY diff --git a/src/components-examples/material/datepicker/datepicker-min-max/datepicker-min-max-example.ts b/src/components-examples/material/datepicker/datepicker-min-max/datepicker-min-max-example.ts index 0d71676c0ca9..d10a9302be26 100644 --- a/src/components-examples/material/datepicker/datepicker-min-max/datepicker-min-max-example.ts +++ b/src/components-examples/material/datepicker/datepicker-min-max/datepicker-min-max-example.ts @@ -1,8 +1,8 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {provideNativeDateAdapter} from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; -import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; -import {provideNativeDateAdapter} from '@angular/material/core'; +import {MatInputModule} from '@angular/material/input'; /** @title Datepicker with min & max validation */ @Component({ @@ -11,15 +11,11 @@ import {provideNativeDateAdapter} from '@angular/material/core'; standalone: true, providers: [provideNativeDateAdapter()], imports: [MatFormFieldModule, MatInputModule, MatDatepickerModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerMinMaxExample { - minDate: Date; - maxDate: Date; - - constructor() { - // Set the minimum to January 1st 20 years in the past and December 31st a year in the future. - const currentYear = new Date().getFullYear(); - this.minDate = new Date(currentYear - 20, 0, 1); - this.maxDate = new Date(currentYear + 1, 11, 31); - } + // Set the minimum to January 1st 20 years in the past and December 31st a year in the future. + private readonly _currentYear = new Date().getFullYear(); + readonly minDate = new Date(this._currentYear - 20, 0, 1); + readonly maxDate = new Date(this._currentYear + 1, 11, 31); } diff --git a/src/components-examples/material/datepicker/datepicker-moment/datepicker-moment-example.ts b/src/components-examples/material/datepicker/datepicker-moment/datepicker-moment-example.ts index 003ded4a8a28..c8d95833d83a 100644 --- a/src/components-examples/material/datepicker/datepicker-moment/datepicker-moment-example.ts +++ b/src/components-examples/material/datepicker/datepicker-moment/datepicker-moment-example.ts @@ -1,7 +1,9 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {provideMomentDateAdapter} from '@angular/material-moment-adapter'; - +import {MatDatepickerModule} from '@angular/material/datepicker'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; // Depending on whether rollup is used, moment needs to be imported differently. // Since Moment.js doesn't have a default export, we normally need to import using the `* as` // syntax. However, rollup creates a synthetic default module and we thus need to import it using @@ -9,9 +11,6 @@ import {provideMomentDateAdapter} from '@angular/material-moment-adapter'; import * as _moment from 'moment'; // tslint:disable-next-line:no-duplicate-imports import {default as _rollupMoment} from 'moment'; -import {MatDatepickerModule} from '@angular/material/datepicker'; -import {MatInputModule} from '@angular/material/input'; -import {MatFormFieldModule} from '@angular/material/form-field'; const moment = _rollupMoment || _moment; @@ -33,8 +32,9 @@ const moment = _rollupMoment || _moment; FormsModule, ReactiveFormsModule, ], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerMomentExample { // Datepicker takes `Moment` objects instead of `Date` objects. - date = new FormControl(moment([2017, 0, 1])); + readonly date = new FormControl(moment([2017, 0, 1])); } diff --git a/src/components-examples/material/datepicker/datepicker-overview/datepicker-overview-example.ts b/src/components-examples/material/datepicker/datepicker-overview/datepicker-overview-example.ts index 635278caf7fd..3e821deee9fb 100644 --- a/src/components-examples/material/datepicker/datepicker-overview/datepicker-overview-example.ts +++ b/src/components-examples/material/datepicker/datepicker-overview/datepicker-overview-example.ts @@ -1,4 +1,4 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; @@ -11,5 +11,6 @@ import {provideNativeDateAdapter} from '@angular/material/core'; standalone: true, providers: [provideNativeDateAdapter()], imports: [MatFormFieldModule, MatInputModule, MatDatepickerModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerOverviewExample {} diff --git a/src/components-examples/material/datepicker/datepicker-start-view/datepicker-start-view-example.ts b/src/components-examples/material/datepicker/datepicker-start-view/datepicker-start-view-example.ts index 9ac8dc7bde55..8f7366ca95f5 100644 --- a/src/components-examples/material/datepicker/datepicker-start-view/datepicker-start-view-example.ts +++ b/src/components-examples/material/datepicker/datepicker-start-view/datepicker-start-view-example.ts @@ -1,8 +1,8 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {provideNativeDateAdapter} from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; -import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; -import {provideNativeDateAdapter} from '@angular/material/core'; +import {MatInputModule} from '@angular/material/input'; /** @title Datepicker start date */ @Component({ @@ -11,7 +11,8 @@ import {provideNativeDateAdapter} from '@angular/material/core'; standalone: true, providers: [provideNativeDateAdapter()], imports: [MatFormFieldModule, MatInputModule, MatDatepickerModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerStartViewExample { - startDate = new Date(1990, 0, 1); + readonly startDate = new Date(1990, 0, 1); } diff --git a/src/components-examples/material/datepicker/datepicker-touch/datepicker-touch-example.ts b/src/components-examples/material/datepicker/datepicker-touch/datepicker-touch-example.ts index b4346df5c648..24d5ed9d1a2a 100644 --- a/src/components-examples/material/datepicker/datepicker-touch/datepicker-touch-example.ts +++ b/src/components-examples/material/datepicker/datepicker-touch/datepicker-touch-example.ts @@ -1,4 +1,4 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; @@ -11,5 +11,6 @@ import {provideNativeDateAdapter} from '@angular/material/core'; standalone: true, providers: [provideNativeDateAdapter()], imports: [MatFormFieldModule, MatInputModule, MatDatepickerModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerTouchExample {} diff --git a/src/components-examples/material/datepicker/datepicker-value/datepicker-value-example.ts b/src/components-examples/material/datepicker/datepicker-value/datepicker-value-example.ts index 34dfd467ff7d..6713992c691f 100644 --- a/src/components-examples/material/datepicker/datepicker-value/datepicker-value-example.ts +++ b/src/components-examples/material/datepicker/datepicker-value/datepicker-value-example.ts @@ -1,9 +1,9 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {provideNativeDateAdapter} from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; -import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; -import {provideNativeDateAdapter} from '@angular/material/core'; +import {MatInputModule} from '@angular/material/input'; /** @title Datepicker selected value */ @Component({ @@ -19,8 +19,9 @@ import {provideNativeDateAdapter} from '@angular/material/core'; FormsModule, ReactiveFormsModule, ], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerValueExample { - date = new FormControl(new Date()); - serializedDate = new FormControl(new Date().toISOString()); + readonly date = new FormControl(new Date()); + readonly serializedDate = new FormControl(new Date().toISOString()); } diff --git a/src/components-examples/material/datepicker/datepicker-views-selection/datepicker-views-selection-example.ts b/src/components-examples/material/datepicker/datepicker-views-selection/datepicker-views-selection-example.ts index 75424004adf0..0524a68c2681 100644 --- a/src/components-examples/material/datepicker/datepicker-views-selection/datepicker-views-selection-example.ts +++ b/src/components-examples/material/datepicker/datepicker-views-selection/datepicker-views-selection-example.ts @@ -1,4 +1,4 @@ -import {Component, ViewEncapsulation} from '@angular/core'; +import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {provideMomentDateAdapter} from '@angular/material-moment-adapter'; import {MatDatepicker, MatDatepickerModule} from '@angular/material/datepicker'; @@ -49,9 +49,10 @@ export const MY_FORMATS = { FormsModule, ReactiveFormsModule, ], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerViewsSelectionExample { - date = new FormControl(moment()); + readonly date = new FormControl(moment()); setMonthAndYear(normalizedMonthAndYear: Moment, datepicker: MatDatepicker) { const ctrlValue = this.date.value ?? moment(); diff --git a/src/components-examples/material/datepicker/index.ts b/src/components-examples/material/datepicker/index.ts index 88b3c566aef1..d62989a242fd 100644 --- a/src/components-examples/material/datepicker/index.ts +++ b/src/components-examples/material/datepicker/index.ts @@ -5,7 +5,6 @@ export {DateRangePickerSelectionStrategyExample} from './date-range-picker-selec export {DatepickerActionsExample} from './datepicker-actions/datepicker-actions-example'; export {DatepickerApiExample} from './datepicker-api/datepicker-api-example'; -export {DatepickerColorExample} from './datepicker-color/datepicker-color-example'; export { DatepickerCustomHeaderExample, ExampleHeader, diff --git a/src/material/datepicker/datepicker-base.ts b/src/material/datepicker/datepicker-base.ts index 71143a3311c9..57ff4dfdc8ec 100644 --- a/src/material/datepicker/datepicker-base.ts +++ b/src/material/datepicker/datepicker-base.ts @@ -6,6 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ +import {AnimationEvent} from '@angular/animations'; +import {CdkTrapFocus} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {coerceStringArray} from '@angular/cdk/coercion'; import { @@ -20,63 +22,61 @@ import { UP_ARROW, } from '@angular/cdk/keycodes'; import { + FlexibleConnectedPositionStrategy, Overlay, OverlayConfig, OverlayRef, ScrollStrategy, - FlexibleConnectedPositionStrategy, } from '@angular/cdk/overlay'; +import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform'; import {CdkPortalOutlet, ComponentPortal, ComponentType, TemplatePortal} from '@angular/cdk/portal'; +import {DOCUMENT} from '@angular/common'; import { + afterNextRender, AfterViewInit, + booleanAttribute, ChangeDetectionStrategy, + ChangeDetectorRef, Component, ComponentRef, + Directive, ElementRef, EventEmitter, Inject, + inject, InjectionToken, + Injector, Input, NgZone, + OnChanges, OnDestroy, + OnInit, Optional, Output, + SimpleChanges, ViewChild, ViewContainerRef, ViewEncapsulation, - ChangeDetectorRef, - Directive, - OnChanges, - SimpleChanges, - OnInit, - inject, - booleanAttribute, - afterNextRender, - Injector, } from '@angular/core'; +import {MatButton} from '@angular/material/button'; import {DateAdapter, ThemePalette} from '@angular/material/core'; -import {AnimationEvent} from '@angular/animations'; -import {merge, Subject, Observable, Subscription} from 'rxjs'; +import {merge, Observable, Subject, Subscription} from 'rxjs'; import {filter, take} from 'rxjs/operators'; -import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform'; import {MatCalendar, MatCalendarView} from './calendar'; -import {matDatepickerAnimations} from './datepicker-animations'; -import {createMissingDateImplError} from './datepicker-errors'; -import {MatCalendarUserEvent, MatCalendarCellClassFunction} from './calendar-body'; -import {DateFilterFn} from './datepicker-input-base'; -import { - ExtractDateTypeFromSelection, - MatDateSelectionModel, - DateRange, -} from './date-selection-model'; +import {MatCalendarCellClassFunction, MatCalendarUserEvent} from './calendar-body'; import { MAT_DATE_RANGE_SELECTION_STRATEGY, MatDateRangeSelectionStrategy, } from './date-range-selection-strategy'; +import { + DateRange, + ExtractDateTypeFromSelection, + MatDateSelectionModel, +} from './date-selection-model'; +import {matDatepickerAnimations} from './datepicker-animations'; +import {createMissingDateImplError} from './datepicker-errors'; +import {DateFilterFn} from './datepicker-input-base'; import {MatDatepickerIntl} from './datepicker-intl'; -import {DOCUMENT} from '@angular/common'; -import {MatButton} from '@angular/material/button'; -import {CdkTrapFocus} from '@angular/cdk/a11y'; /** Used to generate a unique ID for each datepicker instance. */ let datepickerUid = 0; @@ -317,7 +317,11 @@ export interface MatDatepickerPanel< > { /** Stream that emits whenever the date picker is closed. */ closedStream: EventEmitter; - /** Color palette to use on the datepicker's calendar. */ + /** + * Color palette to use on the datepicker's calendar. This API is supported in M2 themes only, it + * has no effect in M3 themes. For information on applying color variants in M3, see + * https://material.angular.io/guide/theming#using-component-color-variants + */ color: ThemePalette; /** The input element the datepicker is associated with. */ datepickerInput: C; @@ -368,7 +372,11 @@ export abstract class MatDatepickerBase< /** The view that the calendar should start in. */ @Input() startView: 'month' | 'year' | 'multi-year' = 'month'; - /** Color palette to use on the datepicker's calendar. */ + /** + * Color palette to use on the datepicker's calendar. This API is supported in M2 themes only, it + * has no effect in M3 themes. For information on applying color variants in M3, see + * https://material.angular.io/guide/theming#using-component-color-variants + */ @Input() get color(): ThemePalette { return ( diff --git a/src/material/datepicker/datepicker.md b/src/material/datepicker/datepicker.md index f14b2f65b1ad..0d5c1e99d1a2 100644 --- a/src/material/datepicker/datepicker.md +++ b/src/material/datepicker/datepicker.md @@ -133,11 +133,11 @@ As with other types of ``, the datepicker works with `@angular/forms` dir ### Changing the datepicker colors -The datepicker popup will automatically inherit the color palette (`primary`, `accent`, or `warn`) -from the `mat-form-field` it is attached to. If you would like to specify a different palette for -the popup you can do so by setting the `color` property on `mat-datepicker`. - - +The color of the datepicker can be changed by specifying a `$color-variant` when applying the +`mat.datepicker-theme` or `mat.datepicker-color` mixins (see the +[theming guide](/guide/theming#using-component-color-variants) to learn more.) By default, the +datepicker uses the theme's primary palette. This can be changed to `'secondary'`, `'tertiary'`, or +`'error'`. ### Date validation @@ -165,9 +165,10 @@ In this example the user cannot select any date that falls on a Saturday or Sund dates which fall on other days of the week are selectable. Each validation property has a different error that can be checked: - * A value that violates the `min` property will have a `matDatepickerMin` error. - * A value that violates the `max` property will have a `matDatepickerMax` error. - * A value that violates the `matDatepickerFilter` property will have a `matDatepickerFilter` error. + +- A value that violates the `min` property will have a `matDatepickerMin` error. +- A value that violates the `max` property will have a `matDatepickerMax` error. +- A value that violates the `matDatepickerFilter` property will have a `matDatepickerFilter` error. ### Input and change events @@ -223,19 +224,15 @@ If your users need to compare the date range that they're currently selecting wi you can provide the comparison range start and end dates to the `mat-date-range-input` using the `comparisonStart` and `comparisonEnd` bindings. The comparison range will be rendered statically within the calendar, but it will change colors to indicate which dates overlap with the user's -selected range. +selected range. The comparison and overlap colors can be customized using the +`datepicker-date-range-colors` mixin. -Note that comparison and overlap colors aren't derived from the current theme, due -to limitations in the Material Design theming system. They can be customized using the -`datepicker-date-range-colors` mixin. - ```scss @use '@angular/material' as mat; -@include mat.datepicker-date-range-colors( - hotpink, teal, yellow, purple); +@include mat.datepicker-date-range-colors(hotpink, teal, yellow, purple); ``` ### Customizing the date selection logic @@ -276,10 +273,11 @@ month. If you want to make the calendar larger or smaller, adjust the width rath ### Internationalization Internationalization of the datepicker is configured via four aspects: - 1. The date locale. - 2. The date implementation that the datepicker accepts. - 3. The display and parse formats used by the datepicker. - 4. The message strings used in the datepicker's UI. + +1. The date locale. +2. The date implementation that the datepicker accepts. +3. The display and parse formats used by the datepicker. +4. The message strings used in the datepicker's UI. #### Setting the locale code @@ -402,12 +400,12 @@ The easiest way to ensure this is to import one of the provided date adapters: -*Please note: `provideNativeDateAdapter` is based off the functionality available in JavaScript's +_Please note: `provideNativeDateAdapter` is based off the functionality available in JavaScript's native [`Date` object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Date). Thus it is not suitable for many locales. One of the biggest shortcomings of the native `Date` object is the inability to set the parse format. We strongly recommend using an adapter based on a more robust formatting and parsing library. You can use `provideMomentDateAdapter` -or a custom `DateAdapter` that works with the library of your choice.* +or a custom `DateAdapter` that works with the library of your choice._ These APIs include providers for `DateAdapter` and `MAT_DATE_FORMATS`. @@ -544,6 +542,7 @@ bootstrapApplication(MyApp, { ``` #### Highlighting specific dates + If you want to apply one or more CSS classes to some dates in the calendar (e.g. to highlight a holiday), you can do so with the `dateClass` input. It accepts a function which will be called with each of the dates in the calendar and will apply any classes that are returned. The return @@ -579,16 +578,15 @@ additional means of opening the pop-up, such as `MatDatepickerToggle`. The datepicker supports the following keyboard shortcuts: -| Keyboard Shortcut | Action | -|----------------------------------------|----------------------------| -| Alt + Down Arrow | Open the calendar pop-up | -| Escape | Close the calendar pop-up | - +| Keyboard Shortcut | Action | +| -------------------------------------- | ------------------------- | +| Alt + Down Arrow | Open the calendar pop-up | +| Escape | Close the calendar pop-up | In month view: | Shortcut | Action | -|---------------------------------------|------------------------------------------| +| ------------------------------------- | ---------------------------------------- | | Left Arrow | Go to previous day | | Right Arrow | Go to next day | | Up Arrow | Go to same day in the previous week | @@ -601,11 +599,10 @@ In month view: | Alt + Page Down | Go to the same day in the next year | | Enter | Select current date | - In year view: | Shortcut | Action | -|---------------------------------------|-------------------------------------------| +| ------------------------------------- | ----------------------------------------- | | Left Arrow | Go to previous month | | Right Arrow | Go to next month | | Up Arrow | Go up a row (back 4 months) | @@ -621,7 +618,7 @@ In year view: In multi-year view: | Shortcut | Action | -|---------------------------------------|-------------------------------------------| +| ------------------------------------- | ----------------------------------------- | | Left Arrow | Go to previous year | | Right Arrow | Go to next year | | Up Arrow | Go up a row (back 4 years) | From ec4e974887289e518a0c7348595e9ceac7e3f26f Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 12 Jun 2024 15:28:25 -0700 Subject: [PATCH 46/61] docs(material/chips): Update chips docs & examples (#29235) --- .../chips-autocomplete-example.html | 16 ++-- .../chips-autocomplete-example.ts | 81 +++++++------------ .../chips-avatar/chips-avatar-example.html | 22 +++-- .../chips-avatar/chips-avatar-example.ts | 3 +- .../chips-drag-drop-example.html | 5 +- .../chips-drag-drop-example.ts | 14 ++-- .../chips-form-control-example.html | 16 ++-- .../chips-form-control-example.ts | 27 ++++--- .../chips-harness/chips-harness-example.ts | 5 +- .../chips-input/chips-input-example.html | 17 ++-- .../chips/chips-input/chips-input-example.ts | 43 +++++----- .../chips-overview/chips-overview-example.ts | 3 +- .../chips-stacked/chips-stacked-example.html | 6 +- .../chips-stacked/chips-stacked-example.ts | 13 +-- src/material/chips/chip.ts | 44 +++++----- src/material/chips/chips.md | 14 ++-- 16 files changed, 174 insertions(+), 155 deletions(-) diff --git a/src/components-examples/material/chips/chips-autocomplete/chips-autocomplete-example.html b/src/components-examples/material/chips/chips-autocomplete/chips-autocomplete-example.html index 0e9f4a806f2b..b9d6271c95cd 100644 --- a/src/components-examples/material/chips/chips-autocomplete/chips-autocomplete-example.html +++ b/src/components-examples/material/chips/chips-autocomplete/chips-autocomplete-example.html @@ -2,7 +2,7 @@ Favorite Fruits - @for (fruit of fruits; track fruit) { + @for (fruit of fruits(); track $index) { {{fruit}} } - + diff --git a/src/components-examples/material/chips/chips-input/chips-input-example.ts b/src/components-examples/material/chips/chips-input/chips-input-example.ts index 73bea6995ced..29be44d93f16 100644 --- a/src/components-examples/material/chips/chips-input/chips-input-example.ts +++ b/src/components-examples/material/chips/chips-input/chips-input-example.ts @@ -1,9 +1,9 @@ +import {LiveAnnouncer} from '@angular/cdk/a11y'; import {COMMA, ENTER} from '@angular/cdk/keycodes'; -import {Component, inject} from '@angular/core'; +import {ChangeDetectionStrategy, Component, inject, signal} from '@angular/core'; import {MatChipEditedEvent, MatChipInputEvent, MatChipsModule} from '@angular/material/chips'; -import {MatIconModule} from '@angular/material/icon'; import {MatFormFieldModule} from '@angular/material/form-field'; -import {LiveAnnouncer} from '@angular/cdk/a11y'; +import {MatIconModule} from '@angular/material/icon'; export interface Fruit { name: string; @@ -18,20 +18,20 @@ export interface Fruit { styleUrl: 'chips-input-example.css', standalone: true, imports: [MatFormFieldModule, MatChipsModule, MatIconModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class ChipsInputExample { - addOnBlur = true; + readonly addOnBlur = true; readonly separatorKeysCodes = [ENTER, COMMA] as const; - fruits: Fruit[] = [{name: 'Lemon'}, {name: 'Lime'}, {name: 'Apple'}]; - - announcer = inject(LiveAnnouncer); + readonly fruits = signal([{name: 'Lemon'}, {name: 'Lime'}, {name: 'Apple'}]); + readonly announcer = inject(LiveAnnouncer); add(event: MatChipInputEvent): void { const value = (event.value || '').trim(); // Add our fruit if (value) { - this.fruits.push({name: value}); + this.fruits.update(fruits => [...fruits, {name: value}]); } // Clear the input value @@ -39,13 +39,16 @@ export class ChipsInputExample { } remove(fruit: Fruit): void { - const index = this.fruits.indexOf(fruit); - - if (index >= 0) { - this.fruits.splice(index, 1); + this.fruits.update(fruits => { + const index = fruits.indexOf(fruit); + if (index < 0) { + return fruits; + } - this.announcer.announce(`Removed ${fruit}`); - } + fruits.splice(index, 1); + this.announcer.announce(`Removed ${fruit.name}`); + return [...fruits]; + }); } edit(fruit: Fruit, event: MatChipEditedEvent) { @@ -58,9 +61,13 @@ export class ChipsInputExample { } // Edit existing fruit - const index = this.fruits.indexOf(fruit); - if (index >= 0) { - this.fruits[index].name = value; - } + this.fruits.update(fruits => { + const index = fruits.indexOf(fruit); + if (index >= 0) { + fruits[index].name = value; + return [...fruits]; + } + return fruits; + }); } } diff --git a/src/components-examples/material/chips/chips-overview/chips-overview-example.ts b/src/components-examples/material/chips/chips-overview/chips-overview-example.ts index 431aebb8cf93..e541dba4b95f 100644 --- a/src/components-examples/material/chips/chips-overview/chips-overview-example.ts +++ b/src/components-examples/material/chips/chips-overview/chips-overview-example.ts @@ -1,4 +1,4 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; import {MatChipsModule} from '@angular/material/chips'; /** @@ -9,5 +9,6 @@ import {MatChipsModule} from '@angular/material/chips'; templateUrl: 'chips-overview-example.html', standalone: true, imports: [MatChipsModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class ChipsOverviewExample {} diff --git a/src/components-examples/material/chips/chips-stacked/chips-stacked-example.html b/src/components-examples/material/chips/chips-stacked/chips-stacked-example.html index 22b676f0ee49..d3273425942b 100644 --- a/src/components-examples/material/chips/chips-stacked/chips-stacked-example.html +++ b/src/components-examples/material/chips/chips-stacked/chips-stacked-example.html @@ -1,5 +1,5 @@ - - @for (chip of availableColors; track chip) { - {{chip.name}} + + @for (dog of bestBoys; track dog) { + {{dog}} } diff --git a/src/components-examples/material/chips/chips-stacked/chips-stacked-example.ts b/src/components-examples/material/chips/chips-stacked/chips-stacked-example.ts index ecc56988b2fc..e95df3c4731f 100644 --- a/src/components-examples/material/chips/chips-stacked/chips-stacked-example.ts +++ b/src/components-examples/material/chips/chips-stacked/chips-stacked-example.ts @@ -1,12 +1,6 @@ import {Component} from '@angular/core'; -import {ThemePalette} from '@angular/material/core'; import {MatChipsModule} from '@angular/material/chips'; -export interface ChipColor { - name: string; - color: ThemePalette; -} - /** * @title Stacked chips */ @@ -18,10 +12,5 @@ export interface ChipColor { imports: [MatChipsModule], }) export class ChipsStackedExample { - availableColors: ChipColor[] = [ - {name: 'none', color: undefined}, - {name: 'Primary', color: 'primary'}, - {name: 'Accent', color: 'accent'}, - {name: 'Warn', color: 'warn'}, - ]; + readonly bestBoys: string[] = ['Samoyed', 'Akita Inu', 'Alaskan Malamute', 'Siberian Husky']; } diff --git a/src/material/chips/chip.ts b/src/material/chips/chip.ts index 27acfcd415ca..71a677c59e91 100644 --- a/src/material/chips/chip.ts +++ b/src/material/chips/chip.ts @@ -6,47 +6,47 @@ * found in the LICENSE file at https://angular.io/license */ +import {FocusMonitor} from '@angular/cdk/a11y'; +import {BACKSPACE, DELETE} from '@angular/cdk/keycodes'; +import {DOCUMENT} from '@angular/common'; import { - AfterViewInit, + ANIMATION_MODULE_TYPE, AfterContentInit, - Component, + AfterViewInit, + Attribute, ChangeDetectionStrategy, ChangeDetectorRef, + Component, ContentChild, + ContentChildren, + DoCheck, ElementRef, EventEmitter, Inject, + Injector, Input, NgZone, OnDestroy, + OnInit, Optional, Output, - ViewEncapsulation, - ViewChild, - Attribute, - ContentChildren, QueryList, - OnInit, - DoCheck, - inject, + ViewChild, + ViewEncapsulation, + afterNextRender, booleanAttribute, + inject, numberAttribute, - ANIMATION_MODULE_TYPE, - afterNextRender, - Injector, } from '@angular/core'; -import {DOCUMENT} from '@angular/common'; import { - MatRipple, MAT_RIPPLE_GLOBAL_OPTIONS, - RippleGlobalOptions, + MatRipple, MatRippleLoader, + RippleGlobalOptions, } from '@angular/material/core'; -import {FocusMonitor} from '@angular/cdk/a11y'; -import {merge, Subject, Subscription} from 'rxjs'; -import {MatChipAvatar, MatChipTrailingIcon, MatChipRemove} from './chip-icons'; +import {Subject, Subscription, merge} from 'rxjs'; import {MatChipAction} from './chip-action'; -import {BACKSPACE, DELETE} from '@angular/cdk/keycodes'; +import {MatChipAvatar, MatChipRemove, MatChipTrailingIcon} from './chip-icons'; import {MAT_CHIP, MAT_CHIP_AVATAR, MAT_CHIP_REMOVE, MAT_CHIP_TRAILING_ICON} from './tokens'; let uid = 0; @@ -172,7 +172,11 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck protected _value: any; // TODO: should be typed as `ThemePalette` but internal apps pass in arbitrary strings. - /** Theme color palette of the chip. */ + /** + * Theme color palette of the chip. This API is supported in M2 themes only, it has no effect in + * M3 themes. For information on applying color variants in M3, see + * https://material.angular.io/guide/theming#using-component-color-variants + */ @Input() color?: string | null; /** diff --git a/src/material/chips/chips.md b/src/material/chips/chips.md index 8b3fd2815eac..7cfb9a44d7ec 100644 --- a/src/material/chips/chips.md +++ b/src/material/chips/chips.md @@ -8,8 +8,6 @@ Chips are always used inside a container. To create chips, start with a `` renders a chip with Material Design styles applied. For a chip with no styles applied, use ``. -*Hint: `` receives the `mat-mdc-basic-chip` CSS class in addition to the `mat-mdc-chip` class.* - #### Disabled appearance Although `` is not interactive, you can set the `disabled` Input to give it disabled appearance. @@ -44,7 +42,7 @@ Chips are always used inside a container. To create chips connected to an input #### Disabled `` -Use the `disabled` Input to disable a ``. This gives the `` a disabled appearance and prevents the user from interacting with it. +Use the `disabled` Input to disable a ``. This gives the `` a disabled appearance and prevents the user from interacting with it. ```html Orange @@ -58,11 +56,12 @@ Users can press delete to remove a chip. Pressing delete triggers the `removed` #### Autocomplete -An example of chip input with autocomplete. +A `` can be combined with `` to enable free-form chip input with suggestions. ### Icons + You can add icons to chips to identify entities (like individuals) and provide additional functionality. #### Adding up to two icons with content projection @@ -99,6 +98,7 @@ By default, chips are displayed horizontally. To stack chips vertically, apply t ### Specifying global configuration defaults + Use the `MAT_CHIPS_DEFAULT_OPTIONS` token to specify default options for the chips module. ```html @@ -116,7 +116,11 @@ Use the `MAT_CHIPS_DEFAULT_OPTIONS` token to specify default options for the chi ### Theming -By default, chips use the primary color. Specify the `color` property to change the color to `accent` or `warn`. +The color of a `` can be changed by specifying a `$color-variant` when applying the +`mat.datepicker-theme` or `mat.datepicker-color` mixins (see the +[theming guide](/guide/theming#using-component-color-variants) to learn more.) By default, the +datepicker uses the theme's primary palette. This can be changed to `'secondary'`, `'tertiary'`, +or `'error'`. ### Interaction Patterns From d7832334214cb66b57033c936b4b669e6094ab33 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 12 Jun 2024 15:28:39 -0700 Subject: [PATCH 47/61] docs(material/checkbox): Update checkbox docs & examples (#29234) --- .../checkbox-configurable-example.html | 11 ++-- .../checkbox-configurable-example.ts | 15 +++--- .../checkbox-harness-example.html | 13 ++--- .../checkbox-harness-example.spec.ts | 2 +- .../checkbox-harness-example.ts | 5 +- .../checkbox-overview-example.html | 19 ++++--- .../checkbox-overview-example.ts | 50 +++++++++---------- .../checkbox-reactive-forms-example.ts | 11 ++-- src/material/checkbox/checkbox-config.ts | 6 ++- src/material/checkbox/checkbox.md | 15 +++++- src/material/checkbox/checkbox.ts | 18 ++++--- 11 files changed, 93 insertions(+), 72 deletions(-) diff --git a/src/components-examples/material/checkbox/checkbox-configurable/checkbox-configurable-example.html b/src/components-examples/material/checkbox/checkbox-configurable/checkbox-configurable-example.html index 4591963c3e23..5e69d1164262 100644 --- a/src/components-examples/material/checkbox/checkbox-configurable/checkbox-configurable-example.html +++ b/src/components-examples/material/checkbox/checkbox-configurable/checkbox-configurable-example.html @@ -27,11 +27,12 @@

Result

+ class="example-margin" + [(ngModel)]="checked" + [(indeterminate)]="indeterminate" + [labelPosition]="labelPosition()" + [disabled]="disabled()" + > I'm a checkbox
diff --git a/src/components-examples/material/checkbox/checkbox-configurable/checkbox-configurable-example.ts b/src/components-examples/material/checkbox/checkbox-configurable/checkbox-configurable-example.ts index 59226280dbc7..988ca6c6ecaa 100644 --- a/src/components-examples/material/checkbox/checkbox-configurable/checkbox-configurable-example.ts +++ b/src/components-examples/material/checkbox/checkbox-configurable/checkbox-configurable-example.ts @@ -1,8 +1,8 @@ -import {Component} from '@angular/core'; -import {MatRadioModule} from '@angular/material/radio'; +import {ChangeDetectionStrategy, Component, model} from '@angular/core'; import {FormsModule} from '@angular/forms'; -import {MatCheckboxModule} from '@angular/material/checkbox'; import {MatCardModule} from '@angular/material/card'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatRadioModule} from '@angular/material/radio'; /** * @title Configurable checkbox @@ -13,10 +13,11 @@ import {MatCardModule} from '@angular/material/card'; styleUrl: 'checkbox-configurable-example.css', standalone: true, imports: [MatCardModule, MatCheckboxModule, FormsModule, MatRadioModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class CheckboxConfigurableExample { - checked = false; - indeterminate = false; - labelPosition: 'before' | 'after' = 'after'; - disabled = false; + readonly checked = model(false); + readonly indeterminate = model(false); + readonly labelPosition = model<'before' | 'after'>('after'); + readonly disabled = model(false); } diff --git a/src/components-examples/material/checkbox/checkbox-harness/checkbox-harness-example.html b/src/components-examples/material/checkbox/checkbox-harness/checkbox-harness-example.html index 016dd3b2fdcf..ec690de63f0f 100644 --- a/src/components-examples/material/checkbox/checkbox-harness/checkbox-harness-example.html +++ b/src/components-examples/material/checkbox/checkbox-harness/checkbox-harness-example.html @@ -1,11 +1,12 @@ + required + [checked]="true" + name="first-name" + value="first-value" + aria-label="First checkbox" +> First - + Second diff --git a/src/components-examples/material/checkbox/checkbox-harness/checkbox-harness-example.spec.ts b/src/components-examples/material/checkbox/checkbox-harness/checkbox-harness-example.spec.ts index fce1d8b50331..a1d933725071 100644 --- a/src/components-examples/material/checkbox/checkbox-harness/checkbox-harness-example.spec.ts +++ b/src/components-examples/material/checkbox/checkbox-harness/checkbox-harness-example.spec.ts @@ -38,7 +38,7 @@ describe('CheckboxHarnessExample', () => { }); it('should toggle checkbox', async () => { - fixture.componentInstance.disabled = false; + fixture.componentRef.setInput('disabled', false); fixture.changeDetectorRef.markForCheck(); const [checkedCheckbox, uncheckedCheckbox] = await loader.getAllHarnesses(MatCheckboxHarness); await checkedCheckbox.toggle(); diff --git a/src/components-examples/material/checkbox/checkbox-harness/checkbox-harness-example.ts b/src/components-examples/material/checkbox/checkbox-harness/checkbox-harness-example.ts index b705d6fb1042..15096bd64c23 100644 --- a/src/components-examples/material/checkbox/checkbox-harness/checkbox-harness-example.ts +++ b/src/components-examples/material/checkbox/checkbox-harness/checkbox-harness-example.ts @@ -1,4 +1,4 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component, input} from '@angular/core'; import {MatCheckboxModule} from '@angular/material/checkbox'; /** @@ -9,7 +9,8 @@ import {MatCheckboxModule} from '@angular/material/checkbox'; templateUrl: 'checkbox-harness-example.html', standalone: true, imports: [MatCheckboxModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class CheckboxHarnessExample { - disabled = true; + readonly disabled = input(true); } diff --git a/src/components-examples/material/checkbox/checkbox-overview/checkbox-overview-example.html b/src/components-examples/material/checkbox/checkbox-overview/checkbox-overview-example.html index ff06d1f31192..c0ef9c350835 100644 --- a/src/components-examples/material/checkbox/checkbox-overview/checkbox-overview-example.html +++ b/src/components-examples/material/checkbox/checkbox-overview/checkbox-overview-example.html @@ -5,21 +5,20 @@
- - {{task.name}} + + {{task().name}}
    - @for (subtask of task.subtasks; track subtask) { + @for (subtask of task().subtasks; track subtask; let i = $index) {
  • - + {{subtask.name}}
  • diff --git a/src/components-examples/material/checkbox/checkbox-overview/checkbox-overview-example.ts b/src/components-examples/material/checkbox/checkbox-overview/checkbox-overview-example.ts index afc6b11d655f..2ec2a90dd804 100644 --- a/src/components-examples/material/checkbox/checkbox-overview/checkbox-overview-example.ts +++ b/src/components-examples/material/checkbox/checkbox-overview/checkbox-overview-example.ts @@ -1,12 +1,10 @@ -import {Component} from '@angular/core'; -import {ThemePalette} from '@angular/material/core'; +import {ChangeDetectionStrategy, Component, computed, signal} from '@angular/core'; import {FormsModule} from '@angular/forms'; import {MatCheckboxModule} from '@angular/material/checkbox'; export interface Task { name: string; completed: boolean; - color: ThemePalette; subtasks?: Task[]; } @@ -19,37 +17,37 @@ export interface Task { styleUrl: 'checkbox-overview-example.css', standalone: true, imports: [MatCheckboxModule, FormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class CheckboxOverviewExample { - task: Task = { - name: 'Indeterminate', + readonly task = signal({ + name: 'Parent task', completed: false, - color: 'primary', subtasks: [ - {name: 'Primary', completed: false, color: 'primary'}, - {name: 'Accent', completed: false, color: 'accent'}, - {name: 'Warn', completed: false, color: 'warn'}, + {name: 'Child task 1', completed: false}, + {name: 'Child task 2', completed: false}, + {name: 'Child task 3', completed: false}, ], - }; + }); - allComplete: boolean = false; - - updateAllComplete() { - this.allComplete = this.task.subtasks != null && this.task.subtasks.every(t => t.completed); - } - - someComplete(): boolean { - if (this.task.subtasks == null) { + readonly partiallyComplete = computed(() => { + const task = this.task(); + if (!task.subtasks) { return false; } - return this.task.subtasks.filter(t => t.completed).length > 0 && !this.allComplete; - } + return task.subtasks.some(t => t.completed) && !task.subtasks.every(t => t.completed); + }); - setAll(completed: boolean) { - this.allComplete = completed; - if (this.task.subtasks == null) { - return; - } - this.task.subtasks.forEach(t => (t.completed = completed)); + update(completed: boolean, index?: number) { + this.task.update(task => { + if (index === undefined) { + task.completed = completed; + task.subtasks?.forEach(t => (t.completed = completed)); + } else { + task.subtasks![index].completed = completed; + task.completed = task.subtasks?.every(t => t.completed) ?? true; + } + return {...task}; + }); } } diff --git a/src/components-examples/material/checkbox/checkbox-reactive-forms/checkbox-reactive-forms-example.ts b/src/components-examples/material/checkbox/checkbox-reactive-forms/checkbox-reactive-forms-example.ts index 1ef154b96e93..da549269c1c2 100644 --- a/src/components-examples/material/checkbox/checkbox-reactive-forms/checkbox-reactive-forms-example.ts +++ b/src/components-examples/material/checkbox/checkbox-reactive-forms/checkbox-reactive-forms-example.ts @@ -1,6 +1,6 @@ -import {Component} from '@angular/core'; -import {FormBuilder, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {JsonPipe} from '@angular/common'; +import {ChangeDetectionStrategy, Component, inject} from '@angular/core'; +import {FormBuilder, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {MatCheckboxModule} from '@angular/material/checkbox'; /** @title Checkboxes with reactive forms */ @@ -10,13 +10,14 @@ import {MatCheckboxModule} from '@angular/material/checkbox'; styleUrl: 'checkbox-reactive-forms-example.css', standalone: true, imports: [FormsModule, ReactiveFormsModule, MatCheckboxModule, JsonPipe], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class CheckboxReactiveFormsExample { - toppings = this._formBuilder.group({ + private readonly _formBuilder = inject(FormBuilder); + + readonly toppings = this._formBuilder.group({ pepperoni: false, extracheese: false, mushroom: false, }); - - constructor(private _formBuilder: FormBuilder) {} } diff --git a/src/material/checkbox/checkbox-config.ts b/src/material/checkbox/checkbox-config.ts index 239c38e15ed6..9c5aeb561b79 100644 --- a/src/material/checkbox/checkbox-config.ts +++ b/src/material/checkbox/checkbox-config.ts @@ -10,7 +10,11 @@ import {ThemePalette} from '@angular/material/core'; /** Default `mat-checkbox` options that can be overridden. */ export interface MatCheckboxDefaultOptions { - /** Default theme color palette to be used for checkboxes. */ + /** + * Default theme color palette to be used for checkboxes. This API is supported in M2 themes + * only, it has no effect in M3 themes. For information on applying color variants in M3, see + * https://material.angular.io/guide/theming#using-component-color-variants + */ color?: ThemePalette; /** Default checkbox click action for checkboxes. */ clickAction?: MatCheckboxClickAction; diff --git a/src/material/checkbox/checkbox.md b/src/material/checkbox/checkbox.md index ac83e70eb271..63e1b8a7a792 100644 --- a/src/material/checkbox/checkbox.md +++ b/src/material/checkbox/checkbox.md @@ -4,6 +4,7 @@ enhanced with Material Design styling and animations. ### Checkbox label + The checkbox label is provided as the content to the `` element. The label can be positioned before or after the checkbox by setting the `labelPosition` property to `'before'` or `'after'`. @@ -14,16 +15,19 @@ If you don't want the label to appear next to the checkbox, you can use specify an appropriate label. ### Use with `@angular/forms` + `` is compatible with `@angular/forms` and supports both `FormsModule` and `ReactiveFormsModule`. ### Indeterminate state + `` supports an `indeterminate` state, similar to the native ``. While the `indeterminate` property of the checkbox is true, it will render as indeterminate regardless of the `checked` value. Any interaction with the checkbox by a user (i.e., clicking) will remove the indeterminate state. ### Click action config + When user clicks on the `mat-checkbox`, the default behavior is toggle `checked` value and set `indeterminate` to `false`. This behavior can be customized by [providing a new value](https://angular.io/guide/dependency-injection) @@ -38,22 +42,29 @@ providers: [ The possible values are: #### `noop` + Do not change the `checked` value or `indeterminate` value. Developers have the power to implement customized click actions. #### `check` + Toggle `checked` value of the checkbox, ignore `indeterminate` value. If the checkbox is in `indeterminate` state, the checkbox will display as an `indeterminate` checkbox regardless the `checked` value. #### `check-indeterminate` + Default behavior of `mat-checkbox`. Always set `indeterminate` to `false` when user click on the `mat-checkbox`. This matches the behavior of native ``. ### Theming -The color of a `` can be changed by using the `color` property. By default, checkboxes -use the theme's accent color. This can be changed to `'primary'` or `'warn'`. + +The color of a `` can be changed by specifying a `$color-variant` when applying the +`mat.checkbox-theme` or `mat.checkbox-color` mixins (see the +[theming guide](/guide/theming#using-component-color-variants) to learn more.) By default, +checkboxes use the theme's primary palette. This can be changed to `'secondary'`, `'tertiary'`, +or `'error'`. ### Accessibility diff --git a/src/material/checkbox/checkbox.ts b/src/material/checkbox/checkbox.ts index 7591b46dac0c..92f424d32214 100644 --- a/src/material/checkbox/checkbox.ts +++ b/src/material/checkbox/checkbox.ts @@ -6,27 +6,28 @@ * found in the LICENSE file at https://angular.io/license */ +import {FocusableOption} from '@angular/cdk/a11y'; import { + ANIMATION_MODULE_TYPE, AfterViewInit, Attribute, - booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, - forwardRef, Inject, Input, NgZone, - numberAttribute, OnChanges, Optional, Output, SimpleChanges, ViewChild, ViewEncapsulation, - ANIMATION_MODULE_TYPE, + booleanAttribute, + forwardRef, + numberAttribute, } from '@angular/core'; import { AbstractControl, @@ -36,8 +37,7 @@ import { ValidationErrors, Validator, } from '@angular/forms'; -import {_MatInternalFormField, MatRipple} from '@angular/material/core'; -import {FocusableOption} from '@angular/cdk/a11y'; +import {MatRipple, _MatInternalFormField} from '@angular/material/core'; import { MAT_CHECKBOX_DEFAULT_OPTIONS, MAT_CHECKBOX_DEFAULT_OPTIONS_FACTORY, @@ -202,7 +202,11 @@ export class MatCheckbox // TODO(crisbeto): this should be a ThemePalette, but some internal apps were abusing // the lack of type checking previously and assigning random strings. - /** Palette color of the checkbox. */ + /** + * Palette color of the checkbox. This API is supported in M2 themes only, it has no effect in M3 + * themes. For information on applying color variants in M3, see + * https://material.angular.io/guide/theming#using-component-color-variants + */ @Input() color: string | undefined; /** From 63eadc88071e97e1c05641a0769ddac4d88a06cc Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 12 Jun 2024 22:48:06 +0000 Subject: [PATCH 48/61] docs: release notes for the v18.0.3 release --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e7e22853525..3a27d77f6998 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ + +# 18.0.3 "gossamer-glacier" (2024-06-12) +### material +| Commit | Type | Description | +| -- | -- | -- | +| [f6b993fdb7](https://github.com/angular/components/commit/f6b993fdb7fbdcfbe0297d320a5961097002308d) | fix | **dialog:** Make autofocus work with animations disabled ([#29195](https://github.com/angular/components/pull/29195)) | +| [6dd1689b51](https://github.com/angular/components/commit/6dd1689b519abf287098d30f7698fc37197e3db0) | fix | **dialog:** Make focus behavior consistent across zoneful/zoneless apps ([#29192](https://github.com/angular/components/pull/29192)) | +| [81d4527f91](https://github.com/angular/components/commit/81d4527f9130605f69dea31a092a60261bde25db) | fix | **radio:** mark radio-group for check on touch ([#29203](https://github.com/angular/components/pull/29203)) | +| [0f4d1862d3](https://github.com/angular/components/commit/0f4d1862d30366978176a4a87b7799915d3caedd) | fix | **schematics:** estimate missing hues in M3 schematic ([#29231](https://github.com/angular/components/pull/29231)) | +| [faf348438d](https://github.com/angular/components/commit/faf348438d57db80e8ac5187ffe3900fe398fe77) | fix | **snack-bar:** fix overrides mixin name typo ([#29180](https://github.com/angular/components/pull/29180)) | + + + # 18.1.0-next.1 "velvet-violoncello" (2024-06-05) ### cdk From 00022d9caf8f7138419a78a43b5c8a88eb0e6a99 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 12 Jun 2024 22:52:45 +0000 Subject: [PATCH 49/61] release: cut the v18.1.0-next.2 release --- CHANGELOG.md | 19 +++++++++++++++++++ package.json | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a27d77f6998..c64263073aaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ + +# 18.1.0-next.2 "ivory-infinity" (2024-06-12) +### cdk +| Commit | Type | Description | +| -- | -- | -- | +| [0bc6583892](https://github.com/angular/components/commit/0bc65838926e88723bfc677fc3e4de81826cfe5b) | feat | **drag-drop:** add mixed orientation support | +### material +| Commit | Type | Description | +| -- | -- | -- | +| [6f698fa4e2](https://github.com/angular/components/commit/6f698fa4e24ef4637b2c83f43cb608df967a78b5) | feat | **core:** add option to configure prefix of system variables ([#29139](https://github.com/angular/components/pull/29139)) | +| [e7312037f7](https://github.com/angular/components/commit/e7312037f75dad5482b06868542ec2a715c116fc) | fix | **dialog:** Make autofocus work with animations disabled ([#29195](https://github.com/angular/components/pull/29195)) | +| [3b32d0e7c9](https://github.com/angular/components/commit/3b32d0e7c95b358d30f8b7e6b0570ab8ba815a06) | fix | **dialog:** Make focus behavior consistent across zoneful/zoneless apps ([#29192](https://github.com/angular/components/pull/29192)) | +| [566057b8f5](https://github.com/angular/components/commit/566057b8f58fab1b5328cbd4336b7b19ea412fd3) | fix | **divider:** non-text color contrast issues ([#28995](https://github.com/angular/components/pull/28995)) | +| [e3abc65d7d](https://github.com/angular/components/commit/e3abc65d7d191f2adf1c294bdb84f532d4eac05c) | fix | **radio:** mark radio-group for check on touch ([#29203](https://github.com/angular/components/pull/29203)) | +| [3da43230e6](https://github.com/angular/components/commit/3da43230e62c8983af5c21c4c1fc66ea2e5e7d52) | fix | **schematics:** estimate missing hues in M3 schematic ([#29231](https://github.com/angular/components/pull/29231)) | +| [d717de5150](https://github.com/angular/components/commit/d717de51501e04a0410217c07fc31929ff2e983a) | fix | **snack-bar:** fix overrides mixin name typo ([#29180](https://github.com/angular/components/pull/29180)) | + + + # 18.0.3 "gossamer-glacier" (2024-06-12) ### material diff --git a/package.json b/package.json index e4304c6124f9..f492e5647721 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "ci-notify-slack-failure": "node --no-warnings=ExperimentalWarning --loader ts-node/esm/transpile-only scripts/circleci/notify-slack-job-failure.mts", "prepare": "husky" }, - "version": "18.1.0-next.1", + "version": "18.1.0-next.2", "dependencies": { "@angular/animations": "^18.1.0-next.1", "@angular/common": "^18.1.0-next.1", From a4846a961924442649f0b19a3979fa92922b5b5f Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 12 Jun 2024 12:11:59 +0200 Subject: [PATCH 50/61] fix(material/core): implement elevation classes in M3 Fixes that we didn't support elevation classes in M3 which was breaking some users. Fixes #28618. --- src/material/core/_core-theme.scss | 18 ++++----------- src/material/core/_core.scss | 22 +++++++++++++++---- src/material/core/tokens/m2/mat/_app.scss | 13 ++++++++++- src/material/core/tokens/m3/mat/_app.scss | 9 ++++++++ ...__name@dasherize__.component.html.template | 2 +- src/material/tabs/tab-header.html | 2 -- .../tabs/tab-nav-bar/tab-nav-bar.html | 2 -- 7 files changed, 44 insertions(+), 24 deletions(-) diff --git a/src/material/core/_core-theme.scss b/src/material/core/_core-theme.scss index 7883579a8923..0a7c013f203f 100644 --- a/src/material/core/_core-theme.scss +++ b/src/material/core/_core-theme.scss @@ -1,12 +1,10 @@ @use './theming/theming'; @use './theming/inspection'; @use './theming/validation'; -@use './style/private'; @use './ripple/ripple-theme'; @use './option/option-theme'; @use './option/optgroup-theme'; @use './selection/pseudo-checkbox/pseudo-checkbox-theme'; -@use './style/elevation'; @use './style/sass-utils'; @use './typography/typography'; @use './tokens/token-utils'; @@ -41,6 +39,10 @@ $_has-inserted-loaded-marker: false; @include option-theme.base($theme); @include optgroup-theme.base($theme); @include pseudo-checkbox-theme.base($theme); + @include sass-utils.current-selector-or-root() { + @include token-utils.create-token-values(tokens-mat-app.$prefix, + tokens-mat-app.get-unthemable-tokens()); + } } // The marker is a concrete style no matter which Material version we're targeting. @@ -60,18 +62,6 @@ $_has-inserted-loaded-marker: false; @include token-utils.create-token-values(tokens-mat-app.$prefix, tokens-mat-app.get-color-tokens($theme)); } - - // Provides external CSS classes for each elevation value. Each CSS class is formatted as - // `mat-elevation-z$zValue` where `$zValue` corresponds to the z-space to which the element is - // elevated. - @for $zValue from 0 through 24 { - $selector: elevation.$prefix + $zValue; - // We need the `mat-mdc-elevation-specific`, because some MDC mixins - // come with elevation baked in and we don't have a way of removing it. - .#{$selector}, .mat-mdc-elevation-specific.#{$selector} { - @include private.private-theme-elevation($zValue, $theme); - } - } } } diff --git a/src/material/core/_core.scss b/src/material/core/_core.scss index c6e3056cf53d..ef7bb5287080 100644 --- a/src/material/core/_core.scss +++ b/src/material/core/_core.scss @@ -2,6 +2,7 @@ @use './tokens/m2/mat/app' as tokens-mat-app; @use './tokens/token-utils'; @use './ripple/ripple'; +@use './style/elevation'; @use './focus-indicators/private'; @use './mdc-helpers/mdc-helpers'; @@ -18,13 +19,26 @@ // Wrapper element that provides the theme background when the // user's content isn't inside of a `mat-sidenav-container`. @at-root { - .mat-app-background { - @include mdc-helpers.disable-mdc-fallback-declarations { - @include token-utils.use-tokens(tokens-mat-app.$prefix, tokens-mat-app.get-token-slots()) { - // Note: we need to emit fallback values here to avoid errors in internal builds. + // Note: we need to emit fallback values here to avoid errors in internal builds. + @include mdc-helpers.disable-mdc-fallback-declarations { + @include token-utils.use-tokens(tokens-mat-app.$prefix, tokens-mat-app.get-token-slots()) { + .mat-app-background { @include token-utils.create-token-slot(background-color, background-color, transparent); @include token-utils.create-token-slot(color, text-color, inherit); } + + // Provides external CSS classes for each elevation value. Each CSS class is formatted as + // `mat-elevation-z$zValue` where `$zValue` corresponds to the z-space to which the element + // is elevated. + @for $zValue from 0 through 24 { + $selector: elevation.$prefix + $zValue; + // We need the `mat-mdc-elevation-specific`, because some MDC mixins + // come with elevation baked in and we don't have a way of removing it. + .#{$selector}, .mat-mdc-elevation-specific.#{$selector} { + @include token-utils.create-token-slot(box-shadow, 'elevation-shadow-level-#{$zValue}', + none); + } + } } } } diff --git a/src/material/core/tokens/m2/mat/_app.scss b/src/material/core/tokens/m2/mat/_app.scss index c82a8b4d1ec2..2443e7f10a6f 100644 --- a/src/material/core/tokens/m2/mat/_app.scss +++ b/src/material/core/tokens/m2/mat/_app.scss @@ -1,3 +1,5 @@ +@use '@material/elevation/elevation-theme' as mdc-elevation; +@use 'sass:map'; @use '../../token-utils'; @use '../../../theming/inspection'; @use '../../../style/sass-utils'; @@ -13,10 +15,19 @@ $prefix: (mat, app); // Tokens that can be configured through Angular Material's color theming API. @function get-color-tokens($theme) { - @return ( + $tokens: ( background-color: inspection.get-theme-color($theme, background, background), text-color: inspection.get-theme-color($theme, foreground, text), ); + + @for $zValue from 0 through 24 { + $elevation-color: inspection.get-theme-color($theme, foreground, elevation); + $shadow: mdc-elevation.elevation-box-shadow($zValue, + if($elevation-color == null, elevation.$color, $elevation-color)); + $tokens: map.set($tokens, 'elevation-shadow-level-#{$zValue}', $shadow); + } + + @return $tokens; } // Tokens that can be configured through Angular Material's typography theming API. diff --git a/src/material/core/tokens/m3/mat/_app.scss b/src/material/core/tokens/m3/mat/_app.scss index 879fdc9570ad..e54826db04c5 100644 --- a/src/material/core/tokens/m3/mat/_app.scss +++ b/src/material/core/tokens/m3/mat/_app.scss @@ -1,4 +1,5 @@ @use 'sass:map'; +@use '@material/elevation' as mdc-elevation; @use '../../token-utils'; // The prefix used to generate the fully qualified name for tokens in this file. @@ -10,10 +11,18 @@ $prefix: (mat, app); /// @param {Map} $token-slots Possible token slots /// @return {Map} A set of custom tokens for the app @function get-tokens($systems, $exclude-hardcoded, $token-slots) { + $shadow-color: map.get($systems, md-sys-color, shadow); $tokens: ( background-color: map.get($systems, md-sys-color, background), text-color: map.get($systems, md-sys-color, on-background), ); + @if ($shadow-color) { + @for $zValue from 0 through 24 { + $shadow: mdc-elevation.elevation-box-shadow($zValue, $shadow-color); + $tokens: map.set($tokens, 'elevation-shadow-level-#{$zValue}', $shadow); + } + } + @return token-utils.namespace-tokens($prefix, $tokens, $token-slots); } diff --git a/src/material/schematics/ng-generate/table/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.html.template b/src/material/schematics/ng-generate/table/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.html.template index f0c7c4669975..fba358b44012 100644 --- a/src/material/schematics/ng-generate/table/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.html.template +++ b/src/material/schematics/ng-generate/table/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.html.template @@ -1,4 +1,4 @@ -
    +
    diff --git a/src/material/tabs/tab-header.html b/src/material/tabs/tab-header.html index 104cd2df74ff..aca77b31e022 100644 --- a/src/material/tabs/tab-header.html +++ b/src/material/tabs/tab-header.html @@ -1,4 +1,3 @@ - - @for (item of extraItems; track item) { + @for (item of extraItems; track $index) { } @@ -2949,7 +2949,7 @@ class NestedMenuCustomElevation { template: ` - @for (item of items; track item) { + @for (item of items; track $index) { - @for (item of items; track item) { + @for (item of items; track $index) { } @@ -3092,7 +3092,7 @@ class SimpleMenuWithRepeater { - @for (item of items; track item) { + @for (item of items; track $index) { } diff --git a/src/material/menu/menu.ts b/src/material/menu/menu.ts index c2e0716ec0fe..b625f64727e2 100644 --- a/src/material/menu/menu.ts +++ b/src/material/menu/menu.ts @@ -120,7 +120,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnI private _firstItemFocusRef?: AfterRenderRef; private _previousElevation: string; private _elevationPrefix = 'mat-elevation-z'; - private _baseElevation = 8; + private _baseElevation: number | null = null; /** All items inside the menu. Includes items nested inside another menu. */ @ContentChildren(MatMenuItem, {descendants: true}) _allItems: QueryList; @@ -470,6 +470,17 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnI * @param depth Number of parent menus that come before the menu. */ setElevation(depth: number): void { + // The base elevation depends on which version of the spec + // we're running so we have to resolve it at runtime. + if (this._baseElevation === null) { + const styles = + typeof getComputedStyle === 'function' + ? getComputedStyle(this._elementRef.nativeElement) + : null; + const value = styles?.getPropertyValue('--mat-menu-base-elevation-level') || '8'; + this._baseElevation = parseInt(value); + } + // The elevation starts at the base and increases by one for each level. // Capped at 24 because that's the maximum elevation defined in the Material design spec. const elevation = Math.min(this._baseElevation + depth, 24); From 799d95269eaae15e0b6c602c4735edd03758cc3f Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 13 Jun 2024 08:08:58 -0700 Subject: [PATCH 52/61] test: Convert material chips tests to zoneless (#29238) --- src/material/chips/chip-grid.spec.ts | 34 ++++++++++++++++--- src/material/chips/chip-grid.ts | 20 ++++++++--- src/material/chips/chip-input.spec.ts | 16 ++++----- src/material/chips/chip-listbox.spec.ts | 28 +++++++++++---- src/material/chips/chip-option.spec.ts | 29 ++++++++++------ src/material/chips/chip-remove.spec.ts | 14 ++++---- src/material/chips/chip-row.spec.ts | 30 ++++++++-------- src/material/chips/chip-row.ts | 30 ++++++++-------- src/material/chips/chip-set.spec.ts | 15 ++++---- src/material/chips/chip-set.ts | 8 +++-- src/material/chips/chip.spec.ts | 18 +++++----- src/material/chips/chip.ts | 2 +- .../chips/testing/chip-grid-harness.spec.ts | 14 ++++---- .../chips/testing/chip-input-harness.spec.ts | 11 ++---- .../testing/chip-listbox-harness.spec.ts | 14 ++++---- .../chips/testing/chip-row-harness.spec.ts | 3 +- tools/public_api_guard/material/chips.md | 7 +++- 17 files changed, 175 insertions(+), 118 deletions(-) diff --git a/src/material/chips/chip-grid.spec.ts b/src/material/chips/chip-grid.spec.ts index 85edeebe8781..cb18d49372f9 100644 --- a/src/material/chips/chip-grid.spec.ts +++ b/src/material/chips/chip-grid.spec.ts @@ -20,6 +20,7 @@ import { typeInElement, } from '@angular/cdk/testing/private'; import { + ChangeDetectorRef, Component, DebugElement, EventEmitter, @@ -27,8 +28,9 @@ import { Type, ViewChild, ViewChildren, - provideZoneChangeDetection, + inject, } from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {ComponentFixture, TestBed, fakeAsync, flush, tick} from '@angular/core/testing'; import {FormControl, FormsModule, NgForm, ReactiveFormsModule, Validators} from '@angular/forms'; import {MatFormFieldModule} from '@angular/material/form-field'; @@ -70,11 +72,13 @@ describe('MDC-based MatChipGrid', () => { expect(chips.toArray().every(chip => chip.disabled)).toBe(false); chipGridInstance.disabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chips.toArray().every(chip => chip.disabled)).toBe(true); chipGridInstance.disabled = false; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chips.toArray().every(chip => chip.disabled)).toBe(false); @@ -84,11 +88,13 @@ describe('MDC-based MatChipGrid', () => { expect(chips.toArray().every(chip => chip.disabled)).toBe(false); chipGridInstance.disabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chips.toArray().every(chip => chip.disabled)).toBe(true); fixture.componentInstance.chips.push(5, 6); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(); fixture.detectChanges(); @@ -98,6 +104,7 @@ describe('MDC-based MatChipGrid', () => { it('should not set a role on the grid when the list is empty', () => { testComponent.chips = []; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipGridNativeElement.hasAttribute('role')).toBe(false); @@ -105,6 +112,7 @@ describe('MDC-based MatChipGrid', () => { it('should be able to set a custom role', () => { testComponent.role = 'listbox'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipGridNativeElement.getAttribute('role')).toBe('listbox'); @@ -140,6 +148,7 @@ describe('MDC-based MatChipGrid', () => { .toBe(false); chipGridInstance.disabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); chipGridInstance.focus(); @@ -154,6 +163,7 @@ describe('MDC-based MatChipGrid', () => { expect(chipGridNativeElement.getAttribute('tabindex')).toBe('0'); chipGridInstance.disabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipGridNativeElement.getAttribute('tabindex')).toBe('-1'); @@ -168,6 +178,7 @@ describe('MDC-based MatChipGrid', () => { // Destroy the middle item testComponent.chips.splice(2, 1); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // It focuses the 4th item @@ -180,6 +191,7 @@ describe('MDC-based MatChipGrid', () => { // Destroy the last item testComponent.chips.pop(); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // It focuses the next-to-last item @@ -196,6 +208,7 @@ describe('MDC-based MatChipGrid', () => { // Destroy the middle item testComponent.chips.splice(2, 1); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -205,12 +218,14 @@ describe('MDC-based MatChipGrid', () => { it('should focus the grid if the last focused item is removed', () => { testComponent.chips = [0]; + fixture.changeDetectorRef.markForCheck(); spyOn(chipGridInstance, 'focus'); patchElementFocus(chips.last.primaryAction!._elementRef.nativeElement); chips.last.focus(); testComponent.chips.pop(); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipGridInstance.focus).toHaveBeenCalled(); @@ -350,6 +365,7 @@ describe('MDC-based MatChipGrid', () => { it(`should use user defined tabIndex`, fakeAsync(() => { chipGridInstance.tabIndex = 4; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipGridInstance.tabIndex) @@ -422,6 +438,7 @@ describe('MDC-based MatChipGrid', () => { it('should ignore all non-tab navigation keyboard events from an editing chip', fakeAsync(() => { testComponent.editable = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); chips.first.focus(); @@ -572,6 +589,7 @@ describe('MDC-based MatChipGrid', () => { it('should take an initial view value with reactive forms', () => { fixture.componentInstance.control = new FormControl('[pizza-1]'); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.chipGrid.value).toEqual('[pizza-1]'); @@ -756,6 +774,7 @@ describe('MDC-based MatChipGrid', () => { it('should set aria-invalid if the form field is invalid', fakeAsync(() => { fixture.componentInstance.control = new FormControl('', [Validators.required]); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const input: HTMLInputElement = fixture.nativeElement.querySelector('input'); @@ -991,10 +1010,7 @@ describe('MDC-based MatChipGrid', () => { MatInputModule, animationsModule, ], - providers: [ - {provide: Directionality, useValue: directionality}, - provideZoneChangeDetection(), - ], + providers: [{provide: Directionality, useValue: directionality}], declarations: [component], }).compileComponents(); @@ -1149,6 +1165,14 @@ class ChipGridWithFormErrorMessages { @ViewChild('form') form: NgForm; formControl = new FormControl('', Validators.required); + + private readonly _changeDetectorRef = inject(ChangeDetectorRef); + + constructor() { + this.formControl.events.pipe(takeUntilDestroyed()).subscribe(() => { + this._changeDetectorRef.markForCheck(); + }); + } } @Component({ diff --git a/src/material/chips/chip-grid.ts b/src/material/chips/chip-grid.ts index c75db9aeaeea..32e9cb47d8e8 100644 --- a/src/material/chips/chip-grid.ts +++ b/src/material/chips/chip-grid.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {Directionality} from '@angular/cdk/bidi'; import {hasModifierKey, TAB} from '@angular/cdk/keycodes'; import { AfterContentInit, @@ -20,6 +21,7 @@ import { EventEmitter, Input, OnDestroy, + OnInit, Optional, Output, QueryList, @@ -33,15 +35,14 @@ import { NgForm, Validators, } from '@angular/forms'; -import {ErrorStateMatcher, _ErrorStateTracker} from '@angular/material/core'; +import {_ErrorStateTracker, ErrorStateMatcher} from '@angular/material/core'; import {MatFormFieldControl} from '@angular/material/form-field'; -import {MatChipTextControl} from './chip-text-control'; -import {Observable, Subject, merge} from 'rxjs'; +import {merge, Observable, Subject} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; import {MatChipEvent} from './chip'; import {MatChipRow} from './chip-row'; import {MatChipSet} from './chip-set'; -import {Directionality} from '@angular/cdk/bidi'; +import {MatChipTextControl} from './chip-text-control'; /** Change event object that is emitted when the chip grid value has changed. */ export class MatChipGridChange { @@ -90,7 +91,8 @@ export class MatChipGrid ControlValueAccessor, DoCheck, MatFormFieldControl, - OnDestroy + OnDestroy, + OnInit { /** * Implemented as part of MatFormFieldControl. @@ -278,6 +280,14 @@ export class MatChipGrid ); } + ngOnInit() { + if (this.ngControl) { + this.ngControl.control?.events.pipe(takeUntil(this._destroyed)).subscribe(() => { + this._changeDetectorRef.markForCheck(); + }); + } + } + ngAfterContentInit() { this.chipBlurChanges.pipe(takeUntil(this._destroyed)).subscribe(() => { this._blur(); diff --git a/src/material/chips/chip-input.spec.ts b/src/material/chips/chip-input.spec.ts index ca0a6fcb416a..32e5d203743e 100644 --- a/src/material/chips/chip-input.spec.ts +++ b/src/material/chips/chip-input.spec.ts @@ -2,8 +2,8 @@ import {Directionality} from '@angular/cdk/bidi'; import {COMMA, ENTER, TAB} from '@angular/cdk/keycodes'; import {PlatformModule} from '@angular/cdk/platform'; import {dispatchKeyboardEvent} from '@angular/cdk/testing/private'; -import {Component, DebugElement, ViewChild, provideZoneChangeDetection} from '@angular/core'; -import {waitForAsync, ComponentFixture, fakeAsync, TestBed, flush} from '@angular/core/testing'; +import {Component, DebugElement, ViewChild} from '@angular/core'; +import {ComponentFixture, TestBed, fakeAsync, flush, waitForAsync} from '@angular/core/testing'; import {MatFormFieldModule} from '@angular/material/form-field'; import {By} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; @@ -25,12 +25,6 @@ describe('MDC-based MatChipInput', () => { let chipInputDirective: MatChipInput; let dir = 'ltr'; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], - }); - }); - beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [PlatformModule, MatChipsModule, MatFormFieldModule, NoopAnimationsModule], @@ -77,6 +71,7 @@ describe('MDC-based MatChipInput', () => { expect(inputNativeElement.hasAttribute('placeholder')).toBe(false); testChipInput.placeholder = 'bound placeholder'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(inputNativeElement.getAttribute('placeholder')).toBe('bound placeholder'); @@ -87,6 +82,7 @@ describe('MDC-based MatChipInput', () => { expect(chipInputDirective.disabled).toBe(false); fixture.componentInstance.chipGridInstance.disabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(inputNativeElement.getAttribute('disabled')).toBe('true'); @@ -97,6 +93,7 @@ describe('MDC-based MatChipInput', () => { expect(inputNativeElement.hasAttribute('aria-required')).toBe(false); fixture.componentInstance.required = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(inputNativeElement.getAttribute('aria-required')).toBe('true'); @@ -106,6 +103,7 @@ describe('MDC-based MatChipInput', () => { expect(inputNativeElement.hasAttribute('required')).toBe(false); fixture.componentInstance.required = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(inputNativeElement.getAttribute('required')).toBe('true'); @@ -144,6 +142,7 @@ describe('MDC-based MatChipInput', () => { spyOn(testChipInput, 'add'); testChipInput.addOnBlur = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); chipInputDirective._blur(); @@ -154,6 +153,7 @@ describe('MDC-based MatChipInput', () => { spyOn(testChipInput, 'add'); testChipInput.addOnBlur = false; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); chipInputDirective._blur(); diff --git a/src/material/chips/chip-listbox.spec.ts b/src/material/chips/chip-listbox.spec.ts index 4ebf4093d7f8..34159fd46cde 100644 --- a/src/material/chips/chip-listbox.spec.ts +++ b/src/material/chips/chip-listbox.spec.ts @@ -13,7 +13,6 @@ import { Type, ViewChild, ViewChildren, - provideZoneChangeDetection, } from '@angular/core'; import {ComponentFixture, TestBed, fakeAsync, flush, tick} from '@angular/core/testing'; import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; @@ -29,11 +28,6 @@ describe('MDC-based MatChipListbox', () => { let chips: QueryList; let directionality: {value: Direction; change: EventEmitter}; let primaryActions: NodeListOf; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], - }); - }); describe('StandardChipList', () => { describe('basic behaviors', () => { @@ -47,6 +41,7 @@ describe('MDC-based MatChipListbox', () => { it('should not have the aria-selected attribute when it is not selectable', fakeAsync(() => { testComponent.selectable = false; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(); @@ -64,11 +59,13 @@ describe('MDC-based MatChipListbox', () => { expect(chips.toArray().every(chip => chip.disabled)).toBe(false); chipListboxInstance.disabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chips.toArray().every(chip => chip.disabled)).toBe(true); chipListboxInstance.disabled = false; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chips.toArray().every(chip => chip.disabled)).toBe(false); @@ -78,11 +75,13 @@ describe('MDC-based MatChipListbox', () => { expect(chips.toArray().every(chip => chip.disabled)).toBe(false); chipListboxInstance.disabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chips.toArray().every(chip => chip.disabled)).toBe(true); fixture.componentInstance.chips.push(5, 6); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(); fixture.detectChanges(); @@ -92,6 +91,7 @@ describe('MDC-based MatChipListbox', () => { it('should not set a role on the grid when the list is empty', () => { testComponent.chips = []; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipListboxNativeElement.hasAttribute('role')).toBe(false); @@ -99,6 +99,7 @@ describe('MDC-based MatChipListbox', () => { it('should be able to set a custom role', () => { testComponent.role = 'grid'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipListboxNativeElement.getAttribute('role')).toBe('grid'); @@ -106,6 +107,7 @@ describe('MDC-based MatChipListbox', () => { it('should not set aria-required when it does not have a role', () => { testComponent.chips = []; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipListboxNativeElement.hasAttribute('role')).toBe(false); @@ -138,6 +140,7 @@ describe('MDC-based MatChipListbox', () => { it('should not have role when empty', () => { fixture.componentInstance.foods = []; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipListboxNativeElement.getAttribute('role')) @@ -171,6 +174,7 @@ describe('MDC-based MatChipListbox', () => { .toBe(false); chipListboxInstance.disabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); chipListboxInstance.focus(); @@ -185,6 +189,7 @@ describe('MDC-based MatChipListbox', () => { expect(chipListboxNativeElement.getAttribute('tabindex')).toBe('0'); chipListboxInstance.disabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipListboxNativeElement.getAttribute('tabindex')).toBe('-1'); @@ -200,6 +205,7 @@ describe('MDC-based MatChipListbox', () => { // Destroy the middle item testComponent.chips.splice(2, 1); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // It focuses the 4th item @@ -213,6 +219,7 @@ describe('MDC-based MatChipListbox', () => { // Destroy the last item testComponent.chips.pop(); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // It focuses the next-to-last item @@ -229,6 +236,7 @@ describe('MDC-based MatChipListbox', () => { // Destroy the middle item testComponent.chips.splice(2, 1); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(); @@ -238,6 +246,7 @@ describe('MDC-based MatChipListbox', () => { it('should focus the listbox if the last focused item is removed', fakeAsync(() => { testComponent.chips = [0]; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); spyOn(chipListboxInstance, 'focus'); @@ -245,6 +254,7 @@ describe('MDC-based MatChipListbox', () => { chips.last.focus(); testComponent.chips.pop(); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipListboxInstance.focus).toHaveBeenCalled(); @@ -370,6 +380,7 @@ describe('MDC-based MatChipListbox', () => { it('should use user defined tabIndex', fakeAsync(() => { chipListboxInstance.tabIndex = 4; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); @@ -429,6 +440,7 @@ describe('MDC-based MatChipListbox', () => { .toBe(chips.first); fixture.componentInstance.foods = []; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(); @@ -440,6 +452,7 @@ describe('MDC-based MatChipListbox', () => { it('should select an option that was added after initialization', () => { fixture = createComponent(BasicChipListbox); fixture.componentInstance.foods.push({viewValue: 'Potatoes', value: 'potatoes-8'}); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); primaryActions = chipListboxNativeElement.querySelectorAll( @@ -509,6 +522,7 @@ describe('MDC-based MatChipListbox', () => { {value: 'tacos-2', viewValue: 'Tacos'}, ]; fixture.componentInstance.selectable = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); primaryActions = chipListboxNativeElement.querySelectorAll( @@ -547,6 +561,7 @@ describe('MDC-based MatChipListbox', () => { {value: 'tacos-2', viewValue: 'Tacos'}, ]; fixture.componentInstance.selectable = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); primaryActions = chipListboxNativeElement.querySelectorAll( @@ -585,6 +600,7 @@ describe('MDC-based MatChipListbox', () => { it('should take an initial view value with reactive forms', fakeAsync(() => { fixture.componentInstance.control = new FormControl('pizza-1'); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(); const array = chips.toArray(); diff --git a/src/material/chips/chip-option.spec.ts b/src/material/chips/chip-option.spec.ts index e7295766a14d..f2a29a30ff9f 100644 --- a/src/material/chips/chip-option.spec.ts +++ b/src/material/chips/chip-option.spec.ts @@ -1,20 +1,20 @@ import {Directionality} from '@angular/cdk/bidi'; +import {ENTER, SPACE} from '@angular/cdk/keycodes'; import {dispatchFakeEvent, dispatchKeyboardEvent} from '@angular/cdk/testing/private'; -import {Component, DebugElement, ViewChild, provideZoneChangeDetection} from '@angular/core'; -import {waitForAsync, ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing'; +import {Component, DebugElement, ViewChild} from '@angular/core'; +import {ComponentFixture, TestBed, fakeAsync, flush, waitForAsync} from '@angular/core/testing'; import {MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions} from '@angular/material/core'; import {By} from '@angular/platform-browser'; import {Subject} from 'rxjs'; import { + MAT_CHIPS_DEFAULT_OPTIONS, MatChipEvent, MatChipListbox, MatChipOption, - MatChipsDefaultOptions, MatChipSelectionChange, + MatChipsDefaultOptions, MatChipsModule, - MAT_CHIPS_DEFAULT_OPTIONS, } from './index'; -import {ENTER, SPACE} from '@angular/cdk/keycodes'; describe('MDC-based Option Chips', () => { let fixture: ComponentFixture; @@ -27,12 +27,6 @@ describe('MDC-based Option Chips', () => { let hideSingleSelectionIndicator: boolean | undefined; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], - }); - }); - beforeEach(waitForAsync(() => { globalRippleOptions = {}; const defaultOptions: MatChipsDefaultOptions = { @@ -96,6 +90,7 @@ describe('MDC-based Option Chips', () => { // Force a destroy callback testComponent.shouldShow = false; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(testComponent.chipDestroy).toHaveBeenCalledTimes(1); @@ -105,6 +100,7 @@ describe('MDC-based Option Chips', () => { expect(chipNativeElement.classList).toContain('mat-primary'); testComponent.color = 'warn'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipNativeElement.classList).not.toContain('mat-primary'); @@ -116,6 +112,7 @@ describe('MDC-based Option Chips', () => { expect(chipNativeElement.classList).not.toContain('mat-mdc-chip-selected'); testComponent.selected = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipNativeElement.classList).toContain('mat-mdc-chip-selected'); @@ -203,6 +200,7 @@ describe('MDC-based Option Chips', () => { it('should be able to set a custom role', () => { chipInstance.role = 'button'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipNativeElement.getAttribute('role')).toBe('button'); @@ -213,6 +211,7 @@ describe('MDC-based Option Chips', () => { describe('when selectable is true', () => { beforeEach(() => { testComponent.selectable = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }); @@ -252,6 +251,7 @@ describe('MDC-based Option Chips', () => { expect(primaryAction.getAttribute('aria-selected')).toBe('false'); testComponent.selected = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(primaryAction.getAttribute('aria-selected')).toBe('true'); @@ -259,11 +259,13 @@ describe('MDC-based Option Chips', () => { it('should have the correct aria-selected in multi-selection mode', fakeAsync(() => { testComponent.chipList.multiple = true; + fixture.changeDetectorRef.markForCheck(); flush(); fixture.detectChanges(); expect(primaryAction.getAttribute('aria-selected')).toBe('false'); testComponent.selected = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(primaryAction.getAttribute('aria-selected')).toBe('true'); @@ -272,6 +274,7 @@ describe('MDC-based Option Chips', () => { it('should disable focus on the checkmark', fakeAsync(() => { // The checkmark is only shown in multi selection mode. testComponent.chipList.multiple = true; + fixture.changeDetectorRef.markForCheck(); flush(); fixture.detectChanges(); @@ -283,6 +286,7 @@ describe('MDC-based Option Chips', () => { describe('when selectable is false', () => { beforeEach(() => { testComponent.selectable = false; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }); @@ -306,6 +310,7 @@ describe('MDC-based Option Chips', () => { expect(primaryAction.getAttribute('aria-disabled')).toBe('false'); testComponent.disabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(primaryAction.getAttribute('aria-disabled')).toBe('true'); @@ -330,6 +335,7 @@ describe('MDC-based Option Chips', () => { it('should apply `ariaLabel` and `ariaDesciption` to the element with option role', () => { testComponent.ariaLabel = 'option name'; testComponent.ariaDescription = 'option description'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); @@ -387,6 +393,7 @@ describe('MDC-based Option Chips', () => { it('displays checkmark graphic when avatar is provided', () => { testComponent.selected = true; testComponent.avatarLabel = 'A'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipNativeElement.querySelector('.mat-mdc-chip-graphic')).toBeTruthy(); diff --git a/src/material/chips/chip-remove.spec.ts b/src/material/chips/chip-remove.spec.ts index 1bffa65230b2..1393c84b078a 100644 --- a/src/material/chips/chip-remove.spec.ts +++ b/src/material/chips/chip-remove.spec.ts @@ -1,19 +1,14 @@ -import {Component, provideZoneChangeDetection} from '@angular/core'; -import {waitForAsync, ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing'; +import {ENTER, SPACE} from '@angular/cdk/keycodes'; import {dispatchKeyboardEvent, dispatchMouseEvent} from '@angular/cdk/testing/private'; +import {Component} from '@angular/core'; +import {ComponentFixture, TestBed, fakeAsync, flush, waitForAsync} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; -import {SPACE, ENTER} from '@angular/cdk/keycodes'; import {MatChip, MatChipsModule} from './index'; describe('MDC-based Chip Remove', () => { let fixture: ComponentFixture; let testChip: TestChip; let chipNativeElement: HTMLElement; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], - }); - }); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -50,6 +45,7 @@ describe('MDC-based Chip Remove', () => { it('should emit (removed) event when exit animation is complete', fakeAsync(() => { testChip.removable = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); chipNativeElement.querySelector('button')!.click(); @@ -70,6 +66,7 @@ describe('MDC-based Chip Remove', () => { const buttonElement = chipNativeElement.querySelector('button')!; testChip.removable = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const event = dispatchKeyboardEvent(buttonElement, 'keydown', SPACE); @@ -83,6 +80,7 @@ describe('MDC-based Chip Remove', () => { const buttonElement = chipNativeElement.querySelector('button')!; testChip.removable = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const event = dispatchKeyboardEvent(buttonElement, 'keydown', ENTER); diff --git a/src/material/chips/chip-row.spec.ts b/src/material/chips/chip-row.spec.ts index 8807a1046d0d..2c139c17cb95 100644 --- a/src/material/chips/chip-row.spec.ts +++ b/src/material/chips/chip-row.spec.ts @@ -6,19 +6,13 @@ import { dispatchFakeEvent, dispatchKeyboardEvent, } from '@angular/cdk/testing/private'; -import { - Component, - DebugElement, - ElementRef, - ViewChild, - provideZoneChangeDetection, -} from '@angular/core'; -import {waitForAsync, ComponentFixture, TestBed, flush, fakeAsync} from '@angular/core/testing'; +import {Component, DebugElement, ElementRef, ViewChild} from '@angular/core'; +import {ComponentFixture, TestBed, fakeAsync, flush, waitForAsync} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {Subject} from 'rxjs'; import { - MatChipEditedEvent, MatChipEditInput, + MatChipEditedEvent, MatChipEvent, MatChipGrid, MatChipRow, @@ -33,12 +27,6 @@ describe('MDC-based Row Chips', () => { let dir = 'ltr'; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], - }); - }); - beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [MatChipsModule, SingleChip], @@ -83,6 +71,7 @@ describe('MDC-based Row Chips', () => { // Force a destroy callback testComponent.shouldShow = false; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(testComponent.chipDestroy).toHaveBeenCalledTimes(1); @@ -92,6 +81,7 @@ describe('MDC-based Row Chips', () => { expect(chipNativeElement.classList).toContain('mat-primary'); testComponent.color = 'warn'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipNativeElement.classList).not.toContain('mat-primary'); @@ -117,6 +107,7 @@ describe('MDC-based Row Chips', () => { it('should be able to set a custom role', () => { chipInstance.role = 'button'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipNativeElement.getAttribute('role')).toBe('button'); @@ -127,6 +118,7 @@ describe('MDC-based Row Chips', () => { describe('when removable is true', () => { beforeEach(() => { testComponent.removable = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }); @@ -170,6 +162,7 @@ describe('MDC-based Row Chips', () => { describe('when removable is false', () => { beforeEach(() => { testComponent.removable = false; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }); @@ -205,6 +198,7 @@ describe('MDC-based Row Chips', () => { expect(primaryActionElement.getAttribute('aria-disabled')).toBe('false'); testComponent.disabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(primaryActionElement.getAttribute('aria-disabled')).toBe('true'); @@ -236,6 +230,7 @@ describe('MDC-based Row Chips', () => { describe('editable behavior', () => { beforeEach(() => { testComponent.editable = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }); @@ -260,6 +255,7 @@ describe('MDC-based Row Chips', () => { beforeEach(fakeAsync(() => { testComponent.editable = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchFakeEvent(chipNativeElement, 'dblclick'); fixture.detectChanges(); @@ -283,10 +279,12 @@ describe('MDC-based Row Chips', () => { it('should set the role of the primary action to gridcell', () => { testComponent.editable = false; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(primaryAction.getAttribute('role')).toBe('gridcell'); testComponent.editable = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Test regression of bug where element is mislabeled as a button role. Element that does not perform its // action on click event is not a button by ARIA spec (#27106). @@ -329,6 +327,7 @@ describe('MDC-based Row Chips', () => { it('should use the default edit input if none is projected', () => { keyDownOnPrimaryAction(ENTER, 'Enter'); testComponent.useCustomEditInput = false; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchFakeEvent(chipNativeElement, 'dblclick'); fixture.detectChanges(); @@ -369,6 +368,7 @@ describe('MDC-based Row Chips', () => { it('should apply `ariaLabel` and `ariaDesciption` to the primary gridcell', () => { fixture.componentInstance.ariaLabel = 'chip name'; fixture.componentInstance.ariaDescription = 'chip description'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); diff --git a/src/material/chips/chip-row.ts b/src/material/chips/chip-row.ts index f790eb8e73fc..bee7f55e087d 100644 --- a/src/material/chips/chip-row.ts +++ b/src/material/chips/chip-row.ts @@ -6,8 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ +import {FocusMonitor} from '@angular/cdk/a11y'; import {ENTER} from '@angular/cdk/keycodes'; +import {DOCUMENT} from '@angular/common'; import { + ANIMATION_MODULE_TYPE, AfterViewInit, Attribute, ChangeDetectionStrategy, @@ -23,16 +26,14 @@ import { Output, ViewChild, ViewEncapsulation, - ANIMATION_MODULE_TYPE, + afterNextRender, } from '@angular/core'; -import {DOCUMENT} from '@angular/common'; import {MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions} from '@angular/material/core'; -import {FocusMonitor} from '@angular/cdk/a11y'; +import {takeUntil} from 'rxjs/operators'; import {MatChip, MatChipEvent} from './chip'; +import {MatChipAction} from './chip-action'; import {MatChipEditInput} from './chip-edit-input'; -import {takeUntil} from 'rxjs/operators'; import {MAT_CHIP} from './tokens'; -import {MatChipAction} from './chip-action'; /** Represents an event fired on an individual `mat-chip` when it is edited. */ export interface MatChipEditedEvent extends MatChipEvent { @@ -182,17 +183,14 @@ export class MatChipRow extends MatChip implements AfterViewInit { this._isEditing = this._editStartPending = true; - // Starting the editing sequence below depends on the edit input - // query resolving on time. Trigger a synchronous change detection to - // ensure that it happens by the time we hit the timeout below. - this._changeDetectorRef.detectChanges(); - - // TODO(crisbeto): this timeout shouldn't be necessary given the `detectChange` call above. - // Defer initializing the input so it has time to be added to the DOM. - setTimeout(() => { - this._getEditInput().initialize(value); - this._editStartPending = false; - }); + // Defer initializing the input until after it has been added to the DOM. + afterNextRender( + () => { + this._getEditInput().initialize(value); + this._editStartPending = false; + }, + {injector: this._injector}, + ); } private _onEditFinish() { diff --git a/src/material/chips/chip-set.spec.ts b/src/material/chips/chip-set.spec.ts index 7b2d5a5690b4..bf556874b336 100644 --- a/src/material/chips/chip-set.spec.ts +++ b/src/material/chips/chip-set.spec.ts @@ -1,6 +1,6 @@ -import {Component, DebugElement, QueryList, provideZoneChangeDetection} from '@angular/core'; -import {waitForAsync, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; import {CommonModule} from '@angular/common'; +import {Component, DebugElement, QueryList} from '@angular/core'; +import {ComponentFixture, TestBed, fakeAsync, tick, waitForAsync} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {MatChip, MatChipSet, MatChipsModule} from './index'; @@ -20,12 +20,6 @@ describe('MDC-based MatChipSet', () => { let chipSetInstance: MatChipSet; let chips: QueryList; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], - }); - }); - describe('basic behaviors', () => { beforeEach(() => { fixture = TestBed.createComponent(BasicChipSet); @@ -45,11 +39,13 @@ describe('MDC-based MatChipSet', () => { expect(chips.toArray().every(chip => chip.disabled)).toBe(false); chipSetInstance.disabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chips.toArray().every(chip => chip.disabled)).toBe(true); chipSetInstance.disabled = false; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chips.toArray().every(chip => chip.disabled)).toBe(false); @@ -59,11 +55,13 @@ describe('MDC-based MatChipSet', () => { expect(chips.toArray().every(chip => chip.disabled)).toBe(false); chipSetInstance.disabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chips.toArray().every(chip => chip.disabled)).toBe(true); fixture.componentInstance.chips.push(5, 6); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(); fixture.detectChanges(); @@ -77,6 +75,7 @@ describe('MDC-based MatChipSet', () => { it('should allow a custom role to be specified', () => { chipSetInstance.role = 'list'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipSetNativeElement.getAttribute('role')).toBe('list'); }); diff --git a/src/material/chips/chip-set.ts b/src/material/chips/chip-set.ts index 5495059a49ee..d38d1ff9a57b 100644 --- a/src/material/chips/chip-set.ts +++ b/src/material/chips/chip-set.ts @@ -23,7 +23,7 @@ import { booleanAttribute, numberAttribute, } from '@angular/core'; -import {merge, Observable, Subject} from 'rxjs'; +import {Observable, Subject, merge} from 'rxjs'; import {startWith, switchMap, takeUntil} from 'rxjs/operators'; import {MatChip, MatChipEvent} from './chip'; import {MatChipAction} from './chip-action'; @@ -194,10 +194,14 @@ export class MatChipSet implements AfterViewInit, OnDestroy { if (this.tabIndex !== -1) { const previousTabIndex = this.tabIndex; this.tabIndex = -1; + this._changeDetectorRef.markForCheck(); // Note that this needs to be a `setTimeout`, because a `Promise.resolve` // doesn't allow enough time for the focus to escape. - setTimeout(() => (this.tabIndex = previousTabIndex)); + setTimeout(() => { + this.tabIndex = previousTabIndex; + this._changeDetectorRef.markForCheck(); + }); } } diff --git a/src/material/chips/chip.spec.ts b/src/material/chips/chip.spec.ts index 475a21ccd5d1..9d4037038bdd 100644 --- a/src/material/chips/chip.spec.ts +++ b/src/material/chips/chip.spec.ts @@ -1,6 +1,6 @@ import {Directionality} from '@angular/cdk/bidi'; -import {Component, DebugElement, ViewChild, provideZoneChangeDetection} from '@angular/core'; -import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing'; +import {Component, DebugElement, ViewChild} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {Subject} from 'rxjs'; import {MatChip, MatChipEvent, MatChipSet, MatChipsModule} from './index'; @@ -13,12 +13,6 @@ describe('MDC-based MatChip', () => { let dir = 'ltr'; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], - }); - }); - beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ @@ -68,6 +62,7 @@ describe('MDC-based MatChip', () => { expect(chip.getAttribute('tabindex')).toBe('12'); fixture.componentInstance.tabindex = 15; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chip.getAttribute('tabindex')).toBe('15'); @@ -112,6 +107,7 @@ describe('MDC-based MatChip', () => { // Force a destroy callback testComponent.shouldShow = false; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(testComponent.chipDestroy).toHaveBeenCalledTimes(1); @@ -121,6 +117,7 @@ describe('MDC-based MatChip', () => { expect(chipNativeElement.classList).toContain('mat-primary'); testComponent.color = 'warn'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipNativeElement.classList).not.toContain('mat-primary'); @@ -142,6 +139,7 @@ describe('MDC-based MatChip', () => { .toBe(false); testComponent.rippleDisabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipInstance.ripple.disabled) @@ -155,6 +153,7 @@ describe('MDC-based MatChip', () => { .toBe(false); testComponent.disabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipInstance.ripple.disabled) @@ -164,6 +163,7 @@ describe('MDC-based MatChip', () => { it('should make disabled chips non-focusable', () => { testComponent.disabled = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(primaryAction.hasAttribute('tabindex')).toBe(false); }); @@ -174,6 +174,7 @@ describe('MDC-based MatChip', () => { it('should return the chip value if defined', () => { fixture.componentInstance.value = 123; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipInstance.value).toBe(123); @@ -181,6 +182,7 @@ describe('MDC-based MatChip', () => { it('should return the chip value if set to null', () => { fixture.componentInstance.value = null; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipInstance.value).toBeNull(); diff --git a/src/material/chips/chip.ts b/src/material/chips/chip.ts index 71a677c59e91..57f6a1899241 100644 --- a/src/material/chips/chip.ts +++ b/src/material/chips/chip.ts @@ -244,7 +244,7 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck */ _rippleLoader: MatRippleLoader = inject(MatRippleLoader); - private _injector = inject(Injector); + protected _injector = inject(Injector); constructor( public _changeDetectorRef: ChangeDetectorRef, diff --git a/src/material/chips/testing/chip-grid-harness.spec.ts b/src/material/chips/testing/chip-grid-harness.spec.ts index e99aad58915f..7751683e33b0 100644 --- a/src/material/chips/testing/chip-grid-harness.spec.ts +++ b/src/material/chips/testing/chip-grid-harness.spec.ts @@ -1,8 +1,8 @@ import {HarnessLoader, parallel} from '@angular/cdk/testing'; import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; -import {FormControl, ReactiveFormsModule, Validators} from '@angular/forms'; -import {Component, provideZoneChangeDetection} from '@angular/core'; +import {Component} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {FormControl, ReactiveFormsModule, Validators} from '@angular/forms'; import {MatChipsModule} from '../index'; import {MatChipGridHarness} from './chip-grid-harness'; @@ -10,12 +10,6 @@ describe('MatChipGridHarness', () => { let fixture: ComponentFixture; let loader: HarnessLoader; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], - }); - }); - beforeEach(async () => { await TestBed.configureTestingModule({ imports: [MatChipsModule, ReactiveFormsModule, ChipGridHarnessTest], @@ -69,6 +63,7 @@ describe('MatChipGridHarness', () => { expect(await harness.isRequired()).toBe(false); fixture.componentInstance.required = true; + fixture.changeDetectorRef.markForCheck(); expect(await harness.isRequired()).toBe(true); }); @@ -88,6 +83,7 @@ describe('MatChipGridHarness', () => { const grid = await loader.getHarness(MatChipGridHarness); const chips = await grid.getRows(); fixture.componentInstance.firstChipEditable = true; + fixture.changeDetectorRef.markForCheck(); expect(await parallel(() => chips.map(chip => chip.isEditable()))).toEqual([ true, @@ -101,6 +97,7 @@ describe('MatChipGridHarness', () => { const chip = (await grid.getRows())[0]; let error: string | null = null; fixture.componentInstance.firstChipEditable = false; + fixture.changeDetectorRef.markForCheck(); try { await chip.startEditing(); @@ -115,6 +112,7 @@ describe('MatChipGridHarness', () => { const grid = await loader.getHarness(MatChipGridHarness); const chip = (await grid.getRows())[0]; fixture.componentInstance.firstChipEditable = true; + fixture.changeDetectorRef.markForCheck(); await chip.startEditing(); await (await chip.getEditInput()).setValue('new value'); diff --git a/src/material/chips/testing/chip-input-harness.spec.ts b/src/material/chips/testing/chip-input-harness.spec.ts index 41305dacda92..9b0e60d6e44d 100644 --- a/src/material/chips/testing/chip-input-harness.spec.ts +++ b/src/material/chips/testing/chip-input-harness.spec.ts @@ -1,7 +1,7 @@ -import {HarnessLoader, TestKey} from '@angular/cdk/testing'; import {COMMA} from '@angular/cdk/keycodes'; +import {HarnessLoader, TestKey} from '@angular/cdk/testing'; import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; -import {Component, provideZoneChangeDetection} from '@angular/core'; +import {Component} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {MatChipsModule} from '../index'; import {MatChipInputHarness} from './chip-input-harness'; @@ -10,12 +10,6 @@ describe('MatChipInputHarness', () => { let fixture: ComponentFixture; let loader: HarnessLoader; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], - }); - }); - beforeEach(async () => { await TestBed.configureTestingModule({ imports: [MatChipsModule, ChipInputHarnessTest], @@ -49,6 +43,7 @@ describe('MatChipInputHarness', () => { expect(await harness.isRequired()).toBe(false); fixture.componentInstance.required = true; + fixture.changeDetectorRef.markForCheck(); expect(await harness.isRequired()).toBe(true); }); diff --git a/src/material/chips/testing/chip-listbox-harness.spec.ts b/src/material/chips/testing/chip-listbox-harness.spec.ts index 2e3b0030f814..6c36fce9f32e 100644 --- a/src/material/chips/testing/chip-listbox-harness.spec.ts +++ b/src/material/chips/testing/chip-listbox-harness.spec.ts @@ -1,6 +1,6 @@ import {HarnessLoader, parallel} from '@angular/cdk/testing'; import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; -import {Component, provideZoneChangeDetection} from '@angular/core'; +import {Component} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {MatChipsModule} from '../index'; import {MatChipListboxHarness} from './chip-listbox-harness'; @@ -8,11 +8,6 @@ import {MatChipListboxHarness} from './chip-listbox-harness'; describe('MatChipListboxHarness', () => { let fixture: ComponentFixture; let loader: HarnessLoader; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], - }); - }); beforeEach(async () => { await TestBed.configureTestingModule({ @@ -39,6 +34,7 @@ describe('MatChipListboxHarness', () => { expect(await harness.isMultiple()).toBe(false); fixture.componentInstance.isMultiple = true; + fixture.changeDetectorRef.markForCheck(); expect(await harness.isMultiple()).toBe(true); }); @@ -49,6 +45,7 @@ describe('MatChipListboxHarness', () => { expect(disabledChips.length).toBe(0); fixture.componentInstance.disabled = true; + fixture.changeDetectorRef.markForCheck(); enabledChips = await loader.getAllHarnesses(MatChipListboxHarness.with({disabled: false})); disabledChips = await loader.getAllHarnesses(MatChipListboxHarness.with({disabled: true})); }); @@ -58,6 +55,7 @@ describe('MatChipListboxHarness', () => { expect(await harness.isDisabled()).toBe(false); fixture.componentInstance.disabled = true; + fixture.changeDetectorRef.markForCheck(); expect(await harness.isDisabled()).toBe(true); }); @@ -66,6 +64,7 @@ describe('MatChipListboxHarness', () => { expect(await harness.isRequired()).toBe(false); fixture.componentInstance.required = true; + fixture.changeDetectorRef.markForCheck(); expect(await harness.isRequired()).toBe(true); }); @@ -77,6 +76,7 @@ describe('MatChipListboxHarness', () => { it('should get selection in single-selection mode', async () => { fixture.componentInstance.options[0].selected = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const harness = await loader.getHarness(MatChipListboxHarness); @@ -89,6 +89,7 @@ describe('MatChipListboxHarness', () => { fixture.componentInstance.isMultiple = true; fixture.componentInstance.options[0].selected = true; fixture.componentInstance.options[1].selected = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const harness = await loader.getHarness(MatChipListboxHarness); @@ -100,6 +101,7 @@ describe('MatChipListboxHarness', () => { it('should be able to select specific options', async () => { fixture.componentInstance.isMultiple = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const harness = await loader.getHarness(MatChipListboxHarness); diff --git a/src/material/chips/testing/chip-row-harness.spec.ts b/src/material/chips/testing/chip-row-harness.spec.ts index ab249204d007..489cd9a896cd 100644 --- a/src/material/chips/testing/chip-row-harness.spec.ts +++ b/src/material/chips/testing/chip-row-harness.spec.ts @@ -1,6 +1,6 @@ import {HarnessLoader} from '@angular/cdk/testing'; import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; -import {Component, signal, provideZoneChangeDetection} from '@angular/core'; +import {Component, signal} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {MatChipsModule} from '../index'; import {MatChipRowHarness} from './chip-row-harness'; @@ -12,7 +12,6 @@ describe('MatChipRowHarness', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [MatChipsModule, ChipRowHarnessTest], - providers: [provideZoneChangeDetection()], }).compileComponents(); fixture = TestBed.createComponent(ChipRowHarnessTest); diff --git a/tools/public_api_guard/material/chips.md b/tools/public_api_guard/material/chips.md index 66249e1a9678..2df6784c6b24 100644 --- a/tools/public_api_guard/material/chips.md +++ b/tools/public_api_guard/material/chips.md @@ -19,6 +19,7 @@ import { FormGroupDirective } from '@angular/forms'; import * as i0 from '@angular/core'; import * as i1 from '@angular/material/core'; import { InjectionToken } from '@angular/core'; +import { Injector } from '@angular/core'; import { MatFormField } from '@angular/material/form-field'; import { MatFormFieldControl } from '@angular/material/form-field'; import { MatRipple } from '@angular/material/core'; @@ -84,6 +85,8 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck _hasTrailingIcon(): boolean; highlighted: boolean; id: string; + // (undocumented) + protected _injector: Injector; _isBasicChip: boolean; _isRippleDisabled(): boolean; leadingIcon: MatChipAvatar; @@ -169,7 +172,7 @@ export interface MatChipEvent { } // @public -export class MatChipGrid extends MatChipSet implements AfterContentInit, AfterViewInit, ControlValueAccessor, DoCheck, MatFormFieldControl, OnDestroy { +export class MatChipGrid extends MatChipSet implements AfterContentInit, AfterViewInit, ControlValueAccessor, DoCheck, MatFormFieldControl, OnDestroy, OnInit { constructor(elementRef: ElementRef, changeDetectorRef: ChangeDetectorRef, dir: Directionality, parentForm: NgForm, parentFormGroup: FormGroupDirective, defaultErrorStateMatcher: ErrorStateMatcher, ngControl: NgControl); protected _allowFocusEscape(): void; _blur(): void; @@ -208,6 +211,8 @@ export class MatChipGrid extends MatChipSet implements AfterContentInit, AfterVi ngDoCheck(): void; // (undocumented) ngOnDestroy(): void; + // (undocumented) + ngOnInit(): void; _onChange: (value: any) => void; onContainerClick(event: MouseEvent): void; _onTouched: () => void; From 243cdf0ab6f88fdcdd58d27f602ab1795329b1de Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 13 Jun 2024 08:10:55 -0700 Subject: [PATCH 53/61] docs(material/form-field): Update form-field docs & examples (#29245) --- .../form-field-appearance-example.ts | 5 +- .../example-tel-input-example.html | 64 +++--- .../form-field-custom-control-example.html | 1 + .../form-field-custom-control-example.ts | 203 +++++++++++------- .../form-field-error-example.html | 14 +- .../form-field-error-example.ts | 17 +- .../form-field-harness-example.ts | 9 +- .../form-field-hint-example.html | 4 +- .../form-field-hint-example.ts | 15 +- .../form-field-label-example.html | 14 +- .../form-field-label-example.ts | 26 +-- .../form-field-overview-example.ts | 3 +- .../form-field-prefix-suffix-example.html | 14 +- .../form-field-prefix-suffix-example.ts | 11 +- .../form-field-theming-example.css | 3 - .../form-field-theming-example.html | 8 - .../form-field-theming-example.ts | 17 -- .../material/form-field/index.ts | 3 +- src/material/form-field/form-field.md | 20 +- src/material/form-field/form-field.ts | 22 +- 20 files changed, 263 insertions(+), 210 deletions(-) delete mode 100644 src/components-examples/material/form-field/form-field-theming/form-field-theming-example.css delete mode 100644 src/components-examples/material/form-field/form-field-theming/form-field-theming-example.html delete mode 100644 src/components-examples/material/form-field/form-field-theming/form-field-theming-example.ts diff --git a/src/components-examples/material/form-field/form-field-appearance/form-field-appearance-example.ts b/src/components-examples/material/form-field/form-field-appearance/form-field-appearance-example.ts index 611acfb5f989..068fcdfc5c23 100644 --- a/src/components-examples/material/form-field/form-field-appearance/form-field-appearance-example.ts +++ b/src/components-examples/material/form-field/form-field-appearance/form-field-appearance-example.ts @@ -1,7 +1,7 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {MatFormFieldModule} from '@angular/material/form-field'; import {MatIconModule} from '@angular/material/icon'; import {MatInputModule} from '@angular/material/input'; -import {MatFormFieldModule} from '@angular/material/form-field'; /** @title Form field appearance variants */ @Component({ @@ -9,5 +9,6 @@ import {MatFormFieldModule} from '@angular/material/form-field'; templateUrl: 'form-field-appearance-example.html', standalone: true, imports: [MatFormFieldModule, MatInputModule, MatIconModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FormFieldAppearanceExample {} diff --git a/src/components-examples/material/form-field/form-field-custom-control/example-tel-input-example.html b/src/components-examples/material/form-field/form-field-custom-control/example-tel-input-example.html index 0103309a96b7..79775ce567d5 100644 --- a/src/components-examples/material/form-field/form-field-custom-control/example-tel-input-example.html +++ b/src/components-examples/material/form-field/form-field-custom-control/example-tel-input-example.html @@ -1,30 +1,40 @@ -
    - +
    + - + - +
    diff --git a/src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.html b/src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.html index cd025c09ba69..450f8afae244 100644 --- a/src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.html +++ b/src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.html @@ -5,4 +5,5 @@ phone Include area code +

    Entered value: {{form.valueChanges | async | json}}

    diff --git a/src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.ts b/src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.ts index 60008128ff42..2c0f4bc39674 100644 --- a/src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.ts +++ b/src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.ts @@ -1,35 +1,39 @@ import {FocusMonitor} from '@angular/cdk/a11y'; -import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; +import {AsyncPipe, JsonPipe} from '@angular/common'; import { + ChangeDetectionStrategy, Component, ElementRef, - Inject, - Input, OnDestroy, - Optional, - Self, - ViewChild, + booleanAttribute, + computed, + effect, forwardRef, + inject, + input, + model, + signal, + viewChild, } from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import { AbstractControl, ControlValueAccessor, FormBuilder, FormControl, FormGroup, - NgControl, - Validators, FormsModule, + NgControl, ReactiveFormsModule, + Validators, } from '@angular/forms'; import { MAT_FORM_FIELD, - MatFormField, MatFormFieldControl, MatFormFieldModule, } from '@angular/material/form-field'; -import {Subject} from 'rxjs'; import {MatIconModule} from '@angular/material/icon'; +import {Subject} from 'rxjs'; /** @title Form field with custom telephone number input control. */ @Component({ @@ -42,10 +46,13 @@ import {MatIconModule} from '@angular/material/icon'; MatFormFieldModule, forwardRef(() => MyTelInput), MatIconModule, + AsyncPipe, + JsonPipe, ], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FormFieldCustomControlExample { - form: FormGroup = new FormGroup({ + readonly form = new FormGroup({ tel: new FormControl(null), }); } @@ -71,26 +78,51 @@ export class MyTel { }, standalone: true, imports: [FormsModule, ReactiveFormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class MyTelInput implements ControlValueAccessor, MatFormFieldControl, OnDestroy { static nextId = 0; - @ViewChild('area') areaInput: HTMLInputElement; - @ViewChild('exchange') exchangeInput: HTMLInputElement; - @ViewChild('subscriber') subscriberInput: HTMLInputElement; - - parts: FormGroup<{ + readonly areaInput = viewChild.required('area'); + readonly exchangeInput = viewChild.required('exchange'); + readonly subscriberInput = viewChild.required('subscriber'); + ngControl = inject(NgControl, {optional: true, self: true}); + readonly parts: FormGroup<{ area: FormControl; exchange: FormControl; subscriber: FormControl; }>; - stateChanges = new Subject(); - focused = false; - touched = false; - controlType = 'example-tel-input'; - id = `example-tel-input-${MyTelInput.nextId++}`; + readonly stateChanges = new Subject(); + readonly touched = signal(false); + readonly controlType = 'example-tel-input'; + readonly id = `example-tel-input-${MyTelInput.nextId++}`; + readonly _userAriaDescribedBy = input('', {alias: 'aria-describedby'}); + readonly _placeholder = input('', {alias: 'placeholder'}); + readonly _required = input(false, { + alias: 'required', + transform: booleanAttribute, + }); + readonly _disabledByInput = input(false, { + alias: 'disabled', + transform: booleanAttribute, + }); + readonly _value = model(null, {alias: 'value'}); onChange = (_: any) => {}; onTouched = () => {}; + protected readonly _formField = inject(MAT_FORM_FIELD, { + optional: true, + }); + + private readonly _focused = signal(false); + private readonly _disabledByCva = signal(false); + private readonly _disabled = computed(() => this._disabledByInput() || this._disabledByCva()); + private readonly _focusMonitor = inject(FocusMonitor); + private readonly _elementRef = inject>(ElementRef); + + get focused(): boolean { + return this._focused(); + } + get empty() { const { value: {area, exchange, subscriber}, @@ -103,75 +135,75 @@ export class MyTelInput implements ControlValueAccessor, MatFormFieldControl, - @Optional() @Inject(MAT_FORM_FIELD) public _formField: MatFormField, - @Optional() @Self() public ngControl: NgControl, - ) { + constructor() { if (this.ngControl != null) { this.ngControl.valueAccessor = this; } - this.parts = formBuilder.group({ + this.parts = inject(FormBuilder).group({ area: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(3)]], exchange: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(3)]], subscriber: ['', [Validators.required, Validators.minLength(4), Validators.maxLength(4)]], }); + + effect(() => { + // Read signals to trigger effect. + this._placeholder(); + this._required(); + this._disabled(); + // Propagate state changes. + this.stateChanges.next(); + }); + + effect(() => { + if (this._disabled()) { + this.parts.disable(); + } else { + this.parts.enable(); + } + }); + + effect(() => { + this.parts.setValue(this._value() || new MyTel('', '', '')); + }); + + this.parts.statusChanges.pipe(takeUntilDestroyed()).subscribe(() => { + this.stateChanges.next(); + }); + + this.parts.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => { + const tel = this.parts.valid + ? new MyTel( + this.parts.value.area || '', + this.parts.value.exchange || '', + this.parts.value.subscriber || '', + ) + : null; + this._updateValue(tel); + }); } ngOnDestroy() { @@ -179,19 +211,17 @@ export class MyTelInput implements ControlValueAccessor, MatFormFieldControl Enter your email - + @if (email.invalid) { - {{errorMessage}} + {{errorMessage()}} } diff --git a/src/components-examples/material/form-field/form-field-error/form-field-error-example.ts b/src/components-examples/material/form-field/form-field-error/form-field-error-example.ts index 891a4432257e..0cb5a84be9b7 100644 --- a/src/components-examples/material/form-field/form-field-error/form-field-error-example.ts +++ b/src/components-examples/material/form-field/form-field-error/form-field-error-example.ts @@ -1,8 +1,8 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component, signal} from '@angular/core'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; -import {FormControl, Validators, FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {MatInputModule} from '@angular/material/input'; +import {FormControl, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms'; import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; import {merge} from 'rxjs'; /** @title Form field with error messages */ @@ -12,11 +12,12 @@ import {merge} from 'rxjs'; styleUrl: 'form-field-error-example.css', standalone: true, imports: [MatFormFieldModule, MatInputModule, FormsModule, ReactiveFormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FormFieldErrorExample { - email = new FormControl('', [Validators.required, Validators.email]); + readonly email = new FormControl('', [Validators.required, Validators.email]); - errorMessage = ''; + errorMessage = signal(''); constructor() { merge(this.email.statusChanges, this.email.valueChanges) @@ -26,11 +27,11 @@ export class FormFieldErrorExample { updateErrorMessage() { if (this.email.hasError('required')) { - this.errorMessage = 'You must enter a value'; + this.errorMessage.set('You must enter a value'); } else if (this.email.hasError('email')) { - this.errorMessage = 'Not a valid email'; + this.errorMessage.set('Not a valid email'); } else { - this.errorMessage = ''; + this.errorMessage.set(''); } } } diff --git a/src/components-examples/material/form-field/form-field-harness/form-field-harness-example.ts b/src/components-examples/material/form-field/form-field-harness/form-field-harness-example.ts index 19c1012604fe..a00eec80535c 100644 --- a/src/components-examples/material/form-field/form-field-harness/form-field-harness-example.ts +++ b/src/components-examples/material/form-field/form-field-harness/form-field-harness-example.ts @@ -1,7 +1,7 @@ -import {Component} from '@angular/core'; -import {FormControl, Validators, FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {MatInputModule} from '@angular/material/input'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {FormControl, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms'; import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; /** * @title Testing with MatFormFieldHarness @@ -11,7 +11,8 @@ import {MatFormFieldModule} from '@angular/material/form-field'; templateUrl: 'form-field-harness-example.html', standalone: true, imports: [MatFormFieldModule, MatInputModule, FormsModule, ReactiveFormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FormFieldHarnessExample { - requiredControl = new FormControl('Initial value', [Validators.required]); + readonly requiredControl = new FormControl('Initial value', [Validators.required]); } diff --git a/src/components-examples/material/form-field/form-field-hint/form-field-hint-example.html b/src/components-examples/material/form-field/form-field-hint/form-field-hint-example.html index 4c46c727f6c5..c08a2d4b11d3 100644 --- a/src/components-examples/material/form-field/form-field-hint/form-field-hint-example.html +++ b/src/components-examples/material/form-field/form-field-hint/form-field-hint-example.html @@ -1,8 +1,8 @@
    Enter some input - - {{input.value.length}}/10 + + {{value().length}}/10 diff --git a/src/components-examples/material/form-field/form-field-hint/form-field-hint-example.ts b/src/components-examples/material/form-field/form-field-hint/form-field-hint-example.ts index 5279fe2a0aef..5f7f5c45b031 100644 --- a/src/components-examples/material/form-field/form-field-hint/form-field-hint-example.ts +++ b/src/components-examples/material/form-field/form-field-hint/form-field-hint-example.ts @@ -1,7 +1,7 @@ -import {Component} from '@angular/core'; -import {MatSelectModule} from '@angular/material/select'; -import {MatInputModule} from '@angular/material/input'; +import {ChangeDetectionStrategy, Component, signal} from '@angular/core'; import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; +import {MatSelectModule} from '@angular/material/select'; /** @title Form field with hints */ @Component({ @@ -10,5 +10,12 @@ import {MatFormFieldModule} from '@angular/material/form-field'; styleUrl: 'form-field-hint-example.css', standalone: true, imports: [MatFormFieldModule, MatInputModule, MatSelectModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class FormFieldHintExample {} +export class FormFieldHintExample { + protected readonly value = signal(''); + + protected onInput(event: Event) { + this.value.set((event.target as HTMLInputElement).value); + } +} diff --git a/src/components-examples/material/form-field/form-field-label/form-field-label-example.html b/src/components-examples/material/form-field/form-field-label/form-field-label-example.html index 0556459e2afe..5caf21f0f5f7 100644 --- a/src/components-examples/material/form-field/form-field-label/form-field-label-example.html +++ b/src/components-examples/material/form-field/form-field-label/form-field-label-example.html @@ -10,20 +10,16 @@
    - - + + - + Both a label and a placeholder - + - + -- None -- Option diff --git a/src/components-examples/material/form-field/form-field-label/form-field-label-example.ts b/src/components-examples/material/form-field/form-field-label/form-field-label-example.ts index 7cdc7a52450b..a11f34d34301 100644 --- a/src/components-examples/material/form-field/form-field-label/form-field-label-example.ts +++ b/src/components-examples/material/form-field/form-field-label/form-field-label-example.ts @@ -1,11 +1,13 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component, inject} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {FormBuilder, FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatCheckboxModule} from '@angular/material/checkbox'; import {FloatLabelType, MatFormFieldModule} from '@angular/material/form-field'; import {MatIconModule} from '@angular/material/icon'; -import {MatSelectModule} from '@angular/material/select'; import {MatInputModule} from '@angular/material/input'; import {MatRadioModule} from '@angular/material/radio'; -import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatSelectModule} from '@angular/material/select'; +import {map} from 'rxjs/operators'; /** @title Form field with label */ @Component({ @@ -23,18 +25,18 @@ import {MatCheckboxModule} from '@angular/material/checkbox'; MatSelectModule, MatIconModule, ], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FormFieldLabelExample { - hideRequiredControl = new FormControl(false); - floatLabelControl = new FormControl('auto' as FloatLabelType); - options = this._formBuilder.group({ + readonly hideRequiredControl = new FormControl(false); + readonly floatLabelControl = new FormControl('auto' as FloatLabelType); + readonly options = inject(FormBuilder).group({ hideRequired: this.hideRequiredControl, floatLabel: this.floatLabelControl, }); - - constructor(private _formBuilder: FormBuilder) {} - - getFloatLabelValue(): FloatLabelType { - return this.floatLabelControl.value || 'auto'; - } + protected readonly hideRequired = toSignal(this.hideRequiredControl.valueChanges); + protected readonly floatLabel = toSignal( + this.floatLabelControl.valueChanges.pipe(map(v => v || 'auto')), + {initialValue: 'auto'}, + ); } diff --git a/src/components-examples/material/form-field/form-field-overview/form-field-overview-example.ts b/src/components-examples/material/form-field/form-field-overview/form-field-overview-example.ts index c5a820960ab9..c1d4ffe7a63a 100644 --- a/src/components-examples/material/form-field/form-field-overview/form-field-overview-example.ts +++ b/src/components-examples/material/form-field/form-field-overview/form-field-overview-example.ts @@ -1,4 +1,4 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; import {MatSelectModule} from '@angular/material/select'; import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; @@ -10,5 +10,6 @@ import {MatFormFieldModule} from '@angular/material/form-field'; styleUrl: 'form-field-overview-example.css', standalone: true, imports: [MatFormFieldModule, MatInputModule, MatSelectModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FormFieldOverviewExample {} diff --git a/src/components-examples/material/form-field/form-field-prefix-suffix/form-field-prefix-suffix-example.html b/src/components-examples/material/form-field/form-field-prefix-suffix/form-field-prefix-suffix-example.html index dd514eb6ba49..3bf388ecc9b2 100644 --- a/src/components-examples/material/form-field/form-field-prefix-suffix/form-field-prefix-suffix-example.html +++ b/src/components-examples/material/form-field/form-field-prefix-suffix/form-field-prefix-suffix-example.html @@ -1,15 +1,21 @@
    Enter your password - - Amount - + .00 diff --git a/src/components-examples/material/form-field/form-field-prefix-suffix/form-field-prefix-suffix-example.ts b/src/components-examples/material/form-field/form-field-prefix-suffix/form-field-prefix-suffix-example.ts index ca65f80ffd12..98f37a5ad0a9 100644 --- a/src/components-examples/material/form-field/form-field-prefix-suffix/form-field-prefix-suffix-example.ts +++ b/src/components-examples/material/form-field/form-field-prefix-suffix/form-field-prefix-suffix-example.ts @@ -1,8 +1,8 @@ -import {Component} from '@angular/core'; -import {MatIconModule} from '@angular/material/icon'; +import {ChangeDetectionStrategy, Component, signal} from '@angular/core'; import {MatButtonModule} from '@angular/material/button'; -import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatIconModule} from '@angular/material/icon'; +import {MatInputModule} from '@angular/material/input'; /** @title Form field with prefix & suffix */ @Component({ @@ -11,11 +11,12 @@ import {MatFormFieldModule} from '@angular/material/form-field'; styleUrl: 'form-field-prefix-suffix-example.css', standalone: true, imports: [MatFormFieldModule, MatInputModule, MatButtonModule, MatIconModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FormFieldPrefixSuffixExample { - hide = true; + hide = signal(true); clickEvent(event: MouseEvent) { - this.hide = !this.hide; + this.hide.set(!this.hide); event.stopPropagation(); } } diff --git a/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.css b/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.css deleted file mode 100644 index d98172fcc0a3..000000000000 --- a/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.css +++ /dev/null @@ -1,3 +0,0 @@ -.example-container mat-form-field + mat-form-field { - margin-left: 8px; -} diff --git a/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.html b/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.html deleted file mode 100644 index 6b455564f846..000000000000 --- a/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.html +++ /dev/null @@ -1,8 +0,0 @@ - - Color - - Primary - Accent - Warn - - diff --git a/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.ts b/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.ts deleted file mode 100644 index 5f21cc74fef9..000000000000 --- a/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {Component} from '@angular/core'; -import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {ThemePalette} from '@angular/material/core'; -import {MatSelectModule} from '@angular/material/select'; -import {MatFormFieldModule} from '@angular/material/form-field'; - -/** @title Form field theming */ -@Component({ - selector: 'form-field-theming-example', - templateUrl: 'form-field-theming-example.html', - styleUrl: 'form-field-theming-example.css', - standalone: true, - imports: [MatFormFieldModule, MatSelectModule, FormsModule, ReactiveFormsModule], -}) -export class FormFieldThemingExample { - colorControl = new FormControl('primary' as ThemePalette); -} diff --git a/src/components-examples/material/form-field/index.ts b/src/components-examples/material/form-field/index.ts index 4e22bcbb0597..99b315986fb7 100644 --- a/src/components-examples/material/form-field/index.ts +++ b/src/components-examples/material/form-field/index.ts @@ -4,9 +4,8 @@ export { MyTelInput, } from './form-field-custom-control/form-field-custom-control-example'; export {FormFieldErrorExample} from './form-field-error/form-field-error-example'; +export {FormFieldHarnessExample} from './form-field-harness/form-field-harness-example'; export {FormFieldHintExample} from './form-field-hint/form-field-hint-example'; export {FormFieldLabelExample} from './form-field-label/form-field-label-example'; export {FormFieldOverviewExample} from './form-field-overview/form-field-overview-example'; export {FormFieldPrefixSuffixExample} from './form-field-prefix-suffix/form-field-prefix-suffix-example'; -export {FormFieldThemingExample} from './form-field-theming/form-field-theming-example'; -export {FormFieldHarnessExample} from './form-field-harness/form-field-harness-example'; diff --git a/src/material/form-field/form-field.md b/src/material/form-field/form-field.md index a393f128eb24..83d0b2b2f45e 100644 --- a/src/material/form-field/form-field.md +++ b/src/material/form-field/form-field.md @@ -7,14 +7,16 @@ In this document, "form field" refers to the wrapper component ` (e.g. the input, textarea, select, etc.) The following Angular Material components are designed to work inside a ``: -* [`` & `