mirror of https://github.com/vector-im/riot-web
				
				
				
			Replace breadcrumbs with recently viewed menu (#7073)
							parent
							
								
									757d473971
								
							
						
					
					
						commit
						4a6d46b76a
					
				| 
						 | 
				
			
			@ -143,6 +143,7 @@
 | 
			
		|||
@import "./views/elements/_ImageView.scss";
 | 
			
		||||
@import "./views/elements/_InfoTooltip.scss";
 | 
			
		||||
@import "./views/elements/_InlineSpinner.scss";
 | 
			
		||||
@import "./views/elements/_InteractiveTooltip.scss";
 | 
			
		||||
@import "./views/elements/_InviteReason.scss";
 | 
			
		||||
@import "./views/elements/_ManageIntegsButton.scss";
 | 
			
		||||
@import "./views/elements/_MiniAvatarUploader.scss";
 | 
			
		||||
| 
						 | 
				
			
			@ -230,6 +231,7 @@
 | 
			
		|||
@import "./views/rooms/_NotificationBadge.scss";
 | 
			
		||||
@import "./views/rooms/_PinnedEventTile.scss";
 | 
			
		||||
@import "./views/rooms/_PresenceLabel.scss";
 | 
			
		||||
@import "./views/rooms/_RecentlyViewedButton.scss";
 | 
			
		||||
@import "./views/rooms/_ReplyPreview.scss";
 | 
			
		||||
@import "./views/rooms/_ReplyTile.scss";
 | 
			
		||||
@import "./views/rooms/_RoomBreadcrumbs.scss";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,6 +37,7 @@ limitations under the License.
 | 
			
		|||
    position: absolute;
 | 
			
		||||
    font-size: $font-14px;
 | 
			
		||||
    z-index: 5001;
 | 
			
		||||
    width: max-content;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_ContextualMenu_right {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -133,7 +133,8 @@ $roomListCollapsedWidth: 68px;
 | 
			
		|||
                    display: none;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                & + .mx_LeftPanel_exploreButton {
 | 
			
		||||
                & + .mx_LeftPanel_exploreButton,
 | 
			
		||||
                & + .mx_LeftPanel_recentsButton {
 | 
			
		||||
                    // Cheaty way to return the occupied space to the filter input
 | 
			
		||||
                    flex-basis: 0;
 | 
			
		||||
                    margin: 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -166,11 +167,12 @@ $roomListCollapsedWidth: 68px;
 | 
			
		|||
                    mask-position: center;
 | 
			
		||||
                    mask-size: contain;
 | 
			
		||||
                    mask-repeat: no-repeat;
 | 
			
		||||
                    background: $secondary-content;
 | 
			
		||||
                    background-color: $secondary-content;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .mx_LeftPanel_exploreButton {
 | 
			
		||||
            .mx_LeftPanel_exploreButton,
 | 
			
		||||
            .mx_LeftPanel_recentsButton {
 | 
			
		||||
                width: 32px;
 | 
			
		||||
                height: 32px;
 | 
			
		||||
                border-radius: 8px;
 | 
			
		||||
| 
						 | 
				
			
			@ -185,11 +187,10 @@ $roomListCollapsedWidth: 68px;
 | 
			
		|||
                    left: 8px;
 | 
			
		||||
                    width: 16px;
 | 
			
		||||
                    height: 16px;
 | 
			
		||||
                    mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
 | 
			
		||||
                    mask-position: center;
 | 
			
		||||
                    mask-size: contain;
 | 
			
		||||
                    mask-repeat: no-repeat;
 | 
			
		||||
                    background: $secondary-content;
 | 
			
		||||
                    background-color: $secondary-content;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                &:hover {
 | 
			
		||||
| 
						 | 
				
			
			@ -200,6 +201,14 @@ $roomListCollapsedWidth: 68px;
 | 
			
		|||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .mx_LeftPanel_exploreButton::before {
 | 
			
		||||
                mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .mx_LeftPanel_recentsButton::before {
 | 
			
		||||
                mask-image: url('$(res)/img/element-icons/clock.svg');
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .mx_LeftPanel_roomListFilterCount {
 | 
			
		||||
| 
						 | 
				
			
			@ -257,7 +266,8 @@ $roomListCollapsedWidth: 68px;
 | 
			
		|||
                    background-color: transparent;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .mx_LeftPanel_exploreButton {
 | 
			
		||||
                .mx_LeftPanel_exploreButton,
 | 
			
		||||
                .mx_LeftPanel_recentsButton {
 | 
			
		||||
                    margin-left: 0;
 | 
			
		||||
                    margin-top: 8px;
 | 
			
		||||
                }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,11 +27,17 @@ limitations under the License.
 | 
			
		|||
    // https://bugzilla.mozilla.org/show_bug.cgi?id=255139
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    user-select: none;
 | 
			
		||||
 | 
			
		||||
    &.mx_RoomAvatar_isSpaceRoom {
 | 
			
		||||
        &.mx_BaseAvatar_image, .mx_BaseAvatar_image {
 | 
			
		||||
            border-radius: 8px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_BaseAvatar_initial {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: 0px;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    color: $avatar-initial-color;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    speak: none;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,97 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
.mx_InteractiveTooltip_wrapper {
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    z-index: 5000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_InteractiveTooltip {
 | 
			
		||||
    border-radius: 8px;
 | 
			
		||||
    background-color: $background;
 | 
			
		||||
    color: $primary-content;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    z-index: 5001;
 | 
			
		||||
    box-shadow: 0 24px 8px rgb(17 17 26 / 4%), 0 8px 32px rgb(17 17 26 / 4%);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_top {
 | 
			
		||||
    top: 10px; // 8px chevron + 2px spacing
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_left {
 | 
			
		||||
    left: 10px; // 8px chevron + 2px spacing
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_right {
 | 
			
		||||
    right: 10px; // 8px chevron + 2px spacing
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_bottom {
 | 
			
		||||
    bottom: 10px; // 8px chevron + 2px spacing
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_InteractiveTooltip_chevron_top {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: calc(50% - 8px);
 | 
			
		||||
    top: -8px;
 | 
			
		||||
    width: 0;
 | 
			
		||||
    height: 0;
 | 
			
		||||
    border-left: 8px solid transparent;
 | 
			
		||||
    border-bottom: 8px solid $background;
 | 
			
		||||
    border-right: 8px solid transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Adapted from https://codyhouse.co/blog/post/css-rounded-triangles-with-clip-path
 | 
			
		||||
// by Sebastiano Guerriero (@guerriero_se)
 | 
			
		||||
@supports (clip-path: polygon(0% 0%, 100% 100%, 0% 100%)) {
 | 
			
		||||
    .mx_InteractiveTooltip_chevron_top {
 | 
			
		||||
        height: 16px;
 | 
			
		||||
        width: 16px;
 | 
			
		||||
        background-color: inherit;
 | 
			
		||||
        border: none;
 | 
			
		||||
        clip-path: polygon(0% 0%, 100% 100%, 0% 100%);
 | 
			
		||||
        transform: rotate(135deg);
 | 
			
		||||
        border-radius: 0 0 0 3px;
 | 
			
		||||
        top: calc(-8px / 1.414); // sqrt(2) because of rotation
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_InteractiveTooltip_chevron_bottom {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: calc(50% - 8px);
 | 
			
		||||
    bottom: -8px;
 | 
			
		||||
    width: 0;
 | 
			
		||||
    height: 0;
 | 
			
		||||
    border-left: 8px solid transparent;
 | 
			
		||||
    border-top: 8px solid $background;
 | 
			
		||||
    border-right: 8px solid transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Adapted from https://codyhouse.co/blog/post/css-rounded-triangles-with-clip-path
 | 
			
		||||
// by Sebastiano Guerriero (@guerriero_se)
 | 
			
		||||
@supports (clip-path: polygon(0% 0%, 100% 100%, 0% 100%)) {
 | 
			
		||||
    .mx_InteractiveTooltip_chevron_bottom {
 | 
			
		||||
        height: 16px;
 | 
			
		||||
        width: 16px;
 | 
			
		||||
        background-color: inherit;
 | 
			
		||||
        border: none;
 | 
			
		||||
        clip-path: polygon(0% 0%, 100% 100%, 0% 100%);
 | 
			
		||||
        transform: rotate(-45deg);
 | 
			
		||||
        border-radius: 0 0 0 3px;
 | 
			
		||||
        bottom: calc(-8px / 1.414); // sqrt(2) because of rotation
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,73 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2021 The Matrix.org Foundation C.I.C.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
.mx_RecentlyViewedButton_ContextMenu {
 | 
			
		||||
    padding: 16px 8px 16px 16px;
 | 
			
		||||
    width: max-content;
 | 
			
		||||
    max-width: 240px;
 | 
			
		||||
    max-height: 400px;
 | 
			
		||||
    border: 1px solid rgba($primary-content, .1);
 | 
			
		||||
    border-radius: 8px;
 | 
			
		||||
    box-shadow: 0 8px 4px rgba(0, 0, 0, 0.08);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
 | 
			
		||||
    > h4 {
 | 
			
		||||
        margin: 0 0 12px 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    > div {
 | 
			
		||||
        overflow-y: auto;
 | 
			
		||||
 | 
			
		||||
        * {
 | 
			
		||||
            margin-right: 4px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .mx_AccessibleButton {
 | 
			
		||||
        margin-top: 2px;
 | 
			
		||||
        padding: 4px;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        border-radius: 8px;
 | 
			
		||||
        min-height: 34px;
 | 
			
		||||
 | 
			
		||||
        &:hover {
 | 
			
		||||
            background-color: $panel-actions;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .mx_BaseAvatar {
 | 
			
		||||
            margin-right: 8px;
 | 
			
		||||
            width: 24px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .mx_RecentlyViewedButton_entry_label {
 | 
			
		||||
            display: grid;
 | 
			
		||||
 | 
			
		||||
            > div {
 | 
			
		||||
                overflow: hidden;
 | 
			
		||||
                white-space: nowrap;
 | 
			
		||||
                text-overflow: ellipsis;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .mx_RecentlyViewedButton_entry_spaces {
 | 
			
		||||
            font-size: $font-12px;
 | 
			
		||||
            line-height: $font-15px;
 | 
			
		||||
            color: $secondary-content;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
<circle cx="8.625" cy="8.625" r="6.375" stroke="#C1C6CD" stroke-width="2" stroke-linecap="round"/>
 | 
			
		||||
<path d="M8.25 5.625V9.375H11.625" stroke="#C1C6CD" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 320 B  | 
| 
						 | 
				
			
			@ -25,12 +25,7 @@ import CallHandler from "../../CallHandler";
 | 
			
		|||
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
 | 
			
		||||
import { Action } from "../../dispatcher/actions";
 | 
			
		||||
import RoomSearch from "./RoomSearch";
 | 
			
		||||
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
 | 
			
		||||
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
 | 
			
		||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
 | 
			
		||||
import ResizeNotifier from "../../utils/ResizeNotifier";
 | 
			
		||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
 | 
			
		||||
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
 | 
			
		||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
 | 
			
		||||
import LeftPanelWidget from "./LeftPanelWidget";
 | 
			
		||||
import { replaceableComponent } from "../../utils/replaceableComponent";
 | 
			
		||||
| 
						 | 
				
			
			@ -41,14 +36,27 @@ import UIStore from "../../stores/UIStore";
 | 
			
		|||
import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
 | 
			
		||||
import RoomListHeader from "../views/rooms/RoomListHeader";
 | 
			
		||||
import { Key } from "../../Keyboard";
 | 
			
		||||
import RecentlyViewedButton from "../views/rooms/RecentlyViewedButton";
 | 
			
		||||
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
 | 
			
		||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
 | 
			
		||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
 | 
			
		||||
import IndicatorScrollbar from "./IndicatorScrollbar";
 | 
			
		||||
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
 | 
			
		||||
import SettingsStore from "../../settings/SettingsStore";
 | 
			
		||||
 | 
			
		||||
interface IProps {
 | 
			
		||||
    isMinimized: boolean;
 | 
			
		||||
    resizeNotifier: ResizeNotifier;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum BreadcrumbsMode {
 | 
			
		||||
    Disabled,
 | 
			
		||||
    Legacy,
 | 
			
		||||
    Labs,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IState {
 | 
			
		||||
    showBreadcrumbs: boolean;
 | 
			
		||||
    showBreadcrumbs: BreadcrumbsMode;
 | 
			
		||||
    activeSpace: SpaceKey;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -65,8 +73,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
 | 
			
		|||
        super(props);
 | 
			
		||||
 | 
			
		||||
        this.state = {
 | 
			
		||||
            showBreadcrumbs: BreadcrumbsStore.instance.visible,
 | 
			
		||||
            activeSpace: SpaceStore.instance.activeSpace,
 | 
			
		||||
            showBreadcrumbs: LeftPanel.breadcrumbsMode,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
 | 
			
		||||
| 
						 | 
				
			
			@ -74,6 +82,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
 | 
			
		|||
        SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static get breadcrumbsMode(): BreadcrumbsMode {
 | 
			
		||||
        if (!SettingsStore.getValue("breadcrumbs")) return BreadcrumbsMode.Disabled;
 | 
			
		||||
        return SettingsStore.getValue("feature_breadcrumbs_v2") ? BreadcrumbsMode.Labs : BreadcrumbsMode.Legacy;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public componentDidMount() {
 | 
			
		||||
        UIStore.instance.trackElementDimensions("LeftPanel", this.ref.current);
 | 
			
		||||
        UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
 | 
			
		||||
| 
						 | 
				
			
			@ -116,7 +129,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
 | 
			
		|||
    };
 | 
			
		||||
 | 
			
		||||
    private onBreadcrumbsUpdate = () => {
 | 
			
		||||
        const newVal = BreadcrumbsStore.instance.visible;
 | 
			
		||||
        const newVal = LeftPanel.breadcrumbsMode;
 | 
			
		||||
        if (newVal !== this.state.showBreadcrumbs) {
 | 
			
		||||
            this.setState({ showBreadcrumbs: newVal });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -323,7 +336,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
 | 
			
		|||
    };
 | 
			
		||||
 | 
			
		||||
    private renderBreadcrumbs(): React.ReactNode {
 | 
			
		||||
        if (this.state.showBreadcrumbs && !this.props.isMinimized) {
 | 
			
		||||
        if (this.state.showBreadcrumbs === BreadcrumbsMode.Legacy && !this.props.isMinimized) {
 | 
			
		||||
            return (
 | 
			
		||||
                <IndicatorScrollbar
 | 
			
		||||
                    className="mx_LeftPanel_breadcrumbsContainer mx_AutoHideScrollbar"
 | 
			
		||||
| 
						 | 
				
			
			@ -349,6 +362,17 @@ export default class LeftPanel extends React.Component<IProps, IState> {
 | 
			
		|||
                />;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let rightButton: JSX.Element;
 | 
			
		||||
        if (this.state.showBreadcrumbs === BreadcrumbsMode.Labs) {
 | 
			
		||||
            rightButton = <RecentlyViewedButton />;
 | 
			
		||||
        } else if (this.state.activeSpace === MetaSpace.Home) {
 | 
			
		||||
            rightButton = <AccessibleTooltipButton
 | 
			
		||||
                className="mx_LeftPanel_exploreButton"
 | 
			
		||||
                onClick={this.onExplore}
 | 
			
		||||
                title={_t("Explore rooms")}
 | 
			
		||||
            />;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div
 | 
			
		||||
                className="mx_LeftPanel_filterContainer"
 | 
			
		||||
| 
						 | 
				
			
			@ -363,12 +387,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
 | 
			
		|||
                />
 | 
			
		||||
 | 
			
		||||
                { dialPadButton }
 | 
			
		||||
 | 
			
		||||
                { this.state.activeSpace === MetaSpace.Home && <AccessibleTooltipButton
 | 
			
		||||
                    className="mx_LeftPanel_exploreButton"
 | 
			
		||||
                    onClick={this.onExplore}
 | 
			
		||||
                    title={_t("Explore rooms")}
 | 
			
		||||
                /> }
 | 
			
		||||
                { rightButton }
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,496 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import React, { CSSProperties, MouseEventHandler, ReactNode, RefCallback } from "react";
 | 
			
		||||
import ReactDOM from "react-dom";
 | 
			
		||||
import classNames from "classnames";
 | 
			
		||||
 | 
			
		||||
import UIStore from "../../../stores/UIStore";
 | 
			
		||||
import { ChevronFace } from "../../structures/ContextMenu";
 | 
			
		||||
 | 
			
		||||
const InteractiveTooltipContainerId = "mx_InteractiveTooltip_Container";
 | 
			
		||||
 | 
			
		||||
// If the distance from tooltip to window edge is below this value, the tooltip
 | 
			
		||||
// will flip around to the other side of the target.
 | 
			
		||||
const MIN_SAFE_DISTANCE_TO_WINDOW_EDGE = 20;
 | 
			
		||||
 | 
			
		||||
function getOrCreateContainer(): HTMLElement {
 | 
			
		||||
    let container = document.getElementById(InteractiveTooltipContainerId);
 | 
			
		||||
 | 
			
		||||
    if (!container) {
 | 
			
		||||
        container = document.createElement("div");
 | 
			
		||||
        container.id = InteractiveTooltipContainerId;
 | 
			
		||||
        document.body.appendChild(container);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return container;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IRect {
 | 
			
		||||
    top: number;
 | 
			
		||||
    right: number;
 | 
			
		||||
    bottom: number;
 | 
			
		||||
    left: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isInRect(x: number, y: number, rect: IRect): boolean {
 | 
			
		||||
    const { top, right, bottom, left } = rect;
 | 
			
		||||
    return x >= left && x <= right && y >= top && y <= bottom;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns the positive slope of the diagonal of the rect.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {DOMRect} rect
 | 
			
		||||
 * @return {number}
 | 
			
		||||
 */
 | 
			
		||||
function getDiagonalSlope(rect: IRect): number {
 | 
			
		||||
    const { top, right, bottom, left } = rect;
 | 
			
		||||
    return (bottom - top) / (right - left);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isInUpperLeftHalf(x: number, y: number, rect: IRect): boolean {
 | 
			
		||||
    const { bottom, left } = rect;
 | 
			
		||||
    // Negative slope because Y values grow downwards and for this case, the
 | 
			
		||||
    // diagonal goes from larger to smaller Y values.
 | 
			
		||||
    const diagonalSlope = getDiagonalSlope(rect) * -1;
 | 
			
		||||
    return isInRect(x, y, rect) && (y <= bottom + diagonalSlope * (x - left));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isInLowerRightHalf(x: number, y: number, rect: IRect): boolean {
 | 
			
		||||
    const { bottom, left } = rect;
 | 
			
		||||
    // Negative slope because Y values grow downwards and for this case, the
 | 
			
		||||
    // diagonal goes from larger to smaller Y values.
 | 
			
		||||
    const diagonalSlope = getDiagonalSlope(rect) * -1;
 | 
			
		||||
    return isInRect(x, y, rect) && (y >= bottom + diagonalSlope * (x - left));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isInUpperRightHalf(x: number, y: number, rect: IRect): boolean {
 | 
			
		||||
    const { top, left } = rect;
 | 
			
		||||
    // Positive slope because Y values grow downwards and for this case, the
 | 
			
		||||
    // diagonal goes from smaller to larger Y values.
 | 
			
		||||
    const diagonalSlope = getDiagonalSlope(rect) * 1;
 | 
			
		||||
    return isInRect(x, y, rect) && (y <= top + diagonalSlope * (x - left));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isInLowerLeftHalf(x: number, y: number, rect: IRect): boolean {
 | 
			
		||||
    const { top, left } = rect;
 | 
			
		||||
    // Positive slope because Y values grow downwards and for this case, the
 | 
			
		||||
    // diagonal goes from smaller to larger Y values.
 | 
			
		||||
    const diagonalSlope = getDiagonalSlope(rect) * 1;
 | 
			
		||||
    return isInRect(x, y, rect) && (y >= top + diagonalSlope * (x - left));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum Direction {
 | 
			
		||||
    Top,
 | 
			
		||||
    Left,
 | 
			
		||||
    Bottom,
 | 
			
		||||
    Right,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// exported for tests
 | 
			
		||||
export function mouseWithinRegion(
 | 
			
		||||
    x: number,
 | 
			
		||||
    y: number,
 | 
			
		||||
    direction: Direction,
 | 
			
		||||
    targetRect: DOMRect,
 | 
			
		||||
    contentRect: DOMRect,
 | 
			
		||||
): boolean {
 | 
			
		||||
    // When moving the mouse from the target to the tooltip, we create a safe area
 | 
			
		||||
    // that includes the tooltip, the target, and the trapezoid ABCD between them:
 | 
			
		||||
    //                            ┌───────────┐
 | 
			
		||||
    //                            │           │
 | 
			
		||||
    //                            │           │
 | 
			
		||||
    //                          A └───E───F───┘ B
 | 
			
		||||
    //                                  V
 | 
			
		||||
    //                                 ┌─┐
 | 
			
		||||
    //                                 │ │
 | 
			
		||||
    //                                C└─┘D
 | 
			
		||||
    //
 | 
			
		||||
    // As long as the mouse remains inside the safe area, the tooltip will stay open.
 | 
			
		||||
    const buffer = 50;
 | 
			
		||||
    if (isInRect(x, y, targetRect)) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    switch (direction) {
 | 
			
		||||
        case Direction.Left: {
 | 
			
		||||
            const contentRectWithBuffer = {
 | 
			
		||||
                top: contentRect.top - buffer,
 | 
			
		||||
                right: contentRect.right,
 | 
			
		||||
                bottom: contentRect.bottom + buffer,
 | 
			
		||||
                left: contentRect.left - buffer,
 | 
			
		||||
            };
 | 
			
		||||
            const trapezoidTop = {
 | 
			
		||||
                top: contentRect.top - buffer,
 | 
			
		||||
                right: targetRect.right,
 | 
			
		||||
                bottom: targetRect.top,
 | 
			
		||||
                left: contentRect.right,
 | 
			
		||||
            };
 | 
			
		||||
            const trapezoidCenter = {
 | 
			
		||||
                top: targetRect.top,
 | 
			
		||||
                right: targetRect.left,
 | 
			
		||||
                bottom: targetRect.bottom,
 | 
			
		||||
                left: contentRect.right,
 | 
			
		||||
            };
 | 
			
		||||
            const trapezoidBottom = {
 | 
			
		||||
                top: targetRect.bottom,
 | 
			
		||||
                right: targetRect.right,
 | 
			
		||||
                bottom: contentRect.bottom + buffer,
 | 
			
		||||
                left: contentRect.right,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
                isInRect(x, y, contentRectWithBuffer) ||
 | 
			
		||||
                isInLowerLeftHalf(x, y, trapezoidTop) ||
 | 
			
		||||
                isInRect(x, y, trapezoidCenter) ||
 | 
			
		||||
                isInUpperLeftHalf(x, y, trapezoidBottom)
 | 
			
		||||
            ) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        case Direction.Right: {
 | 
			
		||||
            const contentRectWithBuffer = {
 | 
			
		||||
                top: contentRect.top - buffer,
 | 
			
		||||
                right: contentRect.right + buffer,
 | 
			
		||||
                bottom: contentRect.bottom + buffer,
 | 
			
		||||
                left: contentRect.left,
 | 
			
		||||
            };
 | 
			
		||||
            const trapezoidTop = {
 | 
			
		||||
                top: contentRect.top - buffer,
 | 
			
		||||
                right: contentRect.left,
 | 
			
		||||
                bottom: targetRect.top,
 | 
			
		||||
                left: targetRect.left,
 | 
			
		||||
            };
 | 
			
		||||
            const trapezoidCenter = {
 | 
			
		||||
                top: targetRect.top,
 | 
			
		||||
                right: contentRect.left,
 | 
			
		||||
                bottom: targetRect.bottom,
 | 
			
		||||
                left: targetRect.right,
 | 
			
		||||
            };
 | 
			
		||||
            const trapezoidBottom = {
 | 
			
		||||
                top: targetRect.bottom,
 | 
			
		||||
                right: contentRect.left,
 | 
			
		||||
                bottom: contentRect.bottom + buffer,
 | 
			
		||||
                left: targetRect.left,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
                isInRect(x, y, contentRectWithBuffer) ||
 | 
			
		||||
                isInLowerRightHalf(x, y, trapezoidTop) ||
 | 
			
		||||
                isInRect(x, y, trapezoidCenter) ||
 | 
			
		||||
                isInUpperRightHalf(x, y, trapezoidBottom)
 | 
			
		||||
            ) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        case Direction.Top: {
 | 
			
		||||
            const contentRectWithBuffer = {
 | 
			
		||||
                top: contentRect.top - buffer,
 | 
			
		||||
                right: contentRect.right + buffer,
 | 
			
		||||
                bottom: contentRect.bottom,
 | 
			
		||||
                left: contentRect.left - buffer,
 | 
			
		||||
            };
 | 
			
		||||
            const trapezoidLeft = {
 | 
			
		||||
                top: contentRect.bottom,
 | 
			
		||||
                right: targetRect.left,
 | 
			
		||||
                bottom: targetRect.bottom,
 | 
			
		||||
                left: contentRect.left - buffer,
 | 
			
		||||
            };
 | 
			
		||||
            const trapezoidCenter = {
 | 
			
		||||
                top: contentRect.bottom,
 | 
			
		||||
                right: targetRect.right,
 | 
			
		||||
                bottom: targetRect.top,
 | 
			
		||||
                left: targetRect.left,
 | 
			
		||||
            };
 | 
			
		||||
            const trapezoidRight = {
 | 
			
		||||
                top: contentRect.bottom,
 | 
			
		||||
                right: contentRect.right + buffer,
 | 
			
		||||
                bottom: targetRect.bottom,
 | 
			
		||||
                left: targetRect.right,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
                isInRect(x, y, contentRectWithBuffer) ||
 | 
			
		||||
                isInUpperRightHalf(x, y, trapezoidLeft) ||
 | 
			
		||||
                isInRect(x, y, trapezoidCenter) ||
 | 
			
		||||
                isInUpperLeftHalf(x, y, trapezoidRight)
 | 
			
		||||
            ) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        case Direction.Bottom: {
 | 
			
		||||
            const contentRectWithBuffer = {
 | 
			
		||||
                top: contentRect.top,
 | 
			
		||||
                right: contentRect.right + buffer,
 | 
			
		||||
                bottom: contentRect.bottom + buffer,
 | 
			
		||||
                left: contentRect.left - buffer,
 | 
			
		||||
            };
 | 
			
		||||
            const trapezoidLeft = {
 | 
			
		||||
                top: targetRect.top,
 | 
			
		||||
                right: targetRect.left,
 | 
			
		||||
                bottom: contentRect.top,
 | 
			
		||||
                left: contentRect.left - buffer,
 | 
			
		||||
            };
 | 
			
		||||
            const trapezoidCenter = {
 | 
			
		||||
                top: targetRect.bottom,
 | 
			
		||||
                right: targetRect.right,
 | 
			
		||||
                bottom: contentRect.top,
 | 
			
		||||
                left: targetRect.left,
 | 
			
		||||
            };
 | 
			
		||||
            const trapezoidRight = {
 | 
			
		||||
                top: targetRect.top,
 | 
			
		||||
                right: contentRect.right + buffer,
 | 
			
		||||
                bottom: contentRect.top,
 | 
			
		||||
                left: targetRect.right,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
                isInRect(x, y, contentRectWithBuffer) ||
 | 
			
		||||
                isInLowerRightHalf(x, y, trapezoidLeft) ||
 | 
			
		||||
                isInRect(x, y, trapezoidCenter) ||
 | 
			
		||||
                isInLowerLeftHalf(x, y, trapezoidRight)
 | 
			
		||||
            ) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IProps {
 | 
			
		||||
    children(props: {
 | 
			
		||||
        ref: RefCallback<HTMLElement>;
 | 
			
		||||
        onMouseOver: MouseEventHandler;
 | 
			
		||||
    }): ReactNode;
 | 
			
		||||
    // Content to show in the tooltip
 | 
			
		||||
    content: ReactNode;
 | 
			
		||||
    direction?: Direction;
 | 
			
		||||
    // Function to call when visibility of the tooltip changes
 | 
			
		||||
    onVisibilityChange?(visible: boolean): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IState {
 | 
			
		||||
    contentRect: DOMRect;
 | 
			
		||||
    visible: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * This style of tooltip takes a "target" element as its child and centers the
 | 
			
		||||
 * tooltip along one edge of the target.
 | 
			
		||||
 */
 | 
			
		||||
export default class InteractiveTooltip extends React.Component<IProps, IState> {
 | 
			
		||||
    private target: HTMLElement;
 | 
			
		||||
 | 
			
		||||
    public static defaultProps = {
 | 
			
		||||
        side: Direction.Top,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
 | 
			
		||||
        this.state = {
 | 
			
		||||
            contentRect: null,
 | 
			
		||||
            visible: false,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentDidUpdate() {
 | 
			
		||||
        // Whenever this passthrough component updates, also render the tooltip
 | 
			
		||||
        // in a separate DOM tree. This allows the tooltip content to participate
 | 
			
		||||
        // the normal React rendering cycle: when this component re-renders, the
 | 
			
		||||
        // tooltip content re-renders.
 | 
			
		||||
        // Once we upgrade to React 16, this could be done a bit more naturally
 | 
			
		||||
        // using the portals feature instead.
 | 
			
		||||
        this.renderTooltip();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentWillUnmount() {
 | 
			
		||||
        document.removeEventListener("mousemove", this.onMouseMove);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private collectContentRect = (element: HTMLElement): void => {
 | 
			
		||||
        // We don't need to clean up when unmounting, so ignore
 | 
			
		||||
        if (!element) return;
 | 
			
		||||
 | 
			
		||||
        this.setState({
 | 
			
		||||
            contentRect: element.getBoundingClientRect(),
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private collectTarget = (element: HTMLElement) => {
 | 
			
		||||
        this.target = element;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private onLeftOfTarget(): boolean {
 | 
			
		||||
        const { contentRect } = this.state;
 | 
			
		||||
        const targetRect = this.target.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
        if (this.props.direction === Direction.Left) {
 | 
			
		||||
            const targetLeft = targetRect.left + window.pageXOffset;
 | 
			
		||||
            return !contentRect || (targetLeft - contentRect.width > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
 | 
			
		||||
        } else {
 | 
			
		||||
            const targetRight = targetRect.right + window.pageXOffset;
 | 
			
		||||
            const spaceOnRight = UIStore.instance.windowWidth - targetRight;
 | 
			
		||||
            return !contentRect || (spaceOnRight - contentRect.width < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private aboveTarget(): boolean {
 | 
			
		||||
        const { contentRect } = this.state;
 | 
			
		||||
        const targetRect = this.target.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
        if (this.props.direction === Direction.Top) {
 | 
			
		||||
            const targetTop = targetRect.top + window.pageYOffset;
 | 
			
		||||
            return !contentRect || (targetTop - contentRect.height > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
 | 
			
		||||
        } else {
 | 
			
		||||
            const targetBottom = targetRect.bottom + window.pageYOffset;
 | 
			
		||||
            const spaceBelow = UIStore.instance.windowHeight - targetBottom;
 | 
			
		||||
            return !contentRect || (spaceBelow - contentRect.height < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private get isOnTheSide(): boolean {
 | 
			
		||||
        return this.props.direction === Direction.Left || this.props.direction === Direction.Right;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private onMouseMove = (ev: MouseEvent) => {
 | 
			
		||||
        const { clientX: x, clientY: y } = ev;
 | 
			
		||||
        const { contentRect } = this.state;
 | 
			
		||||
        const targetRect = this.target.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
        let direction: Direction;
 | 
			
		||||
        if (this.isOnTheSide) {
 | 
			
		||||
            direction = this.onLeftOfTarget() ? Direction.Left : Direction.Right;
 | 
			
		||||
        } else {
 | 
			
		||||
            direction = this.aboveTarget() ? Direction.Top : Direction.Bottom;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!mouseWithinRegion(x, y, direction, targetRect, contentRect)) {
 | 
			
		||||
            this.hideTooltip();
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private onTargetMouseOver = (): void => {
 | 
			
		||||
        this.showTooltip();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private showTooltip(): void {
 | 
			
		||||
        // Don't enter visible state if we haven't collected the target yet
 | 
			
		||||
        if (!this.target) return;
 | 
			
		||||
 | 
			
		||||
        this.setState({
 | 
			
		||||
            visible: true,
 | 
			
		||||
        });
 | 
			
		||||
        this.props.onVisibilityChange?.(true);
 | 
			
		||||
        document.addEventListener("mousemove", this.onMouseMove);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public hideTooltip() {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            visible: false,
 | 
			
		||||
        });
 | 
			
		||||
        this.props.onVisibilityChange?.(false);
 | 
			
		||||
        document.removeEventListener("mousemove", this.onMouseMove);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private renderTooltip() {
 | 
			
		||||
        const { contentRect, visible } = this.state;
 | 
			
		||||
        if (!visible) {
 | 
			
		||||
            ReactDOM.unmountComponentAtNode(getOrCreateContainer());
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const targetRect = this.target.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
        // The window X and Y offsets are to adjust position when zoomed in to page
 | 
			
		||||
        const targetLeft = targetRect.left + window.pageXOffset;
 | 
			
		||||
        const targetRight = targetRect.right + window.pageXOffset;
 | 
			
		||||
        const targetBottom = targetRect.bottom + window.pageYOffset;
 | 
			
		||||
        const targetTop = targetRect.top + window.pageYOffset;
 | 
			
		||||
 | 
			
		||||
        // Place the tooltip above the target by default. If we find that the
 | 
			
		||||
        // tooltip content would extend past the safe area towards the window
 | 
			
		||||
        // edge, flip around to below the target.
 | 
			
		||||
        const position: Partial<IRect> = {};
 | 
			
		||||
        let chevronFace: ChevronFace = null;
 | 
			
		||||
        if (this.isOnTheSide) {
 | 
			
		||||
            if (this.onLeftOfTarget()) {
 | 
			
		||||
                position.left = targetLeft;
 | 
			
		||||
                chevronFace = ChevronFace.Right;
 | 
			
		||||
            } else {
 | 
			
		||||
                position.left = targetRight;
 | 
			
		||||
                chevronFace = ChevronFace.Left;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            position.top = targetTop;
 | 
			
		||||
        } else {
 | 
			
		||||
            if (this.aboveTarget()) {
 | 
			
		||||
                position.bottom = UIStore.instance.windowHeight - targetTop;
 | 
			
		||||
                chevronFace = ChevronFace.Bottom;
 | 
			
		||||
            } else {
 | 
			
		||||
                position.top = targetBottom;
 | 
			
		||||
                chevronFace = ChevronFace.Top;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Center the tooltip horizontally with the target's center.
 | 
			
		||||
            position.left = targetLeft + targetRect.width / 2;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const chevron = <div className={"mx_InteractiveTooltip_chevron_" + chevronFace} />;
 | 
			
		||||
 | 
			
		||||
        const menuClasses = classNames({
 | 
			
		||||
            'mx_InteractiveTooltip': true,
 | 
			
		||||
            'mx_InteractiveTooltip_withChevron_top': chevronFace === ChevronFace.Top,
 | 
			
		||||
            'mx_InteractiveTooltip_withChevron_left': chevronFace === ChevronFace.Left,
 | 
			
		||||
            'mx_InteractiveTooltip_withChevron_right': chevronFace === ChevronFace.Right,
 | 
			
		||||
            'mx_InteractiveTooltip_withChevron_bottom': chevronFace === ChevronFace.Bottom,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const menuStyle: CSSProperties = {};
 | 
			
		||||
        if (contentRect && !this.isOnTheSide) {
 | 
			
		||||
            menuStyle.left = `-${contentRect.width / 2}px`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const tooltip = <div className="mx_InteractiveTooltip_wrapper" style={{ ...position }}>
 | 
			
		||||
            <div className={menuClasses} style={menuStyle} ref={this.collectContentRect}>
 | 
			
		||||
                { chevron }
 | 
			
		||||
                { this.props.content }
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>;
 | 
			
		||||
 | 
			
		||||
        ReactDOM.render(tooltip, getOrCreateContainer());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return this.props.children({
 | 
			
		||||
            ref: this.collectTarget,
 | 
			
		||||
            onMouseOver: this.onTargetMouseOver,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,73 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2021 The Matrix.org Foundation C.I.C.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import React, { useRef } from "react";
 | 
			
		||||
 | 
			
		||||
import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore";
 | 
			
		||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
 | 
			
		||||
import { MenuItem } from "../../structures/ContextMenu";
 | 
			
		||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
 | 
			
		||||
import { _t } from "../../../languageHandler";
 | 
			
		||||
import RoomAvatar from "../avatars/RoomAvatar";
 | 
			
		||||
import dis from "../../../dispatcher/dispatcher";
 | 
			
		||||
import InteractiveTooltip, { Direction } from "../elements/InteractiveTooltip";
 | 
			
		||||
import { roomContextDetailsText } from "../../../Rooms";
 | 
			
		||||
 | 
			
		||||
const RecentlyViewedButton = () => {
 | 
			
		||||
    const tooltipRef = useRef<InteractiveTooltip>();
 | 
			
		||||
    const crumbs = useEventEmitterState(BreadcrumbsStore.instance, UPDATE_EVENT, () => BreadcrumbsStore.instance.rooms);
 | 
			
		||||
 | 
			
		||||
    const content = <div className="mx_RecentlyViewedButton_ContextMenu">
 | 
			
		||||
        <h4>{ _t("Recently viewed") }</h4>
 | 
			
		||||
        <div>
 | 
			
		||||
            { crumbs.map(crumb => {
 | 
			
		||||
                const contextDetails = roomContextDetailsText(crumb);
 | 
			
		||||
 | 
			
		||||
                return <MenuItem
 | 
			
		||||
                    key={crumb.roomId}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                        dis.dispatch({
 | 
			
		||||
                            action: "view_room",
 | 
			
		||||
                            room_id: crumb.roomId,
 | 
			
		||||
                        });
 | 
			
		||||
                        tooltipRef.current?.hideTooltip();
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    <RoomAvatar room={crumb} width={24} height={24} />
 | 
			
		||||
                    <span className="mx_RecentlyViewedButton_entry_label">
 | 
			
		||||
                        <div>{ crumb.name }</div>
 | 
			
		||||
                        { contextDetails && <div className="mx_RecentlyViewedButton_entry_spaces">
 | 
			
		||||
                            { contextDetails }
 | 
			
		||||
                        </div> }
 | 
			
		||||
                    </span>
 | 
			
		||||
                </MenuItem>;
 | 
			
		||||
            }) }
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>;
 | 
			
		||||
 | 
			
		||||
    return <InteractiveTooltip content={content} direction={Direction.Right} ref={tooltipRef}>
 | 
			
		||||
        { ({ ref, onMouseOver }) => (
 | 
			
		||||
            <span
 | 
			
		||||
                className="mx_LeftPanel_recentsButton"
 | 
			
		||||
                title={_t("Recently viewed")}
 | 
			
		||||
                ref={ref}
 | 
			
		||||
                onMouseOver={onMouseOver}
 | 
			
		||||
            />
 | 
			
		||||
        ) }
 | 
			
		||||
    </InteractiveTooltip>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default RecentlyViewedButton;
 | 
			
		||||
| 
						 | 
				
			
			@ -332,10 +332,12 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
 | 
			
		|||
            <div className="mx_SettingsTab mx_PreferencesUserSettingsTab">
 | 
			
		||||
                <div className="mx_SettingsTab_heading">{ _t("Preferences") }</div>
 | 
			
		||||
 | 
			
		||||
                <div className="mx_SettingsTab_section">
 | 
			
		||||
                    <span className="mx_SettingsTab_subheading">{ _t("Room list") }</span>
 | 
			
		||||
                    { this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
 | 
			
		||||
                </div>
 | 
			
		||||
                { !SettingsStore.getValue("feature_breadcrumbs_v2") &&
 | 
			
		||||
                    <div className="mx_SettingsTab_section">
 | 
			
		||||
                        <span className="mx_SettingsTab_subheading">{ _t("Room list") }</span>
 | 
			
		||||
                        { this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
 | 
			
		||||
                    </div>
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                <div className="mx_SettingsTab_section">
 | 
			
		||||
                    <span className="mx_SettingsTab_subheading">{ _t("Spaces") }</span>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -850,6 +850,7 @@
 | 
			
		|||
    "Show info about bridges in room settings": "Show info about bridges in room settings",
 | 
			
		||||
    "New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)",
 | 
			
		||||
    "Meta Spaces": "Meta Spaces",
 | 
			
		||||
    "Use new room breadcrumbs": "Use new room breadcrumbs",
 | 
			
		||||
    "Don't send read receipts": "Don't send read receipts",
 | 
			
		||||
    "Font size": "Font size",
 | 
			
		||||
    "Use custom size": "Use custom size",
 | 
			
		||||
| 
						 | 
				
			
			@ -1693,6 +1694,7 @@
 | 
			
		|||
    "Unknown": "Unknown",
 | 
			
		||||
    "Seen by %(userName)s at %(dateTime)s": "Seen by %(userName)s at %(dateTime)s",
 | 
			
		||||
    "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
 | 
			
		||||
    "Recently viewed": "Recently viewed",
 | 
			
		||||
    "Replying": "Replying",
 | 
			
		||||
    "Room %(name)s": "Room %(name)s",
 | 
			
		||||
    "Recently visited rooms": "Recently visited rooms",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -355,6 +355,13 @@ export const SETTINGS: {[setting: string]: ISetting} = {
 | 
			
		|||
            new ReloadOnChangeController(),
 | 
			
		||||
        ]),
 | 
			
		||||
    },
 | 
			
		||||
    "feature_breadcrumbs_v2": {
 | 
			
		||||
        isFeature: true,
 | 
			
		||||
        labsGroup: LabGroup.Rooms,
 | 
			
		||||
        supportedLevels: LEVELS_FEATURE,
 | 
			
		||||
        displayName: _td("Use new room breadcrumbs"),
 | 
			
		||||
        default: false,
 | 
			
		||||
    },
 | 
			
		||||
    "RoomList.backgroundImage": {
 | 
			
		||||
        supportedLevels: LEVELS_ACCOUNT_SETTINGS,
 | 
			
		||||
        default: null,
 | 
			
		||||
| 
						 | 
				
			
			@ -711,6 +718,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
 | 
			
		|||
        supportedLevels: LEVELS_ACCOUNT_SETTINGS,
 | 
			
		||||
        displayName: _td("Show shortcuts to recently viewed rooms above the room list"),
 | 
			
		||||
        default: true,
 | 
			
		||||
        controller: new IncompatibleController("feature_breadcrumbs_v2", true),
 | 
			
		||||
    },
 | 
			
		||||
    "showHiddenEventsInTimeline": {
 | 
			
		||||
        displayName: _td("Show hidden events in timeline"),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2020 The Matrix.org Foundation C.I.C.
 | 
			
		||||
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
| 
						 | 
				
			
			@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import SettingsStore from "../settings/SettingsStore";
 | 
			
		||||
import { Room } from "matrix-js-sdk/src/models/room";
 | 
			
		||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
 | 
			
		||||
 | 
			
		||||
import SettingsStore from "../settings/SettingsStore";
 | 
			
		||||
import { ActionPayload } from "../dispatcher/payloads";
 | 
			
		||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
 | 
			
		||||
import defaultDispatcher from "../dispatcher/dispatcher";
 | 
			
		||||
import { arrayHasDiff } from "../utils/arrays";
 | 
			
		||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
 | 
			
		||||
import { SettingLevel } from "../settings/SettingLevel";
 | 
			
		||||
import SpaceStore from "./spaces/SpaceStore";
 | 
			
		||||
import { Action } from "../dispatcher/actions";
 | 
			
		||||
import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -44,6 +44,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
 | 
			
		|||
 | 
			
		||||
        SettingsStore.monitorSetting("breadcrumb_rooms", null);
 | 
			
		||||
        SettingsStore.monitorSetting("breadcrumbs", null);
 | 
			
		||||
        SettingsStore.monitorSetting("feature_breadcrumbs_v2", null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static get instance(): BreadcrumbsStore {
 | 
			
		||||
| 
						 | 
				
			
			@ -58,8 +59,9 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
 | 
			
		|||
        return this.state.enabled && this.meetsRoomRequirement;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private get meetsRoomRequirement(): boolean {
 | 
			
		||||
        return this.matrixClient && this.matrixClient.getVisibleRooms().length >= 20;
 | 
			
		||||
    public get meetsRoomRequirement(): boolean {
 | 
			
		||||
        if (SettingsStore.getValue("feature_breadcrumbs_v2")) return true;
 | 
			
		||||
        return this.matrixClient?.getVisibleRooms().length >= 20;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async onAction(payload: ActionPayload) {
 | 
			
		||||
| 
						 | 
				
			
			@ -69,7 +71,9 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
 | 
			
		|||
            const settingUpdatedPayload = payload as SettingUpdatedPayload;
 | 
			
		||||
            if (settingUpdatedPayload.settingName === 'breadcrumb_rooms') {
 | 
			
		||||
                await this.updateRooms();
 | 
			
		||||
            } else if (settingUpdatedPayload.settingName === 'breadcrumbs') {
 | 
			
		||||
            } else if (settingUpdatedPayload.settingName === 'breadcrumbs' ||
 | 
			
		||||
                settingUpdatedPayload.settingName === 'feature_breadcrumbs_v2'
 | 
			
		||||
            ) {
 | 
			
		||||
                await this.updateState({ enabled: SettingsStore.getValue("breadcrumbs", null) });
 | 
			
		||||
            }
 | 
			
		||||
        } else if (payload.action === Action.ViewRoom) {
 | 
			
		||||
| 
						 | 
				
			
			@ -126,7 +130,6 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    private async appendRoom(room: Room) {
 | 
			
		||||
        if (SpaceStore.spacesEnabled && room.isSpaceRoom()) return; // hide space rooms
 | 
			
		||||
        let updated = false;
 | 
			
		||||
        const rooms = (this.state.rooms || []).slice(); // cheap clone
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,162 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2021 The Matrix.org Foundation C.I.C.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import '../../../skinned-sdk';
 | 
			
		||||
import { Direction, mouseWithinRegion } from "../../../../src/components/views/elements/InteractiveTooltip";
 | 
			
		||||
 | 
			
		||||
describe("InteractiveTooltip", () => {
 | 
			
		||||
    describe("mouseWithinRegion", () => {
 | 
			
		||||
        it("direction=left", () => {
 | 
			
		||||
            const targetRect = {
 | 
			
		||||
                width: 20,
 | 
			
		||||
                height: 20,
 | 
			
		||||
                top: 300,
 | 
			
		||||
                right: 370,
 | 
			
		||||
                bottom: 320,
 | 
			
		||||
                left: 350,
 | 
			
		||||
            } as DOMRect;
 | 
			
		||||
 | 
			
		||||
            const contentRect = {
 | 
			
		||||
                width: 100,
 | 
			
		||||
                height: 400,
 | 
			
		||||
                top: 100,
 | 
			
		||||
                right: 200,
 | 
			
		||||
                bottom: 500,
 | 
			
		||||
                left: 100,
 | 
			
		||||
            } as DOMRect;
 | 
			
		||||
 | 
			
		||||
            // just within top left corner of contentRect
 | 
			
		||||
            expect(mouseWithinRegion(101, 101, Direction.Left, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // just outside top left corner of contentRect, within buffer
 | 
			
		||||
            expect(mouseWithinRegion(101, 90, Direction.Left, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // just within top right corner of targetRect
 | 
			
		||||
            expect(mouseWithinRegion(369, 301, Direction.Left, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // within the top triangular portion of the trapezoid
 | 
			
		||||
            expect(mouseWithinRegion(300, 200, Direction.Left, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // within the bottom triangular portion of the trapezoid
 | 
			
		||||
            expect(mouseWithinRegion(300, 350, Direction.Left, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // outside the top triangular portion of the trapezoid
 | 
			
		||||
            expect(mouseWithinRegion(300, 140, Direction.Left, targetRect, contentRect)).toBe(false);
 | 
			
		||||
            // outside the bottom triangular portion of the trapezoid
 | 
			
		||||
            expect(mouseWithinRegion(300, 460, Direction.Left, targetRect, contentRect)).toBe(false);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("direction=right", () => {
 | 
			
		||||
            const targetRect = {
 | 
			
		||||
                width: 20,
 | 
			
		||||
                height: 20,
 | 
			
		||||
                top: 300,
 | 
			
		||||
                right: 370,
 | 
			
		||||
                bottom: 320,
 | 
			
		||||
                left: 350,
 | 
			
		||||
            } as DOMRect;
 | 
			
		||||
 | 
			
		||||
            const contentRect = {
 | 
			
		||||
                width: 100,
 | 
			
		||||
                height: 400,
 | 
			
		||||
                top: 100,
 | 
			
		||||
                right: 620,
 | 
			
		||||
                bottom: 500,
 | 
			
		||||
                left: 520,
 | 
			
		||||
            } as DOMRect;
 | 
			
		||||
 | 
			
		||||
            // just within top right corner of contentRect
 | 
			
		||||
            expect(mouseWithinRegion(619, 101, Direction.Right, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // just outside top right corner of contentRect, within buffer
 | 
			
		||||
            expect(mouseWithinRegion(619, 90, Direction.Right, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // just within top left corner of targetRect
 | 
			
		||||
            expect(mouseWithinRegion(351, 301, Direction.Right, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // within the top triangular portion of the trapezoid
 | 
			
		||||
            expect(mouseWithinRegion(420, 200, Direction.Right, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // within the bottom triangular portion of the trapezoid
 | 
			
		||||
            expect(mouseWithinRegion(420, 350, Direction.Right, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // outside the top triangular portion of the trapezoid
 | 
			
		||||
            expect(mouseWithinRegion(420, 140, Direction.Right, targetRect, contentRect)).toBe(false);
 | 
			
		||||
            // outside the bottom triangular portion of the trapezoid
 | 
			
		||||
            expect(mouseWithinRegion(420, 460, Direction.Right, targetRect, contentRect)).toBe(false);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("direction=top", () => {
 | 
			
		||||
            const targetRect = {
 | 
			
		||||
                width: 20,
 | 
			
		||||
                height: 20,
 | 
			
		||||
                top: 300,
 | 
			
		||||
                right: 370,
 | 
			
		||||
                bottom: 320,
 | 
			
		||||
                left: 350,
 | 
			
		||||
            } as DOMRect;
 | 
			
		||||
 | 
			
		||||
            const contentRect = {
 | 
			
		||||
                width: 400,
 | 
			
		||||
                height: 100,
 | 
			
		||||
                top: 100,
 | 
			
		||||
                right: 550,
 | 
			
		||||
                bottom: 200,
 | 
			
		||||
                left: 150,
 | 
			
		||||
            } as DOMRect;
 | 
			
		||||
 | 
			
		||||
            // just within top right corner of contentRect
 | 
			
		||||
            expect(mouseWithinRegion(549, 101, Direction.Top, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // just outside top right corner of contentRect, within buffer
 | 
			
		||||
            expect(mouseWithinRegion(549, 99, Direction.Top, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // just within bottom left corner of targetRect
 | 
			
		||||
            expect(mouseWithinRegion(351, 319, Direction.Top, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // within the left triangular portion of the trapezoid
 | 
			
		||||
            expect(mouseWithinRegion(240, 260, Direction.Top, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // within the right triangular portion of the trapezoid
 | 
			
		||||
            expect(mouseWithinRegion(480, 260, Direction.Top, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // outside the left triangular portion of the trapezoid
 | 
			
		||||
            expect(mouseWithinRegion(220, 260, Direction.Top, targetRect, contentRect)).toBe(false);
 | 
			
		||||
            // outside the right triangular portion of the trapezoid
 | 
			
		||||
            expect(mouseWithinRegion(500, 260, Direction.Top, targetRect, contentRect)).toBe(false);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("direction=bottom", () => {
 | 
			
		||||
            const targetRect = {
 | 
			
		||||
                width: 20,
 | 
			
		||||
                height: 20,
 | 
			
		||||
                top: 300,
 | 
			
		||||
                right: 370,
 | 
			
		||||
                bottom: 320,
 | 
			
		||||
                left: 350,
 | 
			
		||||
            } as DOMRect;
 | 
			
		||||
 | 
			
		||||
            const contentRect = {
 | 
			
		||||
                width: 400,
 | 
			
		||||
                height: 100,
 | 
			
		||||
                top: 420,
 | 
			
		||||
                right: 550,
 | 
			
		||||
                bottom: 520,
 | 
			
		||||
                left: 150,
 | 
			
		||||
            } as DOMRect;
 | 
			
		||||
 | 
			
		||||
            // just within bottom left corner of contentRect
 | 
			
		||||
            expect(mouseWithinRegion(101, 519, Direction.Bottom, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // just outside bottom left corner of contentRect, within buffer
 | 
			
		||||
            expect(mouseWithinRegion(101, 521, Direction.Bottom, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // just within top left corner of targetRect
 | 
			
		||||
            expect(mouseWithinRegion(351, 301, Direction.Bottom, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // within the left triangular portion of the trapezoid
 | 
			
		||||
            expect(mouseWithinRegion(240, 360, Direction.Bottom, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // within the right triangular portion of the trapezoid
 | 
			
		||||
            expect(mouseWithinRegion(480, 360, Direction.Bottom, targetRect, contentRect)).toBe(true);
 | 
			
		||||
            // outside the left triangular portion of the trapezoid
 | 
			
		||||
            expect(mouseWithinRegion(220, 360, Direction.Bottom, targetRect, contentRect)).toBe(false);
 | 
			
		||||
            // outside the right triangular portion of the trapezoid
 | 
			
		||||
            expect(mouseWithinRegion(500, 360, Direction.Bottom, targetRect, contentRect)).toBe(false);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
		Loading…
	
		Reference in New Issue