Line data Source code
1 : import * as React from 'react';
2 :
3 : import { ChevronDownIcon, ChevronRightIcon } from './symbol_icon.js';
4 : import { classNames } from '../../utils/classnames.js';
5 : import * as styles from './tree_entry.module.css';
6 :
7 : /// The indentation of a tree level
8 : export const TREE_ENTRY_INDENT_PX = 16;
9 : /// The left padding of an entry at depth 0
10 1 : const TREE_ENTRY_PADDING_PX = 8;
11 :
12 : export interface TreeEntryProps extends Omit<React.HTMLAttributes<HTMLElement>, 'className' | 'style'> {
13 : /// The level of the entry, the root level is 0
14 : depth: number;
15 : /// Is the folder expanded? Files leave it undefined and get no caret.
16 : expanded?: boolean;
17 : /// The icon before the label, e.g. a folder or a file. Without one the label starts after the caret.
18 : icon?: React.ReactElement | null;
19 : /// The label of the entry, usually a TreeEntryName
20 : label: React.ReactNode;
21 : /// Is the entry selected?
22 : selected?: boolean;
23 : /// Additional classes, e.g. a grid with trailing columns
24 : className?: string;
25 : /// The trailing cells after the label
26 : children?: React.ReactNode;
27 : }
28 :
29 : /// An entry of a file tree: caret, icon and label in a padded, rounded card that highlights on hover.
30 : /// Clickable entries are buttons, folders toggle themselves with their onClick.
31 1 : export function TreeEntry({ depth, expanded, icon, label, selected, className, children, ...attributes }: TreeEntryProps): React.ReactElement {
32 1 : const Caret = expanded == null ? null : expanded ? ChevronDownIcon : ChevronRightIcon;
33 1 : const content = (
34 1 : <>
35 1 : <span className={classNames(styles.cell_name, icon == null && styles.cell_name_without_icon)}>
36 1 : <span className={styles.caret}>{Caret && <Caret size={12} />}</span>
37 1 : {icon != null && <span className={styles.icon} aria-hidden="true">{icon}</span>}
38 1 : {label}
39 1 : </span>
40 1 : {children}
41 1 : </>
42 : );
43 1 : const props = {
44 1 : ...attributes,
45 1 : className: classNames(styles.entry, attributes.onClick != null && styles.entry_interactive, selected && styles.entry_selected, className),
46 1 : style: { paddingLeft: TREE_ENTRY_PADDING_PX + depth * TREE_ENTRY_INDENT_PX },
47 1 : };
48 1 : return attributes.onClick == null
49 1 : ? <div {...props}>{content}</div>
50 1 : : <button type="button" aria-expanded={expanded} {...props}>{content}</button>;
51 1 : }
52 :
53 : export interface TreeEntryNameProps {
54 : /// The name of the entry
55 : children: string;
56 : /// The hover text, e.g. the full path
57 : title?: string;
58 : }
59 :
60 : /// The name of a tree entry, cut with an ellipsis instead of wrapping
61 1 : export function TreeEntryName(props: TreeEntryNameProps): React.ReactElement {
62 1 : return <span className={styles.name} title={props.title ?? props.children}>{props.children}</span>;
63 1 : }
|