diff --git a/client/scripts/eslint-baseline.json b/client/scripts/eslint-baseline.json index e412118b1..94c8138df 100644 --- a/client/scripts/eslint-baseline.json +++ b/client/scripts/eslint-baseline.json @@ -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, diff --git a/client/scripts/rtl-baseline.json b/client/scripts/rtl-baseline.json index 336a0ca7f..35e465910 100644 --- a/client/scripts/rtl-baseline.json +++ b/client/scripts/rtl-baseline.json @@ -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, diff --git a/client/src/mobile/screens/trip/MTripShellParts.tsx b/client/src/mobile/screens/trip/MTripShellParts.tsx index 626edf02b..3ed7a44af 100644 --- a/client/src/mobile/screens/trip/MTripShellParts.tsx +++ b/client/src/mobile/screens/trip/MTripShellParts.tsx @@ -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(null); const [track, setTrack] = useState(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(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('[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('[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" >
-
+
{children}
diff --git a/client/src/mobile/screens/trip/tabs/MCostsPayment.tsx b/client/src/mobile/screens/trip/tabs/MCostsPayment.tsx index 30fad58f7..5842368c9 100644 --- a/client/src/mobile/screens/trip/tabs/MCostsPayment.tsx +++ b/client/src/mobile/screens/trip/tabs/MCostsPayment.tsx @@ -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" /> {currency}
diff --git a/client/tests/unit/mobile/formSheet.test.tsx b/client/tests/unit/mobile/formSheet.test.tsx index 7f5a8e740..90f006a8f 100644 --- a/client/tests/unit/mobile/formSheet.test.tsx +++ b/client/tests/unit/mobile/formSheet.test.tsx @@ -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', () => { diff --git a/shared/src/i18n/native-dashes.spec.ts b/shared/src/i18n/native-dashes.spec.ts index ef6faa4e9..e5d2dcfdb 100644 --- a/shared/src/i18n/native-dashes.spec.ts +++ b/shared/src/i18n/native-dashes.spec.ts @@ -1,6 +1,9 @@ -/// +/// 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 = Object.fromEntries( + await Promise.all( + locales.map(async (locale) => [ + `./${locale}/native.ts`, + (await import(`./${locale}/native.ts`)) as { default: TranslationStrings }, + ]), + ), +); const DASH = /[‒–—―]| - /;