From dfbab435cffdebadd7e1dce641ac7acd67122aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 23 Sep 2026 06:56:29 +0200 Subject: [PATCH] fix(mobile): keep repository connect button labels on one line https://github.com/Kilo-Org/cloud/pull/6507 --- ...-session-repository-section-labels.test.ts | 93 +++++++++++++++++++ .../agents/new-session-repository-section.tsx | 3 +- .../agents/session-filter-button.tsx | 16 +++- .../agents/session-list-header-actions.tsx | 11 ++- 4 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 apps/mobile/src/components/agents/new-session-repository-section-labels.test.ts diff --git a/apps/mobile/src/components/agents/new-session-repository-section-labels.test.ts b/apps/mobile/src/components/agents/new-session-repository-section-labels.test.ts new file mode 100644 index 0000000000..3bd2b9dd9a --- /dev/null +++ b/apps/mobile/src/components/agents/new-session-repository-section-labels.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { i18n } from '@/i18n'; +import { type TestRenderer } from '@/test/renderer'; +import { group, mountSection } from './new-session-repository-section.test-helpers'; + +vi.mock('react-native', () => ({ + ActivityIndicator: 'ActivityIndicator', + Pressable: 'Pressable', + View: 'View', +})); +vi.mock('@/components/ui/text', async () => { + const React = await import('react'); + return { Text: 'Text', TextClassContext: React.createContext(undefined) }; +}); +vi.mock('@/components/ui/button', () => ({ Button: 'Button' })); +vi.mock('@/components/ui/icons', () => ({ + ChevronDown: 'ChevronDown', + ExternalLink: 'ExternalLink', + RefreshCw: 'RefreshCw', +})); +vi.mock('@/components/ui/activity-indicator', () => ({ ActivityIndicator: 'ActivityIndicator' })); +vi.mock('@/components/query-error', () => ({ QueryError: 'QueryError' })); +vi.mock('@/components/agents/repo-selector', () => ({ RepoSelector: 'RepoSelector' })); +vi.mock('@/components/agents/repository-branch-selector', () => ({ + RepositoryBranchSelector: 'RepositoryBranchSelector', +})); +vi.mock('@/lib/hooks/use-theme-colors', () => ({ + useThemeColors: () => ({ foreground: '#000000', mutedForeground: '#777777' }), +})); +vi.mock('@/lib/a11y/motion', () => ({ + useMotionPolicy: () => ({ reducedMotion: false, scrollAnimated: true }), + selectReducedMotionEntrance: (reducedMotion: boolean, entrance: T) => + reducedMotion ? undefined : entrance, +})); +vi.mock('react-native-reanimated', () => ({ + default: { View: 'Animated.View' }, + useSharedValue: (value: unknown) => ({ value }), + useAnimatedStyle: () => ({}), + withTiming: (value: number, config: unknown) => ({ value, config }), + FadeIn: { duration: (ms: number) => ({ __fadeIn: ms }) }, + LinearTransition: { duration: (ms: number) => ({ __linearTransition: ms }) }, +})); +vi.mock('@/lib/hooks/use-collapsed-connect-ctas-preference', () => ({ + useCollapsedConnectCtas: () => ({ collapsedCtas: [], hasLoaded: true }), + setConnectCtaCollapsed: vi.fn(), +})); + +/** The connect action's own label node, matched by the copy it renders. */ +function actionLabel(renderer: TestRenderer.ReactTestRenderer, copy: string) { + const node = renderer.root + .findAllByType('Text' as never) + .find(candidate => candidate.children.includes(copy)); + if (!node) { + throw new Error(`no action label for ${copy}`); + } + return node; +} + +// The connect action is a fixed one-row control: an icon, the label, and the +// refresh button beside it. A label that wraps grows its own text block without +// growing the row, so "Open GitLab" broke onto two lines inside a button left at +// the height of the one-line "Open GitHub" sibling (explorer: new-session-filled +// / new-session-kb-down). Every action label is held to one line, like the +// segmented control's options; a longer locale ellipsizes and the full copy +// stays the control's accessible name. +describe('NewSessionRepositorySection connect action labels', () => { + it.each(['openGithub', 'openGitlab', 'openBitbucket'] as const)( + 'keeps the %s action label on one line', + key => { + const renderer = mountSection({ + groups: [ + group('github', 'connect'), + group('gitlab', 'connect'), + group('bitbucket', 'connect'), + ], + }); + + expect(actionLabel(renderer, i18n.t(`agentChat.newSession.${key}`)).props.numberOfLines).toBe( + 1 + ); + } + ); + + it('keeps the selected-provider action label on one line', () => { + const renderer = mountSection({ + value: 'github:owner/repo', + groups: [group('github', 'repos'), group('gitlab', 'connect')], + }); + + expect(actionLabel(renderer, i18n.t('common.connectGitlab')).props.numberOfLines).toBe(1); + }); +}); diff --git a/apps/mobile/src/components/agents/new-session-repository-section.tsx b/apps/mobile/src/components/agents/new-session-repository-section.tsx index b1fae54acb..26c92b7d30 100644 --- a/apps/mobile/src/components/agents/new-session-repository-section.tsx +++ b/apps/mobile/src/components/agents/new-session-repository-section.tsx @@ -269,7 +269,8 @@ export function NewSessionRepositorySection({ than the glyphs Android lays out, so "Open GitLab" wrapped onto two lines and grew the button taller than its one-line siblings; giving the label the free space keeps its box wider than the text, and - `numberOfLines` pins the line. + `numberOfLines` pins the line. The explorer captures + new-session-filled and new-session-kb-down both showed that wrap. */} {t(selectedRepository === null ? copy.openLabel : copy.connectTitle)} diff --git a/apps/mobile/src/components/agents/session-filter-button.tsx b/apps/mobile/src/components/agents/session-filter-button.tsx index 4787e3eddb..bd28112d14 100644 --- a/apps/mobile/src/components/agents/session-filter-button.tsx +++ b/apps/mobile/src/components/agents/session-filter-button.tsx @@ -1,5 +1,5 @@ import { SlidersHorizontal } from '@/components/ui/icons'; -import { Pressable, View } from 'react-native'; +import { type Insets, Pressable, View } from 'react-native'; import { useTranslation } from 'react-i18next'; import { filterButtonAccessibilityLabel } from '@/components/agents/session-filter-button-label'; @@ -12,6 +12,13 @@ type SessionFilterButtonProps = { activeCount: number; onPress: () => void; testID?: string; + /** + * Per-side reach override. The control's own default is the same slop on + * every side; a caller that shares a row with another control needs to state + * the facing sides, so the two touch regions can be checked against the row + * gap instead of overlapping inside it. + */ + hitSlop?: number | Insets; }; /** @@ -43,6 +50,7 @@ export function SessionFilterButton({ activeCount, onPress, testID, + hitSlop = FILTER_HIT_SLOP, }: Readonly) { const colors = useThemeColors(); const { t } = useTranslation(); @@ -51,7 +59,11 @@ export function SessionFilterButton({ return (