diff --git a/cypress/e2e/useMultipleCombobox.cy.js b/cypress/e2e/useMultipleCombobox.cy.js
index 20fec955..dfc05dea 100644
--- a/cypress/e2e/useMultipleCombobox.cy.js
+++ b/cypress/e2e/useMultipleCombobox.cy.js
@@ -6,11 +6,73 @@ describe('useMultipleCombobox', () => {
it('can select multiple items', () => {
cy.findByRole('button', {name: 'toggle menu'}).click()
cy.findByRole('option', {name: 'Green'}).click()
+ cy.findByRole('button', {name: 'toggle menu'}).should(
+ 'have.attr',
+ 'aria-expanded',
+ 'true',
+ )
cy.findByRole('option', {name: 'Gray'}).click()
cy.findByRole('button', {name: 'toggle menu'}).click()
+ cy.findByRole('button', {name: 'toggle menu'}).should(
+ 'have.attr',
+ 'aria-expanded',
+ 'false',
+ )
cy.findByText('Black').should('be.visible')
cy.findByText('Red').should('be.visible')
cy.findByText('Green').should('be.visible')
cy.findByText('Gray').should('be.visible')
})
})
+
+describe('useMultipleCombobox in shadow DOM', () => {
+ beforeEach(() => {
+ cy.visit('/shadow-dom/useMultipleCombobox')
+ })
+
+ it('can select multiple items within a shadow DOM', () => {
+ cy.get('[data-testid="shadow-root"]')
+ .shadow()
+ .within(() => {
+ cy.findByRole('button', {name: 'toggle menu'}).click()
+ cy.findByRole('option', {name: 'Green'}).click()
+ cy.findByRole('button', {name: 'toggle menu'}).should(
+ 'have.attr',
+ 'aria-expanded',
+ 'true',
+ )
+ cy.findByRole('option', {name: 'Gray'}).click()
+ cy.findByRole('button', {name: 'toggle menu'}).click()
+ cy.findByRole('button', {name: 'toggle menu'}).should(
+ 'have.attr',
+ 'aria-expanded',
+ 'false',
+ )
+ cy.findByText('Black').should('be.visible')
+ cy.findByText('Red').should('be.visible')
+ cy.findByText('Green').should('be.visible')
+ cy.findByText('Gray').should('be.visible')
+ })
+ })
+
+ it('can filter the items', () => {
+ cy.get('[data-testid="shadow-root"]')
+ .shadow()
+ .within(() => {
+ cy.findByRole('button', {name: 'toggle menu'}).click()
+ cy.findAllByRole('option').should('have.length', 12)
+ cy.findByRole('combobox').type('g')
+ cy.findByRole('button', {name: 'toggle menu'}).should(
+ 'have.attr',
+ 'aria-expanded',
+ 'true',
+ )
+ cy.findAllByRole('option').should('have.length', 2)
+ cy.findByText('Green').should('be.visible')
+ cy.findByText('Gray').should('be.visible')
+
+ cy.findByRole('combobox').type('{backspace}')
+ cy.findAllByRole('option').should('have.length', 12)
+ })
+ })
+})
diff --git a/docusaurus/pages/shadow-dom/useCombobox.js b/docusaurus/pages/shadow-dom/useCombobox.js
new file mode 100644
index 00000000..424959b5
--- /dev/null
+++ b/docusaurus/pages/shadow-dom/useCombobox.js
@@ -0,0 +1,21 @@
+import * as React from 'react'
+
+import DropdownCombobox from '../useCombobox'
+import {ReactShadowRoot} from '../../../test/react-shadow'
+
+const style = {
+ padding: '20px',
+}
+
+export default function MultipleComboboxShadow() {
+ return (
+
+ )
+}
diff --git a/docusaurus/pages/shadow-dom/useMultipleCombobox.js b/docusaurus/pages/shadow-dom/useMultipleCombobox.js
new file mode 100644
index 00000000..f7b2359c
--- /dev/null
+++ b/docusaurus/pages/shadow-dom/useMultipleCombobox.js
@@ -0,0 +1,27 @@
+import * as React from 'react'
+
+import DropdownMultipleCombobox from '../useMultipleCombobox'
+import {ReactShadowRoot} from '../../../test/react-shadow'
+
+const style = {
+ padding: '20px',
+}
+
+export default function MultipleComboboxShadow() {
+ return (
+
+
+
+
+
Shadow DOM
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/docusaurus/pages/shadow-dom/useSelect.js b/docusaurus/pages/shadow-dom/useSelect.js
new file mode 100644
index 00000000..d7c0d313
--- /dev/null
+++ b/docusaurus/pages/shadow-dom/useSelect.js
@@ -0,0 +1,12 @@
+import * as React from 'react'
+
+import DropdownSelect from '../useSelect'
+import {ReactShadowRoot} from '../../../test/react-shadow'
+
+export default function DropdownSelectShadow() {
+ return (
+
+
+
+ )
+}
diff --git a/src/__tests__/.eslintrc b/src/__tests__/.eslintrc
index 4e44adf7..ec672d99 100644
--- a/src/__tests__/.eslintrc
+++ b/src/__tests__/.eslintrc
@@ -5,6 +5,7 @@
"react/prop-types": "off",
"react/display-name": "off",
"react/no-deprecated": "off",
- "no-console": "off"
- }
+ "no-console": "off",
+ "no-unused-vars": "off",
+ },
}
diff --git a/src/__tests__/downshift.shadow-root.js b/src/__tests__/downshift.shadow-root.js
new file mode 100644
index 00000000..e7661041
--- /dev/null
+++ b/src/__tests__/downshift.shadow-root.js
@@ -0,0 +1,282 @@
+import * as React from 'react'
+import {render, buildQueries, queryAllByRole} from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import Downshift from '../'
+import DropdownSelect from '../../test/useSelect.test'
+import DropdownCombobox, {colors} from '../../test/useCombobox.test'
+import DropdownMultipleSelect from '../../test/useMultipleSelect.test'
+import ComboBox from '../../test/downshift.test'
+import {ReactShadowRoot} from '../../test/react-shadow'
+
+function _queryAllByRoleDeep(container, ...rest) {
+ // eslint-disable-next-line testing-library/prefer-screen-queries
+ const result = queryAllByRole(container, ...rest) // replace here with different queryAll* variants.
+ for (const element of container.querySelectorAll('*')) {
+ if (element.shadowRoot) {
+ result.push(..._queryAllByRoleDeep(element.shadowRoot, ...rest))
+ }
+ }
+
+ return result
+}
+
+const [
+ _queryByRoleDeep,
+ _getAllByRoleDeep,
+ _getByRoleDeep,
+ _findAllByRoleDeep,
+ _findByRoleDeep,
+] = buildQueries(
+ _queryAllByRoleDeep,
+ (_, role) => `Found multiple elements with the role ${role}`,
+ (_, role) => `Unable to find an element with the role ${role}`,
+)
+
+const getAllByRoleDeep = _getAllByRoleDeep.bind(null, document.body)
+const getByRoleDeep = _getByRoleDeep.bind(null, document.body)
+
+const Wrapper = ({children}) => {
+ return {children}
+}
+
+test('Downshift renders with a shadow root', () => {
+ const {container} = render(, {wrapper: Wrapper})
+
+ expect(container.shadowRoot).toBeDefined()
+})
+
+test('DropdownSelect renders with a shadow root', () => {
+ const {container} = render(, {wrapper: Wrapper})
+
+ expect(container.shadowRoot).toBeDefined()
+})
+
+test('DropdownCombobox renders with a shadow root', () => {
+ const {container} = render(, {wrapper: Wrapper})
+
+ expect(container.shadowRoot).toBeDefined()
+})
+
+test('DropdownMultipleSelect renders with a shadow root', () => {
+ const {container} = render(, {wrapper: Wrapper})
+
+ expect(container.shadowRoot).toBeDefined()
+})
+
+test('DropdownSelect works correctly in shadow DOM', async () => {
+ const user = userEvent.setup()
+ const {container} = render(, {wrapper: Wrapper})
+
+ // Verify shadow root exists
+ expect(container.shadowRoot).toBeDefined()
+
+ // Get elements within the shadow root
+ const toggleButton = getByRoleDeep('combobox')
+ const menu = getByRoleDeep('listbox')
+ expect(toggleButton).toBeInTheDocument()
+ expect(menu).toBeInTheDocument()
+
+ // Initially menu should be closed
+ expect(toggleButton).toHaveAttribute('aria-expanded', 'false')
+
+ // Open the dropdown
+ await user.click(toggleButton)
+
+ // Menu should now be open
+ expect(toggleButton).toHaveAttribute('aria-expanded', 'true')
+
+ // Select an item
+ const blackOption = getByRoleDeep('option', {name: 'Black'})
+ await user.click(blackOption)
+
+ // Menu should close and selected item should appear in button
+ expect(toggleButton).toHaveAttribute('aria-expanded', 'false')
+ expect(toggleButton).toHaveTextContent('Black')
+
+ // Open it again
+ await user.click(toggleButton)
+ expect(toggleButton).toHaveAttribute('aria-expanded', 'true')
+
+ // Click outside (this tests our targetWithinDownshift with composedPath)
+ await user.click(document.body)
+
+ // Menu should close
+ expect(toggleButton).toHaveAttribute('aria-expanded', 'false')
+})
+
+test('DropdownCombobox works correctly in shadow DOM', async () => {
+ const user = userEvent.setup()
+ const {container} = render(, {wrapper: Wrapper})
+
+ // Verify shadow root exists
+ expect(container.shadowRoot).toBeDefined()
+
+ // Get elements within the shadow root
+ const input = getByRoleDeep('combobox')
+ const toggleButton = getByRoleDeep('button', {name: 'toggle menu'})
+ const clearButton = getByRoleDeep('button', {name: 'clear'})
+ const menu = getByRoleDeep('listbox')
+
+ expect(input).toBeInTheDocument()
+ expect(toggleButton).toBeInTheDocument()
+ expect(clearButton).toBeInTheDocument()
+ expect(menu).toBeInTheDocument()
+
+ // Initially menu should be closed
+ expect(input).toHaveAttribute('aria-expanded', 'false')
+ expect(toggleButton).toHaveAttribute('aria-expanded', 'false')
+
+ // Open the dropdown
+ await user.click(toggleButton)
+
+ // Menu should now be open
+ expect(input).toHaveAttribute('aria-expanded', 'true')
+
+ // All colors should initially be visible
+ const items = getAllByRoleDeep('option')
+ expect(items).toHaveLength(colors.length)
+
+ // Select an item
+ await user.click(items[0])
+
+ // Menu should close and input should have selected value
+ expect(input).toHaveAttribute('aria-expanded', 'false')
+ expect(toggleButton).toHaveAttribute('aria-expanded', 'false')
+ expect(input).toHaveValue('Black')
+
+ // Clear the selection
+ await user.click(clearButton)
+
+ // Input should be empty
+ expect(input).toHaveValue('')
+
+ // Open it again
+ await user.click(toggleButton)
+ expect(input).toHaveAttribute('aria-expanded', 'true')
+
+ // Click outside to close
+ await user.click(document.body)
+
+ // Menu should close
+ expect(input).toHaveAttribute('aria-expanded', 'false')
+})
+
+test('Downshift button blur correctly handles focus moving to external shadow DOM', async () => {
+ const user = userEvent.setup()
+ const {container} = render(, {wrapper: Wrapper})
+
+ // Verify Downshift's own shadow root exists
+ expect(container.shadowRoot).toBeDefined()
+
+ const comboboxRoot = getByRoleDeep('combobox')
+ const toggleButton = container.shadowRoot.querySelector(
+ '[data-testid="combobox-toggle-button"]',
+ )
+
+ // Open the dropdown
+ await user.click(toggleButton)
+ expect(comboboxRoot).toHaveAttribute('aria-expanded', 'true')
+
+ // Click the element inside the external shadow DOM
+ // This should focus externalFocusableButton and blur toggleButton (or the root/input depending on what had focus)
+ const externalHost = document.createElement('div')
+ document.body.appendChild(externalHost)
+ const shadow = externalHost.attachShadow({mode: 'open'})
+ const externalFocusableButton = document.createElement('button')
+ shadow.appendChild(externalFocusableButton)
+ await user.click(externalFocusableButton)
+
+ // Assert that the menu closes due to blur
+ // Downshift's blur handlers use setTimeout, so wait for the next macrotask
+ await new Promise(resolve => setTimeout(resolve, 0))
+ expect(comboboxRoot).toHaveAttribute('aria-expanded', 'false')
+
+ // Cleanup
+ document.body.removeChild(externalHost)
+})
+
+test('Downshift input blur correctly handles focus moving to external shadow DOM', async () => {
+ const user = userEvent.setup()
+ const {container} = render(, {wrapper: Wrapper})
+
+ // Verify Downshift's own shadow root exists
+ expect(container.shadowRoot).toBeDefined()
+
+ const comboboxRoot = getByRoleDeep('combobox')
+ const inputField = container.shadowRoot.querySelector(
+ '[data-testid="combobox-input"]',
+ )
+ const downshiftToggleButton = container.shadowRoot.querySelector(
+ '[data-testid="combobox-toggle-button"]',
+ )
+
+ // Create an external element with its own shadow DOM
+ const externalHost = document.createElement('div')
+ document.body.appendChild(externalHost)
+ const shadow = externalHost.attachShadow({mode: 'open'})
+ const externalFocusableButton = document.createElement('button')
+ shadow.appendChild(externalFocusableButton)
+
+ // Open the dropdown by clicking the toggle button
+ await user.click(downshiftToggleButton)
+ expect(comboboxRoot).toHaveAttribute('aria-expanded', 'true')
+
+ // Ensure the input itself is focused before it blurs
+ inputField.focus()
+ await user.type(inputField, 'b')
+ await user.keyboard('{Tab}')
+ // Click the element inside the external shadow DOM
+ // This should focus externalFocusableButton and blur the input
+ await user.click(externalFocusableButton)
+
+ // Assert that the menu closes due to blur
+ // Downshift's blur handlers use setTimeout, so wait for the next macrotask
+ await new Promise(resolve => setTimeout(resolve, 0))
+ expect(comboboxRoot).toHaveAttribute('aria-expanded', 'false')
+
+ // Cleanup
+ document.body.removeChild(externalHost)
+})
+
+test('useCombobox input blur correctly handles focus moving to external shadow DOM', async () => {
+ const user = userEvent.setup()
+ const {container} = render(, {wrapper: Wrapper})
+
+ // Verify DropdownCombobox's own shadow root exists via the Wrapper
+ expect(container.shadowRoot).toBeDefined()
+
+ const input = container.shadowRoot.querySelector(
+ '[data-testid="combobox-input"]',
+ )
+ const toggleButton = container.shadowRoot.querySelector(
+ '[data-testid="combobox-toggle-button"]',
+ )
+
+ // Create an external element with its own shadow DOM
+ const externalHost = document.createElement('div')
+ document.body.appendChild(externalHost)
+ const shadow = externalHost.attachShadow({mode: 'open'})
+ const externalFocusableButton = document.createElement('button')
+ shadow.appendChild(externalFocusableButton)
+
+ // Open the dropdown by clicking the toggle button
+ await user.click(toggleButton)
+ expect(input).toHaveAttribute('aria-expanded', 'true')
+
+ // Ensure the input itself is focused before it blurs
+ input.focus()
+ await user.type(input, 'b')
+ await user.keyboard('{Tab}')
+
+ // Click the element inside the external shadow DOM
+ // This should focus externalFocusableButton and blur the input in DropdownCombobox
+ await user.click(externalFocusableButton)
+
+ // Assert that the menu closes due to blur
+ // useCombobox's blur handler uses setTimeout, so wait for the next macrotask
+ await new Promise(resolve => setTimeout(resolve, 0))
+ expect(input).toHaveAttribute('aria-expanded', 'false')
+
+ // Cleanup
+ document.body.removeChild(externalHost)
+})
diff --git a/src/__tests__/utils.target-within-downshift.js b/src/__tests__/utils.target-within-downshift.js
new file mode 100644
index 00000000..4621c494
--- /dev/null
+++ b/src/__tests__/utils.target-within-downshift.js
@@ -0,0 +1,59 @@
+import {targetWithinDownshift} from '../utils'
+
+test('returns false if environment is not defined', () => {
+ expect(targetWithinDownshift(null, [], null)).toBe(false)
+})
+
+test('returns false if the target is not within the downshift', () => {
+ const downshift = document.createElement('div')
+ downshift.innerHTML = ''
+ expect(targetWithinDownshift(null, [downshift], window)).toBe(false)
+})
+
+test('correctly identifies active elements nested in shadow DOM', () => {
+ // Create a host element for the shadow DOM
+ const hostElement = document.createElement('div')
+ document.body.appendChild(hostElement)
+
+ // Attach a shadow root to the host element
+ const shadowRoot = hostElement.attachShadow({mode: 'open'})
+
+ // Create an element inside the shadow root
+ const innerElement = document.createElement('button')
+ innerElement.setAttribute('data-testid', 'inner-button')
+ shadowRoot.appendChild(innerElement)
+
+ // Create another nested level with another shadow root
+ const nestedHost = document.createElement('div')
+ shadowRoot.appendChild(nestedHost)
+
+ const nestedShadowRoot = nestedHost.attachShadow({mode: 'open'})
+
+ const deepActiveElement = document.createElement('button')
+
+ // Create some downshift elements
+ const downshiftElement = document.createElement('div')
+ nestedShadowRoot.appendChild(downshiftElement)
+ downshiftElement.appendChild(deepActiveElement)
+
+ // Focus the deep element to make it the active element
+ deepActiveElement.focus()
+
+ expect(hostElement.shadowRoot).toBe(shadowRoot)
+ expect(nestedHost.shadowRoot).toBe(nestedShadowRoot)
+ expect(nestedShadowRoot.activeElement).toBe(deepActiveElement)
+
+ // Test the function with the active element in the shadow DOM
+ expect(
+ targetWithinDownshift(deepActiveElement, [downshiftElement], window, true),
+ ).toBe(true)
+
+ // Test with a non-related element
+ const unrelatedElement = document.createElement('div')
+ expect(
+ targetWithinDownshift(unrelatedElement, [downshiftElement], window, true),
+ ).toBe(true) // Should still be true because of activeElement check
+
+ // Cleanup
+ document.body.removeChild(hostElement)
+})
diff --git a/src/downshift.js b/src/downshift.js
index 49bdda0f..af7cb9e3 100644
--- a/src/downshift.js
+++ b/src/downshift.js
@@ -764,7 +764,16 @@ class Downshift extends Component {
return
}
- const {activeElement} = this.props.environment.document
+ let {activeElement} = this.props.environment.document
+
+ // find the real activeElement not a custom element with a shadowRoot
+ /* istanbul ignore next -- JSDOM always reports the focused element as document.activeElement */
+ while (
+ activeElement?.shadowRoot &&
+ activeElement.shadowRoot.activeElement != null
+ ) {
+ activeElement = activeElement.shadowRoot.activeElement
+ }
if (
(activeElement == null || activeElement.id !== this.inputId) &&
@@ -877,7 +886,15 @@ class Downshift extends Component {
return
}
- const {activeElement} = this.props.environment.document
+ let {activeElement} = this.props.environment.document
+ /* istanbul ignore next -- JSDOM always reports the focused element as document.activeElement */
+ while (
+ activeElement?.shadowRoot &&
+ activeElement.shadowRoot.activeElement != null
+ ) {
+ activeElement = activeElement.shadowRoot.activeElement
+ }
+
const downshiftButtonIsActive =
activeElement?.dataset?.toggle &&
this._rootNode &&
@@ -1093,6 +1110,8 @@ class Downshift extends Component {
event.target,
[this._rootNode, this._menuNode],
this.props.environment,
+ true,
+ 'composedPath' in event && event.composedPath(),
)
if (!contextWithinDownshift && this.getState().isOpen) {
this.reset({type: stateChangeTypes.mouseUp}, () =>
@@ -1120,6 +1139,7 @@ class Downshift extends Component {
[this._rootNode, this._menuNode],
this.props.environment,
false,
+ 'composedPath' in event && event.composedPath(),
)
if (
!this.isTouchMove &&
diff --git a/src/hooks/useCombobox/index.js b/src/hooks/useCombobox/index.js
index d9e357c8..f078bfa5 100644
--- a/src/hooks/useCombobox/index.js
+++ b/src/hooks/useCombobox/index.js
@@ -129,8 +129,17 @@ function useCombobox(userProps = {}) {
if (!isOpen || !environment?.document || !inputRef?.current?.focus) {
return
}
+ let {activeElement} = environment.document
+ // find the real activeElement not a custom element with a shadowRoot
+ /* istanbul ignore next -- JSDOM always reports the focused element as document.activeElement */
+ while (
+ activeElement?.shadowRoot &&
+ activeElement.shadowRoot.activeElement != null
+ ) {
+ activeElement = activeElement.shadowRoot.activeElement
+ }
- if (environment.document.activeElement !== inputRef.current) {
+ if (activeElement !== inputRef.current) {
inputRef.current.focus()
}
}, [isOpen, environment])
diff --git a/src/hooks/utils.js b/src/hooks/utils.js
index b2c583c9..b0e7dd4c 100644
--- a/src/hooks/utils.js
+++ b/src/hooks/utils.js
@@ -390,7 +390,13 @@ function useMouseAndTouchTracker(
mouseAndTouchTrackersRef.current.isMouseDown = false
if (
- !targetWithinDownshift(event.target, downshiftElements, environment)
+ !targetWithinDownshift(
+ event.target,
+ downshiftElements,
+ environment,
+ true,
+ 'composedPath' in event && event.composedPath(),
+ )
) {
handleBlur()
}
@@ -412,6 +418,7 @@ function useMouseAndTouchTracker(
downshiftElements,
environment,
false,
+ 'composedPath' in event && event.composedPath(),
)
) {
handleBlur()
diff --git a/src/utils.js b/src/utils.js
index 23b99223..efcbbff1 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -417,6 +417,7 @@ function getNonDisabledIndex(
* @param {HTMLElement[]} downshiftElements The elements that form downshift (list, toggle button etc).
* @param {Window} environment The window context where downshift renders.
* @param {boolean} checkActiveElement Whether to also check activeElement.
+ * @param {EventTarget[]} composedPath The composed path of the event.
*
* @returns {boolean} Whether or not the target is within downshift elements.
*/
@@ -425,20 +426,34 @@ function targetWithinDownshift(
downshiftElements,
environment,
checkActiveElement = true,
+ composedPath,
) {
- return (
- environment &&
- downshiftElements.some(
- contextNode =>
- contextNode &&
- (isOrContainsNode(contextNode, target, environment) ||
+ if (!environment) {
+ return false
+ }
+
+ // Find the real activeElement by drilling through shadow roots
+ let activeElement = environment.document.activeElement
+ while (
+ activeElement != null &&
+ activeElement.shadowRoot != null &&
+ activeElement.shadowRoot.activeElement != null
+ ) {
+ activeElement = activeElement.shadowRoot.activeElement
+ }
+
+ return downshiftElements.some(
+ contextNode =>
+ contextNode &&
+ (composedPath
+ ? // Check if the contextNode is in the event's composed path
+ composedPath.indexOf(contextNode) !== -1 ||
(checkActiveElement &&
- isOrContainsNode(
- contextNode,
- environment.document.activeElement,
- environment,
- ))),
- )
+ isOrContainsNode(contextNode, activeElement, environment))
+ : // Fall back to regular DOM traversal when composedPath not available
+ isOrContainsNode(contextNode, target, environment) ||
+ (checkActiveElement &&
+ isOrContainsNode(contextNode, activeElement, environment))),
)
}
diff --git a/test/react-shadow.js b/test/react-shadow.js
new file mode 100644
index 00000000..5b4de84d
--- /dev/null
+++ b/test/react-shadow.js
@@ -0,0 +1,74 @@
+import React, {useState, useRef, useEffect} from 'react'
+import ReactDOM from 'react-dom'
+
+/**
+ * Utility to render a component with a shadow root.
+ *
+ * Based on React Shadow: https://github.com/apearce/react-shadow-root/blob/master/src/lib/ReactShadowRoot.js
+ */
+
+const constructableStylesheetsSupported =
+ typeof window !== 'undefined' &&
+ window.ShadowRoot &&
+ window.ShadowRoot.prototype.hasOwnProperty('adoptedStyleSheets') &&
+ window.CSSStyleSheet &&
+ window.CSSStyleSheet.prototype.hasOwnProperty('replace')
+
+const shadowRootSupported =
+ typeof window !== 'undefined' &&
+ window.Element &&
+ window.Element.prototype.hasOwnProperty('attachShadow')
+
+/**
+ * @param {object} props Properties passed to the component
+ * @param {boolean} [props.declarative] When true, uses a declarative shadow root
+ * @param {boolean} [props.delegatesFocus] Expands the focus behavior of elements within the shadow DOM.
+ * @param {string} [props.mode] Sets the mode of the shadow root. (open or closed)
+ * @param {CSSStyleSheet[]} [props.stylesheets] Takes an array of CSSStyleSheet objects for constructable stylesheets.
+ * @param {React.ReactNode} props.children The component to render within the shadow root.
+ */
+const ReactShadowRoot = ({
+ declarative = false,
+ delegatesFocus = false,
+ mode = 'open',
+ stylesheets,
+ children,
+}) => {
+ const [initialized, setInitialized] = useState(false)
+ const placeholder = useRef(null)
+ const shadowRootRef = useRef(null)
+
+ useEffect(() => {
+ if (placeholder.current) {
+ shadowRootRef.current = placeholder.current.parentNode.attachShadow({
+ delegatesFocus,
+ mode,
+ })
+
+ if (stylesheets) {
+ shadowRootRef.current.adoptedStyleSheets = stylesheets
+ }
+
+ setInitialized(true)
+ }
+ }, [delegatesFocus, mode, stylesheets])
+
+ if (!initialized) {
+ if (declarative) {
+ // @ts-ignore
+ return (
+
+ {children}
+
+ )
+ }
+
+ return
+ }
+
+ return ReactDOM.createPortal(children, shadowRootRef.current)
+}
+
+ReactShadowRoot.displayName = 'ReactShadowRoot'
+
+export {ReactShadowRoot, constructableStylesheetsSupported, shadowRootSupported}
diff --git a/test/useCombobox.test.tsx b/test/useCombobox.test.tsx
index 08cd8528..b9c6e22b 100644
--- a/test/useCombobox.test.tsx
+++ b/test/useCombobox.test.tsx
@@ -52,7 +52,7 @@ export default function DropdownCombobox() {
{isOpen ? <>↑> : <>↓>}