diff --git a/package.json b/package.json
index 7e4c459eb0d00..40db11eb88a59 100644
--- a/package.json
+++ b/package.json
@@ -70,6 +70,7 @@
"@babel/plugin-transform-object-rest-spread": "7.29.7",
"@babel/traverse": "catalog:",
"@babel/types": "7.29.7",
+ "@base-ui/utils": "catalog:",
"@emotion/cache": "catalog:",
"@inquirer/prompts": "8.5.1",
"@mui/internal-babel-plugin-display-name": "1.0.4-canary.20",
diff --git a/packages/x-data-grid-premium/package.json b/packages/x-data-grid-premium/package.json
index 19c73a18dba58..0a7e2b6423635 100644
--- a/packages/x-data-grid-premium/package.json
+++ b/packages/x-data-grid-premium/package.json
@@ -40,6 +40,7 @@
},
"dependencies": {
"@babel/runtime": "catalog:",
+ "@base-ui/utils": "catalog:",
"@mui/utils": "catalog:",
"@mui/x-data-grid": "workspace:^",
"@mui/x-data-grid-pro": "workspace:^",
diff --git a/packages/x-data-grid-premium/src/hooks/features/cellSelection/useGridCellSelection.ts b/packages/x-data-grid-premium/src/hooks/features/cellSelection/useGridCellSelection.ts
index 81719b7b71933..45566d7e36df8 100644
--- a/packages/x-data-grid-premium/src/hooks/features/cellSelection/useGridCellSelection.ts
+++ b/packages/x-data-grid-premium/src/hooks/features/cellSelection/useGridCellSelection.ts
@@ -1,5 +1,6 @@
'use client';
import * as React from 'react';
+import { platform } from '@base-ui/utils/platform';
import type { RefObject } from '@mui/x-internals/types';
import ownerDocument from '@mui/utils/ownerDocument';
import useEventCallback from '@mui/utils/useEventCallback';
@@ -302,8 +303,7 @@ export const useGridCellSelection = (
(params, event) => {
// Skip if the click comes from the right-button or, only on macOS, Ctrl is pressed
// Fix for https://github.com/mui/mui-x/pull/6567#issuecomment-1329155578
- const isMacOs = window.navigator.platform.toUpperCase().indexOf('MAC') >= 0;
- if (event.button !== 0 || (event.ctrlKey && isMacOs)) {
+ if (event.button !== 0 || (event.ctrlKey && platform.os.mac)) {
return;
}
diff --git a/packages/x-data-grid-premium/src/tests/cellSelection.DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/cellSelection.DataGridPremium.test.tsx
index 566bb5127aef8..81560b6783dc4 100644
--- a/packages/x-data-grid-premium/src/tests/cellSelection.DataGridPremium.test.tsx
+++ b/packages/x-data-grid-premium/src/tests/cellSelection.DataGridPremium.test.tsx
@@ -13,7 +13,7 @@ import {
gridClasses,
} from '@mui/x-data-grid-premium';
import { getBasicGridData } from '@mui/x-data-grid-generator';
-import { isJSDOM } from 'test/utils/skipIf';
+import { isJSDOM, isOSX } from 'test/utils/skipIf';
describe(' - Cell selection', () => {
const { render } = createRenderer();
@@ -416,9 +416,7 @@ describe(' - Cell selection', () => {
);
// Add a new cell range to the selection
- const isMac = window.navigator.platform.toUpperCase().indexOf('MAC') >= 0;
-
- await user.keyboard(isMac ? '{Meta>}' : '{Control>}');
+ await user.keyboard(isOSX ? '{Meta>}' : '{Control>}');
await user.pointer([
// touch the screen at element1
{ keys: '[MouseLeft>]', target: getCell(2, 0) },
@@ -427,7 +425,7 @@ describe(' - Cell selection', () => {
// release the touch pointer at the last position (element2)
{ keys: '[/MouseLeft]' },
]);
- await user.keyboard(isMac ? '{/Meta}' : '{/Control}');
+ await user.keyboard(isOSX ? '{/Meta}' : '{/Control}');
expect(onCellSelectionModelChange.lastCall.args[0]).to.deep.equal({
'0': { id: true },
diff --git a/packages/x-data-grid/package.json b/packages/x-data-grid/package.json
index ef683ee5d154c..8d95b83a5aff8 100644
--- a/packages/x-data-grid/package.json
+++ b/packages/x-data-grid/package.json
@@ -44,6 +44,7 @@
},
"dependencies": {
"@babel/runtime": "catalog:",
+ "@base-ui/utils": "catalog:",
"@mui/utils": "catalog:",
"@mui/x-internals": "workspace:^",
"@mui/x-virtualizer": "workspace:*",
diff --git a/packages/x-data-grid/src/hooks/features/dimensions/useGridDimensions.ts b/packages/x-data-grid/src/hooks/features/dimensions/useGridDimensions.ts
index 664cc191d8309..c34fc78147553 100644
--- a/packages/x-data-grid/src/hooks/features/dimensions/useGridDimensions.ts
+++ b/packages/x-data-grid/src/hooks/features/dimensions/useGridDimensions.ts
@@ -1,5 +1,6 @@
'use client';
import * as React from 'react';
+import { platform } from '@base-ui/utils/platform';
import type { RefObject } from '@mui/x-internals/types';
import { useStoreEffect } from '@mui/x-internals/store';
import type { GridEventListener } from '../../../models/events';
@@ -27,7 +28,6 @@ import { getTotalHeaderHeight } from '../columns/gridColumnsUtils';
import type { GridStateInitializer } from '../../utils/useGridInitializeState';
import { DATA_GRID_PROPS_DEFAULT_VALUES } from '../../../constants/dataGridPropsDefaultValues';
import { roundToDecimalPlaces } from '../../../utils/roundToDecimalPlaces';
-import { isJSDOM } from '../../../utils/isJSDOM';
type RootProps = Pick<
DataGridProcessedProps,
@@ -148,7 +148,7 @@ export function useGridDimensions(apiRef: RefObject, pr
if (!getRootDimensions().isReady) {
return;
}
- if (size.height === 0 && !errorShown.current && !props.autoHeight && !isJSDOM) {
+ if (size.height === 0 && !errorShown.current && !props.autoHeight && !platform.env.jsdom) {
logger.error(
[
'The parent DOM element of the Data Grid has an empty height.',
@@ -160,7 +160,7 @@ export function useGridDimensions(apiRef: RefObject, pr
);
errorShown.current = true;
}
- if (size.width === 0 && !errorShown.current && !isJSDOM) {
+ if (size.width === 0 && !errorShown.current && !platform.env.jsdom) {
logger.error(
[
'The parent DOM element of the Data Grid has an empty width.',
diff --git a/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualization.tsx b/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualization.tsx
index 3d6e695171ea5..c500f66860a1f 100644
--- a/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualization.tsx
+++ b/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualization.tsx
@@ -2,14 +2,14 @@
import * as React from 'react';
import type { RefObject } from '@mui/x-internals/types';
import { type Virtualization, type LayoutDataGrid, EMPTY_RENDER_CONTEXT } from '@mui/x-virtualizer';
-import { isJSDOM } from '../../../utils/isJSDOM';
+import { platform } from '@base-ui/utils/platform';
import type { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity';
import { useGridApiMethod } from '../../utils/useGridApiMethod';
import type { GridStateInitializer } from '../../utils/useGridInitializeState';
import { useGridEventPriority } from '../../utils';
import type { DataGridProcessedProps } from '../../../models/props/DataGridProps';
-const HAS_LAYOUT = !isJSDOM;
+const HAS_LAYOUT = !platform.env.jsdom;
type RootProps = DataGridProcessedProps;
diff --git a/packages/x-data-grid/src/utils/formatNumber.test.ts b/packages/x-data-grid/src/utils/formatNumber.test.ts
index 01c599a336abe..ef621cc00f111 100644
--- a/packages/x-data-grid/src/utils/formatNumber.test.ts
+++ b/packages/x-data-grid/src/utils/formatNumber.test.ts
@@ -1,5 +1,5 @@
+import { isJSDOM } from 'test/utils/skipIf';
import { formatNumber } from './getGridLocalization';
-import { isJSDOM } from './isJSDOM';
describe('formatNumber', () => {
it('should format numbers with thousands separators', () => {
diff --git a/packages/x-data-grid/src/utils/isJSDOM.ts b/packages/x-data-grid/src/utils/isJSDOM.ts
deleted file mode 100644
index 635b110eb7758..0000000000000
--- a/packages/x-data-grid/src/utils/isJSDOM.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export const isJSDOM =
- typeof window !== 'undefined' && /jsdom|HappyDOM/.test(window.navigator.userAgent);
diff --git a/packages/x-date-pickers/src/internals/hooks/useField/useField.utils.ts b/packages/x-date-pickers/src/internals/hooks/useField/useField.utils.ts
index f22fb2b135852..55ba88be10337 100644
--- a/packages/x-date-pickers/src/internals/hooks/useField/useField.utils.ts
+++ b/packages/x-date-pickers/src/internals/hooks/useField/useField.utils.ts
@@ -585,8 +585,6 @@ export const mergeDateIntoReferenceDate = (
return mergedDate;
}, referenceDate);
-export const isAndroid = () => navigator.userAgent.toLowerCase().includes('android');
-
export const getSectionOrder = (sections: FieldSection[]): SectionOrdering => {
const neighbors: SectionNeighbors = {};
sections.forEach((_, index) => {
diff --git a/packages/x-date-pickers/src/internals/hooks/useReduceAnimations.ts b/packages/x-date-pickers/src/internals/hooks/useReduceAnimations.ts
index 1c8a174c85908..bd7a4f008ab1b 100644
--- a/packages/x-date-pickers/src/internals/hooks/useReduceAnimations.ts
+++ b/packages/x-date-pickers/src/internals/hooks/useReduceAnimations.ts
@@ -2,13 +2,17 @@ import useMediaQuery from '@mui/material/useMediaQuery';
const PREFERS_REDUCED_MOTION = '@media (prefers-reduced-motion: reduce)';
-// detect if user agent has Android version < 10 or iOS version < 13
+// TODO(v10): Remove user-agent sniffing. The Android branch is dead and the iOS branch is becoming
+// irrelevant.
+// https://github.com/mui/mui-x/pull/22710#discussion_r3377072061
+
const mobileVersionMatches =
typeof navigator !== 'undefined' && navigator.userAgent.match(/android\s(\d+)|OS\s(\d+)/i);
const androidVersion =
mobileVersionMatches && mobileVersionMatches[1] ? parseInt(mobileVersionMatches[1], 10) : null;
const iOSVersion =
mobileVersionMatches && mobileVersionMatches[2] ? parseInt(mobileVersionMatches[2], 10) : null;
+
export const slowAnimationDevices =
(androidVersion && androidVersion < 10) || (iOSVersion && iOSVersion < 13) || false;
diff --git a/packages/x-internal-gestures/package.json b/packages/x-internal-gestures/package.json
index a43f167956217..b27ed5fdb16e2 100644
--- a/packages/x-internal-gestures/package.json
+++ b/packages/x-internal-gestures/package.json
@@ -39,7 +39,8 @@
"prebuild": "rimraf build tsconfig.build.tsbuildinfo"
},
"dependencies": {
- "@babel/runtime": "catalog:"
+ "@babel/runtime": "catalog:",
+ "@base-ui/utils": "catalog:"
},
"sideEffects": false,
"exports": {
diff --git a/packages/x-internal-gestures/src/core/KeyboardManager.ts b/packages/x-internal-gestures/src/core/KeyboardManager.ts
index 90cc642e669b5..b02a28d8e024f 100644
--- a/packages/x-internal-gestures/src/core/KeyboardManager.ts
+++ b/packages/x-internal-gestures/src/core/KeyboardManager.ts
@@ -6,6 +6,8 @@
* 2. Providing methods to check if specific keys are pressed
*/
+import { platform } from '@base-ui/utils/platform';
+
/**
* Type definition for keyboard keys
*/
@@ -103,9 +105,8 @@ export class KeyboardManager {
return keys.every((key) => {
if (key === 'ControlOrMeta') {
- // May be "deprecated" on types, but it is still the best option for cross-platform detection
- // https://stackoverflow.com/a/71785253/24269134
- return navigator.platform.includes('Mac')
+ // Apple platforms (incl. iPadOS with a keyboard) use Cmd/Meta as the primary modifier.
+ return platform.os.apple
? this.pressedKeys.has('Meta')
: this.pressedKeys.has('Control');
}
diff --git a/packages/x-internals/src/platform/index.ts b/packages/x-internals/src/platform/index.ts
deleted file mode 100644
index e2044e1d3a2e9..0000000000000
--- a/packages/x-internals/src/platform/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase() : 'empty';
-
-export const isFirefox = userAgent.includes('firefox');
-
-export const isJSDOM =
- typeof window !== 'undefined' && /jsdom|HappyDOM/.test(window.navigator.userAgent);
-
-export default {
- isFirefox,
- isJSDOM,
-};
diff --git a/packages/x-telemetry/src/runtime/config.test.ts b/packages/x-telemetry/src/runtime/config.test.ts
index 6831cf435307c..10310868ada77 100644
--- a/packages/x-telemetry/src/runtime/config.test.ts
+++ b/packages/x-telemetry/src/runtime/config.test.ts
@@ -2,7 +2,7 @@
import { vi } from 'vitest';
import { muiXTelemetrySettings } from '@mui/x-telemetry';
-import { isJSDOM } from '@mui/x-internals/platform';
+import { isJSDOM } from 'test/utils/skipIf';
import { getTelemetryEnvConfig } from './config';
describe.runIf(isJSDOM)('Telemetry: getTelemetryConfig', () => {
diff --git a/packages/x-telemetry/src/runtime/get-context.test.ts b/packages/x-telemetry/src/runtime/get-context.test.ts
index d7719feafa329..bd576279a1e4a 100644
--- a/packages/x-telemetry/src/runtime/get-context.test.ts
+++ b/packages/x-telemetry/src/runtime/get-context.test.ts
@@ -1,6 +1,6 @@
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createHash } from 'crypto';
-import { isJSDOM } from '@mui/x-internals/platform';
+import { isJSDOM } from 'test/utils/skipIf';
import telemetryContext from '../context';
vi.mock('../context', () => ({
diff --git a/packages/x-telemetry/src/runtime/hash-string.test.ts b/packages/x-telemetry/src/runtime/hash-string.test.ts
index fba7ba0e3126e..3c55d31b0ad69 100644
--- a/packages/x-telemetry/src/runtime/hash-string.test.ts
+++ b/packages/x-telemetry/src/runtime/hash-string.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from 'vitest';
-import { isJSDOM } from '@mui/x-internals/platform';
+import { isJSDOM } from 'test/utils/skipIf';
async function nodeHash(input: string): Promise {
const { createHash } = await import('crypto');
diff --git a/packages/x-telemetry/src/runtime/sender.test.ts b/packages/x-telemetry/src/runtime/sender.test.ts
index 7f2ca905a3350..85343f0b5141d 100644
--- a/packages/x-telemetry/src/runtime/sender.test.ts
+++ b/packages/x-telemetry/src/runtime/sender.test.ts
@@ -1,6 +1,6 @@
import { vi } from 'vitest';
import { muiXTelemetrySettings } from '@mui/x-telemetry';
-import { isJSDOM } from '@mui/x-internals/platform';
+import { isJSDOM } from 'test/utils/skipIf';
import telemetryContext from '../context';
import { getTelemetryEnvConfig } from './config';
diff --git a/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/itemPlugin.ts b/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/itemPlugin.ts
index b6a109a927e17..a461c00e58099 100644
--- a/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/itemPlugin.ts
+++ b/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/itemPlugin.ts
@@ -1,5 +1,6 @@
'use client';
import * as React from 'react';
+import { platform } from '@base-ui/utils/platform';
import { useStore } from '@mui/x-internals/store';
import { TreeViewCancellableEvent, TreeViewCancellableEventHandler } from '@mui/x-tree-view/models';
import {
@@ -12,8 +13,6 @@ import { TreeViewItemItemReorderingValidActions } from './types';
import { itemsReorderingSelectors } from './selectors';
import { RichTreeViewProStore } from '../../RichTreeViewProStore';
-export const isAndroid = () => navigator.userAgent.toLowerCase().includes('android');
-
export const useTreeViewItemsReorderingItemPlugin: TreeViewItemPlugin = ({ props }) => {
const { store } = useTreeViewContext>();
const { itemId } = props;
@@ -53,7 +52,11 @@ export const useTreeViewItemsReorderingItemPlugin: TreeViewItemPlugin = ({ props
event.dataTransfer.setDragImage(contentRefObject.current!, 0, 0);
const { types } = event.dataTransfer;
- if (isAndroid() && !types.includes('text/plain') && !types.includes('text/uri-list')) {
+ if (
+ platform.os.android &&
+ !types.includes('text/plain') &&
+ !types.includes('text/uri-list')
+ ) {
event.dataTransfer.setData('text/plain', 'android-fallback');
}
diff --git a/packages/x-virtualizer/package.json b/packages/x-virtualizer/package.json
index 8effb517e0544..f6c6b5667ca76 100644
--- a/packages/x-virtualizer/package.json
+++ b/packages/x-virtualizer/package.json
@@ -34,6 +34,7 @@
},
"dependencies": {
"@babel/runtime": "catalog:",
+ "@base-ui/utils": "catalog:",
"@mui/utils": "catalog:",
"@mui/x-internals": "workspace:^"
},
diff --git a/packages/x-virtualizer/src/features/virtualization/layout.ts b/packages/x-virtualizer/src/features/virtualization/layout.ts
index 89d083e5ba0dc..028900bc3bd7d 100644
--- a/packages/x-virtualizer/src/features/virtualization/layout.ts
+++ b/packages/x-virtualizer/src/features/virtualization/layout.ts
@@ -2,7 +2,7 @@
import * as React from 'react';
import useForkRef from '@mui/utils/useForkRef';
import useEventCallback from '@mui/utils/useEventCallback';
-import * as platform from '@mui/x-internals/platform';
+import { platform } from '@base-ui/utils/platform';
import { Store, createSelectorMemoized } from '@mui/x-internals/store';
import { Dimensions } from '../../features/dimensions';
import { Virtualization, type VirtualizationLayoutParams } from './virtualization';
@@ -106,7 +106,7 @@ export class LayoutDataGrid extends Layout {
role: 'presentation',
// `tabIndex` shouldn't be used along role=presentation, but it fixes a Firefox bug
// https://github.com/mui/mui-x/pull/13891#discussion_r1683416024
- tabIndex: platform.isFirefox ? -1 : undefined,
+ tabIndex: platform.engine.gecko ? -1 : undefined,
}),
),
@@ -269,7 +269,7 @@ export class LayoutList extends Layout {
role: 'presentation',
// `tabIndex` shouldn't be used along role=presentation, but it fixes a Firefox bug
// https://github.com/mui/mui-x/pull/13891#discussion_r1683416024
- tabIndex: platform.isFirefox ? -1 : undefined,
+ tabIndex: platform.engine.gecko ? -1 : undefined,
}),
),
diff --git a/packages/x-virtualizer/src/features/virtualization/virtualization.ts b/packages/x-virtualizer/src/features/virtualization/virtualization.ts
index caf22288f88ea..61db9445231fd 100644
--- a/packages/x-virtualizer/src/features/virtualization/virtualization.ts
+++ b/packages/x-virtualizer/src/features/virtualization/virtualization.ts
@@ -6,7 +6,7 @@ import useTimeout from '@mui/utils/useTimeout';
import useEventCallback from '@mui/utils/useEventCallback';
import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
import type { integer } from '@mui/x-internals/types';
-import * as platform from '@mui/x-internals/platform';
+import { platform } from '@base-ui/utils/platform';
import { useRunOnce } from '@mui/x-internals/useRunOnce';
import { createSelector, useStore, useStoreEffect, Store } from '@mui/x-internals/store';
import useRefCallback from '../../utils/useRefCallback';
@@ -155,9 +155,9 @@ function initializeState(params: ParamsWithDefaults) {
const state: Virtualization.State = {
virtualization: {
- enabled: !platform.isJSDOM,
- enabledForRows: !platform.isJSDOM,
- enabledForColumns: !platform.isJSDOM,
+ enabled: !platform.env.jsdom,
+ enabledForRows: !platform.env.jsdom,
+ enabledForColumns: !platform.env.jsdom,
renderContext,
props: (params.layout.constructor as typeof Layout).elements.reduce(
(acc, key) => (acc[key as string], acc),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cf3732dbaf289..08ac6f92d5d95 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -34,8 +34,8 @@ catalogs:
specifier: ^1.5.0
version: 1.5.0
'@base-ui/utils':
- specifier: ^0.2.9
- version: 0.2.9
+ specifier: ^0.3.0
+ version: 0.3.0
'@date-fns/tz':
specifier: ^1.5.0
version: 1.5.0
@@ -254,6 +254,9 @@ importers:
'@babel/types':
specifier: 7.29.7
version: 7.29.7
+ '@base-ui/utils':
+ specifier: 'catalog:'
+ version: 0.3.0(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@emotion/cache':
specifier: 'catalog:'
version: 11.14.0
@@ -1172,6 +1175,9 @@ importers:
'@babel/runtime':
specifier: 'catalog:'
version: 7.29.7
+ '@base-ui/utils':
+ specifier: 'catalog:'
+ version: 0.3.0(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@emotion/react':
specifier: ^11.9.0
version: 11.14.0(@types/react@19.2.15)(react@19.2.6)
@@ -1287,6 +1293,9 @@ importers:
'@babel/runtime':
specifier: 'catalog:'
version: 7.29.7
+ '@base-ui/utils':
+ specifier: 'catalog:'
+ version: 0.3.0(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@emotion/react':
specifier: ^11.9.0
version: 11.14.0(@types/react@19.2.15)(react@19.2.6)
@@ -1580,6 +1589,9 @@ importers:
'@babel/runtime':
specifier: 'catalog:'
version: 7.29.7
+ '@base-ui/utils':
+ specifier: 'catalog:'
+ version: 0.3.0(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
publishDirectory: build
packages/x-internals:
@@ -1653,7 +1665,7 @@ importers:
version: 1.5.0(@date-fns/tz@1.5.0)(@types/react@19.2.15)(date-fns@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@base-ui/utils':
specifier: 'catalog:'
- version: 0.2.9(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 0.3.0(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@emotion/react':
specifier: ^11.9.0
version: 11.14.0(@types/react@19.2.15)(react@19.2.6)
@@ -1724,7 +1736,7 @@ importers:
version: 1.5.0(@date-fns/tz@1.5.0)(@types/react@19.2.15)(date-fns@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@base-ui/utils':
specifier: 'catalog:'
- version: 0.2.9(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 0.3.0(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@date-fns/tz':
specifier: 'catalog:'
version: 1.5.0
@@ -1780,7 +1792,7 @@ importers:
version: 1.5.0(@date-fns/tz@1.5.0)(@types/react@19.2.15)(date-fns@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@base-ui/utils':
specifier: 'catalog:'
- version: 0.2.9(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 0.3.0(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@mui/x-internals':
specifier: workspace:^
version: link:../x-internals/build
@@ -1827,7 +1839,7 @@ importers:
version: 1.5.0(@date-fns/tz@1.5.0)(@types/react@19.2.15)(date-fns@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@base-ui/utils':
specifier: 'catalog:'
- version: 0.2.9(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 0.3.0(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@emotion/react':
specifier: ^11.9.0
version: 11.14.0(@types/react@19.2.15)(react@19.2.6)
@@ -1927,7 +1939,7 @@ importers:
version: 7.29.7
'@base-ui/utils':
specifier: 'catalog:'
- version: 0.2.9(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 0.3.0(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@emotion/react':
specifier: ^11.9.0
version: 11.14.0(@types/react@19.2.15)(react@19.2.6)
@@ -1983,7 +1995,7 @@ importers:
version: 7.29.7
'@base-ui/utils':
specifier: 'catalog:'
- version: 0.2.9(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 0.3.0(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@emotion/react':
specifier: ^11.9.0
version: 11.14.0(@types/react@19.2.15)(react@19.2.6)
@@ -2055,6 +2067,9 @@ importers:
'@babel/runtime':
specifier: 'catalog:'
version: 7.29.7
+ '@base-ui/utils':
+ specifier: 'catalog:'
+ version: 0.3.0(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@mui/utils':
specifier: 'catalog:'
version: 9.0.1(@types/react@19.2.15)(react@19.2.6)
@@ -2086,7 +2101,7 @@ importers:
version: 7.29.7
'@base-ui/utils':
specifier: 'catalog:'
- version: 0.2.9(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 0.3.0(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@emotion/cache':
specifier: 'catalog:'
version: 11.14.0
@@ -3220,6 +3235,16 @@ packages:
'@types/react':
optional: true
+ '@base-ui/utils@0.3.0':
+ resolution: {integrity: sha512-IbZYmvB99kna6u75q8SPCV5LU+bxRzuBHIsimMf1S2Iy10K7qUuZrS2FP3RFZ17ROzt0YXxxJd70QRcnAKWpcw==}
+ peerDependencies:
+ '@types/react': ^17 || ^18 || ^19
+ react: ^17 || ^18 || ^19
+ react-dom: ^17 || ^18 || ^19
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
@@ -12407,6 +12432,17 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.15
+ '@base-ui/utils@0.3.0(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ dependencies:
+ '@babel/runtime': 7.29.7
+ '@floating-ui/utils': 0.2.11
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+ reselect: 5.2.0
+ use-sync-external-store: 1.6.0(react@19.2.6)
+ optionalDependencies:
+ '@types/react': 19.2.15
+
'@bcoe/v8-coverage@1.0.2': {}
'@blazediff/core@1.9.1': {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index b68bd57136fac..48b80078747d2 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -15,7 +15,7 @@ catalog:
'@babel/runtime': ^7.29.7
'@babel/traverse': ^7.29.7
'@base-ui/react': ^1.5.0
- '@base-ui/utils': ^0.2.9
+ '@base-ui/utils': ^0.3.0
'@date-fns/tz': ^1.5.0
'@emotion/cache': ^11.14.0
'@emotion/react': ^11.14.0
diff --git a/test/utils/skipIf.ts b/test/utils/skipIf.ts
index dea5c0218ca30..94c2a8687b7f8 100644
--- a/test/utils/skipIf.ts
+++ b/test/utils/skipIf.ts
@@ -1,4 +1,6 @@
-export const isJSDOM = /jsdom/.test(window.navigator.userAgent);
-export const isOSX = /macintosh/i.test(window.navigator.userAgent);
+import { platform } from '@base-ui/utils/platform';
+
+export const isJSDOM = platform.env.jsdom;
+export const isOSX = platform.os.mac;
export const hasTouchSupport =
typeof window.Touch !== 'undefined' && typeof window.TouchEvent !== 'undefined';