diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx deleted file mode 100644 index d481b030a8..0000000000 --- a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx +++ /dev/null @@ -1,158 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { injectIntl, defineMessages } from 'react-intl'; - -import classNames from 'classnames'; - -import Overlay from 'react-overlays/Overlay'; - -import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; -import LockIcon from '@/material-icons/400-24px/lock.svg?react'; -import PublicIcon from '@/material-icons/400-24px/public.svg?react'; -import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react'; -import { DropdownSelector } from 'flavours/glitch/components/dropdown_selector'; -import { Icon } from 'flavours/glitch/components/icon'; - -export const messages = defineMessages({ - public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, - public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' }, - unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Quiet public' }, - unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Hidden from Mastodon search results, trending, and public timelines' }, - private_short: { id: 'privacy.private.short', defaultMessage: 'Followers' }, - private_long: { id: 'privacy.private.long', defaultMessage: 'Only your followers' }, - direct_short: { id: 'privacy.direct.short', defaultMessage: 'Specific people' }, - direct_long: { id: 'privacy.direct.long', defaultMessage: 'Everyone mentioned in the post' }, - change_privacy: { id: 'privacy.change', defaultMessage: 'Change post privacy' }, - unlisted_extra: { id: 'privacy.unlisted.additional', defaultMessage: 'This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.' }, -}); - -class PrivacyDropdown extends PureComponent { - - static propTypes = { - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - noDirect: PropTypes.bool, - container: PropTypes.func, - disabled: PropTypes.bool, - intl: PropTypes.object.isRequired, - }; - - state = { - open: false, - placement: 'bottom', - }; - - handleToggle = () => { - if (this.state.open && this.activeElement) { - this.activeElement.focus({ preventScroll: true }); - } - - this.setState({ open: !this.state.open }); - }; - - handleKeyDown = e => { - switch(e.key) { - case 'Escape': - this.handleClose(); - break; - } - }; - - handleMouseDown = () => { - if (!this.state.open) { - this.activeElement = document.activeElement; - } - }; - - handleButtonKeyDown = (e) => { - switch(e.key) { - case ' ': - case 'Enter': - this.handleMouseDown(); - break; - } - }; - - handleClose = () => { - if (this.state.open && this.activeElement) { - this.activeElement.focus({ preventScroll: true }); - } - this.setState({ open: false }); - }; - - handleChange = value => { - this.props.onChange(value); - }; - - UNSAFE_componentWillMount () { - const { intl: { formatMessage } } = this.props; - - this.options = [ - { icon: 'globe', iconComponent: PublicIcon, value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) }, - { icon: 'unlock', iconComponent: QuietTimeIcon, value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long), extra: formatMessage(messages.unlisted_extra) }, - { icon: 'lock', iconComponent: LockIcon, value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) }, - ]; - - if (!this.props.noDirect) { - this.options.push( - { icon: 'at', iconComponent: AlternateEmailIcon, value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) }, - ); - } - } - - setTargetRef = c => { - this.target = c; - }; - - findTarget = () => { - return this.target; - }; - - handleOverlayEnter = (state) => { - this.setState({ placement: state.placement }); - }; - - render () { - const { value, container, disabled, intl } = this.props; - const { open, placement } = this.state; - - const valueOption = this.options.find(item => item.value === value); - - return ( -
- - - - {({ props, placement }) => ( -
-
- -
-
- )} -
-
- ); - } - -} - -export default injectIntl(PrivacyDropdown); diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.tsx b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.tsx new file mode 100644 index 0000000000..ae15d01dce --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.tsx @@ -0,0 +1,199 @@ +import { useCallback, useRef, useState } from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import classNames from 'classnames'; + +import type { OverlayProps } from 'react-overlays/Overlay'; +import Overlay from 'react-overlays/Overlay'; + +import type { StatusVisibility } from '@/flavours/glitch/api_types/statuses'; +import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; +import LockIcon from '@/material-icons/400-24px/lock.svg?react'; +import PublicIcon from '@/material-icons/400-24px/public.svg?react'; +import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react'; +import { DropdownSelector } from 'flavours/glitch/components/dropdown_selector'; +import { Icon } from 'flavours/glitch/components/icon'; + +export const messages = defineMessages({ + public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, + public_long: { + id: 'privacy.public.long', + defaultMessage: 'Anyone on and off Mastodon', + }, + unlisted_short: { + id: 'privacy.unlisted.short', + defaultMessage: 'Quiet public', + }, + unlisted_long: { + id: 'privacy.unlisted.long', + defaultMessage: + 'Hidden from Mastodon search results, trending, and public timelines', + }, + private_short: { id: 'privacy.private.short', defaultMessage: 'Followers' }, + private_long: { + id: 'privacy.private.long', + defaultMessage: 'Only your followers', + }, + direct_short: { + id: 'privacy.direct.short', + defaultMessage: 'Specific people', + }, + direct_long: { + id: 'privacy.direct.long', + defaultMessage: 'Everyone mentioned in the post', + }, + change_privacy: { + id: 'privacy.change', + defaultMessage: 'Change post privacy', + }, + unlisted_extra: { + id: 'privacy.unlisted.additional', + defaultMessage: + 'This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.', + }, +}); + +interface PrivacyDropdownProps { + value: StatusVisibility; + onChange: (value: StatusVisibility) => void; + noDirect?: boolean; + container?: OverlayProps['container']; + disabled?: boolean; +} + +const PrivacyDropdown: React.FC = ({ + value, + onChange, + noDirect, + container, + disabled, +}) => { + const intl = useIntl(); + const overlayTargetRef = useRef(null); + const previousFocusTargetRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); + + const handleClose = useCallback(() => { + if (isOpen && previousFocusTargetRef.current) { + previousFocusTargetRef.current.focus({ preventScroll: true }); + } + setIsOpen(false); + }, [isOpen]); + + const handleToggle = useCallback(() => { + if (isOpen) { + handleClose(); + } + setIsOpen((prev) => !prev); + }, [handleClose, isOpen]); + + const registerPreviousFocusTarget = useCallback(() => { + if (!isOpen) { + previousFocusTargetRef.current = document.activeElement as HTMLElement; + } + }, [isOpen]); + + const handleButtonKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if ([' ', 'Enter'].includes(e.key)) { + registerPreviousFocusTarget(); + } + }, + [registerPreviousFocusTarget], + ); + + const options = [ + { + icon: 'globe', + iconComponent: PublicIcon, + value: 'public', + text: intl.formatMessage(messages.public_short), + meta: intl.formatMessage(messages.public_long), + }, + { + icon: 'unlock', + iconComponent: QuietTimeIcon, + value: 'unlisted', + text: intl.formatMessage(messages.unlisted_short), + meta: intl.formatMessage(messages.unlisted_long), + extra: intl.formatMessage(messages.unlisted_extra), + }, + { + icon: 'lock', + iconComponent: LockIcon, + value: 'private', + text: intl.formatMessage(messages.private_short), + meta: intl.formatMessage(messages.private_long), + }, + ]; + + if (!noDirect) { + options.push({ + icon: 'at', + iconComponent: AlternateEmailIcon, + value: 'direct', + text: intl.formatMessage(messages.direct_short), + meta: intl.formatMessage(messages.direct_long), + }); + } + + const selectedOption = + options.find((item) => item.value === value) ?? options.at(0); + + return ( +
+ + + + {({ props, placement }) => ( +
+
+ +
+
+ )} +
+
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export default PrivacyDropdown; diff --git a/app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js b/app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js deleted file mode 100644 index a44b5c0d97..0000000000 --- a/app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js +++ /dev/null @@ -1,19 +0,0 @@ -import { connect } from 'react-redux'; - -import { changeComposeVisibility } from '@/flavours/glitch/actions/compose_typed'; - -import PrivacyDropdown from '../components/privacy_dropdown'; - -const mapStateToProps = state => ({ - value: state.getIn(['compose', 'privacy']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (value) { - dispatch(changeComposeVisibility(value)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown); diff --git a/app/javascript/flavours/glitch/features/ui/components/boost_modal.tsx b/app/javascript/flavours/glitch/features/ui/components/boost_modal.tsx index 3af3b28890..fb86a215f2 100644 --- a/app/javascript/flavours/glitch/features/ui/components/boost_modal.tsx +++ b/app/javascript/flavours/glitch/features/ui/components/boost_modal.tsx @@ -53,7 +53,10 @@ export const BoostModal: React.FC<{ }, [onClose]); const findContainer = useCallback( - () => document.getElementsByClassName('modal-root__container')[0], + () => + document.getElementsByClassName( + 'modal-root__container', + )[0] as HTMLDivElement, [], );