fix: give the folder name its own line next to the docked file preview

This commit is contained in:
Linus Rath
2026-10-31 20:46:09 +01:00
parent 34c65765ba
commit 3c95bba0d2
3 changed files with 70 additions and 6 deletions
+27 -1
View File
@@ -5,7 +5,10 @@ import path from 'node:path';
vi.mock('react-native', () => ({ useWindowDimensions: () => ({ width: 800, height: 1280 }) }));
vi.mock('react-native-safe-area-context', () => ({ useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }) }));
import { FILES_LIST_MIN, PREVIEW_HEADER_CHROME, PREVIEW_TITLE_MIN, filesListWidth } from '../files-split';
import {
FILES_LIST_MIN, FILES_TITLE_MIN, PREVIEW_HEADER_CHROME, PREVIEW_TITLE_MIN,
filesHeaderTitleRoom, filesListWidth, filesTitleOnOwnRow,
} from '../files-split';
// In the tablet split view the docked file preview had a fixed 45 % of the
// row. On a Pixel Tablet in portrait (about 720 dp next to the tab rail)
@@ -28,6 +31,29 @@ describe('filesListWidth', () => {
});
});
describe('the folder name in the Files header next to the docked preview', () => {
it('had about 50 dp beside four buttons and Back in a 320 dp list column', () => {
expect(filesHeaderTitleRoom(320, 4, true)).toBe(52);
expect(filesHeaderTitleRoom(320, 5, false)).toBe(52);
});
it('moves under the buttons when that is too little, and stays beside them when there is room', () => {
expect(filesTitleOnOwnRow(filesListWidth(720), 4, true)).toBe(true);
expect(filesTitleOnOwnRow(FILES_LIST_MIN, 5, false)).toBe(true);
expect(filesTitleOnOwnRow(800, 4, true)).toBe(false);
expect(filesHeaderTitleRoom(800, 4, true)).toBeGreaterThanOrEqual(FILES_TITLE_MIN);
// Not measured yet: beside the buttons, as before.
expect(filesTitleOnOwnRow(0, 5, true)).toBe(false);
});
it('is how the header lays out its title', () => {
const source = fs.readFileSync(path.resolve(__dirname, '../../screens/FilesScreen.tsx'), 'utf8');
expect(source).toMatch(/const titleOwnRow = isSplit && previewTarget != null && filesTitleOnOwnRow\(headerWidth, headerButtons, canBack\);/);
expect(source).toMatch(/onLayout=\{\(e\) => setHeaderWidth\(e\.nativeEvent\.layout\.width\)\}/);
expect(source).toMatch(/\{titleOwnRow \? \(\s*<Text style=\{styles\.title\} numberOfLines=\{2\}>/);
});
});
describe('the Files split view', () => {
it('sizes the list column with filesListWidth and lets the preview take the rest', () => {
const source = fs.readFileSync(path.resolve(__dirname, '../../screens/FilesScreen.tsx'), 'utf8');
+24
View File
@@ -22,3 +22,27 @@ export function filesListWidth(contentWidth: number): number {
const previewMin = PREVIEW_HEADER_CHROME + PREVIEW_TITLE_MIN;
return Math.max(FILES_LIST_MIN, Math.min(listPaneWidth(contentWidth), contentWidth - previewMin));
}
/** Room the folder name keeps beside the Files header buttons before it moves to a line of its own. */
export const FILES_TITLE_MIN = 120;
const HEADER_PADDING = 12; // spacing.md, each side
const HEADER_BUTTON = 44; // componentSizes.buttonLg
const HEADER_GAP = 4; // spacing.xs, between buttons
const HEADER_ROW_GAP = 8; // spacing.sm, title side to buttons
/**
* Width left for the folder name in the Files header, `width` wide, with
* `buttons` buttons on the right and a back button or not. Next to the docked
* preview (280 to 420 dp) the four or five buttons left a folder name about
* 50 dp: "…".
*/
export function filesHeaderTitleRoom(width: number, buttons: number, back: boolean): number {
const actions = buttons * HEADER_BUTTON + Math.max(0, buttons - 1) * HEADER_GAP;
const left = back ? HEADER_BUTTON + HEADER_GAP : 0;
return width - 2 * HEADER_PADDING - actions - HEADER_ROW_GAP - left;
}
/** Whether the folder name goes on its own line under the header buttons. A width not measured yet keeps it beside them. */
export function filesTitleOnOwnRow(width: number, buttons: number, back: boolean): boolean {
return width > 0 && filesHeaderTitleRoom(width, buttons, back) < FILES_TITLE_MIN;
}
+19 -5
View File
@@ -71,7 +71,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { formatListDate } from '../lib/date-format';
import { useLayout, useSheetInsets } from '../lib/use-layout';
import { useSheetBottomInset } from '../lib/sheet-bottom-inset';
import { filesListWidth } from '../lib/files-split';
import { filesListWidth, filesTitleOnOwnRow } from '../lib/files-split';
import { radioOptionA11y } from '../components/radio-a11y';
import { useBackWhileFocused } from '../lib/use-back-while-focused';
import { filesBackStep } from '../lib/files-back';
@@ -232,6 +232,9 @@ export default function FilesScreen() {
// whole screen keeps clear of the side insets in landscape.
const insets = useSafeAreaInsets();
const headerInsets = { paddingTop: insets.top + spacing.sm, paddingHorizontal: spacing.md };
// The header's width: next to the docked preview the folder name moves
// under the buttons when they leave it too little room.
const [headerWidth, setHeaderWidth] = useState(0);
// Folder navigation stack. Hierarchy is real parentId nesting (#379) — each
// entry is the folder's node id (namespaced "accountId:nodeId" inside a
@@ -1015,8 +1018,12 @@ export default function FilesScreen() {
</View>
);
}
// Search and view toggle; New folder and Upload; Trash at the top level.
// Only the list column next to the docked preview is that narrow.
const headerButtons = 2 + (canCreateHere ? 2 : 0) + (path.length === 0 && !inTrash ? 1 : 0);
const titleOwnRow = isSplit && previewTarget != null && filesTitleOnOwnRow(headerWidth, headerButtons, canBack);
return (
<View style={[styles.header, headerInsets]}>
<View style={[styles.header, headerInsets]} onLayout={(e) => setHeaderWidth(e.nativeEvent.layout.width)}>
<View style={styles.headerRow}>
<View style={styles.headerLeft}>
{canBack ? (
@@ -1030,9 +1037,11 @@ export default function FilesScreen() {
<ChevronLeft size={22} color={c.text} />
</Pressable>
) : null}
<Text style={styles.title} numberOfLines={1}>
{headerName}
</Text>
{!titleOwnRow ? (
<Text style={styles.title} numberOfLines={1}>
{headerName}
</Text>
) : null}
</View>
<View style={styles.headerActions}>
<Pressable
@@ -1092,6 +1101,11 @@ export default function FilesScreen() {
) : null}
</View>
</View>
{titleOwnRow ? (
<Text style={styles.title} numberOfLines={2}>
{headerName}
</Text>
) : null}
{canBack ? (
<Text style={styles.breadcrumb} numberOfLines={1}>
{[rootLabel, ...(inTrash ? [trashLabel] : path.map((p) => p.name))].join(' / ')}