Skip to content

Commit

Permalink
feat(directionality): a provider to get directionality (#4044)
Browse files Browse the repository at this point in the history
- Looks at the `html` and `body` elements for `dir` attribute and sets the Directionality service value to it
- Whenever someone would try to inject Directionality - if there's a Dir directive up the dom tree it would be provided

fixes #3600
  • Loading branch information
EladBezalel authored and jelbourn committed Jun 19, 2017
1 parent c220bb5 commit 61d979e
Show file tree
Hide file tree
Showing 33 changed files with 361 additions and 148 deletions.
5 changes: 3 additions & 2 deletions src/lib/autocomplete/autocomplete-trigger.ts
Expand Up @@ -28,7 +28,7 @@ import {ConnectedPositionStrategy} from '../core/overlay/position/connected-posi
import {Observable} from 'rxjs/Observable';
import {MdOptionSelectionChange, MdOption} from '../core/option/option';
import {ENTER, UP_ARROW, DOWN_ARROW, ESCAPE} from '../core/keyboard/keycodes';
import {Dir} from '../core/rtl/dir';
import {Directionality} from '../core/bidi/index';
import {MdInputContainer} from '../input/input-container';
import {Subscription} from 'rxjs/Subscription';
import 'rxjs/add/observable/merge';
Expand Down Expand Up @@ -120,8 +120,9 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {

constructor(private _element: ElementRef, private _overlay: Overlay,
private _viewContainerRef: ViewContainerRef,
private _zone: NgZone,
private _changeDetectorRef: ChangeDetectorRef,
@Optional() private _dir: Dir, private _zone: NgZone,
@Optional() private _dir: Directionality,
@Optional() @Host() private _inputContainer: MdInputContainer,
@Optional() @Inject(DOCUMENT) private _document: any) {}

Expand Down
6 changes: 3 additions & 3 deletions src/lib/autocomplete/autocomplete.spec.ts
Expand Up @@ -19,7 +19,7 @@ import {
} from './index';
import {OverlayContainer} from '../core/overlay/overlay-container';
import {MdInputModule} from '../input/index';
import {Dir, LayoutDirection} from '../core/rtl/dir';
import {Directionality, Direction} from '../core/bidi/index';
import {Subscription} from 'rxjs/Subscription';
import {ENTER, DOWN_ARROW, SPACE, UP_ARROW, ESCAPE} from '../core/keyboard/keycodes';
import {MdOption} from '../core/option/option';
Expand All @@ -35,7 +35,7 @@ import 'rxjs/add/operator/map';

describe('MdAutocomplete', () => {
let overlayContainerElement: HTMLElement;
let dir: LayoutDirection;
let dir: Direction;
let scrolledSubject = new Subject();

beforeEach(async(() => {
Expand Down Expand Up @@ -70,7 +70,7 @@ describe('MdAutocomplete', () => {

return {getContainerElement: () => overlayContainerElement};
}},
{provide: Dir, useFactory: () => ({value: dir})},
{provide: Directionality, useFactory: () => ({value: dir})},
{provide: ScrollDispatcher, useFactory: () => {
return {scrolled: (_delay: number, callback: () => any) => {
return scrolledSubject.asObservable().subscribe(callback);
Expand Down
66 changes: 66 additions & 0 deletions src/lib/core/bidi/dir.ts
@@ -0,0 +1,66 @@
/**
* @license
* Copyright Google Inc. 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 {
Directive,
HostBinding,
Output,
Input,
EventEmitter
} from '@angular/core';

import {Direction, Directionality} from './directionality';

/**
* Directive to listen for changes of direction of part of the DOM.
*
* Would provide itself in case a component looks for the Directionality service
*/
@Directive({
selector: '[dir]',
// TODO(hansl): maybe `$implicit` isn't the best option here, but for now that's the best we got.
exportAs: '$implicit',
providers: [
{provide: Directionality, useExisting: Dir}
]
})
export class Dir implements Directionality {
/** Layout direction of the element. */
_dir: Direction = 'ltr';

/** Whether the `value` has been set to its initial value. */
private _isInitialized: boolean = false;

/** Event emitted when the direction changes. */
@Output('dirChange') change = new EventEmitter<void>();

/** @docs-private */
@HostBinding('attr.dir')
@Input('dir')
get dir(): Direction {
return this._dir;
}

set dir(v: Direction) {
let old = this._dir;
this._dir = v;
if (old !== this._dir && this._isInitialized) {
this.change.emit();
}
}

/** Current layout direction of the element. */
get value(): Direction { return this.dir; }
set value(v: Direction) { this.dir = v; }

/** Initialize once default value has been set. */
ngAfterContentInit() {
this._isInitialized = true;
}
}

104 changes: 104 additions & 0 deletions src/lib/core/bidi/directionality.spec.ts
@@ -0,0 +1,104 @@
import {async, fakeAsync, TestBed, tick} from '@angular/core/testing';
import {Component} from '@angular/core';
import {By} from '@angular/platform-browser';
import {BidiModule, Directionality, DIR_DOCUMENT} from './index';

describe('Directionality', () => {
let fakeDocument: FakeDocument;

beforeEach(async(() => {
fakeDocument = {body: {}, documentElement: {}};

TestBed.configureTestingModule({
imports: [BidiModule],
declarations: [ElementWithDir, InjectsDirectionality],
providers: [{provide: DIR_DOCUMENT, useFactory: () => fakeDocument}],
}).compileComponents();
}));

describe('Service', () => {
it('should read dir from the html element if not specified on the body', () => {
fakeDocument.documentElement.dir = 'rtl';

let fixture = TestBed.createComponent(InjectsDirectionality);
let testComponent = fixture.debugElement.componentInstance;

expect(testComponent.dir.value).toBe('rtl');
});

it('should read dir from the body even it is also specified on the html element', () => {
fakeDocument.documentElement.dir = 'ltr';
fakeDocument.body.dir = 'rtl';

let fixture = TestBed.createComponent(InjectsDirectionality);
let testComponent = fixture.debugElement.componentInstance;

expect(testComponent.dir.value).toBe('rtl');
});

it('should default to ltr if nothing is specified on either body or the html element', () => {
let fixture = TestBed.createComponent(InjectsDirectionality);
let testComponent = fixture.debugElement.componentInstance;

expect(testComponent.dir.value).toBe('ltr');
});
});

describe('Dir directive', () => {
it('should provide itself as Directionality', () => {
let fixture = TestBed.createComponent(ElementWithDir);
const injectedDirectionality =
fixture.debugElement.query(By.directive(InjectsDirectionality)).componentInstance.dir;

fixture.detectChanges();

expect(injectedDirectionality.value).toBe('rtl');
});

it('should emit a change event when the value changes', fakeAsync(() => {
let fixture = TestBed.createComponent(ElementWithDir);
const injectedDirectionality =
fixture.debugElement.query(By.directive(InjectsDirectionality)).componentInstance.dir;

fixture.detectChanges();

expect(injectedDirectionality.value).toBe('rtl');
expect(fixture.componentInstance.changeCount).toBe(0);

fixture.componentInstance.direction = 'ltr';

fixture.detectChanges();
tick();

expect(injectedDirectionality.value).toBe('ltr');
expect(fixture.componentInstance.changeCount).toBe(1);
}));
});
});


@Component({
template: `
<div [dir]="direction" (dirChange)="changeCount= changeCount + 1">
<injects-directionality></injects-directionality>
</div>
`
})
class ElementWithDir {
direction = 'rtl';
changeCount = 0;
}

/** Test component with Dir directive. */
@Component({
selector: 'injects-directionality',
template: `<div></div>`
})
class InjectsDirectionality {
constructor(public dir: Directionality) { }
}

interface FakeDocument {
documentElement: {dir?: string};
body: {dir?: string};
}
64 changes: 64 additions & 0 deletions src/lib/core/bidi/directionality.ts
@@ -0,0 +1,64 @@
/**
* @license
* Copyright Google Inc. 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 {
EventEmitter,
Injectable,
Optional,
SkipSelf,
Inject,
InjectionToken,
} from '@angular/core';
import {DOCUMENT} from '@angular/platform-browser';


export type Direction = 'ltr' | 'rtl';

/**
* Injection token used to inject the document into Directionality.
* This is used so that the value can be faked in tests.
*
* We can't use the real document in tests because changing the real `dir` causes geometry-based
* tests in Safari to fail.
*
* We also can't re-provide the DOCUMENT token from platform-brower because the unit tests
* themselves use things like `querySelector` in test code.
*/
export const DIR_DOCUMENT = new InjectionToken<Document>('md-dir-doc');

/**
* The directionality (LTR / RTL) context for the application (or a subtree of it).
* Exposes the current direction and a stream of direction changes.
*/
@Injectable()
export class Directionality {
value: Direction = 'ltr';
change = new EventEmitter<void>();

constructor(@Optional() @Inject(DIR_DOCUMENT) _document?: any) {
if (typeof _document === 'object' && !!_document) {
// TODO: handle 'auto' value -
// We still need to account for dir="auto".
// It looks like HTMLElemenet.dir is also "auto" when that's set to the attribute,
// but getComputedStyle return either "ltr" or "rtl". avoiding getComputedStyle for now
// though, we're already calling it for the theming check.
this.value = (_document.body.dir || _document.documentElement.dir || 'ltr') as Direction;
}
}
}

export function DIRECTIONALITY_PROVIDER_FACTORY(parentDirectionality, _document) {
return parentDirectionality || new Directionality(_document);
}

export const DIRECTIONALITY_PROVIDER = {
// If there is already a Directionality available, use that. Otherwise, provide a new one.
provide: Directionality,
deps: [[new Optional(), new SkipSelf(), Directionality], [new Optional(), DOCUMENT]],
useFactory: DIRECTIONALITY_PROVIDER_FACTORY
};
31 changes: 31 additions & 0 deletions src/lib/core/bidi/index.ts
@@ -0,0 +1,31 @@
/**
* @license
* Copyright Google Inc. 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 {NgModule} from '@angular/core';
import {DOCUMENT} from '@angular/platform-browser';
import {Dir} from './dir';
import {DIR_DOCUMENT, Directionality, DIRECTIONALITY_PROVIDER} from './directionality';


export {
Directionality,
DIRECTIONALITY_PROVIDER,
DIR_DOCUMENT,
Direction,
} from './directionality';
export {Dir} from './dir';

@NgModule({
exports: [Dir],
declarations: [Dir],
providers: [
{provide: DIR_DOCUMENT, useExisting: DOCUMENT},
Directionality,
]
})
export class BidiModule { }
5 changes: 3 additions & 2 deletions src/lib/core/common-behaviors/common-module.ts
Expand Up @@ -9,6 +9,7 @@
import {NgModule, InjectionToken, Optional, Inject, isDevMode} from '@angular/core';
import {DOCUMENT} from '@angular/platform-browser';
import {CompatibilityModule} from '../compatibility/compatibility';
import {BidiModule} from '../bidi/index';


/** Injection token that configures whether the Material sanity checks are enabled. */
Expand All @@ -22,8 +23,8 @@ export const MATERIAL_SANITY_CHECKS = new InjectionToken<boolean>('md-sanity-che
* This module should be imported to each top-level component module (e.g., MdTabsModule).
*/
@NgModule({
imports: [CompatibilityModule],
exports: [CompatibilityModule],
imports: [CompatibilityModule, BidiModule],
exports: [CompatibilityModule, BidiModule],
providers: [{
provide: MATERIAL_SANITY_CHECKS, useValue: true,
}],
Expand Down
8 changes: 4 additions & 4 deletions src/lib/core/core.ts
Expand Up @@ -8,7 +8,7 @@

import {NgModule} from '@angular/core';
import {MdLineModule} from './line/line';
import {RtlModule} from './rtl/dir';
import {BidiModule} from './bidi/index';
import {ObserveContentModule} from './observe-content/observe-content';
import {MdOptionModule} from './option/index';
import {PortalModule} from './portal/portal-directives';
Expand All @@ -19,7 +19,7 @@ import {MdRippleModule} from './ripple/index';


// RTL
export {Dir, LayoutDirection, RtlModule} from './rtl/dir';
export {Dir, Direction, Directionality, BidiModule} from './bidi/index';

// Mutation Observer
export {ObserveContentModule, ObserveContent} from './observe-content/observe-content';
Expand Down Expand Up @@ -121,7 +121,7 @@ export {
@NgModule({
imports: [
MdLineModule,
RtlModule,
BidiModule,
MdRippleModule,
ObserveContentModule,
PortalModule,
Expand All @@ -132,7 +132,7 @@ export {
],
exports: [
MdLineModule,
RtlModule,
BidiModule,
MdRippleModule,
ObserveContentModule,
PortalModule,
Expand Down
4 changes: 2 additions & 2 deletions src/lib/core/overlay/overlay-directives.spec.ts
Expand Up @@ -5,7 +5,7 @@ import {ConnectedOverlayDirective, OverlayModule, OverlayOrigin} from './overlay
import {OverlayContainer} from './overlay-container';
import {ConnectedPositionStrategy} from './position/connected-position-strategy';
import {ConnectedOverlayPositionChange} from './position/connected-position';
import {Dir} from '../rtl/dir';
import {Directionality} from '../bidi/index';
import {dispatchKeyboardEvent} from '../testing/dispatch-events';
import {ESCAPE} from '../keyboard/keycodes';

Expand All @@ -24,7 +24,7 @@ describe('Overlay directives', () => {
overlayContainerElement = document.createElement('div');
return {getContainerElement: () => overlayContainerElement};
}},
{provide: Dir, useFactory: () => {
{provide: Directionality, useFactory: () => {
return dir = { value: 'ltr' };
}}
],
Expand Down

0 comments on commit 61d979e

Please sign in to comment.