Skip to content

Commit

Permalink
New theme ui in user settings (#12576)
Browse files Browse the repository at this point in the history
* Add hook to get the theme

* Adapt subsection settings to new ui

* WIP new theme subsection

* Add theme selection

* Fix test types

* Disabled theme selector when system theme is used

* Update compound to `4.4.1`

* Add custom theme support

* Remove old ThemChoicePanel

* Fix QuickThemeSwitcher-test.tsx

* Fix AppearanceUserSettingsTab-test.tsx

* Update i18n

* Fix ThemeChoicePanel-test.tsx

* Update `@vector-im/compound-web`

* Small tweaks

* Fix CSS comments and use compound variable

* Remove custom theme title

* i18n: update

* test: add tests to theme selection

* test: update AppearanceUserSettingsTab-test snapshot

* test: rework custom theme

* playwright: fix audio-player.spec.ts

* playwright: appearance tab

* test: update snapshot

* playright: add custom theme

* i18n: use correct char for ellipsis

* a11y: add missing aria-label to delete button

* dialog: update close button tooltip

* theme: remove local state and handle custom delete

* theme: don't add twice the same custom theme

* test: update snapshot

* playwright: update snapshot

* custom theme: add background to custom theme list

* update compound web

* Use new destructive property on `IconButton` of theme panel

* test: update snapshots

* rename new ui into legacy

* remove wrong constructor doc

* fix theme selector padding

* theme selector: fix key

* test: fix e2e
  • Loading branch information
florianduros authored Jun 26, 2024
1 parent 8ede891 commit 33a017b
Show file tree
Hide file tree
Showing 30 changed files with 1,731 additions and 459 deletions.
2 changes: 1 addition & 1 deletion playwright/e2e/audio-player/audio-player.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ test.describe("Audio player", () => {

// Enable high contrast manually
const settings = await app.settings.openUserSettings("Appearance");
await settings.getByTestId("mx_ThemeChoicePanel").getByText("Use high contrast").click();
await settings.getByRole("radio", { name: "High contrast" }).click();

await app.closeDialog();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { test, expect } from "../../element-web-test";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { expect, test } from ".";

test.describe("Appearance user settings tab", () => {
test.use({
Expand Down Expand Up @@ -151,69 +150,68 @@ test.describe("Appearance user settings tab", () => {
});

test.describe("Theme Choice Panel", () => {
test.beforeEach(async ({ app, user }) => {
test.beforeEach(async ({ app, user, util }) => {
// Disable the default theme for consistency in case ThemeWatcher automatically chooses it
await app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
await util.disableSystemTheme();
await util.openAppearanceTab();
});

test("should be rendered with the light theme selected", async ({ page, app }) => {
await app.settings.openUserSettings("Appearance");
const themePanel = page.getByTestId("mx_ThemeChoicePanel");

const useSystemTheme = themePanel.getByTestId("checkbox-use-system-theme");
await expect(useSystemTheme.getByText("Match system theme")).toBeVisible();
test("should be rendered with the light theme selected", async ({ page, app, util }) => {
// Assert that 'Match system theme' is not checked
// Note that mx_Checkbox_checkmark exists and is hidden by CSS if it is not checked
await expect(useSystemTheme.locator(".mx_Checkbox_checkmark")).not.toBeVisible();
await expect(util.getMatchSystemThemeCheckbox()).not.toBeChecked();

const selectors = themePanel.getByTestId("theme-choice-panel-selectors");
await expect(selectors.locator(".mx_ThemeSelector_light")).toBeVisible();
await expect(selectors.locator(".mx_ThemeSelector_dark")).toBeVisible();
// Assert that the light theme is selected
await expect(selectors.locator(".mx_ThemeSelector_light.mx_StyledRadioButton_enabled")).toBeVisible();
// Assert that the buttons for the light and dark theme are not enabled
await expect(selectors.locator(".mx_ThemeSelector_light.mx_StyledRadioButton_disabled")).not.toBeVisible();
await expect(selectors.locator(".mx_ThemeSelector_dark.mx_StyledRadioButton_disabled")).not.toBeVisible();
await expect(util.getLightTheme()).toBeChecked();
// Assert that the dark and high contrast themes are not selected
await expect(util.getDarkTheme()).not.toBeChecked();
await expect(util.getHighContrastTheme()).not.toBeChecked();

// Assert that the checkbox for the high contrast theme is rendered
await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).toBeVisible();
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png");
});

test("should disable the labels for themes and the checkbox for the high contrast theme if the checkbox for the system theme is clicked", async ({
page,
app,
}) => {
await app.settings.openUserSettings("Appearance");
const themePanel = page.getByTestId("mx_ThemeChoicePanel");
test("should disable the themes when the system theme is clicked", async ({ page, app, util }) => {
await util.getMatchSystemThemeCheckbox().click();

// Assert that the themes are disabled
await expect(util.getLightTheme()).toBeDisabled();
await expect(util.getDarkTheme()).toBeDisabled();
await expect(util.getHighContrastTheme()).toBeDisabled();

await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-match-system-enabled.png");
});

await themePanel.locator(".mx_Checkbox", { hasText: "Match system theme" }).click();
test("should change the theme to dark", async ({ page, app, util }) => {
// Assert that the light theme is selected
await expect(util.getLightTheme()).toBeChecked();

// Assert that the labels for the light theme and dark theme are disabled
await expect(themePanel.locator(".mx_ThemeSelector_light.mx_StyledRadioButton_disabled")).toBeVisible();
await expect(themePanel.locator(".mx_ThemeSelector_dark.mx_StyledRadioButton_disabled")).toBeVisible();
await util.getDarkTheme().click();

// Assert that there does not exist a label for an enabled theme
await expect(themePanel.locator("label.mx_StyledRadioButton_enabled")).not.toBeVisible();
// Assert that the light and high contrast themes are not selected
await expect(util.getLightTheme()).not.toBeChecked();
await expect(util.getDarkTheme()).toBeChecked();
await expect(util.getHighContrastTheme()).not.toBeChecked();

// Assert that the checkbox and label to enable the high contrast theme should not exist
await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).not.toBeVisible();
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-dark.png");
});

test("should not render the checkbox and the label for the high contrast theme if the dark theme is selected", async ({
page,
app,
}) => {
await app.settings.openUserSettings("Appearance");
const themePanel = page.getByTestId("mx_ThemeChoicePanel");
test.describe("custom theme", () => {
test.use({
labsFlags: ["feature_custom_themes"],
});

test("should render the custom theme section", async ({ page, app, util }) => {
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme.png");
});

// Assert that the checkbox and the label to enable the high contrast theme should exist
await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).toBeVisible();
test("should be able to add and remove a custom theme", async ({ page, app, util }) => {
await util.addCustomTheme();

// Enable the dark theme
await themePanel.locator(".mx_ThemeSelector_dark").click();
await expect(util.getCustomTheme()).not.toBeChecked();
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-added.png");

// Assert that the checkbox and the label should not exist
await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).not.toBeVisible();
await util.removeCustomTheme();
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme.png");
});
});
});
});
139 changes: 139 additions & 0 deletions playwright/e2e/settings/appearance-user-settings-tab/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Page } from "@playwright/test";

import { ElementAppPage } from "../../../pages/ElementAppPage";
import { test as base, expect } from "../../../element-web-test";
import { SettingLevel } from "../../../../src/settings/SettingLevel";

export { expect };

/**
* Set up for the appearance tab test
*/
export const test = base.extend<{
util: Helpers;
}>({
util: async ({ page, app }, use) => {
await use(new Helpers(page, app));
},
});

/**
* A collection of helper functions for the appearance tab test
* The goal is to make easier to get and interact with the button, input, or other elements of the appearance tab
*/
class Helpers {
private CUSTOM_THEME_URL = "http://custom.theme";
private CUSTOM_THEME = {
name: "Custom theme",
isDark: false,
colors: {},
};

constructor(
private page: Page,
private app: ElementAppPage,
) {}

/**
* Open the appearance tab
*/
openAppearanceTab() {
return this.app.settings.openUserSettings("Appearance");
}

// Theme Panel

/**
* Disable in the settings the system theme
*/
disableSystemTheme() {
return this.app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
}

/**
* Return the theme section
*/
getThemePanel() {
return this.page.getByTestId("themePanel");
}

/**
* Return the system theme toggle
*/
getMatchSystemThemeCheckbox() {
return this.getThemePanel().getByRole("checkbox");
}

/**
* Return the theme radio button
* @param theme - the theme to select
* @private
*/
private getThemeRadio(theme: string) {
return this.getThemePanel().getByRole("radio", { name: theme });
}

/**
* Return the light theme radio button
*/
getLightTheme() {
return this.getThemeRadio("Light");
}

/**
* Return the dark theme radio button
*/
getDarkTheme() {
return this.getThemeRadio("Dark");
}

/**
* Return the custom theme radio button
*/
getCustomTheme() {
return this.getThemeRadio(this.CUSTOM_THEME.name);
}

/**
* Return the high contrast theme radio button
*/
getHighContrastTheme() {
return this.getThemeRadio("High contrast");
}

/**
* Add a custom theme
* Mock the request to the custom and return a fake local custom theme
*/
async addCustomTheme() {
await this.page.route(this.CUSTOM_THEME_URL, (route) =>
route.fulfill({ body: JSON.stringify(this.CUSTOM_THEME) }),
);
await this.page.getByRole("textbox", { name: "Add custom theme" }).fill(this.CUSTOM_THEME_URL);
await this.page.getByRole("button", { name: "Add custom theme" }).click();
await this.page.unroute(this.CUSTOM_THEME_URL);
}

/**
* Remove the custom theme
*/
removeCustomTheme() {
return this.getThemePanel().getByRole("listitem", { name: this.CUSTOM_THEME.name }).getByRole("button").click();
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 8 additions & 6 deletions res/css/_common.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,7 @@ legend {
.mx_Dialog
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button
),
):not(.mx_ThemeChoicePanel_CustomTheme button),
.mx_Dialog input[type="submit"],
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton),
.mx_Dialog_buttons input[type="submit"] {
Expand All @@ -624,14 +624,14 @@ legend {
.mx_Dialog
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button
):last-child {
):not(.mx_ThemeChoicePanel_CustomTheme button):last-child {
margin-right: 0px;
}

.mx_Dialog
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button
):focus,
):not(.mx_ThemeChoicePanel_CustomTheme button):focus,
.mx_Dialog input[type="submit"]:focus,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus,
.mx_Dialog_buttons input[type="submit"]:focus {
Expand All @@ -643,7 +643,7 @@ legend {
.mx_Dialog_buttons
button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button
),
):not(.mx_ThemeChoicePanel_CustomTheme button),
.mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
color: var(--cpd-color-text-on-solid-primary);
background-color: var(--cpd-color-bg-action-primary-rest);
Expand All @@ -654,7 +654,9 @@ legend {
.mx_Dialog button.danger:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]),
.mx_Dialog input[type="submit"].danger,
.mx_Dialog_buttons
button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button),
button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not(
.mx_ThemeChoicePanel_CustomTheme button
),
.mx_Dialog_buttons input[type="submit"].danger {
background-color: var(--cpd-color-bg-critical-primary);
border: solid 1px var(--cpd-color-bg-critical-primary);
Expand All @@ -670,7 +672,7 @@ legend {
.mx_Dialog
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button
):disabled,
):not(.mx_ThemeChoicePanel_CustomTheme button):disabled,
.mx_Dialog input[type="submit"]:disabled,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled,
.mx_Dialog_buttons input[type="submit"]:disabled {
Expand Down
10 changes: 10 additions & 0 deletions res/css/components/views/settings/shared/_SettingsSubsection.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ limitations under the License.
.mx_SettingsSubsection {
width: 100%;
box-sizing: border-box;

&.mx_SettingsSubsection_newUi {
display: flex;
flex-direction: column;
gap: var(--cpd-space-8x);
}
}

.mx_SettingsSubsection_description {
Expand Down Expand Up @@ -54,4 +60,8 @@ limitations under the License.
&.mx_SettingsSubsection_noHeading {
margin-top: 0;
}
&.mx_SettingsSubsection_content_newUi {
gap: var(--cpd-space-6x);
margin-top: 0;
}
}
Loading

0 comments on commit 33a017b

Please sign in to comment.