Skip to content

feat: make the Slack channel picker a type-to-filter combobox - #194

Merged
StephenTangCook merged 1 commit into
mainfrom
claude/slack-channel-picker-combobox-m5m1yr
Aug 17, 2026
Merged

feat: make the Slack channel picker a type-to-filter combobox#194
StephenTangCook merged 1 commit into
mainfrom
claude/slack-channel-picker-combobox-m5m1yr

Conversation

@StephenTangCook

@StephenTangCook StephenTangCook commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces both <select> channel pickers — the send dialog's, and the load dialog's recent-messages scope — with a shared ChannelCombobox: a text input that filters the channel list as you type, over a listbox of the matches. Channel names now render as # name rather than #name.

Why

A <select> stops being usable once a workspace has more channels than fit on a screen — there's no way to search, only to scroll. Typing to narrow the list is the interaction users already expect from Slack's own channel switcher.

The space after the # keeps the hash a marker rather than the first character of the name, so # release-notes stays legible at badge sizes.

What changed

src/components/channel-combobox.tsx (new) — follows the WAI-ARIA combobox pattern with manual selection: focus stays in the input and the active row is pointed at with aria-activedescendant, so typing only ever filters and nothing is committed until Enter or a click.

  • Arrow keys move the active row and wrap at the ends; Home/End jump to either.
  • Escape dismisses the list without closing the enclosing dialog; Tab closes it and moves on.
  • Opening seeds the active row on the current selection (or the first match), so Enter always has something sensible to commit.
  • Filtering strips a typed # and its spacing, so pasting # gen back into the field still finds general.
  • An abandoned search never lingers: closed, the field renders the selection's label, not the query.

The list is portalled through the existing Radix popover rather than laid out under the field — both dialogs put the picker inside a pane that clips its own overflow, so an in-flow dropdown would be cut off. It matches the field's width and flips above it on a short viewport.

src/lib/channel-label.ts (new) — one channelLabel() helper, shared by the picker, the toolbar edit badge, the update dialog, and the load preview, so the # name spelling can't drift between them.

test/setup.ts — stubs ResizeObserver and DOMRect.fromRect, which jsdom doesn't implement and Radix's popper constructs on mount.

Test plan

  • pnpm typecheck
  • pnpm lint
  • pnpm test — 402 unit + 124 Storybook browser tests pass
  • Spot-checked in a real Chromium against the built stylesheet

New coverage: test/channel-combobox.test.tsx (filtering, hash-insensitive matching, empty state, click and Enter commits, arrow navigation and wrapping, Escape-without-closing-the-dialog, selected marking) and test/send-dialog-channel-picker.test.tsx (the picker inside a modal dialog, where the portalled popup is most likely to go wrong). The suites that drove the old <select> now drive the combobox via a pickChannel helper.

Manually verified in Chromium against the built stylesheet, since jsdom can't see any of it: the dropdown escapes both dialogs' clipping panes, matches the field's width exactly (462px / 304px in the two layouts), caps at 240px and scrolls, and flips above the field on a 420px-tall viewport while staying inside it.

Notes for reviewer

The # name spelling is applied everywhere the package names a channel, not just in the picker — leaving the update dialog on #general while the send dialog said # general would read as a bug. Happy to scope it back to the picker alone if you'd rather.


Generated by Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Both channel pickers — the send dialog's and the load dialog's
recent-messages scope — were plain `<select>`s, which stop being usable
once a workspace has more channels than fit on a screen.

Replace them with a shared `ChannelCombobox`: a text input that filters
the list as you type, over a listbox of the matches. It follows the
WAI-ARIA combobox pattern with manual selection — focus stays in the
input and the active row is pointed at with `aria-activedescendant`, so
typing only filters and nothing is committed until Enter or a click.
Arrow keys (wrapping), Home/End, Escape-to-dismiss (without closing the
enclosing dialog), and Tab are all wired.

The list is portalled through the existing Radix popover rather than laid
out under the field: both dialogs put the picker inside a pane that clips
its own overflow, so an in-flow dropdown would be cut off. It matches the
field's width and flips above it on a short viewport.

Channel names now render as `# name` rather than `#name`, via a single
`channelLabel` helper shared by the picker, the edit badge, the update
dialog, and the load preview, so the spelling can't drift between them.

Tests: `test/setup.ts` gains ResizeObserver / DOMRect.fromRect stubs that
jsdom lacks and Radix's popper needs; the suites that drove the old
`<select>` now drive the combobox via a `pickChannel` helper.
@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare preview

✅ Deployed 017eaef · Branch preview · Commit preview · Build logs

@StephenTangCook
StephenTangCook merged commit bd1f878 into main Aug 17, 2026
12 of 14 checks passed
@StephenTangCook
StephenTangCook deleted the claude/slack-channel-picker-combobox-m5m1yr branch August 17, 2026 18:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants