Fix issues with the new topic dialog (#8608)
This commit is contained in:
parent
e1d11db256
commit
fb30b67b14
8 changed files with 112 additions and 51 deletions
|
@ -142,6 +142,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_RoomTopic {
|
.mx_RoomTopic {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomHeader_topic {
|
.mx_RoomHeader_topic {
|
||||||
|
|
|
@ -14,26 +14,31 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useLayoutEffect, useRef } from "react";
|
||||||
import linkifyElement from "linkify-element";
|
|
||||||
|
import { linkifyElement } from "../../../HtmlUtils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
as?: string;
|
as?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
onClick?: (ev: MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Linkify({
|
export function Linkify({
|
||||||
as = "div",
|
as = "div",
|
||||||
children,
|
children,
|
||||||
|
onClick,
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
|
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
linkifyElement(ref.current);
|
linkifyElement(ref.current);
|
||||||
}, [children]);
|
}, [children]);
|
||||||
|
|
||||||
return React.createElement(as, {
|
return React.createElement(as, {
|
||||||
children,
|
children,
|
||||||
ref,
|
ref,
|
||||||
|
onClick,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,15 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useContext, useEffect, useRef } from "react";
|
import React, { useCallback, useContext, useRef } from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
import { linkifyElement } from "../../../HtmlUtils";
|
|
||||||
import { useTopic } from "../../../hooks/room/useTopic";
|
import { useTopic } from "../../../hooks/room/useTopic";
|
||||||
import useHover from "../../../hooks/useHover";
|
import { Alignment } from "./Tooltip";
|
||||||
import Tooltip, { Alignment } from "./Tooltip";
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
@ -32,6 +30,7 @@ import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import AccessibleButton from "./AccessibleButton";
|
import AccessibleButton from "./AccessibleButton";
|
||||||
import { Linkify } from "./Linkify";
|
import { Linkify } from "./Linkify";
|
||||||
|
import TooltipTarget from "./TooltipTarget";
|
||||||
|
|
||||||
interface IProps extends React.HTMLProps<HTMLDivElement> {
|
interface IProps extends React.HTMLProps<HTMLDivElement> {
|
||||||
room?: Room;
|
room?: Room;
|
||||||
|
@ -43,7 +42,6 @@ export default function RoomTopic({
|
||||||
}: IProps) {
|
}: IProps) {
|
||||||
const client = useContext(MatrixClientContext);
|
const client = useContext(MatrixClientContext);
|
||||||
const ref = useRef<HTMLDivElement>();
|
const ref = useRef<HTMLDivElement>();
|
||||||
const hovered = useHover(ref);
|
|
||||||
|
|
||||||
const topic = useTopic(room);
|
const topic = useTopic(room);
|
||||||
|
|
||||||
|
@ -57,6 +55,10 @@ export default function RoomTopic({
|
||||||
dis.fire(Action.ShowRoomTopic);
|
dis.fire(Action.ShowRoomTopic);
|
||||||
}, [props]);
|
}, [props]);
|
||||||
|
|
||||||
|
const ignoreHover = (ev: React.MouseEvent): boolean => {
|
||||||
|
return (ev.target as HTMLElement).tagName.toUpperCase() === "A";
|
||||||
|
};
|
||||||
|
|
||||||
useDispatcher(dis, (payload) => {
|
useDispatcher(dis, (payload) => {
|
||||||
if (payload.action === Action.ShowRoomTopic) {
|
if (payload.action === Action.ShowRoomTopic) {
|
||||||
const canSetTopic = room.currentState.maySendStateEvent(EventType.RoomTopic, client.getUserId());
|
const canSetTopic = room.currentState.maySendStateEvent(EventType.RoomTopic, client.getUserId());
|
||||||
|
@ -64,7 +66,16 @@ export default function RoomTopic({
|
||||||
const modal = Modal.createDialog(InfoDialog, {
|
const modal = Modal.createDialog(InfoDialog, {
|
||||||
title: room.name,
|
title: room.name,
|
||||||
description: <div>
|
description: <div>
|
||||||
<Linkify as="p">{ topic }</Linkify>
|
<Linkify
|
||||||
|
as="p"
|
||||||
|
onClick={(ev: MouseEvent) => {
|
||||||
|
if ((ev.target as HTMLElement).tagName.toUpperCase() === "A") {
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ topic }
|
||||||
|
</Linkify>
|
||||||
{ canSetTopic && <AccessibleButton
|
{ canSetTopic && <AccessibleButton
|
||||||
kind="primary_outline"
|
kind="primary_outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -80,10 +91,6 @@ export default function RoomTopic({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
linkifyElement(ref.current);
|
|
||||||
}, [topic]);
|
|
||||||
|
|
||||||
const className = classNames(props.className, "mx_RoomTopic");
|
const className = classNames(props.className, "mx_RoomTopic");
|
||||||
|
|
||||||
return <div {...props}
|
return <div {...props}
|
||||||
|
@ -92,9 +99,10 @@ export default function RoomTopic({
|
||||||
dir="auto"
|
dir="auto"
|
||||||
className={className}
|
className={className}
|
||||||
>
|
>
|
||||||
{ topic }
|
<TooltipTarget label={_t("Click to read topic")} alignment={Alignment.Bottom} ignoreHover={ignoreHover}>
|
||||||
{ hovered && (
|
<Linkify>
|
||||||
<Tooltip label={_t("Click to read topic")} alignment={Alignment.Bottom} />
|
{ topic }
|
||||||
) }
|
</Linkify>
|
||||||
|
</TooltipTarget>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,12 +14,15 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, HTMLAttributes } from 'react';
|
import React, { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
import useFocus from "../../../hooks/useFocus";
|
||||||
|
import useHover from "../../../hooks/useHover";
|
||||||
import Tooltip, { ITooltipProps } from './Tooltip';
|
import Tooltip, { ITooltipProps } from './Tooltip';
|
||||||
|
|
||||||
interface IProps extends HTMLAttributes<HTMLSpanElement>, Omit<ITooltipProps, 'visible'> {
|
interface IProps extends HTMLAttributes<HTMLSpanElement>, Omit<ITooltipProps, 'visible'> {
|
||||||
tooltipTargetClassName?: string;
|
tooltipTargetClassName?: string;
|
||||||
|
ignoreHover?: (ev: React.MouseEvent) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,34 +39,31 @@ const TooltipTarget: React.FC<IProps> = ({
|
||||||
alignment,
|
alignment,
|
||||||
tooltipClassName,
|
tooltipClassName,
|
||||||
maxParentWidth,
|
maxParentWidth,
|
||||||
|
ignoreHover,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isFocused, focusProps] = useFocus();
|
||||||
|
const [isHovering, hoverProps] = useHover(ignoreHover);
|
||||||
const show = () => setIsVisible(true);
|
|
||||||
const hide = () => setIsVisible(false);
|
|
||||||
|
|
||||||
// No need to fill up the DOM with hidden tooltip elements. Only add the
|
// No need to fill up the DOM with hidden tooltip elements. Only add the
|
||||||
// tooltip when we're hovering over the item (performance)
|
// tooltip when we're hovering over the item (performance)
|
||||||
const tooltip = isVisible && <Tooltip
|
const tooltip = (isFocused || isHovering) && <Tooltip
|
||||||
id={id}
|
id={id}
|
||||||
className={className}
|
className={className}
|
||||||
tooltipClassName={tooltipClassName}
|
tooltipClassName={tooltipClassName}
|
||||||
label={label}
|
label={label}
|
||||||
alignment={alignment}
|
alignment={alignment}
|
||||||
visible={isVisible}
|
visible={isFocused || isHovering}
|
||||||
maxParentWidth={maxParentWidth}
|
maxParentWidth={maxParentWidth}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
{...hoverProps}
|
||||||
|
{...focusProps}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-describedby={id}
|
aria-describedby={id}
|
||||||
className={tooltipTargetClassName}
|
className={tooltipTargetClassName}
|
||||||
onMouseOver={show}
|
|
||||||
onMouseLeave={hide}
|
|
||||||
onFocus={show}
|
|
||||||
onBlur={hide}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{ children }
|
{ children }
|
||||||
|
|
29
src/hooks/useFocus.ts
Normal file
29
src/hooks/useFocus.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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 { useState } from "react";
|
||||||
|
|
||||||
|
export default function useFocus(
|
||||||
|
): [boolean, {onFocus: () => void, onBlur: () => void}] {
|
||||||
|
const [focused, setFocused] = useState(false);
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
onFocus: () => setFocused(true),
|
||||||
|
onBlur: () => setFocused(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
return [focused, props];
|
||||||
|
}
|
|
@ -14,29 +14,20 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function useHover(ref: React.MutableRefObject<HTMLElement>) {
|
export default function useHover(
|
||||||
|
ignoreHover?: (ev: React.MouseEvent) => boolean,
|
||||||
|
): [boolean, { onMouseOver: () => void, onMouseLeave: () => void, onMouseMove: (ev: React.MouseEvent) => void }] {
|
||||||
const [hovered, setHoverState] = useState(false);
|
const [hovered, setHoverState] = useState(false);
|
||||||
|
|
||||||
const handleMouseOver = () => setHoverState(true);
|
const props = {
|
||||||
const handleMouseOut = () => setHoverState(false);
|
onMouseOver: () => setHoverState(true),
|
||||||
|
onMouseLeave: () => setHoverState(false),
|
||||||
useEffect(
|
onMouseMove: (ev: React.MouseEvent): void => {
|
||||||
() => {
|
setHoverState(!ignoreHover(ev));
|
||||||
const node = ref.current;
|
|
||||||
if (node) {
|
|
||||||
node.addEventListener("mouseover", handleMouseOver);
|
|
||||||
node.addEventListener("mouseout", handleMouseOut);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
node.removeEventListener("mouseover", handleMouseOver);
|
|
||||||
node.removeEventListener("mouseout", handleMouseOut);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[ref],
|
};
|
||||||
);
|
|
||||||
|
|
||||||
return hovered;
|
return [hovered, props];
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,23 @@ describe("Linkify", () => {
|
||||||
const wrapper = mount(<Linkify>
|
const wrapper = mount(<Linkify>
|
||||||
https://perdu.com
|
https://perdu.com
|
||||||
</Linkify>);
|
</Linkify>);
|
||||||
expect(wrapper.html()).toBe('<div><a href="https://perdu.com">https://perdu.com</a></div>');
|
expect(wrapper.html()).toBe(
|
||||||
|
"<div><a href=\"https://perdu.com\" class=\"linkified\" target=\"_blank\" rel=\"noreferrer noopener\">"+
|
||||||
|
"https://perdu.com" +
|
||||||
|
"</a></div>",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correctly linkifies a room alias", () => {
|
||||||
|
const wrapper = mount(<Linkify>
|
||||||
|
#element-web:matrix.org
|
||||||
|
</Linkify>);
|
||||||
|
expect(wrapper.html()).toBe(
|
||||||
|
"<div>" +
|
||||||
|
"<a href=\"https://matrix.to/#/#element-web:matrix.org\" class=\"linkified\" rel=\"noreferrer noopener\">" +
|
||||||
|
"#element-web:matrix.org" +
|
||||||
|
"</a></div>",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("changes the root tag name", () => {
|
it("changes the root tag name", () => {
|
||||||
|
@ -55,10 +71,20 @@ describe("Linkify", () => {
|
||||||
|
|
||||||
const wrapper = mount(<DummyTest />);
|
const wrapper = mount(<DummyTest />);
|
||||||
|
|
||||||
expect(wrapper.html()).toBe('<div><div><a href="https://perdu.com">https://perdu.com</a></div></div>');
|
expect(wrapper.html()).toBe(
|
||||||
|
"<div><div>" +
|
||||||
|
"<a href=\"https://perdu.com\" class=\"linkified\" target=\"_blank\" rel=\"noreferrer noopener\">" +
|
||||||
|
"https://perdu.com" +
|
||||||
|
"</a></div></div>",
|
||||||
|
);
|
||||||
|
|
||||||
wrapper.find('div').at(0).simulate('click');
|
wrapper.find('div').at(0).simulate('click');
|
||||||
|
|
||||||
expect(wrapper.html()).toBe('<div><div><a href="https://matrix.org">https://matrix.org</a></div></div>');
|
expect(wrapper.html()).toBe(
|
||||||
|
"<div><div>" +
|
||||||
|
"<a href=\"https://matrix.org\" class=\"linkified\" target=\"_blank\" rel=\"noreferrer noopener\">" +
|
||||||
|
"https://matrix.org" +
|
||||||
|
"</a></div></div>",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -95,6 +95,7 @@ exports[`MLocationBody <MLocationBody> without error renders map correctly 1`] =
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
|
onMouseMove={[Function]}
|
||||||
onMouseOver={[Function]}
|
onMouseOver={[Function]}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
|
|
Loading…
Reference in a new issue