@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 CHANGED
@@ -1,4 +1,4 @@
1
- import { type ReactNode } from 'react';
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?: 'scroll' | 'marquee';
10
+ mode?: "scroll" | "marquee";
11
11
  /** Which edge the strip sits on; picks the border side. */
12
- edge?: 'top' | 'bottom';
12
+ edge?: "top" | "bottom";
13
13
  /** Which way the ambient motion travels. */
14
- direction?: 'left' | 'right';
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 'react';
3
- import { cx } from './cx';
4
- import { usePrefersReducedMotion } from './usePrefersReducedMotion';
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 === 'marquee' ? (_jsx(MarqueeTicker, { ...props })) : (_jsx(ScrollTicker, { ...props }));
14
+ return props.mode === "marquee" ? (_jsx(MarqueeTicker, { ...props })) : (_jsx(ScrollTicker, { ...props }));
15
15
  }
16
16
  function edgeClassFor(edge) {
17
- return edge === 'top' ? 'ticker--top' : 'ticker--bottom';
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 = '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 })] }) }));
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 = 'top', direction = 'left', speed = 40, className, children, }) {
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
- // Fallback for browsers without `inert`.
35
+ // Take the clone out of the tab order, but leave it clickable.
36
36
  //
37
- // The clone carries `inert` in the markup, which drops its descendants from
38
- // the tab order and the accessibility tree together, before first paint. This
39
- // effect used to be the only mechanism, and it left a gap: an effect runs
40
- // after the DOM exists, so between render and this call the duplicate held
41
- // tabbable controls inside an aria-hidden container. Hiding something from
42
- // assistive tech while leaving it reachable by keyboard is worse than not
43
- // hiding it the user lands on a control a screen reader insists is absent.
44
- useEffect(() => {
45
- if ('inert' in HTMLElement.prototype)
46
- return;
47
- const focusables = cloneRef.current?.querySelectorAll('a[href], button, input, select, textarea, [tabindex]');
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 === 'left' ? 1 : -1;
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('ticker', edgeClassFor(edge), className);
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", inert: true, className: "ticker__group", children: children })] }) }));
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paul-portfolio/react",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "React components for the Paul Design System",
5
5
  "license": "MIT",
6
6
  "repository": {