@recursica/mui-adapter 0.19.0 → 0.21.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +3 -3
  3. package/dist/mui-adapter.cjs +89 -58
  4. package/dist/mui-adapter.cjs.map +1 -1
  5. package/dist/mui-adapter.css +1 -1
  6. package/dist/mui-adapter.js +26163 -8727
  7. package/dist/mui-adapter.js.map +1 -1
  8. package/dist/src/components/Dropdown/BareDropdown.d.ts +41 -0
  9. package/dist/src/components/TimePicker/TimePicker.d.ts +16 -3
  10. package/dist/src/components/Tree/Tree.d.ts +5 -0
  11. package/dist/src/index.d.ts +1 -1
  12. package/docs/PHILOSOPHY.md +39 -0
  13. package/package.json +10 -3
  14. package/src/components/Box/USAGE.md +1 -1
  15. package/src/components/Button/BUTTON_IMPLEMENTATION_NOTES.md +22 -0
  16. package/src/components/Button/Button.module.css +10 -7
  17. package/src/components/Button/Button.tsx +4 -0
  18. package/src/components/Button/USAGE.md +1 -13
  19. package/src/components/Card/USAGE.md +1 -13
  20. package/src/components/Container/USAGE.md +1 -1
  21. package/src/components/Dropdown/BareDropdown.tsx +135 -0
  22. package/src/components/Dropdown/Dropdown.tsx +16 -0
  23. package/src/components/Grid/USAGE.md +6 -10
  24. package/src/components/Loader/USAGE.md +1 -23
  25. package/src/components/Menu/USAGE.md +0 -7
  26. package/src/components/Pagination/USAGE.md +0 -6
  27. package/src/components/Panel/USAGE.md +1 -58
  28. package/src/components/Stepper/USAGE.md +0 -7
  29. package/src/components/Tabs/USAGE.md +0 -7
  30. package/src/components/TimePicker/TIMEPICKER_IMPLEMENTATION_NOTES.md +68 -0
  31. package/src/components/TimePicker/TimePicker.module.css +213 -37
  32. package/src/components/TimePicker/TimePicker.stories.tsx +119 -4
  33. package/src/components/TimePicker/TimePicker.tsx +257 -8
  34. package/src/components/TimePicker/USAGE.md +27 -2
  35. package/src/components/Tree/IMPLEMENTATION_NOTES.md +28 -1
  36. package/src/components/Tree/Tree.module.css +114 -69
  37. package/src/components/Tree/Tree.stories.tsx +13 -0
  38. package/src/components/Tree/Tree.tsx +99 -32
  39. package/src/components/Tree/USAGE.md +18 -2
  40. package/src/index.ts +1 -0
@@ -4,7 +4,7 @@ import {
4
4
  useTreeItem,
5
5
  type UseTreeItemParameters,
6
6
  } from "@mui/x-tree-view/useTreeItem";
7
- import { useTreeItemModel } from "@mui/x-tree-view/hooks";
7
+ import { useTreeItemModel, useTreeItemUtils } from "@mui/x-tree-view/hooks";
8
8
  import { TreeItemProvider } from "@mui/x-tree-view/TreeItemProvider";
9
9
  import {
10
10
  TreeItemRoot,
@@ -22,6 +22,7 @@ import {
22
22
  type RecursicaTreeProps,
23
23
  type RecursicaTreeNode,
24
24
  } from "@recursica/adapter-common";
25
+ import { Button } from "../Button/Button";
25
26
  import styles from "./Tree.module.css";
26
27
 
27
28
  export type TreeProps = RecursicaOverStyled<
@@ -40,13 +41,16 @@ function collectAllValues(nodes: RecursicaTreeNode[]): string[] {
40
41
  return values;
41
42
  }
42
43
 
43
- /** Simple chevron glyph; MUI renders a different component for expanded vs collapsed rather
44
- * than one rotating icon (unlike Mantine's `[data-expanded]` approach), so collapseIcon carries
45
- * its own rotated variant. Both use `currentColor` — no dedicated icon-color token exists. */
46
- function ExpandGlyph() {
44
+ /** Chevron glyph; rendered inside a `Button` below. MUI renders a different component for
45
+ * expanded vs collapsed rather than one rotating icon (unlike Mantine's `[data-expanded]`
46
+ * approach), so `rotated` picks the CSS class that applies the rotation. Uses `currentColor` so
47
+ * it matches Button's own tokened icon color for the "text" variant. */
48
+ function ChevronGlyph({ rotated }: { rotated?: boolean }) {
47
49
  return (
48
50
  <svg
49
- className={styles.chevron}
51
+ className={
52
+ rotated ? `${styles.chevron} ${styles.chevronExpanded}` : styles.chevron
53
+ }
50
54
  viewBox="0 0 16 16"
51
55
  width="1em"
52
56
  height="1em"
@@ -61,29 +65,47 @@ function ExpandGlyph() {
61
65
  </svg>
62
66
  );
63
67
  }
64
- function CollapseGlyph() {
68
+
69
+ /** Never independently focusable/tab-stoppable (tabIndex={-1}, aria-hidden) — expand/collapse is
70
+ * driven by the surrounding `TreeItemIconContainer`'s own click handling (see `CustomTreeItem`),
71
+ * not by this Button itself, so the row stays the only focusable element and the button never
72
+ * shows its own focus state. Rendered for every row, including leaf items, so row alignment
73
+ * stays consistent; `hidden` (visibility, not display) is used for leaf rows instead of omitting
74
+ * the button, so the layout space is reserved without duplicating Button's own size tokens. */
75
+ function ExpandToggleButton({
76
+ rotated,
77
+ hidden,
78
+ }: {
79
+ rotated?: boolean;
80
+ hidden?: boolean;
81
+ }) {
65
82
  return (
66
- <svg
67
- className={`${styles.chevron} ${styles.chevronExpanded}`}
68
- viewBox="0 0 16 16"
69
- width="1em"
70
- height="1em"
71
- fill="none"
72
- stroke="currentColor"
73
- strokeWidth="2"
74
- strokeLinecap="round"
75
- strokeLinejoin="round"
83
+ <Button
84
+ overStyled
85
+ variant="text"
86
+ size="small"
87
+ icon={<ChevronGlyph rotated={rotated} />}
88
+ aria-label="Toggle subtree"
76
89
  aria-hidden="true"
77
- >
78
- <path d="M5 3l5 5-5 5" />
79
- </svg>
90
+ tabIndex={-1}
91
+ className={
92
+ hidden
93
+ ? `${styles.expandButton} ${styles.expandButtonHidden}`
94
+ : styles.expandButton
95
+ }
96
+ />
80
97
  );
81
98
  }
99
+ function ExpandGlyph() {
100
+ return <ExpandToggleButton />;
101
+ }
102
+ function CollapseGlyph() {
103
+ return <ExpandToggleButton rotated />;
104
+ }
82
105
  /** Rendered for leaf items so the icon-container box (and therefore row alignment) stays
83
- * consistent whether or not a row has an expand chevron, matching the mantine-adapter
84
- * implementation's always-present `.expandIcon` span. */
106
+ * consistent whether or not a row has an expand chevron. */
85
107
  function EndGlyph() {
86
- return <span aria-hidden="true" />;
108
+ return <ExpandToggleButton hidden />;
87
109
  }
88
110
 
89
111
  interface CustomTreeItemProps extends UseTreeItemParameters {
@@ -111,14 +133,43 @@ const CustomTreeItem = forwardRef<HTMLLIElement, CustomTreeItemProps>(
111
133
  status,
112
134
  } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref });
113
135
 
136
+ // Exposes the same select/expand actions `useTreeItem`'s internal keyboard-navigation plugin
137
+ // uses, so we can override just the `Enter` key below without reimplementing selection.
138
+ const { interactions } = useTreeItemUtils({ itemId, children });
139
+
114
140
  const item = useTreeItemModel<RecursicaTreeNode>(itemId);
115
141
 
142
+ const iconContainerProps = getIconContainerProps({
143
+ className: styles.iconContainer,
144
+ });
145
+
146
+ // `Enter` selects the focused node, full stop — never expands it, unlike
147
+ // `@mui/x-tree-view`'s own default (which expands an expandable node on `Enter` and only
148
+ // falls back to selecting leaf nodes). Setting `defaultMuiPrevented` is the documented
149
+ // extension point `createRootHandleKeyDown` checks before running its own handling, so this
150
+ // fully replaces rather than races the built-in behavior. `Space` already selects-only by
151
+ // default, so it needs no override.
152
+ const handleRootKeyDown = (
153
+ event: React.KeyboardEvent<HTMLLIElement> & {
154
+ defaultMuiPrevented?: boolean;
155
+ },
156
+ ) => {
157
+ if (event.key !== "Enter") return;
158
+ event.preventDefault();
159
+ event.defaultMuiPrevented = true;
160
+ // `handleSelection` only reads modifier keys (shiftKey/ctrlKey/metaKey) off the event and
161
+ // forwards it on for focus bookkeeping — its `React.MouseEvent` type is narrower than
162
+ // necessary for what it actually uses, so a KeyboardEvent works fine at runtime here.
163
+ interactions.handleSelection(event as unknown as React.MouseEvent);
164
+ };
165
+
116
166
  return (
117
167
  <TreeItemProvider {...getContextProviderProps()}>
118
168
  <TreeItemRoot
119
169
  {...getRootProps({
120
170
  ...rest,
121
171
  className: `${styles.node} ${className ?? ""}`,
172
+ onKeyDown: handleRootKeyDown,
122
173
  })}
123
174
  >
124
175
  <TreeItemContent
@@ -126,7 +177,15 @@ const CustomTreeItem = forwardRef<HTMLLIElement, CustomTreeItemProps>(
126
177
  status={status}
127
178
  >
128
179
  <TreeItemIconContainer
129
- {...getIconContainerProps({ className: styles.iconContainer })}
180
+ {...iconContainerProps}
181
+ // Stops the click from also reaching `TreeItemContent`'s own click handler, which
182
+ // otherwise selects on *any* click inside it (including the icon container) once
183
+ // `expansionTrigger` is `"iconContainer"` — the button must expand/collapse
184
+ // without ever selecting.
185
+ onClick={(event) => {
186
+ event.stopPropagation();
187
+ iconContainerProps.onClick(event);
188
+ }}
130
189
  >
131
190
  <TreeItemIcon status={status} />
132
191
  </TreeItemIconContainer>
@@ -152,6 +211,11 @@ const CustomTreeItem = forwardRef<HTMLLIElement, CustomTreeItemProps>(
152
211
  * Wraps `@mui/x-tree-view`'s `RichTreeView` with a fully custom item renderer so every visual
153
212
  * aspect (row box model, selected/unselected colors and typography, indent, item spacing)
154
213
  * comes from Recursica's `tree` design tokens rather than MUI's defaults.
214
+ *
215
+ * **Interaction pattern (fixed, not prop-configurable):** expand/collapse and select are
216
+ * independent — the chevron button toggles a node's subtree only, clicking the rest of a row
217
+ * (or pressing `Enter`/`Space`) selects it only, and `ArrowLeft`/`ArrowRight` toggle expansion
218
+ * only. See `RecursicaTreeProps` for the full breakdown.
155
219
  */
156
220
  export const Tree = forwardRef<HTMLUListElement, TreeProps>(function Tree(
157
221
  {
@@ -160,8 +224,7 @@ export const Tree = forwardRef<HTMLUListElement, TreeProps>(function Tree(
160
224
  initialExpandedValues,
161
225
  initialSelectedValues,
162
226
  multiple = false,
163
- expandOnClick = true,
164
- selectOnClick = true,
227
+ disabled = false,
165
228
  onNodeExpand,
166
229
  onNodeCollapse,
167
230
  onSelectedChange,
@@ -195,12 +258,16 @@ export const Tree = forwardRef<HTMLUListElement, TreeProps>(function Tree(
195
258
  multiple ? (initialSelectedValues ?? []) : initialSelectedValues?.[0]
196
259
  }
197
260
  multiSelect={multiple}
198
- // MUI has no separate expandOnClick/selectOnClick toggles; `expansionTrigger` controls
199
- // whether clicking the row content (vs only the icon container) also expands/collapses.
200
- // `selectOnClick={false}` has no exact MUI equivalent (selection always follows content
201
- // clicks unless disabled entirely) — see IMPLEMENTATION_NOTES.md.
202
- expansionTrigger={expandOnClick ? "content" : "iconContainer"}
203
- disableSelection={!selectOnClick}
261
+ // Whole-tree `disabled` reuses `@mui/x-tree-view`'s own per-item `isItemDisabled` — marking
262
+ // every item disabled at once — rather than a bespoke mechanism, since MUI already handles
263
+ // blocking click/keyboard select/expand and dimming (`.row[data-disabled]` in
264
+ // Tree.module.css) for disabled items internally.
265
+ isItemDisabled={disabled ? () => true : undefined}
266
+ // Fixed, not prop-driven: only the icon container (the chevron button) expands/collapses
267
+ // on click; clicking anywhere else on the row selects instead. `CustomTreeItem` additionally
268
+ // stops the icon container's click from also bubbling into the content's own selection
269
+ // handler — see IMPLEMENTATION_NOTES.md.
270
+ expansionTrigger="iconContainer"
204
271
  onItemExpansionToggle={(_event, itemId, isExpanded) => {
205
272
  if (isExpanded) onNodeExpand?.(itemId);
206
273
  else onNodeCollapse?.(itemId);
@@ -60,7 +60,24 @@ Each node needs a unique `value` and a `label`. Any node with a `children` array
60
60
 
61
61
  - `initialExpandedValues` accepts an array of node values, or `"*"` to start with every node expanded.
62
62
  - `multiple` allows more than one node to be selected at once (default: single-select).
63
- - `expandOnClick` (default `true`) controls whether clicking a row's content also expands/collapses it (vs. only clicking its chevron); `selectOnClick` (default `true`) controls whether selection is enabled at all. See **Known Constraints** below for how these map onto the underlying library.
63
+
64
+ ### Interaction pattern
65
+
66
+ Expanding/collapsing and selecting are independent, fixed interactions (not prop-configurable):
67
+
68
+ - Clicking the expand/collapse chevron toggles that node's subtree only — it never selects.
69
+ - Clicking anywhere else on a row selects it — it never toggles expansion.
70
+ - `Enter`/`Space` on a focused node selects it, even if the node has children.
71
+ - `ArrowLeft`/`ArrowRight` expand/collapse the focused node only, without changing selection.
72
+ - `ArrowUp`/`ArrowDown` move focus between rows.
73
+
74
+ ### Disabled
75
+
76
+ ```tsx
77
+ <Tree data={data} initialSelectedValues={["1.1"]} disabled />
78
+ ```
79
+
80
+ `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).
64
81
 
65
82
  ---
66
83
 
@@ -78,6 +95,5 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
78
95
 
79
96
  ## 5. Known Constraints
80
97
 
81
- - **`expandOnClick`/`selectOnClick` are approximated, not exact.** `@mui/x-tree-view` doesn't expose the same two independent toggles Mantine's `Tree` does. `expandOnClick` maps to MUI's `expansionTrigger` (`"content"` vs `"iconContainer"`); `selectOnClick={false}` maps to `disableSelection` (which disables selection entirely, rather than only suppressing it on a content click while leaving some other path to select). See `IMPLEMENTATION_NOTES.md`.
82
98
  - No checkbox/multi-check styling is exposed — `@mui/x-tree-view` supports checkbox selection, but Recursica's `tree` design tokens don't yet define a checked visual state, so it isn't part of the Recursica API.
83
99
  - No per-node `disabled` state — the Figma UI Kit tokens don't define one, and `RecursicaTreeNode` has no `disabled` field either.
package/src/index.ts CHANGED
@@ -183,5 +183,6 @@ export type {
183
183
  RecursicaSliderProps,
184
184
  RecursicaTextAreaProps,
185
185
  RecursicaTextFieldProps,
186
+ RecursicaTimePickerProps,
186
187
  RecursicaToastProps,
187
188
  } from "./components";