Add popover component
This commit is contained in:
parent
af69c7564e
commit
283935e837
4 changed files with 148 additions and 18 deletions
|
@ -39,6 +39,7 @@ export {
|
||||||
} from './menu/menu';
|
} from './menu/menu';
|
||||||
export { default as Modal } from './modal/modal';
|
export { default as Modal } from './modal/modal';
|
||||||
export { default as PhoneInput } from './phone-input/phone-input';
|
export { default as PhoneInput } from './phone-input/phone-input';
|
||||||
|
export { default as Popover } from './popover/popover';
|
||||||
export { default as Portal } from './portal/portal';
|
export { default as Portal } from './portal/portal';
|
||||||
export { default as ProgressBar } from './progress-bar/progress-bar';
|
export { default as ProgressBar } from './progress-bar/progress-bar';
|
||||||
export { default as RadioButton } from './radio-button/radio-button';
|
export { default as RadioButton } from './radio-button/radio-button';
|
||||||
|
|
90
app/soapbox/components/ui/popover/popover.tsx
Normal file
90
app/soapbox/components/ui/popover/popover.tsx
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import {
|
||||||
|
arrow,
|
||||||
|
FloatingArrow,
|
||||||
|
offset,
|
||||||
|
useClick,
|
||||||
|
useDismiss,
|
||||||
|
useFloating,
|
||||||
|
useInteractions,
|
||||||
|
useTransitionStyles,
|
||||||
|
} from '@floating-ui/react';
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
|
||||||
|
interface IPopover {
|
||||||
|
children: React.ReactElement<any, string | React.JSXElementConstructor<any>>
|
||||||
|
content: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Popover
|
||||||
|
*
|
||||||
|
* Similar to tooltip, but requires a click and is used for larger blocks
|
||||||
|
* of information.
|
||||||
|
*/
|
||||||
|
const Popover: React.FC<IPopover> = (props) => {
|
||||||
|
const { children, content } = props;
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const arrowRef = useRef<SVGSVGElement>(null);
|
||||||
|
|
||||||
|
const { x, y, strategy, refs, context } = useFloating({
|
||||||
|
open: isOpen,
|
||||||
|
onOpenChange: setIsOpen,
|
||||||
|
placement: 'top',
|
||||||
|
middleware: [
|
||||||
|
offset(10),
|
||||||
|
arrow({
|
||||||
|
element: arrowRef,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const click = useClick(context);
|
||||||
|
const dismiss = useDismiss(context);
|
||||||
|
const { isMounted, styles } = useTransitionStyles(context, {
|
||||||
|
initial: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'scale(0.8)',
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
open: 200,
|
||||||
|
close: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getReferenceProps, getFloatingProps } = useInteractions([
|
||||||
|
click,
|
||||||
|
dismiss,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{React.cloneElement(children, {
|
||||||
|
ref: refs.setReference,
|
||||||
|
...getReferenceProps(),
|
||||||
|
className: 'cursor-help',
|
||||||
|
})}
|
||||||
|
|
||||||
|
{(isMounted) && (
|
||||||
|
<div
|
||||||
|
ref={refs.setFloating}
|
||||||
|
style={{
|
||||||
|
position: strategy,
|
||||||
|
top: y ?? 0,
|
||||||
|
left: x ?? 0,
|
||||||
|
...styles,
|
||||||
|
}}
|
||||||
|
className='rounded-lg bg-white p-6 shadow-2xl dark:bg-gray-900 dark:ring-2 dark:ring-primary-700'
|
||||||
|
{...getFloatingProps()}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
|
||||||
|
<FloatingArrow ref={arrowRef} context={context} className='fill-white dark:hidden' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Popover;
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { HStack, Icon, Text } from 'soapbox/components/ui';
|
import { HStack, Icon, Popover, Stack, Text } from 'soapbox/components/ui';
|
||||||
import { Group } from 'soapbox/types/entities';
|
import { Group } from 'soapbox/types/entities';
|
||||||
|
|
||||||
interface IGroupPolicy {
|
interface IGroupPolicy {
|
||||||
|
@ -9,24 +9,59 @@ interface IGroupPolicy {
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupPrivacy = ({ group }: IGroupPolicy) => (
|
const GroupPrivacy = ({ group }: IGroupPolicy) => (
|
||||||
<HStack space={1} alignItems='center' data-testid='group-privacy'>
|
<Popover
|
||||||
<Icon
|
content={
|
||||||
className='h-4 w-4'
|
<Stack space={4} alignItems='center' className='w-72'>
|
||||||
src={
|
<div className='rounded-full bg-gray-200 p-3 dark:bg-gray-800'>
|
||||||
group.locked
|
<Icon
|
||||||
? require('@tabler/icons/lock.svg')
|
src={
|
||||||
: require('@tabler/icons/world.svg')
|
group.locked
|
||||||
}
|
? require('@tabler/icons/lock.svg')
|
||||||
/>
|
: require('@tabler/icons/world.svg')
|
||||||
|
}
|
||||||
|
className='h-6 w-6 text-gray-600 dark:text-gray-600'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Text theme='inherit' tag='span' size='sm' weight='medium'>
|
<Stack space={1} alignItems='center'>
|
||||||
{group.locked ? (
|
<Text size='lg' weight='bold' align='center'>
|
||||||
<FormattedMessage id='group.privacy.locked' defaultMessage='Private' />
|
{group.locked ? (
|
||||||
) : (
|
<FormattedMessage id='group.privacy.locked.full' defaultMessage='Private Group' />
|
||||||
<FormattedMessage id='group.privacy.public' defaultMessage='Public' />
|
) : (
|
||||||
)}
|
<FormattedMessage id='group.privacy.public.full' defaultMessage='Public Group' />
|
||||||
</Text>
|
)}
|
||||||
</HStack>
|
</Text>
|
||||||
|
|
||||||
|
<Text theme='muted' align='center'>
|
||||||
|
{group.locked ? (
|
||||||
|
<FormattedMessage id='group.privacy.locked.info' defaultMessage='Discoverable. Users can join after their request is approved.' />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage id='group.privacy.public.info' defaultMessage='Discoverable. Anyone can join.' />
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<HStack space={1} alignItems='center' data-testid='group-privacy'>
|
||||||
|
<Icon
|
||||||
|
className='h-4 w-4'
|
||||||
|
src={
|
||||||
|
group.locked
|
||||||
|
? require('@tabler/icons/lock.svg')
|
||||||
|
: require('@tabler/icons/world.svg')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text theme='inherit' tag='span' size='sm' weight='medium'>
|
||||||
|
{group.locked ? (
|
||||||
|
<FormattedMessage id='group.privacy.locked' defaultMessage='Private' />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage id='group.privacy.public' defaultMessage='Public' />
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Popover>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default GroupPrivacy;
|
export default GroupPrivacy;
|
|
@ -793,7 +793,11 @@
|
||||||
"group.leave.success": "Left the group",
|
"group.leave.success": "Left the group",
|
||||||
"group.manage": "Manage Group",
|
"group.manage": "Manage Group",
|
||||||
"group.privacy.locked": "Private",
|
"group.privacy.locked": "Private",
|
||||||
|
"group.privacy.locked.full": "Private Group",
|
||||||
|
"group.privacy.locked.info": "Discoverable. Users can join after their request is approved.",
|
||||||
"group.privacy.public": "Public",
|
"group.privacy.public": "Public",
|
||||||
|
"group.privacy.public.full": "Public Group",
|
||||||
|
"group.privacy.public.info": "Discoverable. Anyone can join.",
|
||||||
"group.role.admin": "Admin",
|
"group.role.admin": "Admin",
|
||||||
"group.role.moderator": "Moderator",
|
"group.role.moderator": "Moderator",
|
||||||
"group.tabs.all": "All",
|
"group.tabs.all": "All",
|
||||||
|
|
Loading…
Reference in a new issue