@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
@@ -1,21 +1,31 @@
1
- import { requestAction, useConstraints } from "@jsenv/validation";
2
1
  import { forwardRef } from "preact/compat";
3
- import { useEffect, useImperativeHandle, useRef, useState } from "preact/hooks";
4
- import { useNavState } from "../../browser_integration/browser_integration.js";
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 { LoaderBackground } from "../loader/loader_background.jsx";
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 { useFormEvents } from "./use_form_event.js";
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-disabled-background-color);
76
- border-color: var(--field-disabled-border-color);
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-disabled-background-color);
84
- border-color: var(--field-disabled-border-color);
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
- return renderActionableComponent(props, ref, {
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
- autoFocus,
131
- constraints = [],
132
- value = "on",
133
- checked,
134
- loading,
169
+ name,
135
170
  readOnly,
136
171
  disabled,
137
- onClick,
138
- onChange,
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 [innerChecked, setInnerChecked] = useState(checked);
149
- const checkedRef = useRef(checked);
150
- if (checkedRef.current !== checked) {
151
- setInnerChecked(checked);
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
- value={value}
166
- checked={innerChecked}
167
- data-readonly={readOnly && !disabled ? "" : undefined}
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 (readOnly) {
215
+ if (innerReadOnly) {
172
216
  e.preventDefault();
173
217
  }
174
218
  onClick?.(e);
175
219
  }}
176
- onChange={handleChange}
177
- {...rest}
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
- const inputCheckboxWithLoader = (
189
- <LoaderBackground
190
- loading={loading}
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
- </LoaderBackground>
253
+ </LoadableInlineElement>
197
254
  );
198
-
199
- return inputCheckboxWithLoader;
200
255
  });
201
- const CustomCheckbox = ({ children }) => {
256
+ const CustomCheckbox = ({ accentColor, children }) => {
202
257
  return (
203
- <div className="custom_checkbox_wrapper" data-field-wrapper="">
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 [navState, setNavState] = useNavState(id);
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
- setNavState(undefined);
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
- resetCheckedValue();
319
+ uiStateController.resetUIState(e);
282
320
  onActionAbort?.(e);
283
321
  },
284
322
  onError: (e) => {
285
- resetCheckedValue();
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
- id={id}
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
- const checkboxIsChecked = e.target.checked;
365
- setCheckedValue(checkboxIsChecked ? value : undefined);
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 { useImperativeHandle, useRef, useState } from "preact/hooks";
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 { LoaderBackground } from "../loader/loader_background.jsx";
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
- return renderActionableComponent(props, ref, {
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
- autoFocus,
182
- constraints = [],
183
- checked,
227
+ name,
184
228
  readOnly,
185
229
  disabled,
230
+ required,
186
231
  loading,
187
- onClick,
188
- onChange,
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 [innerChecked, setInnerChecked] = useState(checked);
199
- const checkedRef = useRef(checked);
200
- if (checkedRef.current !== checked) {
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
- const handleChange = (e) => {
206
- const isChecked = e.target.checked;
207
- setInnerChecked(isChecked);
208
- onChange?.(e);
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
- checked={innerChecked}
216
- data-readonly={readOnly && !disabled ? "" : undefined}
217
- disabled={disabled}
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 (readOnly) {
299
+ if (innerReadOnly) {
221
300
  e.preventDefault();
222
301
  }
223
302
  onClick?.(e);
224
303
  }}
225
- onChange={handleChange}
226
- {...rest}
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
- const inputRadioWithLoader = (
237
- <LoaderBackground
238
- loading={loading}
330
+ return (
331
+ <LoadableInlineElement
332
+ data-action={actionName}
333
+ loading={innerLoading}
239
334
  targetSelector={appeareance === "custom" ? ".custom_radio" : ""}
240
- inset={-2}
335
+ inset={-1}
241
336
  color="light-dark(#355fcc, #3b82f6)"
242
337
  >
243
338
  {inputRadioDisplayed}
244
- </LoaderBackground>
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
- `Do not use <Input type="radio" />, use <RadioList /> instead`,
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;