diff --git a/packages/@adobe/react-spectrum/src/button/Button.tsx b/packages/@adobe/react-spectrum/src/button/Button.tsx index 39f6d340c98..2ee93c95785 100644 --- a/packages/@adobe/react-spectrum/src/button/Button.tsx +++ b/packages/@adobe/react-spectrum/src/button/Button.tsx @@ -15,7 +15,7 @@ import {classNames} from '../utils/classNames'; import {FocusableRef, StyleProps} from '@react-types/shared'; import {FocusRing} from 'react-aria/FocusRing'; import intlMessages from '../../intl/button/*.json'; -import {isAppleDevice, isFirefox} from 'react-aria/private/utils/platform'; +import {isAppleDevice, isFirefox, isWebKit} from 'react-aria/private/utils/platform'; import {mergeProps} from 'react-aria/mergeProps'; import {ProgressCircle} from '../progress/ProgressCircle'; import React, {ElementType, ReactElement, useEffect, useState} from 'react'; @@ -143,7 +143,7 @@ export const Button = React.forwardRef(function Button(); export function disableTextSelection(target?: Element): void { - if (isIOS()) { + if (isIOS() && isWebKit()) { if (state === 'default') { const documentObject = getOwnerDocument(target); savedUserSelect = documentObject.documentElement.style.webkitUserSelect; @@ -55,7 +54,7 @@ export function disableTextSelection(target?: Element): void { } export function restoreTextSelection(target?: Element): void { - if (isIOS()) { + if (isIOS() && isWebKit()) { // If the state is already default, there's nothing to do. // If it is restoring, then there's no need to queue a second restore. if (state !== 'disabled') { diff --git a/packages/react-aria/src/overlays/usePreventScroll.ts b/packages/react-aria/src/overlays/usePreventScroll.ts index 6ed499570d6..7cde4d0ff2e 100644 --- a/packages/react-aria/src/overlays/usePreventScroll.ts +++ b/packages/react-aria/src/overlays/usePreventScroll.ts @@ -15,7 +15,7 @@ import {chain} from '../utils/chain'; import {getActiveElement, getEventTarget} from '../utils/shadowdom/DOMFunctions'; import {getNonce} from '../utils/getNonce'; import {getScrollParent} from '../utils/getScrollParent'; -import {isIOS} from '../utils/platform'; +import {isIOS, isWebKit} from '../utils/platform'; import {isScrollable} from '../utils/isScrollable'; import {useLayoutEffect} from '../utils/useLayoutEffect'; import {willOpenKeyboard} from '../utils/keyboard'; @@ -46,8 +46,8 @@ export function usePreventScroll(options: PreventScrollOptions = {}): void { preventScrollCount++; if (preventScrollCount === 1) { - if (isIOS()) { - restore = preventScrollMobileSafari(); + if (isIOS() && isWebKit()) { + restore = preventScrollMobileWebKit(); } else { restore = preventScrollStandard(); } @@ -96,10 +96,7 @@ function preventScrollStandard() { // by preventing default in a `touchmove` event. This is best effort: we can't prevent default when pinch // zooming or when an element contains text selection, which may allow scrolling in some cases. // 3. Prevent default on `touchend` events on input elements and handle focusing the element ourselves. -// 4. When focus moves to an input, create an off screen input and focus that temporarily. This prevents -// Safari from scrolling the page. After a small delay, focus the real input and scroll it into view -// ourselves, without scrolling the whole page. -function preventScrollMobileSafari() { +function preventScrollMobileWebKit() { // Set overflow hidden so scrollIntoViewport() (useSelectableCollection) sees isScrollPrevented and // scrolls only scroll parents instead of calling native scrollIntoView() which moves the window. let restoreOverflow = setStyle(document.documentElement, 'overflow', 'hidden'); diff --git a/packages/react-aria/src/utils/openLink.tsx b/packages/react-aria/src/utils/openLink.tsx index ffb3f24b878..1fb9f49a741 100644 --- a/packages/react-aria/src/utils/openLink.tsx +++ b/packages/react-aria/src/utils/openLink.tsx @@ -110,7 +110,12 @@ export function openLink(target: HTMLAnchorElement, modifiers: Modifiers, setOpe // will prevent links with target="_blank" from opening. However, it does allow the event if the // Command/Control key is held, which opens the link in a background tab. This seems like the best we can do. // See https://bugzilla.mozilla.org/show_bug.cgi?id=257870 and https://bugzilla.mozilla.org/show_bug.cgi?id=746640. - if (isFirefox() && window.event?.type?.startsWith('key') && target.target === '_blank') { + if ( + !isWebKit() && + isFirefox() && + window.event?.type?.startsWith('key') && + target.target === '_blank' + ) { if (isMac()) { metaKey = true; } else { diff --git a/packages/react-aria/src/utils/platform.ts b/packages/react-aria/src/utils/platform.ts index 673368b737a..f0d91b0824f 100644 --- a/packages/react-aria/src/utils/platform.ts +++ b/packages/react-aria/src/utils/platform.ts @@ -67,11 +67,15 @@ export const isAppleDevice: () => boolean = cached(function () { }); export const isWebKit: () => boolean = cached(function () { - return testUserAgent(/AppleWebKit/i) && !isChrome(); + return testUserAgent(/AppleWebKit/i) && (isIOS() || !isChrome()); +}); + +export const isSafari: () => boolean = cached(function () { + return isWebKit() && !isChrome() && !isFirefox(); }); export const isChrome: () => boolean = cached(function () { - return testUserAgent(/Chrome/i); + return testUserAgent(/Chrome|CriOS|CrMo/i); }); export const isAndroid: () => boolean = cached(function () { @@ -79,5 +83,5 @@ export const isAndroid: () => boolean = cached(function () { }); export const isFirefox: () => boolean = cached(function () { - return testUserAgent(/Firefox/i); + return testUserAgent(/(Firefox|FxiOS)/i); }); diff --git a/packages/react-aria/src/utils/scrollIntoView.ts b/packages/react-aria/src/utils/scrollIntoView.ts index ff975f6c7d0..86bbfcf5ac8 100644 --- a/packages/react-aria/src/utils/scrollIntoView.ts +++ b/packages/react-aria/src/utils/scrollIntoView.ts @@ -11,7 +11,7 @@ */ import {getScrollParents} from './getScrollParents'; -import {isIOS} from './platform'; +import {isIOS, isWebKit} from '../utils/platform'; interface ScrollIntoViewOpts { /** The position to align items along the block axis in. */ @@ -89,11 +89,11 @@ export function scrollIntoView( let scrollPortLeft = viewLeft + (isRoot ? 0 : borderLeftWidth) + scrollPaddingLeft; let scrollPortRight = viewRight - (isRoot ? 0 : borderRightWidth) - scrollPaddingRight; - // IOS always positions the scrollbar on the right ¯\_(ツ)_/¯ - if (viewStyle.direction === 'rtl' && !isIOS()) { - scrollPortLeft += scrollBarWidth; - } else { + // WebKit on iOS always positions the scrollbar on the right ¯\_(ツ)_/¯ + if ((isIOS() && isWebKit()) || viewStyle.direction === 'ltr') { scrollPortRight -= scrollBarWidth; + } else if (viewStyle.direction === 'rtl') { + scrollPortLeft += scrollBarWidth; } let shouldScrollBlock = scrollAreaTop < scrollPortTop || scrollAreaBottom > scrollPortBottom; diff --git a/packages/react-aria/src/utils/useViewportSize.ts b/packages/react-aria/src/utils/useViewportSize.ts index 6ff0c4f7d94..5ef634a91e1 100644 --- a/packages/react-aria/src/utils/useViewportSize.ts +++ b/packages/react-aria/src/utils/useViewportSize.ts @@ -11,7 +11,7 @@ */ import {getActiveElement, getEventTarget} from './shadowdom/DOMFunctions'; -import {isIOS} from './platform'; +import {isIOS, isWebKit} from './platform'; import {useEffect, useState} from 'react'; import {useIsSSR} from '../ssr/SSRProvider'; import {willOpenKeyboard} from './keyboard'; @@ -47,7 +47,7 @@ export function useViewportSize(): ViewportSize { updateSize(getViewportSize()); }; - // When closing the keyboard, iOS does not fire the visual viewport resize event until the animation is complete. + // When closing the keyboard, WebKit on iOS does not fire the visual viewport resize event until the animation is complete. // We can anticipate this and resize early by handling the blur event and using the layout size. let frame: number; let onBlur = (e: FocusEvent) => { @@ -71,7 +71,7 @@ export function useViewportSize(): ViewportSize { updateSize(getViewportSize()); - if (isIOS()) { + if (isIOS() && isWebKit()) { window.addEventListener('blur', onBlur, true); } @@ -83,7 +83,7 @@ export function useViewportSize(): ViewportSize { return () => { cancelAnimationFrame(frame); - if (isIOS()) { + if (isIOS() && isWebKit()) { window.removeEventListener('blur', onBlur, true); } if (!visualViewport) { diff --git a/packages/react-aria/stories/utils/platform.stories.tsx b/packages/react-aria/stories/utils/platform.stories.tsx index ffa858125f3..0534f515756 100644 --- a/packages/react-aria/stories/utils/platform.stories.tsx +++ b/packages/react-aria/stories/utils/platform.stories.tsx @@ -14,10 +14,12 @@ import { isAndroid, isAppleDevice, isChrome, + isFirefox, isIOS, isIPad, isIPhone, isMac, + isSafari, isWebKit } from '../../src/utils/platform'; import {Meta, StoryObj} from '@storybook/react'; @@ -65,6 +67,14 @@ const Template = (args: any): JSX.Element => ( isWebKit: {isWebKit().toString()} + + isSafari: + {isSafari().toString()} + + + isFirefox: + {isFirefox().toString()} + );