fix(layout): silence viewport meta warning on non-Android browsers (#4097)
`interactive-widget=resizes-content` was set in the SSR viewport metadata so Android Chrome would shrink the layout viewport when the on-screen keyboard opens (matching iOS default behavior). Other browsers — Safari on macOS / iOS, desktop Chrome, Firefox — log a console warning every page load because they don't recognize the key. Move the attachment client-side, gated on a UA sniff for Android, so the meta tag stays clean for everyone else. The Android-specific behavior (modals centered above the keyboard) is preserved on the platform that actually needed it. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { getAndroidPatchedViewportContent } from '@/utils/viewport';
|
||||
|
||||
const ANDROID_UA =
|
||||
'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36';
|
||||
const IOS_UA =
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1';
|
||||
const MACOS_UA =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15';
|
||||
const BASE_VIEWPORT =
|
||||
'width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover';
|
||||
|
||||
describe('getAndroidPatchedViewportContent', () => {
|
||||
test('returns null on macOS Safari', () => {
|
||||
expect(getAndroidPatchedViewportContent(MACOS_UA, BASE_VIEWPORT)).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null on iOS Safari', () => {
|
||||
expect(getAndroidPatchedViewportContent(IOS_UA, BASE_VIEWPORT)).toBeNull();
|
||||
});
|
||||
|
||||
test('appends interactive-widget on Android Chrome', () => {
|
||||
expect(getAndroidPatchedViewportContent(ANDROID_UA, BASE_VIEWPORT)).toBe(
|
||||
`${BASE_VIEWPORT}, interactive-widget=resizes-content`,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns null when interactive-widget is already present', () => {
|
||||
const existing = `${BASE_VIEWPORT}, interactive-widget=resizes-visual`;
|
||||
expect(getAndroidPatchedViewportContent(ANDROID_UA, existing)).toBeNull();
|
||||
});
|
||||
|
||||
test('handles empty current content without leading comma', () => {
|
||||
expect(getAndroidPatchedViewportContent(ANDROID_UA, '')).toBe(
|
||||
'interactive-widget=resizes-content',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -67,15 +67,9 @@ export const viewport: Viewport = {
|
||||
maximumScale: 1,
|
||||
userScalable: false,
|
||||
viewportFit: 'cover',
|
||||
// Make Android Chrome's layout viewport shrink when the on-screen
|
||||
// keyboard opens (matches iOS behavior). Without this, Android's
|
||||
// default `interactive-widget=resizes-visual` keeps the layout
|
||||
// viewport at full screen height while only the visual viewport
|
||||
// shrinks — causing fixed `inset-0`-centered modals (passphrase
|
||||
// prompt, group picker, etc.) to render under the keyboard. With
|
||||
// `resizes-content`, `100vh` / flex centering naturally targets
|
||||
// the available space above the keyboard.
|
||||
interactiveWidget: 'resizes-content',
|
||||
// `interactive-widget=resizes-content` is appended client-side on
|
||||
// Android only — see Providers.tsx. Other browsers warn about the
|
||||
// unrecognized key on every page load, so we keep it out of SSR.
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useBackgroundTexture } from '@/hooks/useBackgroundTexture';
|
||||
import { useEinkMode } from '@/hooks/useEinkMode';
|
||||
import { getLocale } from '@/utils/misc';
|
||||
import { getDirFromUILanguage } from '@/utils/rtl';
|
||||
import { getAndroidPatchedViewportContent } from '@/utils/viewport';
|
||||
import { DropdownProvider } from '@/context/DropdownContext';
|
||||
import { CommandPaletteProvider, CommandPalette } from '@/components/command-palette';
|
||||
import AtmosphereOverlay from '@/components/AtmosphereOverlay';
|
||||
@@ -102,6 +103,13 @@ const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const meta = document.querySelector<HTMLMetaElement>('meta[name="viewport"]');
|
||||
if (!meta) return;
|
||||
const updated = getAndroidPatchedViewportContent(navigator.userAgent, meta.content);
|
||||
if (updated) meta.content = updated;
|
||||
}, []);
|
||||
|
||||
// Subscribe the bundled-settings publisher to settingsStore changes.
|
||||
// After this fires, every setSettings call diffs the whitelist
|
||||
// against the last-published snapshot and emits a single replica
|
||||
|
||||
@@ -10,15 +10,13 @@ function MyApp({ Component, pageProps }: AppProps) {
|
||||
<>
|
||||
<Head>
|
||||
{/*
|
||||
* `interactive-widget=resizes-content` makes Android Chrome
|
||||
* shrink the layout viewport when the on-screen keyboard
|
||||
* opens (matches iOS default behavior). Without it, the
|
||||
* layout viewport stays full-height and `fixed inset-0`-
|
||||
* centered modals render under the keyboard.
|
||||
* `interactive-widget=resizes-content` is appended client-side
|
||||
* on Android only (Providers.tsx) so other browsers don't
|
||||
* warn about the unrecognized key on every page load.
|
||||
*/}
|
||||
<meta
|
||||
name='viewport'
|
||||
content='minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content'
|
||||
content='minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, user-scalable=no, viewport-fit=cover'
|
||||
/>
|
||||
<meta name='application-name' content='Readest' />
|
||||
<meta name='apple-mobile-web-app-capable' content='yes' />
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// `interactive-widget=resizes-content` makes Android Chrome shrink the
|
||||
// layout viewport when the on-screen keyboard opens, matching iOS
|
||||
// default behavior so `fixed inset-0`-centered modals (passphrase
|
||||
// prompt, group picker, etc.) sit above the keyboard. Other browsers
|
||||
// (Safari on macOS/iOS, desktop Chrome, Firefox) emit a console warning
|
||||
// when they encounter the unrecognized key on every page load — so we
|
||||
// only attach it on Android.
|
||||
export const getAndroidPatchedViewportContent = (
|
||||
userAgent: string,
|
||||
currentContent: string,
|
||||
): string | null => {
|
||||
if (!/android/i.test(userAgent)) return null;
|
||||
if (currentContent.includes('interactive-widget=')) return null;
|
||||
const trimmed = currentContent.trim().replace(/,\s*$/, '');
|
||||
if (!trimmed) return 'interactive-widget=resizes-content';
|
||||
return `${trimmed}, interactive-widget=resizes-content`;
|
||||
};
|
||||
Reference in New Issue
Block a user