From c8a6e6c383f542a1bceaaa02742675ab35b6576f Mon Sep 17 00:00:00 2001 From: Elad Bezalel Date: Tue, 20 Dec 2016 23:57:25 +0200 Subject: [PATCH] fix(tabs): observing tab header label changes to recalculate width - Uses observe-changes directive that emit an event when the mutation observer notifies a change fixes #2155 --- src/lib/core/core.ts | 6 ++ .../observe-content/observe-content.spec.ts | 68 +++++++++++++++++++ .../core/observe-content/observe-content.ts | 50 ++++++++++++++ src/lib/module.ts | 3 + src/lib/tabs/tab-group.ts | 3 +- src/lib/tabs/tab-header.html | 2 +- src/lib/tabs/tab-header.ts | 13 +++- 7 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 src/lib/core/observe-content/observe-content.spec.ts create mode 100644 src/lib/core/observe-content/observe-content.ts diff --git a/src/lib/core/core.ts b/src/lib/core/core.ts index 686bfc319a5b..c8c20f9b6228 100644 --- a/src/lib/core/core.ts +++ b/src/lib/core/core.ts @@ -1,6 +1,7 @@ import {NgModule, ModuleWithProviders} from '@angular/core'; import {MdLineModule} from './line/line'; import {RtlModule} from './rtl/dir'; +import {ObserveContentModule} from './observe-content/observe-content'; import {MdRippleModule} from './ripple/ripple'; import {PortalModule} from './portal/portal-directives'; import {OverlayModule} from './overlay/overlay-directives'; @@ -11,6 +12,9 @@ import {OVERLAY_PROVIDERS} from './overlay/overlay'; // RTL export {Dir, LayoutDirection, RtlModule} from './rtl/dir'; +// Mutation Observer +export {ObserveContentModule, ObserveContent} from './observe-content/observe-content'; + // Portals export { Portal, @@ -115,6 +119,7 @@ export {NoConflictStyleCompatibilityMode} from './compatibility/no-conflict-mode MdLineModule, RtlModule, MdRippleModule, + ObserveContentModule, PortalModule, OverlayModule, A11yModule, @@ -123,6 +128,7 @@ export {NoConflictStyleCompatibilityMode} from './compatibility/no-conflict-mode MdLineModule, RtlModule, MdRippleModule, + ObserveContentModule, PortalModule, OverlayModule, A11yModule, diff --git a/src/lib/core/observe-content/observe-content.spec.ts b/src/lib/core/observe-content/observe-content.spec.ts new file mode 100644 index 000000000000..888a37391a0e --- /dev/null +++ b/src/lib/core/observe-content/observe-content.spec.ts @@ -0,0 +1,68 @@ +import {Component} from '@angular/core'; +import {async, TestBed} from '@angular/core/testing'; +import {ObserveContentModule} from './observe-content'; + +/** + * TODO(elad): `ProxyZone` doesn't seem to capture the events raised by + * `MutationObserver` and needs to be investigated + */ + +describe('Observe content', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ObserveContentModule], + declarations: [ComponentWithTextContent, ComponentWithChildTextContent], + }); + + TestBed.compileComponents(); + })); + + describe('text content change', () => { + it('should call the registered for changes function', done => { + let fixture = TestBed.createComponent(ComponentWithTextContent); + fixture.detectChanges(); + + // If the hint label is empty, expect no label. + const spy = spyOn(fixture.componentInstance, 'doSomething').and.callFake(() => { + expect(spy.calls.any()).toBe(true); + done(); + }); + + expect(spy.calls.any()).toBe(false); + + fixture.componentInstance.text = 'text'; + fixture.detectChanges(); + }); + }); + + describe('child text content change', () => { + it('should call the registered for changes function', done => { + let fixture = TestBed.createComponent(ComponentWithChildTextContent); + fixture.detectChanges(); + + // If the hint label is empty, expect no label. + const spy = spyOn(fixture.componentInstance, 'doSomething').and.callFake(() => { + expect(spy.calls.any()).toBe(true); + done(); + }); + + expect(spy.calls.any()).toBe(false); + + fixture.componentInstance.text = 'text'; + fixture.detectChanges(); + }); + }); +}); + + +@Component({ template: `
{{text}}
` }) +class ComponentWithTextContent { + text = ''; + doSomething() {} +} + +@Component({ template: `
{{text}}
` }) +class ComponentWithChildTextContent { + text = ''; + doSomething() {} +} diff --git a/src/lib/core/observe-content/observe-content.ts b/src/lib/core/observe-content/observe-content.ts new file mode 100644 index 000000000000..23a86b496e29 --- /dev/null +++ b/src/lib/core/observe-content/observe-content.ts @@ -0,0 +1,50 @@ +import { + Directive, + ElementRef, + NgModule, + ModuleWithProviders, + Output, + EventEmitter, + OnDestroy, + AfterContentInit +} from '@angular/core'; + +@Directive({ + selector: '[cdkObserveContent]' +}) +export class ObserveContent implements AfterContentInit, OnDestroy { + private _observer: MutationObserver; + + @Output('cdkObserveContent') event = new EventEmitter(); + + constructor(private _elementRef: ElementRef) {} + + ngAfterContentInit() { + this._observer = new MutationObserver(mutations => mutations.forEach(() => this.event.emit())); + + this._observer.observe(this._elementRef.nativeElement, { + characterData: true, + childList: true, + subtree: true + }); + } + + ngOnDestroy() { + if (this._observer) { + this._observer.disconnect(); + } + } +} + +@NgModule({ + exports: [ObserveContent], + declarations: [ObserveContent] +}) +export class ObserveContentModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: ObserveContentModule, + providers: [] + }; + } +} diff --git a/src/lib/module.ts b/src/lib/module.ts index c74a5bcf61a5..2847cde70f9e 100644 --- a/src/lib/module.ts +++ b/src/lib/module.ts @@ -3,6 +3,7 @@ import {NgModule, ModuleWithProviders} from '@angular/core'; import { MdRippleModule, RtlModule, + ObserveContentModule, PortalModule, OverlayModule, A11yModule, @@ -67,6 +68,7 @@ const MATERIAL_MODULES = [ PlatformModule, ProjectionModule, DefaultStyleCompatibilityModeModule, + ObserveContentModule ]; @NgModule({ @@ -89,6 +91,7 @@ const MATERIAL_MODULES = [ PortalModule.forRoot(), ProjectionModule.forRoot(), RtlModule.forRoot(), + ObserveContentModule.forRoot(), // These modules include providers. A11yModule.forRoot(), diff --git a/src/lib/tabs/tab-group.ts b/src/lib/tabs/tab-group.ts index 91f919b4450d..bd887d2c5254 100644 --- a/src/lib/tabs/tab-group.ts +++ b/src/lib/tabs/tab-group.ts @@ -23,6 +23,7 @@ import {MdInkBar} from './ink-bar'; import {Observable} from 'rxjs/Observable'; import 'rxjs/add/operator/map'; import {MdRippleModule} from '../core/ripple/ripple'; +import {ObserveContentModule} from '../core/observe-content/observe-content'; import {MdTab} from './tab'; import {MdTabBody} from './tab-body'; import {ViewportRuler} from '../core/overlay/position/viewport-ruler'; @@ -194,7 +195,7 @@ export class MdTabGroup { } @NgModule({ - imports: [CommonModule, PortalModule, MdRippleModule], + imports: [CommonModule, PortalModule, MdRippleModule, ObserveContentModule], // Don't export all components because some are only to be used internally. exports: [MdTabGroup, MdTabLabel, MdTab, MdTabNavBar, MdTabLink, MdTabLinkRipple], declarations: [MdTabGroup, MdTabLabel, MdTab, MdInkBar, MdTabLabelWrapper, diff --git a/src/lib/tabs/tab-header.html b/src/lib/tabs/tab-header.html index 238c98f7d8e1..ad18fb49bbad 100644 --- a/src/lib/tabs/tab-header.html +++ b/src/lib/tabs/tab-header.html @@ -8,7 +8,7 @@
-
+
diff --git a/src/lib/tabs/tab-header.ts b/src/lib/tabs/tab-header.ts index 1eea13700a66..29f231ba10ca 100644 --- a/src/lib/tabs/tab-header.ts +++ b/src/lib/tabs/tab-header.ts @@ -101,9 +101,7 @@ export class MdTabHeader { ngAfterContentChecked(): void { // If the number of tab labels have changed, check if scrolling should be enabled if (this._tabLabelCount != this._labelWrappers.length) { - this._checkPaginationEnabled(); - this._checkScrollingControls(); - this._updateTabScrollPosition(); + this._updatePagination(); this._tabLabelCount = this._labelWrappers.length; } @@ -150,6 +148,15 @@ export class MdTabHeader { } } + /** + * Updating the view whether pagination should be enabled or not + */ + _updatePagination() { + this._checkPaginationEnabled(); + this._checkScrollingControls(); + this._updateTabScrollPosition(); + } + /** When the focus index is set, we must manually send focus to the correct label */ set focusIndex(value: number) { if (!this._isValidIndex(value) || this._focusIndex == value) { return; }