Skip to content

Commit d351ef7

Browse files
authored
Merge pull request #181 from Nurvive/feat/useVibrate
[feat]: useVibrate
2 parents 774c7b0 + 67dd8fd commit d351ef7

File tree

4 files changed

+234
-1
lines changed

4 files changed

+234
-1
lines changed

src/hooks/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,4 @@ export * from './useWindowEvent/useWindowEvent';
104104
export * from './useWindowFocus/useWindowFocus';
105105
export * from './useWindowScroll/useWindowScroll';
106106
export * from './useWindowSize/useWindowSize';
107-
export * from './useWizard/useWizard';
107+
export * from './useWizard/useWizard';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useVibrate } from './useVibrate';
2+
3+
const Demo = () => {
4+
const { isSupported, isVibrating, vibrate, stop } = useVibrate({
5+
pattern: [300, 100, 200, 100, 1000, 300]
6+
});
7+
8+
return (
9+
<div>
10+
<button type='button' disabled={!isSupported || isVibrating} onClick={() => vibrate()}>
11+
{isSupported ? 'Vibrate' : 'Web vibrate is not supported in your browser'}
12+
</button>
13+
<button type='button' disabled={!isSupported} onClick={() => stop()}>
14+
Stop
15+
</button>
16+
</div>
17+
);
18+
};
19+
20+
export default Demo;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
3+
import { useVibrate } from './useVibrate';
4+
5+
const vibrateMock = vi.fn();
6+
7+
describe('useVibrate', () => {
8+
beforeAll(() => {
9+
Object.defineProperty(navigator, 'vibrate', {
10+
writable: true,
11+
value: vibrateMock
12+
});
13+
});
14+
15+
it('should indicate support for vibration API', () => {
16+
const { result } = renderHook(() => useVibrate(1000));
17+
18+
expect(result.current.isSupported).toBe(true);
19+
});
20+
21+
it('should start and stop vibration', () => {
22+
const { result } = renderHook(() => useVibrate(1000));
23+
24+
act(() => {
25+
result.current.vibrate();
26+
});
27+
28+
expect(vibrateMock).toHaveBeenCalledWith(1000);
29+
expect(result.current.isVibrating).toBe(true);
30+
31+
act(() => {
32+
result.current.stop();
33+
});
34+
35+
expect(vibrateMock).toHaveBeenCalledWith(0);
36+
expect(result.current.isVibrating).toBe(false);
37+
});
38+
39+
it('should handle looped vibration', () => {
40+
vi.useFakeTimers();
41+
const { result } = renderHook(() => useVibrate({ pattern: [200, 100, 200], loop: true }));
42+
43+
act(() => {
44+
result.current.vibrate();
45+
});
46+
47+
expect(vibrateMock).toHaveBeenCalledWith([200, 100, 200]);
48+
expect(result.current.isVibrating).toBe(true);
49+
50+
vi.advanceTimersByTime(500);
51+
52+
expect(vibrateMock).toHaveBeenCalledTimes(2);
53+
54+
act(() => {
55+
result.current.stop();
56+
});
57+
58+
expect(vibrateMock).toHaveBeenCalledWith(0);
59+
expect(result.current.isVibrating).toBe(false);
60+
61+
vi.useRealTimers();
62+
});
63+
64+
it('should respect the enabled parameter', () => {
65+
const { result, rerender } = renderHook(
66+
({ enabled }) => useVibrate({ pattern: [200, 100, 200], enabled }),
67+
{
68+
initialProps: { enabled: false }
69+
}
70+
);
71+
72+
expect(result.current.isVibrating).toBe(false);
73+
74+
rerender({ enabled: true });
75+
76+
expect(result.current.isVibrating).toBe(true);
77+
expect(vibrateMock).toHaveBeenCalledWith([200, 100, 200]);
78+
79+
rerender({ enabled: false });
80+
81+
expect(result.current.isVibrating).toBe(false);
82+
expect(vibrateMock).toHaveBeenCalledWith(0);
83+
});
84+
});

src/hooks/useVibrate/useVibrate.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { useEffect, useState } from 'react';
2+
3+
import { isClient } from '@/utils/helpers';
4+
5+
/** The use vibration params */
6+
export interface UseVibrateParams {
7+
/** Pattern for vibration */
8+
pattern: number | number[];
9+
/** Alternate way to enable vibration */
10+
enabled?: boolean;
11+
/** Indicates thar vibration will be endless */
12+
loop?: boolean;
13+
}
14+
15+
/** The use vibration options */
16+
export interface UseVibrateOptions {
17+
/** Alternate way to enable vibration */
18+
enabled?: boolean;
19+
/** Indicates thar vibration will be endless */
20+
loop?: boolean;
21+
}
22+
23+
/** The use vibration return type */
24+
export interface UseVibrateReturn {
25+
/** Indicates that the device supports Vibration API */
26+
isSupported: boolean;
27+
/** Indicates that the device is vibrating */
28+
isVibrating: boolean;
29+
/** Start vibration function */
30+
vibrate: (pattern?: number | number[]) => void;
31+
/** Stop vibration function */
32+
stop: () => void;
33+
}
34+
35+
export type UseVibrate = {
36+
(pattern: number | number[], options?: UseVibrateOptions): UseVibrateReturn;
37+
({ pattern, loop, enabled }: UseVibrateParams, options?: never): UseVibrateReturn;
38+
};
39+
40+
let interval: NodeJS.Timeout;
41+
/**
42+
* @name useVibrate
43+
* @description - Hook that provides Vibrate API
44+
* @category Browser
45+
*
46+
* @overload
47+
* @param {(number|number[])} pattern Pattern for vibration
48+
* @param {boolean} [options.loop] Indicates thar vibration will be endless
49+
* @param {boolean} [options.enabled] Alternate way to enable vibration
50+
* @returns {UseVibrateReturn} An object containing support indicator, start vibration and stop vibration functions
51+
*
52+
* @example
53+
* const { isSupported, isVibrating, vibrate, stop } = useVibrate(1000);
54+
*
55+
* @overload
56+
* @param {(number|number[])} params.pattern Pattern for vibration
57+
* @param {boolean} [params.loop] Indicates thar vibration will be endless
58+
* @param {boolean} [params.enabled] Alternate way to enable vibration
59+
* @returns {UseVibrateReturn} An object containing support indicator, vibrating indicator, start vibration and stop vibration functions
60+
*
61+
* @example
62+
* const { isSupported, isVibrating, vibrate, stop } = useVibrate({pattern: [200, 100, 200], loop: true});
63+
* */
64+
export const useVibrate: UseVibrate = (...params) => {
65+
const pattern =
66+
typeof params[0] === 'number' || Array.isArray(params[0]) ? params[0] : params[0]?.pattern;
67+
const { loop, enabled } =
68+
typeof params[0] === 'number' || Array.isArray(params[0]) ? params[1] ?? {} : params[0] ?? {};
69+
70+
const [isSupported, setIsSupported] = useState(false);
71+
const [isVibrating, setIsVibrating] = useState(false);
72+
73+
useEffect(() => {
74+
if (isClient && navigator && 'vibrate' in navigator) {
75+
setIsSupported(true);
76+
}
77+
}, []);
78+
79+
const vibrate = (curPattern = pattern) => {
80+
if (!isSupported || isVibrating) return;
81+
82+
const duration = Array.isArray(curPattern) ? curPattern.reduce((a, b) => a + b) : curPattern;
83+
84+
setIsVibrating(true);
85+
navigator.vibrate(curPattern);
86+
87+
if (loop) {
88+
interval = setInterval(() => {
89+
navigator.vibrate(curPattern);
90+
}, duration);
91+
} else {
92+
setTimeout(() => {
93+
setIsVibrating(false);
94+
}, duration);
95+
}
96+
};
97+
98+
const stop = () => {
99+
if (!isSupported) return;
100+
101+
setIsVibrating(false);
102+
navigator.vibrate(0);
103+
104+
if (loop) {
105+
clearInterval(interval);
106+
}
107+
};
108+
109+
useEffect(() => {
110+
if (!isSupported || isVibrating) return;
111+
112+
if (enabled) {
113+
vibrate();
114+
}
115+
116+
return () => {
117+
if (enabled) {
118+
setIsVibrating(false);
119+
navigator.vibrate(0);
120+
121+
if (loop) {
122+
clearInterval(interval);
123+
}
124+
}
125+
};
126+
}, [enabled]);
127+
128+
return { isSupported, vibrate, stop, isVibrating } as const;
129+
};

0 commit comments

Comments
 (0)