Skip to content

Commit

Permalink
feat(directionality): a provider to get the overall directionality
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
EladBezalel committed Jun 3, 2017
1 parent cebb516 commit 1b22426
Show file tree
Hide file tree
Showing 31 changed files with 326 additions and 133 deletions.
5 changes: 3 additions & 2 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {ConnectedPositionStrategy} from '../core/overlay/position/connected-posi
import {Observable} from 'rxjs/Observable';
import {MdOptionSelectionChange, MdOption} from '../core/option/option';
import {ENTER, UP_ARROW, DOWN_ARROW, ESCAPE} from '../core/keyboard/keycodes';
import {Dir} from '../core/rtl/dir';
import {Directionality} from '../core/bidi/index';
import {MdInputContainer} from '../input/input-container';
import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher';
import {Subscription} from 'rxjs/Subscription';
Expand Down Expand Up @@ -103,9 +103,10 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {

constructor(private _element: ElementRef, private _overlay: Overlay,
private _viewContainerRef: ViewContainerRef,
private _zone: NgZone,
private _changeDetectorRef: ChangeDetectorRef,
private _scrollDispatcher: ScrollDispatcher,
@Optional() private _dir: Dir, private _zone: NgZone,
@Optional() private _dir: Directionality,
@Optional() @Host() private _inputContainer: MdInputContainer,
@Optional() @Inject(DOCUMENT) private _document: any) {}

Expand Down
6 changes: 3 additions & 3 deletions src/lib/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {MdAutocompleteModule, MdAutocompleteTrigger} from './index';
import {OverlayContainer} from '../core/overlay/overlay-container';
import {MdInputModule} from '../input/index';
import {Dir, LayoutDirection} from '../core/rtl/dir';
import {Directionality, Direction} from '../core/bidi/index';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {Subscription} from 'rxjs/Subscription';
import {ENTER, DOWN_ARROW, SPACE, UP_ARROW, ESCAPE} from '../core/keyboard/keycodes';
Expand All @@ -31,7 +31,7 @@ import 'rxjs/add/operator/map';

describe('MdAutocomplete', () => {
let overlayContainerElement: HTMLElement;
let dir: LayoutDirection;
let dir: Direction;
let scrolledSubject = new Subject();

beforeEach(async(() => {
Expand Down Expand Up @@ -65,7 +65,7 @@ describe('MdAutocomplete', () => {

return {getContainerElement: () => overlayContainerElement};
}},
{provide: Dir, useFactory: () => ({value: dir})},
{provide: Directionality, useFactory: () => ({value: dir})},
{provide: ScrollDispatcher, useFactory: () => {
return {scrolled: (delay: number, callback: () => any) => {
return scrolledSubject.asObservable().subscribe(callback);
Expand Down
58 changes: 58 additions & 0 deletions src/lib/core/bidi/dir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
Directive,
HostBinding,
Output,
Input,
EventEmitter
} from '@angular/core';

import {Direction, Directionality} from './directionality';

/**
* Directive to listen for changes of direction of part of the DOM.
*
* Would provide itself in case a component looks for the Directionality service
*/
@Directive({
selector: '[dir]',
// TODO(hansl): maybe `$implicit` isn't the best option here, but for now that's the best we got.
exportAs: '$implicit',
providers: [
{provide: Directionality, useExisting: Dir}
]
})
export class Dir implements Directionality {
/** Layout direction of the element. */
_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<void>();

/** @docs-private */
@HostBinding('attr.dir')
@Input('dir')
get dir(): Direction {
return this._dir;
}

set dir(v: Direction) {
let old = this._dir;
this._dir = v;
if (old !== this._dir && this._isInitialized) {
this.change.emit();
}
}

/** Current layout direction of the element. */
get value(): Direction { return this.dir; }
set value(v: Direction) { this.dir = v; }

/** Initialize once default value has been set. */
ngAfterContentInit() {
this._isInitialized = true;
}
}

120 changes: 120 additions & 0 deletions src/lib/core/bidi/directionality.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {async, fakeAsync, TestBed, tick} from '@angular/core/testing';
import {Component, getDebugNode} from '@angular/core';
import {Dir, Directionality} from './index';

function initDir () {
document.documentElement.dir = '';
document.body.dir = '';
}

describe('Directionality', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [Directionality],
declarations: [TestAppWithoutDir]
});

TestBed.compileComponents();

initDir();
}));

it('should read dir from the html element if not specified on the body', () => {
document.documentElement.dir = 'rtl';

let fixture = TestBed.createComponent(TestAppWithoutDir);
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', () => {
document.documentElement.dir = 'ltr';
document.body.dir = 'rtl';

let fixture = TestBed.createComponent(TestAppWithoutDir);
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(TestAppWithoutDir);
let testComponent = fixture.debugElement.componentInstance;

expect(testComponent.dir.value).toBe('ltr');
});
});

describe('Dir directive', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [Dir, TestAppWithDir, DirTest]
});

TestBed.compileComponents();

initDir();
}));

it('should provide itself as Directionality', () => {
let fixture = TestBed.createComponent(TestAppWithDir);
let testComponent =
getDebugNode(fixture.nativeElement.querySelector('dir-test')).componentInstance;

fixture.detectChanges();

expect(testComponent.dir.value).toBe('rtl');
});

it('should emit a change event when the value changes', fakeAsync(() => {
let fixture = TestBed.createComponent(TestAppWithDir);
let testComponent =
getDebugNode(fixture.nativeElement.querySelector('dir-test')).componentInstance;

fixture.detectChanges();

expect(testComponent.dir.value).toBe('rtl');
expect(fixture.componentInstance.changeCount).toBe(0);

fixture.componentInstance.direction = 'ltr';

fixture.detectChanges();
tick();

expect(testComponent.dir.value).toBe('ltr');
expect(fixture.componentInstance.changeCount).toBe(1);
}));
});

/** Test component without Dir. */
@Component({
selector: 'test-app-without-dir',
template: `<div></div>`
})
class TestAppWithoutDir {
constructor(public dir: Directionality) {}
}

/** Test component without Dir. */
@Component({
selector: 'test-app-with-dir',
template: `
<div [dir]="direction" (dirChange)="changeCount= changeCount + 1">
<dir-test></dir-test>
</div>
`
})
class TestAppWithDir {
direction = 'rtl';
changeCount = 0;
}

/** Test component with Dir directive. */
@Component({
selector: 'dir-test',
template: `<div></div>`
})
class DirTest {
constructor(public dir: Directionality) {}
}
40 changes: 40 additions & 0 deletions src/lib/core/bidi/directionality.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
EventEmitter,
Injectable,
Optional,
SkipSelf
} from '@angular/core';

export type Direction = 'ltr' | 'rtl';

/**
* 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 {
value: Direction = 'ltr';
public change = new EventEmitter<void>();

constructor() {
if (typeof 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
// though, we're already calling it for the theming check.
this.value = (document.body.dir || document.documentElement.dir || 'ltr') as Direction;
}
}
}

export function DIRECTIONALITY_PROVIDER_FACTORY(parentDirectionality) {
return parentDirectionality || new Directionality();
}

export const DIRECTIONALITY_PROVIDER = {
// If there is already a Directionality available, use that. Otherwise, provide a new one.
provide: Directionality,
deps: [[new Optional(), new SkipSelf(), Directionality]],
useFactory: DIRECTIONALITY_PROVIDER_FACTORY
};
25 changes: 25 additions & 0 deletions src/lib/core/bidi/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {ModuleWithProviders, NgModule} from '@angular/core';
import {Dir} from './dir';
import {Directionality, DIRECTIONALITY_PROVIDER} from './directionality';

export {
Directionality,
DIRECTIONALITY_PROVIDER,
Direction
} from './directionality';
export {Dir} from './dir';

@NgModule({
exports: [Dir],
declarations: [Dir],
providers: [Directionality]
})
export class BidiModule {
/** @deprecated */
static forRoot(): ModuleWithProviders {
return {
ngModule: BidiModule,
providers: [DIRECTIONALITY_PROVIDER]
};
}
}
8 changes: 4 additions & 4 deletions src/lib/core/core.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {NgModule} from '@angular/core';
import {MdLineModule} from './line/line';
import {RtlModule} from './rtl/dir';
import {BidiModule} from './bidi/index';
import {ObserveContentModule} from './observe-content/observe-content';
import {MdOptionModule} from './option/option';
import {PortalModule} from './portal/portal-directives';
Expand All @@ -11,7 +11,7 @@ import {MdRippleModule} from './ripple/index';


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

// Mutation Observer
export {ObserveContentModule, ObserveContent} from './observe-content/observe-content';
Expand Down Expand Up @@ -106,7 +106,7 @@ export * from './datetime/index';
@NgModule({
imports: [
MdLineModule,
RtlModule,
BidiModule,
MdRippleModule,
ObserveContentModule,
PortalModule,
Expand All @@ -117,7 +117,7 @@ export * from './datetime/index';
],
exports: [
MdLineModule,
RtlModule,
BidiModule,
MdRippleModule,
ObserveContentModule,
PortalModule,
Expand Down
4 changes: 2 additions & 2 deletions src/lib/core/overlay/overlay-directives.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {ConnectedOverlayDirective, OverlayModule, OverlayOrigin} from './overlay
import {OverlayContainer} from './overlay-container';
import {ConnectedPositionStrategy} from './position/connected-position-strategy';
import {ConnectedOverlayPositionChange} from './position/connected-position';
import {Dir} from '../rtl/dir';
import {Directionality} from '../bidi/index';
import {dispatchKeyboardEvent} from '../testing/dispatch-events';
import {ESCAPE} from '../keyboard/keycodes';

Expand All @@ -24,7 +24,7 @@ describe('Overlay directives', () => {
overlayContainerElement = document.createElement('div');
return {getContainerElement: () => overlayContainerElement};
}},
{provide: Dir, useFactory: () => {
{provide: Directionality, useFactory: () => {
return dir = { value: 'ltr' };
}}
],
Expand Down
6 changes: 3 additions & 3 deletions src/lib/core/overlay/overlay-directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
} from './position/connected-position';
import {PortalModule} from '../portal/portal-directives';
import {ConnectedPositionStrategy} from './position/connected-position-strategy';
import {Dir, LayoutDirection} from '../rtl/dir';
import {Directionality, Direction} from '../bidi/index';
import {Scrollable} from './scroll/scrollable';
import {RepositionScrollStrategy} from './scroll/reposition-scroll-strategy';
import {ScrollStrategy} from './scroll/scroll-strategy';
Expand Down Expand Up @@ -160,7 +160,7 @@ export class ConnectedOverlayDirective implements OnDestroy, OnChanges {
private _scrollDispatcher: ScrollDispatcher,
templateRef: TemplateRef<any>,
viewContainerRef: ViewContainerRef,
@Optional() private _dir: Dir) {
@Optional() private _dir: Directionality) {
this._templatePortal = new TemplatePortal(templateRef, viewContainerRef);
}

Expand All @@ -170,7 +170,7 @@ export class ConnectedOverlayDirective implements OnDestroy, OnChanges {
}

/** The element's layout direction. */
get dir(): LayoutDirection {
get dir(): Direction {
return this._dir ? this._dir.value : 'ltr';
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/core/overlay/overlay-state.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {PositionStrategy} from './position/position-strategy';
import {LayoutDirection} from '../rtl/dir';
import {Direction} from '../bidi/index';
import {ScrollStrategy} from './scroll/scroll-strategy';
import {NoopScrollStrategy} from './scroll/noop-scroll-strategy';

Expand Down Expand Up @@ -37,7 +37,7 @@ export class OverlayState {
minHeight: number | string;

/** The direction of the text in the overlay panel. */
direction: LayoutDirection = 'ltr';
direction: Direction = 'ltr';

// TODO(jelbourn): configuration still to add
// - focus trap
Expand Down

0 comments on commit 1b22426

Please sign in to comment.