@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/docs/Hooks.md
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Hooks
|
|
2
|
+
|
|
3
|
+
**When to use:** 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.
|
|
4
|
+
|
|
5
|
+
**Keywords:** hotkey, keybinding, keyboard shortcut, cmd+k, command palette, breakpoint, responsive, touch device, on-screen keyboard, focus trap, scroll lock, swipe
|
|
6
|
+
|
|
7
|
+
**Import:** `import { useHotkeys, formatHotkey, useMediaQuery, useCoarsePointer, useVisualViewportSize, useScrollEdges, useSwipeDismiss, useOverlay, tabbablesIn } from '@ahrowe/ui'`
|
|
8
|
+
**Types:** `import type { HotkeyMap, HotkeyHandler, UseHotkeysOptions, VisualViewportSize, ScrollEdges, ScrollEdgesOptions, UseSwipeDismissOptions, UseSwipeDismissResult, UseOverlayArgs } from '@ahrowe/ui'`
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## useHotkeys
|
|
13
|
+
|
|
14
|
+
Keyboard shortcuts for a whole app or for one element.
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import { useHotkeys } from '@ahrowe/ui';
|
|
18
|
+
|
|
19
|
+
function App() {
|
|
20
|
+
const [isPaletteOpen, setPaletteOpen] = useState(false);
|
|
21
|
+
|
|
22
|
+
useHotkeys({
|
|
23
|
+
'mod+k': () => setPaletteOpen(true),
|
|
24
|
+
'mod+shift+d': () => toggleDarkMode(),
|
|
25
|
+
'?': () => setHelpOpen(true),
|
|
26
|
+
Escape: () => setPaletteOpen(false),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The map is read through a ref at event time, so an inline object literal is fine: no handler needs
|
|
32
|
+
`useCallback` and no combo needs `useMemo`.
|
|
33
|
+
|
|
34
|
+
**Combo syntax** — modifiers and the key joined by `+`, case-insensitive:
|
|
35
|
+
|
|
36
|
+
| Part | Means |
|
|
37
|
+
| ---- | ----- |
|
|
38
|
+
| `mod` | Cmd on Apple platforms, Ctrl everywhere else |
|
|
39
|
+
| `ctrl` / `control` | Ctrl |
|
|
40
|
+
| `meta` / `cmd` / `command` | Cmd (Win key) |
|
|
41
|
+
| `alt` / `option` | Alt |
|
|
42
|
+
| `shift` | Shift |
|
|
43
|
+
| the key | any `event.key` value, lower-cased: `k`, `1`, `escape`, `arrowup`, `enter` |
|
|
44
|
+
|
|
45
|
+
Aliases for keys that are awkward to write: `esc`, `space`, `plus`, `up`, `down`, `left`, `right`.
|
|
46
|
+
|
|
47
|
+
Two rules worth knowing:
|
|
48
|
+
|
|
49
|
+
- **Shift is only compared when the combo names it.** `'?'` matches, even though `?` is typed with
|
|
50
|
+
Shift on most layouts.
|
|
51
|
+
- **The key is matched against the keycap as well as the event.** `'mod+1'` matches Ctrl+Shift+1,
|
|
52
|
+
and `'alt+k'` matches on a Mac, where Alt+K produces `˚` rather than `k`.
|
|
53
|
+
|
|
54
|
+
**Options**
|
|
55
|
+
|
|
56
|
+
| Prop | Type | Default | Description |
|
|
57
|
+
| ---- | ---- | ------- | ----------- |
|
|
58
|
+
| `enabled` | `boolean` | `true` | Attach the listener at all. Suspends every combo at once |
|
|
59
|
+
| `target` | `RefObject<HTMLElement \| null>` | window | Scope the shortcuts to one element |
|
|
60
|
+
| `enableInFields` | `boolean` | `false` | Let a combo fire while a text field or `contenteditable` has focus. Only combos the field would otherwise consume are held back: a single character, or an editing key (`arrowup`, `home`, `backspace`, `enter`, `tab`, …). Anything carrying Ctrl, Cmd or Alt fires regardless, and so do `f2`, `escape` and the other keys a field does not use |
|
|
61
|
+
| `preventDefault` | `boolean` | `true` | Call `preventDefault()` on a match, so `mod+s` does not also open the browser's save dialog |
|
|
62
|
+
|
|
63
|
+
Shortcuts keep firing while a `Modal` or `Drawer` is open, which is usually what a command palette
|
|
64
|
+
wants. Pass `enabled` for the ones that should not:
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
useHotkeys({ n: createRecord }, { enabled: !isModalOpen });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A held key repeats, as it does natively. Check `event.repeat` in the handler to ignore repeats.
|
|
71
|
+
|
|
72
|
+
### formatHotkey
|
|
73
|
+
|
|
74
|
+
The same combo written for the reader, so a hint beside a button cannot contradict what the binding
|
|
75
|
+
listens for:
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
formatHotkey('mod+shift+k'); // '⇧⌘K' on an Apple platform, 'Ctrl+Shift+K' elsewhere
|
|
79
|
+
formatHotkey('f2'); // 'F2'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This is what `Menu` uses to draw the shortcut on an entry, from the same string `menuHotkeys`
|
|
83
|
+
turns into a binding, see [Menu.md](Menu.md). A combo it cannot parse is handed back unchanged. The platform is read at call
|
|
84
|
+
time, so a server-rendered page renders the non-Apple spelling.
|
|
85
|
+
|
|
86
|
+
## useMediaQuery
|
|
87
|
+
|
|
88
|
+
`true` while the query matches, re-rendering on every change. Backed by `useSyncExternalStore`, so
|
|
89
|
+
it hydrates from the server's answer (`false`) instead of making the first client render disagree
|
|
90
|
+
with the server's HTML.
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
const isWide = useMediaQuery('(min-width: 1024px)');
|
|
94
|
+
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Prefer this over reading `window.innerWidth`: a resize listener re-renders on every pixel, a media
|
|
98
|
+
query only when the answer changes.
|
|
99
|
+
|
|
100
|
+
## useCoarsePointer
|
|
101
|
+
|
|
102
|
+
`true` when the primary pointing device is touch — `useMediaQuery('(pointer: coarse)')`.
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
const isTouch = useCoarsePointer();
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Use it for affordances, not for layout: a touchscreen laptop is a coarse pointer at desktop width.
|
|
109
|
+
For "is this a phone", ask for both, as `Presentation.Auto` does — see [ConfigProvider.md](ConfigProvider.md).
|
|
110
|
+
|
|
111
|
+
## useVisualViewportSize
|
|
112
|
+
|
|
113
|
+
Tracks `window.visualViewport`, so layout can react to the mobile on-screen keyboard opening.
|
|
114
|
+
`window.innerHeight` and CSS `vh` stay pinned to the full layout viewport and do not.
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
const { height, offsetTop } = useVisualViewportSize();
|
|
118
|
+
|
|
119
|
+
<div style={{ height, transform: `translateY(${offsetTop}px)` }} />;
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Falls back to `window.innerHeight` / `0` where the API is missing.
|
|
123
|
+
|
|
124
|
+
## useScrollEdges
|
|
125
|
+
|
|
126
|
+
Which edges of a scroll container currently hide content, so the boundary can be marked — most
|
|
127
|
+
often by fading it.
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
131
|
+
const { hasStart, hasEnd } = useScrollEdges(listRef, { axis: 'vertical' });
|
|
132
|
+
|
|
133
|
+
<div ref={listRef} data-fade-top={hasStart} data-fade-bottom={hasEnd}>
|
|
134
|
+
{items}
|
|
135
|
+
</div>;
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
| Option | Type | Default | Description |
|
|
139
|
+
| ------ | ---- | ------- | ----------- |
|
|
140
|
+
| `axis` | `'vertical' \| 'horizontal'` | `'vertical'` | Which axis to measure |
|
|
141
|
+
| `isEnabled` | `boolean` | `true` | Pass `false` while the element is hidden |
|
|
142
|
+
|
|
143
|
+
For the ready-made fade, use [ScrollFade.md](ScrollFade.md) instead.
|
|
144
|
+
|
|
145
|
+
## useSwipeDismiss
|
|
146
|
+
|
|
147
|
+
Swipe-to-dismiss on any element. Attaches its own pointer listeners to the node, so there is
|
|
148
|
+
nothing to spread but the ref.
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
const { ref, isDragging } = useSwipeDismiss<HTMLDivElement>({
|
|
152
|
+
onDismiss: () => remove(notification.id),
|
|
153
|
+
axis: 'x',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
<div ref={ref}>{notification.message}</div>;
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
| Option | Type | Default | Description |
|
|
160
|
+
| ------ | ---- | ------- | ----------- |
|
|
161
|
+
| `onDismiss` | `() => void` | — | Called once the swipe commits, after the exit transition |
|
|
162
|
+
| `axis` | `'x' \| 'y'` | `'x'` | Which axis the drag tracks |
|
|
163
|
+
| `distanceThreshold` | `number` | `80` | px past which releasing commits, regardless of speed |
|
|
164
|
+
| `velocityThreshold` | `number` | `0.5` | px/ms past which releasing commits, regardless of distance |
|
|
165
|
+
| `mobileOnly` | `boolean` | `true` | Restrict the gesture to coarse pointers. On a mouse a drag is more likely a text selection |
|
|
166
|
+
| `disabled` | `boolean` | `false` | Attach no listeners at all |
|
|
167
|
+
|
|
168
|
+
`isDragging` is `true` for the duration of a drag — use it to pause an auto-dismiss timer.
|
|
169
|
+
|
|
170
|
+
`ref` is a **callback ref**, not a ref object: the hook has to know when the node changes, and an
|
|
171
|
+
element unmounted and rendered again is a different node. Attach it with `ref={ref}` as above. To
|
|
172
|
+
keep a ref of your own on the same element, call both from one callback:
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
<div
|
|
176
|
+
ref={(node) => {
|
|
177
|
+
ref(node);
|
|
178
|
+
myRef.current = node;
|
|
179
|
+
}}
|
|
180
|
+
/>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## useOverlay
|
|
184
|
+
|
|
185
|
+
The three behaviours every blocking overlay needs: Escape to dismiss, a focus trap, and a scroll
|
|
186
|
+
lock on the page behind it. `Modal`, `Drawer` and `Popover` are built on it, and they all share one
|
|
187
|
+
stack: a custom overlay using this hook nests correctly with theirs, so Escape closes only the
|
|
188
|
+
innermost layer and the page stays locked until the last one closes.
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
function CustomOverlay({ isOpen, onClose, children }) {
|
|
192
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
193
|
+
useOverlay({ isActive: isOpen, containerRef: panelRef, onEscape: onClose });
|
|
194
|
+
|
|
195
|
+
if (!isOpen) return null;
|
|
196
|
+
return (
|
|
197
|
+
<div ref={panelRef} role="dialog" aria-modal="true">
|
|
198
|
+
{children}
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
| Arg | Type | Default | Description |
|
|
205
|
+
| --- | ---- | ------- | ----------- |
|
|
206
|
+
| `isActive` | `boolean` | — | Everything below is set up while this is true |
|
|
207
|
+
| `containerRef` | `RefObject<HTMLElement \| null>` | — | The panel. Focus is trapped inside it |
|
|
208
|
+
| `onEscape` | `() => void` | — | Called when Escape is pressed while this is the topmost layer. Omit to not handle Escape |
|
|
209
|
+
| `trapFocus` | `boolean` | `true` | Keep Tab inside the panel, and restore focus on close |
|
|
210
|
+
| `lockScroll` | `boolean` | `true` | Prevent the page behind it from scrolling |
|
|
211
|
+
|
|
212
|
+
Reach for [Modal.md](Modal.md) or [Drawer.md](Drawer.md) first; this is for an overlay they do not cover.
|
|
213
|
+
|
|
214
|
+
## tabbablesIn
|
|
215
|
+
|
|
216
|
+
The tabbable descendants of an element, in DOM order. An element taken out of the tab order with
|
|
217
|
+
`tabindex="-1"` is skipped, whatever it is, so a roving-tabindex list reads as the one tab stop it
|
|
218
|
+
actually is.
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
const focusables = tabbablesIn(panelRef.current);
|
|
222
|
+
focusables[0]?.focus();
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Also exported
|
|
226
|
+
|
|
227
|
+
Hooks documented with the thing they belong to:
|
|
228
|
+
|
|
229
|
+
- `useTheme` — the active theme, see [ThemeProvider.md](ThemeProvider.md)
|
|
230
|
+
- `useComponentDefaults`, `useLabels` — see [ConfigProvider.md](ConfigProvider.md)
|
|
231
|
+
- `useFormValidator` — see [FormValidator.md](FormValidator.md)
|
|
232
|
+
- `useFormValidatorGroup` — see [FormValidatorGroup.md](FormValidatorGroup.md)
|
package/docs/Loading.md
CHANGED
|
@@ -1,27 +1,68 @@
|
|
|
1
|
-
# Loading (
|
|
1
|
+
# Loading (SpinnerLoading)
|
|
2
2
|
|
|
3
|
-
**When to use:**
|
|
3
|
+
**When to use:** A loading indicator for async operations: page loads, data fetching, or covering a region while it refreshes. For a button use `Button`'s built-in `isLoading`, and for placeholder content that mirrors the shape of what is coming use [Skeleton.md](Skeleton.md).
|
|
4
4
|
|
|
5
|
-
**Keywords:** busy, waiting, activity indicator, throbber
|
|
5
|
+
**Keywords:** busy, waiting, activity indicator, throbber, progress, async, fetching, overlay
|
|
6
6
|
|
|
7
|
-
**Import:** `import {
|
|
7
|
+
**Import:** `import { SpinnerLoading } from '@ahrowe/ui'`
|
|
8
8
|
|
|
9
9
|
```tsx
|
|
10
|
-
import {
|
|
10
|
+
import { SpinnerLoading } from '@ahrowe/ui';
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
{isLoading && <IconLoading />}
|
|
14
|
-
|
|
15
|
-
// Circular spinner
|
|
12
|
+
// Default: a 70x70 circular spinner stroked in var(--primary-color)
|
|
16
13
|
{isLoading && <SpinnerLoading />}
|
|
17
14
|
|
|
18
|
-
//
|
|
15
|
+
// Sized through className
|
|
19
16
|
<SpinnerLoading className="my-spinner" />
|
|
20
17
|
```
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
```css
|
|
20
|
+
.my-spinner {
|
|
21
|
+
width: 24px;
|
|
22
|
+
height: 24px;
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Key props:**
|
|
27
|
+
|
|
28
|
+
| Prop | Type | Description |
|
|
29
|
+
|------|------|-------------|
|
|
30
|
+
| `className` | `string` | Root `svg` element. This is also how you size it |
|
|
31
|
+
|
|
32
|
+
## Covering a region while it loads
|
|
33
|
+
|
|
34
|
+
`SpinnerLoading` is the indicator only. To overlay it on content, centre it in a positioned parent:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
<div className="panel">
|
|
38
|
+
<YourContent />
|
|
39
|
+
{isLoading && (
|
|
40
|
+
<div className="panel-overlay">
|
|
41
|
+
<SpinnerLoading className="my-spinner" />
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```css
|
|
48
|
+
.panel {
|
|
49
|
+
position: relative;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.panel-overlay {
|
|
53
|
+
position: absolute;
|
|
54
|
+
inset: 0;
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
pointer-events: none;
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Drop the `pointer-events: none` if the overlay should also block interaction with what is underneath, and add a translucent `background` if it should dim it.
|
|
63
|
+
|
|
64
|
+
For a branded loading screen rather than a generic spinner, [AnimatedLogo.md](AnimatedLogo.md) draws an SVG mark and sits in the same overlay.
|
|
65
|
+
|
|
66
|
+
**Note:** the spinner animates continuously even under `prefers-reduced-motion`. A frozen spinner would say the app has hung, which is the opposite of what it is there to communicate.
|
|
26
67
|
|
|
27
|
-
|
|
68
|
+
**Slots:** none. The root `svg` takes `className`.
|
package/docs/Menu.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**Keywords:** context menu, action menu, overflow menu, kebab menu, three dots, popup menu, right click, anchored list
|
|
6
6
|
|
|
7
|
-
**Import:** `import { Menu } from '@ahrowe/ui'`
|
|
7
|
+
**Import:** `import { Menu, menuHotkeys } from '@ahrowe/ui'`
|
|
8
8
|
**Types:** `import type { MenuProps, MenuEntry, MenuAction } from '@ahrowe/ui'`
|
|
9
9
|
|
|
10
10
|
**Requires:** `<div id="bodyEnd"></div>` in your app: the menu is portalled there, like every other anchored panel in the library.
|
|
@@ -15,7 +15,7 @@ import type { MenuEntry } from '@ahrowe/ui';
|
|
|
15
15
|
import { faPen, faCopy, faTrash } from '@fortawesome/free-solid-svg-icons';
|
|
16
16
|
|
|
17
17
|
const items: MenuEntry[] = [
|
|
18
|
-
{ id: 'edit', label: 'Edit', icon: faPen, shortcut: '
|
|
18
|
+
{ id: 'edit', label: 'Edit', icon: faPen, shortcut: 'mod+e' },
|
|
19
19
|
{ id: 'duplicate', label: 'Duplicate', icon: faCopy },
|
|
20
20
|
{ separator: true },
|
|
21
21
|
{ id: 'delete', label: 'Delete', icon: faTrash, danger: true },
|
|
@@ -34,6 +34,28 @@ const items: MenuEntry[] = [
|
|
|
34
34
|
{ heading: 'This document' } // a label over what follows
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
**Shortcuts are drawn, not bound.** A `shortcut` is a combo in [useHotkeys](Hooks.md) spelling, and the menu writes it on the trailing edge for the platform the reader is on: `mod+s` shows as `⌘S` on an Apple one and `Ctrl+S` everywhere else. So there is one spelling per shortcut and a hand-written `'⌘S'` can no longer be wrong on Windows.
|
|
38
|
+
|
|
39
|
+
Listening for it is yours, because only your app knows what else is listening: if it already binds `⌘S` and the menu bound it too, both would fire and the action would run twice. `menuHotkeys` turns the same entries into a map for `useHotkeys`, so the one spelling covers the hint and the binding:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { Menu, menuHotkeys, useHotkeys } from '@ahrowe/ui';
|
|
43
|
+
|
|
44
|
+
const items: MenuEntry[] = [
|
|
45
|
+
{ id: 'save', label: 'Save', shortcut: 'mod+s', onClick: save },
|
|
46
|
+
{ id: 'rename', label: 'Rename', shortcut: 'f2', onClick: rename },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// Whenever the page is up, which is what a shortcut on a menu item usually means:
|
|
50
|
+
// ⌘S in a File menu works without opening the File menu.
|
|
51
|
+
useHotkeys(menuHotkeys(items, run));
|
|
52
|
+
|
|
53
|
+
// Or only while the menu is open, if that is what you want:
|
|
54
|
+
useHotkeys(menuHotkeys(items, run), { enabled: isOpen });
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
It runs each entry the way clicking it does, its own `onClick` then `onAction`, and leaves out a `disabled` entry and any entry with no `shortcut`. Bind them yourself instead if the actions already live in a keymap of your own; the menu does not mind either way.
|
|
58
|
+
|
|
37
59
|
An action runs its own `onClick` first, then `onAction` on the menu, then the menu closes. Use whichever of the two fits: a handler per entry when they are unrelated, `onAction` when they funnel into one place.
|
|
38
60
|
|
|
39
61
|
```tsx
|
|
@@ -95,7 +117,7 @@ Separators and headings are never focused. The trigger is given `aria-haspopup`,
|
|
|
95
117
|
| `id` | `string` | Stable id, also what `onAction` reports |
|
|
96
118
|
| `label` | `ReactNode` | What the entry says |
|
|
97
119
|
| `icon` | `IconContent` | Leading icon |
|
|
98
|
-
| `shortcut` | `
|
|
120
|
+
| `shortcut` | `string` | A combo in `useHotkeys` spelling, e.g. `'mod+k'`. Shown on the trailing edge, written for the platform. Binding it is yours, see `menuHotkeys` |
|
|
99
121
|
| `disabled` | `boolean` | Not clickable, and skipped by the arrow keys |
|
|
100
122
|
| `danger` | `boolean` | Renders in the destructive colour |
|
|
101
123
|
| `onClick` | `() => void` | Runs before `onAction` |
|
package/docs/Popover.md
CHANGED
|
@@ -77,4 +77,6 @@ Only a `Click` trigger presents as a sheet. `Hover` and `Focus` open something s
|
|
|
77
77
|
|
|
78
78
|
Requires a `<div id="bodyEnd"></div>` at the app root — the panel is portaled there, same as `Tooltip` and `Menu`.
|
|
79
79
|
|
|
80
|
+
**Scrolling:** where the browser supports CSS anchor positioning, including the trigger's transforms, the panel is anchored to the trigger in CSS, so it moves with a scrolling trigger in the same frame instead of catching up a frame later. Firefox resolves anchors without transforms, so it keeps the pixel positioning. While it is open the trigger carries an inline `anchor-name`, added to any inline one it already has; an `anchor-name` your stylesheet gives the trigger is overridden for that time.
|
|
81
|
+
|
|
80
82
|
**Keyboard & focus (click trigger):** the panel is portaled to the end of the DOM, so Tab can't reach it naturally. When the panel has focusable content and the keyboard opened it (Enter or Space on the trigger, or ArrowDown/ArrowUp), focus moves to the first focusable element and the panel gets `role="dialog"`; a pointer click leaves the focus where it already is. `focusOnOpen` forces either. Tabbing past the last element closes the popover and moves focus to the element after the trigger; Shift+Tab past the first element, and Escape, close it and return focus to the trigger. This is non-modal: focus is not trapped, and hover/focus triggers don't steal focus. Escape closes only the innermost open layer, so a popover inside a `Modal` closes on the first press and leaves the modal open. Popovers with only static content don't manage focus or claim the dialog role.
|
package/docs/Sticky.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**When to use:** Pin a piece of content to the top of the viewport once the page scrolls past it — section headers, toolbars, filter bars, table headers. The content sticks while its original position is above the fold and releases the moment that position scrolls back into view. A placeholder preserves the original space, so nothing below jumps when it detaches.
|
|
4
4
|
|
|
5
|
-
**Keywords:** sticky header, pinned toolbar, scroll container, app shell, scrollable panel
|
|
5
|
+
**Keywords:** sticky header, pinned toolbar, scroll container, app shell, scrollable panel, stop at the end, confine, within a section, unpin, release
|
|
6
6
|
|
|
7
7
|
**Import:** `import { Sticky } from '@ahrowe/ui'`
|
|
8
8
|
|
|
@@ -36,6 +36,19 @@ const headerRef = useRef<HTMLElement>(null);
|
|
|
36
36
|
<Content />
|
|
37
37
|
</div>
|
|
38
38
|
|
|
39
|
+
// Let it go once the section it labels ends, instead of pinning down the page.
|
|
40
|
+
// A toolbar and a header pointed at the same boundary leave together.
|
|
41
|
+
const section = useRef<HTMLDivElement>(null);
|
|
42
|
+
<div ref={section}>
|
|
43
|
+
<Sticky boundary={section} boundaryInset={120}>
|
|
44
|
+
<Toolbar />
|
|
45
|
+
</Sticky>
|
|
46
|
+
<Sticky boundary={section} boundaryInset={120}>
|
|
47
|
+
<TableHeader />
|
|
48
|
+
</Sticky>
|
|
49
|
+
<Rows />
|
|
50
|
+
</div>
|
|
51
|
+
|
|
39
52
|
// React to the stuck state (e.g. add a shadow)
|
|
40
53
|
<Sticky onStuckChange={(stuck) => setElevated(stuck)}>
|
|
41
54
|
<Toolbar />
|
|
@@ -60,6 +73,12 @@ const headerRef = useRef<HTMLElement>(null);
|
|
|
60
73
|
|
|
61
74
|
**`stickToScrollParent`:** by default `Sticky` always pins relative to the browser viewport, which can overlap other fixed/pinned chrome (e.g. an app header) that sits outside the element's own scroll container. Set `stickToScrollParent` to pin relative to the nearest ancestor with its own scrollbar (`overflow-y: auto`/`scroll`/`overlay`) instead — found once on mount by walking up from the root. Falls back to the viewport if no scrollable ancestor is found. Combine with `offsetTop` for extra spacing from that container's top (e.g. its own internal header).
|
|
62
75
|
|
|
76
|
+
**`boundary`:** without it the content stays pinned for as long as the page scrolls, which is right for an app bar and wrong for anything that labels a section: a table header outlives its table and ends up captioning whatever scrolls under it next. Point `boundary` at the element the content belongs to, and once that element's bottom edge reaches the pinned content, the content is pushed up with it and leaves the screen together with it. This is what native `position: sticky` gets from its containing block, which a `position: fixed` implementation has no equivalent of, and it works whether the pin is relative to the viewport or to a `stickToScrollParent` container.
|
|
77
|
+
|
|
78
|
+
**A group confined to one boundary leaves as one block.** Several `Sticky` elements stacked on top of each other and pointed at the same boundary are all lifted by the same amount, the largest any of them needs, so they keep their spacing and the lowest one's bottom edge lands on the boundary. Letting each clamp its own bottom edge instead would pile them all onto that edge and slide the lower ones up through the ones above.
|
|
79
|
+
|
|
80
|
+
**`boundaryInset`** decides how much of the boundary's lower end stays uncovered while the content leaves. At `0` the content rests on the very bottom edge, so the last stretch of the boundary, as tall as the pinned content itself, is hidden underneath it for the whole exit: a table header ends up floating over nothing with its final rows behind it. Set it to how much you want to stay in sight, which for a table header is a few rows, and the content lifts off that much earlier and keeps that gap all the way out.
|
|
81
|
+
|
|
63
82
|
**Stacking:** every mounted `Sticky` coordinates automatically, no extra markup needed. When more than one is stuck at the same time, a later one (in DOM order) that horizontally overlaps an earlier stuck one pins below it — `offsetTop` plus the overlapping ones' heights — instead of both landing on the same spot. Stickies that don't overlap horizontally (e.g. two half-width ones side by side under a full-width one) land at the same `top`, next to each other, rather than pushing each other down.
|
|
64
83
|
|
|
65
84
|
**Key props:**
|
|
@@ -70,6 +89,8 @@ const headerRef = useRef<HTMLElement>(null);
|
|
|
70
89
|
| `offsetTop` | `number` | Distance in px from the top of the viewport the content sticks to (default `0`). Ignored if `offsetElement` is set |
|
|
71
90
|
| `offsetElement` | `RefObject<HTMLElement \| null>` | Element to stick below (e.g. a fixed/pinned header outside this library) — its live height is used as the offset instead of a fixed `offsetTop` |
|
|
72
91
|
| `stickToScrollParent` | `boolean` | Stick to the top of the nearest scrollable ancestor instead of the browser viewport (default `false`). Falls back to the viewport if no scrollable ancestor is found |
|
|
92
|
+
| `boundary` | `RefObject<HTMLElement \| null>` | Element the content is confined to. Once its bottom edge reaches the pinned content, the content rides it up and off screen instead of staying pinned over what follows. Several Stickies sharing one leave together |
|
|
93
|
+
| `boundaryInset` | `number` | How much of the boundary's lower end stays uncovered while the content leaves, in px (default `0`). At `0` the last stretch, as tall as the content, is hidden under it for the whole exit. Ignored without `boundary` |
|
|
73
94
|
| `zIndex` | `number` | z-index applied while stuck (default `20`) |
|
|
74
95
|
| `disabled` | `boolean` | Disable sticking — content stays in normal flow |
|
|
75
96
|
| `onStuckChange` | `(isStuck: boolean) => void` | Fired whenever the stuck state flips |
|
package/docs/VirtualList.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**When to use:** Efficiently render large lists or tables — thousands of rows rendered with a virtualised scroll window so only visible rows are in the DOM. Supports column definitions, row selection, multi-select, column sorting, column resizing, column visibility toggling, and infinite scroll.
|
|
4
4
|
|
|
5
|
-
**Keywords:** data table, virtualized table, sort, sorting, order by, ascending, descending, column header click, resize, resizable, drag column edge, column width, autofit
|
|
5
|
+
**Keywords:** data table, virtualized table, sort, sorting, order by, ascending, descending, column header click, resize, resizable, drag column edge, column width, autofit, double scrollbar, nested scroll, window scrolling, spreadsheet, data entry grid, editable grid
|
|
6
6
|
|
|
7
7
|
**Import:** `import { VirtualList } from '@ahrowe/ui'`
|
|
8
8
|
**Types:** `import type { VirtualListProps, VirtualListColumn, VirtualListHandle, VirtualListSort } from '@ahrowe/ui'`
|
|
@@ -124,7 +124,8 @@ const columns: VirtualListColumn<User>[] = [
|
|
|
124
124
|
| `items` | `T[]` | Data array (required) |
|
|
125
125
|
| `renderRow` | `(item, index) => ReactNode` | Row render fn — list mode (no columns) |
|
|
126
126
|
| `columns` | `VirtualListColumn<T>[]` | Column definitions — enables table/header mode |
|
|
127
|
-
| `height` | `number \| string` | Scroll viewport height (default `'100%'`) |
|
|
127
|
+
| `height` | `number \| string` | Scroll viewport height (default `'100%'`). Ignored with `scrollParent` |
|
|
128
|
+
| `scrollParent` | `'nearest' \| 'window' \| HTMLElement \| (() => HTMLElement \| null)` | Drop the list's own scrollbar and virtualize against this scrollport instead — see **No scroll of its own** |
|
|
128
129
|
| `estimatedRowHeight` | `number` | Row height assumed before anything has been measured (default `40`). Once rows have been measured, their average is used for the ones that haven't, so this only has to be roughly right |
|
|
129
130
|
| `overscan` | `number` | Extra rows rendered outside the viewport (default `3`) |
|
|
130
131
|
| `rowGap` | `number \| string` | Gap between rows — number = px; string = any CSS length (`'1em'`, `'var(--spacing-s)'`). Default `0` |
|
|
@@ -135,6 +136,7 @@ const columns: VirtualListColumn<User>[] = [
|
|
|
135
136
|
| `selectedKey` | `string \| number \| null` | Controlled single-select key |
|
|
136
137
|
| `onRowClick` | `(item, index) => void` | Row click handler, also fired by `Enter` on the active row |
|
|
137
138
|
| `keyboardNavigation` | `boolean` | Arrow-key navigation over the rows (default `true`). See **Keyboard** |
|
|
139
|
+
| `tabAcrossRows` | `boolean` | Let `Tab` carry on into the next row instead of leaving the list at the end of the current one (default `false`). See **Keyboard** |
|
|
138
140
|
| `multiSelect` | `boolean` | Enable checkbox multi-select column |
|
|
139
141
|
| `selectedKeys` | `Set<string \| number>` | Controlled multi-select keys |
|
|
140
142
|
| `onSelectionChange` | `(keys) => void` | Multi-select change callback |
|
|
@@ -241,6 +243,10 @@ Keys are ignored while the focus is inside a cell, so an input, a `Dropdown` or
|
|
|
241
243
|
|
|
242
244
|
**Controls inside rows are not tab stops.** The list holds them at `tabindex="-1"` and puts back the ones of the row being used, so `Tab` walks that row and then leaves the list. Without that, every control in every *rendered* row is a stop, the set changes as the list scrolls, and tabbing through a long list drops out of it at an arbitrary row, because the next one isn't in the DOM yet when the browser looks for the next stop. Entering a row happens with `F2`, with `Enter` when the list has no `onRowClick`, or simply by clicking a control. A `tabindex` you set yourself is restored exactly as it was, and an element you already parked at `-1` stays parked.
|
|
243
245
|
|
|
246
|
+
**`tabAcrossRows` makes `Tab` walk the whole grid**, for rows that are filled in rather than read: from the last field of a row it enters the next row's first field, `Shift+Tab` walks back the same way, and `Tab` out of the last row (or back out of the first) leaves the list as it always did. The list mounts the row it is moving into, so it works across the edge of the rendered window. A row with nothing focusable in it is stepped over, and when nothing past it has anything either, the focus goes back to the list so the next `Tab` leaves. `Tab` on the list's own stop steps into the active row instead of over the whole list, since otherwise nothing inside it would ever be reachable by tabbing.
|
|
247
|
+
|
|
248
|
+
It is off by default on purpose: for a list that is read, one tab stop plus the arrow keys is what a screen-reader user expects, and a few thousand rows of tab stops is a trap with no way past it. Turn it on for a grid of inputs — invoice lines, a timesheet — where field-by-field entry is the whole interaction. It does not need `keyboardNavigation`: with that off the list takes no stop of its own and `tabAcrossRows` only carries `Tab` across the boundary between two rows.
|
|
249
|
+
|
|
244
250
|
The row being used also stays mounted while it holds the focus, even once it scrolls out of the window, so scrolling can't drop the focus to the document body. Set `keyboardNavigation={false}` for a list whose rows bring their own key handling, as [Tree](Tree.md) does.
|
|
245
251
|
|
|
246
252
|
Because `role="list"` is not a composite role, the active row is only announced in table mode. In list mode, pass `ariaRoles={{ container: 'listbox', row: 'option' }}` (or another composite pair) to have it announced there too.
|
|
@@ -280,7 +286,8 @@ listRef.current?.scrollToKey(user.id, { align: 'center' });
|
|
|
280
286
|
| `scrollToIndex` | `(index, options?) => void` | Scroll so the row at `index` is visible. `options`: `{ align?: 'start' \| 'center' \| 'end'; behavior?: ScrollBehavior }`. A jump into rows that have never been rendered re-aims itself as their real heights are measured, so it lands on the row you asked for rather than on estimated arithmetic. Input of your own during the jump cancels it |
|
|
281
287
|
| `scrollToKey` | `(key, options?) => void` | Scroll to the row whose `getItemKey` matches `key` (no-op if not found) |
|
|
282
288
|
| `scrollToTop` | `(options?) => void` | Scroll back to the top |
|
|
283
|
-
| `getScrollElement` | `() =>
|
|
289
|
+
| `getScrollElement` | `() => HTMLElement \| null` | The element that scrolls: the list itself, or the `scrollParent` it was given. `null` for `scrollParent='window'`, where the page scrolls |
|
|
290
|
+
| `remeasure` | `() => void` | Re-read where the list sits in its `scrollParent`. Only needed in that mode, and only when something **above** the list changed height without the list re-rendering — an element that merely moves fires no event of its own |
|
|
284
291
|
| `resetColumnWidths` | `() => void` | Drop every user-set column width, returning the table to the widths its columns declare, and clear them from `persistColumnsKey` storage |
|
|
285
292
|
| `recalculateColumns` | `() => void` | Re-measure `fit` columns on the next layout. Only needed after an out-of-band content change that doesn't flow through a new `items` array or `columns` set — those re-measure automatically, as does content first appearing in a cell |
|
|
286
293
|
|
|
@@ -355,7 +362,40 @@ interface TreeDropTarget {
|
|
|
355
362
|
|
|
356
363
|
**Touch:** a press-and-hold of `longPressDelay` lifts a floating copy of the row that follows your finger, with auto-scroll near the list's top and bottom edges. A swipe before the hold completes scrolls the list as normal instead of starting a drag. With `dragHandle`, only a press that starts on the handle arms it, so the rest of the row stays tappable.
|
|
357
364
|
|
|
358
|
-
**
|
|
365
|
+
**No scroll of its own (`scrollParent`):** the list renders at its full height as a plain block in the page flow and virtualizes against a scrollport it does not own. For a long list at the end of a scrolling page, where an inner scrollbar inside the page's own is the thing you are trying to avoid.
|
|
366
|
+
|
|
367
|
+
```tsx
|
|
368
|
+
// Whatever scrolls around it, found once on mount. The usual answer.
|
|
369
|
+
<VirtualList items={positions} columns={columns} scrollParent='nearest' estimatedRowHeight={48} />
|
|
370
|
+
|
|
371
|
+
// Or name it: the page, an element, or a getter for a ref that fills later.
|
|
372
|
+
<VirtualList items={positions} columns={columns} scrollParent='window' estimatedRowHeight={48} />
|
|
373
|
+
<VirtualList items={positions} columns={columns} scrollParent={() => shellRef.current} estimatedRowHeight={48} />
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
`'nearest'` walks up to the first ancestor whose `overflow-y` is `auto`, `scroll`
|
|
377
|
+
or `overlay`, whether or not it currently scrolls, and falls back to the page. A
|
|
378
|
+
wrapper carrying `overflow: auto` as a layout fix is therefore picked ahead of
|
|
379
|
+
the real scrollport, and the list then renders its first window and stops; name
|
|
380
|
+
the element outright where that happens. That is the same ancestor
|
|
381
|
+
[Sticky](Sticky.md) pins to, which is what a sticky header above the list needs:
|
|
382
|
+
the two have to answer to the same scrollport or they come apart on the first
|
|
383
|
+
scroll. It is resolved once and re-read only if that element leaves the DOM,
|
|
384
|
+
because walking the tree reads computed style.
|
|
385
|
+
|
|
386
|
+
Everything else works as before — the same windowing, the same `scrollToIndex`, the same keyboard model. What changes:
|
|
387
|
+
|
|
388
|
+
- **`height` no longer applies.** The list is as tall as its rows.
|
|
389
|
+
- **Set `estimatedRowHeight` to the real height** where the rows have one. Row heights are corrected as rows are measured, and here that correction resizes the *page*, so a guess that is wrong by 10px across 2000 rows moves the page's scrollbar under the user's hand. Rows of one fixed height cost nothing: the first measurement is right and the page height never moves again.
|
|
390
|
+
- **The header sticks to that scrollport**, which is the top of the viewport rather than the top of the list. Give the list `--vl-header-top` to clear a fixed app bar: `<VirtualList style={{ '--vl-header-top': '64px' }} … />`.
|
|
391
|
+
- **A frozen column layout wider than the list overflows the page sideways** instead of scrolling inside the list. A container cannot scroll one axis and not the other, and getting the vertical scrollbar back would undo the whole mode. Only reachable with `resizableColumns`.
|
|
392
|
+
- **Call `remeasure()`** when something above the list changes height without the list re-rendering. The list watches the scrollport's own content box, which covers most of it; an element that merely moves fires nothing.
|
|
393
|
+
|
|
394
|
+
**The list says so when it isn't virtualizing.** A viewport as tall as its own content renders every row, which is what a percentage `height` amounts to inside a parent with no definite height: the scroll container grows to fit the rows instead of clipping them. Nothing looks broken, the page is just slow. Past a hundred rows the list writes a `console.warn` naming that, which is the one failure mode of this component that is otherwise invisible. Passing `height` alongside `scrollParent` warns too, since the list then has no viewport to size.
|
|
395
|
+
|
|
396
|
+
Only the rendered rows are in the DOM, in this mode as in the other one, so the browser's own find-in-page and print reach the window on screen and not the rest of the list. That is the price of virtualization, and it is the reason to use it only where the row count earns it.
|
|
397
|
+
|
|
398
|
+
**Lazy loading:** `onLoadMore` fires when the user scrolls within `loadMoreThreshold` px of the bottom, **and** repeatedly whenever the rows don't fill the list's viewport. That second trigger matters because a list shorter than its own height can't be scrolled at all, so a first batch smaller than the viewport would otherwise be the last batch ever loaded. The list keeps asking for pages until the content overflows. Whichever trigger fires, one row count is asked at most once, so the loop ends as soon as a call appends nothing, and a page scrolled well past the end of the list does not go on asking with every scroll event. Gate the prop on your own "has more" flag to skip even that last probe:
|
|
359
399
|
|
|
360
400
|
```tsx
|
|
361
401
|
<VirtualList items={items} onLoadMore={hasMore ? loadNextPage : undefined} height={600} />
|
package/package.json
CHANGED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
"use client";import e from"./iconLoading.module.mjs";import"react";import t from"classnames";import{jsx as n,jsxs as r}from"react/jsx-runtime";function i({className:i=``}){return n(`div`,{className:t(e.iconLoading,i),children:r(`svg`,{xmlns:`http://www.w3.org/2000/svg`,viewBox:`-8 -1 16 14`,children:[n(`path`,{className:e.iconLoadingFirst,d:`M 0 0 C 3 5 6 5 6 12`,strokeWidth:`1.7`,fill:`none`}),n(`path`,{className:e.iconLoadingFirst,d:`M 0 0 C -3 5 -6 5 -6 12`,strokeWidth:`1.7`,fill:`none`}),n(`path`,{className:e.iconLoadingFirst,d:`M 0 0 L 0 10`,strokeWidth:`1.7`,fill:`none`}),n(`path`,{className:e.iconLoadingSecond,d:`M 0.1 10 C -3 10 -4 10 -6 12`,strokeWidth:`1.7`,fill:`none`}),n(`path`,{className:e.iconLoadingSecond,d:`M -0.1 10 C 3 10 4 10 6 12`,strokeWidth:`1.7`,fill:`none`})]})})}export{i as default};
|
|
2
|
-
//# sourceMappingURL=iconLoading.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"iconLoading.mjs","names":[],"sources":["../../../../../package/common/loading/iconLoading/iconLoading.tsx"],"sourcesContent":["import React from 'react';\nimport cx from 'classnames';\n\nimport styles from './iconLoading.module.pcss';\n\nfunction IconLoading({ className = '' }) {\n return (\n <div className={cx(styles.iconLoading, className)}>\n <svg xmlns='http://www.w3.org/2000/svg' viewBox='-8 -1 16 14'>\n <path className={styles.iconLoadingFirst} d='M 0 0 C 3 5 6 5 6 12' strokeWidth='1.7' fill='none' />\n <path className={styles.iconLoadingFirst} d='M 0 0 C -3 5 -6 5 -6 12' strokeWidth='1.7' fill='none' />\n <path className={styles.iconLoadingFirst} d='M 0 0 L 0 10' strokeWidth='1.7' fill='none' />\n <path className={styles.iconLoadingSecond} d='M 0.1 10 C -3 10 -4 10 -6 12' strokeWidth='1.7' fill='none' />\n <path className={styles.iconLoadingSecond} d='M -0.1 10 C 3 10 4 10 6 12' strokeWidth='1.7' fill='none' />\n </svg>\n </div>\n );\n}\n\nexport default IconLoading;\n"],"mappings":"+IAKA,SAAS,EAAY,CAAE,YAAY,IAAM,CACvC,OACE,EAAC,MAAD,CAAK,UAAW,EAAG,EAAO,YAAa,CAAS,EAC9C,SAAA,EAAC,MAAD,CAAK,MAAM,6BAA6B,QAAQ,cAAhD,SAAA,CACE,EAAC,OAAD,CAAM,UAAW,EAAO,iBAAkB,EAAE,uBAAuB,YAAY,MAAM,KAAK,MAAQ,CAAA,EAClG,EAAC,OAAD,CAAM,UAAW,EAAO,iBAAkB,EAAE,0BAA0B,YAAY,MAAM,KAAK,MAAQ,CAAA,EACrG,EAAC,OAAD,CAAM,UAAW,EAAO,iBAAkB,EAAE,eAAe,YAAY,MAAM,KAAK,MAAQ,CAAA,EAC1F,EAAC,OAAD,CAAM,UAAW,EAAO,kBAAmB,EAAE,+BAA+B,YAAY,MAAM,KAAK,MAAQ,CAAA,EAC3G,EAAC,OAAD,CAAM,UAAW,EAAO,kBAAmB,EAAE,6BAA6B,YAAY,MAAM,KAAK,MAAQ,CAAA,CACtG,GACF,CAAA,CAET"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
"use client";var e=`iconLoading_wovj4`,t=`spin-to-oblivion_0tZtm`,n=`iconLoading-first_SBwP3`,r=`draw-line_1NiWA`,i=`iconLoading-second_nHrW3`,a=`draw-line-two_HmaHl`,o={iconLoading:e,"spin-to-oblivion":`spin-to-oblivion_0tZtm`,spinToOblivion:t,"iconLoading-first":`iconLoading-first_SBwP3`,iconLoadingFirst:n,"draw-line":`draw-line_1NiWA`,drawLine:r,"iconLoading-second":`iconLoading-second_nHrW3`,iconLoadingSecond:i,"draw-line-two":`draw-line-two_HmaHl`,drawLineTwo:a};export{o as default,r as drawLine,a as drawLineTwo,e as iconLoading,n as iconLoadingFirst,i as iconLoadingSecond,t as spinToOblivion};
|
|
2
|
-
//# sourceMappingURL=iconLoading.module.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"iconLoading.module.mjs","names":[],"sources":["../../../../../package/common/loading/iconLoading/iconLoading.module.pcss"],"sourcesContent":[".iconLoading {\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100%;\n width: 100%;\n position: absolute;\n left: 0;\n top: 0;\n user-select: none;\n pointer-events: none;\n\n --draw-duration: 0.8s;\n --delay: 0.4s;\n\n svg {\n stroke-dasharray: 14;\n stroke-dashoffset: 14;\n width: 200px;\n aspect-ratio: 1;\n animation: calc(var(--draw-duration) * 3) spin-to-oblivion var(--delay) infinite;\n transform: rotate(0deg);\n stroke: var(--primary-color);\n stroke-linecap: round;\n }\n\n &-first {\n animation: calc(var(--draw-duration) * 3) draw-line var(--delay) infinite;\n }\n\n &-second {\n animation: calc(var(--draw-duration) * 3) draw-line-two var(--delay) infinite;\n }\n\n @keyframes draw-line {\n 33% {\n stroke-dashoffset: 0;\n }\n\n 95% {\n stroke-dashoffset: 0;\n }\n\n 96% {\n stroke-dashoffset: 14;\n }\n }\n\n @keyframes draw-line-two {\n 33% {\n stroke-dashoffset: 14;\n }\n\n 66% {\n stroke-dashoffset: 0;\n }\n\n 95% {\n stroke-dashoffset: 0;\n }\n\n 96% {\n stroke-dashoffset: 14;\n }\n }\n\n @keyframes spin-to-oblivion {\n 50% {\n transform: rotate(180deg);\n }\n\n 75% {\n transform: rotate(180deg) scale(0);\n }\n\n 100% {\n transform: rotate(180deg) scale(0);\n }\n }\n}\n"],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../package/common/loading/iconLoading/index.ts"],"sourcesContent":["import IconLoading from './iconLoading.js';\n\nexport default IconLoading;\n"],"mappings":"8CAEA,IAAA,EAAe"}
|