Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-triggers-render.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@radui/ui": patch
---

Fix Select and Combobox trigger callback type inference.
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 10 additions & 2 deletions docs/app/docs/components/select/content.mdx
Original file line number Diff line number Diff line change
@@ -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"

<Documentation
title="Select"
Expand All @@ -10,6 +10,14 @@ import { code, largeListCode, anatomy, ariaReferences, keyboardShortcuts, api_do
<SelectExample />
</Documentation.ComponentHero>

<Documentation.Section title="Custom trigger value">
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.
<Documentation.CodeBlock language="tsx">
{renderValueCode.javascript.code}
</Documentation.CodeBlock>
</Documentation.Section>

<Documentation.Section title="Large Lists">
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.
Expand All @@ -21,7 +29,7 @@ import { code, largeListCode, anatomy, ariaReferences, keyboardShortcuts, api_do
<Documentation.ApiReference
anatomy={anatomy.code}
tables={api_documentation}
order={['root', 'item']}
order={['root', 'trigger', 'item']}
/>

<Documentation.Table title="Keyboard Interactions" columns={keyboardShortcuts.columns} data={keyboardShortcuts.data} />
Expand Down
7 changes: 7 additions & 0 deletions docs/app/docs/components/select/docs/codeUsage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: '""' }
]
Expand Down
15 changes: 15 additions & 0 deletions docs/app/docs/components/select/docs/component_api/trigger.tsx
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions docs/app/docs/components/select/docs/render-value.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client"

import Select from "@radui/ui/Select"

export default function CustomSelectValue() {
return (
<Select.Root defaultValue="auto">
<Select.Trigger aria-label="Move" renderValue={(value: string) => value === "auto" ? "whip-l" : value}>
Choose a move
</Select.Trigger>
<Select.Portal>
<Select.Content>
<Select.Item value="auto">Auto (whip-l)</Select.Item>
<Select.Item value="manual">Manual</Select.Item>
</Select.Content>
</Select.Portal>
</Select.Root>
)
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 21 additions & 0 deletions scripts/type-tests/select-trigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Select from '@radui/ui/Select';
import Combobox from '@radui/ui/Combobox';
import type { ComponentProps } from 'react';

const selectProps: ComponentProps<typeof Select.Trigger> = { 'data-testid': 'select', customProp: 'select' };
const comboboxProps: ComponentProps<typeof Combobox.Trigger> = { 'data-custom-state': 'ready', customProp: 'combobox' };
<Select.Trigger {...selectProps} />;
<Combobox.Trigger {...comboboxProps} />;

<Select.Trigger renderValue={(value, label) => {
const display: string = `${value.toUpperCase()}: ${label.toUpperCase()}`;
// @ts-expect-error Selected values are strings, not numbers.
value.toFixed(2);
return display;
}} />;

<Combobox.Trigger renderValue={(value, label) => {
// @ts-expect-error The label is a string, not a number.
label.toFixed(2);
return `${value.toUpperCase()}: ${label.toUpperCase()}`;
}} />;
13 changes: 13 additions & 0 deletions scripts/type-tests/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noEmit": true,
"skipLibCheck": false,
"types": ["react"]
},
"include": ["./*.tsx"]
}
3 changes: 2 additions & 1 deletion src/components/ui/Combobox/fragments/ComboboxTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type ComboboxTriggerElement = React.ElementRef<typeof ComboboxPrimitive.Trigger>
export type ComboboxTriggerProps = React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Trigger> & {
customRootClass?: string;
placeholder?: boolean;
renderValue?: (value: string, label: string) => React.ReactNode;
};

const ComboboxTrigger = React.forwardRef<ComboboxTriggerElement, ComboboxTriggerProps>(({ customRootClass, children, disabled, placeholder, className, ...props }, forwardedRef) => {
Expand All @@ -27,7 +28,7 @@ const ComboboxTrigger = React.forwardRef<ComboboxTriggerElement, ComboboxTrigger
{children}
</ComboboxPrimitive.Trigger>
);
});
}) as React.ForwardRefExoticComponent<ComboboxTriggerProps & React.RefAttributes<ComboboxTriggerElement>>;

ComboboxTrigger.displayName = 'ComboboxTrigger';

Expand Down
3 changes: 2 additions & 1 deletion src/components/ui/Select/fragments/SelectTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type SelectTriggerElement = React.ElementRef<typeof ComboboxPrimitive.Trigger>;
export type SelectTriggerProps = React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Trigger> & {
customRootClass?: string;
placeholder?: boolean;
renderValue?: (value: string, label: string) => React.ReactNode;
};

const SelectTrigger = React.forwardRef<SelectTriggerElement, SelectTriggerProps>(({ customRootClass, children, className, disabled, placeholder, ...props }, forwardedRef) => {
Expand All @@ -29,7 +30,7 @@ const SelectTrigger = React.forwardRef<SelectTriggerElement, SelectTriggerProps>

</ComboboxPrimitive.Trigger>
);
});
}) as React.ForwardRefExoticComponent<SelectTriggerProps & React.RefAttributes<SelectTriggerElement>>;

SelectTrigger.displayName = 'SelectTrigger';

Expand Down
Loading