Skip to content

Commit c1d9b44

Browse files
committed
main 🧊 add demos, rework layout
1 parent 6b81d01 commit c1d9b44

14 files changed

Lines changed: 257 additions & 398 deletions

File tree

‎packages/core/src/hooks/useDeviceMotion/useDeviceMotion.demo.tsx‎

Lines changed: 68 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
11
import { useDeviceMotion } from '@siberiacancode/reactuse';
2-
import { SmartphoneIcon } from 'lucide-react';
2+
import { MoveHorizontalIcon, MoveVerticalIcon } from 'lucide-react';
33

44
const CIRCLE_SIZE = 200;
5-
const BUBBLE_SIZE = 36;
5+
const BUBBLE_SIZE = 24;
66
const MAX_OFFSET = CIRCLE_SIZE / 2 - BUBBLE_SIZE / 2 - 8;
77
const GRAVITY = 9.8;
8-
const LEVEL_THRESHOLD = 0.5;
98

109
const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
1110

1211
const Demo = () => {
13-
const motion = useDeviceMotion({ delay: 100 });
12+
const deviceMotion = useDeviceMotion();
13+
const value = deviceMotion.watch();
1414

15-
const x = motion.accelerationIncludingGravity.x;
16-
const y = motion.accelerationIncludingGravity.y;
15+
const x = value.accelerationIncludingGravity.x;
16+
const y = value.accelerationIncludingGravity.y;
1717

18-
if (x === null || y === null) {
18+
if (!x || !y) {
1919
return (
20-
<section className='flex w-full max-w-sm flex-col items-center p-4'>
21-
<div className='bg-muted/40 flex flex-col items-center gap-3 rounded-2xl p-8 text-center'>
22-
<SmartphoneIcon className='text-muted-foreground size-10' />
23-
<p className='text-sm font-medium'>Device motion not supported</p>
24-
<p className='text-muted-foreground text-xs'>
25-
Open this page on a mobile device to use the spirit level.
26-
</p>
27-
</div>
28-
</section>
20+
<p>
21+
Api not supported, make sure to check for compatibility with different browsers when using
22+
this{' '}
23+
<a
24+
href='https://developer.mozilla.org/en-US/docs/Web/API/Window/DeviceMotionEvent'
25+
rel='noreferrer'
26+
target='_blank'
27+
>
28+
api
29+
</a>
30+
</p>
2931
);
3032
}
3133

@@ -35,56 +37,61 @@ const Demo = () => {
3537
const tiltX = ((-x / GRAVITY) * 90).toFixed(1);
3638
const tiltY = ((y / GRAVITY) * 90).toFixed(1);
3739

38-
const isLevel = Math.abs(x) < LEVEL_THRESHOLD && Math.abs(y) < LEVEL_THRESHOLD;
39-
4040
return (
41-
<section className='flex w-full max-w-sm flex-col items-center gap-5 p-4'>
42-
<div className='flex flex-col items-center gap-1 text-center'>
43-
<h3>Spirit level</h3>
44-
<p className='text-muted-foreground text-sm'>
45-
Tilt your device — the bubble follows gravity.
46-
</p>
47-
</div>
41+
<section className='flex flex-col items-center p-4'>
42+
<div className='relative' style={{ width: CIRCLE_SIZE, height: CIRCLE_SIZE }}>
43+
<svg height={CIRCLE_SIZE} viewBox={`0 0 ${CIRCLE_SIZE} ${CIRCLE_SIZE}`} width={CIRCLE_SIZE}>
44+
<circle
45+
className='text-border'
46+
cx={CIRCLE_SIZE / 2}
47+
cy={CIRCLE_SIZE / 2}
48+
fill='transparent'
49+
r={CIRCLE_SIZE / 2 - 1}
50+
stroke='currentColor'
51+
strokeWidth='2'
52+
/>
4853

49-
<div
50-
className='border-border bg-muted/30 relative rounded-full border-2'
51-
style={{ width: CIRCLE_SIZE, height: CIRCLE_SIZE }}
52-
>
53-
<div className='bg-border absolute top-1/2 left-0 h-px w-full -translate-y-1/2' />
54-
<div className='bg-border absolute top-0 left-1/2 h-full w-px -translate-x-1/2' />
54+
<line
55+
className='text-border'
56+
stroke='currentColor'
57+
strokeLinecap='round'
58+
strokeWidth='1'
59+
x1='20'
60+
x2={CIRCLE_SIZE - 20}
61+
y1={CIRCLE_SIZE / 2}
62+
y2={CIRCLE_SIZE / 2}
63+
/>
64+
<line
65+
className='text-border'
66+
stroke='currentColor'
67+
strokeLinecap='round'
68+
strokeWidth='1'
69+
x1={CIRCLE_SIZE / 2}
70+
x2={CIRCLE_SIZE / 2}
71+
y1='20'
72+
y2={CIRCLE_SIZE - 20}
73+
/>
5574

56-
<div
57-
style={{
58-
width: BUBBLE_SIZE,
59-
height: BUBBLE_SIZE,
60-
marginLeft: -BUBBLE_SIZE / 2,
61-
marginTop: -BUBBLE_SIZE / 2
62-
}}
63-
className='border-foreground/40 absolute top-1/2 left-1/2 rounded-full border-2'
64-
/>
75+
<circle
76+
className='text-border'
77+
cx={CIRCLE_SIZE / 2 + offsetX}
78+
cy={CIRCLE_SIZE / 2 + offsetY}
79+
fill='white'
80+
r={BUBBLE_SIZE / 2}
81+
stroke='currentColor'
82+
strokeWidth='1'
83+
style={{ transition: 'all 100ms ease-out' }}
84+
/>
85+
</svg>
6586

66-
<div
67-
style={{
68-
width: BUBBLE_SIZE,
69-
height: BUBBLE_SIZE,
70-
marginLeft: -BUBBLE_SIZE / 2,
71-
marginTop: -BUBBLE_SIZE / 2,
72-
backgroundColor: isLevel ? 'oklch(0.65 0.15 160)' : 'oklch(0.55 0.18 250)',
73-
transform: `translate(${offsetX}px, ${offsetY}px)`,
74-
boxShadow: '0 2px 8px rgba(0,0,0,0.15)'
75-
}}
76-
className='absolute top-1/2 left-1/2 rounded-full transition-transform duration-100 ease-out'
77-
/>
78-
</div>
79-
80-
<div className='flex gap-6 text-sm'>
81-
<div className='flex flex-col items-center'>
82-
<span className='text-muted-foreground text-xs'>Tilt X</span>
83-
<code>{tiltX}°</code>
87+
<div className='absolute top-1.5 left-1/2 flex -translate-x-1/2 items-center gap-1 text-[10px] text-neutral-400'>
88+
<MoveVerticalIcon className='size-3' />
89+
{tiltY}°
8490
</div>
85-
<div className='flex flex-col items-center'>
86-
<span className='text-muted-foreground text-xs'>Tilt Y</span>
87-
<code>{tiltY}°</code>
91+
92+
<div className='absolute top-1/2 right-1.5 flex -translate-y-1/2 items-center gap-1 text-[10px] text-neutral-400'>
93+
<MoveHorizontalIcon className='size-3' />
94+
{tiltX}°
8895
</div>
8996
</div>
9097
</section>

‎packages/core/src/hooks/useDeviceMotion/useDeviceMotion.test.ts‎

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { act, renderHook } from '@testing-library/react';
22

3+
import { renderHookServer } from '@/tests';
4+
35
import { useDeviceMotion } from './useDeviceMotion';
46

57
const DEVICE_MOTION_EVENT_INIT: DeviceMotionEventInit = {
@@ -41,26 +43,46 @@ globalThis.DeviceMotionEvent = MockDeviceMotionEvent as any;
4143
it('Should use device motion', () => {
4244
const { result } = renderHook(useDeviceMotion);
4345

44-
expect(result.current.interval).toBe(0);
45-
expect(result.current.rotationRate).toEqual({
46+
expect(result.current.watch).toBeTypeOf('function');
47+
expect(result.current.snapshot.interval).toBe(0);
48+
expect(result.current.snapshot.rotationRate).toEqual({
4649
alpha: null,
4750
beta: null,
4851
gamma: null
4952
});
50-
expect(result.current.acceleration).toEqual({ x: null, y: null, z: null });
51-
expect(result.current.accelerationIncludingGravity).toEqual({
53+
expect(result.current.snapshot.acceleration).toEqual({ x: null, y: null, z: null });
54+
expect(result.current.snapshot.accelerationIncludingGravity).toEqual({
5255
x: null,
5356
y: null,
5457
z: null
5558
});
5659
});
5760

58-
it('Should handle device motion event', () => {
61+
it('Should use device motion on server side', () => {
62+
const { result } = renderHookServer(useDeviceMotion);
63+
64+
expect(result.current.watch).toBeTypeOf('function');
65+
expect(result.current.snapshot.interval).toBe(0);
66+
expect(result.current.snapshot.rotationRate).toEqual({
67+
alpha: null,
68+
beta: null,
69+
gamma: null
70+
});
71+
expect(result.current.snapshot.acceleration).toEqual({ x: null, y: null, z: null });
72+
expect(result.current.snapshot.accelerationIncludingGravity).toEqual({
73+
x: null,
74+
y: null,
75+
z: null
76+
});
77+
});
78+
79+
it('Should return reactive value on watch', () => {
5980
const { result } = renderHook(useDeviceMotion);
6081

82+
act(() => result.current.watch());
6183
act(() => window.dispatchEvent(new DeviceMotionEvent('devicemotion', DEVICE_MOTION_EVENT_INIT)));
6284

63-
expect(result.current).toEqual({
85+
expect(result.current.snapshot).toEqual({
6486
interval: DEVICE_MOTION_EVENT_INIT.interval,
6587
rotationRate: DEVICE_MOTION_EVENT_INIT.rotationRate,
6688
acceleration: DEVICE_MOTION_EVENT_INIT.acceleration,
@@ -71,10 +93,11 @@ it('Should handle device motion event', () => {
7193
it('Should call callback when motion detected', () => {
7294
const onChange = vi.fn();
7395
renderHook(() => useDeviceMotion({ onChange }));
96+
const event = new DeviceMotionEvent('devicemotion', DEVICE_MOTION_EVENT_INIT);
7497

75-
act(() => window.dispatchEvent(new DeviceMotionEvent('devicemotion', DEVICE_MOTION_EVENT_INIT)));
98+
act(() => window.dispatchEvent(event));
7699

77-
expect(onChange).toBeCalledWith(new DeviceMotionEvent('devicemotion', DEVICE_MOTION_EVENT_INIT));
100+
expect(onChange).toHaveBeenCalledWith(event);
78101
});
79102

80103
it('Should be listen enabled param', () => {
@@ -84,45 +107,21 @@ it('Should be listen enabled param', () => {
84107
initialProps: { enabled: false }
85108
});
86109

87-
expect(addEventListenerSpy).not.toBeCalledWith('devicemotion', expect.any(Function));
110+
expect(addEventListenerSpy).not.toHaveBeenCalledWith('devicemotion', expect.any(Function));
88111

89112
rerender({ enabled: true });
90113

91-
expect(addEventListenerSpy).toBeCalledWith('devicemotion', expect.any(Function));
114+
expect(addEventListenerSpy).toHaveBeenCalledWith('devicemotion', expect.any(Function));
92115
});
93116

94-
it('Should throttle events by delay', () => {
95-
vi.useFakeTimers();
117+
it('Should handle events without throttling', () => {
96118
const onChange = vi.fn();
97119
renderHook(() => useDeviceMotion({ onChange }));
98120

99121
act(() => window.dispatchEvent(new DeviceMotionEvent('devicemotion', DEVICE_MOTION_EVENT_INIT)));
100-
101-
expect(onChange).toHaveBeenCalledOnce();
102-
103122
act(() => window.dispatchEvent(new DeviceMotionEvent('devicemotion', DEVICE_MOTION_EVENT_INIT)));
104123

105-
act(() => vi.advanceTimersByTime(1000));
106-
107124
expect(onChange).toBeCalledTimes(2);
108-
109-
vi.useRealTimers();
110-
});
111-
112-
it('Should update with new delay', () => {
113-
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
114-
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
115-
116-
const { rerender } = renderHook((delay: number) => useDeviceMotion({ delay }), {
117-
initialProps: 1000
118-
});
119-
120-
expect(addEventListenerSpy).toHaveBeenCalledOnce();
121-
122-
rerender(500);
123-
124-
expect(removeEventListenerSpy).toHaveBeenCalledOnce();
125-
expect(addEventListenerSpy).toBeCalledTimes(2);
126125
});
127126

128127
it('Should handle enabled changes', () => {
@@ -146,9 +145,9 @@ it('Should handle enabled changes', () => {
146145
expect(removeEventListenerSpy).toHaveBeenCalledOnce();
147146
});
148147

149-
it('Should handle delay and callback', () => {
148+
it('Should handle callback', () => {
150149
const callback = vi.fn();
151-
renderHook(() => useDeviceMotion(callback, 1000));
150+
renderHook(() => useDeviceMotion(callback));
152151

153152
act(() => window.dispatchEvent(new DeviceMotionEvent('devicemotion', DEVICE_MOTION_EVENT_INIT)));
154153

0 commit comments

Comments
 (0)