@ahrowe/ui 0.36.0 → 0.37.0

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 (100) hide show
  1. package/README.md +3 -2
  2. package/dist/cjs-types/common/hooks/index.d.ts +13 -0
  3. package/dist/cjs-types/common/hooks/useHotkeys.d.ts +45 -0
  4. package/dist/cjs-types/common/hooks/useSwipeDismiss.d.ts +6 -2
  5. package/dist/cjs-types/common/loading/index.d.ts +0 -1
  6. package/dist/cjs-types/common/menu/index.d.ts +1 -0
  7. package/dist/cjs-types/common/menu/menu.types.d.ts +9 -3
  8. package/dist/cjs-types/common/menu/menuHotkeys.d.ts +20 -0
  9. package/dist/cjs-types/common/popover/usePopoverPosition.d.ts +1 -0
  10. package/dist/cjs-types/common/sticky/sticky.d.ts +1 -1
  11. package/dist/cjs-types/common/sticky/sticky.types.d.ts +15 -0
  12. package/dist/cjs-types/common/sticky/stickyStack.d.ts +5 -0
  13. package/dist/cjs-types/common/utils/scrollAncestors.d.ts +10 -0
  14. package/dist/cjs-types/common/virtualList/scroller.d.ts +33 -0
  15. package/dist/cjs-types/common/virtualList/useRowDrag.d.ts +6 -1
  16. package/dist/cjs-types/common/virtualList/useRowNavigation.d.ts +11 -1
  17. package/dist/cjs-types/common/virtualList/useVirtualWindow.d.ts +22 -3
  18. package/dist/cjs-types/common/virtualList/virtualList.d.ts +1 -1
  19. package/dist/cjs-types/common/virtualList/virtualList.types.d.ts +43 -3
  20. package/dist/cjs-types/index.d.ts +1 -0
  21. package/dist/esm/common/dropdown/dropdown.module.mjs.map +1 -1
  22. package/dist/esm/common/hooks/useHotkeys.mjs +2 -0
  23. package/dist/esm/common/hooks/useHotkeys.mjs.map +1 -0
  24. package/dist/esm/common/hooks/useSwipeDismiss.mjs +1 -1
  25. package/dist/esm/common/hooks/useSwipeDismiss.mjs.map +1 -1
  26. package/dist/esm/common/input/input.mjs +1 -1
  27. package/dist/esm/common/input/input.mjs.map +1 -1
  28. package/dist/esm/common/input/input.module.mjs +1 -1
  29. package/dist/esm/common/input/input.module.mjs.map +1 -1
  30. package/dist/esm/common/menu/menu.mjs +1 -1
  31. package/dist/esm/common/menu/menu.mjs.map +1 -1
  32. package/dist/esm/common/menu/menu.types.mjs.map +1 -1
  33. package/dist/esm/common/menu/menuHotkeys.mjs +2 -0
  34. package/dist/esm/common/menu/menuHotkeys.mjs.map +1 -0
  35. package/dist/esm/common/multiSelect/multiSelect.mjs +1 -1
  36. package/dist/esm/common/multiSelect/multiSelect.mjs.map +1 -1
  37. package/dist/esm/common/multiSelect/multiSelect.module.mjs +1 -1
  38. package/dist/esm/common/multiSelect/multiSelect.module.mjs.map +1 -1
  39. package/dist/esm/common/otpInput/otpInput.module.mjs.map +1 -1
  40. package/dist/esm/common/popover/usePopoverPosition.mjs +1 -1
  41. package/dist/esm/common/popover/usePopoverPosition.mjs.map +1 -1
  42. package/dist/esm/common/sticky/sticky.mjs +1 -1
  43. package/dist/esm/common/sticky/sticky.mjs.map +1 -1
  44. package/dist/esm/common/sticky/stickyStack.mjs +1 -1
  45. package/dist/esm/common/sticky/stickyStack.mjs.map +1 -1
  46. package/dist/esm/common/utils/scrollAncestors.mjs +1 -1
  47. package/dist/esm/common/utils/scrollAncestors.mjs.map +1 -1
  48. package/dist/esm/common/virtualList/scroller.mjs +2 -0
  49. package/dist/esm/common/virtualList/scroller.mjs.map +1 -0
  50. package/dist/esm/common/virtualList/useRowDrag.mjs +1 -1
  51. package/dist/esm/common/virtualList/useRowDrag.mjs.map +1 -1
  52. package/dist/esm/common/virtualList/useRowNavigation.mjs +1 -1
  53. package/dist/esm/common/virtualList/useRowNavigation.mjs.map +1 -1
  54. package/dist/esm/common/virtualList/useVirtualWindow.mjs +1 -1
  55. package/dist/esm/common/virtualList/useVirtualWindow.mjs.map +1 -1
  56. package/dist/esm/common/virtualList/virtualList.mjs +1 -1
  57. package/dist/esm/common/virtualList/virtualList.mjs.map +1 -1
  58. package/dist/esm/common/virtualList/virtualList.module.mjs +1 -1
  59. package/dist/esm/common/virtualList/virtualList.module.mjs.map +1 -1
  60. package/dist/esm/index.mjs +1 -1
  61. package/dist/index.cjs +3 -3
  62. package/dist/index.cjs.map +1 -1
  63. package/dist/style.css +1 -1
  64. package/dist/types/common/hooks/index.d.ts +13 -0
  65. package/dist/types/common/hooks/useHotkeys.d.ts +45 -0
  66. package/dist/types/common/hooks/useSwipeDismiss.d.ts +6 -2
  67. package/dist/types/common/loading/index.d.ts +0 -1
  68. package/dist/types/common/menu/index.d.ts +1 -0
  69. package/dist/types/common/menu/menu.types.d.ts +9 -3
  70. package/dist/types/common/menu/menuHotkeys.d.ts +20 -0
  71. package/dist/types/common/popover/usePopoverPosition.d.ts +1 -0
  72. package/dist/types/common/sticky/sticky.d.ts +1 -1
  73. package/dist/types/common/sticky/sticky.types.d.ts +15 -0
  74. package/dist/types/common/sticky/stickyStack.d.ts +5 -0
  75. package/dist/types/common/utils/scrollAncestors.d.ts +10 -0
  76. package/dist/types/common/virtualList/scroller.d.ts +33 -0
  77. package/dist/types/common/virtualList/useRowDrag.d.ts +6 -1
  78. package/dist/types/common/virtualList/useRowNavigation.d.ts +11 -1
  79. package/dist/types/common/virtualList/useVirtualWindow.d.ts +22 -3
  80. package/dist/types/common/virtualList/virtualList.d.ts +1 -1
  81. package/dist/types/common/virtualList/virtualList.types.d.ts +43 -3
  82. package/dist/types/index.d.ts +1 -0
  83. package/docs/CLAUDE.md +3 -2
  84. package/docs/Hooks.md +232 -0
  85. package/docs/Loading.md +56 -15
  86. package/docs/Menu.md +25 -3
  87. package/docs/Popover.md +2 -0
  88. package/docs/Sticky.md +22 -1
  89. package/docs/VirtualList.md +44 -4
  90. package/package.json +1 -1
  91. package/dist/cjs-types/common/loading/iconLoading/iconLoading.d.ts +0 -5
  92. package/dist/cjs-types/common/loading/iconLoading/index.d.ts +0 -2
  93. package/dist/esm/common/loading/iconLoading/iconLoading.mjs +0 -2
  94. package/dist/esm/common/loading/iconLoading/iconLoading.mjs.map +0 -1
  95. package/dist/esm/common/loading/iconLoading/iconLoading.module.mjs +0 -2
  96. package/dist/esm/common/loading/iconLoading/iconLoading.module.mjs.map +0 -1
  97. package/dist/esm/common/loading/iconLoading/index.mjs +0 -2
  98. package/dist/esm/common/loading/iconLoading/index.mjs.map +0 -1
  99. package/dist/types/common/loading/iconLoading/iconLoading.d.ts +0 -5
  100. package/dist/types/common/loading/iconLoading/index.d.ts +0 -2
@@ -0,0 +1,13 @@
1
+ export { useHotkeys, formatHotkey } from './useHotkeys.js';
2
+ export type { HotkeyHandler, HotkeyMap, UseHotkeysOptions } from './useHotkeys.js';
3
+ export { useMediaQuery } from './useMediaQuery.js';
4
+ export { useCoarsePointer } from './useCoarsePointer.js';
5
+ export { useVisualViewportSize } from './useVisualViewportSize.js';
6
+ export type { VisualViewportSize } from './useVisualViewportSize.js';
7
+ export { useScrollEdges } from './useScrollEdges.js';
8
+ export type { ScrollEdges, ScrollEdgesOptions } from './useScrollEdges.js';
9
+ export { useSwipeDismiss } from './useSwipeDismiss.js';
10
+ export type { UseSwipeDismissOptions, UseSwipeDismissResult } from './useSwipeDismiss.js';
11
+ export { useOverlay } from './useOverlay.js';
12
+ export type { UseOverlayArgs } from './useOverlay.js';
13
+ export { tabbablesIn } from './useFocusBoundary.js';
@@ -0,0 +1,45 @@
1
+ import { RefObject } from 'react';
2
+ export type HotkeyHandler = (event: KeyboardEvent) => void;
3
+ /** Combo → handler, e.g. `{ 'mod+k': open, Escape: close }`. */
4
+ export type HotkeyMap = Record<string, HotkeyHandler>;
5
+ export interface UseHotkeysOptions {
6
+ /** Attach the listener at all. Default `true`; pass `false` to suspend every combo at once. */
7
+ enabled?: boolean;
8
+ /** Scope the shortcuts to one element instead of the whole window. */
9
+ target?: RefObject<HTMLElement | null>;
10
+ /**
11
+ * Let plain-key combos fire while a text field or `contenteditable` has focus. Default `false`,
12
+ * so `'n'` does not fire for every `n` typed into an input. Combos carrying Ctrl, Cmd or Alt
13
+ * are never suppressed — they cannot be confused with typing.
14
+ */
15
+ enableInFields?: boolean;
16
+ /**
17
+ * Call `preventDefault()` on a match, so `mod+s` does not also open the browser's save dialog.
18
+ * Default `true`.
19
+ */
20
+ preventDefault?: boolean;
21
+ }
22
+ /**
23
+ * A combo written for the reader: `'mod+shift+k'` reads `⇧⌘K` on an Apple platform and
24
+ * `Ctrl+Shift+K` everywhere else.
25
+ *
26
+ * So a shortcut is written once, in one spelling, and the hint beside it cannot contradict what
27
+ * the binding actually listens for. Hand-written `'⌘K'` hints are wrong on every other platform,
28
+ * silently, because nothing compares the label to the binding.
29
+ *
30
+ * Resolved from the platform at call time, so a server-rendered page renders the non-Apple form.
31
+ */
32
+ export declare function formatHotkey(combo: string): string;
33
+ /**
34
+ * Keyboard shortcuts, for a whole app or for one element.
35
+ *
36
+ * ```tsx
37
+ * useHotkeys({ 'mod+k': () => setPaletteOpen(true), '?': () => setHelpOpen(true) });
38
+ * ```
39
+ *
40
+ * The map is read through a ref at event time, so an inline object literal is fine and no handler
41
+ * needs memoising. Shortcuts keep firing while a Modal or Drawer is open — pass `enabled` for the
42
+ * ones that should not.
43
+ */
44
+ export declare function useHotkeys(hotkeys: HotkeyMap, options?: UseHotkeysOptions): void;
45
+ export default useHotkeys;
@@ -19,8 +19,12 @@ export interface UseSwipeDismissOptions {
19
19
  }
20
20
  export interface UseSwipeDismissResult<T extends HTMLElement> {
21
21
  /** Attach to the element that should become draggable. The hook wires up its own pointer
22
- * listeners on this node directly (no handlers to spread), so this is the only thing to attach. */
23
- ref: React.RefObject<T | null>;
22
+ * listeners on this node directly (no handlers to spread), so this is the only thing to attach.
23
+ *
24
+ * A callback ref rather than an object one, because the hook has to know when the node changes:
25
+ * an element that is unmounted and rendered again is a *different* node, and a ref object cannot
26
+ * say so. To keep a ref of your own on the element, call both from one callback. */
27
+ ref: React.RefCallback<T>;
24
28
  /** True for the duration of an active drag — e.g. use it to pause an auto-dismiss timer. */
25
29
  isDragging: boolean;
26
30
  }
@@ -1,2 +1 @@
1
- export { default as IconLoading } from './iconLoading/index.js';
2
1
  export { default as SpinnerLoading } from './spinnerLoading/index.js';
@@ -1,2 +1,3 @@
1
1
  export { default } from './menu.js';
2
+ export { menuHotkeys } from './menuHotkeys.js';
2
3
  export * from './menu.types.js';
@@ -10,10 +10,16 @@ export interface MenuAction {
10
10
  label: ReactNode;
11
11
  icon?: IconContent;
12
12
  /**
13
- * Keyboard hint on the trailing edge, e.g. `⌘K`. Display only: binding the
14
- * shortcut is yours, since only you know what else is listening.
13
+ * A keyboard shortcut for this action, in `useHotkeys` spelling: `'mod+k'`,
14
+ * `'shift+f2'`, `'delete'`. Shown on the trailing edge written for the
15
+ * platform: `⌘K` on an Apple one, `Ctrl+K` elsewhere.
16
+ *
17
+ * The menu draws it and nothing more. Binding is yours, because only the app
18
+ * knows what else is listening and whether the shortcut should work at all
19
+ * times or only while the menu is open. `menuHotkeys` turns these entries
20
+ * into a `useHotkeys` map, so the one spelling covers both ends.
15
21
  */
16
- shortcut?: ReactNode;
22
+ shortcut?: string;
17
23
  disabled?: boolean;
18
24
  /** Renders in the destructive colour. For deleting, discarding, revoking. */
19
25
  danger?: boolean;
@@ -0,0 +1,20 @@
1
+ import { HotkeyMap } from '../hooks/useHotkeys.js';
2
+ import { MenuEntry } from './menu.types.js';
3
+ /**
4
+ * The bindings for a menu's `shortcut` entries, to hand to `useHotkeys` yourself.
5
+ *
6
+ * The menu draws a shortcut but never listens for one, because only the app knows what else is
7
+ * listening: binding it here as well as in an app's own keymap would run the action twice, and
8
+ * whether a shortcut should work at all times or only while the menu is open is the app's
9
+ * question, not the menu's. Calling this keeps the answer in one spelling, so the hint the menu
10
+ * draws cannot drift from what actually fires.
11
+ *
12
+ * ```tsx
13
+ * useHotkeys(menuHotkeys(items, run)); // whenever the page is up
14
+ * useHotkeys(menuHotkeys(items, run), { enabled: isOpen }); // only while the menu is open
15
+ * ```
16
+ *
17
+ * Runs each entry the way clicking it does: its own `onClick`, then `onAction`. A `disabled`
18
+ * entry, and an entry with no `shortcut`, is left out.
19
+ */
20
+ export declare function menuHotkeys(items: MenuEntry[], onAction?: (id: string) => void): HotkeyMap;
@@ -1,5 +1,6 @@
1
1
  import { RefObject } from 'react';
2
2
  import { PopoverPlacement, PopoverAlign } from './popover.types.js';
3
+ export declare function supportsAnchoring(): boolean;
3
4
  export interface UsePopoverPositionArgs {
4
5
  isOpen: boolean;
5
6
  placement: PopoverPlacement;
@@ -1,3 +1,3 @@
1
1
  import { default as React } from 'react';
2
2
  import { StickyProps } from './sticky.types.js';
3
- export default function Sticky({ children, offsetTop, offsetElement, stickToScrollParent, zIndex, disabled, onStuckChange, className, style, classNames, styles: slotStyles, ...rest }: StickyProps): React.JSX.Element;
3
+ export default function Sticky({ children, offsetTop, offsetElement, stickToScrollParent, boundary, boundaryInset, zIndex, disabled, onStuckChange, className, style, classNames, styles: slotStyles, ...rest }: StickyProps): React.JSX.Element;
@@ -9,6 +9,21 @@ export interface StickyProps extends HtmlProps {
9
9
  offsetElement?: React.RefObject<HTMLElement | null>;
10
10
  /** Stick to the top of the nearest scrollable ancestor instead of the browser viewport. Falls back to the viewport if no scrollable ancestor is found. */
11
11
  stickToScrollParent?: boolean;
12
+ /**
13
+ * Element the content is confined to, normally the one it labels. Once that
14
+ * element's bottom edge reaches the pinned content, the content is pushed up
15
+ * with it and scrolls away, instead of staying pinned over whatever follows.
16
+ * Without it the content pins for as long as the page scrolls.
17
+ */
18
+ boundary?: React.RefObject<HTMLElement | null>;
19
+ /**
20
+ * How much of the boundary's lower end stays uncovered as the content leaves,
21
+ * in px. At 0 the content rests on the very bottom edge, which means the last
22
+ * stretch of the boundary, as tall as the pinned content, spends the whole
23
+ * exit hidden underneath it. Set it to how much you want to stay in sight:
24
+ * for a table header, a few rows. Default 0. Ignored without `boundary`.
25
+ */
26
+ boundaryInset?: number;
12
27
  /** z-index applied to the content while stuck. Default 20. */
13
28
  zIndex?: number;
14
29
  /** Disable sticking entirely — content stays in normal flow. */
@@ -15,6 +15,11 @@ export interface StickyStackMetrics {
15
15
  * in unrelated scroll panels should never push each other down just because they happen to
16
16
  * share horizontal DOM coordinates. */
17
17
  container: HTMLElement | null;
18
+ /** The element the entry is confined to, or null when nothing confines it.
19
+ * Entries sharing one leave together. */
20
+ boundary: HTMLElement | null;
21
+ /** Bottom edge of that element (less its inset), in the same coordinate space as `top`. */
22
+ boundaryBottom: number | null;
18
23
  }
19
24
  export interface StickyStackEntry {
20
25
  node: HTMLElement;
@@ -1,2 +1,12 @@
1
1
  export declare function getScrollableAncestors(el: HTMLElement): HTMLElement[];
2
2
  export declare function isHiddenByAnyAncestor(rect: DOMRect, ancestors: HTMLElement[]): boolean;
3
+ /**
4
+ * The ancestor whose scrollbar carries `el`, or null when that is the page.
5
+ * `Sticky` pins against it and `VirtualList` virtualizes against it, and the two
6
+ * have to agree or a sticky header and the rows under it come apart.
7
+ *
8
+ * Narrower than `getScrollableAncestors` above, which is about who *clips* and so
9
+ * counts `hidden` too: a `hidden` box never scrolls, so pinning to it would pin
10
+ * to something that never moves.
11
+ */
12
+ export declare function getScrollParent(el: HTMLElement | null): HTMLElement | null;
@@ -0,0 +1,33 @@
1
+ import { VirtualListScrollParent } from './virtualList.types.js';
2
+ /**
3
+ * Whatever carries the list's scroll, and how the list is measured against it.
4
+ *
5
+ * Everything here works in *list space*: offset 0 is the top of the first row,
6
+ * which is the root's `scrollTop` when the list scrolls itself and its distance
7
+ * above the scrollport's top edge when it doesn't. One coordinate system, so the
8
+ * windowing, anchoring and jump math never branch.
9
+ */
10
+ export type Scroller = HTMLElement | Window;
11
+ export declare function isWindowScroller(scroller: Scroller): scroller is Window;
12
+ /** `'nearest'` is resolved by the caller, which caches the ancestor walk. */
13
+ export declare function resolveScroller(scrollParent: Exclude<VirtualListScrollParent, 'nearest'> | undefined): Scroller | null;
14
+ /** In viewport coordinates, which is what the rects below are compared against. */
15
+ export declare function scrollportBounds(scroller: Scroller): {
16
+ top: number;
17
+ height: number;
18
+ };
19
+ export interface Viewport {
20
+ /** List-space scroll position, unclamped: negative while the list starts below
21
+ * the scrollport, which is what makes a target reachable from either side. */
22
+ rel: number;
23
+ /** The slice of the scrollport the list occupies now. */
24
+ visibleHeight: number;
25
+ /** The whole scrollport, which is what a scroll target is aimed within. */
26
+ viewportHeight: number;
27
+ /** From the list's last row to the end of the scrollport. The browser clamps
28
+ * `scrollTop`, so a list that scrolls itself never reports this negative;
29
+ * riding a scrollport, it does, for the whole rest of the page. */
30
+ distanceToEnd: number;
31
+ width: number;
32
+ }
33
+ export declare function measureViewport(root: HTMLElement, scroller: Scroller | null): Viewport;
@@ -1,5 +1,6 @@
1
1
  import { default as React } from 'react';
2
2
  import { TreeDropPosition, TreeDropTarget } from './virtualList.types.js';
3
+ import { ListViewportRect } from './useVirtualWindow.js';
3
4
  interface UseRowDragArgs {
4
5
  reorderable: boolean;
5
6
  treeReorder: boolean;
@@ -15,6 +16,10 @@ interface UseRowDragArgs {
15
16
  rootRef: React.RefObject<HTMLDivElement | null>;
16
17
  /** Latest keys for async (touch / rAF) consumers — owned by useVirtualWindow. */
17
18
  keysRef: React.RefObject<(string | number)[]>;
19
+ /** Where the list is on screen, which is not its own box once it rides a
20
+ * scrollport — owned by useVirtualWindow, as is `scrollBy`. */
21
+ getVisibleRect: () => ListViewportRect | null;
22
+ scrollBy: (dy: number) => void;
18
23
  }
19
24
  /** The drag-related props the parent spreads onto a given row. */
20
25
  export interface RowDragProps {
@@ -54,5 +59,5 @@ interface UseRowDragResult {
54
59
  * Touch uses a long-press → lifted-clone → pointer-move path with edge
55
60
  * auto-scroll. `dragHandle` restricts initiation to `[data-drag-handle]`.
56
61
  */
57
- export declare function useRowDrag({ reorderable, treeReorder, onReorder, onTreeDrop, longPressDelay, dragHandle, treeIndentPx, keys, depths, isGroupAt, rootRef, keysRef, }: UseRowDragArgs): UseRowDragResult;
62
+ export declare function useRowDrag({ reorderable, treeReorder, onReorder, onTreeDrop, longPressDelay, dragHandle, treeIndentPx, keys, depths, isGroupAt, rootRef, keysRef, getVisibleRect, scrollBy, }: UseRowDragArgs): UseRowDragResult;
58
63
  export {};
@@ -1,12 +1,18 @@
1
1
  import { default as React } from 'react';
2
2
  import { VirtualListScrollOptions } from './virtualList.types.js';
3
+ import { VisibleSlice } from './useVirtualWindow.js';
3
4
  interface UseRowNavigationArgs {
4
5
  enabled: boolean;
6
+ tabAcrossRows: boolean;
5
7
  rowCount: number;
6
8
  /** Row tops, from the windowing engine, used for paging and visibility. */
7
9
  offsets: number[];
8
10
  rootRef: React.RefObject<HTMLDivElement | null>;
9
11
  headerRef: React.RefObject<HTMLDivElement | null>;
12
+ /** Read live: a held arrow key moves the slice faster than the state commits. */
13
+ getVisibleSlice: () => VisibleSlice;
14
+ /** The whole scrollport, which is what one PageDown covers. */
15
+ viewportHeight: number;
10
16
  scrollToIndex: (index: number, options?: VirtualListScrollOptions) => void;
11
17
  /** Row index the active row starts on, normally the selected one. */
12
18
  initialIndex: number;
@@ -45,6 +51,10 @@ interface UseRowNavigationResult {
45
51
  * hold an input, a dropdown or a button, and every one of them wants the arrows,
46
52
  * Enter and Space for itself. `F2` steps into such a row and `Escape` steps back
47
53
  * out, which is the one pair of keys that does work from inside a cell.
54
+ *
55
+ * `tabAcrossRows` adds the second pair, Tab and Shift+Tab, which the browser
56
+ * cannot carry across a row boundary itself: the next row may not be in the DOM
57
+ * when it looks for the next stop.
48
58
  */
49
- export declare function useRowNavigation({ enabled, rowCount, offsets, rootRef, headerRef, scrollToIndex, initialIndex, multiSelect, onActivate, onToggleSelect, }: UseRowNavigationArgs): UseRowNavigationResult;
59
+ export declare function useRowNavigation({ enabled, tabAcrossRows, rowCount, offsets, rootRef, headerRef, getVisibleSlice, viewportHeight, scrollToIndex, initialIndex, multiSelect, onActivate, onToggleSelect, }: UseRowNavigationArgs): UseRowNavigationResult;
50
60
  export {};
@@ -1,5 +1,5 @@
1
1
  import { default as React } from 'react';
2
- import { VirtualListHandle } from './virtualList.types.js';
2
+ import { VirtualListHandle, VirtualListScrollParent } from './virtualList.types.js';
3
3
  interface UseVirtualWindowArgs<T> {
4
4
  items: T[];
5
5
  /** Stable key per row (already derived by the caller). */
@@ -12,6 +12,19 @@ interface UseVirtualWindowArgs<T> {
12
12
  loadMoreThreshold: number;
13
13
  /** Initial/filter load in progress — the body is replaced by a spinner. */
14
14
  isLoading: boolean;
15
+ scrollParent?: VirtualListScrollParent;
16
+ }
17
+ /** Where the list is on screen, which is not its own box once it rides a scrollport. */
18
+ export interface ListViewportRect {
19
+ top: number;
20
+ bottom: number;
21
+ left: number;
22
+ right: number;
23
+ }
24
+ /** The list-space slice currently on screen, read live for keyboard navigation. */
25
+ export interface VisibleSlice {
26
+ top: number;
27
+ height: number;
15
28
  }
16
29
  interface UseVirtualWindowResult {
17
30
  rootRef: React.RefObject<HTMLDivElement | null>;
@@ -22,15 +35,21 @@ interface UseVirtualWindowResult {
22
35
  keysRef: React.RefObject<(string | number)[]>;
23
36
  offsets: number[];
24
37
  totalHeight: number;
38
+ /** The list's visible slice, which is what decides which rows are rendered. */
25
39
  containerHeight: number;
40
+ /** The whole scrollport, which is what a scroll target is aimed within. */
41
+ viewportHeight: number;
26
42
  containerWidth: number;
27
43
  startIndex: number;
28
44
  endIndex: number;
29
- handleScroll: (e: React.UIEvent<HTMLDivElement>) => void;
30
45
  showLoadMoreSpinner: boolean;
46
+ isWindowed: boolean;
47
+ getVisibleSlice: () => VisibleSlice;
48
+ getVisibleRect: () => ListViewportRect | null;
49
+ scrollBy: (dy: number) => void;
31
50
  /** The scroll-related imperative methods, for the component to compose into
32
51
  * the forwarded handle alongside column controls. */
33
52
  scrollApi: Omit<VirtualListHandle, 'recalculateColumns' | 'resetColumnWidths'>;
34
53
  }
35
- export declare function useVirtualWindow<T>({ items, keys, getItemKey, estimatedRowHeight, rowGap, overscan, onLoadMore, loadMoreThreshold, isLoading, }: UseVirtualWindowArgs<T>): UseVirtualWindowResult;
54
+ export declare function useVirtualWindow<T>({ items, keys, getItemKey, estimatedRowHeight, rowGap, overscan, onLoadMore, loadMoreThreshold, isLoading, scrollParent, }: UseVirtualWindowArgs<T>): UseVirtualWindowResult;
36
55
  export {};
@@ -1,5 +1,5 @@
1
1
  import { default as React } from 'react';
2
2
  import { VirtualListProps } from './virtualList.types.js';
3
3
  export { buildOffsets, findStartIndex, getVisibleRange, resolveReorderTarget, resolveTreeDropTarget, lastDescendantIndex, buildColumnTemplate, compareSortValues, isColumnSortable, isEmptySortValue, nextSort, sortItems, } from './virtualList.utils.js';
4
- declare function VirtualList<T>({ ref, items, renderRow, ariaRoles, columns, height, estimatedRowHeight, overscan, rowGap, rowPadding, selectedKey, getItemKey, onRowClick, keyboardNavigation, visibleColumnKeys, onVisibleColumnsChange, persistColumnsKey, resizableColumns, columnWidths, onColumnWidthsChange, labels, sort, defaultSort, onSortChange, showColumnToggle, onLoadMore, loadMoreThreshold, isLoading, multiSelect, selectedKeys, onSelectionChange, reorderable, onReorder, longPressDelay, treeReorder, getItemDepth, isGroup, onTreeDrop, treeIndentPx, dragHandle, showDivider, rowHover, className, style, classNames, styles: slotStyles, ...rest }: VirtualListProps<T>): React.JSX.Element;
4
+ declare function VirtualList<T>(props: VirtualListProps<T>): React.JSX.Element;
5
5
  export default VirtualList;
@@ -74,6 +74,13 @@ export interface VirtualListColumn<T> {
74
74
  */
75
75
  sortable?: boolean;
76
76
  }
77
+ /**
78
+ * What scrolls, when the list is not to scroll itself. `'nearest'` is the first
79
+ * ancestor set to scroll, the same one `Sticky` pins to; `'window'` is the page;
80
+ * an element (or a getter for one, for a ref that fills after the first render)
81
+ * names it outright.
82
+ */
83
+ export type VirtualListScrollParent = 'nearest' | 'window' | HTMLElement | (() => HTMLElement | null);
77
84
  export interface VirtualListScrollOptions {
78
85
  /** Where the target row lands in the viewport. Default: 'start'. */
79
86
  align?: 'start' | 'center' | 'end';
@@ -88,8 +95,18 @@ export interface VirtualListHandle {
88
95
  scrollToKey: (key: string | number, options?: VirtualListScrollOptions) => void;
89
96
  /** Scroll back to the top. */
90
97
  scrollToTop: (options?: Pick<VirtualListScrollOptions, 'behavior'>) => void;
91
- /** The underlying scroll container element, for advanced use. */
92
- getScrollElement: () => HTMLDivElement | null;
98
+ /**
99
+ * The element that scrolls: the list itself, or the `scrollParent` it was
100
+ * given. `null` for `scrollParent='window'`, where the page scrolls.
101
+ */
102
+ getScrollElement: () => HTMLElement | null;
103
+ /**
104
+ * Re-read where the list sits in its `scrollParent`. Only needed in that mode,
105
+ * and only when something *above* the list changed height without the list
106
+ * re-rendering: an element moving fires no event of its own, and the list
107
+ * would keep rendering the window it had before.
108
+ */
109
+ remeasure: () => void;
93
110
  /**
94
111
  * Re-measure `fit` columns on the next layout. Call this after an out-of-band
95
112
  * content change that doesn't flow through a new `items` array or `columns`
@@ -143,8 +160,22 @@ export interface VirtualListProps<T> extends HtmlProps {
143
160
  container: string;
144
161
  row: string;
145
162
  };
146
- /** Height of the scroll viewport. Defaults to '100%' to fill the parent container. */
163
+ /** Height of the scroll viewport. Defaults to '100%' to fill the parent container. Ignored with `scrollParent`. */
147
164
  height?: number | string;
165
+ /**
166
+ * Drops the list's own scrollbar: it renders at its full height in the page
167
+ * flow and virtualizes against whatever is given here, so a long list at the
168
+ * end of a scrolling page doesn't put a second scrollbar inside the first.
169
+ * `height` no longer applies. `'nearest'` covers most cases: it walks up to
170
+ * the first ancestor whose `overflow-y` is set to scroll, whether or not it
171
+ * currently does, and falls back to the page.
172
+ *
173
+ * The header still sticks, now to the top of that scrollport; give the list
174
+ * `--vl-header-top` to clear a fixed app bar. Rows of uneven height move the
175
+ * page's own scrollbar as they are measured, so set `estimatedRowHeight` to
176
+ * the real height where the rows have one.
177
+ */
178
+ scrollParent?: VirtualListScrollParent;
148
179
  /** Estimated row height before measurement. Default: 40. */
149
180
  estimatedRowHeight?: number;
150
181
  /** Extra rows to render above/below the visible window. Default: 3. */
@@ -187,6 +218,15 @@ export interface VirtualListProps<T> extends HtmlProps {
187
218
  * `Tree` does.
188
219
  */
189
220
  keyboardNavigation?: boolean;
221
+ /**
222
+ * Lets Tab carry on into the next row instead of leaving the list at the end
223
+ * of the current one, for a grid of inputs a user fills in field by field.
224
+ * Shift+Tab walks back the same way, and Tab out of the last row (or back out
225
+ * of the first) leaves the list as it always did. Default: false, because for
226
+ * a list that is read rather than filled in, one tab stop and the arrow keys
227
+ * are the expected behaviour, and 5000 rows of tab stops are a trap.
228
+ */
229
+ tabAcrossRows?: boolean;
190
230
  /** Controlled visible column keys. When provided, internal state is ignored. */
191
231
  visibleColumnKeys?: string[];
192
232
  /** Called when the user toggles a column in the popover. */
@@ -148,6 +148,7 @@ export { default as VirtualList } from './common/virtualList/index.js';
148
148
  export * from './common/virtualList/index.js';
149
149
  export { default as Wizard } from './common/wizard/index.js';
150
150
  export * from './common/wizard/index.js';
151
+ export * from './common/hooks/index.js';
151
152
  export { FormValidator, FormValidatorGroup, Validators, ValidatableComponent, useFormValidator, useFormValidatorGroup, } from './services/formValidation/index.js';
152
153
  export type { ValidationError, ValidatorFunction, ErrorParser, FormValidatorGroupOptions, InferGroupValues, MergedValues, } from './services/formValidation/index.js';
153
154
  export { mapLocale, setLocale, getLocale } from './services/localization.js';
package/docs/CLAUDE.md CHANGED
@@ -73,6 +73,7 @@ enums and slot names. Do not infer props from a component's name.
73
73
  | FloorPlan.md | emptyFloorPlan, addWall, derivePlan | The shared data model behind `RoomDrawer` and `RoomViewer` |
74
74
  | FormValidator.md | FormValidator, Validators, useFormValidator | Track and validate a single form field's value, dirty/touched state, and errors |
75
75
  | FormValidatorGroup.md | useFormValidatorGroup, FormValidatorGroup, FormValidator, Validators | Manage a whole form — multiple fields, a single submit handler, validate-on-submit, dirty/touched tracking across the form, and mapping server-side errors back onto the right fields |
76
+ | Hooks.md | useHotkeys, formatHotkey, useMediaQuery, useCoarsePointer, useVisualViewportSize, useScrollEdges, useSwipeDismiss, useOverlay, tabbablesIn | Standalone React hooks the library uses internally and publishes for the same jobs in your own code: keyboard shortcuts, media queries, the mobile keyboard's viewport, scroll-edge detection, swipe-to-dismiss, and the Escape / focus-trap / scroll-lock behaviour behind a custom overlay |
76
77
  | IconPicker.md | IconPicker | Pick one item from a visual icon/image grid — category selectors, emoji-style pickers, avatar selectors, anything where each option is best represented by an icon or image rather than text alone |
77
78
  | IdleManager.md | IdleManager | Show something only once the user has stopped interacting — a screen saver, a session-timeout warning, an "are you still there?" prompt, an idle dashboard view |
78
79
  | Input.md | Input, InputType | Text input with floating label, validation state, and type variants |
@@ -80,8 +81,8 @@ enums and slot names. Do not infer props from a component's name.
80
81
  | InteractableDiv.md | InteractableDiv | Accessible `<div>` that behaves like a button — click and keyboard (Enter/Space) events, with proper ARIA semantics |
81
82
  | KanbanBoard.md | KanbanBoard, KanbanColumn | Drag-and-drop board with columns — task management, workflow stages, status boards (e.g. Todo / In Progress / Done) |
82
83
  | KlipyPicker.md | KlipyPicker | GIF picker backed by the Klipy API — chat reactions, message composers, content authoring tools |
83
- | Loading.md | IconLoading, SpinnerLoading | Loading indicators for async operations — page loads, data fetching, button actions (when not using Button's built-in `isLoading`) |
84
- | Menu.md | Menu | A list of actions hanging off a trigger — a row's "more" button, a toolbar overflow, a context menu on a card |
84
+ | Loading.md | SpinnerLoading | A loading indicator for async operations: page loads, data fetching, or covering a region while it refreshes |
85
+ | Menu.md | Menu, menuHotkeys | A list of actions hanging off a trigger — a row's "more" button, a toolbar overflow, a context menu on a card |
85
86
  | Modal.md | Modal | Full overlay dialog — forms, detail views, confirmations, wizards that need to block the rest of the UI |
86
87
  | MultiSelect.md | MultiSelect | Pick several options from a list |
87
88
  | NumberInput.md | NumberInput | Numeric input with formatting — prices, quantities, percentages, phone numbers with separators |