Skip to content

Commit e5b9774

Browse files
chrisgzfravernkokrui
authored
feat: add support for system color scheme (#3167)
* Add typings for default display mode * Add default mode to dark mode UI selector * Add CSS util to detect OS display mode * Add OS display mode detection for AppShell * Update defaults and update reducers * Delay evaluation of matchMedia * Add comments * OS Default -> Auto-detect * feat: undo media query in reducer * feat: add color scheme selection, remove toggle * refactor: ExportMenu into functional component * fix: add colorscheme to export preferences * fix: tests * fix: type error in reducer * chore: fix typecheck * chore: remove obsolete snapshot * fix: toggling --------- Co-authored-by: Ravern Koh <[email protected]> Co-authored-by: Kok Rui Wong <[email protected]>
1 parent c26ef0b commit e5b9774

File tree

21 files changed

+324
-236
lines changed

21 files changed

+324
-236
lines changed

website/src/actions/__snapshots__/settings.test.ts.snap

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,9 @@ exports[`settings should dispatch a select of a semester value 1`] = `
2121
}
2222
`;
2323

24-
exports[`settings should dispatch a selection of a mode 1`] = `
24+
exports[`settings should dispatch a selection of a color scheme preference 1`] = `
2525
{
26-
"payload": "LIGHT",
27-
"type": "SELECT_MODE",
28-
}
29-
`;
30-
31-
exports[`settings should dispatch a toggle of a mode 1`] = `
32-
{
33-
"payload": null,
34-
"type": "TOGGLE_MODE",
26+
"payload": "LIGHT_COLOR_SCHEME_PREFERENCE",
27+
"type": "SELECT_COLOR_SCHEME",
3528
}
3629
`;

website/src/actions/settings.test.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { Faculty, Semester } from 'types/modules';
2+
import { LIGHT_COLOR_SCHEME_PREFERENCE } from 'types/settings';
23

34
import * as actions from 'actions/settings';
45

5-
import { LIGHT_MODE } from 'types/settings';
6-
76
describe('settings', () => {
87
test('should dispatch a select of a semester value', () => {
98
const semester: Semester = 1;
@@ -15,12 +14,9 @@ describe('settings', () => {
1514
expect(actions.selectNewStudent(newStudent)).toMatchSnapshot();
1615
});
1716

18-
test('should dispatch a selection of a mode', () => {
19-
expect(actions.selectMode(LIGHT_MODE)).toMatchSnapshot();
20-
});
21-
22-
test('should dispatch a toggle of a mode', () => {
23-
expect(actions.toggleMode()).toMatchSnapshot();
17+
test('should dispatch a selection of a color scheme preference', () => {
18+
const colorSchemePreference = LIGHT_COLOR_SCHEME_PREFERENCE;
19+
expect(actions.selectColorScheme(colorSchemePreference)).toMatchSnapshot();
2420
});
2521

2622
test('should dispatch a select of a faculty value', () => {

website/src/actions/settings.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Faculty, Semester } from 'types/modules';
2-
import { Mode } from 'types/settings';
2+
import { ColorSchemePreference } from 'types/settings';
33
import { ModuleTableOrder } from 'types/reducers';
44

55
import { RegPeriod, ScheduleType } from 'config';
@@ -29,22 +29,14 @@ export function selectFaculty(faculty: Faculty) {
2929
};
3030
}
3131

32-
export const SELECT_MODE = 'SELECT_MODE' as const;
33-
export function selectMode(mode: Mode) {
32+
export const SELECT_COLOR_SCHEME = 'SELECT_COLOR_SCHEME' as const;
33+
export function selectColorScheme(mode: ColorSchemePreference) {
3434
return {
35-
type: SELECT_MODE,
35+
type: SELECT_COLOR_SCHEME,
3636
payload: mode,
3737
};
3838
}
3939

40-
export const TOGGLE_MODE = 'TOGGLE_MODE' as const;
41-
export function toggleMode() {
42-
return {
43-
type: TOGGLE_MODE,
44-
payload: null,
45-
};
46-
}
47-
4840
export const DISMISS_MODREG_NOTIFICATION = 'DISMISS_MODREG_NOTIFICATION' as const;
4941
export function dismissModregNotification(round: RegPeriod) {
5042
return {

website/src/apis/export.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Semester } from 'types/modules';
44
import { extractStateForExport } from 'utils/export';
55
import { State } from 'types/state';
66
import { SemTimetableConfig } from 'types/timetables';
7+
import { ColorScheme } from 'types/settings';
78

89
export type ExportOptions = {
910
pixelRatio?: number;
@@ -14,18 +15,29 @@ const baseUrl = 'https://export.nusmods.com/api/export';
1415
function serializeState(
1516
semester: Semester,
1617
timetable: SemTimetableConfig,
18+
colorScheme: ColorScheme,
1719
state: State,
1820
options: ExportOptions = {},
1921
) {
2022
return qs.stringify({
21-
data: JSON.stringify(extractStateForExport(semester, timetable, state)),
23+
data: JSON.stringify(extractStateForExport(semester, timetable, colorScheme, state)),
2224
...options,
2325
});
2426
}
2527

2628
export default {
27-
image: (semester: Semester, timetable: SemTimetableConfig, state: State, pixelRatio = 1) =>
28-
`${baseUrl}/image?${serializeState(semester, timetable, state, { pixelRatio })}`,
29-
pdf: (semester: Semester, timetable: SemTimetableConfig, state: State) =>
30-
`${baseUrl}/pdf?${serializeState(semester, timetable, state)}`,
29+
image: (
30+
semester: Semester,
31+
timetable: SemTimetableConfig,
32+
colorScheme: ColorScheme,
33+
state: State,
34+
pixelRatio = 1,
35+
) =>
36+
`${baseUrl}/image?${serializeState(semester, timetable, colorScheme, state, { pixelRatio })}`,
37+
pdf: (
38+
semester: Semester,
39+
timetable: SemTimetableConfig,
40+
colorScheme: ColorScheme,
41+
state: State,
42+
) => `${baseUrl}/pdf?${serializeState(semester, timetable, colorScheme, state)}`,
3143
};

website/src/entry/export/main.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ExportData } from 'types/export';
77

88
import configureStore from 'bootstrapping/configure-store';
99
import { setExportedData } from 'actions/export';
10-
import { DARK_MODE } from 'types/settings';
10+
import { DARK_COLOR_SCHEME } from 'types/settings';
1111
import { State as StoreState } from 'types/state';
1212

1313
import TimetableOnly from './TimetableOnly';
@@ -32,7 +32,7 @@ window.setData = function setData(modules, data, callback) {
3232
const { semester, timetable, colors } = data;
3333

3434
if (document.body) {
35-
document.body.classList.toggle('mode-dark', data.settings.mode === DARK_MODE);
35+
document.body.classList.toggle('mode-dark', data.settings.colorScheme === DARK_COLOR_SCHEME);
3636
}
3737

3838
store.dispatch(setExportedData(modules, data));

website/src/reducers/index.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { VERTICAL } from 'types/reducers';
33
import reducers from 'reducers';
44
import { setExportedData } from 'actions/export';
55
import modules from '__mocks__/modules/index';
6+
import { DARK_COLOR_SCHEME, DARK_COLOR_SCHEME_PREFERENCE } from 'types/settings';
67

78
/* eslint-disable no-useless-computed-key */
89

@@ -34,7 +35,7 @@ const exportData: ExportData = {
3435
showTitle: true,
3536
},
3637
settings: {
37-
mode: 'DARK',
38+
colorScheme: DARK_COLOR_SCHEME,
3839
},
3940
};
4041

@@ -78,7 +79,7 @@ test('reducers should set export data state', () => {
7879
});
7980

8081
expect(state.settings).toMatchObject({
81-
mode: 'DARK',
82+
colorScheme: DARK_COLOR_SCHEME_PREFERENCE,
8283
});
8384

8485
expect(state.theme).toEqual({

website/src/reducers/settings.test.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ import { SettingsState } from 'types/reducers';
33

44
import * as actions from 'actions/settings';
55
import reducer from 'reducers/settings';
6-
import { DARK_MODE, LIGHT_MODE } from 'types/settings';
6+
import {
7+
DARK_COLOR_SCHEME_PREFERENCE,
8+
LIGHT_COLOR_SCHEME_PREFERENCE,
9+
SYSTEM_COLOR_SCHEME_PREFERENCE,
10+
} from 'types/settings';
711
import { initAction, rehydrateAction } from 'test-utils/redux';
812
import config, { RegPeriod } from 'config';
913

1014
const initialState: SettingsState = {
1115
newStudent: false,
1216
faculty: '',
13-
mode: LIGHT_MODE,
17+
colorScheme: SYSTEM_COLOR_SCHEME_PREFERENCE,
1418
hiddenInTimetable: [],
1519
modRegNotification: {
1620
enabled: true,
@@ -25,7 +29,14 @@ const initialState: SettingsState = {
2529
const settingsWithNewStudent: SettingsState = { ...initialState, newStudent: true };
2630
const faculty = 'School of Computing';
2731
const settingsWithFaculty: SettingsState = { ...initialState, faculty };
28-
const settingsWithDarkMode: SettingsState = { ...initialState, mode: DARK_MODE };
32+
const settingsWithLightMode: SettingsState = {
33+
...initialState,
34+
colorScheme: LIGHT_COLOR_SCHEME_PREFERENCE,
35+
};
36+
const settingsWithDarkMode: SettingsState = {
37+
...initialState,
38+
colorScheme: DARK_COLOR_SCHEME_PREFERENCE,
39+
};
2940
const settingsWithDismissedNotifications: SettingsState = produce(initialState, (draft) => {
3041
draft.modRegNotification.dismissed = [
3142
{ type: 'Select Courses', name: '1' },
@@ -52,23 +63,18 @@ describe('settings', () => {
5263
expect(nextState).toEqual(settingsWithFaculty);
5364
});
5465

55-
test('can select mode', () => {
56-
const action = actions.selectMode(DARK_MODE);
66+
test('can select color scheme', () => {
67+
const action = actions.selectColorScheme(DARK_COLOR_SCHEME_PREFERENCE);
5768
const nextState: SettingsState = reducer(initialState, action);
5869
expect(nextState).toEqual(settingsWithDarkMode);
5970

60-
const action2 = actions.selectMode(LIGHT_MODE);
61-
const nextState2: SettingsState = reducer(nextState, action2);
62-
expect(nextState2).toEqual(initialState);
63-
});
64-
65-
test('can toggle mode', () => {
66-
const action = actions.toggleMode();
67-
const nextState: SettingsState = reducer(initialState, action);
68-
expect(nextState).toEqual(settingsWithDarkMode);
71+
const action2 = actions.selectColorScheme(LIGHT_COLOR_SCHEME_PREFERENCE);
72+
const nextState2: SettingsState = reducer(initialState, action2);
73+
expect(nextState2).toEqual(settingsWithLightMode);
6974

70-
const nextState2: SettingsState = reducer(nextState, action);
71-
expect(nextState2).toEqual(initialState);
75+
const action3 = actions.selectColorScheme(SYSTEM_COLOR_SCHEME_PREFERENCE);
76+
const nextState3: SettingsState = reducer(nextState, action3);
77+
expect(nextState3).toEqual(initialState);
7278
});
7379

7480
test('set module table order', () => {

website/src/reducers/settings.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,20 @@ import {
99
DISMISS_MODREG_NOTIFICATION,
1010
ENABLE_MODREG_NOTIFICATION,
1111
SELECT_FACULTY,
12-
SELECT_MODE,
12+
SELECT_COLOR_SCHEME,
1313
SELECT_NEW_STUDENT,
1414
SET_LOAD_DISQUS_MANUALLY,
1515
SET_MODULE_TABLE_SORT,
1616
TOGGLE_BETA_TESTING_STATUS,
1717
TOGGLE_MODREG_NOTIFICATION_GLOBALLY,
18-
TOGGLE_MODE,
1918
SET_MODREG_SCHEDULE_TYPE,
2019
} from 'actions/settings';
2120
import { SET_EXPORTED_DATA } from 'actions/constants';
2221
import { DIMENSIONS, withTracker } from 'bootstrapping/matomo';
23-
import { DARK_MODE, LIGHT_MODE } from 'types/settings';
22+
import { SYSTEM_COLOR_SCHEME_PREFERENCE } from 'types/settings';
2423
import config from 'config';
2524
import { isRoundDismissed } from 'selectors/modreg';
25+
import { colorSchemeToPreference } from 'utils/colorScheme';
2626

2727
export const defaultModRegNotificationState = {
2828
semesterKey: config.getSemesterKey(),
@@ -34,7 +34,7 @@ export const defaultModRegNotificationState = {
3434
const defaultSettingsState: SettingsState = {
3535
newStudent: false,
3636
faculty: '',
37-
mode: LIGHT_MODE,
37+
colorScheme: SYSTEM_COLOR_SCHEME_PREFERENCE,
3838
hiddenInTimetable: [],
3939
modRegNotification: defaultModRegNotificationState,
4040
moduleTableOrder: 'exam',
@@ -54,17 +54,11 @@ function settings(state: SettingsState = defaultSettingsState, action: Actions):
5454
...state,
5555
faculty: action.payload,
5656
};
57-
case SELECT_MODE:
57+
case SELECT_COLOR_SCHEME:
5858
return {
5959
...state,
60-
mode: action.payload,
60+
colorScheme: action.payload,
6161
};
62-
case TOGGLE_MODE:
63-
return {
64-
...state,
65-
mode: state.mode === LIGHT_MODE ? DARK_MODE : LIGHT_MODE,
66-
};
67-
6862
case TOGGLE_MODREG_NOTIFICATION_GLOBALLY:
6963
return produce(state, (draft) => {
7064
draft.modRegNotification.enabled = action.payload.enabled;
@@ -89,11 +83,14 @@ function settings(state: SettingsState = defaultSettingsState, action: Actions):
8983
draft.modRegNotification.scheduleType = action.payload;
9084
});
9185

92-
case SET_EXPORTED_DATA:
86+
case SET_EXPORTED_DATA: {
87+
const { colorScheme, ...otherSettings } = action.payload.settings;
9388
return {
9489
...state,
95-
...action.payload.settings,
90+
...otherSettings,
91+
colorScheme: colorSchemeToPreference(action.payload.settings.colorScheme),
9692
};
93+
}
9794

9895
case SET_MODULE_TABLE_SORT:
9996
return {

website/src/types/export.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { SemTimetableConfig } from 'types/timetables';
22
import { Semester, ModuleCode } from 'types/modules';
3-
import { Mode } from 'types/settings';
3+
import type { ColorScheme } from 'types/settings';
44
import { ColorMapping, ThemeState } from 'types/reducers';
55

66
export type ExportData = {
@@ -10,6 +10,6 @@ export type ExportData = {
1010
readonly hidden: ModuleCode[];
1111
readonly theme: ThemeState;
1212
readonly settings: {
13-
mode: Mode;
13+
colorScheme: ColorScheme;
1414
};
1515
};

website/src/types/reducers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AxiosError } from 'axios';
22
import { RegPeriodType, ScheduleType } from 'config';
33

4-
import { Mode } from './settings';
4+
import { ColorSchemePreference } from './settings';
55
import { ColorIndex, Lesson, TimetableConfig } from './timetables';
66
import {
77
Faculty,
@@ -98,7 +98,7 @@ export type ModuleTableOrder = 'exam' | 'mc' | 'code';
9898
export type SettingsState = {
9999
readonly newStudent: boolean;
100100
readonly faculty: Faculty | null;
101-
readonly mode: Mode;
101+
readonly colorScheme: ColorSchemePreference;
102102
readonly hiddenInTimetable: ModuleCode[];
103103
readonly modRegNotification: ModRegNotificationSettings;
104104
readonly moduleTableOrder: ModuleTableOrder;

0 commit comments

Comments
 (0)