Skip to content

fix(blocks): render the option label in a select trigger - #8

Open
vedanshujain wants to merge 1 commit into
mainfrom
fix/select-value-label
Open

fix(blocks): render the option label in a select trigger#8
vedanshujain wants to merge 1 commit into
mainfrom
fix/select-value-label

Conversation

@vedanshujain

Copy link
Copy Markdown
Owner

What does this PR do?

A Block Kit select rendered the raw option value in its closed trigger instead of the option's label, and rendered completely empty when the value was "", null or absent.

Root cause, in two halves:

  • packages/blocks/src/elements/select.tsx passed the options only as Select.Option children — no items, placeholder or renderValue. Kumo 2.6 wraps Base UI 1.5 (not Radix), and Base UI's Select.Value only resolves a label when the root has items: without it, resolveSelectedLabel(value, undefined, undefined) falls through to serializeValue(value), i.e. the value itself.
  • Base UI's store counts "" as "no value" (hasSelectedValue runs the value through serializeValue and compares to ""), so a select whose value was an empty string rendered a blank trigger. With no placeholder either, an unset value was blank too.

The fix:

  • Pass the options as items as well. That is Base UI's documented mechanism — "When specified, <Select.Value> renders the label of the selected item instead of the raw value" — and it is what the admin's own DynamicSelect already does. items only feeds label resolution in the store; it does not touch selection semantics. The children still render the popup, so option order, keys and DOM are unchanged.
  • Add an optional placeholder to SelectElement, mirroring Kumo's Select prop and the existing combobox / text_input / date_input fields (type, validation.ts, elements.select() builder, docs).
  • Default that placeholder to the label of an option whose value is "", and otherwise to Select.... Because Base UI treats "" and "unset" as the same display state, they must share one string; the common { value: "", label: "All statuses" } filter entry is the author's own empty-state label, so it wins over an invented default. An explicit placeholder always wins over both.

Backward compatibility: display-only. Submitted values are untouched — including an empty-string option value, which is still selectable and still submits "" (verified by test and in the browser). A select that previously showed a blank trigger now shows a label or placeholder; nothing about the payload changes.

Scoped out, after checking: combobox already passes items and uses the option object as its value, so Base UI resolves value.label — verified rendering "Published" for value: "pub_1". radio has no selected-value display surface at all (every Radio.Item carries its own label), so the root cause cannot reach it. Neither needed a change.

Closes #

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes (pnpm lint:json → 0 diagnostics)
  • pnpm test passes (all packages; @emdash-cms/blocks 106 tests, @emdash-cms/admin 1249 tests)
  • pnpm format has been run (oxfmt --check clean)
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation — n/a: packages/blocks has no Lingui setup; the new default follows the neighbouring combobox's literal "Search...". Localising Block Kit element strings is a separate, package-wide change.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion — n/a, bug fix. The added placeholder field is the additive minimum needed to fix the empty trigger.

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 5 (Claude Code)

Screenshots / test output

New suite packages/blocks/tests/select-element.test.tsx renders the real Kumo Select (no mock — a mocked Select cannot exhibit the bug). Against main 6 of its 9 tests fail; the 3 value-submission tests pass both before and after, which is the backward-compatibility check:

× renders the label of the selected option, not its value   → expected 'pub_1' to contain 'Published'
× renders the label of the option picked by the user        → expected 'dft_2' to contain 'Draft'
× renders a placeholder when nothing is selected            → expected '' to contain 'Any status'
× renders a default placeholder when the element has none   → expected '' to contain 'Select...'
× renders the label of an empty-value option ...            → expected '' to contain 'All statuses'
× prefers an explicit placeholder over an empty-value ...   → expected '' to contain 'Filter by status'
✓ submits the option value unchanged
✓ submits an empty-string option value unchanged
✓ reports the value through onAction when no onChange is given

After the fix, all 9 pass (pnpm --filter @emdash-cms/blocks test: 4 files, 106 tests).

Also exercised in a real browser (Chromium, blocks playground). Trigger text vs. the value in the hidden form input:

select trigger submitted
initial_value: "pub_1" Published pub_1
option { value: "", label: "All statuses" }, initial_value: "" All statuses ""
no initial_value, no placeholder Select... ""
placeholder: "Any status" Any status ""

The existing SEO template's select in the playground shows None (extractive) where it previously showed none.

Block Kit's select passed its options only as children, so Kumo had no
value-to-label map to hand Base UI. The trigger serialized the raw value,
and rendered nothing at all when the value was "", null or absent, since
Base UI counts an empty string as "no value".

Pass the options as `items` too, which is the documented way to make the
selected value render as a label, and add an optional `placeholder` for
the unselected state, mirroring Kumo's own prop. The placeholder defaults
to the label of an option whose value is "" -- the usual "All ..." filter
entry, which Base UI would otherwise hide behind the placeholder -- and
to "Select..." when there is none.

The popup still renders the same children, so option order, keys and the
submitted values are unchanged.
@github-actions

Copy link
Copy Markdown


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@github-actions

Copy link
Copy Markdown

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@github-actions

Copy link
Copy Markdown

This PR has been inactive for 14 days. It will be closed automatically in 7 days if there is no further activity.

If you're still working on this, please push an update or leave a comment.

@github-actions github-actions Bot added stale and removed stale labels Aug 13, 2026
@github-actions github-actions Bot added stale and removed stale labels Aug 27, 2026
@github-actions github-actions Bot added stale and removed stale labels Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant