Skip to content

Commit

Permalink
Add dark mode support to graph. Refactor color variables, standardize…
Browse files Browse the repository at this point in the history
… existing color palette.

PiperOrigin-RevId: 702667240
  • Loading branch information
biharygergo authored and copybara-github committed Dec 13, 2024
1 parent e9324c1 commit 354645d
Show file tree
Hide file tree
Showing 43 changed files with 592 additions and 117 deletions.
3 changes: 3 additions & 0 deletions src/app/color_theme_loader.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@use 'colors';

@include colors.workflow-graph-colors();
119 changes: 119 additions & 0 deletions src/app/color_theme_loader.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* @license
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {DOCUMENT} from '@angular/common';
import {Component, ViewChild} from '@angular/core';
import {ComponentFixture, fakeAsync, TestBed, waitForAsync} from '@angular/core/testing';
import {BehaviorSubject} from 'rxjs';

import {ColorThemeLoader} from './color_theme_loader';
import {DagStateService} from './dag-state.service';
import {defaultFeatures, FeatureToggleOptions} from './data_types_internal';
import {initTestBed, keyWithCtrlOrCommand} from './test_resources/test_utils';


describe('ColorThemeLoader', () => {
let toggleSpy: jasmine.Spy;
let loaderInstance: ColorThemeLoader;
const fakeFeatures =
new BehaviorSubject<FeatureToggleOptions>(defaultFeatures);

beforeEach(waitForAsync(async () => {
fakeFeatures.next(defaultFeatures);

const fakeDagStateService =
jasmine.createSpyObj<DagStateService>('DagStateService', ['features$']);
fakeDagStateService.features$ = fakeFeatures;

await initTestBed({
declarations: [TestComponent],
imports: [ColorThemeLoader],
providers: [
{
provide: DagStateService,
useValue: fakeDagStateService,
},
],
});
const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();

loaderInstance = fixture.componentInstance.loader;
toggleSpy = spyOn(
fixture.debugElement.injector.get(DOCUMENT).body.classList, 'toggle');
}));

it('should set light mode by default', async () => {
expect(
window.document.body.classList.contains('workflow-graph-theme-light'))
.toBeTrue();
expect(window.document.body.classList.contains('workflow-graph-theme-dark'))
.toBeFalse();
})

describe('when dark mode is set in features', () => {
beforeEach(waitForAsync(async () => {
fakeFeatures.next({
...defaultFeatures,
theme: 'dark',
});
}));

it('should set dark mode', async () => {
expect(toggleSpy).toHaveBeenCalledWith(
'workflow-graph-theme-light', false);
expect(toggleSpy).toHaveBeenCalledWith('workflow-graph-theme-dark', true);
})
})

describe('when the theme is set to device', () => {
beforeEach(waitForAsync(async () => {
fakeFeatures.next({
...defaultFeatures,
theme: 'device',
});
}));

describe('when the user prefers dark mode', () => {
beforeEach(waitForAsync(async () => {
toggleSpy.calls.reset();
loaderInstance.prefersColorSchemeDarkMediaQuery.dispatchEvent(
new MediaQueryListEvent('change', {
matches: true,
media: '(prefers-color-scheme: dark)',
}));
}));

it('should set dark mode', () => {
expect(toggleSpy).toHaveBeenCalledWith(
'workflow-graph-theme-light', false);
expect(toggleSpy).toHaveBeenCalledWith(
'workflow-graph-theme-dark', true);
});
});
});
});

@Component({
standalone: false,
template:
'<workflow-graph-color-theme-loader #loader></workflow-graph-color-theme-loader>',
jit: true,
})
class TestComponent {
@ViewChild('loader', {static: false}) loader!: ColorThemeLoader;
}
69 changes: 69 additions & 0 deletions src/app/color_theme_loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @license
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {DOCUMENT} from '@angular/common';
import {Component, DestroyRef, Inject, ViewEncapsulation} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {combineLatest, fromEvent, Observable} from 'rxjs';
import {distinctUntilChanged, map, startWith} from 'rxjs/operators';

import {DagStateService} from './dag-state.service';
import {STATE_SERVICE_PROVIDER} from './dag-state.service.provider';
import {Theme} from './data_types_internal';

@Component({
selector: 'workflow-graph-color-theme-loader',
template: '',
styleUrls: ['./color_theme_loader.scss'],
standalone: true,
encapsulation: ViewEncapsulation.None,
providers: [
STATE_SERVICE_PROVIDER,
],
})
export class ColorThemeLoader {
private readonly theme: Observable<Theme|undefined> =
this.dagStateService.features$.pipe(
map(features => features.theme), distinctUntilChanged());
readonly prefersColorSchemeDarkMediaQuery: MediaQueryList = window.matchMedia(
'(prefers-color-scheme: dark)',
);
private readonly deviceColorSchemeDarkChange: Observable<boolean> =
fromEvent<MediaQueryListEvent>(
this.prefersColorSchemeDarkMediaQuery,
'change',
)
.pipe(
map((event) => event.matches),
startWith(this.prefersColorSchemeDarkMediaQuery.matches),
);

constructor(
private readonly dagStateService: DagStateService,
@Inject(DOCUMENT) private readonly document: Document,
private readonly destroyRef: DestroyRef) {
combineLatest([this.theme, this.deviceColorSchemeDarkChange])
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(([theme, isDeviceColorDark]) => {
const isDarkTheme =
theme === 'dark' || (theme === 'device' && isDeviceColorDark);
this.document.body.classList.toggle(
'workflow-graph-theme-dark', isDarkTheme);
this.document.body.classList.toggle(
'workflow-graph-theme-light', !isDarkTheme);
});
}
}
198 changes: 198 additions & 0 deletions src/app/colors.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
@mixin colors() {
--workflow-graph-color-primary-container: var(
--cm-sys-color-primary-container,
#e8f0fe
);
--workflow-graph-color-surface: var(--cm-sys-color-surface, #fff);
--workflow-graph-color-surface-variant: var(
--cm-sys-color-surface-variant,
#f9fafb
);
--workflow-graph-color-surface-on-surface: var(
--cm-sys-color-on-surface,
#202124
);
--workflow-graph-color-surface-on-surface-variant: var(
--cm-sys-color-on-surface-variant,
#5f6368
);
--workflow-graph-color-surface-elevation-1: var(
--cm-sys-color-surface-elevation1,
#fff
);
--workflow-graph-color-hairline: var(--cm-sys-color-hairline, #dadce0);
--workflow-graph-color-non-primary: var(--cm-sys-color-non-primary, #5f6368);
--workflow-graph-color-outline: #263c4043;
--workflow-graph-color-neutral-container: var(
--cm-sys-color-neutral-container,
#f1f3f4
);
--workflow-graph-color-on-primary: var(--cm-sys-color-on-primary, #fff);
--workflow-graph-color-status-error: var(
--cm-sys-color-status-error,
#d93025
);
--workflow-graph-color-status-on-error: var(
--cm-sys-color-status-on-error,
#fff
);
--workflow-graph-color-state-disabled: var(
--cm-sys-color-state-disabled,
#b5b6b8
);
--workflow-graph-color-state-disabled-container: var(
--cm-sys-color-state-disabled-container,
#e8e8e8
);

--workflow-graph-base-color-black: #171717;
--workflow-graph-base-color-blue: #1a73e8;
--workflow-graph-base-color-blue-transparent-05: transparentize(#1a73e8, 0.5);
--workflow-graph-base-color-blue-transparent-088: transparentize(
#1a73e8,
0.88
);
--workflow-graph-base-color-blue-transparent-095: transparentize(
#1a73e8,
0.95
);
--workflow-graph-base-color-blue-transparent-1: transparentize(#1a73e8, 1);
--workflow-graph-base-color-green: var(
--cm-sys-color-status-success,
#188038
);
--workflow-graph-base-color-red: var(--cm-sys-color-status-error, #d93025);
--workflow-graph-base-color-yellow: var(
--cm-sys-color-status-warning,
#e37400
);
--workflow-graph-base-color-orange: #fbbc04;
--workflow-graph-base-color-purple: #af5cf7;
--workflow-graph-base-color-gray: #5f6368;

--workflow-graph-base-color-background-blue: #e8f0fe;
--workflow-graph-base-color-background-green: var(
--cm-sys-color-status-success-container,
#e6f4ea
);
--workflow-graph-base-color-background-red: var(
--cm-sys-color-status-error-container,
#fce8e6
);
--workflow-graph-base-color-background-yellow: var(
--cm-sys-color-status-warning-container,
#fef7e0
);
--workflow-graph-base-color-background-gray: var(
--cm-sys-color-status-neutral-container,
#f0f0f0
);
--workflow-graph-base-color-background-white: #fff;
}

@mixin colors-dark() {
--workflow-graph-color-primary-container: var(
--cm-sys-color-primary-container,
#394457
);
--workflow-graph-color-surface: var(--cm-sys-color-surface, #131314);
--workflow-graph-color-surface-variant: var(
--cm-sys-color-surface-variant,
#1f2123
);
--workflow-graph-color-hairline: var(--cm-sys-color-hairline, #5f6368);
--workflow-graph-color-non-primary: var(--cm-sys-color-non-primary, #e8eaed);
--workflow-graph-color-outline: var(--cm-sys-color-outline, #9aa0a6);
--workflow-graph-color-neutral-container: var(
--cm-sys-color-neutral-container,
#4d4e51
);
--workflow-graph-color-on-primary: var(--cm-sys-color-on-primary, #202124);
--workflow-graph-color-status-error: var(
--cm-sys-color-status-error,
#ee675c
);
--workflow-graph-color-status-on-error: var(
--cm-sys-color-status-on-error,
#000
);
--workflow-graph-color-state-disabled: var(
--cm-sys-color-state-disabled,
#6c6d70
)
--workflow-graph-color-state-disabled-container: var(
--cm-sys-color-state-disabled-container,
#38393c
);

--workflow-graph-color-surface-on-surface: var(
--cm-sys-color-on-surface,
#e8eaed
);
--workflow-graph-color-surface-on-surface-variant: var(
--cm-sys-color-on-surface-variant,
#9aa0a6
);
--workflow-graph-color-surface-elevation-1: var(
--cm-sys-color-surface-elevation1,
#2a2b2e
);

--workflow-graph-base-color-black: #171717;
--workflow-graph-base-color-blue: #8ab4f8; // theme-primary
--workflow-graph-base-color-blue-transparent-05: transparentize(#8ab4f8, 0.5);
--workflow-graph-base-color-blue-transparent-088: transparentize(
#8ab4f8,
0.88
);
--workflow-graph-base-color-blue-transparent-095: transparentize(
#8ab4f8,
0.95
);
--workflow-graph-base-color-blue-transparent-1: transparentize(#8ab4f8, 1);
--workflow-graph-base-color-green: var(
--cm-sys-color-status-success,
#5bb974
);
--workflow-graph-base-color-red: var(--cm-sys-color-status-error, #ee675c);
--workflow-graph-base-color-yellow: var(
--cm-sys-color-status-warning,
#fcc934
);
--workflow-graph-base-color-orange: #fbbc04;
--workflow-graph-base-color-purple: #af5cf7;
--workflow-graph-base-color-gray: #5f6368;

--workflow-graph-base-color-background-blue: #020a17;
--workflow-graph-base-color-background-green: var(
--cm-sys-color-status-success-container,
#37493f
);
--workflow-graph-base-color-background-red: var(
--cm-sys-color-status-error-container,
#523a3b
);
--workflow-graph-base-color-background-yellow: var(
--cm-sys-color-status-warning-container,
#554c33
);
--workflow-graph-base-color-background-gray: var(
--cm-sys-color-status-neutral-container,
#1e1e1f
);
--workflow-graph-base-color-background-white: #fff;
}

@mixin workflow-graph-colors($parentSelector: 'body') {
@layer styles;

@layer styles {
#{$parentSelector} {
@include colors();
}

#{$parentSelector}.workflow-graph-theme-dark {
@include colors-dark();
}
}
}
Loading

0 comments on commit 354645d

Please sign in to comment.