Refactor MessageActionBar using MVVM and move to shared-components (#32784)
* Refactor MessageActionBar into MVVM ActionBarView * Adding tooltips for menu items and correct i18n strings * Layout changes * Renaming some properties * Rename property * Create a first version of the view model and refactor media visibility logic * Refactor view to take options and rections menu as optional properties * Cleaner interface between view and view model * Refactor view properties and replace Menu and MenuItem * Bugfixes and switching to ActionBarView instead of MessageActionBar in element-web * Avoid creating view models and render toolbar until it is actually shown * Added unit and playwright tests and documented the view * Added view model unit tests and updated snapshots of dependant tests * Remove unused components and unnecessary css * Remove unused language tags * Fix for handling join-rules correctly * Prettier * Add handling of stale view model in async calls * Prettier * Split the element-web css into two different. One for legacy components and one for the ActionBarView * Missing variables used for linting * Fix for showing ActionBarView when using keyboard for navigation * Handle visibility on context menu closing * ThreadPanel uses the ActionBarView so restore css rule * Fix for visibility of the ActionBarView in Thread panel * Fix for ActionBarVuew visibility when closing right-click context menu and not still hovering * Add roving index to function as a toolbar * Adjust the RoomView test to send hover to the EventTile instead of the message text * Fix SonarCloud issues * Fix for SonarCloud issue * Merge fix * Rename mx_LegacyActionBar to mx_ThreadActionBar * Added documentation and simplified join rules * Generalize the ActionBarView and move logic to view model * Add the four new buttons to the ActionBarView * Update view model and tests to use the updated ActionBarView * Refactor element-web to use ActionBarView * Clean up styling in element-web * Clean up and updating snaps and screenshots * Added unit-tests for better coverage * Moving ActionBarView to the correct folder in shared components * Update snaps in element-web * Better documentation in stories * Merge fixes * Updates after review comments * Review comment fixes * Added documentation to view models and updated snaps * Hide button had the wrong label * Replace createRef with useRef
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
},
|
||||
"action": {
|
||||
"back": "Back",
|
||||
"click": "Click",
|
||||
"collapse": "Collapse",
|
||||
"delete": "Delete",
|
||||
"dismiss": "Dismiss",
|
||||
@@ -11,24 +12,35 @@
|
||||
"edit": "Edit",
|
||||
"explore_rooms": "Explore rooms",
|
||||
"go": "Go",
|
||||
"hide": "Hide",
|
||||
"invite": "Invite",
|
||||
"new_conversation": "New conversation",
|
||||
"new_room": "New room",
|
||||
"new_video_room": "New video room",
|
||||
"open_menu": "Open menu",
|
||||
"pause": "Pause",
|
||||
"pin": "Pin",
|
||||
"play": "Play",
|
||||
"react": "React",
|
||||
"remove": "Remove",
|
||||
"reply": "Reply",
|
||||
"reply_in_thread": "Reply in thread",
|
||||
"retry": "Retry",
|
||||
"search": "Search",
|
||||
"start_chat": "Start chat"
|
||||
"start_chat": "Start chat",
|
||||
"unpin": "Unpin",
|
||||
"view_source": "View Source"
|
||||
},
|
||||
"common": {
|
||||
"attachment": "Attachment",
|
||||
"encryption_enabled": "Encryption enabled",
|
||||
"options": "Options",
|
||||
"preferences": "Preferences",
|
||||
"state_encryption_enabled": "Experimental state encryption enabled"
|
||||
},
|
||||
"keyboard": {
|
||||
"shift": "Shift"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "Open dial pad",
|
||||
"separator_label": "Click or drag to expand"
|
||||
@@ -146,6 +158,9 @@
|
||||
"terms": {
|
||||
"tac_button": "Review terms and conditions"
|
||||
},
|
||||
"threads": {
|
||||
"error_start_thread_existing_relation": "Can't create a thread from an event with an existing relation"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "about a day ago",
|
||||
"about_hour_ago": "about an hour ago",
|
||||
@@ -172,6 +187,8 @@
|
||||
"sender_unsigned_device": "Sent from an insecure device.",
|
||||
"unable_to_decrypt": "Unable to decrypt message"
|
||||
},
|
||||
"download_action_decrypting": "Decrypting",
|
||||
"download_action_downloading": "Downloading",
|
||||
"m.audio": {
|
||||
"audio_player": "Audio player",
|
||||
"error_downloading_audio": "Error downloading audio",
|
||||
@@ -190,6 +207,13 @@
|
||||
"state_enabled": "Messages and state events in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their profile picture.",
|
||||
"unsupported": "The encryption used by this room isn't supported."
|
||||
},
|
||||
"mab": {
|
||||
"collapse_reply_chain": "Collapse quotes",
|
||||
"copy_link_thread": "Copy link to thread",
|
||||
"expand_reply_chain": "Expand quotes",
|
||||
"label": "Message Actions",
|
||||
"view_in_room": "View in room"
|
||||
},
|
||||
"message_timestamp_received_at": "Received at: %(dateTime)s",
|
||||
"message_timestamp_sent_at": "Sent at: %(dateTime)s",
|
||||
"url_preview": {
|
||||
|
||||
@@ -24,6 +24,7 @@ export * from "./room/WidgetPip";
|
||||
export * from "./room/HistoryVisibilityBadge";
|
||||
export * from "./room/timeline/DateSeparatorView";
|
||||
export * from "./room/timeline/TimelineSeparator";
|
||||
export * from "./room/timeline/event-tile/actions/ActionBarView";
|
||||
export * from "./room/timeline/event-tile/EventTileView/DisambiguatedProfile";
|
||||
export * from "./room/timeline/event-tile/EventTileView/EncryptionEventView";
|
||||
export * from "./room/timeline/event-tile/EventTileView/EventTileBubble";
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations 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, Tooltip } from "@vector-im/compound-web";
|
||||
|
||||
import styles from "./ActionBarView.module.css";
|
||||
|
||||
interface ActionBarButtonProps {
|
||||
presentation: "icon" | "label";
|
||||
buttonRef: React.Ref<HTMLButtonElement>;
|
||||
label: string;
|
||||
onActivate?: (anchor: HTMLElement | null) => void;
|
||||
icon?: React.ComponentProps<typeof Button>["Icon"];
|
||||
disabled?: boolean;
|
||||
ariaPressed?: boolean;
|
||||
ariaExpanded?: boolean;
|
||||
tooltipDescription?: string;
|
||||
tooltipCaption?: string;
|
||||
}
|
||||
|
||||
export function ActionBarButton({
|
||||
presentation,
|
||||
buttonRef,
|
||||
label,
|
||||
onActivate,
|
||||
icon,
|
||||
disabled,
|
||||
ariaPressed,
|
||||
ariaExpanded,
|
||||
tooltipDescription,
|
||||
tooltipCaption,
|
||||
}: Readonly<ActionBarButtonProps>): JSX.Element {
|
||||
const iconOnly = presentation === "icon";
|
||||
|
||||
const handleContextMenu = (event: React.MouseEvent<HTMLButtonElement>): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onActivate?.(event.currentTarget);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip description={tooltipDescription ?? label} caption={tooltipCaption} placement="top">
|
||||
<Button
|
||||
data-presentation={presentation}
|
||||
ref={buttonRef}
|
||||
kind="tertiary"
|
||||
size="sm"
|
||||
iconOnly={iconOnly}
|
||||
aria-label={label}
|
||||
aria-pressed={ariaPressed}
|
||||
aria-expanded={ariaExpanded}
|
||||
disabled={disabled}
|
||||
onClick={(event) => onActivate?.(event.currentTarget)}
|
||||
onContextMenu={handleContextMenu}
|
||||
className={styles.toolbar_item}
|
||||
Icon={iconOnly ? icon : undefined}
|
||||
>
|
||||
{iconOnly ? undefined : label}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations 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.
|
||||
*/
|
||||
|
||||
.toolbar {
|
||||
gap: var(--cpd-space-0-5x);
|
||||
border-radius: 8px;
|
||||
background-color: var(--cpd-color-bg-canvas-default);
|
||||
border: var(--cpd-border-width-1) solid var(--cpd-color-border-disabled);
|
||||
|
||||
.toolbar_item {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 !important;
|
||||
min-block-size: 0 !important;
|
||||
|
||||
margin: 3px !important;
|
||||
border-radius: 6px !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--cpd-color-bg-subtle-secondary) !important;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar_item[data-presentation="icon"] {
|
||||
block-size: 28px !important;
|
||||
inline-size: 28px !important;
|
||||
|
||||
color: var(--cpd-color-icon-secondary) !important;
|
||||
}
|
||||
|
||||
.toolbar_item[data-presentation="label"] {
|
||||
padding-inline-start: var(--cpd-space-2x) !important;
|
||||
padding-inline-end: var(--cpd-space-2x) !important;
|
||||
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
text-decoration: none !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations 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 { fn } from "storybook/test";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { ActionBarAction, ActionBarView, type ActionBarViewActions, type ActionBarViewSnapshot } from "./ActionBarView";
|
||||
import { useMockedViewModel } from "../../../../../core/viewmodel";
|
||||
import { withViewDocs } from "../../../../../../.storybook/withViewDocs";
|
||||
|
||||
type ActionBarProps = ActionBarViewSnapshot & ActionBarViewActions;
|
||||
|
||||
const ActionBarViewWrapperImpl = ({ ...snapshotAndActions }: ActionBarProps): JSX.Element => {
|
||||
const {
|
||||
onCancelClick = fn(),
|
||||
onCopyLinkClick = fn(),
|
||||
onDownloadClick = fn(),
|
||||
onEditClick = fn(),
|
||||
onHideClick = fn(),
|
||||
onOptionsClick = fn(),
|
||||
onPinClick = fn(),
|
||||
onReactionsClick = fn(),
|
||||
onRemoveClick = fn(),
|
||||
onReplyClick = fn(),
|
||||
onReplyInThreadClick = fn(),
|
||||
onResendClick = fn(),
|
||||
onToggleThreadExpanded = fn(),
|
||||
onViewInRoomClick = fn(),
|
||||
onViewSourceClick = fn(),
|
||||
...snapshot
|
||||
} = snapshotAndActions;
|
||||
|
||||
const vm = useMockedViewModel(snapshot, {
|
||||
onCancelClick,
|
||||
onCopyLinkClick,
|
||||
onDownloadClick,
|
||||
onEditClick,
|
||||
onHideClick,
|
||||
onOptionsClick,
|
||||
onPinClick,
|
||||
onReactionsClick,
|
||||
onRemoveClick,
|
||||
onReplyClick,
|
||||
onReplyInThreadClick,
|
||||
onResendClick,
|
||||
onToggleThreadExpanded,
|
||||
onViewInRoomClick,
|
||||
onViewSourceClick,
|
||||
});
|
||||
|
||||
return <ActionBarView vm={vm} className="mx_MessageActionBar" />;
|
||||
};
|
||||
const ActionBarViewWrapper = withViewDocs(ActionBarViewWrapperImpl, ActionBarView);
|
||||
|
||||
const meta = {
|
||||
title: "Room/Timeline/EventTile/Actions/ActionBarView",
|
||||
component: ActionBarViewWrapper,
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
presentation: {
|
||||
control: { type: "select" },
|
||||
options: ["icon", "label"],
|
||||
},
|
||||
},
|
||||
args: {
|
||||
actions: [
|
||||
ActionBarAction.Hide,
|
||||
ActionBarAction.Download,
|
||||
ActionBarAction.React,
|
||||
ActionBarAction.Reply,
|
||||
ActionBarAction.ReplyInThread,
|
||||
ActionBarAction.Edit,
|
||||
ActionBarAction.Pin,
|
||||
ActionBarAction.Resend,
|
||||
ActionBarAction.Cancel,
|
||||
ActionBarAction.Expand,
|
||||
ActionBarAction.Options,
|
||||
ActionBarAction.ViewInRoom,
|
||||
ActionBarAction.CopyLink,
|
||||
ActionBarAction.Remove,
|
||||
ActionBarAction.ViewSource,
|
||||
],
|
||||
presentation: "icon",
|
||||
isDownloadEncrypted: false,
|
||||
isDownloadLoading: false,
|
||||
isPinned: false,
|
||||
isQuoteExpanded: false,
|
||||
isThreadReplyAllowed: true,
|
||||
},
|
||||
} satisfies Meta<typeof ActionBarViewWrapper>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const AllIconActions: Story = {};
|
||||
|
||||
export const AllLabelActions: Story = {
|
||||
args: {
|
||||
actions: [
|
||||
ActionBarAction.Hide,
|
||||
ActionBarAction.Download,
|
||||
ActionBarAction.React,
|
||||
ActionBarAction.Reply,
|
||||
ActionBarAction.ReplyInThread,
|
||||
ActionBarAction.Edit,
|
||||
ActionBarAction.Pin,
|
||||
ActionBarAction.Resend,
|
||||
ActionBarAction.Cancel,
|
||||
ActionBarAction.Expand,
|
||||
ActionBarAction.Options,
|
||||
ActionBarAction.ViewInRoom,
|
||||
ActionBarAction.CopyLink,
|
||||
ActionBarAction.Remove,
|
||||
ActionBarAction.ViewSource,
|
||||
],
|
||||
presentation: "label",
|
||||
},
|
||||
};
|
||||
|
||||
export const DownloadingAttachment: Story = {
|
||||
args: {
|
||||
actions: [ActionBarAction.Download, ActionBarAction.Options],
|
||||
isDownloadLoading: true,
|
||||
isDownloadEncrypted: false,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Attachment download in progress. The download action is disabled and shows a spinner with the downloading label.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const DecryptingAttachment: Story = {
|
||||
args: {
|
||||
...DownloadingAttachment.args,
|
||||
isDownloadEncrypted: true,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Encrypted attachment state. Uses the same loading UI as download, but with the decrypting label.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const PinnedMessage: Story = {
|
||||
args: {
|
||||
actions: [ActionBarAction.React, ActionBarAction.Reply, ActionBarAction.Pin, ActionBarAction.Options],
|
||||
isPinned: true,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Pinned-state variant showing the unpin affordance instead of pin.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ExpandedReplyChain: Story = {
|
||||
args: {
|
||||
actions: [ActionBarAction.Reply, ActionBarAction.Expand, ActionBarAction.Options],
|
||||
isQuoteExpanded: true,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Reply-chain control in its expanded state, showing the collapse action and tooltip copy.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const DisabledThreadReply: Story = {
|
||||
args: {
|
||||
actions: [ActionBarAction.React, ActionBarAction.Reply, ActionBarAction.ReplyInThread, ActionBarAction.Options],
|
||||
isThreadReplyAllowed: false,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Thread reply action present but disabled.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations 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 { composeStories } from "@storybook/react-vite";
|
||||
import { fireEvent, render, screen } from "@test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import * as stories from "./ActionBarView.stories.tsx";
|
||||
import {
|
||||
ActionBarAction,
|
||||
type ActionBarViewActions,
|
||||
ActionBarView,
|
||||
type ActionBarViewSnapshot,
|
||||
} from "./ActionBarView.tsx";
|
||||
import { MockViewModel } from "../../../../../core/viewmodel/MockViewModel.ts";
|
||||
|
||||
const composedStories = composeStories(stories);
|
||||
|
||||
const { DownloadingAttachment, DecryptingAttachment, PinnedMessage, ExpandedReplyChain, DisabledThreadReply } =
|
||||
composedStories;
|
||||
|
||||
describe("ActionBarView", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("story snapshots", () => {
|
||||
for (const [storyName, Story] of Object.entries(composedStories)) {
|
||||
it(`renders ${storyName}`, () => {
|
||||
const { container } = render(<Story />);
|
||||
|
||||
expect(screen.getByRole("toolbar")).toBeInTheDocument();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("renders retry and delete only when those are the resolved actions", () => {
|
||||
const vm = new MockViewModel<ActionBarViewSnapshot>({
|
||||
actions: [ActionBarAction.Resend, ActionBarAction.Cancel],
|
||||
presentation: "icon",
|
||||
isDownloadEncrypted: false,
|
||||
isDownloadLoading: false,
|
||||
isPinned: false,
|
||||
isQuoteExpanded: false,
|
||||
isThreadReplyAllowed: true,
|
||||
});
|
||||
|
||||
render(<ActionBarView vm={vm} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /delete/i })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /options/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /reply/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a loading download action with the downloading label", () => {
|
||||
render(<DownloadingAttachment />);
|
||||
|
||||
const button = screen.getByRole("button", { name: /downloading/i });
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
|
||||
it("renders a loading download action with the decrypting label for encrypted media", () => {
|
||||
render(<DecryptingAttachment />);
|
||||
|
||||
const button = screen.getByRole("button", { name: /decrypting/i });
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
|
||||
it("renders the pinned message state with an unpin action", () => {
|
||||
render(<PinnedMessage />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /unpin/i })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /^pin$/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the expanded reply chain state with a collapse action", () => {
|
||||
render(<ExpandedReplyChain />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /collapse/i })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /expand/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a disabled thread reply button when thread reply is not allowed", () => {
|
||||
render(<DisabledThreadReply />);
|
||||
|
||||
const threadButton = screen.getByRole("button", { name: /reply in thread/i });
|
||||
expect(threadButton).toHaveAttribute("aria-disabled", "true");
|
||||
});
|
||||
|
||||
it("renders thread-list actions in icon mode", () => {
|
||||
const vm = new MockViewModel<ActionBarViewSnapshot>({
|
||||
actions: [ActionBarAction.ViewInRoom, ActionBarAction.CopyLink],
|
||||
presentation: "icon",
|
||||
isDownloadEncrypted: false,
|
||||
isDownloadLoading: false,
|
||||
isPinned: false,
|
||||
isQuoteExpanded: false,
|
||||
isThreadReplyAllowed: true,
|
||||
});
|
||||
|
||||
render(<ActionBarView vm={vm} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /view in room/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /copy link/i })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /options/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders edit-history actions in label mode", () => {
|
||||
const vm = new MockViewModel<ActionBarViewSnapshot>({
|
||||
actions: [ActionBarAction.Remove, ActionBarAction.ViewSource],
|
||||
presentation: "label",
|
||||
isDownloadEncrypted: false,
|
||||
isDownloadLoading: false,
|
||||
isPinned: false,
|
||||
isQuoteExpanded: false,
|
||||
isThreadReplyAllowed: true,
|
||||
});
|
||||
|
||||
render(<ActionBarView vm={vm} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /remove/i })).toHaveTextContent(/remove/i);
|
||||
expect(screen.getByRole("button", { name: /view source/i })).toHaveTextContent(/view source/i);
|
||||
});
|
||||
|
||||
it("renders only the options button in the minimal state", () => {
|
||||
const vm = new MockViewModel<ActionBarViewSnapshot>({
|
||||
actions: [ActionBarAction.Options],
|
||||
presentation: "icon",
|
||||
isDownloadEncrypted: false,
|
||||
isDownloadLoading: false,
|
||||
isPinned: false,
|
||||
isQuoteExpanded: false,
|
||||
isThreadReplyAllowed: true,
|
||||
});
|
||||
|
||||
render(<ActionBarView vm={vm} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /options/i })).toBeInTheDocument();
|
||||
expect(screen.getAllByRole("button")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("uses roving tab index and arrow keys within the toolbar", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const minimalVm = new MockViewModel<ActionBarViewSnapshot>({
|
||||
actions: [ActionBarAction.Options],
|
||||
presentation: "icon",
|
||||
isDownloadEncrypted: false,
|
||||
isDownloadLoading: false,
|
||||
isPinned: false,
|
||||
isQuoteExpanded: false,
|
||||
isThreadReplyAllowed: true,
|
||||
});
|
||||
|
||||
const pinnedVm = new MockViewModel<ActionBarViewSnapshot>({
|
||||
actions: [ActionBarAction.React, ActionBarAction.Reply, ActionBarAction.Pin, ActionBarAction.Options],
|
||||
presentation: "icon",
|
||||
isDownloadEncrypted: false,
|
||||
isDownloadLoading: false,
|
||||
isPinned: true,
|
||||
isQuoteExpanded: false,
|
||||
isThreadReplyAllowed: true,
|
||||
});
|
||||
|
||||
const { rerender } = render(<ActionBarView vm={minimalVm} />);
|
||||
|
||||
const optionsButton = screen.getByRole("button", { name: /options/i });
|
||||
expect(optionsButton).toHaveAttribute("tabindex", "0");
|
||||
|
||||
rerender(<ActionBarView vm={pinnedVm} />);
|
||||
|
||||
const reactButton = screen.getByRole("button", { name: /react/i });
|
||||
const replyButton = screen.getByRole("button", { name: /^reply$/i });
|
||||
const unpinButton = screen.getByRole("button", { name: /unpin/i });
|
||||
const optionsButtonInToolbar = screen.getByRole("button", { name: /options/i });
|
||||
|
||||
expect(reactButton).toHaveAttribute("tabindex", "0");
|
||||
expect(replyButton).toHaveAttribute("tabindex", "-1");
|
||||
expect(unpinButton).toHaveAttribute("tabindex", "-1");
|
||||
expect(optionsButtonInToolbar).toHaveAttribute("tabindex", "-1");
|
||||
|
||||
await user.tab();
|
||||
expect(reactButton).toHaveFocus();
|
||||
|
||||
await user.keyboard("{ArrowRight}");
|
||||
expect(replyButton).toHaveFocus();
|
||||
expect(replyButton).toHaveAttribute("tabindex", "0");
|
||||
expect(reactButton).toHaveAttribute("tabindex", "-1");
|
||||
|
||||
await user.keyboard("{End}");
|
||||
expect(optionsButtonInToolbar).toHaveFocus();
|
||||
expect(optionsButtonInToolbar).toHaveAttribute("tabindex", "0");
|
||||
|
||||
await user.keyboard("{ArrowLeft}");
|
||||
expect(unpinButton).toHaveFocus();
|
||||
expect(unpinButton).toHaveAttribute("tabindex", "0");
|
||||
});
|
||||
|
||||
it("uses roving tab index and arrow keys within a label toolbar", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const vm = new MockViewModel<ActionBarViewSnapshot>({
|
||||
actions: [ActionBarAction.Remove, ActionBarAction.ViewSource],
|
||||
presentation: "label",
|
||||
isDownloadEncrypted: false,
|
||||
isDownloadLoading: false,
|
||||
isPinned: false,
|
||||
isQuoteExpanded: false,
|
||||
isThreadReplyAllowed: true,
|
||||
});
|
||||
|
||||
render(<ActionBarView vm={vm} />);
|
||||
|
||||
const removeButton = screen.getByRole("button", { name: /remove/i });
|
||||
const viewSourceButton = screen.getByRole("button", { name: /view source/i });
|
||||
|
||||
expect(removeButton).toHaveAttribute("tabindex", "0");
|
||||
expect(viewSourceButton).toHaveAttribute("tabindex", "-1");
|
||||
|
||||
await user.tab();
|
||||
expect(removeButton).toHaveFocus();
|
||||
|
||||
await user.keyboard("{ArrowRight}");
|
||||
expect(viewSourceButton).toHaveFocus();
|
||||
expect(viewSourceButton).toHaveAttribute("tabindex", "0");
|
||||
});
|
||||
|
||||
it("applies a custom class name to the toolbar", () => {
|
||||
const vm = new MockViewModel<ActionBarViewSnapshot>({
|
||||
actions: [ActionBarAction.Options],
|
||||
presentation: "icon",
|
||||
isDownloadEncrypted: false,
|
||||
isDownloadLoading: false,
|
||||
isPinned: false,
|
||||
isQuoteExpanded: false,
|
||||
isThreadReplyAllowed: true,
|
||||
});
|
||||
|
||||
render(<ActionBarView vm={vm} className="extra_class_1 extra_class_2" />);
|
||||
|
||||
expect(screen.getByRole("toolbar")).toHaveClass("extra_class_1", "extra_class_2");
|
||||
});
|
||||
|
||||
it("forwards click and context-menu actions with the triggering button as anchor", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onReplyClick = vi.fn();
|
||||
const onOptionsClick = vi.fn();
|
||||
|
||||
class ActionBarViewModel extends MockViewModel<ActionBarViewSnapshot> implements ActionBarViewActions {
|
||||
public onReplyClick = onReplyClick;
|
||||
public onOptionsClick = onOptionsClick;
|
||||
}
|
||||
|
||||
const vm = new ActionBarViewModel({
|
||||
actions: [ActionBarAction.Reply, ActionBarAction.Options],
|
||||
presentation: "icon",
|
||||
isDownloadEncrypted: false,
|
||||
isDownloadLoading: false,
|
||||
isPinned: false,
|
||||
isQuoteExpanded: false,
|
||||
isThreadReplyAllowed: true,
|
||||
});
|
||||
|
||||
render(<ActionBarView vm={vm} />);
|
||||
|
||||
const replyButton = screen.getByRole("button", { name: /^reply$/i });
|
||||
const optionsButton = screen.getByRole("button", { name: /options/i });
|
||||
|
||||
await user.click(replyButton);
|
||||
expect(onReplyClick).toHaveBeenCalledWith(replyButton);
|
||||
|
||||
fireEvent.contextMenu(optionsButton);
|
||||
expect(onOptionsClick).toHaveBeenCalledWith(optionsButton);
|
||||
});
|
||||
|
||||
it("forwards label-mode actions with the triggering button as anchor", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onRemoveClick = vi.fn();
|
||||
const onViewSourceClick = vi.fn();
|
||||
|
||||
class ActionBarViewModel extends MockViewModel<ActionBarViewSnapshot> implements ActionBarViewActions {
|
||||
public onRemoveClick = onRemoveClick;
|
||||
public onViewSourceClick = onViewSourceClick;
|
||||
}
|
||||
|
||||
const vm = new ActionBarViewModel({
|
||||
actions: [ActionBarAction.Remove, ActionBarAction.ViewSource],
|
||||
presentation: "label",
|
||||
isDownloadEncrypted: false,
|
||||
isDownloadLoading: false,
|
||||
isPinned: false,
|
||||
isQuoteExpanded: false,
|
||||
isThreadReplyAllowed: true,
|
||||
});
|
||||
|
||||
render(<ActionBarView vm={vm} />);
|
||||
|
||||
const removeButton = screen.getByRole("button", { name: /remove/i });
|
||||
const viewSourceButton = screen.getByRole("button", { name: /view source/i });
|
||||
|
||||
await user.click(removeButton);
|
||||
expect(onRemoveClick).toHaveBeenCalledWith(removeButton);
|
||||
|
||||
fireEvent.contextMenu(viewSourceButton);
|
||||
expect(onViewSourceClick).toHaveBeenCalledWith(viewSourceButton);
|
||||
});
|
||||
});
|
||||
+460
@@ -0,0 +1,460 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations 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, useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
CollapseIcon,
|
||||
DeleteIcon,
|
||||
EditIcon,
|
||||
ExpandIcon,
|
||||
InlineCodeIcon,
|
||||
LinkIcon,
|
||||
PinIcon,
|
||||
ReplyIcon,
|
||||
RestartIcon,
|
||||
UnpinIcon,
|
||||
ThreadsIcon,
|
||||
VisibilityOnIcon,
|
||||
VisibilityOffIcon,
|
||||
DownloadIcon,
|
||||
OverflowHorizontalIcon,
|
||||
ReactionAddIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { InlineSpinner } from "@vector-im/compound-web";
|
||||
|
||||
import { useI18n } from "../../../../../core/i18n/i18nContext";
|
||||
import { Flex } from "../../../../../core/utils/Flex";
|
||||
import { type ViewModel, useViewModel } from "../../../../../core/viewmodel";
|
||||
import { ActionBarButton } from "./ActionBarButton";
|
||||
import styles from "./ActionBarView.module.css";
|
||||
|
||||
/**
|
||||
* Snapshot state for the message action toolbar.
|
||||
*
|
||||
* The snapshot carries the resolved actions to render plus the small amount of
|
||||
* per-action state the view needs for labels, icons, and disabled state.
|
||||
*/
|
||||
export interface ActionBarViewSnapshot {
|
||||
/** Explicitly resolved actions to render, in order. */
|
||||
actions: ActionBarAction[];
|
||||
/** Whether actions should render as icon buttons or label buttons. */
|
||||
presentation?: "icon" | "label";
|
||||
/** Whether an in-progress download should be presented as decrypting rather than downloading. */
|
||||
isDownloadEncrypted: boolean;
|
||||
/** Whether a media download or decryption is currently in progress. */
|
||||
isDownloadLoading: boolean;
|
||||
/** Whether the message is currently pinned. */
|
||||
isPinned: boolean;
|
||||
/** Whether the reply chain is currently expanded. */
|
||||
isQuoteExpanded: boolean;
|
||||
/** Whether starting or replying in a thread is allowed for this event. */
|
||||
isThreadReplyAllowed: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handlers for toolbar actions.
|
||||
*
|
||||
* Each callback receives the triggering button so menus can be positioned from
|
||||
* the action anchor when needed.
|
||||
*/
|
||||
export interface ActionBarViewActions {
|
||||
onCancelClick?: (anchor: HTMLElement | null) => void;
|
||||
onCopyLinkClick?: (anchor: HTMLElement | null) => void;
|
||||
onDownloadClick?: (anchor: HTMLElement | null) => void;
|
||||
onEditClick?: (anchor: HTMLElement | null) => void;
|
||||
onHideClick?: (anchor: HTMLElement | null) => void;
|
||||
onOptionsClick?: (anchor: HTMLElement | null) => void;
|
||||
onPinClick?: (anchor: HTMLElement | null) => void;
|
||||
onReactionsClick?: (anchor: HTMLElement | null) => void;
|
||||
onRemoveClick?: (anchor: HTMLElement | null) => void;
|
||||
onReplyClick?: (anchor: HTMLElement | null) => void;
|
||||
onReplyInThreadClick?: (anchor: HTMLElement | null) => void;
|
||||
onResendClick?: (anchor: HTMLElement | null) => void;
|
||||
onToggleThreadExpanded?: (anchor: HTMLElement | null) => void;
|
||||
onViewInRoomClick?: (anchor: HTMLElement | null) => void;
|
||||
onViewSourceClick?: (anchor: HTMLElement | null) => void;
|
||||
}
|
||||
|
||||
export type ActionBarViewModel = ViewModel<ActionBarViewSnapshot, ActionBarViewActions>;
|
||||
|
||||
/**
|
||||
* Resolved actions that `ActionBarView` can render.
|
||||
*
|
||||
* The order of actions in `ActionBarViewSnapshot.actions` defines the visual
|
||||
* order of buttons in the toolbar.
|
||||
*/
|
||||
export enum ActionBarAction {
|
||||
Cancel = "cancel",
|
||||
CopyLink = "copyLink",
|
||||
Download = "download",
|
||||
Edit = "edit",
|
||||
Expand = "expand",
|
||||
Hide = "hide",
|
||||
Options = "options",
|
||||
Pin = "pin",
|
||||
React = "react",
|
||||
Remove = "remove",
|
||||
Reply = "reply",
|
||||
ReplyInThread = "replyInThread",
|
||||
Resend = "resend",
|
||||
ViewInRoom = "viewInRoom",
|
||||
ViewSource = "viewSource",
|
||||
}
|
||||
|
||||
interface ToolbarButtonMeta {
|
||||
action: ActionBarAction;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface ActionBarViewProps {
|
||||
/** The view model for the component. */
|
||||
vm: ActionBarViewModel;
|
||||
/** Optional CSS class names to apply to the component container.*/
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact toolbar for message-level actions such as reply, react, edit,
|
||||
* download, and overflow options.
|
||||
*
|
||||
* Use `className` for host-level container styling, following standard React patterns.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <ActionBarView vm={actionBarVm} className="mx_MessageActionBar" />
|
||||
* ```
|
||||
*/
|
||||
export function ActionBarView({ vm, className }: Readonly<ActionBarViewProps>): JSX.Element | null {
|
||||
const { translate: _t } = useI18n();
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const {
|
||||
actions,
|
||||
presentation = "icon",
|
||||
isThreadReplyAllowed,
|
||||
isDownloadEncrypted,
|
||||
isDownloadLoading,
|
||||
isPinned,
|
||||
isQuoteExpanded,
|
||||
} = useViewModel(vm);
|
||||
|
||||
// Track the live button element for each action and keep the callback refs stable
|
||||
// so React does not detach and reattach them on every render.
|
||||
const actionButtonRefs = useRef<Partial<Record<ActionBarAction, HTMLButtonElement | null>>>({});
|
||||
const actionButtonRefSetters = useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
Object.values(ActionBarAction).map((action) => [
|
||||
action,
|
||||
(element: HTMLButtonElement | null) => {
|
||||
actionButtonRefs.current[action] = element;
|
||||
},
|
||||
]),
|
||||
) as Record<ActionBarAction, React.RefCallback<HTMLButtonElement>>,
|
||||
[],
|
||||
);
|
||||
|
||||
const actionButtons: Partial<Record<ActionBarAction, JSX.Element>> = {};
|
||||
|
||||
actionButtons[ActionBarAction.Edit] = (
|
||||
<ActionBarButton
|
||||
key={ActionBarAction.Edit}
|
||||
presentation={presentation}
|
||||
buttonRef={actionButtonRefSetters[ActionBarAction.Edit]}
|
||||
label={_t("action|edit")}
|
||||
onActivate={vm.onEditClick}
|
||||
icon={EditIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
const pinDescription = isPinned ? _t("action|unpin") : _t("action|pin");
|
||||
actionButtons[ActionBarAction.Pin] = (
|
||||
<ActionBarButton
|
||||
key={ActionBarAction.Pin}
|
||||
presentation={presentation}
|
||||
buttonRef={actionButtonRefSetters[ActionBarAction.Pin]}
|
||||
label={pinDescription}
|
||||
onActivate={vm.onPinClick}
|
||||
icon={isPinned ? UnpinIcon : PinIcon}
|
||||
ariaPressed={isPinned}
|
||||
/>
|
||||
);
|
||||
|
||||
actionButtons[ActionBarAction.Cancel] = (
|
||||
<ActionBarButton
|
||||
key={ActionBarAction.Cancel}
|
||||
presentation={presentation}
|
||||
buttonRef={actionButtonRefSetters[ActionBarAction.Cancel]}
|
||||
label={_t("action|delete")}
|
||||
onActivate={vm.onCancelClick}
|
||||
icon={DeleteIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
actionButtons[ActionBarAction.CopyLink] = (
|
||||
<ActionBarButton
|
||||
key={ActionBarAction.CopyLink}
|
||||
presentation={presentation}
|
||||
buttonRef={actionButtonRefSetters[ActionBarAction.CopyLink]}
|
||||
label={_t("timeline|mab|copy_link_thread")}
|
||||
onActivate={vm.onCopyLinkClick}
|
||||
icon={LinkIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
actionButtons[ActionBarAction.Reply] = (
|
||||
<ActionBarButton
|
||||
key={ActionBarAction.Reply}
|
||||
presentation={presentation}
|
||||
buttonRef={actionButtonRefSetters[ActionBarAction.Reply]}
|
||||
label={_t("action|reply")}
|
||||
onActivate={vm.onReplyClick}
|
||||
icon={ReplyIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
actionButtons[ActionBarAction.React] = (
|
||||
<ActionBarButton
|
||||
key={ActionBarAction.React}
|
||||
presentation={presentation}
|
||||
buttonRef={actionButtonRefSetters[ActionBarAction.React]}
|
||||
label={_t("action|react")}
|
||||
onActivate={vm.onReactionsClick}
|
||||
icon={ReactionAddIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
let downloadTitle = _t("action|download");
|
||||
if (isDownloadLoading) {
|
||||
downloadTitle = isDownloadEncrypted
|
||||
? _t("timeline|download_action_decrypting")
|
||||
: _t("timeline|download_action_downloading");
|
||||
}
|
||||
actionButtons[ActionBarAction.Download] = (
|
||||
<ActionBarButton
|
||||
key={ActionBarAction.Download}
|
||||
presentation={presentation}
|
||||
buttonRef={actionButtonRefSetters[ActionBarAction.Download]}
|
||||
label={downloadTitle}
|
||||
onActivate={vm.onDownloadClick}
|
||||
icon={isDownloadLoading ? InlineSpinner : DownloadIcon}
|
||||
disabled={isDownloadLoading}
|
||||
/>
|
||||
);
|
||||
|
||||
actionButtons[ActionBarAction.Hide] = (
|
||||
<ActionBarButton
|
||||
key={ActionBarAction.Hide}
|
||||
presentation={presentation}
|
||||
buttonRef={actionButtonRefSetters[ActionBarAction.Hide]}
|
||||
label={_t("action|hide")}
|
||||
onActivate={vm.onHideClick}
|
||||
icon={VisibilityOffIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
const threadTooltipDescription = isThreadReplyAllowed
|
||||
? _t("action|reply_in_thread")
|
||||
: _t("threads|error_start_thread_existing_relation");
|
||||
actionButtons[ActionBarAction.ReplyInThread] = (
|
||||
<ActionBarButton
|
||||
key={ActionBarAction.ReplyInThread}
|
||||
presentation={presentation}
|
||||
buttonRef={actionButtonRefSetters[ActionBarAction.ReplyInThread]}
|
||||
label={_t("action|reply_in_thread")}
|
||||
tooltipDescription={threadTooltipDescription}
|
||||
onActivate={vm.onReplyInThreadClick}
|
||||
icon={ThreadsIcon}
|
||||
disabled={!isThreadReplyAllowed}
|
||||
/>
|
||||
);
|
||||
|
||||
actionButtons[ActionBarAction.Resend] = (
|
||||
<ActionBarButton
|
||||
key={ActionBarAction.Resend}
|
||||
presentation={presentation}
|
||||
buttonRef={actionButtonRefSetters[ActionBarAction.Resend]}
|
||||
label={_t("action|retry")}
|
||||
onActivate={vm.onResendClick}
|
||||
icon={RestartIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
const expandDescription = isQuoteExpanded
|
||||
? _t("timeline|mab|collapse_reply_chain")
|
||||
: _t("timeline|mab|expand_reply_chain");
|
||||
actionButtons[ActionBarAction.Expand] = (
|
||||
<ActionBarButton
|
||||
key={ActionBarAction.Expand}
|
||||
presentation={presentation}
|
||||
buttonRef={actionButtonRefSetters[ActionBarAction.Expand]}
|
||||
label={expandDescription}
|
||||
tooltipCaption={`${_t("keyboard|shift")} + ${_t("action|click")}`}
|
||||
onActivate={vm.onToggleThreadExpanded}
|
||||
icon={isQuoteExpanded ? CollapseIcon : ExpandIcon}
|
||||
ariaExpanded={isQuoteExpanded}
|
||||
/>
|
||||
);
|
||||
|
||||
actionButtons[ActionBarAction.Options] = (
|
||||
<ActionBarButton
|
||||
key={ActionBarAction.Options}
|
||||
presentation={presentation}
|
||||
buttonRef={actionButtonRefSetters[ActionBarAction.Options]}
|
||||
label={_t("common|options")}
|
||||
onActivate={vm.onOptionsClick}
|
||||
icon={OverflowHorizontalIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
actionButtons[ActionBarAction.Remove] = (
|
||||
<ActionBarButton
|
||||
key={ActionBarAction.Remove}
|
||||
presentation={presentation}
|
||||
buttonRef={actionButtonRefSetters[ActionBarAction.Remove]}
|
||||
label={_t("action|remove")}
|
||||
onActivate={vm.onRemoveClick}
|
||||
icon={DeleteIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
actionButtons[ActionBarAction.ViewInRoom] = (
|
||||
<ActionBarButton
|
||||
key={ActionBarAction.ViewInRoom}
|
||||
presentation={presentation}
|
||||
buttonRef={actionButtonRefSetters[ActionBarAction.ViewInRoom]}
|
||||
label={_t("timeline|mab|view_in_room")}
|
||||
onActivate={vm.onViewInRoomClick}
|
||||
icon={VisibilityOnIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
actionButtons[ActionBarAction.ViewSource] = (
|
||||
<ActionBarButton
|
||||
key={ActionBarAction.ViewSource}
|
||||
presentation={presentation}
|
||||
buttonRef={actionButtonRefSetters[ActionBarAction.ViewSource]}
|
||||
label={_t("action|view_source")}
|
||||
onActivate={vm.onViewSourceClick}
|
||||
icon={InlineCodeIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
const isActionDisabled = useCallback(
|
||||
(action: ActionBarAction): boolean => {
|
||||
switch (action) {
|
||||
case ActionBarAction.Download:
|
||||
return isDownloadLoading;
|
||||
case ActionBarAction.ReplyInThread:
|
||||
return !isThreadReplyAllowed;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[isDownloadLoading, isThreadReplyAllowed],
|
||||
);
|
||||
|
||||
const toolbarButtons = useMemo<ToolbarButtonMeta[]>(() => {
|
||||
return actions.map((action) => ({
|
||||
action,
|
||||
disabled: isActionDisabled(action),
|
||||
}));
|
||||
}, [actions, isActionDisabled]);
|
||||
|
||||
// Handle RovingIndex for toolbar
|
||||
const enabledIndices = toolbarButtons
|
||||
.map((item, index) => (item.disabled ? -1 : index))
|
||||
.filter((index) => index >= 0);
|
||||
const fallbackIndex = enabledIndices[0] ?? 0;
|
||||
const currentIndex =
|
||||
toolbarButtons[activeIndex] && !toolbarButtons[activeIndex].disabled ? activeIndex : fallbackIndex;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setActiveIndex(currentIndex);
|
||||
|
||||
toolbarButtons.forEach(({ action }, index) => {
|
||||
const button = actionButtonRefs.current[action] ?? null;
|
||||
if (button) {
|
||||
button.tabIndex = index === currentIndex ? 0 : -1;
|
||||
}
|
||||
});
|
||||
}, [currentIndex, toolbarButtons]);
|
||||
|
||||
const focusButtonAtIndex = (index: number): void => {
|
||||
const action = toolbarButtons[index]?.action;
|
||||
const button = action ? (actionButtonRefs.current[action] ?? null) : null;
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveIndex(index);
|
||||
button.focus();
|
||||
};
|
||||
|
||||
const handleToolbarKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
if (enabledIndices.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusedIndex = toolbarButtons.findIndex(
|
||||
({ action }) => actionButtonRefs.current[action] === document.activeElement,
|
||||
);
|
||||
const startIndex = focusedIndex >= 0 ? focusedIndex : currentIndex;
|
||||
|
||||
switch (event.key) {
|
||||
case "ArrowRight": {
|
||||
event.preventDefault();
|
||||
const nextIndex = enabledIndices.find((index) => index > startIndex) ?? enabledIndices[0];
|
||||
focusButtonAtIndex(nextIndex);
|
||||
break;
|
||||
}
|
||||
case "ArrowLeft": {
|
||||
event.preventDefault();
|
||||
const previousIndex = [...enabledIndices].reverse().find((index) => index < startIndex);
|
||||
focusButtonAtIndex(previousIndex ?? enabledIndices[enabledIndices.length - 1]);
|
||||
break;
|
||||
}
|
||||
case "Home":
|
||||
event.preventDefault();
|
||||
focusButtonAtIndex(enabledIndices[0]);
|
||||
break;
|
||||
case "End":
|
||||
event.preventDefault();
|
||||
focusButtonAtIndex(enabledIndices[enabledIndices.length - 1]);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleToolbarFocusCapture = (): void => {
|
||||
const focusedIndex = toolbarButtons.findIndex(
|
||||
({ action }) => actionButtonRefs.current[action] === document.activeElement,
|
||||
);
|
||||
if (focusedIndex >= 0 && focusedIndex !== activeIndex) {
|
||||
setActiveIndex(focusedIndex);
|
||||
}
|
||||
};
|
||||
|
||||
if (toolbarButtons.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive.
|
||||
return (
|
||||
<Flex
|
||||
display="inline-flex"
|
||||
direction="row"
|
||||
role="toolbar"
|
||||
aria-label={_t("timeline|mab|label")}
|
||||
aria-live="off"
|
||||
onKeyDown={handleToolbarKeyDown}
|
||||
onFocusCapture={handleToolbarFocusCapture}
|
||||
className={classNames(className, styles.toolbar)}
|
||||
>
|
||||
{toolbarButtons.map((meta) => actionButtons[meta.action])}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
+962
@@ -0,0 +1,962 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ActionBarView > story snapshots > renders AllIconActions 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="Message Actions"
|
||||
aria-live="off"
|
||||
class="flex mx_MessageActionBar toolbar"
|
||||
role="toolbar"
|
||||
style="--mx-flex-display: inline-flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-label="Hide"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
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="m16.1 13.3-1.45-1.45q.225-1.175-.675-2.2t-2.325-.8L10.2 7.4q.424-.2.863-.3A4.2 4.2 0 0 1 12 7q1.875 0 3.188 1.312Q16.5 9.625 16.5 11.5q0 .5-.1.938t-.3.862m3.2 3.15-1.45-1.4a11 11 0 0 0 1.688-1.588A9 9 0 0 0 20.8 11.5q-1.25-2.524-3.588-4.013Q14.875 6 12 6q-.724 0-1.425.1a10 10 0 0 0-1.375.3L7.65 4.85A11.1 11.1 0 0 1 12 4q3.575 0 6.425 1.887T22.7 10.8a.8.8 0 0 1 .1.313q.025.188.025.387a2 2 0 0 1-.125.7 10.9 10.9 0 0 1-3.4 4.25m-.2 5.45-3.5-3.45q-.874.274-1.762.413Q12.95 19 12 19q-3.575 0-6.425-1.887T1.3 12.2a.8.8 0 0 1-.1-.312 3 3 0 0 1 0-.763.8.8 0 0 1 .1-.3Q1.825 9.7 2.55 8.75A13.3 13.3 0 0 1 4.15 7L2.075 4.9a.93.93 0 0 1-.275-.688q0-.412.3-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275l17 17q.275.275.288.688a.93.93 0 0 1-.288.712.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275M5.55 8.4q-.725.65-1.325 1.425A9 9 0 0 0 3.2 11.5q1.25 2.524 3.588 4.012T12 17q.5 0 .975-.062.475-.063.975-.138l-.9-.95q-.274.075-.525.113A3.5 3.5 0 0 1 12 16q-1.875 0-3.187-1.312Q7.5 13.375 7.5 11.5q0-.274.038-.525.037-.25.112-.525z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Download"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<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 15.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-3.6-3.6a.95.95 0 0 1-.275-.7q0-.425.275-.7.274-.275.712-.288t.713.263L11 12.15V5q0-.424.287-.713A.97.97 0 0 1 12 4q.424 0 .713.287Q13 4.576 13 5v7.15l1.875-1.875q.274-.274.713-.263.437.014.712.288a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-3.6 3.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063M6 20q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 18v-2q0-.424.287-.713A.97.97 0 0 1 5 15q.424 0 .713.287Q6 15.576 6 16v2h12v-2q0-.424.288-.713A.97.97 0 0 1 19 15q.424 0 .712.287.288.288.288.713v2q0 .824-.587 1.413A1.93 1.93 0 0 1 18 20z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="React"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14.74 2.38C13.87 2.133 12.95 2 12 2 6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10c0-.95-.133-1.87-.38-2.74a5 5 0 0 1-1.886.687 8 8 0 1 1-5.68-5.68c.1-.684.339-1.323.687-1.887"
|
||||
/>
|
||||
<path
|
||||
d="M15.536 14.121a1 1 0 0 1 0 1.415A5 5 0 0 1 12 17c-1.38 0-2.632-.56-3.535-1.464a1 1 0 1 1 1.414-1.415A3 3 0 0 0 12 15c.829 0 1.577-.335 2.121-.879a1 1 0 0 1 1.415 0M8.5 12a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3m8.5-1.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0M18 6h-1a.97.97 0 0 1-.712-.287A.97.97 0 0 1 16 5q0-.424.288-.713A.97.97 0 0 1 17 4h1V3q0-.424.288-.712A.97.97 0 0 1 19 2q.424 0 .712.288Q20 2.575 20 3v1h1q.424 0 .712.287Q22 4.576 22 5t-.288.713A.97.97 0 0 1 21 6h-1v1q0 .424-.288.713A.97.97 0 0 1 19 8a.97.97 0 0 1-.712-.287A.97.97 0 0 1 18 7z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Reply"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.405 5.708c.39-.39.39-1.025 0-1.416a.996.996 0 0 0-1.412 0L3.294 9.006a1.004 1.004 0 0 0 0 1.416l4.699 4.714a.996.996 0 0 0 1.412 0c.39-.39.39-1.025 0-1.416l-3.043-3.053h9.153c1.887 0 3.485 1.604 3.485 3.666C19 16.396 17.402 18 15.515 18h-2.093a1 1 0 1 0 0 2h2.093C18.58 20 21 17.425 21 14.333s-2.419-5.666-5.485-5.666H6.456z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Reply in thread"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 10a.97.97 0 0 1-.713-.287A.97.97 0 0 1 6 9q0-.424.287-.713A.97.97 0 0 1 7 8h10q.424 0 .712.287Q18 8.576 18 9t-.288.713A.97.97 0 0 1 17 10zm0 4a.97.97 0 0 1-.713-.287A.97.97 0 0 1 6 13q0-.424.287-.713A.97.97 0 0 1 7 12h6q.424 0 .713.287.287.288.287.713 0 .424-.287.713A.97.97 0 0 1 13 14z"
|
||||
/>
|
||||
<path
|
||||
d="M3.707 21.293c-.63.63-1.707.184-1.707-.707V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6zM6 17h14V5H4v13.172l.586-.586A2 2 0 0 1 6 17"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Edit"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M15.706 2.637a2 2 0 0 1 2.829 0l2.828 2.828a2 2 0 0 1 0 2.829L9.605 20.052a1 1 0 0 1-.465.263L3.483 21.73a1 1 0 0 1-1.212-1.213l1.414-5.657a1 1 0 0 1 .263-.465zm1.224 7.262L14.102 7.07l-8.544 8.544-.943 3.771 3.771-.943z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Pin"
|
||||
aria-pressed="false"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M6.119 2a.5.5 0 0 0-.35.857L7.85 4.9a.5.5 0 0 1 .15.357v4.487a.5.5 0 0 1-.15.356l-3.7 3.644A.5.5 0 0 0 4 14.1v1.4a.5.5 0 0 0 .5.5H11v6a1 1 0 1 0 2 0v-6h6.5a.5.5 0 0 0 .5-.5v-1.4a.5.5 0 0 0-.15-.356l-3.7-3.644a.5.5 0 0 1-.15-.356V5.257a.5.5 0 0 1 .15-.357l2.081-2.043a.5.5 0 0 0-.35-.857zM10 4h4v5.744a2.5 2.5 0 0 0 .746 1.781L17.26 14H6.74l2.514-2.475A2.5 2.5 0 0 0 10 9.744z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Retry"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18.93 8A8 8 0 1 1 4 12a1 1 0 1 0-2 0c0 5.523 4.477 10 10 10s10-4.477 10-10a10 10 0 0 0-.832-4A10 10 0 0 0 12 2a9.99 9.99 0 0 0-8 3.999V4a1 1 0 0 0-2 0v4a1 1 0 0 0 1 1h4a1 1 0 0 0 0-2H5.755A7.99 7.99 0 0 1 12 4a8 8 0 0 1 6.93 4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Delete"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 21q-.824 0-1.412-.587A1.93 1.93 0 0 1 5 19V6a.97.97 0 0 1-.713-.287A.97.97 0 0 1 4 5q0-.424.287-.713A.97.97 0 0 1 5 4h4q0-.424.287-.712A.97.97 0 0 1 10 3h4q.424 0 .713.288Q15 3.575 15 4h4q.424 0 .712.287Q20 4.576 20 5t-.288.713A.97.97 0 0 1 19 6v13q0 .824-.587 1.413A1.93 1.93 0 0 1 17 21zM7 6v13h10V6zm2 10q0 .424.287.712Q9.576 17 10 17t.713-.288A.97.97 0 0 0 11 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 10 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 9 9zm4 0q0 .424.287.712.288.288.713.288.424 0 .713-.288A.97.97 0 0 0 15 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 14 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 13 9z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-label="Expand quotes"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21 3.997a1 1 0 0 0-.29-.702l-.005-.004A1 1 0 0 0 20 3h-8a1 1 0 1 0 0 2h5.586L5 17.586V12a1 1 0 1 0-2 0v8.003a1 1 0 0 0 .29.702l.005.004c.18.18.43.291.705.291h8a1 1 0 1 0 0-2H6.414L19 6.414V12a1 1 0 1 0 2 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Options"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="View in room"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<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 16q1.875 0 3.188-1.312Q16.5 13.375 16.5 11.5t-1.312-3.187T12 7 8.813 8.313 7.5 11.5t1.313 3.188T12 16m0-1.8q-1.125 0-1.912-.787A2.6 2.6 0 0 1 9.3 11.5q0-1.125.787-1.912A2.6 2.6 0 0 1 12 8.8q1.125 0 1.912.787.788.788.788 1.913t-.787 1.912A2.6 2.6 0 0 1 12 14.2m0 4.8q-3.475 0-6.35-1.837Q2.775 15.324 1.3 12.2a.8.8 0 0 1-.1-.312 3 3 0 0 1 0-.775.8.8 0 0 1 .1-.313q1.475-3.125 4.35-4.962Q8.525 4 12 4t6.35 1.838T22.7 10.8a.8.8 0 0 1 .1.313 3 3 0 0 1 0 .774.8.8 0 0 1-.1.313q-1.475 3.125-4.35 4.963Q15.475 19 12 19m0-2a9.54 9.54 0 0 0 5.188-1.488A9.77 9.77 0 0 0 20.8 11.5a9.77 9.77 0 0 0-3.613-4.012A9.54 9.54 0 0 0 12 6a9.55 9.55 0 0 0-5.187 1.487A9.77 9.77 0 0 0 3.2 11.5a9.77 9.77 0 0 0 3.613 4.012A9.54 9.54 0 0 0 12 17"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Copy link to thread"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<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 19.071q-1.467 1.467-3.536 1.467-2.067 0-3.535-1.467t-1.467-3.535q0-2.07 1.467-3.536L7.05 9.879q.3-.3.707-.3t.707.3.301.707-.3.707l-2.122 2.121a2.9 2.9 0 0 0-.884 2.122q0 1.237.884 2.12.884.885 2.121.885t2.122-.884l2.121-2.121q.3-.3.707-.3t.707.3.3.707q0 .405-.3.707zm-1.414-4.243q-.3.3-.707.301a.97.97 0 0 1-.707-.3q-.3-.3-.301-.708 0-.405.3-.707l4.243-4.242q.3-.3.707-.3t.707.3.3.707-.3.707zm6.364-.707q-.3.3-.707.3a.97.97 0 0 1-.707-.3q-.3-.3-.301-.707 0-.405.3-.707l2.122-2.121q.884-.885.884-2.121 0-1.238-.884-2.122a2.9 2.9 0 0 0-2.121-.884q-1.237 0-2.122.884l-2.121 2.122q-.3.3-.707.3a.97.97 0 0 1-.707-.3q-.3-.3-.3-.708 0-.405.3-.707L12 4.93q1.467-1.467 3.536-1.467t3.535 1.467 1.467 3.536T19.071 12z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Remove"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 21q-.824 0-1.412-.587A1.93 1.93 0 0 1 5 19V6a.97.97 0 0 1-.713-.287A.97.97 0 0 1 4 5q0-.424.287-.713A.97.97 0 0 1 5 4h4q0-.424.287-.712A.97.97 0 0 1 10 3h4q.424 0 .713.288Q15 3.575 15 4h4q.424 0 .712.287Q20 4.576 20 5t-.288.713A.97.97 0 0 1 19 6v13q0 .824-.587 1.413A1.93 1.93 0 0 1 17 21zM7 6v13h10V6zm2 10q0 .424.287.712Q9.576 17 10 17t.713-.288A.97.97 0 0 0 11 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 10 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 9 9zm4 0q0 .424.287.712.288.288.713.288.424 0 .713-.288A.97.97 0 0 0 15 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 14 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 13 9z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="View Source"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14.958 5.62a1 1 0 0 0-1.916-.574l-4 13.333a1 1 0 0 0 1.916.575zM5.974 7.232a1 1 0 0 0-1.409.128l-3.333 4a1 1 0 0 0 0 1.28l3.333 4a1 1 0 1 0 1.537-1.28L3.302 12l2.8-3.36a1 1 0 0 0-.128-1.408m12.053 0a1 1 0 0 1 1.408.128l3.333 4a1 1 0 0 1 0 1.28l-3.333 4a1 1 0 1 1-1.537-1.28l2.8-3.36-2.8-3.36a1 1 0 0 1 .128-1.408"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ActionBarView > story snapshots > renders AllLabelActions 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="Message Actions"
|
||||
aria-live="off"
|
||||
class="flex mx_MessageActionBar toolbar"
|
||||
role="toolbar"
|
||||
style="--mx-flex-display: inline-flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-label="Hide"
|
||||
class="_button_13vu4_8 toolbar_item"
|
||||
data-kind="tertiary"
|
||||
data-presentation="label"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Hide
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Download"
|
||||
class="_button_13vu4_8 toolbar_item"
|
||||
data-kind="tertiary"
|
||||
data-presentation="label"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
aria-label="React"
|
||||
class="_button_13vu4_8 toolbar_item"
|
||||
data-kind="tertiary"
|
||||
data-presentation="label"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
React
|
||||
</button>
|
||||
<button
|
||||
aria-label="Reply"
|
||||
class="_button_13vu4_8 toolbar_item"
|
||||
data-kind="tertiary"
|
||||
data-presentation="label"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
Reply
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Reply in thread"
|
||||
class="_button_13vu4_8 toolbar_item"
|
||||
data-kind="tertiary"
|
||||
data-presentation="label"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
Reply in thread
|
||||
</button>
|
||||
<button
|
||||
aria-label="Edit"
|
||||
class="_button_13vu4_8 toolbar_item"
|
||||
data-kind="tertiary"
|
||||
data-presentation="label"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
aria-label="Pin"
|
||||
aria-pressed="false"
|
||||
class="_button_13vu4_8 toolbar_item"
|
||||
data-kind="tertiary"
|
||||
data-presentation="label"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
Pin
|
||||
</button>
|
||||
<button
|
||||
aria-label="Retry"
|
||||
class="_button_13vu4_8 toolbar_item"
|
||||
data-kind="tertiary"
|
||||
data-presentation="label"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
<button
|
||||
aria-label="Delete"
|
||||
class="_button_13vu4_8 toolbar_item"
|
||||
data-kind="tertiary"
|
||||
data-presentation="label"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-label="Expand quotes"
|
||||
class="_button_13vu4_8 toolbar_item"
|
||||
data-kind="tertiary"
|
||||
data-presentation="label"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
Expand quotes
|
||||
</button>
|
||||
<button
|
||||
aria-label="Options"
|
||||
class="_button_13vu4_8 toolbar_item"
|
||||
data-kind="tertiary"
|
||||
data-presentation="label"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
Options
|
||||
</button>
|
||||
<button
|
||||
aria-label="View in room"
|
||||
class="_button_13vu4_8 toolbar_item"
|
||||
data-kind="tertiary"
|
||||
data-presentation="label"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
View in room
|
||||
</button>
|
||||
<button
|
||||
aria-label="Copy link to thread"
|
||||
class="_button_13vu4_8 toolbar_item"
|
||||
data-kind="tertiary"
|
||||
data-presentation="label"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
Copy link to thread
|
||||
</button>
|
||||
<button
|
||||
aria-label="Remove"
|
||||
class="_button_13vu4_8 toolbar_item"
|
||||
data-kind="tertiary"
|
||||
data-presentation="label"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
<button
|
||||
aria-label="View Source"
|
||||
class="_button_13vu4_8 toolbar_item"
|
||||
data-kind="tertiary"
|
||||
data-presentation="label"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
View Source
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ActionBarView > story snapshots > renders DecryptingAttachment 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="Message Actions"
|
||||
aria-live="off"
|
||||
class="flex mx_MessageActionBar toolbar"
|
||||
role="toolbar"
|
||||
style="--mx-flex-display: inline-flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
aria-label="Decrypting"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_icon_11k6c_18"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
style="width: 20px; height: 20px;"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Options"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
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="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ActionBarView > story snapshots > renders DisabledThreadReply 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="Message Actions"
|
||||
aria-live="off"
|
||||
class="flex mx_MessageActionBar toolbar"
|
||||
role="toolbar"
|
||||
style="--mx-flex-display: inline-flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-label="React"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
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="M14.74 2.38C13.87 2.133 12.95 2 12 2 6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10c0-.95-.133-1.87-.38-2.74a5 5 0 0 1-1.886.687 8 8 0 1 1-5.68-5.68c.1-.684.339-1.323.687-1.887"
|
||||
/>
|
||||
<path
|
||||
d="M15.536 14.121a1 1 0 0 1 0 1.415A5 5 0 0 1 12 17c-1.38 0-2.632-.56-3.535-1.464a1 1 0 1 1 1.414-1.415A3 3 0 0 0 12 15c.829 0 1.577-.335 2.121-.879a1 1 0 0 1 1.415 0M8.5 12a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3m8.5-1.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0M18 6h-1a.97.97 0 0 1-.712-.287A.97.97 0 0 1 16 5q0-.424.288-.713A.97.97 0 0 1 17 4h1V3q0-.424.288-.712A.97.97 0 0 1 19 2q.424 0 .712.288Q20 2.575 20 3v1h1q.424 0 .712.287Q22 4.576 22 5t-.288.713A.97.97 0 0 1 21 6h-1v1q0 .424-.288.713A.97.97 0 0 1 19 8a.97.97 0 0 1-.712-.287A.97.97 0 0 1 18 7z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Reply"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.405 5.708c.39-.39.39-1.025 0-1.416a.996.996 0 0 0-1.412 0L3.294 9.006a1.004 1.004 0 0 0 0 1.416l4.699 4.714a.996.996 0 0 0 1.412 0c.39-.39.39-1.025 0-1.416l-3.043-3.053h9.153c1.887 0 3.485 1.604 3.485 3.666C19 16.396 17.402 18 15.515 18h-2.093a1 1 0 1 0 0 2h2.093C18.58 20 21 17.425 21 14.333s-2.419-5.666-5.485-5.666H6.456z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
aria-label="Reply in thread"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 10a.97.97 0 0 1-.713-.287A.97.97 0 0 1 6 9q0-.424.287-.713A.97.97 0 0 1 7 8h10q.424 0 .712.287Q18 8.576 18 9t-.288.713A.97.97 0 0 1 17 10zm0 4a.97.97 0 0 1-.713-.287A.97.97 0 0 1 6 13q0-.424.287-.713A.97.97 0 0 1 7 12h6q.424 0 .713.287.287.288.287.713 0 .424-.287.713A.97.97 0 0 1 13 14z"
|
||||
/>
|
||||
<path
|
||||
d="M3.707 21.293c-.63.63-1.707.184-1.707-.707V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6zM6 17h14V5H4v13.172l.586-.586A2 2 0 0 1 6 17"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Options"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ActionBarView > story snapshots > renders DownloadingAttachment 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="Message Actions"
|
||||
aria-live="off"
|
||||
class="flex mx_MessageActionBar toolbar"
|
||||
role="toolbar"
|
||||
style="--mx-flex-display: inline-flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
aria-label="Downloading"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_icon_11k6c_18"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
style="width: 20px; height: 20px;"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Options"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
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="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ActionBarView > story snapshots > renders ExpandedReplyChain 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="Message Actions"
|
||||
aria-live="off"
|
||||
class="flex mx_MessageActionBar toolbar"
|
||||
role="toolbar"
|
||||
style="--mx-flex-display: inline-flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-label="Reply"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
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="M9.405 5.708c.39-.39.39-1.025 0-1.416a.996.996 0 0 0-1.412 0L3.294 9.006a1.004 1.004 0 0 0 0 1.416l4.699 4.714a.996.996 0 0 0 1.412 0c.39-.39.39-1.025 0-1.416l-3.043-3.053h9.153c1.887 0 3.485 1.604 3.485 3.666C19 16.396 17.402 18 15.515 18h-2.093a1 1 0 1 0 0 2h2.093C18.58 20 21 17.425 21 14.333s-2.419-5.666-5.485-5.666H6.456z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-expanded="true"
|
||||
aria-label="Collapse quotes"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<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 11.034a1 1 0 0 0 .29.702l.005.005c.18.18.43.29.705.29h8a1 1 0 0 0 0-2h-5.586L22 3.445a1 1 0 0 0-1.414-1.414L14 8.617V3.031a1 1 0 1 0-2 0zm0 1.963a1 1 0 0 0-.29-.702l-.005-.004A1 1 0 0 0 11 12H3a1 1 0 1 0 0 2h5.586L2 20.586A1 1 0 1 0 3.414 22L10 15.414V21a1 1 0 0 0 2 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Options"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ActionBarView > story snapshots > renders PinnedMessage 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="Message Actions"
|
||||
aria-live="off"
|
||||
class="flex mx_MessageActionBar toolbar"
|
||||
role="toolbar"
|
||||
style="--mx-flex-display: inline-flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-label="React"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
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="M14.74 2.38C13.87 2.133 12.95 2 12 2 6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10c0-.95-.133-1.87-.38-2.74a5 5 0 0 1-1.886.687 8 8 0 1 1-5.68-5.68c.1-.684.339-1.323.687-1.887"
|
||||
/>
|
||||
<path
|
||||
d="M15.536 14.121a1 1 0 0 1 0 1.415A5 5 0 0 1 12 17c-1.38 0-2.632-.56-3.535-1.464a1 1 0 1 1 1.414-1.415A3 3 0 0 0 12 15c.829 0 1.577-.335 2.121-.879a1 1 0 0 1 1.415 0M8.5 12a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3m8.5-1.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0M18 6h-1a.97.97 0 0 1-.712-.287A.97.97 0 0 1 16 5q0-.424.288-.713A.97.97 0 0 1 17 4h1V3q0-.424.288-.712A.97.97 0 0 1 19 2q.424 0 .712.288Q20 2.575 20 3v1h1q.424 0 .712.287Q22 4.576 22 5t-.288.713A.97.97 0 0 1 21 6h-1v1q0 .424-.288.713A.97.97 0 0 1 19 8a.97.97 0 0 1-.712-.287A.97.97 0 0 1 18 7z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Reply"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.405 5.708c.39-.39.39-1.025 0-1.416a.996.996 0 0 0-1.412 0L3.294 9.006a1.004 1.004 0 0 0 0 1.416l4.699 4.714a.996.996 0 0 0 1.412 0c.39-.39.39-1.025 0-1.416l-3.043-3.053h9.153c1.887 0 3.485 1.604 3.485 3.666C19 16.396 17.402 18 15.515 18h-2.093a1 1 0 1 0 0 2h2.093C18.58 20 21 17.425 21 14.333s-2.419-5.666-5.485-5.666H6.456z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Unpin"
|
||||
aria-pressed="true"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M5.457 2.083a1 1 0 0 0-1.414 1.414L8.04 7.494v2.25a.5.5 0 0 1-.15.356l-3.7 3.644a.5.5 0 0 0-.15.356v1.4a.5.5 0 0 0 .5.5h6.5v6a1 1 0 0 0 2 0v-6h3.506l4.497 4.497a1 1 0 0 0 1.414-1.414zM14.546 14 10.04 9.494v.25a2.5 2.5 0 0 1-.746 1.781L6.78 14z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M14.04 4v3.85l2.015 2.015a.5.5 0 0 1-.015-.12V5.257a.5.5 0 0 1 .15-.357l2.081-2.043a.5.5 0 0 0-.35-.857h-9.73l2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Options"
|
||||
class="_button_13vu4_8 toolbar_item _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="tertiary"
|
||||
data-presentation="icon"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations 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 {
|
||||
ActionBarView,
|
||||
type ActionBarViewActions,
|
||||
type ActionBarViewModel,
|
||||
type ActionBarViewSnapshot,
|
||||
ActionBarAction,
|
||||
} from "./ActionBarView";
|
||||
Reference in New Issue
Block a user