Skip to content

Commit 0fcfaa7

Browse files
author
Kubit
committed
Improve and add new hooks
Improve and add new hooks useInput, hooks about scroll and trapFocus
1 parent 5e8e0f1 commit 0fcfaa7

File tree

14 files changed

+529
-150
lines changed

14 files changed

+529
-150
lines changed

src/hooks/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ export { useZoomEffect } from './useZoomEffect/useZoomEffect';
2020
export { useDeviceHeight } from './useDeviceHeight/useDeviceHeight';
2121
export { useElementBoundingClientRect } from './useElementBoundingClientRect/useElementBoundingClientRect';
2222
export { useSwipeDown } from './useSwipeDown/useSwipeDown';
23+
export * from './useScrollDetection';
24+
export * from './useScrollDetectionWithAutoFocus';

src/hooks/useInput/__tests__/useInput.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { act, renderHook } from '@testing-library/react-hooks';
22
import React, { ChangeEvent } from 'react';
33

4-
import { FormatNumber } from '@/components';
54
import * as validationsProvider from '@/provider/validations/validationsProvider';
65

76
import { useInput } from '../useInput';
87

98
describe('useInput Hook', () => {
109
it('useInput - on internal change should call parent onChange', () => {
1110
const onChange = jest.fn();
12-
const formatNumber = { style: 'decimal' } as FormatNumber;
11+
const formatNumber = { style: 'decimal' };
1312
const ref = React.createRef<HTMLInputElement | undefined>();
1413
const currentValue = '123234';
1514
const regex = new RegExp('^[0-9]*$');
@@ -44,7 +43,7 @@ describe('useInput Hook', () => {
4443
});
4544
it('useInput - on internal blur should call parent onBlur', () => {
4645
const onBlur = jest.fn();
47-
const formatNumber = { style: 'decimal' } as FormatNumber;
46+
const formatNumber = { style: 'decimal' };
4847
const { result } = renderHook(() => useInput({ onBlur, formatNumber }));
4948

5049
act(() => {

src/hooks/useInput/types/inputHook.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from 'react';
88

99
import { FormatNumber, InputState, InputTypeType, MASK_TYPE } from '@/components/input/types';
10+
import { EventKeyPressRefType } from '@/types';
1011

1112
export type ParamsTypeInputHook = {
1213
ref?: ForwardedRef<HTMLInputElement | undefined>;
@@ -45,6 +46,7 @@ export type ParamsTypeInputHook = {
4546
export type ReturnTypeInputHook = {
4647
value: string | number;
4748
state: InputState;
49+
eventKeyPressRef: MutableRefObject<EventKeyPressRefType | undefined>;
4850
inputRef?: MutableRefObject<HTMLInputElement | undefined>;
4951
handleBlurInternal: FocusEventHandler<HTMLInputElement>;
5052
handleChangeInternal: ChangeEventHandler<HTMLInputElement>;

src/hooks/useInput/useInput.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ export const useInput = (props: ParamsTypeInputHook): ReturnTypeInputHook => {
371371
value,
372372
state,
373373
inputRef,
374+
eventKeyPressRef,
374375
handleChangeInternal,
375376
handleBlurInternal,
376377
handleFocusInternal,

src/hooks/useScrollDetection/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useScrollDetection';
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { act, renderHook } from '@testing-library/react-hooks';
2+
3+
import { useScrollDetection } from './useScrollDetection';
4+
5+
const resizeObserverDisconnectMock = jest.fn();
6+
jest.mock('@/utils/resizeObserver', () => {
7+
return {
8+
ResizeObserver: class ResizeObserver {
9+
callback;
10+
constructor(callback) {
11+
this.callback = callback;
12+
}
13+
observe() {
14+
// Call the callback
15+
this.callback();
16+
}
17+
unobserve() {
18+
// do nothing
19+
}
20+
disconnect() {
21+
resizeObserverDisconnectMock();
22+
}
23+
},
24+
};
25+
});
26+
27+
describe('useScrollDetection', () => {
28+
let element;
29+
30+
beforeEach(() => {
31+
element = document.createElement('div');
32+
});
33+
34+
it('Should return hasScroll false if it does not have scroll', () => {
35+
const { result } = renderHook(() => useScrollDetection());
36+
37+
act(() => {
38+
result.current.handleScrollDetection(element);
39+
});
40+
41+
expect(result.current.hasScroll).toBe(false);
42+
});
43+
44+
it('should return hasScroll true when the element container has scroll', () => {
45+
Object.defineProperty(element, 'scrollHeight', {
46+
value: 200,
47+
});
48+
Object.defineProperty(element, 'clientHeight', {
49+
value: 100,
50+
});
51+
52+
const { result } = renderHook(() => useScrollDetection());
53+
act(() => {
54+
result.current.handleScrollDetection(element);
55+
});
56+
expect(result.current.hasScroll).toBe(true);
57+
});
58+
59+
it('When the node is deleted the inner observer is deleted', () => {
60+
const { result } = renderHook(() => useScrollDetection());
61+
62+
act(() => {
63+
result.current.handleScrollDetection(element);
64+
});
65+
act(() => {
66+
result.current.handleScrollDetection(null);
67+
});
68+
69+
expect(resizeObserverDisconnectMock).toHaveBeenCalled();
70+
});
71+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react';
2+
3+
import { hasScroll as checkHasScroll } from '@/utils';
4+
import { ResizeObserver } from '@/utils/resizeObserver';
5+
6+
type UseScrollDetectionReturnType = {
7+
handleScrollDetection: (element: HTMLElement | null | undefined) => void;
8+
hasScroll: boolean;
9+
};
10+
11+
/**
12+
* Custom hook that determines if an element has scroll
13+
*
14+
* @returns An object containing the `hasScroll` boolean and the `handleScrollDetection` callback function to initialize the hook.
15+
*/
16+
export const useScrollDetection = (): UseScrollDetectionReturnType => {
17+
const [hasScroll, setHasScroll] = React.useState(false);
18+
const resizeObserverRef = React.useRef<ResizeObserver>();
19+
20+
const handleScrollDetection = React.useCallback((element: HTMLElement | null | undefined) => {
21+
if (element) {
22+
const handleInnerContentResize = (element: HTMLElement) => {
23+
const _hasScroll = checkHasScroll(element);
24+
setHasScroll(_hasScroll);
25+
};
26+
handleInnerContentResize(element);
27+
resizeObserverRef.current = new ResizeObserver(() => {
28+
handleInnerContentResize(element);
29+
});
30+
resizeObserverRef.current.observe(element);
31+
} else {
32+
resizeObserverRef.current?.disconnect();
33+
}
34+
}, []);
35+
36+
return { hasScroll, handleScrollDetection };
37+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useScrollDetectionWithAutoFocus';
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { act, renderHook } from '@testing-library/react-hooks';
2+
3+
import { useScrollDetectionWithAutoFocus } from './useScrollDetectionWithAutoFocus';
4+
5+
const resizeObserverDisconnectMock = jest.fn();
6+
jest.mock('@/utils/resizeObserver', () => {
7+
return {
8+
ResizeObserver: class ResizeObserver {
9+
callback;
10+
constructor(callback) {
11+
this.callback = callback;
12+
}
13+
observe() {
14+
// Call the callback
15+
this.callback();
16+
}
17+
unobserve() {
18+
// do nothing
19+
}
20+
disconnect() {
21+
resizeObserverDisconnectMock();
22+
}
23+
},
24+
};
25+
});
26+
27+
describe('useScrollDetectionWithAutoFocus', () => {
28+
let parent;
29+
let element;
30+
let internalFocusableElement;
31+
let parentElementRef;
32+
33+
beforeEach(() => {
34+
parent = document.createElement('div');
35+
element = document.createElement('div');
36+
internalFocusableElement = document.createElement('button');
37+
parent.appendChild(element);
38+
parent.appendChild(internalFocusableElement);
39+
document.body.appendChild(parent);
40+
parentElementRef = { current: parent };
41+
});
42+
43+
afterEach(() => {
44+
parent.remove();
45+
jest.clearAllMocks();
46+
jest.resetAllMocks();
47+
jest.restoreAllMocks();
48+
});
49+
50+
it('Should return hasScroll false if it does not have scroll', () => {
51+
const { result } = renderHook(() => useScrollDetectionWithAutoFocus({ parentElementRef }));
52+
53+
act(() => {
54+
result.current.handleScrollDetection(element);
55+
});
56+
57+
expect(result.current.hasScroll).toBe(false);
58+
});
59+
60+
it('should return hasScroll true when the element container has scroll', () => {
61+
Object.defineProperty(element, 'scrollHeight', {
62+
value: 200,
63+
});
64+
Object.defineProperty(element, 'clientHeight', {
65+
value: 100,
66+
});
67+
68+
const { result } = renderHook(() => useScrollDetectionWithAutoFocus({ parentElementRef }));
69+
act(() => {
70+
result.current.handleScrollDetection(element);
71+
});
72+
expect(result.current.hasScroll).toBe(true);
73+
});
74+
75+
it('When it has scroll and document.active element is null, it should focus on the element with scroll when opening', () => {
76+
Object.defineProperty(element, 'scrollHeight', {
77+
value: 200,
78+
});
79+
Object.defineProperty(element, 'clientHeight', {
80+
value: 100,
81+
});
82+
jest.spyOn(document, 'activeElement', 'get').mockReturnValue(null);
83+
const mockFocus = jest.spyOn(element, 'focus');
84+
85+
const { result } = renderHook(() => useScrollDetectionWithAutoFocus({ parentElementRef }));
86+
act(() => {
87+
result.current.handleScrollDetection(element);
88+
});
89+
90+
expect(mockFocus).toHaveBeenCalled();
91+
});
92+
93+
it('When it has scroll and parentElementRef does not contains the active element, it should focus on the element with scroll when opening', () => {
94+
Object.defineProperty(element, 'scrollHeight', {
95+
value: 200,
96+
});
97+
Object.defineProperty(element, 'clientHeight', {
98+
value: 100,
99+
});
100+
101+
const mockFocus = jest.spyOn(element, 'focus');
102+
103+
const { result } = renderHook(() => useScrollDetectionWithAutoFocus({ parentElementRef }));
104+
act(() => {
105+
result.current.handleScrollDetection(element);
106+
});
107+
108+
expect(mockFocus).toHaveBeenCalled();
109+
});
110+
111+
it('When it has scroll and focus is after element, it should focus on the element with scroll when opening', () => {
112+
Object.defineProperty(element, 'scrollHeight', {
113+
value: 200,
114+
});
115+
Object.defineProperty(element, 'clientHeight', {
116+
value: 100,
117+
});
118+
119+
// Focus in the "after" element
120+
act(() => {
121+
internalFocusableElement.focus();
122+
});
123+
124+
const mockFocus = jest.spyOn(element, 'focus');
125+
126+
const { result } = renderHook(() => useScrollDetectionWithAutoFocus({ parentElementRef }));
127+
act(() => {
128+
result.current.handleScrollDetection(element);
129+
});
130+
131+
expect(mockFocus).toHaveBeenCalled();
132+
});
133+
134+
it('When the node is deleted the inner observer is deleted', () => {
135+
const { result } = renderHook(() => useScrollDetectionWithAutoFocus({ parentElementRef }));
136+
137+
act(() => {
138+
result.current.handleScrollDetection(element);
139+
});
140+
act(() => {
141+
result.current.handleScrollDetection(null);
142+
});
143+
144+
expect(resizeObserverDisconnectMock).toHaveBeenCalled();
145+
});
146+
});
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React from 'react';
2+
3+
import { hasScroll as checkHasScroll } from '@/utils';
4+
import { ResizeObserver } from '@/utils/resizeObserver';
5+
6+
type UseScrollDetectionWithAutoFocusParamsType = {
7+
parentElementRef?: React.RefObject<HTMLElement>;
8+
};
9+
10+
type UseScrollDetectionWithAutoFocusReturnType = {
11+
handleScrollDetection: (element: HTMLElement | null | undefined) => void;
12+
hasScroll: boolean;
13+
};
14+
15+
/**
16+
* Custom hook that determines if an element has scroll. Besides it will focus on the element if it has scroll and the active element is not inside the parent element.
17+
*
18+
* @returns An object containing the `hasScroll` boolean and the `handleScrollDetection` callback function to initialize the hook.
19+
*/
20+
export const useScrollDetectionWithAutoFocus = ({
21+
parentElementRef,
22+
}: UseScrollDetectionWithAutoFocusParamsType): UseScrollDetectionWithAutoFocusReturnType => {
23+
const [hasScroll, setHasScroll] = React.useState(false);
24+
const resizeObserverRef = React.useRef<ResizeObserver>();
25+
26+
const handleScrollDetection = React.useCallback((element: HTMLElement | null | undefined) => {
27+
if (element) {
28+
const handleInnerContentResize = (element: HTMLElement) => {
29+
const _hasScroll = checkHasScroll(element);
30+
setHasScroll(_hasScroll);
31+
};
32+
const handleAutoFocus = element => {
33+
const _hasScroll = checkHasScroll(element);
34+
if (!_hasScroll) {
35+
return;
36+
}
37+
const autoFocus = () => {
38+
element.setAttribute('tabindex', '0');
39+
element.focus();
40+
};
41+
42+
if (!document.activeElement) {
43+
autoFocus();
44+
return;
45+
}
46+
47+
if (!parentElementRef?.current?.contains(document.activeElement)) {
48+
autoFocus();
49+
return;
50+
}
51+
52+
if (
53+
element.compareDocumentPosition(document.activeElement) & Node.DOCUMENT_POSITION_FOLLOWING
54+
) {
55+
autoFocus();
56+
}
57+
};
58+
handleInnerContentResize(element);
59+
// Autofocus is only executed on mount
60+
handleAutoFocus(element);
61+
resizeObserverRef.current = new ResizeObserver(() => {
62+
handleInnerContentResize(element);
63+
});
64+
resizeObserverRef.current.observe(element);
65+
} else {
66+
resizeObserverRef.current?.disconnect();
67+
}
68+
}, []);
69+
70+
return { hasScroll, handleScrollDetection };
71+
};

0 commit comments

Comments
 (0)