diff --git a/app/soapbox/components/ui/index.ts b/app/soapbox/components/ui/index.ts index 20bcd31c8..d66c98195 100644 --- a/app/soapbox/components/ui/index.ts +++ b/app/soapbox/components/ui/index.ts @@ -39,6 +39,7 @@ export { } from './menu/menu'; export { default as Modal } from './modal/modal'; 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 ProgressBar } from './progress-bar/progress-bar'; export { default as RadioButton } from './radio-button/radio-button'; diff --git a/app/soapbox/components/ui/popover/popover.tsx b/app/soapbox/components/ui/popover/popover.tsx new file mode 100644 index 000000000..51dc19b4c --- /dev/null +++ b/app/soapbox/components/ui/popover/popover.tsx @@ -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> + content: React.ReactNode +} + +/** + * Popover + * + * Similar to tooltip, but requires a click and is used for larger blocks + * of information. + */ +const Popover: React.FC = (props) => { + const { children, content } = props; + + const [isOpen, setIsOpen] = useState(false); + + const arrowRef = useRef(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) && ( +
+ {content} + + +
+ )} + + ); +}; + +export default Popover; \ No newline at end of file diff --git a/app/soapbox/features/group/components/group-privacy.tsx b/app/soapbox/features/group/components/group-privacy.tsx index fdbbe2977..d4f1b5b90 100644 --- a/app/soapbox/features/group/components/group-privacy.tsx +++ b/app/soapbox/features/group/components/group-privacy.tsx @@ -1,7 +1,7 @@ import React from 'react'; 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'; interface IGroupPolicy { @@ -9,24 +9,59 @@ interface IGroupPolicy { } const GroupPrivacy = ({ group }: IGroupPolicy) => ( - - + +
+ +
- - {group.locked ? ( - - ) : ( - - )} - -
+ + + {group.locked ? ( + + ) : ( + + )} + + + + {group.locked ? ( + + ) : ( + + )} + + + + } + > + + + + + {group.locked ? ( + + ) : ( + + )} + + + ); export default GroupPrivacy; \ No newline at end of file diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index 9bccb68a2..453639a91 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -793,7 +793,11 @@ "group.leave.success": "Left the group", "group.manage": "Manage Group", "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.full": "Public Group", + "group.privacy.public.info": "Discoverable. Anyone can join.", "group.role.admin": "Admin", "group.role.moderator": "Moderator", "group.tabs.all": "All",