Skip to content

Commit 886b569

Browse files
committed
Add test for search
1 parent f7e0452 commit 886b569

7 files changed

Lines changed: 203 additions & 10 deletions

File tree

cypress/e2e/useMultipleCombobox.cy.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('useMultipleCombobox', () => {
2626
})
2727

2828
describe('useMultipleCombobox in shadow DOM', () => {
29-
before(() => {
29+
beforeEach(() => {
3030
cy.visit('/shadow-dom/useMultipleCombobox')
3131
})
3232

@@ -54,4 +54,25 @@ describe('useMultipleCombobox in shadow DOM', () => {
5454
cy.findByText('Gray').should('be.visible')
5555
})
5656
})
57+
58+
it('can filter the items', () => {
59+
cy.get('[data-testid="shadow-root"]')
60+
.shadow()
61+
.within(() => {
62+
cy.findByRole('button', {name: 'toggle menu'}).click()
63+
cy.findAllByRole('option').should('have.length', 12)
64+
cy.findByRole('combobox').type('g')
65+
cy.findByRole('button', {name: 'toggle menu'}).should(
66+
'have.attr',
67+
'aria-expanded',
68+
'true',
69+
)
70+
cy.findAllByRole('option').should('have.length', 2)
71+
cy.findByText('Green').should('be.visible')
72+
cy.findByText('Gray').should('be.visible')
73+
74+
cy.findByRole('combobox').type('{backspace}')
75+
cy.findAllByRole('option').should('have.length', 12)
76+
})
77+
})
5778
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as React from 'react'
2+
3+
import DropdownCombobox from '../useCombobox'
4+
import {ReactShadowRoot} from '../../../test/react-shadow'
5+
6+
const style = {
7+
padding: '20px',
8+
}
9+
10+
export default function MultipleComboboxShadow() {
11+
return (
12+
<div style={style}>
13+
<h2>Shadow DOM</h2>
14+
<div data-testid="shadow-root">
15+
<ReactShadowRoot>
16+
<DropdownCombobox />
17+
</ReactShadowRoot>
18+
</div>
19+
</div>
20+
)
21+
}

docusaurus/pages/shadow-dom/useMultipleCombobox.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ import * as React from 'react'
33
import DropdownMultipleCombobox from '../useMultipleCombobox'
44
import {ReactShadowRoot} from '../../../test/react-shadow'
55

6+
const style = {
7+
padding: '20px',
8+
}
9+
610
export default function MultipleComboboxShadow() {
711
return (
8-
<div>
12+
<div style={style}>
913
<div>
1014
<button>Button before shadow root</button>
1115
</div>

src/__tests__/downshift.shadow-root.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Downshift from '../'
55
import DropdownSelect from '../../test/useSelect.test'
66
import DropdownCombobox, {colors} from '../../test/useCombobox.test'
77
import DropdownMultipleSelect from '../../test/useMultipleSelect.test'
8+
import ComboBox from '../../test/downshift.test'
89
import {ReactShadowRoot} from '../../test/react-shadow'
910

1011
function _queryAllByRoleDeep(container, ...rest) {
@@ -159,3 +160,123 @@ test('DropdownCombobox works correctly in shadow DOM', async () => {
159160
// Menu should close
160161
expect(input).toHaveAttribute('aria-expanded', 'false')
161162
})
163+
164+
test('Downshift button blur correctly handles focus moving to external shadow DOM', async () => {
165+
const user = userEvent.setup()
166+
const {container} = render(<ComboBox />, {wrapper: Wrapper})
167+
168+
// Verify Downshift's own shadow root exists
169+
expect(container.shadowRoot).toBeDefined()
170+
171+
const comboboxRoot = getByRoleDeep('combobox')
172+
const toggleButton = container.shadowRoot.querySelector(
173+
'[data-testid="combobox-toggle-button"]',
174+
)
175+
176+
// Open the dropdown
177+
await user.click(toggleButton)
178+
expect(comboboxRoot).toHaveAttribute('aria-expanded', 'true')
179+
180+
// Click the element inside the external shadow DOM
181+
// This should focus externalFocusableButton and blur toggleButton (or the root/input depending on what had focus)
182+
const externalHost = document.createElement('div')
183+
document.body.appendChild(externalHost)
184+
const shadow = externalHost.attachShadow({mode: 'open'})
185+
const externalFocusableButton = document.createElement('button')
186+
shadow.appendChild(externalFocusableButton)
187+
await user.click(externalFocusableButton)
188+
189+
// Assert that the menu closes due to blur
190+
// Downshift's blur handlers use setTimeout, so wait for the next macrotask
191+
await new Promise(resolve => setTimeout(resolve, 0))
192+
expect(comboboxRoot).toHaveAttribute('aria-expanded', 'false')
193+
194+
// Cleanup
195+
document.body.removeChild(externalHost)
196+
})
197+
198+
test('Downshift input blur correctly handles focus moving to external shadow DOM', async () => {
199+
const user = userEvent.setup()
200+
const {container} = render(<ComboBox />, {wrapper: Wrapper})
201+
202+
// Verify Downshift's own shadow root exists
203+
expect(container.shadowRoot).toBeDefined()
204+
205+
const comboboxRoot = getByRoleDeep('combobox')
206+
const inputField = container.shadowRoot.querySelector(
207+
'[data-testid="combobox-input"]',
208+
)
209+
const downshiftToggleButton = container.shadowRoot.querySelector(
210+
'[data-testid="combobox-toggle-button"]',
211+
)
212+
213+
// Create an external element with its own shadow DOM
214+
const externalHost = document.createElement('div')
215+
document.body.appendChild(externalHost)
216+
const shadow = externalHost.attachShadow({mode: 'open'})
217+
const externalFocusableButton = document.createElement('button')
218+
shadow.appendChild(externalFocusableButton)
219+
220+
// Open the dropdown by clicking the toggle button
221+
await user.click(downshiftToggleButton)
222+
expect(comboboxRoot).toHaveAttribute('aria-expanded', 'true')
223+
224+
// Ensure the input itself is focused before it blurs
225+
inputField.focus()
226+
await user.type(inputField, 'b')
227+
await user.keyboard('{Tab}')
228+
// Click the element inside the external shadow DOM
229+
// This should focus externalFocusableButton and blur the input
230+
await user.click(externalFocusableButton)
231+
232+
// Assert that the menu closes due to blur
233+
// Downshift's blur handlers use setTimeout, so wait for the next macrotask
234+
await new Promise(resolve => setTimeout(resolve, 0))
235+
expect(comboboxRoot).toHaveAttribute('aria-expanded', 'false')
236+
237+
// Cleanup
238+
document.body.removeChild(externalHost)
239+
})
240+
241+
test('useCombobox input blur correctly handles focus moving to external shadow DOM', async () => {
242+
const user = userEvent.setup()
243+
const {container} = render(<DropdownCombobox />, {wrapper: Wrapper})
244+
245+
// Verify DropdownCombobox's own shadow root exists via the Wrapper
246+
expect(container.shadowRoot).toBeDefined()
247+
248+
const input = container.shadowRoot.querySelector(
249+
'[data-testid="combobox-input"]',
250+
)
251+
const toggleButton = container.shadowRoot.querySelector(
252+
'[data-testid="combobox-toggle-button"]',
253+
)
254+
255+
// Create an external element with its own shadow DOM
256+
const externalHost = document.createElement('div')
257+
document.body.appendChild(externalHost)
258+
const shadow = externalHost.attachShadow({mode: 'open'})
259+
const externalFocusableButton = document.createElement('button')
260+
shadow.appendChild(externalFocusableButton)
261+
262+
// Open the dropdown by clicking the toggle button
263+
await user.click(toggleButton)
264+
expect(input).toHaveAttribute('aria-expanded', 'true')
265+
266+
// Ensure the input itself is focused before it blurs
267+
input.focus()
268+
await user.type(input, 'b')
269+
await user.keyboard('{Tab}')
270+
271+
// Click the element inside the external shadow DOM
272+
// This should focus externalFocusableButton and blur the input in DropdownCombobox
273+
await user.click(externalFocusableButton)
274+
275+
// Assert that the menu closes due to blur
276+
// useCombobox's blur handler uses setTimeout, so wait for the next macrotask
277+
await new Promise(resolve => setTimeout(resolve, 0))
278+
expect(input).toHaveAttribute('aria-expanded', 'false')
279+
280+
// Cleanup
281+
document.body.removeChild(externalHost)
282+
})

src/downshift.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,16 @@ class Downshift extends Component {
764764
return
765765
}
766766

767-
const {activeElement} = this.props.environment.document
767+
let {activeElement} = this.props.environment.document
768+
769+
// find the real activeElement not a custom element with a shadowRoot
770+
/* istanbul ignore next -- JSDOM always reports the focused element as document.activeElement */
771+
while (
772+
activeElement?.shadowRoot &&
773+
activeElement.shadowRoot.activeElement != null
774+
) {
775+
activeElement = activeElement.shadowRoot.activeElement
776+
}
768777

769778
if (
770779
(activeElement == null || activeElement.id !== this.inputId) &&
@@ -877,7 +886,15 @@ class Downshift extends Component {
877886
return
878887
}
879888

880-
const {activeElement} = this.props.environment.document
889+
let {activeElement} = this.props.environment.document
890+
/* istanbul ignore next -- JSDOM always reports the focused element as document.activeElement */
891+
while (
892+
activeElement?.shadowRoot &&
893+
activeElement.shadowRoot.activeElement != null
894+
) {
895+
activeElement = activeElement.shadowRoot.activeElement
896+
}
897+
881898
const downshiftButtonIsActive =
882899
activeElement?.dataset?.toggle &&
883900
this._rootNode &&

src/hooks/useCombobox/index.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,17 @@ function useCombobox(userProps = {}) {
129129
if (!isOpen || !environment?.document || !inputRef?.current?.focus) {
130130
return
131131
}
132+
let {activeElement} = environment.document
133+
// find the real activeElement not a custom element with a shadowRoot
134+
/* istanbul ignore next -- JSDOM always reports the focused element as document.activeElement */
135+
while (
136+
activeElement?.shadowRoot &&
137+
activeElement.shadowRoot.activeElement != null
138+
) {
139+
activeElement = activeElement.shadowRoot.activeElement
140+
}
132141

133-
if (environment.document.activeElement !== inputRef.current) {
142+
if (activeElement !== inputRef.current) {
134143
inputRef.current.focus()
135144
}
136145
}, [isOpen, environment])

test/react-shadow.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ const shadowRootSupported =
2121

2222
/**
2323
* @param {object} props Properties passed to the component
24-
* @param {boolean} props.declarative When true, uses a declarative shadow root
25-
* @param {boolean} props.delegatesFocus Expands the focus behavior of elements within the shadow DOM.
26-
* @param {string} props.mode Sets the mode of the shadow root. (open or closed)
27-
* @param {CSSStyleSheet[]} props.stylesheets Takes an array of CSSStyleSheet objects for constructable stylesheets.
28-
* @param {React.ReactNode} props.children The children of the component
24+
* @param {boolean} [props.declarative] When true, uses a declarative shadow root
25+
* @param {boolean} [props.delegatesFocus] Expands the focus behavior of elements within the shadow DOM.
26+
* @param {string} [props.mode] Sets the mode of the shadow root. (open or closed)
27+
* @param {CSSStyleSheet[]} [props.stylesheets] Takes an array of CSSStyleSheet objects for constructable stylesheets.
28+
* @param {React.ReactNode} props.children The component to render within the shadow root.
2929
*/
3030
const ReactShadowRoot = ({
3131
declarative = false,

0 commit comments

Comments
 (0)