1
0
mirror of https://github.com/element-hq/element-web.git synced 2025-08-08 03:42:14 +03:00

New room list: add search section (#29251)

* feat(new room list): move `RoomListView` to its own folder and add styling

* feat(new room list): add search section

* test(new room list): add tests for `RoomListSearch`

* test(new room list): add tests for `RoomListView`

* test(e2e): add method to close notification toast to `ElementAppPage`

* test(e2e): add tests for the search section

* test(e2e): add tests for the room list view

* refactor: use Flex component

* fix: loop icon size in search button

* refactor: remove `focus_room_filter` listener
This commit is contained in:
Florian D
2025-02-13 16:49:09 +01:00
committed by GitHub
parent 85f80b1d0a
commit 2abd5342c2
18 changed files with 609 additions and 21 deletions

View File

@@ -0,0 +1,53 @@
/*
* 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 { type Page } from "@playwright/test";
import { test, expect } from "../../../element-web-test";
test.describe("Search section of the room list", () => {
test.use({
labsFlags: ["feature_new_room_list"],
});
/**
* Get the search section of the room list
* @param page
*/
function getSearchSection(page: Page) {
return page.getByRole("search");
}
test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
await app.closeNotificationToast();
});
test("should render the search section", { tag: "@screenshot" }, async ({ page, app, user }) => {
const searchSection = getSearchSection(page);
// exact=false to ignore the shortcut which is related to the OS
await expect(searchSection.getByRole("button", { name: "Search", exact: false })).toBeVisible();
await expect(searchSection).toMatchScreenshot("search-section.png");
});
test("should open the spotlight when the search button is clicked", async ({ page, app, user }) => {
const searchSection = getSearchSection(page);
await searchSection.getByRole("button", { name: "Search", exact: false }).click();
// The spotlight should be displayed
await expect(page.getByRole("dialog", { name: "Search Dialog" })).toBeVisible();
});
test("should open the room directory when the search button is clicked", async ({ page, app, user }) => {
const searchSection = getSearchSection(page);
await searchSection.getByRole("button", { name: "Explore rooms" }).click();
const dialog = page.getByRole("dialog", { name: "Search Dialog" });
// The room directory should be displayed
await expect(dialog).toBeVisible();
// The public room filter should be displayed
await expect(dialog.getByText("Public rooms")).toBeVisible();
});
});

View File

@@ -0,0 +1,34 @@
/*
* 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 { type Page } from "@playwright/test";
import { test, expect } from "../../../element-web-test";
test.describe("Search section of the room list", () => {
test.use({
labsFlags: ["feature_new_room_list"],
});
/**
* Get the room list view
* @param page
*/
function getRoomListView(page: Page) {
return page.getByTestId("room-list-view");
}
test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
await app.closeNotificationToast();
});
test("should render the room list view", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomListView(page);
await expect(roomListView).toMatchScreenshot("room-list-view.png");
});
});

View File

@@ -25,13 +25,9 @@ test.describe("Security user settings tab", () => {
},
});
test.beforeEach(async ({ page, user }) => {
test.beforeEach(async ({ page, app, user }) => {
// Dismiss "Notification" toast
await page
.locator(".mx_Toast_toast", { hasText: "Notifications" })
.getByRole("button", { name: "Dismiss" })
.click();
await app.closeNotificationToast();
await page.locator(".mx_Toast_buttons").getByRole("button", { name: "Yes" }).click(); // Allow analytics
});

View File

@@ -202,4 +202,15 @@ export class ElementAppPage {
}
return this.page.locator(`id=${labelledById ?? describedById}`);
}
/**
* Close the notification toast
*/
public closeNotificationToast(): Promise<void> {
// Dismiss "Notification" toast
return this.page
.locator(".mx_Toast_toast", { hasText: "Notifications" })
.getByRole("button", { name: "Dismiss" })
.click();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -269,6 +269,8 @@
@import "./views/right_panel/_VerificationPanel.pcss";
@import "./views/right_panel/_WidgetCard.pcss";
@import "./views/room_settings/_AliasSettings.pcss";
@import "./views/rooms/RoomListView/_RoomListSearch.pcss";
@import "./views/rooms/RoomListView/_RoomListView.pcss";
@import "./views/rooms/_AppsDrawer.pcss";
@import "./views/rooms/_Autocomplete.pcss";
@import "./views/rooms/_AuxPanel.pcss";

View File

@@ -0,0 +1,39 @@
/*
* 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.
*/
.mx_RoomListSearch {
/* From figma, this should be aligned with the room header */
height: 64px;
box-sizing: border-box;
border-bottom: 1px solid var(--cpd-color-bg-subtle-primary);
padding: 0 var(--cpd-space-3x);
svg {
fill: var(--cpd-color-icon-secondary);
}
.mx_RoomListSearch_search {
/* The search button should take all the remaining space */
flex: 1;
font: var(--cpd-font-body-md-regular);
color: var(--cpd-color-text-secondary);
span {
flex: 1;
kbd {
font-family: inherit;
}
}
}
.mx_RoomListSearch_explore:hover {
svg {
fill: var(--cpd-color-icon-primary);
}
}
}

View File

@@ -0,0 +1,12 @@
/*
* 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.
*/
.mx_RoomListView {
background-color: var(--cpd-color-bg-canvas-default);
height: 100%;
border-right: 1px solid var(--cpd-color-bg-subtle-primary);
}

View File

@@ -390,7 +390,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
return (
<div className={containerClasses}>
<div className="mx_LeftPanel_roomListContainer">
<RoomListView />
<RoomListView activeSpace={this.state.activeSpace} />
</div>
</div>
);

View File

@@ -1,14 +0,0 @@
/*
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";
type IProps = unknown;
export const RoomListView: React.FC<IProps> = (props: IProps) => {
return <div>New Room List</div>;
};

View File

@@ -0,0 +1,69 @@
/*
* 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, { type JSX } from "react";
import { Button } from "@vector-im/compound-web";
import ExploreIcon from "@vector-im/compound-design-tokens/assets/web/icons/explore";
import SearchIcon from "@vector-im/compound-design-tokens/assets/web/icons/search";
import { IS_MAC, Key } from "../../../../Keyboard";
import { _t } from "../../../../languageHandler";
import { ALTERNATE_KEY_NAME } from "../../../../accessibility/KeyboardShortcuts";
import { shouldShowComponent } from "../../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../../settings/UIFeature";
import { MetaSpace } from "../../../../stores/spaces";
import { Action } from "../../../../dispatcher/actions";
import PosthogTrackers from "../../../../PosthogTrackers";
import defaultDispatcher from "../../../../dispatcher/dispatcher";
import { Flex } from "../../../utils/Flex";
type RoomListSearchProps = {
/**
* Current active space
* The explore button is only displayed in the Home meta space
*/
activeSpace: string;
};
/**
* A search component to be displayed at the top of the room list
* The `Explore` button is displayed only in the Home meta space and when UIComponent.ExploreRooms is enabled.
*/
export function RoomListSearch({ activeSpace }: RoomListSearchProps): JSX.Element {
const displayExploreButton = activeSpace === MetaSpace.Home && shouldShowComponent(UIComponent.ExploreRooms);
return (
<Flex className="mx_RoomListSearch" role="search" gap="var(--cpd-space-2x)" align="center">
<Button
className="mx_RoomListSearch_search"
kind="secondary"
size="sm"
Icon={SearchIcon}
onClick={() => defaultDispatcher.fire(Action.OpenSpotlight)}
>
<Flex as="span" justify="space-between">
{_t("action|search")}
<kbd>{IS_MAC ? "⌘ K" : _t(ALTERNATE_KEY_NAME[Key.CONTROL]) + " K"}</kbd>
</Flex>
</Button>
{displayExploreButton && (
<Button
className="mx_RoomListSearch_explore"
kind="secondary"
size="sm"
Icon={ExploreIcon}
iconOnly={true}
aria-label={_t("action|explore_rooms")}
onClick={(ev) => {
defaultDispatcher.fire(Action.ViewRoomDirectory);
PosthogTrackers.trackInteraction("WebLeftPanelExploreRoomsButton", ev);
}}
/>
)}
</Flex>
);
}

View File

@@ -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 { shouldShowComponent } from "../../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../../settings/UIFeature";
import { RoomListSearch } from "./RoomListSearch";
type RoomListViewProps = {
/**
* Current active space
* See {@link RoomListSearch}
*/
activeSpace: string;
};
/**
* A view component for the room list.
*/
export const RoomListView: React.FC<RoomListViewProps> = ({ activeSpace }) => {
const displayRoomSearch = shouldShowComponent(UIComponent.FilterContainer);
return (
<div className="mx_RoomListView" data-testid="room-list-view">
{displayRoomSearch && <RoomListSearch activeSpace={activeSpace} />}
</div>
);
};

View File

@@ -0,0 +1,8 @@
/*
* 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.
*/
export { RoomListView } from "./RoomListView";

View File

@@ -0,0 +1,84 @@
/*
* 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 { render, screen } from "jest-matrix-react";
import { mocked } from "jest-mock";
import userEvent from "@testing-library/user-event";
import { RoomListSearch } from "../../../../../../src/components/views/rooms/RoomListView/RoomListSearch";
import { MetaSpace } from "../../../../../../src/stores/spaces";
import { shouldShowComponent } from "../../../../../../src/customisations/helpers/UIComponents";
import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../../src/dispatcher/actions";
jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => ({
shouldShowComponent: jest.fn(),
}));
describe("<RoomListSearch />", () => {
function renderComponent(activeSpace = MetaSpace.Home) {
return render(<RoomListSearch activeSpace={activeSpace} />);
}
beforeEach(() => {
// By default, we consider shouldShowComponent(UIComponent.ExploreRooms) should return true
mocked(shouldShowComponent).mockReturnValue(true);
});
it("should display all the buttons", () => {
const { asFragment } = renderComponent();
// The search and explore buttons should be displayed
expect(screen.getByRole("button", { name: "Search Ctrl K" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Explore rooms" })).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
it("should hide the explore button when the active space is not MetaSpace.Home", () => {
const { asFragment } = renderComponent(MetaSpace.VideoRooms);
// The search button should be displayed but not the explore button
expect(screen.getByRole("button", { name: "Search Ctrl K" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Explore rooms" })).not.toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
it("should hide the explore button when UIComponent.ExploreRooms is disabled", () => {
mocked(shouldShowComponent).mockReturnValue(false);
const { asFragment } = renderComponent();
// The search button should be displayed but not the explore button
expect(screen.getByRole("button", { name: "Search Ctrl K" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Explore rooms" })).not.toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
it("should open the spotlight when the search button is clicked", async () => {
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
const user = userEvent.setup();
renderComponent();
// Click on the search button
await user.click(screen.getByRole("button", { name: "Search Ctrl K" }));
// The spotlight should be opened
expect(fireSpy).toHaveBeenCalledWith(Action.OpenSpotlight);
});
it("should open the room directory when the explore button is clicked", async () => {
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
const user = userEvent.setup();
renderComponent();
// Click on the search button
await user.click(screen.getByRole("button", { name: "Explore rooms" }));
// The spotlight should be opened
expect(fireSpy).toHaveBeenCalledWith(Action.ViewRoomDirectory);
});
});

View File

@@ -0,0 +1,43 @@
/*
* 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 { render, screen } from "jest-matrix-react";
import { mocked } from "jest-mock";
import { RoomListView } from "../../../../../../src/components/views/rooms/RoomListView";
import { shouldShowComponent } from "../../../../../../src/customisations/helpers/UIComponents";
import { MetaSpace } from "../../../../../../src/stores/spaces";
jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => ({
shouldShowComponent: jest.fn(),
}));
describe("<RoomListView />", () => {
function renderComponent() {
return render(<RoomListView activeSpace={MetaSpace.Home} />);
}
beforeEach(() => {
// By default, we consider shouldShowComponent(UIComponent.FilterContainer) should return true
mocked(shouldShowComponent).mockReturnValue(true);
});
it("should render the RoomListSearch component when UIComponent.FilterContainer is at true", () => {
const { asFragment } = renderComponent();
expect(screen.getByRole("button", { name: "Search Ctrl K" })).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
it("should not render the RoomListSearch component when UIComponent.FilterContainer is at false", () => {
mocked(shouldShowComponent).mockReturnValue(false);
const { asFragment } = renderComponent();
expect(screen.queryByRole("button", { name: "Search Ctrl K" })).toBeNull();
expect(asFragment()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,142 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RoomListSearch /> should display all the buttons 1`] = `
<DocumentFragment>
<div
class="mx_Flex mx_RoomListSearch"
role="search"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button
class="_button_i91xf_17 mx_RoomListSearch_search _has-icon_i91xf_66"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.05 16.463a7.5 7.5 0 1 1 1.414-1.414l3.243 3.244a1 1 0 0 1-1.414 1.414l-3.244-3.244ZM16 10.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Z"
/>
</svg>
<span
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0;"
>
Search
<kbd>
Ctrl K
</kbd>
</span>
</button>
<button
aria-label="Explore rooms"
class="_button_i91xf_17 mx_RoomListSearch_explore _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 13a.968.968 0 0 1-.713-.287A.968.968 0 0 1 11 12c0-.283.096-.52.287-.713A.968.968 0 0 1 12 11c.283 0 .52.096.713.287.191.192.287.43.287.713s-.096.52-.287.713A.968.968 0 0 1 12 13Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Zm0-2c2.233 0 4.125-.775 5.675-2.325C19.225 16.125 20 14.233 20 12c0-2.233-.775-4.125-2.325-5.675C16.125 4.775 14.233 4 12 4c-2.233 0-4.125.775-5.675 2.325C4.775 7.875 4 9.767 4 12c0 2.233.775 4.125 2.325 5.675C7.875 19.225 9.767 20 12 20Zm0 0c-2.233 0-4.125-.775-5.675-2.325C4.775 16.125 4 14.233 4 12c0-2.233.775-4.125 2.325-5.675C7.875 4.775 9.767 4 12 4c2.233 0 4.125.775 5.675 2.325C19.225 7.875 20 9.767 20 12c0 2.233-.775 4.125-2.325 5.675C16.125 19.225 14.233 20 12 20Zm1.675-5.85c.1-.05.192-.117.275-.2.083-.083.15-.175.2-.275l2.925-6.25c.083-.167.063-.313-.063-.438-.125-.125-.27-.145-.437-.062l-6.25 2.925c-.1.05-.192.117-.275.2-.083.083-.15.175-.2.275l-2.925 6.25c-.083.167-.063.313.063.438.124.124.27.145.437.062l6.25-2.925Z"
/>
</svg>
</button>
</div>
</DocumentFragment>
`;
exports[`<RoomListSearch /> should hide the explore button when UIComponent.ExploreRooms is disabled 1`] = `
<DocumentFragment>
<div
class="mx_Flex mx_RoomListSearch"
role="search"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button
class="_button_i91xf_17 mx_RoomListSearch_search _has-icon_i91xf_66"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.05 16.463a7.5 7.5 0 1 1 1.414-1.414l3.243 3.244a1 1 0 0 1-1.414 1.414l-3.244-3.244ZM16 10.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Z"
/>
</svg>
<span
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0;"
>
Search
<kbd>
Ctrl K
</kbd>
</span>
</button>
</div>
</DocumentFragment>
`;
exports[`<RoomListSearch /> should hide the explore button when the active space is not MetaSpace.Home 1`] = `
<DocumentFragment>
<div
class="mx_Flex mx_RoomListSearch"
role="search"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button
class="_button_i91xf_17 mx_RoomListSearch_search _has-icon_i91xf_66"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.05 16.463a7.5 7.5 0 1 1 1.414-1.414l3.243 3.244a1 1 0 0 1-1.414 1.414l-3.244-3.244ZM16 10.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Z"
/>
</svg>
<span
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0;"
>
Search
<kbd>
Ctrl K
</kbd>
</span>
</button>
</div>
</DocumentFragment>
`;

View File

@@ -0,0 +1,76 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RoomListView /> should not render the RoomListSearch component when UIComponent.FilterContainer is at false 1`] = `
<DocumentFragment>
<div
class="mx_RoomListView"
data-testid="room-list-view"
/>
</DocumentFragment>
`;
exports[`<RoomListView /> should render the RoomListSearch component when UIComponent.FilterContainer is at true 1`] = `
<DocumentFragment>
<div
class="mx_RoomListView"
data-testid="room-list-view"
>
<div
class="mx_Flex mx_RoomListSearch"
role="search"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button
class="_button_i91xf_17 mx_RoomListSearch_search _has-icon_i91xf_66"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.05 16.463a7.5 7.5 0 1 1 1.414-1.414l3.243 3.244a1 1 0 0 1-1.414 1.414l-3.244-3.244ZM16 10.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Z"
/>
</svg>
<span
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0;"
>
Search
<kbd>
Ctrl K
</kbd>
</span>
</button>
<button
aria-label="Explore rooms"
class="_button_i91xf_17 mx_RoomListSearch_explore _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 13a.968.968 0 0 1-.713-.287A.968.968 0 0 1 11 12c0-.283.096-.52.287-.713A.968.968 0 0 1 12 11c.283 0 .52.096.713.287.191.192.287.43.287.713s-.096.52-.287.713A.968.968 0 0 1 12 13Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Zm0-2c2.233 0 4.125-.775 5.675-2.325C19.225 16.125 20 14.233 20 12c0-2.233-.775-4.125-2.325-5.675C16.125 4.775 14.233 4 12 4c-2.233 0-4.125.775-5.675 2.325C4.775 7.875 4 9.767 4 12c0 2.233.775 4.125 2.325 5.675C7.875 19.225 9.767 20 12 20Zm0 0c-2.233 0-4.125-.775-5.675-2.325C4.775 16.125 4 14.233 4 12c0-2.233.775-4.125 2.325-5.675C7.875 4.775 9.767 4 12 4c2.233 0 4.125.775 5.675 2.325C19.225 7.875 20 9.767 20 12c0 2.233-.775 4.125-2.325 5.675C16.125 19.225 14.233 20 12 20Zm1.675-5.85c.1-.05.192-.117.275-.2.083-.083.15-.175.2-.275l2.925-6.25c.083-.167.063-.313-.063-.438-.125-.125-.27-.145-.437-.062l-6.25 2.925c-.1.05-.192.117-.275.2-.083.083-.15.175-.2.275l-2.925 6.25c-.083.167-.063.313.063.438.124.124.27.145.437.062l6.25-2.925Z"
/>
</svg>
</button>
</div>
</div>
</DocumentFragment>
`;