Skip to content

Commit eefc03c

Browse files
authored
Ensure Escape propagates correctly in Combobox component (#1511)
* ensure `Escape` propagates correctly in Combobox component * update changelog
1 parent 08b419e commit eefc03c

File tree

5 files changed

+142
-0
lines changed

5 files changed

+142
-0
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Add `by` prop for `Listbox`, `Combobox` and `RadioGroup` ([#1482](https://github.com/tailwindlabs/headlessui/pull/1482))
1313
- Add `@headlessui/tailwindcss` plugin ([#1487](https://github.com/tailwindlabs/headlessui/pull/1487))
1414

15+
### Fixed
16+
17+
- Ensure `Escape` propagates correctly in `Combobox` component ([#1511](https://github.com/tailwindlabs/headlessui/pull/1511))
18+
1519
## [Unreleased - @headlessui/vue]
1620

1721
### Added
1822

1923
- Add `by` prop for `Listbox`, `Combobox` and `RadioGroup` ([#1482](https://github.com/tailwindlabs/headlessui/pull/1482))
2024
- Add `@headlessui/tailwindcss` plugin ([#1487](https://github.com/tailwindlabs/headlessui/pull/1487))
2125

26+
### Fixed
27+
28+
- Ensure `Escape` propagates correctly in `Combobox` component ([#1511](https://github.com/tailwindlabs/headlessui/pull/1511))
29+
2230
## [Unreleased - @headlessui/tailwindcss]
2331

2432
- Nothing yet!

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1499,6 +1499,64 @@ describe('Keyboard interactions', () => {
14991499
assertActiveElement(getComboboxInput())
15001500
})
15011501
)
1502+
1503+
it(
1504+
'should not propagate the Escape event when the combobox is open',
1505+
suppressConsoleLogs(async () => {
1506+
let handleKeyDown = jest.fn()
1507+
render(
1508+
<div onKeyDown={handleKeyDown}>
1509+
<Combobox value="test" onChange={console.log}>
1510+
<Combobox.Input onChange={NOOP} />
1511+
<Combobox.Button>Trigger</Combobox.Button>
1512+
<Combobox.Options>
1513+
<Combobox.Option value="a">Option A</Combobox.Option>
1514+
<Combobox.Option value="b">Option B</Combobox.Option>
1515+
<Combobox.Option value="c">Option C</Combobox.Option>
1516+
</Combobox.Options>
1517+
</Combobox>
1518+
</div>
1519+
)
1520+
1521+
// Open combobox
1522+
await click(getComboboxButton())
1523+
1524+
// Close combobox
1525+
await press(Keys.Escape)
1526+
1527+
// We should never see the Escape event
1528+
expect(handleKeyDown).toHaveBeenCalledTimes(0)
1529+
})
1530+
)
1531+
1532+
it(
1533+
'should propagate the Escape event when the combobox is closed',
1534+
suppressConsoleLogs(async () => {
1535+
let handleKeyDown = jest.fn()
1536+
render(
1537+
<div onKeyDown={handleKeyDown}>
1538+
<Combobox value="test" onChange={console.log}>
1539+
<Combobox.Input onChange={NOOP} />
1540+
<Combobox.Button>Trigger</Combobox.Button>
1541+
<Combobox.Options>
1542+
<Combobox.Option value="a">Option A</Combobox.Option>
1543+
<Combobox.Option value="b">Option B</Combobox.Option>
1544+
<Combobox.Option value="c">Option C</Combobox.Option>
1545+
</Combobox.Options>
1546+
</Combobox>
1547+
</div>
1548+
)
1549+
1550+
// Focus the input field
1551+
await focus(getComboboxInput())
1552+
1553+
// Close combobox
1554+
await press(Keys.Escape)
1555+
1556+
// We should never see the Escape event
1557+
expect(handleKeyDown).toHaveBeenCalledTimes(1)
1558+
})
1559+
)
15021560
})
15031561

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

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,7 @@ let Input = forwardRefWithAs(function Input<
621621

622622
case Keys.Backspace:
623623
case Keys.Delete:
624+
if (data.comboboxState !== ComboboxState.Open) return
624625
if (data.mode !== ValueMode.Single) return
625626
if (!data.nullable) return
626627

@@ -707,13 +708,15 @@ let Input = forwardRefWithAs(function Input<
707708
return actions.goToOption(Focus.Last)
708709

709710
case Keys.Escape:
711+
if (data.comboboxState !== ComboboxState.Open) return
710712
event.preventDefault()
711713
if (data.optionsRef.current && !data.optionsPropsRef.current.static) {
712714
event.stopPropagation()
713715
}
714716
return actions.closeCombobox()
715717

716718
case Keys.Tab:
719+
if (data.comboboxState !== ComboboxState.Open) return
717720
actions.selectActiveOption()
718721
actions.closeCombobox()
719722
break
@@ -830,6 +833,7 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
830833
return d.nextFrame(() => data.inputRef.current?.focus({ preventScroll: true }))
831834

832835
case Keys.Escape:
836+
if (data.comboboxState !== ComboboxState.Open) return
833837
event.preventDefault()
834838
if (data.optionsRef.current && !data.optionsPropsRef.current.static) {
835839
event.stopPropagation()

packages/@headlessui-vue/src/components/combobox/combobox.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1644,6 +1644,74 @@ describe('Keyboard interactions', () => {
16441644
assertActiveElement(getComboboxInput())
16451645
})
16461646
)
1647+
1648+
it(
1649+
'should not propagate the Escape event when the combobox is open',
1650+
suppressConsoleLogs(async () => {
1651+
let handleKeyDown = jest.fn()
1652+
renderTemplate({
1653+
template: html`
1654+
<Combobox v-model="value">
1655+
<ComboboxInput />
1656+
<ComboboxButton>Trigger</ComboboxButton>
1657+
<ComboboxOptions>
1658+
<ComboboxOption value="a">Option A</ComboboxOption>
1659+
<ComboboxOption value="b">Option B</ComboboxOption>
1660+
<ComboboxOption value="c">Option C</ComboboxOption>
1661+
</ComboboxOptions>
1662+
</Combobox>
1663+
`,
1664+
setup: () => ({ value: ref(null) }),
1665+
})
1666+
1667+
window.addEventListener('keydown', handleKeyDown)
1668+
1669+
// Open combobox
1670+
await click(getComboboxButton())
1671+
1672+
// Close combobox
1673+
await press(Keys.Escape)
1674+
1675+
// We should never see the Escape event
1676+
expect(handleKeyDown).toHaveBeenCalledTimes(0)
1677+
1678+
window.removeEventListener('keydown', handleKeyDown)
1679+
})
1680+
)
1681+
1682+
it(
1683+
'should propagate the Escape event when the combobox is closed',
1684+
suppressConsoleLogs(async () => {
1685+
let handleKeyDown = jest.fn()
1686+
renderTemplate({
1687+
template: html`
1688+
<Combobox v-model="value">
1689+
<ComboboxInput />
1690+
<ComboboxButton>Trigger</ComboboxButton>
1691+
<ComboboxOptions>
1692+
<ComboboxOption value="a">Option A</ComboboxOption>
1693+
<ComboboxOption value="b">Option B</ComboboxOption>
1694+
<ComboboxOption value="c">Option C</ComboboxOption>
1695+
</ComboboxOptions>
1696+
</Combobox>
1697+
`,
1698+
setup: () => ({ value: ref(null) }),
1699+
})
1700+
1701+
window.addEventListener('keydown', handleKeyDown)
1702+
1703+
// Focus the input field
1704+
await focus(getComboboxInput())
1705+
1706+
// Close combobox
1707+
await press(Keys.Escape)
1708+
1709+
// We should never see the Escape event
1710+
expect(handleKeyDown).toHaveBeenCalledTimes(1)
1711+
1712+
window.removeEventListener('keydown', handleKeyDown)
1713+
})
1714+
)
16471715
})
16481716

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

packages/@headlessui-vue/src/components/combobox/combobox.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ export let ComboboxButton = defineComponent({
576576
return
577577

578578
case Keys.Escape:
579+
if (api.comboboxState.value !== ComboboxStates.Open) return
579580
event.preventDefault()
580581
if (api.optionsRef.value && !api.optionsPropsRef.value.static) {
581582
event.stopPropagation()
@@ -649,6 +650,7 @@ export let ComboboxInput = defineComponent({
649650

650651
case Keys.Backspace:
651652
case Keys.Delete:
653+
if (api.comboboxState.value !== ComboboxStates.Open) return
652654
if (api.mode.value !== ValueMode.Single) return
653655
if (!api.nullable.value) return
654656

@@ -725,6 +727,7 @@ export let ComboboxInput = defineComponent({
725727
return api.goToOption(Focus.Last)
726728

727729
case Keys.Escape:
730+
if (api.comboboxState.value !== ComboboxStates.Open) return
728731
event.preventDefault()
729732
if (api.optionsRef.value && !api.optionsPropsRef.value.static) {
730733
event.stopPropagation()
@@ -733,6 +736,7 @@ export let ComboboxInput = defineComponent({
733736
break
734737

735738
case Keys.Tab:
739+
if (api.comboboxState.value !== ComboboxStates.Open) return
736740
api.selectActiveOption()
737741
api.closeCombobox()
738742
break

0 commit comments

Comments
 (0)