From 3be0f270966a6176712fd6590fe7f20bdd554540 Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sun, 18 Oct 2026 12:06:47 +0530 Subject: [PATCH 1/4] docs(select): explain custom trigger values --- docs/app/docs/components/select/content.mdx | 12 ++++++++++-- .../docs/components/select/docs/codeUsage.js | 7 +++++++ .../select/docs/component_api/item.tsx | 1 + .../select/docs/component_api/trigger.tsx | 15 +++++++++++++++ .../components/select/docs/render-value.tsx | 19 +++++++++++++++++++ 5 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 docs/app/docs/components/select/docs/component_api/trigger.tsx create mode 100644 docs/app/docs/components/select/docs/render-value.tsx 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..38a769d66 --- /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 + + + + ) +} From fb211eb560aec6f791ef8c5db20d1303452dd58a Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sun, 18 Oct 2026 12:14:14 +0530 Subject: [PATCH 2/4] fix(select): preserve trigger callback types --- .changeset/calm-triggers-render.md | 5 +++++ .github/workflows/test.yml | 3 +++ package.json | 1 + scripts/type-tests/select-trigger.tsx | 15 +++++++++++++++ scripts/type-tests/tsconfig.json | 13 +++++++++++++ .../fragments/ComboboxPrimitiveTrigger.tsx | 2 +- 6 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 .changeset/calm-triggers-render.md create mode 100644 scripts/type-tests/select-trigger.tsx create mode 100644 scripts/type-tests/tsconfig.json diff --git a/.changeset/calm-triggers-render.md b/.changeset/calm-triggers-render.md new file mode 100644 index 000000000..d8bb5d198 --- /dev/null +++ b/.changeset/calm-triggers-render.md @@ -0,0 +1,5 @@ +--- +"@radui/ui": patch +--- + +Fix type inference for Select and Combobox trigger props. 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/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..f307e3023 --- /dev/null +++ b/scripts/type-tests/select-trigger.tsx @@ -0,0 +1,15 @@ +import Select from '@radui/ui/Select'; +import Combobox from '@radui/ui/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/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx b/src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx index 4f3c06deb..aaa1a5175 100644 --- a/src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx +++ b/src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx @@ -10,7 +10,7 @@ export type ComboboxPrimitiveTriggerProps = { className?: string; disabled?: boolean; renderValue?: (value: string, label: string) => React.ReactNode; - [key: string]: any; + 'data-placeholder'?: string; }; const ComboboxPrimitiveTrigger = React.forwardRef< From b4e6d971263d6fce82ad3076fb117fc2768db72d Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sun, 18 Oct 2026 12:15:48 +0530 Subject: [PATCH 3/4] fix(select): preserve typed data attributes --- .changeset/calm-triggers-render.md | 2 +- docs/app/docs/components/select/docs/render-value.tsx | 2 +- scripts/type-tests/select-trigger.tsx | 6 ++++++ .../Combobox/fragments/ComboboxPrimitiveTrigger.tsx | 2 ++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.changeset/calm-triggers-render.md b/.changeset/calm-triggers-render.md index d8bb5d198..e239cfa30 100644 --- a/.changeset/calm-triggers-render.md +++ b/.changeset/calm-triggers-render.md @@ -2,4 +2,4 @@ "@radui/ui": patch --- -Fix type inference for Select and Combobox trigger props. +Fix type inference for Select and Combobox trigger props. Arbitrary trigger prop keys are now limited to standard button props and `data-*` attributes. diff --git a/docs/app/docs/components/select/docs/render-value.tsx b/docs/app/docs/components/select/docs/render-value.tsx index 38a769d66..3c40cf556 100644 --- a/docs/app/docs/components/select/docs/render-value.tsx +++ b/docs/app/docs/components/select/docs/render-value.tsx @@ -5,7 +5,7 @@ import Select from "@radui/ui/Select" export default function CustomSelectValue() { return ( - value === "auto" ? "whip-l" : value}> + value === "auto" ? "whip-l" : value}> Choose a move diff --git a/scripts/type-tests/select-trigger.tsx b/scripts/type-tests/select-trigger.tsx index f307e3023..2b2fa65df 100644 --- a/scripts/type-tests/select-trigger.tsx +++ b/scripts/type-tests/select-trigger.tsx @@ -1,5 +1,11 @@ import Select from '@radui/ui/Select'; import Combobox from '@radui/ui/Combobox'; +import type { ComponentProps } from 'react'; + +const selectProps: ComponentProps = { 'data-testid': 'select' }; +const comboboxProps: ComponentProps = { 'data-custom-state': 'ready' }; +; +; { const display: string = `${value.toUpperCase()}: ${label.toUpperCase()}`; diff --git a/src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx b/src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx index aaa1a5175..1ea63d9b2 100644 --- a/src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx +++ b/src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx @@ -11,6 +11,8 @@ export type ComboboxPrimitiveTriggerProps = { disabled?: boolean; renderValue?: (value: string, label: string) => React.ReactNode; 'data-placeholder'?: string; + // A broad string index erases named props when React removes `ref`. + [key: `data-${string}`]: unknown; }; const ComboboxPrimitiveTrigger = React.forwardRef< From f617a24c3c851acb71075d0e0d15ddce4d01ac7b Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sun, 18 Oct 2026 12:17:51 +0530 Subject: [PATCH 4/4] fix(select): preserve custom trigger props --- .changeset/calm-triggers-render.md | 2 +- scripts/type-tests/select-trigger.tsx | 4 ++-- src/components/ui/Combobox/fragments/ComboboxTrigger.tsx | 3 ++- src/components/ui/Select/fragments/SelectTrigger.tsx | 3 ++- .../Combobox/fragments/ComboboxPrimitiveTrigger.tsx | 4 +--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.changeset/calm-triggers-render.md b/.changeset/calm-triggers-render.md index e239cfa30..b74c404c0 100644 --- a/.changeset/calm-triggers-render.md +++ b/.changeset/calm-triggers-render.md @@ -2,4 +2,4 @@ "@radui/ui": patch --- -Fix type inference for Select and Combobox trigger props. Arbitrary trigger prop keys are now limited to standard button props and `data-*` attributes. +Fix Select and Combobox trigger callback type inference. diff --git a/scripts/type-tests/select-trigger.tsx b/scripts/type-tests/select-trigger.tsx index 2b2fa65df..442e293fb 100644 --- a/scripts/type-tests/select-trigger.tsx +++ b/scripts/type-tests/select-trigger.tsx @@ -2,8 +2,8 @@ import Select from '@radui/ui/Select'; import Combobox from '@radui/ui/Combobox'; import type { ComponentProps } from 'react'; -const selectProps: ComponentProps = { 'data-testid': 'select' }; -const comboboxProps: ComponentProps = { 'data-custom-state': 'ready' }; +const selectProps: ComponentProps = { 'data-testid': 'select', customProp: 'select' }; +const comboboxProps: ComponentProps = { 'data-custom-state': 'ready', customProp: 'combobox' }; ; ; 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'; diff --git a/src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx b/src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx index 1ea63d9b2..4f3c06deb 100644 --- a/src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx +++ b/src/core/primitives/Combobox/fragments/ComboboxPrimitiveTrigger.tsx @@ -10,9 +10,7 @@ export type ComboboxPrimitiveTriggerProps = { className?: string; disabled?: boolean; renderValue?: (value: string, label: string) => React.ReactNode; - 'data-placeholder'?: string; - // A broad string index erases named props when React removes `ref`. - [key: `data-${string}`]: unknown; + [key: string]: any; }; const ComboboxPrimitiveTrigger = React.forwardRef<