Commit e727ed0d authored by Shane Eckenrode's avatar Shane Eckenrode

Merge branch 'develop' into 'master'

Develop

See merge request !9
parents 4c6134d6 31f050be
Pipeline #85120 passed with stages
in 4 minutes and 27 seconds
......@@ -15,6 +15,9 @@ Monorepo for building-block type utilities that don't have dependencies on any o
- Http Retry Backoff Operator
- [Security](https://git.psu.edu/ais-swe/ux/utils/tree/develop/libs/utils/security)
- `REQUIRE_AUTH_HEADER` string constant
- [Responsive](https://git.psu.edu/ais-swe/ux/utils/tree/develop/libs/utils/responsive)
- Responsive Service
- ScreenSize Enumeration
# Contributing
......
......@@ -152,7 +152,7 @@
"projectType": "library",
"root": "libs/utils/security",
"sourceRoot": "libs/utils/security/src",
"prefix": "psu",
"prefix": "ut",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
......@@ -180,7 +180,7 @@
"projectType": "library",
"root": "libs/utils/rx",
"sourceRoot": "libs/utils/rx/src",
"prefix": "psu",
"prefix": "ut",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
......@@ -208,7 +208,7 @@
"projectType": "library",
"root": "libs/utils/browser",
"sourceRoot": "libs/utils/browser/src",
"prefix": "psu",
"prefix": "ut",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
......@@ -236,7 +236,7 @@
"projectType": "library",
"root": "libs/utils/angular",
"sourceRoot": "libs/utils/angular/src",
"prefix": "psu",
"prefix": "ut",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
......@@ -259,6 +259,62 @@
"styleext": "scss"
}
}
},
"utils-responsive": {
"projectType": "library",
"root": "libs/utils/responsive",
"sourceRoot": "libs/utils/responsive/src",
"prefix": "ut",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["libs/utils/responsive/tsconfig.lib.json", "libs/utils/responsive/tsconfig.spec.json"],
"exclude": ["**/node_modules/**", "!libs/utils/responsive/**"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/utils/responsive/jest.config.js",
"tsConfig": "libs/utils/responsive/tsconfig.spec.json",
"setupFile": "libs/utils/responsive/src/test-setup.ts"
}
}
},
"schematics": {
"@nrwl/angular:component": {
"styleext": "scss"
}
}
},
"utils-theming": {
"projectType": "library",
"root": "libs/utils/theming",
"sourceRoot": "libs/utils/theming/src",
"prefix": "psu",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["libs/utils/theming/tsconfig.lib.json", "libs/utils/theming/tsconfig.spec.json"],
"exclude": ["**/node_modules/**", "!libs/utils/theming/**"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/utils/theming/jest.config.js",
"tsConfig": "libs/utils/theming/tsconfig.spec.json",
"setupFile": "libs/utils/theming/src/test-setup.ts"
}
}
},
"schematics": {
"@nrwl/angular:component": {
"styleext": "scss"
}
}
}
},
"cli": {
......
......@@ -3,7 +3,8 @@
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^8.0.0",
"@angular/core": "^8.0.0"
"@angular/core": "^8.0.0",
"@angular/cdk": "^8.2.3"
},
"private": false,
"repository": {
......
# Responsive Utils
## ResponsiveService
Injectable singleton service responsible for tracking changes to screen sizes. To use, simply inject the service into the constructor.
```typescript
import { ResponsiveService } from '@psu/utils/responsive';
constructor(public responsiveService: ResponsiveService) {}
```
### Public API
```typescript
// Reactive context
public currentScreenSize$: Observable<ScreenSize>;
public isMobile$: Observable<boolean>;
public isTabletSmall$: Observable<boolean>;
public isTabletLarge$: Observable<boolean>;
public isDesktop$: Observable<boolean>;
// Static context
public currentScreenSize: ScreenSize;
public isMobile: boolean;
public isTabletSmall: boolean;
public isTabletLarge: boolean;
public isDesktop: boolean;
```
module.exports = {
name: 'utils-responsive',
preset: '../../../jest.config.js',
coverageDirectory: '../../../coverage/libs/utils/responsive',
snapshotSerializers: [
'jest-preset-angular/AngularSnapshotSerializer.js',
'jest-preset-angular/HTMLCommentSerializer.js'
]
};
{
"lib": {
"entryFile": "src/index.ts"
}
}
export { ResponsiveModule } from './lib/responsive.module';
export { ResponsiveService } from './lib/responsive.service';
export { ScreenSize } from './lib/screen-size.model';
export interface PsuBreakpoint {
mobile: string;
tabletSmall: string;
tabletLarge: string;
desktop: string;
}
export const PSU_BREAKPOINTS: PsuBreakpoint = {
mobile: '(max-width: 599px)',
tabletSmall: '(min-width: 600px) and (max-width: 729px)',
tabletLarge: '(min-width: 730px) and (max-width: 959px)',
desktop: '(min-width: 960px)'
};
import { CommonModule } from '@angular/common';
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Observable, Subject } from 'rxjs';
import { ResponsiveModule } from './responsive.module';
import { ResponsiveService } from './responsive.service';
import { ScreenSize } from './screen-size.model';
const o = new Subject<ScreenSize>();
class ResponsiveServiceStub {
currentScreenSize$: Observable<ScreenSize> = o.asObservable();
}
@Component({
selector: 'ut-test-comp',
template: `
<button utResponsive></button>
`
})
class TestComponent {}
describe('ResponsiveDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let classes: any;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CommonModule, ResponsiveModule],
declarations: [TestComponent],
providers: [{ provide: ResponsiveService, useClass: ResponsiveServiceStub }],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
classes = fixture.debugElement.query(By.css('button')).classes;
});
describe('when screen size is DESKTOP', () => {
beforeEach(() => {
o.next(ScreenSize.DESKTOP);
fixture.detectChanges();
});
it('should only have desktop class', () => {
expect(classes.desktop).toBeTruthy();
expect(classes.tablet).toBeFalsy();
expect(classes.mobile).toBeFalsy();
});
});
describe('when screen size is TABLET_SM', () => {
beforeEach(() => {
o.next(ScreenSize.TABLET_SM);
fixture.detectChanges();
});
it('should have tablet-sm class', () => {
expect(classes.desktop).toBeFalsy();
expect(classes.tablet).toBeFalsy();
expect(classes['tablet-sm']).toBeTruthy();
expect(classes.mobile).toBeFalsy();
});
});
describe('when screen size is TABLET', () => {
beforeEach(() => {
o.next(ScreenSize.TABLET);
fixture.detectChanges();
});
it('should have tablet class', () => {
expect(classes.desktop).toBeFalsy();
expect(classes.tablet).toBeTruthy();
expect(classes['tablet-sm']).toBeFalsy();
expect(classes.mobile).toBeFalsy();
});
});
describe('when screen size is MOBILE', () => {
beforeEach(() => {
o.next(ScreenSize.MOBILE);
fixture.detectChanges();
});
it('should only have mobile class', () => {
expect(classes.desktop).toBeFalsy();
expect(classes.tablet).toBeFalsy();
expect(classes.mobile).toBeTruthy();
});
});
});
import { Directive, HostBinding, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { ResponsiveService } from './responsive.service';
import { ScreenSize } from './screen-size.model';
@Directive({
selector: '[utResponsive]'
})
export class ResponsiveDirective implements OnInit, OnDestroy {
@HostBinding('class.desktop')
public isDesktop = false;
@HostBinding('class.tablet')
public isTablet = false;
@HostBinding('class.tablet-sm')
public isSmallTablet = false;
@HostBinding('class.mobile')
public isMobile = false;
private destroyed$ = new Subject<any>();
constructor(private responsiveService: ResponsiveService) {}
ngOnInit(): void {
this.responsiveService.currentScreenSize$
.pipe(
filter(size => !!size),
takeUntil(this.destroyed$)
)
.subscribe((size: ScreenSize) => {
this.isMobile = size === ScreenSize.MOBILE;
this.isSmallTablet = size === ScreenSize.TABLET_SM;
this.isTablet = size === ScreenSize.TABLET;
this.isDesktop = size === ScreenSize.DESKTOP;
});
}
public ngOnDestroy(): void {
this.destroyed$.next();
this.destroyed$.complete();
}
}
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ResponsiveDirective } from './responsive.directive';
@NgModule({
imports: [CommonModule],
declarations: [ResponsiveDirective],
exports: [ResponsiveDirective]
})
export class ResponsiveModule {}
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { Subject } from 'rxjs';
import { Mock } from 'ts-mocks';
import { ResponsiveService } from './responsive.service';
describe('ScreenService', () => {
let service: ResponsiveService;
let mockBreakpointObserver: Mock<BreakpointObserver>;
const breakpointObserverSubj: Subject<BreakpointState> = new Subject();
beforeEach(() => {
mockBreakpointObserver = new Mock<BreakpointObserver>({
observe: query => breakpointObserverSubj.asObservable()
});
});
it('should be created', () => {
service = new ResponsiveService(mockBreakpointObserver.Object);
expect(service).toBeTruthy();
});
});
import { BreakpointObserver } from '@angular/cdk/layout';
import { Injectable, OnDestroy } from '@angular/core';
import { combineLatest, Observable, ReplaySubject, Subject } from 'rxjs';
import { map, takeUntil, tap } from 'rxjs/operators';
import { PSU_BREAKPOINTS } from './breakpoints.model';
import { ScreenSize } from './screen-size.model';
// TODO Enhancement: Provide injection token for configuring custom breakpoints to support different breakpoint configurations per application
@Injectable({ providedIn: 'root' })
export class ResponsiveService implements OnDestroy {
// Reactive context
public currentScreenSize$: Observable<ScreenSize>;
public isMobile$: Observable<boolean>;
public isTabletSmall$: Observable<boolean>;
public isTabletLarge$: Observable<boolean>;
public isDesktop$: Observable<boolean>;
// Static context
public currentScreenSize: ScreenSize;
public isMobile: boolean;
public isTabletSmall: boolean;
public isTabletLarge: boolean;
public isDesktop: boolean;
private _screenSizeSubject = new ReplaySubject<ScreenSize>(1);
private _destroy$: Subject<any> = new Subject();
// **** TODO Not convinced that this stuff belongs here with the elimination of ngrx.
public isMenuOpen$: Observable<boolean>;
public isSideSheetOpen$: Observable<boolean>;
private _isMenuOpen = false;
private _isSideSheetOpen = false;
private _menuSubject = new ReplaySubject<boolean>(1);
private _sideSheetSubject = new ReplaySubject<boolean>(1);
// ****
constructor(private breakpointObserver: BreakpointObserver) {
this.currentScreenSize$ = this._screenSizeSubject.asObservable();
this.isMobile$ = this.breakpointMatches(PSU_BREAKPOINTS.mobile);
this.isTabletSmall$ = this.breakpointMatches(PSU_BREAKPOINTS.tabletSmall);
this.isTabletLarge$ = this.breakpointMatches(PSU_BREAKPOINTS.tabletLarge);
this.isDesktop$ = this.breakpointMatches(PSU_BREAKPOINTS.desktop);
combineLatest(this.isMobile$, this.isTabletSmall$, this.isTabletLarge$, this.isDesktop$)
.pipe(
map(([mobile, tabletSmall, tabletLarge, desktop]) => ({ mobile, tabletSmall, tabletLarge, desktop })),
tap(responsiveChange => {
let screenSize: ScreenSize;
if (responsiveChange.mobile) {
screenSize = ScreenSize.MOBILE;
this.isMobile = true;
} else if (responsiveChange.tabletSmall) {
screenSize = ScreenSize.TABLET_SM;
this.isTabletSmall = true;
} else if (responsiveChange.tabletLarge) {
screenSize = ScreenSize.TABLET;
this.isTabletLarge = true;
} else {
screenSize = ScreenSize.DESKTOP;
this.isDesktop = true;
}
this.screenSizeChange(screenSize);
}),
takeUntil(this._destroy$)
)
.subscribe();
this.isMenuOpen$ = this._menuSubject.asObservable();
this.isSideSheetOpen$ = this._sideSheetSubject.asObservable();
}
public ngOnDestroy(): void {
this._destroy$.next();
this._destroy$.complete();
}
public closeMenu(): void {
this._isMenuOpen = false;
this._menuSubject.next(this._isMenuOpen);
}
public closeSideSheet(): void {
this._isSideSheetOpen = false;
this._sideSheetSubject.next(this._isSideSheetOpen);
}
public openMenu(): void {
this._isMenuOpen = true;
this._menuSubject.next(this._isMenuOpen);
}
public openSideSheet(): void {
this._isSideSheetOpen = true;
this._sideSheetSubject.next(this._isSideSheetOpen);
}
public toggleMenu(): void {
this._isMenuOpen = !this._isMenuOpen;
this._menuSubject.next(this._isMenuOpen);
}
public toggleSideSheet(): void {
this._isSideSheetOpen = !this._isSideSheetOpen;
this._sideSheetSubject.next(this._isSideSheetOpen);
}
private screenSizeChange(newScreenSize: ScreenSize): void {
this.currentScreenSize = newScreenSize;
this._screenSizeSubject.next(this.currentScreenSize);
}
private breakpointMatches(breakpoints: string | string[]): Observable<boolean> {
return this.breakpointObserver.observe(breakpoints).pipe(map(result => result.matches));
}
}
// TABLET_LG and DESKTOP_LG are not used for now.
export enum ScreenSize {
MOBILE = 'MOBILE',
TABLET = 'TABLET',
TABLET_SM = 'TABLET_SM',
DESKTOP = 'DESKTOP',
DESKTOP_LG = 'DESKTOP_LG'
}
import 'jest-preset-angular';
import 'jestGlobalMocks';
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"types": ["node", "jest"]
},
"include": ["**/*.ts"]
}
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"target": "es2015",
"declaration": true,
"inlineSources": true,
"types": [],
"lib": ["dom", "es2018"]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": ["src/test-setup.ts", "**/*.spec.ts"]
}
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"files": ["src/test-setup.ts"],
"include": ["**/*.spec.ts", "**/*.d.ts"]
}
{
"extends": "../../../tslint.json",
"rules": {
"directive-selector": [true, "attribute", "ut", "camelCase"],
"component-selector": [true, "element", "ut", "kebab-case"]
}
}
# utils-theming
# Setup for Alternate Theming
## Prerequisites
- Install @psu/components, @psu/utils, @psu/palette
- Install node-sass and node-sass-tilde-importer as Dev Dependencies
- Application must be setup to use scss and recommended to adhere to the following scss styling structure
- If not already existing, you must have the following files within the src directory at the **_root of the application_**
- `_app-theme.scss`: Import all application, internal application libraries, or external libraries that have theming here and include them within a mixin named `@mixin app-theme($theme)`.
- `_theme.scss`: Set up the primary application theme variables here. This file should import the theme palette from `@psu/palette` and set up the base theme colors. It should follow the structure from the snippet below
```scss
@import '~@psu/palette/beaver-blue';
// Setting Theme Colors
$primaryPalette: mat-palette($primary, 700, 50, 900);
$secondaryPalette: mat-palette($secondary);
$errorPalette: mat-palette($error);
$appBackground: #f5f5f5;
```
- `_typography.scss`: Set up the application typography variables here. Follow the structure from the snippet below. More information can be found in the [typography section](https://git.psu.edu/ais-swe/ux/theme-lib#typography) of the theme utils readme.
```scss
// Setting Typography for all themes
$titleFont: 'Roboto Slab';
$bodyFont: 'Open Sans';
$titleRegularWeight: 400;
$titleBoldWeight: 700;
$bodyRegularWeight: 400;
$bodyBoldWeight: 600;
```
- Import each of the above files into `styles.scss` and follow instructions for [theme generation](https://git.psu.edu/ais-swe/ux/theme-lib#theming) and [typography generation](https://git.psu.edu/ais-swe/ux/theme-lib#typography) from theme utils readme.
## Application-Specific Setup
1. Copy the following snippet into a new file named `build-themes.sh` and place in the `tools` directory. The tools directory should be located at the root of the project.
```sh
#!/bin/bash
DEST_PATH=apps/ui/src/assets
INPUT_PATH=$DEST_PATH/alternate-themes/
echo Building custom theme scss files.
# Get the files
FILES=$(find apps/ui/src/assets/alternate-themes -name "*.scss")
for FILE in $FILES
do
FILENAME=${FILE#$INPUT_PATH}
BASENAME=${FILENAME%.scss}
$(npm bin)/node-sass --importer=node_modules/node-sass-tilde-importer $FILE > $DEST_PATH/$BASENAME.css
done
echo Finished building CSS.
```
2. Create the `alternate-themes` directory at the path `apps/ui/src/assets`. This is where you will add your alternate theme scss files. These scss files should follow the following structure: