Skip to content

Commit a2dc548

Browse files
authored
fix(ios): fill the full screen in standalone PWAs instead of a bottom chin (#1853)
<!-- Please read https://github.com/SableClient/Sable/blob/dev/CONTRIBUTING.md before submitting your pull request --> ### Description <!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. --> Fixes # #### Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ### Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings ### AI disclosure: - [ ] Partially AI assisted (clarify which code was AI assisted and briefly explain what it does). - [ ] Fully AI generated (explain what all the generated code does in moderate detail). <!-- Write any explanation required here, but do not generate the explanation using AI!! You must prove you understand what the code in this PR does. -->
2 parents 8b6fd28 + 463d6e3 commit a2dc548

3 files changed

Lines changed: 46 additions & 10 deletions

File tree

src/app/styles/edgeToEdgeInsets.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,24 +79,24 @@ describe('android edge-to-edge inset contract', () => {
7979
expect(mobileCapability).toContain('"edge-to-edge:default"');
8080
});
8181

82-
it('extends only standalone iOS PWAs to the dynamic viewport bottom', () => {
82+
it('fills the full screen in standalone iOS PWAs unless the keyboard is open', () => {
8383
const indexCss = readWorkspaceFile('src/index.css');
8484
const indexTsx = readWorkspaceFile('src/index.tsx');
8585
const iosPwaViewport = readWorkspaceFile('src/app/utils/iosPwaViewport.ts');
8686

8787
expect(indexCss).toContain('@media (display-mode: standalone)');
8888
expect(indexCss).toContain('@supports (-webkit-touch-callout: none)');
89-
expect(indexCss).toContain('var(--sable-ios-pwa-viewport-height, 100dvh)');
89+
expect(indexCss).toContain('var(--sable-ios-pwa-viewport-height, 100vh)');
9090
expect(indexTsx).toContain('installIosPwaViewportHeight();');
9191
expect(iosPwaViewport).toContain("window.matchMedia('(display-mode: standalone)').matches");
9292
expect(iosPwaViewport).toContain('viewport.height + viewport.offsetTop');
9393
expect(iosPwaViewport).toContain('window.setTimeout(updateHeight, 350)');
94-
expect(iosPwaViewport).not.toContain('fullHeight');
95-
expect(iosPwaViewport).not.toContain('viewportWidth');
94+
expect(iosPwaViewport).toContain('100vh');
95+
expect(iosPwaViewport).toContain('fullHeight');
96+
// Physical screen geometry reports device pixels and is wrong on iPad.
9697
expect(iosPwaViewport).not.toContain('window.screen');
97-
// The height must not depend on keyboard detection or on a stale window.innerHeight.
98-
expect(iosPwaViewport).not.toContain('MIN_KEYBOARD_HEIGHT');
99-
expect(iosPwaViewport).not.toContain('isEditableFocused');
98+
expect(iosPwaViewport).toContain('MIN_KEYBOARD_HEIGHT');
99+
expect(iosPwaViewport).toContain('isEditableFocused');
100100
});
101101

102102
it('removes the scattered safe-area css consumers', () => {

src/app/utils/iosPwaViewport.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,60 @@
11
const IOS_PWA_VIEWPORT_HEIGHT = '--sable-ios-pwa-viewport-height';
2+
const MIN_KEYBOARD_HEIGHT = 100;
23

34
const isStandaloneIosPwa = (): boolean =>
45
window.matchMedia('(display-mode: standalone)').matches &&
56
CSS.supports('-webkit-touch-callout: none');
67

8+
const isEditableFocused = (): boolean => {
9+
const el = document.activeElement as HTMLElement | null;
10+
if (!el) return false;
11+
return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable;
12+
};
13+
14+
// 100vh is the only unit that includes the safe-area insets in installed PWAs; probe it so we
15+
// never depend on window.innerHeight, which iOS shrinks on the first keyboard open.
16+
function measureFullHeight(): number {
17+
const probe = document.createElement('div');
18+
probe.style.position = 'fixed';
19+
probe.style.top = '0';
20+
probe.style.left = '0';
21+
probe.style.height = '100vh';
22+
probe.style.visibility = 'hidden';
23+
probe.style.pointerEvents = 'none';
24+
document.documentElement.appendChild(probe);
25+
const height = probe.offsetHeight;
26+
probe.remove();
27+
return height;
28+
}
29+
730
export function installIosPwaViewportHeight(): void {
831
if (!isStandaloneIosPwa()) return;
932

1033
let frame = 0;
1134
let settleTimer = 0;
35+
let fullHeight = measureFullHeight();
36+
let viewportWidth = window.innerWidth;
1237

1338
const updateHeight = () => {
1439
frame = 0;
40+
41+
// Re-probe after rotation.
42+
if (window.innerWidth !== viewportWidth) {
43+
viewportWidth = window.innerWidth;
44+
fullHeight = measureFullHeight();
45+
}
46+
1547
const viewport = window.visualViewport;
1648
// Reach the bottom of the visible area. window.innerHeight is unusable here: iOS shrinks it
1749
// on the first keyboard open and never restores it until the app is force-quit.
18-
const visibleBottom = viewport ? viewport.height + viewport.offsetTop : window.innerHeight;
50+
const visibleBottom = viewport ? viewport.height + viewport.offsetTop : fullHeight;
51+
52+
// Only shrink above the keyboard; idle visualViewport heights under-report by the insets.
53+
const keyboardOpen = isEditableFocused() && fullHeight - visibleBottom > MIN_KEYBOARD_HEIGHT;
1954

2055
document.documentElement.style.setProperty(
2156
IOS_PWA_VIEWPORT_HEIGHT,
22-
`${Math.round(visibleBottom)}px`
57+
`${Math.round(keyboardOpen ? visibleBottom : fullHeight)}px`
2358
);
2459
};
2560

src/index.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ body {
6363
html,
6464
body,
6565
#root {
66-
height: var(--sable-ios-pwa-viewport-height, 100dvh);
66+
/* 100vh is the only unit that includes the safe-area insets in installed PWAs */
67+
height: var(--sable-ios-pwa-viewport-height, 100vh);
6768
}
6869
}
6970
}

0 commit comments

Comments
 (0)