Skip to content
Open
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
4 changes: 2 additions & 2 deletions packages/@adobe/react-spectrum/src/button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -143,7 +143,7 @@ export const Button = React.forwardRef(function Button<T extends ElementType = '
: `${hasIcon ? iconId : ''} ${hasLabel ? textId : ''} ${spinnerId}`.trim();

let ariaLive: 'off' | 'polite' | 'assertive' = 'polite';
if (isAppleDevice() && (!hasAriaLabel || isFirefox())) {
if (isAppleDevice() && (!hasAriaLabel || (!isWebKit() && isFirefox()))) {

@snowystinger snowystinger Jul 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to spell this one out for practice:

Previous
iOS firefox -> false, no FxiOS matching
macOS firefox -> true

Now
iOS firefox today

  • isWebKit true, isFirefox true -> false

eventually

  • isWebKit false, isFirefox true -> true

macOS firefox -> true

The thinking being that Firefox will eventually have the same issues on iOS when it moves off webkit to their own engine, so it'll match desktop

@nwidynski nwidynski Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct. it also purposefully uses isWebKit rather than isIOS because these alt engines on ios may use an android ua for compatibility. The exception will be Chrome, which locked to AppleWebKit everywhere, so we will have to see whether they will offer something to differentiate on ios - tbd.

ariaLive = 'off';
}

Expand Down
2 changes: 1 addition & 1 deletion packages/@react-spectrum/s2/src/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ export function SpectrumToast(props: SpectrumToastProps): ReactNode {
}

function fixSafariTransform(el: HTMLDivElement | null) {
// Safari has a bug where the toasts display in the wrong position (CSS transform is not applied correctly).
// WebKit has a bug where the toasts display in the wrong position (CSS transform is not applied correctly).
// Work around this by removing it, forcing a reflow, and re-applying.
if (el && isWebKit()) {
let translate = el.style.translate;
Expand Down
3 changes: 2 additions & 1 deletion packages/react-aria/exports/private/utils/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export {
isAppleDevice,
isChrome,
isAndroid,
isFirefox
isFirefox,
isSafari
} from '../../../src/utils/platform';
9 changes: 4 additions & 5 deletions packages/react-aria/src/interactions/textSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@
*/

import {getOwnerDocument} from '../utils/domHelpers';

import {isIOS} from '../utils/platform';
import {isIOS, isWebKit} from '../utils/platform';
import {runAfterTransition} from '../utils/runAfterTransition';

// Safari on iOS starts selecting text on long press. The only way to avoid this, it seems,
// WebKit on iOS starts selecting text on long press. The only way to avoid this, it seems,
// is to add user-select: none to the entire page. Adding it to the pressable element prevents
// that element from being selected, but nearby elements may still receive selection. We add
// user-select: none on touch start, and remove it again on touch end to prevent this.
Expand All @@ -37,7 +36,7 @@ let savedUserSelect = '';
let modifiedElementMap = new WeakMap<Element, string>();

export function disableTextSelection(target?: Element): void {
if (isIOS()) {
if (isIOS() && isWebKit()) {
if (state === 'default') {
const documentObject = getOwnerDocument(target);
savedUserSelect = documentObject.documentElement.style.webkitUserSelect;
Expand All @@ -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') {
Expand Down
11 changes: 4 additions & 7 deletions packages/react-aria/src/overlays/usePreventScroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -46,8 +46,8 @@ export function usePreventScroll(options: PreventScrollOptions = {}): void {

preventScrollCount++;
if (preventScrollCount === 1) {
if (isIOS()) {
restore = preventScrollMobileSafari();
if (isIOS() && isWebKit()) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apple opened up their platform for alternative rendering engines a while back. This is meant to future proof against something like https://github.com/minh-ton/reynard-browser.

restore = preventScrollMobileWebKit();
} else {
restore = preventScrollStandard();
}
Expand Down Expand Up @@ -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');
Expand Down
7 changes: 6 additions & 1 deletion packages/react-aria/src/utils/openLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 7 additions & 3 deletions packages/react-aria/src/utils/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,21 @@ 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 () {
return testUserAgent(/Android/i);
});

export const isFirefox: () => boolean = cached(function () {
return testUserAgent(/Firefox/i);
return testUserAgent(/(Firefox|FxiOS)/i);
});
10 changes: 5 additions & 5 deletions packages/react-aria/src/utils/scrollIntoView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions packages/react-aria/src/utils/useViewportSize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) => {
Expand All @@ -71,7 +71,7 @@ export function useViewportSize(): ViewportSize {

updateSize(getViewportSize());

if (isIOS()) {
if (isIOS() && isWebKit()) {
window.addEventListener('blur', onBlur, true);
}

Expand All @@ -83,7 +83,7 @@ export function useViewportSize(): ViewportSize {

return () => {
cancelAnimationFrame(frame);
if (isIOS()) {
if (isIOS() && isWebKit()) {
window.removeEventListener('blur', onBlur, true);
}
if (!visualViewport) {
Expand Down
10 changes: 10 additions & 0 deletions packages/react-aria/stories/utils/platform.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -65,6 +67,14 @@ const Template = (args: any): JSX.Element => (
<td>isWebKit: </td>
<td>{isWebKit().toString()}</td>
</tr>
<tr>
<td>isSafari: </td>
<td>{isSafari().toString()}</td>
</tr>
Comment thread
nwidynski marked this conversation as resolved.
<tr>
<td>isFirefox: </td>
<td>{isFirefox().toString()}</td>
</tr>
</table>
);

Expand Down