update folds

This commit is contained in:
Ajay Bura 2024-04-14 11:21:36 +05:30
parent e4c6f455ef
commit fb642087f7
13 changed files with 393 additions and 322 deletions

8
package-lock.json generated
View file

@ -31,7 +31,7 @@
"file-saver": "2.0.5", "file-saver": "2.0.5",
"flux": "4.0.3", "flux": "4.0.3",
"focus-trap-react": "10.0.2", "focus-trap-react": "10.0.2",
"folds": "1.7.0", "folds": "2.0.0",
"formik": "2.2.9", "formik": "2.2.9",
"html-dom-parser": "4.0.0", "html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0", "html-react-parser": "4.2.0",
@ -5247,9 +5247,9 @@
} }
}, },
"node_modules/folds": { "node_modules/folds": {
"version": "1.7.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/folds/-/folds-1.7.0.tgz", "resolved": "https://registry.npmjs.org/folds/-/folds-2.0.0.tgz",
"integrity": "sha512-Uz8RIsABKbWT/mBRCaiScxuYhmA3v6HrJ0UdiRSM/mG7ssCaAIMRP3aBa5zQwdiCvqsHgZqhFZT1AFgY6PGA7Q==", "integrity": "sha512-lKv31vij4GEpEzGKWk5c3ar78fMZ9Di5n1XFR14Z2wnnpqhiiM5JTIzr127Gk5dOfy4mJkjnv/ZfMZvM2k+OQg==",
"peerDependencies": { "peerDependencies": {
"@vanilla-extract/css": "^1.9.2", "@vanilla-extract/css": "^1.9.2",
"@vanilla-extract/recipes": "^0.3.0", "@vanilla-extract/recipes": "^0.3.0",

View file

@ -41,7 +41,7 @@
"file-saver": "2.0.5", "file-saver": "2.0.5",
"flux": "4.0.3", "flux": "4.0.3",
"focus-trap-react": "10.0.2", "focus-trap-react": "10.0.2",
"folds": "1.7.0", "folds": "2.0.0",
"formik": "2.2.9", "formik": "2.2.9",
"html-dom-parser": "4.0.0", "html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0", "html-react-parser": "4.2.0",

View file

@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
import React, { FormEventHandler, useEffect, useRef, useState } from 'react'; import React, { FormEventHandler, MouseEventHandler, useEffect, useRef, useState } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { import {
Box, Box,
@ -13,6 +13,7 @@ import {
Input, Input,
Menu, Menu,
PopOut, PopOut,
RectCords,
Scroll, Scroll,
Spinner, Spinner,
Text, Text,
@ -48,7 +49,7 @@ export const PdfViewer = as<'div', PdfViewerProps>(
const isError = const isError =
pdfJSState.status === AsyncStatus.Error || docState.status === AsyncStatus.Error; pdfJSState.status === AsyncStatus.Error || docState.status === AsyncStatus.Error;
const [pageNo, setPageNo] = useState(1); const [pageNo, setPageNo] = useState(1);
const [openJump, setOpenJump] = useState(false); const [jumpAnchor, setJumpAnchor] = useState<RectCords>();
useEffect(() => { useEffect(() => {
loadPdfJS(); loadPdfJS();
@ -86,7 +87,7 @@ export const PdfViewer = as<'div', PdfViewerProps>(
if (!jumpInput) return; if (!jumpInput) return;
const jumpTo = parseInt(jumpInput.value, 10); const jumpTo = parseInt(jumpInput.value, 10);
setPageNo(Math.max(1, Math.min(docState.data.numPages, jumpTo))); setPageNo(Math.max(1, Math.min(docState.data.numPages, jumpTo)));
setOpenJump(false); setJumpAnchor(undefined);
}; };
const handlePrevPage = () => { const handlePrevPage = () => {
@ -98,6 +99,10 @@ export const PdfViewer = as<'div', PdfViewerProps>(
setPageNo((n) => Math.min(n + 1, docState.data.numPages)); setPageNo((n) => Math.min(n + 1, docState.data.numPages));
}; };
const handleOpenJump: MouseEventHandler<HTMLButtonElement> = (evt) => {
setJumpAnchor(evt.currentTarget.getBoundingClientRect());
};
return ( return (
<Box className={classNames(css.PdfViewer, className)} direction="Column" {...props} ref={ref}> <Box className={classNames(css.PdfViewer, className)} direction="Column" {...props} ref={ref}>
<Header className={css.PdfViewerHeader} size="400"> <Header className={css.PdfViewerHeader} size="400">
@ -187,14 +192,14 @@ export const PdfViewer = as<'div', PdfViewerProps>(
</Chip> </Chip>
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200"> <Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
<PopOut <PopOut
open={openJump} anchor={jumpAnchor}
align="Center" align="Center"
position="Top" position="Top"
content={ content={
<FocusTrap <FocusTrap
focusTrapOptions={{ focusTrapOptions={{
initialFocus: false, initialFocus: false,
onDeactivate: () => setOpenJump(false), onDeactivate: () => setJumpAnchor(undefined),
clickOutsideDeactivates: true, clickOutsideDeactivates: true,
}} }}
> >
@ -227,17 +232,14 @@ export const PdfViewer = as<'div', PdfViewerProps>(
</FocusTrap> </FocusTrap>
} }
> >
{(anchorRef) => (
<Chip <Chip
onClick={() => setOpenJump(!openJump)} onClick={handleOpenJump}
ref={anchorRef}
variant="SurfaceVariant" variant="SurfaceVariant"
radii="300" radii="300"
aria-pressed={openJump} aria-pressed={jumpAnchor !== undefined}
> >
<Text size="B300">{`${pageNo}/${docState.data.numPages}`}</Text> <Text size="B300">{`${pageNo}/${docState.data.numPages}`}</Text>
</Chip> </Chip>
)}
</PopOut> </PopOut>
</Box> </Box>
<Chip <Chip

View file

@ -10,13 +10,14 @@ import {
Line, Line,
Menu, Menu,
PopOut, PopOut,
RectCords,
Scroll, Scroll,
Text, Text,
Tooltip, Tooltip,
TooltipProvider, TooltipProvider,
toRem, toRem,
} from 'folds'; } from 'folds';
import React, { ReactNode, useState } from 'react'; import React, { MouseEventHandler, ReactNode, useState } from 'react';
import { ReactEditor, useSlate } from 'slate-react'; import { ReactEditor, useSlate } from 'slate-react';
import { import {
headingLevel, headingLevel,
@ -119,26 +120,33 @@ export function BlockButton({ format, icon, tooltip }: BlockButtonProps) {
export function HeadingBlockButton() { export function HeadingBlockButton() {
const editor = useSlate(); const editor = useSlate();
const level = headingLevel(editor); const level = headingLevel(editor);
const [open, setOpen] = useState(false); const [anchor, setAnchor] = useState<RectCords>();
const isActive = isBlockActive(editor, BlockType.Heading); const isActive = isBlockActive(editor, BlockType.Heading);
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl'; const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
const handleMenuSelect = (selectedLevel: HeadingLevel) => { const handleMenuSelect = (selectedLevel: HeadingLevel) => {
setOpen(false); setAnchor(undefined);
toggleBlock(editor, BlockType.Heading, { level: selectedLevel }); toggleBlock(editor, BlockType.Heading, { level: selectedLevel });
ReactEditor.focus(editor); ReactEditor.focus(editor);
}; };
const handleMenuOpen: MouseEventHandler<HTMLButtonElement> = (evt) => {
if (isActive) {
toggleBlock(editor, BlockType.Heading);
return;
}
setAnchor(evt.currentTarget.getBoundingClientRect());
};
return ( return (
<PopOut <PopOut
open={open} anchor={anchor}
offset={5} offset={5}
position="Top" position="Top"
content={ content={
<FocusTrap <FocusTrap
focusTrapOptions={{ focusTrapOptions={{
initialFocus: false, initialFocus: false,
onDeactivate: () => setOpen(false), onDeactivate: () => setAnchor(undefined),
clickOutsideDeactivates: true, clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => isKeyForward: (evt: KeyboardEvent) =>
evt.key === 'ArrowDown' || evt.key === 'ArrowRight', evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
@ -197,12 +205,10 @@ export function HeadingBlockButton() {
</FocusTrap> </FocusTrap>
} }
> >
{(ref) => (
<IconButton <IconButton
style={{ width: 'unset' }} style={{ width: 'unset' }}
ref={ref}
variant="SurfaceVariant" variant="SurfaceVariant"
onClick={() => (isActive ? toggleBlock(editor, BlockType.Heading) : setOpen(!open))} onClick={handleMenuOpen}
aria-pressed={isActive} aria-pressed={isActive}
size="400" size="400"
radii="300" radii="300"
@ -210,7 +216,6 @@ export function HeadingBlockButton() {
<Icon size="200" src={level ? Icons[`Heading${level}`] : Icons.Heading1} /> <Icon size="200" src={level ? Icons[`Heading${level}`] : Icons.Heading1} />
<Icon size="200" src={isActive ? Icons.Cross : Icons.ChevronBottom} /> <Icon size="200" src={isActive ? Icons.Cross : Icons.ChevronBottom} />
</IconButton> </IconButton>
)}
</PopOut> </PopOut>
); );
} }

View file

@ -23,6 +23,7 @@ import {
Button, Button,
Input, Input,
Badge, Badge,
RectCords,
} from 'folds'; } from 'folds';
import { SearchOrderBy } from 'matrix-js-sdk'; import { SearchOrderBy } from 'matrix-js-sdk';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
@ -43,24 +44,27 @@ type OrderButtonProps = {
onChange: (order?: string) => void; onChange: (order?: string) => void;
}; };
function OrderButton({ order, onChange }: OrderButtonProps) { function OrderButton({ order, onChange }: OrderButtonProps) {
const [menu, setMenu] = useState(false); const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const rankOrder = order === SearchOrderBy.Rank; const rankOrder = order === SearchOrderBy.Rank;
const setOrder = (o?: string) => { const setOrder = (o?: string) => {
setMenu(false); setMenuAnchor(undefined);
onChange(o); onChange(o);
}; };
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
};
return ( return (
<PopOut <PopOut
open={menu} anchor={menuAnchor}
align="End" align="End"
position="Bottom" position="Bottom"
content={ content={
<FocusTrap <FocusTrap
focusTrapOptions={{ focusTrapOptions={{
initialFocus: false, initialFocus: false,
onDeactivate: () => setMenu(false), onDeactivate: () => setMenuAnchor(undefined),
clickOutsideDeactivates: true, clickOutsideDeactivates: true,
}} }}
> >
@ -93,17 +97,14 @@ function OrderButton({ order, onChange }: OrderButtonProps) {
</FocusTrap> </FocusTrap>
} }
> >
{(anchorRef) => (
<Chip <Chip
ref={anchorRef}
variant="SurfaceVariant" variant="SurfaceVariant"
radii="Pill" radii="Pill"
after={<Icon size="50" src={Icons.Sort} />} after={<Icon size="50" src={Icons.Sort} />}
onClick={() => setMenu(true)} onClick={handleOpenMenu}
> >
{rankOrder ? <Text size="T200">Relevance</Text> : <Text size="T200">Recent</Text>} {rankOrder ? <Text size="T200">Relevance</Text> : <Text size="T200">Recent</Text>}
</Chip> </Chip>
)}
</PopOut> </PopOut>
); );
} }
@ -126,7 +127,7 @@ type SelectRoomButtonProps = {
function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButtonProps) { function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButtonProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const [menu, setMenu] = useState(false); const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const [localSelected, setLocalSelected] = useState(selectedRooms); const [localSelected, setLocalSelected] = useState(selectedRooms);
const getRoomNameStr: SearchItemStrGetter<string> = useCallback( const getRoomNameStr: SearchItemStrGetter<string> = useCallback(
@ -172,30 +173,34 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto
}; };
const handleSave = () => { const handleSave = () => {
setMenu(false); setMenuAnchor(undefined);
onChange(localSelected); onChange(localSelected);
}; };
const handleDeselectAll = () => { const handleDeselectAll = () => {
setMenu(false); setMenuAnchor(undefined);
onChange(undefined); onChange(undefined);
}; };
useEffect(() => { useEffect(() => {
setLocalSelected(selectedRooms); setLocalSelected(selectedRooms);
resetSearch(); resetSearch();
}, [menu, selectedRooms, resetSearch]); }, [menuAnchor, selectedRooms, resetSearch]);
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
};
return ( return (
<PopOut <PopOut
open={menu} anchor={menuAnchor}
align="Center" align="Center"
position="Bottom" position="Bottom"
content={ content={
<FocusTrap <FocusTrap
focusTrapOptions={{ focusTrapOptions={{
initialFocus: false, initialFocus: false,
onDeactivate: () => setMenu(false), onDeactivate: () => setMenuAnchor(undefined),
clickOutsideDeactivates: true, clickOutsideDeactivates: true,
}} }}
> >
@ -307,17 +312,14 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto
</FocusTrap> </FocusTrap>
} }
> >
{(anchorRef) => (
<Chip <Chip
onClick={() => setMenu(true)} onClick={handleOpenMenu}
ref={anchorRef}
variant="SurfaceVariant" variant="SurfaceVariant"
radii="Pill" radii="Pill"
before={<Icon size="100" src={Icons.PlusCircle} />} before={<Icon size="100" src={Icons.PlusCircle} />}
> >
<Text size="T200">Select Rooms</Text> <Text size="T200">Select Rooms</Text>
</Chip> </Chip>
)}
</PopOut> </PopOut>
); );
} }

View file

@ -13,6 +13,7 @@ import {
PopOut, PopOut,
toRem, toRem,
Line, Line,
RectCords,
} from 'folds'; } from 'folds';
import { useFocusWithin, useHover } from 'react-aria'; import { useFocusWithin, useHover } from 'react-aria';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
@ -150,15 +151,24 @@ export function RoomNavItem({ room, selected, showAvatar, muted, linkPath }: Roo
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
const { hoverProps } = useHover({ onHoverChange: setHover }); const { hoverProps } = useHover({ onHoverChange: setHover });
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover }); const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
const [menu, setMenu] = useState(false); const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const unread = useRoomUnread(room.roomId, roomToUnreadAtom); const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const handleContextMenu: MouseEventHandler<HTMLElement> = (evt) => { const handleContextMenu: MouseEventHandler<HTMLElement> = (evt) => {
evt.preventDefault(); evt.preventDefault();
setMenu(true); setMenuAnchor({
x: evt.clientX,
y: evt.clientY,
width: 0,
height: 0,
});
}; };
const optionsVisible = hover || menu; const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
};
const optionsVisible = hover || !!menuAnchor;
return ( return (
<NavItem <NavItem
@ -166,7 +176,7 @@ export function RoomNavItem({ room, selected, showAvatar, muted, linkPath }: Roo
radii="400" radii="400"
highlight={unread !== undefined || selected} highlight={unread !== undefined || selected}
aria-selected={selected} aria-selected={selected}
data-hover={menu} data-hover={!!menuAnchor}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
{...hoverProps} {...hoverProps}
{...focusWithinProps} {...focusWithinProps}
@ -208,16 +218,17 @@ export function RoomNavItem({ room, selected, showAvatar, muted, linkPath }: Roo
{optionsVisible && ( {optionsVisible && (
<NavItemOptions> <NavItemOptions>
<PopOut <PopOut
open={menu} anchor={menuAnchor}
alignOffset={-5} offset={menuAnchor?.width === 0 ? 0 : undefined}
alignOffset={menuAnchor?.width === 0 ? 0 : -5}
position="Bottom" position="Bottom"
align="End" align={menuAnchor?.width === 0 ? 'Start' : 'End'}
content={ content={
<FocusTrap <FocusTrap
focusTrapOptions={{ focusTrapOptions={{
initialFocus: false, initialFocus: false,
returnFocusOnDeactivate: false, returnFocusOnDeactivate: false,
onDeactivate: () => setMenu(false), onDeactivate: () => setMenuAnchor(undefined),
clickOutsideDeactivates: true, clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
@ -226,16 +237,14 @@ export function RoomNavItem({ room, selected, showAvatar, muted, linkPath }: Roo
<RoomNavItemMenu <RoomNavItemMenu
room={room} room={room}
linkPath={linkPath} linkPath={linkPath}
requestClose={() => setMenu(false)} requestClose={() => setMenuAnchor(undefined)}
/> />
</FocusTrap> </FocusTrap>
} }
> >
{(anchorRef) => (
<IconButton <IconButton
ref={anchorRef} onClick={handleOpenMenu}
onClick={() => setMenu(true)} aria-pressed={!!menuAnchor}
aria-pressed={menu}
variant="Background" variant="Background"
fill="None" fill="None"
size="300" size="300"
@ -243,7 +252,6 @@ export function RoomNavItem({ room, selected, showAvatar, muted, linkPath }: Roo
> >
<Icon size="50" src={Icons.VerticalDots} /> <Icon size="50" src={Icons.VerticalDots} />
</IconButton> </IconButton>
)}
</PopOut> </PopOut>
</NavItemOptions> </NavItemOptions>
)} )}

View file

@ -22,6 +22,7 @@ import {
Menu, Menu,
MenuItem, MenuItem,
PopOut, PopOut,
RectCords,
Scroll, Scroll,
Spinner, Spinner,
Text, Text,
@ -289,10 +290,10 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
<Box className={css.MemberDrawerContent} direction="Column" gap="200"> <Box className={css.MemberDrawerContent} direction="Column" gap="200">
<Box ref={scrollTopAnchorRef} className={css.DrawerGroup} direction="Column" gap="200"> <Box ref={scrollTopAnchorRef} className={css.DrawerGroup} direction="Column" gap="200">
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200"> <Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
<UseStateProvider initial={false}> <UseStateProvider initial={undefined}>
{(open, setOpen) => ( {(anchor: RectCords | undefined, setAnchor) => (
<PopOut <PopOut
open={open} anchor={anchor}
position="Bottom" position="Bottom"
align="Start" align="Start"
offset={4} offset={4}
@ -300,7 +301,7 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
<FocusTrap <FocusTrap
focusTrapOptions={{ focusTrapOptions={{
initialFocus: false, initialFocus: false,
onDeactivate: () => setOpen(false), onDeactivate: () => setAnchor(undefined),
clickOutsideDeactivates: true, clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
@ -319,7 +320,7 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
radii="300" radii="300"
onClick={() => { onClick={() => {
setMembershipFilterIndex(index); setMembershipFilterIndex(index);
setOpen(false); setAnchor(undefined);
}} }}
> >
<Text>{menuItem.name}</Text> <Text>{menuItem.name}</Text>
@ -329,10 +330,13 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
</FocusTrap> </FocusTrap>
} }
> >
{(anchorRef) => (
<Chip <Chip
ref={anchorRef} onClick={
onClick={() => setOpen(!open)} ((evt) =>
setAnchor(
evt.currentTarget.getBoundingClientRect()
)) as MouseEventHandler<HTMLButtonElement>
}
variant={membershipFilter.color} variant={membershipFilter.color}
size="400" size="400"
radii="300" radii="300"
@ -340,14 +344,13 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
> >
<Text size="T200">{membershipFilter.name}</Text> <Text size="T200">{membershipFilter.name}</Text>
</Chip> </Chip>
)}
</PopOut> </PopOut>
)} )}
</UseStateProvider> </UseStateProvider>
<UseStateProvider initial={false}> <UseStateProvider initial={undefined}>
{(open, setOpen) => ( {(anchor: RectCords | undefined, setAnchor) => (
<PopOut <PopOut
open={open} anchor={anchor}
position="Bottom" position="Bottom"
align="End" align="End"
offset={4} offset={4}
@ -355,7 +358,7 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
<FocusTrap <FocusTrap
focusTrapOptions={{ focusTrapOptions={{
initialFocus: false, initialFocus: false,
onDeactivate: () => setOpen(false), onDeactivate: () => setAnchor(undefined),
clickOutsideDeactivates: true, clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
@ -370,7 +373,7 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
radii="300" radii="300"
onClick={() => { onClick={() => {
setSortFilterIndex(index); setSortFilterIndex(index);
setOpen(false); setAnchor(undefined);
}} }}
> >
<Text>{menuItem.name}</Text> <Text>{menuItem.name}</Text>
@ -380,10 +383,13 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
</FocusTrap> </FocusTrap>
} }
> >
{(anchorRef) => (
<Chip <Chip
ref={anchorRef} onClick={
onClick={() => setOpen(!open)} ((evt) =>
setAnchor(
evt.currentTarget.getBoundingClientRect()
)) as MouseEventHandler<HTMLButtonElement>
}
variant="Background" variant="Background"
size="400" size="400"
radii="300" radii="300"
@ -391,7 +397,6 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
> >
<Text size="T200">{sortFilter.name}</Text> <Text size="T200">{sortFilter.name}</Text>
</Chip> </Chip>
)}
</PopOut> </PopOut>
)} )}
</UseStateProvider> </UseStateProvider>

View file

@ -119,6 +119,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline'); const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline');
const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown'); const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown');
const commands = useCommands(mx, room); const commands = useCommands(mx, room);
const emojiBtnRef = useRef<HTMLButtonElement>(null);
const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId)); const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId));
const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId)); const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId));
@ -521,7 +522,11 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
alignOffset={-44} alignOffset={-44}
position="Top" position="Top"
align="End" align="End"
open={!!emojiBoardTab} anchor={
emojiBoardTab === undefined
? undefined
: emojiBtnRef.current?.getBoundingClientRect() ?? undefined
}
content={ content={
<EmojiBoard <EmojiBoard
tab={emojiBoardTab} tab={emojiBoardTab}
@ -538,8 +543,6 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
/> />
} }
> >
{(anchorRef) => (
<>
{!hideStickerBtn && ( {!hideStickerBtn && (
<IconButton <IconButton
aria-pressed={emojiBoardTab === EmojiBoardTab.Sticker} aria-pressed={emojiBoardTab === EmojiBoardTab.Sticker}
@ -555,7 +558,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
</IconButton> </IconButton>
)} )}
<IconButton <IconButton
ref={anchorRef} ref={emojiBtnRef}
aria-pressed={ aria-pressed={
hideStickerBtn ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji hideStickerBtn ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
} }
@ -567,14 +570,10 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
<Icon <Icon
src={Icons.Smile} src={Icons.Smile}
filled={ filled={
hideStickerBtn hideStickerBtn ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
? !!emojiBoardTab
: emojiBoardTab === EmojiBoardTab.Emoji
} }
/> />
</IconButton> </IconButton>
</>
)}
</PopOut> </PopOut>
)} )}
</UseStateProvider> </UseStateProvider>

View file

@ -18,6 +18,7 @@ import {
OverlayBackdrop, OverlayBackdrop,
OverlayCenter, OverlayCenter,
PopOut, PopOut,
RectCords,
Spinner, Spinner,
Text, Text,
as, as,
@ -610,8 +611,8 @@ export const Message = as<'div', MessageProps>(
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
const { hoverProps } = useHover({ onHoverChange: setHover }); const { hoverProps } = useHover({ onHoverChange: setHover });
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover }); const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
const [menu, setMenu] = useState(false); const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const [emojiBoard, setEmojiBoard] = useState(false); const [emojiBoardAnchor, setEmojiBoardAnchor] = useState<RectCords>();
const senderDisplayName = const senderDisplayName =
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId; getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
@ -706,11 +707,36 @@ export const Message = as<'div', MessageProps>(
const tag = (evt.target as any).tagName; const tag = (evt.target as any).tagName;
if (typeof tag === 'string' && tag.toLowerCase() === 'a') return; if (typeof tag === 'string' && tag.toLowerCase() === 'a') return;
evt.preventDefault(); evt.preventDefault();
setMenu(true); setMenuAnchor({
x: evt.clientX,
y: evt.clientY,
width: 0,
height: 0,
});
};
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
const target = evt.currentTarget.parentElement?.parentElement ?? evt.currentTarget;
setMenuAnchor(target.getBoundingClientRect());
}; };
const closeMenu = () => { const closeMenu = () => {
setMenu(false); setMenuAnchor(undefined);
};
const handleOpenEmojiBoard: MouseEventHandler<HTMLButtonElement> = (evt) => {
const target = evt.currentTarget.parentElement?.parentElement ?? evt.currentTarget;
setEmojiBoardAnchor(target.getBoundingClientRect());
};
const handleAddReactions: MouseEventHandler<HTMLButtonElement> = () => {
const rect = menuAnchor;
closeMenu();
// open it with timeout because closeMenu
// FocusTrap will return focus from emojiBoard
setTimeout(() => {
setEmojiBoardAnchor(rect);
}, 100);
}; };
return ( return (
@ -720,22 +746,22 @@ export const Message = as<'div', MessageProps>(
space={messageSpacing} space={messageSpacing}
collapse={collapse} collapse={collapse}
highlight={highlight} highlight={highlight}
selected={menu || emojiBoard} selected={!!menuAnchor || !!emojiBoardAnchor}
{...props} {...props}
{...hoverProps} {...hoverProps}
{...focusWithinProps} {...focusWithinProps}
ref={ref} ref={ref}
> >
{!edit && (hover || menu || emojiBoard) && ( {!edit && (hover || !!menuAnchor || !!emojiBoardAnchor) && (
<div className={css.MessageOptionsBase}> <div className={css.MessageOptionsBase}>
<Menu className={css.MessageOptionsBar} variant="SurfaceVariant"> <Menu className={css.MessageOptionsBar} variant="SurfaceVariant">
<Box gap="100"> <Box gap="100">
{canSendReaction && ( {canSendReaction && (
<PopOut <PopOut
alignOffset={-65}
position="Bottom" position="Bottom"
align="End" align={emojiBoardAnchor?.width === 0 ? 'Start' : 'End'}
open={emojiBoard} offset={emojiBoardAnchor?.width === 0 ? 0 : undefined}
anchor={emojiBoardAnchor}
content={ content={
<EmojiBoard <EmojiBoard
imagePackRooms={imagePackRooms ?? []} imagePackRooms={imagePackRooms ?? []}
@ -743,30 +769,27 @@ export const Message = as<'div', MessageProps>(
allowTextCustomEmoji allowTextCustomEmoji
onEmojiSelect={(key) => { onEmojiSelect={(key) => {
onReactionToggle(mEvent.getId()!, key); onReactionToggle(mEvent.getId()!, key);
setEmojiBoard(false); setEmojiBoardAnchor(undefined);
}} }}
onCustomEmojiSelect={(mxc, shortcode) => { onCustomEmojiSelect={(mxc, shortcode) => {
onReactionToggle(mEvent.getId()!, mxc, shortcode); onReactionToggle(mEvent.getId()!, mxc, shortcode);
setEmojiBoard(false); setEmojiBoardAnchor(undefined);
}} }}
requestClose={() => { requestClose={() => {
setEmojiBoard(false); setEmojiBoardAnchor(undefined);
}} }}
/> />
} }
> >
{(anchorRef) => (
<IconButton <IconButton
ref={anchorRef} onClick={handleOpenEmojiBoard}
onClick={() => setEmojiBoard(true)}
variant="SurfaceVariant" variant="SurfaceVariant"
size="300" size="300"
radii="300" radii="300"
aria-pressed={emojiBoard} aria-pressed={!!emojiBoardAnchor}
> >
<Icon src={Icons.SmilePlus} size="100" /> <Icon src={Icons.SmilePlus} size="100" />
</IconButton> </IconButton>
)}
</PopOut> </PopOut>
)} )}
<IconButton <IconButton
@ -789,15 +812,15 @@ export const Message = as<'div', MessageProps>(
</IconButton> </IconButton>
)} )}
<PopOut <PopOut
open={menu} anchor={menuAnchor}
alignOffset={-5}
position="Bottom" position="Bottom"
align="End" align={menuAnchor?.width === 0 ? 'Start' : 'End'}
offset={menuAnchor?.width === 0 ? 0 : undefined}
content={ content={
<FocusTrap <FocusTrap
focusTrapOptions={{ focusTrapOptions={{
initialFocus: false, initialFocus: false,
onDeactivate: () => setMenu(false), onDeactivate: () => setMenuAnchor(undefined),
clickOutsideDeactivates: true, clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
@ -818,12 +841,7 @@ export const Message = as<'div', MessageProps>(
size="300" size="300"
after={<Icon size="100" src={Icons.SmilePlus} />} after={<Icon size="100" src={Icons.SmilePlus} />}
radii="300" radii="300"
onClick={() => { onClick={handleAddReactions}
closeMenu();
// open it with timeout because closeMenu
// FocusTrap will return focus from emojiBoard
setTimeout(() => setEmojiBoard(true), 100);
}}
> >
<Text <Text
className={css.MessageMenuItemText} className={css.MessageMenuItemText}
@ -915,18 +933,15 @@ export const Message = as<'div', MessageProps>(
</FocusTrap> </FocusTrap>
} }
> >
{(targetRef) => (
<IconButton <IconButton
ref={targetRef}
variant="SurfaceVariant" variant="SurfaceVariant"
size="300" size="300"
radii="300" radii="300"
onClick={() => setMenu((v) => !v)} onClick={handleOpenMenu}
aria-pressed={menu} aria-pressed={!!menuAnchor}
> >
<Icon src={Icons.VerticalDots} size="100" /> <Icon src={Icons.VerticalDots} size="100" />
</IconButton> </IconButton>
)}
</PopOut> </PopOut>
</Box> </Box>
</Menu> </Menu>
@ -967,7 +982,7 @@ export const Event = as<'div', EventProps>(
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
const { hoverProps } = useHover({ onHoverChange: setHover }); const { hoverProps } = useHover({ onHoverChange: setHover });
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover }); const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
const [menu, setMenu] = useState(false); const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const stateEvent = typeof mEvent.getStateKey() === 'string'; const stateEvent = typeof mEvent.getStateKey() === 'string';
const handleContextMenu: MouseEventHandler<HTMLDivElement> = (evt) => { const handleContextMenu: MouseEventHandler<HTMLDivElement> = (evt) => {
@ -975,11 +990,21 @@ export const Event = as<'div', EventProps>(
const tag = (evt.target as any).tagName; const tag = (evt.target as any).tagName;
if (typeof tag === 'string' && tag.toLowerCase() === 'a') return; if (typeof tag === 'string' && tag.toLowerCase() === 'a') return;
evt.preventDefault(); evt.preventDefault();
setMenu(true); setMenuAnchor({
x: evt.clientX,
y: evt.clientY,
width: 0,
height: 0,
});
};
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
const target = evt.currentTarget.parentElement?.parentElement ?? evt.currentTarget;
setMenuAnchor(target.getBoundingClientRect());
}; };
const closeMenu = () => { const closeMenu = () => {
setMenu(false); setMenuAnchor(undefined);
}; };
return ( return (
@ -989,26 +1014,26 @@ export const Event = as<'div', EventProps>(
space={messageSpacing} space={messageSpacing}
autoCollapse autoCollapse
highlight={highlight} highlight={highlight}
selected={menu} selected={!!menuAnchor}
{...props} {...props}
{...hoverProps} {...hoverProps}
{...focusWithinProps} {...focusWithinProps}
ref={ref} ref={ref}
> >
{(hover || menu) && ( {(hover || !!menuAnchor) && (
<div className={css.MessageOptionsBase}> <div className={css.MessageOptionsBase}>
<Menu className={css.MessageOptionsBar} variant="SurfaceVariant"> <Menu className={css.MessageOptionsBar} variant="SurfaceVariant">
<Box gap="100"> <Box gap="100">
<PopOut <PopOut
open={menu} anchor={menuAnchor}
alignOffset={-5}
position="Bottom" position="Bottom"
align="End" align={menuAnchor?.width === 0 ? 'Start' : 'End'}
offset={menuAnchor?.width === 0 ? 0 : undefined}
content={ content={
<FocusTrap <FocusTrap
focusTrapOptions={{ focusTrapOptions={{
initialFocus: false, initialFocus: false,
onDeactivate: () => setMenu(false), onDeactivate: () => setMenuAnchor(undefined),
clickOutsideDeactivates: true, clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
@ -1049,18 +1074,15 @@ export const Event = as<'div', EventProps>(
</FocusTrap> </FocusTrap>
} }
> >
{(targetRef) => (
<IconButton <IconButton
ref={targetRef}
variant="SurfaceVariant" variant="SurfaceVariant"
size="300" size="300"
radii="300" radii="300"
onClick={() => setMenu((v) => !v)} onClick={handleOpenMenu}
aria-pressed={menu} aria-pressed={!!menuAnchor}
> >
<Icon src={Icons.VerticalDots} size="100" /> <Icon src={Icons.VerticalDots} size="100" />
</IconButton> </IconButton>
)}
</PopOut> </PopOut>
</Box> </Box>
</Menu> </Menu>

View file

@ -1,5 +1,24 @@
import React, { KeyboardEventHandler, useCallback, useEffect, useState } from 'react'; import React, {
import { Box, Chip, Icon, IconButton, Icons, Line, PopOut, Spinner, Text, as, config } from 'folds'; KeyboardEventHandler,
MouseEventHandler,
useCallback,
useEffect,
useState,
} from 'react';
import {
Box,
Chip,
Icon,
IconButton,
Icons,
Line,
PopOut,
RectCords,
Spinner,
Text,
as,
config,
} from 'folds';
import { Editor, Transforms } from 'slate'; import { Editor, Transforms } from 'slate';
import { ReactEditor } from 'slate-react'; import { ReactEditor } from 'slate-react';
import { IContent, MatrixEvent, RelationType, Room } from 'matrix-js-sdk'; import { IContent, MatrixEvent, RelationType, Room } from 'matrix-js-sdk';
@ -258,13 +277,13 @@ export const MessageEditor = as<'div', MessageEditorProps>(
> >
<Icon size="400" src={toolbar ? Icons.AlphabetUnderline : Icons.Alphabet} /> <Icon size="400" src={toolbar ? Icons.AlphabetUnderline : Icons.Alphabet} />
</IconButton> </IconButton>
<UseStateProvider initial={false}> <UseStateProvider initial={undefined}>
{(emojiBoard: boolean, setEmojiBoard) => ( {(anchor: RectCords | undefined, setAnchor) => (
<PopOut <PopOut
anchor={anchor}
alignOffset={-8} alignOffset={-8}
position="Top" position="Top"
align="End" align="End"
open={!!emojiBoard}
content={ content={
<EmojiBoard <EmojiBoard
imagePackRooms={imagePackRooms ?? []} imagePackRooms={imagePackRooms ?? []}
@ -272,24 +291,26 @@ export const MessageEditor = as<'div', MessageEditorProps>(
onEmojiSelect={handleEmoticonSelect} onEmojiSelect={handleEmoticonSelect}
onCustomEmojiSelect={handleEmoticonSelect} onCustomEmojiSelect={handleEmoticonSelect}
requestClose={() => { requestClose={() => {
setEmojiBoard(false); setAnchor(undefined);
if (!mobileOrTablet()) ReactEditor.focus(editor); if (!mobileOrTablet()) ReactEditor.focus(editor);
}} }}
/> />
} }
> >
{(anchorRef) => (
<IconButton <IconButton
ref={anchorRef} aria-pressed={anchor !== undefined}
aria-pressed={emojiBoard} onClick={
onClick={() => setEmojiBoard(true)} ((evt) =>
setAnchor(
evt.currentTarget.getBoundingClientRect()
)) as MouseEventHandler<HTMLButtonElement>
}
variant="SurfaceVariant" variant="SurfaceVariant"
size="300" size="300"
radii="300" radii="300"
> >
<Icon size="400" src={Icons.Smile} filled={emojiBoard} /> <Icon size="400" src={Icons.Smile} filled={anchor !== undefined} />
</IconButton> </IconButton>
)}
</PopOut> </PopOut>
)} )}
</UseStateProvider> </UseStateProvider>

View file

@ -15,6 +15,7 @@ import {
Menu, Menu,
MenuItem, MenuItem,
PopOut, PopOut,
RectCords,
Text, Text,
config, config,
} from 'folds'; } from 'folds';
@ -33,7 +34,7 @@ export function ServerPicker({
allowCustomServer?: boolean; allowCustomServer?: boolean;
onServerChange: (server: string) => void; onServerChange: (server: string) => void;
}) { }) {
const [serverMenu, setServerMenu] = useState(false); const [serverMenuAnchor, setServerMenuAnchor] = useState<RectCords>();
const serverInputRef = useRef<HTMLInputElement>(null); const serverInputRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
@ -53,7 +54,7 @@ export function ServerPicker({
const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (evt) => { const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (evt) => {
if (evt.key === 'ArrowDown') { if (evt.key === 'ArrowDown') {
evt.preventDefault(); evt.preventDefault();
setServerMenu(true); setServerMenuAnchor(undefined);
} }
if (evt.key === 'Enter') { if (evt.key === 'Enter') {
evt.preventDefault(); evt.preventDefault();
@ -67,7 +68,12 @@ export function ServerPicker({
if (selectedServer) { if (selectedServer) {
onServerChange(selectedServer); onServerChange(selectedServer);
} }
setServerMenu(false); setServerMenuAnchor(undefined);
};
const handleOpenServerMenu: MouseEventHandler<HTMLElement> = (evt) => {
const target = evt.currentTarget.parentElement ?? evt.currentTarget;
setServerMenuAnchor(target.getBoundingClientRect());
}; };
return ( return (
@ -81,11 +87,11 @@ export function ServerPicker({
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
size="500" size="500"
readOnly={!allowCustomServer} readOnly={!allowCustomServer}
onClick={allowCustomServer ? undefined : () => setServerMenu(true)} onClick={allowCustomServer ? undefined : handleOpenServerMenu}
after={ after={
serverList.length === 0 || (serverList.length === 1 && !allowCustomServer) ? undefined : ( serverList.length === 0 || (serverList.length === 1 && !allowCustomServer) ? undefined : (
<PopOut <PopOut
open={serverMenu} anchor={serverMenuAnchor}
position="Bottom" position="Bottom"
align="End" align="End"
offset={4} offset={4}
@ -93,7 +99,7 @@ export function ServerPicker({
<FocusTrap <FocusTrap
focusTrapOptions={{ focusTrapOptions={{
initialFocus: false, initialFocus: false,
onDeactivate: () => setServerMenu(false), onDeactivate: () => setServerMenuAnchor(undefined),
clickOutsideDeactivates: true, clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
@ -120,18 +126,15 @@ export function ServerPicker({
</FocusTrap> </FocusTrap>
} }
> >
{(anchorRef) => (
<IconButton <IconButton
ref={anchorRef} onClick={handleOpenServerMenu}
onClick={() => setServerMenu(true)}
variant={allowCustomServer ? 'Background' : 'Surface'} variant={allowCustomServer ? 'Background' : 'Surface'}
size="300" size="300"
aria-pressed={serverMenu} aria-pressed={!!serverMenuAnchor}
radii="300" radii="300"
> >
<Icon src={Icons.ChevronBottom} /> <Icon src={Icons.ChevronBottom} />
</IconButton> </IconButton>
)}
</PopOut> </PopOut>
) )
} }

View file

@ -1,4 +1,4 @@
import React, { FormEventHandler, useCallback, useState } from 'react'; import React, { FormEventHandler, MouseEventHandler, useCallback, useState } from 'react';
import { import {
Box, Box,
Button, Button,
@ -12,6 +12,7 @@ import {
OverlayBackdrop, OverlayBackdrop,
OverlayCenter, OverlayCenter,
PopOut, PopOut,
RectCords,
Spinner, Spinner,
Text, Text,
config, config,
@ -37,17 +38,21 @@ import { FieldError } from '../FiledError';
import { getResetPasswordPath } from '../../pathUtils'; import { getResetPasswordPath } from '../../pathUtils';
function UsernameHint({ server }: { server: string }) { function UsernameHint({ server }: { server: string }) {
const [open, setOpen] = useState(false); const [anchor, setAnchor] = useState<RectCords>();
const handleOpenMenu: MouseEventHandler<HTMLElement> = (evt) => {
setAnchor(evt.currentTarget.getBoundingClientRect());
};
return ( return (
<PopOut <PopOut
open={open} anchor={anchor}
position="Top" position="Top"
align="End" align="End"
content={ content={
<FocusTrap <FocusTrap
focusTrapOptions={{ focusTrapOptions={{
initialFocus: false, initialFocus: false,
onDeactivate: () => setOpen(false), onDeactivate: () => setAnchor(undefined),
clickOutsideDeactivates: true, clickOutsideDeactivates: true,
}} }}
> >
@ -84,20 +89,17 @@ function UsernameHint({ server }: { server: string }) {
</FocusTrap> </FocusTrap>
} }
> >
{(targetRef) => (
<IconButton <IconButton
tabIndex={-1} tabIndex={-1}
onClick={() => setOpen(true)} onClick={handleOpenMenu}
ref={targetRef}
type="button" type="button"
variant="Background" variant="Background"
size="300" size="300"
radii="300" radii="300"
aria-pressed={open} aria-pressed={!!anchor}
> >
<Icon style={{ opacity: config.opacity.P300 }} size="100" src={Icons.Info} /> <Icon style={{ opacity: config.opacity.P300 }} size="100" src={Icons.Info} />
</IconButton> </IconButton>
)}
</PopOut> </PopOut>
); );
} }

View file

@ -19,6 +19,7 @@ import {
Menu, Menu,
MenuItem, MenuItem,
PopOut, PopOut,
RectCords,
Scroll, Scroll,
Spinner, Spinner,
Text, Text,
@ -149,7 +150,7 @@ function ThirdPartyProtocolsSelector({
onChange: (instanceId?: string) => void; onChange: (instanceId?: string) => void;
}) { }) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const [menu, setMenu] = useState(false); const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const { data } = useQuery({ const { data } = useQuery({
queryKey: ['thirdparty', 'protocols'], queryKey: ['thirdparty', 'protocols'],
@ -159,7 +160,11 @@ function ThirdPartyProtocolsSelector({
const handleInstanceSelect: MouseEventHandler<HTMLButtonElement> = (evt): void => { const handleInstanceSelect: MouseEventHandler<HTMLButtonElement> = (evt): void => {
const insId = evt.currentTarget.getAttribute('data-instance-id') ?? undefined; const insId = evt.currentTarget.getAttribute('data-instance-id') ?? undefined;
onChange(insId); onChange(insId);
setMenu(false); setMenuAnchor(undefined);
};
const handleOpenMenu: MouseEventHandler<HTMLElement> = (evt) => {
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
}; };
const instances = data && Object.keys(data).flatMap((protocol) => data[protocol].instances); const instances = data && Object.keys(data).flatMap((protocol) => data[protocol].instances);
@ -168,14 +173,14 @@ function ThirdPartyProtocolsSelector({
return ( return (
<PopOut <PopOut
open={menu} anchor={menuAnchor}
align="End" align="End"
position="Bottom" position="Bottom"
content={ content={
<FocusTrap <FocusTrap
focusTrapOptions={{ focusTrapOptions={{
initialFocus: false, initialFocus: false,
onDeactivate: () => setMenu(false), onDeactivate: () => setMenuAnchor(undefined),
clickOutsideDeactivates: true, clickOutsideDeactivates: true,
}} }}
> >
@ -221,11 +226,9 @@ function ThirdPartyProtocolsSelector({
</FocusTrap> </FocusTrap>
} }
> >
{(anchorRef) => (
<Chip <Chip
ref={anchorRef} onClick={handleOpenMenu}
onClick={() => setMenu(!menu)} aria-pressed={!!menuAnchor}
aria-pressed={menu}
radii="Pill" radii="Pill"
size="400" size="400"
variant={instanceId ? 'Success' : 'SurfaceVariant'} variant={instanceId ? 'Success' : 'SurfaceVariant'}
@ -235,7 +238,6 @@ function ThirdPartyProtocolsSelector({
{selectedInstance?.desc ?? DEFAULT_INSTANCE_NAME} {selectedInstance?.desc ?? DEFAULT_INSTANCE_NAME}
</Text> </Text>
</Chip> </Chip>
)}
</PopOut> </PopOut>
); );
} }
@ -245,7 +247,7 @@ type LimitButtonProps = {
onLimitChange: (limit: string) => void; onLimitChange: (limit: string) => void;
}; };
function LimitButton({ limit, onLimitChange }: LimitButtonProps) { function LimitButton({ limit, onLimitChange }: LimitButtonProps) {
const [openLimit, setOpenLimit] = useState(false); const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const handleLimitSubmit: FormEventHandler<HTMLFormElement> = (evt) => { const handleLimitSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
evt.preventDefault(); evt.preventDefault();
@ -257,20 +259,23 @@ function LimitButton({ limit, onLimitChange }: LimitButtonProps) {
}; };
const setLimit = (l: string) => { const setLimit = (l: string) => {
setOpenLimit(false); setMenuAnchor(undefined);
onLimitChange(l); onLimitChange(l);
}; };
const handleOpenMenu: MouseEventHandler<HTMLElement> = (evt) => {
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
};
return ( return (
<PopOut <PopOut
open={openLimit} anchor={menuAnchor}
align="End" align="End"
position="Bottom" position="Bottom"
content={ content={
<FocusTrap <FocusTrap
focusTrapOptions={{ focusTrapOptions={{
initialFocus: false, initialFocus: false,
onDeactivate: () => setOpenLimit(false), onDeactivate: () => setMenuAnchor(undefined),
clickOutsideDeactivates: true, clickOutsideDeactivates: true,
}} }}
> >
@ -315,11 +320,9 @@ function LimitButton({ limit, onLimitChange }: LimitButtonProps) {
</FocusTrap> </FocusTrap>
} }
> >
{(anchorRef) => (
<Chip <Chip
ref={anchorRef} onClick={handleOpenMenu}
onClick={() => setOpenLimit(!openLimit)} aria-pressed={!!menuAnchor}
aria-pressed={openLimit}
radii="Pill" radii="Pill"
size="400" size="400"
variant="SurfaceVariant" variant="SurfaceVariant"
@ -327,7 +330,6 @@ function LimitButton({ limit, onLimitChange }: LimitButtonProps) {
> >
<Text size="T200" truncate>{`Page Limit: ${limit}`}</Text> <Text size="T200" truncate>{`Page Limit: ${limit}`}</Text>
</Chip> </Chip>
)}
</PopOut> </PopOut>
); );
} }