@astryxdesign/core 0.7.7-canary.f9ac85f → 0.7.7

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 (198) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/AppShell/useCloseMobileDrawerOnActivate.d.ts +13 -0
  3. package/dist/AppShell/useCloseMobileDrawerOnActivate.d.ts.map +1 -0
  4. package/dist/AppShell/useCloseMobileDrawerOnActivate.js +45 -0
  5. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  6. package/dist/BottomSheet/BottomSheet.js +79 -9
  7. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  8. package/dist/BottomSheet/BottomSheetPanel.js +1 -1
  9. package/dist/BottomSheet/bottomSheetStack.d.ts.map +1 -1
  10. package/dist/BottomSheet/bottomSheetStack.js +5 -4
  11. package/dist/Breadcrumbs/BreadcrumbItem.d.ts +2 -1
  12. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  13. package/dist/Breadcrumbs/BreadcrumbItem.js +7 -3
  14. package/dist/Button/Button.d.ts.map +1 -1
  15. package/dist/Button/Button.js +30 -2
  16. package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
  17. package/dist/CheckboxList/CheckboxList.js +7 -1
  18. package/dist/CheckboxList/CheckboxListContext.d.ts +6 -0
  19. package/dist/CheckboxList/CheckboxListContext.d.ts.map +1 -1
  20. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  21. package/dist/CheckboxList/CheckboxListItem.js +10 -2
  22. package/dist/DateInput/DateInput.d.ts.map +1 -1
  23. package/dist/DateInput/DateInput.js +5 -2
  24. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  25. package/dist/DateInput/MonthScroller.js +13 -27
  26. package/dist/DateInput/Wheel.d.ts.map +1 -1
  27. package/dist/DateInput/Wheel.js +22 -41
  28. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  29. package/dist/DateTimeInput/DateTimeInput.js +5 -2
  30. package/dist/Dialog/Dialog.d.ts.map +1 -1
  31. package/dist/Dialog/Dialog.js +4 -3
  32. package/dist/Drawer/Drawer.js +0 -1
  33. package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -1
  34. package/dist/Drawer/useDrawerDialogPresence.js +5 -1
  35. package/dist/Field/PanelSearchInput.d.ts +9 -1
  36. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  37. package/dist/Field/PanelSearchInput.js +3 -1
  38. package/dist/InfoTip/InfoTip.d.ts +2 -2
  39. package/dist/InfoTip/InfoTip.js +4 -4
  40. package/dist/Layer/layerAnimations.stylex.d.ts +7 -8
  41. package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
  42. package/dist/Layer/layerAnimations.stylex.js +25 -20
  43. package/dist/Layer/usePressPin.d.ts +3 -5
  44. package/dist/Layer/usePressPin.d.ts.map +1 -1
  45. package/dist/Layer/usePressPin.js +4 -6
  46. package/dist/MobileNav/MobileNav.js +0 -1
  47. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  48. package/dist/MultiSelector/hooks.js +2 -4
  49. package/dist/Outline/Outline.d.ts +2 -4
  50. package/dist/Outline/Outline.d.ts.map +1 -1
  51. package/dist/Outline/Outline.js +4 -52
  52. package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
  53. package/dist/Outline/useOutlineFromMarkdown.js +1 -0
  54. package/dist/Outline/useScrollSpy.js +3 -3
  55. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  56. package/dist/PowerSearch/PowerSearch.js +134 -179
  57. package/dist/RadioList/RadioList.d.ts.map +1 -1
  58. package/dist/RadioList/RadioList.js +5 -0
  59. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  60. package/dist/Resizable/ResizeHandle.js +118 -260
  61. package/dist/Resizable/resizeGesture.d.ts +91 -0
  62. package/dist/Resizable/resizeGesture.d.ts.map +1 -0
  63. package/dist/Resizable/resizeGesture.js +251 -0
  64. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  65. package/dist/SideNav/SideNavItem.js +3 -9
  66. package/dist/Token/Token.d.ts +2 -2
  67. package/dist/Token/Token.d.ts.map +1 -1
  68. package/dist/Token/Token.js +12 -4
  69. package/dist/Token/TokenNamingContext.d.ts +14 -0
  70. package/dist/Token/TokenNamingContext.d.ts.map +1 -0
  71. package/dist/Token/TokenNamingContext.js +23 -0
  72. package/dist/Tokenizer/Tokenizer.d.ts +46 -59
  73. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  74. package/dist/Tokenizer/Tokenizer.js +35 -58
  75. package/dist/Tokenizer/TokenizerBottomSheet.d.ts +25 -28
  76. package/dist/Tokenizer/TokenizerBottomSheet.d.ts.map +1 -1
  77. package/dist/Tokenizer/TokenizerBottomSheet.js +157 -54
  78. package/dist/Tokenizer/TokenizerItemRemovableContext.d.ts.map +1 -1
  79. package/dist/Tokenizer/TokenizerItemRemovableContext.js +5 -3
  80. package/dist/Tooltip/Tooltip.d.ts +25 -30
  81. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  82. package/dist/Tooltip/Tooltip.js +24 -5
  83. package/dist/Tooltip/index.d.ts +1 -1
  84. package/dist/Tooltip/index.d.ts.map +1 -1
  85. package/dist/Tooltip/useTooltip.d.ts +28 -29
  86. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  87. package/dist/Tooltip/useTooltip.js +20 -8
  88. package/dist/TopNav/TopNavItem.d.ts.map +1 -1
  89. package/dist/TopNav/TopNavItem.js +4 -7
  90. package/dist/TopNav/TopNavMegaMenuItem.d.ts.map +1 -1
  91. package/dist/TopNav/TopNavMegaMenuItem.js +4 -7
  92. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  93. package/dist/TopNav/TopNavMenu.js +4 -7
  94. package/dist/astryx.css +5 -5
  95. package/dist/gestures/GestureHost.d.ts +27 -1
  96. package/dist/gestures/GestureHost.d.ts.map +1 -1
  97. package/dist/gestures/GestureHost.js +90 -8
  98. package/dist/hooks/useHighlightedOptionScroll.d.ts +11 -1
  99. package/dist/hooks/useHighlightedOptionScroll.d.ts.map +1 -1
  100. package/dist/hooks/useHighlightedOptionScroll.js +103 -5
  101. package/dist/hooks/useMenuPress.d.ts.map +1 -1
  102. package/dist/hooks/useMenuPress.js +4 -2
  103. package/dist/i18n/generated-english/PowerSearch/PowerSearch.generated.d.ts.map +1 -1
  104. package/dist/i18n/generated-english/PowerSearch/PowerSearch.generated.js +1 -0
  105. package/dist/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.d.ts.map +1 -1
  106. package/dist/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.js +7 -1
  107. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  108. package/dist/i18n/generated-locales/en.generated.js +1 -0
  109. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  110. package/dist/i18n/generated-locales/pseudo.generated.js +1 -0
  111. package/dist/internal/SheetConfirmation.d.ts +11 -0
  112. package/dist/internal/SheetConfirmation.d.ts.map +1 -0
  113. package/dist/internal/SheetConfirmation.js +92 -0
  114. package/dist/internal/sheetDraft.d.ts +35 -0
  115. package/dist/internal/sheetDraft.d.ts.map +1 -0
  116. package/dist/internal/sheetDraft.js +65 -0
  117. package/dist/internal/touch-sheet.d.ts +21 -0
  118. package/dist/internal/touch-sheet.d.ts.map +1 -0
  119. package/dist/internal/touch-sheet.js +19 -0
  120. package/dist/internal/useDismissSearchOnTouchDrag.d.ts +24 -0
  121. package/dist/internal/useDismissSearchOnTouchDrag.d.ts.map +1 -0
  122. package/dist/internal/useDismissSearchOnTouchDrag.js +56 -0
  123. package/dist/motion/reducedMotion.d.ts +24 -0
  124. package/dist/motion/reducedMotion.d.ts.map +1 -1
  125. package/dist/motion/reducedMotion.js +78 -1
  126. package/dist/utils/clickGuard.d.ts +8 -1
  127. package/dist/utils/clickGuard.d.ts.map +1 -1
  128. package/dist/utils/clickGuard.js +24 -1
  129. package/locales/en.json +4 -0
  130. package/locales/pseudo.json +3 -0
  131. package/package.json +9 -9
  132. package/src/AppShell/useCloseMobileDrawerOnActivate.ts +52 -0
  133. package/src/BottomSheet/BottomSheet.tsx +95 -10
  134. package/src/BottomSheet/BottomSheetPanel.tsx +7 -2
  135. package/src/BottomSheet/bottomSheetStack.ts +8 -4
  136. package/src/Breadcrumbs/BreadcrumbItem.tsx +13 -3
  137. package/src/Button/Button.tsx +37 -1
  138. package/src/CheckboxList/CheckboxList.doc.mjs +4 -2
  139. package/src/CheckboxList/CheckboxList.tsx +6 -0
  140. package/src/CheckboxList/CheckboxListContext.tsx +6 -0
  141. package/src/CheckboxList/CheckboxListItem.tsx +11 -2
  142. package/src/DateInput/DateInput.tsx +8 -2
  143. package/src/DateInput/MonthScroller.tsx +18 -22
  144. package/src/DateInput/Wheel.tsx +30 -32
  145. package/src/DateTimeInput/DateTimeInput.tsx +8 -2
  146. package/src/Dialog/Dialog.tsx +19 -4
  147. package/src/Drawer/Drawer.tsx +1 -1
  148. package/src/Drawer/useDrawerDialogPresence.ts +5 -1
  149. package/src/Field/PanelSearchInput.tsx +11 -0
  150. package/src/InfoTip/InfoTip.doc.mjs +3 -3
  151. package/src/InfoTip/InfoTip.tsx +5 -5
  152. package/src/Layer/layerAnimations.stylex.ts +24 -16
  153. package/src/Layer/usePressPin.ts +5 -11
  154. package/src/MobileNav/MobileNav.tsx +1 -1
  155. package/src/MultiSelector/hooks.ts +2 -4
  156. package/src/Outline/Outline.doc.mjs +177 -51
  157. package/src/Outline/Outline.tsx +5 -57
  158. package/src/Outline/useOutlineFromMarkdown.doc.mjs +157 -0
  159. package/src/Outline/useOutlineFromMarkdown.ts +1 -0
  160. package/src/Outline/useScrollSpy.ts +3 -3
  161. package/src/PowerSearch/PowerSearch.doc.mjs +3 -3
  162. package/src/PowerSearch/PowerSearch.tsx +196 -183
  163. package/src/RadioList/RadioList.doc.mjs +4 -2
  164. package/src/RadioList/RadioList.tsx +4 -0
  165. package/src/Resizable/ResizeHandle.tsx +133 -328
  166. package/src/Resizable/resizeGesture.ts +368 -0
  167. package/src/SideNav/SideNavItem.tsx +3 -7
  168. package/src/Token/Token.tsx +13 -4
  169. package/src/Token/TokenNamingContext.ts +28 -0
  170. package/src/Tokenizer/Tokenizer.doc.mjs +16 -0
  171. package/src/Tokenizer/Tokenizer.tsx +81 -86
  172. package/src/Tokenizer/TokenizerBottomSheet.tsx +219 -93
  173. package/src/Tokenizer/TokenizerItemRemovableContext.ts +5 -3
  174. package/src/Tooltip/Tooltip.doc.mjs +123 -54
  175. package/src/Tooltip/Tooltip.tsx +36 -39
  176. package/src/Tooltip/index.ts +0 -1
  177. package/src/Tooltip/useTooltip.doc.mjs +147 -33
  178. package/src/Tooltip/useTooltip.tsx +39 -32
  179. package/src/TopNav/TopNavItem.tsx +6 -5
  180. package/src/TopNav/TopNavMegaMenuItem.tsx +6 -5
  181. package/src/TopNav/TopNavMenu.tsx +6 -5
  182. package/src/gestures/GestureHost.ts +105 -10
  183. package/src/hooks/useHighlightedOptionScroll.ts +113 -9
  184. package/src/hooks/useMenuPress.ts +8 -2
  185. package/src/i18n/generated-english/PowerSearch/PowerSearch.generated.ts +1 -0
  186. package/src/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.ts +7 -1
  187. package/src/i18n/generated-locales/en.generated.ts +1 -0
  188. package/src/i18n/generated-locales/pseudo.generated.ts +1 -0
  189. package/src/internal/SheetConfirmation.tsx +101 -0
  190. package/src/internal/sheetDraft.ts +80 -0
  191. package/src/internal/touch-sheet.ts +21 -0
  192. package/src/internal/useDismissSearchOnTouchDrag.ts +69 -0
  193. package/src/motion/reducedMotion.ts +79 -1
  194. package/src/utils/clickGuard.ts +37 -1
  195. package/dist/internal/tokenizer-presentation.d.ts +0 -21
  196. package/dist/internal/tokenizer-presentation.d.ts.map +0 -1
  197. package/dist/internal/tokenizer-presentation.js +0 -21
  198. package/src/internal/tokenizer-presentation.ts +0 -25
@@ -22,21 +22,17 @@
22
22
  * (a value below the minimum is invalid per WCAG 4.1.2) and a localized
23
23
  * "Collapsed" aria-valuetext announces the real state.
24
24
  *
25
- * Dragging takes pointer capture on the grab zone, so the rest of the gesture
26
- * is delivered to that element instead of being hit-tested against whatever
27
- * the cursor happens to be over. A resizable region that contains an embedded
28
- * frame otherwise loses the drag the moment the cursor crosses the frame: the
29
- * events go to the guest document, and a listener on the host window never
30
- * hears them again. Captured events are delivered to the capturing element,
31
- * so the move/up/cancel handlers live on the grab zone (as in Slider), not on
32
- * window.
25
+ * The handle's input (the pointer drag, the keys, the double-click) is
26
+ * resizeGesture.ts. ResizeHandle forwards its React events to it, over the
27
+ * region its `resizable` prop describes. A drag takes pointer capture on the grab zone.
28
+ * The gesture's move, up and cancel events then go there, as in Slider, even
29
+ * over an embedded frame.
33
30
  */
34
31
 
35
- import {useCallback, useEffect, useRef, useState} from 'react';
32
+ import {useEffect, useRef, useState} from 'react';
36
33
  import type {ReactNode} from 'react';
37
34
  import * as stylex from '@stylexjs/stylex';
38
35
  import type {BaseProps} from '../BaseProps';
39
- import {devWarn} from '../utils/devWarning';
40
36
  import {
41
37
  colorVars,
42
38
  durationVars,
@@ -51,8 +47,14 @@ import {
51
47
  getEffectiveMaxPx,
52
48
  type ResizableProps,
53
49
  } from './useResizable';
50
+ import {
51
+ createResizeGesture,
52
+ type ResizeGesture,
53
+ type ResizeGestureModel,
54
+ } from './resizeGesture';
54
55
  import {themeProps} from '../utils/themeProps';
55
56
 
57
+ import {useDevWarning} from '../hooks/useDevWarning';
56
58
  import {useMergedRefs} from '../hooks/useMergedRefs';
57
59
  import {
58
60
  registerEnglishFallback,
@@ -61,8 +63,6 @@ import {
61
63
  import english from '../i18n/generated-english/Resizable/ResizeHandle.generated';
62
64
 
63
65
  registerEnglishFallback(english);
64
- const KEYBOARD_STEP = 10;
65
- const KEYBOARD_LARGE_STEP = 50;
66
66
 
67
67
  type PillPlacement = 'start' | 'end' | 'center' | 'auto';
68
68
 
@@ -105,21 +105,6 @@ function hitAreaBiasDir(
105
105
  return effectiveSide === 'start' ? -1 : 1;
106
106
  }
107
107
 
108
- /**
109
- * Drag feedback lives on `<body>`, not on the handle: the pointer spends the
110
- * drag off the handle, over whatever the panels contain, and the resize cursor
111
- * has to hold there. Module scope so the writes stay outside the component.
112
- */
113
- function applyBodyDragStyles(isHorizontal: boolean): void {
114
- document.body.style.cursor = isHorizontal ? 'col-resize' : 'row-resize';
115
- document.body.style.userSelect = 'none';
116
- }
117
-
118
- function clearBodyDragStyles(): void {
119
- document.body.style.cursor = '';
120
- document.body.style.userSelect = '';
121
- }
122
-
123
108
  const styles = stylex.create({
124
109
  handle: {
125
110
  position: 'relative',
@@ -370,60 +355,6 @@ export interface ResizeHandleProps extends Omit<
370
355
  children?: ReactNode;
371
356
  }
372
357
 
373
- /**
374
- * The snap point `count` points away from the region's current size in
375
- * `direction` (+1 grows, -1 shrinks). A point beyond `minSize` or `maxSize`
376
- * counts as that bound; the nearest end point serves when fewer remain, and
377
- * the current size when it is already there.
378
- */
379
- function adjacentSnapPoint(
380
- resizable: ResizableProps,
381
- direction: 1 | -1,
382
- count: number,
383
- ): number {
384
- const points = [
385
- ...new Set(
386
- resizable._snaps.map(p =>
387
- Math.min(resizable._maxSizePx, Math.max(resizable._minSizePx, p)),
388
- ),
389
- ),
390
- ].sort((a, b) => a - b);
391
- const size = resizable._size;
392
- const ahead =
393
- direction > 0
394
- ? points.filter(p => p > size)
395
- : points.filter(p => p < size).reverse();
396
- if (ahead.length === 0) {
397
- return size;
398
- }
399
- return ahead[Math.min(count, ahead.length) - 1];
400
- }
401
-
402
- /**
403
- * Collapse an open pane, or restore a collapsed one to the size it had before
404
- * it collapsed (APG window splitter: Enter "restores the splitter to its
405
- * previous position"). The hook's own collapse/expand do exactly that; a
406
- * hand-built `ResizableProps` (no registered controls) falls back to a resize
407
- * gesture to the minimum, which also cannot reopen a pane whose minimum sits below its
408
- * collapse threshold.
409
- */
410
- function toggleCollapse(resizable: ResizableProps): void {
411
- const controls = getCollapseControls(resizable);
412
- if (controls) {
413
- if (resizable._isCollapsed) {
414
- controls.expand();
415
- } else {
416
- controls.collapse();
417
- }
418
- return;
419
- }
420
- resizable._onResizeStart();
421
- resizable._onResizeMove(
422
- resizable._isCollapsed ? resizable._minSizePx : -resizable._size,
423
- );
424
- resizable._onResizeEnd();
425
- }
426
-
427
358
  /**
428
359
  * Draggable resize handle placed between resizable panels. Renders as a thin
429
360
  * divider line with a wider invisible hit area and optional pill grip indicator.
@@ -459,271 +390,133 @@ export function ResizeHandle({
459
390
  const t = useComponentTranslator();
460
391
  const label = labelFromProps ?? t('@astryx.resizable.handle.label');
461
392
  const handleRef = useRef<HTMLDivElement>(null);
462
- // The pointer that owns the in-flight drag, plus the values its deltas are
463
- // measured against. A ref so moves don't re-render, and so unmount can tell
464
- // a live drag from a finished one.
465
- const dragRef = useRef<{
466
- pointerId: number;
467
- startPos: number;
468
- rtl: number;
469
- } | null>(null);
470
- const [isDragging, setIsDragging] = useState(false);
471
393
  const [isHovered, setIsHovered] = useState(false);
472
394
  const [isFocused, setIsFocused] = useState(false);
473
395
  const isHorizontal = direction === 'horizontal';
474
396
  const isOverlay = positionMode === 'overlay';
475
- const sign = isReversed ? -1 : 1;
397
+
398
+ // --- The region a `resizable` prop describes ---
399
+
400
+ // The gesture reads the latest props at each input. A drag sees the region
401
+ // as it is now, not as it was at the press.
402
+ const resizableRef = useRef(resizable);
403
+ resizableRef.current = resizable;
404
+ // The region a drag in flight started on: told the gesture is over even
405
+ // after the prop has gone.
406
+ const dragRegionRef = useRef<ResizableProps | undefined>(undefined);
407
+ const configRef = useRef({direction, isReversed, isDisabled});
408
+ configRef.current = {direction, isReversed, isDisabled};
409
+ const [isDragging, setIsDragging] = useState(false);
410
+ const [regionGesture] = useState<ResizeGesture>(() => {
411
+ const model: ResizeGestureModel = {
412
+ state() {
413
+ const region = resizableRef.current;
414
+ return {
415
+ size: region?._size ?? 0,
416
+ isCollapsed: region?._isCollapsed ?? false,
417
+ minPx: region?._minSizePx ?? 0,
418
+ maxPx: region?._maxSizePx ?? Infinity,
419
+ snaps: region?._snaps ?? [],
420
+ collapsedSize: region?._collapsedSize ?? 0,
421
+ collapsible: region?._collapsible ?? false,
422
+ };
423
+ },
424
+ start: () => {
425
+ dragRegionRef.current = resizableRef.current;
426
+ resizableRef.current?._onResizeStart();
427
+ },
428
+ move: delta => resizableRef.current?._onResizeMove(delta),
429
+ end: () => {
430
+ const region = resizableRef.current ?? dragRegionRef.current;
431
+ dragRegionRef.current = undefined;
432
+ region?._onResizeEnd();
433
+ },
434
+ // Not a resize end, but the gesture is over. The region froze its state
435
+ // for the drag, so it must hear that the drag ended.
436
+ cancel: () => {
437
+ const region = resizableRef.current ?? dragRegionRef.current;
438
+ dragRegionRef.current = undefined;
439
+ region?._onResizeCancel?.();
440
+ },
441
+ // The hook's own collapse and expand restore the size before the
442
+ // collapse; a hand-built `ResizableProps` has none.
443
+ get collapse() {
444
+ const region = resizableRef.current;
445
+ return region ? getCollapseControls(region)?.collapse : undefined;
446
+ },
447
+ get expand() {
448
+ const region = resizableRef.current;
449
+ return region ? getCollapseControls(region)?.expand : undefined;
450
+ },
451
+ };
452
+ return createResizeGesture(
453
+ () => handleRef.current,
454
+ model,
455
+ () => ({
456
+ ...configRef.current,
457
+ isDisabled:
458
+ configRef.current.isDisabled || resizableRef.current == null,
459
+ }),
460
+ setIsDragging,
461
+ );
462
+ });
463
+
464
+ const gesture = resizable != null ? regionGesture : null;
465
+ const isCollapsed = resizable?._isCollapsed ?? false;
476
466
  const effectiveSide = resolveEffectiveSide(
477
467
  pillPlacement,
478
468
  isReversed,
479
- resizable?._isCollapsed ?? false,
469
+ isCollapsed,
480
470
  );
481
471
  // Physical offset direction shared by the pill and the grab zone (null =
482
472
  // centered / no bias). Keeps the two positioned identically in LTR and RTL.
483
473
  const hitBiasDir = hitAreaBiasDir(effectiveSide);
484
-
485
- const getRTLMultiplier = useCallback((): number => {
486
- const el = handleRef.current;
487
- if (!el) {
488
- return 1;
489
- }
490
- return getComputedStyle(el).direction === 'rtl' ? -1 : 1;
491
- }, []);
492
-
493
474
  const isInteracting = isHovered || isFocused;
494
475
 
495
- // A handle whose axis disagrees with its hook drags along one axis while the
496
- // hook measures percentage bounds on the other. `ResizableProps` is exported,
497
- // so `_direction` is optional for old hand-built objects; absent means there
498
- // is no hook axis to compare, not that the hook is horizontal.
499
- const didWarnAxisRef = useRef(false);
500
- useEffect(() => {
501
- const hookDirection = resizable?._direction;
502
- if (
503
- hookDirection != null &&
504
- hookDirection !== direction &&
505
- !didWarnAxisRef.current
506
- ) {
507
- didWarnAxisRef.current = true;
508
- devWarn(
509
- 'ResizeHandle',
510
- `direction="${direction}" but its useResizable region is ` +
511
- `"${hookDirection}". They must match: the hook measures one axis ` +
512
- 'and the handle drags the other.',
513
- );
514
- }
515
- }, [direction, resizable]);
516
-
517
- // --- Pointer drag ---
518
- const clearDrag = useCallback(() => {
519
- dragRef.current = null;
520
- setIsDragging(false);
521
- clearBodyDragStyles();
522
- }, []);
523
-
524
- const handlePointerDown = useCallback(
525
- (e: React.PointerEvent<HTMLDivElement>) => {
526
- if (isDisabled || !resizable || dragRef.current) {
527
- return;
528
- }
529
- e.preventDefault();
530
- e.stopPropagation();
531
- // Take the pointer for the whole gesture. Without capture the browser
532
- // re-hit-tests every move, so a frame anywhere under the drag path
533
- // swallows the rest of it into the guest document.
534
- e.currentTarget.setPointerCapture?.(e.pointerId);
535
- dragRef.current = {
536
- pointerId: e.pointerId,
537
- startPos: isHorizontal ? e.clientX : e.clientY,
538
- rtl: isHorizontal ? getRTLMultiplier() : 1,
539
- };
540
- setIsDragging(true);
541
- resizable._onResizeStart();
542
- applyBodyDragStyles(isHorizontal);
543
- },
544
- [isDisabled, resizable, isHorizontal, getRTLMultiplier],
545
- );
546
-
547
- const handlePointerMove = useCallback(
548
- (e: React.PointerEvent<HTMLDivElement>) => {
549
- const drag = dragRef.current;
550
- if (!drag || drag.pointerId !== e.pointerId || !resizable) {
551
- return;
552
- }
553
- const currentPos = isHorizontal ? e.clientX : e.clientY;
554
- resizable._onResizeMove((currentPos - drag.startPos) * drag.rtl * sign);
555
- },
556
- [resizable, isHorizontal, sign],
557
- );
558
-
559
- const handlePointerUp = useCallback(
560
- (e: React.PointerEvent<HTMLDivElement>) => {
561
- if (dragRef.current?.pointerId !== e.pointerId) {
562
- return;
563
- }
564
- clearDrag();
565
- resizable?._onResizeEnd();
566
- },
567
- [clearDrag, resizable],
476
+ // A handle whose axis disagrees with its region drags along one axis while
477
+ // the region measures percentage bounds on the other. `ResizableProps` is
478
+ // exported, so `_direction` is optional for old hand-built objects. Without
479
+ // it there is no axis to compare.
480
+ const regionDirection = resizable?._direction;
481
+ useDevWarning(
482
+ 'ResizeHandle',
483
+ `direction="${direction}" but its useResizable region is ` +
484
+ `"${regionDirection}". They must match: the hook measures one axis ` +
485
+ 'and the handle drags the other.',
486
+ regionDirection != null && regionDirection !== direction,
568
487
  );
569
488
 
570
- // Serves pointercancel and lostpointercapture alike: an interrupted drag
571
- // ends without signalling a resize end. lostpointercapture also fires on the
572
- // implicit release after every pointerup/pointercancel, by which point the
573
- // drag is already cleared and this is a no-op.
574
- const cancelDrag = useCallback(
575
- (e: React.PointerEvent<HTMLDivElement>) => {
576
- if (dragRef.current?.pointerId !== e.pointerId) {
577
- return;
578
- }
579
- clearDrag();
580
- // Not a resize end, but still the end of the gesture: the region froze
581
- // state for the duration of the drag and has to be told it is over.
582
- resizable?._onResizeCancel?.();
583
- },
584
- [clearDrag, resizable],
585
- );
586
-
587
- // --- Keyboard ---
588
- const handleKeyDown = useCallback(
589
- (e: React.KeyboardEvent) => {
590
- if (isDisabled || !resizable) {
591
- return;
592
- }
593
- const step = e.shiftKey ? KEYBOARD_LARGE_STEP : KEYBOARD_STEP;
594
- const rtl = isHorizontal ? getRTLMultiplier() : 1;
595
-
596
- // The pane's grow direction on this key: ArrowRight/Down grow unless the
597
- // layout is reversed or, on the inline axis, right-to-left.
598
- const grows = (key: string) =>
599
- (key === 'ArrowRight' || key === 'ArrowDown'
600
- ? 1
601
- : key === 'ArrowLeft' || key === 'ArrowUp'
602
- ? -1
603
- : 0) *
604
- (isHorizontal ? rtl : 1) *
605
- sign >
606
- 0;
607
-
608
- switch (e.key) {
609
- case 'ArrowRight':
610
- case 'ArrowDown':
611
- case 'ArrowLeft':
612
- case 'ArrowUp': {
613
- e.preventDefault();
614
- if (resizable._isCollapsed) {
615
- // A collapsed pane answers only the grow arrow: it opens one step
616
- // above its minimum, the value the separator announces while
617
- // collapsed, or at the collapse threshold when that is larger (APG
618
- // window splitter: the arrow moves the separator from where it is;
619
- // a bare step would stay below the threshold and leave it in
620
- // place). The shrink arrow is a no-op.
621
- if (grows(e.key)) {
622
- resizable._onResizeStart();
623
- // At least the collapse threshold, or the move would stay
624
- // collapsed when the minimum sits below it.
625
- resizable._onResizeMove(
626
- Math.max(resizable._minSizePx + step, resizable._collapsedSize),
627
- );
628
- resizable._onResizeEnd();
629
- }
630
- break;
631
- }
632
- if (resizable._snaps.length > 0) {
633
- // With snap points the region can rest only on a point, so a
634
- // pixel step would snap straight back; the arrow moves to the
635
- // adjacent point in its direction instead (Shift, two points),
636
- // staying within the bounds.
637
- resizable._onResizeStart();
638
- resizable._onResizeMove(
639
- adjacentSnapPoint(
640
- resizable,
641
- grows(e.key) ? 1 : -1,
642
- e.shiftKey ? 2 : 1,
643
- ) - resizable._size,
644
- );
645
- resizable._onResizeEnd();
646
- break;
647
- }
648
- const direction =
649
- e.key === 'ArrowRight' || e.key === 'ArrowDown' ? 1 : -1;
650
- resizable._onResizeStart();
651
- resizable._onResizeMove(
652
- direction * step * (isHorizontal ? rtl : 1) * sign,
653
- );
654
- resizable._onResizeEnd();
655
- break;
656
- }
657
- case 'Home': {
658
- e.preventDefault();
659
- resizable._onResizeStart();
660
- resizable._onResizeMove(resizable._minSizePx - resizable._size);
661
- resizable._onResizeEnd();
662
- break;
663
- }
664
- case 'End': {
665
- e.preventDefault();
666
- // Only a configured maximum is the region's own limit; the reported
667
- // stand-in for an unbounded region is where the pane *could* reach,
668
- // not where End may safely move it (a recorded gap).
669
- if (resizable._maxSizePx !== Infinity) {
670
- resizable._onResizeStart();
671
- resizable._onResizeMove(resizable._maxSizePx - resizable._size);
672
- resizable._onResizeEnd();
673
- }
674
- break;
675
- }
676
- case 'Enter': {
677
- e.preventDefault();
678
- if (resizable._collapsible) {
679
- toggleCollapse(resizable);
680
- }
681
- break;
682
- }
683
- }
684
- },
685
- [isDisabled, resizable, isHorizontal, getRTLMultiplier, sign],
686
- );
489
+ // --- Cleanup on unmount, or when the `resizable` prop goes ---
687
490
 
688
- // --- Double-click collapse ---
689
- const handleDoubleClick = useCallback(() => {
690
- if (isDisabled || !resizable || !resizable._collapsible) {
691
- return;
692
- }
693
- toggleCollapse(resizable);
694
- }, [isDisabled, resizable]);
695
-
696
- // --- Cleanup on unmount ---
697
- // Removing the grab zone implicitly releases its captured pointer and takes
698
- // its listeners with it, so an in-flight drag stops driving the region on
699
- // its own. The body cursor/user-select overrides are not on the element,
700
- // though, so they have to be released here or they stick after the handle
701
- // is gone — and the region, which outlives this handle, still has to hear
491
+ // Removing the grab zone releases its captured pointer and its listeners.
492
+ // A `resizable` prop that goes away takes the grab zone's handlers too. The
493
+ // body's cursor and user-select overrides are not on the element, so they
494
+ // are released here. The region outlives this handle and still has to hear
702
495
  // that its gesture is over.
703
- const cancelOnUnmountRef = useRef<(() => void) | undefined>(undefined);
704
- cancelOnUnmountRef.current = resizable?._onResizeCancel;
496
+ const hasRegion = resizable != null;
705
497
  useEffect(() => {
498
+ if (!hasRegion) {
499
+ return;
500
+ }
706
501
  return () => {
707
- if (dragRef.current) {
708
- dragRef.current = null;
709
- clearBodyDragStyles();
710
- cancelOnUnmountRef.current?.();
711
- }
502
+ regionGesture.interrupt();
712
503
  };
713
- }, []);
504
+ }, [hasRegion, regionGesture]);
714
505
 
715
506
  // --- ARIA ---
716
507
  // When collapsed the panel's real size (0) sits below aria-valuemin, which
717
508
  // is invalid per WCAG 4.1.2. Clamp aria-valuenow to the minimum and announce
718
509
  // the true state via aria-valuetext instead; the valuetext is removed as
719
510
  // soon as the panel expands so the numeric value reads again.
720
- const isCollapsed = resizable?._isCollapsed ?? false;
721
- const ariaValueNow = resizable
722
- ? isCollapsed
723
- ? Math.max(resizable._size, resizable._minSizePx)
724
- : resizable._size
725
- : undefined;
726
- const ariaValueMin = resizable ? resizable._minSizePx : undefined;
511
+ const sizeNow = resizable?._size;
512
+ const minNow = resizable?._minSizePx;
513
+ const ariaValueNow =
514
+ sizeNow == null || minNow == null
515
+ ? undefined
516
+ : isCollapsed
517
+ ? Math.max(sizeNow, minNow)
518
+ : sizeNow;
519
+ const ariaValueMin = minNow;
727
520
  // A separator with a value needs a maximum (WAI-ARIA, APG window splitter).
728
521
  // Unbounded regions report the basis they can fill; a hand-built props
729
522
  // object without it reports only a configured maximum.
@@ -736,7 +529,9 @@ export function ResizeHandle({
736
529
  ? effectiveMax
737
530
  : undefined;
738
531
  const ariaValueText =
739
- resizable && isCollapsed ? t('@astryx.resizable.collapsed') : undefined;
532
+ resizable != null && isCollapsed
533
+ ? t('@astryx.resizable.collapsed')
534
+ : undefined;
740
535
 
741
536
  return (
742
537
  <div
@@ -750,7 +545,11 @@ export function ResizeHandle({
750
545
  aria-label={label}
751
546
  aria-disabled={isDisabled || undefined}
752
547
  tabIndex={isDisabled ? -1 : 0}
753
- onDoubleClick={handleDoubleClick}
548
+ onDoubleClick={() => {
549
+ if (!isDisabled) {
550
+ gesture?.doubleClick();
551
+ }
552
+ }}
754
553
  onFocus={() => setIsFocused(true)}
755
554
  onBlur={() => setIsFocused(false)}
756
555
  data-resizing={isDragging || undefined}
@@ -786,7 +585,9 @@ export function ResizeHandle({
786
585
  // this accessibility-critical handler can't be clobbered by a passed prop.
787
586
  onKeyDown={e => {
788
587
  props.onKeyDown?.(e);
789
- handleKeyDown(e);
588
+ if (!isDisabled) {
589
+ gesture?.keyDown(e);
590
+ }
790
591
  }}>
791
592
  {/* Wider invisible hit area for pointer interaction */}
792
593
  <div
@@ -806,11 +607,15 @@ export function ResizeHandle({
806
607
  : dynamicStyles.hitAreaOffsetY(hitBiasDir),
807
608
  isDisabled && styles.disabled,
808
609
  )}
809
- onPointerDown={handlePointerDown}
810
- onPointerMove={handlePointerMove}
811
- onPointerUp={handlePointerUp}
812
- onPointerCancel={cancelDrag}
813
- onLostPointerCapture={cancelDrag}
610
+ onPointerDown={e => {
611
+ if (!isDisabled) {
612
+ gesture?.pointerDown(e);
613
+ }
614
+ }}
615
+ onPointerMove={e => gesture?.pointerMove(e)}
616
+ onPointerUp={e => gesture?.pointerUp(e)}
617
+ onPointerCancel={e => gesture?.pointerCancel(e)}
618
+ onLostPointerCapture={e => gesture?.pointerCancel(e)}
814
619
  onPointerEnter={() => setIsHovered(true)}
815
620
  onPointerLeave={() => {
816
621
  if (!isDragging) {