From 89b6d236cb40cb43f3f64f7567cab3a052877c25 Mon Sep 17 00:00:00 2001 From: Oleg Pimenov Date: Wed, 16 May 2018 16:03:55 +0300 Subject: [PATCH] feat(bidi): added bidi to cdk --- package-lock.json | 6 - package.json | 1 - src/cdk/bidi/bidi-module.ts | 11 ++ src/cdk/bidi/dir-document-token.ts | 28 ++++ src/cdk/bidi/dir.ts | 63 ++++++++ src/cdk/bidi/directionality.spec.ts | 140 ++++++++++++++++++ src/cdk/bidi/directionality.ts | 36 +++++ src/cdk/bidi/index.ts | 2 + src/cdk/bidi/public-api.ts | 7 + src/cdk/bidi/tsconfig.build.json | 13 ++ .../core/common-behaviors/common-module.ts | 32 ++-- src/lib/divider/README.md | 1 - tests/karma-test-shim.js | 1 + tools/dgeni/processors/component-grouper.ts | 4 +- tools/packages/build-bundles.ts | 2 +- tools/packages/rollup-globals.ts | 2 +- 16 files changed, 315 insertions(+), 34 deletions(-) create mode 100644 src/cdk/bidi/bidi-module.ts create mode 100644 src/cdk/bidi/dir-document-token.ts create mode 100644 src/cdk/bidi/dir.ts create mode 100644 src/cdk/bidi/directionality.spec.ts create mode 100644 src/cdk/bidi/directionality.ts create mode 100644 src/cdk/bidi/index.ts create mode 100644 src/cdk/bidi/public-api.ts create mode 100644 src/cdk/bidi/tsconfig.build.json diff --git a/package-lock.json b/package-lock.json index 8d56d9811..2ecd3bbfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -418,12 +418,6 @@ "@types/node": "9.4.6" } }, - "@types/hammerjs": { - "version": "2.0.35", - "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.35.tgz", - "integrity": "sha512-4mUIMSZ2U4UOWq1b+iV7XUTE4w+Kr3x+Zb/Qz5ROO6BTZLw2c8/ftjq0aRgluguLs4KRuBnrOy/s389HVn1/zA==", - "dev": true - }, "@types/jasmine": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.7.tgz", diff --git a/package.json b/package.json index bbf9c63d4..094e0e547 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "@types/chalk": "^2.2.0", "@types/fs-extra": "^5.0.2", "@types/glob": "^5.0.35", - "@types/hammerjs": "2.0.35", "@types/jasmine": "^2.8.7", "@types/node": "9.4.6", "@types/rx": "4.1.1", diff --git a/src/cdk/bidi/bidi-module.ts b/src/cdk/bidi/bidi-module.ts new file mode 100644 index 000000000..b55fe8893 --- /dev/null +++ b/src/cdk/bidi/bidi-module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; + +import { Dir } from './dir'; + + +@NgModule({ + exports: [Dir], + declarations: [Dir] +}) +export class BidiModule { +} diff --git a/src/cdk/bidi/dir-document-token.ts b/src/cdk/bidi/dir-document-token.ts new file mode 100644 index 000000000..5f989866e --- /dev/null +++ b/src/cdk/bidi/dir-document-token.ts @@ -0,0 +1,28 @@ +import { DOCUMENT } from '@angular/common'; +import { inject, InjectionToken } from '@angular/core'; + + +/** + * 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. + * + * This token is defined in a separate file from Directionality as a workaround for + * https://github.com/angular/angular/issues/22559 + * + * @docs-private + */ +export const DIR_DOCUMENT = new InjectionToken('cdk-dir-doc', { + providedIn: 'root', + factory: DIR_DOCUMENT_FACTORY +}); + +/** @docs-private */ +export function DIR_DOCUMENT_FACTORY(): Document { + return inject(DOCUMENT); +} diff --git a/src/cdk/bidi/dir.ts b/src/cdk/bidi/dir.ts new file mode 100644 index 000000000..98c7b525d --- /dev/null +++ b/src/cdk/bidi/dir.ts @@ -0,0 +1,63 @@ +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'; + + /** 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; + } + + /** Whether the `value` has been set to its initial value. */ + private _isInitialized: boolean = false; + + /** Initialize once default value has been set. */ + ngAfterContentInit() { + this._isInitialized = true; + } + + ngOnDestroy() { + this.change.complete(); + } +} + diff --git a/src/cdk/bidi/directionality.spec.ts b/src/cdk/bidi/directionality.spec.ts new file mode 100644 index 000000000..462175789 --- /dev/null +++ b/src/cdk/bidi/directionality.spec.ts @@ -0,0 +1,140 @@ +import {Component} from '@angular/core'; +import {async, fakeAsync, TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; + +import {BidiModule, Directionality, Direction, DIR_DOCUMENT} from './index'; + + +describe('Directionality', () => { + let fakeDocument: IFakeDocument; + + 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'; + + const fixture = TestBed.createComponent(InjectsDirectionality); + const 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'; + + const fixture = TestBed.createComponent(InjectsDirectionality); + const 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', () => { + const fixture = TestBed.createComponent(InjectsDirectionality); + const testComponent = fixture.debugElement.componentInstance; + + expect(testComponent.dir.value).toBe('ltr'); + }); + + it('should complete the `change` stream on destroy', () => { + const fixture = TestBed.createComponent(InjectsDirectionality); + const spy = jasmine.createSpy('complete spy'); + const subscription = + fixture.componentInstance.dir.change.subscribe(undefined, undefined, spy); + + fixture.componentInstance.dir.ngOnDestroy(); + expect(spy).toHaveBeenCalled(); + + subscription.unsubscribe(); + }); + + }); + + describe('Dir directive', () => { + it('should provide itself as Directionality', () => { + const 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(() => { + const 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 IFakeDocument { + documentElement: { dir?: string }; + body: { dir?: string }; +} diff --git a/src/cdk/bidi/directionality.ts b/src/cdk/bidi/directionality.ts new file mode 100644 index 000000000..c8888829b --- /dev/null +++ b/src/cdk/bidi/directionality.ts @@ -0,0 +1,36 @@ +import { EventEmitter, Inject, Injectable, Optional, OnDestroy } from '@angular/core'; + +import {DIR_DOCUMENT} from './dir-document-token'; + + +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({providedIn: 'root'}) +export class Directionality implements OnDestroy { + /** 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; + } + } + + ngOnDestroy() { + this.change.complete(); + } +} diff --git a/src/cdk/bidi/index.ts b/src/cdk/bidi/index.ts new file mode 100644 index 000000000..2530908a6 --- /dev/null +++ b/src/cdk/bidi/index.ts @@ -0,0 +1,2 @@ + +export * from './public-api'; diff --git a/src/cdk/bidi/public-api.ts b/src/cdk/bidi/public-api.ts new file mode 100644 index 000000000..a4135f4c8 --- /dev/null +++ b/src/cdk/bidi/public-api.ts @@ -0,0 +1,7 @@ + +export { Directionality, Direction } from './directionality'; +export { DIR_DOCUMENT } from './dir-document-token'; +export { Dir } from './dir'; + +export * from './bidi-module'; + diff --git a/src/cdk/bidi/tsconfig.build.json b/src/cdk/bidi/tsconfig.build.json new file mode 100644 index 000000000..69ab5ed1d --- /dev/null +++ b/src/cdk/bidi/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.build", + "files": [ + "public-api.ts" + ], + "angularCompilerOptions": { + "strictMetadataEmit": true, + "flatModuleOutFile": "index.js", + "flatModuleId": "@ptsecurity/cdk/bidi", + "skipTemplateCodegen": true, + "fullTemplateTypeCheck": true + } +} diff --git a/src/lib/core/common-behaviors/common-module.ts b/src/lib/core/common-behaviors/common-module.ts index 85b2fd4eb..13c846cec 100644 --- a/src/lib/core/common-behaviors/common-module.ts +++ b/src/lib/core/common-behaviors/common-module.ts @@ -1,9 +1,16 @@ import { NgModule, InjectionToken, Optional, Inject, isDevMode } from '@angular/core'; +import { BidiModule } from '@ptsecurity/cdk/bidi'; // Injection token that configures whether the Mosaic sanity checks are enabled. -export const MС_SANITY_CHECKS = new InjectionToken('mc-sanity-checks'); +export const MС_SANITY_CHECKS = new InjectionToken('mc-sanity-checks', { + providedIn: 'root', + factory: MC_SANITY_CHECKS_FACTORY +}); +export function MC_SANITY_CHECKS_FACTORY(): boolean { + return true; +} /** * Module that captures anything that should be loaded and/or run for *all* Mosaic @@ -12,19 +19,13 @@ export const MС_SANITY_CHECKS = new InjectionToken('mc-sanity-checks') * This module should be imported to each top-level component module (e.g., MatTabsModule). */ @NgModule({ - imports: [], - exports: [], - providers: [{ - provide: MС_SANITY_CHECKS, useValue: true - }] + imports: [ BidiModule ], + exports: [ BidiModule ] }) export class McCommonModule { // Whether we've done the global sanity checks (e.g. a theme is loaded, there is a doctype). private _hasDoneGlobalChecks = false; - // Whether we've already checked for HammerJs availability. - private _hasCheckedHammer = false; - // Reference to the global `document` object. private _document = typeof document === 'object' && document ? document : null; @@ -81,17 +82,4 @@ export class McCommonModule { this._document.body.removeChild(testElement); } } - - // Checks whether HammerJS is available. - _checkHammerIsAvailable(): void { - if (this._hasCheckedHammer || !this._window) { - return; - } - - if (this._areChecksEnabled() && !this._window['Hammer']) { - console.warn( - 'Could not find HammerJS. Certain Mosaic components may not work correctly.'); - } - this._hasCheckedHammer = true; - } } diff --git a/src/lib/divider/README.md b/src/lib/divider/README.md index 0ccb4d491..e69de29bb 100644 --- a/src/lib/divider/README.md +++ b/src/lib/divider/README.md @@ -1 +0,0 @@ -Please see the official documentation at https://material.angular.io/components/component/divider \ No newline at end of file diff --git a/tests/karma-test-shim.js b/tests/karma-test-shim.js index 367dd1fca..0383a0c8b 100644 --- a/tests/karma-test-shim.js +++ b/tests/karma-test-shim.js @@ -51,6 +51,7 @@ System.config({ '@ptsecurity/cdk': 'dist/packages/cdk/index.js', '@ptsecurity/cdk/a11y': 'dist/packages/cdk/a11y/index.js', + '@ptsecurity/cdk/bidi': 'dist/packages/cdk/bidi/index.js', '@ptsecurity/cdk/collections': 'dist/packages/cdk/collections/index.js', '@ptsecurity/cdk/keycodes': 'dist/packages/cdk/keycodes/index.js', '@ptsecurity/cdk/platform': 'dist/packages/cdk/platform/index.js', diff --git a/tools/dgeni/processors/component-grouper.ts b/tools/dgeni/processors/component-grouper.ts index 90fbe7e08..e4ac646f4 100644 --- a/tools/dgeni/processors/component-grouper.ts +++ b/tools/dgeni/processors/component-grouper.ts @@ -21,7 +21,7 @@ export class ComponentGroup { /** Module import path for the component group. */ moduleImportPath: string; - /** Name of the package, either material or cdk */ + /** Name of the package, either mosaic or cdk */ packageName: string; /** Display name of the package. */ @@ -55,7 +55,7 @@ export class ComponentGroup { } /** - * Processor to group docs into top-level "Components" WRT material design, e.g., "Button", "Tabs", + * Processor to group docs into top-level "Components", e.g., "Button", "Tabs", * where each group may conists of several directives and services. */ export class ComponentGrouper implements Processor { diff --git a/tools/packages/build-bundles.ts b/tools/packages/build-bundles.ts index c1f1444a2..7a0145671 100644 --- a/tools/packages/build-bundles.ts +++ b/tools/packages/build-bundles.ts @@ -166,7 +166,7 @@ export class PackageBundler { const importRegex = new RegExp(`@ptsecurity/${this.buildPackage.name}/.+`); external = external.filter((e) => !importRegex.test(e)); - // Use the rollup-alias plugin to map imports of the form `@angular/material/button` + // Use the rollup-alias plugin to map imports of the form `@ptsecurity/mosaic/button` // to the actual file location so that rollup can resolve the imports (otherwise they // will be treated as external dependencies and not included in the bundle). bundleOptions.plugins.push( diff --git a/tools/packages/rollup-globals.ts b/tools/packages/rollup-globals.ts index 0cec5bd2e..04f701575 100644 --- a/tools/packages/rollup-globals.ts +++ b/tools/packages/rollup-globals.ts @@ -14,7 +14,7 @@ const rollupCdkEntryPoints = cdkSecondaryEntryPoints.reduce((globals: any, entry return globals; }, {}); -/** List of potential secondary entry-points for the material package. */ +/** List of potential secondary entry-points for the package. */ const mcSecondaryEntryPoints = getSubdirectoryNames(join(buildConfig.packagesDir, 'lib')); const rollupMcEntryPoints = mcSecondaryEntryPoints.reduce((globals: any, entryPoint: string) => {