feat(docs-infra): port over material io theming logic to angular io (#41129)

brings in theming tools from material io into angular io in preparation of implementing darkmode

PR Close #41129
This commit is contained in:
AleksanderBodurri 2021-02-05 21:18:31 -05:00 committed by Andrew Kushnir
parent e73fef4d0b
commit 85a627f741
18 changed files with 262 additions and 10 deletions

View File

@ -41,7 +41,17 @@
"src/google385281288605d160.html"
],
"styles": [
"src/styles/main.scss"
"src/styles/main.scss",
{
"inject": false,
"input": "src/styles/custom-themes/light.scss",
"bundleName": "assets/light"
},
{
"inject": false,
"input": "src/styles/custom-themes/dark.scss",
"bundleName": "assets/dark"
},
],
"scripts": [],
"budgets": [

View File

@ -1,5 +1,4 @@
<div id="top-of-page"></div>
<div *ngIf="isFetching" class="progress-bar-container">
<mat-progress-bar mode="indeterminate" color="warn"></mat-progress-bar>
</div>
@ -24,6 +23,7 @@
</a>
<aio-top-menu *ngIf="showTopMenu" [nodes]="topMenuNodes" [currentNode]="currentNodes?.TopBar"></aio-top-menu>
<aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event)"></aio-search-box>
<aio-theme-picker></aio-theme-picker>
<div class="toolbar-external-icons-container">
<a href="https://twitter.com/angular" title="Twitter" aria-label="Angular on twitter">
<mat-icon svgIcon="logos:twitter"></mat-icon>

View File

@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SearchResultsComponent } from './search-results/search-results.component';
import { SelectComponent } from './select/select.component';
import { ThemePickerComponent } from './theme-picker/theme-picker.component';
@NgModule({
imports: [
@ -9,11 +10,13 @@ import { SelectComponent } from './select/select.component';
],
exports: [
SearchResultsComponent,
SelectComponent
SelectComponent,
ThemePickerComponent
],
declarations: [
SearchResultsComponent,
SelectComponent
SelectComponent,
ThemePickerComponent
]
})
export class SharedModule {}

View File

@ -0,0 +1,3 @@
// Taken from Angular Material docs repo
export * from './style-manager';

View File

@ -0,0 +1,56 @@
// Taken from Angular Material docs repo
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {inject, TestBed} from '@angular/core/testing';
import {StyleManager} from './style-manager';
describe('StyleManager', () => {
let styleManager: StyleManager;
beforeEach(() => TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [StyleManager]
}));
beforeEach(inject([StyleManager], (sm: StyleManager) => {
styleManager = sm;
}));
afterEach(() => {
const links = document.head.querySelectorAll('link');
for (const link of Array.prototype.slice.call(links)) {
if (link.className.includes('style-manager-')) {
document.head.removeChild(link);
}
}
});
it('should add stylesheet to head', () => {
styleManager.setStyle('test', 'test.css');
const styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement;
expect(styleEl).not.toBeNull();
expect(styleEl.href.endsWith('test.css')).toBe(true);
});
it('should change existing stylesheet', () => {
styleManager.setStyle('test', 'test.css');
const styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement;
expect(styleEl).not.toBeNull();
expect(styleEl.href.endsWith('test.css')).toBe(true);
styleManager.setStyle('test', 'new.css');
expect(styleEl.href.endsWith('new.css')).toBe(true);
});
it('should remove existing stylesheet', () => {
styleManager.setStyle('test', 'test.css');
let styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement;
expect(styleEl).not.toBeNull();
expect(styleEl.href.endsWith('test.css')).toBe(true);
styleManager.removeStyle('test');
styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement;
expect(styleEl).toBeNull();
});
});

View File

@ -0,0 +1,48 @@
// Taken from Angular Material docs repo
import {Injectable} from '@angular/core';
/**
* Class for managing stylesheets. Stylesheets are loaded into named slots so that they can be
* removed or changed later.
*/
@Injectable()
export class StyleManager {
/**
* Set the stylesheet with the specified key.
*/
setStyle(key: string, href: string) {
getLinkElementForKey(key).setAttribute('href', href);
}
/**
* Remove the stylesheet with the specified key.
*/
removeStyle(key: string) {
const existingLinkElement = getExistingLinkElementByKey(key);
if (existingLinkElement) {
document.head.removeChild(existingLinkElement);
}
}
}
function getLinkElementForKey(key: string) {
return getExistingLinkElementByKey(key) || createLinkElementWithKey(key);
}
function getExistingLinkElementByKey(key: string) {
return document.head.querySelector(`link[rel="stylesheet"].${getClassNameForKey(key)}`);
}
function createLinkElementWithKey(key: string) {
const linkEl = document.createElement('link');
linkEl.setAttribute('rel', 'stylesheet');
linkEl.classList.add(getClassNameForKey(key));
document.head.appendChild(linkEl);
return linkEl;
}
function getClassNameForKey(key: string) {
return `style-manager-${key}`;
}

View File

@ -0,0 +1,3 @@
<button (click)="selectTheme({ 'light': 'dark', 'dark': 'light' }[currentTheme])">
Change theme (Current: {{ currentTheme }})
</button>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ThemePickerComponent } from './theme-picker.component';
describe('ThemePickerComponent', () => {
let component: ThemePickerComponent;
let fixture: ComponentFixture<ThemePickerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ThemePickerComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ThemePickerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,36 @@
import { Component, OnInit } from '@angular/core';
import { StyleManager } from './style-manager';
import { ThemeStorage } from './theme-storage/theme-storage';
@Component({
selector: 'aio-theme-picker',
templateUrl: './theme-picker.component.html',
providers: [ThemeStorage, StyleManager]
})
export class ThemePickerComponent implements OnInit {
currentTheme: 'light' | 'dark' = 'light';
constructor(private _themeStorage: ThemeStorage, private _styleManager: StyleManager) { }
ngOnInit(): void {
const theme = this._themeStorage.getStoredThemeName();
if (theme) {
this.selectTheme(theme);
}
}
selectTheme(theme: string) {
this.currentTheme = theme as 'light' | 'dark';
if (theme === 'light') {
this._styleManager.removeStyle('theme');
} else {
this._styleManager.setStyle('theme', `assets/${theme}.css`);
}
if (this.currentTheme) {
this._themeStorage.storeTheme(this.currentTheme);
}
}
}

View File

@ -0,0 +1,3 @@
// Taken from Angular Material docs repo
export * from './theme-storage';

View File

@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
@Injectable()
export class ThemeStorage {
static storageKey = 'io-theme-storage-current-name';
storeTheme(theme: 'light' | 'dark') {
try {
window.localStorage[ThemeStorage.storageKey] = theme;
} catch { }
}
getStoredThemeName(): 'light' | 'dark' | null {
try {
return window.localStorage[ThemeStorage.storageKey] || null;
} catch {
return null;
}
}
clearStorage(): void {
try {
window.localStorage.removeItem(ThemeStorage.storageKey);
} catch { }
}
}

View File

@ -6,7 +6,7 @@
@import 'doc-viewer';
@import 'footer';
@import 'layout-global';
@import 'marketing-layout';
@import 'marketing-layout/marketing-layout';
@import 'not-found';
@import 'sidenav';
@import 'top-menu';

View File

@ -0,0 +1,13 @@
@mixin marketing-layout-theme($theme) {
$primary: map-get($theme, primary);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
.background-sky {
background-color: mat-color($primary, default);
background: linear-gradient(145deg, mat-color($primary, 900), mat-color($primary, 400));
color: mat-color($foreground, text);
}
}

View File

@ -183,11 +183,6 @@ section#intro {
}
// ANGULAR LINE
.background-sky {
background-color: $blue;
background: $bluegradient;
color: $white;
}
.home-row .card {
@include card(70%, auto);

View File

@ -0,0 +1,7 @@
@import '~@angular/material/theming';
@import '1-layouts/marketing-layout/marketing-layout-theme';
@mixin app-theme($theme) {
@include marketing-layout-theme($theme);
}

View File

@ -0,0 +1,10 @@
@import '../app-theme';
$ng-io-primary: mat-palette($mat-red, 700, 600, 800);
$ng-io-accent: mat-palette($mat-blue, 700, 600, 800);
$ng-io-warn: mat-palette($mat-blue);
$ng-io-theme: mat-dark-theme($ng-io-primary, $ng-io-accent, $ng-io-warn);
@include angular-material-theme($ng-io-theme);
@include app-theme($ng-io-theme);

View File

@ -0,0 +1,10 @@
@import '../app-theme';
$ng-io-primary: mat-palette($mat-blue, 700, 600, 800);
$ng-io-accent: mat-palette($mat-red, 700, 600, 800);
$ng-io-warn: mat-palette($mat-red);
$ng-io-theme: mat-light-theme($ng-io-primary, $ng-io-accent, $ng-io-warn);
@include angular-material-theme($ng-io-theme);
@include app-theme($ng-io-theme);

View File

@ -1,4 +1,5 @@
@import '~@angular/material/theming';
@import 'app-theme';
// Plus imports for other components in your app.
// Include the base styles for Angular Material core. We include this here so that you only
@ -21,3 +22,4 @@ $ng-io-theme: mat-light-theme($ng-io-primary, $ng-io-accent, $ng-io-warn);
// Alternatively, you can import and @include the theme mixins for each component
// that you are using.
@include angular-material-theme($ng-io-theme);
@include app-theme($ng-io-theme);