# rn-tree-view URL: /docs/native/docyrus/tree-view Hierarchical tree with search, selection, checkboxes, long-press actions and drag-and-drop reordering — API-aligned with the web TreeView. ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/rn-tree-view ``` **Dependencies:** - [react-native-gesture-handler](https://www.npmjs.com/package/react-native-gesture-handler) - [react-native-reanimated](https://www.npmjs.com/package/react-native-reanimated) - [tailwind-variants](https://www.npmjs.com/package/tailwind-variants) ## Usage ```tsx import { useState } from 'react'; import { TreeView, type TreeViewItem, type TreeViewMenuItem } from '@/components/docyrus-native/tree-view'; const menuItems: TreeViewMenuItem[] = [ { id: 'open', label: 'Open', icon: 'fal eye', action: items => console.log(items) } ]; export function FilesTree({ initial }: { initial: TreeViewItem[] }) { const [data, setData] = useState(initial); return ( console.log(items.map(i => i.id))} onCheckChange={(item, checked) => setData(prev => setChecked(prev, item.id, checked))} onDataChange={nextData => setData(nextData)} /> ); } ``` ### Interaction model | Gesture | Result | |---------|--------| | Tap row | Fires `onItemClick`; replaces the selection (toggles it with `multiSelect`); a folder expands when it is already selected, when `expandFolderOnRowClick` is set, or when `selectOnRowClick` is `false` | | Tap chevron | Expands / collapses the folder without selecting it | | Tap checkbox | Fires `onCheckChange(item, checked)` — the tree reads `item.checked` from `data`, so the host applies the change (same contract as web) | | Long-press (non-draggable) | Opens the row action sheet: `menuItems`, **Select / Deselect** (adds to the selection — the mobile stand-in for ⌘/Ctrl-click), **Move to…** (when draggable), **Details** (when `showInfo`) | | Long-press + drag (`draggable`) | Lifts the row; dragging over rows shows a before / after line or an *inside* highlight (inside only when `canDropInside` allows, never into the item's own subtree). Hovering *inside* a collapsed folder opens it; the tree's own scroller (with `maxHeight`) auto-scrolls at its edges. Releasing below the last row moves the item to the root. Drop calls `moveTreeViewItem` and fires `onDataChange` | | Long-press + release without moving (`draggable`) | Opens the row action sheet | **Move to…** is the accessible fallback for drag and drop: it lists every valid target (the moved item and its descendants are excluded) plus **Top level**, and offers **Before / Inside / After** for the picked target. It fires the same `onDataChange(nextData, details)` contract. Drag is disabled while a search query is active (web parity). `onSelectionChange` is emitted **only from user interaction** (tap, action sheet, clear) — never because a callback identity or `data` changed — so inline handlers cannot cause update loops. ## API Reference ### TreeViewProps | Prop | Type | Default | Description | |------|------|---------|-------------| | `data` | `TreeViewItem[]` | - | Tree data. The tree keeps an internal copy (so drag moves apply immediately) that is re-seeded whenever `data` changes | | `variant` | `'default' \| 'outline' \| 'ghost'` | `'default'` | Container style | | `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Row height and text size | | `showExpandAll` | `boolean` | `true` | Show the expand / collapse all toggle next to the search input | | `showCheckboxes` | `boolean` | `false` | Show checkboxes next to items (leaf state from `item.checked`, folder state derived from children incl. indeterminate) | | `checkboxPosition` | `'left' \| 'right'` | `'left'` | Checkbox position | | `searchPlaceholder` | `string` | `t('ui.treeView.searchPlaceholder', 'Search...')` | Search input placeholder | | `selectionText` | `string` | `t('ui.treeView.selected', 'selected')` | Text after the selection count ("3 selected") | | `checkboxLabels` | `{ check: string; uncheck: string }` | `{ check: 'Check', uncheck: 'Uncheck' }` (translated) | Labels of the bulk check / uncheck buttons shown in the selection toolbar when `showCheckboxes` | | `getIcon` | `(item: TreeViewItem, depth: number) => ReactNode` | - | Custom icon renderer (wins over `iconMap`) | | `iconMap` | `TreeViewIconMap` | `{}` | `item.type` → icon. A string renders as a `DocyrusIcon` identifier (`'fal folder'`), any other node renders as-is | | `onSelectionChange` | `(selectedItems: TreeViewItem[]) => void` | - | Selection changed (items in tree order) | | `onCheckChange` | `(item: TreeViewItem, checked: boolean) => void` | - | A checkbox or bulk check / uncheck changed an item (bulk calls it for every selected item and its descendants) | | `menuItems` | `TreeViewMenuItem[]` | - | Long-press action sheet items | | `onAction` | `(action: string, items: TreeViewItem[]) => void` | - | Fired after a menu item's `action` with the menu item `id` | | `draggable` | `boolean` | `false` | Enable long-press drag reorder / re-parenting and the "Move to…" action | | `canDropInside` | `(item: TreeViewItem) => boolean` | `canTreeViewItemAcceptChildren` | Whether an item can accept children (default: it has a `children` array) | | `onDataChange` | `(nextData: TreeViewItem[], details: TreeViewMoveDetails) => void` | - | Tree changed by drag or "Move to…" | | `showInfo` | `boolean` | `true` | Offer a **Details** entry (type, id, location path, direct item count) in the long-press sheet | | `maxHeight` | `number \| string` | - | Max height before the tree scrolls in its own scroller — `420`, `'420px'` or `'60%'` | | `defaultExpandedIds` | `ReadonlyArray` | - | Ids expanded on mount (uncontrolled — later changes do not re-seed) | | `expandFolderOnRowClick` | `boolean` | `false` | Tapping a folder row toggles it on the first tap, not only when it is already selected | | `onItemClick` | `(item: TreeViewItem) => void` | - | Row tapped — always fired, independent of `selectOnRowClick` | | `selectOnRowClick` | `boolean` | `true` | When `false`, a row tap never changes the selection (picker mode) — it only expands folders and fires `onItemClick`; the Select entry is hidden | | `multiSelect` | `boolean` | `false` | Native adaptation of web's ⌘/Ctrl-click: a row tap toggles the row in the selection instead of replacing it | | `longPressDelay` | `number` | `400` | Long-press duration (ms) before a row lifts for dragging / the action sheet opens | | `className` | `string` | - | Additional classes for the root container | | `testID` | `string` | - | Test id for the root container | ### TreeViewItem | Field | Type | Default | Description | |-------|------|---------|-------------| | `id` | `string` | - | Unique identifier | | `name` | `string` | - | Display name (also searched) | | `type` | `string` | - | Item type — keys `iconMap`, shown in Details | | `children` | `TreeViewItem[]` | - | Child items. An array (even empty) makes the item a folder | | `checked` | `boolean` | - | Checked state for leaves (folders derive theirs) | | `draggable` | `boolean` | - | `false` excludes the item from drag / "Move to…" | | `[key: string]` | `unknown` | - | Any extra payload | ### TreeViewMenuItem | Field | Type | Description | |-------|------|-------------| | `id` | `string` | Action id passed to `onAction` | | `label` | `string` | Action label | | `icon` | `ReactNode` | A `DocyrusIcon` string (`'fal eye'`) is shown in the action sheet; other nodes are ignored on native | | `action` | `(items: TreeViewItem[]) => void` | Receives the whole selection when the long-pressed row is selected, otherwise just that row | ### TreeViewMoveDetails | Field | Type | Description | |-------|------|-------------| | `activeItem` | `TreeViewItem` | The moved item | | `overItem` | `TreeViewItem \| null` | The target item (`null` for `root`) | | `parentItem` | `TreeViewItem \| null` | The new parent (`null` at root level) | | `position` | `TreeViewDropPosition` | `'before' \| 'inside' \| 'after' \| 'root'` | ## Helpers Pure helpers synced byte-for-byte with web `@docyrus/ui` — use them to apply moves or validate targets outside the component. | Export | Signature | Description | |--------|-----------|-------------| | `moveTreeViewItem` | `(items, { activeId, overId, position, canDropInside? }) => { nextData, details } \| null` | Immutable move; returns `null` for invalid moves (onto itself, into its own subtree, inside a non-container) | | `canTreeViewItemAcceptChildren` | `(item) => boolean` | Default `canDropInside` — `true` when `children` is an array | | `treeItemContainsId` | `(item, targetId) => boolean` | `true` when `targetId` is the item or one of its descendants | | `treeViewVariants` | `tv()` → `string` | Root container classes (`variant`, `size`) | ## Translation keys Copy resolves through `useUiTranslation()` (explicit prop → `` → English fallback). Shared with web: `ui.treeView.searchPlaceholder`, `ui.treeView.selected`, `ui.treeView.check`, `ui.treeView.uncheck`, `ui.treeView.clearSelection`, `ui.treeView.noItemsFound`. Native additions: `ui.treeView.expandAll`, `ui.treeView.collapseAll`, `ui.treeView.clearSearch`, `ui.treeView.select`, `ui.treeView.deselect`, `ui.treeView.moveTo`, `ui.treeView.moveToRoot`, `ui.treeView.moveBefore`, `ui.treeView.moveInside`, `ui.treeView.moveAfter`, `ui.treeView.details`, `ui.treeView.close`, `ui.treeView.infoType`, `ui.treeView.infoId`, `ui.treeView.infoLocation`, `ui.treeView.infoItems`, `ui.treeView.directItems`. ## Components | Component | Description | |-----------|-------------| | `TreeView` | Main component | ## Type Exports | Type | Description | |------|-------------| | `TreeViewProps` | Props for `TreeView` | | `TreeViewItem` | Tree node | | `TreeViewIconMap` | `item.type` → icon map | | `TreeViewMenuItem` | Long-press action | | `TreeViewMoveDetails` | Payload of `onDataChange` | | `TreeViewDropPosition` | `'before' \| 'inside' \| 'after' \| 'root'` | | `TreeViewCheckboxPosition` | `'left' \| 'right'` | | `TreeViewVariant` | `'default' \| 'outline' \| 'ghost'` | | `TreeViewSize` | `'sm' \| 'default' \| 'lg'` |