@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,262 +0,0 @@
1
- import { canInterceptKeys } from "@jsenv/dom";
2
- import { createContext } from "preact";
3
- import { useContext, useLayoutEffect, useRef } from "preact/hooks";
4
-
5
- const SelectionContext = createContext(null);
6
-
7
- export const SelectionProvider = ({ value = [], onChange, children }) => {
8
- const selection = value || [];
9
- const registryRef = useRef([]); // Array<value>
10
- const anchorRef = useRef(null);
11
-
12
- const contextValue = {
13
- selection,
14
-
15
- register: (value) => {
16
- const registry = registryRef.current;
17
- const existingIndex = registry.indexOf(value);
18
- if (existingIndex >= 0) {
19
- console.warn(
20
- `SelectionContext: Attempted to register an already registered value: ${value}. All values must be unique.`,
21
- );
22
- return;
23
- }
24
- registry.push(value);
25
- },
26
- unregister: (value) => {
27
- const registry = registryRef.current;
28
- const index = registry.indexOf(value);
29
- if (index >= 0) {
30
- registry.splice(index, 1);
31
- }
32
- },
33
- setAnchor: (value) => {
34
- anchorRef.current = value;
35
- },
36
- isSelected: (itemValue) => {
37
- return selection.includes(itemValue);
38
- },
39
- getAllItems: () => {
40
- return registryRef.current;
41
- },
42
- getRange: (fromValue, toValue) => {
43
- const registry = registryRef.current;
44
-
45
- // Find indices of fromValue and toValue
46
- let fromIndex = -1;
47
- let toIndex = -1;
48
- let index = 0;
49
- for (const valueCandidate of registry) {
50
- if (valueCandidate === fromValue) {
51
- fromIndex = index;
52
- }
53
- if (valueCandidate === toValue) {
54
- toIndex = index;
55
- }
56
- index++;
57
- }
58
-
59
- if (fromIndex >= 0 && toIndex >= 0) {
60
- // Select all items between fromIndex and toIndex (inclusive)
61
- const start = Math.min(fromIndex, toIndex);
62
- const end = Math.max(fromIndex, toIndex);
63
- const valueInRangeArray = registry.slice(start, end + 1);
64
- return valueInRangeArray;
65
- }
66
- return [];
67
- },
68
-
69
- // basic methods to manipulate selection
70
- set: (newSelection, event = null) => {
71
- if (
72
- newSelection.length === selection.length &&
73
- newSelection.every((value, index) => value === selection[index])
74
- ) {
75
- return;
76
- }
77
- onChange?.(newSelection, event);
78
- },
79
- add: (arrayOfValueToAddToSelection, event = null) => {
80
- const selectionWithValues = [];
81
- for (const value of selection) {
82
- selectionWithValues.push(value);
83
- }
84
- let modified = false;
85
- for (const valueToAdd of arrayOfValueToAddToSelection) {
86
- if (selectionWithValues.includes(valueToAdd)) {
87
- continue;
88
- }
89
- modified = true;
90
- selectionWithValues.push(valueToAdd);
91
- }
92
- if (modified) {
93
- onChange?.(selectionWithValues, event);
94
- }
95
- },
96
- remove: (arrayOfValueToRemoveFromSelection, event = null) => {
97
- let modified = false;
98
- const selectionWithoutValues = [];
99
- for (const value of selection) {
100
- if (arrayOfValueToRemoveFromSelection.includes(value)) {
101
- modified = true;
102
- // If we're removing the last selected value, clear it
103
- if (value === anchorRef.current) {
104
- anchorRef.current = null;
105
- }
106
- } else {
107
- selectionWithoutValues.push(value);
108
- }
109
- }
110
-
111
- if (modified) {
112
- onChange?.(selectionWithoutValues, event);
113
- }
114
- },
115
-
116
- // Convenience method for multi-select: toggle, addFromLastSelectedTo
117
- toggle: (value, event = null) => {
118
- if (selection.includes(value)) {
119
- contextValue.remove([value], event);
120
- } else {
121
- contextValue.add([value], event);
122
- }
123
- },
124
- // Convenience method for shift-click: add range from last selected to target value
125
- setFromAnchorTo: (value, event = null) => {
126
- const anchorValue = anchorRef.current;
127
-
128
- // Make sure the last selected value is still in the current selection
129
- if (anchorValue && selection.includes(anchorValue)) {
130
- const range = contextValue.getRange(anchorValue, value);
131
- contextValue.set(range, event);
132
- } else {
133
- // No valid previous selection, just select this one
134
- contextValue.set([value], event);
135
- }
136
- },
137
-
138
- getValueAfter: (value) => {
139
- const registry = registryRef.current;
140
- const index = registry.indexOf(value);
141
- if (index < 0 || index >= registry.length - 1) {
142
- return null; // No next value
143
- }
144
- return registry[index + 1];
145
- },
146
- getValueBefore: (value) => {
147
- const registry = registryRef.current;
148
- const index = registry.indexOf(value);
149
- if (index <= 0) {
150
- return null; // No previous value
151
- }
152
- return registry[index - 1];
153
- },
154
- };
155
-
156
- return (
157
- <SelectionContext.Provider value={contextValue}>
158
- {children}
159
- </SelectionContext.Provider>
160
- );
161
- };
162
-
163
- export const useSelectionContext = () => {
164
- return useContext(SelectionContext);
165
- };
166
-
167
- export const useRegisterSelectionValue = (value) => {
168
- const selectionContext = useSelectionContext();
169
-
170
- useLayoutEffect(() => {
171
- if (selectionContext) {
172
- selectionContext.register(value);
173
- return () => selectionContext.unregister(value);
174
- }
175
- return undefined;
176
- }, [selectionContext, value]);
177
- };
178
-
179
- export const clickToSelect = (clickEvent, { selectionContext, value }) => {
180
- if (clickEvent.defaultPrevented) {
181
- // If the click was prevented by another handler, do not interfere
182
- return;
183
- }
184
-
185
- const isMultiSelect = clickEvent.metaKey || clickEvent.ctrlKey;
186
- const isShiftSelect = clickEvent.shiftKey;
187
- const isSingleSelect = !isMultiSelect && !isShiftSelect;
188
-
189
- if (isSingleSelect) {
190
- // Single select - replace entire selection with just this item
191
- selectionContext.set([value], clickEvent);
192
- return;
193
- }
194
- if (isMultiSelect) {
195
- // here no need to prevent nav on <a> but it means cmd + click will both multi select
196
- // and open in a new tab
197
- selectionContext.toggle(value, clickEvent);
198
- return;
199
- }
200
- if (isShiftSelect) {
201
- clickEvent.preventDefault(); // Prevent navigation
202
- selectionContext.setFromAnchorTo(value, clickEvent);
203
- return;
204
- }
205
- };
206
-
207
- export const keydownToSelect = (keydownEvent, { selectionContext, value }) => {
208
- if (!canInterceptKeys(keydownEvent)) {
209
- return;
210
- }
211
-
212
- if (keydownEvent.key === "Shift") {
213
- selectionContext.setAnchor(value);
214
- return;
215
- }
216
-
217
- const isMultiSelect = keydownEvent.metaKey || keydownEvent.ctrlKey;
218
- const isShiftSelect = keydownEvent.shiftKey;
219
- const { key } = keydownEvent;
220
- if (key === "a") {
221
- if (!isMultiSelect) {
222
- return;
223
- }
224
- keydownEvent.preventDefault(); // prevent default select all text behavior
225
- selectionContext.set(selectionContext.getAllItems(), keydownEvent);
226
- return;
227
- }
228
- if (key === "ArrowDown") {
229
- const nextValue = selectionContext.getValueAfter(value);
230
- if (!nextValue) {
231
- return; // No next value to select
232
- }
233
- keydownEvent.preventDefault(); // Prevent default scrolling behavior
234
- if (isShiftSelect) {
235
- selectionContext.setFromAnchorTo(nextValue, keydownEvent);
236
- return;
237
- }
238
- if (isMultiSelect) {
239
- selectionContext.add([nextValue], keydownEvent);
240
- return;
241
- }
242
- selectionContext.set([nextValue], keydownEvent);
243
- return;
244
- }
245
- if (key === "ArrowUp") {
246
- const previousValue = selectionContext.getValueBefore(value);
247
- if (!previousValue) {
248
- return; // No previous value to select
249
- }
250
- keydownEvent.preventDefault(); // Prevent default scrolling behavior
251
- if (isShiftSelect) {
252
- selectionContext.setFromAnchorTo(previousValue, keydownEvent);
253
- return;
254
- }
255
- if (isMultiSelect) {
256
- selectionContext.add([previousValue], keydownEvent);
257
- return;
258
- }
259
- selectionContext.set([previousValue], keydownEvent);
260
- return;
261
- }
262
- };
@@ -1,390 +0,0 @@
1
- import { canInterceptKeys } from "@jsenv/dom";
2
- import { requestAction } from "@jsenv/validation";
3
- import { createContext } from "preact";
4
- import { useContext, useEffect, useRef, useState } from "preact/hooks";
5
- import { useAction } from "../action_execution/use_action.js";
6
- import { useExecuteAction } from "../action_execution/use_execute_action.js";
7
- import { useActionEvents } from "../use_action_events.js";
8
- import { isMac } from "./os.js";
9
-
10
- import.meta.css = /* css */ `
11
- .navi_shortcut_container {
12
- /* Visually hidden container - doesn't affect layout */
13
- position: absolute;
14
- width: 1px;
15
- height: 1px;
16
- padding: 0;
17
- margin: -1px;
18
- overflow: hidden;
19
- clip: rect(0, 0, 0, 0);
20
- white-space: nowrap;
21
- border: 0;
22
-
23
- /* Ensure it's not interactable */
24
- opacity: 0;
25
- pointer-events: none;
26
- }
27
-
28
- .navi_shortcut_button {
29
- /* Visually hidden but accessible to screen readers */
30
- position: absolute;
31
- width: 1px;
32
- height: 1px;
33
- padding: 0;
34
- margin: -1px;
35
- overflow: hidden;
36
- clip: rect(0, 0, 0, 0);
37
- white-space: nowrap;
38
- border: 0;
39
-
40
- /* Ensure it's not focusable via tab navigation */
41
- opacity: 0;
42
- pointer-events: none;
43
- }
44
- `;
45
-
46
- const ShortcutContext = createContext();
47
- export const useShortcutContext = () => {
48
- return useContext(ShortcutContext);
49
- };
50
-
51
- export const ShortcutProvider = ({
52
- elementRef,
53
- shortcuts,
54
- onActionPrevented,
55
- onActionStart,
56
- onActionAbort,
57
- onActionError,
58
- onActionEnd,
59
- allowConcurrentActions,
60
- children,
61
- }) => {
62
- if (!elementRef) {
63
- throw new Error(
64
- "ShortcutProvider requires an elementRef to attach shortcuts to.",
65
- );
66
- }
67
-
68
- const shortcutElements = [];
69
- shortcuts.forEach((shortcut) => {
70
- const combinationString = useAriaKeyShortcuts(shortcut.key);
71
- shortcutElements.push(
72
- <button
73
- className="navi_shortcut_button"
74
- key={combinationString}
75
- aria-keyshortcuts={combinationString}
76
- tabIndex="-1"
77
- action={shortcut.action}
78
- data-action={shortcut.action.name}
79
- data-confirm-message={shortcut.confirmMessage}
80
- >
81
- {shortcut.description}
82
- </button>,
83
- );
84
- });
85
- const shortcutElementRef = useRef();
86
- const shortcutHiddenElement = (
87
- <div ref={shortcutElementRef} className="navi_shortcut_container">
88
- {shortcutElements}
89
- </div>
90
- );
91
-
92
- const executeAction = useExecuteAction(shortcutElementRef);
93
- const [shortcutActionIsBusy, setShortcutActionIsBusy] = useState(false);
94
- useActionEvents(shortcutElementRef, {
95
- onPrevented: onActionPrevented,
96
- onAction: (actionEvent) => {
97
- // action can be a function or an action object, whem a function we must "wrap" it in a function returning that function
98
- // otherwise setState would call that action immediately
99
- setAction(() => actionEvent.detail.action);
100
- executeAction(actionEvent, { requester: elementRef.current });
101
- },
102
- onStart: (e) => {
103
- if (!allowConcurrentActions) {
104
- setShortcutActionIsBusy(true);
105
- }
106
- onActionStart?.(e);
107
- },
108
- onAbort: (e) => {
109
- setShortcutActionIsBusy(false);
110
- onActionAbort?.(e);
111
- },
112
- onError: (e) => {
113
- setShortcutActionIsBusy(false);
114
- onActionError?.(e);
115
- },
116
- onEnd: (e) => {
117
- setShortcutActionIsBusy(false);
118
- onActionEnd?.(e);
119
- },
120
- });
121
-
122
- const [action, setAction] = useState(null);
123
- for (const shortcut of shortcuts) {
124
- shortcut.action = useAction(shortcut.action);
125
- }
126
-
127
- useKeyboardShortcuts(elementRef, shortcuts, (shortcut, event) => {
128
- if (shortcutActionIsBusy) {
129
- return;
130
- }
131
- event.preventDefault();
132
- const { action } = shortcut;
133
- requestAction(action, {
134
- event,
135
- target: shortcutElementRef.current,
136
- requester: elementRef.current,
137
- confirmMessage: shortcut.confirmMessage,
138
- });
139
- });
140
-
141
- return (
142
- <ShortcutContext.Provider
143
- value={{
144
- shortcutAction: action,
145
- shortcutActionIsBusy,
146
- }}
147
- >
148
- {children}
149
- {shortcutHiddenElement}
150
- </ShortcutContext.Provider>
151
- );
152
- };
153
-
154
- export const useKeyboardShortcuts = (elementRef, shortcuts, onShortcut) => {
155
- const shortcutsRef = useRef(shortcuts);
156
- shortcutsRef.current = shortcuts;
157
-
158
- const onShortcutRef = useRef(onShortcut);
159
- onShortcutRef.current = onShortcut;
160
-
161
- useEffect(() => {
162
- const element = elementRef.current;
163
-
164
- const onKeydown = (event) => {
165
- if (!canInterceptKeys(event)) {
166
- return;
167
- }
168
-
169
- let shortcutFound;
170
- for (const shortcutCandidate of shortcutsRef.current) {
171
- const { enabled = true, key } = shortcutCandidate;
172
- if (!enabled) {
173
- continue;
174
- }
175
-
176
- // Handle platform-specific combination objects
177
- let actualCombination;
178
- let crossPlatformCombination;
179
-
180
- if (typeof key === "object" && key !== null) {
181
- actualCombination = isMac ? key.mac : key.other;
182
- } else {
183
- actualCombination = key;
184
-
185
- // Auto-generate cross-platform combination if needed
186
- if (containsPlatformSpecificKeys(key)) {
187
- crossPlatformCombination = generateCrossPlatformCombination(key);
188
- }
189
- }
190
-
191
- // Check both the actual combination and cross-platform combination
192
- const matchesActual =
193
- actualCombination &&
194
- eventIsMatchingKeyCombination(event, actualCombination);
195
- const matchesCrossPlatform =
196
- crossPlatformCombination &&
197
- crossPlatformCombination !== actualCombination &&
198
- eventIsMatchingKeyCombination(event, crossPlatformCombination);
199
-
200
- if (!matchesActual && !matchesCrossPlatform) {
201
- continue;
202
- }
203
- if (shortcutCandidate.when && !shortcutCandidate.when(event)) {
204
- continue;
205
- }
206
- shortcutFound = shortcutCandidate;
207
- break;
208
- }
209
- if (!shortcutFound) {
210
- return;
211
- }
212
- onShortcutRef.current(shortcutFound, event);
213
- };
214
-
215
- element.addEventListener("keydown", onKeydown);
216
- return () => {
217
- element.removeEventListener("keydown", onKeydown);
218
- };
219
- }, []);
220
- };
221
-
222
- // Configuration for mapping shortcut key names to browser event properties
223
- const modifierKeyMapping = {
224
- metaKey: {
225
- names: ["meta"],
226
- macNames: ["command", "cmd"],
227
- },
228
- ctrlKey: {
229
- names: ["control", "ctrl"],
230
- },
231
- shiftKey: {
232
- names: ["shift"],
233
- },
234
- altKey: {
235
- names: ["alt"],
236
- macNames: ["option"],
237
- },
238
- };
239
- // Maps canonical browser key names to their user-friendly aliases.
240
- // Used for both event matching and ARIA normalization.
241
- const keyMapping = {
242
- " ": { alias: ["space"] },
243
- "escape": { alias: ["esc"] },
244
- "arrowup": { alias: ["up"] },
245
- "arrowdown": { alias: ["down"] },
246
- "arrowleft": { alias: ["left"] },
247
- "arrowright": { alias: ["right"] },
248
- "delete": { alias: ["del"] },
249
- // Platform-specific mappings
250
- ...(isMac
251
- ? { delete: { alias: ["backspace"] } }
252
- : { backspace: { alias: ["delete"] } }),
253
- };
254
- const keyToAriaKeyMapping = {
255
- // Platform-specific ARIA names
256
- command: "meta",
257
- option: "altgraph", // Mac option key uses "altgraph" in ARIA spec
258
-
259
- // Regular keys - platform-specific normalization
260
- delete: isMac ? "backspace" : "delete", // Mac delete key is backspace semantically
261
- backspace: isMac ? "backspace" : "delete",
262
- };
263
-
264
- const eventIsMatchingKeyCombination = (event, keyCombination) => {
265
- const keys = keyCombination.toLowerCase().split("+");
266
-
267
- for (const key of keys) {
268
- let modifierFound = false;
269
-
270
- // Check if this key is a modifier
271
- for (const [eventProperty, config] of Object.entries(modifierKeyMapping)) {
272
- const allNames = [...config.names];
273
-
274
- // Add Mac-specific names only if we're on Mac and they exist
275
- if (isMac && config.macNames) {
276
- allNames.push(...config.macNames);
277
- }
278
-
279
- if (allNames.includes(key)) {
280
- // Check if the corresponding event property is pressed
281
- if (!event[eventProperty]) {
282
- return false;
283
- }
284
- modifierFound = true;
285
- break;
286
- }
287
- }
288
- if (modifierFound) {
289
- continue;
290
- }
291
-
292
- // If it's not a modifier, check if it matches the actual key
293
- if (!isSameKey(event.key, key)) {
294
- return false;
295
- }
296
- }
297
- return true;
298
- };
299
-
300
- const isSameKey = (browserEventKey, key) => {
301
- browserEventKey = browserEventKey.toLowerCase();
302
- key = key.toLowerCase();
303
-
304
- if (browserEventKey === key) {
305
- return true;
306
- }
307
-
308
- // Check if either key is an alias for the other
309
- for (const [canonicalKey, config] of Object.entries(keyMapping)) {
310
- const allKeys = [canonicalKey, ...config.alias];
311
- if (allKeys.includes(browserEventKey) && allKeys.includes(key)) {
312
- return true;
313
- }
314
- }
315
-
316
- return false;
317
- };
318
-
319
- const normalizeKey = (key) => {
320
- key = key.toLowerCase();
321
-
322
- // Find the canonical form for this key
323
- for (const [canonicalKey, config] of Object.entries(keyMapping)) {
324
- const allKeys = [canonicalKey, ...config.alias];
325
- if (allKeys.includes(key)) {
326
- return canonicalKey;
327
- }
328
- }
329
-
330
- return key;
331
- };
332
-
333
- const normalizeKeyCombination = (combination) => {
334
- const lowerCaseCombination = combination.toLowerCase();
335
- const keys = lowerCaseCombination.split("+");
336
-
337
- // First normalize keys to their canonical form, then apply ARIA mapping
338
- for (let i = 0; i < keys.length; i++) {
339
- let key = normalizeKey(keys[i]);
340
-
341
- // Then apply ARIA-specific mappings if they exist
342
- if (keyToAriaKeyMapping[key]) {
343
- key = keyToAriaKeyMapping[key];
344
- }
345
-
346
- keys[i] = key;
347
- }
348
-
349
- return keys.join("+");
350
- };
351
-
352
- // http://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-keyshortcuts
353
- const useAriaKeyShortcuts = (key) => {
354
- let actualCombination;
355
-
356
- // Handle platform-specific combination objects
357
- if (typeof key === "object" && key !== null) {
358
- actualCombination = isMac ? key.mac : key.other;
359
- } else {
360
- actualCombination = key;
361
- }
362
-
363
- if (actualCombination) {
364
- return normalizeKeyCombination(actualCombination);
365
- }
366
-
367
- return "";
368
- };
369
-
370
- const containsPlatformSpecificKeys = (combination) => {
371
- const lowerCombination = combination.toLowerCase();
372
- const macSpecificKeys = ["command", "cmd"];
373
-
374
- return macSpecificKeys.some((key) => lowerCombination.includes(key));
375
- };
376
-
377
- const generateCrossPlatformCombination = (combination) => {
378
- let crossPlatform = combination;
379
-
380
- if (isMac) {
381
- // No need to convert anything TO Windows/Linux-specific format since we're on Mac
382
- return null;
383
- }
384
-
385
- // If not on Mac but combination contains Mac-specific keys, generate Windows equivalent
386
- crossPlatform = crossPlatform.replace(/\bcommand\b/gi, "control");
387
- crossPlatform = crossPlatform.replace(/\bcmd\b/gi, "control");
388
-
389
- return crossPlatform;
390
- };
@@ -1,37 +0,0 @@
1
- import { useLayoutEffect } from "preact/hooks";
2
- import { addManyEventListeners } from "../utils/add_many_event_listeners.js";
3
-
4
- export const useActionEvents = (
5
- elementRef,
6
- {
7
- /**
8
- * @param {Event} e - L'événement original
9
- * @param {"form_reset" | "blur_invalid" | "escape_key"} reason - Raison du cancel
10
- */
11
- onCancel,
12
- onPrevented,
13
- onAction,
14
- onStart,
15
- onAbort,
16
- onError,
17
- onEnd,
18
- },
19
- ) => {
20
- useLayoutEffect(() => {
21
- const element = elementRef.current;
22
-
23
- return addManyEventListeners(element, {
24
- cancel: (e) => {
25
- onCancel?.(e, e.detail.reason);
26
- },
27
- actionprevented: onPrevented,
28
- action: onAction,
29
- actionstart: onStart,
30
- actionabort: onAbort,
31
- actionerror: (e) => {
32
- onError?.(e.detail.error);
33
- },
34
- actionend: onEnd,
35
- });
36
- }, [onCancel, onPrevented, onAction, onStart, onError, onEnd]);
37
- };