Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/app/components/app-shell/SystemBarShell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,17 @@ describe('SystemBarShell', () => {
const mutationObserver = vi.fn<() => void>();
vi.stubGlobal('MutationObserver', mutationObserver);

render(
const { container } = render(
<SystemBarShell onPortalContainerChange={vi.fn<(node: HTMLDivElement | null) => void>()}>
<div>Content</div>
</SystemBarShell>
);

expect(mockOsType).not.toHaveBeenCalled();
expect(mutationObserver).not.toHaveBeenCalled();
expect(container.querySelector('[data-system-bar-position="top"]')).toHaveStyle({
height: 'var(--safe-area-inset-top, env(safe-area-inset-top, 0px))',
});
expect(container.querySelector('[data-system-bar-position="bottom"]')).not.toBeInTheDocument();
});
});
19 changes: 11 additions & 8 deletions src/app/components/app-shell/SystemBarShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,16 +99,19 @@ function useBarColor(
}

type SystemBarStripProps = {
edge: 'top' | 'bottom';
size: string;
background: string;
stripRef?: RefObject<HTMLDivElement>;
transition?: string;
};

function SystemBarStrip({ size, background, stripRef, transition }: SystemBarStripProps) {
function SystemBarStrip({ edge, size, background, stripRef, transition }: SystemBarStripProps) {
return (
<div
ref={stripRef}
aria-hidden="true"
data-system-bar-position={edge}
style={{
height: size,
flexShrink: 0,
Expand Down Expand Up @@ -143,13 +146,12 @@ export function SystemBarShell({ children, onPortalContainerChange }: SystemBarS

return (
<>
{enabled && (
<SystemBarStrip
size={safeAreaTop}
background={topColor ?? 'var(--sable-bg-container)'}
stripRef={topStripRef}
/>
)}
<SystemBarStrip
edge="top"
size={safeAreaTop}
background={topColor ?? 'var(--sable-bg-container)'}
stripRef={topStripRef}
/>

<div
style={{
Expand Down Expand Up @@ -180,6 +182,7 @@ export function SystemBarShell({ children, onPortalContainerChange }: SystemBarS

{enabled && (
<SystemBarStrip
edge="bottom"
size={tauriOs === 'ios' ? iosBottomInset : safeAreaBottom}
background={bottomColor ?? 'var(--sable-surface-container)'}
stripRef={bottomStripRef}
Expand Down
20 changes: 20 additions & 0 deletions src/app/styles/edgeToEdgeInsets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,29 @@ describe('android edge-to-edge inset contract', () => {
expect(systemBarShell).toContain(
'var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))'
);
expect(systemBarShell).toContain('<SystemBarStrip\n edge="top"');
expect(systemBarShell).toContain(
'{enabled && (\n <SystemBarStrip\n edge="bottom"'
);
expect(mobileCapability).toContain('"edge-to-edge:default"');
});

it('extends only standalone iOS PWAs to the dynamic viewport bottom', () => {
const indexCss = readWorkspaceFile('src/index.css');
const indexTsx = readWorkspaceFile('src/index.tsx');
const iosPwaViewport = readWorkspaceFile('src/app/utils/iosPwaViewport.ts');

expect(indexCss).toContain('@media (display-mode: standalone)');
expect(indexCss).toContain('@supports (-webkit-touch-callout: none)');
expect(indexCss).toContain('var(--sable-ios-pwa-viewport-height, 100dvh)');
expect(indexTsx).toContain('installIosPwaViewportHeight();');
expect(iosPwaViewport).toContain("window.matchMedia('(display-mode: standalone)').matches");
expect(iosPwaViewport).toContain('const MIN_KEYBOARD_HEIGHT = 100');
expect(iosPwaViewport).toContain('const keyboardOpen = fullHeight - visibleHeight');
expect(iosPwaViewport).toContain('const height = keyboardOpen ? visibleBottom : fullHeight');
expect(iosPwaViewport).toContain('window.setTimeout(updateHeight, 350)');
});

it('removes the scattered safe-area css consumers', () => {
const indexCss = readWorkspaceFile('src/index.css');
const pageStyles = readWorkspaceFile('src/app/components/page/style.css.ts');
Expand Down
49 changes: 49 additions & 0 deletions src/app/utils/iosPwaViewport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const IOS_PWA_VIEWPORT_HEIGHT = '--sable-ios-pwa-viewport-height';
const MIN_KEYBOARD_HEIGHT = 100;

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

export function installIosPwaViewportHeight(): void {
if (!isStandaloneIosPwa()) return;

let frame = 0;
let settleTimer = 0;
let fullHeight = 0;
let viewportWidth = window.innerWidth;

const updateHeight = () => {
frame = 0;
const viewport = window.visualViewport;
const visibleHeight = viewport?.height ?? window.innerHeight;
const visibleBottom = visibleHeight + (viewport?.offsetTop ?? 0);

if (window.innerWidth !== viewportWidth) {
viewportWidth = window.innerWidth;
fullHeight = visibleBottom;
}

const keyboardOpen = fullHeight - visibleHeight > MIN_KEYBOARD_HEIGHT;
if (!keyboardOpen) fullHeight = Math.max(fullHeight, visibleBottom);

const height = keyboardOpen ? visibleBottom : fullHeight;
document.documentElement.style.setProperty(IOS_PWA_VIEWPORT_HEIGHT, `${Math.round(height)}px`);
};

const scheduleUpdate = () => {
if (frame) cancelAnimationFrame(frame);
frame = requestAnimationFrame(updateHeight);

window.clearTimeout(settleTimer);
settleTimer = window.setTimeout(updateHeight, 350);
};

updateHeight();
window.addEventListener('resize', scheduleUpdate);
window.addEventListener('orientationchange', scheduleUpdate);
window.visualViewport?.addEventListener('resize', scheduleUpdate);
window.visualViewport?.addEventListener('scroll', scheduleUpdate);
document.addEventListener('focusin', scheduleUpdate);
document.addEventListener('focusout', scheduleUpdate);
}
10 changes: 10 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ body {
flex-direction: column;
}

@media (display-mode: standalone) {
@supports (-webkit-touch-callout: none) {
html,
body,
#root {
height: var(--sable-ios-pwa-viewport-height, 100dvh);
}
}
}

*,
*::before,
*::after {
Expand Down
2 changes: 2 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ import { registerMatrixUriProtocol } from './app/plugins/matrix-uri';
import { initTauriMediaSession } from './app/utils/tauriMediaAuth';
import { registerAppServiceWorker } from './serviceWorkerBootstrap';
import { hasServiceWorker } from './app/utils/platform';
import { installIosPwaViewportHeight } from './app/utils/iosPwaViewport';

enableMapSet();
installConsolePasteScamWarning();
registerMatrixUriProtocol();
const log = createLogger('index');

document.body.classList.add(configClass, varsClass);
installIosPwaViewportHeight();

registerAppServiceWorker();

Expand Down
Loading