Merge branch 'fix-dropdown' into 'develop'
Fix Profile Dropdown See merge request soapbox-pub/soapbox!2261
This commit is contained in:
commit
261d900b51
|
@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Posts: don't have to click the play button twice for embedded videos.
|
||||
- index.html: remove `referrer` meta tag so it doesn't conflict with backend's `Referrer-Policy` header.
|
||||
- Modals: fix media modal automatically switching to video.
|
||||
- Navigation: profile dropdown erratic behavior.
|
||||
|
||||
### Removed
|
||||
- Admin: single user mode. Now the homepage can be redirected to any URL.
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
|
||||
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
|
||||
import VerificationBadge from 'soapbox/components/verification-badge';
|
||||
import ActionButton from 'soapbox/features/ui/components/action-button';
|
||||
import { useAppSelector, useOnScreen } from 'soapbox/hooks';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { getAcct } from 'soapbox/utils/accounts';
|
||||
import { displayFqn } from 'soapbox/utils/state';
|
||||
|
||||
|
@ -117,19 +117,14 @@ const Account = ({
|
|||
emoji,
|
||||
note,
|
||||
}: IAccount) => {
|
||||
const overflowRef = React.useRef<HTMLDivElement>(null);
|
||||
const actionRef = React.useRef<HTMLDivElement>(null);
|
||||
// @ts-ignore
|
||||
const isOnScreen = useOnScreen(overflowRef);
|
||||
|
||||
const [style, setStyle] = React.useState<React.CSSProperties>({ visibility: 'hidden' });
|
||||
const overflowRef = useRef<HTMLDivElement>(null);
|
||||
const actionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const me = useAppSelector((state) => state.me);
|
||||
const username = useAppSelector((state) => account ? getAcct(account, displayFqn(state)) : null);
|
||||
|
||||
const handleAction = () => {
|
||||
// @ts-ignore
|
||||
onActionClick(account);
|
||||
onActionClick!(account);
|
||||
};
|
||||
|
||||
const renderAction = () => {
|
||||
|
@ -162,19 +157,6 @@ const Account = ({
|
|||
|
||||
const intl = useIntl();
|
||||
|
||||
React.useEffect(() => {
|
||||
const style: React.CSSProperties = {};
|
||||
const actionWidth = actionRef.current?.clientWidth || 0;
|
||||
|
||||
if (overflowRef.current) {
|
||||
style.maxWidth = overflowRef.current.clientWidth - 30 - avatarSize - actionWidth;
|
||||
} else {
|
||||
style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
setStyle(style);
|
||||
}, [isOnScreen, overflowRef, actionRef]);
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
@ -195,7 +177,7 @@ const Account = ({
|
|||
return (
|
||||
<div data-testid='account' className='group block w-full shrink-0' ref={overflowRef}>
|
||||
<HStack alignItems={actionAlignment} justifyContent='between'>
|
||||
<HStack alignItems={withAccountNote || note ? 'top' : 'center'} space={3}>
|
||||
<HStack alignItems={withAccountNote || note ? 'top' : 'center'} space={3} className='overflow-hidden'>
|
||||
<ProfilePopper
|
||||
condition={showProfileHoverCard}
|
||||
wrapper={(children) => <HoverRefWrapper className='relative' accountId={account.id} inline>{children}</HoverRefWrapper>}
|
||||
|
@ -215,7 +197,7 @@ const Account = ({
|
|||
</LinkEl>
|
||||
</ProfilePopper>
|
||||
|
||||
<div className='grow'>
|
||||
<div className='grow overflow-hidden'>
|
||||
<ProfilePopper
|
||||
condition={showProfileHoverCard}
|
||||
wrapper={(children) => <HoverRefWrapper accountId={account.id} inline>{children}</HoverRefWrapper>}
|
||||
|
@ -225,7 +207,7 @@ const Account = ({
|
|||
title={account.acct}
|
||||
onClick={(event: React.MouseEvent) => event.stopPropagation()}
|
||||
>
|
||||
<HStack space={1} alignItems='center' grow style={style}>
|
||||
<HStack space={1} alignItems='center' grow>
|
||||
<Text
|
||||
size='sm'
|
||||
weight='semibold'
|
||||
|
@ -241,7 +223,7 @@ const Account = ({
|
|||
</ProfilePopper>
|
||||
|
||||
<Stack space={withAccountNote || note ? 1 : 0}>
|
||||
<HStack alignItems='center' space={1} style={style}>
|
||||
<HStack alignItems='center' space={1}>
|
||||
<Text theme='muted' size='sm' direction='ltr' truncate>@{username}</Text>
|
||||
|
||||
{account.favicon && (
|
||||
|
|
|
@ -31,7 +31,3 @@ div:focus[data-reach-menu-list] {
|
|||
[data-reach-menu-link][data-disabled] {
|
||||
@apply opacity-25 cursor-default;
|
||||
}
|
||||
|
||||
[data-reach-menu-popover] hr {
|
||||
@apply my-1 mx-2 border-t-2 border-gray-100 dark:border-gray-800;
|
||||
}
|
||||
|
|
|
@ -37,6 +37,6 @@ const MenuList: React.FC<IMenuList> = (props) => {
|
|||
};
|
||||
|
||||
/** Divides menu items. */
|
||||
const MenuDivider = () => <hr />;
|
||||
const MenuDivider = () => <hr className='my-1 mx-2 border-t-2 border-gray-100 dark:border-gray-800' />;
|
||||
|
||||
export { Menu, MenuButton, MenuDivider, MenuItems, MenuItem, MenuList, MenuLink };
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { useFloating } from '@floating-ui/react';
|
||||
import clsx from 'clsx';
|
||||
import throttle from 'lodash/throttle';
|
||||
import React from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { fetchOwnAccounts, logOut, switchAccount } from 'soapbox/actions/auth';
|
||||
import Account from 'soapbox/components/account';
|
||||
import { Menu, MenuButton, MenuDivider, MenuItem, MenuLink, MenuList } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
import { MenuDivider } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useClickOutside, useFeatures } from 'soapbox/hooks';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
||||
import ThemeToggle from './theme-toggle';
|
||||
|
@ -39,6 +41,8 @@ const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
|
|||
const features = useFeatures();
|
||||
const intl = useIntl();
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { x, y, strategy, refs } = useFloating<HTMLButtonElement>({ placement: 'bottom-end' });
|
||||
const authUsers = useAppSelector((state) => state.auth.users);
|
||||
const otherAccounts = useAppSelector((state) => authUsers.map((authUser: any) => getAccount(state, authUser.id)!));
|
||||
|
||||
|
@ -62,7 +66,7 @@ const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const menu: IMenuItem[] = React.useMemo(() => {
|
||||
const menu: IMenuItem[] = useMemo(() => {
|
||||
const menu: IMenuItem[] = [];
|
||||
|
||||
menu.push({ text: renderAccount(account), to: `/@${account.acct}` });
|
||||
|
@ -96,42 +100,82 @@ const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
|
|||
return menu;
|
||||
}, [account, authUsers, features]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const toggleVisible = () => setVisible(!visible);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOwnAccountThrottled();
|
||||
}, [account, authUsers]);
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton>
|
||||
{children}
|
||||
</MenuButton>
|
||||
useClickOutside(refs, () => {
|
||||
setVisible(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type='button' ref={refs.setReference} onClick={toggleVisible}>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
{visible && (
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
className='z-[1003] mt-2 max-w-xs rounded-md bg-white shadow-lg focus:outline-none dark:bg-gray-900 dark:ring-2 dark:ring-primary-700'
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
left: x ?? 0,
|
||||
width: 'max-content',
|
||||
}}
|
||||
>
|
||||
{menu.map((menuItem, i) => (
|
||||
<MenuItem key={i} menuItem={menuItem} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface MenuItemProps {
|
||||
className?: string
|
||||
menuItem: IMenuItem
|
||||
}
|
||||
|
||||
const MenuItem: React.FC<MenuItemProps> = ({ className, menuItem }) => {
|
||||
const baseClassName = clsx(className, 'block w-full cursor-pointer truncate px-4 py-2.5 text-left text-sm text-gray-700 hover:bg-gray-100 rtl:text-right dark:text-gray-500 dark:hover:bg-gray-800');
|
||||
|
||||
<MenuList>
|
||||
{menu.map((menuItem, idx) => {
|
||||
if (menuItem.toggle) {
|
||||
return (
|
||||
<div key={idx} className='flex flex-row items-center justify-between space-x-4 px-4 py-1 text-sm text-gray-700 dark:text-gray-400'>
|
||||
<div className='flex flex-row items-center justify-between space-x-4 px-4 py-1 text-sm text-gray-700 dark:text-gray-400'>
|
||||
<span>{menuItem.text}</span>
|
||||
|
||||
{menuItem.toggle}
|
||||
</div>
|
||||
);
|
||||
} else if (!menuItem.text) {
|
||||
return <MenuDivider key={idx} />;
|
||||
} else {
|
||||
const Comp: any = menuItem.action ? MenuItem : MenuLink;
|
||||
const itemProps = menuItem.action ? { onSelect: menuItem.action } : { to: menuItem.to, as: Link };
|
||||
|
||||
return <MenuDivider />;
|
||||
} else if (menuItem.action) {
|
||||
return (
|
||||
<Comp key={idx} {...itemProps} className='truncate'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={menuItem.action}
|
||||
className={baseClassName}
|
||||
>
|
||||
{menuItem.text}
|
||||
</Comp>
|
||||
</button>
|
||||
);
|
||||
} else if (menuItem.to) {
|
||||
return (
|
||||
<Link
|
||||
to={menuItem.to}
|
||||
className={baseClassName}
|
||||
>
|
||||
{menuItem.text}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
throw menuItem;
|
||||
}
|
||||
})}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileDropdown;
|
||||
|
|
|
@ -2,6 +2,7 @@ export { useAccount } from './useAccount';
|
|||
export { useApi } from './useApi';
|
||||
export { useAppDispatch } from './useAppDispatch';
|
||||
export { useAppSelector } from './useAppSelector';
|
||||
export { useClickOutside } from './useClickOutside';
|
||||
export { useCompose } from './useCompose';
|
||||
export { useDebounce } from './useDebounce';
|
||||
export { useDimensions } from './useDimensions';
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { ExtendedRefs } from '@floating-ui/react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
/** Trigger `callback` when a Floating UI element is clicked outside from. */
|
||||
export const useClickOutside = <T extends HTMLElement>(
|
||||
refs: ExtendedRefs<T>,
|
||||
callback: (e: MouseEvent) => void,
|
||||
) => {
|
||||
const handleWindowClick = useCallback((e: MouseEvent) => {
|
||||
if (e.target) {
|
||||
const target = e.target as Node;
|
||||
|
||||
const floating = refs.floating.current;
|
||||
const reference = refs.reference.current as T | undefined;
|
||||
|
||||
if (!(floating?.contains(target) || reference?.contains(target))) {
|
||||
callback(e);
|
||||
}
|
||||
}
|
||||
}, [refs.floating.current, refs.reference.current]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('click', handleWindowClick);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('click', handleWindowClick);
|
||||
};
|
||||
}, []);
|
||||
};
|
|
@ -1,13 +1,17 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export const useOnScreen = (ref: React.MutableRefObject<HTMLElement>) => {
|
||||
const [isIntersecting, setIntersecting] = React.useState(false);
|
||||
/** Detect whether a given element is on the screen. */
|
||||
// https://stackoverflow.com/a/64892655
|
||||
export const useOnScreen = <T>(ref: React.RefObject<T & Element>) => {
|
||||
const [isIntersecting, setIntersecting] = useState(false);
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
const observer = useMemo(() => {
|
||||
return new IntersectionObserver(
|
||||
([entry]) => setIntersecting(entry.isIntersecting),
|
||||
);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue