Skip to content

Commit 706f42b

Browse files
authored
Bubble Escape event even if Combobox.Options is not rendered at all (#1104)
* bubble Escape event even if `Combobox.Options` is not rendered at all If you use `<Combobox.Options static />` it means that you are in control of rendering and in that case we also bubble the `Escape` because you are in control of it. However, if you do something like this: ```js {filteredList.length > 0 && ( <Combobox.Options static> ... </Combobox.Options> )} ``` Then whenever the `filteredList` is empty, the Combobox.Options are not rendered at all which means that we can't look at the `static` prop. To fix this, we also bubble the `Escape` event if we don't have a `Combobox.Options` at all so that the above example works as expected. * update changelog
1 parent 4ed344a commit 706f42b

File tree

5 files changed

+218
-141
lines changed

5 files changed

+218
-141
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818

1919
### Added
2020

21-
- Add `Combobox` component ([#1047](https://github.com/tailwindlabs/headlessui/pull/1047), [#1099](https://github.com/tailwindlabs/headlessui/pull/1099), [#1101](https://github.com/tailwindlabs/headlessui/pull/1101))
21+
- Add `Combobox` component ([#1047](https://github.com/tailwindlabs/headlessui/pull/1047), [#1099](https://github.com/tailwindlabs/headlessui/pull/1099), [#1101](https://github.com/tailwindlabs/headlessui/pull/1101), [#1104](https://github.com/tailwindlabs/headlessui/pull/1104))
2222

2323
## [Unreleased - @headlessui/vue]
2424

@@ -32,7 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3232

3333
### Added
3434

35-
- Add `Combobox` component ([#1047](https://github.com/tailwindlabs/headlessui/pull/1047), [#1099](https://github.com/tailwindlabs/headlessui/pull/1099), [#1101](https://github.com/tailwindlabs/headlessui/pull/1101))
35+
- Add `Combobox` component ([#1047](https://github.com/tailwindlabs/headlessui/pull/1047), [#1099](https://github.com/tailwindlabs/headlessui/pull/1099), [#1101](https://github.com/tailwindlabs/headlessui/pull/1101), - Bubble Escape event even if `Combobox.Options` is not rendered at all ()
36+
)
3637

3738
## [@headlessui/react@v1.4.3] - 2022-01-14
3839

packages/@headlessui-react/src/components/combobox/combobox.test.tsx

Lines changed: 97 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,71 +1387,6 @@ describe('Keyboard interactions', () => {
13871387
assertActiveElement(getComboboxInput())
13881388
})
13891389
)
1390-
1391-
it(
1392-
'Static options should allow escape to bubble',
1393-
suppressConsoleLogs(async () => {
1394-
render(
1395-
<Combobox value="test" onChange={console.log}>
1396-
<Combobox.Input onChange={NOOP} />
1397-
<Combobox.Button>Trigger</Combobox.Button>
1398-
<Combobox.Options static>
1399-
<Combobox.Option value="a">Option A</Combobox.Option>
1400-
<Combobox.Option value="b">Option B</Combobox.Option>
1401-
<Combobox.Option value="c">Option C</Combobox.Option>
1402-
</Combobox.Options>
1403-
</Combobox>
1404-
)
1405-
1406-
let spy = jest.fn()
1407-
1408-
window.addEventListener(
1409-
'keydown',
1410-
(evt) => {
1411-
if (evt.key === 'Escape') {
1412-
spy()
1413-
}
1414-
},
1415-
{ capture: true }
1416-
)
1417-
1418-
window.addEventListener('keydown', (evt) => {
1419-
if (evt.key === 'Escape') {
1420-
spy()
1421-
}
1422-
})
1423-
1424-
// Open combobox
1425-
await click(getComboboxButton())
1426-
1427-
// Verify it is visible
1428-
assertComboboxButton({ state: ComboboxState.Visible })
1429-
assertComboboxList({
1430-
state: ComboboxState.Visible,
1431-
attributes: { id: 'headlessui-combobox-options-3' },
1432-
})
1433-
assertActiveElement(getComboboxInput())
1434-
assertComboboxButtonLinkedWithCombobox()
1435-
1436-
// Re-focus the button
1437-
getComboboxButton()?.focus()
1438-
assertActiveElement(getComboboxButton())
1439-
1440-
// Close combobox
1441-
await press(Keys.Escape)
1442-
1443-
// TODO: Verify it is rendered — with static it's not visible or invisible from an assert perspective
1444-
// assertComboboxButton({ state: ComboboxState.InvisibleUnmounted })
1445-
// assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
1446-
1447-
// Verify the input is focused again
1448-
assertActiveElement(getComboboxInput())
1449-
1450-
// The external event handler should've been called twice
1451-
// Once in the capture phase and once in the bubble phase
1452-
expect(spy).toHaveBeenCalledTimes(2)
1453-
})
1454-
)
14551390
})
14561391

14571392
describe('`ArrowDown` key', () => {
@@ -2021,6 +1956,103 @@ describe('Keyboard interactions', () => {
20211956
assertActiveElement(getComboboxInput())
20221957
})
20231958
)
1959+
1960+
it(
1961+
'should bubble escape when using `static` on Combobox.Options',
1962+
suppressConsoleLogs(async () => {
1963+
render(
1964+
<Combobox value="test" onChange={console.log}>
1965+
<Combobox.Input onChange={NOOP} />
1966+
<Combobox.Button>Trigger</Combobox.Button>
1967+
<Combobox.Options static>
1968+
<Combobox.Option value="a">Option A</Combobox.Option>
1969+
<Combobox.Option value="b">Option B</Combobox.Option>
1970+
<Combobox.Option value="c">Option C</Combobox.Option>
1971+
</Combobox.Options>
1972+
</Combobox>
1973+
)
1974+
1975+
let spy = jest.fn()
1976+
1977+
window.addEventListener(
1978+
'keydown',
1979+
(evt) => {
1980+
if (evt.key === 'Escape') {
1981+
spy()
1982+
}
1983+
},
1984+
{ capture: true }
1985+
)
1986+
1987+
window.addEventListener('keydown', (evt) => {
1988+
if (evt.key === 'Escape') {
1989+
spy()
1990+
}
1991+
})
1992+
1993+
// Open combobox
1994+
await click(getComboboxButton())
1995+
1996+
// Verify the input is focused
1997+
assertActiveElement(getComboboxInput())
1998+
1999+
// Close combobox
2000+
await press(Keys.Escape)
2001+
2002+
// Verify the input is still focused
2003+
assertActiveElement(getComboboxInput())
2004+
2005+
// The external event handler should've been called twice
2006+
// Once in the capture phase and once in the bubble phase
2007+
expect(spy).toHaveBeenCalledTimes(2)
2008+
})
2009+
)
2010+
2011+
it(
2012+
'should bubble escape when not using Combobox.Options at all',
2013+
suppressConsoleLogs(async () => {
2014+
render(
2015+
<Combobox value="test" onChange={console.log}>
2016+
<Combobox.Input onChange={NOOP} />
2017+
<Combobox.Button>Trigger</Combobox.Button>
2018+
</Combobox>
2019+
)
2020+
2021+
let spy = jest.fn()
2022+
2023+
window.addEventListener(
2024+
'keydown',
2025+
(evt) => {
2026+
if (evt.key === 'Escape') {
2027+
spy()
2028+
}
2029+
},
2030+
{ capture: true }
2031+
)
2032+
2033+
window.addEventListener('keydown', (evt) => {
2034+
if (evt.key === 'Escape') {
2035+
spy()
2036+
}
2037+
})
2038+
2039+
// Open combobox
2040+
await click(getComboboxButton())
2041+
2042+
// Verify the input is focused
2043+
assertActiveElement(getComboboxInput())
2044+
2045+
// Close combobox
2046+
await press(Keys.Escape)
2047+
2048+
// Verify the input is still focused
2049+
assertActiveElement(getComboboxInput())
2050+
2051+
// The external event handler should've been called twice
2052+
// Once in the capture phase and once in the bubble phase
2053+
expect(spy).toHaveBeenCalledTimes(2)
2054+
})
2055+
)
20242056
})
20252057

20262058
describe('`ArrowDown` key', () => {

packages/@headlessui-react/src/components/combobox/combobox.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,11 @@ let reducers: {
114114
},
115115
[ActionTypes.GoToOption](state, action) {
116116
if (state.disabled) return state
117-
if (!state.optionsPropsRef.current.static && state.comboboxState === ComboboxStates.Closed)
117+
if (
118+
state.optionsRef.current &&
119+
!state.optionsPropsRef.current.static &&
120+
state.comboboxState === ComboboxStates.Closed
121+
)
118122
return state
119123

120124
let activeOptionIndex = calculateActiveIndex(action, {
@@ -480,7 +484,7 @@ let Input = forwardRefWithAs(function Input<
480484

481485
case Keys.Escape:
482486
event.preventDefault()
483-
if (!state.optionsPropsRef.current.static) {
487+
if (state.optionsRef.current && !state.optionsPropsRef.current.static) {
484488
event.stopPropagation()
485489
}
486490
return dispatch({ type: ActionTypes.CloseCombobox })
@@ -607,7 +611,7 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
607611

608612
case Keys.Escape:
609613
event.preventDefault()
610-
if (!state.optionsPropsRef.current.static) {
614+
if (state.optionsRef.current && !state.optionsPropsRef.current.static) {
611615
event.stopPropagation()
612616
}
613617
dispatch({ type: ActionTypes.CloseCombobox })

0 commit comments

Comments
 (0)