mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 08:06:36 +00:00
feat: add window size classes for tablet and landscape layouts
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
vi.mock('react-native', () => ({ useWindowDimensions: () => ({ width: 411, height: 914 }) }));
|
||||
|
||||
import { layoutFor, listPaneWidth } from '../use-layout';
|
||||
|
||||
describe('layoutFor', () => {
|
||||
it('keeps a phone in portrait on the single-pane layout', () => {
|
||||
expect(layoutFor(411, 914)).toEqual({
|
||||
widthClass: 'compact', shortHeight: false, isSplit: false, hasSidebar: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps a phone in landscape single-pane although it is wide', () => {
|
||||
const l = layoutFor(914, 411);
|
||||
expect(l.widthClass).toBe('expanded');
|
||||
expect(l.shortHeight).toBe(true);
|
||||
expect(l.isSplit).toBe(false);
|
||||
expect(l.hasSidebar).toBe(false);
|
||||
});
|
||||
|
||||
it('splits a tablet in portrait without a sidebar', () => {
|
||||
expect(layoutFor(800, 1280)).toEqual({
|
||||
widthClass: 'medium', shortHeight: false, isSplit: true, hasSidebar: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('splits a tablet in landscape and adds the sidebar', () => {
|
||||
expect(layoutFor(1280, 800)).toEqual({
|
||||
widthClass: 'expanded', shortHeight: false, isSplit: true, hasSidebar: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the window, so a narrow split-screen window is compact', () => {
|
||||
expect(layoutFor(500, 800).isSplit).toBe(false);
|
||||
});
|
||||
|
||||
it('puts the breakpoints at 600 and 840', () => {
|
||||
expect(layoutFor(599, 900).widthClass).toBe('compact');
|
||||
expect(layoutFor(600, 900).widthClass).toBe('medium');
|
||||
expect(layoutFor(839, 900).widthClass).toBe('medium');
|
||||
expect(layoutFor(840, 900).widthClass).toBe('expanded');
|
||||
});
|
||||
});
|
||||
|
||||
describe('listPaneWidth', () => {
|
||||
it('stays between 320 and 420 dp', () => {
|
||||
expect(listPaneWidth(600)).toBe(320);
|
||||
expect(listPaneWidth(1000)).toBe(340);
|
||||
expect(listPaneWidth(2000)).toBe(420);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
|
||||
/**
|
||||
* Window size classes for the adaptive layouts (docs/ux-audit-2026-10.md,
|
||||
* "Adaptive layout plan"). They come from the window, not the device, so
|
||||
* split-screen, freeform windows and foldables get the layout that fits.
|
||||
*/
|
||||
export type WidthClass = 'compact' | 'medium' | 'expanded';
|
||||
|
||||
export const MEDIUM_MIN_WIDTH = 600;
|
||||
export const EXPANDED_MIN_WIDTH = 840;
|
||||
/** Below this a window is "short": a phone in landscape. */
|
||||
export const SHORT_MAX_HEIGHT = 480;
|
||||
|
||||
export interface Layout {
|
||||
widthClass: WidthClass;
|
||||
/** A phone in landscape: compress chrome, keep the single-pane layout. */
|
||||
shortHeight: boolean;
|
||||
/** List and detail side by side (medium or expanded, and not short). */
|
||||
isSplit: boolean;
|
||||
/** A persistent sidebar next to the list (expanded and not short). */
|
||||
hasSidebar: boolean;
|
||||
}
|
||||
|
||||
export function layoutFor(width: number, height: number): Layout {
|
||||
const widthClass: WidthClass =
|
||||
width >= EXPANDED_MIN_WIDTH ? 'expanded' : width >= MEDIUM_MIN_WIDTH ? 'medium' : 'compact';
|
||||
const shortHeight = height < SHORT_MAX_HEIGHT;
|
||||
const isSplit = widthClass !== 'compact' && !shortHeight;
|
||||
return { widthClass, shortHeight, isSplit, hasSidebar: isSplit && widthClass === 'expanded' };
|
||||
}
|
||||
|
||||
export function useLayout(): Layout {
|
||||
const { width, height } = useWindowDimensions();
|
||||
return layoutFor(width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Width of the list pane next to a detail pane: a third of the window,
|
||||
* between 320 and 420 dp (the webmail's list column is about 400).
|
||||
*/
|
||||
export function listPaneWidth(windowWidth: number): number {
|
||||
return Math.round(Math.max(320, Math.min(420, windowWidth * 0.34)));
|
||||
}
|
||||
|
||||
/** Width for sheets, dialogs and floating cards that would span a wide window. */
|
||||
export const SHEET_MAX_WIDTH = 560;
|
||||
Reference in New Issue
Block a user