Skip to content

Commit 5f8c663

Browse files
EladBezaleljelbourn
authored andcommitted
feat(directionality): a provider to get the overall directionality
- 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
1 parent 0aaeb69 commit 5f8c663

33 files changed

+361
-148
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {ConnectedPositionStrategy} from '../core/overlay/position/connected-posi
2828
import {Observable} from 'rxjs/Observable';
2929
import {MdOptionSelectionChange, MdOption} from '../core/option/option';
3030
import {ENTER, UP_ARROW, DOWN_ARROW, ESCAPE} from '../core/keyboard/keycodes';
31-
import {Dir} from '../core/rtl/dir';
31+
import {Directionality} from '../core/bidi/index';
3232
import {MdInputContainer} from '../input/input-container';
3333
import {Subscription} from 'rxjs/Subscription';
3434
import 'rxjs/add/observable/merge';
@@ -120,8 +120,9 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
120120

121121
constructor(private _element: ElementRef, private _overlay: Overlay,
122122
private _viewContainerRef: ViewContainerRef,
123+
private _zone: NgZone,
123124
private _changeDetectorRef: ChangeDetectorRef,
124-
@Optional() private _dir: Dir, private _zone: NgZone,
125+
@Optional() private _dir: Directionality,
125126
@Optional() @Host() private _inputContainer: MdInputContainer,
126127
@Optional() @Inject(DOCUMENT) private _document: any) {}
127128

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
} from './index';
2020
import {OverlayContainer} from '../core/overlay/overlay-container';
2121
import {MdInputModule} from '../input/index';
22-
import {Dir, LayoutDirection} from '../core/rtl/dir';
22+
import {Directionality, Direction} from '../core/bidi/index';
2323
import {Subscription} from 'rxjs/Subscription';
2424
import {ENTER, DOWN_ARROW, SPACE, UP_ARROW, ESCAPE} from '../core/keyboard/keycodes';
2525
import {MdOption} from '../core/option/option';
@@ -35,7 +35,7 @@ import 'rxjs/add/operator/map';
3535

3636
describe('MdAutocomplete', () => {
3737
let overlayContainerElement: HTMLElement;
38-
let dir: LayoutDirection;
38+
let dir: Direction;
3939
let scrolledSubject = new Subject();
4040

4141
beforeEach(async(() => {
@@ -70,7 +70,7 @@ describe('MdAutocomplete', () => {
7070

7171
return {getContainerElement: () => overlayContainerElement};
7272
}},
73-
{provide: Dir, useFactory: () => ({value: dir})},
73+
{provide: Directionality, useFactory: () => ({value: dir})},
7474
{provide: ScrollDispatcher, useFactory: () => {
7575
return {scrolled: (_delay: number, callback: () => any) => {
7676
return scrolledSubject.asObservable().subscribe(callback);

src/lib/core/bidi/dir.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {
10+
Directive,
11+
HostBinding,
12+
Output,
13+
Input,
14+
EventEmitter
15+
} from '@angular/core';
16+
17+
import {Direction, Directionality} from './directionality';
18+
19+
/**
20+
* Directive to listen for changes of direction of part of the DOM.
21+
*
22+
* Would provide itself in case a component looks for the Directionality service
23+
*/
24+
@Directive({
25+
selector: '[dir]',
26+
// TODO(hansl): maybe `$implicit` isn't the best option here, but for now that's the best we got.
27+
exportAs: '$implicit',
28+
providers: [
29+
{provide: Directionality, useExisting: Dir}
30+
]
31+
})
32+
export class Dir implements Directionality {
33+
/** Layout direction of the element. */
34+
_dir: Direction = 'ltr';
35+
36+
/** Whether the `value` has been set to its initial value. */
37+
private _isInitialized: boolean = false;
38+
39+
/** Event emitted when the direction changes. */
40+
@Output('dirChange') change = new EventEmitter<void>();
41+
42+
/** @docs-private */
43+
@HostBinding('attr.dir')
44+
@Input('dir')
45+
get dir(): Direction {
46+
return this._dir;
47+
}
48+
49+
set dir(v: Direction) {
50+
let old = this._dir;
51+
this._dir = v;
52+
if (old !== this._dir && this._isInitialized) {
53+
this.change.emit();
54+
}
55+
}
56+
57+
/** Current layout direction of the element. */
58+
get value(): Direction { return this.dir; }
59+
set value(v: Direction) { this.dir = v; }
60+
61+
/** Initialize once default value has been set. */
62+
ngAfterContentInit() {
63+
this._isInitialized = true;
64+
}
65+
}
66+
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import {async, fakeAsync, TestBed, tick} from '@angular/core/testing';
2+
import {Component} from '@angular/core';
3+
import {By} from '@angular/platform-browser';
4+
import {BidiModule, Directionality, DIR_DOCUMENT} from './index';
5+
6+
describe('Directionality', () => {
7+
let fakeDocument: FakeDocument;
8+
9+
beforeEach(async(() => {
10+
fakeDocument = {body: {}, documentElement: {}};
11+
12+
TestBed.configureTestingModule({
13+
imports: [BidiModule],
14+
declarations: [ElementWithDir, InjectsDirectionality],
15+
providers: [{provide: DIR_DOCUMENT, useFactory: () => fakeDocument}],
16+
}).compileComponents();
17+
}));
18+
19+
describe('Service', () => {
20+
it('should read dir from the html element if not specified on the body', () => {
21+
fakeDocument.documentElement.dir = 'rtl';
22+
23+
let fixture = TestBed.createComponent(InjectsDirectionality);
24+
let testComponent = fixture.debugElement.componentInstance;
25+
26+
expect(testComponent.dir.value).toBe('rtl');
27+
});
28+
29+
it('should read dir from the body even it is also specified on the html element', () => {
30+
fakeDocument.documentElement.dir = 'ltr';
31+
fakeDocument.body.dir = 'rtl';
32+
33+
let fixture = TestBed.createComponent(InjectsDirectionality);
34+
let testComponent = fixture.debugElement.componentInstance;
35+
36+
expect(testComponent.dir.value).toBe('rtl');
37+
});
38+
39+
it('should default to ltr if nothing is specified on either body or the html element', () => {
40+
let fixture = TestBed.createComponent(InjectsDirectionality);
41+
let testComponent = fixture.debugElement.componentInstance;
42+
43+
expect(testComponent.dir.value).toBe('ltr');
44+
});
45+
});
46+
47+
describe('Dir directive', () => {
48+
it('should provide itself as Directionality', () => {
49+
let fixture = TestBed.createComponent(ElementWithDir);
50+
const injectedDirectionality =
51+
fixture.debugElement.query(By.directive(InjectsDirectionality)).componentInstance.dir;
52+
53+
fixture.detectChanges();
54+
55+
expect(injectedDirectionality.value).toBe('rtl');
56+
});
57+
58+
it('should emit a change event when the value changes', fakeAsync(() => {
59+
let fixture = TestBed.createComponent(ElementWithDir);
60+
const injectedDirectionality =
61+
fixture.debugElement.query(By.directive(InjectsDirectionality)).componentInstance.dir;
62+
63+
fixture.detectChanges();
64+
65+
expect(injectedDirectionality.value).toBe('rtl');
66+
expect(fixture.componentInstance.changeCount).toBe(0);
67+
68+
fixture.componentInstance.direction = 'ltr';
69+
70+
fixture.detectChanges();
71+
tick();
72+
73+
expect(injectedDirectionality.value).toBe('ltr');
74+
expect(fixture.componentInstance.changeCount).toBe(1);
75+
}));
76+
});
77+
});
78+
79+
80+
@Component({
81+
template: `
82+
<div [dir]="direction" (dirChange)="changeCount= changeCount + 1">
83+
<injects-directionality></injects-directionality>
84+
</div>
85+
`
86+
})
87+
class ElementWithDir {
88+
direction = 'rtl';
89+
changeCount = 0;
90+
}
91+
92+
/** Test component with Dir directive. */
93+
@Component({
94+
selector: 'injects-directionality',
95+
template: `<div></div>`
96+
})
97+
class InjectsDirectionality {
98+
constructor(public dir: Directionality) { }
99+
}
100+
101+
interface FakeDocument {
102+
documentElement: {dir?: string};
103+
body: {dir?: string};
104+
}

src/lib/core/bidi/directionality.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {
10+
EventEmitter,
11+
Injectable,
12+
Optional,
13+
SkipSelf,
14+
Inject,
15+
InjectionToken,
16+
} from '@angular/core';
17+
import {DOCUMENT} from '@angular/platform-browser';
18+
19+
20+
export type Direction = 'ltr' | 'rtl';
21+
22+
/**
23+
* Injection token used to inject the document into Directionality.
24+
* This is used so that the value can be faked in tests.
25+
*
26+
* We can't use the real document in tests because changing the real `dir` causes geometry-based
27+
* tests in Safari to fail.
28+
*
29+
* We also can't re-provide the DOCUMENT token from platform-brower because the unit tests
30+
* themselves use things like `querySelector` in test code.
31+
*/
32+
export const DIR_DOCUMENT = new InjectionToken<Document>('md-dir-doc');
33+
34+
/**
35+
* The directionality (LTR / RTL) context for the application (or a subtree of it).
36+
* Exposes the current direction and a stream of direction changes.
37+
*/
38+
@Injectable()
39+
export class Directionality {
40+
value: Direction = 'ltr';
41+
change = new EventEmitter<void>();
42+
43+
constructor(@Optional() @Inject(DIR_DOCUMENT) _document?: any) {
44+
if (typeof _document === 'object' && !!_document) {
45+
// TODO: handle 'auto' value -
46+
// We still need to account for dir="auto".
47+
// It looks like HTMLElemenet.dir is also "auto" when that's set to the attribute,
48+
// but getComputedStyle return either "ltr" or "rtl". avoiding getComputedStyle for now
49+
// though, we're already calling it for the theming check.
50+
this.value = (_document.body.dir || _document.documentElement.dir || 'ltr') as Direction;
51+
}
52+
}
53+
}
54+
55+
export function DIRECTIONALITY_PROVIDER_FACTORY(parentDirectionality, _document) {
56+
return parentDirectionality || new Directionality(_document);
57+
}
58+
59+
export const DIRECTIONALITY_PROVIDER = {
60+
// If there is already a Directionality available, use that. Otherwise, provide a new one.
61+
provide: Directionality,
62+
deps: [[new Optional(), new SkipSelf(), Directionality], [new Optional(), DOCUMENT]],
63+
useFactory: DIRECTIONALITY_PROVIDER_FACTORY
64+
};

src/lib/core/bidi/index.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {NgModule} from '@angular/core';
10+
import {DOCUMENT} from '@angular/platform-browser';
11+
import {Dir} from './dir';
12+
import {DIR_DOCUMENT, Directionality, DIRECTIONALITY_PROVIDER} from './directionality';
13+
14+
15+
export {
16+
Directionality,
17+
DIRECTIONALITY_PROVIDER,
18+
DIR_DOCUMENT,
19+
Direction,
20+
} from './directionality';
21+
export {Dir} from './dir';
22+
23+
@NgModule({
24+
exports: [Dir],
25+
declarations: [Dir],
26+
providers: [
27+
{provide: DIR_DOCUMENT, useExisting: DOCUMENT},
28+
Directionality,
29+
]
30+
})
31+
export class BidiModule { }

src/lib/core/common-behaviors/common-module.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {NgModule, InjectionToken, Optional, Inject, isDevMode} from '@angular/core';
1010
import {DOCUMENT} from '@angular/platform-browser';
1111
import {CompatibilityModule} from '../compatibility/compatibility';
12+
import {BidiModule} from '../bidi/index';
1213

1314

1415
/** Injection token that configures whether the Material sanity checks are enabled. */
@@ -22,8 +23,8 @@ export const MATERIAL_SANITY_CHECKS = new InjectionToken<boolean>('md-sanity-che
2223
* This module should be imported to each top-level component module (e.g., MdTabsModule).
2324
*/
2425
@NgModule({
25-
imports: [CompatibilityModule],
26-
exports: [CompatibilityModule],
26+
imports: [CompatibilityModule, BidiModule],
27+
exports: [CompatibilityModule, BidiModule],
2728
providers: [{
2829
provide: MATERIAL_SANITY_CHECKS, useValue: true,
2930
}],

src/lib/core/core.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {NgModule} from '@angular/core';
1010
import {MdLineModule} from './line/line';
11-
import {RtlModule} from './rtl/dir';
11+
import {BidiModule} from './bidi/index';
1212
import {ObserveContentModule} from './observe-content/observe-content';
1313
import {MdOptionModule} from './option/index';
1414
import {PortalModule} from './portal/portal-directives';
@@ -19,7 +19,7 @@ import {MdRippleModule} from './ripple/index';
1919

2020

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

2424
// Mutation Observer
2525
export {ObserveContentModule, ObserveContent} from './observe-content/observe-content';
@@ -121,7 +121,7 @@ export {
121121
@NgModule({
122122
imports: [
123123
MdLineModule,
124-
RtlModule,
124+
BidiModule,
125125
MdRippleModule,
126126
ObserveContentModule,
127127
PortalModule,
@@ -132,7 +132,7 @@ export {
132132
],
133133
exports: [
134134
MdLineModule,
135-
RtlModule,
135+
BidiModule,
136136
MdRippleModule,
137137
ObserveContentModule,
138138
PortalModule,

src/lib/core/overlay/overlay-directives.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {ConnectedOverlayDirective, OverlayModule, OverlayOrigin} from './overlay
55
import {OverlayContainer} from './overlay-container';
66
import {ConnectedPositionStrategy} from './position/connected-position-strategy';
77
import {ConnectedOverlayPositionChange} from './position/connected-position';
8-
import {Dir} from '../rtl/dir';
8+
import {Directionality} from '../bidi/index';
99
import {dispatchKeyboardEvent} from '../testing/dispatch-events';
1010
import {ESCAPE} from '../keyboard/keycodes';
1111

@@ -24,7 +24,7 @@ describe('Overlay directives', () => {
2424
overlayContainerElement = document.createElement('div');
2525
return {getContainerElement: () => overlayContainerElement};
2626
}},
27-
{provide: Dir, useFactory: () => {
27+
{provide: Directionality, useFactory: () => {
2828
return dir = { value: 'ltr' };
2929
}}
3030
],

0 commit comments

Comments
 (0)