From b6b7a97bdba2dce82fb38d059837b1b42322f48f Mon Sep 17 00:00:00 2001 From: Vedanshu Date: Wed, 29 Jul 2026 23:05:23 +0000 Subject: [PATCH] fix(blocks): render the option label in a select trigger 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. --- .changeset/fix-block-kit-select-label.md | 5 + .../plugins/creating-plugins/block-kit.mdx | 6 +- packages/blocks/src/builders.ts | 3 +- packages/blocks/src/elements/select.tsx | 14 ++ packages/blocks/src/types.ts | 5 + packages/blocks/src/validation.ts | 6 + packages/blocks/tests/select-element.test.tsx | 137 ++++++++++++++++++ packages/blocks/tests/validation.test.ts | 19 +++ .../creating-plugins/references/block-kit.md | 6 +- 9 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 .changeset/fix-block-kit-select-label.md create mode 100644 packages/blocks/tests/select-element.test.tsx diff --git a/.changeset/fix-block-kit-select-label.md b/.changeset/fix-block-kit-select-label.md new file mode 100644 index 0000000000..309295e00e --- /dev/null +++ b/.changeset/fix-block-kit-select-label.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/blocks": patch +--- + +Fixes Block Kit `select` fields displaying the raw option value — or nothing at all — instead of the selected option's label. Adds an optional `placeholder` for the unselected state, defaulting to the label of an option whose `value` is `""` and otherwise to `Select...`. Submitted values are unchanged. diff --git a/docs/src/content/docs/plugins/creating-plugins/block-kit.mdx b/docs/src/content/docs/plugins/creating-plugins/block-kit.mdx index 79ffd1a0bd..c23d1de00f 100644 --- a/docs/src/content/docs/plugins/creating-plugins/block-kit.mdx +++ b/docs/src/content/docs/plugins/creating-plugins/block-kit.mdx @@ -99,10 +99,14 @@ The route handler takes two arguments: `routeCtx` (with `input`, `request`, `req | `button` | Action button with optional confirmation dialog | | `text_input` | Single-line or multiline text input | | `number_input` | Numeric input with min/max | -| `select` | Dropdown select | +| `select` | Dropdown select with optional `placeholder` | | `toggle` | On/off switch | | `secret_input` | Masked input for API keys and tokens | +A `select` renders the label of the selected option. When nothing is selected it renders its +`placeholder`, which defaults to the label of an option whose `value` is `""` (the usual "All" +entry) and otherwise to `Select...`. + ## Builder helpers The `@emdash-cms/blocks` package exports builder helpers for cleaner code: diff --git a/packages/blocks/src/builders.ts b/packages/blocks/src/builders.ts index ce71b2d027..21260c9206 100644 --- a/packages/blocks/src/builders.ts +++ b/packages/blocks/src/builders.ts @@ -210,7 +210,7 @@ function select( actionId: string, label: string, options: Array<{ label: string; value: string }>, - opts?: { initialValue?: string }, + opts?: { initialValue?: string; placeholder?: string }, ): SelectElement { return { type: "select", @@ -220,6 +220,7 @@ function select( ...(opts?.initialValue !== undefined && { initial_value: opts.initialValue, }), + ...(opts?.placeholder !== undefined && { placeholder: opts.placeholder }), }; } diff --git a/packages/blocks/src/elements/select.tsx b/packages/blocks/src/elements/select.tsx index 36cf63796e..3489d44666 100644 --- a/packages/blocks/src/elements/select.tsx +++ b/packages/blocks/src/elements/select.tsx @@ -3,6 +3,8 @@ import { useCallback } from "react"; import type { BlockInteraction, SelectElement } from "../types.js"; +const DEFAULT_PLACEHOLDER = "Select..."; + export function SelectElementComponent({ element, onAction, @@ -27,9 +29,21 @@ export function SelectElementComponent({ [onChange, onAction, element.action_id], ); + // An empty-string value counts as "no value" to the underlying Select, which + // then shows the placeholder. An option declaring `value: ""` *is* that empty + // state, so its label is the placeholder text unless the element sets one. + const placeholder = + element.placeholder ?? + element.options.find((opt) => opt.value === "")?.label ?? + DEFAULT_PLACEHOLDER; + + // `items` is what the trigger resolves its label from; without it the trigger + // renders the raw selected value. The children still render the popup. return (