@paul-portfolio/react 0.5.0 → 0.5.1
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/dist/Ticker.d.ts +4 -4
- package/dist/Ticker.js +29 -23
- package/package.json +1 -1
package/dist/Ticker.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ReactNode } from
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
2
|
type TickerProps = {
|
|
3
3
|
/** Accessible name for the strip. Scroll mode renders a labelled region. */
|
|
4
4
|
label: string;
|
|
@@ -7,11 +7,11 @@ type TickerProps = {
|
|
|
7
7
|
* auto-scroll — every item stays reachable. `marquee` is a decorative,
|
|
8
8
|
* aria-hidden CSS loop for pure flavour.
|
|
9
9
|
*/
|
|
10
|
-
mode?:
|
|
10
|
+
mode?: "scroll" | "marquee";
|
|
11
11
|
/** Which edge the strip sits on; picks the border side. */
|
|
12
|
-
edge?:
|
|
12
|
+
edge?: "top" | "bottom";
|
|
13
13
|
/** Which way the ambient motion travels. */
|
|
14
|
-
direction?:
|
|
14
|
+
direction?: "left" | "right";
|
|
15
15
|
/** Ambient auto-scroll speed for scroll mode, in px/sec. */
|
|
16
16
|
speed?: number;
|
|
17
17
|
className?: string;
|
package/dist/Ticker.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef, useState } from
|
|
3
|
-
import { cx } from
|
|
4
|
-
import { usePrefersReducedMotion } from
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState, } from "react";
|
|
3
|
+
import { cx } from "./cx";
|
|
4
|
+
import { usePrefersReducedMotion } from "./usePrefersReducedMotion";
|
|
5
5
|
/** How long a touch keeps the strip frozen before the ambient scroll resumes. */
|
|
6
6
|
const TOUCH_RESUME_MS = 4000;
|
|
7
7
|
/**
|
|
@@ -11,17 +11,17 @@ const TOUCH_RESUME_MS = 4000;
|
|
|
11
11
|
* children, so the strip stays content-agnostic.
|
|
12
12
|
*/
|
|
13
13
|
export function Ticker(props) {
|
|
14
|
-
return props.mode ===
|
|
14
|
+
return props.mode === "marquee" ? (_jsx(MarqueeTicker, { ...props })) : (_jsx(ScrollTicker, { ...props }));
|
|
15
15
|
}
|
|
16
16
|
function edgeClassFor(edge) {
|
|
17
|
-
return edge ===
|
|
17
|
+
return edge === "top" ? "ticker--top" : "ticker--bottom";
|
|
18
18
|
}
|
|
19
19
|
/** Decorative marquee: aria-hidden, CSS-driven, content duplicated for the loop. */
|
|
20
|
-
function MarqueeTicker({ edge =
|
|
21
|
-
return (_jsx("div", { "aria-hidden": "true", "data-direction": direction, className: cx(
|
|
20
|
+
function MarqueeTicker({ edge = "top", direction = "left", className, children, }) {
|
|
21
|
+
return (_jsx("div", { "aria-hidden": "true", "data-direction": direction, className: cx("ticker", "ticker--marquee", edgeClassFor(edge), className), children: _jsxs("div", { className: "ticker__track", children: [_jsx("div", { className: "ticker__group", children: children }), _jsx("div", { className: "ticker__group", children: children })] }) }));
|
|
22
22
|
}
|
|
23
23
|
/** Accessible scroll container with an ambient JS auto-scroll loop. */
|
|
24
|
-
function ScrollTicker({ label, edge =
|
|
24
|
+
function ScrollTicker({ label, edge = "top", direction = "left", speed = 40, className, children, }) {
|
|
25
25
|
const reduced = usePrefersReducedMotion();
|
|
26
26
|
const scrollerRef = useRef(null);
|
|
27
27
|
const cloneRef = useRef(null);
|
|
@@ -32,19 +32,25 @@ function ScrollTicker({ label, edge = 'top', direction = 'left', speed = 40, cla
|
|
|
32
32
|
useEffect(() => {
|
|
33
33
|
pausedRef.current = paused;
|
|
34
34
|
}, [paused]);
|
|
35
|
-
//
|
|
35
|
+
// Take the clone out of the tab order, but leave it clickable.
|
|
36
36
|
//
|
|
37
|
-
// The clone
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
// The clone used to carry `inert`, which was the right instinct aimed one
|
|
38
|
+
// notch too broadly: `inert` removes a subtree from the accessibility tree
|
|
39
|
+
// AND from the pointer. Since the loop wraps at half the scroll width,
|
|
40
|
+
// roughly half of what is on screen at any moment is the clone, so half the
|
|
41
|
+
// strip silently ignored clicks.
|
|
42
|
+
//
|
|
43
|
+
// What is actually wanted is narrower: hidden from assistive tech
|
|
44
|
+
// (`aria-hidden` on the element), not tabbable (`tabIndex = -1` here), and
|
|
45
|
+
// still interactive with a pointer. Both copies render the same children, so
|
|
46
|
+
// clicking either runs the same handler and the reader cannot tell which one
|
|
47
|
+
// they hit — which is the point.
|
|
48
|
+
//
|
|
49
|
+
// A layout effect rather than a passive one: it runs before the browser
|
|
50
|
+
// paints, so there is no frame in which the duplicate holds tabbable controls
|
|
51
|
+
// inside an aria-hidden container, which axe rates serious.
|
|
52
|
+
useLayoutEffect(() => {
|
|
53
|
+
const focusables = cloneRef.current?.querySelectorAll("a[href], button, input, select, textarea, [tabindex]");
|
|
48
54
|
focusables?.forEach((el) => {
|
|
49
55
|
el.tabIndex = -1;
|
|
50
56
|
});
|
|
@@ -57,7 +63,7 @@ function ScrollTicker({ label, edge = 'top', direction = 'left', speed = 40, cla
|
|
|
57
63
|
const el = scrollerRef.current;
|
|
58
64
|
if (!el)
|
|
59
65
|
return;
|
|
60
|
-
const dir = direction ===
|
|
66
|
+
const dir = direction === "left" ? 1 : -1;
|
|
61
67
|
// Start the rightward strip one copy in, so it has somewhere to scroll back.
|
|
62
68
|
if (dir < 0)
|
|
63
69
|
el.scrollLeft = el.scrollWidth / 2;
|
|
@@ -87,10 +93,10 @@ function ScrollTicker({ label, edge = 'top', direction = 'left', speed = 40, cla
|
|
|
87
93
|
clearTimeout(resumeTimer.current);
|
|
88
94
|
resumeTimer.current = setTimeout(() => setPaused(false), TOUCH_RESUME_MS);
|
|
89
95
|
};
|
|
90
|
-
const classes = cx(
|
|
96
|
+
const classes = cx("ticker", edgeClassFor(edge), className);
|
|
91
97
|
// Reduced motion: a plain, single-copy scrollable row. No clone, no loop.
|
|
92
98
|
if (reduced) {
|
|
93
99
|
return (_jsx("section", { "aria-label": label, className: classes, children: _jsx("div", { className: "ticker__group", children: children }) }));
|
|
94
100
|
}
|
|
95
|
-
return (_jsx("section", { ref: scrollerRef, "aria-label": label, "data-direction": direction, className: classes, onMouseEnter: () => setPaused(true), onMouseLeave: () => setPaused(false), onTouchStart: freezeForTouch, children: _jsxs("div", { className: "ticker__track", "data-paused": paused || undefined, children: [_jsx("div", { className: "ticker__group", children: children }), _jsx("div", { ref: cloneRef, "aria-hidden": "true",
|
|
101
|
+
return (_jsx("section", { ref: scrollerRef, "aria-label": label, "data-direction": direction, className: classes, onMouseEnter: () => setPaused(true), onMouseLeave: () => setPaused(false), onTouchStart: freezeForTouch, children: _jsxs("div", { className: "ticker__track", "data-paused": paused || undefined, children: [_jsx("div", { className: "ticker__group", children: children }), _jsx("div", { ref: cloneRef, "aria-hidden": "true", className: "ticker__group", children: children })] }) }));
|
|
96
102
|
}
|