@jsenv/navi 0.0.1 → 0.1.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/dist/jsenv_navi.js +22954 -0
- package/index.js +66 -16
- package/package.json +22 -11
- package/src/actions.js +50 -26
- package/src/browser_integration/browser_integration.js +31 -6
- package/src/browser_integration/via_history.js +42 -9
- package/src/components/action_execution/render_actionable_component.jsx +6 -4
- package/src/components/action_execution/use_action.js +51 -282
- package/src/components/action_execution/use_execute_action.js +106 -92
- package/src/components/action_execution/use_run_on_mount.js +9 -0
- package/src/components/action_renderer.jsx +21 -32
- package/src/components/demos/0_button_demo.html +574 -103
- package/src/components/demos/10_column_reordering_debug.html +277 -0
- package/src/components/demos/11_table_selection_debug.html +432 -0
- package/src/components/demos/1_checkbox_demo.html +579 -202
- package/src/components/demos/2_input_textual_demo.html +81 -138
- package/src/components/demos/3_radio_demo.html +0 -2
- package/src/components/demos/4_select_demo.html +19 -23
- package/src/components/demos/6_tablist_demo.html +77 -0
- package/src/components/demos/7_table_selection_demo.html +176 -0
- package/src/components/demos/8_table_fixed_headers_demo.html +584 -0
- package/src/components/demos/9_table_column_drag_demo.html +325 -0
- package/src/components/demos/action/0_button_demo.html +2 -4
- package/src/components/demos/action/1_input_text_demo.html +643 -222
- package/src/components/demos/action/3_details_demo.html +146 -115
- package/src/components/demos/action/4_input_checkbox_demo.html +442 -322
- package/src/components/demos/action/5_input_checkbox_state_demo.html +270 -0
- package/src/components/demos/action/6_checkbox_list_demo.html +304 -72
- package/src/components/demos/action/7_radio_list_demo.html +310 -170
- package/src/components/demos/action/{8_editable_text_demo.html → 8_editable_demo.html} +65 -76
- package/src/components/demos/action/9_link_demo.html +84 -62
- package/src/components/demos/ui_transition/0_action_renderer_ui_transition_demo.html +695 -0
- package/src/components/demos/ui_transition/1_nested_ui_transition_demo.html +429 -0
- package/src/components/demos/ui_transition/2_height_transition_test.html +295 -0
- package/src/components/details/details.jsx +62 -64
- package/src/components/edition/editable.jsx +186 -0
- package/src/components/field/README.md +247 -0
- package/src/components/{input → field}/button.jsx +151 -130
- package/src/components/field/checkbox_list.jsx +184 -0
- package/src/components/{collect_form_element_values.js → field/collect_form_element_values.js} +7 -4
- package/src/components/{input → field}/field_css.js +4 -1
- package/src/components/field/form.jsx +211 -0
- package/src/components/{input → field}/input.jsx +1 -0
- package/src/components/{input → field}/input_checkbox.jsx +132 -155
- package/src/components/{input → field}/input_radio.jsx +135 -46
- package/src/components/{input → field}/input_textual.jsx +247 -173
- package/src/components/field/label.jsx +32 -0
- package/src/components/field/radio_list.jsx +182 -0
- package/src/components/{input → field}/select.jsx +17 -32
- package/src/components/field/use_action_events.js +132 -0
- package/src/components/field/use_form_events.js +55 -0
- package/src/components/field/use_ui_state_controller.js +506 -0
- package/src/components/item_tracker/README.md +461 -0
- package/src/components/item_tracker/use_isolated_item_tracker.jsx +209 -0
- package/src/components/item_tracker/use_isolated_item_tracker_demo.html +148 -0
- package/src/components/item_tracker/use_isolated_item_tracker_demo.jsx +460 -0
- package/src/components/item_tracker/use_item_tracker.jsx +143 -0
- package/src/components/item_tracker/use_item_tracker_demo.html +207 -0
- package/src/components/item_tracker/use_item_tracker_demo.jsx +216 -0
- package/src/components/keyboard_shortcuts/active_keyboard_shortcuts.jsx +87 -0
- package/src/components/keyboard_shortcuts/aria_key_shortcuts.js +61 -0
- package/src/components/keyboard_shortcuts/keyboard_key_meta.js +17 -0
- package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +371 -0
- package/src/components/link/link.jsx +65 -102
- package/src/components/link/link_with_icon.jsx +52 -0
- package/src/components/loader/loader_background.jsx +85 -64
- package/src/components/loader/rectangle_loading.jsx +38 -19
- package/src/components/route.jsx +8 -4
- package/src/components/selection/selection.jsx +1583 -0
- package/src/components/svg/font_sized_svg.jsx +45 -0
- package/src/components/svg/icon_and_text.jsx +21 -0
- package/src/components/svg/svg_mask_overlay.jsx +105 -0
- package/src/components/table/drag/table_drag.jsx +506 -0
- package/src/components/table/resize/table_resize.jsx +650 -0
- package/src/components/table/resize/table_size.js +43 -0
- package/src/components/table/selection/table_selection.js +106 -0
- package/src/components/table/selection/table_selection.jsx +203 -0
- package/src/components/table/sticky/sticky_group.js +354 -0
- package/src/components/table/sticky/table_sticky.js +25 -0
- package/src/components/table/sticky/table_sticky.jsx +501 -0
- package/src/components/table/table.jsx +721 -0
- package/src/components/table/table_css.js +211 -0
- package/src/components/table/table_ui.jsx +49 -0
- package/src/components/table/use_cells_and_columns.js +90 -0
- package/src/components/table/use_object_array_to_cells.js +46 -0
- package/src/components/table/z_indexes.js +23 -0
- package/src/components/tablist/tablist.jsx +99 -0
- package/src/components/text/overflow.jsx +15 -0
- package/src/components/text/text_and_count.jsx +28 -0
- package/src/components/ui_transition.jsx +128 -0
- package/src/components/use_auto_focus.js +58 -7
- package/src/components/use_batch_during_render.js +33 -0
- package/src/components/use_debounce_true.js +7 -7
- package/src/components/use_dependencies_diff.js +35 -0
- package/src/components/use_focus_group.js +4 -3
- package/src/components/use_initial_value.js +8 -34
- package/src/components/use_signal_sync.js +1 -1
- package/src/components/use_stable_callback.js +68 -0
- package/src/components/use_state_array.js +16 -9
- package/src/docs/actions.md +22 -0
- package/src/notes.md +33 -12
- package/src/route/route.js +97 -47
- package/src/store/resource_graph.js +2 -1
- package/src/store/tests/{resource_graph_dependencies.test.js → resource_graph_dependencies.test_manual.js} +13 -13
- package/src/utils/is_signal.js +20 -0
- package/src/utils/stringify_for_display.js +4 -23
- package/src/validation/constraints/confirm_constraint.js +14 -0
- package/src/validation/constraints/create_unique_value_constraint.js +27 -0
- package/src/validation/constraints/native_constraints.js +313 -0
- package/src/validation/constraints/readonly_constraint.js +36 -0
- package/src/validation/constraints/single_space_constraint.js +13 -0
- package/src/validation/custom_constraint_validation.js +599 -0
- package/src/validation/custom_message.js +18 -0
- package/src/validation/demos/browser_style.png +0 -0
- package/src/validation/demos/form_validation_demo.html +142 -0
- package/src/validation/demos/form_validation_demo_preact.html +87 -0
- package/src/validation/demos/form_validation_native_popover_demo.html +168 -0
- package/src/validation/demos/form_validation_vs_native_demo.html +172 -0
- package/src/validation/demos/validation_message_demo.html +203 -0
- package/src/validation/hooks/use_constraints.js +23 -0
- package/src/validation/hooks/use_custom_validation_ref.js +73 -0
- package/src/validation/hooks/use_validation_message.js +19 -0
- package/src/validation/validation_message.js +741 -0
- package/src/components/editable_text/editable_text.jsx +0 -96
- package/src/components/form.jsx +0 -144
- package/src/components/input/checkbox_list.jsx +0 -294
- package/src/components/input/field.jsx +0 -61
- package/src/components/input/radio_list.jsx +0 -283
- package/src/components/input/use_form_event.js +0 -20
- package/src/components/input/use_on_change.js +0 -12
- package/src/components/selection/selection.js +0 -5
- package/src/components/selection/selection_context.jsx +0 -262
- package/src/components/shortcut/shortcut_context.jsx +0 -390
- package/src/components/use_action_events.js +0 -37
- package/src/utils/iterable_weak_set.js +0 -62
- /package/src/components/demos/action/{11_nested_shortcuts_demo.html → 11_nested_shortcuts_demo.xhtml} +0 -0
- /package/src/components/{shortcut → keyboard_shortcuts}/os.js +0 -0
- /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 {
|
|
12
|
-
import {
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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:
|
|
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
|
+
});
|