Skip to content

Commit 6f9ec6f

Browse files
authored
fix: respect device safe areas in mobile fullscreen overlays (#1721)
<!-- 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 - [x] 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: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] 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 c493818 + 0538e0f commit 6f9ec6f

4 files changed

Lines changed: 40 additions & 1 deletion

File tree

src/app/components/image-viewer/RoomMediaViewer.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,13 @@ export function RoomMediaViewer({
185185
if (!item) return null;
186186

187187
return (
188-
<ModalOverlay open requestClose={requestClose} mobile="fullscreen" background="#000">
188+
<ModalOverlay
189+
open
190+
requestClose={requestClose}
191+
mobile="fullscreen"
192+
background="#000"
193+
respectSafeArea={false}
194+
>
189195
<ResolvedRoomMedia
190196
item={item}
191197
requestClose={requestClose}

src/app/components/message/content/ImageContent.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ export const ImageContent = as<'div', ImageContentProps>(
311311
requestClose={() => setViewer(false)}
312312
mobile="fullscreen"
313313
background="#000"
314+
respectSafeArea={false}
314315
>
315316
{isMobile ? (
316317
viewerContent

src/app/components/modal-overlay/ModalOverlay.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,24 @@ describe('ModalOverlay', () => {
209209
expect(screen.getByTestId('modal-child')).toBeInTheDocument();
210210
});
211211

212+
it('pads the fullscreen wrapper against device safe areas by default', () => {
213+
mobile({ mobile: 'fullscreen' });
214+
215+
const wrapper = screen
216+
.getByTestId('modal-child')
217+
.closest('div[style*="padding-top"]') as HTMLElement;
218+
expect(wrapper.style.paddingTop).toBe(
219+
'var(--safe-area-inset-top, env(safe-area-inset-top, 0px))'
220+
);
221+
});
222+
223+
it('skips safe-area padding when respectSafeArea is false', () => {
224+
mobile({ mobile: 'fullscreen', respectSafeArea: false });
225+
226+
const wrapper = screen.getByTestId('modal-child').parentElement as HTMLElement;
227+
expect(wrapper.style.paddingTop ?? '').toBe('');
228+
});
229+
212230
it('wraps children in a Modal on desktop when size is set', () => {
213231
desktop({ size: '500' });
214232

src/app/components/modal-overlay/ModalOverlay.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ import * as messageCss from '$features/room/message/styles.css';
1212
type FocusTrapOptions = ComponentProps<typeof FocusTrap>['focusTrapOptions'];
1313
type ModalSize = '300' | '400' | '500';
1414

15+
const safeArea = {
16+
top: 'var(--safe-area-inset-top, env(safe-area-inset-top, 0px))',
17+
bottom: 'var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))',
18+
left: 'var(--safe-area-inset-left, env(safe-area-inset-left, 0px))',
19+
right: 'var(--safe-area-inset-right, env(safe-area-inset-right, 0px))',
20+
};
21+
1522
type ModalOverlayProps = {
1623
open?: boolean;
1724
requestClose: () => void;
@@ -30,6 +37,8 @@ type ModalOverlayProps = {
3037
escapeDeactivates?: FocusTrapOptions['escapeDeactivates'];
3138
/** Fills the mobile fullscreen wrapper, for content that does not paint its own. */
3239
background?: string;
40+
/** Set false for full-bleed viewers that inset their own controls. */
41+
respectSafeArea?: boolean;
3342
children: ReactNode;
3443
};
3544

@@ -42,6 +51,7 @@ export function ModalOverlay({
4251
contentRef,
4352
escapeDeactivates = stopPropagation,
4453
background,
54+
respectSafeArea = true,
4555
children,
4656
}: ModalOverlayProps) {
4757
// Null outside a provider, where desktop is the safe assumption.
@@ -75,6 +85,10 @@ export function ModalOverlay({
7585
display: 'flex',
7686
flexDirection: 'column',
7787
background,
88+
paddingTop: respectSafeArea ? safeArea.top : undefined,
89+
paddingBottom: respectSafeArea ? safeArea.bottom : undefined,
90+
paddingLeft: respectSafeArea ? safeArea.left : undefined,
91+
paddingRight: respectSafeArea ? safeArea.right : undefined,
7892
}}
7993
>
8094
{children}

0 commit comments

Comments
 (0)