@studiometa/js-toolkit 4.0.0-alpha.1 → 4.0.0-alpha.2
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 +10 -0
- package/package.json +5 -2
- package/skills/studiometa-js-toolkit/SKILL.md +87 -0
- package/skills/studiometa-js-toolkit/references/api/configuration.md +169 -0
- package/skills/studiometa-js-toolkit/references/api/context/createContext.md +46 -0
- package/skills/studiometa-js-toolkit/references/api/context/createGroup.md +67 -0
- package/skills/studiometa-js-toolkit/references/api/context/index.md +69 -0
- package/skills/studiometa-js-toolkit/references/api/context/injectContext.md +58 -0
- package/skills/studiometa-js-toolkit/references/api/context/injectContextSync.md +44 -0
- package/skills/studiometa-js-toolkit/references/api/context/provideContext.md +61 -0
- package/skills/studiometa-js-toolkit/references/api/context/provideRootContext.md +42 -0
- package/skills/studiometa-js-toolkit/references/api/context/signal.md +85 -0
- package/skills/studiometa-js-toolkit/references/api/context/subscribeContext.md +71 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/children.md +92 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/component.md +75 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/index.md +41 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/inject.md +73 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/on.md +149 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/provide.md +77 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/read-write.md +93 -0
- package/skills/studiometa-js-toolkit/references/api/diagnostics/DIAGNOSTICS.md +137 -0
- package/skills/studiometa-js-toolkit/references/api/diagnostics/EVENTS.md +45 -0
- package/skills/studiometa-js-toolkit/references/api/diagnostics/index.md +58 -0
- package/skills/studiometa-js-toolkit/references/api/diagnostics/reportDiagnostic.md +67 -0
- package/skills/studiometa-js-toolkit/references/api/diagnostics/warn.md +54 -0
- package/skills/studiometa-js-toolkit/references/api/dom/breakpoints.md +81 -0
- package/skills/studiometa-js-toolkit/references/api/dom/domUpdate.md +70 -0
- package/skills/studiometa-js-toolkit/references/api/dom/emitExtendable.md +68 -0
- package/skills/studiometa-js-toolkit/references/api/dom/index.md +26 -0
- package/skills/studiometa-js-toolkit/references/api/dom/swap.md +105 -0
- package/skills/studiometa-js-toolkit/references/api/dom/watchAttributeNamespace.md +96 -0
- package/skills/studiometa-js-toolkit/references/api/dom/watchAttributes.md +56 -0
- package/skills/studiometa-js-toolkit/references/api/dom/whenDOMSettled.md +41 -0
- package/skills/studiometa-js-toolkit/references/api/html/data-component.md +105 -0
- package/skills/studiometa-js-toolkit/references/api/html/data-mount.md +69 -0
- package/skills/studiometa-js-toolkit/references/api/html/data-option.md +106 -0
- package/skills/studiometa-js-toolkit/references/api/html/data-ref.md +63 -0
- package/skills/studiometa-js-toolkit/references/api/html/index.md +32 -0
- package/skills/studiometa-js-toolkit/references/api/index.md +73 -0
- package/skills/studiometa-js-toolkit/references/api/instance-events.md +75 -0
- package/skills/studiometa-js-toolkit/references/api/instance-methods.md +267 -0
- package/skills/studiometa-js-toolkit/references/api/instance-properties.md +90 -0
- package/skills/studiometa-js-toolkit/references/api/methods-hooks-events.md +149 -0
- package/skills/studiometa-js-toolkit/references/api/methods-hooks-lifecycle.md +90 -0
- package/skills/studiometa-js-toolkit/references/api/methods-hooks-options.md +96 -0
- package/skills/studiometa-js-toolkit/references/api/methods-hooks-services.md +164 -0
- package/skills/studiometa-js-toolkit/references/api/registry/defineManifest.md +57 -0
- package/skills/studiometa-js-toolkit/references/api/registry/fromMetaGlob.md +33 -0
- package/skills/studiometa-js-toolkit/references/api/registry/fromWebpackContext.md +37 -0
- package/skills/studiometa-js-toolkit/references/api/registry/getInstance.md +52 -0
- package/skills/studiometa-js-toolkit/references/api/registry/getInstances.md +48 -0
- package/skills/studiometa-js-toolkit/references/api/registry/getMountedInstances.md +39 -0
- package/skills/studiometa-js-toolkit/references/api/registry/getUnmountedInstances.md +46 -0
- package/skills/studiometa-js-toolkit/references/api/registry/index.md +49 -0
- package/skills/studiometa-js-toolkit/references/api/registry/registerComponent.md +93 -0
- package/skills/studiometa-js-toolkit/references/api/registry/registerComponents.md +37 -0
- package/skills/studiometa-js-toolkit/references/api/registry/registerManifest.md +72 -0
- package/skills/studiometa-js-toolkit/references/api/scheduler/defaultScheduler.md +104 -0
- package/skills/studiometa-js-toolkit/references/api/scheduler/index.md +27 -0
- package/skills/studiometa-js-toolkit/references/api/scheduler/nextFrame.md +31 -0
- package/skills/studiometa-js-toolkit/references/api/scheduler/viewTransition.md +53 -0
- package/skills/studiometa-js-toolkit/references/api/services/createService.md +95 -0
- package/skills/studiometa-js-toolkit/references/api/services/createServiceMixin.md +81 -0
- package/skills/studiometa-js-toolkit/references/api/services/index.md +94 -0
- package/skills/studiometa-js-toolkit/references/api/services/mixins.md +148 -0
- package/skills/studiometa-js-toolkit/references/api/services/perTarget.md +65 -0
- package/skills/studiometa-js-toolkit/references/api/services/toggle.md +80 -0
- package/skills/studiometa-js-toolkit/references/api/services/until.md +44 -0
- package/skills/studiometa-js-toolkit/references/api/services/useBreakpoint.md +42 -0
- package/skills/studiometa-js-toolkit/references/api/services/useDrag.md +119 -0
- package/skills/studiometa-js-toolkit/references/api/services/useInView.md +64 -0
- package/skills/studiometa-js-toolkit/references/api/services/useKey.md +105 -0
- package/skills/studiometa-js-toolkit/references/api/services/useMediaQuery.md +45 -0
- package/skills/studiometa-js-toolkit/references/api/services/useMutation.md +69 -0
- package/skills/studiometa-js-toolkit/references/api/services/usePointer.md +91 -0
- package/skills/studiometa-js-toolkit/references/api/services/usePrefersReducedMotion.md +42 -0
- package/skills/studiometa-js-toolkit/references/api/services/useRaf.md +91 -0
- package/skills/studiometa-js-toolkit/references/api/services/useResize.md +60 -0
- package/skills/studiometa-js-toolkit/references/api/services/useScroll.md +88 -0
- package/skills/studiometa-js-toolkit/references/api/services/useScrollProgress.md +88 -0
- package/skills/studiometa-js-toolkit/references/api/services/useWindowScroll.md +25 -0
- package/skills/studiometa-js-toolkit/references/api/services/useWindowSize.md +25 -0
- package/skills/studiometa-js-toolkit/references/api/storage/createStorage.md +126 -0
- package/skills/studiometa-js-toolkit/references/api/storage/index.md +33 -0
- package/skills/studiometa-js-toolkit/references/api/storage/presets.md +58 -0
- package/skills/studiometa-js-toolkit/references/api/storage/providers.md +109 -0
- package/skills/studiometa-js-toolkit/references/api/test/index.md +214 -0
- package/skills/studiometa-js-toolkit/references/guide/concepts/attribute-grammar.md +93 -0
- package/skills/studiometa-js-toolkit/references/guide/concepts/mutation-engine.md +71 -0
- package/skills/studiometa-js-toolkit/references/guide/concepts/philosophy.md +78 -0
- package/skills/studiometa-js-toolkit/references/guide/concepts/the-registry.md +110 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/autoloading.md +104 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/handling-diagnostics.md +104 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/mount-strategies.md +95 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/scheduling-work.md +124 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/sharing-state.md +218 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/swapping-content.md +92 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/testing-components.md +137 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/typing-components.md +190 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/using-decorators.md +168 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/using-storage.md +125 -0
- package/skills/studiometa-js-toolkit/references/guide/index.md +163 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/installation.md +144 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/lifecycle-hooks.md +174 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/managing-components.md +229 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/managing-options.md +235 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/managing-refs.md +174 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/using-services.md +218 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/working-with-events.md +251 -0
- package/skills/studiometa-js-toolkit/references/guide/migration/v3-to-v4.md +373 -0
- package/skills/studiometa-js-toolkit/references/utils/css.md +98 -0
- package/skills/studiometa-js-toolkit/references/utils/dom.md +54 -0
- package/skills/studiometa-js-toolkit/references/utils/easings.md +217 -0
- package/skills/studiometa-js-toolkit/references/utils/focus.md +84 -0
- package/skills/studiometa-js-toolkit/references/utils/history.md +69 -0
- package/skills/studiometa-js-toolkit/references/utils/index.md +53 -0
- package/skills/studiometa-js-toolkit/references/utils/is.md +84 -0
- package/skills/studiometa-js-toolkit/references/utils/load.md +59 -0
- package/skills/studiometa-js-toolkit/references/utils/math.md +129 -0
- package/skills/studiometa-js-toolkit/references/utils/motion.md +218 -0
- package/skills/studiometa-js-toolkit/references/utils/objects.md +68 -0
- package/skills/studiometa-js-toolkit/references/utils/scroll.md +128 -0
- package/skills/studiometa-js-toolkit/references/utils/strings.md +176 -0
- package/skills/studiometa-js-toolkit/references/utils/timing.md +111 -0
- package/skills/studiometa-js-toolkit/references/utils/transitions.md +122 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# useBreakpoint
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
useBreakpoint(): Service<{ readonly name: string }>
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
The active breakpoint name, as a service. It is its **own source** — it is not derived from a resize.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { useBreakpoint } from '@studiometa/js-toolkit';
|
|
13
|
+
|
|
14
|
+
const unsubscribe = useBreakpoint().subscribe(({ name }) => {
|
|
15
|
+
document.documentElement.dataset.breakpoint = name;
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Why it is not part of the resize service
|
|
20
|
+
|
|
21
|
+
It is backed by `matchMedia` `change` listeners, so it emits **on crossings** and it reports a change of the reader's font size — which a `ResizeObserver` on a box cannot see.
|
|
22
|
+
|
|
23
|
+
The values are in `rem`, and in a media query `rem` resolves against the **initial** font size, not against the root element.
|
|
24
|
+
|
|
25
|
+
`ResizeProps` therefore dropped `breakpoints` and `activeBreakpoints`: they were a different measurement wearing the same shape.
|
|
26
|
+
|
|
27
|
+
## The named set
|
|
28
|
+
|
|
29
|
+
See [Breakpoints](../dom/breakpoints.md) for `BREAKPOINTS`, `getBreakpoints()` and `setBreakpoints()`.
|
|
30
|
+
|
|
31
|
+
`setBreakpoints()` replaces the set and emits at once. The `MediaQueryList` objects are built once, and the active name is memoised for the length of one task — `setBreakpoints()` and the `change` handler of a running service clear that memo immediately.
|
|
32
|
+
|
|
33
|
+
## Who else uses it
|
|
34
|
+
|
|
35
|
+
- Every responsive **option** resolves through the same active-name read, but a `matchMedia` subscription only opens for a component that declares [`option<Name>Changed()`](../methods-hooks-options.md).
|
|
36
|
+
- Connected elements with a scoped `data-component` declaration share **one** reference-counted subscription to this service. A page with plain declarations opens none.
|
|
37
|
+
|
|
38
|
+
The breakpoint service is part of the core graph on every page. What it costs is a subscription, and a page that asks for nothing opens none.
|
|
39
|
+
|
|
40
|
+
## `{ immediate: true }` works here
|
|
41
|
+
|
|
42
|
+
There is always a current breakpoint.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# useDrag
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
useDrag(target: HTMLElement | SVGElement, options?: DragOptions): Service<DragProps>
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
A drag gesture, with an optional inertia phase.
|
|
8
|
+
|
|
9
|
+
## Props
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
interface DragProps {
|
|
13
|
+
readonly mode: DragMode;
|
|
14
|
+
readonly x: number;
|
|
15
|
+
readonly y: number;
|
|
16
|
+
readonly deltaX: number;
|
|
17
|
+
readonly deltaY: number;
|
|
18
|
+
readonly originX: number;
|
|
19
|
+
readonly originY: number;
|
|
20
|
+
readonly distanceX: number;
|
|
21
|
+
readonly distanceY: number;
|
|
22
|
+
readonly finalX: number;
|
|
23
|
+
readonly finalY: number;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`isGrabbing`, `hasInertia` and `target` are removed, and `props.MODES` with them.
|
|
28
|
+
|
|
29
|
+
## The modes
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import { DRAG_MODES } from '@studiometa/js-toolkit';
|
|
33
|
+
|
|
34
|
+
DRAG_MODES.IDLE; // 'idle'
|
|
35
|
+
DRAG_MODES.START; // 'start'
|
|
36
|
+
DRAG_MODES.DRAG; // 'drag'
|
|
37
|
+
DRAG_MODES.DROP; // 'drop'
|
|
38
|
+
DRAG_MODES.INERTIA; // 'inertia'
|
|
39
|
+
DRAG_MODES.STOP; // 'stop'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`idle` is new in v4. `DragMode` is derived from the frozen object — that is the pattern for every closed set of strings in the framework.
|
|
43
|
+
|
|
44
|
+
## Options
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
interface DragOptions {
|
|
48
|
+
axis?: 'x' | 'y' | 'both';
|
|
49
|
+
dampFactor?: number;
|
|
50
|
+
dragThreshold?: number;
|
|
51
|
+
inertia?: boolean;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The v3 spelling `dragTreshold` is fixed.
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
import { Base, DRAG_MODES, useDrag } from '@studiometa/js-toolkit';
|
|
61
|
+
|
|
62
|
+
class Slide extends Base {
|
|
63
|
+
static config = { name: 'Slide' };
|
|
64
|
+
|
|
65
|
+
mounted() {
|
|
66
|
+
return useDrag(this.$el, { axis: 'x' }).subscribe(({ mode, x, finalX }) => {
|
|
67
|
+
if (mode === DRAG_MODES.DROP) {
|
|
68
|
+
this.settle(finalX);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.$el.style.transform = `translateX(${x}px)`;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
settle(x) {}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## A gesture the browser can steal is not a gesture
|
|
80
|
+
|
|
81
|
+
`useDrag` owns both axes by default and writes `touch-action: none`.
|
|
82
|
+
|
|
83
|
+
| `axis` | writes | and |
|
|
84
|
+
| -------- | ------- | ----------------------------- |
|
|
85
|
+
| `'both'` | `none` | — |
|
|
86
|
+
| `'x'` | `pan-y` | every Y movement prop is zero |
|
|
87
|
+
| `'y'` | `pan-x` | every X movement prop is zero |
|
|
88
|
+
|
|
89
|
+
It writes **only when the computed value is `auto`**, and it restores the previous inline value when the last service of those options leaves. Services on one target share that ownership.
|
|
90
|
+
|
|
91
|
+
The click that ends a drag is suppressed from a flag that movement on the owned axis arms and the next `pointerdown` disarms — and only for a **trusted** click with a non-zero `detail`.
|
|
92
|
+
|
|
93
|
+
## Inertia
|
|
94
|
+
|
|
95
|
+
With `{ inertia: false }` a drag publishes the **exact projected destination** at `drop`, then goes through `stop` to `idle` with no tick subscription at all.
|
|
96
|
+
|
|
97
|
+
With inertia, the coast subscribes to `scheduler.tick()`. Decay is expressed in time, not in frames:
|
|
98
|
+
|
|
99
|
+
- `INERTIA_FRAME` (16.67 ms) is the reference of every factor;
|
|
100
|
+
- `inertiaStep()` integrates the decay across the step, so any sequence of frames sums to `velocity · τ` exactly;
|
|
101
|
+
- the settle position is `value + velocity · τ` with `τ = INERTIA_FRAME / ln(1 / damp)`.
|
|
102
|
+
|
|
103
|
+
The velocity is sampled as a distance over the interval between events, smoothed, with the interval clamped at both ends. **At the drop, the velocity is decayed by the idle time through the same law** — so a pointer that stopped moving before release does not fling.
|
|
104
|
+
|
|
105
|
+
## Keying
|
|
106
|
+
|
|
107
|
+
`useDrag()` keys its axis, inertia, damping and threshold, so two callers asking for the same gesture on the same element share one service. See [`perTarget()`](perTarget.md).
|
|
108
|
+
|
|
109
|
+
## `{ immediate: true }`
|
|
110
|
+
|
|
111
|
+
Does nothing outside a gesture: there is no current value between gestures.
|
|
112
|
+
|
|
113
|
+
## Mixin
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
class Slide extends withDrag(Base, { axis: 'x', inertia: false }) {
|
|
117
|
+
dragged({ mode, x }) {}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# useInView
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
useInView(target: Element, init?: IntersectionObserverInit): Service<InViewProps>
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
An `IntersectionObserver` as a service.
|
|
8
|
+
|
|
9
|
+
## Props
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
interface InViewProps {
|
|
13
|
+
readonly isInView: boolean;
|
|
14
|
+
readonly entry: IntersectionObserverEntry | null;
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { Base, useInView } from '@studiometa/js-toolkit';
|
|
22
|
+
|
|
23
|
+
class Reveal extends Base {
|
|
24
|
+
static config = { name: 'Reveal' };
|
|
25
|
+
|
|
26
|
+
mounted() {
|
|
27
|
+
return useInView(this.$el, { threshold: 0.25 }).subscribe(({ isInView }) => {
|
|
28
|
+
this.$el.classList.toggle('is-in-view', isInView);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Keying
|
|
35
|
+
|
|
36
|
+
`useInView()` keys **every `IntersectionObserverInit` field**, and gives an object root a stable weak identity so two callers passing the same root element share one service.
|
|
37
|
+
|
|
38
|
+
The options are read by meaning, not by spelling: keys are sorted at every depth and the ones holding `undefined` are dropped, so `{ threshold: 0.5, rootMargin: '0px' }` and `{ rootMargin: '0px', threshold: 0.5 }` are the same service.
|
|
39
|
+
|
|
40
|
+
**Nothing groups observers across targets.**
|
|
41
|
+
|
|
42
|
+
## `{ immediate: true }` is the default here
|
|
43
|
+
|
|
44
|
+
`withInView` defaults it to `true`, because "am I in view" has a current answer and a consumer that has to wait for the first crossing to find out would be wrong on load.
|
|
45
|
+
|
|
46
|
+
## This is not a mount strategy
|
|
47
|
+
|
|
48
|
+
`useInView` and `withInView` observe a component that is **already mounted**. They do not replace `data-mount="visible"` or `data-mount="in-view"`, which decide whether the instance exists at all.
|
|
49
|
+
|
|
50
|
+
| Want | Use |
|
|
51
|
+
| ---------------------------------------------- | ---------------------------- |
|
|
52
|
+
| the component to exist only once seen | `data-mount="visible"` |
|
|
53
|
+
| the component to come and go with the viewport | `data-mount="in-view"` |
|
|
54
|
+
| a mounted component to react to crossings | `useInView()` / `withInView` |
|
|
55
|
+
|
|
56
|
+
See [Mount strategies](../../guide/going-further/mount-strategies.md).
|
|
57
|
+
|
|
58
|
+
## Mixin
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
class Reveal extends withInView(Base, { threshold: 0.25 }) {
|
|
62
|
+
intersected({ isInView }) {}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# useKey
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
useKey(target?: Document | Element | Window): Service<KeyProps>
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
The keyboard as a service, one instance per target, defaulting to the document.
|
|
8
|
+
|
|
9
|
+
## Props
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
interface KeyProps {
|
|
13
|
+
readonly event: KeyboardEvent | null;
|
|
14
|
+
readonly triggered: number;
|
|
15
|
+
readonly isDown: boolean;
|
|
16
|
+
readonly isUp: boolean;
|
|
17
|
+
// plus one boolean per named key
|
|
18
|
+
readonly ENTER: boolean;
|
|
19
|
+
readonly SPACE: boolean;
|
|
20
|
+
readonly TAB: boolean;
|
|
21
|
+
readonly ESC: boolean;
|
|
22
|
+
readonly LEFT: boolean;
|
|
23
|
+
readonly UP: boolean;
|
|
24
|
+
readonly RIGHT: boolean;
|
|
25
|
+
readonly DOWN: boolean;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import { Base, useKey } from '@studiometa/js-toolkit';
|
|
33
|
+
|
|
34
|
+
class Dialog extends Base {
|
|
35
|
+
static config = { name: 'Dialog' };
|
|
36
|
+
|
|
37
|
+
mounted() {
|
|
38
|
+
return useKey().subscribe(({ ESC, isDown }) => {
|
|
39
|
+
if (ESC && isDown) this.$el.removeAttribute('data-option-open');
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
A region rather than the document:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import { Base, useKey } from '@studiometa/js-toolkit';
|
|
49
|
+
|
|
50
|
+
class Menu extends Base {
|
|
51
|
+
static config = { name: 'Menu' };
|
|
52
|
+
|
|
53
|
+
mounted() {
|
|
54
|
+
return useKey(this.$el).subscribe(({ DOWN, isDown }) => {
|
|
55
|
+
if (DOWN && isDown) this.focusNext();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
focusNext() {}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**An element target is what removes the `hasFocus` bookkeeping** a document-only service forces on a consumer.
|
|
64
|
+
|
|
65
|
+
## The eight names
|
|
66
|
+
|
|
67
|
+
The eight names of v3 are kept, resolved from `KeyboardEvent.key` rather than from the deprecated `keyCode`:
|
|
68
|
+
|
|
69
|
+
| Name | `event.key` |
|
|
70
|
+
| ------- | ------------ |
|
|
71
|
+
| `ENTER` | `Enter` |
|
|
72
|
+
| `SPACE` | `' '` |
|
|
73
|
+
| `TAB` | `Tab` |
|
|
74
|
+
| `ESC` | `Escape` |
|
|
75
|
+
| `LEFT` | `ArrowLeft` |
|
|
76
|
+
| `UP` | `ArrowUp` |
|
|
77
|
+
| `RIGHT` | `ArrowRight` |
|
|
78
|
+
| `DOWN` | `ArrowDown` |
|
|
79
|
+
|
|
80
|
+
The constant that maps them is **module-internal**: the names reach a consumer as props, so nothing is left to compare against. The flags are a mapped type over that constant, so the props cannot drift from it.
|
|
81
|
+
|
|
82
|
+
**Why they are an exception**
|
|
83
|
+
|
|
84
|
+
Props are flat and nothing derivable is a field — and each of these compares `event.key` against a named value, so nothing about them is unavailable from `event`. They are kept for call-site parity with v3, where `keyed({ ESC })` is how components read the keyboard.
|
|
85
|
+
|
|
86
|
+
## `triggered` counts repeats of _one_ key
|
|
87
|
+
|
|
88
|
+
A keydown whose key matches the previous event's **while that key is still down** increments it. A different key, or any keyup, sets it back to `1`.
|
|
89
|
+
|
|
90
|
+
v3 incremented on any two consecutive keydowns, so holding `A` and then pressing `B` reported `2`.
|
|
91
|
+
|
|
92
|
+
## The listeners are neither passive nor capturing
|
|
93
|
+
|
|
94
|
+
- **Not passive**, because `trapFocus()` and every keyboard shortcut call `preventDefault()` on the event the subscriber is handed, and a passive listener cannot.
|
|
95
|
+
- **No capture**, unlike the pointer service, because a descendant that handles its own keys and stops the propagation is **respected** rather than overheard.
|
|
96
|
+
|
|
97
|
+
## Mixin
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
class Dialog extends withKey(Base) {
|
|
101
|
+
keyed({ ESC, isDown }) {}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`withKey` takes the document default too, as `withScroll` and `withResize` do for their own page-wide source. A region is `withKey(Base, { target: (instance) => instance.$refs.wrapper })`.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# useMediaQuery
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
useMediaQuery(query: string): Service<{ readonly matches: boolean }>
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
The `matchMedia` engine, exposed. One instance per query string.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { useMediaQuery } from '@studiometa/js-toolkit';
|
|
13
|
+
|
|
14
|
+
const wide = useMediaQuery('(min-width: 64rem)');
|
|
15
|
+
|
|
16
|
+
// `props()` answers with no subscription, because asking a MediaQueryList is a read.
|
|
17
|
+
if (wide.props().matches) {
|
|
18
|
+
// …
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const unsubscribe = wide.subscribe(({ matches }) => {
|
|
22
|
+
console.log(matches);
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Parameters**
|
|
27
|
+
|
|
28
|
+
- `query` — any media query string.
|
|
29
|
+
|
|
30
|
+
## `props()` needs no subscription
|
|
31
|
+
|
|
32
|
+
Asking a `MediaQueryList` whether it matches **is a read**, so this service can answer without starting anything. Emissions are crossings.
|
|
33
|
+
|
|
34
|
+
That makes it the one service where `props()` outside a subscription is not a compromise.
|
|
35
|
+
|
|
36
|
+
## Where it is used
|
|
37
|
+
|
|
38
|
+
`data-mount="media:<query>"` is this service, which is why that strategy is **reversible**: a crossing out unmounts the instance and a crossing back mounts it again.
|
|
39
|
+
|
|
40
|
+
See [Mount strategies](../../guide/going-further/mount-strategies.md).
|
|
41
|
+
|
|
42
|
+
## See also
|
|
43
|
+
|
|
44
|
+
- [`usePrefersReducedMotion()`](usePrefersReducedMotion.md) — the named case
|
|
45
|
+
- [`useBreakpoint()`](useBreakpoint.md) — the named breakpoint set
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# useMutation
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
useMutation(target: Node, init?: MutationObserverInit): Service<MutationProps>
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
A general `MutationObserver` as a service, for "tell me when anything under this node changes".
|
|
8
|
+
|
|
9
|
+
## Props
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
interface MutationProps {
|
|
13
|
+
readonly records: readonly MutationRecord[];
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The default observation is `{ childList: true, subtree: true }`.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import { Base, useMutation } from '@studiometa/js-toolkit';
|
|
23
|
+
|
|
24
|
+
class Counter extends Base {
|
|
25
|
+
static config = { name: 'Counter', refs: ['list'] };
|
|
26
|
+
|
|
27
|
+
mounted() {
|
|
28
|
+
return useMutation(this.$el, { childList: true }).subscribe(({ records }) => {
|
|
29
|
+
this.$el.dataset.count = String(this.$el.children.length);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Reach for something narrower first
|
|
36
|
+
|
|
37
|
+
| Want | Use |
|
|
38
|
+
| -------------------------------------------- | -------------------------------------------------------------------- |
|
|
39
|
+
| one attribute of one element | [`watchAttributes()`](../dom/watchAttributes.md) |
|
|
40
|
+
| a whole namespace of attributes | [`watchAttributeNamespace()`](../dom/watchAttributeNamespace.md) |
|
|
41
|
+
| what the framework already reconciles | the registry — it is already watching |
|
|
42
|
+
| a subtree, character data, or a foreign node | **this** |
|
|
43
|
+
|
|
44
|
+
## It keeps nothing after the delivery
|
|
45
|
+
|
|
46
|
+
`props()` is **empty between deliveries**, so `{ immediate: true }` waits for a real batch rather than inventing one. That is the honest answer for a service whose value _is_ a batch.
|
|
47
|
+
|
|
48
|
+
## Platform timing, not framework order
|
|
49
|
+
|
|
50
|
+
This service delivers on the **platform's** timing. A subscriber that needs the framework's order awaits `whenDOMSettled()` in its callback:
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
useMutation(el).subscribe(async () => {
|
|
54
|
+
await whenDOMSettled();
|
|
55
|
+
// the new components have mounted
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Keying
|
|
60
|
+
|
|
61
|
+
Its key is a **canonical init**, through `resolveInit()`, which keeps the DOM contract rather than sorting the object blindly. See [`perTarget()`](perTarget.md).
|
|
62
|
+
|
|
63
|
+
## Mixin
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
class Counter extends withMutation(Base, { childList: true }) {
|
|
67
|
+
mutated({ records }) {}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# usePointer
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
usePointer(): Service<PointerProps>
|
|
5
|
+
usePointer(target: Element): Service<ElementPointerProps>
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
The pointer, in the viewport or in a box.
|
|
9
|
+
|
|
10
|
+
## Props
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
interface PointerProps {
|
|
14
|
+
readonly event: PointerEvent | null;
|
|
15
|
+
readonly isDown: boolean;
|
|
16
|
+
readonly x: number;
|
|
17
|
+
readonly y: number;
|
|
18
|
+
readonly deltaX: number;
|
|
19
|
+
readonly deltaY: number;
|
|
20
|
+
readonly maxX: number;
|
|
21
|
+
readonly maxY: number;
|
|
22
|
+
readonly progressX: number;
|
|
23
|
+
readonly progressY: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ElementPointerProps extends PointerProps {
|
|
27
|
+
readonly relativeX: number;
|
|
28
|
+
readonly relativeY: number;
|
|
29
|
+
readonly relativeProgressX: number;
|
|
30
|
+
readonly relativeProgressY: number;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`isGrabbing` is removed. `usePointer()` uses **pointer events only** and follows one `pointerId` at a time.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
import { Base, usePointer } from '@studiometa/js-toolkit';
|
|
40
|
+
|
|
41
|
+
class Cursor extends Base {
|
|
42
|
+
static config = { name: 'Cursor' };
|
|
43
|
+
|
|
44
|
+
mounted() {
|
|
45
|
+
return usePointer().subscribe(({ x, y }) => {
|
|
46
|
+
this.$el.style.transform = `translate(${x}px, ${y}px)`;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## The pointer in a box
|
|
53
|
+
|
|
54
|
+
`usePointer(el)` is one lazy service per target, and its props add the four `relative*` fields beside the viewport ones:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
import { Base, usePointer } from '@studiometa/js-toolkit';
|
|
58
|
+
|
|
59
|
+
class Tilt extends Base {
|
|
60
|
+
static config = { name: 'Tilt' };
|
|
61
|
+
|
|
62
|
+
mounted() {
|
|
63
|
+
return usePointer(this.$el).subscribe(({ relativeProgressX, relativeProgressY }) => {
|
|
64
|
+
this.$el.style.setProperty('--tilt-x', String(relativeProgressX * 2 - 1));
|
|
65
|
+
this.$el.style.setProperty('--tilt-y', String(relativeProgressY * 2 - 1));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**The targeted service subscribes to the singleton**, so one set of document listeners serves every target however many boxes are being watched.
|
|
72
|
+
|
|
73
|
+
## When the box is measured
|
|
74
|
+
|
|
75
|
+
On demand, and kept until a `scroll` (captured at the document), a `resize`, or the target's `ResizeObserver` can have moved it.
|
|
76
|
+
|
|
77
|
+
**The layout box is the frame of reference**, so a transform the consumer applies from its own callback does not invalidate it — which is exactly what a tilt or a parallax needs.
|
|
78
|
+
|
|
79
|
+
## `{ immediate: true }`
|
|
80
|
+
|
|
81
|
+
Does nothing before the pointer has been seen: there is no current value to deliver, which is what `hasProps()` says.
|
|
82
|
+
|
|
83
|
+
## Mixin
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
class Tilt extends withPointer(Base) {
|
|
87
|
+
moved({ relativeProgressX }) {}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`withPointer` defaults its target to `$el`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# usePrefersReducedMotion
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
usePrefersReducedMotion(): Service<{ readonly matches: boolean }>
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
`(prefers-reduced-motion: reduce)` as a named service.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { Base, usePrefersReducedMotion } from '@studiometa/js-toolkit';
|
|
13
|
+
|
|
14
|
+
class Carousel extends Base {
|
|
15
|
+
static config = { name: 'Carousel' };
|
|
16
|
+
|
|
17
|
+
mounted() {
|
|
18
|
+
return usePrefersReducedMotion().subscribe(({ matches }) => {
|
|
19
|
+
matches ? this.stopAutoplay() : this.startAutoplay();
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
startAutoplay() {}
|
|
24
|
+
stopAutoplay() {}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Why it is a service and not a read
|
|
29
|
+
|
|
30
|
+
**The preference changes while the page is open.** A read at load time is a snapshot that goes stale, and the user who turns reduced motion on mid-visit is exactly the user who most wants it honoured.
|
|
31
|
+
|
|
32
|
+
`props()` answers with no subscription, like every [`useMediaQuery()`](useMediaQuery.md) instance — so a one-off check is still one line. Subscribe when the component should follow the change.
|
|
33
|
+
|
|
34
|
+
## Mounting on the preference instead
|
|
35
|
+
|
|
36
|
+
To decide whether a component exists at all, use the mount strategy:
|
|
37
|
+
|
|
38
|
+
```html
|
|
39
|
+
<div data-component="Parallax" data-mount="media:(prefers-reduced-motion: no-preference)"></div>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
That strategy is **reversible**, so turning the preference on unmounts the component and turning it off mounts it again. `withMountWhenPrefersMotion` is deleted.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# useRaf
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
useRaf(): Service<RafProps, void | RafRender>
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
The frame clock as a service. It has nothing to scope, so there is one instance.
|
|
8
|
+
|
|
9
|
+
## Props
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
interface RafProps {
|
|
13
|
+
readonly time: DOMHighResTimeStamp;
|
|
14
|
+
readonly delta: number;
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`delta` is clamped to `[1, 40]` ms, and the first tick after the loop wakes reports `1000/60`. `time` stays the raw rAF timestamp.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
import { Base, useRaf } from '@studiometa/js-toolkit';
|
|
24
|
+
|
|
25
|
+
class Ticker extends Base {
|
|
26
|
+
static config = { name: 'Ticker' };
|
|
27
|
+
|
|
28
|
+
mounted() {
|
|
29
|
+
return useRaf().subscribe(({ delta }) => {
|
|
30
|
+
// …
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## A callback can return a render function
|
|
37
|
+
|
|
38
|
+
`useRaf` is the one service whose callback return value is used. Return a function and it runs in the frame's `write` phase:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import { Base, useRaf } from '@studiometa/js-toolkit';
|
|
42
|
+
|
|
43
|
+
class Follower extends Base {
|
|
44
|
+
static config = { name: 'Follower' };
|
|
45
|
+
|
|
46
|
+
mounted() {
|
|
47
|
+
return useRaf().subscribe(({ delta }) => {
|
|
48
|
+
const x = this.$el.offsetLeft + delta;
|
|
49
|
+
return () => {
|
|
50
|
+
this.$el.style.transform = `translateX(${x}px)`;
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The service collects the render functions of its callbacks and **cancels a render whose subscriber left between the two phases**.
|
|
58
|
+
|
|
59
|
+
## It does not own a loop
|
|
60
|
+
|
|
61
|
+
It subscribes to `scheduler.tick()`. The scheduler requests the next frame while a tick subscriber stays, so there is no permanent rAF loop and no loop of the service's own.
|
|
62
|
+
|
|
63
|
+
`{ immediate: true }` does nothing here: the frame tick has **no current value between two frames**, which is what its `hasProps()` says.
|
|
64
|
+
|
|
65
|
+
## Switching it on and off
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import { Base, toggle, useRaf } from '@studiometa/js-toolkit';
|
|
69
|
+
|
|
70
|
+
class SliderItem extends Base {
|
|
71
|
+
static config = { name: 'SliderItem' };
|
|
72
|
+
|
|
73
|
+
#frame = toggle(() => useRaf().subscribe(({ delta }) => this.follow(delta)));
|
|
74
|
+
|
|
75
|
+
mounted() {
|
|
76
|
+
return this.#frame.stop;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
follow(delta) {}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
See [`toggle()`](toggle.md), and `withRaf(Base, { manual: true })` in [Mixins](mixins.md).
|
|
84
|
+
|
|
85
|
+
## Mixin
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
class Follower extends withRaf(Base) {
|
|
89
|
+
ticked({ delta }) {}
|
|
90
|
+
}
|
|
91
|
+
```
|