panelui-native 0.46.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/lib/module/components/accordion/index.js +32 -4
- package/lib/module/components/accordion/index.js.map +1 -1
- package/lib/module/components/button/index.js +83 -15
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/button-group/index.js +186 -0
- package/lib/module/components/button-group/index.js.map +1 -0
- package/lib/module/components/color-picker/index.js +110 -1
- package/lib/module/components/color-picker/index.js.map +1 -1
- package/lib/module/components/date-time-picker/index.js +23 -5
- package/lib/module/components/date-time-picker/index.js.map +1 -1
- package/lib/module/components/fab/index.js +514 -0
- package/lib/module/components/fab/index.js.map +1 -0
- package/lib/module/components/markdown-editor/index.js +406 -0
- package/lib/module/components/markdown-editor/index.js.map +1 -0
- package/lib/module/components/markdown-editor/markdown-transforms.js +243 -0
- package/lib/module/components/markdown-editor/markdown-transforms.js.map +1 -0
- package/lib/module/components/questionnaire/index.js +1312 -0
- package/lib/module/components/questionnaire/index.js.map +1 -0
- package/lib/module/components/tabs/index.js +94 -18
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/time-picker/index.js +34 -6
- package/lib/module/components/time-picker/index.js.map +1 -1
- package/lib/module/components/tree/index.js +500 -0
- package/lib/module/components/tree/index.js.map +1 -0
- package/lib/module/icons/index.js +217 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +6 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts +21 -0
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts +21 -0
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button-group/index.d.ts +212 -0
- package/lib/typescript/src/components/button-group/index.d.ts.map +1 -0
- package/lib/typescript/src/components/color-picker/index.d.ts +82 -1
- package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/date-time-picker/index.d.ts +7 -1
- package/lib/typescript/src/components/date-time-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/fab/index.d.ts +285 -0
- package/lib/typescript/src/components/fab/index.d.ts.map +1 -0
- package/lib/typescript/src/components/markdown-editor/index.d.ts +102 -0
- package/lib/typescript/src/components/markdown-editor/index.d.ts.map +1 -0
- package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts +76 -0
- package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts.map +1 -0
- package/lib/typescript/src/components/questionnaire/index.d.ts +336 -0
- package/lib/typescript/src/components/questionnaire/index.d.ts.map +1 -0
- package/lib/typescript/src/components/tabs/index.d.ts +9 -0
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/index.d.ts +19 -1
- package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tree/index.d.ts +125 -0
- package/lib/typescript/src/components/tree/index.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +22 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +8 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/accordion/index.tsx +48 -6
- package/src/components/button/index.tsx +97 -15
- package/src/components/button-group/index.tsx +199 -0
- package/src/components/color-picker/index.tsx +140 -3
- package/src/components/date-time-picker/index.tsx +39 -3
- package/src/components/fab/index.tsx +583 -0
- package/src/components/markdown-editor/index.tsx +526 -0
- package/src/components/markdown-editor/markdown-transforms.ts +228 -0
- package/src/components/questionnaire/index.tsx +1615 -0
- package/src/components/tabs/index.tsx +100 -28
- package/src/components/time-picker/index.tsx +42 -6
- package/src/components/tree/index.tsx +564 -0
- package/src/icons/index.tsx +154 -0
- package/src/index.ts +77 -1
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree — a hierarchy you can open a level at a time.
|
|
3
|
+
*
|
|
4
|
+
* The shape behind a file browser, a folder of settings, a category picker, a
|
|
5
|
+
* table of contents: rows that contain other rows, where a parent's children
|
|
6
|
+
* only exist on screen once you ask for them. Accordion is the one-level
|
|
7
|
+
* version of this and stops there — its items cannot hold items. A tree's can,
|
|
8
|
+
* to any depth, and everything that follows from that is what this component
|
|
9
|
+
* owns: which node a row sits under, how far in it is drawn, whether it is a
|
|
10
|
+
* branch at all, and which of its ancestors are open.
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <Tree defaultExpanded={['src']} selectionMode="single">
|
|
14
|
+
* <Tree.Item value="src">
|
|
15
|
+
* <Tree.Trigger>
|
|
16
|
+
* <Tree.Indicator />
|
|
17
|
+
* <Tree.Label>src</Tree.Label>
|
|
18
|
+
* </Tree.Trigger>
|
|
19
|
+
* <Tree.Group>
|
|
20
|
+
* <Tree.Item value="src/index.ts">
|
|
21
|
+
* <Tree.Trigger>
|
|
22
|
+
* <Tree.Indicator />
|
|
23
|
+
* <Tree.Label>index.ts</Tree.Label>
|
|
24
|
+
* </Tree.Trigger>
|
|
25
|
+
* </Tree.Item>
|
|
26
|
+
* </Tree.Group>
|
|
27
|
+
* </Tree.Item>
|
|
28
|
+
* </Tree>
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* A closed branch renders nothing below it — the subtree is unmounted, not
|
|
32
|
+
* hidden — so the cost of a tree is what is open in it rather than what is in
|
|
33
|
+
* it. A folder of ten thousand files that nobody has opened costs one row.
|
|
34
|
+
*
|
|
35
|
+
* An item is a branch because it holds a `Tree.Group`, not because it was
|
|
36
|
+
* declared one, so there is no second fact to keep true. The exception is a
|
|
37
|
+
* branch whose children have not been fetched yet: it has no group to be
|
|
38
|
+
* detected by, so it says `hasChildren` and gets its chevron, and the fetch
|
|
39
|
+
* hangs off `onExpandedChange`.
|
|
40
|
+
*
|
|
41
|
+
* Expansion and selection are separate pieces of state because they answer
|
|
42
|
+
* separate questions — which parts of the hierarchy are open, and which row is
|
|
43
|
+
* the chosen one — and a tree commonly needs one without the other. Either can
|
|
44
|
+
* be controlled or left alone.
|
|
45
|
+
*/
|
|
46
|
+
import {
|
|
47
|
+
Children,
|
|
48
|
+
createContext,
|
|
49
|
+
forwardRef,
|
|
50
|
+
isValidElement,
|
|
51
|
+
useCallback,
|
|
52
|
+
useContext,
|
|
53
|
+
useMemo,
|
|
54
|
+
useState,
|
|
55
|
+
type ReactNode,
|
|
56
|
+
} from 'react';
|
|
57
|
+
import { Pressable, View, type Text as RNText, type ViewProps } from 'react-native';
|
|
58
|
+
import Animated, {
|
|
59
|
+
LinearTransition,
|
|
60
|
+
useAnimatedStyle,
|
|
61
|
+
useDerivedValue,
|
|
62
|
+
withTiming,
|
|
63
|
+
} from 'react-native-reanimated';
|
|
64
|
+
import { tv } from 'tailwind-variants';
|
|
65
|
+
import { useCSSVariable } from 'uniwind';
|
|
66
|
+
import { ChevronRightIcon, IconColorProvider } from '../../icons';
|
|
67
|
+
import { useDirection } from '../../hooks/use-direction';
|
|
68
|
+
import { Text, type TextProps } from '../../primitives/text';
|
|
69
|
+
|
|
70
|
+
export type TreeSize = 'sm' | 'default';
|
|
71
|
+
|
|
72
|
+
/** `none` makes the rows expanders only — nothing is ever the chosen one. */
|
|
73
|
+
export type TreeSelectionMode = 'none' | 'single' | 'multiple';
|
|
74
|
+
|
|
75
|
+
/** How far one level is drawn in from its parent, in points. */
|
|
76
|
+
const DEFAULT_INDENT = 16;
|
|
77
|
+
|
|
78
|
+
/** Matches the disclosure and layout timings the rest of the library uses. */
|
|
79
|
+
const TRANSITION_DURATION = 200;
|
|
80
|
+
|
|
81
|
+
const treeVariants = tv({
|
|
82
|
+
slots: {
|
|
83
|
+
root: 'w-full',
|
|
84
|
+
item: '',
|
|
85
|
+
trigger: 'flex-row items-center gap-2 rounded-lg',
|
|
86
|
+
indicator: 'items-center justify-center',
|
|
87
|
+
icon: 'items-center justify-center',
|
|
88
|
+
label: 'flex-1 text-foreground',
|
|
89
|
+
actions: 'flex-row items-center gap-1',
|
|
90
|
+
// The colour is set unconditionally and the width is not: `showLines` puts
|
|
91
|
+
// a border on the start edge, and a colour with no width draws nothing.
|
|
92
|
+
group: 'border-border',
|
|
93
|
+
},
|
|
94
|
+
variants: {
|
|
95
|
+
size: {
|
|
96
|
+
sm: {
|
|
97
|
+
trigger: 'px-1.5 py-1',
|
|
98
|
+
indicator: 'h-4 w-4',
|
|
99
|
+
icon: 'h-4 w-4',
|
|
100
|
+
label: 'text-xs',
|
|
101
|
+
},
|
|
102
|
+
default: {
|
|
103
|
+
trigger: 'px-2 py-1.5',
|
|
104
|
+
indicator: 'h-5 w-5',
|
|
105
|
+
icon: 'h-5 w-5',
|
|
106
|
+
label: 'text-sm',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
isSelected: {
|
|
110
|
+
true: { trigger: 'bg-accent', label: 'font-medium text-accent-foreground' },
|
|
111
|
+
},
|
|
112
|
+
isDisabled: {
|
|
113
|
+
true: { trigger: 'opacity-50' },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
defaultVariants: {
|
|
117
|
+
size: 'default',
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
interface TreeContextValue {
|
|
122
|
+
expanded: string[];
|
|
123
|
+
toggleExpanded: (value: string) => void;
|
|
124
|
+
selected: string[];
|
|
125
|
+
select: (value: string) => void;
|
|
126
|
+
selectionMode: TreeSelectionMode;
|
|
127
|
+
expandOnPress: boolean;
|
|
128
|
+
showLines: boolean;
|
|
129
|
+
indent: number;
|
|
130
|
+
size: TreeSize;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface TreeItemContextValue {
|
|
134
|
+
value: string;
|
|
135
|
+
/** 0 for a root item; one more than its parent's for anything nested. */
|
|
136
|
+
level: number;
|
|
137
|
+
isExpanded: boolean;
|
|
138
|
+
isSelected: boolean;
|
|
139
|
+
isDisabled: boolean;
|
|
140
|
+
isBranch: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const TreeContext = createContext<TreeContextValue | null>(null);
|
|
144
|
+
const TreeItemContext = createContext<TreeItemContextValue | null>(null);
|
|
145
|
+
|
|
146
|
+
function useTree(component: string): TreeContextValue {
|
|
147
|
+
const context = useContext(TreeContext);
|
|
148
|
+
if (!context) throw new Error(`${component} must be used within a <Tree>`);
|
|
149
|
+
return context;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function useTreeItem(component: string): TreeItemContextValue {
|
|
153
|
+
const context = useContext(TreeItemContext);
|
|
154
|
+
if (!context) throw new Error(`${component} must be used within a <Tree.Item>`);
|
|
155
|
+
return context;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const toArray = (value: string | string[] | undefined): string[] =>
|
|
159
|
+
value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
160
|
+
|
|
161
|
+
export interface TreeProps extends ViewProps {
|
|
162
|
+
className?: string;
|
|
163
|
+
/** Row density. `sm` for a sidebar or a picker inside a sheet. */
|
|
164
|
+
size?: TreeSize;
|
|
165
|
+
/** Whether a row can be the chosen one, and how many can be at once. */
|
|
166
|
+
selectionMode?: TreeSelectionMode;
|
|
167
|
+
/** Selected value(s), controlled. An array when `selectionMode` is `multiple`. */
|
|
168
|
+
value?: string | string[];
|
|
169
|
+
defaultValue?: string | string[];
|
|
170
|
+
/** Handed back in the shape it was given — a string when single, an array when multiple. */
|
|
171
|
+
onValueChange?: (value: string | string[]) => void;
|
|
172
|
+
/** Values of the open branches, controlled. */
|
|
173
|
+
expanded?: string[];
|
|
174
|
+
defaultExpanded?: string[];
|
|
175
|
+
/** Fires with the next set of open branches — the hook to load a branch's children on. */
|
|
176
|
+
onExpandedChange?: (expanded: string[]) => void;
|
|
177
|
+
/**
|
|
178
|
+
* Whether pressing anywhere on a branch's row opens it, as well as selecting
|
|
179
|
+
* it. Turn it off when selecting a branch has to be possible without opening
|
|
180
|
+
* it; the chevron still opens it either way.
|
|
181
|
+
*/
|
|
182
|
+
expandOnPress?: boolean;
|
|
183
|
+
/** Draw a hairline down each level, connecting a branch to the rows inside it. */
|
|
184
|
+
showLines?: boolean;
|
|
185
|
+
/** How far one level is drawn in from its parent, in points. */
|
|
186
|
+
indent?: number;
|
|
187
|
+
children?: ReactNode;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const TreeRoot = forwardRef<View, TreeProps>(
|
|
191
|
+
(
|
|
192
|
+
{
|
|
193
|
+
className,
|
|
194
|
+
size = 'default',
|
|
195
|
+
selectionMode = 'none',
|
|
196
|
+
value,
|
|
197
|
+
defaultValue,
|
|
198
|
+
onValueChange,
|
|
199
|
+
expanded,
|
|
200
|
+
defaultExpanded,
|
|
201
|
+
onExpandedChange,
|
|
202
|
+
expandOnPress = true,
|
|
203
|
+
showLines = false,
|
|
204
|
+
indent = DEFAULT_INDENT,
|
|
205
|
+
children,
|
|
206
|
+
...props
|
|
207
|
+
},
|
|
208
|
+
ref
|
|
209
|
+
) => {
|
|
210
|
+
const [internalExpanded, setInternalExpanded] = useState<string[]>(
|
|
211
|
+
() => defaultExpanded ?? []
|
|
212
|
+
);
|
|
213
|
+
const isExpandedControlled = expanded !== undefined;
|
|
214
|
+
const expandedValues = isExpandedControlled ? expanded : internalExpanded;
|
|
215
|
+
|
|
216
|
+
const [internalSelected, setInternalSelected] = useState<string[]>(() =>
|
|
217
|
+
toArray(defaultValue)
|
|
218
|
+
);
|
|
219
|
+
const isSelectionControlled = value !== undefined;
|
|
220
|
+
const selected = isSelectionControlled ? toArray(value) : internalSelected;
|
|
221
|
+
|
|
222
|
+
const toggleExpanded = useCallback(
|
|
223
|
+
(itemValue: string) => {
|
|
224
|
+
const next = expandedValues.includes(itemValue)
|
|
225
|
+
? expandedValues.filter((entry) => entry !== itemValue)
|
|
226
|
+
: [...expandedValues, itemValue];
|
|
227
|
+
|
|
228
|
+
if (!isExpandedControlled) setInternalExpanded(next);
|
|
229
|
+
onExpandedChange?.(next);
|
|
230
|
+
},
|
|
231
|
+
[expandedValues, isExpandedControlled, onExpandedChange]
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const select = useCallback(
|
|
235
|
+
(itemValue: string) => {
|
|
236
|
+
if (selectionMode === 'none') return;
|
|
237
|
+
|
|
238
|
+
const next =
|
|
239
|
+
selectionMode === 'single'
|
|
240
|
+
? [itemValue]
|
|
241
|
+
: selected.includes(itemValue)
|
|
242
|
+
? selected.filter((entry) => entry !== itemValue)
|
|
243
|
+
: [...selected, itemValue];
|
|
244
|
+
|
|
245
|
+
if (!isSelectionControlled) setInternalSelected(next);
|
|
246
|
+
// Hand back the shape the caller gave us, as Accordion does.
|
|
247
|
+
onValueChange?.(selectionMode === 'single' ? (next[0] ?? '') : next);
|
|
248
|
+
},
|
|
249
|
+
[selected, selectionMode, isSelectionControlled, onValueChange]
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const context = useMemo(
|
|
253
|
+
() => ({
|
|
254
|
+
expanded: expandedValues,
|
|
255
|
+
toggleExpanded,
|
|
256
|
+
selected,
|
|
257
|
+
select,
|
|
258
|
+
selectionMode,
|
|
259
|
+
expandOnPress,
|
|
260
|
+
showLines,
|
|
261
|
+
indent,
|
|
262
|
+
size,
|
|
263
|
+
}),
|
|
264
|
+
[
|
|
265
|
+
expandedValues,
|
|
266
|
+
toggleExpanded,
|
|
267
|
+
selected,
|
|
268
|
+
select,
|
|
269
|
+
selectionMode,
|
|
270
|
+
expandOnPress,
|
|
271
|
+
showLines,
|
|
272
|
+
indent,
|
|
273
|
+
size,
|
|
274
|
+
]
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const { root } = treeVariants({ size });
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<TreeContext.Provider value={context}>
|
|
281
|
+
<Animated.View
|
|
282
|
+
ref={ref}
|
|
283
|
+
layout={LinearTransition.duration(TRANSITION_DURATION)}
|
|
284
|
+
accessibilityRole="list"
|
|
285
|
+
className={root({ className })}
|
|
286
|
+
{...props}
|
|
287
|
+
>
|
|
288
|
+
{children}
|
|
289
|
+
</Animated.View>
|
|
290
|
+
</TreeContext.Provider>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
TreeRoot.displayName = 'Tree';
|
|
295
|
+
|
|
296
|
+
export interface TreeItemProps extends ViewProps {
|
|
297
|
+
className?: string;
|
|
298
|
+
/** Identifies this node in the tree's expanded and selected state. */
|
|
299
|
+
value: string;
|
|
300
|
+
isDisabled?: boolean;
|
|
301
|
+
/**
|
|
302
|
+
* Marks the item as a branch when it has no `Tree.Group` to be detected by —
|
|
303
|
+
* a folder whose contents are fetched the first time it is opened. It gets a
|
|
304
|
+
* chevron, and opening it fires `onExpandedChange` with nothing to show yet.
|
|
305
|
+
*/
|
|
306
|
+
hasChildren?: boolean;
|
|
307
|
+
children?: ReactNode;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* One node. Whether it is a branch or a leaf is read off its children rather
|
|
312
|
+
* than declared: an item holding a `Tree.Group` is a branch, and anything else
|
|
313
|
+
* is a leaf that renders a chevron-sized gap in place of the chevron so its
|
|
314
|
+
* label still lines up with its siblings'.
|
|
315
|
+
*/
|
|
316
|
+
const TreeItem = forwardRef<View, TreeItemProps>(
|
|
317
|
+
({ className, value, isDisabled = false, hasChildren, children, ...props }, ref) => {
|
|
318
|
+
const { expanded, selected } = useTree('Tree.Item');
|
|
319
|
+
const parent = useContext(TreeItemContext);
|
|
320
|
+
const { item } = treeVariants();
|
|
321
|
+
|
|
322
|
+
const isBranch =
|
|
323
|
+
hasChildren ??
|
|
324
|
+
Children.toArray(children).some(
|
|
325
|
+
(child) => isValidElement(child) && child.type === TreeGroup
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const context = useMemo(
|
|
329
|
+
() => ({
|
|
330
|
+
value,
|
|
331
|
+
level: parent ? parent.level + 1 : 0,
|
|
332
|
+
isExpanded: expanded.includes(value),
|
|
333
|
+
isSelected: selected.includes(value),
|
|
334
|
+
isDisabled,
|
|
335
|
+
isBranch,
|
|
336
|
+
}),
|
|
337
|
+
[value, parent, expanded, selected, isDisabled, isBranch]
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<TreeItemContext.Provider value={context}>
|
|
342
|
+
<Animated.View
|
|
343
|
+
ref={ref}
|
|
344
|
+
layout={LinearTransition.duration(TRANSITION_DURATION)}
|
|
345
|
+
className={item({ className })}
|
|
346
|
+
{...props}
|
|
347
|
+
>
|
|
348
|
+
{children}
|
|
349
|
+
</Animated.View>
|
|
350
|
+
</TreeItemContext.Provider>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
TreeItem.displayName = 'Tree.Item';
|
|
355
|
+
|
|
356
|
+
export interface TreeTriggerProps extends ViewProps {
|
|
357
|
+
className?: string;
|
|
358
|
+
/** Runs after the press has been handled, with the tree's own state already updated. */
|
|
359
|
+
onPress?: () => void;
|
|
360
|
+
children?: ReactNode;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* The node's row: everything you see on one line, and the thing you press.
|
|
365
|
+
*
|
|
366
|
+
* `aria-level` is set from the item's depth, but the role stays `button`.
|
|
367
|
+
* React Native has no tree role that any platform screen reader implements, so
|
|
368
|
+
* announcing the row as what it behaves like — a button that opens something,
|
|
369
|
+
* with its expanded and selected state attached — is the description that
|
|
370
|
+
* actually survives to the user.
|
|
371
|
+
*/
|
|
372
|
+
const TreeTrigger = forwardRef<View, TreeTriggerProps>(
|
|
373
|
+
({ className, onPress, children, ...props }, ref) => {
|
|
374
|
+
const { toggleExpanded, select, selectionMode, expandOnPress, size } =
|
|
375
|
+
useTree('Tree.Trigger');
|
|
376
|
+
const { value, level, isExpanded, isSelected, isDisabled, isBranch } =
|
|
377
|
+
useTreeItem('Tree.Trigger');
|
|
378
|
+
const { trigger } = treeVariants({ size, isSelected, isDisabled });
|
|
379
|
+
|
|
380
|
+
const accentForeground = useCSSVariable('--color-accent-foreground');
|
|
381
|
+
const selectedColor = typeof accentForeground === 'string' ? accentForeground : undefined;
|
|
382
|
+
|
|
383
|
+
const handlePress = () => {
|
|
384
|
+
if (isBranch && expandOnPress) toggleExpanded(value);
|
|
385
|
+
select(value);
|
|
386
|
+
onPress?.();
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<Pressable
|
|
391
|
+
ref={ref}
|
|
392
|
+
accessibilityRole="button"
|
|
393
|
+
accessibilityState={{
|
|
394
|
+
expanded: isBranch ? isExpanded : undefined,
|
|
395
|
+
selected: selectionMode === 'none' ? undefined : isSelected,
|
|
396
|
+
disabled: isDisabled,
|
|
397
|
+
}}
|
|
398
|
+
aria-level={level + 1}
|
|
399
|
+
disabled={isDisabled}
|
|
400
|
+
onPress={handlePress}
|
|
401
|
+
className={trigger({ className })}
|
|
402
|
+
{...props}
|
|
403
|
+
>
|
|
404
|
+
<IconColorProvider color={isSelected ? selectedColor : undefined}>
|
|
405
|
+
{children}
|
|
406
|
+
</IconColorProvider>
|
|
407
|
+
</Pressable>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
);
|
|
411
|
+
TreeTrigger.displayName = 'Tree.Trigger';
|
|
412
|
+
|
|
413
|
+
export interface TreeIndicatorProps extends ViewProps {
|
|
414
|
+
className?: string;
|
|
415
|
+
/** Replaces the default chevron. It is rotated for you while the branch is open. */
|
|
416
|
+
children?: ReactNode;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* The chevron, and the second way to open a branch.
|
|
421
|
+
*
|
|
422
|
+
* It is pressable in its own right, and because a press is consumed by the
|
|
423
|
+
* innermost target that handles it, hitting the chevron opens the branch
|
|
424
|
+
* without also selecting the row — which is the only way to look inside a
|
|
425
|
+
* folder without choosing it when `expandOnPress` is off.
|
|
426
|
+
*
|
|
427
|
+
* On a leaf it becomes an empty box of the same size rather than disappearing,
|
|
428
|
+
* so a leaf's label starts where its siblings' labels do.
|
|
429
|
+
*/
|
|
430
|
+
const TreeIndicator = forwardRef<View, TreeIndicatorProps>(
|
|
431
|
+
({ className, children, ...props }, ref) => {
|
|
432
|
+
const { toggleExpanded, size } = useTree('Tree.Indicator');
|
|
433
|
+
const { value, isExpanded, isDisabled, isBranch } = useTreeItem('Tree.Indicator');
|
|
434
|
+
const { indicator } = treeVariants({ size });
|
|
435
|
+
const direction = useDirection();
|
|
436
|
+
|
|
437
|
+
/*
|
|
438
|
+
* The glyph already points along the reading direction — it is drawn
|
|
439
|
+
* mirrored in a right-to-left subtree — so the quarter turn that makes it
|
|
440
|
+
* point downwards has to follow it round, or an open branch in Arabic
|
|
441
|
+
* would have its chevron pointing at the ceiling.
|
|
442
|
+
*/
|
|
443
|
+
const openRotation = direction === 'rtl' ? -90 : 90;
|
|
444
|
+
const rotation = useDerivedValue(
|
|
445
|
+
() => withTiming(isExpanded ? openRotation : 0, { duration: TRANSITION_DURATION }),
|
|
446
|
+
[isExpanded, openRotation]
|
|
447
|
+
);
|
|
448
|
+
const rotationStyle = useAnimatedStyle(() => ({
|
|
449
|
+
transform: [{ rotate: `${rotation.value}deg` }],
|
|
450
|
+
}));
|
|
451
|
+
|
|
452
|
+
// After the hooks, never before them: a leaf still runs every one of them.
|
|
453
|
+
if (!isBranch) {
|
|
454
|
+
return <View ref={ref} className={indicator({ className })} {...props} />;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return (
|
|
458
|
+
<Pressable
|
|
459
|
+
ref={ref}
|
|
460
|
+
accessibilityRole="button"
|
|
461
|
+
accessibilityState={{ expanded: isExpanded, disabled: isDisabled }}
|
|
462
|
+
disabled={isDisabled}
|
|
463
|
+
onPress={() => toggleExpanded(value)}
|
|
464
|
+
className={indicator({ className })}
|
|
465
|
+
{...props}
|
|
466
|
+
>
|
|
467
|
+
<Animated.View style={rotationStyle}>
|
|
468
|
+
{children ?? <ChevronRightIcon size={size === 'sm' ? 14 : 16} />}
|
|
469
|
+
</Animated.View>
|
|
470
|
+
</Pressable>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
);
|
|
474
|
+
TreeIndicator.displayName = 'Tree.Indicator';
|
|
475
|
+
|
|
476
|
+
export interface TreeIconProps extends ViewProps {
|
|
477
|
+
className?: string;
|
|
478
|
+
children?: ReactNode;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** The leading glyph, between the chevron and the label. */
|
|
482
|
+
const TreeIcon = forwardRef<View, TreeIconProps>(({ className, ...props }, ref) => {
|
|
483
|
+
const { size } = useTree('Tree.Icon');
|
|
484
|
+
const { icon } = treeVariants({ size });
|
|
485
|
+
return <View ref={ref} className={icon({ className })} {...props} />;
|
|
486
|
+
});
|
|
487
|
+
TreeIcon.displayName = 'Tree.Icon';
|
|
488
|
+
|
|
489
|
+
/** The row's text. Takes the selected colour with the rest of the row. */
|
|
490
|
+
const TreeLabel = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
|
|
491
|
+
const { size } = useTree('Tree.Label');
|
|
492
|
+
const { isSelected } = useTreeItem('Tree.Label');
|
|
493
|
+
const { label } = treeVariants({ size, isSelected });
|
|
494
|
+
return <Text ref={ref} numberOfLines={1} className={label({ className })} {...props} />;
|
|
495
|
+
});
|
|
496
|
+
TreeLabel.displayName = 'Tree.Label';
|
|
497
|
+
|
|
498
|
+
export interface TreeActionsProps extends ViewProps {
|
|
499
|
+
className?: string;
|
|
500
|
+
children?: ReactNode;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** The trailing slot: a count, a badge, a menu button for the node. */
|
|
504
|
+
const TreeActions = forwardRef<View, TreeActionsProps>(({ className, ...props }, ref) => {
|
|
505
|
+
const { actions } = treeVariants();
|
|
506
|
+
return <View ref={ref} className={actions({ className })} {...props} />;
|
|
507
|
+
});
|
|
508
|
+
TreeActions.displayName = 'Tree.Actions';
|
|
509
|
+
|
|
510
|
+
export interface TreeGroupProps extends ViewProps {
|
|
511
|
+
className?: string;
|
|
512
|
+
children?: ReactNode;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* The rows inside a branch, and the reason its item is a branch at all.
|
|
517
|
+
*
|
|
518
|
+
* It unmounts when the branch is closed, so a subtree nobody has opened has
|
|
519
|
+
* never rendered and a tree costs what is open in it rather than what is in it.
|
|
520
|
+
* The item's layout transition animates the height the group's rows take up,
|
|
521
|
+
* the same way `Accordion` animates a panel.
|
|
522
|
+
*
|
|
523
|
+
* The indent is `paddingStart`, not `paddingLeft`, and the guide line is a
|
|
524
|
+
* start-edge border: an indent that always fell on the left would run the wrong
|
|
525
|
+
* way in a right-to-left subtree and put every level further from its parent.
|
|
526
|
+
*/
|
|
527
|
+
const TreeGroup = forwardRef<View, TreeGroupProps>(
|
|
528
|
+
({ className, children, ...props }, ref) => {
|
|
529
|
+
const { showLines, indent } = useTree('Tree.Group');
|
|
530
|
+
const { isExpanded } = useTreeItem('Tree.Group');
|
|
531
|
+
const { group } = treeVariants();
|
|
532
|
+
|
|
533
|
+
if (!isExpanded) return null;
|
|
534
|
+
|
|
535
|
+
return (
|
|
536
|
+
<View
|
|
537
|
+
ref={ref}
|
|
538
|
+
style={{
|
|
539
|
+
// Split in half when there is a line to put between the halves, so a
|
|
540
|
+
// level is indented by the same amount either way and the hairline
|
|
541
|
+
// lands midway rather than adding a step of its own.
|
|
542
|
+
marginStart: showLines ? indent / 2 : 0,
|
|
543
|
+
paddingStart: showLines ? indent / 2 : indent,
|
|
544
|
+
borderStartWidth: showLines ? 1 : 0,
|
|
545
|
+
}}
|
|
546
|
+
className={group({ className })}
|
|
547
|
+
{...props}
|
|
548
|
+
>
|
|
549
|
+
{children}
|
|
550
|
+
</View>
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
);
|
|
554
|
+
TreeGroup.displayName = 'Tree.Group';
|
|
555
|
+
|
|
556
|
+
export const Tree = Object.assign(TreeRoot, {
|
|
557
|
+
Item: TreeItem,
|
|
558
|
+
Trigger: TreeTrigger,
|
|
559
|
+
Indicator: TreeIndicator,
|
|
560
|
+
Icon: TreeIcon,
|
|
561
|
+
Label: TreeLabel,
|
|
562
|
+
Actions: TreeActions,
|
|
563
|
+
Group: TreeGroup,
|
|
564
|
+
});
|