diff --git a/app/assets/images/icons/computer-desktop.svg b/app/assets/images/icons/computer-desktop.svg new file mode 100644 index 0000000000..0836863e6b --- /dev/null +++ b/app/assets/images/icons/computer-desktop.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/components/theme/mobile_switcher_component.html.erb b/app/components/theme/mobile_switcher_component.html.erb new file mode 100644 index 0000000000..e725c0c5a4 --- /dev/null +++ b/app/components/theme/mobile_switcher_component.html.erb @@ -0,0 +1,29 @@ +
+ + + +
+ diff --git a/app/components/theme/mobile_switcher_component.rb b/app/components/theme/mobile_switcher_component.rb new file mode 100644 index 0000000000..20d56aa883 --- /dev/null +++ b/app/components/theme/mobile_switcher_component.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Theme::MobileSwitcherComponent < ApplicationComponent + def initialize(current_theme:) + @current_theme = current_theme + end + + private + + attr_reader :current_theme +end diff --git a/app/components/theme/switcher_component.html.erb b/app/components/theme/switcher_component.html.erb index ee8f34aedb..571ee69839 100644 --- a/app/components/theme/switcher_component.html.erb +++ b/app/components/theme/switcher_component.html.erb @@ -1,4 +1,36 @@ -<%= link_to themes_path(theme: other_theme.name), class: yass(link: type), data: { turbo_method: :put } do %> - <%= inline_svg_tag "icons/#{current_theme.icon}.svg", class: yass(icon: type), aria: true, title: 'theme icon' %> - <%= text unless icon_only? %> +<% if mobile? %> + <%= render Theme::MobileSwitcherComponent.new(current_theme:) %> +<% else %> +
+
+ +
+ + +
<% end %> diff --git a/app/components/theme/switcher_component.rb b/app/components/theme/switcher_component.rb index 4e05d3c36d..19c0d4ac4b 100644 --- a/app/components/theme/switcher_component.rb +++ b/app/components/theme/switcher_component.rb @@ -8,10 +8,6 @@ def text "#{current_theme.name.capitalize} mode" end - def other_theme - Users::Theme.default_themes.find { |other_theme| other_theme.name != current_theme.name } - end - def icon_only? type == :icon_only end diff --git a/app/javascript/controllers/theme_switcher_controller.js b/app/javascript/controllers/theme_switcher_controller.js new file mode 100644 index 0000000000..589f45b6f8 --- /dev/null +++ b/app/javascript/controllers/theme_switcher_controller.js @@ -0,0 +1,49 @@ +import { Controller } from '@hotwired/stimulus'; +import { put } from '@rails/request.js'; + +export default class ThemeSwitcherController extends Controller { + static values = { + currentTheme: String, + url: String, + }; + + connect() { + const userThemePreference = this.currentThemeValue; + + this.updateTheme(userThemePreference); + this.addThemeChangeListener(userThemePreference); + } + + addThemeChangeListener(userThemePreference) { + const themeChangeHandler = () => this.updateTheme(userThemePreference); + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', themeChangeHandler); + } + + async handleThemeChangeRequest(event) { + const { theme } = event.currentTarget.dataset; + + await this.updateThemeAndSendRequest(theme); + } + + async updateThemeAndSendRequest(theme) { + this.updateTheme(theme); + await put(this.urlValue, { body: JSON.stringify({ theme }) }); + } + + updateTheme(theme) { + const validThemes = ['light', 'dark']; + const userSystemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + const selectedTheme = validThemes.includes(theme) ? theme : userSystemTheme; + + this.setUserTheme(selectedTheme); + } + + // eslint-disable-next-line class-methods-use-this + setUserTheme(theme) { + const rootElement = document.getElementById('root-element'); + const availableThemes = ['system', 'light', 'dark']; + + availableThemes.forEach((t) => rootElement.classList.remove(t)); + rootElement.classList.add(theme); + } +} diff --git a/app/models/users/theme.rb b/app/models/users/theme.rb index d8f0a94e7c..a95b410368 100644 --- a/app/models/users/theme.rb +++ b/app/models/users/theme.rb @@ -4,9 +4,15 @@ class Theme DEFAULT_THEMES = [ %w[light sun], - %w[dark moon] + %w[dark moon], + %w[system computer-desktop] ].freeze + def initialize(name:, icon:) + @name = name + @icon = icon + end + def self.default_themes DEFAULT_THEMES.map { |name, icon| new(name:, icon:) } end @@ -19,17 +25,6 @@ def self.for(value) default_themes.find { |theme| theme.name == value } end - attr_reader :name, :icon - - def initialize(name:, icon:) - @name = name - @icon = icon - end - - def <=>(other) - name <=> other.name - end - def to_s name end @@ -37,5 +32,11 @@ def to_s def dark_mode? name == 'dark' end + + def system_mode? + name == 'system' + end + + attr_reader :name, :icon end end diff --git a/spec/components/theme/switcher_component_spec.rb b/spec/components/theme/switcher_component_spec.rb index 56c0b8ef79..a25eb0081e 100644 --- a/spec/components/theme/switcher_component_spec.rb +++ b/spec/components/theme/switcher_component_spec.rb @@ -2,22 +2,22 @@ RSpec.describe Theme::SwitcherComponent, type: :component do context 'when dark mode is enabled' do - it 'renders the light mode button' do + it 'renders the moon icon' do component = described_class.new(current_theme: Users::Theme.for('dark')) render_inline(component) - expect(page).to have_link(href: '/themes?theme=light') + expect(page).to have_css('title', text: 'moon') end end - context 'when dark mode is not enabled' do - it 'renders the dark mode button' do - component = described_class.new(current_theme: Users::Theme.for('light')) + context 'when system mode is not enabled' do + it 'renders the computer desktop icon' do + component = described_class.new(current_theme: Users::Theme.for('system')) render_inline(component) - expect(page).to have_link(href: '/themes?theme=dark') + expect(page).to have_css('title', text: 'computer-desktop') end end end diff --git a/spec/models/users/theme_spec.rb b/spec/models/users/theme_spec.rb index 594c19a4cc..e7e32aca0f 100644 --- a/spec/models/users/theme_spec.rb +++ b/spec/models/users/theme_spec.rb @@ -5,7 +5,8 @@ it 'returns the default themes' do expect(described_class.default_themes).to contain_exactly( an_object_having_attributes(name: 'light', icon: 'sun'), - an_object_having_attributes(name: 'dark', icon: 'moon') + an_object_having_attributes(name: 'dark', icon: 'moon'), + an_object_having_attributes(name: 'system', icon: 'computer-desktop') ) end end @@ -43,6 +44,12 @@ end end + context 'when the theme is system default' do + it 'returns true' do + expect(described_class.new(name: 'system', icon: 'system')).to be_system_mode + end + end + context 'when the theme is light' do it 'returns false' do expect(described_class.new(name: 'light', icon: 'sun')).not_to be_dark_mode