fix(mobile): the checks the large-screen merge left red

- The trip's segmented control writes its fit through a ref, not into the
  element held in state (react-hooks/immutability).
- The amount field of a payment uses the body type tier, which follows the
  text size setting, instead of a raw size.
- The form sheet tests wait the frame the edge fade now measures on, and read
  the wide variant from the size class the shell writes.
- The native dashes spec lists its locales through node:fs, so shared needs no
  vite types for it (knip).
- Baselines lowered where the fixes removed warnings: no-misused-promises,
  no-non-null-assertion in tests, the MSheet rtl entry.
This commit is contained in:
Maurice
2026-11-07 23:56:33 +01:00
parent 8955790f17
commit a7cc1b9e57
6 changed files with 42 additions and 15 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
{
"src": {
"@typescript-eslint/no-non-null-assertion": 375,
"@typescript-eslint/no-misused-promises": 847,
"@typescript-eslint/no-misused-promises": 845,
"no-console": 88,
"complexity": 218,
"@typescript-eslint/no-explicit-any": 144,
@@ -35,7 +35,7 @@
},
"tests": {
"@typescript-eslint/no-explicit-any": 489,
"@typescript-eslint/no-non-null-assertion": 1912,
"@typescript-eslint/no-non-null-assertion": 1911,
"@typescript-eslint/no-misused-promises": 22,
"@typescript-eslint/no-unused-vars": 51,
"@typescript-eslint/no-floating-promises": 33,
-1
View File
@@ -79,7 +79,6 @@
"hooks/useAnchoredPosition.ts": 3,
"hooks/useElementRect.ts": 3,
"mobile/components/MSegmented.tsx": 1,
"mobile/components/MSheet.tsx": 1,
"mobile/screens/journey/MJourneyDetail.tsx": 3,
"mobile/screens/journey/MJourneyEntrySheet.tsx": 1,
"mobile/screens/trip/map/MMapArea.tsx": 3,
@@ -1,5 +1,5 @@
import { ChevronLeft, ChevronRight, FolderSync, List, Plus, Rows3 } from 'lucide-react';
import { useLayoutEffect, useState, type ReactNode } from 'react';
import { useCallback, useLayoutEffect, useRef, useState, type ReactNode } from 'react';
import { canManageDocSync } from '../../../components/Files/docsync/useDocSync';
import { useDocSyncOffered } from '../../../components/Files/docsync/useDocSyncOffered';
import { useIsWide } from '../../../hooks/sizeClass';
@@ -26,14 +26,21 @@ import { collabBoardFor, collabSideBySide } from './tabs/collabPanes';
export function GlassSegment({ children, activeKey }: { children: ReactNode; activeKey?: string }) {
const [region, setRegion] = useState<HTMLDivElement | null>(null);
const [track, setTrack] = useState<HTMLDivElement | null>(null);
// The element is also kept in a ref: the fit below writes to it, and state is not to be written.
const trackRef = useRef<HTMLDivElement | null>(null);
const attachTrack = useCallback((el: HTMLDivElement | null) => {
trackRef.current = el;
setTrack(el);
}, []);
const fade = useEdgeFade(track);
useLayoutEffect(() => {
if (!region || !track) return;
const el = trackRef.current;
if (!region || !el) return;
const fit = () => {
track.dataset.tight = 'false';
track.dataset.tight = track.scrollWidth > track.clientWidth + 0.5 ? 'true' : 'false';
const active = track.querySelector<HTMLElement>('[aria-pressed="true"]');
if (active && track.scrollWidth > track.clientWidth + 0.5) {
el.dataset.tight = 'false';
el.dataset.tight = el.scrollWidth > el.clientWidth + 0.5 ? 'true' : 'false';
const active = el.querySelector<HTMLElement>('[aria-pressed="true"]');
if (active && el.scrollWidth > el.clientWidth + 0.5) {
// 'nearest' on both axes: the shell root is fixed, anything else scrolls the page behind it.
active.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
@@ -49,7 +56,7 @@ export function GlassSegment({ children, activeKey }: { children: ReactNode; act
className="pointer-events-none absolute inset-y-0 end-12 start-12 flex items-center justify-center"
>
<div className="pointer-events-auto flex min-w-0 max-w-full rounded-full border border-[color:var(--m-gbr)] bg-[color:var(--m-glass)] p-[3px] backdrop-blur-[24px] backdrop-saturate-[1.7]">
<div ref={setTrack} className={`m-seg flex min-w-0 overflow-x-auto rounded-full ${edgeFadeClass(fade)}`}>
<div ref={attachTrack} className={`m-seg flex min-w-0 overflow-x-auto rounded-full ${edgeFadeClass(fade)}`}>
{children}
</div>
</div>
@@ -182,7 +182,7 @@ export function AddPaymentSheet({
onChange={(e) => setAmount(e.target.value)}
placeholder={zeroAmountInput(currency)}
aria-label={t('costs.amount')}
className="min-w-0 flex-1 border-0 bg-transparent p-0 text-[0.84375rem] font-semibold text-m-ink outline-none [font-variant-numeric:tabular-nums] placeholder:font-normal placeholder:text-m-faint"
className="min-w-0 flex-1 border-0 bg-transparent p-0 text-body font-semibold text-m-ink outline-none [font-variant-numeric:tabular-nums] placeholder:font-normal placeholder:text-m-faint"
/>
<span className="flex-none font-geist text-caption font-bold text-m-faint">{currency}</span>
</div>
+9 -2
View File
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { useRef, useState } from 'react';
import { describe, expect, it, vi } from 'vitest';
import { sizeClassOf } from '../../../src/hooks/sizeClass';
import { useAutoGrow } from '../../../src/hooks/useAutoGrow';
import MSheet from '../../../src/mobile/components/MSheet';
import { edgeFadeClass, useEdgeFade } from '../../../src/mobile/components/useEdgeFade';
@@ -115,7 +116,8 @@ describe('useEdgeFade on the vertical axis', () => {
Object.defineProperty(el, 'scrollHeight', { configurable: true, value: 800 });
await act(async () => {
el.appendChild(document.createElement('div'));
await Promise.resolve();
// Measured on the next frame, so a burst of changes is measured once.
await new Promise((resolve) => setTimeout(resolve, 40));
});
expect(result.current.end).toBe(true);
});
@@ -162,7 +164,12 @@ describe('form sheet stylesheet', () => {
it('FE-MOB-FORMSHEET-010: the short-window variant is the other side of the wide size class height bound', () => {
expect(tailwind).toContain("addVariant('short', '@media (max-height: 500px)')");
expect(tailwind).toContain("addVariant('wide', '@media (min-width: 600px) and (min-height: 501px)')");
// `wide:` follows the size class the shell writes (data-size), whose height bound starts at 501.
expect(tailwind).toContain(
`addVariant('wide', ':where(html:has(.m-shell[data-size="wide"], .m-shell[data-size="split"])) &')`
);
expect(sizeClassOf(1024, 500)).toBe('compact');
expect(sizeClassOf(1024, 501)).not.toBe('compact');
});
it('FE-MOB-FORMSHEET-011: on a short window the actions sit beside the title and the fields take the rest', () => {
+16 -2
View File
@@ -1,6 +1,9 @@
/// <reference types="vite/client" />
/// <reference types="node" />
import type { TranslationStrings } from './types';
import { existsSync, readdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
/**
@@ -11,7 +14,18 @@ import { describe, expect, it } from 'vitest';
* reads, and the style is held here because a re-translation can bring a dash
* back without any other check noticing.
*/
const nativeModules = import.meta.glob<{ default: TranslationStrings }>('./*/native.ts', { eager: true });
const here = dirname(fileURLToPath(import.meta.url));
const locales = readdirSync(here, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && existsSync(join(here, entry.name, 'native.ts')))
.map((entry) => entry.name);
const nativeModules: Record<string, { default: TranslationStrings }> = Object.fromEntries(
await Promise.all(
locales.map(async (locale) => [
`./${locale}/native.ts`,
(await import(`./${locale}/native.ts`)) as { default: TranslationStrings },
]),
),
);
const DASH = /[‒–—―]| - /;