As a work-around we had Claude write us a substitute called AppModal and AppModal.Section for Polaris Modal and Modal.Section.
On small displays where Shopify Mobile displays the floating toolbar, this component displays our own full screen modal using a div with the action buttons at the bottom padded with space. On larger displays it falls back to using the Polaris Modal component.
To use import AppModal.js and replace all Modal / Modal.Section with AppModal / AppModal.Section
import AppModal from "AppModal";
AppModal.js
// On condensed (mobile) displays the Modal footer buttons are currently
// buggy, so we render the same content inside a full screen <Page> instead.
// Use Polaris Modal on non-condensed displays.
//
// Supported props mirror the subset of Polaris <Modal>:
// open, title, sectioned, size, primaryAction, secondaryActions, onClose, children.
// Wrap children in <AppModal.Section> the same way you'd use <Modal.Section>.
import React, { useEffect, useCallback, createContext, useContext } from "react";
import { Modal, Page, Button, InlineStack, useBreakpoints } from "@shopify/polaris";
const ModeContext = createContext("modal"); // "modal" | "page"
const AppModal = ({
open,
title,
sectioned = false,
size = "medium",
primaryAction,
secondaryActions = [],
onClose,
children,
}) => {
const { mdDown } = useBreakpoints();
const useFullscreenPage = mdDown;
const handleKey = useCallback(
(e) => {
if (e.key === "Escape" && onClose) onClose();
},
[onClose],
);
useEffect(() => {
if (!open || !useFullscreenPage) return undefined;
document.addEventListener("keydown", handleKey);
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", handleKey);
document.body.style.overflow = prevOverflow;
};
}, [open, useFullscreenPage, handleKey]);
if (!open) return null;
if (!useFullscreenPage) {
return (
<ModeContext.Provider value="modal">
<Modal
open={open}
title={title}
sectioned={sectioned}
size={size}
primaryAction={primaryAction}
secondaryActions={secondaryActions}
onClose={onClose}
>
{children}
</Modal>
</ModeContext.Provider>
);
}
return (
<ModeContext.Provider value="page">
<div
role="dialog"
aria-modal="true"
aria-label={typeof title === "string" ? title : undefined}
style={{
position: "fixed",
inset: 0,
zIndex: 510,
background: "var(--p-color-bg, #f6f6f7)",
overflowY: "auto",
WebkitOverflowScrolling: "touch",
}}
>
<Page
title={title}
backAction={
onClose ? { content: "Close", onAction: onClose } : undefined
}
fullWidth
>
<div style={{ padding: sectioned ? "16px 0" : 0 }}>{children}</div>
{(primaryAction ||
(Array.isArray(secondaryActions) &&
secondaryActions.length > 0)) && (
<div style={{ padding: "16px 12px 80px" }}>
<InlineStack gap="200" wrap align="end">
{Array.isArray(secondaryActions) &&
secondaryActions.map((action, i) => {
if (!action) return null;
const {
content,
onAction,
loading,
disabled,
destructive,
icon,
size,
} = action;
return (
<Button
key={i}
onClick={onAction}
loading={loading}
disabled={disabled}
tone={destructive ? "critical" : undefined}
icon={icon}
size={size}
>
{content}
</Button>
);
})}
{primaryAction &&
(() => {
const {
content,
onAction,
loading,
disabled,
destructive,
icon,
size,
} = primaryAction;
return (
<Button
variant="primary"
onClick={onAction}
loading={loading}
disabled={disabled}
tone={destructive ? "critical" : undefined}
icon={icon}
size={size}
>
{content}
</Button>
);
})()}
</InlineStack>
</div>
)}
</Page>
</div>
</ModeContext.Provider>
);
};
const Section = ({ children }) => {
const mode = useContext(ModeContext);
if (mode === "page") {
return (
<div
style={{
padding: "16px 16px",
borderBottom: "1px solid var(--p-color-border-secondary, #e1e3e5)",
}}
>
{children}
</div>
);
}
return <Modal.Section>{children}</Modal.Section>;
};
AppModal.Section = Section;
export default AppModal;