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