From 60e8535abde83b7a7a7eaa5599496968ad0a1464 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 22 Sep 2025 09:17:19 -0700 Subject: [PATCH 1/6] fix: Fix collection dirty node tracking with suspense (#8892) --- .../@react-aria/collections/src/Document.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/@react-aria/collections/src/Document.ts b/packages/@react-aria/collections/src/Document.ts index 7c50285dcf2..453ce572404 100644 --- a/packages/@react-aria/collections/src/Document.ts +++ b/packages/@react-aria/collections/src/Document.ts @@ -461,16 +461,14 @@ export class Document = BaseCollection> extend } private removeNode(node: ElementNode): void { - if (node.node == null) { - return; - } - for (let child of node) { this.removeNode(child); } - let collection = this.getMutableCollection(); - collection.removeNode(node.node.key); + if (node.node) { + let collection = this.getMutableCollection(); + collection.removeNode(node.node.key); + } } /** Finalizes the collection update, updating all nodes and freezing the collection. */ @@ -508,12 +506,16 @@ export class Document = BaseCollection> extend this.addNode(element); } + if (element.node) { + this.dirtyNodes.delete(element); + } + element.isMutated = false; + } else { + this.dirtyNodes.delete(element); } } - this.dirtyNodes.clear(); - // Finally, update the collection. if (this.nextCollection) { this.nextCollection.commit(this.firstVisibleChild?.node?.key ?? null, this.lastVisibleChild?.node?.key ?? null, this.isSSR); From f1a021ec2dbaba345da10fac4e6691fb057f30ca Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 22 Sep 2025 10:02:55 -0700 Subject: [PATCH 2/6] fix: Improve behavior of modals on iOS 26 (#8888) * Improve usePreventScroll focus behavior on iOS * Make visual viewport size update before keyboard animation * Update modals and trays to extend underlay behind browser UI * Fix S2 docs header appearing above modals * fix removing event * Clamp scroll position to prevent over-scrolling * Prevent scrolling the whole page when programmatically focusing * Avoid setting size on blur too early * Example app fix * Ensure temporary input has attributes that might affect keyboard size * typo * Fix scroll into view calculation when the address bar is collapsed * Try removing the hidden input entirely --- .../components/tray/index.css | 13 +- .../components/underlay/index.css | 3 +- .../@react-aria/dialog/docs/useDialog.mdx | 22 ++- .../overlays/docs/useModalOverlay.mdx | 22 ++- .../overlays/src/usePreventScroll.ts | 177 +++++++----------- packages/@react-aria/utils/src/index.ts | 2 +- packages/@react-aria/utils/src/keyboard.tsx | 21 +++ .../@react-aria/utils/src/useViewportSize.ts | 25 +++ .../@react-spectrum/overlays/src/Tray.tsx | 6 +- .../@react-spectrum/overlays/src/Underlay.tsx | 10 +- packages/@react-spectrum/s2/src/Dialog.tsx | 7 +- .../s2/src/FullscreenDialog.tsx | 2 +- packages/@react-spectrum/s2/src/Modal.tsx | 170 +++++++++-------- .../docs/pages/react-aria/home/ExampleApp.tsx | 27 ++- packages/dev/s2-docs/src/Layout.tsx | 157 ++++++++-------- packages/react-aria-components/docs/Modal.mdx | 31 +-- .../docs/examples/command-palette.mdx | 62 +++--- .../docs/examples/destructive-dialog.mdx | 8 +- packages/react-aria-components/src/Modal.tsx | 3 +- starters/docs/src/Modal.css | 13 +- starters/docs/src/Sheet.css | 19 +- starters/tailwind/src/Modal.tsx | 6 +- 22 files changed, 458 insertions(+), 348 deletions(-) diff --git a/packages/@adobe/spectrum-css-temp/components/tray/index.css b/packages/@adobe/spectrum-css-temp/components/tray/index.css index cd58246a1eb..b82f9ae6250 100644 --- a/packages/@adobe/spectrum-css-temp/components/tray/index.css +++ b/packages/@adobe/spectrum-css-temp/components/tray/index.css @@ -20,14 +20,13 @@ .spectrum-Tray-wrapper { inset-inline-start: 0; - /* Positioned at the top of the window */ - position: fixed; + position: absolute; top: 0; display: flex; justify-content: center; width: 100%; - height: 100vh; + height: 100dvh; /* Don't catch clicks */ pointer-events: none; @@ -55,10 +54,12 @@ max-height: calc(var(--spectrum-visual-viewport-height) - var(--spectrum-tray-margin-top)); /* Add padding at the bottom to account for the rest of the viewport height behind the keyboard. * This is necessary so that there isn't a visible gap that appears while the keyboard is animating - * in and out. Fall back to the safe area inset to account for things like iOS home indicator. */ - padding-bottom: max(calc(100vh - var(--spectrum-visual-viewport-height)), env(safe-area-inset-bottom)); + * in and out. Fall back to the safe area inset to account for things like iOS home indicator. + We also add an additional 100vh of padding (offset by the bottom position below) so the tray + extends behind Safari's address bar and keyboard in iOS 26. */ + padding-bottom: calc(max(calc(100dvh - var(--spectrum-visual-viewport-height)), env(safe-area-inset-bottom)) + 100vh); position: absolute; - bottom: 0; + bottom: -100vh; outline: none; display: flex; flex-direction: column; diff --git a/packages/@adobe/spectrum-css-temp/components/underlay/index.css b/packages/@adobe/spectrum-css-temp/components/underlay/index.css index 95de662061b..82da0097342 100644 --- a/packages/@adobe/spectrum-css-temp/components/underlay/index.css +++ b/packages/@adobe/spectrum-css-temp/components/underlay/index.css @@ -23,7 +23,8 @@ governing permissions and limitations under the License. .spectrum-Underlay { composes: spectrum-overlay; - position: fixed; + /* Use position: absolute instead of fixed to avoid being clipped to the "inner" viewport in iOS 26 */ + position: absolute; top: 0; right: 0; bottom: 0; diff --git a/packages/@react-aria/dialog/docs/useDialog.mdx b/packages/@react-aria/dialog/docs/useDialog.mdx index b206ed281ff..58a006f6f8c 100644 --- a/packages/@react-aria/dialog/docs/useDialog.mdx +++ b/packages/@react-aria/dialog/docs/useDialog.mdx @@ -131,6 +131,7 @@ The `Modal` and `ModalTrigger` components render the dialog within a typical mod ```tsx example export=true render=false import {useOverlayTriggerState} from '@react-stately/overlays'; import {Overlay, useModalOverlay, useOverlayTrigger} from '@react-aria/overlays'; +import {useViewportSize} from '@react-aria/utils'; function Modal({state, children, ...props}) { let ref = React.useRef(null); @@ -140,18 +141,27 @@ function Modal({state, children, ...props}) {
+
+ }}>
+
+ }}>
{ // Store the nearest scrollable parent element from the element that the user touched. scrollable = getScrollParent(e.target as Element, true); if (scrollable === document.documentElement && scrollable === document.body) { return; } - - // Prevent scrolling up when at the top and scrolling down when at the bottom - // of a nested scrollable area, otherwise mobile Safari will start scrolling - // the window instead. - if (scrollable instanceof HTMLElement && window.getComputedStyle(scrollable).overscrollBehavior === 'auto') { - restoreScrollableStyles = setStyle(scrollable, 'overscrollBehavior', 'contain'); - } }; + // Prevent scrolling up when at the top and scrolling down when at the bottom + // of a nested scrollable area, otherwise mobile Safari will start scrolling + // the window instead. + // This must be applied before the touchstart event as of iOS 26, so inject it as a