From d57d070fd9e07f672cd5fb20f6a3ca6c9cf22aa6 Mon Sep 17 00:00:00 2001
From: Scott de Jonge <sdejonge@gitlab.com>
Date: Fri, 10 May 2024 08:20:51 +0000
Subject: [PATCH] Add automatic color mode

Add support for system mode to set mode from prefers-color-scheme

Changelog: added
---
 app/assets/javascripts/main.js                  | 17 +++++++++++++++++
 .../components/profile_preferences.vue          | 10 +++-------
 app/helpers/preferences_helper.rb               |  4 ++++
 app/views/layouts/_head.html.haml               |  7 +++++++
 lib/gitlab/color_modes.rb                       |  4 +++-
 lib/gitlab/gon_helper.rb                        |  1 +
 locale/gitlab.pot                               |  3 +++
 .../components/profile_preferences_spec.js      | 15 +++++++++++++++
 spec/frontend/profile/preferences/mock_data.js  |  2 ++
 9 files changed, 55 insertions(+), 8 deletions(-)

diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index edafab8dddcd3..fcc2f064866cf 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -49,6 +49,23 @@ if (process.env.NODE_ENV !== 'production' && gon?.test_env) {
   import(/* webpackMode: "eager" */ './test_utils');
 }
 
+if (gon?.user_color_mode === 'gl-system') {
+  const root = document.documentElement;
+  // eslint-disable-next-line @gitlab/require-i18n-strings
+  if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
+    root.classList.add('gl-dark');
+  }
+
+  // eslint-disable-next-line @gitlab/require-i18n-strings
+  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
+    if (e.matches) {
+      root.classList.add('gl-dark');
+    } else {
+      root.classList.remove('gl-dark');
+    }
+  });
+}
+
 document.addEventListener('beforeunload', () => {
   // Unbind scroll events
   // eslint-disable-next-line @gitlab/no-global-event-off
diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
index 8729620082282..8e002a0049c73 100644
--- a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
+++ b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
@@ -47,7 +47,7 @@ export default {
   data() {
     return {
       isSubmitEnabled: true,
-      darkModeOnCreate: null,
+      colorModeOnCreate: null,
       schemeOnCreate: null,
     };
   },
@@ -55,7 +55,7 @@ export default {
     this.formEl.addEventListener('ajax:beforeSend', this.handleLoading);
     this.formEl.addEventListener('ajax:success', this.handleSuccess);
     this.formEl.addEventListener('ajax:error', this.handleError);
-    this.darkModeOnCreate = this.darkModeSelected();
+    this.colorModeOnCreate = this.getSelectedColorMode();
     this.schemeOnCreate = this.getSelectedScheme();
   },
   beforeDestroy() {
@@ -64,10 +64,6 @@ export default {
     this.formEl.removeEventListener('ajax:error', this.handleError);
   },
   methods: {
-    darkModeSelected() {
-      const mode = this.getSelectedColorMode();
-      return mode ? mode.css_class === 'gl-dark' : null;
-    },
     getSelectedColorMode() {
       const modeId = new FormData(this.formEl).get('user[color_mode_id]');
       const mode = this.colorModes.find((item) => item.id === Number(modeId));
@@ -88,7 +84,7 @@ export default {
       // Reload the page if the theme has changed from light to dark mode or vice versa
       // or if color scheme has changed to correctly load all required styles.
       if (
-        this.darkModeOnCreate !== this.darkModeSelected() ||
+        this.colorModeOnCreate !== this.getSelectedColorMode() ||
         this.schemeOnCreate !== this.getSelectedScheme()
       ) {
         window.location.reload();
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 53d6d35ba8cc4..e91d18d2c3258 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -79,6 +79,10 @@ def user_application_dark_mode?
     user_application_color_mode == 'gl-dark'
   end
 
+  def user_application_system_mode?
+    user_application_color_mode == 'gl-system'
+  end
+
   def user_theme_primary_color
     Gitlab::Themes.for_user(current_user).primary_color
   end
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index ea770a076f3c4..f54019a2a8723 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -30,6 +30,13 @@
     = stylesheet_link_tag_defer "application_dark"
     = yield :page_specific_styles
     = stylesheet_link_tag_defer "application_utilities_dark"
+  - elsif user_application_system_mode?
+    %meta{ name: 'color-scheme', content: 'light dark' }
+    = stylesheet_link_tag "application", media: "(prefers-color-scheme: light)"
+    = stylesheet_link_tag "application_dark", media: "(prefers-color-scheme: dark)"
+    = yield :page_specific_styles
+    = stylesheet_link_tag "application_utilities", media: "(prefers-color-scheme: light)"
+    = stylesheet_link_tag "application_utilities_dark", media: "(prefers-color-scheme: dark)"
   - else
     = stylesheet_link_tag_defer "application"
     = yield :page_specific_styles
diff --git a/lib/gitlab/color_modes.rb b/lib/gitlab/color_modes.rb
index cba984519da7d..0114c44f4cd9d 100644
--- a/lib/gitlab/color_modes.rb
+++ b/lib/gitlab/color_modes.rb
@@ -7,6 +7,7 @@ module ColorModes
     # Color mode ID used when no `default_color_mode` configuration setting is provided.
     APPLICATION_DEFAULT = 1
     APPLICATION_DARK = 2
+    APPLICATION_SYSTEM = 3
 
     # Struct class representing a single Mode
     Mode = Struct.new(:id, :name, :css_class)
@@ -14,7 +15,8 @@ module ColorModes
     def self.available_modes
       [
         Mode.new(APPLICATION_DEFAULT, s_('ColorMode|Light'), 'gl-light'),
-        Mode.new(APPLICATION_DARK, s_('ColorMode|Dark (Experiment)'), 'gl-dark')
+        Mode.new(APPLICATION_DARK, s_('ColorMode|Dark (Experiment)'), 'gl-dark'),
+        Mode.new(APPLICATION_SYSTEM, s_('ColorMode|Auto (Experiment)'), 'gl-system')
       ]
     end
 
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index de4dde89f0a05..db9962d6ef167 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -13,6 +13,7 @@ def add_gon_variables
       gon.asset_host                    = ActionController::Base.asset_host
       gon.webpack_public_path           = webpack_public_path
       gon.relative_url_root             = Gitlab.config.gitlab.relative_url_root
+      gon.user_color_mode               = Gitlab::ColorModes.for_user(current_user).css_class
       gon.user_color_scheme             = Gitlab::ColorSchemes.for_user(current_user).css_class
       gon.markdown_surround_selection   = current_user&.markdown_surround_selection
       gon.markdown_automatic_lists      = current_user&.markdown_automatic_lists
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b14d7a613c888..0e37d04df924a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -12645,6 +12645,9 @@ msgstr ""
 msgid "Color"
 msgstr ""
 
+msgid "ColorMode|Auto (Experiment)"
+msgstr ""
+
 msgid "ColorMode|Dark (Experiment)"
 msgstr ""
 
diff --git a/spec/frontend/profile/preferences/components/profile_preferences_spec.js b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
index 6fa7f4303f446..0e50603ebc530 100644
--- a/spec/frontend/profile/preferences/components/profile_preferences_spec.js
+++ b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
@@ -14,6 +14,7 @@ import {
   colorModes,
   lightColorModeId,
   darkColorModeId,
+  autoColorModeId,
   themes,
   themeId1,
 } from '../mock_data';
@@ -242,5 +243,19 @@ describe('ProfilePreferences component', () => {
 
       expect(window.location.reload).toHaveBeenCalledTimes(1);
     });
+
+    it('reloads the page when switching from auto to light mode', async () => {
+      selectColorModeId(autoColorModeId);
+      setupWrapper();
+
+      selectColorModeId(lightColorModeId);
+      dispatchBeforeSendEvent();
+      await nextTick();
+
+      dispatchSuccessEvent();
+      await nextTick();
+
+      expect(window.location.reload).toHaveBeenCalledTimes(1);
+    });
   });
 });
diff --git a/spec/frontend/profile/preferences/mock_data.js b/spec/frontend/profile/preferences/mock_data.js
index 2eabba4924301..98a28df363f5f 100644
--- a/spec/frontend/profile/preferences/mock_data.js
+++ b/spec/frontend/profile/preferences/mock_data.js
@@ -21,10 +21,12 @@ export const bodyClasses = 'ui-light-indigo ui-light gl-dark';
 
 export const lightColorModeId = 1;
 export const darkColorModeId = 2;
+export const autoColorModeId = 3;
 
 export const colorModes = [
   { id: lightColorModeId, css_class: 'gl-light' },
   { id: darkColorModeId, css_class: 'gl-dark' },
+  { id: autoColorModeId, css_class: 'gl-system' },
 ];
 
 export const themes = [
-- 
GitLab