etudes 6.3.0 → 7.0.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.
@@ -14,7 +14,7 @@ export type AccordionSelection = Record<number, number[]>;
14
14
  */
15
15
  export type AccordionSection<T> = Pick<CollectionProps<T>, 'isSelectionTogglable' | 'itemLength' | 'itemPadding' | 'items' | 'layout' | 'numSegments'> & {
16
16
  /**
17
- * Padding (in pixels) between the sectionheader and the internal collection.
17
+ * Padding (in pixels) between the section header and the internal collection.
18
18
  */
19
19
  collectionPadding?: number;
20
20
  /**
@@ -118,7 +118,7 @@ export const Accordion = forwardRef(({ children, style, autoCollapseSections = f
118
118
  const selection = sanitizeSelection(externalSelection ?? {});
119
119
  const expandedSectionIndices = sanitizeExpandedSectionIndices(externalExpandedSectionIndices ?? []);
120
120
  const fixedStyles = getFixedStyles({ orientation });
121
- const prevSelectionRef = useRef();
121
+ const prevSelectionRef = useRef(undefined);
122
122
  const prevSelection = prevSelectionRef.current;
123
123
  const components = asComponentDict(children, {
124
124
  collapseIcon: AccordionCollapseIcon,
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import clsx from 'clsx';
3
- import { Link } from 'react-router-dom';
3
+ import { Link } from 'react-router';
4
4
  export function Button({ children, className, href, isDisabled = false, label, opensInNewTab, to, type, ...props }) {
5
5
  if (href) {
6
6
  return (_jsx("a", { ...props, "aria-disabled": isDisabled, "aria-label": label, className: clsx(className, { disabled: isDisabled }), href: href, rel: opensInNewTab ? 'noopener,noreferrer' : undefined, target: opensInNewTab ? '_blank' : undefined, children: children ?? label }));
@@ -30,11 +30,20 @@ export const Carousel = forwardRef(({ autoAdvanceInterval = 0, index = 0, isDrag
30
30
  pointerDownPositionRef.current = undefined;
31
31
  pointerUpPositionRef.current = undefined;
32
32
  };
33
- const normalizeScrollPosition = () => scrollToIndex(viewportRef, index, orientation);
34
- const prevIndexRef = useRef();
33
+ const normalizeScrollPosition = () => {
34
+ scrollToIndex(viewportRef, index, orientation);
35
+ clearTimeout(autoScrollTimeoutRef.current);
36
+ autoScrollTimeoutRef.current = setTimeout(() => {
37
+ clearTimeout(autoScrollTimeoutRef.current);
38
+ autoScrollTimeoutRef.current = undefined;
39
+ }, autoScrollTimeoutMs);
40
+ };
41
+ const prevIndexRef = useRef(undefined);
35
42
  const viewportRef = useRef(null);
36
- const pointerDownPositionRef = useRef();
37
- const pointerUpPositionRef = useRef();
43
+ const pointerDownPositionRef = useRef(undefined);
44
+ const pointerUpPositionRef = useRef(undefined);
45
+ const autoScrollTimeoutRef = useRef(undefined);
46
+ const autoScrollTimeoutMs = 1000;
38
47
  const [exposures, setExposures] = useState(getItemExposures(viewportRef, orientation));
39
48
  const [isPointerDown, setIsPointerDown] = useState(false);
40
49
  const fixedStyles = getFixedStyles({ scrollSnapEnabled: !isPointerDown, orientation });
@@ -49,6 +58,8 @@ export const Carousel = forwardRef(({ autoAdvanceInterval = 0, index = 0, isDrag
49
58
  if (tracksItemExposure) {
50
59
  setExposures(getItemExposures(viewportRef, orientation));
51
60
  }
61
+ if (autoScrollTimeoutRef.current !== undefined)
62
+ return;
52
63
  const newIndex = orientation === 'horizontal'
53
64
  ? Math.round(viewport.scrollLeft / viewport.clientWidth)
54
65
  : Math.round(viewport.scrollTop / viewport.clientHeight);
@@ -114,7 +125,7 @@ export const Carousel = forwardRef(({ autoAdvanceInterval = 0, index = 0, isDrag
114
125
  return (_jsx("div", { ...props, ref: ref, role: 'region', onClick: event => handleClick(event), onPointerCancel: event => handlePointerUp(event), onPointerDown: event => handlePointerDown(event), onPointerLeave: event => handlePointerUp(event), onPointerUp: event => handlePointerUp(event), children: _jsx("div", { ref: viewportRef, style: styles(fixedStyles.viewport), children: _jsx(Each, { in: items, children: ({ style: itemStyle, ...itemProps }, idx) => (_jsx("div", { style: styles(fixedStyles.itemContainer), children: _jsx(ItemComponent, { "aria-hidden": idx !== index, exposure: tracksItemExposure ? exposures?.[idx] : undefined, style: styles(itemStyle, fixedStyles.item), ...itemProps }) })) }) }) }));
115
126
  });
116
127
  function scrollToIndex(ref, index, orientation) {
117
- const viewport = ref.current;
128
+ const viewport = ref?.current;
118
129
  if (!viewport)
119
130
  return;
120
131
  const top = orientation === 'horizontal' ? 0 : viewport.clientHeight * index;
@@ -124,7 +135,7 @@ function scrollToIndex(ref, index, orientation) {
124
135
  viewport.scrollTo({ top, left, behavior: 'smooth' });
125
136
  }
126
137
  function getItemExposures(ref, orientation) {
127
- const viewport = ref.current;
138
+ const viewport = ref?.current;
128
139
  if (!viewport)
129
140
  return undefined;
130
141
  const exposures = [];
@@ -134,7 +145,7 @@ function getItemExposures(ref, orientation) {
134
145
  return exposures;
135
146
  }
136
147
  function getItemExposureAt(idx, ref, orientation) {
137
- const viewport = ref.current;
148
+ const viewport = ref?.current;
138
149
  const child = viewport?.children[idx];
139
150
  if (!child)
140
151
  return 0;
@@ -79,7 +79,7 @@ export const Collection = forwardRef(({ className, style, isSelectionTogglable =
79
79
  };
80
80
  const selection = sanitizeSelection(externalSelection ?? []);
81
81
  const fixedStyles = getFixedStyles({ itemLength, itemPadding, layout, numSegments, orientation });
82
- const prevSelectionRef = useRef();
82
+ const prevSelectionRef = useRef(undefined);
83
83
  const prevSelection = prevSelectionRef.current;
84
84
  useEffect(() => {
85
85
  prevSelectionRef.current = selection;
@@ -61,7 +61,7 @@ export type DropdownProps<T extends DropdownItemData = DropdownItemData> = HTMLA
61
61
  */
62
62
  isInverted?: boolean;
63
63
  /**
64
- * Maximum number of items that are viside when the component expands. When a
64
+ * Maximum number of items that are visible when the component expands. When a
65
65
  * value greater than or equal to 0 is specified, only that number of items
66
66
  * will be visible at a time and a scrollbar will appear to enable scrolling
67
67
  * to remaining items. Any value less than 0 indicates that all items will be
@@ -34,7 +34,7 @@ export function WithTooltip({ children, className, style, alignment: externalAli
34
34
  dialogRef.current.ariaHidden = 'true';
35
35
  };
36
36
  const targetRef = useRef(null);
37
- const dialogRef = useRef();
37
+ const dialogRef = useRef(undefined);
38
38
  const targetRect = useRect(targetRef);
39
39
  useEffect(() => {
40
40
  const dialogNode = createDialog();
@@ -1,2 +1,2 @@
1
1
  import { type DependencyList, type RefObject } from 'react';
2
- export declare function useClickOutsideEffect(targetRef: RefObject<HTMLElement>, handler: () => void, deps?: DependencyList): void;
2
+ export declare function useClickOutsideEffect(targetRef: RefObject<HTMLElement> | RefObject<HTMLElement | undefined> | RefObject<HTMLElement | null>, handler: () => void, deps?: DependencyList): void;
@@ -44,5 +44,5 @@ type Options = {
44
44
  *
45
45
  * @returns The states created for this effect.
46
46
  */
47
- export declare function useDragEffect(targetRef: RefObject<HTMLElement>, { isEnabled, updatesCursor, onDragStart, onDragMove, onDragEnd, }: Options, deps?: DependencyList): void;
47
+ export declare function useDragEffect(targetRef: RefObject<HTMLElement> | RefObject<HTMLElement | undefined> | RefObject<HTMLElement | null>, { isEnabled, updatesCursor, onDragStart, onDragMove, onDragEnd, }: Options, deps?: DependencyList): void;
48
48
  export {};
@@ -13,8 +13,8 @@ import { Point } from 'spase';
13
13
  */
14
14
  export function useDragEffect(targetRef, { isEnabled = true, updatesCursor = true, onDragStart, onDragMove, onDragEnd, }, deps = []) {
15
15
  const element = targetRef.current;
16
- const startPositionRef = useRef();
17
- const dragPositionRef = useRef();
16
+ const startPositionRef = useRef(undefined);
17
+ const dragPositionRef = useRef(undefined);
18
18
  if (updatesCursor && element)
19
19
  element.style.cursor = 'grab';
20
20
  useEffect(() => {
@@ -52,5 +52,5 @@ type Options<T> = Omit<InteractDraggableOptions, 'onstart' | 'onmove' | 'onend'>
52
52
  *
53
53
  * @returns The states created for this effect.
54
54
  */
55
- export declare function useDragValueEffect<T = [number, number]>(targetRef: RefObject<HTMLElement>, { initialValue, transform, onDragStart, onDragMove, onDragEnd, ...options }: Options<T>, deps?: DependencyList): ReturnedStates<T>;
55
+ export declare function useDragValueEffect<T = [number, number]>(targetRef: RefObject<HTMLElement> | RefObject<HTMLElement | undefined> | RefObject<HTMLElement | null>, { initialValue, transform, onDragStart, onDragMove, onDragEnd, ...options }: Options<T>, deps?: DependencyList): ReturnedStates<T>;
56
56
  export {};
@@ -7,7 +7,7 @@ type Options = {
7
7
  shouldInvokeInitially?: boolean;
8
8
  };
9
9
  /**
10
- * Hoook for invoking a method repeatedly on every set interval.
10
+ * Hook for invoking a method repeatedly on every set interval.
11
11
  *
12
12
  * @param handler The method to invoke on every interval.
13
13
  * @param interval Time (in milliseconds) between each invocation.
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
  /**
3
- * Hoook for invoking a method repeatedly on every set interval.
3
+ * Hook for invoking a method repeatedly on every set interval.
4
4
  *
5
5
  * @param handler The method to invoke on every interval.
6
6
  * @param interval Time (in milliseconds) between each invocation.
@@ -8,7 +8,7 @@ import { useEffect, useRef } from 'react';
8
8
  * @param deps Dependencies that trigger this effect.
9
9
  */
10
10
  export function useInterval(handler, interval, { shouldInvokeInitially = false } = {}, deps = []) {
11
- const handlerRef = useRef();
11
+ const handlerRef = useRef(undefined);
12
12
  useEffect(() => {
13
13
  handlerRef.current = handler;
14
14
  }, [handler]);
@@ -6,7 +6,7 @@ import { useEffect, useRef } from 'react';
6
6
  * @param options See {@link Options}.
7
7
  */
8
8
  export function usePrevious(value, { sanitizeDependency = t => t } = {}) {
9
- const ref = useRef();
9
+ const ref = useRef(undefined);
10
10
  useEffect(() => {
11
11
  ref.current = value;
12
12
  }, [sanitizeDependency(value)]);
@@ -8,4 +8,4 @@ import { Rect } from 'spase';
8
8
  *
9
9
  * @returns The most current {@link Rect} of the target element.
10
10
  */
11
- export declare function useRect(targetRef: RefObject<HTMLElement>): Rect;
11
+ export declare function useRect(targetRef: RefObject<HTMLElement> | RefObject<HTMLElement | undefined> | RefObject<HTMLElement | null>): Rect;
@@ -14,4 +14,4 @@ export type UseResizeEffectOptions = {
14
14
  * @param options See {@link Options}.
15
15
  * @param deps Additional dependencies.
16
16
  */
17
- export declare function useResizeEffect(targetRef: RefObject<HTMLElement>, { onResize }?: UseResizeEffectOptions, deps?: DependencyList): void;
17
+ export declare function useResizeEffect(targetRef: RefObject<HTMLElement> | RefObject<HTMLElement | undefined> | RefObject<HTMLElement | null>, { onResize }?: UseResizeEffectOptions, deps?: DependencyList): void;
@@ -23,7 +23,7 @@ export function useScrollPositionEffect({ onChange }, deps = []) {
23
23
  step,
24
24
  };
25
25
  };
26
- const prevInfo = useRef();
26
+ const prevInfo = useRef(undefined);
27
27
  useEffect(() => {
28
28
  window.addEventListener('scroll', handleScrollPositionChange);
29
29
  window.addEventListener('resize', handleScrollPositionChange);
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from 'react';
2
- import { useSearchParams } from 'react-router-dom';
2
+ import { useSearchParams } from 'react-router';
3
3
  /**
4
4
  * Hook for mapping a search param to a state. Whenever the value of the target
5
5
  * search param changes, the mapped state will change as well, and vice versa.
@@ -7,4 +7,4 @@ import { type Size } from 'spase';
7
7
  *
8
8
  * @returns The most current {@link Size} of the target element.
9
9
  */
10
- export declare function useSize(targetRef: RefObject<HTMLElement>): Size;
10
+ export declare function useSize(targetRef: RefObject<HTMLElement> | RefObject<HTMLElement | undefined> | RefObject<HTMLElement | null>): Size;
@@ -6,7 +6,7 @@ type Options = {
6
6
  type ReturnValue = {
7
7
  start: () => void;
8
8
  stop: () => void;
9
- ref: RefObject<NodeJS.Timeout | undefined>;
9
+ ref: RefObject<NodeJS.Timeout> | RefObject<NodeJS.Timeout | undefined> | RefObject<NodeJS.Timeout | null>;
10
10
  };
11
11
  export declare function useTimeout(timeout?: number, { autoStart, onTimeout }?: Options, deps?: DependencyList): ReturnValue;
12
12
  export {};
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useRef } from 'react';
2
2
  export function useTimeout(timeout = 0, { autoStart = true, onTimeout } = {}, deps = []) {
3
- const timeoutRef = useRef();
4
- const handlerRef = useRef();
3
+ const timeoutRef = useRef(undefined);
4
+ const handlerRef = useRef(undefined);
5
5
  const start = useCallback(() => {
6
6
  stop();
7
7
  if (timeout < 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "etudes",
3
- "version": "6.3.0",
3
+ "version": "7.0.1",
4
4
  "description": "A study of headless React components",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -35,48 +35,47 @@
35
35
  "utils"
36
36
  ],
37
37
  "devDependencies": {
38
- "@eslint/js": "^9.13.0",
38
+ "@commitlint/config-conventional": "^19.6.0",
39
+ "@eslint/js": "^9.17.0",
39
40
  "@semantic-release/changelog": "^6.0.3",
40
41
  "@semantic-release/git": "^10.0.1",
41
- "@stylistic/eslint-plugin": "^2.9.0",
42
- "@types/node": "^22.7.9",
43
- "@types/react": "^18.3.12",
44
- "@types/react-dom": "^18.3.1",
45
- "@vitejs/plugin-react": "^4.3.3",
46
- "@vitest/coverage-v8": "^2.1.3",
42
+ "@stylistic/eslint-plugin": "^2.12.1",
43
+ "@types/node": "^22.10.2",
44
+ "@types/react": "^19.0.2",
45
+ "@types/react-dom": "^19.0.2",
46
+ "@vitejs/plugin-react": "^4.3.4",
47
+ "@vitest/coverage-v8": "^2.1.8",
47
48
  "autoprefixer": "^10.4.20",
48
- "concurrently": "^9.0.1",
49
- "eslint": "^9.13.0",
49
+ "concurrently": "^9.1.0",
50
+ "eslint": "^9.17.0",
50
51
  "eslint-plugin-tailwindcss": "^3.17.5",
51
- "postcss": "^8.4.47",
52
- "react": "^18.3.1",
53
- "react-dom": "^18.3.1",
54
- "react-router": "^6.27.0",
55
- "react-router-dom": "^6.27.0",
52
+ "postcss": "^8.4.49",
53
+ "react": "^19.0.0",
54
+ "react-dom": "^19.0.0",
55
+ "react-router": "^7.0.2",
56
56
  "rimraf": "^6.0.1",
57
- "semantic-release": "^24.1.3",
58
- "tailwindcss": "^3.4.14",
57
+ "semantic-release": "^24.2.0",
58
+ "tailwindcss": "^3.4.17",
59
59
  "tailwindcss-safe-area": "^0.6.0",
60
- "typescript": "^5.6.3",
61
- "typescript-eslint": "^8.11.0",
62
- "vite": "^5.4.10",
63
- "vitest": "^2.1.3",
60
+ "typescript": "^5.7.2",
61
+ "typescript-eslint": "^8.18.1",
62
+ "vite": "^6.0.4",
63
+ "vitest": "^2.1.8",
64
64
  "wait-on": "^8.0.1"
65
65
  },
66
66
  "dependencies": {
67
67
  "clsx": "^2.1.1",
68
68
  "fast-deep-equal": "^3.1.3",
69
- "fast-xml-parser": "^4.5.0",
69
+ "fast-xml-parser": "^4.5.1",
70
70
  "interactjs": "^1.10.27",
71
71
  "resize-observer-polyfill": "^1.5.1",
72
- "spase": "^9.0.1"
72
+ "spase": "^9.1.0"
73
73
  },
74
74
  "peerDependencies": {
75
- "react": "^18.2.0"
75
+ "react": "^19.0.0"
76
76
  },
77
77
  "optionalDependencies": {
78
- "react-router": "^6.27.0",
79
- "react-router-dom": "^6.27.0"
78
+ "react-router": "^7.0.2"
80
79
  },
81
- "packageManager": "pnpm@9.7.0+sha512.dc09430156b427f5ecfc79888899e1c39d2d690f004be70e05230b72cb173d96839587545d09429b55ac3c429c801b4dc3c0e002f653830a420fa2dd4e3cf9cf"
80
+ "packageManager": "pnpm@9.15.0"
82
81
  }
@@ -11,5 +11,5 @@ type ScrollPositionContextValue = ScrollPosition & {
11
11
  type ScrollPositionProviderProps = PropsWithChildren;
12
12
  export declare const ScrollPositionContext: import("react").Context<ScrollPositionContextValue | undefined>;
13
13
  export declare function ScrollPositionProvider({ children, }: Readonly<ScrollPositionProviderProps>): import("react/jsx-runtime").JSX.Element;
14
- export declare function useScrollPosition(targetRef?: RefObject<Element>): ScrollPosition;
14
+ export declare function useScrollPosition(targetRef?: RefObject<Element> | RefObject<Element | undefined> | RefObject<Element | null>): ScrollPosition;
15
15
  export {};
@@ -1,4 +1,4 @@
1
- import { type JSXElementConstructor, type ReactNode } from 'react';
1
+ import { type JSX, type JSXElementConstructor, type ReactNode } from 'react';
2
2
  type ComponentTypeDict = Record<string, JSXElementConstructor<any>>;
3
3
  type ComponentElementDict<T extends ComponentTypeDict> = Record<keyof T, JSX.Element>;
4
4
  export declare function asComponentDict<T extends ComponentTypeDict>(children?: ReactNode, typeDict?: T): Partial<ComponentElementDict<T>>;
@@ -1,4 +1,4 @@
1
- import { type Attributes, type CElement, type ClassAttributes, type Component, type ComponentState, type FunctionComponentElement, type ReactElement, type ReactNode } from 'react';
1
+ import { type Attributes, type ClassAttributes, type Component, type ComponentState, type ReactElement, type ReactNode } from 'react';
2
2
  /**
3
3
  * Wrapper for {@link cloneElement} but instead of overwriting `className` and
4
4
  * `style` of the cloned element with the values specified in the `props`
@@ -12,6 +12,6 @@ import { type Attributes, type CElement, type ClassAttributes, type Component, t
12
12
  *
13
13
  * @returns The cloned element.
14
14
  */
15
- export declare function cloneStyledElement<P>(element: FunctionComponentElement<P>, props?: Partial<P> & Attributes, ...children: ReactNode[]): FunctionComponentElement<P>;
16
- export declare function cloneStyledElement<P, T extends Component<P, ComponentState>>(element: CElement<P, T>, props?: Partial<P> & ClassAttributes<T>, ...children: ReactNode[]): CElement<P, T>;
15
+ export declare function cloneStyledElement<P>(element: ReactElement<P, React.FunctionComponent<P>>, props?: Partial<P> & Attributes, ...children: ReactNode[]): ReactElement<P, React.FunctionComponent<P>>;
16
+ export declare function cloneStyledElement<P, T extends Component<P, ComponentState>>(element: ReactElement<P, React.ComponentClass<T>>, props?: Partial<P> & ClassAttributes<T>, ...children: ReactNode[]): ReactElement<P, React.ComponentClass<T>>;
17
17
  export declare function cloneStyledElement<P>(element: ReactElement<P>, props?: Partial<P> & Attributes, ...children: ReactNode[]): ReactElement<P>;