@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.
Files changed (138) hide show
  1. package/dist/jsenv_navi.js +22954 -0
  2. package/index.js +66 -16
  3. package/package.json +22 -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/{input → field}/input_textual.jsx +247 -173
  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/radio_list.jsx +0 -283
  129. package/src/components/input/use_form_event.js +0 -20
  130. package/src/components/input/use_on_change.js +0 -12
  131. package/src/components/selection/selection.js +0 -5
  132. package/src/components/selection/selection_context.jsx +0 -262
  133. package/src/components/shortcut/shortcut_context.jsx +0 -390
  134. package/src/components/use_action_events.js +0 -37
  135. package/src/utils/iterable_weak_set.js +0 -62
  136. /package/src/components/demos/action/{11_nested_shortcuts_demo.html → 11_nested_shortcuts_demo.xhtml} +0 -0
  137. /package/src/components/{shortcut → keyboard_shortcuts}/os.js +0 -0
  138. /package/src/route/{route.test.html → route.xtest.html} +0 -0
@@ -16,173 +16,170 @@
16
16
  * - <InputRadio /> for type="radio"
17
17
  */
18
18
 
19
- import { requestAction, useConstraints } from "@jsenv/validation";
20
19
  import { forwardRef } from "preact/compat";
21
- import { useEffect, useImperativeHandle, useRef } from "preact/hooks";
22
- import { useNavState } from "../../browser_integration/browser_integration.js";
20
+ import {
21
+ useContext,
22
+ useEffect,
23
+ useImperativeHandle,
24
+ useLayoutEffect,
25
+ useRef,
26
+ } from "preact/hooks";
27
+
23
28
  import { useActionStatus } from "../../use_action_status.js";
29
+ import { requestAction } from "../../validation/custom_constraint_validation.js";
30
+ import { useConstraints } from "../../validation/hooks/use_constraints.js";
24
31
  import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
25
- import {
26
- useActionBoundToOneParam,
27
- useOneFormParam,
28
- } from "../action_execution/use_action.js";
32
+ import { useActionBoundToOneParam } from "../action_execution/use_action.js";
29
33
  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";
34
+ import { LoadableInlineElement } from "../loader/loader_background.jsx";
32
35
  import { useAutoFocus } from "../use_auto_focus.js";
33
36
  import "./field_css.js";
34
- import { useOnChange } from "./use_on_change.js";
37
+ import { ReportReadOnlyOnLabelContext } from "./label.jsx";
38
+ import { useActionEvents } from "./use_action_events.js";
39
+ import {
40
+ DisabledContext,
41
+ LoadingContext,
42
+ LoadingElementContext,
43
+ ReadOnlyContext,
44
+ UIStateContext,
45
+ UIStateControllerContext,
46
+ useUIState,
47
+ useUIStateController,
48
+ } from "./use_ui_state_controller.js";
35
49
 
36
50
  export const InputTextual = forwardRef((props, ref) => {
37
- return renderActionableComponent(props, ref, {
51
+ const uiStateController = useUIStateController(props, "input");
52
+ const uiState = useUIState(uiStateController);
53
+
54
+ const input = renderActionableComponent(props, ref, {
38
55
  Basic: InputTextualBasic,
39
56
  WithAction: InputTextualWithAction,
40
57
  InsideForm: InputTextualInsideForm,
41
58
  });
59
+ return (
60
+ <UIStateControllerContext.Provider value={uiStateController}>
61
+ <UIStateContext.Provider value={uiState}>{input}</UIStateContext.Provider>
62
+ </UIStateControllerContext.Provider>
63
+ );
42
64
  });
43
65
 
44
66
  const InputTextualBasic = forwardRef((props, ref) => {
45
- let {
67
+ const contextReadOnly = useContext(ReadOnlyContext);
68
+ const contextDisabled = useContext(DisabledContext);
69
+ const contextLoading = useContext(LoadingContext);
70
+ const contextLoadingElement = useContext(LoadingElementContext);
71
+ const reportReadOnlyOnLabel = useContext(ReportReadOnlyOnLabelContext);
72
+ const uiStateController = useContext(UIStateControllerContext);
73
+ const uiState = useContext(UIStateContext);
74
+ const {
46
75
  type,
47
- value,
76
+ onInput,
77
+
78
+ readOnly,
79
+ disabled,
80
+ constraints = [],
81
+ loading,
82
+
48
83
  autoFocus,
49
84
  autoFocusVisible,
50
85
  autoSelect,
51
- constraints = [],
52
- loading,
53
86
  appearance = "custom",
87
+ width,
88
+ height,
54
89
  ...rest
55
90
  } = props;
56
-
57
91
  const innerRef = useRef();
58
92
  useImperativeHandle(ref, () => innerRef.current);
93
+
94
+ const innerValue =
95
+ type === "datetime-local" ? convertToLocalTimezone(uiState) : uiState;
96
+ const innerLoading =
97
+ loading || (contextLoading && contextLoadingElement === innerRef.current);
98
+ const innerReadOnly =
99
+ readOnly || contextReadOnly || innerLoading || uiStateController.readOnly;
100
+ const innerDisabled = disabled || contextDisabled;
101
+ // infom any <label> parent of our readOnly state
102
+ reportReadOnlyOnLabel?.(innerReadOnly);
59
103
  useAutoFocus(innerRef, autoFocus, {
60
104
  autoFocusVisible,
61
105
  autoSelect,
62
106
  });
63
107
  useConstraints(innerRef, constraints);
64
108
 
65
- if (type === "datetime-local") {
66
- value = convertToLocalTimezone(value);
67
- }
68
-
69
109
  const inputTextual = (
70
110
  <input
111
+ {...rest}
71
112
  ref={innerRef}
72
113
  type={type}
73
- value={value}
114
+ data-value={uiState}
115
+ value={innerValue}
74
116
  data-field=""
75
117
  data-field-with-border=""
76
118
  data-custom={appearance === "custom" ? "" : undefined}
77
- {...rest}
119
+ readOnly={innerReadOnly}
120
+ disabled={innerDisabled}
121
+ onInput={(e) => {
122
+ let inputValue;
123
+ if (type === "number") {
124
+ inputValue = e.target.valueAsNumber;
125
+ } else if (type === "datetime-local") {
126
+ inputValue = convertToUTCTimezone(e.target.value);
127
+ } else {
128
+ inputValue = e.target.value;
129
+ }
130
+ uiStateController.setUIState(inputValue, e);
131
+ onInput?.(e);
132
+ }}
133
+ // eslint-disable-next-line react/no-unknown-property
134
+ onresetuistate={(e) => {
135
+ uiStateController.resetUIState(e);
136
+ }}
137
+ // eslint-disable-next-line react/no-unknown-property
138
+ onsetuistate={(e) => {
139
+ uiStateController.setUIState(e.detail.value, e);
140
+ }}
78
141
  />
79
142
  );
80
143
 
81
144
  return (
82
- <LoaderBackground loading={loading} color="light-dark(#355fcc, #3b82f6)">
145
+ <LoadableInlineElement
146
+ loading={innerLoading}
147
+ color="light-dark(#355fcc, #3b82f6)"
148
+ width={width}
149
+ height={height}
150
+ >
83
151
  {inputTextual}
84
- </LoaderBackground>
152
+ </LoadableInlineElement>
85
153
  );
86
154
  });
87
155
 
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
156
  const InputTextualWithAction = forwardRef((props, ref) => {
157
+ const uiState = useContext(UIStateContext);
141
158
  const {
142
- id,
143
- type,
144
159
  action,
145
- name,
146
- value: externalValue,
147
- valueSignal,
148
- cancelOnBlurInvalid,
149
- cancelOnEscape,
150
- actionErrorEffect,
151
- readOnly,
152
160
  loading,
153
- onInput,
154
161
  onCancel,
155
162
  onActionPrevented,
156
163
  onActionStart,
157
164
  onActionError,
158
165
  onActionEnd,
166
+ cancelOnBlurInvalid,
167
+ cancelOnEscape,
168
+ actionErrorEffect,
169
+ onInput,
170
+ onKeyDown,
159
171
  ...rest
160
172
  } = props;
161
- if (import.meta.dev && !name && !valueSignal) {
162
- console.warn(`InputTextual with action requires a name prop to be set.`);
163
- }
164
-
165
173
  const innerRef = useRef(null);
166
174
  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
- );
175
+ const [boundAction] = useActionBoundToOneParam(action, uiState);
176
176
  const { loading: actionLoading } = useActionStatus(boundAction);
177
177
  const executeAction = useExecuteAction(innerRef, {
178
178
  errorEffect: actionErrorEffect,
179
179
  });
180
- useEffect(() => {
181
- setNavState(value);
182
- }, [value]);
183
-
184
180
  const valueAtInteractionRef = useRef(null);
185
- useOnChange(innerRef, (e) => {
181
+
182
+ useOnInputChange(innerRef, (e) => {
186
183
  if (
187
184
  valueAtInteractionRef.current !== null &&
188
185
  e.target.value === valueAtInteractionRef.current
@@ -190,9 +187,15 @@ const InputTextualWithAction = forwardRef((props, ref) => {
190
187
  valueAtInteractionRef.current = null;
191
188
  return;
192
189
  }
193
- requestAction(boundAction, { event: e });
190
+ requestAction(e.target, boundAction, {
191
+ event: e,
192
+ actionOrigin: "action_prop",
193
+ });
194
194
  });
195
-
195
+ // here updating the input won't call the associated action
196
+ // (user have to blur or press enter for this to happen)
197
+ // so we can keep the ui state on cancel/abort/error and let user decide
198
+ // to update ui state or retry via blur/enter as is
196
199
  useActionEvents(innerRef, {
197
200
  onCancel: (e, reason) => {
198
201
  if (reason.startsWith("blur_invalid")) {
@@ -219,44 +222,23 @@ const InputTextualWithAction = forwardRef((props, ref) => {
219
222
  */
220
223
  valueAtInteractionRef.current = e.target.value;
221
224
  }
222
- resetValue();
223
225
  onCancel?.(e, reason);
224
226
  },
225
227
  onPrevented: onActionPrevented,
226
228
  onAction: executeAction,
227
229
  onStart: onActionStart,
228
230
  onError: onActionError,
229
- onEnd: (e) => {
230
- setNavState(undefined);
231
- onActionEnd?.(e);
232
- },
231
+ onEnd: onActionEnd,
233
232
  });
234
233
 
235
- const innerLoading = loading || actionLoading;
236
-
237
234
  return (
238
235
  <InputTextualBasic
236
+ data-action={boundAction.name}
239
237
  {...rest}
240
- data-action={boundAction}
241
238
  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}
239
+ loading={loading || actionLoading}
251
240
  onInput={(e) => {
252
241
  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
242
  onInput?.(e);
261
243
  }}
262
244
  onKeyDown={(e) => {
@@ -270,53 +252,22 @@ const InputTextualWithAction = forwardRef((props, ref) => {
270
252
  * We need to prevent the next change event otherwise we would request action twice
271
253
  */
272
254
  valueAtInteractionRef.current = e.target.value;
273
- requestAction(boundAction, { event: e });
255
+ requestAction(e.target, boundAction, {
256
+ event: e,
257
+ actionOrigin: "action_prop",
258
+ });
259
+ onKeyDown?.(e);
274
260
  }}
275
261
  />
276
262
  );
277
263
  });
278
-
279
264
  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]);
265
+ const { onKeyDown, ...rest } = props;
302
266
 
303
267
  return (
304
268
  <InputTextualBasic
305
269
  {...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
- }}
270
+ ref={ref}
320
271
  onKeyDown={(e) => {
321
272
  if (e.key === "Enter") {
322
273
  const inputElement = e.target;
@@ -325,14 +276,137 @@ const InputTextualInsideForm = forwardRef((props, ref) => {
325
276
  "button[type='submit'], input[type='submit'], input[type='image']",
326
277
  );
327
278
  e.preventDefault();
328
- requestAction(formAction, {
329
- event: e,
330
- target: form,
331
- requester: formSubmitButton ? formSubmitButton : inputElement,
332
- });
279
+ form.dispatchEvent(
280
+ new CustomEvent("actionrequested", {
281
+ detail: {
282
+ requester: formSubmitButton ? formSubmitButton : inputElement,
283
+ event: e,
284
+ meta: { isSubmit: true },
285
+ actionOrigin: "action_prop",
286
+ },
287
+ }),
288
+ );
333
289
  }
334
290
  onKeyDown?.(e);
335
291
  }}
336
292
  />
337
293
  );
338
294
  });
295
+
296
+ const useOnInputChange = (inputRef, callback) => {
297
+ // we must use a custom event listener because preact bind onChange to onInput for compat with react
298
+ useEffect(() => {
299
+ const input = inputRef.current;
300
+ input.addEventListener("change", callback);
301
+ return () => {
302
+ input.removeEventListener("change", callback);
303
+ };
304
+ }, [callback]);
305
+
306
+ // Handle programmatic value changes that don't trigger browser change events
307
+ //
308
+ // Problem: When input values are set programmatically (not by user typing),
309
+ // browsers don't fire the 'change' event. However, our application logic
310
+ // still needs to detect these changes.
311
+ //
312
+ // Example scenario:
313
+ // 1. User starts editing (letter key pressed, value set programmatically)
314
+ // 2. User doesn't type anything additional (this is the key part)
315
+ // 3. User clicks outside to finish editing
316
+ // 4. Without this code, no change event would fire despite the fact that the input value did change from its original state
317
+ //
318
+ // This distinction is crucial because:
319
+ //
320
+ // - If the user typed additional text after the initial programmatic value,
321
+ // the browser would fire change events normally
322
+ // - But when they don't type anything else, the browser considers it as "no user interaction"
323
+ // even though the programmatic initial value represents a meaningful change
324
+ const valueAtStartRef = useRef();
325
+ const interactedRef = useRef(false);
326
+ useLayoutEffect(() => {
327
+ const input = inputRef.current;
328
+ valueAtStartRef.current = input.value;
329
+
330
+ const onfocus = () => {
331
+ interactedRef.current = false;
332
+ valueAtStartRef.current = input.value;
333
+ };
334
+ const oninput = (e) => {
335
+ if (!e.isTrusted) {
336
+ // non trusted "input" events will be ignored by the browser when deciding to fire "change" event
337
+ // we ignore them too
338
+ return;
339
+ }
340
+ interactedRef.current = true;
341
+ };
342
+ const onblur = (e) => {
343
+ if (interactedRef.current) {
344
+ return;
345
+ }
346
+ if (valueAtStartRef.current === input.value) {
347
+ return;
348
+ }
349
+ callback(e);
350
+ };
351
+
352
+ input.addEventListener("focus", onfocus);
353
+ input.addEventListener("input", oninput);
354
+ input.addEventListener("blur", onblur);
355
+
356
+ return () => {
357
+ input.removeEventListener("focus", onfocus);
358
+ input.removeEventListener("input", oninput);
359
+ input.removeEventListener("blur", onblur);
360
+ };
361
+ }, []);
362
+ };
363
+ // As explained in https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/datetime-local#setting_timezones
364
+ // datetime-local does not support timezones
365
+ const convertToLocalTimezone = (dateTimeString) => {
366
+ const date = new Date(dateTimeString);
367
+ // Check if the date is valid
368
+ if (isNaN(date.getTime())) {
369
+ return dateTimeString;
370
+ }
371
+
372
+ // Format to YYYY-MM-DDThh:mm:ss
373
+ const year = date.getFullYear();
374
+ const month = String(date.getMonth() + 1).padStart(2, "0");
375
+ const day = String(date.getDate()).padStart(2, "0");
376
+ const hours = String(date.getHours()).padStart(2, "0");
377
+ const minutes = String(date.getMinutes()).padStart(2, "0");
378
+ const seconds = String(date.getSeconds()).padStart(2, "0");
379
+
380
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
381
+ };
382
+ /**
383
+ * Converts a datetime string without timezone (local time) to UTC format with 'Z' notation
384
+ *
385
+ * @param {string} localDateTimeString - Local datetime string without timezone (e.g., "2023-07-15T14:30:00")
386
+ * @returns {string} Datetime string in UTC with 'Z' notation (e.g., "2023-07-15T12:30:00Z")
387
+ */
388
+ const convertToUTCTimezone = (localDateTimeString) => {
389
+ if (!localDateTimeString) {
390
+ return localDateTimeString;
391
+ }
392
+
393
+ try {
394
+ // Create a Date object using the local time string
395
+ // The browser will interpret this as local timezone
396
+ const localDate = new Date(localDateTimeString);
397
+
398
+ // Check if the date is valid
399
+ if (isNaN(localDate.getTime())) {
400
+ return localDateTimeString;
401
+ }
402
+
403
+ // Convert to UTC ISO string
404
+ const utcString = localDate.toISOString();
405
+
406
+ // Return the UTC string (which includes the 'Z' notation)
407
+ return utcString;
408
+ } catch (error) {
409
+ console.error("Error converting local datetime to UTC:", error);
410
+ return localDateTimeString;
411
+ }
412
+ };
@@ -0,0 +1,32 @@
1
+ import { createContext } from "preact";
2
+ import { forwardRef } from "preact/compat";
3
+ import { useImperativeHandle, useRef, useState } from "preact/hooks";
4
+
5
+ import.meta.css = /* css */ `
6
+ label[data-readonly] {
7
+ color: rgba(0, 0, 0, 0.5);
8
+ }
9
+ `;
10
+
11
+ export const ReportReadOnlyOnLabelContext = createContext();
12
+
13
+ export const Label = forwardRef((props, ref) => {
14
+ const { readOnly, children, ...rest } = props;
15
+ const innerRef = useRef();
16
+ useImperativeHandle(ref, () => innerRef.current);
17
+
18
+ const [inputReadOnly, setInputReadOnly] = useState(false);
19
+ const innerReadOnly = readOnly || inputReadOnly;
20
+
21
+ return (
22
+ <label
23
+ ref={innerRef}
24
+ data-readonly={innerReadOnly ? "" : undefined}
25
+ {...rest}
26
+ >
27
+ <ReportReadOnlyOnLabelContext.Provider value={setInputReadOnly}>
28
+ {children}
29
+ </ReportReadOnlyOnLabelContext.Provider>
30
+ </label>
31
+ );
32
+ });