@jsenv/navi 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/jsenv_navi.js +22959 -0
  2. package/index.js +66 -16
  3. package/package.json +23 -11
  4. package/src/actions.js +50 -26
  5. package/src/browser_integration/browser_integration.js +31 -6
  6. package/src/browser_integration/via_history.js +42 -9
  7. package/src/components/action_execution/render_actionable_component.jsx +6 -4
  8. package/src/components/action_execution/use_action.js +51 -282
  9. package/src/components/action_execution/use_execute_action.js +106 -92
  10. package/src/components/action_execution/use_run_on_mount.js +9 -0
  11. package/src/components/action_renderer.jsx +21 -32
  12. package/src/components/demos/0_button_demo.html +574 -103
  13. package/src/components/demos/10_column_reordering_debug.html +277 -0
  14. package/src/components/demos/11_table_selection_debug.html +432 -0
  15. package/src/components/demos/1_checkbox_demo.html +579 -202
  16. package/src/components/demos/2_input_textual_demo.html +81 -138
  17. package/src/components/demos/3_radio_demo.html +0 -2
  18. package/src/components/demos/4_select_demo.html +19 -23
  19. package/src/components/demos/6_tablist_demo.html +77 -0
  20. package/src/components/demos/7_table_selection_demo.html +176 -0
  21. package/src/components/demos/8_table_fixed_headers_demo.html +584 -0
  22. package/src/components/demos/9_table_column_drag_demo.html +325 -0
  23. package/src/components/demos/action/0_button_demo.html +2 -4
  24. package/src/components/demos/action/1_input_text_demo.html +643 -222
  25. package/src/components/demos/action/3_details_demo.html +146 -115
  26. package/src/components/demos/action/4_input_checkbox_demo.html +442 -322
  27. package/src/components/demos/action/5_input_checkbox_state_demo.html +270 -0
  28. package/src/components/demos/action/6_checkbox_list_demo.html +304 -72
  29. package/src/components/demos/action/7_radio_list_demo.html +310 -170
  30. package/src/components/demos/action/{8_editable_text_demo.html → 8_editable_demo.html} +65 -76
  31. package/src/components/demos/action/9_link_demo.html +84 -62
  32. package/src/components/demos/ui_transition/0_action_renderer_ui_transition_demo.html +695 -0
  33. package/src/components/demos/ui_transition/1_nested_ui_transition_demo.html +429 -0
  34. package/src/components/demos/ui_transition/2_height_transition_test.html +295 -0
  35. package/src/components/details/details.jsx +62 -64
  36. package/src/components/edition/editable.jsx +186 -0
  37. package/src/components/field/README.md +247 -0
  38. package/src/components/{input → field}/button.jsx +151 -130
  39. package/src/components/field/checkbox_list.jsx +184 -0
  40. package/src/components/{collect_form_element_values.js → field/collect_form_element_values.js} +7 -4
  41. package/src/components/{input → field}/field_css.js +4 -1
  42. package/src/components/field/form.jsx +211 -0
  43. package/src/components/{input → field}/input.jsx +1 -0
  44. package/src/components/{input → field}/input_checkbox.jsx +132 -155
  45. package/src/components/{input → field}/input_radio.jsx +135 -46
  46. package/src/components/field/input_textual.jsx +418 -0
  47. package/src/components/field/label.jsx +32 -0
  48. package/src/components/field/radio_list.jsx +182 -0
  49. package/src/components/{input → field}/select.jsx +17 -32
  50. package/src/components/field/use_action_events.js +132 -0
  51. package/src/components/field/use_form_events.js +55 -0
  52. package/src/components/field/use_ui_state_controller.js +506 -0
  53. package/src/components/item_tracker/README.md +461 -0
  54. package/src/components/item_tracker/use_isolated_item_tracker.jsx +209 -0
  55. package/src/components/item_tracker/use_isolated_item_tracker_demo.html +148 -0
  56. package/src/components/item_tracker/use_isolated_item_tracker_demo.jsx +460 -0
  57. package/src/components/item_tracker/use_item_tracker.jsx +143 -0
  58. package/src/components/item_tracker/use_item_tracker_demo.html +207 -0
  59. package/src/components/item_tracker/use_item_tracker_demo.jsx +216 -0
  60. package/src/components/keyboard_shortcuts/active_keyboard_shortcuts.jsx +87 -0
  61. package/src/components/keyboard_shortcuts/aria_key_shortcuts.js +61 -0
  62. package/src/components/keyboard_shortcuts/keyboard_key_meta.js +17 -0
  63. package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +371 -0
  64. package/src/components/link/link.jsx +65 -102
  65. package/src/components/link/link_with_icon.jsx +52 -0
  66. package/src/components/loader/loader_background.jsx +85 -64
  67. package/src/components/loader/rectangle_loading.jsx +38 -19
  68. package/src/components/route.jsx +8 -4
  69. package/src/components/selection/selection.jsx +1583 -0
  70. package/src/components/svg/font_sized_svg.jsx +45 -0
  71. package/src/components/svg/icon_and_text.jsx +21 -0
  72. package/src/components/svg/svg_mask_overlay.jsx +105 -0
  73. package/src/components/table/drag/table_drag.jsx +506 -0
  74. package/src/components/table/resize/table_resize.jsx +650 -0
  75. package/src/components/table/resize/table_size.js +43 -0
  76. package/src/components/table/selection/table_selection.js +106 -0
  77. package/src/components/table/selection/table_selection.jsx +203 -0
  78. package/src/components/table/sticky/sticky_group.js +354 -0
  79. package/src/components/table/sticky/table_sticky.js +25 -0
  80. package/src/components/table/sticky/table_sticky.jsx +501 -0
  81. package/src/components/table/table.jsx +721 -0
  82. package/src/components/table/table_css.js +211 -0
  83. package/src/components/table/table_ui.jsx +49 -0
  84. package/src/components/table/use_cells_and_columns.js +90 -0
  85. package/src/components/table/use_object_array_to_cells.js +46 -0
  86. package/src/components/table/z_indexes.js +23 -0
  87. package/src/components/tablist/tablist.jsx +99 -0
  88. package/src/components/text/overflow.jsx +15 -0
  89. package/src/components/text/text_and_count.jsx +28 -0
  90. package/src/components/ui_transition.jsx +128 -0
  91. package/src/components/use_auto_focus.js +58 -7
  92. package/src/components/use_batch_during_render.js +33 -0
  93. package/src/components/use_debounce_true.js +7 -7
  94. package/src/components/use_dependencies_diff.js +35 -0
  95. package/src/components/use_focus_group.js +4 -3
  96. package/src/components/use_initial_value.js +8 -34
  97. package/src/components/use_signal_sync.js +1 -1
  98. package/src/components/use_stable_callback.js +68 -0
  99. package/src/components/use_state_array.js +16 -9
  100. package/src/docs/actions.md +22 -0
  101. package/src/notes.md +33 -12
  102. package/src/route/route.js +97 -47
  103. package/src/store/resource_graph.js +2 -1
  104. package/src/store/tests/{resource_graph_dependencies.test.js → resource_graph_dependencies.test_manual.js} +13 -13
  105. package/src/utils/is_signal.js +20 -0
  106. package/src/utils/stringify_for_display.js +4 -23
  107. package/src/validation/constraints/confirm_constraint.js +14 -0
  108. package/src/validation/constraints/create_unique_value_constraint.js +27 -0
  109. package/src/validation/constraints/native_constraints.js +313 -0
  110. package/src/validation/constraints/readonly_constraint.js +36 -0
  111. package/src/validation/constraints/single_space_constraint.js +13 -0
  112. package/src/validation/custom_constraint_validation.js +599 -0
  113. package/src/validation/custom_message.js +18 -0
  114. package/src/validation/demos/browser_style.png +0 -0
  115. package/src/validation/demos/form_validation_demo.html +142 -0
  116. package/src/validation/demos/form_validation_demo_preact.html +87 -0
  117. package/src/validation/demos/form_validation_native_popover_demo.html +168 -0
  118. package/src/validation/demos/form_validation_vs_native_demo.html +172 -0
  119. package/src/validation/demos/validation_message_demo.html +203 -0
  120. package/src/validation/hooks/use_constraints.js +23 -0
  121. package/src/validation/hooks/use_custom_validation_ref.js +73 -0
  122. package/src/validation/hooks/use_validation_message.js +19 -0
  123. package/src/validation/validation_message.js +741 -0
  124. package/src/components/editable_text/editable_text.jsx +0 -96
  125. package/src/components/form.jsx +0 -144
  126. package/src/components/input/checkbox_list.jsx +0 -294
  127. package/src/components/input/field.jsx +0 -61
  128. package/src/components/input/input_textual.jsx +0 -338
  129. package/src/components/input/radio_list.jsx +0 -283
  130. package/src/components/input/use_form_event.js +0 -20
  131. package/src/components/input/use_on_change.js +0 -12
  132. package/src/components/selection/selection.js +0 -5
  133. package/src/components/selection/selection_context.jsx +0 -262
  134. package/src/components/shortcut/shortcut_context.jsx +0 -390
  135. package/src/components/use_action_events.js +0 -37
  136. package/src/utils/iterable_weak_set.js +0 -62
  137. /package/src/components/demos/action/{11_nested_shortcuts_demo.html → 11_nested_shortcuts_demo.xhtml} +0 -0
  138. /package/src/components/{shortcut → keyboard_shortcuts}/os.js +0 -0
  139. /package/src/route/{route.test.html → route.xtest.html} +0 -0
@@ -0,0 +1,501 @@
1
+ // TODO: sticky left/top frontier should likely use "followPosition"
2
+ // to be properly position in absolute in the document body
3
+ // because otherwise they can't properly follow table as it scrolls
4
+ // (they can't be sticky, they can't react to scroll, so they have to be absolute in the document body)
5
+
6
+ import {
7
+ createDragToMoveGestureController,
8
+ getDropTargetInfo,
9
+ getScrollContainer,
10
+ } from "@jsenv/dom";
11
+ import { useContext, useRef } from "preact/hooks";
12
+
13
+ import {
14
+ Z_INDEX_STICKY_COLUMN,
15
+ Z_INDEX_STICKY_CORNER,
16
+ Z_INDEX_STICKY_FRONTIER_BACKDROP,
17
+ Z_INDEX_STICKY_FRONTIER_GHOST,
18
+ Z_INDEX_STICKY_FRONTIER_PREVIEW,
19
+ Z_INDEX_STICKY_ROW,
20
+ } from "../z_indexes.js";
21
+ import { TableStickyContext } from "./table_sticky.js";
22
+
23
+ import.meta.css = /* css */ `
24
+ body {
25
+ --sticky-frontier-color: #c0c0c0;
26
+ --sticky-frontier-size: 12px;
27
+ --sticky-frontier-ghost-size: 8px;
28
+ }
29
+
30
+ .navi_table_cell[data-sticky-top] {
31
+ position: sticky;
32
+ top: var(--sticky-group-item-top, 0);
33
+ z-index: ${Z_INDEX_STICKY_ROW};
34
+ }
35
+ .navi_table_cell[data-sticky-left] {
36
+ position: sticky;
37
+ left: var(--sticky-group-item-left, 0);
38
+ z-index: ${Z_INDEX_STICKY_COLUMN};
39
+ }
40
+ .navi_table_cell[data-sticky-left][data-sticky-top] {
41
+ position: sticky;
42
+ top: var(--sticky-group-item-top, 0);
43
+ left: var(--sticky-group-item-left, 0);
44
+ z-index: ${Z_INDEX_STICKY_CORNER};
45
+ }
46
+
47
+ /* Useful because drag gesture will read this value to detect <col>, <tr> virtual position */
48
+ .navi_col {
49
+ left: var(--sticky-group-item-left, 0);
50
+ }
51
+ .navi_tr {
52
+ top: var(--sticky-group-item-top, 0);
53
+ }
54
+
55
+ .navi_table_sticky_frontier {
56
+ position: absolute;
57
+ cursor: grab;
58
+ pointer-events: auto;
59
+ }
60
+
61
+ .navi_table_sticky_frontier[data-left] {
62
+ left: calc(var(--table-visual-left) + var(--sticky-group-left));
63
+ width: var(--sticky-frontier-size);
64
+ top: calc(var(--table-visual-top) + var(--sticky-group-top));
65
+ height: calc(var(--table-visual-height) - var(--sticky-group-top));
66
+ background: linear-gradient(
67
+ to right,
68
+ rgba(0, 0, 0, 0.1) 0%,
69
+ rgba(0, 0, 0, 0) 100%
70
+ );
71
+ }
72
+
73
+ .navi_table_sticky_frontier[data-top] {
74
+ left: calc(var(--table-visual-left) + var(--sticky-group-left));
75
+ width: calc(var(--table-visual-width) - var(--sticky-group-left));
76
+ top: calc(var(--table-visual-top) + var(--sticky-group-top));
77
+ height: var(--sticky-frontier-size);
78
+ background: linear-gradient(
79
+ to bottom,
80
+ rgba(0, 0, 0, 0.1) 0%,
81
+ rgba(0, 0, 0, 0) 100%
82
+ );
83
+ }
84
+
85
+ .navi_table_sticky_frontier_ghost,
86
+ .navi_table_sticky_frontier_preview {
87
+ position: absolute;
88
+ pointer-events: none;
89
+ opacity: 0;
90
+ }
91
+ .navi_table_sticky_frontier_ghost {
92
+ z-index: ${Z_INDEX_STICKY_FRONTIER_GHOST};
93
+ background: rgba(68, 71, 70, 0.5);
94
+ }
95
+ .navi_table_sticky_frontier_preview {
96
+ z-index: ${Z_INDEX_STICKY_FRONTIER_PREVIEW};
97
+ background: rgba(56, 121, 200, 0.5);
98
+ }
99
+ .navi_table_sticky_frontier_ghost[data-visible],
100
+ .navi_table_sticky_frontier_preview[data-visible] {
101
+ opacity: 1;
102
+ }
103
+ .navi_table_sticky_frontier_ghost[data-left],
104
+ .navi_table_sticky_frontier_preview[data-left] {
105
+ top: 0;
106
+ width: var(--sticky-frontier-ghost-size);
107
+ height: var(--table-height, 100%);
108
+ }
109
+ .navi_table_sticky_frontier_ghost[data-left] {
110
+ left: var(--sticky-frontier-ghost-position, 0px);
111
+ }
112
+ .navi_table_sticky_frontier_preview[data-left] {
113
+ left: var(--sticky-frontier-preview-position, 0px);
114
+ }
115
+
116
+ .navi_table_sticky_frontier_ghost[data-top],
117
+ .navi_table_sticky_frontier_preview[data-top] {
118
+ left: 0;
119
+ width: var(--table-width, 100%);
120
+ height: var(--sticky-frontier-ghost-size);
121
+ }
122
+
123
+ .navi_table_sticky_frontier_ghost[data-top] {
124
+ top: var(--sticky-frontier-ghost-position, 0px);
125
+ }
126
+ .navi_table_sticky_frontier_preview[data-top] {
127
+ top: var(--sticky-frontier-preview-position, 0px);
128
+ }
129
+
130
+ /* Positioning adjustments for ::after pseudo-elements on cells adjacent to sticky frontiers */
131
+ /* These ensure selection and focus borders align with the ::before borders */
132
+ .navi_table[data-border-collapse]
133
+ .navi_table_cell[data-after-sticky-left-frontier]::after {
134
+ left: 0;
135
+ }
136
+
137
+ .navi_table[data-border-collapse]
138
+ .navi_table_cell[data-after-sticky-top-frontier]::after {
139
+ top: 0;
140
+ }
141
+
142
+ /* Base borders for sticky cells (will be overridden by frontier rules) */
143
+ .navi_table[data-border-collapse] .navi_table_cell[data-sticky-left]::before {
144
+ box-shadow:
145
+ inset -1px 0 0 0 var(--border-color),
146
+ inset 0 -1px 0 0 var(--border-color);
147
+ }
148
+
149
+ .navi_table[data-border-collapse] .navi_table_cell[data-sticky-top]::before {
150
+ box-shadow:
151
+ inset -1px 0 0 0 var(--border-color),
152
+ inset 0 -1px 0 0 var(--border-color);
153
+ }
154
+
155
+ /* Header row sticky cells need top border */
156
+ .navi_table[data-border-collapse]
157
+ .navi_table_cell[data-first-row][data-sticky-left]::before {
158
+ box-shadow:
159
+ inset 0 1px 0 0 var(--border-color),
160
+ inset -1px 0 0 0 var(--border-color),
161
+ inset 0 -1px 0 0 var(--border-color);
162
+ }
163
+
164
+ .navi_table[data-border-collapse]
165
+ .navi_table_cell[data-first-row][data-sticky-top]::before {
166
+ box-shadow:
167
+ inset 0 1px 0 0 var(--border-color),
168
+ inset -1px 0 0 0 var(--border-color),
169
+ inset 0 -1px 0 0 var(--border-color);
170
+ }
171
+
172
+ /* First column sticky cells need left border */
173
+ .navi_table[data-border-collapse]
174
+ .navi_table_cell[data-first-column][data-sticky-left]::before {
175
+ box-shadow:
176
+ inset 1px 0 0 0 var(--border-color),
177
+ inset -1px 0 0 0 var(--border-color),
178
+ inset 0 -1px 0 0 var(--border-color);
179
+ }
180
+
181
+ .navi_table[data-border-collapse]
182
+ .navi_table_cell[data-first-column][data-sticky-top]::before {
183
+ box-shadow:
184
+ inset 1px 0 0 0 var(--border-color),
185
+ inset -1px 0 0 0 var(--border-color),
186
+ inset 0 -1px 0 0 var(--border-color);
187
+ }
188
+
189
+ /* Header first column sticky cells get all four regular borders */
190
+ .navi_table[data-border-collapse]
191
+ .navi_table_cell[data-first-row][data-first-column][data-sticky-left]::before,
192
+ .navi_table[data-border-collapse]
193
+ .navi_table_cell[data-first-row][data-first-column][data-sticky-top]::before {
194
+ box-shadow:
195
+ inset 0 1px 0 0 var(--border-color),
196
+ inset 1px 0 0 0 var(--border-color),
197
+ inset -1px 0 0 0 var(--border-color),
198
+ inset 0 -1px 0 0 var(--border-color);
199
+ }
200
+
201
+ /* Borders for cells immediately after sticky frontiers */
202
+
203
+ /* Left border for the column after sticky-x-frontier */
204
+ .navi_table[data-border-collapse]
205
+ .navi_table_cell[data-after-sticky-left-frontier]::before {
206
+ box-shadow:
207
+ inset 1px 0 0 0 var(--border-color),
208
+ inset -1px 0 0 0 var(--border-color),
209
+ inset 0 -1px 0 0 var(--border-color);
210
+ }
211
+
212
+ /* Header cells after sticky-x-frontier also need top border (for border-collapse) */
213
+ .navi_table[data-border-collapse]
214
+ .navi_table_cell[data-first-row][data-after-sticky-left-frontier]::before {
215
+ box-shadow:
216
+ inset 0 1px 0 0 var(--border-color),
217
+ inset 1px 0 0 0 var(--border-color),
218
+ inset -1px 0 0 0 var(--border-color),
219
+ inset 0 -1px 0 0 var(--border-color);
220
+ }
221
+
222
+ /* Top border for the row after sticky-y-frontier */
223
+ .navi_table[data-border-collapse]
224
+ .navi_table_cell[data-after-sticky-top-frontier]::before {
225
+ box-shadow:
226
+ inset 0 1px 0 0 var(--border-color),
227
+ inset -1px 0 0 0 var(--border-color),
228
+ inset 0 -1px 0 0 var(--border-color);
229
+ }
230
+
231
+ /* Header cells after sticky-y-frontier also need left border (for border-collapse) */
232
+ .navi_table[data-border-collapse]
233
+ .navi_table_cell[data-first-row][data-after-sticky-top-frontier]::before {
234
+ box-shadow:
235
+ inset 0 1px 0 0 var(--border-color),
236
+ inset 1px 0 0 0 var(--border-color),
237
+ inset -1px 0 0 0 var(--border-color),
238
+ inset 0 -1px 0 0 var(--border-color);
239
+ }
240
+
241
+ /* First column cells after sticky-y-frontier need all four borders (for border-collapse) */
242
+ .navi_table[data-border-collapse]
243
+ .navi_table_cell[data-first-column][data-after-sticky-top-frontier]::before {
244
+ box-shadow:
245
+ inset 0 1px 0 0 var(--border-color),
246
+ inset 1px 0 0 0 var(--border-color),
247
+ inset -1px 0 0 0 var(--border-color),
248
+ inset 0 -1px 0 0 var(--border-color);
249
+ }
250
+
251
+ /* Corner case: cell after both sticky frontiers */
252
+ .navi_table[data-border-collapse]
253
+ .navi_table_cell[data-after-sticky-left-frontier][data-after-sticky-top-frontier]::before {
254
+ box-shadow:
255
+ inset 1px 0 0 0 var(--border-color),
256
+ inset 0 1px 0 0 var(--border-color),
257
+ inset -1px 0 0 0 var(--border-color),
258
+ inset 0 -1px 0 0 var(--border-color);
259
+ }
260
+ `;
261
+
262
+ export const TableStickyFrontier = ({ tableRef }) => {
263
+ const stickyLeftFrontierGhostRef = useRef();
264
+ const stickyLeftFrontierPreviewRef = useRef();
265
+ const stickyTopFrontierGhostRef = useRef();
266
+ const stickyTopFrontierPreviewRef = useRef();
267
+
268
+ return (
269
+ <>
270
+ <TableStickyLeftFrontier
271
+ tableRef={tableRef}
272
+ stickyLeftFrontierGhostRef={stickyLeftFrontierGhostRef}
273
+ stickyLeftFrontierPreviewRef={stickyLeftFrontierPreviewRef}
274
+ />
275
+ <TableStickyTopFrontier
276
+ tableRef={tableRef}
277
+ stickyTopFrontierGhostRef={stickyTopFrontierGhostRef}
278
+ stickyTopFrontierPreviewRef={stickyTopFrontierPreviewRef}
279
+ />
280
+ <div
281
+ ref={stickyLeftFrontierGhostRef}
282
+ className="navi_table_sticky_frontier_ghost"
283
+ data-left=""
284
+ ></div>
285
+ <div
286
+ ref={stickyLeftFrontierPreviewRef}
287
+ className="navi_table_sticky_frontier_preview"
288
+ data-left=""
289
+ ></div>
290
+ <div
291
+ ref={stickyTopFrontierGhostRef}
292
+ className="navi_table_sticky_frontier_ghost"
293
+ data-top=""
294
+ ></div>
295
+ <div
296
+ ref={stickyTopFrontierPreviewRef}
297
+ className="navi_table_sticky_frontier_preview"
298
+ data-top=""
299
+ ></div>
300
+ </>
301
+ );
302
+ };
303
+
304
+ const TableStickyLeftFrontier = ({
305
+ tableRef,
306
+ stickyLeftFrontierGhostRef,
307
+ stickyLeftFrontierPreviewRef,
308
+ }) => {
309
+ const { stickyLeftFrontierColumnIndex, onStickyLeftFrontierChange } =
310
+ useContext(TableStickyContext);
311
+ const canMoveFrontier = Boolean(onStickyLeftFrontierChange);
312
+
313
+ return (
314
+ <div
315
+ className="navi_table_sticky_frontier"
316
+ data-left=""
317
+ inert={!canMoveFrontier}
318
+ onPointerDown={(e) => {
319
+ if (e.button !== 0) {
320
+ return;
321
+ }
322
+ e.preventDefault(); // prevent text selection
323
+ e.stopPropagation(); // prevent drag column
324
+
325
+ const table = tableRef.current;
326
+ const stickyLeftFrontierGhost = stickyLeftFrontierGhostRef.current;
327
+ const stickyLeftFrontierPreview = stickyLeftFrontierPreviewRef.current;
328
+ const colgroup = table.querySelector("colgroup");
329
+ const colElements = Array.from(colgroup.children);
330
+ initMoveStickyFrontierViaPointer(e, {
331
+ table,
332
+ frontierGhost: stickyLeftFrontierGhost,
333
+ frontierPreview: stickyLeftFrontierPreview,
334
+ elements: colElements,
335
+ frontierIndex: stickyLeftFrontierColumnIndex,
336
+ axis: "x",
337
+ onRelease: (_, index) => {
338
+ if (index !== stickyLeftFrontierColumnIndex) {
339
+ onStickyLeftFrontierChange(index);
340
+ }
341
+ },
342
+ });
343
+ }}
344
+ ></div>
345
+ );
346
+ };
347
+ const TableStickyTopFrontier = ({
348
+ tableRef,
349
+ stickyTopFrontierGhostRef,
350
+ stickyTopFrontierPreviewRef,
351
+ }) => {
352
+ const { stickyTopFrontierRowIndex, onStickyTopFrontierChange } =
353
+ useContext(TableStickyContext);
354
+ const canMoveFrontier = Boolean(onStickyTopFrontierChange);
355
+
356
+ return (
357
+ <div
358
+ className="navi_table_sticky_frontier"
359
+ data-top=""
360
+ inert={!canMoveFrontier}
361
+ onPointerDown={(e) => {
362
+ if (e.button !== 0) {
363
+ return;
364
+ }
365
+ e.preventDefault(); // prevent text selection
366
+ e.stopPropagation(); // prevent drag column
367
+
368
+ const table = tableRef.current;
369
+ const rowElements = Array.from(table.querySelectorAll("tr"));
370
+ initMoveStickyFrontierViaPointer(e, {
371
+ table,
372
+ frontierGhost: stickyTopFrontierGhostRef.current,
373
+ frontierPreview: stickyTopFrontierPreviewRef.current,
374
+ elements: rowElements,
375
+ frontierIndex: stickyTopFrontierRowIndex,
376
+ axis: "y",
377
+ onRelease: (_, index) => {
378
+ if (index !== stickyTopFrontierRowIndex) {
379
+ onStickyTopFrontierChange(index);
380
+ }
381
+ },
382
+ });
383
+ }}
384
+ ></div>
385
+ );
386
+ };
387
+
388
+ // Generic function to handle sticky frontier movement for both axes
389
+ const initMoveStickyFrontierViaPointer = (
390
+ pointerdownEvent,
391
+ {
392
+ table,
393
+ frontierGhost,
394
+ frontierPreview,
395
+ frontierIndex,
396
+ onGrab,
397
+ onDrag,
398
+ onRelease,
399
+ // Axis-specific configuration
400
+ axis, // 'x' or 'y'
401
+ elements, // array of colElements or rowElements
402
+ },
403
+ ) => {
404
+ // Get elements based on axis
405
+ const gestureName =
406
+ axis === "x" ? "move-sticky-left-frontier" : "move-sticky-top-frontier";
407
+ const scrollProperty = axis === "x" ? "scrollLeft" : "scrollTop";
408
+ const ghostVariableName = "--sticky-frontier-ghost-position";
409
+ const previewVariableName = "--sticky-frontier-preview-position";
410
+ const ghostElement = frontierGhost;
411
+ const previewElement = frontierPreview;
412
+ const scrollContainer = getScrollContainer(table);
413
+ // Reset scroll to prevent starting drag in obstacle position
414
+ scrollContainer[scrollProperty] = 0;
415
+
416
+ // Setup table dimensions for ghost/preview
417
+ const ghostOffsetParent = ghostElement.offsetParent;
418
+ const ghostOffsetParentRect = ghostOffsetParent.getBoundingClientRect();
419
+
420
+ const getPosition = (elementRect) => {
421
+ if (axis === "x") {
422
+ const elementLeftRelative = elementRect.left - ghostOffsetParentRect.left;
423
+ return elementLeftRelative + elementRect.width;
424
+ }
425
+ const elementTopRelative = elementRect.top - ghostOffsetParentRect.top;
426
+ return elementTopRelative + elementRect.height;
427
+ };
428
+
429
+ // Setup initial ghost position
430
+ if (frontierIndex === -1) {
431
+ ghostElement.style.setProperty(ghostVariableName, "0px");
432
+ } else {
433
+ const element = elements[frontierIndex];
434
+ const elementRect = element.getBoundingClientRect();
435
+ const position = getPosition(elementRect);
436
+ ghostElement.style.setProperty(ghostVariableName, `${position}px`);
437
+ }
438
+ ghostElement.setAttribute("data-visible", "");
439
+
440
+ let futureFrontierIndex = frontierIndex;
441
+ const onFutureFrontierIndexChange = (index) => {
442
+ futureFrontierIndex = index;
443
+
444
+ // We maintain a visible preview of the frontier
445
+ // to tell user "hey if you grab here, the frontier will be there"
446
+ // At this stage user can see 3 frontiers. Where it is, the one he grab, the future one if he releases.
447
+ if (index === frontierIndex) {
448
+ previewElement.removeAttribute("data-visible");
449
+ return;
450
+ }
451
+ let previewPosition;
452
+ if (index === -1) {
453
+ previewPosition = 0;
454
+ } else {
455
+ const element = elements[index];
456
+ const elementRect = element.getBoundingClientRect();
457
+ previewPosition = getPosition(elementRect);
458
+ }
459
+ previewElement.style.setProperty(
460
+ previewVariableName,
461
+ `${previewPosition}px`,
462
+ );
463
+ previewElement.setAttribute("data-visible", "");
464
+ };
465
+
466
+ const moveFrontierGestureController = createDragToMoveGestureController({
467
+ name: gestureName,
468
+ direction: { [axis]: true },
469
+ backdropZIndex: Z_INDEX_STICKY_FRONTIER_BACKDROP,
470
+ areaConstraint: "visible",
471
+ areaConstraintElement: table.closest(".navi_table_root"),
472
+
473
+ onGrab,
474
+ onDrag: (gestureInfo) => {
475
+ const dropTargetInfo = getDropTargetInfo(gestureInfo, elements);
476
+ if (dropTargetInfo) {
477
+ const dropColumnIndex = dropTargetInfo.index;
478
+ const dropFrontierIndex =
479
+ dropTargetInfo.elementSide[axis] === "start"
480
+ ? dropColumnIndex - 1
481
+ : dropColumnIndex;
482
+ if (dropFrontierIndex !== futureFrontierIndex) {
483
+ onFutureFrontierIndexChange(dropFrontierIndex);
484
+ }
485
+ }
486
+ onDrag?.(gestureInfo, futureFrontierIndex);
487
+ },
488
+ onRelease: (gesture) => {
489
+ previewElement.removeAttribute("data-visible");
490
+ previewElement.style.removeProperty(previewVariableName);
491
+ ghostElement.removeAttribute("data-visible");
492
+ ghostElement.style.removeProperty(ghostVariableName);
493
+ ghostElement.style[axis === "x" ? "left" : "top"] = ""; // reset position set by drag
494
+
495
+ onRelease?.(gesture, futureFrontierIndex);
496
+ },
497
+ });
498
+ moveFrontierGestureController.grabViaPointer(pointerdownEvent, {
499
+ element: ghostElement,
500
+ });
501
+ };