Merge branch 'fork' into pl-api

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2024-08-17 00:38:53 +02:00
12 changed files with 342 additions and 618 deletions

View File

@ -1,6 +1,4 @@
import { offset, useFloating, flip, arrow, shift } from '@floating-ui/react';
import clsx from 'clsx';
import { supportsPassiveEvents } from 'detect-passive-events';
import fuzzysort from 'fuzzysort';
import { Map as ImmutableMap } from 'immutable';
import React, { useEffect, useMemo, useRef, useState } from 'react';
@ -8,7 +6,8 @@ import { defineMessages, useIntl } from 'react-intl';
import { createSelector } from 'reselect';
import { addComposeLanguage, changeComposeLanguage, changeComposeModifiedLanguage, deleteComposeLanguage } from 'soapbox/actions/compose';
import { Button, Icon, Input, Portal } from 'soapbox/components/ui';
import DropdownMenu from 'soapbox/components/dropdown-menu';
import { Button, Icon, Input } from 'soapbox/components/ui';
import { type Language, languages as languagesObject } from 'soapbox/features/preferences';
import { useAppDispatch, useAppSelector, useCompose, useFeatures } from 'soapbox/hooks';
@ -21,8 +20,6 @@ const getFrequentlyUsedLanguages = createSelector([
.toArray()
));
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
const languages = Object.entries(languagesObject) as Array<[Language, string]>;
const messages = defineMessages({
@ -35,95 +32,26 @@ const messages = defineMessages({
});
interface ILanguageDropdown {
composeId: string;
handleClose: () => any;
}
const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
const getLanguageDropdown = (composeId: string): React.FC<ILanguageDropdown> => ({ handleClose: handleMenuClose }) => {
const intl = useIntl();
const features = useFeatures();
const dispatch = useAppDispatch();
const frequentlyUsedLanguages = useAppSelector(getFrequentlyUsedLanguages);
const node = useRef<HTMLDivElement>(null);
const focusedItem = useRef<HTMLDivElement>(null);
const arrowRef = useRef<HTMLDivElement>(null);
const focusedItem = useRef<HTMLButtonElement>(null);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [searchValue, setSearchValue] = useState('');
const { x, y, strategy, refs, middlewareData, placement } = useFloating<HTMLButtonElement>({
placement: 'top',
middleware: [
offset(12),
flip(),
shift({
padding: 8,
}),
arrow({
element: arrowRef,
}),
],
});
const {
language,
modified_language: modifiedLanguage,
suggested_language: suggestedLanguage,
textMap,
} = useCompose(composeId);
const handleClick: React.EventHandler<
React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
> = (event) => {
event.stopPropagation();
setIsOpen(!isOpen);
};
const handleKeyPress: React.EventHandler<React.KeyboardEvent<HTMLButtonElement>> = (event) => {
switch (event.key) {
case ' ':
case 'Enter':
event.stopPropagation();
event.preventDefault();
handleClick(event);
break;
}
};
const handleOptionKeyDown: React.KeyboardEventHandler = e => {
const value = e.currentTarget.getAttribute('data-index');
const index = results.findIndex(([key]) => key === value);
let element: ChildNode | null | undefined = null;
switch (e.key) {
case 'Escape':
handleClose();
break;
case 'Enter':
handleOptionClick(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 'Home':
element = node.current?.firstChild;
break;
case 'End':
element = node.current?.lastChild;
break;
}
if (element) {
(element as HTMLElement).focus();
e.preventDefault();
e.stopPropagation();
}
};
const handleOptionClick: React.EventHandler<any> = (e: MouseEvent | KeyboardEvent) => {
const value = (e.currentTarget as HTMLElement)?.getAttribute('data-index') as Language;
@ -146,7 +74,6 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
e.preventDefault();
e.stopPropagation();
handleClose();
dispatch(addComposeLanguage(composeId, value));
};
@ -156,7 +83,6 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
e.preventDefault();
e.stopPropagation();
handleClose();
dispatch(deleteComposeLanguage(composeId, value));
};
@ -202,104 +128,105 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
}).map(result => result.obj);
};
const handleDocumentClick = (event: Event) => {
if (refs.floating.current && !refs.floating.current.contains(event.target as Node)) {
handleClose();
}
};
const handleKeyDown = (e: KeyboardEvent) => {
if (!refs.floating.current) return;
const items = Array.from(refs.floating.current.getElementsByTagName('a'));
const index = items.indexOf(document.activeElement as any);
let element = null;
switch (e.key) {
case 'ArrowDown':
element = items[index + 1] || items[0];
break;
case 'ArrowUp':
element = items[index - 1] || items[items.length - 1];
break;
case 'Tab':
if (e.shiftKey) {
element = items[index - 1] || items[items.length - 1];
} else {
element = items[index + 1] || items[0];
}
break;
case 'Home':
element = items[0];
break;
case 'End':
element = items[items.length - 1];
break;
case 'Escape':
handleClose();
break;
}
if (element) {
element.focus();
e.preventDefault();
e.stopPropagation();
}
};
const handleClose = () => {
setSearchValue('');
setIsOpen(false);
handleMenuClose();
};
const arrowProps: React.CSSProperties = useMemo(() => {
if (middlewareData.arrow) {
const { x, y } = middlewareData.arrow;
const staticPlacement = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
}[placement.split('-')[0]];
return {
left: x !== null ? `${x}px` : '',
top: y !== null ? `${y}px` : '',
// Ensure the static side gets unset when
// flipping to other placements' axes.
right: '',
bottom: '',
[staticPlacement as string]: `${(-(arrowRef.current?.offsetWidth || 0)) / 2}px`,
transform: 'rotate(45deg)',
};
}
return {};
}, [middlewareData.arrow, placement]);
useEffect(() => {
if (isOpen) {
if (refs.floating.current) {
(refs.floating.current?.querySelector('div[aria-selected=true]') as HTMLDivElement)?.focus();
}
document.addEventListener('click', handleDocumentClick, false);
document.addEventListener('keydown', handleKeyDown, false);
document.addEventListener('touchend', handleDocumentClick, listenerOptions);
return () => {
document.removeEventListener('click', handleDocumentClick);
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('touchend', handleDocumentClick);
};
if (node.current) {
(node.current?.querySelector('div[aria-selected=true]') as HTMLDivElement)?.focus();
}
}, [isOpen, refs.floating.current]);
}, [node.current]);
const isSearching = searchValue !== '';
const results = search();
return (
<>
<label className='relative block grow p-2 pt-1'>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
<Input
className='w-64'
type='text'
value={searchValue}
onChange={({ target }) => setSearchValue(target.value)}
outerClassName='mt-0'
placeholder={intl.formatMessage(messages.search)}
/>
<div role='button' tabIndex={0} className='absolute inset-y-0 right-0 flex cursor-pointer items-center px-5 rtl:left-0 rtl:right-auto' onClick={handleClear}>
<Icon
className='h-5 w-5 text-gray-600'
src={isSearching ? require('@tabler/icons/outline/backspace.svg') : require('@tabler/icons/outline/search.svg')}
aria-label={intl.formatMessage(messages.search)}
/>
</div>
</label>
<div className='-mb-1 h-96 w-full overflow-auto' ref={node} tabIndex={-1}>
{results.map(([code, name]) => {
const active = code === language;
const modified = code === modifiedLanguage;
return (
<button
role='option'
tabIndex={0}
key={code}
data-index={code}
onClick={handleOptionClick}
className={clsx(
'flex w-full gap-2 p-2.5 text-left text-sm text-gray-700 dark:text-gray-400',
{
'bg-gray-100 dark:bg-gray-800 black:bg-gray-900 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700': modified,
'cursor-pointer hover:bg-gray-100 black:hover:bg-gray-900 dark:hover:bg-gray-800': !textMap.size || textMap.has(code),
'cursor-pointer': active,
'cursor-default': !active && !(!textMap.size || textMap.has(code)),
},
)}
aria-selected={active}
ref={active ? focusedItem : null}
>
<div
className={clsx('flex-auto grow text-primary-600 dark:text-primary-400', {
'text-black dark:text-white': modified,
})}
>
{name}
</div>
{features.multiLanguage && !!language && !active && (
textMap.has(code) ? (
<button title={intl.formatMessage(messages.deleteLanguage)} onClick={handleDeleteLanguageClick}>
<Icon className='h-4 w-4' src={require('@tabler/icons/outline/minus.svg')} />
</button>
) : (
<button title={intl.formatMessage(messages.addLanguage)} onClick={handleAddLanguageClick}>
<Icon className='h-4 w-4' src={require('@tabler/icons/outline/plus.svg')} />
</button>
)
)}
</button>
);
})}
</div>
</>
);
};
interface ILanguageDropdownButton {
composeId: string;
}
const LanguageDropdownButton: React.FC<ILanguageDropdownButton> = ({ composeId }) => {
const intl = useIntl();
const {
language,
modified_language: modifiedLanguage,
suggested_language: suggestedLanguage,
textMap,
} = useCompose(composeId);
let buttonLabel = intl.formatMessage(messages.languagePrompt);
if (language) {
const list: string[] = [languagesObject[modifiedLanguage || language]];
@ -311,8 +238,12 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
language: languagesObject[suggestedLanguage as Language] || suggestedLanguage,
});
const LanguageDropdown = useMemo(() => getLanguageDropdown(composeId), [composeId]);
return (
<>
<DropdownMenu
component={LanguageDropdown}
>
<Button
theme='muted'
size='xs'
@ -320,102 +251,10 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
icon={require('@tabler/icons/outline/language.svg')}
secondaryIcon={require('@tabler/icons/outline/chevron-down.svg')}
title={intl.formatMessage(messages.languagePrompt)}
onClick={handleClick}
onKeyPress={handleKeyPress}
ref={refs.setReference}
/>
{isOpen ? (
<Portal>
<div
id='language-dropdown'
ref={refs.setFloating}
className={clsx('z-[1001] flex flex-col rounded-md bg-white text-sm shadow-lg transition-opacity duration-100 focus:outline-none black:border black:border-gray-800 black:bg-black dark:bg-gray-900 dark:ring-2 dark:ring-primary-700', {
'opacity-0 pointer-events-none': !isOpen,
})}
style={{
position: strategy,
top: y ?? 0,
left: x ?? 0,
}}
role='listbox'
>
<label className='relative grow p-2'>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
<Input
className='w-64'
type='text'
value={searchValue}
onChange={({ target }) => setSearchValue(target.value)}
outerClassName='mt-0'
placeholder={intl.formatMessage(messages.search)}
/>
<div role='button' tabIndex={0} className='absolute inset-y-0 right-0 flex cursor-pointer items-center px-5 rtl:left-0 rtl:right-auto' onClick={handleClear}>
<Icon
className='h-5 w-5 text-gray-600'
src={isSearching ? require('@tabler/icons/outline/backspace.svg') : require('@tabler/icons/outline/search.svg')}
aria-label={intl.formatMessage(messages.search)}
/>
</div>
</label>
<div className='h-96 w-full overflow-scroll' ref={node} tabIndex={-1}>
{results.map(([code, name]) => {
const active = code === language;
const modified = code === modifiedLanguage;
return (
<div
role='option'
tabIndex={0}
key={code}
data-index={code}
onKeyDown={handleOptionKeyDown}
onClick={handleOptionClick}
className={clsx(
'flex gap-2 p-2.5 text-sm text-gray-700 dark:text-gray-400',
{
'bg-gray-100 dark:bg-gray-800 black:bg-gray-900 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700': modified,
'cursor-pointer hover:bg-gray-100 black:hover:bg-gray-900 dark:hover:bg-gray-800': !textMap.size || textMap.has(code),
'cursor-pointer': active,
'cursor-default': !active && !(!textMap.size || textMap.has(code)),
},
)}
aria-selected={active}
ref={active ? focusedItem : null}
>
<div
className={clsx('flex-auto grow text-primary-600 dark:text-primary-400', {
'text-black dark:text-white': modified,
})}
>
{name}
</div>
{features.multiLanguage && !!language && !active && (
textMap.has(code) ? (
<button title={intl.formatMessage(messages.deleteLanguage)} onClick={handleDeleteLanguageClick}>
<Icon className='h-4 w-4' src={require('@tabler/icons/outline/minus.svg')} />
</button>
) : (
<button title={intl.formatMessage(messages.addLanguage)} onClick={handleAddLanguageClick}>
<Icon className='h-4 w-4' src={require('@tabler/icons/outline/plus.svg')} />
</button>
)
)}
</div>
);
})}
</div>
<div
ref={arrowRef}
style={arrowProps}
className='pointer-events-none absolute z-[-1] h-3 w-3 bg-white black:bg-black dark:bg-gray-900'
/>
</div>
</Portal>
) : null}
</>
</DropdownMenu>
);
};
export { LanguageDropdown as default };
export { LanguageDropdownButton as default };

View File

@ -1,19 +1,12 @@
import clsx from 'clsx';
import { supportsPassiveEvents } from 'detect-passive-events';
import React, { useState, useRef, useEffect } from 'react';
import React, { useRef } from 'react';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { spring } from 'react-motion';
// @ts-ignore
import Overlay from 'react-overlays/lib/Overlay';
import { changeComposeFederated, changeComposeVisibility } from 'soapbox/actions/compose';
import { closeModal, openModal } from 'soapbox/actions/modals';
import DropdownMenu from 'soapbox/components/dropdown-menu';
import Icon from 'soapbox/components/icon';
import { Button, Toggle } from 'soapbox/components/ui';
import { useAppDispatch, useCompose, useFeatures } from 'soapbox/hooks';
import { userTouching } from 'soapbox/is-mobile';
import Motion from '../../ui/util/optional-motion';
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
@ -33,8 +26,6 @@ const messages = defineMessages({
local: { id: 'privacy.local', defaultMessage: '{privacy} (local-only)' },
});
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
interface Option {
icon: string;
value: string;
@ -43,10 +34,8 @@ interface Option {
}
interface IPrivacyDropdownMenu {
style?: React.CSSProperties;
items: any[];
value: string;
placement: string;
onClose: () => void;
onChange: (value: string | null) => void;
unavailable?: boolean;
@ -56,21 +45,14 @@ interface IPrivacyDropdownMenu {
}
const PrivacyDropdownMenu: React.FC<IPrivacyDropdownMenu> = ({
style, items, placement, value, onClose, onChange, showFederated, federated, onChangeFederated,
items, value, onClose, onChange, showFederated, federated, onChangeFederated,
}) => {
const node = useRef<HTMLDivElement>(null);
const focusedItem = useRef<HTMLDivElement>(null);
const [mounted, setMounted] = useState<boolean>(false);
const handleDocumentClick = (e: MouseEvent | TouchEvent) => {
if (node.current && !node.current.contains(e.target as HTMLElement)) {
onClose();
}
};
const node = useRef<HTMLUListElement>(null);
const focusedItem = useRef<HTMLLIElement>(null);
const handleKeyDown: React.KeyboardEventHandler = e => {
const index = [...e.currentTarget.parentElement!.children].indexOf(e.currentTarget); let element: ChildNode | null | undefined = null;
const index = [...e.currentTarget.parentElement!.children].indexOf(e.currentTarget);
let element: ChildNode | null | undefined = null;
switch (e.key) {
case 'Escape':
@ -121,96 +103,67 @@ const PrivacyDropdownMenu: React.FC<IPrivacyDropdownMenu> = ({
}
};
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 (
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
{({ 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
<div
id='privacy-dropdown'
className={clsx('absolute z-[1000] ml-10 overflow-hidden rounded-md bg-white text-sm shadow-lg black:border black:border-gray-800 black:bg-black dark:bg-gray-900', {
'block shadow-md': open,
'origin-[50%_100%]': placement === 'top',
'origin-[50%_0]': placement === 'bottom',
})}
style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : undefined }}
role='listbox'
ref={node}
>
{items.map(item => {
const active = item.value === value;
return (
<div
role='option'
tabIndex={0}
key={item.value}
data-index={item.value}
onKeyDown={handleKeyDown}
onClick={handleClick}
className={clsx(
'flex cursor-pointer items-center p-2.5 text-gray-700 hover:bg-gray-100 black:hover:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800',
{ 'bg-gray-100 dark:bg-gray-800 black:bg-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700': active },
)}
aria-selected={active}
ref={active ? focusedItem : null}
>
<div className='mr-2.5 flex items-center justify-center rtl:ml-2.5 rtl:mr-0'>
<Icon src={item.icon} />
</div>
<div
className={clsx('flex-auto text-xs text-primary-600 dark:text-primary-400', {
'text-black dark:text-white': active,
})}
>
<strong className='block text-sm font-medium text-black dark:text-white'>{item.text}</strong>
{item.meta}
</div>
</div>
);
})}
{showFederated && (
<div
role='option'
tabIndex={0}
data-index='local_switch'
onKeyDown={handleKeyDown}
onClick={onChangeFederated}
className='flex cursor-pointer items-center p-2.5 text-xs text-gray-700 hover:bg-gray-100 focus:bg-gray-100 black:hover:bg-gray-900 black:focus:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:focus:bg-gray-800'
>
<div className='mr-2.5 flex items-center justify-center rtl:ml-2.5 rtl:mr-0'>
<Icon src={require('@tabler/icons/outline/affiliate.svg')} />
</div>
<div
className='flex-auto text-xs text-primary-600 dark:text-primary-400'
>
<strong className='block text-sm font-medium text-black focus:text-black dark:text-white dark:focus:text-primary-400'>
<FormattedMessage id='privacy.local.short' defaultMessage='Local-only' />
</strong>
<FormattedMessage id='privacy.local.long' defaultMessage='Only visible on your instance' />
</div>
<Toggle checked={!federated} onChange={onChangeFederated} />
<ul ref={node}>
{items.map(item => {
const active = item.value === value;
return (
<li
role='option'
tabIndex={0}
key={item.value}
data-index={item.value}
onKeyDown={handleKeyDown}
onClick={handleClick}
className={clsx(
'flex cursor-pointer items-center p-2.5 text-gray-700 hover:bg-gray-100 black:hover:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800',
{ 'bg-gray-100 dark:bg-gray-800 black:bg-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700': active },
)}
aria-selected={active}
ref={active ? focusedItem : null}
>
<div className='mr-2.5 flex items-center justify-center rtl:ml-2.5 rtl:mr-0'>
<Icon src={item.icon} />
</div>
)}
</div>
<div
className={clsx('flex-auto text-xs text-primary-600 dark:text-primary-400', {
'text-black dark:text-white': active,
})}
>
<strong className='block text-sm font-medium text-black dark:text-white'>{item.text}</strong>
{item.meta}
</div>
</li>
);
})}
{showFederated && (
<li
role='option'
tabIndex={0}
data-index='local_switch'
onKeyDown={handleKeyDown}
onClick={onChangeFederated}
className='flex cursor-pointer items-center p-2.5 text-xs text-gray-700 hover:bg-gray-100 focus:bg-gray-100 black:hover:bg-gray-900 black:focus:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:focus:bg-gray-800'
>
<div className='mr-2.5 flex items-center justify-center rtl:ml-2.5 rtl:mr-0'>
<Icon src={require('@tabler/icons/outline/affiliate.svg')} />
</div>
<div
className='flex-auto text-xs text-primary-600 dark:text-primary-400'
>
<strong className='block text-sm font-medium text-black focus:text-black dark:text-white dark:focus:text-primary-400'>
<FormattedMessage id='privacy.local.short' defaultMessage='Local-only' />
</strong>
<FormattedMessage id='privacy.local.long' defaultMessage='Only visible on your instance' />
</div>
<Toggle checked={!federated} onChange={onChangeFederated} />
</li>
)}
</Motion>
</ul>
);
};
@ -223,8 +176,6 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
}) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const node = useRef<HTMLDivElement>(null);
const activeElement = useRef<HTMLElement | null>(null);
const features = useFeatures();
const compose = useCompose(composeId);
@ -232,9 +183,6 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
const value = compose.privacy;
const unavailable = compose.id;
const [open, setOpen] = useState(false);
const [placement, setPlacement] = useState('bottom');
const options = [
{ icon: require('@tabler/icons/outline/world.svg'), value: 'public', text: intl.formatMessage(messages.public_short), meta: intl.formatMessage(messages.public_long) },
{ icon: require('@tabler/icons/outline/lock-open.svg'), value: 'unlisted', text: intl.formatMessage(messages.unlisted_short), meta: intl.formatMessage(messages.unlisted_long) },
@ -248,70 +196,6 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
const onChangeFederated = () => dispatch(changeComposeFederated(composeId));
const onModalOpen = (props: Record<string, any>) => dispatch(openModal('ACTIONS', props));
const onModalClose = () => dispatch(closeModal('ACTIONS'));
const handleToggle: React.MouseEventHandler<HTMLButtonElement> = (e) => {
if (userTouching.matches) {
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;
}
@ -319,41 +203,30 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
const valueOption = options.find(item => item.value === value);
return (
<div onKeyDown={handleKeyDown} ref={node}>
<div
className={clsx({
'rounded-t-md': open && placement === 'top',
active: valueOption && options.indexOf(valueOption) === 0,
})}
>
<Button
theme='muted'
size='xs'
text={compose.federated ? valueOption?.text : intl.formatMessage(messages.local, {
privacy: valueOption?.text,
})}
icon={valueOption?.icon}
secondaryIcon={require('@tabler/icons/outline/chevron-down.svg')}
title={intl.formatMessage(messages.change_privacy)}
onClick={handleToggle}
onMouseDown={handleMouseDown}
onKeyDown={handleButtonKeyDown}
/>
</div>
<Overlay show={open} placement={placement} target={node.current}>
<DropdownMenu
component={({ handleClose }) => (
<PrivacyDropdownMenu
items={options}
value={value}
onClose={handleClose}
onChange={onChange}
placement={placement}
showFederated={features.localOnlyStatuses}
federated={compose.federated}
onChangeFederated={onChangeFederated}
/>
</Overlay>
</div>
)}
>
<Button
theme='muted'
size='xs'
text={compose.federated ? valueOption?.text : intl.formatMessage(messages.local, {
privacy: valueOption?.text,
})}
icon={valueOption?.icon}
secondaryIcon={require('@tabler/icons/outline/chevron-down.svg')}
title={intl.formatMessage(messages.change_privacy)}
/>
</DropdownMenu>
);
};