Describe the bug
When a Tabs component is composed inside a Dialog (a very common settings-dialog pattern), pressing Escape while a tab trigger has focus does not close the dialog. Since Dialog.Content's focus trap auto-focuses the selected tab trigger on open, the practical effect is that Escape appears completely broken the moment the dialog opens.
Root cause
createSelectableCollection's keydown handler (selection/create-selectable-collection.ts L320–327):
case "Escape":
if (!e.defaultPrevented) {
e.preventDefault();
if (!access(mergedProps.disallowEmptySelection)) {
manager.clearSelection();
}
}
break;
e.preventDefault() is called unconditionally, even though TabsList hardcodes disallowEmptySelection: true (tabs/tabs-list.tsx L83), so the clearSelection() that the preventDefault() is guarding never runs — the call has no visible effect on the collection itself.
But the poisoned event.defaultPrevented then reaches DismissableLayer's document-level escape handler, which gates dismissal on !e.defaultPrevented (dismissable-layer/dismissable-layer.tsx) — so the dialog's onDismiss never fires. The layer believes a descendant meaningfully consumed Escape when nothing actually happened.
Both code paths above are current main as of this report; reproduced concretely on @kobalte/core@0.13.11.
Suggested fix
Only claim the event when the handler actually does something:
case "Escape":
if (!e.defaultPrevented && !access(mergedProps.disallowEmptySelection)) {
e.preventDefault();
manager.clearSelection();
}
break;
(This also affects any other selectable collection rendered inside a dismissable layer whenever the Escape press ends up being a selection no-op.)
Minimal reproduction
import { Dialog, Tabs } from "@kobalte/core";
<Dialog.Root>
<Dialog.Trigger>Open settings</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Tabs.Root orientation="vertical">
<Tabs.List>
<Tabs.Trigger value="a">Tab A</Tabs.Trigger>
<Tabs.Trigger value="b">Tab B</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="a">…</Tabs.Content>
<Tabs.Content value="b">…</Tabs.Content>
</Tabs.Root>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
- Open the dialog — the focus trap focuses the selected tab trigger.
- Press Escape.
- Expected: the dialog closes. Actual: nothing happens. (Clicking the overlay still closes it; Escape works again only if focus moves off the tab triggers first.)
Environment
@kobalte/core: 0.13.11 (bug also present in current main source)
- solid-js: 1.x
Describe the bug
When a
Tabscomponent is composed inside aDialog(a very common settings-dialog pattern), pressing Escape while a tab trigger has focus does not close the dialog. SinceDialog.Content's focus trap auto-focuses the selected tab trigger on open, the practical effect is that Escape appears completely broken the moment the dialog opens.Root cause
createSelectableCollection's keydown handler (selection/create-selectable-collection.tsL320–327):e.preventDefault()is called unconditionally, even thoughTabsListhardcodesdisallowEmptySelection: true(tabs/tabs-list.tsxL83), so theclearSelection()that thepreventDefault()is guarding never runs — the call has no visible effect on the collection itself.But the poisoned
event.defaultPreventedthen reachesDismissableLayer's document-level escape handler, which gates dismissal on!e.defaultPrevented(dismissable-layer/dismissable-layer.tsx) — so the dialog'sonDismissnever fires. The layer believes a descendant meaningfully consumed Escape when nothing actually happened.Both code paths above are current
mainas of this report; reproduced concretely on@kobalte/core@0.13.11.Suggested fix
Only claim the event when the handler actually does something:
(This also affects any other selectable collection rendered inside a dismissable layer whenever the Escape press ends up being a selection no-op.)
Minimal reproduction
Environment
@kobalte/core: 0.13.11 (bug also present in currentmainsource)