From f25dfcf00ed17235e185607a2a4595c1334d67c4 Mon Sep 17 00:00:00 2001
From: Takshit Saini <94343242+takshittt@users.noreply.github.com>
Date: Fri, 7 Mar 2025 03:59:16 +0530
Subject: [PATCH 1/6] Added accesibility for default system theme to
p5.js-web-editor
---
client/jest.setup.js | 32 +++++++++++
.../modules/App/components/ThemeProvider.jsx | 57 ++++++++++++++++++-
client/modules/IDE/actions/preferences.js | 9 ++-
.../Preferences/Preferences.unit.test.jsx | 15 +++++
.../IDE/components/Preferences/index.jsx | 38 ++++++++++++-
translations/locales/en-US/translations.json | 4 +-
6 files changed, 147 insertions(+), 8 deletions(-)
diff --git a/client/jest.setup.js b/client/jest.setup.js
index ccc7ab57a7..c075aa4eb8 100644
--- a/client/jest.setup.js
+++ b/client/jest.setup.js
@@ -5,3 +5,35 @@ import 'regenerator-runtime/runtime';
// See: https://github.com/testing-library/jest-dom
// eslint-disable-next-line import/no-extraneous-dependencies
import '@testing-library/jest-dom';
+
+// Mock matchMedia
+window.matchMedia = jest.fn().mockImplementation((query) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: jest.fn(), // Deprecated
+ removeListener: jest.fn(), // Deprecated
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+ dispatchEvent: jest.fn()
+}));
+
+// Mock localStorage
+const localStorageMock = (function () {
+ let store = {};
+ return {
+ getItem: jest.fn((key) => store[key] || null),
+ setItem: jest.fn((key, value) => {
+ store[key] = value.toString();
+ }),
+ removeItem: jest.fn((key) => {
+ delete store[key];
+ }),
+ clear: jest.fn(() => {
+ store = {};
+ })
+ };
+})();
+Object.defineProperty(window, 'localStorage', {
+ value: localStorageMock
+});
diff --git a/client/modules/App/components/ThemeProvider.jsx b/client/modules/App/components/ThemeProvider.jsx
index 8bbef6931e..b6d450099b 100644
--- a/client/modules/App/components/ThemeProvider.jsx
+++ b/client/modules/App/components/ThemeProvider.jsx
@@ -1,11 +1,62 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
-import { useSelector } from 'react-redux';
+import { useSelector, useDispatch } from 'react-redux';
import { ThemeProvider } from 'styled-components';
-import theme from '../../../theme';
+import theme, { Theme } from '../../../theme';
+import { setTheme } from '../../IDE/actions/preferences';
const Provider = ({ children }) => {
const currentTheme = useSelector((state) => state.preferences.theme);
+ const dispatch = useDispatch();
+
+ // Detect system color scheme preference on initial load
+ useEffect(() => {
+ // Only apply system preference if the user hasn't explicitly set a theme
+ const userHasExplicitlySetTheme =
+ localStorage.getItem('has_set_theme') === 'true';
+ if (!userHasExplicitlySetTheme) {
+ const prefersDarkMode =
+ window.matchMedia &&
+ window.matchMedia('(prefers-color-scheme: dark)').matches;
+
+ if (prefersDarkMode) {
+ dispatch(setTheme(Theme.dark, { isSystemPreference: true }));
+ } else {
+ dispatch(setTheme(Theme.light, { isSystemPreference: true }));
+ }
+ }
+
+ // Listen for changes to system color scheme preference
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+
+ const handleChange = (e) => {
+ if (localStorage.getItem('has_set_theme') !== 'true') {
+ dispatch(
+ setTheme(e.matches ? Theme.dark : Theme.light, {
+ isSystemPreference: true
+ })
+ );
+ }
+ };
+
+ // Add event listener with modern API if available
+ if (mediaQuery.addEventListener) {
+ mediaQuery.addEventListener('change', handleChange);
+ } else {
+ // Fallback for older browsers
+ mediaQuery.addListener(handleChange);
+ }
+
+ // Clean up event listener
+ return () => {
+ if (mediaQuery.removeEventListener) {
+ mediaQuery.removeEventListener('change', handleChange);
+ } else {
+ mediaQuery.removeListener(handleChange);
+ }
+ };
+ }, [dispatch]);
+
return (
{children}
);
diff --git a/client/modules/IDE/actions/preferences.js b/client/modules/IDE/actions/preferences.js
index e0473bd995..defd242eed 100644
--- a/client/modules/IDE/actions/preferences.js
+++ b/client/modules/IDE/actions/preferences.js
@@ -177,7 +177,7 @@ export function setGridOutput(value) {
};
}
-export function setTheme(value) {
+export function setTheme(value, { isSystemPreference = false } = {}) {
// return {
// type: ActionTypes.SET_THEME,
// value
@@ -187,6 +187,13 @@ export function setTheme(value) {
type: ActionTypes.SET_THEME,
value
});
+
+ // If this is a user-initiated theme change (not from system preference),
+ // mark that the user has explicitly set a theme
+ if (!isSystemPreference) {
+ localStorage.setItem('has_set_theme', 'true');
+ }
+
const state = getState();
if (state.user.authenticated) {
const formParams = {
diff --git a/client/modules/IDE/components/Preferences/Preferences.unit.test.jsx b/client/modules/IDE/components/Preferences/Preferences.unit.test.jsx
index 55f9827d23..fdba967652 100644
--- a/client/modules/IDE/components/Preferences/Preferences.unit.test.jsx
+++ b/client/modules/IDE/components/Preferences/Preferences.unit.test.jsx
@@ -256,6 +256,21 @@ describe('', () => {
};
describe('testing theme switching', () => {
+ beforeEach(() => {
+ // Mock localStorage for theme tests
+ Object.defineProperty(window, 'localStorage', {
+ value: {
+ getItem: jest.fn().mockImplementation((key) => {
+ if (key === 'has_set_theme') return 'true';
+ return null;
+ }),
+ setItem: jest.fn(),
+ removeItem: jest.fn()
+ },
+ writable: true
+ });
+ });
+
describe('dark mode', () => {
it('switch to light', () => {
subject({ theme: 'dark' });
diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx
index fa5859400e..bd09c8da9b 100644
--- a/client/modules/IDE/components/Preferences/index.jsx
+++ b/client/modules/IDE/components/Preferences/index.jsx
@@ -102,6 +102,29 @@ export default function Preferences() {
{t('Preferences.Theme')}
+
{
+ localStorage.removeItem('has_set_theme');
+ const prefersDarkMode =
+ window.matchMedia &&
+ window.matchMedia('(prefers-color-scheme: dark)').matches;
+ dispatch(
+ setTheme(prefersDarkMode ? 'dark' : 'light', {
+ isSystemPreference: true
+ })
+ );
+ }}
+ aria-label={t('Preferences.SystemThemeARIA')}
+ name="system theme"
+ id="system-theme-on"
+ className="preference__radio-button"
+ value="system"
+ checked={localStorage.getItem('has_set_theme') !== 'true'}
+ />
+
dispatch(setTheme('light'))}
@@ -110,7 +133,10 @@ export default function Preferences() {
id="light-theme-on"
className="preference__radio-button"
value="light"
- checked={theme === 'light'}
+ checked={
+ theme === 'light' &&
+ localStorage.getItem('has_set_theme') === 'true'
+ }
/>