@jobber/hooks 2.15.1-export-use-69f7bac.88 → 2.17.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/README.md CHANGED
@@ -14,6 +14,7 @@ Shared hooks for components in Atlantis.
14
14
  - [useOnKeyDown](../?path=/docs/hooks-useonkeydown--docs)
15
15
  - [useRefocusOnActivator](../?path=/docs/hooks-userefocusonactivator--docs)
16
16
  - [useResizeObserver](../?path=/docs/hooks-useresizeobserver--docs)
17
+ - [useStepper](../?path=/docs/hooks-usestepper--docs)
17
18
  - [useWindowDimensions](../?path=/docs/hooks-usewindowdimensions--docs)
18
19
 
19
20
  ## Installing
package/dist/index.d.ts CHANGED
@@ -14,4 +14,5 @@ export * from "./useRefocusOnActivator";
14
14
  export * from "./useResizeObserver";
15
15
  export * from "./useSafeLayoutEffect";
16
16
  export * from "./useShowClear";
17
+ export * from "./useStepper";
17
18
  export * from "./useWindowDimensions";
package/dist/index.js CHANGED
@@ -30,4 +30,5 @@ __exportStar(require("./useRefocusOnActivator"), exports);
30
30
  __exportStar(require("./useResizeObserver"), exports);
31
31
  __exportStar(require("./useSafeLayoutEffect"), exports);
32
32
  __exportStar(require("./useShowClear"), exports);
33
+ __exportStar(require("./useStepper"), exports);
33
34
  __exportStar(require("./useWindowDimensions"), exports);
@@ -1 +1 @@
1
- export * from "./useDebounce";
1
+ export { useDebounce } from "./useDebounce";
@@ -1,17 +1,5 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
- };
16
2
  Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("./useDebounce"), exports);
3
+ exports.useDebounce = void 0;
4
+ var useDebounce_1 = require("./useDebounce");
5
+ Object.defineProperty(exports, "useDebounce", { enumerable: true, get: function () { return useDebounce_1.useDebounce; } });
@@ -1,4 +1,4 @@
1
- import debounce from "lodash/debounce";
1
+ import { debounce } from "es-toolkit";
2
2
  type AnyFunction = (...args: any[]) => any;
3
3
  /**
4
4
  * A hook to easily manage debounced functions, including their cleanup.
@@ -7,5 +7,5 @@ type AnyFunction = (...args: any[]) => any;
7
7
  * @param options Optional debounce settings.
8
8
  * @returns The debounced function.
9
9
  */
10
- export declare function useDebounce<T extends AnyFunction>(func: T, wait: number, options?: Parameters<typeof debounce>[2]): ((...args: Parameters<T>) => any) & import("lodash").Cancelable;
10
+ export declare function useDebounce<T extends AnyFunction>(func: T, wait: number, options?: Parameters<typeof debounce>[2]): import("es-toolkit").DebouncedFunction<(...args: Parameters<T>) => any>;
11
11
  export {};
@@ -1,11 +1,9 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.useDebounce = useDebounce;
7
4
  const react_1 = require("react");
8
- const debounce_1 = __importDefault(require("lodash/debounce"));
5
+ const es_toolkit_1 = require("es-toolkit");
6
+ const useCallbackRef_1 = require("../useCallbackRef");
9
7
  /**
10
8
  * A hook to easily manage debounced functions, including their cleanup.
11
9
  * @param func The function to debounce.
@@ -14,21 +12,15 @@ const debounce_1 = __importDefault(require("lodash/debounce"));
14
12
  * @returns The debounced function.
15
13
  */
16
14
  function useDebounce(func, wait, options) {
17
- const funcRef = (0, react_1.useRef)(func);
18
- // We're keeping the provided func wrapped in a ref to avoid forcing the consumer to memoize it.
19
- // This is a defense against consumers who aren't memoizing their functions.. or if they are
20
- // memoized but invalidating too frequently due to possible bugs.
21
- if (funcRef.current !== func) {
22
- funcRef.current = func;
23
- }
15
+ const funcRef = (0, useCallbackRef_1.useCallbackRef)(func);
16
+ // Note: do not add additional dependencies! There is currently no use case where we would change
17
+ // the wait delay or options between renders. By not tracking as dependencies, this allows
18
+ // consumers of useDebounce to provide a raw object for options rather than forcing them to
19
+ // memoize that param. Same with the func they provide, we're using a ref to keep that up
20
+ // to date and to avoid forcing the consumer to memoize it.
24
21
  const debounced = (0, react_1.useMemo)(() => {
25
- return (0, debounce_1.default)((...args) => funcRef.current(...args), wait, options);
26
- // Note: do not add any dependencies! There is currently no use case where we would change
27
- // the wait delay or options between renders. By not tracking as dependencies, this allows
28
- // consumers of useDebounce to provide a raw object for options rather than forcing them to
29
- // memoize that param. Same with the func they provide, we're using a ref to keep that up
30
- // to date and to avoid forcing the consumer to memoize it.
31
- }, []);
22
+ return (0, es_toolkit_1.debounce)((...args) => funcRef(...args), wait, options);
23
+ }, [funcRef]);
32
24
  (0, react_1.useEffect)(() => {
33
25
  // If the debounced function is recreated (or unmounted), cancel the last call.
34
26
  return () => debounced.cancel();
@@ -31,9 +31,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
31
31
  step((generator = generator.apply(thisArg, _arguments || [])).next());
32
32
  });
33
33
  };
34
+ var __importDefault = (this && this.__importDefault) || function (mod) {
35
+ return (mod && mod.__esModule) ? mod : { "default": mod };
36
+ };
34
37
  Object.defineProperty(exports, "__esModule", { value: true });
35
38
  const react_1 = __importStar(require("react"));
36
39
  const react_2 = require("@testing-library/react");
40
+ const user_event_1 = __importDefault(require("@testing-library/user-event"));
37
41
  const useDebounce_1 = require("./useDebounce");
38
42
  const DEBOUNCE_WAIT = 300;
39
43
  describe("useDebounce", () => {
@@ -48,7 +52,6 @@ describe("useDebounce", () => {
48
52
  const { result } = (0, react_2.renderHook)(() => (0, useDebounce_1.useDebounce)(mockFn, DEBOUNCE_WAIT));
49
53
  result.current("test");
50
54
  expect(mockFn).not.toHaveBeenCalled();
51
- // Fast-forward time
52
55
  (0, react_2.act)(() => {
53
56
  jest.advanceTimersByTime(DEBOUNCE_WAIT);
54
57
  });
@@ -60,7 +63,6 @@ describe("useDebounce", () => {
60
63
  const { result, unmount } = (0, react_2.renderHook)(() => (0, useDebounce_1.useDebounce)(mockFn, DEBOUNCE_WAIT));
61
64
  result.current("test");
62
65
  unmount();
63
- // Fast-forward time
64
66
  (0, react_2.act)(() => {
65
67
  jest.advanceTimersByTime(DEBOUNCE_WAIT);
66
68
  });
@@ -70,18 +72,47 @@ describe("useDebounce", () => {
70
72
  const mockFn = jest.fn();
71
73
  const { result } = (0, react_2.renderHook)(() => (0, useDebounce_1.useDebounce)(mockFn, DEBOUNCE_WAIT));
72
74
  result.current("first");
73
- // Fast-forward half the debounce time
74
75
  (0, react_2.act)(() => {
75
76
  jest.advanceTimersByTime(DEBOUNCE_WAIT / 2);
76
77
  });
77
78
  result.current("second");
78
- // Fast-forward remaining debounce time
79
79
  (0, react_2.act)(() => {
80
80
  jest.advanceTimersByTime(DEBOUNCE_WAIT);
81
81
  });
82
82
  expect(mockFn).toHaveBeenCalledTimes(1);
83
83
  expect(mockFn).toHaveBeenCalledWith("second");
84
84
  });
85
+ it("should not recreate debounced function when options object reference changes", () => {
86
+ const mockFn = jest.fn();
87
+ const debounceEgdesOption = ["trailing"];
88
+ // Use a function that returns a new options object each time
89
+ const { result, rerender } = (0, react_2.renderHook)(({ options }) => (0, useDebounce_1.useDebounce)(mockFn, DEBOUNCE_WAIT, options), { initialProps: { options: { edges: debounceEgdesOption } } });
90
+ const debounceRef = result.current;
91
+ rerender({ options: { edges: debounceEgdesOption } });
92
+ expect(debounceRef).toBe(result.current);
93
+ });
94
+ it("should not recreate debounced function when options config changes", () => {
95
+ const mockFn = jest.fn();
96
+ // Largely arbitrary, this value x 2 must simply be less than the debounce wait
97
+ const TIME_INCREMENT_LESSER_THAN_DEBOUNCE_WAIT = 1;
98
+ // Start with trailing edge
99
+ const debounceEgdesOption = ["trailing"];
100
+ // Use a function that returns a new options object each time
101
+ const { result, rerender } = (0, react_2.renderHook)(({ options }) => (0, useDebounce_1.useDebounce)(mockFn, DEBOUNCE_WAIT, options), { initialProps: { options: { edges: debounceEgdesOption } } });
102
+ result.current("first");
103
+ (0, react_2.act)(() => {
104
+ jest.advanceTimersByTime(TIME_INCREMENT_LESSER_THAN_DEBOUNCE_WAIT);
105
+ });
106
+ expect(mockFn).not.toHaveBeenCalled();
107
+ // This means it calls immediately at the leading edge of the timeout.
108
+ rerender({ options: { edges: ["leading"] } });
109
+ result.current("second");
110
+ (0, react_2.act)(() => {
111
+ jest.advanceTimersByTime(TIME_INCREMENT_LESSER_THAN_DEBOUNCE_WAIT);
112
+ });
113
+ // The config change should be ignored, options are hardcoded
114
+ expect(mockFn).not.toHaveBeenCalled();
115
+ });
85
116
  it("should work with React components", () => __awaiter(void 0, void 0, void 0, function* () {
86
117
  function DebouncedComponent() {
87
118
  const [value, setValue] = (0, react_1.useState)("");
@@ -99,13 +130,61 @@ describe("useDebounce", () => {
99
130
  (0, react_2.render)(react_1.default.createElement(DebouncedComponent, null));
100
131
  const input = react_2.screen.getByTestId("input");
101
132
  const debouncedValue = react_2.screen.getByTestId("debounced-value");
102
- // Using fireEvent instead of userEvent
103
- react_2.fireEvent.change(input, { target: { value: "test" } });
133
+ const user = user_event_1.default.setup({ advanceTimers: jest.advanceTimersByTime });
134
+ yield user.type(input, "test");
104
135
  expect(debouncedValue.textContent).toBe("");
105
- // Fast-forward time
106
136
  (0, react_2.act)(() => {
107
137
  jest.advanceTimersByTime(DEBOUNCE_WAIT + 100);
108
138
  });
109
139
  expect(debouncedValue.textContent).toBe("test");
110
- }), 10000); // 10 second timeout
140
+ }), 10000);
141
+ it("should properly handle options object", () => __awaiter(void 0, void 0, void 0, function* () {
142
+ function DebouncedComponent() {
143
+ const [count, setCount] = (0, react_1.useState)(0);
144
+ const [debouncedCount, setDebouncedCount] = (0, react_1.useState)(0);
145
+ const options = {
146
+ edges: ["leading", "trailing"],
147
+ };
148
+ const debouncedSetCount = (0, useDebounce_1.useDebounce)((value) => {
149
+ setDebouncedCount(value);
150
+ }, DEBOUNCE_WAIT, options);
151
+ return (react_1.default.createElement("div", null,
152
+ react_1.default.createElement("button", { "data-testid": "increment", onClick: () => {
153
+ const newCount = count + 1;
154
+ setCount(newCount);
155
+ debouncedSetCount(newCount);
156
+ }, type: "button" }, "Increment"),
157
+ react_1.default.createElement("div", { "data-testid": "count" }, count),
158
+ react_1.default.createElement("div", { "data-testid": "debounced-count" }, debouncedCount)));
159
+ }
160
+ (0, react_2.render)(react_1.default.createElement(DebouncedComponent, null));
161
+ const incrementButton = react_2.screen.getByTestId("increment");
162
+ const debouncedCount = react_2.screen.getByTestId("debounced-count");
163
+ const user = user_event_1.default.setup({ advanceTimers: jest.advanceTimersByTime });
164
+ yield user.click(incrementButton);
165
+ // With leading edge, the value should be updated immediately
166
+ expect(debouncedCount.textContent).toBe("1");
167
+ yield user.click(incrementButton);
168
+ yield user.click(incrementButton);
169
+ // Additional clicks shouldn't update immediately (debounced)
170
+ expect(debouncedCount.textContent).toBe("1");
171
+ // After the debounce period, the trailing edge should update with the latest value
172
+ (0, react_2.act)(() => {
173
+ jest.advanceTimersByTime(DEBOUNCE_WAIT);
174
+ });
175
+ expect(debouncedCount.textContent).toBe("3");
176
+ }));
177
+ it("should abort debounced function when signal is aborted", () => __awaiter(void 0, void 0, void 0, function* () {
178
+ const mockFn = jest.fn();
179
+ const controller = new AbortController();
180
+ const { result } = (0, react_2.renderHook)(() => (0, useDebounce_1.useDebounce)(mockFn, DEBOUNCE_WAIT, { signal: controller.signal }));
181
+ result.current("test");
182
+ (0, react_2.act)(() => {
183
+ controller.abort();
184
+ });
185
+ (0, react_2.act)(() => {
186
+ jest.advanceTimersByTime(DEBOUNCE_WAIT + 100);
187
+ });
188
+ expect(mockFn).not.toHaveBeenCalled();
189
+ }));
111
190
  });
@@ -0,0 +1 @@
1
+ export { useStepper } from "./useStepper";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useStepper = void 0;
4
+ var useStepper_1 = require("./useStepper");
5
+ Object.defineProperty(exports, "useStepper", { enumerable: true, get: function () { return useStepper_1.useStepper; } });
@@ -0,0 +1,12 @@
1
+ interface UseStepOptions<StepName extends string> {
2
+ defaultStep?: StepName;
3
+ }
4
+ export declare function useStepper<StepName extends string>(steps: readonly StepName[], options?: UseStepOptions<StepName>): {
5
+ goToStep: (step: StepName) => void;
6
+ goToNextStep: () => void;
7
+ goToPreviousStep: () => void;
8
+ currentStep: StepName;
9
+ isFirst: boolean;
10
+ isLast: boolean;
11
+ };
12
+ export {};
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.useStepper = useStepper;
7
+ const react_1 = require("react");
8
+ const tiny_invariant_1 = __importDefault(require("tiny-invariant"));
9
+ function useStepper(steps, options = {}) {
10
+ var _a;
11
+ const firstStep = (_a = options === null || options === void 0 ? void 0 : options.defaultStep) !== null && _a !== void 0 ? _a : steps[0];
12
+ (0, tiny_invariant_1.default)(firstStep, "Steps array cannot be empty");
13
+ const [currentStep, setCurrentStep] = (0, react_1.useState)(firstStep);
14
+ const currentActiveStep = (0, react_1.useMemo)(() => ({
15
+ currentStep,
16
+ isFirst: currentStep === steps[0],
17
+ isLast: currentStep === steps[steps.length - 1],
18
+ }), [currentStep, steps]);
19
+ const handlers = (0, react_1.useMemo)(() => ({
20
+ goToStep: (step) => {
21
+ setCurrentStep(step);
22
+ },
23
+ goToNextStep: () => {
24
+ setCurrentStep(prevCurrentStep => {
25
+ const currentIndex = steps.indexOf(prevCurrentStep);
26
+ const nextStep = steps[Math.min(currentIndex + 1, steps.length - 1)];
27
+ (0, tiny_invariant_1.default)(nextStep, `Index out of bounds: ${currentIndex + 1}`);
28
+ return nextStep;
29
+ });
30
+ },
31
+ goToPreviousStep: () => {
32
+ setCurrentStep(prevCurrentStep => {
33
+ const currentIndex = steps.indexOf(prevCurrentStep);
34
+ const previousStep = steps[Math.max(currentIndex - 1, 0)];
35
+ (0, tiny_invariant_1.default)(previousStep, `Index out of bounds: ${currentIndex - 1}`);
36
+ return previousStep;
37
+ });
38
+ },
39
+ }), [steps, setCurrentStep]);
40
+ return Object.assign(Object.assign({}, currentActiveStep), handlers);
41
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const react_1 = require("@testing-library/react");
4
+ const _1 = require(".");
5
+ const steps = ["step1", "step2", "step3"];
6
+ describe("useStepper", () => {
7
+ it("initializes with first step when no initial step provided", () => {
8
+ const { result } = (0, react_1.renderHook)(() => (0, _1.useStepper)(steps));
9
+ expect(result.current.currentStep).toBe("step1");
10
+ expect(result.current.isFirst).toBe(true);
11
+ expect(result.current.isLast).toBe(false);
12
+ });
13
+ it("initializes with provided initial step", () => {
14
+ const { result } = (0, react_1.renderHook)(() => (0, _1.useStepper)(["step1", "step2", "step3"], {
15
+ defaultStep: "step2",
16
+ }));
17
+ expect(result.current.currentStep).toBe("step2");
18
+ expect(result.current.isFirst).toBe(false);
19
+ expect(result.current.isLast).toBe(false);
20
+ });
21
+ it("moves to next step when nextStep is called", () => {
22
+ const { result } = (0, react_1.renderHook)(() => (0, _1.useStepper)(steps));
23
+ (0, react_1.act)(() => {
24
+ result.current.goToNextStep();
25
+ });
26
+ expect(result.current.currentStep).toBe("step2");
27
+ expect(result.current.isLast).toBe(false);
28
+ expect(result.current.isFirst).toBe(false);
29
+ });
30
+ it("moves to previous step when previousStep is called", () => {
31
+ const { result } = (0, react_1.renderHook)(() => (0, _1.useStepper)(steps, {
32
+ defaultStep: "step2",
33
+ }));
34
+ (0, react_1.act)(() => {
35
+ result.current.goToPreviousStep();
36
+ });
37
+ expect(result.current.currentStep).toBe("step1");
38
+ expect(result.current.isLast).toBe(false);
39
+ expect(result.current.isFirst).toBe(true);
40
+ });
41
+ it("does not move past the last step", () => {
42
+ const { result } = (0, react_1.renderHook)(() => (0, _1.useStepper)(steps, {
43
+ defaultStep: "step3",
44
+ }));
45
+ (0, react_1.act)(() => {
46
+ result.current.goToNextStep();
47
+ });
48
+ expect(result.current.currentStep).toBe("step3");
49
+ expect(result.current.isLast).toBe(true);
50
+ expect(result.current.isFirst).toBe(false);
51
+ });
52
+ it("does not move before the first step", () => {
53
+ const { result } = (0, react_1.renderHook)(() => (0, _1.useStepper)(steps, {
54
+ defaultStep: "step1",
55
+ }));
56
+ (0, react_1.act)(() => {
57
+ result.current.goToPreviousStep();
58
+ });
59
+ expect(result.current.currentStep).toBe("step1");
60
+ expect(result.current.isFirst).toBe(true);
61
+ expect(result.current.isLast).toBe(false);
62
+ });
63
+ it("moves to specific step when goToStep is called", () => {
64
+ const { result } = (0, react_1.renderHook)(() => (0, _1.useStepper)(steps));
65
+ (0, react_1.act)(() => {
66
+ result.current.goToStep("step3");
67
+ });
68
+ expect(result.current.currentStep).toBe("step3");
69
+ expect(result.current.isLast).toBe(true);
70
+ expect(result.current.isFirst).toBe(false);
71
+ });
72
+ describe("error handling", () => {
73
+ it("throws error when steps array is empty", () => {
74
+ expect(() => {
75
+ (0, react_1.renderHook)(() => (0, _1.useStepper)([]));
76
+ }).toThrow(new Error("Invariant failed: Steps array cannot be empty"));
77
+ });
78
+ });
79
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/hooks",
3
- "version": "2.15.1-export-use-69f7bac.88+69f7bac8",
3
+ "version": "2.17.0",
4
4
  "license": "MIT",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.js",
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "devDependencies": {
22
22
  "@apollo/react-testing": "^4.0.0",
23
- "@jobber/formatters": "^0.4.1-export-use-69f7bac.274+69f7bac8",
23
+ "@jobber/formatters": "^0.4.0",
24
24
  "@types/lodash": "4.14.136",
25
25
  "@types/react": "^18.0.28",
26
26
  "@types/react-dom": "^18.0.11",
@@ -29,8 +29,10 @@
29
29
  "uuid": "^8.3.2"
30
30
  },
31
31
  "dependencies": {
32
+ "es-toolkit": "^1.39.7",
32
33
  "lodash": "^4.17.20",
33
34
  "resize-observer-polyfill": "^1.5.1",
35
+ "tiny-invariant": "^1.3.3",
34
36
  "ts-xor": "^1.0.8",
35
37
  "use-resize-observer": "^6.1.0"
36
38
  },
@@ -38,5 +40,5 @@
38
40
  "@apollo/client": "^3.0.0",
39
41
  "react": "^18.2.0"
40
42
  },
41
- "gitHead": "69f7bac808b5c8f20f459bad3b158f7578a36384"
43
+ "gitHead": "5b2a570f0d657e82d04a8d60964242f67da3fab0"
42
44
  }
@@ -0,0 +1 @@
1
+ export * from "./dist/useStepper";
package/useStepper.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true,
5
+ });
6
+
7
+ var useStepper = require("./dist/useStepper");
8
+
9
+ Object.keys(useStepper).forEach(function(key) {
10
+ if (key === "default" || key === "__esModule") return;
11
+ Object.defineProperty(exports, key, {
12
+ enumerable: true,
13
+ get: function get() {
14
+ return useStepper[key];
15
+ },
16
+ });
17
+ });