diff --git a/apps/web/playwright/e2e/settings/account-user-settings-tab.spec.ts b/apps/web/playwright/e2e/settings/account-user-settings-tab.spec.ts index 07c3a9592d..b3ec952b01 100644 --- a/apps/web/playwright/e2e/settings/account-user-settings-tab.spec.ts +++ b/apps/web/playwright/e2e/settings/account-user-settings-tab.spec.ts @@ -24,7 +24,7 @@ test.describe("Account user settings tab", () => { }, }); - test("should be rendered properly", { tag: "@screenshot" }, async ({ uut, user }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ uut, user, axe }) => { await expect(uut).toMatchScreenshot("account.png"); // Assert that the top heading is rendered @@ -70,6 +70,8 @@ test.describe("Account user settings tab", () => { await expect(accountManagementSection.getByRole("button", { name: "Deactivate Account" })).toHaveClass( /mx_AccessibleButton_kind_danger/, ); + + await expect(axe).toHaveNoViolations(); }); test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page, uut }) => { diff --git a/apps/web/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts b/apps/web/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts index c6af256a5d..457c5c17cf 100644 --- a/apps/web/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts +++ b/apps/web/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts @@ -13,7 +13,7 @@ test.describe("Appearance user settings tab", () => { displayName: "Hanako", }); - test("should be rendered properly", { tag: "@screenshot" }, async ({ page, user, app }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ page, user, app, axe }) => { const tab = await app.settings.openUserSettings("Appearance"); // Click "Show advanced" link button @@ -23,6 +23,8 @@ test.describe("Appearance user settings tab", () => { await expect(tab.getByRole("button", { name: "Hide advanced" })).toBeVisible(); await expect(tab).toMatchScreenshot("appearance-tab.png"); + + await expect(axe).toHaveNoViolations(); }); test( diff --git a/apps/web/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts b/apps/web/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts index 8b7cf720c8..cb38f923e9 100644 --- a/apps/web/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts +++ b/apps/web/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts @@ -23,7 +23,7 @@ test.describe("Appearance user settings tab", () => { test( "should be rendered with the light theme selected", { tag: "@screenshot" }, - async ({ page, app, util }) => { + async ({ page, app, util, axe }) => { // Assert that 'Match system theme' is not checked await expect(util.getMatchSystemThemeSwitch()).not.toBeChecked(); @@ -34,6 +34,8 @@ test.describe("Appearance user settings tab", () => { await expect(util.getHighContrastTheme()).not.toBeChecked(); await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png"); + + await expect(axe).toHaveNoViolations(); }, ); diff --git a/apps/web/playwright/e2e/settings/device-management.spec.ts b/apps/web/playwright/e2e/settings/device-management.spec.ts index 9f60b2433c..265ec9c877 100644 --- a/apps/web/playwright/e2e/settings/device-management.spec.ts +++ b/apps/web/playwright/e2e/settings/device-management.spec.ts @@ -23,7 +23,7 @@ test.describe("Device manager", () => { } }); - test("should display sessions", async ({ page, app }) => { + test("should display sessions", async ({ page, app, axe }) => { await app.settings.openUserSettings("Sessions"); const tab = page.locator(".mx_SettingsTab"); @@ -85,7 +85,7 @@ test.describe("Device manager", () => { // session name updated in details await expect(firstSession.locator(".mx_DeviceDetailHeading h4").getByText(sessionName)).toBeVisible(); // and main list item - await expect(firstSession.locator(".mx_DeviceTile h4").getByText(sessionName)).toBeVisible(); + await expect(firstSession.locator(".mx_DeviceTile h3").getByText(sessionName)).toBeVisible(); // sign out using the device details sign out await firstSession.getByRole("button", { name: "Remove this session" }).click(); @@ -96,5 +96,7 @@ test.describe("Device manager", () => { // no other sessions or security recommendations sections when only one session await expect(tab.getByText("Other sessions")).not.toBeVisible(); await expect(tab.getByTestId("security-recommendations-section")).not.toBeVisible(); + + await expect(axe).toHaveNoViolations(); }); }); diff --git a/apps/web/playwright/e2e/settings/encryption-user-tab/advanced.spec.ts b/apps/web/playwright/e2e/settings/encryption-user-tab/advanced.spec.ts index 1d8ae102fd..0167a321b7 100644 --- a/apps/web/playwright/e2e/settings/encryption-user-tab/advanced.spec.ts +++ b/apps/web/playwright/e2e/settings/encryption-user-tab/advanced.spec.ts @@ -16,7 +16,7 @@ test.describe("Advanced section in Encryption tab", () => { await bootstrapCrossSigningForClient(clientHandle, credentials, true); }); - test("should show the encryption details", { tag: "@screenshot" }, async ({ page, app, util }) => { + test("should show the encryption details", { tag: "@screenshot" }, async ({ page, app, util, axe }) => { await util.openEncryptionTab(); const section = util.getEncryptionDetailsSection(); @@ -26,6 +26,8 @@ test.describe("Advanced section in Encryption tab", () => { await expect(section).toMatchScreenshot("encryption-details.png", { mask: [section.getByTestId("deviceId"), section.getByTestId("sessionKey")], }); + + await expect(axe).toHaveNoViolations(); }); test("should show the import room keys dialog", async ({ page, app, util }) => { diff --git a/apps/web/playwright/e2e/settings/general-room-settings-tab.spec.ts b/apps/web/playwright/e2e/settings/general-room-settings-tab.spec.ts index 376412914a..e3f0963b59 100644 --- a/apps/web/playwright/e2e/settings/general-room-settings-tab.spec.ts +++ b/apps/web/playwright/e2e/settings/general-room-settings-tab.spec.ts @@ -20,7 +20,7 @@ test.describe("General room settings tab", () => { await app.viewRoomByName(roomName); }); - test("should be rendered properly", { tag: "@screenshot" }, async ({ page, app }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ page, app, axe }) => { const settings = await app.settings.openRoomSettings("General"); // Assert that "Show less" details element is rendered @@ -34,6 +34,9 @@ test.describe("General room settings tab", () => { // Assert that "Show more" details element is rendered instead of "Show more" await expect(settings.getByText("Show less")).not.toBeVisible(); await expect(settings.getByText("Show more")).toBeVisible(); + + axe.disableRules("color-contrast"); // XXX: We have some known contrast issues here + await expect(axe).toHaveNoViolations(); }); test("long address should not cause dialog to overflow", { tag: "@no-webkit" }, async ({ page, app, user }) => { diff --git a/apps/web/playwright/e2e/settings/preferences-user-settings-tab.spec.ts b/apps/web/playwright/e2e/settings/preferences-user-settings-tab.spec.ts index 8adbc74cc5..5414055534 100644 --- a/apps/web/playwright/e2e/settings/preferences-user-settings-tab.spec.ts +++ b/apps/web/playwright/e2e/settings/preferences-user-settings-tab.spec.ts @@ -25,7 +25,7 @@ test.describe("Preferences user settings tab", () => { labsFlags: ["feature_new_room_list"], }); - test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user, axe }) => { await page.setViewportSize({ width: 1024, height: 4000 }); const tab = await app.settings.openUserSettings("Preferences"); // Assert that the top heading is rendered @@ -39,6 +39,8 @@ test.describe("Preferences user settings tab", () => { } `, }); + + await expect(axe).toHaveNoViolations(); }); test("should be able to change the app language", { tag: ["@no-firefox", "@no-webkit"] }, async ({ uut, user }) => { diff --git a/apps/web/playwright/e2e/settings/quick-settings-menu.spec.ts b/apps/web/playwright/e2e/settings/quick-settings-menu.spec.ts index e58d523c21..37d9adc458 100644 --- a/apps/web/playwright/e2e/settings/quick-settings-menu.spec.ts +++ b/apps/web/playwright/e2e/settings/quick-settings-menu.spec.ts @@ -8,11 +8,13 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "../../element-web-test"; test.describe("Quick settings menu", () => { - test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user, axe }) => { await page.getByRole("button", { name: "Quick settings" }).click(); // Assert that the top heading is renderedc const settings = page.getByTestId("quick-settings-menu"); await expect(settings).toBeVisible(); await expect(settings).toMatchScreenshot("quick-settings.png"); + + await expect(axe).toHaveNoViolations(); }); }); diff --git a/apps/web/playwright/e2e/settings/room-settings/roles-permissions-room-settings-tab.spec.ts b/apps/web/playwright/e2e/settings/room-settings/roles-permissions-room-settings-tab.spec.ts index e575f71b72..c646e613c4 100644 --- a/apps/web/playwright/e2e/settings/room-settings/roles-permissions-room-settings-tab.spec.ts +++ b/apps/web/playwright/e2e/settings/room-settings/roles-permissions-room-settings-tab.spec.ts @@ -25,7 +25,7 @@ test.describe("Roles & Permissions room settings tab", () => { settings = await app.settings.openRoomSettings("Roles & Permissions"); }); - test("should be able to change the role of a user", async ({ page, app, user }) => { + test("should be able to change the role of a user", async ({ page, app, user, axe }) => { const privilegedUserSection = settings.locator(".mx_SettingsFieldset").first(); const applyButton = privilegedUserSection.getByRole("button", { name: "Apply" }); @@ -55,5 +55,7 @@ test.describe("Roles & Permissions room settings tab", () => { settings = await app.settings.openRoomSettings("Roles & Permissions"); combobox = privilegedUserSection.getByRole("combobox", { name: user.userId }); await expect(combobox).toHaveValue("50"); + + await expect(axe).toHaveNoViolations(); }); }); diff --git a/apps/web/playwright/e2e/settings/security-user-settings-tab.spec.ts b/apps/web/playwright/e2e/settings/security-user-settings-tab.spec.ts index 377cae7495..25f430c68d 100644 --- a/apps/web/playwright/e2e/settings/security-user-settings-tab.spec.ts +++ b/apps/web/playwright/e2e/settings/security-user-settings-tab.spec.ts @@ -32,12 +32,14 @@ test.describe("Security user settings tab", () => { }); test.describe("AnalyticsLearnMoreDialog", () => { - test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user, axe }) => { const tab = await app.settings.openUserSettings("Security"); await tab.getByRole("button", { name: "Learn more" }).click(); await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot( "Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1.png", ); + + await expect(axe).toHaveNoViolations(); }); }); diff --git a/apps/web/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png b/apps/web/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png index bd01876a5a..8cd0cbf863 100644 Binary files a/apps/web/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png and b/apps/web/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png b/apps/web/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png index dd449fae6c..14fd316f79 100644 Binary files a/apps/web/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png and b/apps/web/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/default-tab-linux.png b/apps/web/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/default-tab-linux.png index bdee266c25..efdd7e3cba 100644 Binary files a/apps/web/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/default-tab-linux.png and b/apps/web/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/default-tab-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png b/apps/web/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png index 2d0190c601..48625def3a 100644 Binary files a/apps/web/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png and b/apps/web/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-linux.png b/apps/web/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-linux.png index 8e1db6f498..95657048ac 100644 Binary files a/apps/web/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-linux.png and b/apps/web/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-world-readable-linux.png b/apps/web/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-world-readable-linux.png index ee764d5835..f5f3fa0382 100644 Binary files a/apps/web/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-world-readable-linux.png and b/apps/web/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-world-readable-linux.png differ diff --git a/apps/web/res/css/views/settings/tabs/_SettingsSection.pcss b/apps/web/res/css/views/settings/tabs/_SettingsSection.pcss index ce3c9266c3..c98732e884 100644 --- a/apps/web/res/css/views/settings/tabs/_SettingsSection.pcss +++ b/apps/web/res/css/views/settings/tabs/_SettingsSection.pcss @@ -36,5 +36,5 @@ Please see LICENSE files in the repository root for full details. grid-template-columns: minmax(0, 1fr); gap: $spacing-32; - padding: $spacing-16 0; + margin: $spacing-16 0; } diff --git a/apps/web/src/components/structures/ContextMenu.tsx b/apps/web/src/components/structures/ContextMenu.tsx index 9d4b76e38e..3cd43b5d91 100644 --- a/apps/web/src/components/structures/ContextMenu.tsx +++ b/apps/web/src/components/structures/ContextMenu.tsx @@ -8,7 +8,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, type CSSProperties, type RefObject, type SyntheticEvent, useRef, useState } from "react"; +import React, { + type JSX, + type CSSProperties, + type RefObject, + type SyntheticEvent, + useRef, + useState, + type AriaRole, +} from "react"; import ReactDOM from "react-dom"; import classNames from "classnames"; import FocusLock from "react-focus-lock"; @@ -74,27 +82,31 @@ export interface MenuProps extends IPosition { export interface IProps extends MenuProps { // If true, insert an invisible screen-sized element behind the menu that when clicked will close it. - hasBackground?: boolean; + "hasBackground"?: boolean; // whether this context menu should be focus managed. If false it must handle itself - managed?: boolean; - wrapperClassName?: string; - menuClassName?: string; + "managed"?: boolean; + "wrapperClassName"?: string; + "menuClassName"?: string; // If true, this context menu will be mounted as a child to the parent container. Otherwise // it will be mounted to a container at the root of the DOM. - mountAsChild?: boolean; + "mountAsChild"?: boolean; // If specified, contents will be wrapped in a FocusLock, this is only needed if the context menu is being rendered // within an existing FocusLock e.g inside a modal. - focusLock?: boolean; + "focusLock"?: boolean; // call onFinished on any interaction with the menu - closeOnInteraction?: boolean; + "closeOnInteraction"?: boolean; // Function to be called on menu close onFinished(this: void): void; // on resize callback windowResize?(this: void): void; + + // Role & label for accessibility + "role"?: AriaRole; + "aria-label"?: string; } interface IState { @@ -257,9 +269,11 @@ export default class ContextMenu extends React.PureComponent {background} diff --git a/apps/web/src/components/views/settings/SetIntegrationManager.tsx b/apps/web/src/components/views/settings/SetIntegrationManager.tsx index 44507067ad..2cf80cebda 100644 --- a/apps/web/src/components/views/settings/SetIntegrationManager.tsx +++ b/apps/web/src/components/views/settings/SetIntegrationManager.tsx @@ -76,8 +76,10 @@ export default class SetIntegrationManager extends React.Component
- {_t("integration_manager|manage_title")} - + + {_t("integration_manager|manage_title")} + + {managerName}
diff --git a/apps/web/src/components/views/settings/SettingsHeader.tsx b/apps/web/src/components/views/settings/SettingsHeader.tsx index 1db7fc9027..6a5cc7b8b2 100644 --- a/apps/web/src/components/views/settings/SettingsHeader.tsx +++ b/apps/web/src/components/views/settings/SettingsHeader.tsx @@ -13,6 +13,10 @@ import { Heading } from "@vector-im/compound-web"; * The heading for a settings section. */ interface SettingsHeaderProps { + /** + * The component to render the heading as, defaults to h2 + */ + as?: React.ComponentProps["as"]; /** * Whether the user has a recommended tag. */ @@ -23,12 +27,12 @@ interface SettingsHeaderProps { label: string; } -export function SettingsHeader({ hasRecommendedTag = false, label }: SettingsHeaderProps): JSX.Element { +export function SettingsHeader({ hasRecommendedTag = false, label, as = "h2" }: SettingsHeaderProps): JSX.Element { const classes = classNames("mx_SettingsHeader", { mx_SettingsHeader_recommended: hasRecommendedTag, }); return ( - + {label} ); diff --git a/apps/web/src/components/views/settings/UserPersonalInfoSettings.tsx b/apps/web/src/components/views/settings/UserPersonalInfoSettings.tsx index a4106341b1..1ef10fe1b0 100644 --- a/apps/web/src/components/views/settings/UserPersonalInfoSettings.tsx +++ b/apps/web/src/components/views/settings/UserPersonalInfoSettings.tsx @@ -18,7 +18,7 @@ import { type ThirdPartyIdentifier } from "../../../AddThreepid"; import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; import { AddRemoveThreepids } from "./AddRemoveThreepids"; -import Heading from "../typography/Heading.tsx"; +import { SettingsSection } from "./shared/SettingsSection.tsx"; type LoadingState = "loading" | "loaded" | "error"; @@ -82,8 +82,7 @@ export const UserPersonalInfoSettings: React.FC = if (!SettingsStore.getValue(UIFeature.ThirdPartyID)) return null; return ( -
- {_t("settings|general|personal_info")} + = /> -
+ ); }; diff --git a/apps/web/src/components/views/settings/UserProfileSettings.tsx b/apps/web/src/components/views/settings/UserProfileSettings.tsx index 5e4391f6e9..92b4390f36 100644 --- a/apps/web/src/components/views/settings/UserProfileSettings.tsx +++ b/apps/web/src/components/views/settings/UserProfileSettings.tsx @@ -27,7 +27,7 @@ import AccessibleButton from "../elements/AccessibleButton"; import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog"; import Modal from "../../../Modal"; import defaultDispatcher from "../../../dispatcher/dispatcher"; -import Heading from "../typography/Heading.tsx"; +import { SettingsSection } from "./shared/SettingsSection.tsx"; const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => ( <> @@ -194,54 +194,55 @@ const UserProfileSettings: React.FC = ({ const someFieldsDisabled = !canSetDisplayName || !canSetAvatar; return ( -
- {_t("common|profile")} -
- {someFieldsDisabled - ? _t("settings|general|profile_subtitle_oidc") - : _t("settings|general|profile_subtitle")} -
-
- - - {displayNameError && {_t("settings|general|display_name_error")}} - -
- {avatarError && ( - - {maxUploadSize === undefined - ? _t("settings|general|avatar_upload_error_text_generic") - : _t("settings|general|avatar_upload_error_text", { size: formatBytes(maxUploadSize) })} - - )} - {userIdentifier && } - - {externalAccountManagementUrl && ( - + +
+
+ {someFieldsDisabled + ? _t("settings|general|profile_subtitle_oidc") + : _t("settings|general|profile_subtitle")} +
+
+ + + {displayNameError && {_t("settings|general|display_name_error")}} + +
+ {avatarError && ( + + {maxUploadSize === undefined + ? _t("settings|general|avatar_upload_error_text_generic") + : _t("settings|general|avatar_upload_error_text", { size: formatBytes(maxUploadSize) })} + )} - - -
+ {userIdentifier && } + + {externalAccountManagementUrl && ( + + )} + + +
+ ); }; diff --git a/apps/web/src/components/views/settings/devices/DeviceTile.tsx b/apps/web/src/components/views/settings/devices/DeviceTile.tsx index 1295b8da95..c2490a06d9 100644 --- a/apps/web/src/components/views/settings/devices/DeviceTile.tsx +++ b/apps/web/src/components/views/settings/devices/DeviceTile.tsx @@ -22,7 +22,11 @@ export interface DeviceTileProps { } const DeviceTileName: React.FC<{ device: ExtendedDevice }> = ({ device }) => { - return {device.display_name || device.device_id}; + return ( + + {device.display_name || device.device_id} + + ); }; const DeviceTile: React.FC = ({ device, children, isSelected, onClick }) => { diff --git a/apps/web/src/components/views/settings/encryption/KeyStoragePanel.tsx b/apps/web/src/components/views/settings/encryption/KeyStoragePanel.tsx index 9f67e90f1a..35073f00e9 100644 --- a/apps/web/src/components/views/settings/encryption/KeyStoragePanel.tsx +++ b/apps/web/src/components/views/settings/encryption/KeyStoragePanel.tsx @@ -6,7 +6,7 @@ */ import React, { useCallback } from "react"; -import { InlineField, InlineSpinner, Label, Root, ToggleControl } from "@vector-im/compound-web"; +import { InlineField, InlineSpinner, Label, Link, Root, ToggleControl } from "@vector-im/compound-web"; import type { FormEvent } from "react"; import { SettingsSection } from "../shared/SettingsSection"; @@ -56,9 +56,9 @@ export const KeyStoragePanel: React.FC = ({ onKeyStorageDisableClick }) = } subHeading={_t("settings|encryption|key_storage|description", undefined, { a: (sub) => ( - + {sub} - + ), })} > diff --git a/apps/web/src/components/views/settings/shared/SettingsSection.tsx b/apps/web/src/components/views/settings/shared/SettingsSection.tsx index b43b8a4469..1d6866ce4b 100644 --- a/apps/web/src/components/views/settings/shared/SettingsSection.tsx +++ b/apps/web/src/components/views/settings/shared/SettingsSection.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import classnames from "classnames"; -import React, { type HTMLAttributes } from "react"; +import React, { type ComponentProps, createContext, type HTMLAttributes, useContext } from "react"; import Heading from "../../typography/Heading"; import { SettingsHeader } from "../SettingsHeader"; @@ -19,15 +19,30 @@ export interface SettingsSectionProps extends HTMLAttributes { legacy?: boolean; } -function renderHeading(heading: string | React.ReactNode | undefined, legacy: boolean): React.ReactNode | undefined { +type HeadingLevel = 2 | 3 | 4 | 5 | 6; + +/** + * React context to correctly set heading levels in nested settings sections dynamically + */ +export const HeadingLevelContext = createContext(2); + +function SectionHeading({ + heading, + legacy, + level, +}: { + heading: string | React.ReactNode | undefined; + legacy: boolean; + level: HeadingLevel; +}): React.ReactNode | undefined { switch (typeof heading) { case "string": return legacy ? ( - + ["size"]}> {heading} ) : ( - + ); case "undefined": return undefined; @@ -60,22 +75,28 @@ export const SettingsSection: React.FC = ({ legacy = true, children, ...rest -}) => ( -
- {heading && - (subHeading ? ( -
- {renderHeading(heading, legacy)} - {subHeading} -
- ) : ( - renderHeading(heading, legacy) - ))} - {legacy ?
{children}
: children} -
-); +}) => { + const level = useContext(HeadingLevelContext); + + return ( +
+ {heading && + (subHeading ? ( +
+ + {subHeading} +
+ ) : ( + + ))} + + {legacy ?
{children}
: children} +
+
+ ); +}; diff --git a/apps/web/src/components/views/settings/shared/SettingsSubsection.tsx b/apps/web/src/components/views/settings/shared/SettingsSubsection.tsx index ac4872f598..ab882cffde 100644 --- a/apps/web/src/components/views/settings/shared/SettingsSubsection.tsx +++ b/apps/web/src/components/views/settings/shared/SettingsSubsection.tsx @@ -7,10 +7,11 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { type HTMLAttributes } from "react"; +import React, { type HTMLAttributes, useContext } from "react"; import { Form, Separator } from "@vector-im/compound-web"; import { SettingsSubsectionHeading } from "./SettingsSubsectionHeading"; +import { HeadingLevelContext } from "./SettingsSection.tsx"; export interface SettingsSubsectionProps extends HTMLAttributes { heading?: string | React.ReactNode; @@ -45,6 +46,8 @@ export const SettingsSubsection: React.FC = ({ formWrap, ...rest }) => { + const level = useContext(HeadingLevelContext); + const content = (
= ({ mx_SettingsSubsection_newUi: !legacy, })} > - {typeof heading === "string" ? : <>{heading}} + {typeof heading === "string" ? ( + + ) : ( + <>{heading} + )} {!!description && (
{description} diff --git a/apps/web/src/components/views/settings/shared/SettingsSubsectionHeading.tsx b/apps/web/src/components/views/settings/shared/SettingsSubsectionHeading.tsx index 1be143504c..9d68810731 100644 --- a/apps/web/src/components/views/settings/shared/SettingsSubsectionHeading.tsx +++ b/apps/web/src/components/views/settings/shared/SettingsSubsectionHeading.tsx @@ -18,7 +18,7 @@ export interface SettingsSubsectionHeadingProps extends HTMLAttributes = ({ heading, - as = "h3", + as = "h2", children, ...rest }) => { diff --git a/apps/web/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx b/apps/web/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx index 014527f697..9bd33fd394 100644 --- a/apps/web/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx +++ b/apps/web/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx @@ -45,7 +45,7 @@ const AccountSection: React.FC = ({ if (!canChangePassword) return <>; return ( - <> + = ({ onFinished={onPasswordChanged} /> - + ); }; @@ -179,21 +179,19 @@ const AccountUserSettingsTab: React.FC = ({ closeSettingsFn }) => { return ( - - - {(!isAccountManagedExternally || canMake3pidChanges) && ( - - )} - - + + {(!isAccountManagedExternally || canMake3pidChanges) && ( + + )} + {accountManagementSection} ); diff --git a/apps/web/src/components/views/spaces/QuickSettingsButton.tsx b/apps/web/src/components/views/spaces/QuickSettingsButton.tsx index 1bd3c49291..3fff4f7a98 100644 --- a/apps/web/src/components/views/spaces/QuickSettingsButton.tsx +++ b/apps/web/src/components/views/spaces/QuickSettingsButton.tsx @@ -58,6 +58,8 @@ const QuickSettingsButton: React.FC<{ onFinished={closeMenu} managed={false} focusLock={true} + role="region" + aria-label={_t("quick_settings|title")} >

{_t("quick_settings|title")}

diff --git a/apps/web/src/components/views/spaces/QuickThemeSwitcher.tsx b/apps/web/src/components/views/spaces/QuickThemeSwitcher.tsx index ddb058f788..8f8df8d441 100644 --- a/apps/web/src/components/views/spaces/QuickThemeSwitcher.tsx +++ b/apps/web/src/components/views/spaces/QuickThemeSwitcher.tsx @@ -72,7 +72,7 @@ const QuickThemeSwitcher: React.FC = ({ requestClose }) => { return (
-

{_t("common|theme")}

+

{_t("common|theme")}

-

Font size -

+
should render 1`] = `
-

Message layout -

+
-

Manage integrations -

-

+

(scalar.vector.im) -

+
custom theme should display custom theme 1`] = `
-

Theme -

+
custom theme should render the custom theme sectio
-

Theme -

+
renders the theme choice UI 1`] = `
-

Theme -

+
handles when device is falsy 1`] = `
-

Current session -

+