From 8edb1f3476dd21760c3bf87b78a24415755dc1c2 Mon Sep 17 00:00:00 2001 From: Kushagra Singh Negi Date: Thu, 30 Apr 2026 16:13:48 +0530 Subject: [PATCH 1/5] feat(FloatingFocusManager): apply inert to background elements and preserve focus guards --- .../components/FloatingFocusManager.test.tsx | 4 ++-- .../components/FloatingFocusManager.tsx | 6 ++++++ .../floating-ui-react/utils/markOthers.test.ts | 16 ++++++++++++++++ .../src/floating-ui-react/utils/markOthers.ts | 4 +++- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/react/src/floating-ui-react/components/FloatingFocusManager.test.tsx b/packages/react/src/floating-ui-react/components/FloatingFocusManager.test.tsx index 9708e2e0249..d923e7ec5ad 100644 --- a/packages/react/src/floating-ui-react/components/FloatingFocusManager.test.tsx +++ b/packages/react/src/floating-ui-react/components/FloatingFocusManager.test.tsx @@ -1921,7 +1921,7 @@ describe('FloatingFocusManager', () => { expect(screen.getByTestId('reference')).not.toHaveFocus(); }); - test('uses aria-hidden instead of inert on outside nodes if opened with hover and modal=true', async () => { + test('uses both aria-hidden and inert on outside nodes if opened with hover and modal=true', async () => { function App() { const [isOpen, setIsOpen] = React.useState(false); @@ -1952,7 +1952,7 @@ describe('FloatingFocusManager', () => { await userEvent.hover(screen.getByTestId('reference')); await flushMicrotasks(); - expect(screen.getByText('outside')).not.toHaveAttribute('inert'); + expect(screen.getByText('outside')).toHaveAttribute('inert'); expect(screen.getByText('outside')).toHaveAttribute('aria-hidden', 'true'); }); diff --git a/packages/react/src/floating-ui-react/components/FloatingFocusManager.tsx b/packages/react/src/floating-ui-react/components/FloatingFocusManager.tsx index 82ea513270c..0c525eb50b9 100644 --- a/packages/react/src/floating-ui-react/components/FloatingFocusManager.tsx +++ b/packages/react/src/floating-ui-react/components/FloatingFocusManager.tsx @@ -629,12 +629,18 @@ export function FloatingFocusManager(props: FloatingFocusManagerProps): React.JS mark: false, }); + const inertCleanup = markOthers(insideElements, { + inert: modal, + mark: false, + }); + const markerInsideElements = [floating, ...portalNodes].filter((x): x is Element => x != null); const markerCleanup = markOthers(markerInsideElements); return () => { markerCleanup(); ariaHiddenCleanup(); + inertCleanup(); }; }, [ open, diff --git a/packages/react/src/floating-ui-react/utils/markOthers.test.ts b/packages/react/src/floating-ui-react/utils/markOthers.test.ts index 1997f1862ea..9c7b2955ef9 100644 --- a/packages/react/src/floating-ui-react/utils/markOthers.test.ts +++ b/packages/react/src/floating-ui-react/utils/markOthers.test.ts @@ -400,3 +400,19 @@ test('uses shadow root host as avoid element when parent chain includes anchor', expect(outside.getAttribute('aria-hidden')).toBe(null); }); + +test('does not apply inert to focus guards', () => { + const target = document.createElement('div'); + const guard = document.createElement('span'); + guard.setAttribute('data-base-ui-focus-guard', ''); + const outside = document.createElement('div'); + + document.body.append(target, guard, outside); + + const cleanup = markOthers([target], { inert: true }); + + expect(outside.hasAttribute('inert')).toBe(true); + expect(guard.hasAttribute('inert')).toBe(false); + + cleanup(); +}); diff --git a/packages/react/src/floating-ui-react/utils/markOthers.ts b/packages/react/src/floating-ui-react/utils/markOthers.ts index 13fc6cd89ae..eac7d99ddd9 100644 --- a/packages/react/src/floating-ui-react/utils/markOthers.ts +++ b/packages/react/src/floating-ui-react/utils/markOthers.ts @@ -2,6 +2,7 @@ // https://github.com/theKashey/aria-hidden/blob/9220c8f4a4fd35f63bee5510a9f41a37264382d4/src/index.ts import { getNodeName, isShadowRoot } from '@floating-ui/utils/dom'; import { ownerDocument } from '@base-ui/utils/owner'; +import { createAttribute } from './createAttribute'; type Undo = () => void; @@ -18,6 +19,7 @@ const counters = { }; const markerName = 'data-base-ui-inert'; +const focusGuardAttribute = createAttribute('focus-guard'); type ControlAttribute = keyof typeof counters; const uncontrolledElementsSets: Record> = { @@ -86,7 +88,7 @@ const collectOutsideElements = ( } Array.from(parent.children).forEach((node: Element) => { - if (getNodeName(node) === 'script') { + if (getNodeName(node) === 'script' || node.hasAttribute(focusGuardAttribute)) { return; } From a3d839eb713685f1196630c73569d929d864b555 Mon Sep 17 00:00:00 2001 From: Kushagra Singh Negi Date: Thu, 30 Apr 2026 17:23:32 +0530 Subject: [PATCH 2/5] fix(markOthers): only preserve active focus guards and mark background ones as inert --- .../utils/markOthers.test.ts | 27 ++++++++++++------- .../src/floating-ui-react/utils/markOthers.ts | 4 +-- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/react/src/floating-ui-react/utils/markOthers.test.ts b/packages/react/src/floating-ui-react/utils/markOthers.test.ts index 9c7b2955ef9..20910f7e7f2 100644 --- a/packages/react/src/floating-ui-react/utils/markOthers.test.ts +++ b/packages/react/src/floating-ui-react/utils/markOthers.test.ts @@ -401,18 +401,25 @@ test('uses shadow root host as avoid element when parent chain includes anchor', expect(outside.getAttribute('aria-hidden')).toBe(null); }); -test('does not apply inert to focus guards', () => { - const target = document.createElement('div'); - const guard = document.createElement('span'); - guard.setAttribute('data-base-ui-focus-guard', ''); - const outside = document.createElement('div'); - document.body.append(target, guard, outside); +test('marks background focus guards as inert but preserves active ones', () => { + const root = document.createElement('div'); + const inside = document.createElement('div'); + const focusGuardOutside = document.createElement('span'); + const focusGuardInside = document.createElement('span'); - const cleanup = markOthers([target], { inert: true }); + focusGuardOutside.setAttribute('data-base-ui-focus-guard', ''); + focusGuardInside.setAttribute('data-base-ui-focus-guard', ''); - expect(outside.hasAttribute('inert')).toBe(true); - expect(guard.hasAttribute('inert')).toBe(false); + root.appendChild(inside); + root.appendChild(focusGuardOutside); + inside.appendChild(focusGuardInside); + document.body.appendChild(root); - cleanup(); + markOthers([inside, focusGuardInside], { inert: true }); + + expect(focusGuardOutside).toHaveAttribute('inert'); + expect(focusGuardInside).not.toHaveAttribute('inert'); + + document.body.removeChild(root); }); diff --git a/packages/react/src/floating-ui-react/utils/markOthers.ts b/packages/react/src/floating-ui-react/utils/markOthers.ts index eac7d99ddd9..13fc6cd89ae 100644 --- a/packages/react/src/floating-ui-react/utils/markOthers.ts +++ b/packages/react/src/floating-ui-react/utils/markOthers.ts @@ -2,7 +2,6 @@ // https://github.com/theKashey/aria-hidden/blob/9220c8f4a4fd35f63bee5510a9f41a37264382d4/src/index.ts import { getNodeName, isShadowRoot } from '@floating-ui/utils/dom'; import { ownerDocument } from '@base-ui/utils/owner'; -import { createAttribute } from './createAttribute'; type Undo = () => void; @@ -19,7 +18,6 @@ const counters = { }; const markerName = 'data-base-ui-inert'; -const focusGuardAttribute = createAttribute('focus-guard'); type ControlAttribute = keyof typeof counters; const uncontrolledElementsSets: Record> = { @@ -88,7 +86,7 @@ const collectOutsideElements = ( } Array.from(parent.children).forEach((node: Element) => { - if (getNodeName(node) === 'script' || node.hasAttribute(focusGuardAttribute)) { + if (getNodeName(node) === 'script') { return; } From aec774e1a46e4a6503fe2a3a77685c92a344a0aa Mon Sep 17 00:00:00 2001 From: Kushagra Singh Negi Date: Thu, 30 Apr 2026 17:28:54 +0530 Subject: [PATCH 3/5] ci: provide labels for required-labels check --- .github/workflows/check-if-pr-has-label.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check-if-pr-has-label.yml b/.github/workflows/check-if-pr-has-label.yml index 851fe469c0d..e63121b6edc 100644 --- a/.github/workflows/check-if-pr-has-label.yml +++ b/.github/workflows/check-if-pr-has-label.yml @@ -17,4 +17,10 @@ jobs: with: mode: minimum count: 1 - labels: '' + labels: | + bug + feature + documentation + maintenance + question + ready for review From 7316aab26f4cc60419b171374456a1ad60b86170 Mon Sep 17 00:00:00 2001 From: Kushagra Singh Negi Date: Thu, 30 Apr 2026 17:48:14 +0530 Subject: [PATCH 4/5] fix(FloatingFocusManager): prevent applying inert to untrapped typeable comboboxes to fix tabbing out regression --- .../src/floating-ui-react/components/FloatingFocusManager.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/floating-ui-react/components/FloatingFocusManager.tsx b/packages/react/src/floating-ui-react/components/FloatingFocusManager.tsx index 0c525eb50b9..415d528589e 100644 --- a/packages/react/src/floating-ui-react/components/FloatingFocusManager.tsx +++ b/packages/react/src/floating-ui-react/components/FloatingFocusManager.tsx @@ -630,7 +630,7 @@ export function FloatingFocusManager(props: FloatingFocusManagerProps): React.JS }); const inertCleanup = markOthers(insideElements, { - inert: modal, + inert: modal && !isUntrappedTypeableCombobox, mark: false, }); From 1a80a7f32e1f6f7c9cbe56d5ba4132e23fe0a5b7 Mon Sep 17 00:00:00 2001 From: Kushagra Singh Negi Date: Thu, 30 Apr 2026 20:11:26 +0530 Subject: [PATCH 5/5] revert: undo incorrect CI workflow modification --- .github/workflows/check-if-pr-has-label.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/check-if-pr-has-label.yml b/.github/workflows/check-if-pr-has-label.yml index e63121b6edc..851fe469c0d 100644 --- a/.github/workflows/check-if-pr-has-label.yml +++ b/.github/workflows/check-if-pr-has-label.yml @@ -17,10 +17,4 @@ jobs: with: mode: minimum count: 1 - labels: | - bug - feature - documentation - maintenance - question - ready for review + labels: ''