diff --git a/package.json b/package.json index 34cb581025..e6cd0b132c 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "@sentry/browser": "^7.0.0", "@testing-library/react-hooks": "^8.0.1", "@vector-im/compound-design-tokens": "^1.2.0", - "@vector-im/compound-web": "^4.3.1", + "@vector-im/compound-web": "^4.4.1", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-en": "^3.0.2", diff --git a/playwright/e2e/settings/general-user-settings-tab.spec.ts b/playwright/e2e/settings/general-user-settings-tab.spec.ts index ae3718aba9..ff2d7c2207 100644 --- a/playwright/e2e/settings/general-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/general-user-settings-tab.spec.ts @@ -21,8 +21,6 @@ const USER_NAME_NEW = "Alice"; const IntegrationManager = "scalar.vector.im"; test.describe("General user settings tab", () => { - let userId: string; - test.use({ displayName: USER_NAME, config: { @@ -34,18 +32,18 @@ test.describe("General user settings tab", () => { }, }); - test("should be rendered properly", async ({ uut }) => { + test("should be rendered properly", async ({ uut, user }) => { await expect(uut).toMatchScreenshot("general.png"); // Assert that the top heading is rendered await expect(uut.getByRole("heading", { name: "General" })).toBeVisible(); - const profile = uut.locator(".mx_ProfileSettings_profile"); + const profile = uut.locator(".mx_UserProfileSettings_profile"); await profile.scrollIntoViewIfNeeded(); await expect(profile.getByRole("textbox", { name: "Display Name" })).toHaveValue(USER_NAME); // Assert that a userId is rendered - await expect(profile.locator(".mx_ProfileSettings_profile_controls_userId", { hasText: userId })).toBeVisible(); + expect(uut.getByLabel("Username")).toHaveText(user.userId); // Check avatar setting const avatar = profile.locator(".mx_AvatarSetting_avatar"); @@ -131,12 +129,15 @@ test.describe("General user settings tab", () => { }); test("should support adding and removing a profile picture", async ({ uut }) => { - const profileSettings = uut.locator(".mx_ProfileSettings"); + const profileSettings = uut.locator(".mx_UserProfileSettings"); // Upload a picture await profileSettings.getByAltText("Upload").setInputFiles("playwright/sample-files/riot.png"); // Find and click "Remove" link button - await profileSettings.locator(".mx_ProfileSettings_profile").getByRole("button", { name: "Remove" }).click(); + await profileSettings + .locator(".mx_UserProfileSettings_profile") + .getByRole("button", { name: "Remove" }) + .click(); // Assert that the link button disappeared await expect( @@ -175,7 +176,7 @@ test.describe("General user settings tab", () => { test("should support changing a display name", async ({ uut, page, app }) => { // Change the diaplay name to USER_NAME_NEW const displayNameInput = uut - .locator(".mx_SettingsTab .mx_ProfileSettings") + .locator(".mx_SettingsTab .mx_UserProfileSettings") .getByRole("textbox", { name: "Display Name" }); await displayNameInput.fill(USER_NAME_NEW); await displayNameInput.press("Enter"); diff --git a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png index e11ef9c410..f6463ffd22 100644 Binary files a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png and b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png differ diff --git a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png index 75febc97d7..7f22193662 100644 Binary files a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png and b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png differ diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 20ed9dfa39..7b5fa8e994 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -597,7 +597,10 @@ legend { * Elements that should not be styled like a dialog button are mentioned in a :not() pseudo-class. * For the widest browser support, we use multiple :not pseudo-classes instead of :not(.a, .b). */ -.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton), +.mx_Dialog + button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( + .mx_ProfileSettings button + ), .mx_Dialog input[type="submit"], .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton), .mx_Dialog_buttons input[type="submit"] { @@ -614,11 +617,17 @@ legend { font-family: inherit; } -.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):last-child { +.mx_Dialog + button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( + .mx_ProfileSettings button + ):last-child { margin-right: 0px; } -.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):focus, +.mx_Dialog + button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( + .mx_ProfileSettings 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 { @@ -627,7 +636,8 @@ legend { .mx_Dialog button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]), .mx_Dialog input[type="submit"].mx_Dialog_primary, -.mx_Dialog_buttons button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton), +.mx_Dialog_buttons + button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_ProfileSettings 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); @@ -637,7 +647,8 @@ 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), +.mx_Dialog_buttons + button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_ProfileSettings 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); @@ -650,7 +661,10 @@ legend { color: var(--cpd-color-text-critical-primary); } -.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):disabled, +.mx_Dialog + button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( + .mx_ProfileSettings 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 { diff --git a/res/css/_components.pcss b/res/css/_components.pcss index cc7e41bc99..a7c79bfbf2 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -337,7 +337,7 @@ @import "./views/settings/_Notifications.pcss"; @import "./views/settings/_PhoneNumbers.pcss"; @import "./views/settings/_PowerLevelSelector.pcss"; -@import "./views/settings/_ProfileSettings.pcss"; +@import "./views/settings/_RoomProfileSettings.pcss"; @import "./views/settings/_SecureBackupPanel.pcss"; @import "./views/settings/_SetIdServer.pcss"; @import "./views/settings/_SetIntegrationManager.pcss"; @@ -345,6 +345,7 @@ @import "./views/settings/_SpellCheckLanguages.pcss"; @import "./views/settings/_ThemeChoicePanel.pcss"; @import "./views/settings/_UpdateCheckButton.pcss"; +@import "./views/settings/_UserProfileSettings.pcss"; @import "./views/settings/tabs/_SettingsBanner.pcss"; @import "./views/settings/tabs/_SettingsIndent.pcss"; @import "./views/settings/tabs/_SettingsSection.pcss"; diff --git a/res/css/views/dialogs/_UserSettingsDialog.pcss b/res/css/views/dialogs/_UserSettingsDialog.pcss index 41d39f8b79..1e27bb4b6a 100644 --- a/res/css/views/dialogs/_UserSettingsDialog.pcss +++ b/res/css/views/dialogs/_UserSettingsDialog.pcss @@ -14,6 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_SettingsDialog_toastContainer { + position: absolute; + bottom: var(--cpd-space-10x); + width: 100%; + display: flex; + justify-content: center; +} + /* ICONS */ /* ========================================================== */ diff --git a/res/css/views/settings/_ProfileSettings.pcss b/res/css/views/settings/_RoomProfileSettings.pcss similarity index 75% rename from res/css/views/settings/_ProfileSettings.pcss rename to res/css/views/settings/_RoomProfileSettings.pcss index 73cdcd75c8..8af0249ab4 100644 --- a/res/css/views/settings/_ProfileSettings.pcss +++ b/res/css/views/settings/_RoomProfileSettings.pcss @@ -1,5 +1,5 @@ /* -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020, 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. @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_ProfileSettings { +.mx_RoomProfileSettings { border-bottom: 1px solid $quinary-content; - .mx_ProfileSettings_profile { + .mx_RoomProfileSettings_profile { display: flex; - .mx_ProfileSettings_profile_controls { + .mx_RoomProfileSettings_profile_controls { flex-grow: 1; margin-inline-end: 54px; @@ -28,7 +28,7 @@ limitations under the License. margin-top: $spacing-8; } - .mx_ProfileSettings_profile_controls_topic { + .mx_RoomProfileSettings_profile_controls_topic { margin-top: $spacing-8; & > textarea { @@ -36,18 +36,18 @@ limitations under the License. resize: vertical; } - &.mx_ProfileSettings_profile_controls_topic--room textarea { + &.mx_RoomProfileSettings_profile_controls_topic--room textarea { min-height: 4em; } } - .mx_ProfileSettings_profile_controls_userId { + .mx_RoomProfileSettings_profile_controls_userId { margin-inline-end: $spacing-20; } } } - .mx_ProfileSettings_buttons { + .mx_RoomProfileSettings_buttons { display: flex; gap: var(--cpd-space-4x); margin-top: 10px; /* 18px is already accounted for by the

above the buttons */ diff --git a/res/css/views/settings/_UserProfileSettings.pcss b/res/css/views/settings/_UserProfileSettings.pcss new file mode 100644 index 0000000000..3a9dc7dcc7 --- /dev/null +++ b/res/css/views/settings/_UserProfileSettings.pcss @@ -0,0 +1,58 @@ +/* +Copyright 2019, 2020, 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. +*/ + +.mx_UserProfileSettings { + border-bottom: 1px solid $quinary-content; + + .mx_UserProfileSettings_profile { + display: flex; + margin-top: var(--cpd-space-6x); + gap: 16px; + /* This is temporary until the 'Remove' link is replaced by a context menu. */ + margin-bottom: 20px; + + .mx_UserProfileSettings_profile_displayName { + flex-grow: 1; + width: 100%; + } + } + + .mx_UserProfileSettings_profile_controls { + flex-grow: 1; + } + + .mx_UserProfileSettings_profile_controls_userId { + width: 100%; + .mx_CopyableText { + margin-top: var(--cpd-space-1x); + width: 100%; + box-sizing: border-box; + } + } + + .mx_UserProfileSettings_profile_controls_userId_label { + font-size: 15px; + font-weight: 500; + } +} + +@media (max-width: 768px) { + .mx_UserProfileSettings_profile { + flex-direction: column; + align-items: center; + gap: 30px; + } +} diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index afcbc182d0..9aafeca2fd 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -15,6 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { Toast } from "@vector-im/compound-web"; import React, { useState } from "react"; import TabbedView, { Tab, useActiveTabWithDefault } from "../../structures/TabbedView"; @@ -38,6 +39,7 @@ import { UserTab } from "./UserTab"; import { NonEmptyArray } from "../../../@types/common"; import { SDKContext, SdkContextClass } from "../../../contexts/SDKContext"; import { useSettingValue } from "../../../hooks/useSettings"; +import { ToastContext, useActiveToast } from "../../../contexts/ToastContext"; interface IProps { initialTabId?: UserTab; @@ -215,27 +217,34 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { setShowMsc4108QrCode(false); }; + const [activeToast, toastRack] = useActiveToast(); + return ( // XXX: SDKContext is provided within the LoggedInView subtree. // Modals function outside the MatrixChat React tree, so sdkContext is reprovided here to simulate that. // The longer term solution is to move our ModalManager into the React tree to inherit contexts properly. - -

- -
- + + +
+ +
+
+ {activeToast && {activeToast}} +
+
+
); } diff --git a/src/components/views/elements/CopyableText.tsx b/src/components/views/elements/CopyableText.tsx index 5d9946d2c1..994d81607b 100644 --- a/src/components/views/elements/CopyableText.tsx +++ b/src/components/views/elements/CopyableText.tsx @@ -22,14 +22,14 @@ import { _t } from "../../../languageHandler"; import { copyPlaintext } from "../../../utils/strings"; import AccessibleButton, { ButtonEvent } from "./AccessibleButton"; -interface IProps { +interface IProps extends React.HTMLAttributes { children?: React.ReactNode; getTextToCopy: () => string | null; border?: boolean; className?: string; } -const CopyableText: React.FC = ({ children, getTextToCopy, border = true, className }) => { +const CopyableText: React.FC = ({ children, getTextToCopy, border = true, className, ...props }) => { const [tooltip, setTooltip] = useState(undefined); const onCopyClickInternal = async (e: ButtonEvent): Promise => { @@ -50,7 +50,7 @@ const CopyableText: React.FC = ({ children, getTextToCopy, border = true }); return ( -
+
{children} let profileSettingsButtons; if (this.state.canSetName || this.state.canSetTopic || this.state.canSetAvatar) { profileSettingsButtons = ( -
+
} return ( -
-
-
+ +
+
/> = ({ avatar, avatarAltText, onChange, remo } }, [avatar]); - // TODO: Use useId() as soon as we're using React 18. // Prevents ID collisions when this component is used more than once on the same page. - const a11yId = useRef(`hover-text-${Math.random()}`); + const a11yId = useId(); const onFileChanged = useCallback( (e: React.ChangeEvent) => { @@ -95,7 +95,7 @@ const AvatarSetting: React.FC = ({ avatar, avatarAltText, onChange, remo element="div" onClick={uploadAvatar} className="mx_AvatarSetting_avatarPlaceholder mx_AvatarSetting_avatarDisplay" - aria-labelledby={disabled ? undefined : a11yId.current} + aria-labelledby={disabled ? undefined : a11yId} // Inhibit tab stop as we have explicit upload/remove buttons tabIndex={-1} /> @@ -122,7 +122,7 @@ const AvatarSetting: React.FC = ({ avatar, avatarAltText, onChange, remo = ({ avatar, avatarAltText, onChange, remo {avatarElement}