@recursica/mantine-adapter 0.36.1 → 0.38.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/CHANGELOG.md +22 -0
- package/dist/mantine-adapter.cjs +2 -2
- package/dist/mantine-adapter.cjs.map +1 -1
- package/dist/mantine-adapter.css +1 -1
- package/dist/mantine-adapter.js +2352 -2099
- package/dist/mantine-adapter.js.map +1 -1
- package/dist/src/components/Dropdown/BareDropdown.d.ts +19 -0
- package/dist/src/components/TimePicker/TimePicker.d.ts +9 -3
- package/dist/src/components/Tree/Tree.d.ts +5 -0
- package/dist/src/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/Accordion/USAGE.md +2 -41
- package/src/components/AutoComplete/USAGE.md +2 -18
- package/src/components/Avatar/USAGE.md +0 -27
- package/src/components/Badge/USAGE.md +3 -6
- package/src/components/Breadcrumb/USAGE.md +1 -5
- package/src/components/Button/Button.tsx +5 -0
- package/src/components/Button/IMPLEMENTATION_NOTES.md +8 -0
- package/src/components/Button/USAGE.md +5 -25
- package/src/components/Card/USAGE.md +4 -12
- package/src/components/Checkbox/USAGE.md +4 -20
- package/src/components/Chip/USAGE.md +3 -32
- package/src/components/DatePicker/USAGE.md +2 -12
- package/src/components/Dropdown/BareDropdown.tsx +85 -0
- package/src/components/Dropdown/Dropdown.tsx +12 -3
- package/src/components/Dropdown/USAGE.md +2 -6
- package/src/components/Flex/USAGE.md +1 -1
- package/src/components/FormControlWrapper/USAGE.md +3 -31
- package/src/components/Grid/USAGE.md +1 -1
- package/src/components/Group/USAGE.md +1 -1
- package/src/components/HoverCard/USAGE.md +3 -72
- package/src/components/Label/USAGE.md +8 -48
- package/src/components/Link/USAGE.md +4 -10
- package/src/components/Loader/USAGE.md +4 -23
- package/src/components/Menu/USAGE.md +3 -73
- package/src/components/Modal/USAGE.md +3 -3
- package/src/components/NumberInput/USAGE.md +5 -8
- package/src/components/Pagination/USAGE.md +0 -19
- package/src/components/Panel/USAGE.md +6 -95
- package/src/components/Popover/USAGE.md +6 -66
- package/src/components/ReadOnlyField/USAGE.md +2 -10
- package/src/components/SegmentedControl/USAGE.md +1 -15
- package/src/components/Slider/USAGE.md +1 -45
- package/src/components/Stack/USAGE.md +1 -1
- package/src/components/Switch/USAGE.md +1 -24
- package/src/components/TextArea/USAGE.md +2 -2
- package/src/components/TextField/USAGE.md +1 -17
- package/src/components/TimePicker/TIMEPICKER_IMPLEMENTATION_NOTES.md +72 -0
- package/src/components/TimePicker/TimePicker.module.css +225 -41
- package/src/components/TimePicker/TimePicker.stories.tsx +105 -4
- package/src/components/TimePicker/TimePicker.tsx +287 -7
- package/src/components/TimePicker/USAGE.md +23 -2
- package/src/components/Timeline/USAGE.md +2 -10
- package/src/components/Toast/USAGE.md +3 -33
- package/src/components/Tooltip/USAGE.md +7 -51
- package/src/components/Tree/IMPLEMENTATION_NOTES.md +31 -3
- package/src/components/Tree/Tree.module.css +84 -40
- package/src/components/Tree/Tree.stories.tsx +13 -0
- package/src/components/Tree/Tree.tsx +116 -27
- package/src/components/Tree/USAGE.md +18 -1
- package/src/index.ts +1 -0
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/* HARDCODED VALUES:
|
|
2
2
|
* - list-style/margin/padding resets on .root/.subtree: layout resets, no corresponding
|
|
3
3
|
* design tokens (structural, not visual design values).
|
|
4
|
-
* - .
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* - .expandGlyph's rotation transition (150ms ease): no dedicated timing token exists for Tree;
|
|
5
|
+
* the glyph itself is sized/colored entirely by Button's own "text"/"small" tokens now, not a
|
|
6
|
+
* hand-drawn size/color like the previous plain-SVG implementation.
|
|
7
7
|
* - Hover overlay uses the generic --recursica_brand_states_hover_* tokens (same technique as
|
|
8
8
|
* Menu/Accordion/Button): Tree has no component-specific hover tokens of its own.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: the row's box model (padding/border/font/color/background) lives entirely on `.label`,
|
|
11
|
+
* not `.row` — the highlighted "chip" (hover/selected/focus) covers only the label, never the
|
|
12
|
+
* expand button. `.row` itself is a plain flex layout container (indentation + button-node-gap).
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
15
|
.root {
|
|
@@ -25,6 +29,16 @@
|
|
|
25
29
|
) !important;
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
/* Whole-tree `disabled` (no per-node concept — see RecursicaTreeProps.ts). `pointer-events: none`
|
|
33
|
+
blocks every mouse interaction tree-wide in one rule (row clicks, chevron clicks); keyboard is
|
|
34
|
+
separately blocked in Tree.tsx, since pointer-events has no effect on Tab/Enter/Space/arrows.
|
|
35
|
+
Uses the generic disabled opacity token, same convention as every other disabled component in
|
|
36
|
+
the design system (no tree-specific disabled token exists). */
|
|
37
|
+
.root[data-disabled] {
|
|
38
|
+
opacity: var(--recursica_brand_states_disabled);
|
|
39
|
+
pointer-events: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
.subtree {
|
|
29
43
|
margin: 0;
|
|
30
44
|
padding: 0;
|
|
@@ -36,11 +50,33 @@
|
|
|
36
50
|
|
|
37
51
|
.node {
|
|
38
52
|
/* Mantine's own base class already resets margin/padding/list-style and sets cursor:pointer
|
|
39
|
-
on this <li>; it also holds the subtree as a second child, so all
|
|
40
|
-
(box model, colors, typography) lives on .
|
|
53
|
+
on this <li>; it also holds the subtree as a second child, so all node-level visual styling
|
|
54
|
+
(box model, colors, typography) lives on .label instead, not here. */
|
|
41
55
|
position: relative;
|
|
42
56
|
}
|
|
43
57
|
|
|
58
|
+
/* Mantine's roving-tabindex keyboard navigation (arrow up/down moves focus between rows, left/
|
|
59
|
+
right expand/collapse, matching the WAI-ARIA treeitem pattern) already works out of the box —
|
|
60
|
+
but with no visible indicator, it looked broken. DOM focus lands on this <li> (.node), not the
|
|
61
|
+
visible row box, so the ring is drawn on .label instead — the "node" content area, deliberately
|
|
62
|
+
excluding the expand Button, which never shows a focus state of its own (it's not independently
|
|
63
|
+
focusable at all: tabIndex={-1}). */
|
|
64
|
+
.node:focus-visible {
|
|
65
|
+
outline: none;
|
|
66
|
+
}
|
|
67
|
+
/* `> .row` (child combinator), not a plain descendant selector: a node's own `.row` is always a
|
|
68
|
+
direct child of its `.node` <li>, while every descendant node's `.row` (inside the nested
|
|
69
|
+
`.subtree` <ul>) is not — without the `>`, focusing a parent node drew the ring on every
|
|
70
|
+
expanded descendant's label too, since they're all still descendants of the focused `.node`. */
|
|
71
|
+
.node:focus-visible > .row .label {
|
|
72
|
+
box-shadow:
|
|
73
|
+
0 0 0 var(--recursica_brand_states_focus_border-size)
|
|
74
|
+
var(--recursica_brand_states_focus_color),
|
|
75
|
+
0 0 var(--recursica_brand_states_focus_blur)
|
|
76
|
+
var(--recursica_brand_states_focus_margin)
|
|
77
|
+
var(--recursica_brand_states_focus_color);
|
|
78
|
+
}
|
|
79
|
+
|
|
44
80
|
.row {
|
|
45
81
|
position: relative;
|
|
46
82
|
box-sizing: border-box;
|
|
@@ -49,6 +85,39 @@
|
|
|
49
85
|
cursor: pointer;
|
|
50
86
|
margin-left: var(--label-offset, 0px);
|
|
51
87
|
gap: var(--recursica_ui-kit_components_tree_properties_button-node-gap);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* The expand Button is always rendered (even for leaf nodes, which have nothing to toggle) so
|
|
91
|
+
every row reserves identical layout space — hidden via visibility (not display) on leaf rows so
|
|
92
|
+
the label stays aligned across the whole tree without duplicating Button's own size tokens. */
|
|
93
|
+
.expandButton {
|
|
94
|
+
position: relative;
|
|
95
|
+
z-index: 1;
|
|
96
|
+
flex-shrink: 0;
|
|
97
|
+
}
|
|
98
|
+
.row:not([data-has-children]) .expandButton {
|
|
99
|
+
visibility: hidden;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.expandGlyph {
|
|
103
|
+
color: inherit;
|
|
104
|
+
}
|
|
105
|
+
.row[data-has-children] .expandGlyph {
|
|
106
|
+
transition: transform 150ms ease;
|
|
107
|
+
}
|
|
108
|
+
.row[data-expanded] .expandGlyph {
|
|
109
|
+
transform: rotate(90deg);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.label {
|
|
113
|
+
position: relative;
|
|
114
|
+
z-index: 1;
|
|
115
|
+
/* Sized to its own content, not stretched to fill the row's remaining width — the
|
|
116
|
+
hover/selected/focus "chip" should cover just the label, not the full row width. */
|
|
117
|
+
flex: 0 1 auto;
|
|
118
|
+
box-sizing: border-box;
|
|
119
|
+
display: flex;
|
|
120
|
+
align-items: center;
|
|
52
121
|
padding: var(--recursica_ui-kit_components_tree_properties_vertical-padding)
|
|
53
122
|
var(--recursica_ui-kit_components_tree_properties_horizontal-padding);
|
|
54
123
|
border-style: solid; /* HARDCODE: structural border rule; thickness/color are tokened */
|
|
@@ -93,24 +162,28 @@
|
|
|
93
162
|
);
|
|
94
163
|
}
|
|
95
164
|
|
|
96
|
-
/* Hover overlay via ::after pseudo-element (same technique as Menu/Accordion)
|
|
97
|
-
.
|
|
165
|
+
/* Hover overlay via ::after pseudo-element (same technique as Menu/Accordion), scoped to the
|
|
166
|
+
label's own box so hover never visually covers the expand button. `.label`'s `position:
|
|
167
|
+
relative` + `z-index: 1` above already forms a stacking context, so `z-index: -1` here paints
|
|
168
|
+
the overlay behind the label's real text content within that context, not behind `.label`
|
|
169
|
+
itself. */
|
|
170
|
+
.label::after {
|
|
98
171
|
content: "";
|
|
99
172
|
position: absolute;
|
|
100
173
|
inset: 0;
|
|
101
174
|
border-radius: inherit;
|
|
102
|
-
z-index:
|
|
175
|
+
z-index: -1;
|
|
103
176
|
pointer-events: none;
|
|
104
177
|
background-color: var(--recursica_brand_states_hover_color);
|
|
105
178
|
opacity: 0;
|
|
106
179
|
transition: opacity 150ms ease;
|
|
107
180
|
}
|
|
108
|
-
.row:hover::after,
|
|
109
|
-
.row[data-hovered]::after {
|
|
181
|
+
.row:hover .label::after,
|
|
182
|
+
.row[data-hovered] .label::after {
|
|
110
183
|
opacity: var(--recursica_brand_states_hover_opacity);
|
|
111
184
|
}
|
|
112
185
|
|
|
113
|
-
.
|
|
186
|
+
.label[data-selected] {
|
|
114
187
|
font-family: var(
|
|
115
188
|
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_font-family
|
|
116
189
|
);
|
|
@@ -146,32 +219,3 @@
|
|
|
146
219
|
--recursica_ui-kit_components_tree_variants_selection-states_selected_properties_colors_border-color
|
|
147
220
|
);
|
|
148
221
|
}
|
|
149
|
-
|
|
150
|
-
.expandIcon {
|
|
151
|
-
position: relative;
|
|
152
|
-
z-index: 1;
|
|
153
|
-
display: flex;
|
|
154
|
-
align-items: center;
|
|
155
|
-
justify-content: center;
|
|
156
|
-
flex-shrink: 0;
|
|
157
|
-
width: 1em;
|
|
158
|
-
height: 1em;
|
|
159
|
-
color: inherit;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
.row[data-has-children] .expandIcon {
|
|
163
|
-
transition: transform 150ms ease;
|
|
164
|
-
}
|
|
165
|
-
.row[data-expanded] .expandIcon {
|
|
166
|
-
transform: rotate(90deg);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
.label {
|
|
170
|
-
position: relative;
|
|
171
|
-
z-index: 1;
|
|
172
|
-
color: inherit;
|
|
173
|
-
font: inherit;
|
|
174
|
-
letter-spacing: inherit;
|
|
175
|
-
text-decoration: inherit;
|
|
176
|
-
text-transform: inherit;
|
|
177
|
-
}
|
|
@@ -80,6 +80,19 @@ export const MultipleSelection: StoryObj<typeof Tree> = {
|
|
|
80
80
|
),
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
/** Whole tree disabled, with a node pre-selected so the selected chip's styling under the
|
|
84
|
+
* disabled dimming can be checked alongside unselected rows. */
|
|
85
|
+
export const Disabled: StoryObj<typeof Tree> = {
|
|
86
|
+
render: () => (
|
|
87
|
+
<Tree
|
|
88
|
+
data={sampleData}
|
|
89
|
+
initialExpandedValues={["documents"]}
|
|
90
|
+
initialSelectedValues={["documents/resume.pdf"]}
|
|
91
|
+
disabled
|
|
92
|
+
/>
|
|
93
|
+
),
|
|
94
|
+
};
|
|
95
|
+
|
|
83
96
|
/** Demonstrates the component nested inside a non-default layer — the one case where an
|
|
84
97
|
* explicit `<Layer>` wrap belongs in a story (see COMPONENT_STORYBOOK_GUIDE.md §9). */
|
|
85
98
|
export const LayerOne: StoryObj<typeof Tree> = {
|
|
@@ -5,22 +5,27 @@ import {
|
|
|
5
5
|
getTreeExpandedState,
|
|
6
6
|
type RenderTreeNodePayload,
|
|
7
7
|
} from "@mantine/core";
|
|
8
|
+
import { useMergedRef } from "@mantine/hooks";
|
|
8
9
|
import {
|
|
9
10
|
filterStylingProps,
|
|
10
11
|
type RecursicaOverStyled,
|
|
11
12
|
} from "../../utils/filterStylingProps";
|
|
12
13
|
import { type RecursicaTreeProps } from "@recursica/adapter-common";
|
|
14
|
+
import { Button } from "../Button/Button";
|
|
13
15
|
import styles from "./Tree.module.css";
|
|
14
16
|
|
|
15
17
|
export type TreeProps = RecursicaOverStyled<
|
|
16
18
|
RecursicaTreeProps & Omit<React.ComponentPropsWithoutRef<"ul">, "children">
|
|
17
19
|
>;
|
|
18
20
|
|
|
19
|
-
/** Simple chevron glyph; rotates via `[data-expanded]` in CSS
|
|
20
|
-
*
|
|
21
|
+
/** Simple chevron glyph; rotates via the row's `[data-expanded]` in CSS (`.row[data-expanded]
|
|
22
|
+
* .expandGlyph`) — this reaches the glyph regardless of how deeply `Button` nests it internally,
|
|
23
|
+
* since it's a plain descendant-combinator selector, not dependent on Button's own DOM structure.
|
|
24
|
+
* Uses `currentColor` so it always matches Button's own tokened icon color for the "text" variant. */
|
|
21
25
|
function ExpandGlyph() {
|
|
22
26
|
return (
|
|
23
27
|
<svg
|
|
28
|
+
className={styles.expandGlyph}
|
|
24
29
|
viewBox="0 0 16 16"
|
|
25
30
|
width="1em"
|
|
26
31
|
height="1em"
|
|
@@ -36,25 +41,56 @@ function ExpandGlyph() {
|
|
|
36
41
|
);
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
{
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
44
|
+
/** Curried so the fixed-shape `renderNode` callback (Mantine controls its signature) can still
|
|
45
|
+
* close over `disabled`, which isn't part of `RenderTreeNodePayload`. */
|
|
46
|
+
function createRenderTreeNode(disabled: boolean) {
|
|
47
|
+
return function renderTreeNode({
|
|
48
|
+
node,
|
|
49
|
+
hasChildren,
|
|
50
|
+
expanded,
|
|
51
|
+
selected,
|
|
52
|
+
tree,
|
|
53
|
+
elementProps,
|
|
54
|
+
}: RenderTreeNodePayload) {
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
{...elementProps}
|
|
58
|
+
className={styles.row}
|
|
59
|
+
data-has-children={hasChildren || undefined}
|
|
60
|
+
data-expanded={hasChildren && expanded ? true : undefined}
|
|
61
|
+
>
|
|
62
|
+
{/* The only element that toggles expand/collapse on click — independent of selection,
|
|
63
|
+
which `elementProps.onClick` (spread onto `.row` above) handles for the rest of the
|
|
64
|
+
row. Stops propagation so the click never also reaches `.row`'s own handler and
|
|
65
|
+
selects the node. Never independently focusable/tab-stoppable (tabIndex={-1},
|
|
66
|
+
aria-hidden), so the row stays the only focusable element and this button never
|
|
67
|
+
shows its own focus state. Always rendered, even for leaf nodes, so every row
|
|
68
|
+
reserves the same layout space; CSS hides it (visibility, not display) when there
|
|
69
|
+
are no children to toggle. Guards `disabled` itself (not just via the CSS
|
|
70
|
+
`pointer-events: none` on `.root`) since this handler bypasses Mantine's own
|
|
71
|
+
`expandOnClick`/`selectOnClick` flags entirely by calling `tree.toggleExpanded`
|
|
72
|
+
directly. */}
|
|
73
|
+
<Button
|
|
74
|
+
overStyled
|
|
75
|
+
variant="text"
|
|
76
|
+
size="small"
|
|
77
|
+
icon={<ExpandGlyph />}
|
|
78
|
+
aria-label="Toggle subtree"
|
|
79
|
+
aria-hidden="true"
|
|
80
|
+
tabIndex={-1}
|
|
81
|
+
className={styles.expandButton}
|
|
82
|
+
onClick={(event) => {
|
|
83
|
+
event.stopPropagation();
|
|
84
|
+
if (disabled) return;
|
|
85
|
+
tree.toggleExpanded(node.value);
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
<span className={styles.label} data-selected={selected || undefined}>
|
|
89
|
+
{node.label}
|
|
90
|
+
</span>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
58
94
|
}
|
|
59
95
|
|
|
60
96
|
/**
|
|
@@ -64,6 +100,11 @@ function renderTreeNode({
|
|
|
64
100
|
* Wraps Mantine's `Tree` with a fully custom node renderer so every visual aspect (row box
|
|
65
101
|
* model, selected/unselected colors and typography, indent, item spacing) comes from
|
|
66
102
|
* Recursica's `tree` design tokens rather than Mantine's defaults.
|
|
103
|
+
*
|
|
104
|
+
* **Interaction pattern (fixed, not prop-configurable):** expand/collapse and select are
|
|
105
|
+
* independent — the chevron button toggles a node's subtree only, clicking the rest of a row
|
|
106
|
+
* (or pressing `Enter`/`Space`) selects it only, and `ArrowLeft`/`ArrowRight` toggle expansion
|
|
107
|
+
* only. See `RecursicaTreeProps` for the full breakdown.
|
|
67
108
|
*/
|
|
68
109
|
export const Tree = forwardRef<HTMLUListElement, TreeProps>(function Tree(
|
|
69
110
|
{
|
|
@@ -72,8 +113,7 @@ export const Tree = forwardRef<HTMLUListElement, TreeProps>(function Tree(
|
|
|
72
113
|
initialExpandedValues,
|
|
73
114
|
initialSelectedValues,
|
|
74
115
|
multiple = false,
|
|
75
|
-
|
|
76
|
-
selectOnClick = true,
|
|
116
|
+
disabled = false,
|
|
77
117
|
onNodeExpand,
|
|
78
118
|
onNodeCollapse,
|
|
79
119
|
onSelectedChange,
|
|
@@ -99,6 +139,53 @@ export const Tree = forwardRef<HTMLUListElement, TreeProps>(function Tree(
|
|
|
99
139
|
onSelectedChange?.(tree.selectedState);
|
|
100
140
|
}, [tree.selectedState, onSelectedChange]);
|
|
101
141
|
|
|
142
|
+
// `Enter`/`Space` select the focused node. Mantine's own `TreeNode` only wires this for
|
|
143
|
+
// `Space`, and only as an expand toggle (`expandOnSpace`, disabled below) — it has no
|
|
144
|
+
// select-on-key behavior and no `Enter` handling at all, and its key handler lives on the
|
|
145
|
+
// `<li role="treeitem">` itself (not reachable through `renderNode`'s `elementProps`), so this
|
|
146
|
+
// is added as a native listener on the tree root instead. `tree.select` is referentially
|
|
147
|
+
// stable (Mantine wraps it in `useCallback` with no deps), so this effect only re-runs when
|
|
148
|
+
// `disabled` actually changes.
|
|
149
|
+
const rootRef = React.useRef<HTMLUListElement>(null);
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
const root = rootRef.current;
|
|
152
|
+
if (!root) return;
|
|
153
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
154
|
+
if (disabled) return;
|
|
155
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
156
|
+
const treeItem = (event.target as HTMLElement).closest<HTMLElement>(
|
|
157
|
+
'[role="treeitem"]',
|
|
158
|
+
);
|
|
159
|
+
const value = treeItem?.dataset.value;
|
|
160
|
+
if (!value) return;
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
tree.select(value);
|
|
163
|
+
};
|
|
164
|
+
root.addEventListener("keydown", handleKeyDown);
|
|
165
|
+
return () => root.removeEventListener("keydown", handleKeyDown);
|
|
166
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
167
|
+
}, [tree.select, disabled]);
|
|
168
|
+
|
|
169
|
+
// When disabled, block every keydown at the capture phase — before it can reach either the
|
|
170
|
+
// listener above or Mantine's own internal per-node keydown handling (`ArrowLeft`/`ArrowRight`
|
|
171
|
+
// expand/collapse, baked into `TreeNode` itself with no prop to disable it). A capture-phase
|
|
172
|
+
// `stopPropagation` here keeps the event from ever reaching its target, so neither listener
|
|
173
|
+
// fires — this is the only reachable way to block Mantine's internal handling for a component
|
|
174
|
+
// with no `disabled` concept of its own. Mouse clicks are separately blocked via `pointer-
|
|
175
|
+
// events: none` in CSS (`.root[data-disabled]`); this effect only needs to cover keyboard.
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
const root = rootRef.current;
|
|
178
|
+
if (!root || !disabled) return;
|
|
179
|
+
const blockAllKeydown = (event: KeyboardEvent) => {
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
event.stopPropagation();
|
|
182
|
+
};
|
|
183
|
+
root.addEventListener("keydown", blockAllKeydown, { capture: true });
|
|
184
|
+
return () =>
|
|
185
|
+
root.removeEventListener("keydown", blockAllKeydown, { capture: true });
|
|
186
|
+
}, [disabled]);
|
|
187
|
+
const mergedRef = useMergedRef(ref, rootRef);
|
|
188
|
+
|
|
102
189
|
const classNameProp = (sanitizedProps as Record<string, unknown>)
|
|
103
190
|
.className as string | undefined;
|
|
104
191
|
const rootClass = classNameProp
|
|
@@ -107,12 +194,14 @@ export const Tree = forwardRef<HTMLUListElement, TreeProps>(function Tree(
|
|
|
107
194
|
|
|
108
195
|
return (
|
|
109
196
|
<MantineTree
|
|
110
|
-
ref={
|
|
197
|
+
ref={mergedRef}
|
|
111
198
|
data={data}
|
|
112
199
|
tree={tree}
|
|
113
|
-
expandOnClick={
|
|
114
|
-
selectOnClick={
|
|
115
|
-
|
|
200
|
+
expandOnClick={false}
|
|
201
|
+
selectOnClick={!disabled}
|
|
202
|
+
expandOnSpace={false}
|
|
203
|
+
renderNode={createRenderTreeNode(disabled)}
|
|
204
|
+
data-disabled={disabled || undefined}
|
|
116
205
|
classNames={{
|
|
117
206
|
root: rootClass,
|
|
118
207
|
node: styles.node,
|
|
@@ -57,7 +57,24 @@ Each node needs a unique `value` and a `label`. Any node with a `children` array
|
|
|
57
57
|
|
|
58
58
|
- `initialExpandedValues` accepts an array of node values, or `"*"` to start with every node expanded.
|
|
59
59
|
- `multiple` allows more than one node to be selected at once (default: single-select).
|
|
60
|
-
|
|
60
|
+
|
|
61
|
+
### Interaction pattern
|
|
62
|
+
|
|
63
|
+
Expanding/collapsing and selecting are independent, fixed interactions (not prop-configurable):
|
|
64
|
+
|
|
65
|
+
- Clicking the expand/collapse chevron toggles that node's subtree only — it never selects.
|
|
66
|
+
- Clicking anywhere else on a row selects it — it never toggles expansion.
|
|
67
|
+
- `Enter`/`Space` on a focused node selects it, even if the node has children.
|
|
68
|
+
- `ArrowLeft`/`ArrowRight` expand/collapse the focused node only, without changing selection.
|
|
69
|
+
- `ArrowUp`/`ArrowDown` move focus between rows.
|
|
70
|
+
|
|
71
|
+
### Disabled
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<Tree data={data} initialSelectedValues={["1.1"]} disabled />
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`disabled` disables the whole tree — no expand/collapse or select via click or keyboard, dimmed to the standard disabled opacity. A pre-selected node stays visibly selected, just dimmed along with the rest. There's no per-node disabled state (see **Known Constraints** below).
|
|
61
78
|
|
|
62
79
|
---
|
|
63
80
|
|