@ledgerhq/lumen-utils-shared 0.1.1 → 0.1.2

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.
Files changed (48) hide show
  1. package/dist/index.d.ts +3 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +164 -130
  4. package/dist/lib/a11y/getButtonA11yProps.d.ts +19 -0
  5. package/dist/lib/a11y/getButtonA11yProps.d.ts.map +1 -0
  6. package/dist/lib/a11y/index.d.ts +2 -0
  7. package/dist/lib/a11y/index.d.ts.map +1 -0
  8. package/dist/lib/components/amountDisplayHelpers/buildAriaLabel.d.ts.map +1 -1
  9. package/dist/lib/components/amountDisplayHelpers/types.d.ts +1 -1
  10. package/dist/lib/components/amountDisplayHelpers/types.d.ts.map +1 -1
  11. package/dist/lib/components/amountDisplayHelpers/useSplitText.d.ts.map +1 -1
  12. package/dist/lib/components/getStepperCalculations/getStepperCalculations.d.ts.map +1 -1
  13. package/dist/lib/context/createSafeContext.d.ts.map +1 -0
  14. package/dist/lib/context/disabledContext.d.ts +14 -0
  15. package/dist/lib/context/disabledContext.d.ts.map +1 -0
  16. package/dist/lib/context/index.d.ts +3 -0
  17. package/dist/lib/context/index.d.ts.map +1 -0
  18. package/dist/lib/getObjectPath/getObjectPath.d.ts.map +1 -1
  19. package/dist/lib/isTextChildren/isTextChildren.d.ts.map +1 -1
  20. package/dist/lib/shallowEqual/index.d.ts +2 -0
  21. package/dist/lib/shallowEqual/index.d.ts.map +1 -0
  22. package/dist/lib/shallowEqual/shallowEqual.d.ts +8 -0
  23. package/dist/lib/shallowEqual/shallowEqual.d.ts.map +1 -0
  24. package/package.json +1 -1
  25. package/src/index.ts +3 -1
  26. package/src/lib/a11y/getButtonA11yProps.test.ts +71 -0
  27. package/src/lib/a11y/getButtonA11yProps.ts +59 -0
  28. package/src/lib/a11y/index.ts +1 -0
  29. package/src/lib/components/amountDisplayHelpers/types.ts +1 -1
  30. package/src/lib/{createSafeContext → context}/createSafeContext.tsx +10 -9
  31. package/src/lib/context/disabledContext.test.tsx +81 -0
  32. package/src/lib/context/disabledContext.tsx +27 -0
  33. package/src/lib/{createSafeContext → context}/index.ts +1 -0
  34. package/src/lib/shallowEqual/index.ts +1 -0
  35. package/src/lib/shallowEqual/shallowEqual.test.ts +48 -0
  36. package/src/lib/shallowEqual/shallowEqual.ts +23 -0
  37. package/dist/lib/createSafeContext/createSafeContext.d.ts.map +0 -1
  38. package/dist/lib/createSafeContext/index.d.ts +0 -2
  39. package/dist/lib/createSafeContext/index.d.ts.map +0 -1
  40. package/dist/lib/getStepperCalculations/getStepperCalculations.d.ts +0 -61
  41. package/dist/lib/getStepperCalculations/getStepperCalculations.d.ts.map +0 -1
  42. package/dist/lib/getStepperCalculations/index.d.ts +0 -2
  43. package/dist/lib/getStepperCalculations/index.d.ts.map +0 -1
  44. package/src/lib/getStepperCalculations/getStepperCalculations.test.ts +0 -139
  45. package/src/lib/getStepperCalculations/getStepperCalculations.ts +0 -103
  46. package/src/lib/getStepperCalculations/index.ts +0 -5
  47. /package/dist/lib/{createSafeContext → context}/createSafeContext.d.ts +0 -0
  48. /package/src/lib/{createSafeContext → context}/createSafeContext.test.tsx +0 -0
@@ -0,0 +1,59 @@
1
+ import type {
2
+ AriaAttributes,
3
+ KeyboardEventHandler,
4
+ MouseEventHandler,
5
+ MouseEvent,
6
+ HTMLAttributes,
7
+ } from 'react';
8
+
9
+ type ButtonA11yOptions<T extends HTMLElement = HTMLElement> = {
10
+ onClick?: MouseEventHandler<T>;
11
+ disabled?: boolean;
12
+ };
13
+
14
+ type ButtonA11yProps<T extends HTMLElement = HTMLElement> = Pick<
15
+ HTMLAttributes<T>,
16
+ 'role' | 'tabIndex' | 'onKeyDown' | 'onClick'
17
+ > &
18
+ Pick<AriaAttributes, 'aria-disabled'>;
19
+
20
+ /**
21
+ * Returns props to make a non-button element (e.g. `<div>`) behave like a
22
+ * button: `role="button"`, `tabIndex`, keyboard activation on Enter/Space,
23
+ * and the original `onClick` passthrough.
24
+ *
25
+ * Returns undefined if `onClick` is not provided
26
+ *
27
+ * When `disabled` is `true`, the element gets `aria-disabled`, is removed
28
+ * from the tab order, and click/keyboard handlers are omitted.
29
+ */
30
+ export const getButtonA11yProps = <T extends HTMLElement = HTMLElement>({
31
+ onClick,
32
+ disabled,
33
+ }: ButtonA11yOptions<T>): ButtonA11yProps<T> | undefined => {
34
+ if (!onClick) {
35
+ return undefined;
36
+ }
37
+
38
+ if (disabled) {
39
+ return {
40
+ role: 'button',
41
+ tabIndex: -1,
42
+ 'aria-disabled': true,
43
+ };
44
+ }
45
+
46
+ const onKeyDown: KeyboardEventHandler<T> = (e) => {
47
+ if (e.key === 'Enter' || e.key === ' ') {
48
+ e.preventDefault();
49
+ onClick?.(e as unknown as MouseEvent<T>);
50
+ }
51
+ };
52
+
53
+ return {
54
+ role: 'button',
55
+ tabIndex: 0,
56
+ onKeyDown,
57
+ onClick,
58
+ };
59
+ };
@@ -0,0 +1 @@
1
+ export { getButtonA11yProps } from './getButtonA11yProps';
@@ -26,5 +26,5 @@ export type FormattedValue = {
26
26
  * @optional
27
27
  * @default 'start'
28
28
  */
29
- currencyPosition?: 'start' | 'end';
29
+ currencyPosition: 'start' | 'end';
30
30
  };
@@ -1,4 +1,5 @@
1
- import { createContext, FC, ReactNode, useContext, useMemo } from 'react';
1
+ import { createContext, FC, ReactNode, useContext, useRef } from 'react';
2
+ import { shallowEqual } from '../shallowEqual';
2
3
 
3
4
  export function createSafeContext<ContextValue extends object>(
4
5
  rootComponentName: string,
@@ -9,14 +10,14 @@ export function createSafeContext<ContextValue extends object>(
9
10
  const Provider: FC<{
10
11
  children: ReactNode;
11
12
  value: ContextValue;
12
- }> = ({ children, value: context }) => {
13
- // Only re-memoize when prop values change
14
- const memoValue = useMemo(
15
- () => context,
16
- Object.values(context ?? {}),
17
- ) as ContextValue;
18
-
19
- return <Context.Provider value={memoValue}>{children}</Context.Provider>;
13
+ }> = ({ children, value }) => {
14
+ const ref = useRef(value);
15
+
16
+ if (!shallowEqual(ref.current, value)) {
17
+ ref.current = value;
18
+ }
19
+
20
+ return <Context.Provider value={ref.current}>{children}</Context.Provider>;
20
21
  };
21
22
 
22
23
  Provider.displayName = rootComponentName + 'Provider';
@@ -0,0 +1,81 @@
1
+ import { cleanup, render } from '@testing-library/react';
2
+ import React from 'react';
3
+ import { DisabledProvider, useDisabledContext } from './disabledContext';
4
+
5
+ afterEach(() => {
6
+ cleanup();
7
+ });
8
+
9
+ const Consumer = ({ mergeWith }: { mergeWith?: { disabled?: boolean } }) => {
10
+ const disabled = useDisabledContext({
11
+ consumerName: 'TestConsumer',
12
+ mergeWith,
13
+ });
14
+ return React.createElement(
15
+ 'span',
16
+ { 'data-testid': 'disabled' },
17
+ String(disabled),
18
+ );
19
+ };
20
+
21
+ describe('disabledContext', () => {
22
+ it('returns false by default when no provider is present', () => {
23
+ const { getByTestId } = render(React.createElement(Consumer));
24
+ expect(getByTestId('disabled').textContent).toBe('false');
25
+ });
26
+
27
+ it('returns true when provider sets disabled to true', () => {
28
+ const { getByTestId } = render(
29
+ React.createElement(
30
+ DisabledProvider,
31
+ { value: { disabled: true }, children: '' },
32
+ React.createElement(Consumer),
33
+ ),
34
+ );
35
+ expect(getByTestId('disabled').textContent).toBe('true');
36
+ });
37
+
38
+ it('returns false when provider sets disabled to false', () => {
39
+ const { getByTestId } = render(
40
+ React.createElement(
41
+ DisabledProvider,
42
+ { value: { disabled: false }, children: '' },
43
+ React.createElement(Consumer),
44
+ ),
45
+ );
46
+ expect(getByTestId('disabled').textContent).toBe('false');
47
+ });
48
+
49
+ it('mergeWith overrides context to disabled when mergeWith.disabled is true', () => {
50
+ const { getByTestId } = render(
51
+ React.createElement(
52
+ DisabledProvider,
53
+ { value: { disabled: false }, children: '' },
54
+ React.createElement(Consumer, { mergeWith: { disabled: true } }),
55
+ ),
56
+ );
57
+ expect(getByTestId('disabled').textContent).toBe('true');
58
+ });
59
+
60
+ it('returns context disabled when mergeWith.disabled is false', () => {
61
+ const { getByTestId } = render(
62
+ React.createElement(
63
+ DisabledProvider,
64
+ { value: { disabled: true }, children: '' },
65
+ React.createElement(Consumer, { mergeWith: { disabled: false } }),
66
+ ),
67
+ );
68
+ expect(getByTestId('disabled').textContent).toBe('true');
69
+ });
70
+
71
+ it('returns false when both context and mergeWith are false', () => {
72
+ const { getByTestId } = render(
73
+ React.createElement(
74
+ DisabledProvider,
75
+ { value: { disabled: false }, children: '' },
76
+ React.createElement(Consumer, { mergeWith: { disabled: false } }),
77
+ ),
78
+ );
79
+ expect(getByTestId('disabled').textContent).toBe('false');
80
+ });
81
+ });
@@ -0,0 +1,27 @@
1
+ import { createSafeContext } from './createSafeContext';
2
+
3
+ type DisabledContextValue = {
4
+ disabled?: boolean;
5
+ };
6
+
7
+ const [DisabledProvider, _useDisabledContext] =
8
+ createSafeContext<DisabledContextValue>('Disabled', { disabled: false });
9
+
10
+ const useDisabledContext = ({
11
+ consumerName,
12
+ contextRequired,
13
+ mergeWith,
14
+ }: {
15
+ consumerName: string;
16
+ contextRequired?: boolean;
17
+ mergeWith?: DisabledContextValue;
18
+ }) => {
19
+ const disabledContext = _useDisabledContext({
20
+ consumerName: consumerName,
21
+ contextRequired: contextRequired ?? false,
22
+ });
23
+
24
+ return Boolean(mergeWith?.disabled || disabledContext.disabled);
25
+ };
26
+
27
+ export { DisabledProvider, useDisabledContext };
@@ -1 +1,2 @@
1
1
  export * from './createSafeContext';
2
+ export * from './disabledContext';
@@ -0,0 +1 @@
1
+ export * from './shallowEqual';
@@ -0,0 +1,48 @@
1
+ import { shallowEqual } from './shallowEqual';
2
+
3
+ describe('shallowEqual', () => {
4
+ it('returns true for the same reference', () => {
5
+ const obj = { a: 1 };
6
+ expect(shallowEqual(obj, obj)).toBe(true);
7
+ });
8
+
9
+ it('returns true for objects with identical primitive values', () => {
10
+ expect(shallowEqual({ a: 1, b: 'x' }, { a: 1, b: 'x' })).toBe(true);
11
+ });
12
+
13
+ it('returns false when values differ', () => {
14
+ expect(shallowEqual({ a: 1 }, { a: 2 })).toBe(false);
15
+ });
16
+
17
+ it('returns false when key counts differ', () => {
18
+ expect(
19
+ shallowEqual({ a: 1 } as Record<string, unknown>, { a: 1, b: 2 }),
20
+ ).toBe(false);
21
+ });
22
+
23
+ it('returns false when keys differ', () => {
24
+ expect(
25
+ shallowEqual(
26
+ { a: 1 } as Record<string, unknown>,
27
+ { b: 1 } as Record<string, unknown>,
28
+ ),
29
+ ).toBe(false);
30
+ });
31
+
32
+ it('returns true when both objects are empty', () => {
33
+ expect(shallowEqual({}, {})).toBe(true);
34
+ });
35
+
36
+ it('does not perform deep comparison on nested objects', () => {
37
+ const inner = { nested: true };
38
+ expect(shallowEqual({ a: inner }, { a: inner })).toBe(true);
39
+ expect(shallowEqual({ a: { nested: true } }, { a: { nested: true } })).toBe(
40
+ false,
41
+ );
42
+ });
43
+
44
+ it('uses Object.is semantics (NaN === NaN, +0 !== -0)', () => {
45
+ expect(shallowEqual({ a: NaN }, { a: NaN })).toBe(true);
46
+ expect(shallowEqual({ a: +0 }, { a: -0 })).toBe(false);
47
+ });
48
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Performs a shallow equality comparison between two objects.
3
+ * Returns true if both objects have the same keys and each value
4
+ * is strictly equal (using Object.is) to the corresponding value
5
+ * in the other object.
6
+ */
7
+ export function shallowEqual<T extends object>(a: T, b: T): boolean {
8
+ if (Object.is(a, b)) return true;
9
+
10
+ const objA = a as Record<string, unknown>;
11
+ const objB = b as Record<string, unknown>;
12
+
13
+ const keysA = Object.keys(objA);
14
+ const keysB = Object.keys(objB);
15
+
16
+ if (keysA.length !== keysB.length) return false;
17
+
18
+ return keysA.every(
19
+ (key) =>
20
+ Object.prototype.hasOwnProperty.call(objB, key) &&
21
+ Object.is(objA[key], objB[key]),
22
+ );
23
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"createSafeContext.d.ts","sourceRoot":"","sources":["../../../src/lib/createSafeContext/createSafeContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,EAAE,EAAE,SAAS,EAAuB,MAAM,OAAO,CAAC;AAE1E,wBAAgB,iBAAiB,CAAC,YAAY,SAAS,MAAM,EAC3D,iBAAiB,EAAE,MAAM,EACzB,cAAc,CAAC,EAAE,YAAY;cAKjB,SAAS;WACZ,YAAY;KAaG,eAAe,SAAS,OAAO,gDAGpD;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,eAAe,CAAC;CAClC,KAAG,eAAe,SAAS,IAAI,GAAG,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,EAgBxE"}
@@ -1,2 +0,0 @@
1
- export * from './createSafeContext';
2
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/createSafeContext/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC"}
@@ -1,61 +0,0 @@
1
- export type StepperCalculationsInput = {
2
- /** Current step number (1-based). Use 0 or negative for minimal dot. */
3
- currentStep: number;
4
- /** Total number of steps. */
5
- totalSteps: number;
6
- /** Size of the circular stepper in pixels. */
7
- size: number;
8
- /** Optional custom label. Defaults to "{currentStep}/{totalSteps}". */
9
- label?: string;
10
- /** Stroke width in pixels. @default 4 */
11
- strokeWidth?: number;
12
- /** Percentage of full circle to show as arc (0–1). @default 0.75 (270°) */
13
- arcPercentage?: number;
14
- };
15
- export type StepperCalculationsOutput = {
16
- /** Label text displayed in the center. */
17
- displayLabel: string;
18
- /** Progress value between 0 and 1. */
19
- progress: number;
20
- /** Circle radius in pixels. */
21
- r: number;
22
- /** Center x coordinate. */
23
- cx: number;
24
- /** Center y coordinate. */
25
- cy: number;
26
- /** Full circle circumference. */
27
- circumference: number;
28
- /** Length of the visible arc (track). */
29
- trackArcLength: number;
30
- /** strokeDasharray for the gray track circle. */
31
- trackDashArray: string;
32
- /** strokeDasharray for the progress (purple) circle. */
33
- progressDashArray: string;
34
- /** strokeDashoffset for the progress (purple) circle. */
35
- progressDashOffset: number;
36
- /** Whether to show minimal dot instead of progress arc. */
37
- showMinimalDot: boolean;
38
- };
39
- /**
40
- * Computes all SVG-related values needed to render a circular stepper.
41
- *
42
- * This is a pure utility shared between React (web) and React Native
43
- * implementations of the Stepper component.
44
- *
45
- * @example
46
- * ```ts
47
- * const calcs = getStepperCalculations({
48
- * currentStep: 2,
49
- * totalSteps: 4,
50
- * size: 48,
51
- * });
52
- *
53
- * // calcs.displayLabel → '2/4'
54
- * // calcs.progress → 0.5
55
- * // calcs.trackDashArray → '<trackArcLength> <circumference>'
56
- * // calcs.progressDashArray → '<trackArcLength> <circumference>'
57
- * // calcs.progressDashOffset → offset for 50% fill
58
- * ```
59
- */
60
- export declare const getStepperCalculations: ({ currentStep, totalSteps, size, label, strokeWidth, arcPercentage, }: StepperCalculationsInput) => StepperCalculationsOutput;
61
- //# sourceMappingURL=getStepperCalculations.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"getStepperCalculations.d.ts","sourceRoot":"","sources":["../../../src/lib/getStepperCalculations/getStepperCalculations.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,wBAAwB,GAAG;IACrC,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAC;IACpB,6BAA6B;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,0CAA0C;IAC1C,YAAY,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,+BAA+B;IAC/B,CAAC,EAAE,MAAM,CAAC;IACV,2BAA2B;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,2BAA2B;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,iCAAiC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,cAAc,EAAE,MAAM,CAAC;IACvB,wDAAwD;IACxD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,2DAA2D;IAC3D,cAAc,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,sBAAsB,0EAOhC,wBAAwB,KAAG,yBAkC7B,CAAC"}
@@ -1,2 +0,0 @@
1
- export { getStepperCalculations, type StepperCalculationsInput, type StepperCalculationsOutput, } from './getStepperCalculations';
2
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/getStepperCalculations/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,0BAA0B,CAAC"}
@@ -1,139 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { getStepperCalculations } from './getStepperCalculations';
3
-
4
- describe('getStepperCalculations', () => {
5
- const defaultInput = {
6
- currentStep: 2,
7
- totalSteps: 4,
8
- size: 48,
9
- };
10
-
11
- it('should compute display label from currentStep and totalSteps', () => {
12
- const result = getStepperCalculations(defaultInput);
13
- expect(result.displayLabel).toBe('2/4');
14
- });
15
-
16
- it('should use custom label when provided', () => {
17
- const result = getStepperCalculations({
18
- ...defaultInput,
19
- label: 'Step 2 of 4',
20
- });
21
- expect(result.displayLabel).toBe('Step 2 of 4');
22
- });
23
-
24
- it('should clamp display label to totalSteps', () => {
25
- const result = getStepperCalculations({
26
- ...defaultInput,
27
- currentStep: 10,
28
- });
29
- expect(result.displayLabel).toBe('4/4');
30
- });
31
-
32
- it('should compute progress as currentStep / totalSteps', () => {
33
- const result = getStepperCalculations(defaultInput);
34
- expect(result.progress).toBe(0.5);
35
- });
36
-
37
- it('should clamp progress between 0 and 1', () => {
38
- const negativeResult = getStepperCalculations({
39
- ...defaultInput,
40
- currentStep: -1,
41
- });
42
- expect(negativeResult.progress).toBe(0);
43
-
44
- const overflowResult = getStepperCalculations({
45
- ...defaultInput,
46
- currentStep: 10,
47
- });
48
- expect(overflowResult.progress).toBe(1);
49
- });
50
-
51
- it('should handle zero totalSteps gracefully', () => {
52
- const result = getStepperCalculations({
53
- ...defaultInput,
54
- totalSteps: 0,
55
- });
56
- expect(result.progress).toBe(0);
57
- });
58
-
59
- it('should compute correct SVG geometry for default size', () => {
60
- const result = getStepperCalculations(defaultInput);
61
- expect(result.cx).toBe(24);
62
- expect(result.cy).toBe(24);
63
- expect(result.r).toBe(22); // (48 - 4) / 2
64
- });
65
-
66
- it('should respect custom strokeWidth', () => {
67
- const result = getStepperCalculations({
68
- ...defaultInput,
69
- strokeWidth: 6,
70
- });
71
- expect(result.r).toBe(21); // (48 - 6) / 2
72
- });
73
-
74
- it('should compute trackArcLength as 75% of circumference by default', () => {
75
- const result = getStepperCalculations(defaultInput);
76
- const expectedCircumference = 2 * Math.PI * 22;
77
- expect(result.circumference).toBeCloseTo(expectedCircumference);
78
- expect(result.trackArcLength).toBeCloseTo(expectedCircumference * 0.75);
79
- });
80
-
81
- it('should respect custom arcPercentage', () => {
82
- const result = getStepperCalculations({
83
- ...defaultInput,
84
- arcPercentage: 0.5,
85
- });
86
- const expectedCircumference = 2 * Math.PI * 22;
87
- expect(result.trackArcLength).toBeCloseTo(expectedCircumference * 0.5);
88
- });
89
-
90
- it('should show minimal dot when currentStep is 0', () => {
91
- const result = getStepperCalculations({
92
- ...defaultInput,
93
- currentStep: 0,
94
- });
95
- expect(result.showMinimalDot).toBe(true);
96
- expect(result.progressDashOffset).toBeCloseTo(result.trackArcLength - 2);
97
- });
98
-
99
- it('should show minimal dot when currentStep is negative', () => {
100
- const result = getStepperCalculations({
101
- ...defaultInput,
102
- currentStep: -1,
103
- });
104
- expect(result.showMinimalDot).toBe(true);
105
- expect(result.progressDashOffset).toBeCloseTo(result.trackArcLength - 2);
106
- });
107
-
108
- it('should show normal progress arc when currentStep > 0', () => {
109
- const result = getStepperCalculations(defaultInput);
110
- expect(result.showMinimalDot).toBe(false);
111
- expect(result.progressDashArray).toContain(String(result.trackArcLength));
112
- });
113
-
114
- it('should compute progressDashOffset as 0 when progress is 100%', () => {
115
- const result = getStepperCalculations({
116
- ...defaultInput,
117
- currentStep: 4,
118
- totalSteps: 4,
119
- });
120
- expect(result.progressDashOffset).toBeCloseTo(0);
121
- });
122
-
123
- it('should compute progressDashOffset equal to trackArcLength when progress is 0%', () => {
124
- const result = getStepperCalculations({
125
- ...defaultInput,
126
- currentStep: 0,
127
- totalSteps: 4,
128
- });
129
- // When showMinimalDot, offset = trackArcLength - 2
130
- expect(result.progressDashOffset).toBeCloseTo(result.trackArcLength - 2);
131
- });
132
-
133
- it('should compute trackDashArray consistently', () => {
134
- const result = getStepperCalculations(defaultInput);
135
- expect(result.trackDashArray).toBe(
136
- `${result.trackArcLength} ${result.circumference}`,
137
- );
138
- });
139
- });
@@ -1,103 +0,0 @@
1
- export type StepperCalculationsInput = {
2
- /** Current step number (1-based). Use 0 or negative for minimal dot. */
3
- currentStep: number;
4
- /** Total number of steps. */
5
- totalSteps: number;
6
- /** Size of the circular stepper in pixels. */
7
- size: number;
8
- /** Optional custom label. Defaults to "{currentStep}/{totalSteps}". */
9
- label?: string;
10
- /** Stroke width in pixels. @default 4 */
11
- strokeWidth?: number;
12
- /** Percentage of full circle to show as arc (0–1). @default 0.75 (270°) */
13
- arcPercentage?: number;
14
- };
15
-
16
- export type StepperCalculationsOutput = {
17
- /** Label text displayed in the center. */
18
- displayLabel: string;
19
- /** Progress value between 0 and 1. */
20
- progress: number;
21
- /** Circle radius in pixels. */
22
- r: number;
23
- /** Center x coordinate. */
24
- cx: number;
25
- /** Center y coordinate. */
26
- cy: number;
27
- /** Full circle circumference. */
28
- circumference: number;
29
- /** Length of the visible arc (track). */
30
- trackArcLength: number;
31
- /** strokeDasharray for the gray track circle. */
32
- trackDashArray: string;
33
- /** strokeDasharray for the progress (purple) circle. */
34
- progressDashArray: string;
35
- /** strokeDashoffset for the progress (purple) circle. */
36
- progressDashOffset: number;
37
- /** Whether to show minimal dot instead of progress arc. */
38
- showMinimalDot: boolean;
39
- };
40
-
41
- /**
42
- * Computes all SVG-related values needed to render a circular stepper.
43
- *
44
- * This is a pure utility shared between React (web) and React Native
45
- * implementations of the Stepper component.
46
- *
47
- * @example
48
- * ```ts
49
- * const calcs = getStepperCalculations({
50
- * currentStep: 2,
51
- * totalSteps: 4,
52
- * size: 48,
53
- * });
54
- *
55
- * // calcs.displayLabel → '2/4'
56
- * // calcs.progress → 0.5
57
- * // calcs.trackDashArray → '<trackArcLength> <circumference>'
58
- * // calcs.progressDashArray → '<trackArcLength> <circumference>'
59
- * // calcs.progressDashOffset → offset for 50% fill
60
- * ```
61
- */
62
- export const getStepperCalculations = ({
63
- currentStep,
64
- totalSteps,
65
- size,
66
- label,
67
- strokeWidth = 4,
68
- arcPercentage = 0.75,
69
- }: StepperCalculationsInput): StepperCalculationsOutput => {
70
- const displayLabel =
71
- label ?? `${Math.min(currentStep, totalSteps)}/${totalSteps}`;
72
- const progress =
73
- totalSteps <= 0 ? 0 : Math.min(1, Math.max(0, currentStep / totalSteps));
74
-
75
- // SVG circle geometry
76
- const r = (size - strokeWidth) / 2;
77
- const cx = size / 2;
78
- const cy = size / 2;
79
- const circumference = 2 * Math.PI * r;
80
-
81
- // Arc calculations
82
- const trackArcLength = circumference * arcPercentage;
83
- const dashOffset = trackArcLength * (1 - progress);
84
-
85
- // Minimal dot handling (currentStep <= 0 means "not started")
86
- const showMinimalDot = currentStep <= 0;
87
- const progressDashArray = `${trackArcLength} ${circumference}`;
88
- const progressDashOffset = showMinimalDot ? trackArcLength - 2 : dashOffset;
89
-
90
- return {
91
- displayLabel,
92
- progress,
93
- r,
94
- cx,
95
- cy,
96
- circumference,
97
- trackArcLength,
98
- trackDashArray: `${trackArcLength} ${circumference}`,
99
- progressDashArray,
100
- progressDashOffset,
101
- showMinimalDot,
102
- };
103
- };
@@ -1,5 +0,0 @@
1
- export {
2
- getStepperCalculations,
3
- type StepperCalculationsInput,
4
- type StepperCalculationsOutput,
5
- } from './getStepperCalculations';