Tree
Datos jerárquicos con nodos expandibles, selección y navegación por teclado.
Nuevosrc
components
button.tsx
card.tsx
tree.tsx
hooks
index.ts
package.json
tsconfig.json
1'use client';23import { Tree } from '@/components/ui/tree';45export function Default() {6 return (7 <Tree defaultExpanded={['src', 'src/components']} className="max-w-xs">8 <Tree.Item value="src" label="src">9 <Tree.Item value="src/components" label="components">10 <Tree.Item value="src/components/button.tsx" label="button.tsx" />11 <Tree.Item value="src/components/card.tsx" label="card.tsx" />12 <Tree.Item value="src/components/tree.tsx" label="tree.tsx" />13 </Tree.Item>14 <Tree.Item value="src/hooks" label="hooks">15 <Tree.Item value="src/hooks/use-local-storage.ts" label="use-local-storage.ts" />16 </Tree.Item>17 <Tree.Item value="src/index.ts" label="index.ts" />18 </Tree.Item>19 <Tree.Item value="package.json" label="package.json" />20 <Tree.Item value="tsconfig.json" label="tsconfig.json" />21 </Tree>22 );23}
Instalación
Copia y pega el siguiente código en tu proyecto.
1'use client';23import { AnimatePresence, motion, useReducedMotion } from 'motion/react';4import * as React from 'react';5import { cn } from '../lib/cn';6import { collapse } from '../lib/motion';7import { useControllableState } from '../lib/use-controllable-state';89type IconProps = React.SVGProps<SVGSVGElement> & {10 size?: number | string;11};1213function ChevronRightIcon({ size = 24, strokeWidth = 1.5, ...props }: IconProps) {14 return (15 <svg16 xmlns="http://www.w3.org/2000/svg"17 width={size}18 height={size}19 viewBox="0 0 24 24"20 fill="none"21 stroke="currentColor"22 strokeWidth={strokeWidth}23 strokeLinecap="round"24 strokeLinejoin="round"25 aria-hidden="true"26 {...props}27 >28 <path d="m9 6 6 6-6 6" />29 </svg>30 );31}3233function MinusIcon({ size = 24, strokeWidth = 1.5, ...props }: IconProps) {34 return (35 <svg36 xmlns="http://www.w3.org/2000/svg"37 width={size}38 height={size}39 viewBox="0 0 24 24"40 fill="none"41 stroke="currentColor"42 strokeWidth={strokeWidth}43 strokeLinecap="round"44 strokeLinejoin="round"45 aria-hidden="true"46 {...props}47 >48 <path d="M5 12h14" />49 </svg>50 );51}5253function PlusIcon({ size = 24, strokeWidth = 1.5, ...props }: IconProps) {54 return (55 <svg56 xmlns="http://www.w3.org/2000/svg"57 width={size}58 height={size}59 viewBox="0 0 24 24"60 fill="none"61 stroke="currentColor"62 strokeWidth={strokeWidth}63 strokeLinecap="round"64 strokeLinejoin="round"65 aria-hidden="true"66 {...props}67 >68 <path d="M12 5v14" />69 <path d="M5 12h14" />70 </svg>71 );72}7374type ToggleIcon = 'chevron' | 'plus-minus';7576interface TreeSnapshot {77 expanded: ReadonlySet<string>;78 selected: string | undefined;79}8081interface TreeStore {82 subscribe: (listener: () => void) => () => void;83 get: () => TreeSnapshot;84 set: (snapshot: TreeSnapshot) => void;85}8687function createTreeStore(initial: TreeSnapshot): TreeStore {88 let snapshot = initial;89 const listeners = new Set<() => void>();90 return {91 subscribe: (listener) => {92 listeners.add(listener);93 return () => listeners.delete(listener);94 },95 get: () => snapshot,96 set: (next) => {97 if (next.expanded === snapshot.expanded && next.selected === snapshot.selected) return;98 snapshot = next;99 for (const listener of listeners) listener();100 },101 };102}103104interface TreeContextValue {105 store: TreeStore;106 toggleExpanded: (value: string) => void;107 select: (value: string) => void;108 indent: number;109 toggleIcon: ToggleIcon;110 showLines: boolean;111}112113interface TreeProps {114 children: React.ReactNode;115 className?: string;116 expanded?: string[];117 defaultExpanded?: string[];118 onExpandedChange?: (expanded: string[]) => void;119 defaultSelected?: string;120 selected?: string;121 onSelectedChange?: (value: string) => void;122 indent?: number;123 toggleIcon?: ToggleIcon;124 showLines?: boolean;125 label?: string;126}127128interface TreeItemProps {129 value: string;130 label: React.ReactNode;131 icon?: React.ReactNode;132 iconOpen?: React.ReactNode;133 disabled?: boolean;134 onSelect?: () => void;135 children?: React.ReactNode;136 className?: string;137}138139const TreeContext = React.createContext<TreeContextValue | null>(null);140const DepthContext = React.createContext(0);141142const useTreeContext = (): TreeContextValue => {143 const context = React.use(TreeContext);144 if (!context) throw new Error('Tree components must be used within Tree');145 return context;146};147148const EMPTY_EXPANDED: string[] = [];149const TYPEAHEAD_RESET_MS = 500;150151function isHidden(item: HTMLElement, root: HTMLElement) {152 const inert = item.parentElement?.closest('[inert]');153 return inert ? root.contains(inert) : false;154}155156function getVisibleItems(root: HTMLElement | null): HTMLElement[] {157 if (!root) return [];158 return Array.from(root.querySelectorAll<HTMLElement>('[role="treeitem"]')).filter(159 (item) => !isHidden(item, root),160 );161}162163function getRow(item: HTMLElement) {164 return item.querySelector<HTMLElement>(':scope > [data-tree-row]');165}166167function getLabelText(item: HTMLElement) {168 const label = item.querySelector(':scope > [data-tree-row] > [data-tree-label]');169 return (label?.textContent ?? '').trim().toLowerCase();170}171172function getParentItem(item: HTMLElement, root: HTMLElement): HTMLElement | null {173 const parent = item.parentElement?.closest<HTMLElement>('[role="treeitem"]');174 return parent && root.contains(parent) ? parent : null;175}176177const TreeRoot = ({178 children,179 className,180 expanded: controlledExpanded,181 defaultExpanded = EMPTY_EXPANDED,182 onExpandedChange,183 defaultSelected,184 selected: controlledSelected,185 onSelectedChange,186 indent = 16,187 toggleIcon = 'chevron',188 showLines = false,189 label = 'Tree',190}: TreeProps): React.JSX.Element => {191 const [expandedList, setExpandedList] = useControllableState<string[]>({192 value: controlledExpanded,193 defaultValue: defaultExpanded,194 onChange: onExpandedChange,195 });196 const [selected, select] = useControllableState<string | undefined>({197 value: controlledSelected,198 defaultValue: defaultSelected,199 onChange: (value) => {200 if (value !== undefined) onSelectedChange?.(value);201 },202 });203 const expanded = React.useMemo<ReadonlySet<string>>(() => new Set(expandedList), [expandedList]);204 const [store] = React.useState(() => createTreeStore({ expanded, selected }));205 const rootRef = React.useRef<HTMLDivElement>(null);206 const tabStopRef = React.useRef<HTMLElement | null>(null);207 const typeaheadRef = React.useRef({ query: '', at: 0 });208 const focusInsideRef = React.useRef(false);209210 React.useLayoutEffect(() => {211 store.set({ expanded, selected });212 }, [store, expanded, selected]);213214 const toggleExpanded = React.useCallback(215 (value: string) => {216 setExpandedList((previous) =>217 previous.includes(value) ? previous.filter((item) => item !== value) : [...previous, value],218 );219 },220 [setExpandedList],221 );222223 const expandMany = React.useCallback(224 (values: string[]) => {225 setExpandedList((previous) => {226 const added = values.filter((value) => !previous.includes(value));227 return added.length > 0 ? [...previous, ...added] : previous;228 });229 },230 [setExpandedList],231 );232233 const moveTabStop = React.useCallback((item: HTMLElement) => {234 const previous = tabStopRef.current;235 if (previous && previous !== item) previous.tabIndex = -1;236 item.tabIndex = 0;237 tabStopRef.current = item;238 }, []);239240 const syncTabStop = React.useCallback(() => {241 const root = rootRef.current;242 if (!root) return;243 const items = getVisibleItems(root);244 const active = document.activeElement;245 const stop =246 items.find((item) => item === active) ??247 items.find((item) => item.getAttribute('aria-selected') === 'true') ??248 items.find((item) => item === tabStopRef.current) ??249 items[0];250 for (const item of items) {251 if (item !== stop && item.tabIndex !== -1) item.tabIndex = -1;252 }253 const previous = tabStopRef.current;254 if (previous && previous !== stop) previous.tabIndex = -1;255 if (stop) stop.tabIndex = 0;256 tabStopRef.current = stop ?? null;257 }, []);258259 const handleKeyDown = React.useCallback(260 (e: React.KeyboardEvent<HTMLDivElement>) => {261 const root = rootRef.current;262 const target = e.target as HTMLElement;263 if (!root || target.getAttribute('role') !== 'treeitem') return;264265 const items = getVisibleItems(root);266 if (items.length === 0) return;267 const currentIndex = items.indexOf(target);268 const isDisabled = target.getAttribute('aria-disabled') === 'true';269270 let nextIndex: number | null = null;271 if (e.key === 'ArrowDown') {272 nextIndex = Math.min(currentIndex + 1, items.length - 1);273 } else if (e.key === 'ArrowUp') {274 nextIndex = Math.max(currentIndex - 1, 0);275 } else if (e.key === 'Home') {276 nextIndex = 0;277 } else if (e.key === 'End') {278 nextIndex = items.length - 1;279 } else if (e.key === 'ArrowLeft') {280 if (target.getAttribute('aria-expanded') === 'true' && !isDisabled) {281 e.preventDefault();282 getRow(target)?.click();283 return;284 }285 const parent = getParentItem(target, root);286 if (parent) {287 e.preventDefault();288 parent.focus();289 }290 return;291 } else if (e.key === 'ArrowRight') {292 if (target.getAttribute('aria-expanded') === 'false' && !isDisabled) {293 e.preventDefault();294 getRow(target)?.click();295 return;296 }297 if (target.getAttribute('aria-expanded') === 'true') {298 nextIndex = Math.min(currentIndex + 1, items.length - 1);299 }300 } else if (e.key === '*') {301 e.preventDefault();302 const siblings = Array.from(target.parentElement?.children ?? []).filter(303 (item): item is HTMLElement =>304 item instanceof HTMLElement &&305 item.getAttribute('role') === 'treeitem' &&306 item.getAttribute('aria-expanded') === 'false' &&307 item.getAttribute('aria-disabled') !== 'true',308 );309 expandMany(siblings.flatMap((item) => (item.dataset.value ? [item.dataset.value] : [])));310 return;311 } else if (e.key.length === 1 && e.key !== ' ' && !e.ctrlKey && !e.metaKey && !e.altKey) {312 const typeahead = typeaheadRef.current;313 const now = Date.now();314 const key = e.key.toLowerCase();315 typeahead.query = now - typeahead.at > TYPEAHEAD_RESET_MS ? key : typeahead.query + key;316 typeahead.at = now;317 const repeated = typeahead.query.split('').every((char) => char === key);318 const query = repeated ? key : typeahead.query;319 const start = repeated ? currentIndex + 1 : currentIndex;320 const ordered = [...items.slice(start), ...items.slice(0, start)];321 const match = ordered.find((item) => getLabelText(item).startsWith(query));322 if (match) {323 e.preventDefault();324 match.focus();325 }326 return;327 }328329 if (nextIndex !== null && nextIndex !== currentIndex) {330 e.preventDefault();331 items[nextIndex]?.focus();332 }333 },334 [expandMany],335 );336337 React.useLayoutEffect(() => {338 if (rootRef.current?.contains(document.activeElement)) return;339 syncTabStop();340 }, [selected, syncTabStop]);341342 React.useEffect(() => {343 const root = rootRef.current;344 if (!root || typeof MutationObserver === 'undefined') return;345 const observer = new MutationObserver((records) => {346 const stop = tabStopRef.current;347 if (stop && !stop.isConnected) {348 const active = document.activeElement;349 if (focusInsideRef.current && (!active || active === document.body)) {350 const record = records.find((entry) =>351 Array.from(entry.removedNodes).some((node) => node.contains(stop)),352 );353 const owner =354 record?.target instanceof HTMLElement355 ? record.target.closest<HTMLElement>('[role="treeitem"]')356 : null;357 if (owner && root.contains(owner) && !isHidden(owner, root)) {358 owner.focus();359 return;360 }361 }362 }363 if (!stop || !stop.isConnected || isHidden(stop, root)) syncTabStop();364 });365 observer.observe(root, {366 childList: true,367 subtree: true,368 attributes: true,369 attributeFilter: ['inert'],370 });371 return () => observer.disconnect();372 }, [syncTabStop]);373374 const contextValue = React.useMemo(375 () => ({ store, toggleExpanded, select, indent, toggleIcon, showLines }),376 [store, toggleExpanded, select, indent, toggleIcon, showLines],377 );378379 return (380 <TreeContext value={contextValue}>381 <div382 ref={rootRef}383 role="tree"384 tabIndex={-1}385 aria-label={label}386 onKeyDown={handleKeyDown}387 onFocus={(e) => {388 focusInsideRef.current = true;389 const target = e.target as HTMLElement;390 if (target.getAttribute('role') === 'treeitem') moveTabStop(target);391 else if (!tabStopRef.current?.isConnected) syncTabStop();392 }}393 onBlur={(e) => {394 if (e.currentTarget.contains(e.relatedTarget as Node | null)) return;395 if ((e.target as Node).isConnected) focusInsideRef.current = false;396 }}397 className={cn('w-full text-sm', className)}398 >399 {children}400 </div>401 </TreeContext>402 );403};404405const TreeItem = ({406 value,407 label,408 icon,409 iconOpen,410 disabled = false,411 onSelect,412 children,413 className,414}: TreeItemProps): React.JSX.Element => {415 const { store, toggleExpanded, select, indent, toggleIcon, showLines } = useTreeContext();416 const depth = React.use(DepthContext);417 const shouldReduceMotion = useReducedMotion();418 const labelId = React.useId();419 const itemRef = React.useRef<HTMLDivElement>(null);420 const groupRef = React.useRef<HTMLDivElement>(null);421422 const hasChildren = React.Children.count(children) > 0;423 const getOpen = () => store.get().expanded.has(value);424 const getSelected = () => store.get().selected === value;425 const isOpen = React.useSyncExternalStore(store.subscribe, getOpen, getOpen);426 const isSelected = React.useSyncExternalStore(store.subscribe, getSelected, getSelected);427 const showSelected = !hasChildren && isSelected;428429 React.useLayoutEffect(() => {430 const group = groupRef.current;431 if (!group) return;432 if (!isOpen && group.contains(document.activeElement)) itemRef.current?.focus();433 group.toggleAttribute('inert', !isOpen);434 }, [isOpen]);435436 const handleActivate = () => {437 if (disabled) return;438 if (hasChildren) {439 toggleExpanded(value);440 return;441 }442 select(value);443 onSelect?.();444 };445446 const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {447 const row = (e.target as Element).closest('[data-tree-row]');448 if (row?.parentElement !== e.currentTarget) return;449 handleActivate();450 };451452 const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {453 if (e.target !== e.currentTarget) return;454 if (e.key === 'Enter' || e.key === ' ') {455 e.preventDefault();456 handleActivate();457 }458 };459460 const resolvedIcon = isOpen && iconOpen !== undefined ? iconOpen : icon;461462 return (463 <div464 ref={itemRef}465 role="treeitem"466 tabIndex={-1}467 aria-labelledby={labelId}468 aria-expanded={hasChildren ? isOpen : undefined}469 aria-selected={hasChildren ? undefined : showSelected}470 aria-level={depth + 1}471 aria-disabled={disabled || undefined}472 data-tree-item=""473 data-value={value}474 onClick={handleClick}475 onKeyDown={handleKeyDown}476 className="[&:focus-visible>[data-tree-row]]:ring-ring focus-visible:outline-none [&:focus-visible>[data-tree-row]]:ring-1"477 >478 <div479 data-tree-row=""480 className={cn(481 'flex items-center gap-1.5 rounded-md px-1.5 py-1 select-none',482 'transition-colors motion-reduce:transition-none',483 disabled ? 'cursor-not-allowed opacity-40' : 'hover:bg-muted cursor-pointer',484 showSelected && 'bg-muted text-foreground font-medium',485 className,486 )}487 >488 {hasChildren ? (489 <span490 aria-hidden="true"491 className="text-muted-foreground flex size-4 shrink-0 items-center justify-center"492 >493 {toggleIcon === 'plus-minus' ? (494 isOpen ? (495 <MinusIcon size={12} />496 ) : (497 <PlusIcon size={12} />498 )499 ) : (500 <span501 className={cn(502 'transition-transform duration-200 motion-reduce:transition-none',503 isOpen && 'rotate-90',504 )}505 >506 <ChevronRightIcon size={16} />507 </span>508 )}509 </span>510 ) : (511 <span aria-hidden="true" className="size-4 shrink-0" />512 )}513514 {resolvedIcon !== undefined && (515 <span516 aria-hidden="true"517 className="text-muted-foreground flex shrink-0 items-center [&_svg]:size-4"518 >519 {resolvedIcon}520 </span>521 )}522523 <span id={labelId} data-tree-label="" className="min-w-0 flex-1 truncate">524 {label}525 </span>526 </div>527528 {hasChildren && (529 <AnimatePresence initial={false}>530 {isOpen && (531 <motion.div532 ref={groupRef}533 data-tree-group=""534 role="group"535 initial={shouldReduceMotion ? false : 'closed'}536 animate="open"537 exit={shouldReduceMotion ? undefined : 'closed'}538 variants={collapse}539 className={cn('ml-3.5 overflow-hidden', showLines && 'border-border border-l')}540 style={{ paddingLeft: Math.max(indent - 8, 4) }}541 >542 <DepthContext value={depth + 1}>{children}</DepthContext>543 </motion.div>544 )}545 </AnimatePresence>546 )}547 </div>548 );549};550551const Tree = Object.assign(TreeRoot, {552 Item: TreeItem,553});554555export { Tree };556export type { TreeItemProps, TreeProps };
Actualiza las rutas de importación para que coincidan con la estructura de tu proyecto.
Anatomía
1import { Tree } from '@/components/ui/tree';
1<Tree defaultExpanded={['src']}>2 <Tree.Item value="src" label="src">3 <Tree.Item value="src/index.ts" label="index.ts" />4 </Tree.Item>5 <Tree.Item value="package.json" label="package.json" />6</Tree>
Un item con hijos es una rama: al hacer clic alterna la expansión. Un item sin hijos es una hoja: al hacer clic se selecciona y dispara
onSelectedChange en la raíz. Las flechas recorren las filas visibles, ArrowRight expande, ArrowLeft colapsa o salta al padre, Home y End van a los extremos.Composición
Usa la siguiente composición para construir un
Tree:Tree└── Tree.Item└── Tree.Item
Variantes
Líneas
showLines dibuja una guía a lo largo de cada nivel.app
docs
elements
page.tsx
layout.tsx
page.tsx
layout.tsx
globals.css
proxy.ts
1'use client';23import { Tree } from '@/components/ui/tree';45export function Lines() {6 return (7 <Tree showLines defaultExpanded={['app', 'app/docs', 'app/docs/elements']} className="max-w-xs">8 <Tree.Item value="app" label="app">9 <Tree.Item value="app/docs" label="docs">10 <Tree.Item value="app/docs/elements" label="elements">11 <Tree.Item value="app/docs/elements/page.tsx" label="page.tsx" />12 <Tree.Item value="app/docs/elements/layout.tsx" label="layout.tsx" />13 </Tree.Item>14 <Tree.Item value="app/docs/page.tsx" label="page.tsx" />15 </Tree.Item>16 <Tree.Item value="app/layout.tsx" label="layout.tsx" />17 <Tree.Item value="app/globals.css" label="globals.css" />18 </Tree.Item>19 <Tree.Item value="proxy.ts" label="proxy.ts" />20 </Tree>21 );22}
Iconos
Pasa
icon en cada nodo y iconOpen en las ramas que cambian al expandirse, como las carpetas.packages
ui
tree.tsx
frame.tsx
cli
turbo.json
1'use client';23import { Tree } from '@/components/ui/tree';4import { FileIcon } from '@/components/ui/file';5import { FolderIcon } from '@/components/ui/folder';6import { FolderOpenIcon } from '@/components/ui/folder-open';78const folderClosed = <FolderIcon size={16} />;9const folderOpen = <FolderOpenIcon size={16} />;10const file = <FileIcon size={16} />;1112export function Icons() {13 return (14 <Tree defaultExpanded={['packages', 'packages/ui']} className="max-w-xs">15 <Tree.Item value="packages" label="packages" icon={folderClosed} iconOpen={folderOpen}>16 <Tree.Item value="packages/ui" label="ui" icon={folderClosed} iconOpen={folderOpen}>17 <Tree.Item value="packages/ui/tree.tsx" label="tree.tsx" icon={file} />18 <Tree.Item value="packages/ui/frame.tsx" label="frame.tsx" icon={file} />19 </Tree.Item>20 <Tree.Item value="packages/cli" label="cli" icon={folderClosed} iconOpen={folderOpen}>21 <Tree.Item value="packages/cli/index.ts" label="index.ts" icon={file} />22 </Tree.Item>23 </Tree.Item>24 <Tree.Item value="turbo.json" label="turbo.json" icon={file} />25 </Tree>26 );27}
Más y menos
toggleIcon="plus-minus" cambia el chevron por conmutadores de más y menos.content
docs
en
es
README.md
1'use client';23import { Tree } from '@/components/ui/tree';4import { FileIcon } from '@/components/ui/file';5import { FolderIcon } from '@/components/ui/folder';6import { FolderOpenIcon } from '@/components/ui/folder-open';78const folderClosed = <FolderIcon size={16} />;9const folderOpen = <FolderOpenIcon size={16} />;10const file = <FileIcon size={16} />;1112export function PlusMinus() {13 return (14 <Tree15 toggleIcon="plus-minus"16 showLines17 defaultExpanded={['content', 'content/docs']}18 className="max-w-xs"19 >20 <Tree.Item value="content" label="content" icon={folderClosed} iconOpen={folderOpen}>21 <Tree.Item value="content/docs" label="docs" icon={folderClosed} iconOpen={folderOpen}>22 <Tree.Item value="content/docs/en" label="en" icon={folderClosed} iconOpen={folderOpen}>23 <Tree.Item value="content/docs/en/tree.mdx" label="tree.mdx" icon={file} />24 </Tree.Item>25 <Tree.Item value="content/docs/es" label="es" icon={folderClosed} iconOpen={folderOpen}>26 <Tree.Item value="content/docs/es/tree.mdx" label="tree.mdx" icon={file} />27 </Tree.Item>28 </Tree.Item>29 </Tree.Item>30 <Tree.Item value="README.md" label="README.md" icon={file} />31 </Tree>32 );33}
Teclado
| Tecla | Acción |
|---|---|
ArrowDown / ArrowUp | Va al ítem visible siguiente o anterior |
ArrowRight | Abre una rama cerrada o entra en una abierta |
ArrowLeft | Cierra una rama abierta o vuelve al padre |
Home / End | Va al primer o al último ítem visible |
Enter / Space | Selecciona una hoja o abre y cierra una rama |
* | Abre todas las ramas del mismo nivel |
| Letras | Va al siguiente ítem cuya etiqueta empieza con lo que escribiste |
Referencia de API
Tree
| Prop | Type | Default | Description |
|---|---|---|---|
expanded | string[] | - | Ramas expandidas en modo controlado |
defaultExpanded | string[] | [] | Ramas expandidas al montar |
onExpandedChange | (expanded: string[]) => void | - | Callback cuando una rama se abre o se cierra |
defaultSelected | string | - | Hoja seleccionada inicialmente |
selected | string | - | Hoja seleccionada en modo controlado |
onSelectedChange | (value: string) => void | - | Callback al seleccionar una hoja |
indent | number | 16 | Píxeles de sangría por nivel |
toggleIcon | 'chevron' | 'plus-minus' | 'chevron' | Estilo del icono de expandir y colapsar |
showLines | boolean | false | Dibuja guías entre niveles |
label | string | 'Tree' | Nombre accesible del árbol |
className | string | - | Clases CSS adicionales |
Tree.Item
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | Valor único, usado para expansión y selección |
label | ReactNode | - | Contenido de la fila |
icon | ReactNode | - | Icono inicial |
iconOpen | ReactNode | - | Icono mientras la rama está expandida |
disabled | boolean | false | Deshabilita la fila. Se puede enfocar con las flechas, pero no se selecciona ni se abre |
onSelect | () => void | - | Callback al seleccionar esta hoja |
className | string | - | Clases CSS adicionales |
¿Encontraste algo que mejorar?
¿Notaste un error, tipografía o detalle faltante en esta página? Ayúdanos a mejorar la documentación abriendo un issue en GitHub.