@helloimjolopez-pathway/pathway-components 0.3.2 → 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,500 @@
1
+ /**
2
+ * Scroll-to-Enable, Pathway Design System
3
+ *
4
+ * Importable React component module. Source of truth for the Scroll-to-Enable
5
+ * implementation; the standalone demo (scroll-to-enable.html, generated from
6
+ * scroll-to-enable-demo.jsx) and the Storybook stories both consume this.
7
+ *
8
+ * Spec: components/scroll-to-enable/scroll-to-enable-spec.md
9
+ * Figma: https://www.figma.com/design/3sw45aVcngFAmpbP6cfrXP/?node-id=40003308-10049
10
+ * (inline), 40003308:11167 (mobile page), 40019965:1078 (sheet)
11
+ *
12
+ * NOTHING IS EVER DISABLED. The old Figma version disabled Continue, Done
13
+ * Reading and the checkbox until the notice had been scrolled. A disabled
14
+ * control cannot be focused, gives no reason and is skipped by screen readers,
15
+ * so every gate here is enforced on activation instead: the control responds,
16
+ * an inline message says what is needed, and focus moves to where it can be
17
+ * done. Do not add `disabled` or `aria-disabled` to anything in this file.
18
+ *
19
+ * THREE PARTS ARE BUILT HERE FOR NOW. Reading progress, the message and the
20
+ * sheet (with its header) stand in for Progress Bar, Help Text, Step
21
+ * Navigation Header and Sheet, none of which are in the repo yet. They are not
22
+ * exported. When those components ship, swap them in (spec §17).
23
+ */
24
+
25
+ import React, { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
26
+ import { Button } from "../button/button.jsx";
27
+ import { Checkbox } from "../checkbox/checkbox.jsx";
28
+ import { Scrollable } from "../scrollbar/scrollbar.jsx";
29
+ import { Icon } from "../icon/icon.jsx";
30
+ import { useReducedMotion } from "../_shared/use-reduced-motion.jsx";
31
+
32
+ // ─── TOKEN HELPERS ────────────────────────────────────────────────────────────
33
+ // SC = semantic-color (themes/*.css)
34
+ // SL = semantic-layout-units (layout.css)
35
+ // CL = contextual-layout-units (layout-contextual.css)
36
+ // RL = responsive-layout (layout-responsive.css, per breakpoint)
37
+ // ST = semantic-type (type.css)
38
+ const SC = (p) => `var(--semantic-color-${p})`;
39
+ const SL = (p) => `var(--semantic-layout-units-${p})`;
40
+ const CL = (p) => `var(--contextual-layout-units-${p})`;
41
+ const RL = (p) => `var(--responsive-layout-${p})`;
42
+ const ST = (p) => `var(--semantic-type-${p})`;
43
+
44
+ // One composed type style per Figma text style. Tracking is named differently
45
+ // in Figma and the CSS (Figma Standard/Wide/ExtraWide = CSS wide/spacious/
46
+ // extraspacious, 0.1/0.3/0.6), so each line says which Figma style it is.
47
+ const TYPE = (size, lineHeight, weight, tracking) => ({
48
+ fontFamily: ST("family-brand"),
49
+ fontSize: ST(`font-size-${size}`),
50
+ lineHeight: ST(`line-height-${lineHeight}`),
51
+ fontWeight: ST(`weight-${weight}`),
52
+ letterSpacing: ST(`letter-spacing-${tracking}`),
53
+ });
54
+
55
+ // ─── LAYOUT CONSTANTS ─────────────────────────────────────────────────────────
56
+ export const T = {
57
+ // Colour
58
+ noticeFill: SC("fill-surface-elevated"),
59
+ noticeStroke: SC("stroke-static-neutral-faint"),
60
+ hintFill: SC("fill-surface-overlay"),
61
+ hintStroke: SC("stroke-static-neutral-bold"),
62
+ messageFill: SC("fill-static-negative-subtle"),
63
+ messageFg: SC("foreground-static-negative-on-subtle"),
64
+ // One token in every state and theme. It does NOT turn green at 100%:
65
+ // Fill/Static/Positive/Strong is 1.85:1 on the page in Amplify Light.
66
+ progressFill: SC("fill-static-brand-strong"),
67
+ progressTrack: SC("stroke-static-neutral-faint"),
68
+ sheetFill: SC("fill-surface-overlay"),
69
+ scrim: SC("scrim-base"),
70
+ fgStrong: SC("foreground-static-neutral-strong"),
71
+ fgBase: SC("foreground-static-neutral-base"),
72
+ focusRing: SC("stroke-focusring-base"),
73
+ // Shadow
74
+ hintShadow: "var(--elevation-lift)",
75
+ sheetShadow: "var(--elevation-sheet)",
76
+ // Geometry
77
+ borderWidth: SL("borderwidth-thin"),
78
+ noticeRadius: SL("cornerradius-base"),
79
+ pillRadius: SL("cornerradius-full"),
80
+ messageRadius: SL("cornerradius-small"),
81
+ sheetRadius: RL("sheet-corner-radius-cornerradius"),
82
+ focusWidth: CL("focusring-width"),
83
+ focusOffset: CL("focusring-offset"),
84
+ // Spacing
85
+ sectionGap: SL("gap-medium"),
86
+ progressGap: SL("gap-xtight"),
87
+ titleGap: SL("gap-tight"),
88
+ agreementGap: SL("gap-xtight"),
89
+ paragraphGap: SL("gap-base"),
90
+ toolbarPad: `${SL("padding-xxtight")} ${SL("padding-xtight")} ${SL("padding-xxxtight")}`,
91
+ contentPad: `${SL("padding-xtight")} ${SL("padding-base")} ${SL("padding-base")}`,
92
+ hintPad: `${SL("padding-xxxtight")} ${SL("padding-xtight")}`,
93
+ hintGap: SL("gap-xxtight"),
94
+ hintOffset: SL("padding-xtight"),
95
+ messagePad: SL("padding-xtight"),
96
+ messageGap: SL("gap-tight"),
97
+ sheetPad: `${RL("sheet-padding-top")} ${RL("sheet-padding-horizontal")} ${SL("padding-wide")}`,
98
+ sheetGap: SL("gap-base"),
99
+ sheetHeaderPad: SL("padding-medium"),
100
+ // Type (Figma text style in the comment)
101
+ noticeType: TYPE("s", "s-single", "regular", "spacious"), // Text/Body/Small/Regular
102
+ titleType: TYPE("r", "r-single", "medium", "wide"), // Heading/Section/Small/Medium
103
+ progressType: TYPE("xs", "xs-single", "regular", "extraspacious"), // Label/Progress/Small/Regular
104
+ hintType: TYPE("xs", "xs-single", "semibold", "extraspacious"), // Label/Badge/Base/Semibold
105
+ messageType: TYPE("xs", "xs-single", "semibold", "extraspacious"), // Text/Supporting/Base/Semibold
106
+ sheetTitleType: TYPE("m", "m-relaxed", "semibold", "wide"), // Heading/Section/Base/Semibold
107
+ // Motion
108
+ progressMotion: "width var(--motion-duration-2) var(--motion-easing-standard)",
109
+ scrimMotion: "pds-ste-fade var(--motion-duration-3) var(--motion-easing-standard)",
110
+ sheetMotion: "pds-ste-rise var(--motion-duration-4) var(--motion-easing-emphasized)",
111
+ // Measurements (widths and heights are not tokens, CLAUDE.md §5)
112
+ noticeHeight: 200, // scroll area, Figma Scroll Area fixed height
113
+ progressHeight: 4, // Figma Progress Bar height
114
+ minWidth: 320,
115
+ readThreshold: 8, // px from the end that counts as read
116
+ breakpoint: 768, // --breakpoints-tablet-768px: inline at and above, sheet below
117
+ sheetFrameHeight: 560, // documentation frames only: room for a contained sheet
118
+ };
119
+
120
+ /** Default message copy. Each one says what to do next. */
121
+ export const MESSAGES = {
122
+ scrollInline: "Scroll to the end of the notice before you continue.",
123
+ readAgreement: "Read the agreement before you agree to it.",
124
+ scrollSheet: "Scroll to the end of the notice to finish reading.",
125
+ agree: "Check the box to confirm you have read the notice.",
126
+ };
127
+
128
+ /**
129
+ * Documentation only: the Figma states, for stories and the demo. Each seeds
130
+ * the internal state once; the component stays fully interactive after that.
131
+ * "Blocked" picks the inline or sheet-page message by layout. The Sheet states
132
+ * keep the sheet inside the component's own frame instead of the viewport.
133
+ */
134
+ export const DEMO_STATES = {
135
+ "Unread": {},
136
+ "Reading": { progress: 0.5 },
137
+ "Read": { progress: 1, read: true },
138
+ "Agreed": { progress: 1, read: true, checked: true },
139
+ "Blocked": { msg: "blocked" },
140
+ "Agreement Missing": { progress: 1, read: true, msg: "agree", missing: true },
141
+ "Sheet Unread": { sheetOpen: true },
142
+ "Sheet Blocked": { sheetOpen: true, progress: 0.4, sheetMsg: "scrollSheet" },
143
+ "Sheet Read": { sheetOpen: true, progress: 1 },
144
+ };
145
+
146
+ // Keyframes and the paragraph reset cannot be inline styles. Injected once,
147
+ // the same way Scrollable and Button add theirs.
148
+ function ensureStyles() {
149
+ if (typeof document === "undefined" || document.getElementById("pds-ste-styles")) return;
150
+ const s = document.createElement("style");
151
+ s.id = "pds-ste-styles";
152
+ s.textContent =
153
+ "@keyframes pds-ste-fade{from{opacity:0}to{opacity:1}}" +
154
+ "@keyframes pds-ste-rise{from{transform:translateY(100%)}to{transform:translateY(0)}}" +
155
+ ".pds-ste__region{outline:none}" +
156
+ ".pds-ste__region p{margin:0}";
157
+ document.head.appendChild(s);
158
+ }
159
+
160
+ const canMatch = () => typeof window !== "undefined" && typeof window.matchMedia === "function";
161
+ const WIDE = `(min-width: ${T.breakpoint}px)`;
162
+
163
+ /** "inline" at the 768 breakpoint and above, "sheet" below. */
164
+ function useAutoLayout(layout) {
165
+ const [wide, setWide] = useState(() => (canMatch() ? window.matchMedia(WIDE).matches : true));
166
+ useEffect(() => {
167
+ if (layout !== "auto" || !canMatch()) return;
168
+ const mq = window.matchMedia(WIDE);
169
+ setWide(mq.matches);
170
+ const onChange = (e) => setWide(e.matches);
171
+ mq.addEventListener("change", onChange);
172
+ return () => mq.removeEventListener("change", onChange);
173
+ }, [layout]);
174
+ if (layout === "inline" || layout === "sheet") return layout;
175
+ return wide ? "inline" : "sheet";
176
+ }
177
+
178
+ /** Focus an element if it can take focus. */
179
+ const focusEl = (el) => { if (el && typeof el.focus === "function") el.focus(); };
180
+
181
+ // ─── INTERNAL PARTS (not exported; see the header) ────────────────────────────
182
+
183
+ function ReadingProgress({ value, labelId }) {
184
+ const noMotion = useReducedMotion();
185
+ const pct = Math.round(value * 100);
186
+ return (
187
+ <div className="pds-ste__progress" style={{ display: "flex", flexDirection: "column", gap: T.progressGap }}>
188
+ <div style={{ display: "flex", justifyContent: "space-between", ...T.progressType, color: T.fgBase }}>
189
+ <span id={labelId}>Reading progress</span>
190
+ <span aria-hidden="true">{pct}%</span>
191
+ </div>
192
+ <div role="progressbar" aria-labelledby={labelId} aria-valuemin={0} aria-valuemax={100} aria-valuenow={pct}
193
+ style={{ height: T.progressHeight, borderRadius: T.pillRadius, background: T.progressTrack, overflow: "hidden" }}>
194
+ <div style={{ width: `${pct}%`, height: "100%", borderRadius: T.pillRadius, background: T.progressFill,
195
+ transition: noMotion ? "none" : T.progressMotion }} />
196
+ </div>
197
+ </div>
198
+ );
199
+ }
200
+
201
+ /* The container always renders so aria-describedby always has a target. The
202
+ alert inside is keyed by `nonce`, so a repeat tap announces again. */
203
+ function FieldMessage({ id, text, nonce }) {
204
+ return (
205
+ <div id={id}>
206
+ {text && (
207
+ <div key={nonce} role="alert" style={{
208
+ display: "flex", alignItems: "flex-start", gap: T.messageGap, padding: T.messagePad,
209
+ borderRadius: T.messageRadius, background: T.messageFill, color: T.messageFg, ...T.messageType,
210
+ }}>
211
+ <Icon name="error" size="M" fill={1} />
212
+ <span>{text}</span>
213
+ </div>
214
+ )}
215
+ </div>
216
+ );
217
+ }
218
+
219
+ function Hint() {
220
+ return (
221
+ <div aria-hidden="true" style={{ position: "absolute", left: 0, right: 0, bottom: T.hintOffset,
222
+ display: "flex", justifyContent: "center", pointerEvents: "none" }}>
223
+ <span style={{ display: "inline-flex", alignItems: "center", gap: T.hintGap, padding: T.hintPad,
224
+ borderRadius: T.pillRadius, background: T.hintFill, boxShadow: T.hintShadow,
225
+ border: `${T.borderWidth} solid ${T.hintStroke}`, ...T.hintType, color: T.fgStrong }}>
226
+ <Icon name="arrow_downward" size="M" />Scroll to continue
227
+ </span>
228
+ </div>
229
+ );
230
+ }
231
+
232
+ /* The notice box: optional Print, the repo Scrollable, the focusable region
233
+ inside it, and the hint. Reports progress 0..1 through onProgress. */
234
+ function Notice({ children, height, titleId, regionRef, showPrint, onPrint, showHint, onProgress, startAt }) {
235
+ const boxRef = useRef(null);
236
+ const [ring, setRing] = useState(false);
237
+
238
+ const report = useCallback((view) => {
239
+ if (!view) return;
240
+ const max = view.scrollHeight - view.clientHeight;
241
+ onProgress(max <= T.readThreshold ? 1 : Math.min(1, view.scrollTop / (max - T.readThreshold)));
242
+ }, [onProgress]);
243
+
244
+ // Measure on mount and whenever the box resizes: a notice that fits is read.
245
+ useLayoutEffect(() => {
246
+ const view = boxRef.current && boxRef.current.querySelector(".pds-scrollable__view");
247
+ if (!view) return;
248
+ if (startAt != null) view.scrollTop = (view.scrollHeight - view.clientHeight) * startAt;
249
+ report(view);
250
+ if (typeof ResizeObserver === "undefined") return;
251
+ const ro = new ResizeObserver(() => report(view));
252
+ ro.observe(view);
253
+ return () => ro.disconnect();
254
+ }, [report, height, startAt]);
255
+
256
+ const onFocus = (e) => {
257
+ let visible = true;
258
+ try { visible = e.target.matches(":focus-visible") || e.target.dataset.moved === "1"; } catch (_) { /* old engines: show it */ }
259
+ delete e.target.dataset.moved;
260
+ setRing(visible);
261
+ };
262
+
263
+ return (
264
+ <div ref={boxRef} className="pds-ste__notice"
265
+ // Scrollable exposes no scroll callback; the scroll event is caught here.
266
+ onScrollCapture={(e) => report(e.target)}
267
+ style={{ position: "relative", background: T.noticeFill, border: `${T.borderWidth} solid ${T.noticeStroke}`,
268
+ borderRadius: T.noticeRadius,
269
+ outline: ring ? `${T.focusWidth} solid ${T.focusRing}` : "none", outlineOffset: T.focusOffset }}>
270
+ {showPrint && (
271
+ <div style={{ padding: T.toolbarPad }}>
272
+ <Button buttonStyle="Naked" type="Secondary" size="S" text="Print" showLeadingIcon leadingIcon="print" onClick={onPrint} />
273
+ </div>
274
+ )}
275
+ <Scrollable style={{ height }}>
276
+ <div ref={regionRef} className="pds-ste__region" role="region" tabIndex={0} aria-labelledby={titleId}
277
+ onFocus={onFocus} onBlur={() => setRing(false)}
278
+ style={{ padding: T.contentPad, display: "flex", flexDirection: "column", gap: T.paragraphGap, ...T.noticeType, color: T.fgStrong }}>
279
+ {children}
280
+ </div>
281
+ </Scrollable>
282
+ {showHint && <Hint />}
283
+ </div>
284
+ );
285
+ }
286
+
287
+ /* Modal bottom sheet on the Radix Dialog anatomy: Overlay, Content, Title, Close. */
288
+ function Sheet({ title, titleId, stepLabel, onClose, children, footer, initialFocusRef, contained }) {
289
+ const noMotion = useReducedMotion();
290
+ const contentRef = useRef(null);
291
+
292
+ useEffect(() => {
293
+ if (contained) return undefined; // documentation frames: no scroll lock, no focus grab
294
+ const prevOverflow = document.body.style.overflow;
295
+ document.body.style.overflow = "hidden";
296
+ const t = setTimeout(() => focusEl(initialFocusRef.current), 0);
297
+ return () => { clearTimeout(t); document.body.style.overflow = prevOverflow; };
298
+ }, [initialFocusRef, contained]);
299
+
300
+ const onKeyDown = (e) => {
301
+ if (e.key === "Escape") { e.stopPropagation(); onClose(); return; }
302
+ if (e.key !== "Tab" || !contentRef.current) return;
303
+ const els = contentRef.current.querySelectorAll("button, input, [tabindex='0']");
304
+ if (!els.length) return;
305
+ const first = els[0], last = els[els.length - 1];
306
+ if (e.shiftKey && document.activeElement === first) { e.preventDefault(); focusEl(last); }
307
+ else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); focusEl(first); }
308
+ };
309
+
310
+ return (
311
+ <div className="pds-ste-sheet" style={{ position: contained ? "absolute" : "fixed", inset: 0, zIndex: contained ? 1 : 1000, display: "flex", flexDirection: "column", justifyContent: "flex-end" }}>
312
+ <div className="pds-ste-sheet__scrim" onClick={onClose}
313
+ style={{ position: "absolute", inset: 0, background: T.scrim, animation: noMotion ? "none" : T.scrimMotion }} />
314
+ <div ref={contentRef} role="dialog" aria-modal="true" aria-labelledby={titleId} onKeyDown={onKeyDown}
315
+ style={{ position: "relative", background: T.sheetFill, boxShadow: T.sheetShadow,
316
+ borderRadius: `${T.sheetRadius} ${T.sheetRadius} 0 0`, padding: T.sheetPad,
317
+ display: "flex", flexDirection: "column", gap: T.sheetGap, maxHeight: "85vh",
318
+ animation: noMotion ? "none" : T.sheetMotion }}>
319
+ <div className="pds-ste-sheet__header" style={{ display: "flex", alignItems: "center", justifyContent: "space-between",
320
+ paddingLeft: T.sheetHeaderPad, paddingRight: T.sheetHeaderPad }}>
321
+ <span style={{ ...T.progressType, color: T.fgBase }}>{stepLabel}</span>
322
+ <Button buttonStyle="Naked" type="Secondary" size="S" showText={false} showLeadingIcon leadingIcon="close"
323
+ text="Close" ariaLabel="Close" onClick={onClose} />
324
+ </div>
325
+ <h2 id={titleId} style={{ margin: 0, textAlign: "center", ...T.sheetTitleType, color: T.fgStrong }}>{title}</h2>
326
+ {children}
327
+ {footer}
328
+ </div>
329
+ </div>
330
+ );
331
+ }
332
+
333
+ // ─── SCROLL-TO-ENABLE ─────────────────────────────────────────────────────────
334
+
335
+ /**
336
+ * @param {string} title Notice title; also the sheet title and the region's name
337
+ * @param {ReactNode} children The notice content (paragraphs)
338
+ * @param {string} agreementLabel Checkbox label
339
+ * @param {function} onContinue Called when the notice is read and the box is ticked
340
+ * @param {"auto"|"inline"|"sheet"} layout auto picks by viewport at 768
341
+ * @param {number} noticeHeight Scroll area height in px (default 200)
342
+ * @param {boolean} showPrint Show Print in the inline layout (never in the sheet)
343
+ * @param {function} onPrint Print handler; defaults to window.print()
344
+ * @param {string} continueLabel
345
+ * @param {string} readAgreementLabel Sheet trigger before reading
346
+ * @param {string} readLabel Sheet trigger after reading
347
+ * @param {string} doneReadingLabel
348
+ * @param {string} stepLabel Optional label at the left of the sheet header
349
+ * @param {object} messages Overrides for MESSAGES
350
+ * @param {function} onAgreeChange Called with the checkbox value when it changes
351
+ * @param {string} demoState Documentation only: start in a Figma state (DEMO_STATES)
352
+ * @param {string} className
353
+ */
354
+ export function ScrollToEnable({
355
+ title,
356
+ children,
357
+ agreementLabel,
358
+ onContinue,
359
+ layout = "auto",
360
+ noticeHeight = T.noticeHeight,
361
+ showPrint = true,
362
+ onPrint,
363
+ continueLabel = "Continue",
364
+ readAgreementLabel = "Read Agreement",
365
+ readLabel = "Agreement read",
366
+ doneReadingLabel = "Done Reading",
367
+ stepLabel,
368
+ messages,
369
+ onAgreeChange,
370
+ demoState,
371
+ className = "",
372
+ }) {
373
+ ensureStyles();
374
+ const mode = useAutoLayout(layout);
375
+ const uid = useId().replace(/:/g, "");
376
+ const ids = {
377
+ title: `${uid}-title`, progress: `${uid}-rp`, msg: `${uid}-msg`, sheetTitle: `${uid}-sheet-title`,
378
+ sheetMsg: `${uid}-sheet-msg`, checkbox: `${uid}-cb`,
379
+ };
380
+ const copy = { ...MESSAGES, ...messages };
381
+ const seed = (demoState && DEMO_STATES[demoState]) || {};
382
+ const contained = !!demoState;
383
+ const seedMsg = seed.msg === "blocked" ? (mode === "inline" ? "scrollInline" : "readAgreement") : (seed.msg || null);
384
+
385
+ const [progress, setProgress] = useState(seed.progress || 0);
386
+ const [read, setRead] = useState(!!seed.read);
387
+ const [checked, setChecked] = useState(!!seed.checked);
388
+ const [msg, setMsg] = useState(seedMsg); // key of MESSAGES on the page
389
+ const [sheetMsg, setSheetMsg] = useState(seed.sheetMsg || null);
390
+ const [nonce, setNonce] = useState(0);
391
+ const [sheetOpen, setSheetOpen] = useState(!!seed.sheetOpen);
392
+ const [missing, setMissing] = useState(!!seed.missing);
393
+
394
+ const regionRef = useRef(null);
395
+ const sheetRegionRef = useRef(null);
396
+ const triggerRef = useRef(null);
397
+
398
+ const say = (key) => { setMsg(key); setNonce((n) => n + 1); };
399
+ const moveFocus = (el) => { if (el) { el.dataset.moved = "1"; focusEl(el); } };
400
+ const triggerButton = () => triggerRef.current && triggerRef.current.querySelector("button");
401
+
402
+ // Progress never goes back; reaching the end inline marks the notice read.
403
+ const onInlineProgress = useCallback((v) => {
404
+ setProgress((p) => Math.max(p, v));
405
+ if (v >= 1) { setRead(true); setMsg((m) => (m === "scrollInline" ? null : m)); }
406
+ }, []);
407
+ const onSheetProgress = useCallback((v) => {
408
+ setProgress((p) => Math.max(p, v));
409
+ if (v >= 1) setSheetMsg(null);
410
+ }, []);
411
+
412
+ const blocked = () => {
413
+ if (mode === "inline") { say("scrollInline"); moveFocus(regionRef.current); }
414
+ else { say("readAgreement"); focusEl(triggerButton()); }
415
+ };
416
+
417
+ const onCheck = (next) => {
418
+ if (!read) { blocked(); return; }
419
+ setChecked(next);
420
+ if (next) { setMsg(null); setMissing(false); }
421
+ if (onAgreeChange) onAgreeChange(next);
422
+ };
423
+
424
+ const onContinueClick = () => {
425
+ if (!read) { blocked(); return; }
426
+ if (!checked) { setMissing(true); say("agree"); focusEl(document.getElementById(ids.checkbox)); return; }
427
+ if (onContinue) onContinue();
428
+ };
429
+
430
+ const closeSheet = () => { setSheetOpen(false); setSheetMsg(null); setTimeout(() => focusEl(triggerButton()), 0); };
431
+
432
+ const onDoneReading = () => {
433
+ if (progress < 1) { setSheetMsg("scrollSheet"); setNonce((n) => n + 1); moveFocus(sheetRegionRef.current); return; }
434
+ setRead(true); setMsg(null); closeSheet();
435
+ };
436
+
437
+ const print = onPrint || (() => { if (typeof window !== "undefined") window.print(); });
438
+
439
+ const agreement = (
440
+ <div className="pds-ste__agreement" style={{ display: "flex", flexDirection: "column", gap: T.agreementGap }}>
441
+ <Checkbox id={ids.checkbox} checked={checked} error={missing && !checked} label={agreementLabel}
442
+ onChange={onCheck} describedBy={ids.msg} />
443
+ <FieldMessage id={ids.msg} text={msg ? copy[msg] : null} nonce={nonce} />
444
+ </div>
445
+ );
446
+
447
+ const actions = (
448
+ <div className="pds-ste__actions" style={{ display: "flex", justifyContent: "center" }}>
449
+ <Button buttonStyle="Fill" type="Primary" size="L" text={continueLabel} onClick={onContinueClick} aria-describedby={ids.msg} />
450
+ </div>
451
+ );
452
+
453
+ const root = { display: "flex", flexDirection: "column", gap: T.sectionGap, minWidth: T.minWidth, width: "100%" };
454
+
455
+ if (mode === "inline") {
456
+ return (
457
+ <div className={`pds-ste ${className}`} style={root}>
458
+ <ReadingProgress value={progress} labelId={ids.progress} />
459
+ <div className="pds-ste__notice-group" style={{ display: "flex", flexDirection: "column", gap: T.titleGap }}>
460
+ <div id={ids.title} style={{ ...T.titleType, color: T.fgStrong }}>{title}</div>
461
+ <Notice height={noticeHeight} titleId={ids.title} regionRef={regionRef} showPrint={showPrint}
462
+ onPrint={print} showHint={!read} onProgress={onInlineProgress} startAt={seed.read ? 1 : seed.progress}>
463
+ {children}
464
+ </Notice>
465
+ </div>
466
+ {agreement}
467
+ {actions}
468
+ </div>
469
+ );
470
+ }
471
+
472
+ return (
473
+ <div className={`pds-ste ${className}`}
474
+ style={contained && sheetOpen ? { ...root, position: "relative", minHeight: T.sheetFrameHeight } : root}>
475
+ <span ref={triggerRef} style={{ display: "flex" }}>
476
+ <Button buttonStyle="Fill" type="Tertiary" size="M" text={read ? readLabel : readAgreementLabel}
477
+ showTrailingIcon trailingIcon={read ? "check" : "open_in_new"} aria-haspopup="dialog"
478
+ onClick={() => { setSheetOpen(true); setSheetMsg(null); }} />
479
+ </span>
480
+ {agreement}
481
+ {actions}
482
+ {sheetOpen && (
483
+ <Sheet title={title} titleId={ids.sheetTitle} stepLabel={stepLabel} onClose={closeSheet} initialFocusRef={sheetRegionRef} contained={contained}
484
+ footer={
485
+ <div style={{ display: "flex", justifyContent: "center" }}>
486
+ <Button buttonStyle="Fill" type="Primary" size="L" text={doneReadingLabel} onClick={onDoneReading} aria-describedby={ids.sheetMsg} />
487
+ </div>
488
+ }>
489
+ <Notice height={noticeHeight} titleId={ids.sheetTitle} regionRef={sheetRegionRef} showPrint={false}
490
+ showHint={progress < 1} onProgress={onSheetProgress} startAt={seed.sheetOpen ? seed.progress : undefined}>
491
+ {children}
492
+ </Notice>
493
+ <FieldMessage id={ids.sheetMsg} text={sheetMsg ? copy[sheetMsg] : null} nonce={nonce} />
494
+ </Sheet>
495
+ )}
496
+ </div>
497
+ );
498
+ }
499
+
500
+ export default ScrollToEnable;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helloimjolopez-pathway/pathway-components",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "description": "Pathway Design System React components",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -18,7 +18,10 @@
18
18
  "./nav-shell": "./components/nav-shell/nav-shell.jsx",
19
19
  "./page-template": "./components/page-template/page-template.jsx",
20
20
  "./tooltip": "./components/tooltip/tooltip.jsx",
21
- "./badge": "./components/badge/badge.jsx"
21
+ "./badge": "./components/badge/badge.jsx",
22
+ "./scroll-to-enable": "./components/scroll-to-enable/scroll-to-enable.jsx",
23
+ "./bottom-sheet": "./components/bottom-sheet/bottom-sheet.jsx",
24
+ "./dropdown": "./components/dropdown/dropdown.jsx"
22
25
  },
23
26
  "peerDependencies": {
24
27
  "react": ">=17",