@conveyorhq/arrow-ds 1.36.0 → 1.37.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.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@conveyorhq/arrow-ds",
3
3
  "author": "Conveyor",
4
4
  "license": "MIT",
5
- "version": "1.36.0",
5
+ "version": "1.37.0",
6
6
  "description": "Arrow Design System",
7
7
  "repository": "https://github.com/conveyor/arrow-ds",
8
8
  "publishConfig": {
@@ -0,0 +1,57 @@
1
+ import React, { ReactNode, Context, MutableRefObject, Dispatch, SetStateAction, HTMLAttributes, ReactElement, AnchorHTMLAttributes, MouseEvent, ChangeEvent, KeyboardEvent, FocusEvent } from "react";
2
+ declare type ScrollSpyContextType = {
3
+ currentIndex: number;
4
+ setCurrentIndex: Dispatch<SetStateAction<number>>;
5
+ currentPage: number;
6
+ setCurrentPage: Dispatch<SetStateAction<number>>;
7
+ refList: MutableRefObject<HTMLElement[]>;
8
+ updateCurrentRef(index: number, refItem: HTMLElement): void;
9
+ scrollToRef(refToScrollTo: HTMLElement): void;
10
+ scrollToFirst(event: MouseEvent): void;
11
+ scrollToLast(event: MouseEvent): void;
12
+ onAnchorClick(event: MouseEvent<HTMLAnchorElement>, index: number): void;
13
+ onInputChange(event: ChangeEvent<HTMLInputElement>): void;
14
+ onInputKeyUp(event: KeyboardEvent<HTMLInputElement>): void;
15
+ onInputFocus(event: FocusEvent<HTMLInputElement>): void;
16
+ items: number;
17
+ id: string;
18
+ };
19
+ export declare const ScrollSpyContext: Context<ScrollSpyContextType>;
20
+ export declare function useScrollSpyContext(): ScrollSpyContextType;
21
+ export interface ScrollSpyPageProps extends HTMLAttributes<HTMLDivElement> {
22
+ children: ReactElement;
23
+ threshold: number | number[] | undefined;
24
+ index: number;
25
+ }
26
+ export declare type ScrollSpyPageUsePropsHookProps = {
27
+ index: number;
28
+ id?: string;
29
+ };
30
+ export declare const useScrollSpyPageProps: ({ index, id }?: ScrollSpyPageUsePropsHookProps) => {
31
+ id: string;
32
+ };
33
+ export declare const ScrollSpyPage: React.ForwardRefExoticComponent<ScrollSpyPageProps & React.RefAttributes<HTMLDivElement>>;
34
+ export interface ScrollSpyAnchorProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
35
+ children: ReactNode;
36
+ index: number;
37
+ }
38
+ export declare type ScrollSpyAnchorUsePropsHookProps = {
39
+ index: number;
40
+ id?: string;
41
+ href?: string;
42
+ onClick?(event: MouseEvent<HTMLAnchorElement>): void;
43
+ };
44
+ export declare const useScrollSpyAnchorProps: ({ index, id, href, onClick }?: ScrollSpyAnchorUsePropsHookProps) => {
45
+ id: string;
46
+ href: string;
47
+ onClick: (event: MouseEvent<HTMLAnchorElement>) => void;
48
+ isActive: boolean;
49
+ };
50
+ export declare const ScrollSpyAnchor: React.ForwardRefExoticComponent<ScrollSpyAnchorProps & React.RefAttributes<HTMLAnchorElement>>;
51
+ export declare type ScrollSpyRootProps = {
52
+ children: ReactNode | ((props: ScrollSpyContextType) => JSX.Element);
53
+ items: number;
54
+ defaultIndex?: number;
55
+ };
56
+ export declare const ScrollSpyRoot: ({ children, items, defaultIndex, }: ScrollSpyRootProps) => JSX.Element;
57
+ export {};
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
+ }) : (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ o[k2] = m[k];
8
+ }));
9
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
11
+ }) : function(o, v) {
12
+ o["default"] = v;
13
+ });
14
+ var __importStar = (this && this.__importStar) || function (mod) {
15
+ if (mod && mod.__esModule) return mod;
16
+ var result = {};
17
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
+ __setModuleDefault(result, mod);
19
+ return result;
20
+ };
21
+ var __importDefault = (this && this.__importDefault) || function (mod) {
22
+ return (mod && mod.__esModule) ? mod : { "default": mod };
23
+ };
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.ScrollSpyRoot = exports.ScrollSpyAnchor = exports.useScrollSpyAnchorProps = exports.ScrollSpyPage = exports.useScrollSpyPageProps = exports.useScrollSpyContext = exports.ScrollSpyContext = void 0;
26
+ const react_1 = __importStar(require("react"));
27
+ const react_merge_refs_1 = __importDefault(require("react-merge-refs"));
28
+ const auto_id_1 = require("@reach/auto-id");
29
+ const hooks_1 = require("../../hooks");
30
+ const defaultRootContext = {
31
+ currentIndex: 0,
32
+ setCurrentIndex: (index) => index,
33
+ currentPage: 1,
34
+ setCurrentPage: (index) => index,
35
+ refList: { current: [] },
36
+ updateCurrentRef: () => { },
37
+ scrollToRef: () => { },
38
+ scrollToFirst: () => { },
39
+ scrollToLast: () => { },
40
+ onAnchorClick: () => { },
41
+ onInputChange: () => { },
42
+ onInputKeyUp: () => { },
43
+ onInputFocus: () => { },
44
+ items: 1,
45
+ id: "scroll-spy-1",
46
+ };
47
+ exports.ScrollSpyContext = react_1.createContext(defaultRootContext);
48
+ function useScrollSpyContext() {
49
+ const context = react_1.useContext(exports.ScrollSpyContext) || {
50
+ ...defaultRootContext,
51
+ };
52
+ return context;
53
+ }
54
+ exports.useScrollSpyContext = useScrollSpyContext;
55
+ const useScrollSpyPageProps = ({ index, id } = { index: 0 }) => {
56
+ const { id: rootId } = useScrollSpyContext();
57
+ const pageId = `${rootId}-page-${index + 1}`;
58
+ return {
59
+ id: id || pageId,
60
+ };
61
+ };
62
+ exports.useScrollSpyPageProps = useScrollSpyPageProps;
63
+ exports.ScrollSpyPage = react_1.forwardRef(({ id: idProp, children, index, threshold = 0.5, ...rest }, forwardedRef) => {
64
+ const ref = react_1.useRef(null);
65
+ const combinedRef = react_merge_refs_1.default([ref, forwardedRef]);
66
+ const isVisible = hooks_1.useIntersection(ref, {
67
+ threshold,
68
+ });
69
+ const { updateCurrentRef, setCurrentIndex, setCurrentPage, } = useScrollSpyContext();
70
+ const { id } = exports.useScrollSpyPageProps({ index, id: idProp });
71
+ const pageNumber = index + 1;
72
+ react_1.useEffect(() => {
73
+ if (ref && ref.current) {
74
+ updateCurrentRef(index, ref.current);
75
+ }
76
+ }, [ref, index, updateCurrentRef]);
77
+ react_1.useEffect(() => {
78
+ if (isVisible) {
79
+ setCurrentIndex(index);
80
+ setCurrentPage(pageNumber);
81
+ }
82
+ }, [isVisible, index, pageNumber, setCurrentIndex, setCurrentPage]);
83
+ return (react_1.default.createElement(react_1.default.Fragment, null, react_1.default.cloneElement(react_1.default.Children.only(children), {
84
+ ...rest,
85
+ ...children.props,
86
+ id,
87
+ ref: combinedRef,
88
+ })));
89
+ });
90
+ const useScrollSpyAnchorProps = ({ index, id, href, onClick } = { index: 0 }) => {
91
+ const { id: rootId, currentIndex, onAnchorClick } = useScrollSpyContext();
92
+ const { id: pageId } = exports.useScrollSpyPageProps({ index });
93
+ const anchorId = `${rootId}-anchor-${index + 1}`;
94
+ return {
95
+ id: id || anchorId,
96
+ href: href || `#${pageId}`,
97
+ onClick: (event) => {
98
+ event.preventDefault();
99
+ if (onClick)
100
+ onClick(event);
101
+ onAnchorClick(event, index);
102
+ },
103
+ isActive: currentIndex === index,
104
+ };
105
+ };
106
+ exports.useScrollSpyAnchorProps = useScrollSpyAnchorProps;
107
+ exports.ScrollSpyAnchor = react_1.forwardRef(({ id, children, onClick, index = 0, ...rest }, forwardedRef) => {
108
+ const { isActive, ...anchorProps } = exports.useScrollSpyAnchorProps({
109
+ index,
110
+ id,
111
+ onClick,
112
+ });
113
+ return (react_1.default.createElement("a", Object.assign({ ref: forwardedRef }, anchorProps, rest), children));
114
+ });
115
+ const ScrollSpyRoot = ({ children, items, defaultIndex = 0, }) => {
116
+ const defaultPageNumber = defaultIndex + 1;
117
+ const [currentIndex, setCurrentIndex] = react_1.useState(defaultIndex);
118
+ const [currentPage, setCurrentPage] = react_1.useState(defaultPageNumber);
119
+ const refList = react_1.useRef([]);
120
+ const id = `scroll-spy-${auto_id_1.useId()}`;
121
+ react_1.useEffect(() => {
122
+ refList.current = refList.current.slice(0, items);
123
+ }, [items]);
124
+ const updateCurrentRef = (index, refItem) => {
125
+ refList.current[index] = refItem;
126
+ };
127
+ const scrollToRef = (refToScrollTo) => {
128
+ refToScrollTo.scrollIntoView({ behavior: "smooth" });
129
+ };
130
+ const scrollToFirst = (event) => {
131
+ event.stopPropagation();
132
+ scrollToRef(refList.current[0]);
133
+ };
134
+ const scrollToLast = (event) => {
135
+ event.stopPropagation();
136
+ scrollToRef(refList.current[items - 1]);
137
+ };
138
+ const onAnchorClick = (event, index) => {
139
+ event.stopPropagation();
140
+ scrollToRef(refList.current[index]);
141
+ setCurrentPage(index + 1);
142
+ };
143
+ const onInputFocus = (event) => event.target.select();
144
+ const onInputChange = (event) => {
145
+ event.stopPropagation();
146
+ event.preventDefault();
147
+ const { value } = event.target;
148
+ const pageNumber = parseInt(value, 10);
149
+ const index = pageNumber - 1;
150
+ setCurrentPage(pageNumber);
151
+ if (pageNumber && pageNumber >= 1 && pageNumber <= items) {
152
+ scrollToRef(refList.current[index]);
153
+ }
154
+ };
155
+ const onInputKeyUp = (event) => {
156
+ event.stopPropagation();
157
+ event.preventDefault();
158
+ const { value } = event.currentTarget;
159
+ const pageNumber = parseInt(value, 10);
160
+ const index = pageNumber - 1;
161
+ if (pageNumber && pageNumber >= 1 && pageNumber <= items) {
162
+ scrollToRef(refList.current[index]);
163
+ }
164
+ };
165
+ const context = {
166
+ currentIndex,
167
+ setCurrentIndex,
168
+ currentPage,
169
+ setCurrentPage,
170
+ refList,
171
+ updateCurrentRef,
172
+ scrollToRef,
173
+ scrollToFirst,
174
+ scrollToLast,
175
+ onAnchorClick,
176
+ onInputChange,
177
+ onInputKeyUp,
178
+ onInputFocus,
179
+ items,
180
+ id,
181
+ };
182
+ return (react_1.default.createElement(exports.ScrollSpyContext.Provider, { value: context }, typeof children === "function" ? children(context) : children));
183
+ };
184
+ exports.ScrollSpyRoot = ScrollSpyRoot;
@@ -0,0 +1 @@
1
+ export * from "./ScrollSpy";
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
+ }) : (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ o[k2] = m[k];
8
+ }));
9
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
10
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ __exportStar(require("./ScrollSpy"), exports);
@@ -1,4 +1,5 @@
1
1
  export { useDisclosure } from "./useDisclosure";
2
+ export { useIntersection } from "./useIntersection";
2
3
  export { useKeyPress } from "./useKeyPress";
3
4
  export { useMatchMedia } from "./useMatchMedia";
4
5
  export { useOutsideClick } from "./useOutsideClick";
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useStep = exports.useScreenOrientation = exports.usePrefersReducedMotion = exports.useOutsideClick = exports.useMatchMedia = exports.useKeyPress = exports.useDisclosure = void 0;
3
+ exports.useStep = exports.useScreenOrientation = exports.usePrefersReducedMotion = exports.useOutsideClick = exports.useMatchMedia = exports.useKeyPress = exports.useIntersection = exports.useDisclosure = void 0;
4
4
  var useDisclosure_1 = require("./useDisclosure");
5
5
  Object.defineProperty(exports, "useDisclosure", { enumerable: true, get: function () { return useDisclosure_1.useDisclosure; } });
6
+ var useIntersection_1 = require("./useIntersection");
7
+ Object.defineProperty(exports, "useIntersection", { enumerable: true, get: function () { return useIntersection_1.useIntersection; } });
6
8
  var useKeyPress_1 = require("./useKeyPress");
7
9
  Object.defineProperty(exports, "useKeyPress", { enumerable: true, get: function () { return useKeyPress_1.useKeyPress; } });
8
10
  var useMatchMedia_1 = require("./useMatchMedia");
@@ -0,0 +1,6 @@
1
+ import { RefObject } from "react";
2
+ export declare function useIntersection(ref: RefObject<HTMLElement>, options?: {
3
+ root?: Element | Document | null | undefined;
4
+ rootMargin?: string | undefined;
5
+ threshold?: number | number[] | undefined;
6
+ }): boolean;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useIntersection = void 0;
4
+ const react_1 = require("react");
5
+ function useIntersection(ref, options) {
6
+ const [isIntersecting, setIsIntersecting] = react_1.useState(false);
7
+ const observer = react_1.useMemo(() => new IntersectionObserver(([entry]) => setIsIntersecting(entry.isIntersecting), {
8
+ root: undefined,
9
+ rootMargin: "0px",
10
+ threshold: 1.0,
11
+ ...options,
12
+ }), [options]);
13
+ react_1.useEffect(() => {
14
+ if (ref && ref.current) {
15
+ observer.observe(ref.current);
16
+ }
17
+ return () => {
18
+ observer.disconnect();
19
+ };
20
+ }, [observer, ref]);
21
+ return isIntersecting;
22
+ }
23
+ exports.useIntersection = useIntersection;
package/public/index.d.ts CHANGED
@@ -61,6 +61,7 @@ export * from "./components/Radio";
61
61
  export * from "./components/Relative";
62
62
  export * from "./components/Reference";
63
63
  export * from "./components/ScrollPane";
64
+ export * from "./components/ScrollSpy";
64
65
  export * from "./components/SearchFilter";
65
66
  export * from "./components/Select";
66
67
  export * from "./components/SelectNew";
package/public/index.js CHANGED
@@ -81,6 +81,7 @@ __exportStar(require("./components/Radio"), exports);
81
81
  __exportStar(require("./components/Relative"), exports);
82
82
  __exportStar(require("./components/Reference"), exports);
83
83
  __exportStar(require("./components/ScrollPane"), exports);
84
+ __exportStar(require("./components/ScrollSpy"), exports);
84
85
  __exportStar(require("./components/SearchFilter"), exports);
85
86
  __exportStar(require("./components/Select"), exports);
86
87
  __exportStar(require("./components/SelectNew"), exports);
@@ -0,0 +1,14 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+ import { ScrollSpy } from "./ScrollSpy";
3
+
4
+ <Meta title="Components/Layout/Scroll Spy" component={ScrollSpy} />
5
+
6
+ # Scroll Spy
7
+
8
+ ScrollSpy tracks scrolling using IntersectionObserver.
9
+
10
+ Documenation in progress…
11
+
12
+ ```jsx
13
+ import { ScrollSpy } from "@conveyorhq/arrow-ds";
14
+ ```
@@ -0,0 +1,322 @@
1
+ import React, {
2
+ ReactNode,
3
+ useState,
4
+ useRef,
5
+ useEffect,
6
+ Context,
7
+ createContext,
8
+ MutableRefObject,
9
+ useContext,
10
+ Dispatch,
11
+ SetStateAction,
12
+ HTMLAttributes,
13
+ forwardRef,
14
+ ReactElement,
15
+ AnchorHTMLAttributes,
16
+ MouseEvent,
17
+ ChangeEvent,
18
+ KeyboardEvent,
19
+ FocusEvent,
20
+ } from "react";
21
+ import mergeRefs from "react-merge-refs";
22
+ import { useId } from "@reach/auto-id";
23
+ import { useIntersection } from "../../hooks";
24
+
25
+ // ------------------------------------------------------------------
26
+ // ScrollSpy.Context
27
+ // ------------------------------------------------------------------
28
+ type ScrollSpyContextType = {
29
+ currentIndex: number;
30
+ setCurrentIndex: Dispatch<SetStateAction<number>>;
31
+ currentPage: number;
32
+ setCurrentPage: Dispatch<SetStateAction<number>>;
33
+ refList: MutableRefObject<HTMLElement[]>;
34
+ updateCurrentRef(index: number, refItem: HTMLElement): void;
35
+ scrollToRef(refToScrollTo: HTMLElement): void;
36
+ scrollToFirst(event: MouseEvent): void;
37
+ scrollToLast(event: MouseEvent): void;
38
+ onAnchorClick(event: MouseEvent<HTMLAnchorElement>, index: number): void;
39
+ onInputChange(event: ChangeEvent<HTMLInputElement>): void;
40
+ onInputKeyUp(event: KeyboardEvent<HTMLInputElement>): void;
41
+ onInputFocus(event: FocusEvent<HTMLInputElement>): void;
42
+ items: number;
43
+ id: string;
44
+ };
45
+
46
+ const defaultRootContext: ScrollSpyContextType = {
47
+ currentIndex: 0,
48
+ setCurrentIndex: (index) => index,
49
+ currentPage: 1,
50
+ setCurrentPage: (index) => index,
51
+ refList: { current: [] },
52
+ updateCurrentRef: () => {},
53
+ scrollToRef: () => {},
54
+ scrollToFirst: () => {},
55
+ scrollToLast: () => {},
56
+ onAnchorClick: () => {},
57
+ onInputChange: () => {},
58
+ onInputKeyUp: () => {},
59
+ onInputFocus: () => {},
60
+ items: 1,
61
+ id: "scroll-spy-1",
62
+ };
63
+
64
+ export const ScrollSpyContext: Context<ScrollSpyContextType> = createContext<ScrollSpyContextType>(
65
+ defaultRootContext,
66
+ );
67
+
68
+ export function useScrollSpyContext() {
69
+ const context = useContext<typeof defaultRootContext>(ScrollSpyContext) || {
70
+ ...defaultRootContext,
71
+ };
72
+
73
+ return context;
74
+ }
75
+
76
+ // ------------------------------------------------------------------
77
+ // ScrollSpy.Page
78
+ // ------------------------------------------------------------------
79
+ export interface ScrollSpyPageProps extends HTMLAttributes<HTMLDivElement> {
80
+ children: ReactElement;
81
+ threshold: number | number[] | undefined;
82
+ index: number;
83
+ }
84
+
85
+ export type ScrollSpyPageUsePropsHookProps = {
86
+ index: number;
87
+ id?: string;
88
+ };
89
+
90
+ export const useScrollSpyPageProps = (
91
+ { index, id }: ScrollSpyPageUsePropsHookProps = { index: 0 },
92
+ ) => {
93
+ const { id: rootId } = useScrollSpyContext();
94
+ const pageId = `${rootId}-page-${index + 1}`;
95
+ return {
96
+ id: id || pageId,
97
+ };
98
+ };
99
+
100
+ export const ScrollSpyPage = forwardRef<HTMLDivElement, ScrollSpyPageProps>(
101
+ ({ id: idProp, children, index, threshold = 0.5, ...rest }, forwardedRef) => {
102
+ const ref = useRef<HTMLDivElement>(null);
103
+ const combinedRef = mergeRefs<HTMLDivElement>([ref, forwardedRef]);
104
+
105
+ const isVisible = useIntersection(ref, {
106
+ threshold,
107
+ });
108
+
109
+ const {
110
+ updateCurrentRef,
111
+ setCurrentIndex,
112
+ setCurrentPage,
113
+ } = useScrollSpyContext();
114
+
115
+ const { id } = useScrollSpyPageProps({ index, id: idProp });
116
+ const pageNumber = index + 1;
117
+
118
+ useEffect(() => {
119
+ if (ref && ref.current) {
120
+ updateCurrentRef(index, ref.current);
121
+ }
122
+ }, [ref, index, updateCurrentRef]);
123
+
124
+ useEffect(() => {
125
+ if (isVisible) {
126
+ setCurrentIndex(index);
127
+ setCurrentPage(pageNumber);
128
+ }
129
+ }, [isVisible, index, pageNumber, setCurrentIndex, setCurrentPage]);
130
+
131
+ return (
132
+ <>
133
+ {React.cloneElement(React.Children.only(children), {
134
+ ...rest,
135
+ ...children.props,
136
+ id,
137
+ ref: combinedRef,
138
+ })}
139
+ </>
140
+ );
141
+ },
142
+ );
143
+
144
+ // ------------------------------------------------------------------
145
+ // ScrollSpy.Anchor
146
+ // ------------------------------------------------------------------
147
+ export interface ScrollSpyAnchorProps
148
+ extends AnchorHTMLAttributes<HTMLAnchorElement> {
149
+ children: ReactNode;
150
+ index: number;
151
+ }
152
+
153
+ export type ScrollSpyAnchorUsePropsHookProps = {
154
+ index: number;
155
+ id?: string;
156
+ href?: string;
157
+ onClick?(event: MouseEvent<HTMLAnchorElement>): void;
158
+ };
159
+
160
+ export const useScrollSpyAnchorProps = (
161
+ { index, id, href, onClick }: ScrollSpyAnchorUsePropsHookProps = { index: 0 },
162
+ ) => {
163
+ const { id: rootId, currentIndex, onAnchorClick } = useScrollSpyContext();
164
+ const { id: pageId } = useScrollSpyPageProps({ index });
165
+ const anchorId = `${rootId}-anchor-${index + 1}`;
166
+
167
+ return {
168
+ id: id || anchorId,
169
+ href: href || `#${pageId}`,
170
+ onClick: (event: MouseEvent<HTMLAnchorElement>) => {
171
+ event.preventDefault();
172
+ if (onClick) onClick(event);
173
+ onAnchorClick(event, index);
174
+ },
175
+ isActive: currentIndex === index,
176
+ };
177
+ };
178
+
179
+ export const ScrollSpyAnchor = forwardRef<
180
+ HTMLAnchorElement,
181
+ ScrollSpyAnchorProps
182
+ >(({ id, children, onClick, index = 0, ...rest }, forwardedRef) => {
183
+ const { isActive, ...anchorProps } = useScrollSpyAnchorProps({
184
+ index,
185
+ id,
186
+ onClick,
187
+ });
188
+
189
+ return (
190
+ <a ref={forwardedRef} {...anchorProps} {...rest}>
191
+ {children}
192
+ </a>
193
+ );
194
+ });
195
+
196
+ // ------------------------------------------------------------------
197
+ // ScrollSpy.Root
198
+ // ------------------------------------------------------------------
199
+ export type ScrollSpyRootProps = {
200
+ children: ReactNode | ((props: ScrollSpyContextType) => JSX.Element);
201
+ items: number;
202
+ defaultIndex?: number;
203
+ };
204
+
205
+ export const ScrollSpyRoot = ({
206
+ children,
207
+ items,
208
+ defaultIndex = 0,
209
+ }: ScrollSpyRootProps) => {
210
+ const defaultPageNumber = defaultIndex + 1;
211
+ const [currentIndex, setCurrentIndex] = useState(defaultIndex);
212
+ const [currentPage, setCurrentPage] = useState(defaultPageNumber);
213
+ const refList = useRef<HTMLElement[]>([]);
214
+ const id = `scroll-spy-${useId()}`;
215
+
216
+ useEffect(() => {
217
+ refList.current = refList.current.slice(0, items);
218
+ }, [items]);
219
+
220
+ const updateCurrentRef = (index: number, refItem: HTMLElement) => {
221
+ refList.current[index] = refItem;
222
+ };
223
+
224
+ const scrollToRef = (refToScrollTo: HTMLElement) => {
225
+ refToScrollTo.scrollIntoView({ behavior: "smooth" });
226
+ };
227
+
228
+ const scrollToFirst = (event: MouseEvent) => {
229
+ event.stopPropagation();
230
+ scrollToRef(refList.current[0]);
231
+ };
232
+
233
+ const scrollToLast = (event: MouseEvent) => {
234
+ event.stopPropagation();
235
+ scrollToRef(refList.current[items - 1]);
236
+ };
237
+
238
+ const onAnchorClick = (
239
+ event: MouseEvent<HTMLAnchorElement>,
240
+ index: number,
241
+ ) => {
242
+ event.stopPropagation();
243
+ scrollToRef(refList.current[index]);
244
+ setCurrentPage(index + 1);
245
+ };
246
+
247
+ const onInputFocus = (event: FocusEvent<HTMLInputElement>) =>
248
+ event.target.select();
249
+
250
+ const onInputChange = (event: ChangeEvent<HTMLInputElement>) => {
251
+ event.stopPropagation();
252
+ event.preventDefault();
253
+
254
+ const { value } = event.target;
255
+ const pageNumber = parseInt(value, 10);
256
+ const index = pageNumber - 1;
257
+
258
+ setCurrentPage(pageNumber);
259
+
260
+ if (pageNumber && pageNumber >= 1 && pageNumber <= items) {
261
+ scrollToRef(refList.current[index]);
262
+ }
263
+ };
264
+
265
+ const onInputKeyUp = (event: KeyboardEvent<HTMLInputElement>) => {
266
+ event.stopPropagation();
267
+ event.preventDefault();
268
+
269
+ const { value } = event.currentTarget;
270
+ const pageNumber = parseInt(value, 10);
271
+ const index = pageNumber - 1;
272
+
273
+ if (pageNumber && pageNumber >= 1 && pageNumber <= items) {
274
+ scrollToRef(refList.current[index]);
275
+ }
276
+ };
277
+
278
+ const context = {
279
+ currentIndex,
280
+ setCurrentIndex,
281
+ currentPage,
282
+ setCurrentPage,
283
+ refList,
284
+ updateCurrentRef,
285
+ scrollToRef,
286
+ scrollToFirst,
287
+ scrollToLast,
288
+ onAnchorClick,
289
+ onInputChange,
290
+ onInputKeyUp,
291
+ onInputFocus,
292
+ items,
293
+ id,
294
+ };
295
+
296
+ return (
297
+ <ScrollSpyContext.Provider value={context}>
298
+ {typeof children === "function" ? children(context) : children}
299
+ </ScrollSpyContext.Provider>
300
+ );
301
+ };
302
+
303
+ /**
304
+ * @TODO get types right and re-enable these exports
305
+ */
306
+ // ------------------------------------------------------------------
307
+ // ScrollSpy
308
+ // ------------------------------------------------------------------
309
+ // export type ScrollSpyProps = ScrollSpyRootProps & {
310
+ // Root: ScrollSpyRootProps;
311
+ // Anchor: ScrollSpyAnchorProps;
312
+ // Page: ScrollSpyPageProps;
313
+ // };
314
+
315
+ // export const ScrollSpy = () => ({
316
+ // ...ScrollSpyRoot,
317
+ // ...{
318
+ // Root: ScrollSpyRoot,
319
+ // Anchor: ScrollSpyAnchor,
320
+ // Page: ScrollSpyPage,
321
+ // },
322
+ // });
@@ -0,0 +1 @@
1
+ export * from "./ScrollSpy";
@@ -1,4 +1,5 @@
1
1
  export { useDisclosure } from "./useDisclosure";
2
+ export { useIntersection } from "./useIntersection";
2
3
  export { useKeyPress } from "./useKeyPress";
3
4
  export { useMatchMedia } from "./useMatchMedia";
4
5
  export { useOutsideClick } from "./useOutsideClick";
@@ -0,0 +1,37 @@
1
+ import { RefObject, useEffect, useMemo, useState } from "react";
2
+
3
+ export function useIntersection(
4
+ ref: RefObject<HTMLElement>,
5
+ options?: {
6
+ root?: Element | Document | null | undefined;
7
+ rootMargin?: string | undefined;
8
+ threshold?: number | number[] | undefined;
9
+ },
10
+ ) {
11
+ const [isIntersecting, setIsIntersecting] = useState(false);
12
+ const observer = useMemo(
13
+ () =>
14
+ new IntersectionObserver(
15
+ ([entry]) => setIsIntersecting(entry.isIntersecting),
16
+ {
17
+ root: undefined,
18
+ rootMargin: "0px",
19
+ threshold: 1.0,
20
+ ...options,
21
+ },
22
+ ),
23
+ [options],
24
+ );
25
+
26
+ useEffect(() => {
27
+ if (ref && ref.current) {
28
+ observer.observe(ref.current);
29
+ }
30
+
31
+ return () => {
32
+ observer.disconnect();
33
+ };
34
+ }, [observer, ref]);
35
+
36
+ return isIntersecting;
37
+ }
package/src/index.ts CHANGED
@@ -62,6 +62,7 @@ export * from "./components/Radio";
62
62
  export * from "./components/Relative";
63
63
  export * from "./components/Reference";
64
64
  export * from "./components/ScrollPane";
65
+ export * from "./components/ScrollSpy";
65
66
  export * from "./components/SearchFilter";
66
67
  export * from "./components/Select";
67
68
  export * from "./components/SelectNew";