Skip to content

Commit e3e0c6b

Browse files
authored
build: improve dev app appearance toggles (#27446)
Currently we have some toggles to change the appearance of the dev app (e.g. changing the theme, updating the direction). It has a few problems: 1. We have a lot of toggles so we're starting to run out of space in the toolbar. 2. The state of some of the toggles is saved in `localStorage` while for others it isn't. 3. The code is a bit messy since we were adding these toggles ad-hoc. These changes address the issues by: 1. Switching all the buttons to icons which take up less space. 2. Having a centralized way to save the state to `localStorage` so we don't need to do it on a case-by-case basis. 3. Cleaning up the logic a bit. I've also added a dedicated button for toggling the tokens theming API, because currently it's done through a query parameter which isn't easy to discover.
1 parent cbac7f6 commit e3e0c6b

File tree

7 files changed

+184
-120
lines changed

7 files changed

+184
-120
lines changed

src/dev-app/dev-app.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Component, inject, ViewEncapsulation} from '@angular/core';
10-
import {ActivatedRoute, RouterModule} from '@angular/router';
9+
import {Component, ViewEncapsulation} from '@angular/core';
10+
import {RouterModule} from '@angular/router';
1111
import {DevAppLayout} from './dev-app/dev-app-layout';
1212

1313
/** Root component for the dev-app demos. */
@@ -18,14 +18,4 @@ import {DevAppLayout} from './dev-app/dev-app-layout';
1818
standalone: true,
1919
imports: [DevAppLayout, RouterModule],
2020
})
21-
export class DevApp {
22-
route = inject(ActivatedRoute);
23-
24-
constructor() {
25-
this.route.queryParams.subscribe(q => {
26-
(document.querySelector('#theme-styles') as any).href = q.hasOwnProperty('tokenapi')
27-
? 'theme-token-api.css'
28-
: 'theme.css';
29-
});
30-
}
31-
}
21+
export class DevApp {}

src/dev-app/dev-app/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ ng_module(
1818
"//src/material/list",
1919
"//src/material/sidenav",
2020
"//src/material/toolbar",
21+
"//src/material/tooltip",
2122
"@npm//@angular/router",
2223
],
2324
)

src/dev-app/dev-app/dev-app-layout.html

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
be one level above the density class in the DOM. At the same time, we want the density to apply
4444
to the toolbar while always keeping it in LTR at the same time.
4545
-->
46-
<main [attr.dir]="dir.value" [ngClass]="getDensityClass()" class="demo-main">
46+
<main [attr.dir]="state.direction" [ngClass]="getDensityClass()" class="demo-main">
4747
<!-- The toolbar should always be in the LTR direction -->
4848
<mat-toolbar color="primary" dir="ltr">
4949
<button mat-icon-button (click)="navigation.open('mouse')">
@@ -52,28 +52,53 @@
5252
<div class="demo-toolbar">
5353
<h1>Angular Material Demos</h1>
5454
<div class="demo-config-buttons">
55-
<button mat-icon-button (click)="toggleFullscreen()" title="Toggle fullscreen">
55+
<button
56+
mat-icon-button
57+
(click)="toggleFullscreen()"
58+
matTooltip="Toggle fullscreen">
5659
<mat-icon>fullscreen</mat-icon>
5760
</button>
58-
<button mat-button (click)="toggleAnimations()">
59-
{{animationsDisabled ? 'Enable' : 'Disable'}} animations
61+
<button
62+
mat-icon-button
63+
(click)="toggleTokens()"
64+
[matTooltip]="state.tokensEnabled ? 'Disable tokens' : 'Enable tokens'">
65+
<mat-icon>brush</mat-icon>
6066
</button>
61-
<button mat-button (click)="isDark = !isDark">
62-
{{isDark ? 'Light' : 'Dark'}} theme
67+
<button
68+
mat-icon-button
69+
(click)="toggleAnimations()"
70+
[matTooltip]="state.animations ? 'Disable animations' : 'Enable animations'">
71+
<mat-icon>animation</mat-icon>
6372
</button>
64-
<button mat-button (click)="rippleOptions.disabled = !rippleOptions.disabled">
65-
{{rippleOptions.disabled ? 'Enable' : 'Disable'}} ripples
73+
<button
74+
mat-icon-button
75+
(click)="toggleTheme()"
76+
[matTooltip]="state.darkTheme ? 'Switch to light theme' : 'Switch to dark theme'">
77+
<mat-icon>dark_mode</mat-icon>
6678
</button>
67-
<button mat-button (click)="toggleStrongFocus()">
68-
{{strongFocus ? 'Disable strong focus' : 'Enable strong focus'}}
79+
<button
80+
mat-icon-button
81+
(click)="toggleRippleDisabled()"
82+
[matTooltip]="state.rippleDisabled ? 'Enable ripples' : 'Disable ripples'">
83+
<mat-icon>waves</mat-icon>
6984
</button>
70-
<button mat-button (click)="dir.value = (dir.value === 'rtl' ? 'ltr' : 'rtl')"
71-
title="Toggle between RTL and LTR">
72-
{{dir.value.toUpperCase()}}
85+
<button
86+
mat-icon-button
87+
(click)="toggleStrongFocus()"
88+
[matTooltip]="state.strongFocusEnabled ? 'Disable strong focus' : 'Enable strong focus'">
89+
<mat-icon>accessibility</mat-icon>
7390
</button>
74-
<button mat-button (click)="selectNextDensity()"
75-
title="Use next density scale: {{densityScales[getNextDensityIndex()]}}">
76-
Density scale: {{densityScales[this.currentDensityIndex]}}
91+
<button
92+
mat-icon-button
93+
(click)="toggleDirection()"
94+
[matTooltip]="state.direction === 'rtl' ? 'Switch to LTR' : 'Switch to RTL'">
95+
<mat-icon>keyboard_tab_rtl</mat-icon>
96+
</button>
97+
<button
98+
mat-icon-button
99+
(click)="toggleDensity()"
100+
[matTooltip]="'Density: ' + state.density">
101+
<mat-icon>grid_on</mat-icon>
77102
</button>
78103
</div>
79104
</div>

src/dev-app/dev-app/dev-app-layout.ts

Lines changed: 55 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,19 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Directionality} from '@angular/cdk/bidi';
109
import {ChangeDetectorRef, Component, ElementRef, Inject, ViewEncapsulation} from '@angular/core';
11-
12-
import {DevAppDirectionality} from './dev-app-directionality';
13-
import {DevAppRippleOptions} from './ripple-options';
1410
import {CommonModule, DOCUMENT} from '@angular/common';
11+
import {Direction, Directionality} from '@angular/cdk/bidi';
1512
import {MatSidenavModule} from '@angular/material/sidenav';
1613
import {MatListModule} from '@angular/material/list';
1714
import {MatButtonModule} from '@angular/material/button';
1815
import {RouterModule} from '@angular/router';
1916
import {MatIconModule} from '@angular/material/icon';
2017
import {MatToolbarModule} from '@angular/material/toolbar';
21-
22-
const isDarkThemeKey = 'ANGULAR_COMPONENTS_DEV_APP_DARK_THEME';
23-
24-
export const ANIMATIONS_STORAGE_KEY = 'ANGULAR_COMPONENTS_ANIMATIONS_DISABLED';
18+
import {MatTooltipModule} from '@angular/material/tooltip';
19+
import {DevAppDirectionality} from './dev-app-directionality';
20+
import {DevAppRippleOptions} from './ripple-options';
21+
import {getAppState, setAppState} from './dev-app-state';
2522

2623
/** Root component for the dev-app demos. */
2724
@Component({
@@ -37,13 +34,12 @@ export const ANIMATIONS_STORAGE_KEY = 'ANGULAR_COMPONENTS_ANIMATIONS_DISABLED';
3734
MatListModule,
3835
MatSidenavModule,
3936
MatToolbarModule,
37+
MatTooltipModule,
4038
RouterModule,
4139
],
4240
})
4341
export class DevAppLayout {
44-
readonly darkThemeClass = 'demo-unicorn-dark-theme';
45-
_isDark = false;
46-
strongFocus = false;
42+
state = getAppState();
4743
navItems = [
4844
{name: 'Examples', route: '/examples'},
4945
{name: 'CDK Dialog', route: '/cdk-dialog'},
@@ -126,108 +122,79 @@ export class DevAppLayout {
126122
{name: 'Legacy Tooltip', route: '/legacy-tooltip'},
127123
];
128124

129-
/** Currently selected density scale based on the index. */
130-
currentDensityIndex = 0;
131-
132125
/** List of possible global density scale values. */
133-
densityScales = [0, -1, -2, -3, 'minimum', 'maximum'];
134-
135-
/** Whether animations are disabled. */
136-
animationsDisabled = localStorage.getItem(ANIMATIONS_STORAGE_KEY) === 'true';
126+
private _densityScales = [0, -1, -2, -3, 'minimum', 'maximum'];
137127

138128
constructor(
139129
private _element: ElementRef<HTMLElement>,
140-
public rippleOptions: DevAppRippleOptions,
141-
@Inject(Directionality) public dir: DevAppDirectionality,
142-
cdr: ChangeDetectorRef,
130+
private _rippleOptions: DevAppRippleOptions,
131+
@Inject(Directionality) private _dir: DevAppDirectionality,
132+
private _changeDetectorRef: ChangeDetectorRef,
143133
@Inject(DOCUMENT) private _document: Document,
144134
) {
145-
dir.change.subscribe(() => cdr.markForCheck());
146-
try {
147-
const isDark = localStorage.getItem(isDarkThemeKey);
148-
if (isDark != null) {
149-
// We avoid calling the setter and apply the themes directly here.
150-
// This avoids writing the same value, that we just read, back to localStorage.
151-
this._isDark = isDark === 'true';
152-
this.updateThemeClass(this._isDark);
153-
}
154-
} catch (error) {
155-
console.error(`Failed to read ${isDarkThemeKey} from localStorage: `, error);
156-
}
135+
this.toggleTheme(this.state.darkTheme);
136+
this.toggleStrongFocus(this.state.strongFocusEnabled);
137+
this.toggleDensity(Math.max(this._densityScales.indexOf(this.state.density), 0));
138+
this.toggleRippleDisabled(this.state.rippleDisabled);
139+
this.toggleDirection(this.state.direction);
157140
}
158141

159-
get isDark(): boolean {
160-
return this._isDark;
161-
}
162-
163-
set isDark(value: boolean) {
164-
// Noop if the value is the same as is already set.
165-
if (value !== this._isDark) {
166-
this._isDark = value;
167-
this.updateThemeClass(this._isDark);
168-
169-
try {
170-
localStorage.setItem(isDarkThemeKey, String(value));
171-
} catch (error) {
172-
console.error(`Failed to write ${isDarkThemeKey} to localStorage: `, error);
173-
}
174-
}
142+
toggleTheme(value = !this.state.darkTheme) {
143+
this.state.darkTheme = value;
144+
this._document.body.classList.toggle('demo-unicorn-dark-theme', value);
145+
setAppState(this.state);
175146
}
176147

177148
toggleFullscreen() {
178-
// Cast to `any`, because the typings don't include the browser-prefixed methods.
179-
const elem = this._element.nativeElement.querySelector('.demo-content') as any;
180-
if (elem.requestFullscreen) {
181-
elem.requestFullscreen();
182-
} else if (elem.webkitRequestFullScreen) {
183-
elem.webkitRequestFullScreen();
184-
} else if (elem.mozRequestFullScreen) {
185-
elem.mozRequestFullScreen();
186-
} else if (elem.msRequestFullScreen) {
187-
elem.msRequestFullScreen();
188-
}
149+
this._element.nativeElement.querySelector('.demo-content')?.requestFullscreen();
189150
}
190151

191-
updateThemeClass(isDark?: boolean) {
192-
if (isDark) {
193-
this._document.body.classList.add(this.darkThemeClass);
194-
} else {
195-
this._document.body.classList.remove(this.darkThemeClass);
196-
}
152+
toggleStrongFocus(value = !this.state.strongFocusEnabled) {
153+
this.state.strongFocusEnabled = value;
154+
this._document.body.classList.toggle('demo-strong-focus', value);
155+
setAppState(this.state);
197156
}
198157

199-
toggleStrongFocus() {
200-
const strongFocusClass = 'demo-strong-focus';
201-
202-
this.strongFocus = !this.strongFocus;
158+
toggleAnimations() {
159+
this.state.animations = !this.state.animations;
160+
setAppState(this.state);
161+
location.reload();
162+
}
203163

204-
if (this.strongFocus) {
205-
this._document.body.classList.add(strongFocusClass);
206-
} else {
207-
this._document.body.classList.remove(strongFocusClass);
164+
toggleDensity(index?: number) {
165+
if (index == null) {
166+
index = (this._densityScales.indexOf(this.state.density) + 1) % this._densityScales.length;
208167
}
168+
169+
this.state.density = this._densityScales[index];
170+
setAppState(this.state);
209171
}
210172

211-
toggleAnimations() {
212-
localStorage.setItem(ANIMATIONS_STORAGE_KEY, !this.animationsDisabled + '');
213-
location.reload();
173+
toggleRippleDisabled(value = !this.state.rippleDisabled) {
174+
this._rippleOptions.disabled = this.state.rippleDisabled = value;
175+
setAppState(this.state);
214176
}
215177

216-
/** Gets the index of the next density scale that can be selected. */
217-
getNextDensityIndex() {
218-
return (this.currentDensityIndex + 1) % this.densityScales.length;
178+
toggleDirection(value: Direction = this.state.direction === 'ltr' ? 'rtl' : 'ltr') {
179+
if (value !== this._dir.value) {
180+
this._dir.value = this.state.direction = value;
181+
this._changeDetectorRef.markForCheck();
182+
setAppState(this.state);
183+
}
219184
}
220185

221-
/** Selects the next possible density scale. */
222-
selectNextDensity() {
223-
this.currentDensityIndex = this.getNextDensityIndex();
186+
toggleTokens(value = !this.state.tokensEnabled) {
187+
// We need to diff this one since it's a bit more expensive to toggle.
188+
if (value !== this.state.tokensEnabled) {
189+
(document.getElementById('theme-styles') as HTMLLinkElement).href = value
190+
? 'theme-token-api.css'
191+
: 'theme.css';
192+
this.state.tokensEnabled = value;
193+
setAppState(this.state);
194+
}
224195
}
225196

226-
/**
227-
* Updates the density classes on the host element. Applies a unique class for
228-
* a given density scale, so that the density styles are conditionally applied.
229-
*/
230197
getDensityClass() {
231-
return `demo-density-${this.densityScales[this.currentDensityIndex]}`;
198+
return `demo-density-${this.state.density}`;
232199
}
233200
}

src/dev-app/dev-app/dev-app-state.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Direction} from '@angular/cdk/bidi';
10+
11+
const KEY = 'MAT_DEV_APP_STATE';
12+
13+
/** State of the appearance of the dev app. */
14+
export interface DevAppState {
15+
density: string | number;
16+
animations: boolean;
17+
darkTheme: boolean;
18+
rippleDisabled: boolean;
19+
strongFocusEnabled: boolean;
20+
tokensEnabled: boolean;
21+
direction: Direction;
22+
}
23+
24+
/** Gets the current appearance state of the dev app. */
25+
export function getAppState(): DevAppState {
26+
let value: DevAppState | null = null;
27+
28+
// Needs a try/catch since some browsers throw an error when accessing in incognito.
29+
try {
30+
const storageValue = localStorage.getItem(KEY);
31+
32+
if (storageValue) {
33+
value = JSON.parse(storageValue);
34+
}
35+
} catch {}
36+
37+
if (!value) {
38+
value = {
39+
density: 0,
40+
animations: true,
41+
darkTheme: false,
42+
rippleDisabled: false,
43+
strongFocusEnabled: false,
44+
tokensEnabled: false,
45+
direction: 'ltr',
46+
};
47+
48+
saveToStorage(value);
49+
}
50+
51+
return value;
52+
}
53+
54+
/** Saves the state of the dev app apperance in local storage. */
55+
export function setAppState(newState: DevAppState): void {
56+
const currentState = getAppState();
57+
const keys = Object.keys(currentState) as (keyof DevAppState)[];
58+
59+
// Only write to storage if something actually changed.
60+
for (const key of keys) {
61+
if (currentState[key] !== newState[key]) {
62+
saveToStorage(newState);
63+
break;
64+
}
65+
}
66+
}
67+
68+
function saveToStorage(value: DevAppState): void {
69+
// Needs a try/catch since some browsers throw an error when accessing in incognito.
70+
try {
71+
localStorage.setItem(KEY, JSON.stringify(value));
72+
} catch {}
73+
}

src/dev-app/index.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
<link rel="preconnect" href="https://fonts.gstatic.com">
1111
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
1212
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
13-
<link href="theme.css" rel="stylesheet" id="theme-styles">
1413

1514
<!-- FontAwesome for mat-icon demo. -->
1615
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">

0 commit comments

Comments
 (0)