@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,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 {
|
|
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
|
-
|
|
50
|
+
|
|
51
|
+
button[data-custom]:active .navi_button_content {
|
|
27
52
|
transform: scale(0.9);
|
|
28
53
|
}
|
|
29
|
-
|
|
54
|
+
|
|
55
|
+
button[data-custom]:disabled .navi_button_content {
|
|
30
56
|
transform: none;
|
|
31
57
|
}
|
|
32
58
|
|
|
33
|
-
button[data-custom]
|
|
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
|
|
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 > .
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
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
|
-
<
|
|
101
|
-
|
|
102
|
-
{
|
|
103
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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,
|
|
185
|
-
|
|
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
|
|
194
|
-
|
|
226
|
+
const innerLoading = loading || (formLoading && wouldSubmitFormByType);
|
|
195
227
|
const handleClick = (event) => {
|
|
196
|
-
const buttonElement =
|
|
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
|
|
204
|
-
wouldSubmitForm =
|
|
235
|
+
const wouldSubmitFormBecauseSingleButtonWithoutType = !formSubmitButton;
|
|
236
|
+
wouldSubmitForm = wouldSubmitFormBecauseSingleButtonWithoutType;
|
|
205
237
|
}
|
|
206
238
|
if (!wouldSubmitForm) {
|
|
207
|
-
if (
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
288
|
-
e.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
onActionAbort?.(e);
|
|
310
|
+
onFormActionStart: (e) => {
|
|
311
|
+
if (e.detail.action === actionBoundToFormParams) {
|
|
312
|
+
onActionStart?.(e);
|
|
313
|
+
}
|
|
292
314
|
},
|
|
293
|
-
|
|
294
|
-
e.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
onActionError?.(e);
|
|
315
|
+
onFormActionAbort: (e) => {
|
|
316
|
+
if (e.detail.action === actionBoundToFormParams) {
|
|
317
|
+
onActionAbort?.(e);
|
|
318
|
+
}
|
|
298
319
|
},
|
|
299
|
-
|
|
300
|
-
e.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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={
|
|
323
|
-
readOnly={readOnly || formIsReadOnly}
|
|
324
|
-
data-readonly-silent={!readOnly && formIsReadOnly ? "" : undefined}
|
|
338
|
+
loading={innerLoading}
|
|
325
339
|
onClick={(event) => {
|
|
326
|
-
|
|
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
|
>
|