Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion aio/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,17 @@
"src/google385281288605d160.html"
],
"styles": [
"src/styles/main.scss"
"src/styles/main.scss",
{
"inject": false,
"input": "src/styles/custom-themes/dark-theme.scss",
"bundleName": "dark-theme"
},
{
"inject": false,
"input": "src/styles/custom-themes/light-theme.scss",
"bundleName": "light-theme"
}
],
"scripts": [],
"budgets": [
Expand Down
4 changes: 1 addition & 3 deletions aio/content/marketing/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
<section id="intro">

<!-- LOGO -->
<div class="hero-logo">
<img src="assets/images/logos/angular/angular.svg" alt="Angular">
</div>
<div class="hero-logo"></div>

<!-- CONTAINER -->
<div class="homepage-container">
Expand Down
4 changes: 2 additions & 2 deletions aio/content/marketing/presskit.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ <h3>Full Color Logo</h3>

<div class="presskit-icon-item">
<div class="presskit-image-container">
<img src="assets/images/logos/angular/angular_solidBlack.svg" alt="Black logo Angular">
<img src="assets/images/logos/angular/angular_solidBlack.svg" class="transparent-img" alt="Black logo Angular">
</div>
<div>
<h3>One Color Logo</h3>
Expand All @@ -54,7 +54,7 @@ <h3>One Color Logo</h3>

<div class="presskit-icon-item">
<div class="presskit-image-container">
<img src="assets/images/logos/angular/angular_whiteTransparent.svg" class="transparent-img" alt="Transparent logo Angular">
<img src="assets/images/logos/angular/angular_whiteTransparent.svg" class="transparent-img-inverse" alt="Transparent logo Angular">
</div>
<div>
<h3>One Color Inverse Logo</h3>
Expand Down
1 change: 1 addition & 0 deletions aio/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,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-toggle></aio-theme-toggle>
<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>
Expand Down
2 changes: 2 additions & 0 deletions aio/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { SharedModule } from 'app/shared/shared.module';
import { SwUpdatesModule } from 'app/sw-updates/sw-updates.module';

import {environment} from '../environments/environment';
import { ThemeToggleComponent } from './shared/theme-picker/theme-toggle.component';

// These are the hardcoded inline svg sources to be used by the `<mat-icon>` component.
// tslint:disable: max-line-length
Expand Down Expand Up @@ -170,6 +171,7 @@ export const svgIconProviders = [
SearchBoxComponent,
NotificationComponent,
TopMenuComponent,
ThemeToggleComponent
],
providers: [
Deployment,
Expand Down
92 changes: 92 additions & 0 deletions aio/src/app/shared/theme-picker/theme-toggle.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { ThemeStorage, ThemeToggleComponent } from './theme-toggle.component';

class FakeThemeStorage implements ThemeStorage {
fakeStorage: string | null = null;

getThemePreference(): string | null {
return this.fakeStorage;
}

setThemePreference(isDark: boolean): void {
this.fakeStorage = String(isDark);
}
}

let themeStorage: ThemeStorage;

// Verify that FakeThemeStorage behaves like ThemeStorage would
describe('FakeThemeStorage', () => {
beforeEach(() => {
themeStorage = new FakeThemeStorage();
});

it('should have null stored initially', () => {
expect(themeStorage.getThemePreference()).toBeNull();
});

it('should store true as a string if isDark is true', () => {
themeStorage.setThemePreference(true);
expect(themeStorage.getThemePreference()).toBe('true');
});

it('should store false as a string if isDark is false', () => {
themeStorage.setThemePreference(false);
expect(themeStorage.getThemePreference()).toBe('false');
});
});


let component: ThemeToggleComponent;
let fixture: ComponentFixture<ThemeToggleComponent>;
let debugElement: DebugElement;

describe('ThemeToggleComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ThemeToggleComponent ],
providers: [ { provide: ThemeStorage, useClass: FakeThemeStorage } ],
})
.compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(ThemeToggleComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should show toggle button', () => {
expect(getToggleButton()).toBeDefined();
});

it('should toggle between light and dark mode', () => {
expect(component.getThemeName()).toBe('light');
getToggleButton().click();
expect(component.getThemeName()).toBe('dark');
});

it('should have the correct next theme', () => {
component.toggleTheme();
expect(component.getThemeName()).toBe('dark');
component.toggleTheme();
expect(component.getThemeName()).toBe('light');
});

it('should have the correct aria-label', () => {
expect(component.getToggleLabel()).toBe(`Switch to dark mode`);
component.toggleTheme();
expect(component.getToggleLabel()).toBe(`Switch to light mode`);
});
});

function getToggleButton(): HTMLButtonElement {
return debugElement.query(By.css('button')).nativeElement;
}
91 changes: 91 additions & 0 deletions aio/src/app/shared/theme-picker/theme-toggle.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { DOCUMENT } from '@angular/common';
import { Component, Inject, Injectable } from '@angular/core';

/** Injectable facade around localStorage for theme preference to make testing easier. */
@Injectable({ providedIn: 'root' })
export class ThemeStorage {
getThemePreference(): string | null {
// Wrap localStorage access in try/catch because user agents can block localStorage. If it is
// blocked, we treat it as if no preference was previously stored.
try {
return localStorage.getItem('aio-theme');
} catch {
return null;
}
}

setThemePreference(isDark: boolean): void {
// Wrap localStorage access in try/catch because user agents can block localStorage. If it
// fails, we persist nothing.
try {
localStorage.setItem('aio-theme', String(isDark));
} catch { }
}
}

@Component({
selector: 'aio-theme-toggle',
template: `
<button mat-icon-button type="button" (click)="toggleTheme()"
[title]="getToggleLabel()" [attr.aria-label]="getToggleLabel()">
<mat-icon>
{{ isDark ? 'light' : 'dark' }}_mode
</mat-icon>
</button>
`,
})
export class ThemeToggleComponent {
isDark = false;

constructor(@Inject(DOCUMENT) private document: Document, private readonly themeStorage: ThemeStorage) {
this.initializeThemeFromPreferences();
}

toggleTheme(): void {
this.isDark = !this.isDark;
this.updateRenderedTheme();
}

private initializeThemeFromPreferences(): void {
// Check whether there's an explicit preference in localStorage.
const storedPreference = this.themeStorage.getThemePreference();

// If we do have a preference in localStorage, use that. Otherwise,
// initialize based on the prefers-color-scheme media query.
if (storedPreference) {
this.isDark = storedPreference === 'true';
} else {
this.isDark = matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add an event listener to sync with theme with the System? In most cases, the theme changes based on the time of the day.

Using local storage in this case will also cause the theme not to be synced with the system on refresh.

I don't know if we want to go down this route but IMHO theming has 3 states and not 2.

  • Auto
  • Dark
  • Light

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In most cases, the theme changes based on the time of the day

What is this based on? While Apple and Android devices support this option, but I think the default is "light" and people can choose either "dark" or "auto" (manually setting up a schedule on Android). On Windows "auto" is not an option. ChromeOS doesn't support system themes yet.

Using local storage in this case will also cause the theme not to be synced with the system on refresh.

That was the intent- if a user never touches the theme toggle, it will respect their system setting. If they manually change the theme, it will remember their choice. I suppose we could drop the part about remembering the user's choice, but my experience has always been that I want sites to retain this preference (especially since not all OSes support dark mode yet).

IMO adding extra code to react to changes in the system theme is overkill.

@AleksanderBodurri the media issue there is due to the critical CSS inlining built into Angular CLI. I see two options:

  • Turn off that feature
  • Change from using the media attribute to having a single link element that does something like this:
@import url("light-theme.css") (prefers-color-scheme: light);
@import url("dark-theme.css") (prefers-color-scheme: dark);

Both of these approaches effectively do the same thing- force the CSS into a separate load. I don't see any way around the inherent limitation of media queries being a purely client-side construct here.

}

const initialTheme = this.document.querySelector('#aio-initial-theme');
if (initialTheme) {
// todo(aleksanderbodurri): change to initialTheme.remove() when ie support is dropped
initialTheme.parentElement?.removeChild(initialTheme);
}

const themeLink = this.document.createElement('link');
themeLink.id = 'aio-custom-theme';
themeLink.rel = 'stylesheet';
themeLink.href = `${this.getThemeName()}-theme.css`;
this.document.head.appendChild(themeLink);
}

getThemeName(): string {
return this.isDark ? 'dark' : 'light';
}

getToggleLabel(): string {
return `Switch to ${this.isDark ? 'light' : 'dark'} mode`;
}

private updateRenderedTheme(): void {
// If we're calling this method, the user has explicitly interacted with the theme toggle.
const customLinkElement = this.document.getElementById('aio-custom-theme') as HTMLLinkElement | null;
if (customLinkElement) {
customLinkElement.href = `${this.getThemeName()}-theme.css`;
}

this.themeStorage.setThemePreference(this.isDark);
}
}
11 changes: 7 additions & 4 deletions aio/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons&display=block">
<!-- -->

<style id="aio-initial-theme">
@import url("light-theme.css") (prefers-color-scheme: light);
@import url("dark-theme.css") (prefers-color-scheme: dark);
</style>

<link rel="manifest" href="pwa-manifest.json">
<meta name="theme-color" content="#1976d2">
<meta name="apple-mobile-web-app-capable" content="yes">
Expand Down Expand Up @@ -97,14 +102,12 @@
<noscript>
<div class="background-sky hero"></div>
<section id="intro" style="text-shadow: 1px 1px #1976d2;">
<div class="hero-logo">
<img src="assets/images/logos/angular/angular.svg" width="250" height="250" alt="Angular">
</div>
<div class="hero-logo"></div>
<div class="homepage-container">
<div class="hero-headline">The modern web<br>developer's platform</div>
</div>
</section>
<h2 style="color: red; margin-top: 40px; position: relative; text-align: center; text-shadow: 1px 1px #fafafa;">
<h2 style="color: red; margin-top: 40px; position: relative; text-align: center; text-shadow: 1px 1px #fafafa; border-top: none;">
<b><i>This website requires JavaScript.</i></b>
</h2>
</noscript>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
BASE STYLES
============================== */

@import 'typography';
@forward 'typography';
52 changes: 52 additions & 0 deletions aio/src/styles/0-base/_typography-theme.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
@use '../constants';

@mixin theme($theme) {
$is-dark-theme: map-get($theme, is-dark);

body {
color: if($is-dark-theme, constants.$offwhite, constants.$darkgray);
}

h1,
h2,
h3,
h4,
h5,
h6 {
color: if($is-dark-theme, constants.$offwhite, constants.$deepgray);
}

h6 {
color: if($is-dark-theme, constants.$offwhite, constants.$mediumgray);
}

h2 {
border-top: 1px solid if($is-dark-theme, constants.$mediumgray, constants.$lightgray);
}

p,
ol,
ul,
ol,
li,
input,
a {
color: if($is-dark-theme, constants.$white, constants.$darkgray);
}

.app-toolbar a {
color: constants.$white;
}

code {
color: if($is-dark-theme, constants.$white, constants.$darkgray);
}

.sidenav-content a {
color: if($is-dark-theme, constants.$lightblue, constants.$blue);
}

.error-text {
color: constants.$brightred;
}
}
Loading