Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ const config: Config = {
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
},
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error)).+$"],
transformIgnorePatterns: [
"/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error|react-merge-refs)).+$",
],
collectCoverageFrom: [
"<rootDir>/src/**/*.{js,ts,tsx}",
// getSessionLock is piped into a different JS context via stringification, and the coverage functionality is
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
"react-blurhash": "^0.3.0",
"react-dom": "^19.0.0",
"react-focus-lock": "^2.5.1",
"react-merge-refs": "^3.0.2",
"react-string-replace": "^1.1.1",
"react-transition-group": "^4.4.1",
"react-virtuoso": "^4.14.0",
Expand Down
4 changes: 1 addition & 3 deletions playwright/e2e/crypto/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ const startDMWithBob = async (page: Page, bob: Bot) => {
await page.getByRole("menuitem", { name: "Start chat" }).click();
await page.getByTestId("invite-dialog-input").fill(bob.credentials.userId);
await page.getByRole("option", { name: bob.credentials.displayName }).click();
await expect(
page.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText("Bob"),
).toBeVisible();
await expect(page.getByTestId("invite-dialog-input-wrapper").getByText("Bob")).toBeVisible();
await page.getByRole("button", { name: "Go" }).click();
};

Expand Down
8 changes: 2 additions & 6 deletions playwright/e2e/invite/invite-dialog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ test.describe("Invite dialog", function () {

await other.getByRole("option", { name: botName }).click();

await expect(
other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName),
).toBeVisible();
await expect(other.getByTestId("invite-dialog-input-wrapper").getByText(botName)).toBeVisible();

// Take a snapshot of the invite dialog with a user pill
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-room-with-user-pill.png");
Expand Down Expand Up @@ -95,9 +93,7 @@ test.describe("Invite dialog", function () {
await expect(other.getByRole("option", { name: botName }).getByText(bot.credentials.userId)).toBeVisible();
await other.getByRole("option", { name: botName }).click();

await expect(
other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName),
).toBeVisible();
await expect(other.getByTestId("invite-dialog-input-wrapper").getByText(botName)).toBeVisible();

// Take a snapshot of the invite dialog with a user pill
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-with-user-pill.png");
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion res/css/_common.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,7 @@ legend {
.mx_IdentityServerPicker button,
.mx_AccessSecretStorageDialog button,
.mx_InviteDialog_section button,
.mx_InviteDialog_editor button,
[class|="maplibregl"]
),
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton),
Expand Down Expand Up @@ -645,7 +646,8 @@ legend {
.mx_UnpinAllDialog button,
.mx_ShareDialog button,
.mx_EncryptionUserSettingsTab button,
.mx_InviteDialog_section button
.mx_InviteDialog_section button,
.mx_InviteDialog_editor button
):focus,
.mx_Dialog input[type="submit"]:focus,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton):focus,
Expand Down
77 changes: 1 addition & 76 deletions res/css/views/dialogs/_InviteDialog.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -24,37 +24,7 @@ Please see LICENSE files in the repository root for full details.

.mx_InviteDialog_editor {
flex: 1;
width: 100%; /* Needed to make the Field inside grow */
background-color: $header-panel-bg-color;
border-radius: 4px;
min-height: 25px;
padding-inline-start: $spacing-8;
overflow-x: hidden;
overflow-y: auto;
display: flex;
flex-wrap: wrap;

.mx_InviteDialog_userTile {
margin: 6px 6px 0 0;
display: inline-block;
min-width: max-content; /* prevent manipulation by flexbox */
}

/* overrides bunch of our default text input styles */
> input[type="text"] {
margin: 6px 0 !important;
height: 24px;
font: var(--cpd-font-body-md-regular);
line-height: $font-24px;
padding-inline-start: $spacing-12;
border: 0 !important;
outline: 0 !important;
resize: none;
box-sizing: border-box;
min-width: 40%;
flex: 1 !important;
color: $primary-content !important;
}
margin-left: var(--cpd-space-0-5x);
}

.mx_InviteDialog_goButton {
Expand Down Expand Up @@ -112,51 +82,6 @@ Please see LICENSE files in the repository root for full details.
}
}

/* Many of these styles are stolen from mx_UserPill, but adjusted for the invite dialog. */
.mx_InviteDialog_userTile {
margin-inline-end: $spacing-8;

.mx_InviteDialog_userTile_pill {
background-color: var(--cpd-color-bg-canvas-default);
border: 1px solid var(--cpd-color-gray-400);
border-radius: 99px;
display: inline-block;
height: 24px;
line-height: $font-24px;
padding-inline: $spacing-8;
vertical-align: middle;
color: var(--cpd-color-gray-1100);

.mx_SearchResultAvatar {
border-radius: 20px;
position: relative;
left: -5px;
top: 2px;
}

img.mx_SearchResultAvatar {
vertical-align: top;
}

.mx_InviteDialog_userTile_name {
vertical-align: top;
}

.mx_SearchResultAvatar_threepidAvatar {
background-color: #ffffff; /* this is fine without a var because it's for both themes */
}
}

.mx_InviteDialog_userTile_remove {
display: inline-block;
vertical-align: middle;

svg {
vertical-align: middle;
}
}
}

.mx_InviteDialog_other {
/* Prevent the dialog from jumping around randomly when elements change. */
display: flex;
Expand Down
89 changes: 27 additions & 62 deletions src/components/views/dialogs/InviteDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { logger } from "matrix-js-sdk/src/logger";
import { uniqBy } from "lodash";
import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons";

import { Icon as EmailPillAvatarIcon } from "../../../../res/img/icon-email-pill-avatar.svg";
import { _t, _td } from "../../../languageHandler";
Expand Down Expand Up @@ -66,6 +65,8 @@ import { type UserProfilesStore } from "../../../stores/UserProfilesStore";
import InviteProgressBody from "./InviteProgressBody.tsx";
import { RichList } from "../../../shared-components/rich-list/RichList";
import { RichItem } from "../../../shared-components/rich-list/RichItem";
import { PillInput } from "../../../shared-components/pill-input/PillInput";
import { Pill } from "../../../shared-components/pill-input/Pill";

// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */
Expand Down Expand Up @@ -121,27 +122,10 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
const avatarSize = "20px";
const avatar = <SearchResultAvatar user={this.props.member} size={avatarSize} />;

let closeButton;
if (this.props.onRemove) {
closeButton = (
<AccessibleButton
className="mx_InviteDialog_userTile_remove"
onClick={this.onRemove}
aria-label={_t("action|remove")}
>
<CloseIcon width="16px" height="16px" />
</AccessibleButton>
);
}

return (
<span className="mx_InviteDialog_userTile">
<span className="mx_InviteDialog_userTile_pill">
{avatar}
<span className="mx_InviteDialog_userTile_name">{this.props.member.name}</span>
</span>
{closeButton}
</span>
<Pill label={this.props.member.name} onClick={this.onRemove}>
{avatar}
</Pill>
);
}
}
Expand Down Expand Up @@ -609,13 +593,6 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
const action = getKeyBindingsManager().getAccessibilityAction(e);

switch (action) {
case KeyBindingAction.Backspace:
if (value || this.state.targets.length <= 0) break;

// when the field is empty and the user hits backspace remove the right-most target
this.removeMember(this.state.targets[this.state.targets.length - 1]);
handled = true;
break;
case KeyBindingAction.Space:
if (!value || !value.includes("@") || value.includes(" ")) break;

Expand Down Expand Up @@ -908,16 +885,6 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
}
};

private onClickInputArea = (e: React.MouseEvent): void => {
// Stop the browser from highlighting text
e.preventDefault();
e.stopPropagation();

if (this.editorRef && this.editorRef.current) {
this.editorRef.current.focus();
}
};

private onUseDefaultIdentityServerClick = (e: ButtonEvent): void => {
e.preventDefault();

Expand Down Expand Up @@ -1041,35 +1008,33 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
}

private renderEditor(): JSX.Element {
const hasPlaceholder =
this.props.kind == InviteKind.CallTransfer &&
this.state.targets.length === 0 &&
this.state.filterText.length === 0;
const targets = this.state.targets.map((t) => (
<DMUserTile member={t} onRemove={this.state.busy ? undefined : this.removeMember} key={t.userId} />
));
const input = (
<input
type="text"
onKeyDown={this.onKeyDown}
onChange={this.updateFilter}
value={this.state.filterText}
ref={this.editorRef}
onPaste={this.onPaste}
autoFocus={true}
disabled={
this.state.busy || (this.props.kind == InviteKind.CallTransfer && this.state.targets.length > 0)
}
autoComplete="off"
placeholder={hasPlaceholder ? _t("action|search") : undefined}
data-testid="invite-dialog-input"
/>
);

return (
<div className="mx_InviteDialog_editor" onClick={this.onClickInputArea}>
<PillInput
data-testid="invite-dialog-input-wrapper"
className="mx_InviteDialog_editor"
inputProps={{
"ref": this.editorRef,
"value": this.state.filterText,
"onKeyDown": this.onKeyDown,
"onChange": this.updateFilter,
"onPaste": this.onPaste,
"placeholder": _t("action|search"),
"autoFocus": true,
"disabled":
this.state.busy ||
(this.props.kind == InviteKind.CallTransfer && this.state.targets.length > 0),
"data-testid": "invite-dialog-input",
}}
onRemoveChildren={() =>
!this.state.busy && this.removeMember(this.state.targets[this.state.targets.length - 1])
}
>
{targets}
{input}
</div>
</PillInput>
);
}

Expand Down
17 changes: 17 additions & 0 deletions src/shared-components/pill-input/Pill/Pill.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/

.pill {
background-color: var(--cpd-color-bg-action-primary-rest);
padding: var(--cpd-space-1x) var(--cpd-space-1-5x) var(--cpd-space-1x) var(--cpd-space-1x);
border-radius: 99px;
}

.label {
color: var(--cpd-color-text-on-solid-primary);
font: var(--cpd-font-body-sm-medium);
}
33 changes: 33 additions & 0 deletions src/shared-components/pill-input/Pill/Pill.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/

import React from "react";
import { fn } from "storybook/test";

import type { Meta, StoryObj } from "@storybook/react-vite";
import { Pill } from "./Pill";

const meta = {
title: "PillInput/Pill",
component: Pill,
tags: ["autodocs"],
args: {
label: "Pill",
children: <div style={{ width: 20, height: 20, borderRadius: "100%", backgroundColor: "#ccc" }} />,
onClick: fn(),
},
} satisfies Meta<typeof Pill>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};
export const WithoutCloseButton: Story = {
args: {
onClick: undefined,
},
};
26 changes: 26 additions & 0 deletions src/shared-components/pill-input/Pill/Pill.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/

import { composeStories } from "@storybook/react-vite";
import { render } from "jest-matrix-react";
import React from "react";

import * as stories from "./Pill.stories";

const { Default, WithoutCloseButton } = composeStories(stories);

describe("Pill", () => {
it("renders the pill", () => {
const { container } = render(<Default />);
expect(container).toMatchSnapshot();
});

it("renders the pill without close button", () => {
const { container } = render(<WithoutCloseButton />);
expect(container).toMatchSnapshot();
});
});
Loading
Loading