diff --git a/app/javascript/mastodon/components/dropdown_selector.tsx b/app/javascript/mastodon/components/dropdown_selector.tsx
new file mode 100644
index 000000000..f8bf96c63
--- /dev/null
+++ b/app/javascript/mastodon/components/dropdown_selector.tsx
@@ -0,0 +1,185 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+import classNames from 'classnames';
+
+import { supportsPassiveEvents } from 'detect-passive-events';
+
+import InfoIcon from '@/material-icons/400-24px/info.svg?react';
+
+import type { IconProp } from './icon';
+import { Icon } from './icon';
+
+const listenerOptions = supportsPassiveEvents
+  ? { passive: true, capture: true }
+  : true;
+
+interface SelectItem {
+  value: string;
+  icon?: string;
+  iconComponent?: IconProp;
+  text: string;
+  meta: string;
+  extra?: string;
+}
+
+interface Props {
+  value: string;
+  classNamePrefix: string;
+  style?: React.CSSProperties;
+  items: SelectItem[];
+  onChange: (value: string) => void;
+  onClose: () => void;
+}
+
+export const DropdownSelector: React.FC<Props> = ({
+  style,
+  items,
+  value,
+  classNamePrefix = 'privacy-dropdown',
+  onClose,
+  onChange,
+}) => {
+  const nodeRef = useRef<HTMLUListElement>(null);
+  const focusedItemRef = useRef<HTMLLIElement>(null);
+  const [currentValue, setCurrentValue] = useState(value);
+
+  const handleDocumentClick = useCallback(
+    (e: MouseEvent | TouchEvent) => {
+      if (
+        nodeRef.current &&
+        e.target instanceof Node &&
+        !nodeRef.current.contains(e.target)
+      ) {
+        onClose();
+        e.stopPropagation();
+      }
+    },
+    [nodeRef, onClose],
+  );
+
+  const handleClick = useCallback(
+    (
+      e: React.MouseEvent<HTMLLIElement> | React.KeyboardEvent<HTMLLIElement>,
+    ) => {
+      const value = e.currentTarget.getAttribute('data-index');
+
+      e.preventDefault();
+
+      onClose();
+      if (value) onChange(value);
+    },
+    [onClose, onChange],
+  );
+
+  const handleKeyDown = useCallback(
+    (e: React.KeyboardEvent<HTMLLIElement>) => {
+      const value = e.currentTarget.getAttribute('data-index');
+      const index = items.findIndex((item) => item.value === value);
+
+      let element: Element | null | undefined = null;
+
+      switch (e.key) {
+        case 'Escape':
+          onClose();
+          break;
+        case ' ':
+        case 'Enter':
+          handleClick(e);
+          break;
+        case 'ArrowDown':
+          element =
+            nodeRef.current?.children[index + 1] ??
+            nodeRef.current?.firstElementChild;
+          break;
+        case 'ArrowUp':
+          element =
+            nodeRef.current?.children[index - 1] ??
+            nodeRef.current?.lastElementChild;
+          break;
+        case 'Tab':
+          if (e.shiftKey) {
+            element =
+              nodeRef.current?.children[index + 1] ??
+              nodeRef.current?.firstElementChild;
+          } else {
+            element =
+              nodeRef.current?.children[index - 1] ??
+              nodeRef.current?.lastElementChild;
+          }
+          break;
+        case 'Home':
+          element = nodeRef.current?.firstElementChild;
+          break;
+        case 'End':
+          element = nodeRef.current?.lastElementChild;
+          break;
+      }
+
+      if (element && element instanceof HTMLElement) {
+        const selectedValue = element.getAttribute('data-index');
+        element.focus();
+        if (selectedValue) setCurrentValue(selectedValue);
+        e.preventDefault();
+        e.stopPropagation();
+      }
+    },
+    [nodeRef, items, onClose, handleClick, setCurrentValue],
+  );
+
+  useEffect(() => {
+    document.addEventListener('click', handleDocumentClick, { capture: true });
+    document.addEventListener('touchend', handleDocumentClick, listenerOptions);
+    focusedItemRef.current?.focus({ preventScroll: true });
+
+    return () => {
+      document.removeEventListener('click', handleDocumentClick, {
+        capture: true,
+      });
+      document.removeEventListener(
+        'touchend',
+        handleDocumentClick,
+        listenerOptions,
+      );
+    };
+  }, [handleDocumentClick]);
+
+  return (
+    <ul style={style} role='listbox' ref={nodeRef}>
+      {items.map((item) => (
+        <li
+          role='option'
+          tabIndex={0}
+          key={item.value}
+          data-index={item.value}
+          onKeyDown={handleKeyDown}
+          onClick={handleClick}
+          className={classNames(`${classNamePrefix}__option`, {
+            active: item.value === currentValue,
+          })}
+          aria-selected={item.value === currentValue}
+          ref={item.value === currentValue ? focusedItemRef : null}
+        >
+          {item.icon && item.iconComponent && (
+            <div className={`${classNamePrefix}__option__icon`}>
+              <Icon id={item.icon} icon={item.iconComponent} />
+            </div>
+          )}
+
+          <div className={`${classNamePrefix}__option__content`}>
+            <strong>{item.text}</strong>
+            {item.meta}
+          </div>
+
+          {item.extra && (
+            <div
+              className={`${classNamePrefix}__option__additional`}
+              title={item.extra}
+            >
+              <Icon id='info-circle' icon={InfoIcon} />
+            </div>
+          )}
+        </li>
+      ))}
+    </ul>
+  );
+};
diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx b/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx
index 071f0a6fa..f474aecf2 100644
--- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx
+++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx
@@ -11,10 +11,9 @@ import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?re
 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 'mastodon/components/dropdown_selector';
 import { Icon }  from 'mastodon/components/icon';
 
-import { PrivacyDropdownMenu } from './privacy_dropdown_menu';
-
 const messages = defineMessages({
   public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
   public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' },
@@ -143,7 +142,7 @@ class PrivacyDropdown extends PureComponent {
           {({ props, placement }) => (
             <div {...props}>
               <div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
-                <PrivacyDropdownMenu
+                <DropdownSelector
                   items={this.options}
                   value={value}
                   onClose={this.handleClose}
diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown_menu.jsx b/app/javascript/mastodon/features/compose/components/privacy_dropdown_menu.jsx
deleted file mode 100644
index 1a5ff1fa8..000000000
--- a/app/javascript/mastodon/features/compose/components/privacy_dropdown_menu.jsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import PropTypes from 'prop-types';
-import { useCallback, useEffect, useRef, useState } from 'react';
-
-import classNames from 'classnames';
-
-import { supportsPassiveEvents } from 'detect-passive-events';
-
-import InfoIcon from '@/material-icons/400-24px/info.svg?react';
-import { Icon } from 'mastodon/components/icon';
-
-const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
-
-export const PrivacyDropdownMenu = ({ style, items, value, onClose, onChange }) => {
-  const nodeRef = useRef(null);
-  const focusedItemRef = useRef(null);
-  const [currentValue, setCurrentValue] = useState(value);
-
-  const handleDocumentClick = useCallback((e) => {
-    if (nodeRef.current && !nodeRef.current.contains(e.target)) {
-      onClose();
-      e.stopPropagation();
-    }
-  }, [nodeRef, onClose]);
-
-  const handleClick = useCallback((e) => {
-    const value = e.currentTarget.getAttribute('data-index');
-
-    e.preventDefault();
-
-    onClose();
-    onChange(value);
-  }, [onClose, onChange]);
-
-  const handleKeyDown = useCallback((e) => {
-    const value = e.currentTarget.getAttribute('data-index');
-    const index = items.findIndex(item => (item.value === value));
-
-    let element = null;
-
-    switch (e.key) {
-    case 'Escape':
-      onClose();
-      break;
-    case ' ':
-    case 'Enter':
-      handleClick(e);
-      break;
-    case 'ArrowDown':
-      element = nodeRef.current.childNodes[index + 1] || nodeRef.current.firstChild;
-      break;
-    case 'ArrowUp':
-      element = nodeRef.current.childNodes[index - 1] || nodeRef.current.lastChild;
-      break;
-    case 'Tab':
-      if (e.shiftKey) {
-        element = nodeRef.current.childNodes[index + 1] || nodeRef.current.firstChild;
-      } else {
-        element = nodeRef.current.childNodes[index - 1] || nodeRef.current.lastChild;
-      }
-      break;
-    case 'Home':
-      element = nodeRef.current.firstChild;
-      break;
-    case 'End':
-      element = nodeRef.current.lastChild;
-      break;
-    }
-
-    if (element) {
-      element.focus();
-      setCurrentValue(element.getAttribute('data-index'));
-      e.preventDefault();
-      e.stopPropagation();
-    }
-  }, [nodeRef, items, onClose, handleClick, setCurrentValue]);
-
-  useEffect(() => {
-    document.addEventListener('click', handleDocumentClick, { capture: true });
-    document.addEventListener('touchend', handleDocumentClick, listenerOptions);
-    focusedItemRef.current?.focus({ preventScroll: true });
-
-    return () => {
-      document.removeEventListener('click', handleDocumentClick, { capture: true });
-      document.removeEventListener('touchend', handleDocumentClick, listenerOptions);
-    };
-  }, [handleDocumentClick]);
-
-  return (
-    <ul style={{ ...style }} role='listbox' ref={nodeRef}>
-      {items.map(item => (
-        <li
-          role='option'
-          tabIndex={0}
-          key={item.value}
-          data-index={item.value}
-          onKeyDown={handleKeyDown}
-          onClick={handleClick}
-          className={classNames('privacy-dropdown__option', { active: item.value === currentValue })}
-          aria-selected={item.value === currentValue}
-          ref={item.value === currentValue ? focusedItemRef : null}
-        >
-          <div className='privacy-dropdown__option__icon'>
-            <Icon id={item.icon} icon={item.iconComponent} />
-          </div>
-
-          <div className='privacy-dropdown__option__content'>
-            <strong>{item.text}</strong>
-            {item.meta}
-          </div>
-
-          {item.extra && (
-            <div className='privacy-dropdown__option__additional' title={item.extra}>
-              <Icon id='info-circle' icon={InfoIcon} />
-            </div>
-          )}
-        </li>
-      ))}
-    </ul>
-  );
-};
-
-PrivacyDropdownMenu.propTypes = {
-  style: PropTypes.object,
-  items: PropTypes.array.isRequired,
-  value: PropTypes.string.isRequired,
-  onClose: PropTypes.func.isRequired,
-  onChange: PropTypes.func.isRequired,
-};