modal impl

This commit is contained in:
Half-Shot
2024-11-07 11:33:10 +00:00
parent 29d562aea1
commit 60300c2f84
2 changed files with 136 additions and 104 deletions

View File

@@ -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 {

View File

@@ -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}
/>
)}
</> </>
); );
} }