Skip to content

Commit ce40d79

Browse files
committed
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 90e6d3c commit ce40d79

33 files changed

+331
-137
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {ConnectedPositionStrategy} from '../core/overlay/position/connected-posi
2020
import {Observable} from 'rxjs/Observable';
2121
import {MdOptionSelectionChange, MdOption} from '../core/option/option';
2222
import {ENTER, UP_ARROW, DOWN_ARROW, ESCAPE} from '../core/keyboard/keycodes';
23-
import {Dir} from '../core/rtl/dir';
23+
import {Directionality} from '../core/bidi/index';
2424
import {MdInputContainer} from '../input/input-container';
2525
import {Subscription} from 'rxjs/Subscription';
2626
import 'rxjs/add/observable/merge';
@@ -112,8 +112,9 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
112112

113113
constructor(private _element: ElementRef, private _overlay: Overlay,
114114
private _viewContainerRef: ViewContainerRef,
115+
private _zone: NgZone,
115116
private _changeDetectorRef: ChangeDetectorRef,
116-
@Optional() private _dir: Dir, private _zone: NgZone,
117+
@Optional() private _dir: Directionality,
117118
@Optional() @Host() private _inputContainer: MdInputContainer,
118119
@Optional() @Inject(DOCUMENT) private _document: any) {}
119120

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

src/lib/core/bidi/directionality.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {
2+
EventEmitter,
3+
Injectable,
4+
Optional,
5+
SkipSelf
6+
} from '@angular/core';
7+
8+
export type Direction = 'ltr' | 'rtl';
9+
10+
/**
11+
* The directionality (LTR / RTL) context for the application (or a subtree of it).
12+
* Exposes the current direction and a stream of direction changes.
13+
*/
14+
@Injectable()
15+
export class Directionality {
16+
value: Direction = 'ltr';
17+
public change = new EventEmitter<void>();
18+
19+
constructor() {
20+
if (typeof document === 'object' && !!document) {
21+
// TODO: handle 'auto' value -
22+
// We still need to account for dir="auto".
23+
// It looks like HTMLElemenet.dir is also "auto" when that's set to the attribute,
24+
// but getComputedStyle return either "ltr" or "rtl". avoiding getComputedStyle for now
25+
// though, we're already calling it for the theming check.
26+
this.value = (document.body.dir || document.documentElement.dir || 'ltr') as Direction;
27+
}
28+
}
29+
}
30+
31+
export function DIRECTIONALITY_PROVIDER_FACTORY(parentDirectionality) {
32+
return parentDirectionality || new Directionality();
33+
}
34+
35+
export const DIRECTIONALITY_PROVIDER = {
36+
// If there is already a Directionality available, use that. Otherwise, provide a new one.
37+
provide: Directionality,
38+
deps: [[new Optional(), new SkipSelf(), Directionality]],
39+
useFactory: DIRECTIONALITY_PROVIDER_FACTORY
40+
};

src/lib/core/bidi/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {ModuleWithProviders, NgModule} from '@angular/core';
2+
import {Dir} from './dir';
3+
import {Directionality, DIRECTIONALITY_PROVIDER} from './directionality';
4+
5+
export {
6+
Directionality,
7+
DIRECTIONALITY_PROVIDER,
8+
Direction
9+
} from './directionality';
10+
export {Dir} from './dir';
11+
12+
@NgModule({
13+
exports: [Dir],
14+
declarations: [Dir],
15+
providers: [Directionality]
16+
})
17+
export class BidiModule {
18+
/** @deprecated */
19+
static forRoot(): ModuleWithProviders {
20+
return {
21+
ngModule: BidiModule,
22+
providers: [DIRECTIONALITY_PROVIDER]
23+
};
24+
}
25+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {NgModule, InjectionToken, Optional, Inject, isDevMode} from '@angular/core';
22
import {DOCUMENT} from '@angular/platform-browser';
33
import {CompatibilityModule} from '../compatibility/compatibility';
4+
import {BidiModule} from '../bidi/index';
45

56

67
/** Injection token that configures whether the Material sanity checks are enabled. */
@@ -14,8 +15,8 @@ export const MATERIAL_SANITY_CHECKS = new InjectionToken<boolean>('md-sanity-che
1415
* This module should be imported to each top-level component module (e.g., MdTabsModule).
1516
*/
1617
@NgModule({
17-
imports: [CompatibilityModule],
18-
exports: [CompatibilityModule],
18+
imports: [CompatibilityModule, BidiModule],
19+
exports: [CompatibilityModule, BidiModule],
1920
providers: [{
2021
provide: MATERIAL_SANITY_CHECKS, useValue: true,
2122
}],

src/lib/core/core.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {NgModule} from '@angular/core';
22
import {MdLineModule} from './line/line';
3-
import {RtlModule} from './rtl/dir';
3+
import {BidiModule} from './bidi/index';
44
import {ObserveContentModule} from './observe-content/observe-content';
55
import {MdOptionModule} from './option/index';
66
import {PortalModule} from './portal/portal-directives';
@@ -11,7 +11,7 @@ import {MdRippleModule} from './ripple/index';
1111

1212

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

1616
// Mutation Observer
1717
export {ObserveContentModule, ObserveContent} from './observe-content/observe-content';
@@ -113,7 +113,7 @@ export {
113113
@NgModule({
114114
imports: [
115115
MdLineModule,
116-
RtlModule,
116+
BidiModule,
117117
MdRippleModule,
118118
ObserveContentModule,
119119
PortalModule,
@@ -124,7 +124,7 @@ export {
124124
],
125125
exports: [
126126
MdLineModule,
127-
RtlModule,
127+
BidiModule,
128128
MdRippleModule,
129129
ObserveContentModule,
130130
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
],

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
} from './position/connected-position';
2424
import {PortalModule} from '../portal/portal-directives';
2525
import {ConnectedPositionStrategy} from './position/connected-position-strategy';
26-
import {Dir, LayoutDirection} from '../rtl/dir';
26+
import {Directionality, Direction} from '../bidi/index';
2727
import {Scrollable} from './scroll/scrollable';
2828
import {ScrollStrategy} from './scroll/scroll-strategy';
2929
import {coerceBooleanProperty} from '../coercion/boolean-property';
@@ -157,7 +157,7 @@ export class ConnectedOverlayDirective implements OnDestroy, OnChanges {
157157
private _renderer: Renderer2,
158158
templateRef: TemplateRef<any>,
159159
viewContainerRef: ViewContainerRef,
160-
@Optional() private _dir: Dir) {
160+
@Optional() private _dir: Directionality) {
161161
this._templatePortal = new TemplatePortal(templateRef, viewContainerRef);
162162
}
163163

@@ -167,7 +167,7 @@ export class ConnectedOverlayDirective implements OnDestroy, OnChanges {
167167
}
168168

169169
/** The element's layout direction. */
170-
get dir(): LayoutDirection {
170+
get dir(): Direction {
171171
return this._dir ? this._dir.value : 'ltr';
172172
}
173173

0 commit comments

Comments
 (0)