Skip to content

Commit

Permalink
feat(directionality): a provider to get the overall directionality
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 angular#3600
  • Loading branch information
EladBezalel committed Jun 13, 2017
1 parent 90e6d3c commit ac874fe
Show file tree
Hide file tree
Showing 33 changed files with 333 additions and 139 deletions.
5 changes: 3 additions & 2 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,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 @@ -112,8 +112,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
Original file line number Diff line number Diff line change
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
58 changes: 58 additions & 0 deletions src/lib/core/bidi/dir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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;
}
}

114 changes: 114 additions & 0 deletions src/lib/core/bidi/directionality.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import {async, fakeAsync, TestBed, tick} from '@angular/core/testing';
import {Component, getDebugNode} from '@angular/core';
import {By} from '@angular/platform-browser';
import {Directionality, BidiModule} from './index';

describe('Directionality', () => {
let documentElementDir, bodyDir;

beforeAll(() => {
documentElementDir = document.documentElement.dir;
bodyDir = document.body.dir;
});

afterAll(() => {
document.documentElement.dir = documentElementDir;
document.body.dir = bodyDir;
});

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [BidiModule],
declarations: [ElementWithDir, InjectsDirectionality]
}).compileComponents();

clearDocumentDirAttributes();
}));

describe('Service', () => {
it('should read dir from the html element if not specified on the body', () => {
document.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', () => {
document.documentElement.dir = 'ltr';
document.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);
}));
});
});


function clearDocumentDirAttributes() {
document.documentElement.dir = '';
document.body.dir = '';
}

@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) {
}
}
40 changes: 40 additions & 0 deletions src/lib/core/bidi/directionality.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
EventEmitter,
Injectable,
Optional,
SkipSelf
} from '@angular/core';

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

/**
* 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';
public change = new EventEmitter<void>();

constructor() {
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) {
return parentDirectionality || new Directionality();
}

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]],
useFactory: DIRECTIONALITY_PROVIDER_FACTORY
};
25 changes: 25 additions & 0 deletions src/lib/core/bidi/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {ModuleWithProviders, NgModule} from '@angular/core';
import {Dir} from './dir';
import {Directionality, DIRECTIONALITY_PROVIDER} from './directionality';

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

@NgModule({
exports: [Dir],
declarations: [Dir],
providers: [Directionality]
})
export class BidiModule {
/** @deprecated */
static forRoot(): ModuleWithProviders {
return {
ngModule: BidiModule,
providers: [DIRECTIONALITY_PROVIDER]
};
}
}
5 changes: 3 additions & 2 deletions src/lib/core/common-behaviors/common-module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,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 @@ -14,8 +15,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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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 @@ -11,7 +11,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 @@ -113,7 +113,7 @@ export {
@NgModule({
imports: [
MdLineModule,
RtlModule,
BidiModule,
MdRippleModule,
ObserveContentModule,
PortalModule,
Expand All @@ -124,7 +124,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
Original file line number Diff line number Diff line change
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
6 changes: 3 additions & 3 deletions src/lib/core/overlay/overlay-directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
} from './position/connected-position';
import {PortalModule} from '../portal/portal-directives';
import {ConnectedPositionStrategy} from './position/connected-position-strategy';
import {Dir, LayoutDirection} from '../rtl/dir';
import {Directionality, Direction} from '../bidi/index';
import {Scrollable} from './scroll/scrollable';
import {ScrollStrategy} from './scroll/scroll-strategy';
import {coerceBooleanProperty} from '../coercion/boolean-property';
Expand Down Expand Up @@ -157,7 +157,7 @@ export class ConnectedOverlayDirective implements OnDestroy, OnChanges {
private _renderer: Renderer2,
templateRef: TemplateRef<any>,
viewContainerRef: ViewContainerRef,
@Optional() private _dir: Dir) {
@Optional() private _dir: Directionality) {
this._templatePortal = new TemplatePortal(templateRef, viewContainerRef);
}

Expand All @@ -167,7 +167,7 @@ export class ConnectedOverlayDirective implements OnDestroy, OnChanges {
}

/** The element's layout direction. */
get dir(): LayoutDirection {
get dir(): Direction {
return this._dir ? this._dir.value : 'ltr';
}

Expand Down
Loading

0 comments on commit ac874fe

Please sign in to comment.