Skip to content

Commit d4f0d53

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 cebb516 commit d4f0d53

31 files changed

+333
-133
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 {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher';
2626
import {Subscription} from 'rxjs/Subscription';
@@ -103,9 +103,10 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
103103

104104
constructor(private _element: ElementRef, private _overlay: Overlay,
105105
private _viewContainerRef: ViewContainerRef,
106+
private _zone: NgZone,
106107
private _changeDetectorRef: ChangeDetectorRef,
107108
private _scrollDispatcher: ScrollDispatcher,
108-
@Optional() private _dir: Dir, private _zone: NgZone,
109+
@Optional() private _dir: Directionality,
109110
@Optional() @Host() private _inputContainer: MdInputContainer,
110111
@Optional() @Inject(DOCUMENT) private _document: any) {}
111112

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {NoopAnimationsModule} from '@angular/platform-browser/animations';
1313
import {MdAutocompleteModule, MdAutocompleteTrigger} from './index';
1414
import {OverlayContainer} from '../core/overlay/overlay-container';
1515
import {MdInputModule} from '../input/index';
16-
import {Dir, LayoutDirection} from '../core/rtl/dir';
16+
import {Directionality, Direction} from '../core/bidi/index';
1717
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
1818
import {Subscription} from 'rxjs/Subscription';
1919
import {ENTER, DOWN_ARROW, SPACE, UP_ARROW, ESCAPE} from '../core/keyboard/keycodes';
@@ -31,7 +31,7 @@ import 'rxjs/add/operator/map';
3131

3232
describe('MdAutocomplete', () => {
3333
let overlayContainerElement: HTMLElement;
34-
let dir: LayoutDirection;
34+
let dir: Direction;
3535
let scrolledSubject = new Subject();
3636

3737
beforeEach(async(() => {
@@ -65,7 +65,7 @@ describe('MdAutocomplete', () => {
6565

6666
return {getContainerElement: () => overlayContainerElement};
6767
}},
68-
{provide: Dir, useFactory: () => ({value: dir})},
68+
{provide: Directionality, useFactory: () => ({value: dir})},
6969
{provide: ScrollDispatcher, useFactory: () => {
7070
return {scrolled: (delay: number, callback: () => any) => {
7171
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: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import {async, fakeAsync, TestBed, tick} from '@angular/core/testing';
2+
import {Component, getDebugNode} from '@angular/core';
3+
import {Dir, Directionality} from './index';
4+
5+
function initDir () {
6+
document.documentElement.dir = '';
7+
document.body.dir = '';
8+
}
9+
10+
describe('Directionality', () => {
11+
beforeEach(async(() => {
12+
TestBed.configureTestingModule({
13+
providers: [Directionality],
14+
declarations: [TestAppWithoutDir]
15+
});
16+
17+
TestBed.compileComponents();
18+
19+
initDir();
20+
}));
21+
22+
it('should read dir from the html element if not specified on the body', () => {
23+
document.documentElement.dir = 'rtl';
24+
25+
let fixture = TestBed.createComponent(TestAppWithoutDir);
26+
let testComponent = fixture.debugElement.componentInstance;
27+
28+
expect(testComponent.dir.value).toBe('rtl');
29+
});
30+
31+
it('should read dir from the body even it is also specified on the html element', () => {
32+
document.documentElement.dir = 'ltr';
33+
document.body.dir = 'rtl';
34+
35+
let fixture = TestBed.createComponent(TestAppWithoutDir);
36+
let testComponent = fixture.debugElement.componentInstance;
37+
38+
expect(testComponent.dir.value).toBe('rtl');
39+
});
40+
41+
it('should default to ltr if nothing is specified on either body or the html element', () => {
42+
let fixture = TestBed.createComponent(TestAppWithoutDir);
43+
let testComponent = fixture.debugElement.componentInstance;
44+
45+
expect(testComponent.dir.value).toBe('ltr');
46+
});
47+
});
48+
49+
describe('Dir directive', () => {
50+
beforeEach(async(() => {
51+
TestBed.configureTestingModule({
52+
declarations: [Dir, TestAppWithDir, DirTest]
53+
});
54+
55+
TestBed.compileComponents();
56+
57+
initDir();
58+
}));
59+
60+
it('should provide itself as Directionality', () => {
61+
let fixture = TestBed.createComponent(TestAppWithDir);
62+
let testComponent =
63+
getDebugNode(fixture.nativeElement.querySelector('dir-test')).componentInstance;
64+
65+
fixture.detectChanges();
66+
67+
expect(testComponent.dir.value).toBe('rtl');
68+
});
69+
70+
it('should emit a change event when the value changes', fakeAsync(() => {
71+
let fixture = TestBed.createComponent(TestAppWithDir);
72+
let testComponent =
73+
getDebugNode(fixture.nativeElement.querySelector('dir-test')).componentInstance;
74+
75+
fixture.detectChanges();
76+
77+
expect(testComponent.dir.value).toBe('rtl');
78+
expect(fixture.componentInstance.changeCount).toBe(0);
79+
80+
fixture.componentInstance.direction = 'ltr';
81+
82+
fixture.detectChanges();
83+
tick();
84+
85+
expect(testComponent.dir.value).toBe('ltr');
86+
expect(fixture.componentInstance.changeCount).toBe(1);
87+
}));
88+
});
89+
90+
/** Test component without Dir. */
91+
@Component({
92+
selector: 'test-app-without-dir',
93+
template: `<div></div>`
94+
})
95+
class TestAppWithoutDir {
96+
constructor(public dir: Directionality) {}
97+
}
98+
99+
/** Test component without Dir. */
100+
@Component({
101+
selector: 'test-app-with-dir',
102+
template: `
103+
<div [dir]="direction" (dirChange)="changeCount= changeCount + 1">
104+
<dir-test></dir-test>
105+
</div>
106+
`
107+
})
108+
class TestAppWithDir {
109+
direction = 'rtl';
110+
changeCount = 0;
111+
}
112+
113+
/** Test component with Dir directive. */
114+
@Component({
115+
selector: 'dir-test',
116+
template: `<div></div>`
117+
})
118+
class DirTest {
119+
constructor(public dir: Directionality) {}
120+
}

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) {
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/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/option';
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';
@@ -106,7 +106,7 @@ export * from './datetime/index';
106106
@NgModule({
107107
imports: [
108108
MdLineModule,
109-
RtlModule,
109+
BidiModule,
110110
MdRippleModule,
111111
ObserveContentModule,
112112
PortalModule,
@@ -117,7 +117,7 @@ export * from './datetime/index';
117117
],
118118
exports: [
119119
MdLineModule,
120-
RtlModule,
120+
BidiModule,
121121
MdRippleModule,
122122
ObserveContentModule,
123123
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 {RepositionScrollStrategy} from './scroll/reposition-scroll-strategy';
2929
import {ScrollStrategy} from './scroll/scroll-strategy';
@@ -160,7 +160,7 @@ export class ConnectedOverlayDirective implements OnDestroy, OnChanges {
160160
private _scrollDispatcher: ScrollDispatcher,
161161
templateRef: TemplateRef<any>,
162162
viewContainerRef: ViewContainerRef,
163-
@Optional() private _dir: Dir) {
163+
@Optional() private _dir: Directionality) {
164164
this._templatePortal = new TemplatePortal(templateRef, viewContainerRef);
165165
}
166166

@@ -170,7 +170,7 @@ export class ConnectedOverlayDirective implements OnDestroy, OnChanges {
170170
}
171171

172172
/** The element's layout direction. */
173-
get dir(): LayoutDirection {
173+
get dir(): Direction {
174174
return this._dir ? this._dir.value : 'ltr';
175175
}
176176

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {PositionStrategy} from './position/position-strategy';
2-
import {LayoutDirection} from '../rtl/dir';
2+
import {Direction} from '../bidi/index';
33
import {ScrollStrategy} from './scroll/scroll-strategy';
44
import {NoopScrollStrategy} from './scroll/noop-scroll-strategy';
55

@@ -37,7 +37,7 @@ export class OverlayState {
3737
minHeight: number | string;
3838

3939
/** The direction of the text in the overlay panel. */
40-
direction: LayoutDirection = 'ltr';
40+
direction: Direction = 'ltr';
4141

4242
// TODO(jelbourn): configuration still to add
4343
// - focus trap

0 commit comments

Comments
 (0)