panelui-native 0.44.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 +6 -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/combobox/index.js +7 -1
- package/lib/module/components/combobox/index.js.map +1 -1
- package/lib/module/components/date-time-picker/index.js +272 -0
- package/lib/module/components/date-time-picker/index.js.map +1 -0
- package/lib/module/components/fab/index.js +514 -0
- package/lib/module/components/fab/index.js.map +1 -0
- package/lib/module/components/grid-item/index.js +486 -0
- package/lib/module/components/grid-item/index.js.map +1 -0
- package/lib/module/components/{kpi-chart → kpi}/index.js +62 -62
- package/lib/module/components/kpi/index.js.map +1 -0
- package/lib/module/components/line-chart/index.js +92 -10
- package/lib/module/components/line-chart/index.js.map +1 -1
- 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/pie-chart/index.js +625 -0
- package/lib/module/components/pie-chart/index.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/scatter-chart/index.js +1173 -0
- package/lib/module/components/scatter-chart/index.js.map +1 -0
- package/lib/module/components/tabs/index.js +359 -41
- 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 +11 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/chart.js +81 -0
- package/lib/module/utils/chart.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/combobox/index.d.ts.map +1 -1
- package/lib/typescript/src/components/date-time-picker/index.d.ts +127 -0
- package/lib/typescript/src/components/date-time-picker/index.d.ts.map +1 -0
- 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/grid-item/index.d.ts +292 -0
- package/lib/typescript/src/components/grid-item/index.d.ts.map +1 -0
- package/lib/typescript/src/components/{kpi-chart → kpi}/index.d.ts +65 -65
- package/lib/typescript/src/components/kpi/index.d.ts.map +1 -0
- package/lib/typescript/src/components/line-chart/index.d.ts +25 -1
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- 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/pie-chart/index.d.ts +245 -0
- package/lib/typescript/src/components/pie-chart/index.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/scatter-chart/index.d.ts +309 -0
- package/lib/typescript/src/components/scatter-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/tabs/index.d.ts +29 -2
- 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 +14 -5
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/chart.d.ts +34 -0
- package/lib/typescript/src/utils/chart.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/combobox/index.tsx +7 -1
- package/src/components/date-time-picker/index.tsx +411 -0
- package/src/components/fab/index.tsx +583 -0
- package/src/components/grid-item/index.tsx +515 -0
- package/src/components/{kpi-chart → kpi}/index.tsx +85 -85
- package/src/components/line-chart/index.tsx +98 -8
- package/src/components/markdown-editor/index.tsx +526 -0
- package/src/components/markdown-editor/markdown-transforms.ts +228 -0
- package/src/components/pie-chart/index.tsx +863 -0
- package/src/components/questionnaire/index.tsx +1615 -0
- package/src/components/scatter-chart/index.tsx +1401 -0
- package/src/components/tabs/index.tsx +392 -50
- 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 +142 -18
- package/src/utils/chart.ts +110 -0
- package/lib/module/components/kpi-chart/index.js.map +0 -1
- package/lib/typescript/src/components/kpi-chart/index.d.ts.map +0 -1
|
@@ -116,6 +116,15 @@ const TICK_SPACING = 12;
|
|
|
116
116
|
/** Settles the clock hands after a pick. Slow enough to be followed by eye. */
|
|
117
117
|
const HAND_SPRING = { damping: 16, stiffness: 140, mass: 0.7 } as const;
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* How loudly the ruler face states the time it is on.
|
|
121
|
+
*
|
|
122
|
+
* `default` is the big centred number, right when the scale is the only thing
|
|
123
|
+
* on the panel. `compact` steps it down to sit under something that outranks
|
|
124
|
+
* it, and `none` drops it for a caller that writes the time itself.
|
|
125
|
+
*/
|
|
126
|
+
export type TimePickerReadout = 'default' | 'compact' | 'none';
|
|
127
|
+
|
|
119
128
|
const timePickerVariants = tv({
|
|
120
129
|
slots: {
|
|
121
130
|
panel: 'gap-3',
|
|
@@ -128,10 +137,20 @@ const timePickerVariants = tv({
|
|
|
128
137
|
row: 'items-center justify-center',
|
|
129
138
|
rowLabel: 'text-lg tabular-nums text-foreground',
|
|
130
139
|
rowLabelSelected: 'font-semibold text-primary',
|
|
131
|
-
readout: 'text-center
|
|
140
|
+
readout: 'text-center font-semibold tabular-nums text-foreground',
|
|
132
141
|
clock: 'flex-row items-center gap-4',
|
|
133
142
|
footer: 'flex-row justify-end gap-2',
|
|
134
143
|
},
|
|
144
|
+
variants: {
|
|
145
|
+
readout: {
|
|
146
|
+
default: { readout: 'text-4xl' },
|
|
147
|
+
compact: { readout: 'text-xl' },
|
|
148
|
+
none: {},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
defaultVariants: {
|
|
152
|
+
readout: 'default',
|
|
153
|
+
},
|
|
135
154
|
});
|
|
136
155
|
|
|
137
156
|
/* -------------------------------------------------------------------------- */
|
|
@@ -296,6 +315,8 @@ interface FaceProps {
|
|
|
296
315
|
minuteStep: number;
|
|
297
316
|
locale?: string;
|
|
298
317
|
disabled?: boolean;
|
|
318
|
+
/** Ruler face only — the other two never draw a number of their own. */
|
|
319
|
+
readout?: TimePickerReadout;
|
|
299
320
|
}
|
|
300
321
|
|
|
301
322
|
function WheelFace({
|
|
@@ -573,8 +594,9 @@ function RulerFace({
|
|
|
573
594
|
minuteStep,
|
|
574
595
|
locale,
|
|
575
596
|
disabled,
|
|
597
|
+
readout = 'default',
|
|
576
598
|
}: FaceProps) {
|
|
577
|
-
const slots = timePickerVariants();
|
|
599
|
+
const slots = timePickerVariants({ readout });
|
|
578
600
|
const ref = useRef<ScrollView>(null);
|
|
579
601
|
const times = useMemo(() => timesOfDay(minuteStep), [minuteStep]);
|
|
580
602
|
const index = Math.min(
|
|
@@ -638,10 +660,12 @@ function RulerFace({
|
|
|
638
660
|
);
|
|
639
661
|
|
|
640
662
|
return (
|
|
641
|
-
<View className=
|
|
642
|
-
|
|
643
|
-
{
|
|
644
|
-
|
|
663
|
+
<View className={readout === 'none' ? undefined : 'gap-4'}>
|
|
664
|
+
{readout === 'none' ? null : (
|
|
665
|
+
<Text className={slots.readout()}>
|
|
666
|
+
{formatTime(value, { hourCycle, locale })}
|
|
667
|
+
</Text>
|
|
668
|
+
)}
|
|
645
669
|
|
|
646
670
|
<View className="relative h-14 justify-center" onLayout={onLayout}>
|
|
647
671
|
<ScrollView
|
|
@@ -768,6 +792,16 @@ export interface TimePickerProps {
|
|
|
768
792
|
format?: (value: TimeValue) => string;
|
|
769
793
|
/** BCP 47 tag for the time's text and the meridiem labels. */
|
|
770
794
|
locale?: string;
|
|
795
|
+
/**
|
|
796
|
+
* How loudly the `ruler` face states the time it is on. The other two faces
|
|
797
|
+
* spell the time out in their own columns and hands, and ignore this.
|
|
798
|
+
*
|
|
799
|
+
* The big centred number is right when the scale is the only thing on the
|
|
800
|
+
* panel. Under something that outranks it — a calendar, a form row — it is
|
|
801
|
+
* the largest text on screen for the smaller half of the answer, so step it
|
|
802
|
+
* down with `compact` or take it over yourself with `none`.
|
|
803
|
+
*/
|
|
804
|
+
readout?: TimePickerReadout;
|
|
771
805
|
/** Stop the trigger opening it, and the faces from being scrolled. */
|
|
772
806
|
disabled?: boolean;
|
|
773
807
|
className?: string;
|
|
@@ -803,6 +837,7 @@ function TimePickerRoot({
|
|
|
803
837
|
placeholder = DEFAULT_PLACEHOLDER,
|
|
804
838
|
format,
|
|
805
839
|
locale,
|
|
840
|
+
readout = 'default',
|
|
806
841
|
disabled = false,
|
|
807
842
|
className,
|
|
808
843
|
children,
|
|
@@ -867,6 +902,7 @@ function TimePickerRoot({
|
|
|
867
902
|
minuteStep: step,
|
|
868
903
|
locale,
|
|
869
904
|
disabled,
|
|
905
|
+
readout,
|
|
870
906
|
};
|
|
871
907
|
|
|
872
908
|
if (presentation === 'inline') {
|
|
@@ -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
|
+
});
|