From faf48d17b95be3d40904f4ea85533720b7feff48 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 30 May 2022 14:43:35 -0500 Subject: [PATCH] PrivacyDropdown: convert to TSX --- .../compose/components/privacy_dropdown.js | Bin 9209 -> 0 bytes .../compose/components/privacy_dropdown.tsx | 263 ++++++++++++++++++ 2 files changed, 263 insertions(+) delete mode 100644 app/soapbox/features/compose/components/privacy_dropdown.js create mode 100644 app/soapbox/features/compose/components/privacy_dropdown.tsx diff --git a/app/soapbox/features/compose/components/privacy_dropdown.js b/app/soapbox/features/compose/components/privacy_dropdown.js deleted file mode 100644 index d6ddf771c0a3bf4de537c71927b1cd0cd2eb1135..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9209 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 Ps+?}T>jyJ)I~e~L5%hwp diff --git a/app/soapbox/features/compose/components/privacy_dropdown.tsx b/app/soapbox/features/compose/components/privacy_dropdown.tsx new file mode 100644 index 0000000000..3e3440ae4e --- /dev/null +++ b/app/soapbox/features/compose/components/privacy_dropdown.tsx @@ -0,0 +1,263 @@ +import classNames from 'classnames'; +import { supportsPassiveEvents } from 'detect-passive-events'; +import React, { useState, useRef, useEffect } from 'react'; +import { useIntl, defineMessages } from 'react-intl'; +import { spring } from 'react-motion'; +// @ts-ignore +import Overlay from 'react-overlays/lib/Overlay'; + +import Icon from 'soapbox/components/icon'; + +import { IconButton } from '../../../components/ui'; +import Motion from '../../ui/util/optional_motion'; + +const messages = defineMessages({ + public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, + public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' }, + unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, + unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not post to public timelines' }, + private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, + private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' }, + direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, + direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' }, + change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust post privacy' }, +}); + +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + +interface IPrivacyDropdownMenu { + style?: React.CSSProperties, + items: any[], + value: string, + placement: string, + onClose: () => void, + onChange: (value: string | null) => void, + unavailable?: boolean, +} + +const PrivacyDropdownMenu: React.FC = ({ style, items, placement, value, onClose, onChange }) => { + const node = useRef(null); + const focusedItem = useRef(null); + + const [mounted, setMounted] = useState(false); + + const handleDocumentClick = (e: MouseEvent | TouchEvent) => { + if (node.current && !node.current.contains(e.target as HTMLElement)) { + onClose(); + } + }; + + const handleKeyDown: React.KeyboardEventHandler = e => { + const value = e.currentTarget.getAttribute('data-index'); + const index = items.findIndex(item => { + return (item.value === value); + }); + let element = null; + + switch (e.key) { + case 'Escape': + onClose(); + break; + case 'Enter': + handleClick(e); + break; + case 'ArrowDown': + element = node.current?.childNodes[index + 1] || node.current?.firstChild; + break; + case 'ArrowUp': + element = node.current?.childNodes[index - 1] || node.current?.lastChild; + break; + case 'Tab': + if (e.shiftKey) { + element = node.current?.childNodes[index - 1] || node.current?.lastChild; + } else { + element = node.current?.childNodes[index + 1] || node.current?.firstChild; + } + break; + case 'Home': + element = node.current?.firstChild; + break; + case 'End': + element = node.current?.lastChild; + break; + } + + if (element) { + (element as HTMLElement).focus(); + onChange((element as HTMLElement).getAttribute('data-index')); + e.preventDefault(); + e.stopPropagation(); + } + }; + + const handleClick: React.EventHandler = (e: MouseEvent | KeyboardEvent) => { + const value = (e.currentTarget as HTMLElement)?.getAttribute('data-index'); + + e.preventDefault(); + + onClose(); + onChange(value); + }; + + useEffect(() => { + document.addEventListener('click', handleDocumentClick, false); + document.addEventListener('touchend', handleDocumentClick, listenerOptions); + + focusedItem.current?.focus({ preventScroll: true }); + setMounted(true); + + return () => { + document.removeEventListener('click', handleDocumentClick, false); + document.removeEventListener('touchend', handleDocumentClick); + }; + }); + + return ( + + {({ opacity, scaleX, scaleY }) => ( + // It should not be transformed when mounting because the resulting + // size will be used to determine the coordinate of the menu by + // react-overlays +
+ {items.map(item => ( +
+
+ +
+ +
+ {item.text} + {item.meta} +
+
+ ))} +
+ )} +
+ ); +}; + +interface IPrivacyDropdown { + isUserTouching: () => boolean, + isModalOpen: boolean, + onModalOpen: (opts: any) => void, + onModalClose: () => void, + value: string, + onChange: (value: string | null) => void, + unavailable: boolean, +} + +const PrivacyDropdown: React.FC = ({ + isUserTouching, + onChange, + onModalClose, + onModalOpen, + value, + unavailable, +}) => { + const intl = useIntl(); + const activeElement = useRef(null); + + const [open, setOpen] = useState(false); + const [placement, setPlacement] = useState('bottom'); + + const options = [ + { icon: require('@tabler/icons/icons/world.svg'), value: 'public', text: intl.formatMessage(messages.public_short), meta: intl.formatMessage(messages.public_long) }, + { icon: require('@tabler/icons/icons/lock-open.svg'), value: 'unlisted', text: intl.formatMessage(messages.unlisted_short), meta: intl.formatMessage(messages.unlisted_long) }, + { icon: require('@tabler/icons/icons/lock.svg'), value: 'private', text: intl.formatMessage(messages.private_short), meta: intl.formatMessage(messages.private_long) }, + { icon: require('@tabler/icons/icons/mail.svg'), value: 'direct', text: intl.formatMessage(messages.direct_short), meta: intl.formatMessage(messages.direct_long) }, + ]; + + const handleToggle: React.MouseEventHandler = (e) => { + if (isUserTouching()) { + if (open) { + onModalClose(); + } else { + onModalOpen({ + actions: options.map(option => ({ ...option, active: option.value === value })), + onClick: handleModalActionClick, + }); + } + } else { + const { top } = e.currentTarget.getBoundingClientRect(); + if (open) { + activeElement.current?.focus(); + } + setPlacement(top * 2 < innerHeight ? 'bottom' : 'top'); + setOpen(!open); + } + e.stopPropagation(); + }; + + const handleModalActionClick: React.MouseEventHandler = (e) => { + e.preventDefault(); + + const { value } = options[e.currentTarget.getAttribute('data-index') as any]; + + onModalClose(); + onChange(value); + }; + + const handleKeyDown: React.KeyboardEventHandler = e => { + switch (e.key) { + case 'Escape': + handleClose(); + break; + } + }; + + const handleMouseDown = () => { + if (!open) { + activeElement.current = document.activeElement as HTMLElement | null; + } + }; + + const handleButtonKeyDown: React.KeyboardEventHandler = (e) => { + switch (e.key) { + case ' ': + case 'Enter': + handleMouseDown(); + break; + } + }; + + const handleClose = () => { + if (open) { + activeElement.current?.focus(); + } + setOpen(false); + }; + + if (unavailable) { + return null; + } + + const valueOption = options.find(item => item.value === value); + + return ( +
+
+ +
+ + + + +
+ ); +}; + +export default PrivacyDropdown;