* Extract EventTile interaction state transitions * Fix sonar issues
This commit is contained in:
@@ -105,6 +105,18 @@ import { E2eMessageSharedIcon } from "./EventTile/E2eMessageSharedIcon.tsx";
|
|||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { CardContext } from "../right_panel/context";
|
import { CardContext } from "../right_panel/context";
|
||||||
import { EventTileViewModel } from "../../../viewmodels/room/timeline/event-tile/EventTileViewModel";
|
import { EventTileViewModel } from "../../../viewmodels/room/timeline/event-tile/EventTileViewModel";
|
||||||
|
import {
|
||||||
|
eventTileActionBarFocusChange,
|
||||||
|
eventTileBlurWithin,
|
||||||
|
eventTileClearHover,
|
||||||
|
eventTileCloseContextMenu,
|
||||||
|
eventTileFocusWithin,
|
||||||
|
eventTileMouseEnter,
|
||||||
|
eventTileMouseLeave,
|
||||||
|
eventTileOpenContextMenu,
|
||||||
|
initialEventTileInteractionState,
|
||||||
|
type EventTileInteractionState,
|
||||||
|
} from "../../../viewmodels/room/timeline/event-tile/EventTileInteractionState";
|
||||||
import {
|
import {
|
||||||
MessageTimestampViewModel,
|
MessageTimestampViewModel,
|
||||||
type MessageTimestampViewModelProps,
|
type MessageTimestampViewModelProps,
|
||||||
@@ -270,9 +282,7 @@ export interface EventTileProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
// Whether the action bar is focused.
|
interaction: EventTileInteractionState;
|
||||||
actionBarFocused: boolean;
|
|
||||||
showActionBarFromFocus: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* E2EE shield we should show for decryption problems.
|
* E2EE shield we should show for decryption problems.
|
||||||
@@ -289,15 +299,6 @@ interface IState {
|
|||||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||||
reactions?: Relations | null | undefined;
|
reactions?: Relations | null | undefined;
|
||||||
|
|
||||||
hover: boolean;
|
|
||||||
focusWithin: boolean;
|
|
||||||
|
|
||||||
// Position of the context menu
|
|
||||||
contextMenu?: {
|
|
||||||
position: Pick<DOMRect, "top" | "left" | "bottom">;
|
|
||||||
link?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
isQuoteExpanded?: boolean;
|
isQuoteExpanded?: boolean;
|
||||||
|
|
||||||
thread: Thread | null;
|
thread: Thread | null;
|
||||||
@@ -346,9 +347,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
const thread = this.thread;
|
const thread = this.thread;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
// Whether the action bar is focused.
|
interaction: initialEventTileInteractionState,
|
||||||
actionBarFocused: false,
|
|
||||||
showActionBarFromFocus: false,
|
|
||||||
|
|
||||||
shieldColour: EventShieldColour.NONE,
|
shieldColour: EventShieldColour.NONE,
|
||||||
shieldReason: null,
|
shieldReason: null,
|
||||||
@@ -356,9 +355,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||||
reactions: this.getReactions(),
|
reactions: this.getReactions(),
|
||||||
|
|
||||||
hover: false,
|
|
||||||
focusWithin: false,
|
|
||||||
|
|
||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -495,9 +491,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
public componentDidUpdate(prevProps: Readonly<EventTileProps>, prevState: Readonly<IState>): void {
|
public componentDidUpdate(prevProps: Readonly<EventTileProps>, prevState: Readonly<IState>): void {
|
||||||
// Some overlays, such as portalled tooltips, can interrupt the normal mouseleave path.
|
// Some overlays, such as portalled tooltips, can interrupt the normal mouseleave path.
|
||||||
// While hover is active, verify it against the browser's real :hover state on mouse movement.
|
// While hover is active, verify it against the browser's real :hover state on mouse movement.
|
||||||
if (!prevState.hover && this.state.hover) {
|
if (!prevState.interaction.hover && this.state.interaction.hover) {
|
||||||
this.startStaleHoverCheck();
|
this.startStaleHoverCheck();
|
||||||
} else if (prevState.hover && !this.state.hover) {
|
} else if (prevState.interaction.hover && !this.state.interaction.hover) {
|
||||||
this.stopStaleHoverCheck();
|
this.stopStaleHoverCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,12 +512,14 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
// Moving between edited messages can remount the editor without a reliable blur event.
|
// Moving between edited messages can remount the editor without a reliable blur event.
|
||||||
// Clear stale focus-derived action bar state when focus has actually left this tile.
|
// Clear stale focus-derived action bar state when focus has actually left this tile.
|
||||||
if (
|
if (
|
||||||
this.state.focusWithin &&
|
this.state.interaction.focusWithin &&
|
||||||
this.ref.current &&
|
this.ref.current &&
|
||||||
document.activeElement instanceof HTMLElement &&
|
document.activeElement instanceof HTMLElement &&
|
||||||
!this.ref.current.contains(document.activeElement)
|
!this.ref.current.contains(document.activeElement)
|
||||||
) {
|
) {
|
||||||
this.setState({ focusWithin: false, showActionBarFromFocus: false });
|
this.setState((prevState) => ({
|
||||||
|
interaction: eventTileBlurWithin(prevState.interaction),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -902,8 +900,11 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
|
|
||||||
private readonly onActionBarFocusChange = (actionBarFocused: boolean): void => {
|
private readonly onActionBarFocusChange = (actionBarFocused: boolean): void => {
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
actionBarFocused,
|
interaction: eventTileActionBarFocusChange(
|
||||||
hover: actionBarFocused ? prevState.hover : (this.ref.current?.matches(":hover") ?? false),
|
prevState.interaction,
|
||||||
|
actionBarFocused,
|
||||||
|
this.ref.current?.matches(":hover") ?? false,
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -920,17 +921,23 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
}
|
}
|
||||||
|
|
||||||
private readonly onDocumentMouseMove = (): void => {
|
private readonly onDocumentMouseMove = (): void => {
|
||||||
if (this.state.hover && !(this.ref.current?.matches(":hover") ?? false)) {
|
if (this.state.interaction.hover && !(this.ref.current?.matches(":hover") ?? false)) {
|
||||||
this.setState({ hover: false });
|
this.setState((prevState) => ({
|
||||||
|
interaction: eventTileClearHover(prevState.interaction),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly onMouseEnter = (): void => {
|
private readonly onMouseEnter = (): void => {
|
||||||
this.setState({ hover: true });
|
this.setState((prevState) => ({
|
||||||
|
interaction: eventTileMouseEnter(prevState.interaction),
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly onMouseLeave = (): void => {
|
private readonly onMouseLeave = (): void => {
|
||||||
this.setState({ hover: false });
|
this.setState((prevState) => ({
|
||||||
|
interaction: eventTileMouseLeave(prevState.interaction),
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly onFocusWithin = (event: FocusEvent<HTMLElement>): void => {
|
private readonly onFocusWithin = (event: FocusEvent<HTMLElement>): void => {
|
||||||
@@ -938,7 +945,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
const showActionBarFromFocus =
|
const showActionBarFromFocus =
|
||||||
target.matches(":focus-visible") || document.body.dataset["data-whatinput"] === "keyboard";
|
target.matches(":focus-visible") || document.body.dataset["data-whatinput"] === "keyboard";
|
||||||
this.setState({ focusWithin: true, showActionBarFromFocus });
|
this.setState((prevState) => ({
|
||||||
|
interaction: eventTileFocusWithin(prevState.interaction, showActionBarFromFocus),
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly onBlurWithin = (event: FocusEvent<HTMLElement>): void => {
|
private readonly onBlurWithin = (event: FocusEvent<HTMLElement>): void => {
|
||||||
@@ -946,7 +955,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ focusWithin: false, showActionBarFromFocus: false });
|
this.setState((prevState) => ({
|
||||||
|
interaction: eventTileBlurWithin(prevState.interaction),
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly getTile: () => IEventTileType | null = () => this.tile.current;
|
private readonly getTile: () => IEventTileType | null = () => this.tile.current;
|
||||||
@@ -997,26 +1008,22 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this.setState({
|
this.setState((prevState) => ({
|
||||||
contextMenu: {
|
interaction: eventTileOpenContextMenu(prevState.interaction, {
|
||||||
position: {
|
position: {
|
||||||
left: ev.clientX,
|
left: ev.clientX,
|
||||||
top: ev.clientY,
|
top: ev.clientY,
|
||||||
bottom: ev.clientY,
|
bottom: ev.clientY,
|
||||||
},
|
},
|
||||||
link: anchorElement?.href || permalink,
|
link: anchorElement?.href || permalink,
|
||||||
},
|
}),
|
||||||
actionBarFocused: true,
|
}));
|
||||||
hover: false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly onCloseMenu = (): void => {
|
private readonly onCloseMenu = (): void => {
|
||||||
this.setState({
|
this.setState((prevState) => ({
|
||||||
contextMenu: undefined,
|
interaction: eventTileCloseContextMenu(prevState.interaction),
|
||||||
actionBarFocused: false,
|
}));
|
||||||
hover: false,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly setQuoteExpanded = (expanded: boolean): void => {
|
private readonly setQuoteExpanded = (expanded: boolean): void => {
|
||||||
@@ -1038,7 +1045,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderContextMenu(): ReactNode {
|
private renderContextMenu(): ReactNode {
|
||||||
if (!this.state.contextMenu) return null;
|
if (!this.state.interaction.contextMenu) return null;
|
||||||
|
|
||||||
const tile = this.getTile();
|
const tile = this.getTile();
|
||||||
const replyChain = this.getReplyChain();
|
const replyChain = this.getReplyChain();
|
||||||
@@ -1047,7 +1054,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageContextMenu
|
<MessageContextMenu
|
||||||
{...aboveRightOf(this.state.contextMenu.position)}
|
{...aboveRightOf(this.state.interaction.contextMenu.position)}
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
eventTileOps={eventTileOps}
|
eventTileOps={eventTileOps}
|
||||||
@@ -1055,7 +1062,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
onFinished={this.onCloseMenu}
|
onFinished={this.onCloseMenu}
|
||||||
rightClick={true}
|
rightClick={true}
|
||||||
reactions={this.state.reactions}
|
reactions={this.state.reactions}
|
||||||
link={this.state.contextMenu.link}
|
link={this.state.interaction.contextMenu.link}
|
||||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -1122,17 +1129,17 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
noBubbleEvent,
|
noBubbleEvent,
|
||||||
isTwelveHour: this.props.isTwelveHour,
|
isTwelveHour: this.props.isTwelveHour,
|
||||||
isHighlighted: this.shouldHighlight(),
|
isHighlighted: this.shouldHighlight(),
|
||||||
isSelected: this.props.isSelectedEvent || !!this.state.contextMenu,
|
isSelected: this.props.isSelectedEvent || !!this.state.interaction.contextMenu,
|
||||||
isLast: this.props.last,
|
isLast: this.props.last,
|
||||||
isLastInSection: this.props.lastInSection,
|
isLastInSection: this.props.lastInSection,
|
||||||
isContextual: this.props.contextual,
|
isContextual: this.props.contextual,
|
||||||
},
|
},
|
||||||
interaction: {
|
interaction: {
|
||||||
hover: this.state.hover,
|
hover: this.state.interaction.hover,
|
||||||
showActionBarFromFocus: this.state.showActionBarFromFocus,
|
showActionBarFromFocus: this.state.interaction.showActionBarFromFocus,
|
||||||
focusWithin: this.state.focusWithin,
|
focusWithin: this.state.interaction.focusWithin,
|
||||||
isActionBarFocused: this.state.actionBarFocused,
|
isActionBarFocused: this.state.interaction.actionBarFocused,
|
||||||
hasContextMenu: !!this.state.contextMenu,
|
hasContextMenu: !!this.state.interaction.contextMenu,
|
||||||
inhibitInteraction: this.props.inhibitInteraction,
|
inhibitInteraction: this.props.inhibitInteraction,
|
||||||
},
|
},
|
||||||
sender: {
|
sender: {
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Context menu state owned by EventTile interaction handling. */
|
||||||
|
export interface EventTileContextMenuState {
|
||||||
|
/** Menu anchor position. */
|
||||||
|
position: Pick<DOMRect, "top" | "left" | "bottom">;
|
||||||
|
/** Optional link target captured from the context-menu target. */
|
||||||
|
link?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Interaction state owned by EventTile. */
|
||||||
|
export interface EventTileInteractionState {
|
||||||
|
/** Whether the action bar is focused. */
|
||||||
|
actionBarFocused: boolean;
|
||||||
|
/** Whether focus should force the action bar visible. */
|
||||||
|
showActionBarFromFocus: boolean;
|
||||||
|
/** Whether the tile is currently hovered. */
|
||||||
|
hover: boolean;
|
||||||
|
/** Whether focus is currently inside the tile. */
|
||||||
|
focusWithin: boolean;
|
||||||
|
/** Context menu state, when the EventTile context menu is open. */
|
||||||
|
contextMenu?: EventTileContextMenuState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Initial EventTile interaction state. */
|
||||||
|
export const initialEventTileInteractionState: EventTileInteractionState = {
|
||||||
|
actionBarFocused: false,
|
||||||
|
showActionBarFromFocus: false,
|
||||||
|
hover: false,
|
||||||
|
focusWithin: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Updates interaction state when the tile is hovered. */
|
||||||
|
export function eventTileMouseEnter(state: EventTileInteractionState): EventTileInteractionState {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
hover: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates interaction state when the tile is no longer hovered. */
|
||||||
|
export function eventTileMouseLeave(state: EventTileInteractionState): EventTileInteractionState {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
hover: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates interaction state when focus enters the tile. */
|
||||||
|
export function eventTileFocusWithin(
|
||||||
|
state: EventTileInteractionState,
|
||||||
|
showActionBarFromFocus: boolean,
|
||||||
|
): EventTileInteractionState {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
focusWithin: true,
|
||||||
|
showActionBarFromFocus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates interaction state when focus leaves the tile. */
|
||||||
|
export function eventTileBlurWithin(state: EventTileInteractionState): EventTileInteractionState {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
focusWithin: false,
|
||||||
|
showActionBarFromFocus: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates interaction state when the action bar focus changes. */
|
||||||
|
export function eventTileActionBarFocusChange(
|
||||||
|
state: EventTileInteractionState,
|
||||||
|
actionBarFocused: boolean,
|
||||||
|
tileHovered: boolean,
|
||||||
|
): EventTileInteractionState {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
actionBarFocused,
|
||||||
|
hover: actionBarFocused ? state.hover : tileHovered,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates interaction state when stale hover is detected. */
|
||||||
|
export function eventTileClearHover(state: EventTileInteractionState): EventTileInteractionState {
|
||||||
|
return eventTileMouseLeave(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates interaction state when the EventTile context menu opens. */
|
||||||
|
export function eventTileOpenContextMenu(
|
||||||
|
state: EventTileInteractionState,
|
||||||
|
contextMenu: EventTileContextMenuState,
|
||||||
|
): EventTileInteractionState {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
contextMenu,
|
||||||
|
actionBarFocused: true,
|
||||||
|
hover: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates interaction state when the EventTile context menu closes. */
|
||||||
|
export function eventTileCloseContextMenu(state: EventTileInteractionState): EventTileInteractionState {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
contextMenu: undefined,
|
||||||
|
actionBarFocused: false,
|
||||||
|
hover: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
eventTileActionBarFocusChange,
|
||||||
|
eventTileBlurWithin,
|
||||||
|
eventTileClearHover,
|
||||||
|
eventTileCloseContextMenu,
|
||||||
|
eventTileFocusWithin,
|
||||||
|
eventTileMouseEnter,
|
||||||
|
eventTileMouseLeave,
|
||||||
|
eventTileOpenContextMenu,
|
||||||
|
initialEventTileInteractionState,
|
||||||
|
type EventTileInteractionState,
|
||||||
|
} from "../../../src/viewmodels/room/timeline/event-tile/EventTileInteractionState";
|
||||||
|
|
||||||
|
function makeState(overrides: Partial<EventTileInteractionState> = {}): EventTileInteractionState {
|
||||||
|
return {
|
||||||
|
...initialEventTileInteractionState,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("EventTileInteractionState", () => {
|
||||||
|
it("tracks hover state", () => {
|
||||||
|
const hovered = eventTileMouseEnter(makeState());
|
||||||
|
const unhovered = eventTileMouseLeave(hovered);
|
||||||
|
|
||||||
|
expect(hovered.hover).toBe(true);
|
||||||
|
expect(unhovered.hover).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tracks focus-within state and keyboard action bar visibility", () => {
|
||||||
|
const focused = eventTileFocusWithin(makeState(), true);
|
||||||
|
const blurred = eventTileBlurWithin(focused);
|
||||||
|
|
||||||
|
expect(focused.focusWithin).toBe(true);
|
||||||
|
expect(focused.showActionBarFromFocus).toBe(true);
|
||||||
|
expect(blurred.focusWithin).toBe(false);
|
||||||
|
expect(blurred.showActionBarFromFocus).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves hover while action bar receives focus", () => {
|
||||||
|
const state = makeState({ hover: true });
|
||||||
|
const focused = eventTileActionBarFocusChange(state, true, false);
|
||||||
|
|
||||||
|
expect(focused.actionBarFocused).toBe(true);
|
||||||
|
expect(focused.hover).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("restores hover from the tile when action bar loses focus", () => {
|
||||||
|
const state = makeState({ actionBarFocused: true, hover: false });
|
||||||
|
const focused = eventTileActionBarFocusChange(state, false, true);
|
||||||
|
|
||||||
|
expect(focused.actionBarFocused).toBe(false);
|
||||||
|
expect(focused.hover).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears stale hover", () => {
|
||||||
|
const state = eventTileClearHover(makeState({ hover: true }));
|
||||||
|
|
||||||
|
expect(state.hover).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens and closes the context menu", () => {
|
||||||
|
const menu = {
|
||||||
|
position: {
|
||||||
|
left: 1,
|
||||||
|
top: 2,
|
||||||
|
bottom: 3,
|
||||||
|
},
|
||||||
|
link: "https://example.org/",
|
||||||
|
};
|
||||||
|
const opened = eventTileOpenContextMenu(makeState({ hover: true }), menu);
|
||||||
|
const closed = eventTileCloseContextMenu(opened);
|
||||||
|
|
||||||
|
expect(opened.contextMenu).toEqual(menu);
|
||||||
|
expect(opened.actionBarFocused).toBe(true);
|
||||||
|
expect(opened.hover).toBe(false);
|
||||||
|
expect(closed.contextMenu).toBeUndefined();
|
||||||
|
expect(closed.actionBarFocused).toBe(false);
|
||||||
|
expect(closed.hover).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user