Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit f4cd71f

Browse files
author
Kerry
authored
Check 'useSystemTheme' in quick settings theme switcher (#7809)
* mock Element.scrollIntoView in jest setup Signed-off-by: Kerry Archibald <[email protected]> * extract theme switcher from quick settings, add match system option, test Signed-off-by: Kerry Archibald <[email protected]> * i18n Signed-off-by: Kerry Archibald <[email protected]> * forgotten copyright Signed-off-by: Kerry Archibald <[email protected]> * stylelint Signed-off-by: Kerry Archibald <[email protected]> * remove old class Signed-off-by: Kerry Archibald <[email protected]>
1 parent 889b0ce commit f4cd71f

File tree

8 files changed

+274
-73
lines changed

8 files changed

+274
-73
lines changed

res/css/_components.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
@import "./_font-sizes.scss";
55
@import "./_font-weights.scss";
66
@import "./_spacing.scss";
7+
@import "./components/views/spaces/_QuickThemeSwitcher.scss";
78
@import "./structures/_AutoHideScrollbar.scss";
89
@import "./structures/_BackdropPanel.scss";
910
@import "./structures/_CompatibilityPage.scss";
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_QuickThemeSwitcher {
18+
display: flex;
19+
align-items: center;
20+
21+
.mx_Dropdown {
22+
min-width: 100px;
23+
margin-left: auto;
24+
height: min-content;
25+
}
26+
27+
.mx_Dropdown_menu {
28+
max-height: 70px;
29+
}
30+
}
31+
32+
.mx_QuickThemeSwitcher_heading {
33+
font-weight: $font-semi-bold;
34+
font-size: $font-12px;
35+
line-height: $font-15px;
36+
color: $secondary-content;
37+
text-transform: uppercase;
38+
display: inline-block;
39+
margin: 0;
40+
}

res/css/structures/_QuickSettingsButton.scss

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -161,29 +161,4 @@ limitations under the License.
161161
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
162162
}
163163
}
164-
165-
.mx_QuickSettingsButton_themePicker {
166-
display: flex;
167-
align-items: center;
168-
169-
> h4 {
170-
font-weight: $font-semi-bold;
171-
font-size: $font-12px;
172-
line-height: $font-15px;
173-
color: $secondary-content;
174-
text-transform: uppercase;
175-
display: inline-block;
176-
margin: 0;
177-
}
178-
179-
.mx_Dropdown {
180-
min-width: 100px;
181-
margin-left: auto;
182-
height: min-content;
183-
}
184-
185-
.mx_Dropdown_menu {
186-
max-height: 70px;
187-
}
188-
}
189164
}

src/components/views/spaces/QuickSettingsButton.tsx

Lines changed: 3 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React, { useMemo } from "react";
17+
import React from "react";
1818
import classNames from "classnames";
1919

2020
import { _t } from "../../../languageHandler";
@@ -28,17 +28,9 @@ import { onMetaSpaceChangeFactory } from "../settings/tabs/user/SidebarUserSetti
2828
import defaultDispatcher from "../../../dispatcher/dispatcher";
2929
import { Action } from "../../../dispatcher/actions";
3030
import { UserTab } from "../dialogs/UserSettingsDialog";
31-
import { findNonHighContrastTheme, getOrderedThemes } from "../../../theme";
32-
import Dropdown from "../elements/Dropdown";
33-
import ThemeChoicePanel from "../settings/ThemeChoicePanel";
34-
import SettingsStore from "../../../settings/SettingsStore";
35-
import { SettingLevel } from "../../../settings/SettingLevel";
36-
import dis from "../../../dispatcher/dispatcher";
37-
import { RecheckThemePayload } from "../../../dispatcher/payloads/RecheckThemePayload";
38-
import PosthogTrackers from "../../../PosthogTrackers";
31+
import QuickThemeSwitcher from "./QuickThemeSwitcher";
3932

4033
const QuickSettingsButton = ({ isPanelCollapsed = false }) => {
41-
const orderedThemes = useMemo(getOrderedThemes, []);
4234
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
4335

4436
const {
@@ -48,10 +40,6 @@ const QuickSettingsButton = ({ isPanelCollapsed = false }) => {
4840

4941
let contextMenu: JSX.Element;
5042
if (menuDisplayed) {
51-
const themeState = ThemeChoicePanel.calculateThemeState();
52-
const nonHighContrast = findNonHighContrastTheme(themeState.theme);
53-
const theme = nonHighContrast ? nonHighContrast : themeState.theme;
54-
5543
contextMenu = <ContextMenu
5644
{...alwaysAboveRightOf(handle.current.getBoundingClientRect(), ChevronFace.None, 16)}
5745
wrapperClassName="mx_QuickSettingsButton_ContextMenuWrapper"
@@ -100,39 +88,7 @@ const QuickSettingsButton = ({ isPanelCollapsed = false }) => {
10088
{ _t("More options") }
10189
</AccessibleButton>
10290

103-
<div className="mx_QuickSettingsButton_themePicker">
104-
<h4>{ _t("Theme") }</h4>
105-
<Dropdown
106-
id="mx_QuickSettingsButton_themePickerDropdown"
107-
onOptionChange={async (newTheme: string) => {
108-
PosthogTrackers.trackInteraction("WebQuickSettingsThemeDropdown");
109-
110-
// XXX: mostly copied from ThemeChoicePanel
111-
// doing getValue in the .catch will still return the value we failed to set,
112-
// so remember what the value was before we tried to set it so we can revert
113-
// const oldTheme: string = SettingsStore.getValue("theme");
114-
SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme).catch(() => {
115-
dis.dispatch<RecheckThemePayload>({ action: Action.RecheckTheme });
116-
});
117-
// The settings watcher doesn't fire until the echo comes back from the
118-
// server, so to make the theme change immediately we need to manually
119-
// do the dispatch now
120-
// XXX: The local echoed value appears to be unreliable, in particular
121-
// when settings custom themes(!) so adding forceTheme to override
122-
// the value from settings.
123-
dis.dispatch<RecheckThemePayload>({ action: Action.RecheckTheme, forceTheme: newTheme });
124-
closeMenu();
125-
}}
126-
value={theme}
127-
label={_t("Space selection")}
128-
>
129-
{ orderedThemes.map((theme) => (
130-
<div key={theme.id}>
131-
{ theme.name }
132-
</div>
133-
)) }
134-
</Dropdown>
135-
</div>
91+
<QuickThemeSwitcher requestClose={closeMenu} />
13692
</ContextMenu>;
13793
}
13894

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React, { useMemo } from "react";
18+
19+
import { _t } from "../../../languageHandler";
20+
import { Action } from "../../../dispatcher/actions";
21+
import { findNonHighContrastTheme, getOrderedThemes } from "../../../theme";
22+
import Dropdown from "../elements/Dropdown";
23+
import ThemeChoicePanel from "../settings/ThemeChoicePanel";
24+
import SettingsStore from "../../../settings/SettingsStore";
25+
import { SettingLevel } from "../../../settings/SettingLevel";
26+
import dis from "../../../dispatcher/dispatcher";
27+
import { RecheckThemePayload } from "../../../dispatcher/payloads/RecheckThemePayload";
28+
import PosthogTrackers from "../../../PosthogTrackers";
29+
30+
type Props = {
31+
requestClose: () => void;
32+
};
33+
34+
const MATCH_SYSTEM_THEME_ID = 'MATCH_SYSTEM_THEME_ID';
35+
36+
const QuickThemeSwitcher: React.FC<Props> = ({ requestClose }) => {
37+
const orderedThemes = useMemo(getOrderedThemes, []);
38+
39+
const themeState = ThemeChoicePanel.calculateThemeState();
40+
const nonHighContrast = findNonHighContrastTheme(themeState.theme);
41+
const theme = nonHighContrast ? nonHighContrast : themeState.theme;
42+
const { useSystemTheme } = themeState;
43+
44+
const themeOptions = [{
45+
id: MATCH_SYSTEM_THEME_ID,
46+
name: 'Match system',
47+
}, ...orderedThemes];
48+
49+
const selectedTheme = useSystemTheme ? MATCH_SYSTEM_THEME_ID : theme;
50+
51+
const onOptionChange = async (newTheme: string) => {
52+
PosthogTrackers.trackInteraction("WebQuickSettingsThemeDropdown");
53+
54+
try {
55+
if (newTheme === MATCH_SYSTEM_THEME_ID) {
56+
await SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, true);
57+
} else {
58+
// The settings watcher doesn't fire until the echo comes back from the
59+
// server, so to make the theme change immediately we need to manually
60+
// do the dispatch now
61+
// XXX: The local echoed value appears to be unreliable, in particular
62+
// when settings custom themes(!) so adding forceTheme to override
63+
// the value from settings.
64+
dis.dispatch<RecheckThemePayload>({ action: Action.RecheckTheme, forceTheme: newTheme });
65+
await Promise.all([
66+
SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme),
67+
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false),
68+
]);
69+
}
70+
} catch (_error) {
71+
dis.dispatch<RecheckThemePayload>({ action: Action.RecheckTheme });
72+
}
73+
74+
requestClose();
75+
};
76+
77+
return <div className="mx_QuickThemeSwitcher">
78+
<h4 className="mx_QuickThemeSwitcher_heading">{ _t("Theme") }</h4>
79+
<Dropdown
80+
id="mx_QuickSettingsButton_themePickerDropdown"
81+
onOptionChange={onOptionChange}
82+
value={selectedTheme}
83+
label={_t("Space selection")}
84+
>
85+
{ themeOptions.map((theme) => (
86+
<div key={theme.id}>
87+
{ theme.name }
88+
</div>
89+
)) }
90+
</Dropdown>
91+
</div>;
92+
};
93+
94+
export default QuickThemeSwitcher;

src/i18n/strings/en_EN.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1112,9 +1112,9 @@
11121112
"All settings": "All settings",
11131113
"Pin to sidebar": "Pin to sidebar",
11141114
"More options": "More options",
1115+
"Settings": "Settings",
11151116
"Theme": "Theme",
11161117
"Space selection": "Space selection",
1117-
"Settings": "Settings",
11181118
"Delete avatar": "Delete avatar",
11191119
"Delete": "Delete",
11201120
"Upload avatar": "Upload avatar",
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from 'react';
18+
import { mount } from 'enzyme';
19+
import { mocked } from 'jest-mock';
20+
import { act } from 'react-dom/test-utils';
21+
22+
import '../../../skinned-sdk';
23+
import QuickThemeSwitcher from '../../../../src/components/views/spaces/QuickThemeSwitcher';
24+
import { getOrderedThemes } from '../../../../src/theme';
25+
import ThemeChoicePanel from '../../../../src/components/views/settings/ThemeChoicePanel';
26+
import SettingsStore from '../../../../src/settings/SettingsStore';
27+
import { findById } from '../../../test-utils';
28+
import { SettingLevel } from '../../../../src/settings/SettingLevel';
29+
import dis from '../../../../src/dispatcher/dispatcher';
30+
import { Action } from '../../../../src/dispatcher/actions';
31+
32+
jest.mock('../../../../src/theme');
33+
jest.mock('../../../../src/components/views/settings/ThemeChoicePanel', () => ({
34+
calculateThemeState: jest.fn(),
35+
}));
36+
jest.mock('../../../../src/settings/SettingsStore', () => ({
37+
setValue: jest.fn(),
38+
getValue: jest.fn(),
39+
monitorSetting: jest.fn(),
40+
}));
41+
42+
jest.mock('../../../../src/dispatcher/dispatcher', () => ({
43+
dispatch: jest.fn(),
44+
register: jest.fn(),
45+
}));
46+
47+
describe('<QuickThemeSwitcher />', () => {
48+
const defaultProps = {
49+
requestClose: jest.fn(),
50+
};
51+
const getComponent = (props = {}) =>
52+
mount(<QuickThemeSwitcher {...defaultProps} {...props} />);
53+
54+
beforeEach(() => {
55+
mocked(getOrderedThemes).mockClear().mockReturnValue([
56+
{ id: 'light', name: 'Light' },
57+
{ id: 'dark', name: 'Dark' },
58+
]);
59+
mocked(ThemeChoicePanel).calculateThemeState.mockClear().mockReturnValue({
60+
theme: 'light', useSystemTheme: false,
61+
});
62+
mocked(SettingsStore).setValue.mockClear().mockResolvedValue();
63+
mocked(dis).dispatch.mockClear();
64+
});
65+
66+
const getSelectedLabel = (component) =>
67+
findById(component, "mx_QuickSettingsButton_themePickerDropdown_value").text();
68+
69+
const openDropdown = component => act(async () => {
70+
component.find('.mx_Dropdown_input').at(0).simulate('click');
71+
component.setProps({});
72+
});
73+
const getOption = (component, themeId) =>
74+
findById(component, `mx_QuickSettingsButton_themePickerDropdown__${themeId}`).at(0);
75+
76+
const selectOption = async (component, themeId: string) => {
77+
await openDropdown(component);
78+
await act(async () => {
79+
getOption(component, themeId).simulate('click');
80+
});
81+
};
82+
83+
it('renders dropdown correctly when light theme is selected', () => {
84+
const component = getComponent();
85+
expect(getSelectedLabel(component)).toEqual('Light');
86+
});
87+
88+
it('renders dropdown correctly when use system theme is truthy', () => {
89+
mocked(ThemeChoicePanel).calculateThemeState.mockClear().mockReturnValue({
90+
theme: 'light', useSystemTheme: true,
91+
});
92+
const component = getComponent();
93+
expect(getSelectedLabel(component)).toEqual('Match system');
94+
});
95+
96+
it('updates settings when match system is selected', async () => {
97+
const requestClose = jest.fn();
98+
const component = getComponent({ requestClose });
99+
100+
await selectOption(component, 'MATCH_SYSTEM_THEME_ID');
101+
102+
expect(SettingsStore.setValue).toHaveBeenCalledTimes(1);
103+
expect(SettingsStore.setValue).toHaveBeenCalledWith('use_system_theme', null, SettingLevel.DEVICE, true);
104+
105+
expect(dis.dispatch).not.toHaveBeenCalled();
106+
expect(requestClose).toHaveBeenCalled();
107+
});
108+
109+
it('updates settings when a theme is selected', async () => {
110+
// ie not match system
111+
const requestClose = jest.fn();
112+
const component = getComponent({ requestClose });
113+
114+
await selectOption(component, 'dark');
115+
116+
expect(SettingsStore.setValue).toHaveBeenCalledWith('use_system_theme', null, SettingLevel.DEVICE, false);
117+
expect(SettingsStore.setValue).toHaveBeenCalledWith('theme', null, SettingLevel.DEVICE, 'dark');
118+
119+
expect(dis.dispatch).toHaveBeenCalledWith({ action: Action.RecheckTheme, forceTheme: 'dark' });
120+
expect(requestClose).toHaveBeenCalled();
121+
});
122+
123+
it('rechecks theme when setting theme fails', async () => {
124+
mocked(SettingsStore.setValue).mockRejectedValue('oops');
125+
const requestClose = jest.fn();
126+
const component = getComponent({ requestClose });
127+
128+
await selectOption(component, 'MATCH_SYSTEM_THEME_ID');
129+
130+
expect(dis.dispatch).toHaveBeenCalledWith({ action: Action.RecheckTheme });
131+
expect(requestClose).toHaveBeenCalled();
132+
});
133+
});

0 commit comments

Comments
 (0)