Skip to content

Commit 780b976

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 780b976

33 files changed

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

src/lib/core/bidi/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 {ModuleWithProviders, NgModule} from '@angular/core';
10+
import {Dir} from './dir';
11+
import {Directionality, DIRECTIONALITY_PROVIDER} from './directionality';
12+
13+
export {
14+
Directionality,
15+
DIRECTIONALITY_PROVIDER,
16+
Direction
17+
} from './directionality';
18+
export {Dir} from './dir';
19+
20+
@NgModule({
21+
exports: [Dir],
22+
declarations: [Dir],
23+
providers: [Directionality]
24+
})
25+
export class BidiModule {
26+
/** @deprecated */
27+
static forRoot(): ModuleWithProviders {
28+
return {
29+
ngModule: BidiModule,
30+
providers: [DIRECTIONALITY_PROVIDER]
31+
};
32+
}
33+
}

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)