@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.
- package/README.md +3 -2
- package/dist/cjs-types/common/hooks/index.d.ts +13 -0
- package/dist/cjs-types/common/hooks/useHotkeys.d.ts +45 -0
- package/dist/cjs-types/common/hooks/useSwipeDismiss.d.ts +6 -2
- package/dist/cjs-types/common/loading/index.d.ts +0 -1
- package/dist/cjs-types/common/menu/index.d.ts +1 -0
- package/dist/cjs-types/common/menu/menu.types.d.ts +9 -3
- package/dist/cjs-types/common/menu/menuHotkeys.d.ts +20 -0
- package/dist/cjs-types/common/popover/usePopoverPosition.d.ts +1 -0
- package/dist/cjs-types/common/sticky/sticky.d.ts +1 -1
- package/dist/cjs-types/common/sticky/sticky.types.d.ts +15 -0
- package/dist/cjs-types/common/sticky/stickyStack.d.ts +5 -0
- package/dist/cjs-types/common/utils/scrollAncestors.d.ts +10 -0
- package/dist/cjs-types/common/virtualList/scroller.d.ts +33 -0
- package/dist/cjs-types/common/virtualList/useRowDrag.d.ts +6 -1
- package/dist/cjs-types/common/virtualList/useRowNavigation.d.ts +11 -1
- package/dist/cjs-types/common/virtualList/useVirtualWindow.d.ts +22 -3
- package/dist/cjs-types/common/virtualList/virtualList.d.ts +1 -1
- package/dist/cjs-types/common/virtualList/virtualList.types.d.ts +43 -3
- package/dist/cjs-types/index.d.ts +1 -0
- package/dist/esm/common/dropdown/dropdown.module.mjs.map +1 -1
- package/dist/esm/common/hooks/useHotkeys.mjs +2 -0
- package/dist/esm/common/hooks/useHotkeys.mjs.map +1 -0
- package/dist/esm/common/hooks/useSwipeDismiss.mjs +1 -1
- package/dist/esm/common/hooks/useSwipeDismiss.mjs.map +1 -1
- package/dist/esm/common/input/input.mjs +1 -1
- package/dist/esm/common/input/input.mjs.map +1 -1
- package/dist/esm/common/input/input.module.mjs +1 -1
- package/dist/esm/common/input/input.module.mjs.map +1 -1
- package/dist/esm/common/menu/menu.mjs +1 -1
- package/dist/esm/common/menu/menu.mjs.map +1 -1
- package/dist/esm/common/menu/menu.types.mjs.map +1 -1
- package/dist/esm/common/menu/menuHotkeys.mjs +2 -0
- package/dist/esm/common/menu/menuHotkeys.mjs.map +1 -0
- package/dist/esm/common/multiSelect/multiSelect.mjs +1 -1
- package/dist/esm/common/multiSelect/multiSelect.mjs.map +1 -1
- package/dist/esm/common/multiSelect/multiSelect.module.mjs +1 -1
- package/dist/esm/common/multiSelect/multiSelect.module.mjs.map +1 -1
- package/dist/esm/common/otpInput/otpInput.module.mjs.map +1 -1
- package/dist/esm/common/popover/usePopoverPosition.mjs +1 -1
- package/dist/esm/common/popover/usePopoverPosition.mjs.map +1 -1
- package/dist/esm/common/sticky/sticky.mjs +1 -1
- package/dist/esm/common/sticky/sticky.mjs.map +1 -1
- package/dist/esm/common/sticky/stickyStack.mjs +1 -1
- package/dist/esm/common/sticky/stickyStack.mjs.map +1 -1
- package/dist/esm/common/utils/scrollAncestors.mjs +1 -1
- package/dist/esm/common/utils/scrollAncestors.mjs.map +1 -1
- package/dist/esm/common/virtualList/scroller.mjs +2 -0
- package/dist/esm/common/virtualList/scroller.mjs.map +1 -0
- package/dist/esm/common/virtualList/useRowDrag.mjs +1 -1
- package/dist/esm/common/virtualList/useRowDrag.mjs.map +1 -1
- package/dist/esm/common/virtualList/useRowNavigation.mjs +1 -1
- package/dist/esm/common/virtualList/useRowNavigation.mjs.map +1 -1
- package/dist/esm/common/virtualList/useVirtualWindow.mjs +1 -1
- package/dist/esm/common/virtualList/useVirtualWindow.mjs.map +1 -1
- package/dist/esm/common/virtualList/virtualList.mjs +1 -1
- package/dist/esm/common/virtualList/virtualList.mjs.map +1 -1
- package/dist/esm/common/virtualList/virtualList.module.mjs +1 -1
- package/dist/esm/common/virtualList/virtualList.module.mjs.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types/common/hooks/index.d.ts +13 -0
- package/dist/types/common/hooks/useHotkeys.d.ts +45 -0
- package/dist/types/common/hooks/useSwipeDismiss.d.ts +6 -2
- package/dist/types/common/loading/index.d.ts +0 -1
- package/dist/types/common/menu/index.d.ts +1 -0
- package/dist/types/common/menu/menu.types.d.ts +9 -3
- package/dist/types/common/menu/menuHotkeys.d.ts +20 -0
- package/dist/types/common/popover/usePopoverPosition.d.ts +1 -0
- package/dist/types/common/sticky/sticky.d.ts +1 -1
- package/dist/types/common/sticky/sticky.types.d.ts +15 -0
- package/dist/types/common/sticky/stickyStack.d.ts +5 -0
- package/dist/types/common/utils/scrollAncestors.d.ts +10 -0
- package/dist/types/common/virtualList/scroller.d.ts +33 -0
- package/dist/types/common/virtualList/useRowDrag.d.ts +6 -1
- package/dist/types/common/virtualList/useRowNavigation.d.ts +11 -1
- package/dist/types/common/virtualList/useVirtualWindow.d.ts +22 -3
- package/dist/types/common/virtualList/virtualList.d.ts +1 -1
- package/dist/types/common/virtualList/virtualList.types.d.ts +43 -3
- package/dist/types/index.d.ts +1 -0
- package/docs/CLAUDE.md +3 -2
- package/docs/Hooks.md +232 -0
- package/docs/Loading.md +56 -15
- package/docs/Menu.md +25 -3
- package/docs/Popover.md +2 -0
- package/docs/Sticky.md +22 -1
- package/docs/VirtualList.md +44 -4
- package/package.json +1 -1
- package/dist/cjs-types/common/loading/iconLoading/iconLoading.d.ts +0 -5
- package/dist/cjs-types/common/loading/iconLoading/index.d.ts +0 -2
- package/dist/esm/common/loading/iconLoading/iconLoading.mjs +0 -2
- package/dist/esm/common/loading/iconLoading/iconLoading.mjs.map +0 -1
- package/dist/esm/common/loading/iconLoading/iconLoading.module.mjs +0 -2
- package/dist/esm/common/loading/iconLoading/iconLoading.module.mjs.map +0 -1
- package/dist/esm/common/loading/iconLoading/index.mjs +0 -2
- package/dist/esm/common/loading/iconLoading/index.mjs.map +0 -1
- package/dist/types/common/loading/iconLoading/iconLoading.d.ts +0 -5
- package/dist/types/common/loading/iconLoading/index.d.ts +0 -2
package/README.md
CHANGED
|
@@ -146,6 +146,7 @@ runs where `@ahrowe/ui` is installed.
|
|
|
146
146
|
| `emptyFloorPlan`, `addWall`, `derivePlan` | The shared data model behind `RoomDrawer` and `RoomViewer` |
|
|
147
147
|
| `FormValidator`, `Validators`, `useFormValidator` | Track and validate a single form field's value, dirty/touched state, and errors |
|
|
148
148
|
| `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 |
|
|
149
|
+
| `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 |
|
|
149
150
|
| `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 |
|
|
150
151
|
| `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 |
|
|
151
152
|
| `Input`, `InputType` | Text input with floating label, validation state, and type variants |
|
|
@@ -153,8 +154,8 @@ runs where `@ahrowe/ui` is installed.
|
|
|
153
154
|
| `InteractableDiv` | Accessible `<div>` that behaves like a button — click and keyboard (Enter/Space) events, with proper ARIA semantics |
|
|
154
155
|
| `KanbanBoard`, `KanbanColumn` | Drag-and-drop board with columns — task management, workflow stages, status boards (e.g. Todo / In Progress / Done) |
|
|
155
156
|
| `KlipyPicker` | GIF picker backed by the Klipy API — chat reactions, message composers, content authoring tools |
|
|
156
|
-
| `
|
|
157
|
-
| `Menu` | A list of actions hanging off a trigger — a row's "more" button, a toolbar overflow, a context menu on a card |
|
|
157
|
+
| `SpinnerLoading` | A loading indicator for async operations: page loads, data fetching, or covering a region while it refreshes |
|
|
158
|
+
| `Menu`, `menuHotkeys` | A list of actions hanging off a trigger — a row's "more" button, a toolbar overflow, a context menu on a card |
|
|
158
159
|
| `Modal` | Full overlay dialog — forms, detail views, confirmations, wizards that need to block the rest of the UI |
|
|
159
160
|
| `MultiSelect` | Pick several options from a list |
|
|
160
161
|
| `NumberInput` | Numeric input with formatting — prices, quantities, percentages, phone numbers with separators |
|
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -10,10 +10,16 @@ export interface MenuAction {
|
|
|
10
10
|
label: ReactNode;
|
|
11
11
|
icon?: IconContent;
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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?:
|
|
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,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>(
|
|
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
|
-
/**
|
|
92
|
-
|
|
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';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dropdown.module.mjs","names":[],"sources":["../../../../package/common/dropdown/dropdown.module.pcss"],"sourcesContent":[".required-mark {\n padding: 0 0 0 4px;\n color: var(--required-mark-color, var(--error-color));\n}\n\n.dropdown {\n position: relative;\n\n /* Nothing inside gives the root a width, so as a flex or grid item it collapsed to the chevron\n and drew its border through the label. `input` and `multiSelect` say the same. */\n width: 100%;\n\n --dropdown-notch-height: 1.75em;\n\n /* See input.module.pcss for why this exists: the visible border lives on `-input`/`-fieldset`\n below, not this root, so a wrapping ButtonGroup's --group-radius-* custom properties (which\n inherit through any nesting depth, unlike border-radius itself) are what let this component\n flatten correctly when it sits inside a ButtonGroup. */\n --dropdown-radius: var(--group-radius-tl, var(--default-border-radius))\n var(--group-radius-tr, var(--default-border-radius)) var(--group-radius-br, var(--default-border-radius))\n var(--group-radius-bl, var(--default-border-radius));\n\n &-label {\n display: flex;\n\n &-column {\n flex-direction: column;\n }\n\n &-text {\n white-space: nowrap;\n margin-right: 5px;\n margin-bottom: 5px;\n display: flex;\n align-items: center;\n color: var(--text-color);\n font-size: 12px;\n position: absolute;\n top: -15px;\n }\n }\n\n &-input {\n position: relative;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n background-color: var(--input-background);\n border-radius: var(--dropdown-radius);\n\n &-plain {\n background: transparent;\n }\n\n &-readOnly {\n cursor: default;\n }\n\n &:focus-within {\n outline: none;\n }\n\n &-button {\n padding: 7px 20% 7px 7.5px !important;\n background-color: transparent;\n border: none;\n font-family: inherit;\n font-size: inherit;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n text-align: left;\n width: 100%;\n line-height: 1.25;\n color: var(--text-color);\n\n &-disabled {\n background-color: var(--disabled-input-color);\n color: var(--disabled-font-color);\n border-radius: var(--dropdown-radius);\n }\n\n &-readOnly {\n cursor: default;\n }\n\n &:focus {\n outline: none;\n }\n\n &-icon {\n float: right;\n background-color: transparent;\n border: none;\n position: absolute;\n outline: none;\n right: var(--spacing-s);\n top: 50%;\n transform: translateY(-50%);\n color: var(--text-color);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n\n &-arrow {\n transition: transform 0.25s;\n pointer-events: none;\n\n &-open {\n transform: rotate(180deg);\n }\n }\n }\n }\n\n /* Chrome, placement and the portal come from the Popover this renders inside.\n What is left is the clipping that keeps the first and last row inside the\n panel's rounded corners. */\n &-list-anim {\n overflow: hidden;\n border-radius: inherit;\n\n &-wider {\n width: max-content !important;\n }\n }\n\n &-list {\n width: 100%;\n display: flex;\n flex-direction: column;\n overflow-y: auto;\n max-height: 10em;\n list-style-type: none;\n\n &-item {\n border: none;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n background-color: var(--background-modal);\n padding: 0.5em;\n text-align: left;\n border-radius: 0;\n transition: background-color 0.3s;\n\n &-active {\n background-color: var(--background-accent);\n }\n\n &-selected {\n font-weight: 500;\n }\n\n &:focus {\n outline: none;\n }\n\n &:hover {\n background-color: var(--background-accent);\n }\n\n &-disabled {\n background-color: var(--disabled-input-color);\n color: var(--disabled-font-color);\n cursor: not-allowed;\n\n &:hover {\n background-color: var(--disabled-input-color);\n }\n }\n }\n }\n }\n\n &-fieldset {\n position: absolute;\n width: 100%;\n margin: 0;\n box-sizing: border-box;\n border-radius: var(--dropdown-radius);\n font-size: 0.8em;\n inset: calc(var(--dropdown-notch-height) / -2) 0 0;\n padding-left: 8px;\n border-width: 1px;\n border-style: solid;\n border-color: var(--border-color);\n pointer-events: none;\n user-select: none;\n\n &-legend {\n display: block;\n max-width: 0;\n transition: max-width 0.2s;\n white-space: nowrap;\n padding: 0;\n\n &-active {\n max-width: 300px;\n }\n\n span {\n line-height: var(--dropdown-notch-height);\n padding: 0 5px;\n opacity: 0;\n }\n\n /* With an empty label the span would still reserve its padding, leaving a gap in\n the fieldset's top border. Collapse it so the notch closes when there's no label. */\n span:empty {\n padding: 0
|
|
1
|
+
{"version":3,"file":"dropdown.module.mjs","names":[],"sources":["../../../../package/common/dropdown/dropdown.module.pcss"],"sourcesContent":[".required-mark {\n padding: 0 0 0 4px;\n color: var(--required-mark-color, var(--error-color));\n}\n\n.dropdown {\n position: relative;\n\n /* Nothing inside gives the root a width, so as a flex or grid item it collapsed to the chevron\n and drew its border through the label. `input` and `multiSelect` say the same. */\n width: 100%;\n\n --dropdown-notch-height: 1.75em;\n\n /* See input.module.pcss for why this exists: the visible border lives on `-input`/`-fieldset`\n below, not this root, so a wrapping ButtonGroup's --group-radius-* custom properties (which\n inherit through any nesting depth, unlike border-radius itself) are what let this component\n flatten correctly when it sits inside a ButtonGroup. */\n --dropdown-radius: var(--group-radius-tl, var(--default-border-radius))\n var(--group-radius-tr, var(--default-border-radius)) var(--group-radius-br, var(--default-border-radius))\n var(--group-radius-bl, var(--default-border-radius));\n\n &-label {\n display: flex;\n\n &-column {\n flex-direction: column;\n }\n\n &-text {\n white-space: nowrap;\n margin-right: 5px;\n margin-bottom: 5px;\n display: flex;\n align-items: center;\n color: var(--text-color);\n font-size: 12px;\n position: absolute;\n top: -15px;\n }\n }\n\n &-input {\n position: relative;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n background-color: var(--input-background);\n border-radius: var(--dropdown-radius);\n\n &-plain {\n background: transparent;\n }\n\n &-readOnly {\n cursor: default;\n }\n\n &:focus-within {\n outline: none;\n }\n\n &-button {\n padding: 7px 20% 7px 7.5px !important;\n background-color: transparent;\n border: none;\n font-family: inherit;\n font-size: inherit;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n text-align: left;\n width: 100%;\n line-height: 1.25;\n color: var(--text-color);\n\n &-disabled {\n background-color: var(--disabled-input-color);\n color: var(--disabled-font-color);\n border-radius: var(--dropdown-radius);\n }\n\n &-readOnly {\n cursor: default;\n }\n\n &:focus {\n outline: none;\n }\n\n &-icon {\n float: right;\n background-color: transparent;\n border: none;\n position: absolute;\n outline: none;\n right: var(--spacing-s);\n top: 50%;\n transform: translateY(-50%);\n color: var(--text-color);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n\n &-arrow {\n transition: transform 0.25s;\n pointer-events: none;\n\n &-open {\n transform: rotate(180deg);\n }\n }\n }\n }\n\n /* Chrome, placement and the portal come from the Popover this renders inside.\n What is left is the clipping that keeps the first and last row inside the\n panel's rounded corners. */\n &-list-anim {\n overflow: hidden;\n border-radius: inherit;\n\n &-wider {\n width: max-content !important;\n }\n }\n\n &-list {\n width: 100%;\n display: flex;\n flex-direction: column;\n overflow-y: auto;\n max-height: 10em;\n list-style-type: none;\n\n &-item {\n border: none;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n background-color: var(--background-modal);\n padding: 0.5em;\n text-align: left;\n border-radius: 0;\n transition: background-color 0.3s;\n\n &-active {\n background-color: var(--background-accent);\n }\n\n &-selected {\n font-weight: 500;\n }\n\n &:focus {\n outline: none;\n }\n\n &:hover {\n background-color: var(--background-accent);\n }\n\n &-disabled {\n background-color: var(--disabled-input-color);\n color: var(--disabled-font-color);\n cursor: not-allowed;\n\n &:hover {\n background-color: var(--disabled-input-color);\n }\n }\n }\n }\n }\n\n &-fieldset {\n position: absolute;\n width: 100%;\n margin: 0;\n box-sizing: border-box;\n border-radius: var(--dropdown-radius);\n font-size: 0.8em;\n inset: calc(var(--dropdown-notch-height) / -2) 0 0;\n padding-left: 8px;\n border-width: 1px;\n border-style: solid;\n border-color: var(--border-color);\n pointer-events: none;\n user-select: none;\n\n &-legend {\n display: block;\n\n /* Not taken from the text: an empty span gets no line box, which raised a label-less\n field's border half a notch. */\n height: var(--dropdown-notch-height);\n max-width: 0;\n transition: max-width 0.2s;\n white-space: nowrap;\n padding: 0;\n\n &-active {\n max-width: 300px;\n }\n\n span {\n line-height: var(--dropdown-notch-height);\n padding: 0 5px;\n opacity: 0;\n }\n\n /* With an empty label the span would still reserve its padding, leaving a gap in\n the fieldset's top border. Collapse it so the notch closes when there's no label. */\n span:empty {\n padding: 0;\n }\n }\n }\n\n &-placeholder {\n position: absolute;\n top: 50%;\n left: 7px;\n transform: translateY(-50%);\n transition: all 0.2s;\n opacity: 0;\n\n &-shown {\n animation: 0.2s fadeIn 0.3s forwards;\n }\n }\n\n &-matLabel {\n position: absolute;\n transition: all 0.3s;\n left: 7px;\n right: 0;\n top: 50%;\n transform: translateY(-50%);\n font-size: 1em;\n color: var(--input-label-resting-color, var(--text-light));\n pointer-events: none;\n user-select: none;\n\n &-floating {\n inset: 0 0 auto 14px;\n font-size: 0.8em;\n color: var(--input-label-floating-color, var(--text-color));\n transform: translateY(-50%);\n }\n }\n\n @keyframes fadeIn {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 0.5;\n }\n }\n\n &-border {\n &-focused {\n outline-color: var(--focus-border-color, var(--primary-color));\n border-color: var(--focus-border-color, var(--primary-color));\n }\n\n &-plain:not(&-focused) {\n border-color: transparent;\n outline-color: transparent;\n }\n\n &-invalid {\n outline-color: var(--error-border-color);\n border-color: var(--error-border-color);\n }\n\n &-invalid:hover {\n outline-color: var(--error-border-color);\n border-color: var(--error-border-color);\n }\n\n &-error {\n outline-color: var(--error-border-color);\n border-color: var(--error-border-color);\n }\n }\n}\n\n/* The list brings its own padding and its rows reach the panel edge. */\n.dropdownPanel {\n padding: 0;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dropdown-matLabel,\n .dropdown-placeholder,\n .dropdown-input-button-icon-arrow {\n transition: none;\n }\n}\n\n/* The sentinel the infinite-scroll hook watches, and the spinner shown while a page loads.\n These came with `InfiniteBlock` when the list rendered through it; the list is its own scroll\n container now, so they live here. */\n.dropdown-sentinel {\n height: 1px;\n}\n\n.dropdown-load-more {\n display: flex;\n justify-content: center;\n align-items: center;\n padding: 4px 0;\n\n &-spinner {\n width: 32px;\n height: 32px;\n }\n}\n"],"mappings":""}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";import{useEffect as e,useRef as t}from"react";var n=`input, textarea, select, [contenteditable=""], [contenteditable="true"]`,r={esc:`escape`,space:` `,spacebar:` `,plus:`+`,up:`arrowup`,down:`arrowdown`,left:`arrowleft`,right:`arrowright`};function i(){return typeof navigator<`u`&&/Mac|iPhone|iPad|iPod/i.test(navigator.userAgent)}function a(e){let t=e.split(`+`).map(e=>e.trim().toLowerCase()).filter(Boolean);if(t.length===0)return null;let n={key:``,ctrl:!1,meta:!1,alt:!1,shift:!1,shiftNamed:!1},a=i();for(let e of t)switch(e){case`mod`:a?n.meta=!0:n.ctrl=!0;break;case`ctrl`:case`control`:n.ctrl=!0;break;case`meta`:case`cmd`:case`command`:n.meta=!0;break;case`alt`:case`option`:n.alt=!0;break;case`shift`:n.shift=!0,n.shiftNamed=!0;break;default:n.key=r[e]??e}return n.key?n:null}function o(e){let t=[e.key.toLowerCase()],n=/^(?:Key|Digit)(.)$/.exec(e.code);return n&&t.push(n[1].toLowerCase()),t}function s(e,t){return e.ctrl!==t.ctrlKey||e.meta!==t.metaKey||e.alt!==t.altKey||e.shiftNamed&&e.shift!==t.shiftKey?!1:o(t).includes(e.key)}function c(e){return e instanceof Element&&!!e.closest(n)}var l=new Set([`arrowup`,`arrowdown`,`arrowleft`,`arrowright`,`home`,`end`,`pageup`,`pagedown`,`backspace`,`delete`,`enter`,`tab`]);function u(e){return e.ctrl||e.meta||e.alt?!1:e.key.length===1||l.has(e.key)}var d={" ":`Space`,arrowup:`↑`,arrowdown:`↓`,arrowleft:`←`,arrowright:`→`,escape:`Esc`,enter:`↵`,backspace:`⌫`,delete:`Del`,pageup:`PgUp`,pagedown:`PgDn`};function f(e){let t=a(e);if(!t)return e;let n=d[t.key]??(t.key.length===1?t.key.toUpperCase():p(t.key));return i()?[t.ctrl&&`⌃`,t.alt&&`⌥`,t.shift&&`⇧`,t.meta&&`⌘`].filter(Boolean).join(``)+n:[...[t.ctrl&&`Ctrl`,t.meta&&`Win`,t.alt&&`Alt`,t.shift&&`Shift`].filter(Boolean),n].join(`+`)}function p(e){return e[0].toUpperCase()+e.slice(1)}function m(n,r={}){let{enabled:i=!0,target:o,enableInFields:l=!1,preventDefault:d=!0}=r,f=t(n);e(()=>{f.current=n}),e(()=>{if(!i)return;let e=o?o.current:window;if(!e)return;function t(e){let t=e;if(!t.isComposing)for(let[e,n]of Object.entries(f.current)){let r=a(e);if(r&&s(r,t)&&!(u(r)&&!l&&c(t.target))){d&&t.preventDefault(),n(t);return}}}return e.addEventListener(`keydown`,t),()=>e.removeEventListener(`keydown`,t)},[i,o,l,d])}export{m as default,m as useHotkeys,f as formatHotkey};
|
|
2
|
+
//# sourceMappingURL=useHotkeys.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useHotkeys.mjs","names":[],"sources":["../../../../package/common/hooks/useHotkeys.ts"],"sourcesContent":["import { useEffect, useRef, type RefObject } from 'react';\n\nexport type HotkeyHandler = (event: KeyboardEvent) => void;\n\n/** Combo → handler, e.g. `{ 'mod+k': open, Escape: close }`. */\nexport type HotkeyMap = Record<string, HotkeyHandler>;\n\nexport interface UseHotkeysOptions {\n /** Attach the listener at all. Default `true`; pass `false` to suspend every combo at once. */\n enabled?: boolean;\n /** Scope the shortcuts to one element instead of the whole window. */\n target?: RefObject<HTMLElement | null>;\n /**\n * Let plain-key combos fire while a text field or `contenteditable` has focus. Default `false`,\n * so `'n'` does not fire for every `n` typed into an input. Combos carrying Ctrl, Cmd or Alt\n * are never suppressed — they cannot be confused with typing.\n */\n enableInFields?: boolean;\n /**\n * Call `preventDefault()` on a match, so `mod+s` does not also open the browser's save dialog.\n * Default `true`.\n */\n preventDefault?: boolean;\n}\n\ninterface Combo {\n key: string;\n ctrl: boolean;\n meta: boolean;\n alt: boolean;\n shift: boolean;\n /** Set when the combo named Shift itself — see `matches`. */\n shiftNamed: boolean;\n}\n\nconst FIELD_SELECTOR = 'input, textarea, select, [contenteditable=\"\"], [contenteditable=\"true\"]';\n\n/** Spellings accepted for keys whose `event.key` is awkward to write inside a `+`-separated combo. */\nconst KEY_ALIASES: Record<string, string> = {\n esc: 'escape',\n space: ' ',\n spacebar: ' ',\n plus: '+',\n up: 'arrowup',\n down: 'arrowdown',\n left: 'arrowleft',\n right: 'arrowright',\n};\n\n/** `mod` is Cmd where Cmd is the command modifier, and Ctrl everywhere else. */\nfunction isApplePlatform(): boolean {\n return typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent);\n}\n\nfunction parseCombo(combo: string): Combo | null {\n const parts = combo\n .split('+')\n .map((part) => part.trim().toLowerCase())\n .filter(Boolean);\n // A trailing separator (`'mod+'`) loses its key to the filter above, so the whole combo is\n // dropped rather than matching on the modifier alone. `'mod+plus'` is how to mean `+` itself.\n if (parts.length === 0) return null;\n\n const parsed: Combo = { key: '', ctrl: false, meta: false, alt: false, shift: false, shiftNamed: false };\n const apple = isApplePlatform();\n\n for (const part of parts) {\n switch (part) {\n case 'mod':\n if (apple) parsed.meta = true;\n else parsed.ctrl = true;\n break;\n case 'ctrl':\n case 'control':\n parsed.ctrl = true;\n break;\n case 'meta':\n case 'cmd':\n case 'command':\n parsed.meta = true;\n break;\n case 'alt':\n case 'option':\n parsed.alt = true;\n break;\n case 'shift':\n parsed.shift = true;\n parsed.shiftNamed = true;\n break;\n default:\n parsed.key = KEY_ALIASES[part] ?? part;\n }\n }\n\n return parsed.key ? parsed : null;\n}\n\n/**\n * The key the event carries, plus the one its physical position would produce unshifted.\n *\n * Both are needed: Alt+K on a Mac arrives as `key: '˚'` and Shift+1 as `key: '!'`, so a combo\n * written against the letter or digit on the keycap only matches through `code`.\n */\nfunction pressedKeys(event: KeyboardEvent): string[] {\n const keys = [event.key.toLowerCase()];\n const physical = /^(?:Key|Digit)(.)$/.exec(event.code);\n if (physical) keys.push(physical[1].toLowerCase());\n return keys;\n}\n\nfunction matches(combo: Combo, event: KeyboardEvent): boolean {\n if (combo.ctrl !== event.ctrlKey || combo.meta !== event.metaKey || combo.alt !== event.altKey) return false;\n // Shift is only compared when the combo named it. A symbol is typed *with* Shift on most\n // layouts, so requiring `shiftKey === false` would make `'?'` and `'mod+/'` unreachable.\n if (combo.shiftNamed && combo.shift !== event.shiftKey) return false;\n return pressedKeys(event).includes(combo.key);\n}\n\nfunction isInField(target: EventTarget | null): boolean {\n return target instanceof Element && !!target.closest(FIELD_SELECTOR);\n}\n\n/** Keys a text field uses for itself, beyond the characters it inserts. */\nconst FIELD_KEYS = new Set([\n 'arrowup',\n 'arrowdown',\n 'arrowleft',\n 'arrowright',\n 'home',\n 'end',\n 'pageup',\n 'pagedown',\n 'backspace',\n 'delete',\n 'enter',\n 'tab',\n]);\n\n/**\n * Whether a combo would be taken out of a focused field's mouth.\n *\n * Ctrl, Cmd and Alt take a key out of the field's hands whatever it is. Without one, a single\n * character is typing and the listed keys are editing, but `F2` and `Escape` are neither, and\n * suppressing those made a menu's `F2` do nothing the moment the page had a focused input.\n */\nfunction isFieldKey(combo: Combo): boolean {\n if (combo.ctrl || combo.meta || combo.alt) return false;\n return combo.key.length === 1 || FIELD_KEYS.has(combo.key);\n}\n\n/** Keys whose name is not what a keycap says. Anything else is capitalised, so `f2` reads `F2`. */\nconst KEY_LABELS: Record<string, string> = {\n ' ': 'Space',\n arrowup: '↑',\n arrowdown: '↓',\n arrowleft: '←',\n arrowright: '→',\n escape: 'Esc',\n enter: '↵',\n backspace: '⌫',\n delete: 'Del',\n pageup: 'PgUp',\n pagedown: 'PgDn',\n};\n\n/**\n * A combo written for the reader: `'mod+shift+k'` reads `⇧⌘K` on an Apple platform and\n * `Ctrl+Shift+K` everywhere else.\n *\n * So a shortcut is written once, in one spelling, and the hint beside it cannot contradict what\n * the binding actually listens for. Hand-written `'⌘K'` hints are wrong on every other platform,\n * silently, because nothing compares the label to the binding.\n *\n * Resolved from the platform at call time, so a server-rendered page renders the non-Apple form.\n */\nexport function formatHotkey(combo: string): string {\n const parsed = parseCombo(combo);\n if (!parsed) return combo;\n\n const key = KEY_LABELS[parsed.key] ?? (parsed.key.length === 1 ? parsed.key.toUpperCase() : capitalise(parsed.key));\n\n // Apple orders modifier symbols ⌃⌥⇧⌘ and runs them into the key; everywhere else they are\n // words joined by `+`.\n if (isApplePlatform()) {\n const symbols = [parsed.ctrl && '⌃', parsed.alt && '⌥', parsed.shift && '⇧', parsed.meta && '⌘'];\n return symbols.filter(Boolean).join('') + key;\n }\n const words = [parsed.ctrl && 'Ctrl', parsed.meta && 'Win', parsed.alt && 'Alt', parsed.shift && 'Shift'];\n return [...words.filter(Boolean), key].join('+');\n}\n\nfunction capitalise(value: string): string {\n return value[0].toUpperCase() + value.slice(1);\n}\n\n/**\n * Keyboard shortcuts, for a whole app or for one element.\n *\n * ```tsx\n * useHotkeys({ 'mod+k': () => setPaletteOpen(true), '?': () => setHelpOpen(true) });\n * ```\n *\n * The map is read through a ref at event time, so an inline object literal is fine and no handler\n * needs memoising. Shortcuts keep firing while a Modal or Drawer is open — pass `enabled` for the\n * ones that should not.\n */\nexport function useHotkeys(hotkeys: HotkeyMap, options: UseHotkeysOptions = {}): void {\n const { enabled = true, target, enableInFields = false, preventDefault = true } = options;\n\n const hotkeysRef = useRef(hotkeys);\n useEffect(() => {\n hotkeysRef.current = hotkeys;\n });\n\n useEffect(() => {\n if (!enabled) return;\n const node: HTMLElement | Window | null = target ? target.current : window;\n if (!node) return;\n\n function onKeyDown(event: Event) {\n const keyboardEvent = event as KeyboardEvent;\n // A key pressed to compose a character in an IME is not a shortcut.\n if (keyboardEvent.isComposing) return;\n\n for (const [spelling, handler] of Object.entries(hotkeysRef.current)) {\n const combo = parseCombo(spelling);\n if (!combo || !matches(combo, keyboardEvent)) continue;\n if (isFieldKey(combo) && !enableInFields && isInField(keyboardEvent.target)) continue;\n if (preventDefault) keyboardEvent.preventDefault();\n handler(keyboardEvent);\n return;\n }\n }\n\n node.addEventListener('keydown', onKeyDown);\n return () => node.removeEventListener('keydown', onKeyDown);\n }, [enabled, target, enableInFields, preventDefault]);\n}\n\nexport default useHotkeys;\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use client";import e from"./useCoarsePointer.mjs";import{
|
|
1
|
+
"use client";import e from"./useCoarsePointer.mjs";import{useCallback as t,useEffect as n,useRef as r,useState as i}from"react";var a=180,o=10;function s({onDismiss:s,axis:c=`x`,distanceThreshold:l=80,velocityThreshold:u=.5,disabled:d=!1,mobileOnly:f=!0}){let[p,m]=i(null),h=t(e=>m(e),[]),[g,_]=i(!1),v=e(),y=d||f&&!v,b=r(null),x=r(s);return n(()=>{x.current=s}),n(()=>{let e=p;if(!e||y)return;function t(t,n){if(!e)return;n!==`none`&&e.offsetHeight;let r=c===`x`?`translateX(${t}px)`:`translateY(${t}px)`;e.style.transition=n,e.style.transform=r;let i=Math.max(0,1-Math.abs(t)/(l*2.5));e.style.opacity=String(i)}function n(e){let n=b.current;if(!n||e.pointerId!==n.pointerId)return;let r=e.clientX-n.startX,i=e.clientY-n.startY;if(!n.axisLocked){let e=c===`x`?r:i,t=c===`x`?i:r;if(Math.abs(e)<o&&Math.abs(t)<o)return;if(Math.abs(t)>Math.abs(e)){n.active=!1;return}n.axisLocked=!0,_(!0)}n.active&&(e.preventDefault(),n.offset=c===`x`?r:i,t(n.offset,`none`))}function r(){let i=b.current;if(b.current=null,window.removeEventListener(`pointermove`,n),window.removeEventListener(`pointerup`,r),window.removeEventListener(`pointercancel`,r),!i||!i.active||!i.axisLocked){_(!1);return}let o=Math.max(1,Date.now()-i.startTime),s=i.offset/o;if(Math.abs(i.offset)>=l||Math.abs(s)>=u){let n=e?.getBoundingClientRect().width??0;t(Math.sign(i.offset||1)*(n+40),`transform ${a}ms ease-out, opacity ${a}ms ease-out`),setTimeout(()=>x.current(),a)}else t(0,`transform ${a}ms ease-out, opacity ${a}ms ease-out`);_(!1)}function i(t){!b.current&&e&&(e.style.animation=`none`,b.current={pointerId:t.pointerId,startX:t.clientX,startY:t.clientY,startTime:Date.now(),offset:0,axisLocked:!1,active:!0},window.addEventListener(`pointermove`,n,{passive:!1}),window.addEventListener(`pointerup`,r),window.addEventListener(`pointercancel`,r))}return e.addEventListener(`pointerdown`,i),()=>{e.removeEventListener(`pointerdown`,i),window.removeEventListener(`pointermove`,n),window.removeEventListener(`pointerup`,r),window.removeEventListener(`pointercancel`,r)}},[p,c,l,u,y]),{ref:h,isDragging:g}}export{s as useSwipeDismiss};
|
|
2
2
|
//# sourceMappingURL=useSwipeDismiss.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSwipeDismiss.mjs","names":[],"sources":["../../../../package/common/hooks/useSwipeDismiss.ts"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\n\nimport { useCoarsePointer } from './useCoarsePointer';\n\nexport interface UseSwipeDismissOptions {\n /** Called once the swipe commits (past the threshold) — after the exit transition finishes,\n * not the instant the pointer is released. */\n onDismiss: () => void;\n /** Which axis the drag tracks (default `'x'`, the natural direction for a toast/notification). */\n axis?: 'x' | 'y';\n /** Distance in px past which releasing counts as a commit, regardless of speed (default `80`). */\n distanceThreshold?: number;\n /** Speed in px/ms past which releasing counts as a commit, regardless of distance (default `0.5`) —\n * lets a fast flick dismiss even if the pointer didn't travel very far. */\n velocityThreshold?: number;\n /** Disables the gesture entirely — no listeners are attached. */\n disabled?: boolean;\n /** Restricts the gesture to coarse (touch) pointers, auto-detected via `matchMedia`. On a\n * mouse/trackpad device a drag is more likely to be a text selection than an intentional\n * dismiss, and other affordances (a close button, auto-dismiss) typically already cover that\n * case. Default `true`; set `false` to allow the gesture on any pointer type. */\n mobileOnly?: boolean;\n}\n\nexport interface UseSwipeDismissResult<T extends HTMLElement> {\n /** Attach to the element that should become draggable. The hook wires up its own pointer\n * listeners on this node directly (no handlers to spread), so this is the only thing to attach. */\n ref: React.RefObject<T | null>;\n /** True for the duration of an active drag — e.g. use it to pause an auto-dismiss timer. */\n isDragging: boolean;\n}\n\nconst EXIT_TRANSITION_MS = 180;\n// Below this many px of movement, direction intent isn't decided yet — lets a vertical scroll\n// gesture that merely starts on the element pass through instead of being hijacked as a swipe.\nconst AXIS_LOCK_THRESHOLD = 10;\n\n/**\n * Drag-to-dismiss on any pointer type (touch, mouse, pen) via the Pointer Events API. Applies the\n * drag offset and a fading opacity directly to the DOM node (bypassing React state) so the visual\n * tracks the pointer at native speed, then either springs back or animates fully off-screen and\n * calls `onDismiss` once that exit transition completes.\n */\nexport function useSwipeDismiss<T extends HTMLElement>({\n onDismiss,\n axis = 'x',\n distanceThreshold = 80,\n velocityThreshold = 0.5,\n disabled = false,\n mobileOnly = true,\n}: UseSwipeDismissOptions): UseSwipeDismissResult<T> {\n const ref = useRef<T>(null);\n const [isDragging, setIsDragging] = useState(false);\n\n const isCoarsePointer = useCoarsePointer();\n\n const effectiveDisabled = disabled || (mobileOnly && !isCoarsePointer);\n\n // Mutable across the life of one drag; doesn't need to trigger re-renders.\n const dragRef = useRef<{\n pointerId: number;\n startX: number;\n startY: number;\n startTime: number;\n offset: number;\n axisLocked: boolean;\n active: boolean;\n } | null>(null);\n\n const onDismissRef = useRef(onDismiss);\n useEffect(() => {\n onDismissRef.current = onDismiss;\n });\n\n useEffect(() => {\n const el = ref.current;\n if (!el || effectiveDisabled) return;\n\n function applyTransform(offset: number, transition: string) {\n if (!el) return;\n if (transition !== 'none') {\n // Switching straight from the live drag's `transition: none` to an animated one, in the\n // same tick as the transform change, can get coalesced into a single frame with no\n // animation in between — the element just jumps to its end state instead of sliding\n // there. Forcing a layout read first commits the current (pre-transition) position as\n // its own frame, so the transition below actually has something to animate from.\n void el.offsetHeight;\n }\n const translate = axis === 'x' ? `translateX(${offset}px)` : `translateY(${offset}px)`;\n el.style.transition = transition;\n el.style.transform = translate;\n const fade = Math.max(0, 1 - Math.abs(offset) / (distanceThreshold * 2.5));\n el.style.opacity = String(fade);\n }\n\n function onPointerMove(event: PointerEvent) {\n const drag = dragRef.current;\n if (!drag || event.pointerId !== drag.pointerId) return;\n\n const deltaX = event.clientX - drag.startX;\n const deltaY = event.clientY - drag.startY;\n\n if (!drag.axisLocked) {\n const primary = axis === 'x' ? deltaX : deltaY;\n const cross = axis === 'x' ? deltaY : deltaX;\n if (Math.abs(primary) < AXIS_LOCK_THRESHOLD && Math.abs(cross) < AXIS_LOCK_THRESHOLD) return;\n if (Math.abs(cross) > Math.abs(primary)) {\n // The gesture is mostly along the other axis (e.g. a page scroll) — hand it back.\n drag.active = false;\n return;\n }\n drag.axisLocked = true;\n setIsDragging(true);\n }\n\n if (!drag.active) return;\n event.preventDefault();\n drag.offset = axis === 'x' ? deltaX : deltaY;\n applyTransform(drag.offset, 'none');\n }\n\n function finishDrag() {\n const drag = dragRef.current;\n dragRef.current = null;\n window.removeEventListener('pointermove', onPointerMove);\n window.removeEventListener('pointerup', finishDrag);\n window.removeEventListener('pointercancel', finishDrag);\n if (!drag || !drag.active || !drag.axisLocked) {\n setIsDragging(false);\n return;\n }\n\n const elapsed = Math.max(1, Date.now() - drag.startTime);\n const velocity = drag.offset / elapsed;\n const committed = Math.abs(drag.offset) >= distanceThreshold || Math.abs(velocity) >= velocityThreshold;\n\n if (committed) {\n const width = el?.getBoundingClientRect().width ?? 0;\n const exitOffset = Math.sign(drag.offset || 1) * (width + 40);\n applyTransform(\n exitOffset,\n `transform ${EXIT_TRANSITION_MS}ms ease-out, opacity ${EXIT_TRANSITION_MS}ms ease-out`,\n );\n setTimeout(() => onDismissRef.current(), EXIT_TRANSITION_MS);\n } else {\n applyTransform(0, `transform ${EXIT_TRANSITION_MS}ms ease-out, opacity ${EXIT_TRANSITION_MS}ms ease-out`);\n }\n setIsDragging(false);\n }\n\n function onPointerDown(event: PointerEvent) {\n if (dragRef.current || !el) return;\n // A still-\"forwards\"-filling CSS animation (e.g. the toast's entrance slide) wins the\n // cascade over an inline style.transform, so the drag offset below would silently never\n // show up on screen unless the animation is cleared first.\n el.style.animation = 'none';\n dragRef.current = {\n pointerId: event.pointerId,\n startX: event.clientX,\n startY: event.clientY,\n startTime: Date.now(),\n offset: 0,\n axisLocked: false,\n active: true,\n };\n window.addEventListener('pointermove', onPointerMove, { passive: false });\n window.addEventListener('pointerup', finishDrag);\n window.addEventListener('pointercancel', finishDrag);\n }\n\n el.addEventListener('pointerdown', onPointerDown);\n return () => {\n el.removeEventListener('pointerdown', onPointerDown);\n window.removeEventListener('pointermove', onPointerMove);\n window.removeEventListener('pointerup', finishDrag);\n window.removeEventListener('pointercancel', finishDrag);\n };\n }, [axis, distanceThreshold, velocityThreshold, effectiveDisabled]);\n\n return { ref, isDragging };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useSwipeDismiss.mjs","names":[],"sources":["../../../../package/common/hooks/useSwipeDismiss.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { useCoarsePointer } from './useCoarsePointer';\n\nexport interface UseSwipeDismissOptions {\n /** Called once the swipe commits (past the threshold) — after the exit transition finishes,\n * not the instant the pointer is released. */\n onDismiss: () => void;\n /** Which axis the drag tracks (default `'x'`, the natural direction for a toast/notification). */\n axis?: 'x' | 'y';\n /** Distance in px past which releasing counts as a commit, regardless of speed (default `80`). */\n distanceThreshold?: number;\n /** Speed in px/ms past which releasing counts as a commit, regardless of distance (default `0.5`) —\n * lets a fast flick dismiss even if the pointer didn't travel very far. */\n velocityThreshold?: number;\n /** Disables the gesture entirely — no listeners are attached. */\n disabled?: boolean;\n /** Restricts the gesture to coarse (touch) pointers, auto-detected via `matchMedia`. On a\n * mouse/trackpad device a drag is more likely to be a text selection than an intentional\n * dismiss, and other affordances (a close button, auto-dismiss) typically already cover that\n * case. Default `true`; set `false` to allow the gesture on any pointer type. */\n mobileOnly?: boolean;\n}\n\nexport interface UseSwipeDismissResult<T extends HTMLElement> {\n /** Attach to the element that should become draggable. The hook wires up its own pointer\n * listeners on this node directly (no handlers to spread), so this is the only thing to attach.\n *\n * A callback ref rather than an object one, because the hook has to know when the node changes:\n * an element that is unmounted and rendered again is a *different* node, and a ref object cannot\n * say so. To keep a ref of your own on the element, call both from one callback. */\n ref: React.RefCallback<T>;\n /** True for the duration of an active drag — e.g. use it to pause an auto-dismiss timer. */\n isDragging: boolean;\n}\n\nconst EXIT_TRANSITION_MS = 180;\n// Below this many px of movement, direction intent isn't decided yet — lets a vertical scroll\n// gesture that merely starts on the element pass through instead of being hijacked as a swipe.\nconst AXIS_LOCK_THRESHOLD = 10;\n\n/**\n * Drag-to-dismiss on any pointer type (touch, mouse, pen) via the Pointer Events API. Applies the\n * drag offset and a fading opacity directly to the DOM node (bypassing React state) so the visual\n * tracks the pointer at native speed, then either springs back or animates fully off-screen and\n * calls `onDismiss` once that exit transition completes.\n */\nexport function useSwipeDismiss<T extends HTMLElement>({\n onDismiss,\n axis = 'x',\n distanceThreshold = 80,\n velocityThreshold = 0.5,\n disabled = false,\n mobileOnly = true,\n}: UseSwipeDismissOptions): UseSwipeDismissResult<T> {\n // The node lives in state, not in a ref: the listeners have to be re-attached when the element\n // is replaced, and a ref object's `current` changing never re-runs the effect below. Rendering\n // the element again after it was unmounted otherwise left the gesture wired to the old node.\n const [node, setNode] = useState<T | null>(null);\n const ref = useCallback((element: T | null) => setNode(element), []);\n const [isDragging, setIsDragging] = useState(false);\n\n const isCoarsePointer = useCoarsePointer();\n\n const effectiveDisabled = disabled || (mobileOnly && !isCoarsePointer);\n\n // Mutable across the life of one drag; doesn't need to trigger re-renders.\n const dragRef = useRef<{\n pointerId: number;\n startX: number;\n startY: number;\n startTime: number;\n offset: number;\n axisLocked: boolean;\n active: boolean;\n } | null>(null);\n\n const onDismissRef = useRef(onDismiss);\n useEffect(() => {\n onDismissRef.current = onDismiss;\n });\n\n useEffect(() => {\n const el = node;\n if (!el || effectiveDisabled) return;\n\n function applyTransform(offset: number, transition: string) {\n if (!el) return;\n if (transition !== 'none') {\n // Switching straight from the live drag's `transition: none` to an animated one, in the\n // same tick as the transform change, can get coalesced into a single frame with no\n // animation in between — the element just jumps to its end state instead of sliding\n // there. Forcing a layout read first commits the current (pre-transition) position as\n // its own frame, so the transition below actually has something to animate from.\n void el.offsetHeight;\n }\n const translate = axis === 'x' ? `translateX(${offset}px)` : `translateY(${offset}px)`;\n el.style.transition = transition;\n el.style.transform = translate;\n const fade = Math.max(0, 1 - Math.abs(offset) / (distanceThreshold * 2.5));\n el.style.opacity = String(fade);\n }\n\n function onPointerMove(event: PointerEvent) {\n const drag = dragRef.current;\n if (!drag || event.pointerId !== drag.pointerId) return;\n\n const deltaX = event.clientX - drag.startX;\n const deltaY = event.clientY - drag.startY;\n\n if (!drag.axisLocked) {\n const primary = axis === 'x' ? deltaX : deltaY;\n const cross = axis === 'x' ? deltaY : deltaX;\n if (Math.abs(primary) < AXIS_LOCK_THRESHOLD && Math.abs(cross) < AXIS_LOCK_THRESHOLD) return;\n if (Math.abs(cross) > Math.abs(primary)) {\n // The gesture is mostly along the other axis (e.g. a page scroll) — hand it back.\n drag.active = false;\n return;\n }\n drag.axisLocked = true;\n setIsDragging(true);\n }\n\n if (!drag.active) return;\n event.preventDefault();\n drag.offset = axis === 'x' ? deltaX : deltaY;\n applyTransform(drag.offset, 'none');\n }\n\n function finishDrag() {\n const drag = dragRef.current;\n dragRef.current = null;\n window.removeEventListener('pointermove', onPointerMove);\n window.removeEventListener('pointerup', finishDrag);\n window.removeEventListener('pointercancel', finishDrag);\n if (!drag || !drag.active || !drag.axisLocked) {\n setIsDragging(false);\n return;\n }\n\n const elapsed = Math.max(1, Date.now() - drag.startTime);\n const velocity = drag.offset / elapsed;\n const committed = Math.abs(drag.offset) >= distanceThreshold || Math.abs(velocity) >= velocityThreshold;\n\n if (committed) {\n const width = el?.getBoundingClientRect().width ?? 0;\n const exitOffset = Math.sign(drag.offset || 1) * (width + 40);\n applyTransform(\n exitOffset,\n `transform ${EXIT_TRANSITION_MS}ms ease-out, opacity ${EXIT_TRANSITION_MS}ms ease-out`,\n );\n setTimeout(() => onDismissRef.current(), EXIT_TRANSITION_MS);\n } else {\n applyTransform(0, `transform ${EXIT_TRANSITION_MS}ms ease-out, opacity ${EXIT_TRANSITION_MS}ms ease-out`);\n }\n setIsDragging(false);\n }\n\n function onPointerDown(event: PointerEvent) {\n if (dragRef.current || !el) return;\n // A still-\"forwards\"-filling CSS animation (e.g. the toast's entrance slide) wins the\n // cascade over an inline style.transform, so the drag offset below would silently never\n // show up on screen unless the animation is cleared first.\n el.style.animation = 'none';\n dragRef.current = {\n pointerId: event.pointerId,\n startX: event.clientX,\n startY: event.clientY,\n startTime: Date.now(),\n offset: 0,\n axisLocked: false,\n active: true,\n };\n window.addEventListener('pointermove', onPointerMove, { passive: false });\n window.addEventListener('pointerup', finishDrag);\n window.addEventListener('pointercancel', finishDrag);\n }\n\n el.addEventListener('pointerdown', onPointerDown);\n return () => {\n el.removeEventListener('pointerdown', onPointerDown);\n window.removeEventListener('pointermove', onPointerMove);\n window.removeEventListener('pointerup', finishDrag);\n window.removeEventListener('pointercancel', finishDrag);\n };\n }, [node, axis, distanceThreshold, velocityThreshold, effectiveDisabled]);\n\n return { ref, isDragging };\n}\n"],"mappings":"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"}
|