diff --git a/.changeset/calm-triggers-render.md b/.changeset/calm-triggers-render.md
new file mode 100644
index 000000000..b74c404c0
--- /dev/null
+++ b/.changeset/calm-triggers-render.md
@@ -0,0 +1,5 @@
+---
+"@radui/ui": patch
+---
+
+Fix Select and Combobox trigger callback type inference.
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1b916cca4..4c79604dc 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -87,3 +87,6 @@ jobs:
- name: Check public API surface
run: npm run check:api-surface
+
+ - name: Check consumer types
+ run: npm run check:consumer-types
diff --git a/docs/app/docs/components/select/content.mdx b/docs/app/docs/components/select/content.mdx
index 7ef4f2bdc..f8cac9202 100644
--- a/docs/app/docs/components/select/content.mdx
+++ b/docs/app/docs/components/select/content.mdx
@@ -1,6 +1,6 @@
import Documentation from '@/components/layout/Documentation/Documentation';
import SelectExample from "./docs/example_1"
-import { code, largeListCode, anatomy, ariaReferences, keyboardShortcuts, api_documentation } from "./docs/codeUsage"
+import { code, largeListCode, renderValueCode, anatomy, ariaReferences, keyboardShortcuts, api_documentation } from "./docs/codeUsage"
+
+ Before a value is selected, the trigger displays its children as a placeholder. Once selected, it displays the item's label: its `label` prop, its text content, or its value as a fallback. Items hidden inside custom components may display their value until the list mounts.
+ Use `renderValue` to customize the selected display. It receives the value and resolved label, and is called only when a value is selected. Returning `null` or `undefined` falls back to the default display.
+
+ {renderValueCode.javascript.code}
+
+
+
Use a controlled value and keep each option's `value` and `label` stable when a Select has hundreds of options.
This incremental pattern keeps keyboard navigation intact by mounting more rows before focus reaches the boundary.
@@ -21,7 +29,7 @@ import { code, largeListCode, anatomy, ariaReferences, keyboardShortcuts, api_do
diff --git a/docs/app/docs/components/select/docs/codeUsage.js b/docs/app/docs/components/select/docs/codeUsage.js
index 87f5576a6..d5a7e955f 100644
--- a/docs/app/docs/components/select/docs/codeUsage.js
+++ b/docs/app/docs/components/select/docs/codeUsage.js
@@ -10,10 +10,12 @@ import {
DOCS_KEYBOARD_SHORTCUTS
} from '../../shared/keyboardShortcuts';
import root_api from './component_api/root.tsx';
+import trigger_api from './component_api/trigger.tsx';
import item_api from './component_api/item.tsx';
const example_1_SourceCode = await getSourceCodeFromPath('docs/app/docs/components/select/docs/example_1.tsx');
const largeList_SourceCode = await getSourceCodeFromPath('docs/app/docs/components/select/docs/large-list.tsx');
+const renderValue_SourceCode = await getSourceCodeFromPath('docs/app/docs/components/select/docs/render-value.tsx');
const scss_SourceCode = await getSourceCodeFromPath('styles/themes/components/select.scss');
const anatomy_SourceCode = await getSourceCodeFromPath('docs/app/docs/components/select/docs/anatomy.tsx');
@@ -26,10 +28,15 @@ export const largeListCode = {
javascript: { code: largeList_SourceCode }
};
+export const renderValueCode = {
+ javascript: { code: renderValue_SourceCode }
+};
+
export const anatomy = { code: anatomy_SourceCode };
export const api_documentation = {
root: root_api,
+ trigger: trigger_api,
item: item_api
};
diff --git a/docs/app/docs/components/select/docs/component_api/item.tsx b/docs/app/docs/components/select/docs/component_api/item.tsx
index d0049b250..3db57efe4 100644
--- a/docs/app/docs/components/select/docs/component_api/item.tsx
+++ b/docs/app/docs/components/select/docs/component_api/item.tsx
@@ -8,6 +8,7 @@ const data = {
],
data: [
{ prop: { name: "value*", info_tooltips: "The value of this option." }, type: "string", default: "--" },
+ { prop: { name: "label", info_tooltips: "Text used for the selected display, search, and typeahead." }, type: "string", default: "Item text, then value" },
{ prop: { name: "disabled", info_tooltips: "Disables this option." }, type: "boolean", default: "false" },
{ prop: { name: "className", info_tooltips: "Additional CSS classes." }, type: "string", default: '""' }
]
diff --git a/docs/app/docs/components/select/docs/component_api/trigger.tsx b/docs/app/docs/components/select/docs/component_api/trigger.tsx
new file mode 100644
index 000000000..4b595dfc0
--- /dev/null
+++ b/docs/app/docs/components/select/docs/component_api/trigger.tsx
@@ -0,0 +1,15 @@
+const data = {
+ name: "Trigger",
+ description: "Opens the select and displays the selected value.",
+ columns: [
+ { name: "Prop", id: "prop" },
+ { name: "Type", id: "type" },
+ { name: "Default", id: "default" }
+ ],
+ data: [
+ { prop: { name: "children", info_tooltips: "Placeholder shown when no value is selected." }, type: "ReactNode", default: "--" },
+ { prop: { name: "renderValue", info_tooltips: "Renders the selected value and resolved label. Return null or undefined to use the default display." }, type: "(value: string, label: string) => ReactNode", default: "--" }
+ ]
+}
+
+export default data
diff --git a/docs/app/docs/components/select/docs/render-value.tsx b/docs/app/docs/components/select/docs/render-value.tsx
new file mode 100644
index 000000000..3c40cf556
--- /dev/null
+++ b/docs/app/docs/components/select/docs/render-value.tsx
@@ -0,0 +1,19 @@
+"use client"
+
+import Select from "@radui/ui/Select"
+
+export default function CustomSelectValue() {
+ return (
+
+ value === "auto" ? "whip-l" : value}>
+ Choose a move
+
+
+
+ Auto (whip-l)
+ Manual
+
+
+
+ )
+}
diff --git a/package.json b/package.json
index 3f165c501..655a5a3d3 100644
--- a/package.json
+++ b/package.json
@@ -401,6 +401,7 @@
"check:clarity": "node scripts/clarity-audit.cjs",
"check:theme-coverage": "node scripts/theme-coverage.cjs",
"check:api-surface": "node scripts/check-public-api-surface.mjs --check",
+ "check:consumer-types": "tsc -p scripts/type-tests/tsconfig.json",
"validate:changesets": "node scripts/validate-changesets.mjs",
"validate:publish": "node scripts/validate-publish-package.cjs",
"build-components:esbuild": "npm run compile-components:esbuild && npm run process-components",
diff --git a/scripts/type-tests/select-trigger.tsx b/scripts/type-tests/select-trigger.tsx
new file mode 100644
index 000000000..442e293fb
--- /dev/null
+++ b/scripts/type-tests/select-trigger.tsx
@@ -0,0 +1,21 @@
+import Select from '@radui/ui/Select';
+import Combobox from '@radui/ui/Combobox';
+import type { ComponentProps } from 'react';
+
+const selectProps: ComponentProps = { 'data-testid': 'select', customProp: 'select' };
+const comboboxProps: ComponentProps = { 'data-custom-state': 'ready', customProp: 'combobox' };
+;
+;
+
+ {
+ const display: string = `${value.toUpperCase()}: ${label.toUpperCase()}`;
+ // @ts-expect-error Selected values are strings, not numbers.
+ value.toFixed(2);
+ return display;
+}} />;
+
+ {
+ // @ts-expect-error The label is a string, not a number.
+ label.toFixed(2);
+ return `${value.toUpperCase()}: ${label.toUpperCase()}`;
+}} />;
diff --git a/scripts/type-tests/tsconfig.json b/scripts/type-tests/tsconfig.json
new file mode 100644
index 000000000..a3e57a6c6
--- /dev/null
+++ b/scripts/type-tests/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "strict": true,
+ "noEmit": true,
+ "skipLibCheck": false,
+ "types": ["react"]
+ },
+ "include": ["./*.tsx"]
+}
diff --git a/src/components/ui/Combobox/fragments/ComboboxTrigger.tsx b/src/components/ui/Combobox/fragments/ComboboxTrigger.tsx
index a02a5bd4d..9d928c398 100644
--- a/src/components/ui/Combobox/fragments/ComboboxTrigger.tsx
+++ b/src/components/ui/Combobox/fragments/ComboboxTrigger.tsx
@@ -9,6 +9,7 @@ type ComboboxTriggerElement = React.ElementRef
export type ComboboxTriggerProps = React.ComponentPropsWithoutRef & {
customRootClass?: string;
placeholder?: boolean;
+ renderValue?: (value: string, label: string) => React.ReactNode;
};
const ComboboxTrigger = React.forwardRef(({ customRootClass, children, disabled, placeholder, className, ...props }, forwardedRef) => {
@@ -27,7 +28,7 @@ const ComboboxTrigger = React.forwardRef
);
-});
+}) as React.ForwardRefExoticComponent>;
ComboboxTrigger.displayName = 'ComboboxTrigger';
diff --git a/src/components/ui/Select/fragments/SelectTrigger.tsx b/src/components/ui/Select/fragments/SelectTrigger.tsx
index 37bd6a4bf..8c3f683cb 100644
--- a/src/components/ui/Select/fragments/SelectTrigger.tsx
+++ b/src/components/ui/Select/fragments/SelectTrigger.tsx
@@ -9,6 +9,7 @@ type SelectTriggerElement = React.ElementRef;
export type SelectTriggerProps = React.ComponentPropsWithoutRef & {
customRootClass?: string;
placeholder?: boolean;
+ renderValue?: (value: string, label: string) => React.ReactNode;
};
const SelectTrigger = React.forwardRef(({ customRootClass, children, className, disabled, placeholder, ...props }, forwardedRef) => {
@@ -29,7 +30,7 @@ const SelectTrigger = React.forwardRef
);
-});
+}) as React.ForwardRefExoticComponent>;
SelectTrigger.displayName = 'SelectTrigger';