Skip to content

Commit adb892c

Browse files
authored
dark mode: button for toggling theme in header (#5096)
This change adds a button in the header that toggles different color theme modes in TensorBoard.
1 parent 05ef0a8 commit adb892c

18 files changed

+383
-22
lines changed

tensorboard/webapp/BUILD

+3
Original file line numberDiff line numberDiff line change
@@ -354,13 +354,15 @@ tf_svg_bundle(
354354
srcs = [
355355
"@com_google_material_design_icon//:arrow_downward_24px.svg",
356356
"@com_google_material_design_icon//:arrow_upward_24px.svg",
357+
"@com_google_material_design_icon//:brightness_6_24px.svg",
357358
"@com_google_material_design_icon//:bug_report_24px.svg",
358359
"@com_google_material_design_icon//:cancel_24px.svg",
359360
"@com_google_material_design_icon//:chevron_left_24px.svg",
360361
"@com_google_material_design_icon//:chevron_right_24px.svg",
361362
"@com_google_material_design_icon//:clear_24px.svg",
362363
"@com_google_material_design_icon//:close_24px.svg",
363364
"@com_google_material_design_icon//:content_copy_24px.svg",
365+
"@com_google_material_design_icon//:dark_mode_24px.svg",
364366
"@com_google_material_design_icon//:done_24px.svg",
365367
"@com_google_material_design_icon//:edit_24px.svg",
366368
"@com_google_material_design_icon//:error_24px.svg",
@@ -377,6 +379,7 @@ tf_svg_bundle(
377379
"@com_google_material_design_icon//:info_outline_24px.svg",
378380
"@com_google_material_design_icon//:keep_24px.svg",
379381
"@com_google_material_design_icon//:keep_outline_24px.svg",
382+
"@com_google_material_design_icon//:light_mode_24px.svg",
380383
"@com_google_material_design_icon//:line_weight_24px.svg",
381384
"@com_google_material_design_icon//:more_vert_24px.svg",
382385
"@com_google_material_design_icon//:notifications_none_24px.svg",

tensorboard/webapp/feature_flag/actions/feature_flag_actions.ts

+7
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,10 @@ export const partialFeatureFlagsLoaded = createAction(
3333
features: Partial<FeatureFlags>;
3434
}>()
3535
);
36+
37+
export const overrideEnableDarkModeChanged = createAction(
38+
'[FEATURE FLAG] Enable Dark Mode Override Changed',
39+
props<{
40+
enableDarkMode: boolean | null;
41+
}>()
42+
);

tensorboard/webapp/feature_flag/store/feature_flag_reducers.ts

+9
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ const reducer = createReducer<FeatureFlagState>(
3535
...features,
3636
},
3737
};
38+
}),
39+
on(actions.overrideEnableDarkModeChanged, (state, {enableDarkMode}) => {
40+
return {
41+
...state,
42+
flagOverrides: {
43+
...state.flagOverrides,
44+
enableDarkModeOverride: enableDarkMode,
45+
},
46+
};
3847
})
3948
);
4049

tensorboard/webapp/feature_flag/store/feature_flag_selectors.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,16 @@ export const getIsAutoDarkModeAllowed = createSelector(
6363
export const getDarkModeEnabled = createSelector(
6464
getFeatureFlags,
6565
(flags): boolean => {
66-
return flags.enableDarkMode;
66+
return flags.enableDarkModeOverride !== null
67+
? flags.enableDarkModeOverride
68+
: flags.defaultEnableDarkMode;
69+
}
70+
);
71+
72+
export const getEnableDarkModeOverride = createSelector(
73+
getFeatureFlags,
74+
(flags): boolean | null => {
75+
return flags.enableDarkModeOverride;
6776
}
6877
);
6978

tensorboard/webapp/feature_flag/store/feature_flag_selectors_test.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ describe('feature_flag_selectors', () => {
8585
let state = buildState(
8686
buildFeatureFlagState({
8787
defaultFlags: buildFeatureFlag({
88-
enableDarkMode: true,
88+
defaultEnableDarkMode: true,
8989
}),
9090
})
9191
);
@@ -94,10 +94,10 @@ describe('feature_flag_selectors', () => {
9494
state = buildState(
9595
buildFeatureFlagState({
9696
defaultFlags: buildFeatureFlag({
97-
enableDarkMode: false,
97+
defaultEnableDarkMode: false,
9898
}),
9999
flagOverrides: {
100-
enableDarkMode: true,
100+
defaultEnableDarkMode: true,
101101
},
102102
})
103103
);
@@ -106,10 +106,10 @@ describe('feature_flag_selectors', () => {
106106
state = buildState(
107107
buildFeatureFlagState({
108108
defaultFlags: buildFeatureFlag({
109-
enableDarkMode: false,
109+
defaultEnableDarkMode: false,
110110
}),
111111
flagOverrides: {
112-
enableDarkMode: false,
112+
defaultEnableDarkMode: false,
113113
},
114114
})
115115
);

tensorboard/webapp/feature_flag/store/feature_flag_store_config_provider.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export const initialState: FeatureFlagState = {
2121
isFeatureFlagsLoaded: false,
2222
defaultFlags: {
2323
isAutoDarkModeAllowed: true,
24-
enableDarkMode: false,
24+
defaultEnableDarkMode: false,
25+
enableDarkModeOverride: null,
2526
enabledColorGroup: false,
2627
enabledExperimentalPlugins: [],
2728
inColab: false,

tensorboard/webapp/feature_flag/testing.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export function buildFeatureFlag(
2020
): FeatureFlags {
2121
return {
2222
isAutoDarkModeAllowed: false,
23-
enableDarkMode: false,
23+
defaultEnableDarkMode: false,
24+
enableDarkModeOverride: null,
2425
enabledColorGroup: false,
2526
enabledExperimentalPlugins: [],
2627
inColab: false,

tensorboard/webapp/feature_flag/types.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ limitations under the License.
1414
==============================================================================*/
1515

1616
export interface FeatureFlags {
17-
// Whether user wants to use dark mode. It can be set via browser setting
18-
// (media query).
19-
enableDarkMode: boolean;
17+
// Whether user wants to use dark mode by default. It can be set via browser setting
18+
// (media query) or media query.
19+
defaultEnableDarkMode: boolean;
20+
// Specific user override to the default dark mode behavior. If `null`, we
21+
// will use the `defaultEnableDarkMode`.
22+
enableDarkModeOverride: boolean | null;
2023
// Whether the dark mode feature is enabled or disabled at the application
2124
// level. Temporary flag to gate the feature until it is more feature
2225
// complete (it is badly broken on Firefox). The feature is still available

tensorboard/webapp/header/BUILD

+11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ tf_sass_binary(
1717
tf_ng_module(
1818
name = "header",
1919
srcs = [
20+
"dark_mode_toggle_component.ts",
21+
"dark_mode_toggle_container.ts",
2022
"header_component.ts",
2123
"header_module.ts",
2224
"plugin_selector_component.ts",
@@ -30,14 +32,19 @@ tf_ng_module(
3032
"plugin_selector_component.ng.html",
3133
],
3234
deps = [
35+
"//tensorboard/webapp:selectors",
3336
"//tensorboard/webapp/angular:expect_angular_material_button",
3437
"//tensorboard/webapp/angular:expect_angular_material_icon",
38+
"//tensorboard/webapp/angular:expect_angular_material_menu",
3539
"//tensorboard/webapp/angular:expect_angular_material_select",
3640
"//tensorboard/webapp/angular:expect_angular_material_tabs",
3741
"//tensorboard/webapp/angular:expect_angular_material_toolbar",
3842
"//tensorboard/webapp/core",
3943
"//tensorboard/webapp/core/actions",
4044
"//tensorboard/webapp/core/store",
45+
"//tensorboard/webapp/feature_flag/actions",
46+
"//tensorboard/webapp/feature_flag/store",
47+
"//tensorboard/webapp/feature_flag/store:types",
4148
"//tensorboard/webapp/settings",
4249
"//tensorboard/webapp/tbdev_upload",
4350
"//tensorboard/webapp/types",
@@ -52,10 +59,13 @@ tf_ts_library(
5259
name = "test_lib",
5360
testonly = True,
5461
srcs = [
62+
"dark_mode_toggle_test.ts",
5563
"header_test.ts",
5664
],
5765
deps = [
5866
":header",
67+
"//tensorboard/webapp:selectors",
68+
"//tensorboard/webapp/angular:expect_angular_cdk_overlay",
5969
"//tensorboard/webapp/angular:expect_angular_core_testing",
6070
"//tensorboard/webapp/angular:expect_angular_material_button",
6171
"//tensorboard/webapp/angular:expect_angular_material_select",
@@ -66,6 +76,7 @@ tf_ts_library(
6676
"//tensorboard/webapp/core/actions",
6777
"//tensorboard/webapp/core/store",
6878
"//tensorboard/webapp/core/testing",
79+
"//tensorboard/webapp/feature_flag/actions",
6980
"//tensorboard/webapp/settings",
7081
"//tensorboard/webapp/testing:mat_icon",
7182
"//tensorboard/webapp/types",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/* Copyright 2021 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
import {Component, EventEmitter, Input, Output} from '@angular/core';
16+
17+
export enum DarkModeOverride {
18+
DEFAULT,
19+
DARK_MODE_ON,
20+
DARK_MODE_OFF,
21+
}
22+
23+
@Component({
24+
selector: 'app-header-dark-mode-toggle-component',
25+
template: `
26+
<button
27+
mat-icon-button
28+
[matMenuTriggerFor]="menu"
29+
aria-label="Menu for changing light or dark theme"
30+
[ngSwitch]="darkModeOverride"
31+
[title]="getButtonTitle()"
32+
>
33+
<mat-icon
34+
*ngSwitchCase="DarkModeOverride.DEFAULT"
35+
svgIcon="brightness_6_24px"
36+
></mat-icon>
37+
<mat-icon
38+
*ngSwitchCase="DarkModeOverride.DARK_MODE_OFF"
39+
svgIcon="light_mode_24px"
40+
></mat-icon>
41+
<mat-icon
42+
*ngSwitchCase="DarkModeOverride.DARK_MODE_ON"
43+
svgIcon="dark_mode_24px"
44+
></mat-icon>
45+
</button>
46+
<mat-menu #menu="matMenu">
47+
<button
48+
mat-menu-item
49+
title="Set the theme to match the default mode in the browser."
50+
(click)="onOverrideChanged.emit(DarkModeOverride.DEFAULT)"
51+
>
52+
<label>Browser default</label>
53+
</button>
54+
<button
55+
mat-menu-item
56+
title="Force light TensorBoard theme."
57+
(click)="onOverrideChanged.emit(DarkModeOverride.DARK_MODE_OFF)"
58+
>
59+
<label>Light</label>
60+
</button>
61+
<button
62+
mat-menu-item
63+
title="Force dark TensorBoard theme."
64+
(click)="onOverrideChanged.emit(DarkModeOverride.DARK_MODE_ON)"
65+
>
66+
<label>Dark</label>
67+
</button>
68+
</mat-menu>
69+
`,
70+
})
71+
export class DarkModeToggleComponent {
72+
readonly DarkModeOverride = DarkModeOverride;
73+
74+
@Input()
75+
darkModeOverride!: DarkModeOverride;
76+
77+
@Output()
78+
onOverrideChanged = new EventEmitter<DarkModeOverride>();
79+
80+
getButtonTitle(): string {
81+
let mode: string;
82+
83+
switch (this.darkModeOverride) {
84+
case DarkModeOverride.DEFAULT:
85+
mode = 'Browser default';
86+
break;
87+
case DarkModeOverride.DARK_MODE_ON:
88+
mode = 'Dark mode';
89+
break;
90+
case DarkModeOverride.DARK_MODE_OFF:
91+
mode = 'Light mode';
92+
break;
93+
}
94+
return `Current mode: [${mode}]. Switch between browser default, light, or dark theme.`;
95+
}
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/* Copyright 2021 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
import {Component} from '@angular/core';
16+
import {Store} from '@ngrx/store';
17+
import {Observable} from 'rxjs';
18+
import {map} from 'rxjs/operators';
19+
20+
import {State as CoreState} from '../core/store/core_types';
21+
import {State as FeatureFlagState} from '../feature_flag/store/feature_flag_types';
22+
import {overrideEnableDarkModeChanged} from '../feature_flag/actions/feature_flag_actions';
23+
import {getEnableDarkModeOverride} from '../feature_flag/store/feature_flag_selectors';
24+
import {DarkModeOverride} from './dark_mode_toggle_component';
25+
26+
@Component({
27+
selector: 'app-header-dark-mode-toggle',
28+
template: `
29+
<app-header-dark-mode-toggle-component
30+
[darkModeOverride]="darkModeOverride$ | async"
31+
(onOverrideChanged)="changeDarkMode($event)"
32+
>
33+
</app-header-dark-mode-toggle-component>
34+
`,
35+
})
36+
export class DarkModeToggleContainer {
37+
readonly darkModeOverride$: Observable<DarkModeOverride> = this.store
38+
.select(getEnableDarkModeOverride)
39+
.pipe(
40+
map(
41+
(override: boolean | null): DarkModeOverride => {
42+
if (override === null) return DarkModeOverride.DEFAULT;
43+
return override
44+
? DarkModeOverride.DARK_MODE_ON
45+
: DarkModeOverride.DARK_MODE_OFF;
46+
}
47+
)
48+
);
49+
50+
constructor(private readonly store: Store<CoreState & FeatureFlagState>) {}
51+
52+
changeDarkMode(newOverride: DarkModeOverride) {
53+
let enableDarkMode: boolean | null = null;
54+
55+
switch (newOverride) {
56+
case DarkModeOverride.DEFAULT:
57+
enableDarkMode = null;
58+
break;
59+
case DarkModeOverride.DARK_MODE_OFF:
60+
enableDarkMode = false;
61+
break;
62+
case DarkModeOverride.DARK_MODE_ON:
63+
enableDarkMode = true;
64+
break;
65+
}
66+
67+
this.store.dispatch(overrideEnableDarkModeChanged({enableDarkMode}));
68+
}
69+
}

0 commit comments

Comments
 (0)