@jsenv/navi 0.0.1 → 0.1.1

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 (139) hide show
  1. package/dist/jsenv_navi.js +22959 -0
  2. package/index.js +66 -16
  3. package/package.json +23 -11
  4. package/src/actions.js +50 -26
  5. package/src/browser_integration/browser_integration.js +31 -6
  6. package/src/browser_integration/via_history.js +42 -9
  7. package/src/components/action_execution/render_actionable_component.jsx +6 -4
  8. package/src/components/action_execution/use_action.js +51 -282
  9. package/src/components/action_execution/use_execute_action.js +106 -92
  10. package/src/components/action_execution/use_run_on_mount.js +9 -0
  11. package/src/components/action_renderer.jsx +21 -32
  12. package/src/components/demos/0_button_demo.html +574 -103
  13. package/src/components/demos/10_column_reordering_debug.html +277 -0
  14. package/src/components/demos/11_table_selection_debug.html +432 -0
  15. package/src/components/demos/1_checkbox_demo.html +579 -202
  16. package/src/components/demos/2_input_textual_demo.html +81 -138
  17. package/src/components/demos/3_radio_demo.html +0 -2
  18. package/src/components/demos/4_select_demo.html +19 -23
  19. package/src/components/demos/6_tablist_demo.html +77 -0
  20. package/src/components/demos/7_table_selection_demo.html +176 -0
  21. package/src/components/demos/8_table_fixed_headers_demo.html +584 -0
  22. package/src/components/demos/9_table_column_drag_demo.html +325 -0
  23. package/src/components/demos/action/0_button_demo.html +2 -4
  24. package/src/components/demos/action/1_input_text_demo.html +643 -222
  25. package/src/components/demos/action/3_details_demo.html +146 -115
  26. package/src/components/demos/action/4_input_checkbox_demo.html +442 -322
  27. package/src/components/demos/action/5_input_checkbox_state_demo.html +270 -0
  28. package/src/components/demos/action/6_checkbox_list_demo.html +304 -72
  29. package/src/components/demos/action/7_radio_list_demo.html +310 -170
  30. package/src/components/demos/action/{8_editable_text_demo.html → 8_editable_demo.html} +65 -76
  31. package/src/components/demos/action/9_link_demo.html +84 -62
  32. package/src/components/demos/ui_transition/0_action_renderer_ui_transition_demo.html +695 -0
  33. package/src/components/demos/ui_transition/1_nested_ui_transition_demo.html +429 -0
  34. package/src/components/demos/ui_transition/2_height_transition_test.html +295 -0
  35. package/src/components/details/details.jsx +62 -64
  36. package/src/components/edition/editable.jsx +186 -0
  37. package/src/components/field/README.md +247 -0
  38. package/src/components/{input → field}/button.jsx +151 -130
  39. package/src/components/field/checkbox_list.jsx +184 -0
  40. package/src/components/{collect_form_element_values.js → field/collect_form_element_values.js} +7 -4
  41. package/src/components/{input → field}/field_css.js +4 -1
  42. package/src/components/field/form.jsx +211 -0
  43. package/src/components/{input → field}/input.jsx +1 -0
  44. package/src/components/{input → field}/input_checkbox.jsx +132 -155
  45. package/src/components/{input → field}/input_radio.jsx +135 -46
  46. package/src/components/field/input_textual.jsx +418 -0
  47. package/src/components/field/label.jsx +32 -0
  48. package/src/components/field/radio_list.jsx +182 -0
  49. package/src/components/{input → field}/select.jsx +17 -32
  50. package/src/components/field/use_action_events.js +132 -0
  51. package/src/components/field/use_form_events.js +55 -0
  52. package/src/components/field/use_ui_state_controller.js +506 -0
  53. package/src/components/item_tracker/README.md +461 -0
  54. package/src/components/item_tracker/use_isolated_item_tracker.jsx +209 -0
  55. package/src/components/item_tracker/use_isolated_item_tracker_demo.html +148 -0
  56. package/src/components/item_tracker/use_isolated_item_tracker_demo.jsx +460 -0
  57. package/src/components/item_tracker/use_item_tracker.jsx +143 -0
  58. package/src/components/item_tracker/use_item_tracker_demo.html +207 -0
  59. package/src/components/item_tracker/use_item_tracker_demo.jsx +216 -0
  60. package/src/components/keyboard_shortcuts/active_keyboard_shortcuts.jsx +87 -0
  61. package/src/components/keyboard_shortcuts/aria_key_shortcuts.js +61 -0
  62. package/src/components/keyboard_shortcuts/keyboard_key_meta.js +17 -0
  63. package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +371 -0
  64. package/src/components/link/link.jsx +65 -102
  65. package/src/components/link/link_with_icon.jsx +52 -0
  66. package/src/components/loader/loader_background.jsx +85 -64
  67. package/src/components/loader/rectangle_loading.jsx +38 -19
  68. package/src/components/route.jsx +8 -4
  69. package/src/components/selection/selection.jsx +1583 -0
  70. package/src/components/svg/font_sized_svg.jsx +45 -0
  71. package/src/components/svg/icon_and_text.jsx +21 -0
  72. package/src/components/svg/svg_mask_overlay.jsx +105 -0
  73. package/src/components/table/drag/table_drag.jsx +506 -0
  74. package/src/components/table/resize/table_resize.jsx +650 -0
  75. package/src/components/table/resize/table_size.js +43 -0
  76. package/src/components/table/selection/table_selection.js +106 -0
  77. package/src/components/table/selection/table_selection.jsx +203 -0
  78. package/src/components/table/sticky/sticky_group.js +354 -0
  79. package/src/components/table/sticky/table_sticky.js +25 -0
  80. package/src/components/table/sticky/table_sticky.jsx +501 -0
  81. package/src/components/table/table.jsx +721 -0
  82. package/src/components/table/table_css.js +211 -0
  83. package/src/components/table/table_ui.jsx +49 -0
  84. package/src/components/table/use_cells_and_columns.js +90 -0
  85. package/src/components/table/use_object_array_to_cells.js +46 -0
  86. package/src/components/table/z_indexes.js +23 -0
  87. package/src/components/tablist/tablist.jsx +99 -0
  88. package/src/components/text/overflow.jsx +15 -0
  89. package/src/components/text/text_and_count.jsx +28 -0
  90. package/src/components/ui_transition.jsx +128 -0
  91. package/src/components/use_auto_focus.js +58 -7
  92. package/src/components/use_batch_during_render.js +33 -0
  93. package/src/components/use_debounce_true.js +7 -7
  94. package/src/components/use_dependencies_diff.js +35 -0
  95. package/src/components/use_focus_group.js +4 -3
  96. package/src/components/use_initial_value.js +8 -34
  97. package/src/components/use_signal_sync.js +1 -1
  98. package/src/components/use_stable_callback.js +68 -0
  99. package/src/components/use_state_array.js +16 -9
  100. package/src/docs/actions.md +22 -0
  101. package/src/notes.md +33 -12
  102. package/src/route/route.js +97 -47
  103. package/src/store/resource_graph.js +2 -1
  104. package/src/store/tests/{resource_graph_dependencies.test.js → resource_graph_dependencies.test_manual.js} +13 -13
  105. package/src/utils/is_signal.js +20 -0
  106. package/src/utils/stringify_for_display.js +4 -23
  107. package/src/validation/constraints/confirm_constraint.js +14 -0
  108. package/src/validation/constraints/create_unique_value_constraint.js +27 -0
  109. package/src/validation/constraints/native_constraints.js +313 -0
  110. package/src/validation/constraints/readonly_constraint.js +36 -0
  111. package/src/validation/constraints/single_space_constraint.js +13 -0
  112. package/src/validation/custom_constraint_validation.js +599 -0
  113. package/src/validation/custom_message.js +18 -0
  114. package/src/validation/demos/browser_style.png +0 -0
  115. package/src/validation/demos/form_validation_demo.html +142 -0
  116. package/src/validation/demos/form_validation_demo_preact.html +87 -0
  117. package/src/validation/demos/form_validation_native_popover_demo.html +168 -0
  118. package/src/validation/demos/form_validation_vs_native_demo.html +172 -0
  119. package/src/validation/demos/validation_message_demo.html +203 -0
  120. package/src/validation/hooks/use_constraints.js +23 -0
  121. package/src/validation/hooks/use_custom_validation_ref.js +73 -0
  122. package/src/validation/hooks/use_validation_message.js +19 -0
  123. package/src/validation/validation_message.js +741 -0
  124. package/src/components/editable_text/editable_text.jsx +0 -96
  125. package/src/components/form.jsx +0 -144
  126. package/src/components/input/checkbox_list.jsx +0 -294
  127. package/src/components/input/field.jsx +0 -61
  128. package/src/components/input/input_textual.jsx +0 -338
  129. package/src/components/input/radio_list.jsx +0 -283
  130. package/src/components/input/use_form_event.js +0 -20
  131. package/src/components/input/use_on_change.js +0 -12
  132. package/src/components/selection/selection.js +0 -5
  133. package/src/components/selection/selection_context.jsx +0 -262
  134. package/src/components/shortcut/shortcut_context.jsx +0 -390
  135. package/src/components/use_action_events.js +0 -37
  136. package/src/utils/iterable_weak_set.js +0 -62
  137. /package/src/components/demos/action/{11_nested_shortcuts_demo.html → 11_nested_shortcuts_demo.xhtml} +0 -0
  138. /package/src/components/{shortcut → keyboard_shortcuts}/os.js +0 -0
  139. /package/src/route/{route.test.html → route.xtest.html} +0 -0
@@ -0,0 +1,295 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" href="data:," />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Height Transition Test - Big to Small</title>
8
+ <style>
9
+ body {
10
+ font-family:
11
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
12
+ max-width: 600px;
13
+ margin: 0 auto;
14
+ padding: 20px;
15
+ background: #f5f5f5;
16
+ line-height: 1.5;
17
+ }
18
+
19
+ h1 {
20
+ color: #333;
21
+ margin-bottom: 20px;
22
+ }
23
+
24
+ .controls {
25
+ background: white;
26
+ border-radius: 8px;
27
+ padding: 20px;
28
+ margin-bottom: 20px;
29
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
30
+ }
31
+
32
+ .controls h3 {
33
+ margin-top: 0;
34
+ margin-bottom: 15px;
35
+ color: #555;
36
+ }
37
+
38
+ .button-group {
39
+ display: flex;
40
+ gap: 10px;
41
+ margin-bottom: 10px;
42
+ flex-wrap: wrap;
43
+ }
44
+
45
+ button {
46
+ padding: 10px 16px;
47
+ border: 1px solid #ddd;
48
+ border-radius: 6px;
49
+ background: #fff;
50
+ cursor: pointer;
51
+ font: inherit;
52
+ transition: all 0.15s;
53
+ }
54
+
55
+ button:hover {
56
+ background: #f8f9fa;
57
+ border-color: #007acc;
58
+ }
59
+
60
+ button:active {
61
+ background: #e9ecef;
62
+ }
63
+
64
+ .demo-container {
65
+ background: white;
66
+ border-radius: 8px;
67
+ padding: 20px;
68
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
69
+ }
70
+
71
+ /* UI Transition Container */
72
+ #transition-box {
73
+ border: 2px solid #007acc;
74
+ border-radius: 8px;
75
+ background: #f8fbff;
76
+ max-width: 400px;
77
+ margin: 20px 0;
78
+ }
79
+
80
+ /* Content styles */
81
+ .big-content {
82
+ padding: 30px;
83
+ background: repeating-linear-gradient(
84
+ to bottom,
85
+ rgba(0, 0, 0, 0.05) 0px,
86
+ rgba(0, 0, 0, 0.05) 20px,
87
+ rgba(0, 0, 0, 0.1) 20px,
88
+ rgba(0, 0, 0, 0.1) 40px
89
+ );
90
+ color: #222;
91
+ border-radius: 6px;
92
+ min-height: 300px;
93
+ display: flex;
94
+ flex-direction: column;
95
+ justify-content: center;
96
+ align-items: center;
97
+ text-align: center;
98
+ }
99
+
100
+ .big-content h2 {
101
+ margin: 0 0 15px 0;
102
+ font-size: 24px;
103
+ }
104
+
105
+ .big-content p {
106
+ margin: 5px 0;
107
+ opacity: 0.9;
108
+ font-size: 16px;
109
+ }
110
+
111
+ .small-content {
112
+ padding: 15px;
113
+ background: transparent; /* reveal any cropping during cross-fade */
114
+ color: #222;
115
+ border-radius: 6px;
116
+ text-align: center;
117
+ font-weight: 600;
118
+ border: 1px dashed #bbb; /* show where the new smaller height is */
119
+ }
120
+
121
+ /* A bright bar anchored at the bottom to make cropping obvious when height shrinks */
122
+ .big-content .bottom-flag {
123
+ margin-top: 20px;
124
+ width: 100%;
125
+ height: 40px;
126
+ border-radius: 4px;
127
+ background: repeating-linear-gradient(
128
+ 45deg,
129
+ #ff1744,
130
+ #ff1744 10px,
131
+ #ff9100 10px,
132
+ #ff9100 20px
133
+ );
134
+ box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.15) inset;
135
+ }
136
+
137
+ .note {
138
+ background: #e7f3ff;
139
+ border: 1px solid #b8daff;
140
+ color: #004085;
141
+ padding: 12px;
142
+ border-radius: 6px;
143
+ font-size: 14px;
144
+ margin-top: 15px;
145
+ }
146
+ </style>
147
+ </head>
148
+ <body>
149
+ <h1>Height Transition Test</h1>
150
+
151
+ <div class="controls">
152
+ <h3>Test Controls</h3>
153
+ <div class="button-group">
154
+ <button id="show-big">Show Big Content</button>
155
+ <button id="show-small">Show Small Content</button>
156
+ <button id="clear">Clear Content</button>
157
+ </div>
158
+ <div class="button-group">
159
+ <label style="display: flex; align-items: center; gap: 8px">
160
+ <input type="checkbox" id="toggle-transitions" />
161
+ Enable Size Transitions
162
+ </label>
163
+ </div>
164
+ </div>
165
+
166
+ <div class="demo-container">
167
+ <h3>Transition Container</h3>
168
+ <div class="note">
169
+ Tip: With size transitions DISABLED (default), switching from the tall
170
+ content to the small content will crop the outgoing element while the
171
+ content transition runs. Toggle size transitions ON to see the
172
+ difference. The striped bar at the bottom of the big content helps
173
+ visualize cropping when the container height shrinks instantly.
174
+ </div>
175
+
176
+ <div
177
+ id="transition-box"
178
+ class="ui_transition_container"
179
+ data-content-transition="cross-fade"
180
+ data-content-transition-duration="600"
181
+ >
182
+ <div class="ui_transition_outer_wrapper">
183
+ <div class="ui_transition_measure_wrapper">
184
+ <div class="ui_transition_slot" data-content-key="initial">
185
+ <div class="big-content">
186
+ <h2>🎯 Big Content</h2>
187
+ <p>This is a tall element with lots of content</p>
188
+ <p>It takes up significant vertical space</p>
189
+ <p>Perfect for testing height transitions</p>
190
+ <p>When you switch to small content,</p>
191
+ <p>watch how smoothly it transitions!</p>
192
+ <div
193
+ class="bottom-flag"
194
+ title="Bottom area to reveal cropping"
195
+ ></div>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ <div class="ui_transition_content_overlay"></div>
201
+ </div>
202
+ </div>
203
+
204
+ <script type="module">
205
+ import { initUITransition } from "../../../../../dom/src/ui_transition/ui_transition.js";
206
+
207
+ const transitionBox = document.getElementById("transition-box");
208
+ const { slot } = initUITransition(transitionBox);
209
+
210
+ // Content templates
211
+ const createBigContent = () => {
212
+ const div = document.createElement("div");
213
+ div.className = "big-content";
214
+ div.setAttribute("data-content-key", "big");
215
+ div.innerHTML = `
216
+ <h2>🎯 Big Content</h2>
217
+ <p>This is a tall element with lots of content</p>
218
+ <p>It takes up significant vertical space</p>
219
+ <p>Perfect for testing height transitions</p>
220
+ <p>When you switch to small content,</p>
221
+ <p>watch how smoothly it transitions!</p>
222
+ <div class="bottom-flag" title="Bottom area to reveal cropping"></div>
223
+ `;
224
+ return div;
225
+ };
226
+
227
+ const createSmallContent = () => {
228
+ const div = document.createElement("div");
229
+ div.className = "small-content";
230
+ div.setAttribute("data-content-key", "small");
231
+ div.textContent = "📦 Small Content - Compact and concise!";
232
+ return div;
233
+ };
234
+
235
+ // no loading state in this simplified demo
236
+
237
+ // Button handlers
238
+ document.getElementById("show-big").addEventListener("click", () => {
239
+ slot.innerHTML = "";
240
+ slot.appendChild(createBigContent());
241
+ });
242
+
243
+ document.getElementById("show-small").addEventListener("click", () => {
244
+ slot.innerHTML = "";
245
+ slot.appendChild(createSmallContent());
246
+ });
247
+
248
+ // removed loading button
249
+
250
+ document.getElementById("clear").addEventListener("click", () => {
251
+ slot.innerHTML = "";
252
+ });
253
+
254
+ // Initialize and handle size transitions checkbox
255
+ const sizeToggle = document.getElementById("toggle-transitions");
256
+ sizeToggle.checked = transitionBox.hasAttribute("data-size-transition");
257
+ sizeToggle.addEventListener("change", (e) => {
258
+ if (e.target.checked) {
259
+ transitionBox.setAttribute("data-size-transition", "");
260
+ } else {
261
+ transitionBox.removeAttribute("data-size-transition");
262
+ }
263
+ });
264
+
265
+ // removed debug toggle
266
+
267
+ // Quick test sequence
268
+ let testRunning = false;
269
+ document.addEventListener("keydown", (e) => {
270
+ if (e.key === " " && !testRunning) {
271
+ e.preventDefault();
272
+ testRunning = true;
273
+
274
+ // Auto-cycle through states (big -> small -> big)
275
+ setTimeout(() => {
276
+ slot.innerHTML = "";
277
+ slot.appendChild(createSmallContent());
278
+ }, 1000);
279
+
280
+ setTimeout(() => {
281
+ slot.innerHTML = "";
282
+ slot.appendChild(createBigContent());
283
+ }, 2500);
284
+
285
+ setTimeout(() => {
286
+ testRunning = false;
287
+ }, 5500);
288
+ }
289
+ });
290
+
291
+ console.log("Height Transition Test loaded!");
292
+ console.log("Press SPACEBAR for auto-cycle test");
293
+ </script>
294
+ </body>
295
+ </html>
@@ -1,15 +1,16 @@
1
1
  import { elementIsFocusable, findAfter } from "@jsenv/dom";
2
- import { requestAction } from "@jsenv/validation";
3
2
  import { forwardRef } from "preact/compat";
4
3
  import { useEffect, useImperativeHandle, useRef, useState } from "preact/hooks";
4
+
5
5
  import { useNavState } from "../../browser_integration/browser_integration.js";
6
6
  import { useActionStatus } from "../../use_action_status.js";
7
+ import { requestAction } from "../../validation/custom_constraint_validation.js";
7
8
  import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
8
9
  import { useAction } from "../action_execution/use_action.js";
9
10
  import { useExecuteAction } from "../action_execution/use_execute_action.js";
10
11
  import { ActionRenderer } from "../action_renderer.jsx";
11
- import { useKeyboardShortcuts } from "../shortcut/shortcut_context.jsx";
12
- import { useActionEvents } from "../use_action_events.js";
12
+ import { useActionEvents } from "../field/use_action_events.js";
13
+ import { useKeyboardShortcuts } from "../keyboard_shortcuts/keyboard_shortcuts.js";
13
14
  import { useFocusGroup } from "../use_focus_group.js";
14
15
  import { SummaryMarker } from "./summary_marker.jsx";
15
16
 
@@ -60,7 +61,6 @@ const DetailsBasic = forwardRef((props, ref) => {
60
61
  const {
61
62
  id,
62
63
  label = "Summary",
63
- children,
64
64
  open,
65
65
  loading,
66
66
  className,
@@ -70,6 +70,7 @@ const DetailsBasic = forwardRef((props, ref) => {
70
70
  openKeyShortcut = "ArrowRight",
71
71
  closeKeyShortcut = "ArrowLeft",
72
72
  onToggle,
73
+ children,
73
74
  ...rest
74
75
  } = props;
75
76
  const innerRef = useRef();
@@ -97,78 +98,72 @@ const DetailsBasic = forwardRef((props, ref) => {
97
98
  * - https://stackoverflow.com/questions/58942600/react-html-details-toggles-uncontrollably-when-starts-open
98
99
  *
99
100
  */
100
- const mountedRef = useRef(false);
101
- useEffect(() => {
102
- mountedRef.current = true;
103
- }, []);
104
101
 
105
102
  const summaryRef = useRef(null);
106
-
107
- useKeyboardShortcuts(
108
- innerRef,
109
- [
110
- {
111
- key: openKeyShortcut,
112
- enabled: arrowKeyShortcuts,
113
- when: (e) =>
114
- document.activeElement === summaryRef.current &&
115
- // avoid handling openKeyShortcut twice when keydown occurs inside nested details
116
- !e.defaultPrevented,
117
- action: (e) => {
118
- const details = innerRef.current;
119
- if (!details.open) {
120
- e.preventDefault();
121
- details.open = true;
122
- return;
123
- }
124
- const summary = summaryRef.current;
125
- const firstFocusableElementInDetails = findAfter(
126
- summary,
127
- elementIsFocusable,
128
- { root: details },
129
- );
130
- if (!firstFocusableElementInDetails) {
131
- return;
132
- }
103
+ useKeyboardShortcuts(innerRef, [
104
+ {
105
+ key: openKeyShortcut,
106
+ enabled: arrowKeyShortcuts,
107
+ when: (e) =>
108
+ document.activeElement === summaryRef.current &&
109
+ // avoid handling openKeyShortcut twice when keydown occurs inside nested details
110
+ !e.defaultPrevented,
111
+ action: (e) => {
112
+ const details = innerRef.current;
113
+ if (!details.open) {
133
114
  e.preventDefault();
134
- firstFocusableElementInDetails.focus();
135
- },
115
+ details.open = true;
116
+ return;
117
+ }
118
+ const summary = summaryRef.current;
119
+ const firstFocusableElementInDetails = findAfter(
120
+ summary,
121
+ elementIsFocusable,
122
+ { root: details },
123
+ );
124
+ if (!firstFocusableElementInDetails) {
125
+ return;
126
+ }
127
+ e.preventDefault();
128
+ firstFocusableElementInDetails.focus();
136
129
  },
137
- {
138
- key: closeKeyShortcut,
139
- enabled: arrowKeyShortcuts,
140
- when: () => {
141
- const details = innerRef.current;
142
- return details.open;
143
- },
144
- action: (e) => {
145
- const details = innerRef.current;
146
- const summary = summaryRef.current;
147
- if (document.activeElement === summary) {
148
- e.preventDefault();
149
- summary.focus();
150
- details.open = false;
151
- } else {
152
- e.preventDefault();
153
- summary.focus();
154
- }
155
- },
130
+ },
131
+ {
132
+ key: closeKeyShortcut,
133
+ enabled: arrowKeyShortcuts,
134
+ when: () => {
135
+ const details = innerRef.current;
136
+ return details.open;
137
+ },
138
+ action: (e) => {
139
+ const details = innerRef.current;
140
+ const summary = summaryRef.current;
141
+ if (document.activeElement === summary) {
142
+ e.preventDefault();
143
+ summary.focus();
144
+ details.open = false;
145
+ } else {
146
+ e.preventDefault();
147
+ summary.focus();
148
+ }
156
149
  },
157
- ],
158
- (shortcut, e) => {
159
- shortcut.action(e);
160
150
  },
161
- );
151
+ ]);
152
+
153
+ const mountedRef = useRef(false);
154
+ useEffect(() => {
155
+ mountedRef.current = true;
156
+ }, []);
162
157
 
163
158
  return (
164
159
  <details
165
160
  {...rest}
161
+ ref={innerRef}
166
162
  id={id}
167
163
  className={[
168
164
  "navi_details",
169
165
  ...(className ? className.split(" ") : []),
170
166
  ].join(" ")}
171
- ref={innerRef}
172
167
  onToggle={(e) => {
173
168
  const isOpen = e.newState === "open";
174
169
  if (mountedRef.current) {
@@ -218,7 +213,9 @@ const DetailsWithAction = forwardRef((props, ref) => {
218
213
  });
219
214
  useActionEvents(innerRef, {
220
215
  onPrevented: onActionPrevented,
221
- onAction: executeAction,
216
+ onAction: (e) => {
217
+ executeAction(e);
218
+ },
222
219
  onStart: onActionStart,
223
220
  onError: onActionError,
224
221
  onEnd: onActionEnd,
@@ -228,10 +225,12 @@ const DetailsWithAction = forwardRef((props, ref) => {
228
225
  <DetailsBasic
229
226
  {...rest}
230
227
  ref={innerRef}
228
+ loading={loading || actionLoading}
231
229
  onToggle={(toggleEvent) => {
232
230
  const isOpen = toggleEvent.newState === "open";
233
231
  if (isOpen) {
234
- requestAction(effectiveAction, {
232
+ requestAction(toggleEvent.target, effectiveAction, {
233
+ actionOrigin: "action_prop",
235
234
  event: toggleEvent,
236
235
  method: "run",
237
236
  });
@@ -240,7 +239,6 @@ const DetailsWithAction = forwardRef((props, ref) => {
240
239
  }
241
240
  onToggle?.(toggleEvent);
242
241
  }}
243
- loading={loading || actionLoading}
244
242
  >
245
243
  <ActionRenderer action={effectiveAction}>{children}</ActionRenderer>
246
244
  </DetailsBasic>
@@ -0,0 +1,186 @@
1
+ /**
2
+ * - We must keep the edited element in the DOM so that
3
+ * the layout remains the same (especially important for table cells)
4
+ * And the editable part is in absolute so that it takes the original content dimensions
5
+ * AND for table cells it can actually take the table cell dimensions
6
+ *
7
+ * This means an editable thing MUST have a parent with position relative that wraps the content and the eventual editable input
8
+ *
9
+ */
10
+
11
+ import { forwardRef } from "preact/compat";
12
+ import {
13
+ useCallback,
14
+ useImperativeHandle,
15
+ useLayoutEffect,
16
+ useRef,
17
+ useState,
18
+ } from "preact/hooks";
19
+
20
+ import { Input } from "../field/input.jsx";
21
+
22
+ import.meta.css = /* css */ `
23
+ .navi_editable_wrapper {
24
+ position: absolute;
25
+ inset: 0;
26
+ }
27
+ `;
28
+
29
+ export const useEditionController = () => {
30
+ const [editing, editingSetter] = useState(null);
31
+ const startEditing = useCallback((event) => {
32
+ editingSetter((current) => {
33
+ return current || { event };
34
+ });
35
+ }, []);
36
+ const stopEditing = useCallback(() => {
37
+ editingSetter(null);
38
+ }, []);
39
+
40
+ const prevEditingRef = useRef(editing);
41
+ const editionJustEnded = prevEditingRef.current && !editing;
42
+ prevEditingRef.current = editing;
43
+
44
+ return { editing, startEditing, stopEditing, editionJustEnded };
45
+ };
46
+
47
+ export const Editable = forwardRef((props, ref) => {
48
+ let {
49
+ children,
50
+ action,
51
+ editing,
52
+ name,
53
+ value,
54
+ valueSignal,
55
+ onEditEnd,
56
+ constraints,
57
+ type,
58
+ required,
59
+ readOnly,
60
+ min,
61
+ max,
62
+ step,
63
+ minLength,
64
+ maxLength,
65
+ pattern,
66
+ wrapperProps,
67
+ autoSelect = true,
68
+ width,
69
+ height,
70
+ ...rest
71
+ } = props;
72
+ if (import.meta.dev && !action) {
73
+ console.warn(`Editable requires an action prop`);
74
+ }
75
+
76
+ const innerRef = useRef();
77
+ useImperativeHandle(ref, () => innerRef.current);
78
+
79
+ if (valueSignal) {
80
+ value = valueSignal.value;
81
+ }
82
+
83
+ const editingPreviousRef = useRef(editing);
84
+ const valueWhenEditStartRef = useRef(editing ? value : undefined);
85
+
86
+ if (editingPreviousRef.current !== editing) {
87
+ if (editing) {
88
+ valueWhenEditStartRef.current = value; // Always store the external value
89
+ }
90
+ editingPreviousRef.current = editing;
91
+ }
92
+
93
+ // Simulate typing the initial value when editing starts with a custom value
94
+ useLayoutEffect(() => {
95
+ if (!editing) {
96
+ return;
97
+ }
98
+ const editingEvent = editing.event;
99
+ if (!editingEvent) {
100
+ return;
101
+ }
102
+ const editingEventInitialValue = editingEvent.detail?.initialValue;
103
+ if (editingEventInitialValue === undefined) {
104
+ return;
105
+ }
106
+ const input = innerRef.current;
107
+ input.value = editingEventInitialValue;
108
+ input.dispatchEvent(
109
+ new CustomEvent("input", {
110
+ bubbles: false,
111
+ }),
112
+ );
113
+ }, [editing]);
114
+
115
+ const input = (
116
+ <Input
117
+ ref={innerRef}
118
+ {...rest}
119
+ type={type}
120
+ name={name}
121
+ value={value}
122
+ valueSignal={valueSignal}
123
+ autoFocus
124
+ autoFocusVisible
125
+ autoSelect={autoSelect}
126
+ cancelOnEscape
127
+ cancelOnBlurInvalid
128
+ constraints={constraints}
129
+ required={required}
130
+ readOnly={readOnly}
131
+ min={min}
132
+ max={max}
133
+ step={step}
134
+ minLength={minLength}
135
+ maxLength={maxLength}
136
+ pattern={pattern}
137
+ width={width}
138
+ height={height}
139
+ onCancel={(e) => {
140
+ if (valueSignal) {
141
+ valueSignal.value = valueWhenEditStartRef.current;
142
+ }
143
+ onEditEnd({
144
+ cancelled: true,
145
+ event: e,
146
+ });
147
+ }}
148
+ onBlur={(e) => {
149
+ const value =
150
+ type === "number" ? e.target.valueAsNumber : e.target.value;
151
+ const valueWhenEditStart = valueWhenEditStartRef.current;
152
+ if (value === valueWhenEditStart) {
153
+ onEditEnd({
154
+ cancelled: true,
155
+ event: e,
156
+ });
157
+ return;
158
+ }
159
+ }}
160
+ action={action || (() => {})}
161
+ onActionEnd={(e) => {
162
+ onEditEnd({
163
+ success: true,
164
+ event: e,
165
+ });
166
+ }}
167
+ />
168
+ );
169
+
170
+ return (
171
+ <>
172
+ {children || <span>{value}</span>}
173
+ {editing && (
174
+ <div
175
+ {...wrapperProps}
176
+ className={[
177
+ "navi_editable_wrapper",
178
+ ...(wrapperProps?.className || "").split(" "),
179
+ ].join(" ")}
180
+ >
181
+ {input}
182
+ </div>
183
+ )}
184
+ </>
185
+ );
186
+ });