From 3cdcf93dd30e7f1f221b6ac2dd107e8d35146c44 Mon Sep 17 00:00:00 2001 From: Echo Date: Sat, 19 Sep 2026 13:10:55 +0000 Subject: [PATCH] Composer redesign: Misc bug fixes (#40596) --- .../autosuggest/autosuggest.stories.tsx | 1 + .../mastodon/components/autosuggest/hooks.tsx | 9 ++++---- .../mastodon/components/autosuggest/list.tsx | 13 +++++++++--- .../components/button/redesign.module.scss | 2 +- .../form_fields/redesign.module.scss | 2 +- .../mastodon/components/modal_root.jsx | 18 +++++++--------- .../mastodon/components/popover/index.tsx | 5 +++-- .../features/compose/redesign/hints.tsx | 6 +++++- .../features/compose/redesign/index.tsx | 21 +++++++++++++++++++ .../compose/redesign/styles.module.scss | 3 ++- .../features/compose/redesign/textarea.tsx | 1 + .../compose/redesign/trigger.module.scss | 1 + app/javascript/mastodon/hooks/useStatus.ts | 2 +- .../styles/mastodon/components.scss | 2 +- 14 files changed, 61 insertions(+), 25 deletions(-) diff --git a/app/javascript/mastodon/components/autosuggest/autosuggest.stories.tsx b/app/javascript/mastodon/components/autosuggest/autosuggest.stories.tsx index f2d47bfb0d0..e2361759e79 100644 --- a/app/javascript/mastodon/components/autosuggest/autosuggest.stories.tsx +++ b/app/javascript/mastodon/components/autosuggest/autosuggest.stories.tsx @@ -46,6 +46,7 @@ const meta = { const { onTextChange, suggestProps, sourceProps } = useAutosuggestMenu({ suggestions, onSelect: selectCb, + sourceRef: textareaRef, onFetch(token) { const newSuggestions = tokenToSuggestions(token); diff --git a/app/javascript/mastodon/components/autosuggest/hooks.tsx b/app/javascript/mastodon/components/autosuggest/hooks.tsx index 9ee463a4d75..ba3435809a9 100644 --- a/app/javascript/mastodon/components/autosuggest/hooks.tsx +++ b/app/javascript/mastodon/components/autosuggest/hooks.tsx @@ -19,6 +19,7 @@ export type OnSuggestionSelect = ( interface UseAutosuggestMenuOptions { suggestions: Suggestion[]; + sourceRef: Source; onSelect: OnSuggestionSelect; onFetch?: (token: string) => void; onClear?: () => void; @@ -31,6 +32,7 @@ type SourceProps = React.DetailedHTMLProps< export function useAutosuggestMenu({ suggestions, + sourceRef, onSelect, onFetch, onClear, @@ -108,6 +110,7 @@ export function useAutosuggestMenu({ suggestProps: { suggestions, onSuggestionClick, + source: sourceToElement(sourceRef), listRef, tokenCb, } satisfies AutosuggestMenuProps, @@ -122,13 +125,11 @@ export function useAutosuggestMenu({ interface UseAutosuggestFloatingMenuOptions extends UseAutosuggestMenuOptions { text?: string; - sourceRef: Source; className?: string; } export function useAutosuggestFloatingMenu({ text, - sourceRef, className, ...suggestOptions }: UseAutosuggestFloatingMenuOptions) { @@ -138,9 +139,9 @@ export function useAutosuggestFloatingMenu({ const { getToken, ...autosuggestProps } = useAutosuggestMenu(suggestOptions); - const { onClear } = suggestOptions; + const source = autosuggestProps.suggestProps.source; - const source = sourceToElement(sourceRef); + const { onClear } = suggestOptions; // Update the popover on scroll or select. const onUpdate = useCallback(() => { diff --git a/app/javascript/mastodon/components/autosuggest/list.tsx b/app/javascript/mastodon/components/autosuggest/list.tsx index 6c3d03592e3..58de461d9ce 100644 --- a/app/javascript/mastodon/components/autosuggest/list.tsx +++ b/app/javascript/mastodon/components/autosuggest/list.tsx @@ -12,12 +12,13 @@ import { Popover } from '../popover'; import { AutosuggestItem } from './items'; import classes from './styles.module.scss'; -import type { Suggestion } from './types'; +import type { AutosuggestSourceElements, Suggestion } from './types'; export interface AutosuggestMenuProps { suggestions: Suggestion[]; tokenCb: () => string | null; onSuggestionClick: React.MouseEventHandler; + source: AutosuggestSourceElements | null; children?: React.ReactNode; listRef?: React.Ref; reference?: HTMLElement | null; @@ -60,6 +61,7 @@ type AutosuggestMenuListProps = Omit< const AutosuggestMenuList: React.FC = ({ children, listRef, + source, tokenCb, suggestions, reference, @@ -72,10 +74,15 @@ const AutosuggestMenuList: React.FC = ({ const { popover, menuListProps } = useMenuContext(); useEffect(() => { - if (!popover.isMenuOpen && token !== lastToken && suggestions.length > 0) { + if ( + source === document.activeElement && + !popover.isMenuOpen && + token !== lastToken && + suggestions.length > 0 + ) { popover.openMenu(); } - }, [lastToken, popover, suggestions.length, token]); + }, [lastToken, popover, suggestions.length, token, source]); const mergedRef = useMergedRefs(menuListProps.ref, listRef); diff --git a/app/javascript/mastodon/components/button/redesign.module.scss b/app/javascript/mastodon/components/button/redesign.module.scss index 605ac00db83..f9e024ebce7 100644 --- a/app/javascript/mastodon/components/button/redesign.module.scss +++ b/app/javascript/mastodon/components/button/redesign.module.scss @@ -36,7 +36,7 @@ &:focus-visible, &:focus-within { - outline: 2px solid var(--color-bg-brand-base); + outline: var(--outline-focus-default); outline-offset: 2px; } diff --git a/app/javascript/mastodon/components/form_fields/redesign.module.scss b/app/javascript/mastodon/components/form_fields/redesign.module.scss index 69b69c9fd8b..9d2099db606 100644 --- a/app/javascript/mastodon/components/form_fields/redesign.module.scss +++ b/app/javascript/mastodon/components/form_fields/redesign.module.scss @@ -64,7 +64,7 @@ input.input { } &:focus { - outline: 2px solid var(--color-border-brand); + outline: var(--outline-focus-default); } &:disabled { diff --git a/app/javascript/mastodon/components/modal_root.jsx b/app/javascript/mastodon/components/modal_root.jsx index c2f51a56731..88a7ac11893 100644 --- a/app/javascript/mastodon/components/modal_root.jsx +++ b/app/javascript/mastodon/components/modal_root.jsx @@ -8,6 +8,7 @@ import { createBrowserHistory } from 'history'; import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router'; import { IGNORE_FOCUS_ON_OPEN } from '../reducers/modal'; +import { normalizeKey } from './hotkeys/utils'; class ModalRoot extends PureComponent { @@ -31,15 +32,12 @@ class ModalRoot extends PureComponent { activeElement = this.props.children ? document.activeElement : null; - handleKeyUp = (e) => { - if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) - && !!this.props.children) { - this.props.onClose(); - } - }; - + /** + * @param {KeyboardEvent} e Event + */ handleKeyDown = (e) => { - if (e.key === 'Tab') { + const key = normalizeKey(e.key); + if (key === 'tab') { const focusable = Array.from(this.node.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')).filter((x) => window.getComputedStyle(x).display !== 'none'); const index = focusable.indexOf(e.target); @@ -56,11 +54,12 @@ class ModalRoot extends PureComponent { e.stopPropagation(); e.preventDefault(); } + } else if (key === 'enter' && !e.defaultPrevented && !!this.props.children) { + this.props.onClose(); } }; componentDidMount () { - window.addEventListener('keyup', this.handleKeyUp, false); window.addEventListener('keydown', this.handleKeyDown, false); this.history = this.props.history || createBrowserHistory(); } @@ -96,7 +95,6 @@ class ModalRoot extends PureComponent { } componentWillUnmount () { - window.removeEventListener('keyup', this.handleKeyUp); window.removeEventListener('keydown', this.handleKeyDown); } diff --git a/app/javascript/mastodon/components/popover/index.tsx b/app/javascript/mastodon/components/popover/index.tsx index 95aa3e814e6..8b258b47973 100644 --- a/app/javascript/mastodon/components/popover/index.tsx +++ b/app/javascript/mastodon/components/popover/index.tsx @@ -175,14 +175,15 @@ export const Popover: React.FC = ({ function closeOnEscape(event: KeyboardEvent) { if (event.key === 'Escape') { + event.preventDefault(); onClose(event); } } - document.addEventListener('keyup', closeOnEscape); + document.addEventListener('keydown', closeOnEscape); return () => { - document.removeEventListener('keyup', closeOnEscape); + document.removeEventListener('keydown', closeOnEscape); }; }, [isOpen, onClose]); diff --git a/app/javascript/mastodon/features/compose/redesign/hints.tsx b/app/javascript/mastodon/features/compose/redesign/hints.tsx index c87e8919a7e..4b82ea8bee6 100644 --- a/app/javascript/mastodon/features/compose/redesign/hints.tsx +++ b/app/javascript/mastodon/features/compose/redesign/hints.tsx @@ -33,8 +33,12 @@ const selectIsFollowersReply = createAppSelector( state, state.compose.get('in_reply_to') as null | string, ), + (state) => state.meta.get('me') as string | null, ], - (status) => (status?.visibility === 'private' ? status.account.acct : null), + (status, me) => + status?.visibility === 'private' && status.account.acct !== me + ? status.account.acct + : null, ); export const ComposeHints = () => { diff --git a/app/javascript/mastodon/features/compose/redesign/index.tsx b/app/javascript/mastodon/features/compose/redesign/index.tsx index 139f5944441..628c9f18084 100644 --- a/app/javascript/mastodon/features/compose/redesign/index.tsx +++ b/app/javascript/mastodon/features/compose/redesign/index.tsx @@ -14,8 +14,10 @@ import { } from '@/mastodon/actions/compose'; import { ToggleButton } from '@/mastodon/components/button/redesign'; import { TextInputField } from '@/mastodon/components/form_fields/redesign'; +import { normalizeKey } from '@/mastodon/components/hotkeys/utils'; import { Icon, useIconWeight } from '@/mastodon/components/icon'; import { + closeComposer, getComposerTextarea, requestComposerFocus, submitComposer, @@ -141,6 +143,25 @@ function useComposeHandlers(redirectOnSuccess?: boolean) { const dispatch = useAppDispatch(); + const isModalOpen = useAppSelector((state) => state.modal.stack.size > 0); + useEffect(() => { + function escapeComposer(event: KeyboardEvent) { + const key = normalizeKey(event.key); + if (key !== 'escape' || isModalOpen) { + return; + } + + if (!event.defaultPrevented) { + dispatch(closeComposer()); + } + } + + document.addEventListener('keydown', escapeComposer); + return () => { + document.removeEventListener('keydown', escapeComposer); + }; + }, [dispatch, isModalOpen]); + // Sensitive handling const isSensitive = useAppSelector((state) => !!state.compose.get('spoiler')); useEffect(() => { diff --git a/app/javascript/mastodon/features/compose/redesign/styles.module.scss b/app/javascript/mastodon/features/compose/redesign/styles.module.scss index 1e2ceb3b1b5..157c2869136 100644 --- a/app/javascript/mastodon/features/compose/redesign/styles.module.scss +++ b/app/javascript/mastodon/features/compose/redesign/styles.module.scss @@ -132,9 +132,10 @@ width: 100%; outline: none; background: none; + min-height: 3lh; &:focus { - outline: 2px solid var(--color-border-brand); + outline: var(--outline-focus-default); outline-offset: -2px; &::placeholder { diff --git a/app/javascript/mastodon/features/compose/redesign/textarea.tsx b/app/javascript/mastodon/features/compose/redesign/textarea.tsx index 93d14ddea8a..ce44cc93ba4 100644 --- a/app/javascript/mastodon/features/compose/redesign/textarea.tsx +++ b/app/javascript/mastodon/features/compose/redesign/textarea.tsx @@ -157,6 +157,7 @@ export const ComposeTextarea: React.FC = ({ event.preventDefault(); onSuggestionClear(); } else if (key === 'escape') { + event.preventDefault(); // Dismiss the suggestions if we're displaying any. if (suggestions.length > 0) { onSuggestionClear(); diff --git a/app/javascript/mastodon/features/compose/redesign/trigger.module.scss b/app/javascript/mastodon/features/compose/redesign/trigger.module.scss index 0a056c74f7f..2ed05078571 100644 --- a/app/javascript/mastodon/features/compose/redesign/trigger.module.scss +++ b/app/javascript/mastodon/features/compose/redesign/trigger.module.scss @@ -19,6 +19,7 @@ .composer { width: 100%; top: env(safe-area-inset-top); + padding-block-end: max(env(safe-area-inset-bottom), var(--space-4)); // Set from the visualViewport API. height: var(--viewport-height, 100vh); diff --git a/app/javascript/mastodon/hooks/useStatus.ts b/app/javascript/mastodon/hooks/useStatus.ts index 788af00d384..713c48c17d1 100644 --- a/app/javascript/mastodon/hooks/useStatus.ts +++ b/app/javascript/mastodon/hooks/useStatus.ts @@ -64,7 +64,7 @@ export function useStatusFetch( } if (!status) { dispatch(fetchStatus(id)); - } else if (withAccount && !account) { + } else if (withAccount && status.account && !account) { dispatch(fetchAccount(status.account)); } else if (withReblog && status.reblog && !reblog) { dispatch(fetchStatus(status.reblog)); diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index eab19f5f825..19ed7583d30 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1491,7 +1491,7 @@ body > [data-popover-placement] { .focusable { &:focus-visible { - outline: 2px solid var(--color-border-brand); + outline: var(--outline-focus-default); outline-offset: -2px; background: var(--color-bg-brand-softest); }