@jsenv/navi 0.0.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 (123) hide show
  1. package/index.js +51 -0
  2. package/package.json +38 -0
  3. package/src/action_private_properties.js +11 -0
  4. package/src/action_proxy_test.html +353 -0
  5. package/src/action_run_states.js +5 -0
  6. package/src/actions.js +1377 -0
  7. package/src/browser_integration/browser_integration.js +191 -0
  8. package/src/browser_integration/document_back_and_forward.js +17 -0
  9. package/src/browser_integration/document_loading_signal.js +100 -0
  10. package/src/browser_integration/document_state_signal.js +9 -0
  11. package/src/browser_integration/document_url_signal.js +9 -0
  12. package/src/browser_integration/use_is_visited.js +19 -0
  13. package/src/browser_integration/via_history.js +199 -0
  14. package/src/browser_integration/via_navigation.js +168 -0
  15. package/src/components/action_execution/form_context.js +8 -0
  16. package/src/components/action_execution/render_actionable_component.jsx +27 -0
  17. package/src/components/action_execution/use_action.js +330 -0
  18. package/src/components/action_execution/use_execute_action.js +161 -0
  19. package/src/components/action_renderer.jsx +136 -0
  20. package/src/components/collect_form_element_values.js +79 -0
  21. package/src/components/demos/0_button_demo.html +155 -0
  22. package/src/components/demos/1_checkbox_demo.html +257 -0
  23. package/src/components/demos/2_input_textual_demo.html +354 -0
  24. package/src/components/demos/3_radio_demo.html +222 -0
  25. package/src/components/demos/4_select_demo.html +104 -0
  26. package/src/components/demos/5_list_scrollable_demo.html +153 -0
  27. package/src/components/demos/action/0_button_demo.html +204 -0
  28. package/src/components/demos/action/10_shortcuts_demo.html +189 -0
  29. package/src/components/demos/action/11_nested_shortcuts_demo.html +401 -0
  30. package/src/components/demos/action/1_input_text_demo.html +461 -0
  31. package/src/components/demos/action/2_form_multiple.html +303 -0
  32. package/src/components/demos/action/3_details_demo.html +172 -0
  33. package/src/components/demos/action/4_input_checkbox_demo.html +611 -0
  34. package/src/components/demos/action/6_checkbox_list_demo.html +109 -0
  35. package/src/components/demos/action/7_radio_list_demo.html +217 -0
  36. package/src/components/demos/action/8_editable_text_demo.html +442 -0
  37. package/src/components/demos/action/9_link_demo.html +172 -0
  38. package/src/components/demos/demo.md +0 -0
  39. package/src/components/demos/route/basic/basic.html +14 -0
  40. package/src/components/demos/route/basic/basic_route_demo.jsx +224 -0
  41. package/src/components/demos/route/multi/multi.html +14 -0
  42. package/src/components/demos/route/multi/multi_route_demo.jsx +277 -0
  43. package/src/components/details/details.jsx +248 -0
  44. package/src/components/details/summary_marker.jsx +141 -0
  45. package/src/components/editable_text/editable_text.jsx +96 -0
  46. package/src/components/error_boundary_context.js +9 -0
  47. package/src/components/form.jsx +144 -0
  48. package/src/components/input/button.jsx +333 -0
  49. package/src/components/input/checkbox_list.jsx +294 -0
  50. package/src/components/input/field.jsx +61 -0
  51. package/src/components/input/field_css.js +118 -0
  52. package/src/components/input/input.jsx +15 -0
  53. package/src/components/input/input_checkbox.jsx +370 -0
  54. package/src/components/input/input_radio.jsx +299 -0
  55. package/src/components/input/input_textual.jsx +338 -0
  56. package/src/components/input/radio_list.jsx +283 -0
  57. package/src/components/input/select.jsx +273 -0
  58. package/src/components/input/use_form_event.js +20 -0
  59. package/src/components/input/use_on_change.js +12 -0
  60. package/src/components/link/link.jsx +291 -0
  61. package/src/components/loader/loader_background.jsx +324 -0
  62. package/src/components/loader/loading_spinner.jsx +68 -0
  63. package/src/components/loader/network_speed.js +83 -0
  64. package/src/components/loader/rectangle_loading.jsx +225 -0
  65. package/src/components/route.jsx +15 -0
  66. package/src/components/selection/selection.js +5 -0
  67. package/src/components/selection/selection_context.jsx +262 -0
  68. package/src/components/shortcut/os.js +9 -0
  69. package/src/components/shortcut/shortcut_context.jsx +390 -0
  70. package/src/components/use_action_events.js +37 -0
  71. package/src/components/use_auto_focus.js +43 -0
  72. package/src/components/use_debounce_true.js +31 -0
  73. package/src/components/use_focus_group.js +19 -0
  74. package/src/components/use_initial_value.js +104 -0
  75. package/src/components/use_is_visited.js +19 -0
  76. package/src/components/use_ref_array.js +38 -0
  77. package/src/components/use_signal_sync.js +50 -0
  78. package/src/components/use_state_array.js +40 -0
  79. package/src/docs/actions.md +228 -0
  80. package/src/docs/demos/resource/action_status.jsx +42 -0
  81. package/src/docs/demos/resource/demo.md +1 -0
  82. package/src/docs/demos/resource/resource_demo_0.html +84 -0
  83. package/src/docs/demos/resource/resource_demo_10_post_gc.html +364 -0
  84. package/src/docs/demos/resource/resource_demo_11_describe_many.html +362 -0
  85. package/src/docs/demos/resource/resource_demo_2.html +173 -0
  86. package/src/docs/demos/resource/resource_demo_3_filtered_users.html +415 -0
  87. package/src/docs/demos/resource/resource_demo_4_details.html +284 -0
  88. package/src/docs/demos/resource/resource_demo_5_renderer_lazy.html +115 -0
  89. package/src/docs/demos/resource/resource_demo_6_gc.html +217 -0
  90. package/src/docs/demos/resource/resource_demo_7_child_gc.html +240 -0
  91. package/src/docs/demos/resource/resource_demo_8_proxy_gc.html +319 -0
  92. package/src/docs/demos/resource/resource_demo_9_describe_one.html +472 -0
  93. package/src/docs/demos/resource/tata.jsx +3 -0
  94. package/src/docs/demos/resource/toto.jsx +3 -0
  95. package/src/docs/demos/user_nav/user_nav.html +12 -0
  96. package/src/docs/demos/user_nav/user_nav.jsx +330 -0
  97. package/src/docs/resource_dependencies.md +103 -0
  98. package/src/docs/resource_with_params.md +80 -0
  99. package/src/notes.md +13 -0
  100. package/src/route/route.js +518 -0
  101. package/src/route/route.test.html +228 -0
  102. package/src/store/array_signal_store.js +537 -0
  103. package/src/store/local_storage_signal.js +17 -0
  104. package/src/store/resource_graph.js +1303 -0
  105. package/src/store/tests/resource_graph_autoreload_demo.html +12 -0
  106. package/src/store/tests/resource_graph_autoreload_demo.jsx +964 -0
  107. package/src/store/tests/resource_graph_dependencies.test.js +95 -0
  108. package/src/store/value_in_local_storage.js +187 -0
  109. package/src/symbol_object_signal.js +1 -0
  110. package/src/use_action_data.js +10 -0
  111. package/src/use_action_status.js +47 -0
  112. package/src/utils/add_many_event_listeners.js +15 -0
  113. package/src/utils/array_add_remove.js +61 -0
  114. package/src/utils/array_signal.js +15 -0
  115. package/src/utils/compare_two_js_values.js +172 -0
  116. package/src/utils/execute_with_cleanup.js +21 -0
  117. package/src/utils/get_caller_info.js +85 -0
  118. package/src/utils/iterable_weak_set.js +62 -0
  119. package/src/utils/js_value_weak_map.js +162 -0
  120. package/src/utils/js_value_weak_map_demo.html +690 -0
  121. package/src/utils/merge_two_js_values.js +53 -0
  122. package/src/utils/stringify_for_display.js +150 -0
  123. package/src/utils/weak_effect.js +48 -0
@@ -0,0 +1,299 @@
1
+ import { useConstraints } from "@jsenv/validation";
2
+ import { forwardRef } from "preact/compat";
3
+ import { useImperativeHandle, useRef, useState } from "preact/hooks";
4
+ import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
5
+ import { LoaderBackground } from "../loader/loader_background.jsx";
6
+ import { useAutoFocus } from "../use_auto_focus.js";
7
+
8
+ import.meta.css = /* css */ `
9
+ .custom_radio_wrapper {
10
+ display: inline-flex;
11
+ box-sizing: content-box;
12
+
13
+ --checked-color: #3b82f6;
14
+ --checked-disabled-color: var(--field-disabled-border-color);
15
+
16
+ --checkmark-color: var(--checked-color);
17
+ --checkmark-disabled-color: var(--field-disabled-text-color);
18
+ }
19
+
20
+ .custom_radio_wrapper input {
21
+ position: absolute;
22
+ opacity: 0;
23
+ inset: 0;
24
+ margin: 0;
25
+ padding: 0;
26
+ border: none;
27
+ }
28
+
29
+ .custom_radio {
30
+ width: 13px;
31
+ height: 13px;
32
+ background: transparent;
33
+ border-radius: 50%;
34
+ display: inline-flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ margin-left: 5px;
38
+ margin-top: 3px;
39
+ margin-right: 3px;
40
+ }
41
+
42
+ .custom_radio svg {
43
+ width: 100%;
44
+ height: 100%;
45
+ transition: all 0.15s ease;
46
+ pointer-events: none;
47
+ }
48
+
49
+ .custom_radio svg .custom_radio_border {
50
+ transition: all 0.15s ease;
51
+ }
52
+
53
+ .custom_radio svg .custom_radio_dashed_border {
54
+ display: none;
55
+ transition: all 0.15s ease;
56
+ }
57
+
58
+ .custom_radio svg .custom_radio_marker {
59
+ fill: var(--checkmark-color);
60
+ opacity: 0;
61
+ transform-origin: center;
62
+ transform: scale(0.3);
63
+ transition: all 0.15s ease;
64
+ }
65
+
66
+ /* États hover */
67
+ .custom_radio_wrapper:hover .custom_radio svg .custom_radio_border {
68
+ stroke: var(--field-hover-border-color);
69
+ }
70
+ .custom_radio_wrapper:hover .custom_radio svg .custom_radio_marker {
71
+ fill: var(--field-strong-color);
72
+ }
73
+
74
+ .custom_radio_wrapper:hover
75
+ input:checked
76
+ + .custom_radio
77
+ svg
78
+ .custom_radio_border {
79
+ stroke: var(--field-strong-color);
80
+ }
81
+
82
+ /* État checked */
83
+ .custom_radio_wrapper input:checked + .custom_radio svg .custom_radio_border {
84
+ stroke: var(--field-strong-color);
85
+ }
86
+
87
+ .custom_radio_wrapper input:checked + .custom_radio svg .custom_radio_marker {
88
+ opacity: 1;
89
+ transform: scale(1);
90
+ }
91
+
92
+ /* États disabled */
93
+ .custom_radio_wrapper
94
+ input[disabled]
95
+ + .custom_radio
96
+ svg
97
+ .custom_radio_border {
98
+ fill: light-dark(rgba(239, 239, 239, 0.3), rgba(59, 59, 59, 0.3));
99
+ stroke: var(--field-disabled-border-color);
100
+ }
101
+
102
+ .custom_radio_wrapper
103
+ input[disabled]:checked
104
+ + .custom_radio
105
+ svg
106
+ .custom_radio_border {
107
+ stroke: var(--checked-disabled-color);
108
+ }
109
+
110
+ .custom_radio_wrapper
111
+ input[disabled]:checked
112
+ + .custom_radio
113
+ svg
114
+ .custom_radio_marker {
115
+ fill: var(--checkmark-disabled-color);
116
+ }
117
+
118
+ .custom_radio_wrapper
119
+ input[data-readonly]
120
+ + .custom_radio
121
+ svg
122
+ .custom_radio_border {
123
+ fill: light-dark(rgba(239, 239, 239, 0.3), rgba(59, 59, 59, 0.3));
124
+ stroke: var(--field-disabled-border-color);
125
+ }
126
+ .custom_radio_wrapper
127
+ input[data-readonly]
128
+ + .custom_radio
129
+ svg
130
+ .custom_radio_dashed_border {
131
+ display: none;
132
+ }
133
+ .custom_radio_wrapper
134
+ input[data-readonly]:checked
135
+ + .custom_radio
136
+ svg
137
+ .custom_radio_border {
138
+ stroke: var(--checked-disabled-color);
139
+ }
140
+ .custom_radio_wrapper
141
+ input[data-readonly]:checked
142
+ + .custom_radio
143
+ svg
144
+ .custom_radio_marker {
145
+ fill: var(--checkmark-disabled-color);
146
+ }
147
+ .custom_radio_wrapper:hover
148
+ input[data-readonly]
149
+ + .custom_radio
150
+ svg
151
+ .custom_radio_border {
152
+ fill: light-dark(rgba(239, 239, 239, 0.3), rgba(59, 59, 59, 0.3));
153
+ stroke: var(--field-disabled-border-color);
154
+ }
155
+ .custom_radio_wrapper:hover
156
+ input[data-readonly]:checked
157
+ + .custom_radio
158
+ svg
159
+ .custom_radio_border {
160
+ stroke: var(--checked-disabled-color);
161
+ }
162
+
163
+ /* Focus state avec outline */
164
+ .custom_radio_wrapper input:focus-visible + .custom_radio {
165
+ outline: 2px solid var(--field-outline-color);
166
+ outline-offset: 1px;
167
+ border-radius: 50%;
168
+ }
169
+ `;
170
+
171
+ export const InputRadio = forwardRef((props, ref) => {
172
+ return renderActionableComponent(props, ref, {
173
+ Basic: InputRadioBasic,
174
+ WithAction: InputRadioWithAction,
175
+ InsideForm: InputRadioInsideForm,
176
+ });
177
+ });
178
+
179
+ const InputRadioBasic = forwardRef((props, ref) => {
180
+ const {
181
+ autoFocus,
182
+ constraints = [],
183
+ checked,
184
+ readOnly,
185
+ disabled,
186
+ loading,
187
+ onClick,
188
+ onChange,
189
+ appeareance = "custom", // "custom" or "default"
190
+ ...rest
191
+ } = props;
192
+
193
+ const innerRef = useRef(null);
194
+ useImperativeHandle(ref, () => innerRef.current);
195
+ useAutoFocus(innerRef, autoFocus);
196
+ useConstraints(innerRef, constraints);
197
+
198
+ const [innerChecked, setInnerChecked] = useState(checked);
199
+ const checkedRef = useRef(checked);
200
+ if (checkedRef.current !== checked) {
201
+ setInnerChecked(checked);
202
+ checkedRef.current = checked;
203
+ }
204
+
205
+ const handleChange = (e) => {
206
+ const isChecked = e.target.checked;
207
+ setInnerChecked(isChecked);
208
+ onChange?.(e);
209
+ };
210
+
211
+ const inputRadio = (
212
+ <input
213
+ ref={innerRef}
214
+ type="radio"
215
+ checked={innerChecked}
216
+ data-readonly={readOnly && !disabled ? "" : undefined}
217
+ disabled={disabled}
218
+ data-validation-message-arrow-x="center"
219
+ onClick={(e) => {
220
+ if (readOnly) {
221
+ e.preventDefault();
222
+ }
223
+ onClick?.(e);
224
+ }}
225
+ onChange={handleChange}
226
+ {...rest}
227
+ />
228
+ );
229
+ const inputRadioDisplayed =
230
+ appeareance === "custom" ? (
231
+ <CustomRadio>{inputRadio}</CustomRadio>
232
+ ) : (
233
+ inputRadio
234
+ );
235
+
236
+ const inputRadioWithLoader = (
237
+ <LoaderBackground
238
+ loading={loading}
239
+ targetSelector={appeareance === "custom" ? ".custom_radio" : ""}
240
+ inset={-2}
241
+ color="light-dark(#355fcc, #3b82f6)"
242
+ >
243
+ {inputRadioDisplayed}
244
+ </LoaderBackground>
245
+ );
246
+
247
+ return inputRadioWithLoader;
248
+ });
249
+ const CustomRadio = ({ children }) => {
250
+ return (
251
+ <div className="custom_radio_wrapper" data-field-wrapper="">
252
+ {children}
253
+ <div className="custom_radio">
254
+ <svg
255
+ viewBox="0 0 12 12"
256
+ aria-hidden="true"
257
+ preserveAspectRatio="xMidYMid meet"
258
+ >
259
+ {/* Border circle - always visible */}
260
+ <circle
261
+ className="custom_radio_border"
262
+ cx="6"
263
+ cy="6"
264
+ r="5.5"
265
+ fill="white"
266
+ stroke="var(--field-border-color)"
267
+ strokeWidth="1"
268
+ />
269
+ {/* Dashed border for readonly - calculated for even distribution */}
270
+ <circle
271
+ className="custom_radio_dashed_border"
272
+ cx="6"
273
+ cy="6"
274
+ r="5.5"
275
+ fill="var(--field-readonly-background-color)"
276
+ stroke="var(--field-border-color)"
277
+ strokeWidth="1"
278
+ strokeDasharray="2.16 2.16"
279
+ strokeDashoffset="0"
280
+ />
281
+ {/* Inner fill circle - only visible when checked */}
282
+ <circle className="custom_radio_marker" cx="6" cy="6" r="3.5" />
283
+ </svg>
284
+ </div>
285
+ </div>
286
+ );
287
+ };
288
+
289
+ const InputRadioWithAction = () => {
290
+ throw new Error(
291
+ `Do not use <Input type="radio" />, use <RadioList /> instead`,
292
+ );
293
+ };
294
+
295
+ const InputRadioInsideForm = () => {
296
+ throw new Error(
297
+ `Do not use <Input type="radio" />, use <RadioList /> instead`,
298
+ );
299
+ };
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Input component for all textual input types.
3
+ *
4
+ * Supports:
5
+ * - text (default)
6
+ * - password
7
+ * - hidden
8
+ * - email
9
+ * - url
10
+ * - search
11
+ * - tel
12
+ * - etc.
13
+ *
14
+ * For non-textual inputs, specialized components will be used:
15
+ * - <InputCheckbox /> for type="checkbox"
16
+ * - <InputRadio /> for type="radio"
17
+ */
18
+
19
+ import { requestAction, useConstraints } from "@jsenv/validation";
20
+ import { forwardRef } from "preact/compat";
21
+ import { useEffect, useImperativeHandle, useRef } from "preact/hooks";
22
+ import { useNavState } from "../../browser_integration/browser_integration.js";
23
+ import { useActionStatus } from "../../use_action_status.js";
24
+ import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
25
+ import {
26
+ useActionBoundToOneParam,
27
+ useOneFormParam,
28
+ } from "../action_execution/use_action.js";
29
+ import { useExecuteAction } from "../action_execution/use_execute_action.js";
30
+ import { LoaderBackground } from "../loader/loader_background.jsx";
31
+ import { useActionEvents } from "../use_action_events.js";
32
+ import { useAutoFocus } from "../use_auto_focus.js";
33
+ import "./field_css.js";
34
+ import { useOnChange } from "./use_on_change.js";
35
+
36
+ export const InputTextual = forwardRef((props, ref) => {
37
+ return renderActionableComponent(props, ref, {
38
+ Basic: InputTextualBasic,
39
+ WithAction: InputTextualWithAction,
40
+ InsideForm: InputTextualInsideForm,
41
+ });
42
+ });
43
+
44
+ const InputTextualBasic = forwardRef((props, ref) => {
45
+ let {
46
+ type,
47
+ value,
48
+ autoFocus,
49
+ autoFocusVisible,
50
+ autoSelect,
51
+ constraints = [],
52
+ loading,
53
+ appearance = "custom",
54
+ ...rest
55
+ } = props;
56
+
57
+ const innerRef = useRef();
58
+ useImperativeHandle(ref, () => innerRef.current);
59
+ useAutoFocus(innerRef, autoFocus, {
60
+ autoFocusVisible,
61
+ autoSelect,
62
+ });
63
+ useConstraints(innerRef, constraints);
64
+
65
+ if (type === "datetime-local") {
66
+ value = convertToLocalTimezone(value);
67
+ }
68
+
69
+ const inputTextual = (
70
+ <input
71
+ ref={innerRef}
72
+ type={type}
73
+ value={value}
74
+ data-field=""
75
+ data-field-with-border=""
76
+ data-custom={appearance === "custom" ? "" : undefined}
77
+ {...rest}
78
+ />
79
+ );
80
+
81
+ return (
82
+ <LoaderBackground loading={loading} color="light-dark(#355fcc, #3b82f6)">
83
+ {inputTextual}
84
+ </LoaderBackground>
85
+ );
86
+ });
87
+
88
+ // As explained in https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/datetime-local#setting_timezones
89
+ // datetime-local does not support timezones
90
+ const convertToLocalTimezone = (dateTimeString) => {
91
+ const date = new Date(dateTimeString);
92
+ // Check if the date is valid
93
+ if (isNaN(date.getTime())) {
94
+ return dateTimeString;
95
+ }
96
+
97
+ // Format to YYYY-MM-DDThh:mm:ss
98
+ const year = date.getFullYear();
99
+ const month = String(date.getMonth() + 1).padStart(2, "0");
100
+ const day = String(date.getDate()).padStart(2, "0");
101
+ const hours = String(date.getHours()).padStart(2, "0");
102
+ const minutes = String(date.getMinutes()).padStart(2, "0");
103
+ const seconds = String(date.getSeconds()).padStart(2, "0");
104
+
105
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
106
+ };
107
+
108
+ /**
109
+ * Converts a datetime string without timezone (local time) to UTC format with 'Z' notation
110
+ *
111
+ * @param {string} localDateTimeString - Local datetime string without timezone (e.g., "2023-07-15T14:30:00")
112
+ * @returns {string} Datetime string in UTC with 'Z' notation (e.g., "2023-07-15T12:30:00Z")
113
+ */
114
+ const convertToUTCTimezone = (localDateTimeString) => {
115
+ if (!localDateTimeString) {
116
+ return localDateTimeString;
117
+ }
118
+
119
+ try {
120
+ // Create a Date object using the local time string
121
+ // The browser will interpret this as local timezone
122
+ const localDate = new Date(localDateTimeString);
123
+
124
+ // Check if the date is valid
125
+ if (isNaN(localDate.getTime())) {
126
+ return localDateTimeString;
127
+ }
128
+
129
+ // Convert to UTC ISO string
130
+ const utcString = localDate.toISOString();
131
+
132
+ // Return the UTC string (which includes the 'Z' notation)
133
+ return utcString;
134
+ } catch (error) {
135
+ console.error("Error converting local datetime to UTC:", error);
136
+ return localDateTimeString;
137
+ }
138
+ };
139
+
140
+ const InputTextualWithAction = forwardRef((props, ref) => {
141
+ const {
142
+ id,
143
+ type,
144
+ action,
145
+ name,
146
+ value: externalValue,
147
+ valueSignal,
148
+ cancelOnBlurInvalid,
149
+ cancelOnEscape,
150
+ actionErrorEffect,
151
+ readOnly,
152
+ loading,
153
+ onInput,
154
+ onCancel,
155
+ onActionPrevented,
156
+ onActionStart,
157
+ onActionError,
158
+ onActionEnd,
159
+ ...rest
160
+ } = props;
161
+ if (import.meta.dev && !name && !valueSignal) {
162
+ console.warn(`InputTextual with action requires a name prop to be set.`);
163
+ }
164
+
165
+ const innerRef = useRef(null);
166
+ useImperativeHandle(ref, () => innerRef.current);
167
+
168
+ const [navState, setNavState] = useNavState(id);
169
+ const [boundAction, value, setValue, resetValue] = useActionBoundToOneParam(
170
+ action,
171
+ name,
172
+ valueSignal ? valueSignal : externalValue,
173
+ navState,
174
+ "",
175
+ );
176
+ const { loading: actionLoading } = useActionStatus(boundAction);
177
+ const executeAction = useExecuteAction(innerRef, {
178
+ errorEffect: actionErrorEffect,
179
+ });
180
+ useEffect(() => {
181
+ setNavState(value);
182
+ }, [value]);
183
+
184
+ const valueAtInteractionRef = useRef(null);
185
+ useOnChange(innerRef, (e) => {
186
+ if (
187
+ valueAtInteractionRef.current !== null &&
188
+ e.target.value === valueAtInteractionRef.current
189
+ ) {
190
+ valueAtInteractionRef.current = null;
191
+ return;
192
+ }
193
+ requestAction(boundAction, { event: e });
194
+ });
195
+
196
+ useActionEvents(innerRef, {
197
+ onCancel: (e, reason) => {
198
+ if (reason.startsWith("blur_invalid")) {
199
+ if (!cancelOnBlurInvalid) {
200
+ return;
201
+ }
202
+ if (
203
+ // error prevent cancellation until the user closes it (or something closes it)
204
+ e.detail.failedConstraintInfo.level === "error" &&
205
+ e.detail.failedConstraintInfo.reportStatus !== "closed"
206
+ ) {
207
+ return;
208
+ }
209
+ }
210
+ if (reason === "escape_key") {
211
+ if (!cancelOnEscape) {
212
+ return;
213
+ }
214
+ /**
215
+ * Browser trigger a "change" event right after the escape is pressed
216
+ * if the input value has changed.
217
+ * We need to prevent the next change event otherwise we would request action when
218
+ * we actually want to cancel
219
+ */
220
+ valueAtInteractionRef.current = e.target.value;
221
+ }
222
+ resetValue();
223
+ onCancel?.(e, reason);
224
+ },
225
+ onPrevented: onActionPrevented,
226
+ onAction: executeAction,
227
+ onStart: onActionStart,
228
+ onError: onActionError,
229
+ onEnd: (e) => {
230
+ setNavState(undefined);
231
+ onActionEnd?.(e);
232
+ },
233
+ });
234
+
235
+ const innerLoading = loading || actionLoading;
236
+
237
+ return (
238
+ <InputTextualBasic
239
+ {...rest}
240
+ data-action={boundAction}
241
+ ref={innerRef}
242
+ type={type}
243
+ id={id}
244
+ name={name}
245
+ value={value}
246
+ data-form-value={
247
+ type === "datetime-local" ? convertToUTCTimezone(value) : undefined
248
+ }
249
+ loading={innerLoading}
250
+ readOnly={readOnly || innerLoading}
251
+ onInput={(e) => {
252
+ valueAtInteractionRef.current = null;
253
+ const inputValue =
254
+ type === "number" ? e.target.valueAsNumber : e.target.value;
255
+ setValue(
256
+ type === "datetime-local"
257
+ ? convertToUTCTimezone(inputValue)
258
+ : inputValue,
259
+ );
260
+ onInput?.(e);
261
+ }}
262
+ onKeyDown={(e) => {
263
+ if (e.key !== "Enter") {
264
+ return;
265
+ }
266
+ e.preventDefault();
267
+ /**
268
+ * Browser trigger a "change" event right after the enter is pressed
269
+ * if the input value has changed.
270
+ * We need to prevent the next change event otherwise we would request action twice
271
+ */
272
+ valueAtInteractionRef.current = e.target.value;
273
+ requestAction(boundAction, { event: e });
274
+ }}
275
+ />
276
+ );
277
+ });
278
+
279
+ const InputTextualInsideForm = forwardRef((props, ref) => {
280
+ const {
281
+ formContext,
282
+ id,
283
+ name,
284
+ value: externalValue,
285
+ loading,
286
+ readOnly,
287
+ onInput,
288
+ onKeyDown,
289
+ ...rest
290
+ } = props;
291
+
292
+ const innerRef = useRef(null);
293
+ useImperativeHandle(ref, () => innerRef.current);
294
+
295
+ const [navState, setNavState] = useNavState(id);
296
+ const { formAction, formIsBusy, formIsReadOnly, formActionRequester } =
297
+ formContext;
298
+ const [value, setValue] = useOneFormParam(name, externalValue, navState, "");
299
+ useEffect(() => {
300
+ setNavState(value);
301
+ }, [value]);
302
+
303
+ return (
304
+ <InputTextualBasic
305
+ {...rest}
306
+ ref={innerRef}
307
+ id={id}
308
+ name={name}
309
+ value={value}
310
+ data-form-value={convertToUTCTimezone(value)}
311
+ loading={
312
+ loading || (formIsBusy && formActionRequester === innerRef.current)
313
+ }
314
+ readOnly={readOnly || formIsReadOnly}
315
+ onInput={(e) => {
316
+ const inputValue = e.target.value;
317
+ setValue(convertToUTCTimezone(inputValue));
318
+ onInput?.(e);
319
+ }}
320
+ onKeyDown={(e) => {
321
+ if (e.key === "Enter") {
322
+ const inputElement = e.target;
323
+ const { form } = inputElement;
324
+ const formSubmitButton = form.querySelector(
325
+ "button[type='submit'], input[type='submit'], input[type='image']",
326
+ );
327
+ e.preventDefault();
328
+ requestAction(formAction, {
329
+ event: e,
330
+ target: form,
331
+ requester: formSubmitButton ? formSubmitButton : inputElement,
332
+ });
333
+ }
334
+ onKeyDown?.(e);
335
+ }}
336
+ />
337
+ );
338
+ });