@anyknown/axis 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +216 -2
- package/dist/axis.d.ts +98 -0
- package/dist/axis.js +655 -0
- package/dist/coordinator.d.ts +36 -0
- package/dist/coordinator.js +247 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/math.d.ts +90 -0
- package/dist/math.js +139 -0
- package/dist/react.d.ts +22 -0
- package/dist/react.js +49 -0
- package/package.json +83 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Senlima Sun
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,217 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @anyknown/axis
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A headless scroll primitive for horizontal rows, vertical lists and 2D canvases: **native scroll first, drag as an enhancement.**
|
|
4
|
+
|
|
5
|
+
You give axis an element with `overflow: auto`. Touch, trackpad, keyboard and scrollbar keep working exactly as the browser makes them work. axis adds mouse drag-scrolling on x, y or both axes, with momentum, snap-aware release, an optional vertical-wheel translation for rows, nested-container hand-off, and an observable scroll state for prev/next buttons and progress indicators.
|
|
6
|
+
|
|
7
|
+
- No CSS shipped, no required DOM structure, no wrapper elements.
|
|
8
|
+
- The native `scrollLeft` / `scrollTop` is the only source of truth. Nothing is moved with `transform`.
|
|
9
|
+
- Nested instances (a kanban board of scrolling columns, a carousel inside a page row) share one coordinator and never fight over a drag.
|
|
10
|
+
- Framework-agnostic core plus an optional React hook.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm add @anyknown/axis
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Vanilla
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<div class="row">
|
|
22
|
+
<div class="card">1</div>
|
|
23
|
+
<div class="card">2</div>
|
|
24
|
+
<!-- … -->
|
|
25
|
+
</div>
|
|
26
|
+
<button id="prev">Prev</button>
|
|
27
|
+
<button id="next">Next</button>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```css
|
|
31
|
+
.row {
|
|
32
|
+
display: flex;
|
|
33
|
+
gap: 12px;
|
|
34
|
+
overflow-x: auto;
|
|
35
|
+
/* optional: */
|
|
36
|
+
scroll-snap-type: x mandatory;
|
|
37
|
+
}
|
|
38
|
+
.card {
|
|
39
|
+
flex: 0 0 240px;
|
|
40
|
+
scroll-snap-align: start;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { createAxis } from "@anyknown/axis"
|
|
46
|
+
|
|
47
|
+
const row = document.querySelector<HTMLElement>(".row")!
|
|
48
|
+
const axis = createAxis(row, { wheel: true })
|
|
49
|
+
|
|
50
|
+
const prev = document.querySelector<HTMLButtonElement>("#prev")!
|
|
51
|
+
const next = document.querySelector<HTMLButtonElement>("#next")!
|
|
52
|
+
prev.addEventListener("click", () => axis.scrollPrev())
|
|
53
|
+
next.addEventListener("click", () => axis.scrollNext())
|
|
54
|
+
|
|
55
|
+
axis.subscribe((state) => {
|
|
56
|
+
prev.disabled = !state.x.canScrollPrev
|
|
57
|
+
next.disabled = !state.x.canScrollNext
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// later
|
|
61
|
+
axis.destroy()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
A vertical list is `createAxis(list, { axis: "y" })`. A pannable canvas is `createAxis(viewport, { axis: "both" })`, where the viewport has `overflow: auto` and one large child.
|
|
65
|
+
|
|
66
|
+
## React
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { useAxis } from "@anyknown/axis/react"
|
|
70
|
+
|
|
71
|
+
function Row({ items }: { items: string[] }) {
|
|
72
|
+
const { ref, state, api } = useAxis<HTMLDivElement>()
|
|
73
|
+
return (
|
|
74
|
+
<>
|
|
75
|
+
<div ref={ref} className="row">
|
|
76
|
+
{items.map((item) => (
|
|
77
|
+
<div key={item} className="card">
|
|
78
|
+
{item}
|
|
79
|
+
</div>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
<button disabled={!state.x.canScrollPrev} onClick={() => api.scrollPrev()}>
|
|
83
|
+
Prev
|
|
84
|
+
</button>
|
|
85
|
+
<button disabled={!state.x.canScrollNext} onClick={() => api.scrollNext()}>
|
|
86
|
+
Next
|
|
87
|
+
</button>
|
|
88
|
+
</>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`ref` is a stable callback ref, so it can be merged with your own ref inline (`ref={(el) => { mine.current = el; ref(el) }}`). The instance is created when the element mounts, destroyed when it unmounts, and moved when the ref is attached to another element. Option changes are applied in place. `state` comes from `useSyncExternalStore`, so the component re-renders only when a value changes. `api` (`scrollPrev`, `scrollNext`, `scrollTo`, `refresh`) is stable and does nothing before the element mounts. React (18 or 19) is an optional peer dependency; the core does not import it.
|
|
94
|
+
|
|
95
|
+
### Selecting part of the state
|
|
96
|
+
|
|
97
|
+
The full state changes on every scrolled pixel. When a component only needs part of it, pass a selector as the second argument. `state` is then the selector's result, and the component re-renders only when that result changes (compared with `Object.is`):
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
function NextButton() {
|
|
101
|
+
const { ref, state: canScrollNext, api } = useAxis(undefined, (state) => state.x.canScrollNext)
|
|
102
|
+
// canScrollNext: boolean, inferred from the selector
|
|
103
|
+
return (
|
|
104
|
+
<>
|
|
105
|
+
<div ref={ref} className="row">
|
|
106
|
+
{/* … */}
|
|
107
|
+
</div>
|
|
108
|
+
<button disabled={!canScrollNext} onClick={() => api.scrollNext()}>
|
|
109
|
+
Next
|
|
110
|
+
</button>
|
|
111
|
+
</>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Return a primitive or an existing object from the state (`(state) => state.x`). A selector that builds a new object or array re-renders on every state change. An inline selector is fine; it does not need `useCallback`.
|
|
117
|
+
|
|
118
|
+
Do not pass type arguments together with a selector: `useAxis<HTMLDivElement>(undefined, selector)` does not compile, because TypeScript cannot infer the selected type once any type argument is given. Leave them out and both the ref and the selected type work: `ref` accepts any HTML element.
|
|
119
|
+
|
|
120
|
+
## API
|
|
121
|
+
|
|
122
|
+
### `createAxis(el, options?) => AxisInstance`
|
|
123
|
+
|
|
124
|
+
| Method | Description |
|
|
125
|
+
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
126
|
+
| `getState()` | Current state. The object is replaced (never mutated) when a value changes. |
|
|
127
|
+
| `subscribe(listener)` | Calls `listener(state)` on every change. Returns an unsubscribe function. |
|
|
128
|
+
| `scrollPrev(axis?)` | Previous snap position on `axis`, or back by 90% of the visible size when that axis does not snap. |
|
|
129
|
+
| `scrollNext(axis?)` | Next snap position on `axis`, or forward by 90% of the visible size when that axis does not snap. |
|
|
130
|
+
| `scrollTo({ x?, y? }, { animate })` | Scroll to a logical position in px from the start edge of each axis. Missing axes stay put. `animate` defaults to `true`. |
|
|
131
|
+
| `setOptions(partial)` | Change options. Missing or `undefined` keys keep their value. |
|
|
132
|
+
| `refresh()` | Re-read the layout: clear the cached snap positions and update the state now. Rarely needed; see Limitations. |
|
|
133
|
+
| `destroy()` | Remove every listener and observer, and restore every inline style and attribute axis changed. |
|
|
134
|
+
|
|
135
|
+
`scrollPrev` and `scrollNext` default to the configured axis. With `axis: "both"` there is no default, and calling them without `"x"` or `"y"` throws an error.
|
|
136
|
+
|
|
137
|
+
### Options
|
|
138
|
+
|
|
139
|
+
| Option | Default | Description |
|
|
140
|
+
| ---------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
141
|
+
| `axis` | `"x"` | `"x"` drags horizontally and ignores vertical mouse movement, `"y"` drags vertically, `"both"` pans freely in 2D. |
|
|
142
|
+
| `momentum` | `true` | Keep gliding after a fast mouse drag is released, along the release velocity vector. Never applied under `prefers-reduced-motion`. |
|
|
143
|
+
| `wheel` | `false` | Translate vertical mouse-wheel scrolling into horizontal scrolling. **Only used with `axis: "x"`**; see below. |
|
|
144
|
+
| `cursor` | `true` | Set `cursor: grab` while the container can be dragged and `grabbing` while dragging. |
|
|
145
|
+
|
|
146
|
+
### State
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
interface AxisScrollState {
|
|
150
|
+
x: AxisState
|
|
151
|
+
y: AxisState
|
|
152
|
+
isDragging: boolean // a mouse drag is scrolling the container
|
|
153
|
+
isMomentum: boolean // the container glides after a drag (momentum, or settling on a snap point)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface AxisState {
|
|
157
|
+
scroll: number // logical distance from the start edge, 0 to maxScroll
|
|
158
|
+
maxScroll: number // 0 when the content fits
|
|
159
|
+
progress: number // scroll / maxScroll, 0 to 1 (0 when the content fits)
|
|
160
|
+
canScrollPrev: boolean // more than 1px away from the start edge
|
|
161
|
+
canScrollNext: boolean // more than 1px away from the end edge
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Both axes are always reported, whatever `axis` is set to. State updates come from `scroll` events, a `ResizeObserver` on the container and its children, and a `MutationObserver` for added or removed children, `class`, `style` and `dir` changes on the container or anything inside it, and `dir` changes on its ancestors. Listeners are not called when nothing changed, and an axis object that did not change keeps its identity.
|
|
166
|
+
|
|
167
|
+
## Behaviour
|
|
168
|
+
|
|
169
|
+
**Only the mouse is taken over.** A drag starts only for `pointerType === "mouse"` with the primary button, and only after the pointer moves more than 5px. Below that it is an ordinary click. Touch and pen keep native scrolling, and presses on either scrollbar are left to the browser.
|
|
170
|
+
|
|
171
|
+
**The axis decides what a drag means.** When the movement passes the threshold, its dominant direction is compared with `axis`. An `"x"` instance takes mostly horizontal drags and ignores the vertical part of the movement; a mostly vertical drag is not an `"x"` drag at all, so text selection and the page behave natively. `"y"` is the mirror image. `"both"` takes any direction and follows the pointer in 2D without locking.
|
|
172
|
+
|
|
173
|
+
**Form controls are left alone.** A press inside `input`, `textarea`, `select`, `[contenteditable]` or any element with `data-axis-no-drag` never starts a drag.
|
|
174
|
+
|
|
175
|
+
**While dragging**, the container gets `data-axis-dragging`, `cursor: grabbing` and `user-select: none`. Native `dragstart` (images, links) is cancelled while the mouse is down. Pointer capture keeps the drag going outside the element. Once the movement passes the threshold, every axis container under the pointer (the one that owns the drag and the ones around it) holds its scroll position on both axes, except the axes being dragged, so Safari's selection autoscroll cannot move them when the pointer leaves them. Until then the press may still be a click and nothing is held: `scrollIntoView`, focus scrolling and scroll anchoring work as usual. While the drag lasts, the page's own programmatic scrolling of those containers is undone too. A wheel event or a scrolling key anywhere (arrows, Page Up/Down, Home, End, Space, outside text fields) lets go of every hold, so scrolling on purpose still works; modifier keys do not. A drag also ends without a `pointerup` when the window loses focus, the tab is hidden, a context menu opens, or the button turns out to be up (a release over an iframe).
|
|
176
|
+
|
|
177
|
+
**Clicks after a drag are swallowed once.** After a real drag, the next `click` is stopped in the capture phase (`preventDefault` + `stopPropagation`), so releasing over a link or button does not activate it. This happens once per drag, also with nested instances. The suppression ends after 100ms or at the next press, whichever comes first, so it never eats a later click (Safari fires no click at all when the press and the release hit different elements).
|
|
178
|
+
|
|
179
|
+
**Momentum** uses the 2D pointer velocity from roughly the last 100ms before release and decays with friction on `requestAnimationFrame`, only on the axes the instance drags. Any `wheel`, `pointerdown`, `touchstart` or scrolling key stops it at once: a mouse press stops the momentum of every axis container under the pointer, like grabbing a flung list. A glide your code started (`scrollTo`, prev/next, wheel snapping) is not momentum: a mouse press that can start a drag leaves it running, and it only stops when that container itself is dragged. A press that cannot start a drag (touch, pen, another button, a scrollbar, or a press in a form field or `data-axis-no-drag` content) stops it like any other native input. A container whose glide is running when another one takes the drag is not held until that glide ends. A pointer that stopped before release does not fling.
|
|
180
|
+
|
|
181
|
+
**scroll-snap works on both axes.** axis reads `scroll-snap-type` (`x`, `y`, `both`, `inline`, `block`), the two values of each child's `scroll-snap-align` (block, then inline), `scroll-padding` and `scroll-margin` on all four sides. While axis drives the scroll it sets `scroll-snap-type: none` and `scroll-behavior: auto` inline. On release it predicts where momentum would land, picks the nearest snap position on each snapping axis independently, glides there on both axes together with its own animation, and only then restores the original inline values. It does not rely on the browser re-snapping when the snap type comes back, because Safari does not do that reliably.
|
|
182
|
+
|
|
183
|
+
**prev/next** go to the adjacent snap position on that axis when it snaps, otherwise by 90% of the visible size. Pressing again during the glide continues from where the glide is heading. Under `prefers-reduced-motion: reduce` they jump.
|
|
184
|
+
|
|
185
|
+
**Wheel translation is for rows only.** With `axis: "x"` and `wheel: true`, a mostly vertical, non-shift wheel event scrolls horizontally (line and page delta modes are normalised). At the edge in the wheel's direction the event is left alone, so the page scrolls on. With snap, it settles on the next snap position in the wheel's direction after the wheel goes quiet. With `axis: "y"` or `"both"` the option is ignored: native wheel and trackpad scrolling already cover those directions, and taking them over would only make scrolling worse.
|
|
186
|
+
|
|
187
|
+
**RTL.** With `direction: rtl`, browsers report `scrollLeft` from `0` down to negative values. axis converts x to logical positions: `state.x.scroll`, `progress`, `canScrollPrev/Next`, `scrollTo` and prev/next all count from the inline start. y is not affected.
|
|
188
|
+
|
|
189
|
+
## Nested containers
|
|
190
|
+
|
|
191
|
+
All instances in a window share one small coordinator. A press does not pick an owner. When the movement passes the 5px threshold, the coordinator takes its dominant direction and walks the instances under the pointer from the innermost outwards. The drag goes to the first instance whose `axis` covers that direction and that can still scroll the way the pointer pushes. An instance that is already at its edge in that direction hands the drag to the next instance out, like native overscroll chaining. If none of them can scroll, the innermost instance whose `axis` covers the direction keeps the gesture, so the drag still does not turn into a click. If no instance covers the direction, the browser handles the movement. `"both"` instances cover both directions. The owner does not change for the rest of that drag.
|
|
192
|
+
|
|
193
|
+
A kanban board is an `axis: "x"` board whose columns are `axis: "y"` containers: vertical drags scroll the column under the pointer, horizontal drags scroll the board.
|
|
194
|
+
|
|
195
|
+
## How this differs from Embla, Keen Slider and other transform-based carousels
|
|
196
|
+
|
|
197
|
+
Transform-based carousels own the position. They intercept every pointer (touch included), move a track with `transform: translate`, and re-implement physics, snapping, looping and accessibility on top. That gives full control (loops, custom easing, slide effects), but scrolling is no longer real scrolling: the browser's touch physics, trackpad momentum, keyboard scrolling, scrollbars, `scroll-snap`, find-in-page and scroll-linked CSS all have to be emulated or are lost.
|
|
198
|
+
|
|
199
|
+
axis does the opposite. The container is a real scroll container and stays one. The browser handles every input except a primary-button mouse drag, which desktop browsers do not offer natively. axis fills that gap and gives you state for building controls.
|
|
200
|
+
|
|
201
|
+
Choose axis when you want native-feeling rows, lists or canvases that also drag with a mouse. Choose a transform-based carousel when you need looping, slide transitions, or full control over the motion on touch devices.
|
|
202
|
+
|
|
203
|
+
## Browser support
|
|
204
|
+
|
|
205
|
+
Tested in Chromium and WebKit (Safari's engine) on every change. Firefox is expected to work but is not part of the test run.
|
|
206
|
+
|
|
207
|
+
## Limitations
|
|
208
|
+
|
|
209
|
+
- Only the direct children of the container are read as snap areas.
|
|
210
|
+
- Snap positions are cached. The cache is cleared when the container or a child resizes, children are added or removed, a `class`, `style` or `dir` attribute changes on the container or anything inside it, a `dir` attribute changes on an ancestor, or `setOptions` is called. Call `refresh()` only when snap properties change in a way none of these see: a `class` change on an ancestor, or a stylesheet or media query that changes `scroll-padding`, `scroll-margin` or `scroll-snap-align` without resizing anything.
|
|
211
|
+
- Overlay scrollbars (macOS default) cannot be told apart from content, so a mouse press on an overlay scrollbar thumb starts a drag instead.
|
|
212
|
+
- `proximity` snapping is settled like `mandatory`: a drag release always lands on the nearest snap position.
|
|
213
|
+
- Horizontal writing modes only: `inline` means x and `block` means y. RTL targets the modern `scrollLeft` model (0 to negative), supported in all current browsers.
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT
|
package/dist/axis.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** The direction(s) a mouse drag scrolls: horizontal, vertical, or free 2D panning. */
|
|
2
|
+
export type AxisMode = "x" | "y" | "both";
|
|
3
|
+
/** A single scroll axis. */
|
|
4
|
+
export type AxisName = "x" | "y";
|
|
5
|
+
/** Options for {@link createAxis}. Every option can be changed later with {@link AxisInstance.setOptions}. */
|
|
6
|
+
export interface AxisOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Which direction a mouse drag scrolls. `"x"` ignores vertical mouse movement, `"y"` ignores horizontal
|
|
9
|
+
* movement, and `"both"` pans freely in 2D.
|
|
10
|
+
* @default "x"
|
|
11
|
+
*/
|
|
12
|
+
axis?: AxisMode;
|
|
13
|
+
/**
|
|
14
|
+
* Keep gliding after a fast mouse drag is released. Never applied under `prefers-reduced-motion: reduce`.
|
|
15
|
+
* @default true
|
|
16
|
+
*/
|
|
17
|
+
momentum?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Translate vertical mouse-wheel scrolling into horizontal scrolling. Only used with `axis: "x"`; at either
|
|
20
|
+
* end the wheel is left alone, so the page keeps scrolling.
|
|
21
|
+
* @default false
|
|
22
|
+
*/
|
|
23
|
+
wheel?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Set `cursor: grab` on the container while it can be dragged, and `grabbing` while dragging.
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
cursor?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** Scroll state of one axis. Positions are logical: measured from the start edge, so positive in RTL too. */
|
|
31
|
+
export interface AxisState {
|
|
32
|
+
/** Distance scrolled from the start edge, in px (0 to `maxScroll`). */
|
|
33
|
+
scroll: number;
|
|
34
|
+
/** Largest possible `scroll`, in px. 0 when the content fits. */
|
|
35
|
+
maxScroll: number;
|
|
36
|
+
/** `scroll / maxScroll`, from 0 to 1. 0 when the content fits. */
|
|
37
|
+
progress: number;
|
|
38
|
+
/** True when the container is more than 1px away from the start edge. */
|
|
39
|
+
canScrollPrev: boolean;
|
|
40
|
+
/** True when the container is more than 1px away from the end edge. */
|
|
41
|
+
canScrollNext: boolean;
|
|
42
|
+
}
|
|
43
|
+
/** Scroll state of both axes, whatever `axis` is set to. */
|
|
44
|
+
export interface AxisScrollState {
|
|
45
|
+
x: AxisState;
|
|
46
|
+
y: AxisState;
|
|
47
|
+
/** True while a mouse drag is scrolling the container. */
|
|
48
|
+
isDragging: boolean;
|
|
49
|
+
/** True while the container glides after a drag is released (momentum or settling on a snap position). */
|
|
50
|
+
isMomentum: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** A logical scroll position. Axes that are left out keep their current position. */
|
|
53
|
+
export interface AxisPosition {
|
|
54
|
+
x?: number;
|
|
55
|
+
y?: number;
|
|
56
|
+
}
|
|
57
|
+
/** Options for {@link AxisInstance.scrollTo}. */
|
|
58
|
+
export interface AxisScrollToOptions {
|
|
59
|
+
/** Animate the scroll. Ignored (jumps) under `prefers-reduced-motion: reduce`. @default true */
|
|
60
|
+
animate?: boolean;
|
|
61
|
+
}
|
|
62
|
+
/** An axis instance bound to one scroll container. */
|
|
63
|
+
export interface AxisInstance {
|
|
64
|
+
/** The current state. The object is replaced, never mutated, when a value changes. */
|
|
65
|
+
getState(): AxisScrollState;
|
|
66
|
+
/** Calls `listener` whenever the state changes. Returns a function that removes the listener. */
|
|
67
|
+
subscribe(listener: (state: AxisScrollState) => void): () => void;
|
|
68
|
+
/**
|
|
69
|
+
* Scrolls `axis` to the previous snap position, or back by 90% of the visible size when it does not snap.
|
|
70
|
+
* `axis` defaults to the configured axis and is required with `axis: "both"`.
|
|
71
|
+
*/
|
|
72
|
+
scrollPrev(axis?: AxisName): void;
|
|
73
|
+
/**
|
|
74
|
+
* Scrolls `axis` to the next snap position, or forward by 90% of the visible size when it does not snap.
|
|
75
|
+
* `axis` defaults to the configured axis and is required with `axis: "both"`.
|
|
76
|
+
*/
|
|
77
|
+
scrollNext(axis?: AxisName): void;
|
|
78
|
+
/** Scrolls to a logical position (px from the start edge of each axis). */
|
|
79
|
+
scrollTo(position: AxisPosition, options?: AxisScrollToOptions): void;
|
|
80
|
+
/** Changes some options. Keys that are left out or `undefined` keep their current value. */
|
|
81
|
+
setOptions(options: AxisOptions): void;
|
|
82
|
+
/**
|
|
83
|
+
* Re-reads the layout: clears the cached snap positions and updates the state. Only needed after a change
|
|
84
|
+
* axis cannot observe, such as a stylesheet or media query changing snap properties without a resize.
|
|
85
|
+
*/
|
|
86
|
+
refresh(): void;
|
|
87
|
+
/** Removes every listener and observer and restores every inline style that axis changed. */
|
|
88
|
+
destroy(): void;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Enhances a scroll container (`overflow: auto`) with mouse drag-scrolling, momentum, snap-aware release,
|
|
92
|
+
* optional wheel translation and an observable scroll state.
|
|
93
|
+
*
|
|
94
|
+
* Native scrolling stays the source of truth: touch, trackpad, keyboard and scrollbar input are untouched,
|
|
95
|
+
* and only a primary-button mouse drag is taken over (by writing `scrollLeft` / `scrollTop`). Nested instances
|
|
96
|
+
* share the drag: the innermost instance that can scroll in the drag's direction takes it.
|
|
97
|
+
*/
|
|
98
|
+
export declare function createAxis(el: HTMLElement, options?: AxisOptions): AxisInstance;
|