Accessibility improvements in settings (#32968)

* Add more playwright axe tests to settings dialogs

* Fix axe issue heading-order

* Fix heading order

* Iterate

* Update snapshots

* Iterate

* Update snapshots

* Iterate

* Update screenshot

* Iterate

* Iterate

* Update snapshot
This commit is contained in:
Michael Telatynski
2026-04-07 16:17:59 +01:00
committed by GitHub
parent 7fd837e723
commit 6c1dc7051f
49 changed files with 334 additions and 259 deletions
@@ -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"); await expect(uut).toMatchScreenshot("account.png");
// Assert that the top heading is rendered // 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( await expect(accountManagementSection.getByRole("button", { name: "Deactivate Account" })).toHaveClass(
/mx_AccessibleButton_kind_danger/, /mx_AccessibleButton_kind_danger/,
); );
await expect(axe).toHaveNoViolations();
}); });
test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page, uut }) => { test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page, uut }) => {
@@ -13,7 +13,7 @@ test.describe("Appearance user settings tab", () => {
displayName: "Hanako", 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"); const tab = await app.settings.openUserSettings("Appearance");
// Click "Show advanced" link button // 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.getByRole("button", { name: "Hide advanced" })).toBeVisible();
await expect(tab).toMatchScreenshot("appearance-tab.png"); await expect(tab).toMatchScreenshot("appearance-tab.png");
await expect(axe).toHaveNoViolations();
}); });
test( test(
@@ -23,7 +23,7 @@ test.describe("Appearance user settings tab", () => {
test( test(
"should be rendered with the light theme selected", "should be rendered with the light theme selected",
{ tag: "@screenshot" }, { tag: "@screenshot" },
async ({ page, app, util }) => { async ({ page, app, util, axe }) => {
// Assert that 'Match system theme' is not checked // Assert that 'Match system theme' is not checked
await expect(util.getMatchSystemThemeSwitch()).not.toBeChecked(); 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.getHighContrastTheme()).not.toBeChecked();
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png"); await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png");
await expect(axe).toHaveNoViolations();
}, },
); );
@@ -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"); await app.settings.openUserSettings("Sessions");
const tab = page.locator(".mx_SettingsTab"); const tab = page.locator(".mx_SettingsTab");
@@ -85,7 +85,7 @@ test.describe("Device manager", () => {
// session name updated in details // session name updated in details
await expect(firstSession.locator(".mx_DeviceDetailHeading h4").getByText(sessionName)).toBeVisible(); await expect(firstSession.locator(".mx_DeviceDetailHeading h4").getByText(sessionName)).toBeVisible();
// and main list item // 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 // sign out using the device details sign out
await firstSession.getByRole("button", { name: "Remove this session" }).click(); 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 // no other sessions or security recommendations sections when only one session
await expect(tab.getByText("Other sessions")).not.toBeVisible(); await expect(tab.getByText("Other sessions")).not.toBeVisible();
await expect(tab.getByTestId("security-recommendations-section")).not.toBeVisible(); await expect(tab.getByTestId("security-recommendations-section")).not.toBeVisible();
await expect(axe).toHaveNoViolations();
}); });
}); });
@@ -16,7 +16,7 @@ test.describe("Advanced section in Encryption tab", () => {
await bootstrapCrossSigningForClient(clientHandle, credentials, true); 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(); await util.openEncryptionTab();
const section = util.getEncryptionDetailsSection(); const section = util.getEncryptionDetailsSection();
@@ -26,6 +26,8 @@ test.describe("Advanced section in Encryption tab", () => {
await expect(section).toMatchScreenshot("encryption-details.png", { await expect(section).toMatchScreenshot("encryption-details.png", {
mask: [section.getByTestId("deviceId"), section.getByTestId("sessionKey")], mask: [section.getByTestId("deviceId"), section.getByTestId("sessionKey")],
}); });
await expect(axe).toHaveNoViolations();
}); });
test("should show the import room keys dialog", async ({ page, app, util }) => { test("should show the import room keys dialog", async ({ page, app, util }) => {
@@ -20,7 +20,7 @@ test.describe("General room settings tab", () => {
await app.viewRoomByName(roomName); 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"); const settings = await app.settings.openRoomSettings("General");
// Assert that "Show less" details element is rendered // 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" // 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 less")).not.toBeVisible();
await expect(settings.getByText("Show more")).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 }) => { test("long address should not cause dialog to overflow", { tag: "@no-webkit" }, async ({ page, app, user }) => {
@@ -25,7 +25,7 @@ test.describe("Preferences user settings tab", () => {
labsFlags: ["feature_new_room_list"], 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 }); await page.setViewportSize({ width: 1024, height: 4000 });
const tab = await app.settings.openUserSettings("Preferences"); const tab = await app.settings.openUserSettings("Preferences");
// Assert that the top heading is rendered // 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 }) => { test("should be able to change the app language", { tag: ["@no-firefox", "@no-webkit"] }, async ({ uut, user }) => {
@@ -8,11 +8,13 @@ Please see LICENSE files in the repository root for full details.
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
test.describe("Quick settings menu", () => { 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(); await page.getByRole("button", { name: "Quick settings" }).click();
// Assert that the top heading is renderedc // Assert that the top heading is renderedc
const settings = page.getByTestId("quick-settings-menu"); const settings = page.getByTestId("quick-settings-menu");
await expect(settings).toBeVisible(); await expect(settings).toBeVisible();
await expect(settings).toMatchScreenshot("quick-settings.png"); await expect(settings).toMatchScreenshot("quick-settings.png");
await expect(axe).toHaveNoViolations();
}); });
}); });
@@ -25,7 +25,7 @@ test.describe("Roles & Permissions room settings tab", () => {
settings = await app.settings.openRoomSettings("Roles & Permissions"); 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 privilegedUserSection = settings.locator(".mx_SettingsFieldset").first();
const applyButton = privilegedUserSection.getByRole("button", { name: "Apply" }); 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"); settings = await app.settings.openRoomSettings("Roles & Permissions");
combobox = privilegedUserSection.getByRole("combobox", { name: user.userId }); combobox = privilegedUserSection.getByRole("combobox", { name: user.userId });
await expect(combobox).toHaveValue("50"); await expect(combobox).toHaveValue("50");
await expect(axe).toHaveNoViolations();
}); });
}); });
@@ -32,12 +32,14 @@ test.describe("Security user settings tab", () => {
}); });
test.describe("AnalyticsLearnMoreDialog", () => { 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"); const tab = await app.settings.openUserSettings("Security");
await tab.getByRole("button", { name: "Learn more" }).click(); await tab.getByRole("button", { name: "Learn more" }).click();
await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot( 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", "Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1.png",
); );
await expect(axe).toHaveNoViolations();
}); });
}); });
Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

@@ -36,5 +36,5 @@ Please see LICENSE files in the repository root for full details.
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
gap: $spacing-32; gap: $spacing-32;
padding: $spacing-16 0; margin: $spacing-16 0;
} }
@@ -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. 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 ReactDOM from "react-dom";
import classNames from "classnames"; import classNames from "classnames";
import FocusLock from "react-focus-lock"; import FocusLock from "react-focus-lock";
@@ -74,27 +82,31 @@ export interface MenuProps extends IPosition {
export interface IProps extends MenuProps { export interface IProps extends MenuProps {
// If true, insert an invisible screen-sized element behind the menu that when clicked will close it. // 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 // whether this context menu should be focus managed. If false it must handle itself
managed?: boolean; "managed"?: boolean;
wrapperClassName?: string; "wrapperClassName"?: string;
menuClassName?: string; "menuClassName"?: string;
// If true, this context menu will be mounted as a child to the parent container. Otherwise // 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. // 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 // 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. // within an existing FocusLock e.g inside a modal.
focusLock?: boolean; "focusLock"?: boolean;
// call onFinished on any interaction with the menu // call onFinished on any interaction with the menu
closeOnInteraction?: boolean; "closeOnInteraction"?: boolean;
// Function to be called on menu close // Function to be called on menu close
onFinished(this: void): void; onFinished(this: void): void;
// on resize callback // on resize callback
windowResize?(this: void): void; windowResize?(this: void): void;
// Role & label for accessibility
"role"?: AriaRole;
"aria-label"?: string;
} }
interface IState { interface IState {
@@ -257,9 +269,11 @@ export default class ContextMenu extends React.PureComponent<React.PropsWithChil
focusLock, focusLock,
managed, managed,
wrapperClassName, wrapperClassName,
chevronFace: propsChevronFace, "chevronFace": propsChevronFace,
chevronOffset: propsChevronOffset, "chevronOffset": propsChevronOffset,
mountAsChild, mountAsChild,
role,
"aria-label": ariaLabel,
...props ...props
} = this.props; } = this.props;
@@ -424,6 +438,8 @@ export default class ContextMenu extends React.PureComponent<React.PropsWithChil
onClick={this.onClick} onClick={this.onClick}
onKeyDown={onKeyDownHandler} onKeyDown={onKeyDownHandler}
onContextMenu={this.onContextMenuPreventBubbling} onContextMenu={this.onContextMenuPreventBubbling}
role={role}
aria-label={ariaLabel}
> >
{background} {background}
<TooltipProvider> <TooltipProvider>
@@ -76,8 +76,10 @@ export default class SetIntegrationManager extends React.Component<EmptyObject,
> >
<div className="mx_SettingsFlag"> <div className="mx_SettingsFlag">
<div className="mx_SetIntegrationManager_heading_manager"> <div className="mx_SetIntegrationManager_heading_manager">
<Heading size="3">{_t("integration_manager|manage_title")}</Heading> <Heading as="h2" size="3">
<Heading id="mx_SetIntegrationManager_ManagerName" size="4"> {_t("integration_manager|manage_title")}
</Heading>
<Heading id="mx_SetIntegrationManager_ManagerName" as="h3" size="4">
{managerName} {managerName}
</Heading> </Heading>
</div> </div>
@@ -13,6 +13,10 @@ import { Heading } from "@vector-im/compound-web";
* The heading for a settings section. * The heading for a settings section.
*/ */
interface SettingsHeaderProps { interface SettingsHeaderProps {
/**
* The component to render the heading as, defaults to h2
*/
as?: React.ComponentProps<typeof Heading>["as"];
/** /**
* Whether the user has a recommended tag. * Whether the user has a recommended tag.
*/ */
@@ -23,12 +27,12 @@ interface SettingsHeaderProps {
label: string; 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", { const classes = classNames("mx_SettingsHeader", {
mx_SettingsHeader_recommended: hasRecommendedTag, mx_SettingsHeader_recommended: hasRecommendedTag,
}); });
return ( return (
<Heading className={classes} as="h2" size="sm" weight="semibold"> <Heading className={classes} as={as} size="sm" weight="semibold">
{label} {label}
</Heading> </Heading>
); );
@@ -18,7 +18,7 @@ import { type ThirdPartyIdentifier } from "../../../AddThreepid";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature"; import { UIFeature } from "../../../settings/UIFeature";
import { AddRemoveThreepids } from "./AddRemoveThreepids"; import { AddRemoveThreepids } from "./AddRemoveThreepids";
import Heading from "../typography/Heading.tsx"; import { SettingsSection } from "./shared/SettingsSection.tsx";
type LoadingState = "loading" | "loaded" | "error"; type LoadingState = "loading" | "loaded" | "error";
@@ -82,8 +82,7 @@ export const UserPersonalInfoSettings: React.FC<UserPersonalInfoSettingsProps> =
if (!SettingsStore.getValue(UIFeature.ThirdPartyID)) return null; if (!SettingsStore.getValue(UIFeature.ThirdPartyID)) return null;
return ( return (
<div className="mx_UserPersonalInfoSettings"> <SettingsSection heading={_t("settings|general|personal_info")} className="mx_UserPersonalInfoSettings">
<Heading size="2">{_t("settings|general|personal_info")}</Heading>
<SettingsSubsection <SettingsSubsection
heading={_t("settings|general|emails_heading")} heading={_t("settings|general|emails_heading")}
stretchContent stretchContent
@@ -123,6 +122,6 @@ export const UserPersonalInfoSettings: React.FC<UserPersonalInfoSettingsProps> =
/> />
</ThreepidSectionWrapper> </ThreepidSectionWrapper>
</SettingsSubsection> </SettingsSubsection>
</div> </SettingsSection>
); );
}; };
@@ -27,7 +27,7 @@ import AccessibleButton from "../elements/AccessibleButton";
import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog"; import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import Heading from "../typography/Heading.tsx"; import { SettingsSection } from "./shared/SettingsSection.tsx";
const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => ( const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => (
<> <>
@@ -194,54 +194,55 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
const someFieldsDisabled = !canSetDisplayName || !canSetAvatar; const someFieldsDisabled = !canSetDisplayName || !canSetAvatar;
return ( return (
<div className="mx_UserProfileSettings"> <SettingsSection heading={_t("common|profile")}>
<Heading size="2">{_t("common|profile")}</Heading> <div className="mx_UserProfileSettings">
<div> <div>
{someFieldsDisabled {someFieldsDisabled
? _t("settings|general|profile_subtitle_oidc") ? _t("settings|general|profile_subtitle_oidc")
: _t("settings|general|profile_subtitle")} : _t("settings|general|profile_subtitle")}
</div> </div>
<div className="mx_UserProfileSettings_profile"> <div className="mx_UserProfileSettings_profile">
<AvatarSetting <AvatarSetting
avatar={avatarURL ?? undefined} avatar={avatarURL ?? undefined}
avatarAccessibleName={_t("common|user_avatar")} avatarAccessibleName={_t("common|user_avatar")}
onChange={onAvatarChange} onChange={onAvatarChange}
removeAvatar={avatarURL ? onAvatarRemove : undefined} removeAvatar={avatarURL ? onAvatarRemove : undefined}
placeholderName={displayName} placeholderName={displayName}
placeholderId={client.getUserId() ?? ""} placeholderId={client.getUserId() ?? ""}
disabled={!canSetAvatar} disabled={!canSetAvatar}
/> />
<EditInPlace <EditInPlace
className="mx_UserProfileSettings_profile_displayName" className="mx_UserProfileSettings_profile_displayName"
label={_t("settings|general|display_name")} label={_t("settings|general|display_name")}
value={displayName} value={displayName}
saveButtonLabel={_t("common|save")} saveButtonLabel={_t("common|save")}
cancelButtonLabel={_t("common|cancel")} cancelButtonLabel={_t("common|cancel")}
savedLabel={_t("common|saved")} savedLabel={_t("common|saved")}
savingLabel={_t("common|updating")} savingLabel={_t("common|updating")}
onChange={onDisplayNameChanged} onChange={onDisplayNameChanged}
onCancel={onDisplayNameCancel} onCancel={onDisplayNameCancel}
onSave={onDisplayNameSave} onSave={onDisplayNameSave}
disabled={!canSetDisplayName} disabled={!canSetDisplayName}
> >
{displayNameError && <ErrorMessage>{_t("settings|general|display_name_error")}</ErrorMessage>} {displayNameError && <ErrorMessage>{_t("settings|general|display_name_error")}</ErrorMessage>}
</EditInPlace> </EditInPlace>
</div> </div>
{avatarError && ( {avatarError && (
<Alert title={_t("settings|general|avatar_upload_error_title")} type="critical"> <Alert title={_t("settings|general|avatar_upload_error_title")} type="critical">
{maxUploadSize === undefined {maxUploadSize === undefined
? _t("settings|general|avatar_upload_error_text_generic") ? _t("settings|general|avatar_upload_error_text_generic")
: _t("settings|general|avatar_upload_error_text", { size: formatBytes(maxUploadSize) })} : _t("settings|general|avatar_upload_error_text", { size: formatBytes(maxUploadSize) })}
</Alert> </Alert>
)}
{userIdentifier && <UsernameBox username={userIdentifier} />}
<Flex gap="var(--cpd-space-4x)" className="mx_UserProfileSettings_profile_buttons">
{externalAccountManagementUrl && (
<ManageAccountButton externalAccountManagementUrl={externalAccountManagementUrl} />
)} )}
<SignOutButton /> {userIdentifier && <UsernameBox username={userIdentifier} />}
</Flex> <Flex gap="var(--cpd-space-4x)" className="mx_UserProfileSettings_profile_buttons">
</div> {externalAccountManagementUrl && (
<ManageAccountButton externalAccountManagementUrl={externalAccountManagementUrl} />
)}
<SignOutButton />
</Flex>
</div>
</SettingsSection>
); );
}; };
@@ -22,7 +22,11 @@ export interface DeviceTileProps {
} }
const DeviceTileName: React.FC<{ device: ExtendedDevice }> = ({ device }) => { const DeviceTileName: React.FC<{ device: ExtendedDevice }> = ({ device }) => {
return <Heading size="4">{device.display_name || device.device_id}</Heading>; return (
<Heading as="h3" size="4">
{device.display_name || device.device_id}
</Heading>
);
}; };
const DeviceTile: React.FC<DeviceTileProps> = ({ device, children, isSelected, onClick }) => { const DeviceTile: React.FC<DeviceTileProps> = ({ device, children, isSelected, onClick }) => {
@@ -6,7 +6,7 @@
*/ */
import React, { useCallback } from "react"; 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 type { FormEvent } from "react";
import { SettingsSection } from "../shared/SettingsSection"; import { SettingsSection } from "../shared/SettingsSection";
@@ -56,9 +56,9 @@ export const KeyStoragePanel: React.FC<Props> = ({ onKeyStorageDisableClick }) =
} }
subHeading={_t("settings|encryption|key_storage|description", undefined, { subHeading={_t("settings|encryption|key_storage|description", undefined, {
a: (sub) => ( a: (sub) => (
<a href={SdkConfig.get("help_key_storage_url")} target="_blank" rel="noreferrer noopener"> <Link href={SdkConfig.get("help_key_storage_url")} target="_blank">
{sub} {sub}
</a> </Link>
), ),
})} })}
> >
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import classnames from "classnames"; 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 Heading from "../../typography/Heading";
import { SettingsHeader } from "../SettingsHeader"; import { SettingsHeader } from "../SettingsHeader";
@@ -19,15 +19,30 @@ export interface SettingsSectionProps extends HTMLAttributes<HTMLDivElement> {
legacy?: boolean; 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<HeadingLevel>(2);
function SectionHeading({
heading,
legacy,
level,
}: {
heading: string | React.ReactNode | undefined;
legacy: boolean;
level: HeadingLevel;
}): React.ReactNode | undefined {
switch (typeof heading) { switch (typeof heading) {
case "string": case "string":
return legacy ? ( return legacy ? (
<Heading as="h2" size="3"> <Heading as={`h${level}`} size={(level + 1).toString() as ComponentProps<typeof Heading>["size"]}>
{heading} {heading}
</Heading> </Heading>
) : ( ) : (
<SettingsHeader label={heading} /> <SettingsHeader as={`h${level}`} label={heading} />
); );
case "undefined": case "undefined":
return undefined; return undefined;
@@ -60,22 +75,28 @@ export const SettingsSection: React.FC<SettingsSectionProps> = ({
legacy = true, legacy = true,
children, children,
...rest ...rest
}) => ( }) => {
<div const level = useContext(HeadingLevelContext);
{...rest}
className={classnames("mx_SettingsSection", className, { return (
mx_SettingsSection_newUi: !legacy, <div
})} {...rest}
> className={classnames("mx_SettingsSection", className, {
{heading && mx_SettingsSection_newUi: !legacy,
(subHeading ? ( })}
<div className="mx_SettingsSection_header"> >
{renderHeading(heading, legacy)} {heading &&
{subHeading} (subHeading ? (
</div> <div className="mx_SettingsSection_header">
) : ( <SectionHeading heading={heading} legacy={legacy} level={level} />
renderHeading(heading, legacy) {subHeading}
))} </div>
{legacy ? <div className="mx_SettingsSection_subSections">{children}</div> : children} ) : (
</div> <SectionHeading heading={heading} legacy={legacy} level={level} />
); ))}
<HeadingLevelContext.Provider value={heading ? ((level + 1) as HeadingLevel) : level}>
{legacy ? <div className="mx_SettingsSection_subSections">{children}</div> : children}
</HeadingLevelContext.Provider>
</div>
);
};
@@ -7,10 +7,11 @@ Please see LICENSE files in the repository root for full details.
*/ */
import classNames from "classnames"; 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 { Form, Separator } from "@vector-im/compound-web";
import { SettingsSubsectionHeading } from "./SettingsSubsectionHeading"; import { SettingsSubsectionHeading } from "./SettingsSubsectionHeading";
import { HeadingLevelContext } from "./SettingsSection.tsx";
export interface SettingsSubsectionProps extends HTMLAttributes<HTMLDivElement> { export interface SettingsSubsectionProps extends HTMLAttributes<HTMLDivElement> {
heading?: string | React.ReactNode; heading?: string | React.ReactNode;
@@ -45,6 +46,8 @@ export const SettingsSubsection: React.FC<SettingsSubsectionProps> = ({
formWrap, formWrap,
...rest ...rest
}) => { }) => {
const level = useContext(HeadingLevelContext);
const content = ( const content = (
<div <div
{...rest} {...rest}
@@ -52,7 +55,11 @@ export const SettingsSubsection: React.FC<SettingsSubsectionProps> = ({
mx_SettingsSubsection_newUi: !legacy, mx_SettingsSubsection_newUi: !legacy,
})} })}
> >
{typeof heading === "string" ? <SettingsSubsectionHeading heading={heading} /> : <>{heading}</>} {typeof heading === "string" ? (
<SettingsSubsectionHeading heading={heading} as={`h${level}`} />
) : (
<>{heading}</>
)}
{!!description && ( {!!description && (
<div className="mx_SettingsSubsection_description"> <div className="mx_SettingsSubsection_description">
<SettingsSubsectionText>{description}</SettingsSubsectionText> <SettingsSubsectionText>{description}</SettingsSubsectionText>
@@ -18,7 +18,7 @@ export interface SettingsSubsectionHeadingProps extends HTMLAttributes<HTMLDivEl
export const SettingsSubsectionHeading: React.FC<SettingsSubsectionHeadingProps> = ({ export const SettingsSubsectionHeading: React.FC<SettingsSubsectionHeadingProps> = ({
heading, heading,
as = "h3", as = "h2",
children, children,
...rest ...rest
}) => { }) => {
@@ -45,7 +45,7 @@ const AccountSection: React.FC<AccountSectionProps> = ({
if (!canChangePassword) return <></>; if (!canChangePassword) return <></>;
return ( return (
<> <SettingsSection>
<SettingsSubsection <SettingsSubsection
heading={_t("settings|general|account_section")} heading={_t("settings|general|account_section")}
stretchContent stretchContent
@@ -59,7 +59,7 @@ const AccountSection: React.FC<AccountSectionProps> = ({
onFinished={onPasswordChanged} onFinished={onPasswordChanged}
/> />
</SettingsSubsection> </SettingsSubsection>
</> </SettingsSection>
); );
}; };
@@ -179,21 +179,19 @@ const AccountUserSettingsTab: React.FC<IProps> = ({ closeSettingsFn }) => {
return ( return (
<SettingsTab data-testid="mx_AccountUserSettingsTab"> <SettingsTab data-testid="mx_AccountUserSettingsTab">
<SettingsSection> <UserProfileSettings
<UserProfileSettings externalAccountManagementUrl={externalAccountManagementUrl}
externalAccountManagementUrl={externalAccountManagementUrl} canSetDisplayName={canSetDisplayName}
canSetDisplayName={canSetDisplayName} canSetAvatar={canSetAvatar}
canSetAvatar={canSetAvatar} />
/> {(!isAccountManagedExternally || canMake3pidChanges) && (
{(!isAccountManagedExternally || canMake3pidChanges) && ( <UserPersonalInfoSettings canMake3pidChanges={canMake3pidChanges} />
<UserPersonalInfoSettings canMake3pidChanges={canMake3pidChanges} /> )}
)} <AccountSection
<AccountSection canChangePassword={canChangePassword}
canChangePassword={canChangePassword} onPasswordChanged={onPasswordChanged}
onPasswordChanged={onPasswordChanged} onPasswordChangeError={onPasswordChangeError}
onPasswordChangeError={onPasswordChangeError} />
/>
</SettingsSection>
{accountManagementSection} {accountManagementSection}
</SettingsTab> </SettingsTab>
); );
@@ -58,6 +58,8 @@ const QuickSettingsButton: React.FC<{
onFinished={closeMenu} onFinished={closeMenu}
managed={false} managed={false}
focusLock={true} focusLock={true}
role="region"
aria-label={_t("quick_settings|title")}
> >
<h2>{_t("quick_settings|title")}</h2> <h2>{_t("quick_settings|title")}</h2>
@@ -72,7 +72,7 @@ const QuickThemeSwitcher: React.FC<Props> = ({ requestClose }) => {
return ( return (
<div className="mx_QuickThemeSwitcher"> <div className="mx_QuickThemeSwitcher">
<h4 className="mx_QuickThemeSwitcher_heading">{_t("common|theme")}</h4> <h3 className="mx_QuickThemeSwitcher_heading">{_t("common|theme")}</h3>
<Dropdown <Dropdown
id="mx_QuickSettingsButton_themePickerDropdown" id="mx_QuickSettingsButton_themePickerDropdown"
onOptionChange={onOptionChange} onOptionChange={onOptionChange}
@@ -9,11 +9,11 @@ exports[`FontScalingPanel renders the font scaling UI 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Font size Font size
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch" class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
@@ -9,11 +9,11 @@ exports[`<LayoutSwitcher /> should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Message layout Message layout
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
@@ -11,17 +11,17 @@ exports[`SetIntegrationManager should render manage integrations sections 1`] =
<div <div
class="mx_SetIntegrationManager_heading_manager" class="mx_SetIntegrationManager_heading_manager"
> >
<h3 <h2
class="mx_Heading_h3" class="mx_Heading_h3"
> >
Manage integrations Manage integrations
</h3> </h2>
<h4 <h3
class="mx_Heading_h4" class="mx_Heading_h4"
id="mx_SetIntegrationManager_ManagerName" id="mx_SetIntegrationManager_ManagerName"
> >
(scalar.vector.im) (scalar.vector.im)
</h4> </h3>
</div> </div>
</div> </div>
<div <div
@@ -9,11 +9,11 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Theme Theme
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
@@ -288,11 +288,11 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Theme Theme
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
@@ -567,11 +567,11 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Theme Theme
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
@@ -137,11 +137,11 @@ exports[`<CurrentDeviceSection /> handles when device is falsy 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Current session Current session
</h3> </h2>
<div <div
aria-disabled="true" aria-disabled="true"
aria-expanded="false" aria-expanded="false"
@@ -183,11 +183,11 @@ exports[`<CurrentDeviceSection /> renders device and correct security card when
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Current session Current session
</h3> </h2>
<div <div
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
@@ -257,11 +257,11 @@ exports[`<CurrentDeviceSection /> renders device and correct security card when
<div <div
class="mx_DeviceTile_info" class="mx_DeviceTile_info"
> >
<h4 <h3
class="mx_Heading_h4" class="mx_Heading_h4"
> >
alices_device alices_device
</h4> </h3>
<div <div
class="mx_DeviceTile_metadata" class="mx_DeviceTile_metadata"
> >
@@ -370,11 +370,11 @@ exports[`<CurrentDeviceSection /> renders device and correct security card when
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Current session Current session
</h3> </h2>
<div <div
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
@@ -444,11 +444,11 @@ exports[`<CurrentDeviceSection /> renders device and correct security card when
<div <div
class="mx_DeviceTile_info" class="mx_DeviceTile_info"
> >
<h4 <h3
class="mx_Heading_h4" class="mx_Heading_h4"
> >
alices_device alices_device
</h4> </h3>
<div <div
class="mx_DeviceTile_metadata" class="mx_DeviceTile_metadata"
> >
@@ -45,11 +45,11 @@ exports[`<DeviceTile /> renders a device with no metadata 1`] = `
<div <div
class="mx_DeviceTile_info" class="mx_DeviceTile_info"
> >
<h4 <h3
class="mx_Heading_h4" class="mx_Heading_h4"
> >
123 123
</h4> </h3>
<div <div
class="mx_DeviceTile_metadata" class="mx_DeviceTile_metadata"
> >
@@ -118,11 +118,11 @@ exports[`<DeviceTile /> renders a verified device with no metadata 1`] = `
<div <div
class="mx_DeviceTile_info" class="mx_DeviceTile_info"
> >
<h4 <h3
class="mx_Heading_h4" class="mx_Heading_h4"
> >
123 123
</h4> </h3>
<div <div
class="mx_DeviceTile_metadata" class="mx_DeviceTile_metadata"
> >
@@ -191,11 +191,11 @@ exports[`<DeviceTile /> renders display name with a tooltip 1`] = `
<div <div
class="mx_DeviceTile_info" class="mx_DeviceTile_info"
> >
<h4 <h3
class="mx_Heading_h4" class="mx_Heading_h4"
> >
My device My device
</h4> </h3>
<div <div
class="mx_DeviceTile_metadata" class="mx_DeviceTile_metadata"
> >
@@ -264,11 +264,11 @@ exports[`<DeviceTile /> separates metadata with a dot 1`] = `
<div <div
class="mx_DeviceTile_info" class="mx_DeviceTile_info"
> >
<h4 <h3
class="mx_Heading_h4" class="mx_Heading_h4"
> >
123 123
</h4> </h3>
<div <div
class="mx_DeviceTile_metadata" class="mx_DeviceTile_metadata"
> >
@@ -9,11 +9,11 @@ exports[`<SecurityRecommendations /> renders both cards when user has both unver
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Security recommendations Security recommendations
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_description" class="mx_SettingsSubsection_description"
@@ -141,11 +141,11 @@ exports[`<SecurityRecommendations /> renders inactive devices section when user
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Security recommendations Security recommendations
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_description" class="mx_SettingsSubsection_description"
@@ -273,11 +273,11 @@ exports[`<SecurityRecommendations /> renders unverified devices section when use
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Security recommendations Security recommendations
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_description" class="mx_SettingsSubsection_description"
@@ -101,11 +101,11 @@ exports[`<SelectableDeviceTile /> renders unselected device tile with checkbox 1
<div <div
class="mx_DeviceTile_info" class="mx_DeviceTile_info"
> >
<h4 <h3
class="mx_Heading_h4" class="mx_Heading_h4"
> >
My Device My Device
</h4> </h3>
<div <div
class="mx_DeviceTile_metadata" class="mx_DeviceTile_metadata"
> >
@@ -948,11 +948,11 @@ exports[`<Notifications /> correctly handles the loading/disabled state 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Email summary Email summary
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
@@ -998,11 +998,11 @@ exports[`<Notifications /> correctly handles the loading/disabled state 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Quick Actions Quick Actions
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -2174,11 +2174,11 @@ exports[`<Notifications /> matches the snapshot 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Email summary Email summary
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
@@ -2280,11 +2280,11 @@ exports[`<Notifications /> matches the snapshot 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Notification targets Notification targets
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
@@ -2308,11 +2308,11 @@ exports[`<Notifications /> matches the snapshot 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Quick Actions Quick Actions
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -8,11 +8,11 @@ exports[`<SettingsSubsection /> renders with plain text description 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Test Test
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_description" class="mx_SettingsSubsection_description"
@@ -42,11 +42,11 @@ exports[`<SettingsSubsection /> renders with plain text heading 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Test Test
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -67,11 +67,11 @@ exports[`<SettingsSubsection /> renders with react element description 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Test Test
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_description" class="mx_SettingsSubsection_description"
@@ -127,11 +127,11 @@ exports[`<SettingsSubsection /> renders without description 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Test Test
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -6,11 +6,11 @@ exports[`<SettingsSubsectionHeading /> renders with children 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
test test
</h3> </h2>
<a <a
href="/#" href="/#"
> >
@@ -27,11 +27,11 @@ exports[`<SettingsSubsectionHeading /> renders without children 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
test test
</h3> </h2>
</div> </div>
</div>, </div>,
} }
@@ -22,11 +22,11 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Theme Theme
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
@@ -152,11 +152,11 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Message layout Message layout
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
@@ -510,11 +510,11 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Font size Font size
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch" class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
@@ -692,11 +692,11 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Image size in the timeline Image size in the timeline
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -20,11 +20,11 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Composer Composer
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -381,11 +381,11 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Calls Calls
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -442,11 +442,11 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Room Room
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -611,11 +611,11 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Room List Room List
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -702,11 +702,11 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Accessibility Accessibility
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -771,11 +771,11 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Navigation Navigation
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -1062,11 +1062,11 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Autocomplete Autocomplete
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -41,11 +41,11 @@ exports[`<MjolnirUserSettingsTab /> renders correctly when user has no ignored u
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Personal ban list Personal ban list
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_description" class="mx_SettingsSubsection_description"
@@ -97,11 +97,11 @@ exports[`<MjolnirUserSettingsTab /> renders correctly when user has no ignored u
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Subscribed lists Subscribed lists
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_description" class="mx_SettingsSubsection_description"
@@ -24,11 +24,11 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Language Language
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -77,11 +77,11 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Room list Room list
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -129,11 +129,11 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Spaces Spaces
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -188,11 +188,11 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Keyboard shortcuts Keyboard shortcuts
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_description" class="mx_SettingsSubsection_description"
@@ -260,11 +260,11 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Displaying time Displaying time
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -421,11 +421,11 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Presence Presence
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_description" class="mx_SettingsSubsection_description"
@@ -523,11 +523,11 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Composer Composer
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -784,11 +784,11 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Code blocks Code blocks
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -902,11 +902,11 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Images, GIFs and videos Images, GIFs and videos
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -1020,11 +1020,11 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Timeline Timeline
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -1401,11 +1401,11 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Moderation and safety Moderation and safety
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
@@ -1615,11 +1615,11 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Room directory Room directory
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content" class="mx_SettingsSubsection_content"
@@ -1668,11 +1668,11 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
General General
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch" class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
@@ -44,17 +44,17 @@ exports[`<SecurityUserSettingsTab /> renders security section 1`] = `
<div <div
class="mx_SetIntegrationManager_heading_manager" class="mx_SetIntegrationManager_heading_manager"
> >
<h3 <h2
class="mx_Heading_h3" class="mx_Heading_h3"
> >
Manage integrations Manage integrations
</h3> </h2>
<h4 <h3
class="mx_Heading_h4" class="mx_Heading_h4"
id="mx_SetIntegrationManager_ManagerName" id="mx_SetIntegrationManager_ManagerName"
> >
(scalar.vector.im) (scalar.vector.im)
</h4> </h3>
</div> </div>
</div> </div>
<div <div
@@ -83,11 +83,11 @@ exports[`<SessionManagerTab /> current session section renders current session s
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Current session Current session
</h3> </h2>
<div <div
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
@@ -159,11 +159,11 @@ exports[`<SessionManagerTab /> current session section renders current session s
<div <div
class="mx_DeviceTile_info" class="mx_DeviceTile_info"
> >
<h4 <h3
class="mx_Heading_h4" class="mx_Heading_h4"
> >
Alices device Alices device
</h4> </h3>
<div <div
class="mx_DeviceTile_metadata" class="mx_DeviceTile_metadata"
> >
@@ -260,11 +260,11 @@ exports[`<SessionManagerTab /> current session section renders current session s
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Current session Current session
</h3> </h2>
<div <div
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
@@ -334,11 +334,11 @@ exports[`<SessionManagerTab /> current session section renders current session s
<div <div
class="mx_DeviceTile_info" class="mx_DeviceTile_info"
> >
<h4 <h3
class="mx_Heading_h4" class="mx_Heading_h4"
> >
Alices device Alices device
</h4> </h3>
<div <div
class="mx_DeviceTile_metadata" class="mx_DeviceTile_metadata"
> >
@@ -20,11 +20,11 @@ exports[`<SidebarUserSettingsTab /> renders sidebar settings with guest spa url
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Spaces to show Spaces to show
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_description" class="mx_SettingsSubsection_description"
@@ -310,11 +310,11 @@ exports[`<SidebarUserSettingsTab /> renders sidebar settings without guest spa u
<div <div
class="mx_SettingsSubsectionHeading" class="mx_SettingsSubsectionHeading"
> >
<h3 <h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
> >
Spaces to show Spaces to show
</h3> </h2>
</div> </div>
<div <div
class="mx_SettingsSubsection_description" class="mx_SettingsSubsection_description"
@@ -173,11 +173,11 @@ exports[`<SpaceSettingsVisibilityTab /> for a public space renders addresses sec
<div <div
class="mx_SettingsSection" class="mx_SettingsSection"
> >
<h2 <h3
class="mx_Heading_h3" class="mx_Heading_h4"
> >
Address Address
</h2> </h3>
<div <div
class="mx_SettingsSection_subSections" class="mx_SettingsSection_subSections"
> >