@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,247 @@
1
+ # UI State Controller
2
+
3
+ The UI State Controller solves a fundamental problem in web applications: managing the relationship between what the user sees (UI state) and what the application knows (external state).
4
+
5
+ ## The Problem
6
+
7
+ Traditional approaches have limitations:
8
+
9
+ 1. **React limitations** - No built-in way to revert UI state back to external state when needed
10
+ 2. **Form limitations** - Regular forms can't do immediate server calls (like PATCH) for instant feedback
11
+ 3. **UX trade-offs** - You're forced to choose between immediate feedback OR traditional form workflow
12
+
13
+ ## The Solution
14
+
15
+ UI State Controller introduces clear separation:
16
+
17
+ - **External State**: The "source of truth" (props, backend data)
18
+ - **UI State**: What the user currently sees and interacts with
19
+ - **Controller**: Manages the relationship between them
20
+
21
+ There are **two distinct usage patterns** depending on your needs:
22
+
23
+ ## Pattern 1: UI with Action (Auto-revert on Error)
24
+
25
+ For interactive components that need immediate feedback with server synchronization:
26
+
27
+ ```jsx
28
+ const [savedValue, setSavedValue] = useState(false);
29
+
30
+ <InputCheckbox
31
+ checked={savedValue} // External state
32
+ action={async (
33
+ // newValue is undefined when unchecked, "on" when checked (default HTML behavior)
34
+ // Customize checked value: <InputCheckbox value="yes" /> sends "yes" instead of "on"
35
+ newValue,
36
+ ) => {
37
+ // PATCH to update existing resource
38
+ const response = await fetch("/api/user/preferences", {
39
+ method: "PATCH",
40
+ headers: { "Content-Type": "application/json" },
41
+ body: JSON.stringify({ emailNotifications: newValue }),
42
+ });
43
+ const result = await response.json();
44
+ setSavedValue(result.emailNotifications);
45
+ }}
46
+ />;
47
+ ```
48
+
49
+ **How it works:**
50
+
51
+ 1. User clicks checkbox → UI updates immediately (responsive)
52
+ 2. Action executes in background
53
+ 3. **Success**: External state updates, UI stays in sync
54
+ 4. **Error**: UI automatically reverts to match external state (auto-revert)
55
+
56
+ **Use when:** You want immediate feedback with automatic error recovery.
57
+
58
+ ## Pattern 2: UI within `<form>` (User Choice on Error)
59
+
60
+ For traditional form workflows where users control submission:
61
+
62
+ ```jsx
63
+ <Form
64
+ action={async ({ email, consent }) => {
65
+ const response = await fetch("/api/user/settings", {
66
+ method: "POST",
67
+ headers: { "Content-Type": "application/json" },
68
+ body: JSON.stringify({
69
+ email,
70
+ consent,
71
+ }),
72
+ });
73
+ if (!response.ok) {
74
+ throw new Error("Failed to save settings");
75
+ }
76
+ // Update your app state here
77
+ const result = await response.json();
78
+ updateUserSettings(result);
79
+ }}
80
+ >
81
+ <label>
82
+ Email Address:
83
+ <Input type="email" name="email" defaultValue="user@example.com" required />
84
+ </label>
85
+
86
+ <label>
87
+ <InputCheckbox name="consent" />I agree to receive marketing emails
88
+ </label>
89
+
90
+ <button type="submit">Save Settings</button>
91
+ <button type="reset">Reset Form</button>
92
+ </Form>
93
+ ```
94
+
95
+ **Key differences:**
96
+
97
+ - **Form submission errors**: UI state is NOT reverted
98
+ - **Reasoning**: User might want to fix the issue and re-submit as-is
99
+ - **Form reset**: UI state is properly restored to original values
100
+ - **Navigation**: Form state persists during page navigation
101
+
102
+ **Use when:** You want traditional form behavior with user control over submission.
103
+
104
+ ## When to Use Each Pattern
105
+
106
+ ### Use Action Pattern When:
107
+
108
+ - Building interactive dashboards or real-time interfaces
109
+ - Each change should be immediately persisted
110
+ - You want automatic error recovery
111
+ - User expects instant feedback
112
+
113
+ ### Use Form Pattern When:
114
+
115
+ - Building traditional forms with submit/reset workflow
116
+ - Users need to make multiple changes before saving
117
+ - You want standard form validation behavior
118
+ - Users should control when changes are persisted
119
+
120
+ ## Advanced APIs
121
+
122
+ ### Tracking State Changes
123
+
124
+ Use `onUIStateChange` to track what the user has selected (like in our demo). This can be useful for showing what would be submitted/reset, though it's not always needed:
125
+
126
+ ```jsx
127
+ const [colorChoices, setColorChoices] = useState([
128
+ { id: 1, color: "red", selected: true },
129
+ { id: 2, color: "blue", selected: false },
130
+ { id: 3, color: "green", selected: true },
131
+ ]);
132
+
133
+ // What's currently saved
134
+ const selectedColors = colorChoices
135
+ .filter((choice) => choice.selected)
136
+ .map((choice) => choice.color);
137
+
138
+ // What user has selected in UI (may differ)
139
+ const [uiSelectedColors, setUiSelectedColors] = useState(selectedColors);
140
+
141
+ <Form action={submitColorPreferences}>
142
+ <CheckboxList
143
+ name="colors"
144
+ onUIStateChange={(colors) => {
145
+ // Track what user has selected
146
+ setUiSelectedColors(colors);
147
+ // Can be used for UI feedback or internal logic
148
+ }}
149
+ >
150
+ {colorChoices.map(({ id, color }) => (
151
+ <Label key={id}>
152
+ {color}
153
+ <Checkbox value={color} checked={selectedColors.includes(color)} />
154
+ </Label>
155
+ ))}
156
+ </CheckboxList>
157
+
158
+ <button type="submit">Submit ({uiSelectedColors.join(", ")})</button>
159
+ <button type="reset">Reset to saved ({selectedColors.join(", ")})</button>
160
+ </Form>;
161
+ ```
162
+
163
+ ### External Control via Custom Events
164
+
165
+ Programmatically control UI state from outside the component:
166
+
167
+ ```jsx
168
+ // Set UI state externally
169
+ const checkbox = document.querySelector("#my-checkbox");
170
+ checkbox.dispatchEvent(
171
+ new CustomEvent("setuistate", {
172
+ detail: { value: true },
173
+ }),
174
+ );
175
+
176
+ // Reset UI state to match external state
177
+ checkbox.dispatchEvent(new CustomEvent("resetuistate"));
178
+ ```
179
+
180
+ ### Error Recovery Patterns
181
+
182
+ ```jsx
183
+ <InputCheckbox
184
+ checked={savedValue}
185
+ action={updateServer}
186
+ onActionError={(error) => {
187
+ // UI already reverted automatically
188
+ showErrorMessage("Failed to save: " + error.message);
189
+ }}
190
+ onActionAbort={() => {
191
+ // UI reverted when action was cancelled
192
+ console.log("Action was cancelled");
193
+ }}
194
+ />
195
+ ```
196
+
197
+ ### Group Controllers (Checkbox Lists)
198
+
199
+ Coordinate multiple related inputs:
200
+
201
+ ```jsx
202
+ const [selectedOptions, setSelectedOptions] = useState([]);
203
+
204
+ <CheckboxList
205
+ values={selectedOptions} // External state
206
+ onUIStateChange={(uiState) => {
207
+ // Track what user has selected (internal use)
208
+ console.log("Currently selected:", uiState);
209
+ }}
210
+ action={async (newValues) => {
211
+ const response = await fetch("/api/options", {
212
+ method: "PATCH",
213
+ headers: { "Content-Type": "application/json" },
214
+ body: JSON.stringify({ selectedOptions: newValues }),
215
+ });
216
+ const result = await response.json();
217
+ setSelectedOptions(result.selectedOptions);
218
+ }}
219
+ >
220
+ <InputCheckbox value="option1" />
221
+ <InputCheckbox value="option2" />
222
+ <InputCheckbox value="option3" />
223
+ </CheckboxList>;
224
+ ```
225
+
226
+ **Group features:**
227
+
228
+ - Aggregates individual checkbox states into arrays
229
+ - Coordinate reset operations across all children
230
+ - Single action handles all checkbox changes
231
+
232
+ ## Key Benefits
233
+
234
+ - **Instant feedback**: UI updates immediately, no lag
235
+ - **Flexible error handling**: Auto-revert for actions, user choice for forms
236
+ - **Form compatibility**: Works seamlessly with native form behavior
237
+ - **External control**: Programmatic state control when needed
238
+ - **Group coordination**: Multiple inputs work together naturally
239
+
240
+ ## Summary
241
+
242
+ Choose the pattern that fits your use case:
243
+
244
+ - **Action pattern**: For immediate persistence with auto-revert
245
+ - **Form pattern**: For traditional submit/reset workflows with user control
246
+
247
+ Both patterns provide responsive UI while maintaining data consistency.
@@ -1,44 +1,68 @@
1
1
  import { resolveCSSSize } from "@jsenv/dom";
2
- import { requestAction, useConstraints } from "@jsenv/validation";
3
2
  import { forwardRef } from "preact/compat";
4
- import { useImperativeHandle, useRef } from "preact/hooks";
3
+ import { useContext, useImperativeHandle, useRef } from "preact/hooks";
4
+
5
5
  import { useActionStatus } from "../../use_action_status.js";
6
+ import { requestAction } from "../../validation/custom_constraint_validation.js";
7
+ import { useConstraints } from "../../validation/hooks/use_constraints.js";
6
8
  import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
7
9
  import { useAction } from "../action_execution/use_action.js";
8
10
  import { useExecuteAction } from "../action_execution/use_execute_action.js";
9
- import { LoaderBackground } from "../loader/loader_background.jsx";
10
- import { useActionEvents } from "../use_action_events.js";
11
+ import { LoadableInlineElement } from "../loader/loader_background.jsx";
11
12
  import { useAutoFocus } from "../use_auto_focus.js";
12
13
  import "./field_css.js";
14
+ import { useActionEvents } from "./use_action_events.js";
15
+ import { useFormEvents } from "./use_form_events.js";
16
+ import {
17
+ DisabledContext,
18
+ LoadingContext,
19
+ LoadingElementContext,
20
+ ReadOnlyContext,
21
+ } from "./use_ui_state_controller.js";
22
+
23
+ /**
24
+ * We need a content the visually shrink (scale down) but the button interactive are must remain intact
25
+ * Otherwise a click on the edges of the button cannot not trigger the click event (mouseup occurs outside the button)
26
+ **/
13
27
 
14
28
  /**
15
29
  * We have to re-define the CSS of button because getComputedStyle(button).borderColor returns
16
30
  * rgb(0, 0, 0) while being visually grey in chrome
17
31
  * So we redefine chrome styles so that loader can keep up with the actual color visible to the user
18
- *
19
32
  */
20
33
  import.meta.css = /* css */ `
21
34
  button[data-custom] {
35
+ border: none;
36
+ background: none;
37
+ display: inline-block;
38
+ padding: 0;
39
+ }
40
+
41
+ button[data-custom] .navi_button_content {
22
42
  transition-duration: 0.15s;
23
43
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
24
44
  transition-property: transform;
45
+ display: inline-flex;
46
+ position: relative;
47
+ padding-block: 1px;
48
+ padding-inline: 6px;
25
49
  }
26
- button[data-custom]:active {
50
+
51
+ button[data-custom]:active .navi_button_content {
27
52
  transform: scale(0.9);
28
53
  }
29
- button[data-custom]:disabled {
54
+
55
+ button[data-custom]:disabled .navi_button_content {
30
56
  transform: none;
31
57
  }
32
58
 
33
- button[data-custom] > .shadow {
59
+ button[data-custom] .navi_button_shadow {
34
60
  position: absolute;
35
- inset: calc(
36
- -1 * (var(--button-border-width) + var(--button-outline-width))
37
- );
61
+ inset: calc(-1 * (var(--field-border-width) + var(--field-outline-width)));
38
62
  pointer-events: none;
39
63
  border-radius: inherit;
40
64
  }
41
- button[data-custom]:active > .shadow {
65
+ button[data-custom]:active .navi_button_shadow {
42
66
  box-shadow:
43
67
  inset 0 3px 6px rgba(0, 0, 0, 0.2),
44
68
  inset 0 1px 2px rgba(0, 0, 0, 0.3),
@@ -46,7 +70,7 @@ import.meta.css = /* css */ `
46
70
  inset 2px 0 4px rgba(0, 0, 0, 0.1),
47
71
  inset -2px 0 4px rgba(0, 0, 0, 0.1);
48
72
  }
49
- button[data-custom]:disabled > .shadow {
73
+ button[data-custom]:disabled > .navi_button_shadow {
50
74
  box-shadow: none;
51
75
  }
52
76
  `;
@@ -55,28 +79,36 @@ export const Button = forwardRef((props, ref) => {
55
79
  Basic: ButtonBasic,
56
80
  WithAction: ButtonWithAction,
57
81
  InsideForm: ButtonInsideForm,
58
- InsideFormWithAction: ButtonWithActionInsideForm,
82
+ WithActionInsideForm: ButtonWithActionInsideForm,
59
83
  });
60
84
  });
61
85
 
62
86
  const ButtonBasic = forwardRef((props, ref) => {
87
+ const contextLoading = useContext(LoadingContext);
88
+ const contextLoadingElement = useContext(LoadingElementContext);
89
+ const contextReadOnly = useContext(ReadOnlyContext);
90
+ const contextDisabled = useContext(DisabledContext);
63
91
  const {
64
- autoFocus,
65
- constraints = [],
66
- loading,
67
92
  readOnly,
68
- children,
93
+ disabled,
94
+ loading,
95
+ constraints = [],
96
+ autoFocus,
69
97
  appearance = "custom",
70
98
  discrete,
71
99
  style = {},
100
+ children,
72
101
  ...rest
73
102
  } = props;
74
-
75
103
  const innerRef = useRef();
76
104
  useImperativeHandle(ref, () => innerRef.current);
105
+
77
106
  useAutoFocus(innerRef, autoFocus);
78
107
  useConstraints(innerRef, constraints);
79
-
108
+ const innerLoading =
109
+ loading || (contextLoading && contextLoadingElement === innerRef.current);
110
+ const innerReadOnly = readOnly || contextReadOnly || innerLoading;
111
+ const innerDisabled = disabled || contextDisabled;
80
112
  let {
81
113
  border,
82
114
  borderWidth = border === "none" || discrete ? 0 : 1,
@@ -88,40 +120,44 @@ const ButtonBasic = forwardRef((props, ref) => {
88
120
  outlineWidth = resolveCSSSize(outlineWidth);
89
121
 
90
122
  return (
91
- <LoaderBackground
92
- loading={loading}
93
- inset={
94
- borderWidth -
95
- // -1 is the outline offset thing
96
- 1
97
- }
98
- color="light-dark(#355fcc, #3b82f6)"
123
+ <button
124
+ {...rest}
125
+ ref={innerRef}
126
+ data-custom={appearance === "custom" ? "" : undefined}
127
+ data-readonly-silent={innerReadOnly ? "" : undefined}
128
+ data-readonly={innerReadOnly ? "" : undefined}
129
+ aria-busy={innerLoading}
130
+ style={{
131
+ ...restStyle,
132
+ }}
99
133
  >
100
- <button
101
- ref={innerRef}
102
- {...rest}
103
- data-field=""
104
- data-field-with-background=""
105
- data-field-with-hover=""
106
- data-field-with-border={borderWidth ? "" : undefined}
107
- data-field-with-border-hover={discrete ? "" : undefined}
108
- data-field-with-background-hover={discrete ? "" : undefined}
109
- data-custom={appearance === "custom" ? "" : undefined}
110
- data-validation-message-arrow-x="center"
111
- data-readonly={readOnly ? "" : undefined}
112
- aria-busy={loading}
113
- style={{
114
- ...restStyle,
115
- "--button-border-width": `${borderWidth}px`,
116
- "--button-outline-width": `${outlineWidth}px`,
117
- "--button-border-color": borderColor,
118
- "position": "relative",
119
- }}
134
+ <LoadableInlineElement
135
+ loading={innerLoading}
136
+ inset={-1}
137
+ color="light-dark(#355fcc, #3b82f6)"
120
138
  >
121
- {children}
122
- <div className="shadow"></div>
123
- </button>
124
- </LoaderBackground>
139
+ <span
140
+ className="navi_button_content"
141
+ data-field=""
142
+ data-field-with-background=""
143
+ data-field-with-hover=""
144
+ data-field-with-border={borderWidth ? "" : undefined}
145
+ data-field-with-border-hover={discrete ? "" : undefined}
146
+ data-field-with-background-hover={discrete ? "" : undefined}
147
+ data-validation-message-arrow-x="center"
148
+ data-readonly={innerReadOnly ? "" : undefined}
149
+ data-disabled={innerDisabled ? "" : undefined}
150
+ style={{
151
+ "--field-border-width": `${borderWidth}px`,
152
+ "--field-outline-width": `${outlineWidth}px`,
153
+ "--field-border-color": borderColor,
154
+ }}
155
+ >
156
+ {children}
157
+ <span className="navi_button_shadow"></span>
158
+ </span>
159
+ </LoadableInlineElement>
160
+ </button>
125
161
  );
126
162
  });
127
163
 
@@ -129,20 +165,17 @@ const ButtonWithAction = forwardRef((props, ref) => {
129
165
  const {
130
166
  action,
131
167
  loading,
132
- readOnly,
133
- children,
134
168
  onClick,
135
169
  actionErrorEffect,
136
170
  onActionPrevented,
137
171
  onActionStart,
138
172
  onActionError,
139
173
  onActionEnd,
174
+ children,
140
175
  ...rest
141
176
  } = props;
142
-
143
177
  const innerRef = useRef();
144
178
  useImperativeHandle(ref, () => innerRef.current);
145
-
146
179
  const boundAction = useAction(action);
147
180
  const { loading: actionLoading } = useActionStatus(boundAction);
148
181
  const executeAction = useExecuteAction(innerRef, {
@@ -156,20 +189,23 @@ const ButtonWithAction = forwardRef((props, ref) => {
156
189
  onError: onActionError,
157
190
  onEnd: onActionEnd,
158
191
  });
159
-
160
192
  const handleClick = (event) => {
161
193
  event.preventDefault();
162
- requestAction(boundAction, { event });
194
+ const button = innerRef.current;
195
+ requestAction(button, boundAction, {
196
+ event,
197
+ actionOrigin: "action_prop",
198
+ });
163
199
  };
164
200
  const innerLoading = loading || actionLoading;
165
201
 
166
202
  return (
167
203
  <ButtonBasic
204
+ // put data-action first to help find it in devtools
168
205
  data-action={boundAction.name}
169
- ref={innerRef}
170
206
  {...rest}
207
+ ref={innerRef}
171
208
  loading={innerLoading}
172
- readOnly={readOnly || innerLoading}
173
209
  onClick={(event) => {
174
210
  handleClick(event);
175
211
  onClick?.(event);
@@ -181,30 +217,26 @@ const ButtonWithAction = forwardRef((props, ref) => {
181
217
  });
182
218
 
183
219
  const ButtonInsideForm = forwardRef((props, ref) => {
184
- const { formContext, type, loading, readOnly, onClick, children, ...rest } =
185
- props;
186
- const { formAction, formIsBusy, formIsReadOnly, formActionRequester } =
187
- formContext;
188
-
220
+ const { formContext, type, onClick, children, loading, ...rest } = props;
221
+ const formLoading = formContext.loading;
189
222
  const innerRef = useRef();
190
223
  useImperativeHandle(ref, () => innerRef.current);
191
224
 
192
225
  const wouldSubmitFormByType = type === "submit" || type === "image";
193
- const innerReadOnly = readOnly || formIsReadOnly;
194
-
226
+ const innerLoading = loading || (formLoading && wouldSubmitFormByType);
195
227
  const handleClick = (event) => {
196
- const buttonElement = event.target;
228
+ const buttonElement = innerRef.current;
197
229
  const { form } = buttonElement;
198
230
  let wouldSubmitForm = wouldSubmitFormByType;
199
- if (!wouldSubmitForm) {
231
+ if (!wouldSubmitForm && type === undefined) {
200
232
  const formSubmitButton = form.querySelector(
201
233
  "button[type='submit'], input[type='submit'], input[type='image']",
202
234
  );
203
- const wouldSubmitFormBecauseSingleButton = !formSubmitButton;
204
- wouldSubmitForm = wouldSubmitFormBecauseSingleButton;
235
+ const wouldSubmitFormBecauseSingleButtonWithoutType = !formSubmitButton;
236
+ wouldSubmitForm = wouldSubmitFormBecauseSingleButtonWithoutType;
205
237
  }
206
238
  if (!wouldSubmitForm) {
207
- if (innerReadOnly) {
239
+ if (buttonElement.hasAttribute("data-readonly")) {
208
240
  event.preventDefault();
209
241
  }
210
242
  return;
@@ -212,24 +244,24 @@ const ButtonInsideForm = forwardRef((props, ref) => {
212
244
  // prevent default behavior that would submit the form
213
245
  // we want to go through the action execution process (with validation and all)
214
246
  event.preventDefault();
215
- requestAction(formAction, {
216
- event,
217
- target: form,
218
- requester: buttonElement,
219
- meta: { isSubmit: true },
220
- });
247
+ form.dispatchEvent(
248
+ new CustomEvent("actionrequested", {
249
+ detail: {
250
+ requester: buttonElement,
251
+ event,
252
+ meta: { isSubmit: true },
253
+ actionOrigin: "action_prop",
254
+ },
255
+ }),
256
+ );
221
257
  };
222
258
 
223
259
  return (
224
260
  <ButtonBasic
225
- ref={innerRef}
226
261
  {...rest}
262
+ ref={innerRef}
227
263
  type={type}
228
- loading={
229
- loading || (formIsBusy && formActionRequester === innerRef.current)
230
- }
231
- readOnly={innerReadOnly}
232
- data-readonly-silent={formIsReadOnly ? "" : undefined}
264
+ loading={innerLoading}
233
265
  onClick={(event) => {
234
266
  handleClick(event);
235
267
  onClick?.(event);
@@ -246,10 +278,8 @@ const ButtonWithActionInsideForm = forwardRef((props, ref) => {
246
278
  type,
247
279
  action,
248
280
  loading,
249
- readOnly,
250
281
  children,
251
282
  onClick,
252
- actionErrorEffect,
253
283
  onActionPrevented,
254
284
  onActionStart,
255
285
  onActionAbort,
@@ -261,69 +291,60 @@ const ButtonWithActionInsideForm = forwardRef((props, ref) => {
261
291
  type === "submit" || type === "reset" || type === "image";
262
292
  if (import.meta.dev && hasEffectOnForm) {
263
293
  throw new Error(
264
- "Button with type submit/reset/image should not have their own action",
294
+ `<Button type="${type}" /> should not have their own action`,
265
295
  );
266
296
  }
267
-
297
+ const { formParamsSignal } = formContext;
268
298
  const innerRef = useRef();
269
299
  useImperativeHandle(ref, () => innerRef.current);
270
-
271
- const { formIsReadOnly, formParamsSignal } = formContext;
272
300
  const actionBoundToFormParams = useAction(action, formParamsSignal);
273
301
  const { loading: actionLoading } = useActionStatus(actionBoundToFormParams);
274
- const executeAction = useExecuteAction(innerRef, {
275
- errorEffect: actionErrorEffect,
276
- });
277
302
 
278
- useActionEvents(innerRef, {
279
- onPrevented: onActionPrevented,
280
- onAction: executeAction,
281
- onStart: (e) => {
282
- e.target.form.dispatchEvent(
283
- new CustomEvent("actionstart", { detail: e.detail }),
284
- );
285
- onActionStart?.(e);
303
+ const innerLoading = loading || actionLoading;
304
+ useFormEvents(innerRef, {
305
+ onFormActionPrevented: (e) => {
306
+ if (e.detail.action === actionBoundToFormParams) {
307
+ onActionPrevented?.(e);
308
+ }
286
309
  },
287
- onAbort: (e) => {
288
- e.target.form.dispatchEvent(
289
- new CustomEvent("actionabort", { detail: e.detail }),
290
- );
291
- onActionAbort?.(e);
310
+ onFormActionStart: (e) => {
311
+ if (e.detail.action === actionBoundToFormParams) {
312
+ onActionStart?.(e);
313
+ }
292
314
  },
293
- onError: (e) => {
294
- e.target.form.dispatchEvent(
295
- new CustomEvent("actionerror", { detail: e.detail }),
296
- );
297
- onActionError?.(e);
315
+ onFormActionAbort: (e) => {
316
+ if (e.detail.action === actionBoundToFormParams) {
317
+ onActionAbort?.(e);
318
+ }
298
319
  },
299
- onEnd: (e) => {
300
- e.target.form.dispatchEvent(
301
- new CustomEvent("actionend", { detail: e.detail }),
302
- );
303
- onActionEnd?.(e);
320
+ onFormActionError: (e) => {
321
+ if (e.detail.action === actionBoundToFormParams) {
322
+ onActionError?.(e.detail.error);
323
+ }
324
+ },
325
+ onFormActionEnd: (e) => {
326
+ if (e.detail.action === actionBoundToFormParams) {
327
+ onActionEnd?.(e);
328
+ }
304
329
  },
305
330
  });
306
331
 
307
- const handleClick = (event) => {
308
- event.preventDefault();
309
- // lorsque cette action s'éxecute elle doit mettre le form en mode busy
310
- // je vois pas encore comment je vais faire ca mais a priori
311
- // on va juste le faire "manuellement"
312
- // en utilisnt un truc du formContext
313
- requestAction(actionBoundToFormParams, { event });
314
- };
315
-
316
332
  return (
317
333
  <ButtonBasic
318
334
  data-action={actionBoundToFormParams.name}
319
- ref={innerRef}
320
335
  {...rest}
336
+ ref={innerRef}
321
337
  type={type}
322
- loading={loading || actionLoading}
323
- readOnly={readOnly || formIsReadOnly}
324
- data-readonly-silent={!readOnly && formIsReadOnly ? "" : undefined}
338
+ loading={innerLoading}
325
339
  onClick={(event) => {
326
- handleClick(event);
340
+ const button = innerRef.current;
341
+ const form = button.form;
342
+ event.preventDefault();
343
+ requestAction(form, actionBoundToFormParams, {
344
+ event,
345
+ requester: button,
346
+ actionOrigin: "action_prop",
347
+ });
327
348
  onClick?.(event);
328
349
  }}
329
350
  >