fix(app): scope message rings and reach the composer by key (#2781)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-14 21:44:12 +02:00
1 parent d0c6146429
commit 1861432a53
9 files changed
+228 -13

No files matched your search

@@ -705,7 +705,6 @@ export const Message: React.FC<MessageProps> = observer((props) => {
<>
<FocusRing
enabled={keyboardNavigationEnabled ? keyboardModeEnabled : undefined}
within={keyboardNavigationEnabled}
offset={keyboardNavigationEnabled ? -2 : undefined}
ringTarget={keyboardNavigationEnabled ? focusRingAnchorRef : undefined}
data-flx="channel.message.focus-ring"
@@ -52,6 +52,7 @@ import {shouldAutoAck} from '@app/features/read_state/utils/AutoAckPredicate';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import {Scroller} from '@app/features/ui/components/Scroller';
import FocusRingScope from '@app/features/ui/focus_ring/FocusRingScope';
import KeyboardMode from '@app/features/ui/state/KeyboardMode';
import MediaViewer from '@app/features/ui/state/MediaViewer';
import Modal from '@app/features/ui/state/Modal';
@@ -480,6 +481,9 @@ export const Messages = observer(function Messages({
scrollManager.jumpCancel();
ComponentBus.dispatch('FOCUS_TEXTAREA', {channelId: channel.id});
},
onNavigatePastNewest: () => {
ComponentBus.dispatch('FOCUS_TEXTAREA', {channelId: channel.id, enterKeyboardMode: true});
},
allowWhenInactive: true,
});
useEffect(() => {
@@ -666,6 +670,17 @@ export const Messages = observer(function Messages({
/>
</>
);
const keyboardNavigationEnabled = MessageKeyboardFocusRollout.enabled;
const messageListContent = (
<NearViewportSurfaceContext.Provider value={resolveMessageScrollSurface}>
<CollapsedMessageVisibilityProvider
value={collapsedMessageVisibility}
data-flx="channel.messages.collapsed-message-visibility-provider"
>
{scrollerInner}
</CollapsedMessageVisibilityProvider>
</NearViewportSurfaceContext.Provider>
);
return (
<div className={styles.messagesWrapper} style={messagesWrapperStyle} data-flx="channel.messages.messages-wrapper">
<UploadManager
@@ -705,14 +720,13 @@ export const Messages = observer(function Messages({
aria-busy={safeMessages.loadingMore ? true : undefined}
data-flx="channel.messages.scroller-inner"
>
<NearViewportSurfaceContext.Provider value={resolveMessageScrollSurface}>
<CollapsedMessageVisibilityProvider
value={collapsedMessageVisibility}
data-flx="channel.messages.collapsed-message-visibility-provider"
>
{scrollerInner}
</CollapsedMessageVisibilityProvider>
</NearViewportSurfaceContext.Provider>
{keyboardNavigationEnabled ? (
<FocusRingScope containerRef={scrollerInnerRef} data-flx="channel.messages.focus-ring-scope">
{messageListContent}
</FocusRingScope>
) : (
messageListContent
)}
</div>
</div>
</Scroller>
@@ -12,6 +12,7 @@ const messageCss = readSource('../../theme/styles/Message.module.css');
const actionBarCss = readSource('./MessageActionBar.module.css');
const focusRingCss = readSource('../../ui/focus_ring/FocusRing.module.css');
const channelMessageSource = readSource('./ChannelMessage.tsx');
const channelMessagesSource = readSource('./ChannelMessages.tsx');
const messageFocusRing = channelMessageSource.match(/<FocusRing\b[^>]*>/)?.[0] ?? '';
interface CssRule {
@@ -46,7 +47,18 @@ describe('message focus ring contract', () => {
it('only enables the ring in keyboard navigation mode in the experiment arm', () => {
expect(messageFocusRing).toMatch(/enabled=\{keyboardNavigationEnabled \? keyboardModeEnabled : undefined\}/);
expect(messageFocusRing).toMatch(/within=\{keyboardNavigationEnabled\}/);
});
it('leaves focused descendants their own rings instead of drawing the row ring over them', () => {
expect(messageFocusRing).not.toMatch(/\bwithin\b/);
});
it('scopes message list rings inside the scroll content in the experiment arm', () => {
const scopedList = channelMessagesSource.match(
/\{keyboardNavigationEnabled \? \(\s*<FocusRingScope containerRef=\{scrollerInnerRef\}[^>]*>\s*\{messageListContent\}\s*<\/FocusRingScope>\s*\) : \(\s*messageListContent\s*\)\}/,
);
expect(scopedList).not.toBeNull();
expect(channelMessagesSource).toMatch(/const keyboardNavigationEnabled = MessageKeyboardFocusRollout\.enabled;/);
});
it('insets the ring inside the row in the experiment arm and keeps the default geometry in control', () => {