@tremolo-ui/dom 0.3.0 → 0.5.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.
@@ -0,0 +1,58 @@
1
+ export interface WheelOptions {
2
+ /**
3
+ * Only report events while the focus is inside the element.
4
+ *
5
+ * A control that reacts to the wheel on hover alone takes the scroll away
6
+ * from the page, so passing over one in a long form silently changes its
7
+ * value. Requiring focus makes that an explicit act.
8
+ *
9
+ * The check is `contains`, not an identity test: the element that actually
10
+ * takes focus is usually a descendant, such as a thumb or an `<input>`, and
11
+ * a caller may have replaced it with markup of their own.
12
+ *
13
+ * @default false
14
+ */
15
+ requireFocus?: boolean
16
+ }
17
+
18
+ export interface WheelInstance {
19
+ /** Replace the given options, keeping the listener in place. */
20
+ update: (options: WheelOptions) => void
21
+ destroy: () => void
22
+ }
23
+
24
+ /**
25
+ * Listen to wheel events on an element.
26
+ *
27
+ * The listener is registered with `passive: false` so that the handler can call
28
+ * `preventDefault()` to stop the page from scrolling.
29
+ */
30
+ export function createWheel(
31
+ element: Element,
32
+ onWheel: (event: WheelEvent) => void,
33
+ options: WheelOptions = {},
34
+ ): WheelInstance {
35
+ let opts = options
36
+
37
+ function hasFocus() {
38
+ const active = element.ownerDocument?.activeElement
39
+ return !!active && element.contains(active)
40
+ }
41
+
42
+ const handler = (event: Event) => {
43
+ if (opts.requireFocus && !hasFocus()) return
44
+ onWheel(event as WheelEvent)
45
+ }
46
+
47
+ element.addEventListener('wheel', handler, { passive: false })
48
+
49
+ return {
50
+ update: (next) => {
51
+ opts = { ...opts, ...next }
52
+ },
53
+ destroy: () => {
54
+ // Only `capture` matters when removing, and it is false here.
55
+ element.removeEventListener('wheel', handler)
56
+ },
57
+ }
58
+ }
package/src/xy.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A pair of per-axis values. The tuple elements are labelled, so editors show
3
+ * `[x: number, y: number]` rather than a bare pair.
4
+ */
5
+ export type XY<T> = [x: T, y: T]
6
+
7
+ /**
8
+ * A setting that may be given once for both axes, or per axis.
9
+ *
10
+ * A single value is told from a pair with `Array.isArray`, so the single form
11
+ * is only offered while `T` cannot itself be an array. Where it can, the pair
12
+ * is the only way to write it, since a lone array would be read as a pair.
13
+ */
14
+ export type XYInput<T> = [T] extends [readonly unknown[]]
15
+ ? readonly [x: T, y: T]
16
+ : T | readonly [x: T, y: T]
17
+
18
+ // `Array.isArray` narrows a mutable tuple on its own, but not a readonly one,
19
+ // hence the explicit predicate.
20
+ function isPair<T>(
21
+ value: T | readonly [x: T, y: T],
22
+ ): value is readonly [x: T, y: T] {
23
+ return Array.isArray(value)
24
+ }
25
+
26
+ /**
27
+ * Spread a setting that may have been given as a single value.
28
+ *
29
+ * The parameter is written out rather than taken as `XYInput<T>`: a
30
+ * conditional type cannot be narrowed, so the constraint stays where it is
31
+ * declared and this takes both forms.
32
+ *
33
+ * The pair is copied rather than passed along, so that the result is a tuple
34
+ * the caller owns even when a readonly one was given.
35
+ */
36
+ export function toXY<T>(value: T | readonly [x: T, y: T]): XY<T> {
37
+ return isPair(value) ? [value[0], value[1]] : [value, value]
38
+ }