diff --git a/app/javascript/mastodon/components/menu/index.tsx b/app/javascript/mastodon/components/menu/index.tsx index 6295254067d..21d963aae81 100644 --- a/app/javascript/mastodon/components/menu/index.tsx +++ b/app/javascript/mastodon/components/menu/index.tsx @@ -46,7 +46,7 @@ interface MenuTriggerContextProps { } interface MenuListContextProps { - ref: (button: HTMLDivElement | null) => void; + ref: (list: HTMLDivElement | null) => void; role?: 'menu'; // only for menus of type === 'actions' tabIndex: -1; id: string; @@ -75,7 +75,7 @@ export function useMenuContext(): MenuState { return context; } -function getAllMenuItems(menuListElement: HTMLDivElement) { +export function getAllMenuItems(menuListElement: HTMLDivElement) { return Array.from( menuListElement.querySelectorAll( ':scope [data-menu-item]:not([disabled])', @@ -91,9 +91,14 @@ interface MenuProps { */ type?: MenuType; children: React.ReactNode; + noFocus?: boolean; } -export const Menu: React.FC = ({ type = 'actions', children }) => { +export const Menu: React.FC = ({ + type = 'actions', + children, + noFocus, +}) => { const id = useId(); const triggerId = `${id}-trigger`; const listId = `${id}-list`; @@ -105,13 +110,13 @@ export const Menu: React.FC = ({ type = 'actions', children }) => { (element: HTMLDivElement | null) => { setListElement(element); - if (element && type === 'actions') { + if (element && type === 'actions' && !noFocus) { const menuItems = getAllMenuItems(element); const elementToFocus = menuItems[0] ?? element; elementToFocus.focus(); } }, - [type], + [noFocus, type], ); const [isMenuOpen, setIsMenuOpen] = useState(false); diff --git a/app/javascript/mastodon/features/compose/redesign/attachments.tsx b/app/javascript/mastodon/features/compose/redesign/attachments.tsx index f262ed0f11a..72fce9bbb70 100644 --- a/app/javascript/mastodon/features/compose/redesign/attachments.tsx +++ b/app/javascript/mastodon/features/compose/redesign/attachments.tsx @@ -10,7 +10,9 @@ import { } from './selectors'; import { ComposeUpload } from './upload'; -export const ComposeAttachments: React.FC = () => { +export const ComposeAttachments: React.FC<{ className?: string }> = ({ + className, +}) => { const { hasPoll, hasAttachments, quotedStatusId } = useAppSelector( selectComposeHasAttachments, ); @@ -20,11 +22,11 @@ export const ComposeAttachments: React.FC = () => { } return ( - <> +
{hasPoll && } {hasAttachments && } {quotedStatusId && } - +
); }; diff --git a/app/javascript/mastodon/features/compose/redesign/hints.tsx b/app/javascript/mastodon/features/compose/redesign/hints.tsx index 1384e2a40fe..fe7afe2e887 100644 --- a/app/javascript/mastodon/features/compose/redesign/hints.tsx +++ b/app/javascript/mastodon/features/compose/redesign/hints.tsx @@ -64,7 +64,8 @@ export const ComposeHints = () => { ); } - if (isDifferentLanguage) { + const { wasDismissed } = useDismissible('compose_language_hint'); + if (isDifferentLanguage && !wasDismissed) { messages.push(); } diff --git a/app/javascript/mastodon/features/compose/redesign/hooks.ts b/app/javascript/mastodon/features/compose/redesign/hooks.ts index d3c1ce14f06..e41800095ed 100644 --- a/app/javascript/mastodon/features/compose/redesign/hooks.ts +++ b/app/javascript/mastodon/features/compose/redesign/hooks.ts @@ -11,7 +11,9 @@ export function useLanguages() { } export function useLanguageGuess() { - const text = useAppSelector((state) => state.compose.get('text') as string); + const text = useAppSelector((state) => + (state.compose.get('text') as string).trim(), + ); const [guess, setGuess] = useState(''); useEffect(() => { diff --git a/app/javascript/mastodon/features/compose/redesign/index.tsx b/app/javascript/mastodon/features/compose/redesign/index.tsx index 1ebb1d51d0c..c1fc123a7bf 100644 --- a/app/javascript/mastodon/features/compose/redesign/index.tsx +++ b/app/javascript/mastodon/features/compose/redesign/index.tsx @@ -6,7 +6,6 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import classNames from 'classnames'; import { LockSimpleOpenIcon } from '@phosphor-icons/react'; -import { useDebouncedCallback } from 'use-debounce'; import { changeComposeSpoilerness, @@ -18,7 +17,7 @@ import { TextInputField, } from '@/mastodon/components/form_fields/redesign'; import { Icon } from '@/mastodon/components/icon'; -import { useResizeObserver } from '@/mastodon/hooks/useObserver'; +import { useScrollSensor } from '@/mastodon/hooks/useScrollSensor'; import { focusComposerTextarea, getComposerTextarea, @@ -69,18 +68,17 @@ export const RedesignComposeForm: React.FC = ({ const type = useAppSelector(selectComposeType); const { sensitive, sensitiveText } = useAppSelector(selectComposeSensitive); - const { - onSensitiveChange, - onSensitiveTextChange, - onEmojiPick, - onSubmit, - onWrapperMount, - onWrapperScroll, - } = useComposeHandlers(redirectOnSuccess); + const { onSensitiveChange, onSensitiveTextChange, onEmojiPick, onSubmit } = + useComposeHandlers(redirectOnSuccess); const intl = useIntl(); const titleId = useId(); + const { sensor, isInViewport } = useScrollSensor({ + placement: 'bottom', + tolerance: 10, + }); + return (
= ({ /> )} -
+
- + + + {sensor}
@@ -209,49 +205,10 @@ function useComposeHandlers(redirectOnSuccess?: boolean) { [canSubmit, dispatch, redirectOnSuccess], ); - // Handle wrapper fade to indicate scroll. - const onWrapperScroll = useDebouncedCallback(wrapperScroll, 20, { - leading: true, - }); - const observer = useResizeObserver(wrapperResize); - const onWrapperMount: React.RefCallback = useCallback( - (ele) => { - if (ele) { - observer.observe(ele); - } - }, - [observer], - ); - return { onSubmit, onEmojiPick, onSensitiveChange, onSensitiveTextChange, - onWrapperScroll, - onWrapperMount, }; } - -function wrapperUpdate(ele: HTMLElement) { - const scrollMax = ele.scrollHeight - ele.offsetHeight - 5; // 5px padding to account for sub-pixel issues - if (scrollMax > 0 && ele.scrollTop < scrollMax) { - ele.dataset.scrollDown = 'true'; - } else { - delete ele.dataset.scrollDown; - } -} - -function wrapperResize(entries: ResizeObserverEntry[]) { - for (const entry of entries) { - if (entry.target instanceof HTMLElement) { - wrapperUpdate(entry.target); - } - } -} - -function wrapperScroll(event: React.UIEvent) { - if (event.target instanceof HTMLElement) { - wrapperUpdate(event.target); - } -} diff --git a/app/javascript/mastodon/features/compose/redesign/styles.module.scss b/app/javascript/mastodon/features/compose/redesign/styles.module.scss index c39c4d64fdd..fd31930eec3 100644 --- a/app/javascript/mastodon/features/compose/redesign/styles.module.scss +++ b/app/javascript/mastodon/features/compose/redesign/styles.module.scss @@ -86,7 +86,6 @@ flex-grow: 1; display: flex; flex-direction: column; - gap: var(--space-md); scrollbar-width: thin; overscroll-behavior-y: contain; position: relative; @@ -102,13 +101,13 @@ position: sticky; bottom: 0; flex-shrink: 0; - margin-top: calc((-1 * var(--fade-size)) - var(--space-md)); + margin-top: calc(-1 * var(--fade-size)); opacity: 0; pointer-events: none; transition: opacity 200ms; } - &[data-scroll-down]::after { + &[data-scroll-down='true']::after { opacity: 1; } } @@ -138,6 +137,10 @@ } } +.attachments { + margin-top: var(--space-md); +} + .footer { display: flex; align-items: center; diff --git a/app/javascript/mastodon/features/compose/redesign/textarea.tsx b/app/javascript/mastodon/features/compose/redesign/textarea.tsx index 3e59ebe57a2..58240fe154e 100644 --- a/app/javascript/mastodon/features/compose/redesign/textarea.tsx +++ b/app/javascript/mastodon/features/compose/redesign/textarea.tsx @@ -119,21 +119,15 @@ export const ComposeTextarea: React.FC = ({ }, [onSubmit], ); - const onPaste: React.ClipboardEventHandler = useCallback( - (event) => { - if (event.clipboardData.files.length === 1) { + + const onPasteOrDrop = useCallback( + (event: React.ClipboardEvent | React.DragEvent) => { + const data = + 'clipboardData' in event ? event.clipboardData : event.dataTransfer; + if (data.files.length === 1) { event.preventDefault(); } - dispatch(processPasteOrDrop(event.clipboardData)); - }, - [dispatch], - ); - const onDrop: React.DragEventHandler = useCallback( - (event) => { - if (event.dataTransfer.files.length === 1) { - event.preventDefault(); - } - dispatch(processPasteOrDrop(event.dataTransfer)); + dispatch(processPasteOrDrop(data)); }, [dispatch], ); @@ -178,8 +172,8 @@ export const ComposeTextarea: React.FC = ({ onSuggestionsClearRequested={onSuggestionsClearRequested} onSuggestionSelected={onSuggestionSelected} onKeyDown={onKeyDown} - onDrop={onDrop} - onPaste={onPaste} + onDrop={onPasteOrDrop} + onPaste={onPasteOrDrop} onChange={onChange} />
diff --git a/app/javascript/mastodon/hooks/useMergedRefs.ts b/app/javascript/mastodon/hooks/useMergedRefs.ts index 5fd126f8c7f..7f2de510550 100644 --- a/app/javascript/mastodon/hooks/useMergedRefs.ts +++ b/app/javascript/mastodon/hooks/useMergedRefs.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react'; -export function useMergedRefs(...refs: React.Ref[]) { +export function useMergedRefs(...refs: (React.Ref | undefined)[]) { const setRef: React.RefCallback = useCallback( (node: T | null) => { const cleanups: (() => void)[] = []; diff --git a/app/javascript/mastodon/utils/strings.ts b/app/javascript/mastodon/utils/strings.ts new file mode 100644 index 00000000000..ef589ef0d8b --- /dev/null +++ b/app/javascript/mastodon/utils/strings.ts @@ -0,0 +1,8 @@ +/** + * Returns the input if it is a string, otherwise returns undefined. + * @param input Any input. + * @returns The string, or undefined. + */ +export function stringOrUndefined(input: unknown) { + return typeof input === 'string' ? input : undefined; +}