mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-01 21:25:18 +00:00
modal impl
This commit is contained in:
@@ -3,17 +3,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.reactionPopupMenu {
|
.reactionPopupMenu {
|
||||||
|
background: var(--cpd-color-bg-canvas-default);
|
||||||
|
border-radius: var(--cpd-space-4x);
|
||||||
|
width: fit-content;
|
||||||
|
top: 70vh;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: var(--cpd-color-bg-canvas-default);
|
border: none;
|
||||||
top: -8em;
|
|
||||||
border-radius: var(--cpd-space-4x);
|
|
||||||
display: flex;
|
|
||||||
width: fit-content;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.reactionPopupMenu menu {
|
.reactionPopupMenu menu {
|
||||||
|
|||||||
@@ -16,15 +16,20 @@ import {
|
|||||||
SearchIcon,
|
SearchIcon,
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
RaisedHandSolidIcon,
|
RaisedHandSolidIcon,
|
||||||
|
ReactionIcon,
|
||||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
import {
|
import {
|
||||||
ChangeEventHandler,
|
ChangeEventHandler,
|
||||||
ComponentPropsWithoutRef,
|
ComponentPropsWithoutRef,
|
||||||
FC,
|
FC,
|
||||||
|
forwardRef,
|
||||||
KeyboardEventHandler,
|
KeyboardEventHandler,
|
||||||
|
PropsWithRef,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -45,9 +50,10 @@ import {
|
|||||||
|
|
||||||
interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
|
interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||||
raised: boolean;
|
raised: boolean;
|
||||||
|
open: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InnerButton: FC<InnerButtonProps> = ({ raised, ...props }) => {
|
const InnerButton: FC<InnerButtonProps> = ({ raised, open, ...props }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -55,26 +61,26 @@ const InnerButton: FC<InnerButtonProps> = ({ raised, ...props }) => {
|
|||||||
<CpdButton
|
<CpdButton
|
||||||
className={classNames(raised && styles.raisedButton)}
|
className={classNames(raised && styles.raisedButton)}
|
||||||
aria-expanded={raised}
|
aria-expanded={raised}
|
||||||
kind={raised ? "primary" : "secondary"}
|
kind={raised || open ? "primary" : "secondary"}
|
||||||
iconOnly
|
iconOnly
|
||||||
Icon={RaisedHandSolidIcon}
|
Icon={raised ? RaisedHandSolidIcon : ReactionIcon}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ReactionPopupMenu({
|
interface ReactionsPopupMenuProps {
|
||||||
sendReaction,
|
|
||||||
toggleRaisedHand,
|
|
||||||
isHandRaised,
|
|
||||||
canReact,
|
|
||||||
}: {
|
|
||||||
sendReaction: (reaction: ReactionOption) => void;
|
sendReaction: (reaction: ReactionOption) => void;
|
||||||
toggleRaisedHand: () => void;
|
toggleRaisedHand: () => void;
|
||||||
isHandRaised: boolean;
|
isHandRaised: boolean;
|
||||||
canReact: boolean;
|
canReact: boolean;
|
||||||
}): ReactNode {
|
}
|
||||||
|
|
||||||
|
export const ReactionPopupMenu = forwardRef<
|
||||||
|
HTMLDialogElement,
|
||||||
|
ReactionsPopupMenuProps
|
||||||
|
>(({ sendReaction, toggleRaisedHand, isHandRaised, canReact }, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
@@ -116,82 +122,84 @@ export function ReactionPopupMenu({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.reactionPopupMenu}>
|
<dialog ref={ref} className={styles.reactionPopupMenu} open={false}>
|
||||||
<section className={styles.handRaiseSection}>
|
<div style={{ display: "flex" }}>
|
||||||
<Tooltip label={t("common.raise_hand")}>
|
<section className={styles.handRaiseSection}>
|
||||||
<CpdButton
|
<Tooltip label={t("common.raise_hand")}>
|
||||||
kind={isHandRaised ? "primary" : "secondary"}
|
<CpdButton
|
||||||
aria-pressed={isHandRaised}
|
kind={isHandRaised ? "primary" : "secondary"}
|
||||||
aria-label="Toggle hand raised"
|
aria-pressed={isHandRaised}
|
||||||
className={styles.reactionButton}
|
aria-label="Toggle hand raised"
|
||||||
key="raise-hand"
|
className={styles.reactionButton}
|
||||||
onClick={() => toggleRaisedHand()}
|
key="raise-hand"
|
||||||
>
|
onClick={() => toggleRaisedHand()}
|
||||||
🖐️
|
>
|
||||||
</CpdButton>
|
🖐️
|
||||||
</Tooltip>
|
</CpdButton>
|
||||||
</section>
|
</Tooltip>
|
||||||
<div className={styles.verticalSeperator} />
|
</section>
|
||||||
<section>
|
<div className={styles.verticalSeperator} />
|
||||||
{isSearching ? (
|
<section>
|
||||||
<>
|
{isSearching ? (
|
||||||
<Form.Root className={styles.searchForm}>
|
<>
|
||||||
<Search
|
<Form.Root className={styles.searchForm}>
|
||||||
required
|
<Search
|
||||||
value={searchText}
|
required
|
||||||
name="reactionSearch"
|
value={searchText}
|
||||||
placeholder="Search reactions…"
|
name="reactionSearch"
|
||||||
onChange={onSearch}
|
placeholder="Search reactions…"
|
||||||
onKeyDown={onSearchKeyDown}
|
onChange={onSearch}
|
||||||
// This is a reasonable use of autofocus, we are focusing when
|
onKeyDown={onSearchKeyDown}
|
||||||
// the search button is clicked (which matches the Element Web reaction picker)
|
// This is a reasonable use of autofocus, we are focusing when
|
||||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
// the search button is clicked (which matches the Element Web reaction picker)
|
||||||
autoFocus
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||||
/>
|
autoFocus
|
||||||
<CpdButton
|
|
||||||
Icon={CloseIcon}
|
|
||||||
aria-label="close search"
|
|
||||||
size="sm"
|
|
||||||
kind="destructive"
|
|
||||||
onClick={() => setIsSearching(false)}
|
|
||||||
/>
|
|
||||||
</Form.Root>
|
|
||||||
<Separator />
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
<menu>
|
|
||||||
{filteredReactionSet.map((reaction) => (
|
|
||||||
<li className={styles.reactionPopupMenuItem} key={reaction.name}>
|
|
||||||
<Tooltip label={reaction.name}>
|
|
||||||
<CpdButton
|
|
||||||
kind="secondary"
|
|
||||||
className={styles.reactionButton}
|
|
||||||
disabled={!canReact}
|
|
||||||
onClick={() => sendReaction(reaction)}
|
|
||||||
>
|
|
||||||
{reaction.emoji}
|
|
||||||
</CpdButton>
|
|
||||||
</Tooltip>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
{!isSearching ? (
|
|
||||||
<li key="search" className={styles.reactionPopupMenuItem}>
|
|
||||||
<Tooltip label="Search">
|
|
||||||
<CpdButton
|
|
||||||
iconOnly
|
|
||||||
aria-label="Open reactions search"
|
|
||||||
Icon={SearchIcon}
|
|
||||||
kind="tertiary"
|
|
||||||
onClick={() => setIsSearching(true)}
|
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
<CpdButton
|
||||||
</li>
|
Icon={CloseIcon}
|
||||||
|
aria-label="close search"
|
||||||
|
size="sm"
|
||||||
|
kind="destructive"
|
||||||
|
onClick={() => setIsSearching(false)}
|
||||||
|
/>
|
||||||
|
</Form.Root>
|
||||||
|
<Separator />
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</menu>
|
<menu>
|
||||||
</section>
|
{filteredReactionSet.map((reaction) => (
|
||||||
</div>
|
<li className={styles.reactionPopupMenuItem} key={reaction.name}>
|
||||||
|
<Tooltip label={reaction.name}>
|
||||||
|
<CpdButton
|
||||||
|
kind="secondary"
|
||||||
|
className={styles.reactionButton}
|
||||||
|
disabled={!canReact}
|
||||||
|
onClick={() => sendReaction(reaction)}
|
||||||
|
>
|
||||||
|
{reaction.emoji}
|
||||||
|
</CpdButton>
|
||||||
|
</Tooltip>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{!isSearching ? (
|
||||||
|
<li key="search" className={styles.reactionPopupMenuItem}>
|
||||||
|
<Tooltip label="Search">
|
||||||
|
<CpdButton
|
||||||
|
iconOnly
|
||||||
|
aria-label="Open reactions search"
|
||||||
|
Icon={SearchIcon}
|
||||||
|
kind="tertiary"
|
||||||
|
onClick={() => setIsSearching(true)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
</menu>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
interface ReactionToggleButtonProps {
|
interface ReactionToggleButtonProps {
|
||||||
rtcSession: MatrixRTCSession;
|
rtcSession: MatrixRTCSession;
|
||||||
@@ -207,10 +215,37 @@ export function ReactionToggleButton({
|
|||||||
const userId = client.getUserId()!;
|
const userId = client.getUserId()!;
|
||||||
const isHandRaised = !!raisedHands[userId];
|
const isHandRaised = !!raisedHands[userId];
|
||||||
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
||||||
const [showReactionsMenu, setShowReactionsMenu] = useState(false);
|
const ref = useRef<HTMLDialogElement>(null);
|
||||||
|
|
||||||
const canReact = !reactions[userId];
|
const canReact = !reactions[userId];
|
||||||
|
|
||||||
|
const showReactionsMenu = useCallback(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.showModal();
|
||||||
|
}
|
||||||
|
}, [ref]);
|
||||||
|
|
||||||
|
const hideReactionsMenu = useCallback(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.close();
|
||||||
|
}
|
||||||
|
}, [ref]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ref.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
function onClick(evt: MouseEvent) {
|
||||||
|
if (evt.target === ref.current) {
|
||||||
|
hideReactionsMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ref.current.addEventListener("click", onClick);
|
||||||
|
return () => {
|
||||||
|
ref.current?.removeEventListener("click", onClick);
|
||||||
|
};
|
||||||
|
}, [ref]);
|
||||||
|
|
||||||
const sendRelation = useCallback(
|
const sendRelation = useCallback(
|
||||||
async (reaction: ReactionOption) => {
|
async (reaction: ReactionOption) => {
|
||||||
const myMembership = memberships.find((m) => m.sender === userId);
|
const myMembership = memberships.find((m) => m.sender === userId);
|
||||||
@@ -238,6 +273,7 @@ export function ReactionToggleButton({
|
|||||||
logger.error("Failed to send reaction", ex);
|
logger.error("Failed to send reaction", ex);
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
|
hideReactionsMenu();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[memberships, client, userId, rtcSession],
|
[memberships, client, userId, rtcSession],
|
||||||
@@ -277,7 +313,7 @@ export function ReactionToggleButton({
|
|||||||
logger.error("Failed to send reaction event", ex);
|
logger.error("Failed to send reaction event", ex);
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
setShowReactionsMenu(false);
|
hideReactionsMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -296,17 +332,17 @@ export function ReactionToggleButton({
|
|||||||
<>
|
<>
|
||||||
<InnerButton
|
<InnerButton
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={() => setShowReactionsMenu((show) => !show)}
|
onClick={showReactionsMenu}
|
||||||
raised={isHandRaised || showReactionsMenu}
|
raised={isHandRaised}
|
||||||
|
open={!!ref.current?.open}
|
||||||
|
/>
|
||||||
|
<ReactionPopupMenu
|
||||||
|
ref={ref}
|
||||||
|
isHandRaised={isHandRaised}
|
||||||
|
canReact={canReact}
|
||||||
|
sendReaction={(reaction) => void sendRelation(reaction)}
|
||||||
|
toggleRaisedHand={toggleRaisedHand}
|
||||||
/>
|
/>
|
||||||
{showReactionsMenu && (
|
|
||||||
<ReactionPopupMenu
|
|
||||||
isHandRaised={isHandRaised}
|
|
||||||
canReact={canReact}
|
|
||||||
sendReaction={(reaction) => void sendRelation(reaction)}
|
|
||||||
toggleRaisedHand={toggleRaisedHand}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user