+ `);
+ fixture.detectChanges();
+
+ let element = queryFor(fixture, '[fxFlex]')[0].nativeElement;
+ expect(element).toHaveStyle({'margin-right': '17px'});
+ });
+
+ it('should set margin-right for rtl layouts on documentElement', () => {
+ fakeDocument.documentElement.dir = 'rtl';
+ componentWithTemplate(`
+
+
+
+ `);
+ fixture.detectChanges();
+
+ let element = queryFor(fixture, '[fxFlex]')[0].nativeElement;
+ expect(element).toHaveStyle({'margin-right': '17px'});
+ });
+
+ it('should set margin-left for ltr layouts', () => {
+ componentWithTemplate(`
+
+
+
+ `);
+ fixture.detectChanges();
+
+ let element = queryFor(fixture, '[fxFlex]')[0].nativeElement;
+ expect(element).toHaveStyle({'margin-left': '17px'});
+ });
+
});
});
diff --git a/src/lib/api/flexbox/flex-offset.ts b/src/lib/api/flexbox/flex-offset.ts
index a97f8992e..0e00ed800 100644
--- a/src/lib/api/flexbox/flex-offset.ts
+++ b/src/lib/api/flexbox/flex-offset.ts
@@ -23,6 +23,7 @@ import {
import {Subscription} from 'rxjs/Subscription';
import {BaseFxDirective} from '../core/base';
+import {Directionality} from '../../bidi/directionality';
import {MediaChange} from '../../media-query/media-change';
import {MediaMonitor} from '../../media-query/media-monitor';
import {LayoutDirective} from './layout';
@@ -39,6 +40,7 @@ import {isFlowHorizontal} from '../../utils/layout-validator';
[fxFlexOffset.gt-xs], [fxFlexOffset.gt-sm], [fxFlexOffset.gt-md], [fxFlexOffset.gt-lg]
`})
export class FlexOffsetDirective extends BaseFxDirective implements OnInit, OnChanges, OnDestroy {
+ private _directionWatcher: Subscription;
/* tslint:disable */
@Input('fxFlexOffset') set offset(val) { this._cacheInput('offset', val); }
@@ -63,8 +65,11 @@ export class FlexOffsetDirective extends BaseFxDirective implements OnInit, OnCh
elRef: ElementRef,
renderer: Renderer2,
@Optional() @SkipSelf() protected _container: LayoutDirective,
- @Inject(PLATFORM_ID) platformId: Object) {
+ @Inject(PLATFORM_ID) platformId: Object,
+ private _directionality: Directionality) {
super(monitor, elRef, renderer, platformId);
+ this._directionWatcher =
+ this._directionality.change.subscribe(this._updateWithValue.bind(this));
this.watchParentFlow();
@@ -91,6 +96,9 @@ export class FlexOffsetDirective extends BaseFxDirective implements OnInit, OnCh
if (this._layoutWatcher) {
this._layoutWatcher.unsubscribe();
}
+ if (this._directionWatcher) {
+ this._directionWatcher.unsubscribe();
+ }
}
/**
@@ -162,8 +170,11 @@ export class FlexOffsetDirective extends BaseFxDirective implements OnInit, OnCh
offset = offset + '%';
}
+ const horizontalLayoutKey =
+ this._directionality.value === 'rtl' ? 'margin-right' : 'margin-left';
// The flex-direction of this element's flex container. Defaults to 'row'.
let layout = this._getFlowDirection(this.parentElement, true);
- return isFlowHorizontal(layout) ? {'margin-left': `${offset}`} : {'margin-top': `${offset}`};
+ return isFlowHorizontal(layout) ? {[horizontalLayoutKey]: `${offset}`} :
+ {'margin-top': `${offset}`};
}
}
diff --git a/src/lib/api/flexbox/layout-gap.spec.ts b/src/lib/api/flexbox/layout-gap.spec.ts
index 9f2f6f729..233874d9c 100644
--- a/src/lib/api/flexbox/layout-gap.spec.ts
+++ b/src/lib/api/flexbox/layout-gap.spec.ts
@@ -9,6 +9,7 @@ import {Component, OnInit} from '@angular/core';
import {CommonModule} from '@angular/common';
import {TestBed, ComponentFixture, async} from '@angular/core/testing';
+import {DIR_DOCUMENT} from '../../bidi/directionality';
import {DEFAULT_BREAKPOINTS_PROVIDER} from '../../media-query/breakpoints/break-points-provider';
import {BreakPointRegistry} from '../../media-query/breakpoints/break-point-registry';
import {MockMatchMedia} from '../../media-query/mock/mock-match-media';
@@ -26,9 +27,11 @@ describe('layout-gap directive', () => {
let fixture: ComponentFixture;
let createTestComponent = makeCreateTestComponent(() => TestLayoutGapComponent);
let expectDomForQuery = makeExpectDOMForQuery(() => TestLayoutGapComponent);
+ let fakeDocument: {body: {dir?: string}, documentElement: {dir?: string}};
beforeEach(() => {
jasmine.addMatchers(customMatchers);
+ fakeDocument = {body: {}, documentElement: {}};
// Configure testbed to prepare services
TestBed.configureTestingModule({
@@ -36,7 +39,8 @@ describe('layout-gap directive', () => {
declarations: [TestLayoutGapComponent],
providers: [
BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER,
- {provide: MatchMedia, useClass: MockMatchMedia}
+ {provide: MatchMedia, useClass: MockMatchMedia},
+ {provide: DIR_DOCUMENT, useValue: fakeDocument}
]
});
});
@@ -287,6 +291,23 @@ describe('layout-gap directive', () => {
});
+ describe('rtl support', () => {
+ it('uses margin-left when document body has rtl dir', () => {
+ fakeDocument.body.dir = 'rtl';
+ verifyCorrectMargin('row', 'margin-left');
+ });
+
+ it('uses margin-left when documentElement has rtl dir', () => {
+ fakeDocument.documentElement.dir = 'rtl';
+ verifyCorrectMargin('row', 'margin-left');
+ });
+
+ it('still uses margin-bottom in column layout when body has rtl dir', () => {
+ fakeDocument.body.dir = 'rtl';
+ verifyCorrectMargin('column', 'margin-bottom');
+ });
+ });
+
});
diff --git a/src/lib/api/flexbox/layout-gap.ts b/src/lib/api/flexbox/layout-gap.ts
index 7626ebe58..dc85f8a46 100644
--- a/src/lib/api/flexbox/layout-gap.ts
+++ b/src/lib/api/flexbox/layout-gap.ts
@@ -24,6 +24,7 @@ import {Subscription} from 'rxjs/Subscription';
import {BaseFxDirective} from '../core/base';
import {LayoutDirective} from './layout';
+import {Directionality} from '../../bidi/directionality';
import {MediaChange} from '../../media-query/media-change';
import {MediaMonitor} from '../../media-query/media-monitor';
import {LAYOUT_VALUES} from '../../utils/layout-validator';
@@ -45,6 +46,7 @@ export class LayoutGapDirective extends BaseFxDirective implements AfterContentI
protected _layout = 'row'; // default flex-direction
protected _layoutWatcher: Subscription;
protected _observer: MutationObserver;
+ private _directionWatcher: Subscription;
/* tslint:disable */
@Input('fxLayoutGap') set gap(val) { this._cacheInput('gap', val); }
@@ -70,12 +72,15 @@ export class LayoutGapDirective extends BaseFxDirective implements AfterContentI
renderer: Renderer2,
@Optional() @Self() container: LayoutDirective,
private _zone: NgZone,
- @Inject(PLATFORM_ID) platformId: Object) {
+ @Inject(PLATFORM_ID) platformId: Object,
+ private _directionality: Directionality) {
super(monitor, elRef, renderer, platformId);
if (container) { // Subscribe to layout direction changes
this._layoutWatcher = container.layout$.subscribe(this._onLayoutChange.bind(this));
}
+ this._directionWatcher =
+ this._directionality.change.subscribe(this._updateWithValue.bind(this));
}
// *********************************************
@@ -108,6 +113,9 @@ export class LayoutGapDirective extends BaseFxDirective implements AfterContentI
if (this._observer) {
this._observer.disconnect();
}
+ if (this._directionWatcher) {
+ this._directionWatcher.unsubscribe();
+ }
}
// *********************************************
@@ -196,7 +204,7 @@ export class LayoutGapDirective extends BaseFxDirective implements AfterContentI
case 'row' :
case 'row-reverse':
default :
- key = 'margin-right';
+ key = this._directionality.value === 'rtl' ? 'margin-left' : 'margin-right';
break;
}
margins[key] = value;
diff --git a/src/lib/bidi/bidi-module.ts b/src/lib/bidi/bidi-module.ts
new file mode 100644
index 000000000..84c482f10
--- /dev/null
+++ b/src/lib/bidi/bidi-module.ts
@@ -0,0 +1,23 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {NgModule} from '@angular/core';
+import {DOCUMENT} from '@angular/common';
+import {Dir} from './dir';
+import {DIR_DOCUMENT, Directionality} from './directionality';
+
+
+@NgModule({
+ exports: [Dir],
+ declarations: [Dir],
+ providers: [
+ {provide: DIR_DOCUMENT, useExisting: DOCUMENT},
+ Directionality,
+ ]
+})
+export class BidiModule { }
diff --git a/src/lib/bidi/bidi.md b/src/lib/bidi/bidi.md
new file mode 100644
index 000000000..0ae5f4ed4
--- /dev/null
+++ b/src/lib/bidi/bidi.md
@@ -0,0 +1,39 @@
+The `bidi` package provides a common system for components to get and respond to change in the
+application's LTR/RTL layout direction. This directory was copied straight from
+https://github.com/angular/material2/blob/master/src/cdk/bidi/
+
+### Directionality
+
+When including the CDK's `BidiModule`, components can inject `Directionality` to get the current
+text direction (RTL or LTR);
+
+#### Example
+```ts
+@Component({ ... })
+export class MyWidget implements OnDestroy {
+
+ /** Whether the widget is in RTL mode or not. */
+ private isRtl: boolean;
+
+ /** Subscription to the Directionality change EventEmitter. */
+ private _dirChangeSubscription = Subscription.EMPTY;
+
+ constructor(dir: Directionality) {
+ this.isRtl = dir.value === 'rtl';
+
+ _dirChangeSubscription = dir.change.subscribe(() => {
+ this.flipDirection();
+ });
+ }
+
+ ngOnDestroy() {
+ this._dirChangeSubscription.unsubscribe();
+ }
+}
+```
+
+### The `Dir` directive
+The `BidiModule` also includes a directive that matches any elements with a `dir` attribute. This
+directive has the same API as Directionality and provides itself _as_ `Directionality`. By doing
+this, any component that injects `Directionality` will get the closest ancestor layout direction
+context.
\ No newline at end of file
diff --git a/src/lib/bidi/dir.ts b/src/lib/bidi/dir.ts
new file mode 100644
index 000000000..56a3f0194
--- /dev/null
+++ b/src/lib/bidi/dir.ts
@@ -0,0 +1,63 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {
+ Directive,
+ Output,
+ Input,
+ EventEmitter,
+ AfterContentInit,
+ OnDestroy,
+ } from '@angular/core';
+
+ import {Direction, Directionality} from './directionality';
+
+ /**
+ * Directive to listen for changes of direction of part of the DOM.
+ *
+ * Provides itself as Directionality such that descendant directives only need to ever inject
+ * Directionality to get the closest direction.
+ */
+ @Directive({
+ selector: '[dir]',
+ providers: [{provide: Directionality, useExisting: Dir}],
+ host: {'[dir]': 'dir'},
+ exportAs: 'dir',
+ })
+ export class Dir implements Directionality, AfterContentInit, OnDestroy {
+ _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();
+
+ /** @docs-private */
+ @Input()
+ get dir(): Direction { return this._dir; }
+ set dir(v: Direction) {
+ const old = this._dir;
+ this._dir = v;
+ if (old !== this._dir && this._isInitialized) {
+ this.change.emit(this._dir);
+ }
+ }
+
+ /** Current layout direction of the element. */
+ get value(): Direction { return this.dir; }
+
+ /** Initialize once default value has been set. */
+ ngAfterContentInit() {
+ this._isInitialized = true;
+ }
+
+ ngOnDestroy() {
+ this.change.complete();
+ }
+ }
diff --git a/src/lib/bidi/directionality.spec.ts b/src/lib/bidi/directionality.spec.ts
new file mode 100644
index 000000000..ad14f0117
--- /dev/null
+++ b/src/lib/bidi/directionality.spec.ts
@@ -0,0 +1,121 @@
+import {async, fakeAsync, TestBed} from '@angular/core/testing';
+import {Component} from '@angular/core';
+import {By} from '@angular/platform-browser';
+import {BidiModule, Directionality, Direction, DIR_DOCUMENT} from './index';
+
+describe('Directionality', () => {
+ let fakeDocument: FakeDocument;
+
+ beforeEach(async(() => {
+ fakeDocument = {body: {}, documentElement: {}};
+
+ TestBed.configureTestingModule({
+ imports: [BidiModule],
+ declarations: [ElementWithDir, InjectsDirectionality],
+ providers: [{provide: DIR_DOCUMENT, useFactory: () => fakeDocument}],
+ }).compileComponents();
+ }));
+
+ describe('Service', () => {
+ it('should read dir from the html element if not specified on the body', () => {
+ fakeDocument.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', () => {
+ fakeDocument.documentElement.dir = 'ltr';
+ fakeDocument.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();
+
+ let direction = injectedDirectionality.value;
+ injectedDirectionality.change.subscribe((dir: Direction) => { direction = dir; });
+
+ expect(direction).toBe('rtl');
+ expect(injectedDirectionality.value).toBe('rtl');
+ expect(fixture.componentInstance.changeCount).toBe(0);
+
+ fixture.componentInstance.direction = 'ltr';
+
+ fixture.detectChanges();
+
+ expect(direction).toBe('ltr');
+ expect(injectedDirectionality.value).toBe('ltr');
+ expect(fixture.componentInstance.changeCount).toBe(1);
+ }));
+
+ it('should complete the change stream on destroy', fakeAsync(() => {
+ const fixture = TestBed.createComponent(ElementWithDir);
+ const dir =
+ fixture.debugElement.query(By.directive(InjectsDirectionality)).componentInstance.dir;
+ const spy = jasmine.createSpy('complete spy');
+ const subscription = dir.change.subscribe(undefined, undefined, spy);
+
+ fixture.destroy();
+ expect(spy).toHaveBeenCalled();
+ subscription.unsubscribe();
+ }));
+
+ });
+});
+
+
+@Component({
+ template: `
+
+
+
+ `
+})
+class ElementWithDir {
+ direction = 'rtl';
+ changeCount = 0;
+}
+
+/** Test component with Dir directive. */
+@Component({
+ selector: 'injects-directionality',
+ template: ``
+})
+class InjectsDirectionality {
+ constructor(public dir: Directionality) { }
+}
+
+interface FakeDocument {
+ documentElement: {dir?: string};
+ body: {dir?: string};
+}
diff --git a/src/lib/bidi/directionality.ts b/src/lib/bidi/directionality.ts
new file mode 100644
index 000000000..323ec17b9
--- /dev/null
+++ b/src/lib/bidi/directionality.ts
@@ -0,0 +1,55 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {
+ EventEmitter,
+ Injectable,
+ Optional,
+ Inject,
+ InjectionToken,
+ } from '@angular/core';
+
+
+ export type Direction = 'ltr' | 'rtl';
+
+ /**
+ * Injection token used to inject the document into Directionality.
+ * This is used so that the value can be faked in tests.
+ *
+ * We can't use the real document in tests because changing the real `dir` causes geometry-based
+ * tests in Safari to fail.
+ *
+ * We also can't re-provide the DOCUMENT token from platform-brower because the unit tests
+ * themselves use things like `querySelector` in test code.
+ */
+ export const DIR_DOCUMENT = new InjectionToken('cdk-dir-doc');
+
+ /**
+ * 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 {
+ /** The current 'ltr' or 'rtl' value. */
+ readonly value: Direction = 'ltr';
+
+ /** Stream that emits whenever the 'ltr' / 'rtl' state changes. */
+ readonly change = new EventEmitter();
+
+ constructor(@Optional() @Inject(DIR_DOCUMENT) _document?: any) {
+ if (_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
+ const bodyDir = _document.body ? _document.body.dir : null;
+ const htmlDir = _document.documentElement ? _document.documentElement.dir : null;
+ this.value = (bodyDir || htmlDir || 'ltr') as Direction;
+ }
+ }
+ }
diff --git a/src/lib/bidi/index.ts b/src/lib/bidi/index.ts
new file mode 100644
index 000000000..676ca90f1
--- /dev/null
+++ b/src/lib/bidi/index.ts
@@ -0,0 +1,9 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+export * from './public-api';
diff --git a/src/lib/bidi/public-api.ts b/src/lib/bidi/public-api.ts
new file mode 100644
index 000000000..3e20fe799
--- /dev/null
+++ b/src/lib/bidi/public-api.ts
@@ -0,0 +1,11 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+export {Directionality, DIR_DOCUMENT, Direction} from './directionality';
+export {Dir} from './dir';
+export * from './bidi-module';
diff --git a/src/lib/module.ts b/src/lib/module.ts
index c0a0f87e1..17e2f82f2 100644
--- a/src/lib/module.ts
+++ b/src/lib/module.ts
@@ -32,6 +32,8 @@ import {ClassDirective} from './api/ext/class';
import {StyleDirective} from './api/ext/style';
import {ImgSrcDirective} from './api/ext/img-src';
+import {BidiModule} from './bidi/bidi-module';
+
/**
* Since the equivalent results are easily achieved with a css class attached to each
* layout child, these have been deprecated and removed from the API.
@@ -59,7 +61,7 @@ const ALL_DIRECTIVES = [
*
*/
@NgModule({
- imports: [MediaQueriesModule],
+ imports: [MediaQueriesModule, BidiModule],
exports: [MediaQueriesModule, ...ALL_DIRECTIVES],
declarations: [...ALL_DIRECTIVES],
providers: [