TreeSelect
Trigger field that opens a searchable, flattened tree in a bottom sheet — single or multi select, leaf-only selection and breadcrumb display, API-aligned with the web TreeSelect.
Installation
pnpm dlx @docyrus/cli add @docyrus/rn-tree-selectpnpm add @shopify/flash-list tailwind-variantsUsage
import { useState } from 'react';
import { TreeSelect, type TreeSelectValue } from '@/components/docyrus-native/tree-select';
import { type TreeViewItem } from '@/components/docyrus-native/tree-view';
const data: TreeViewItem[] = [
{
id: 'eng',
name: 'Engineering',
type: 'group',
children: [
{ id: 'eng-fe', name: 'Frontend', type: 'team' },
{ id: 'eng-be', name: 'Backend', type: 'team' }
]
}
];
export function TeamPicker() {
const [team, setTeam] = useState<TreeSelectValue>(null);
const [teams, setTeams] = useState<TreeSelectValue>([]);
return (
<>
<TreeSelect
data={data}
value={team}
onValueChange={setTeam}
iconMap={{ group: 'fal folder', team: 'fal users' }}
leafOnly
showBreadcrumb />
<TreeSelect
data={data}
multiple
value={teams}
onValueChange={setTeams}
sheetTitle="Teams" />
</>
);
}Interaction model
| Gesture | Single | Multiple |
|---|---|---|
| Tap trigger | Opens the sheet (search + tree) | Opens the sheet |
| Tap leaf row | Commits the id and closes | Toggles the leaf |
| Tap folder row | Commits the folder id and closes; with leafOnly it expands / collapses instead | Toggles the folder — cascades to the folder id and every descendant, or only to leaf descendants with leafOnly; with leafOnly the row tap expands / collapses and the checkbox cascades |
| Tap chevron | Expands / collapses | Expands / collapses |
Tap chip × | — | Removes the item (and its cascade) |
| Done (header) | — | Closes the sheet |
The sheet lists the tree as a flattened FlashList (depth indent, chevrons, tri-state checkboxes in multi mode — a folder shows indeterminate when only some of its leaves are selected), so large trees stay smooth. Searching filters by name and opens every folder on the path to a match. Expansion is re-seeded from defaultExpanded and the search is cleared every time the sheet opens (the web tree remounts per open).
The multi-mode trigger shows up to maxVisibleChips chips and collapses the rest into +N.
API Reference
TreeSelectProps
| Prop | Type | Default | Description |
|---|---|---|---|
data | TreeViewItem[] | - | Tree data shown in the sheet |
value | TreeSelectValue | - | Selected id (single) or ids (multiple) |
onValueChange | (value: TreeSelectValue) => void | - | Selection changed — string in single mode, string[] in multi mode, null when cleared |
multiple | boolean | false | Multi-select mode (checkboxes + Done button) |
placeholder | string | t('ui.treeSelect.placeholder', 'Select...') / t('ui.treeSelect.multiPlaceholder', 'Select items...') | Trigger placeholder |
disabled | boolean | false | Disable the control |
className | string | - | Additional classes for the trigger |
onBlur | () => void | - | Called when the sheet closes (native stand-in for the trigger blur) |
invalid | boolean | false | Error border on the trigger (replaces web aria-invalid) |
detent | 'small' | 'medium' | 'large' | 'fit' | 'large' | Sheet height (replaces web maxHeight) |
getIcon | (item: TreeViewItem, depth: number) => ReactNode | - | Custom icon renderer (wins over iconMap) |
iconMap | TreeViewIconMap | - | item.type → icon; a string renders as a DocyrusIcon identifier |
hideInfo | boolean | true | Hide the per-row info caption (type · N direct items) — the native counterpart of the web hover card |
searchPlaceholder | string | t('ui.treeView.searchPlaceholder', 'Search...') | Search placeholder |
leafOnly | boolean | false | Only leaves become values (see the interaction model) |
defaultExpanded | 'none' | 'all' | 'selected' | ReadonlyArray<string> | 'selected' | Folders expanded each time the sheet opens ('selected' = ancestors of the current value) |
showBreadcrumb | boolean | false | Render Group › Subgroup › Item in the trigger; ancestors are muted |
renderSelected | (item: TreeViewItem) => ReactNode | - | Custom trigger rendering for a selected item (wins over showBreadcrumb; strings are wrapped in Text) |
open | boolean | - | Controlled open state |
onOpenChange | (open: boolean) => void | - | Open state changed |
sheetTitle | string | placeholder | Sheet title (native addition) |
maxVisibleChips | number | 3 | Multi mode: chips shown before collapsing the rest into +N (native addition) |
testID | string | - | Test id for the trigger |
accessibilityLabel | string | placeholder | Accessibility label for the trigger |
Differences from web
ref,id,nameandaria-invalidare dropped — useinvalid,testIDandaccessibilityLabel.maxHeightis replaced bydetent.sheetTitleandmaxVisibleChipsare native additions.
Translation keys
Copy resolves through useUiTranslation() (explicit prop → <UiTranslationProvider> → English fallback). Shared with web: ui.treeSelect.placeholder, ui.treeSelect.multiPlaceholder, ui.treeSelect.removeItem, ui.treeView.searchPlaceholder, ui.treeView.noItemsFound. Native additions: ui.treeSelect.done, ui.treeView.selected, ui.treeView.expand, ui.treeView.collapse, ui.treeView.directItems.
Components
| Component | Description |
|---|---|
TreeSelect | Trigger + tree bottom sheet |
Type Exports
| Type | Description |
|---|---|
TreeSelectProps | Props for TreeSelect |
TreeSelectValue | string | string[] | null | undefined |
TreeSelectDefaultExpanded | 'none' | 'all' | 'selected' | ReadonlyArray<string> |