@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.
- package/dist/jsenv_navi.js +22959 -0
- package/index.js +66 -16
- package/package.json +23 -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/field/input_textual.jsx +418 -0
- 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/input_textual.jsx +0 -338
- 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
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
import { requestAction, useConstraints } from "@jsenv/validation";
|
|
2
1
|
import { forwardRef } from "preact/compat";
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { useContext, useImperativeHandle, useRef } from "preact/hooks";
|
|
3
|
+
|
|
5
4
|
import { useActionStatus } from "../../use_action_status.js";
|
|
5
|
+
import { requestAction } from "../../validation/custom_constraint_validation.js";
|
|
6
|
+
import { useConstraints } from "../../validation/hooks/use_constraints.js";
|
|
6
7
|
import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
|
|
7
|
-
import {
|
|
8
|
-
useActionBoundToOneParam,
|
|
9
|
-
useOneFormParam,
|
|
10
|
-
} from "../action_execution/use_action.js";
|
|
8
|
+
import { useActionBoundToOneParam } from "../action_execution/use_action.js";
|
|
11
9
|
import { useExecuteAction } from "../action_execution/use_execute_action.js";
|
|
12
|
-
import {
|
|
13
|
-
import { useActionEvents } from "../use_action_events.js";
|
|
10
|
+
import { LoadableInlineElement } from "../loader/loader_background.jsx";
|
|
14
11
|
import { useAutoFocus } from "../use_auto_focus.js";
|
|
15
|
-
import {
|
|
12
|
+
import { ReportReadOnlyOnLabelContext } from "./label.jsx";
|
|
13
|
+
import { useActionEvents } from "./use_action_events.js";
|
|
14
|
+
import {
|
|
15
|
+
DisabledContext,
|
|
16
|
+
FieldNameContext,
|
|
17
|
+
LoadingContext,
|
|
18
|
+
LoadingElementContext,
|
|
19
|
+
ReadOnlyContext,
|
|
20
|
+
RequiredContext,
|
|
21
|
+
UIStateContext,
|
|
22
|
+
UIStateControllerContext,
|
|
23
|
+
useUIState,
|
|
24
|
+
useUIStateController,
|
|
25
|
+
} from "./use_ui_state_controller.js";
|
|
16
26
|
|
|
17
27
|
import.meta.css = /* css */ `
|
|
18
|
-
.custom_checkbox_wrapper {
|
|
28
|
+
.custom_checkbox_wrapper[data-field-wrapper] {
|
|
19
29
|
display: inline-flex;
|
|
20
30
|
box-sizing: content-box;
|
|
21
31
|
|
|
@@ -23,6 +33,9 @@ import.meta.css = /* css */ `
|
|
|
23
33
|
--checkmark-disabled-color: #eeeeee;
|
|
24
34
|
--checked-color: #3b82f6;
|
|
25
35
|
--checked-disabled-color: #d3d3d3;
|
|
36
|
+
|
|
37
|
+
/* TODO: find a better way maybe? */
|
|
38
|
+
--field-strong-color: var(--checked-color);
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
.custom_checkbox_wrapper input {
|
|
@@ -72,16 +85,16 @@ import.meta.css = /* css */ `
|
|
|
72
85
|
}
|
|
73
86
|
|
|
74
87
|
.custom_checkbox_wrapper input[data-readonly] + .custom_checkbox {
|
|
75
|
-
background-color: var(--field-
|
|
76
|
-
border-color: var(--field-
|
|
88
|
+
background-color: var(--field-readonly-background-color);
|
|
89
|
+
border-color: var(--field-readonly-border-color);
|
|
77
90
|
}
|
|
78
91
|
.custom_checkbox_wrapper input[data-readonly]:checked + .custom_checkbox {
|
|
79
92
|
background: var(--checked-disabled-color);
|
|
80
93
|
border-color: var(--checked-disabled-color);
|
|
81
94
|
}
|
|
82
95
|
.custom_checkbox_wrapper:hover input[data-readonly] + .custom_checkbox {
|
|
83
|
-
background-color: var(--field-
|
|
84
|
-
border-color: var(--field-
|
|
96
|
+
background-color: var(--field-readonly-background-color);
|
|
97
|
+
border-color: var(--field-readonly-border-color);
|
|
85
98
|
}
|
|
86
99
|
.custom_checkbox_wrapper:hover
|
|
87
100
|
input[data-readonly]:checked
|
|
@@ -118,89 +131,137 @@ import.meta.css = /* css */ `
|
|
|
118
131
|
`;
|
|
119
132
|
|
|
120
133
|
export const InputCheckbox = forwardRef((props, ref) => {
|
|
121
|
-
|
|
134
|
+
const { value = "on" } = props;
|
|
135
|
+
const uiStateController = useUIStateController(props, "checkbox", {
|
|
136
|
+
statePropName: "checked",
|
|
137
|
+
defaultStatePropName: "defaultChecked",
|
|
138
|
+
fallbackState: false,
|
|
139
|
+
getStateFromProp: (checked) => (checked ? value : undefined),
|
|
140
|
+
getPropFromState: Boolean,
|
|
141
|
+
});
|
|
142
|
+
const uiState = useUIState(uiStateController);
|
|
143
|
+
|
|
144
|
+
const checkbox = renderActionableComponent(props, ref, {
|
|
122
145
|
Basic: InputCheckboxBasic,
|
|
123
146
|
WithAction: InputCheckboxWithAction,
|
|
124
147
|
InsideForm: InputCheckboxInsideForm,
|
|
125
148
|
});
|
|
149
|
+
return (
|
|
150
|
+
<UIStateControllerContext.Provider value={uiStateController}>
|
|
151
|
+
<UIStateContext.Provider value={uiState}>
|
|
152
|
+
{checkbox}
|
|
153
|
+
</UIStateContext.Provider>
|
|
154
|
+
</UIStateControllerContext.Provider>
|
|
155
|
+
);
|
|
126
156
|
});
|
|
127
157
|
|
|
128
158
|
const InputCheckboxBasic = forwardRef((props, ref) => {
|
|
159
|
+
const contextFieldName = useContext(FieldNameContext);
|
|
160
|
+
const contextReadOnly = useContext(ReadOnlyContext);
|
|
161
|
+
const contextDisabled = useContext(DisabledContext);
|
|
162
|
+
const contextRequired = useContext(RequiredContext);
|
|
163
|
+
const contextLoading = useContext(LoadingContext);
|
|
164
|
+
const loadingElement = useContext(LoadingElementContext);
|
|
165
|
+
const uiStateController = useContext(UIStateControllerContext);
|
|
166
|
+
const uiState = useContext(UIStateContext);
|
|
167
|
+
const reportReadOnlyOnLabel = useContext(ReportReadOnlyOnLabelContext);
|
|
129
168
|
const {
|
|
130
|
-
|
|
131
|
-
constraints = [],
|
|
132
|
-
value = "on",
|
|
133
|
-
checked,
|
|
134
|
-
loading,
|
|
169
|
+
name,
|
|
135
170
|
readOnly,
|
|
136
171
|
disabled,
|
|
137
|
-
|
|
138
|
-
|
|
172
|
+
required,
|
|
173
|
+
loading,
|
|
174
|
+
|
|
175
|
+
autoFocus,
|
|
176
|
+
constraints = [],
|
|
139
177
|
appeareance = "custom", // "custom" or "default"
|
|
178
|
+
accentColor,
|
|
179
|
+
onClick,
|
|
180
|
+
onInput,
|
|
140
181
|
...rest
|
|
141
182
|
} = props;
|
|
142
|
-
|
|
143
|
-
const innerRef = useRef();
|
|
183
|
+
const innerRef = useRef(null);
|
|
144
184
|
useImperativeHandle(ref, () => innerRef.current);
|
|
185
|
+
|
|
186
|
+
const innerName = name || contextFieldName;
|
|
187
|
+
const innerDisabled = disabled || contextDisabled;
|
|
188
|
+
const innerRequired = required || contextRequired;
|
|
189
|
+
const innerLoading =
|
|
190
|
+
loading || (contextLoading && loadingElement === innerRef.current);
|
|
191
|
+
const innerReadOnly =
|
|
192
|
+
readOnly || contextReadOnly || innerLoading || uiStateController.readOnly;
|
|
193
|
+
reportReadOnlyOnLabel?.(innerReadOnly);
|
|
145
194
|
useAutoFocus(innerRef, autoFocus);
|
|
146
195
|
useConstraints(innerRef, constraints);
|
|
147
196
|
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
checkedRef.current = checked;
|
|
197
|
+
const checked = Boolean(uiState);
|
|
198
|
+
const actionName = rest["data-action"];
|
|
199
|
+
if (actionName) {
|
|
200
|
+
delete rest["data-action"];
|
|
153
201
|
}
|
|
154
|
-
|
|
155
|
-
const handleChange = (e) => {
|
|
156
|
-
const isChecked = e.target.checked;
|
|
157
|
-
setInnerChecked(isChecked);
|
|
158
|
-
onChange?.(e);
|
|
159
|
-
};
|
|
160
|
-
|
|
161
202
|
const inputCheckbox = (
|
|
162
203
|
<input
|
|
204
|
+
{...rest}
|
|
163
205
|
ref={innerRef}
|
|
164
206
|
type="checkbox"
|
|
165
|
-
|
|
166
|
-
checked={
|
|
167
|
-
data-readonly={
|
|
207
|
+
name={innerName}
|
|
208
|
+
checked={checked}
|
|
209
|
+
data-readonly={innerReadOnly ? "" : undefined}
|
|
210
|
+
readOnly={innerReadOnly}
|
|
211
|
+
disabled={innerDisabled}
|
|
212
|
+
required={innerRequired}
|
|
168
213
|
data-validation-message-arrow-x="center"
|
|
169
|
-
disabled={disabled}
|
|
170
214
|
onClick={(e) => {
|
|
171
|
-
if (
|
|
215
|
+
if (innerReadOnly) {
|
|
172
216
|
e.preventDefault();
|
|
173
217
|
}
|
|
174
218
|
onClick?.(e);
|
|
175
219
|
}}
|
|
176
|
-
|
|
177
|
-
|
|
220
|
+
onInput={(e) => {
|
|
221
|
+
const checkbox = e.target;
|
|
222
|
+
const checkboxIsChecked = checkbox.checked;
|
|
223
|
+
uiStateController.setUIState(checkboxIsChecked, e);
|
|
224
|
+
onInput?.(e);
|
|
225
|
+
}}
|
|
226
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
227
|
+
onresetuistate={(e) => {
|
|
228
|
+
uiStateController.resetUIState(e);
|
|
229
|
+
}}
|
|
230
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
231
|
+
onsetuistate={(e) => {
|
|
232
|
+
uiStateController.setUIState(e.detail.value, e);
|
|
233
|
+
}}
|
|
178
234
|
/>
|
|
179
235
|
);
|
|
180
236
|
|
|
181
237
|
const inputCheckboxDisplayed =
|
|
182
238
|
appeareance === "custom" ? (
|
|
183
|
-
<CustomCheckbox>{inputCheckbox}</CustomCheckbox>
|
|
239
|
+
<CustomCheckbox accentColor={accentColor}>{inputCheckbox}</CustomCheckbox>
|
|
184
240
|
) : (
|
|
185
241
|
inputCheckbox
|
|
186
242
|
);
|
|
187
243
|
|
|
188
|
-
|
|
189
|
-
<
|
|
190
|
-
|
|
244
|
+
return (
|
|
245
|
+
<LoadableInlineElement
|
|
246
|
+
data-action={actionName}
|
|
247
|
+
loading={innerLoading}
|
|
191
248
|
inset={-1}
|
|
192
249
|
targetSelector={appeareance === "custom" ? ".custom_checkbox" : ""}
|
|
193
250
|
color="light-dark(#355fcc, #3b82f6)"
|
|
194
251
|
>
|
|
195
252
|
{inputCheckboxDisplayed}
|
|
196
|
-
</
|
|
253
|
+
</LoadableInlineElement>
|
|
197
254
|
);
|
|
198
|
-
|
|
199
|
-
return inputCheckboxWithLoader;
|
|
200
255
|
});
|
|
201
|
-
const CustomCheckbox = ({ children }) => {
|
|
256
|
+
const CustomCheckbox = ({ accentColor, children }) => {
|
|
202
257
|
return (
|
|
203
|
-
<div
|
|
258
|
+
<div
|
|
259
|
+
className="custom_checkbox_wrapper"
|
|
260
|
+
data-field-wrapper=""
|
|
261
|
+
style={{
|
|
262
|
+
...(accentColor ? { "--checked-color": accentColor } : {}),
|
|
263
|
+
}}
|
|
264
|
+
>
|
|
204
265
|
{children}
|
|
205
266
|
<div className="custom_checkbox">
|
|
206
267
|
<svg viewBox="0 0 12 12" aria-hidden="true">
|
|
@@ -217,15 +278,10 @@ const CustomCheckbox = ({ children }) => {
|
|
|
217
278
|
};
|
|
218
279
|
|
|
219
280
|
const InputCheckboxWithAction = forwardRef((props, ref) => {
|
|
281
|
+
const uiStateController = useContext(UIStateControllerContext);
|
|
282
|
+
const uiState = useContext(UIStateContext);
|
|
220
283
|
const {
|
|
221
|
-
id,
|
|
222
|
-
name,
|
|
223
|
-
value = "on",
|
|
224
|
-
checked: checkedExternal,
|
|
225
|
-
valueSignal,
|
|
226
284
|
action,
|
|
227
|
-
readOnly,
|
|
228
|
-
loading,
|
|
229
285
|
onCancel,
|
|
230
286
|
onChange,
|
|
231
287
|
actionErrorEffect,
|
|
@@ -234,137 +290,58 @@ const InputCheckboxWithAction = forwardRef((props, ref) => {
|
|
|
234
290
|
onActionAbort,
|
|
235
291
|
onActionError,
|
|
236
292
|
onActionEnd,
|
|
293
|
+
loading,
|
|
237
294
|
...rest
|
|
238
295
|
} = props;
|
|
239
|
-
if (import.meta.dev && !name && !valueSignal) {
|
|
240
|
-
console.warn(`InputCheckboxWithAction requires a name prop to be set.`);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
296
|
const innerRef = useRef(null);
|
|
244
297
|
useImperativeHandle(ref, () => innerRef.current);
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
const [boundAction, checkedValue, setCheckedValue, resetCheckedValue] =
|
|
248
|
-
useActionBoundToOneParam(
|
|
249
|
-
action,
|
|
250
|
-
name,
|
|
251
|
-
valueSignal ? valueSignal : checkedExternal ? value : undefined,
|
|
252
|
-
navState ? value : undefined,
|
|
253
|
-
);
|
|
254
|
-
const checked = checkedValue === value;
|
|
255
|
-
useEffect(() => {
|
|
256
|
-
if (checkedExternal) {
|
|
257
|
-
setNavState(checked ? false : undefined);
|
|
258
|
-
} else {
|
|
259
|
-
setNavState(checked ? true : undefined);
|
|
260
|
-
}
|
|
261
|
-
}, [checkedExternal, checked]);
|
|
262
|
-
const { loading: actionLoading } = useActionStatus(boundAction);
|
|
298
|
+
const [actionBoundToUIState] = useActionBoundToOneParam(action, uiState);
|
|
299
|
+
const { loading: actionLoading } = useActionStatus(actionBoundToUIState);
|
|
263
300
|
const executeAction = useExecuteAction(innerRef, {
|
|
264
301
|
errorEffect: actionErrorEffect,
|
|
265
302
|
});
|
|
266
|
-
const innerLoading = loading || actionLoading;
|
|
267
303
|
|
|
304
|
+
// In this situation updating the ui state === calling associated action
|
|
305
|
+
// so cance/abort/error have to revert the ui state to the one before user interaction
|
|
306
|
+
// to show back the real state of the checkbox (not the one user tried to set)
|
|
268
307
|
useActionEvents(innerRef, {
|
|
269
308
|
onCancel: (e, reason) => {
|
|
270
309
|
if (reason === "blur_invalid") {
|
|
271
310
|
return;
|
|
272
311
|
}
|
|
273
|
-
|
|
274
|
-
resetCheckedValue();
|
|
312
|
+
uiStateController.resetUIState(e);
|
|
275
313
|
onCancel?.(e, reason);
|
|
276
314
|
},
|
|
277
315
|
onPrevented: onActionPrevented,
|
|
278
316
|
onAction: executeAction,
|
|
279
317
|
onStart: onActionStart,
|
|
280
318
|
onAbort: (e) => {
|
|
281
|
-
|
|
319
|
+
uiStateController.resetUIState(e);
|
|
282
320
|
onActionAbort?.(e);
|
|
283
321
|
},
|
|
284
322
|
onError: (e) => {
|
|
285
|
-
|
|
323
|
+
uiStateController.resetUIState(e);
|
|
286
324
|
onActionError?.(e);
|
|
287
325
|
},
|
|
288
326
|
onEnd: (e) => {
|
|
289
|
-
setNavState(undefined);
|
|
290
327
|
onActionEnd?.(e);
|
|
291
328
|
},
|
|
292
329
|
});
|
|
293
330
|
|
|
294
331
|
return (
|
|
295
332
|
<InputCheckboxBasic
|
|
333
|
+
data-action={actionBoundToUIState.name}
|
|
296
334
|
{...rest}
|
|
297
335
|
ref={innerRef}
|
|
298
|
-
|
|
299
|
-
name={name}
|
|
300
|
-
value={value}
|
|
301
|
-
checked={checked}
|
|
302
|
-
data-action={boundAction}
|
|
303
|
-
loading={innerLoading}
|
|
304
|
-
readOnly={readOnly || innerLoading}
|
|
305
|
-
onChange={(e) => {
|
|
306
|
-
const checkboxIsChecked = e.target.checked;
|
|
307
|
-
setCheckedValue(checkboxIsChecked ? value : undefined);
|
|
308
|
-
requestAction(boundAction, { event: e });
|
|
309
|
-
onChange?.(e);
|
|
310
|
-
}}
|
|
311
|
-
/>
|
|
312
|
-
);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
const InputCheckboxInsideForm = forwardRef((props, ref) => {
|
|
316
|
-
const {
|
|
317
|
-
formContext,
|
|
318
|
-
id,
|
|
319
|
-
name,
|
|
320
|
-
value = "on",
|
|
321
|
-
checked: checkedExternal,
|
|
322
|
-
readOnly,
|
|
323
|
-
onChange,
|
|
324
|
-
...rest
|
|
325
|
-
} = props;
|
|
326
|
-
const { formIsReadOnly } = formContext;
|
|
327
|
-
|
|
328
|
-
const innerRef = useRef(null);
|
|
329
|
-
useImperativeHandle(ref, () => innerRef.current);
|
|
330
|
-
|
|
331
|
-
const [navState, setNavState] = useNavState(id);
|
|
332
|
-
const [checkedValue, setCheckedValue, resetCheckedValue] = useOneFormParam(
|
|
333
|
-
name,
|
|
334
|
-
checkedExternal ? value : undefined,
|
|
335
|
-
navState ? value : undefined,
|
|
336
|
-
);
|
|
337
|
-
const checked = checkedValue === value;
|
|
338
|
-
useEffect(() => {
|
|
339
|
-
if (checkedExternal) {
|
|
340
|
-
setNavState(checked ? false : undefined);
|
|
341
|
-
} else {
|
|
342
|
-
setNavState(checked ? true : undefined);
|
|
343
|
-
}
|
|
344
|
-
}, [checkedExternal, checked]);
|
|
345
|
-
|
|
346
|
-
useFormEvents(innerRef, {
|
|
347
|
-
onFormActionAbort: () => {
|
|
348
|
-
resetCheckedValue();
|
|
349
|
-
},
|
|
350
|
-
onFormActionError: () => {
|
|
351
|
-
resetCheckedValue();
|
|
352
|
-
},
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
return (
|
|
356
|
-
<InputCheckboxBasic
|
|
357
|
-
{...rest}
|
|
358
|
-
ref={innerRef}
|
|
359
|
-
id={id}
|
|
360
|
-
name={name}
|
|
361
|
-
checked={checked}
|
|
362
|
-
readOnly={readOnly || formIsReadOnly}
|
|
336
|
+
loading={loading || actionLoading}
|
|
363
337
|
onChange={(e) => {
|
|
364
|
-
|
|
365
|
-
|
|
338
|
+
requestAction(e.target, actionBoundToUIState, {
|
|
339
|
+
event: e,
|
|
340
|
+
actionOrigin: "action_prop",
|
|
341
|
+
});
|
|
366
342
|
onChange?.(e);
|
|
367
343
|
}}
|
|
368
344
|
/>
|
|
369
345
|
);
|
|
370
346
|
});
|
|
347
|
+
const InputCheckboxInsideForm = InputCheckboxBasic;
|
|
@@ -1,12 +1,32 @@
|
|
|
1
|
-
import { useConstraints } from "@jsenv/validation";
|
|
2
1
|
import { forwardRef } from "preact/compat";
|
|
3
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
useContext,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
} from "preact/hooks";
|
|
8
|
+
|
|
9
|
+
import { useConstraints } from "../../validation/hooks/use_constraints.js";
|
|
4
10
|
import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
|
|
5
|
-
import {
|
|
11
|
+
import { LoadableInlineElement } from "../loader/loader_background.jsx";
|
|
6
12
|
import { useAutoFocus } from "../use_auto_focus.js";
|
|
13
|
+
import { ReportReadOnlyOnLabelContext } from "./label.jsx";
|
|
14
|
+
import {
|
|
15
|
+
DisabledContext,
|
|
16
|
+
FieldNameContext,
|
|
17
|
+
LoadingContext,
|
|
18
|
+
LoadingElementContext,
|
|
19
|
+
ReadOnlyContext,
|
|
20
|
+
RequiredContext,
|
|
21
|
+
UIStateContext,
|
|
22
|
+
UIStateControllerContext,
|
|
23
|
+
useUIState,
|
|
24
|
+
useUIStateController,
|
|
25
|
+
} from "./use_ui_state_controller.js";
|
|
7
26
|
|
|
8
27
|
import.meta.css = /* css */ `
|
|
9
28
|
.custom_radio_wrapper {
|
|
29
|
+
position: relative;
|
|
10
30
|
display: inline-flex;
|
|
11
31
|
box-sizing: content-box;
|
|
12
32
|
|
|
@@ -42,17 +62,11 @@ import.meta.css = /* css */ `
|
|
|
42
62
|
.custom_radio svg {
|
|
43
63
|
width: 100%;
|
|
44
64
|
height: 100%;
|
|
45
|
-
transition: all 0.15s ease;
|
|
46
65
|
pointer-events: none;
|
|
47
66
|
}
|
|
48
67
|
|
|
49
|
-
.custom_radio svg .custom_radio_border {
|
|
50
|
-
transition: all 0.15s ease;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
68
|
.custom_radio svg .custom_radio_dashed_border {
|
|
54
69
|
display: none;
|
|
55
|
-
transition: all 0.15s ease;
|
|
56
70
|
}
|
|
57
71
|
|
|
58
72
|
.custom_radio svg .custom_radio_marker {
|
|
@@ -60,6 +74,15 @@ import.meta.css = /* css */ `
|
|
|
60
74
|
opacity: 0;
|
|
61
75
|
transform-origin: center;
|
|
62
76
|
transform: scale(0.3);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.custom_radio[data-transition] svg {
|
|
80
|
+
transition: all 0.15s ease;
|
|
81
|
+
}
|
|
82
|
+
.custom_radio[data-transition] svg .custom_radio_dashed_border {
|
|
83
|
+
transition: all 0.15s ease;
|
|
84
|
+
}
|
|
85
|
+
.custom_radio[data-transition] svg .custom_radio_border {
|
|
63
86
|
transition: all 0.15s ease;
|
|
64
87
|
}
|
|
65
88
|
|
|
@@ -169,82 +192,152 @@ import.meta.css = /* css */ `
|
|
|
169
192
|
`;
|
|
170
193
|
|
|
171
194
|
export const InputRadio = forwardRef((props, ref) => {
|
|
172
|
-
|
|
195
|
+
const { value = "on" } = props;
|
|
196
|
+
const uiStateController = useUIStateController(props, "radio", {
|
|
197
|
+
statePropName: "checked",
|
|
198
|
+
fallbackState: false,
|
|
199
|
+
getStateFromProp: (checked) => (checked ? value : undefined),
|
|
200
|
+
getPropFromState: Boolean,
|
|
201
|
+
});
|
|
202
|
+
const uiState = useUIState(uiStateController);
|
|
203
|
+
|
|
204
|
+
const radio = renderActionableComponent(props, ref, {
|
|
173
205
|
Basic: InputRadioBasic,
|
|
174
206
|
WithAction: InputRadioWithAction,
|
|
175
207
|
InsideForm: InputRadioInsideForm,
|
|
176
208
|
});
|
|
209
|
+
return (
|
|
210
|
+
<UIStateControllerContext.Provider value={uiStateController}>
|
|
211
|
+
<UIStateContext.Provider value={uiState}>{radio}</UIStateContext.Provider>
|
|
212
|
+
</UIStateControllerContext.Provider>
|
|
213
|
+
);
|
|
177
214
|
});
|
|
178
215
|
|
|
179
216
|
const InputRadioBasic = forwardRef((props, ref) => {
|
|
217
|
+
const contextName = useContext(FieldNameContext);
|
|
218
|
+
const contextReadOnly = useContext(ReadOnlyContext);
|
|
219
|
+
const contextDisabled = useContext(DisabledContext);
|
|
220
|
+
const contextRequired = useContext(RequiredContext);
|
|
221
|
+
const contextLoading = useContext(LoadingContext);
|
|
222
|
+
const contextLoadingElement = useContext(LoadingElementContext);
|
|
223
|
+
const uiStateController = useContext(UIStateControllerContext);
|
|
224
|
+
const uiState = useContext(UIStateContext);
|
|
225
|
+
const reportReadOnlyOnLabel = useContext(ReportReadOnlyOnLabelContext);
|
|
180
226
|
const {
|
|
181
|
-
|
|
182
|
-
constraints = [],
|
|
183
|
-
checked,
|
|
227
|
+
name,
|
|
184
228
|
readOnly,
|
|
185
229
|
disabled,
|
|
230
|
+
required,
|
|
186
231
|
loading,
|
|
187
|
-
|
|
188
|
-
|
|
232
|
+
|
|
233
|
+
autoFocus,
|
|
234
|
+
constraints = [],
|
|
235
|
+
|
|
189
236
|
appeareance = "custom", // "custom" or "default"
|
|
237
|
+
accentColor,
|
|
238
|
+
onClick,
|
|
239
|
+
onInput,
|
|
190
240
|
...rest
|
|
191
241
|
} = props;
|
|
192
|
-
|
|
193
242
|
const innerRef = useRef(null);
|
|
194
243
|
useImperativeHandle(ref, () => innerRef.current);
|
|
244
|
+
|
|
245
|
+
const innerName = name || contextName;
|
|
246
|
+
const innerDisabled = disabled || contextDisabled;
|
|
247
|
+
const innerRequired = required || contextRequired;
|
|
248
|
+
const innerLoading =
|
|
249
|
+
loading || (contextLoading && contextLoadingElement === innerRef.current);
|
|
250
|
+
const innerReadOnly =
|
|
251
|
+
readOnly || contextReadOnly || innerLoading || uiStateController.readOnly;
|
|
252
|
+
|
|
253
|
+
reportReadOnlyOnLabel?.(innerReadOnly);
|
|
195
254
|
useAutoFocus(innerRef, autoFocus);
|
|
196
255
|
useConstraints(innerRef, constraints);
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
setInnerChecked(checked);
|
|
202
|
-
checkedRef.current = checked;
|
|
256
|
+
const checked = Boolean(uiState);
|
|
257
|
+
const actionName = rest["data-action"];
|
|
258
|
+
if (actionName) {
|
|
259
|
+
delete rest["data-action"];
|
|
203
260
|
}
|
|
204
261
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
262
|
+
// we must first dispatch an event to inform all other radios they where unchecked
|
|
263
|
+
// this way each other radio uiStateController knows thery are unchecked
|
|
264
|
+
// we do this on "input"
|
|
265
|
+
// but also when we are becoming checked from outside (hence the useLayoutEffect)
|
|
266
|
+
const updateOtherRadiosInGroup = () => {
|
|
267
|
+
const thisRadio = innerRef.current;
|
|
268
|
+
const radioList = thisRadio.closest("[data-radio-list]");
|
|
269
|
+
const radioInputs = radioList.querySelectorAll(
|
|
270
|
+
`input[type="radio"][name="${thisRadio.name}"]`,
|
|
271
|
+
);
|
|
272
|
+
for (const radioInput of radioInputs) {
|
|
273
|
+
if (radioInput === thisRadio) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
radioInput.dispatchEvent(
|
|
277
|
+
new CustomEvent("setuistate", { detail: false }),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
209
280
|
};
|
|
281
|
+
useLayoutEffect(() => {
|
|
282
|
+
if (checked) {
|
|
283
|
+
updateOtherRadiosInGroup();
|
|
284
|
+
}
|
|
285
|
+
}, [checked]);
|
|
210
286
|
|
|
211
287
|
const inputRadio = (
|
|
212
288
|
<input
|
|
289
|
+
{...rest}
|
|
213
290
|
ref={innerRef}
|
|
214
291
|
type="radio"
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
292
|
+
name={innerName}
|
|
293
|
+
checked={checked}
|
|
294
|
+
data-readonly={innerReadOnly ? "" : undefined}
|
|
295
|
+
disabled={innerDisabled}
|
|
296
|
+
required={innerRequired}
|
|
218
297
|
data-validation-message-arrow-x="center"
|
|
219
298
|
onClick={(e) => {
|
|
220
|
-
if (
|
|
299
|
+
if (innerReadOnly) {
|
|
221
300
|
e.preventDefault();
|
|
222
301
|
}
|
|
223
302
|
onClick?.(e);
|
|
224
303
|
}}
|
|
225
|
-
|
|
226
|
-
|
|
304
|
+
onInput={(e) => {
|
|
305
|
+
const radio = e.target;
|
|
306
|
+
const radioIsChecked = radio.checked;
|
|
307
|
+
if (radioIsChecked) {
|
|
308
|
+
updateOtherRadiosInGroup();
|
|
309
|
+
}
|
|
310
|
+
uiStateController.setUIState(radioIsChecked, e);
|
|
311
|
+
onInput?.(e);
|
|
312
|
+
}}
|
|
313
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
314
|
+
onresetuistate={(e) => {
|
|
315
|
+
uiStateController.resetUIState(e);
|
|
316
|
+
}}
|
|
317
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
318
|
+
onsetuistate={(e) => {
|
|
319
|
+
uiStateController.setUIState(e.detail.value, e);
|
|
320
|
+
}}
|
|
227
321
|
/>
|
|
228
322
|
);
|
|
229
323
|
const inputRadioDisplayed =
|
|
230
324
|
appeareance === "custom" ? (
|
|
231
|
-
<CustomRadio>{inputRadio}</CustomRadio>
|
|
325
|
+
<CustomRadio accentColor={accentColor}>{inputRadio}</CustomRadio>
|
|
232
326
|
) : (
|
|
233
327
|
inputRadio
|
|
234
328
|
);
|
|
235
329
|
|
|
236
|
-
|
|
237
|
-
<
|
|
238
|
-
|
|
330
|
+
return (
|
|
331
|
+
<LoadableInlineElement
|
|
332
|
+
data-action={actionName}
|
|
333
|
+
loading={innerLoading}
|
|
239
334
|
targetSelector={appeareance === "custom" ? ".custom_radio" : ""}
|
|
240
|
-
inset={-
|
|
335
|
+
inset={-1}
|
|
241
336
|
color="light-dark(#355fcc, #3b82f6)"
|
|
242
337
|
>
|
|
243
338
|
{inputRadioDisplayed}
|
|
244
|
-
</
|
|
339
|
+
</LoadableInlineElement>
|
|
245
340
|
);
|
|
246
|
-
|
|
247
|
-
return inputRadioWithLoader;
|
|
248
341
|
});
|
|
249
342
|
const CustomRadio = ({ children }) => {
|
|
250
343
|
return (
|
|
@@ -288,12 +381,8 @@ const CustomRadio = ({ children }) => {
|
|
|
288
381
|
|
|
289
382
|
const InputRadioWithAction = () => {
|
|
290
383
|
throw new Error(
|
|
291
|
-
|
|
384
|
+
`<Input type="radio" /> with an action make no sense. Use <RadioList action={something} /> instead`,
|
|
292
385
|
);
|
|
293
386
|
};
|
|
294
387
|
|
|
295
|
-
const InputRadioInsideForm =
|
|
296
|
-
throw new Error(
|
|
297
|
-
`Do not use <Input type="radio" />, use <RadioList /> instead`,
|
|
298
|
-
);
|
|
299
|
-
};
|
|
388
|
+
const InputRadioInsideForm = InputRadio;
|