@alessiofrittoli/react-hooks 1.0.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 +830 -0
- package/dist/browser-api/index.d.mts +6 -0
- package/dist/browser-api/index.d.ts +6 -0
- package/dist/browser-api/index.js +2 -0
- package/dist/browser-api/index.js.map +1 -0
- package/dist/browser-api/index.mjs +2 -0
- package/dist/browser-api/index.mjs.map +1 -0
- package/dist/browser-api/storage/index.d.mts +4 -0
- package/dist/browser-api/storage/index.d.ts +4 -0
- package/dist/browser-api/storage/index.js +2 -0
- package/dist/browser-api/storage/index.js.map +1 -0
- package/dist/browser-api/storage/index.mjs +2 -0
- package/dist/browser-api/storage/index.mjs.map +1 -0
- package/dist/browser-api/storage/useLocalStorage.d.mts +11 -0
- package/dist/browser-api/storage/useLocalStorage.d.ts +11 -0
- package/dist/browser-api/storage/useLocalStorage.js +2 -0
- package/dist/browser-api/storage/useLocalStorage.js.map +1 -0
- package/dist/browser-api/storage/useLocalStorage.mjs +2 -0
- package/dist/browser-api/storage/useLocalStorage.mjs.map +1 -0
- package/dist/browser-api/storage/useSessionStorage.d.mts +11 -0
- package/dist/browser-api/storage/useSessionStorage.d.ts +11 -0
- package/dist/browser-api/storage/useSessionStorage.js +2 -0
- package/dist/browser-api/storage/useSessionStorage.js.map +1 -0
- package/dist/browser-api/storage/useSessionStorage.mjs +2 -0
- package/dist/browser-api/storage/useSessionStorage.mjs.map +1 -0
- package/dist/browser-api/storage/useStorage.d.mts +12 -0
- package/dist/browser-api/storage/useStorage.d.ts +12 -0
- package/dist/browser-api/storage/useStorage.js +2 -0
- package/dist/browser-api/storage/useStorage.js.map +1 -0
- package/dist/browser-api/storage/useStorage.mjs +2 -0
- package/dist/browser-api/storage/useStorage.mjs.map +1 -0
- package/dist/browser-api/useIsPortrait.d.mts +10 -0
- package/dist/browser-api/useIsPortrait.d.ts +10 -0
- package/dist/browser-api/useIsPortrait.js +2 -0
- package/dist/browser-api/useIsPortrait.js.map +1 -0
- package/dist/browser-api/useIsPortrait.mjs +2 -0
- package/dist/browser-api/useIsPortrait.mjs.map +1 -0
- package/dist/browser-api/useMediaQuery.d.mts +11 -0
- package/dist/browser-api/useMediaQuery.d.ts +11 -0
- package/dist/browser-api/useMediaQuery.js +2 -0
- package/dist/browser-api/useMediaQuery.js.map +1 -0
- package/dist/browser-api/useMediaQuery.mjs +2 -0
- package/dist/browser-api/useMediaQuery.mjs.map +1 -0
- package/dist/dom-api/index.d.mts +2 -0
- package/dist/dom-api/index.d.ts +2 -0
- package/dist/dom-api/index.js +2 -0
- package/dist/dom-api/index.js.map +1 -0
- package/dist/dom-api/index.mjs +2 -0
- package/dist/dom-api/index.mjs.map +1 -0
- package/dist/dom-api/useFocusTrap.d.mts +15 -0
- package/dist/dom-api/useFocusTrap.d.ts +15 -0
- package/dist/dom-api/useFocusTrap.js +2 -0
- package/dist/dom-api/useFocusTrap.js.map +1 -0
- package/dist/dom-api/useFocusTrap.mjs +2 -0
- package/dist/dom-api/useFocusTrap.mjs.map +1 -0
- package/dist/dom-api/useScrollBlock.d.mts +8 -0
- package/dist/dom-api/useScrollBlock.d.ts +8 -0
- package/dist/dom-api/useScrollBlock.js +2 -0
- package/dist/dom-api/useScrollBlock.js.map +1 -0
- package/dist/dom-api/useScrollBlock.mjs +2 -0
- package/dist/dom-api/useScrollBlock.mjs.map +1 -0
- package/dist/eslint.d.mts +11 -0
- package/dist/eslint.d.ts +11 -0
- package/dist/eslint.js +2 -0
- package/dist/eslint.js.map +1 -0
- package/dist/eslint.mjs +2 -0
- package/dist/eslint.mjs.map +1 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/dist/misc/index.d.mts +3 -0
- package/dist/misc/index.d.ts +3 -0
- package/dist/misc/index.js +2 -0
- package/dist/misc/index.js.map +1 -0
- package/dist/misc/index.mjs +2 -0
- package/dist/misc/index.mjs.map +1 -0
- package/dist/misc/useIsClient.d.mts +8 -0
- package/dist/misc/useIsClient.d.ts +8 -0
- package/dist/misc/useIsClient.js +2 -0
- package/dist/misc/useIsClient.js.map +1 -0
- package/dist/misc/useIsClient.mjs +2 -0
- package/dist/misc/useIsClient.mjs.map +1 -0
- package/dist/misc/useIsFirstRender.d.mts +9 -0
- package/dist/misc/useIsFirstRender.d.ts +9 -0
- package/dist/misc/useIsFirstRender.js +2 -0
- package/dist/misc/useIsFirstRender.js.map +1 -0
- package/dist/misc/useIsFirstRender.mjs +2 -0
- package/dist/misc/useIsFirstRender.mjs.map +1 -0
- package/dist/misc/useUpdateEffect.d.mts +9 -0
- package/dist/misc/useUpdateEffect.d.ts +9 -0
- package/dist/misc/useUpdateEffect.js +2 -0
- package/dist/misc/useUpdateEffect.js.map +1 -0
- package/dist/misc/useUpdateEffect.mjs +2 -0
- package/dist/misc/useUpdateEffect.mjs.map +1 -0
- package/license.md +21 -0
- package/package.json +144 -0
package/README.md
ADDED
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
# React Hooks 🪝
|
|
2
|
+
|
|
3
|
+
[![NPM Latest Version][version-badge]][npm-url] [![Coverage Status][coverage-badge]][coverage-url] [![Socket Status][socket-badge]][socket-url] [![NPM Monthly Downloads][downloads-badge]][npm-url] [![Dependencies][deps-badge]][deps-url]
|
|
4
|
+
|
|
5
|
+
[![GitHub Sponsor][sponsor-badge]][sponsor-url]
|
|
6
|
+
|
|
7
|
+
[version-badge]: https://img.shields.io/npm/v/%40alessiofrittoli%2Freact-hooks
|
|
8
|
+
[npm-url]: https://npmjs.org/package/%40alessiofrittoli%2Freact-hooks
|
|
9
|
+
[coverage-badge]: https://coveralls.io/repos/github/alessiofrittoli/react-hooks/badge.svg
|
|
10
|
+
[coverage-url]: https://coveralls.io/github/alessiofrittoli/react-hooks
|
|
11
|
+
[socket-badge]: https://socket.dev/api/badge/npm/package/@alessiofrittoli/react-hooks
|
|
12
|
+
[socket-url]: https://socket.dev/npm/package/@alessiofrittoli/react-hooks/overview
|
|
13
|
+
[downloads-badge]: https://img.shields.io/npm/dm/%40alessiofrittoli%2Freact-hooks.svg
|
|
14
|
+
[deps-badge]: https://img.shields.io/librariesio/release/npm/%40alessiofrittoli%2Freact-hooks
|
|
15
|
+
[deps-url]: https://libraries.io/npm/%40alessiofrittoli%2Freact-hooks
|
|
16
|
+
|
|
17
|
+
[sponsor-badge]: https://img.shields.io/static/v1?label=Fund%20this%20package&message=%E2%9D%A4&logo=GitHub&color=%23DB61A2
|
|
18
|
+
[sponsor-url]: https://github.com/sponsors/alessiofrittoli
|
|
19
|
+
|
|
20
|
+
## TypeScript React utility Hooks
|
|
21
|
+
|
|
22
|
+
### Table of Contents
|
|
23
|
+
|
|
24
|
+
- [Getting started](#getting-started)
|
|
25
|
+
- [ESLint Configuration](#eslint-configuration)
|
|
26
|
+
- [API Reference](#api-reference)
|
|
27
|
+
- [Browser API](#browser-api)
|
|
28
|
+
- [DOM API](#dom-api)
|
|
29
|
+
- [Miscellaneous](#miscellaneous)
|
|
30
|
+
- [Development](#development)
|
|
31
|
+
- [Install depenendencies](#install-depenendencies)
|
|
32
|
+
- [Build the source code](#build-the-source-code)
|
|
33
|
+
- [ESLint](#eslint)
|
|
34
|
+
- [Jest](#jest)
|
|
35
|
+
- [Contributing](#contributing)
|
|
36
|
+
- [Security](#security)
|
|
37
|
+
- [Credits](#made-with-)
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
### Getting started
|
|
42
|
+
|
|
43
|
+
Run the following command to start using `react-hooks` in your projects:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm i @alessiofrittoli/react-hooks
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
or using `pnpm`
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pnpm i @alessiofrittoli/react-hooks
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### ESLint Configuration
|
|
58
|
+
|
|
59
|
+
This library may define and exports hooks that requires additional ESLint configuration for your project such as [`useUpdateEffect`](#useupdateeffect).
|
|
60
|
+
|
|
61
|
+
Simply imports recommended configuration from `@alessiofrittoli/react-hooks/eslint` and add them to your ESLint configuration like so:
|
|
62
|
+
|
|
63
|
+
```mjs
|
|
64
|
+
import { config as AFReactHooksEslint } from '@alessiofrittoli/react-hooks/eslint'
|
|
65
|
+
|
|
66
|
+
/** @type {import('eslint').Linter.Config[]} */
|
|
67
|
+
const config = [
|
|
68
|
+
...AFReactHooksEslint.recommended,
|
|
69
|
+
// ... other configurations
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
export default config
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### API Reference
|
|
79
|
+
|
|
80
|
+
#### Browser API
|
|
81
|
+
|
|
82
|
+
##### Storage
|
|
83
|
+
|
|
84
|
+
The following storage hooks use Storage Utilities from [`@alessiofrittoli/web-utils`](https://npmjs.com/package/@alessiofrittoli/web-utils#storage-utilities) adding a React oriented implementation.
|
|
85
|
+
|
|
86
|
+
###### `useStorage`
|
|
87
|
+
|
|
88
|
+
Easly handle Local or Session Storage State.
|
|
89
|
+
|
|
90
|
+
<details>
|
|
91
|
+
|
|
92
|
+
<summary style="cursor:pointer">Type parameters</summary>
|
|
93
|
+
|
|
94
|
+
| Parameter | Type | Default | Description |
|
|
95
|
+
|-----------|------|---------|-------------|
|
|
96
|
+
| `T` | `any` | `string` | A custom type applied to the stored item. |
|
|
97
|
+
|
|
98
|
+
</details>
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
<details>
|
|
103
|
+
|
|
104
|
+
<summary style="cursor:pointer">Parameters</summary>
|
|
105
|
+
|
|
106
|
+
| Parameter | Type | Default | Description |
|
|
107
|
+
|-----------|------|---------|-------------|
|
|
108
|
+
| `key` | `string` | - | The storage item key. |
|
|
109
|
+
| `initialValue` | `T` | - | The storage item initial value. |
|
|
110
|
+
| `type` | `local\|session` | local | (Optional) The storage API to use. |
|
|
111
|
+
|
|
112
|
+
</details>
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
<details>
|
|
117
|
+
|
|
118
|
+
<summary style="cursor:pointer">Returns</summary>
|
|
119
|
+
|
|
120
|
+
Type: `[ Value<T>, SetValue<Value<T>> ]`
|
|
121
|
+
|
|
122
|
+
A tuple with the stored item value or initial value and the setter function.
|
|
123
|
+
|
|
124
|
+
</details>
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
<details>
|
|
129
|
+
|
|
130
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
131
|
+
|
|
132
|
+
###### Importing the hooks
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import {
|
|
136
|
+
useStorage, useLocalStorage, useSessionStorage
|
|
137
|
+
} from '@alessiofrittoli/react-hooks'
|
|
138
|
+
// or
|
|
139
|
+
import {
|
|
140
|
+
useStorage, useLocalStorage, useSessionStorage
|
|
141
|
+
} from '@alessiofrittoli/react-hooks/browser-api'
|
|
142
|
+
// or
|
|
143
|
+
import {
|
|
144
|
+
useStorage, useLocalStorage, useSessionStorage
|
|
145
|
+
} from '@alessiofrittoli/react-hooks/browser-api/storage'
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
###### Reading item value from storage
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
'use client'
|
|
154
|
+
|
|
155
|
+
import { useStorage } from '@alessiofrittoli/react-hooks/browser-api/storage'
|
|
156
|
+
|
|
157
|
+
type Locale = 'it' | 'en'
|
|
158
|
+
|
|
159
|
+
const storage = 'local' // or 'session'
|
|
160
|
+
const defaultLocale = 'it'
|
|
161
|
+
|
|
162
|
+
export const SomeComponent: React.FC = () => {
|
|
163
|
+
|
|
164
|
+
const [ userLocale ] = useStorage<Locale>( 'user-locale', defaultLocale, storage )
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
...
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
###### Updating storage item value
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
'use client'
|
|
179
|
+
|
|
180
|
+
import { useCallback } from 'react'
|
|
181
|
+
import { useStorage } from '@alessiofrittoli/react-hooks/browser-api/storage'
|
|
182
|
+
|
|
183
|
+
type Locale = 'it' | 'en'
|
|
184
|
+
|
|
185
|
+
const storage = 'local' // or 'session'
|
|
186
|
+
const defaultLocale = 'it'
|
|
187
|
+
|
|
188
|
+
export const LanguageSwitcher: React.FC = () => {
|
|
189
|
+
|
|
190
|
+
const [ userLocale, setUserLocale ] = useStorage<Locale>( 'user-locale', defaultLocale, storage )
|
|
191
|
+
|
|
192
|
+
const clickHandler = useCallback( () => {
|
|
193
|
+
setUserLocale( 'en' )
|
|
194
|
+
}, [ setUserLocale ] )
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
...
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
###### Deleting storage item
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
'use client'
|
|
209
|
+
|
|
210
|
+
import { useCallback } from 'react'
|
|
211
|
+
import { useStorage } from '@alessiofrittoli/react-hooks/browser-api/storage'
|
|
212
|
+
|
|
213
|
+
type Locale = 'it' | 'en'
|
|
214
|
+
|
|
215
|
+
const storage = 'local' // or 'session'
|
|
216
|
+
const defaultLocale = 'it'
|
|
217
|
+
|
|
218
|
+
export const LanguageSwitcher: React.FC = () => {
|
|
219
|
+
|
|
220
|
+
const [ userLocale, setUserLocale ] = useStorage<Locale>( 'user-locale', defaultLocale, storage )
|
|
221
|
+
|
|
222
|
+
const deleteHandler = useCallback( () => {
|
|
223
|
+
setUserLocale( null )
|
|
224
|
+
// or
|
|
225
|
+
setUserLocale( undefined )
|
|
226
|
+
// or
|
|
227
|
+
setUserLocale( '' )
|
|
228
|
+
}, [ setUserLocale ] )
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
...
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
</details>
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
###### `useLocalStorage`
|
|
242
|
+
|
|
243
|
+
Shortcut React Hook for [`useStorage`](#usestorage).
|
|
244
|
+
|
|
245
|
+
Applies the same API Reference.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
###### `useSessionStorage`
|
|
250
|
+
|
|
251
|
+
Shortcut React Hook for [`useStorage`](#usestorage).
|
|
252
|
+
|
|
253
|
+
Applies the same API Reference.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
##### `useMediaQuery`
|
|
258
|
+
|
|
259
|
+
Get Document Media matches and listen for changes.
|
|
260
|
+
|
|
261
|
+
<details>
|
|
262
|
+
|
|
263
|
+
<summary style="cursor:pointer">Parameters</summary>
|
|
264
|
+
|
|
265
|
+
| Parameter | Type | Description |
|
|
266
|
+
|-----------|----------|-------------|
|
|
267
|
+
| `query` | `string` | A string specifying the media query to parse into a `MediaQueryList`. |
|
|
268
|
+
|
|
269
|
+
</details>
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
<details>
|
|
274
|
+
|
|
275
|
+
<summary style="cursor:pointer">Returns</summary>
|
|
276
|
+
|
|
277
|
+
Type: `boolean`
|
|
278
|
+
|
|
279
|
+
- `true` if the document currently matches the media query list.
|
|
280
|
+
- `false` otherwise.
|
|
281
|
+
|
|
282
|
+
</details>
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
<details>
|
|
287
|
+
|
|
288
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
289
|
+
|
|
290
|
+
###### Importing the hook
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
import { useMediaQuery } from '@alessiofrittoli/react-hooks'
|
|
294
|
+
// or
|
|
295
|
+
import { useMediaQuery } from '@alessiofrittoli/react-hooks/browser-api'
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
###### Check if user device prefers dark color scheme
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
const isDarkOS = useMediaQuery( '(prefers-color-scheme: dark)' )
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
</details>
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
##### `useIsPortrait`
|
|
311
|
+
|
|
312
|
+
Check if device is portrait oriented.
|
|
313
|
+
|
|
314
|
+
React State get updated when device orientation changes.
|
|
315
|
+
|
|
316
|
+
<details>
|
|
317
|
+
|
|
318
|
+
<summary style="cursor:pointer">Returns</summary>
|
|
319
|
+
|
|
320
|
+
Type: `boolean`
|
|
321
|
+
|
|
322
|
+
- `true` if the device is portrait oriented.
|
|
323
|
+
- `false` otherwise.
|
|
324
|
+
|
|
325
|
+
</details>
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
<details>
|
|
330
|
+
|
|
331
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
332
|
+
|
|
333
|
+
###### Importing the hook
|
|
334
|
+
|
|
335
|
+
```tsx
|
|
336
|
+
import { useIsPortrait } from '@alessiofrittoli/react-hooks'
|
|
337
|
+
// or
|
|
338
|
+
import { useIsPortrait } from '@alessiofrittoli/react-hooks/browser-api'
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
###### Check if user device is in landscape
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
const isLandscape = ! useIsPortrait()
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
</details>
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
#### DOM API
|
|
354
|
+
|
|
355
|
+
##### `useScrollBlock`
|
|
356
|
+
|
|
357
|
+
Prevent Element overflow.
|
|
358
|
+
|
|
359
|
+
<details>
|
|
360
|
+
|
|
361
|
+
<summary style="cursor:pointer">Parameters</summary>
|
|
362
|
+
|
|
363
|
+
| Parameter | Type | Default | Description |
|
|
364
|
+
|-----------|------|---------|-------------|
|
|
365
|
+
| `target` | `React.RefObject<HTMLElement\|null>` | `Document.documentElement` | (Optional) The React RefObject target HTMLElement. |
|
|
366
|
+
|
|
367
|
+
</details>
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
<details>
|
|
372
|
+
|
|
373
|
+
<summary style="cursor:pointer">Returns</summary>
|
|
374
|
+
|
|
375
|
+
Type: `[ () => void, () => void ]`
|
|
376
|
+
|
|
377
|
+
A tuple with block and restore scroll callbacks.
|
|
378
|
+
|
|
379
|
+
</details>
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
<details>
|
|
384
|
+
|
|
385
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
386
|
+
|
|
387
|
+
###### Importing the hook
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
import { useScrollBlock } from '@alessiofrittoli/react-hooks'
|
|
391
|
+
// or
|
|
392
|
+
import { useScrollBlock } from '@alessiofrittoli/react-hooks/dom-api'
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
###### Block Document Overflow
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
const [ blockScroll, restoreScroll ] = useScrollBlock()
|
|
401
|
+
|
|
402
|
+
const openPopUpHandler = useCallback( () => {
|
|
403
|
+
...
|
|
404
|
+
blockScroll()
|
|
405
|
+
}, [ blockScroll ] )
|
|
406
|
+
|
|
407
|
+
const closePopUpHandler = useCallback( () => {
|
|
408
|
+
...
|
|
409
|
+
restoreScroll()
|
|
410
|
+
}, [ restoreScroll ] )
|
|
411
|
+
|
|
412
|
+
...
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
###### Block HTML Element Overflow
|
|
418
|
+
|
|
419
|
+
```tsx
|
|
420
|
+
const elementRef = useRef<HTMLDivElement>( null )
|
|
421
|
+
|
|
422
|
+
const [ blockScroll, restoreScroll ] = useScrollBlock( elementRef )
|
|
423
|
+
|
|
424
|
+
const scrollBlockHandler = useCallback( () => {
|
|
425
|
+
...
|
|
426
|
+
blockScroll()
|
|
427
|
+
}, [ blockScroll ] )
|
|
428
|
+
|
|
429
|
+
const scrollRestoreHandler = useCallback( () => {
|
|
430
|
+
...
|
|
431
|
+
restoreScroll()
|
|
432
|
+
}, [ restoreScroll ] )
|
|
433
|
+
|
|
434
|
+
...
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
</details>
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
##### `useFocusTrap`
|
|
442
|
+
|
|
443
|
+
Trap focus inside the given HTML Element.
|
|
444
|
+
|
|
445
|
+
This comes pretty handy when rendering a modal that shouldn't be closed without a user required action.
|
|
446
|
+
|
|
447
|
+
<details>
|
|
448
|
+
|
|
449
|
+
<summary style="cursor:pointer">Parameters</summary>
|
|
450
|
+
|
|
451
|
+
| Parameter | Type | Description |
|
|
452
|
+
|-----------|------|-------------|
|
|
453
|
+
| `target` | `React.RefObject<HTMLElement\|null>` | The target HTMLElement React RefObject to trap focus within. |
|
|
454
|
+
| | | If no target is given, you must provide the target HTMLElement when calling `setFocusTrap`. |
|
|
455
|
+
|
|
456
|
+
</details>
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
<details>
|
|
461
|
+
|
|
462
|
+
<summary style="cursor:pointer">Returns</summary>
|
|
463
|
+
|
|
464
|
+
Type: `readonly [ SetFocusTrap, RestoreFocusTrap ]`
|
|
465
|
+
|
|
466
|
+
A tuple containing:
|
|
467
|
+
|
|
468
|
+
- `setFocusTrap`: A function to enable the focus trap. Optionally accept an HTMLElement as target.
|
|
469
|
+
- `restoreFocusTrap`: A function to restore the previous focus state.
|
|
470
|
+
|
|
471
|
+
</details>
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
<details>
|
|
476
|
+
|
|
477
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
478
|
+
|
|
479
|
+
###### Importing the hook
|
|
480
|
+
|
|
481
|
+
```tsx
|
|
482
|
+
import { useFocusTrap } from '@alessiofrittoli/react-hooks'
|
|
483
|
+
// or
|
|
484
|
+
import { useFocusTrap } from '@alessiofrittoli/react-hooks/dom-api'
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
###### Defining the target on hook initialization
|
|
490
|
+
|
|
491
|
+
```tsx
|
|
492
|
+
const modalRef = useRef<HTMLDivElement>( null )
|
|
493
|
+
const [ setFocusTrap, restoreFocusTrap ] = useFocusTrap( modalRef )
|
|
494
|
+
|
|
495
|
+
const modalOpenHandler = useCallback( () => {
|
|
496
|
+
if ( ! modalRef.current ) return
|
|
497
|
+
// ... open modal
|
|
498
|
+
setFocusTrap()
|
|
499
|
+
modalRef.current.focus() // focus the dialog so next tab will focus the next element inside the modal
|
|
500
|
+
}, [ setFocusTrap ] )
|
|
501
|
+
|
|
502
|
+
const modalCloseHandler = useCallback( () => {
|
|
503
|
+
// ... close modal
|
|
504
|
+
restoreFocusTrap() // cancel focus trap and restore focus to the last active element before enablig the focus trap
|
|
505
|
+
}, [ restoreFocusTrap ] )
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
###### Defining the target ondemand
|
|
511
|
+
|
|
512
|
+
```tsx
|
|
513
|
+
const modalRef = useRef<HTMLDivElement>( null )
|
|
514
|
+
const modal2Ref = useRef<HTMLDivElement>( null )
|
|
515
|
+
const [ setFocusTrap, restoreFocusTrap ] = useFocusTrap()
|
|
516
|
+
|
|
517
|
+
const modalOpenHandler = useCallback( () => {
|
|
518
|
+
if ( ! modalRef.current ) return
|
|
519
|
+
// ... open modal
|
|
520
|
+
setFocusTrap( modalRef.current )
|
|
521
|
+
modalRef.current.focus()
|
|
522
|
+
}, [ setFocusTrap ] )
|
|
523
|
+
|
|
524
|
+
const modal2OpenHandler = useCallback( () => {
|
|
525
|
+
if ( ! modal2Ref.current ) return
|
|
526
|
+
// ... open modal
|
|
527
|
+
setFocusTrap( modal2Ref.current )
|
|
528
|
+
modal2Ref.current.focus()
|
|
529
|
+
}, [ setFocusTrap ] )
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
</details>
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
#### Miscellaneous
|
|
537
|
+
|
|
538
|
+
##### `useIsClient`
|
|
539
|
+
|
|
540
|
+
Check if the React Hook or Component where this hook is executed is running in a browser environment.
|
|
541
|
+
|
|
542
|
+
This is pretty usefull to avoid hydration errors.
|
|
543
|
+
|
|
544
|
+
<details>
|
|
545
|
+
|
|
546
|
+
<summary style="cursor:pointer">Returns</summary>
|
|
547
|
+
|
|
548
|
+
Type: `boolean`
|
|
549
|
+
|
|
550
|
+
- `true` if the React Hook or Component is running in a browser environment.
|
|
551
|
+
- `false` otherwise.
|
|
552
|
+
|
|
553
|
+
</details>
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
<details>
|
|
558
|
+
|
|
559
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
560
|
+
|
|
561
|
+
###### Importing the hook
|
|
562
|
+
|
|
563
|
+
```tsx
|
|
564
|
+
import { useIsClient } from '@alessiofrittoli/react-hooks'
|
|
565
|
+
// or
|
|
566
|
+
import { useIsClient } from '@alessiofrittoli/react-hooks/misc'
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
###### Basic usage
|
|
570
|
+
|
|
571
|
+
```tsx
|
|
572
|
+
'use client'
|
|
573
|
+
|
|
574
|
+
import { useIsClient } from '@alessiofrittoli/react-hooks/misc'
|
|
575
|
+
|
|
576
|
+
export const ClientComponent: React.FC = () => {
|
|
577
|
+
|
|
578
|
+
const isClient = useIsClient()
|
|
579
|
+
|
|
580
|
+
return (
|
|
581
|
+
<div>Running { ! isClient ? 'server' : 'client' }-side</div>
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
</details>
|
|
588
|
+
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
##### `useIsFirstRender`
|
|
592
|
+
|
|
593
|
+
Check if is first React Hook/Component render.
|
|
594
|
+
|
|
595
|
+
<details>
|
|
596
|
+
|
|
597
|
+
<summary style="cursor:pointer">Returns</summary>
|
|
598
|
+
|
|
599
|
+
Type: `boolean`
|
|
600
|
+
|
|
601
|
+
- `true` at the mount time.
|
|
602
|
+
- `false` otherwise.
|
|
603
|
+
|
|
604
|
+
Note that if the React Hook/Component has no state updates, `useIsFirstRender` will always return `true`.
|
|
605
|
+
|
|
606
|
+
</details>
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
<details>
|
|
611
|
+
|
|
612
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
613
|
+
|
|
614
|
+
###### Importing the hook
|
|
615
|
+
|
|
616
|
+
```tsx
|
|
617
|
+
import { useIsFirstRender } from '@alessiofrittoli/react-hooks'
|
|
618
|
+
// or
|
|
619
|
+
import { useIsFirstRender } from '@alessiofrittoli/react-hooks/misc'
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
###### Basic usage
|
|
623
|
+
|
|
624
|
+
```tsx
|
|
625
|
+
'use client'
|
|
626
|
+
|
|
627
|
+
import { useIsFirstRender } from '@alessiofrittoli/react-hooks/misc'
|
|
628
|
+
|
|
629
|
+
export const ClientComponent: React.FC = () => {
|
|
630
|
+
|
|
631
|
+
const isFirstRender = useIsFirstRender()
|
|
632
|
+
const [ counter, setCounter ] = useState( 0 )
|
|
633
|
+
|
|
634
|
+
useEffect( () => {
|
|
635
|
+
const intv = setInterval( () => {
|
|
636
|
+
setCounter( prev => prev + 1 )
|
|
637
|
+
}, 1000 )
|
|
638
|
+
return () => clearInterval( intv )
|
|
639
|
+
}, [] )
|
|
640
|
+
|
|
641
|
+
return (
|
|
642
|
+
<div>
|
|
643
|
+
{ isFirstRender ? 'First render' : 'Subsequent render' }
|
|
644
|
+
<hr />
|
|
645
|
+
{ counter }
|
|
646
|
+
</div>
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
</details>
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
##### `useUpdateEffect`
|
|
657
|
+
|
|
658
|
+
Modified version of `useEffect` that skips the first render.
|
|
659
|
+
|
|
660
|
+
<details>
|
|
661
|
+
|
|
662
|
+
<summary style="cursor:pointer">Parameters</summary>
|
|
663
|
+
|
|
664
|
+
| Parameter | Type | Description |
|
|
665
|
+
|-----------|------------------------|-------------|
|
|
666
|
+
| `effect` | `React.EffectCallback` | Imperative function that can return a cleanup function. |
|
|
667
|
+
| `deps` | `React.DependencyList` | If present, effect will only activate if the values in the list change. |
|
|
668
|
+
|
|
669
|
+
</details>
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
<details>
|
|
674
|
+
|
|
675
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
676
|
+
|
|
677
|
+
###### Importing the hook
|
|
678
|
+
|
|
679
|
+
```tsx
|
|
680
|
+
import { useUpdateEffect } from '@alessiofrittoli/react-hooks'
|
|
681
|
+
// or
|
|
682
|
+
import { useUpdateEffect } from '@alessiofrittoli/react-hooks/misc'
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
###### Basic usage
|
|
686
|
+
|
|
687
|
+
```tsx
|
|
688
|
+
'use client'
|
|
689
|
+
|
|
690
|
+
import { useEffect, useState } from 'react'
|
|
691
|
+
import { useUpdateEffect } from '@alessiofrittoli/react-hooks/misc'
|
|
692
|
+
|
|
693
|
+
export const ClientComponent: React.FC = () => {
|
|
694
|
+
|
|
695
|
+
const [ count, setCount ] = useState( 0 )
|
|
696
|
+
|
|
697
|
+
useEffect( () => {
|
|
698
|
+
const intv = setInterval( () => {
|
|
699
|
+
setCount( prev => prev + 1 )
|
|
700
|
+
}, 1000 )
|
|
701
|
+
return () => clearInterval( intv )
|
|
702
|
+
}, [] )
|
|
703
|
+
|
|
704
|
+
useEffect( () => {
|
|
705
|
+
console.log( 'useEffect', count ) // starts from 0
|
|
706
|
+
return () => {
|
|
707
|
+
console.log( 'useEffect - clean up', count ) // starts from 0
|
|
708
|
+
}
|
|
709
|
+
}, [ count ] )
|
|
710
|
+
|
|
711
|
+
useUpdateEffect( () => {
|
|
712
|
+
console.log( 'useUpdateEffect', count ) // starts from 1
|
|
713
|
+
return () => {
|
|
714
|
+
console.log( 'useUpdateEffect - clean up', count ) // starts from 1
|
|
715
|
+
}
|
|
716
|
+
}, [ count ] )
|
|
717
|
+
|
|
718
|
+
return (
|
|
719
|
+
<div>{ count }</div>
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
</details>
|
|
726
|
+
|
|
727
|
+
---
|
|
728
|
+
|
|
729
|
+
### Development
|
|
730
|
+
|
|
731
|
+
#### Install depenendencies
|
|
732
|
+
|
|
733
|
+
```bash
|
|
734
|
+
npm install
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
or using `pnpm`
|
|
738
|
+
|
|
739
|
+
```bash
|
|
740
|
+
pnpm i
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
#### Build the source code
|
|
744
|
+
|
|
745
|
+
Run the following command to test and build code for distribution.
|
|
746
|
+
|
|
747
|
+
```bash
|
|
748
|
+
pnpm build
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
#### [ESLint](https://www.npmjs.com/package/eslint)
|
|
752
|
+
|
|
753
|
+
warnings / errors check.
|
|
754
|
+
|
|
755
|
+
```bash
|
|
756
|
+
pnpm lint
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
#### [Jest](https://npmjs.com/package/jest)
|
|
760
|
+
|
|
761
|
+
Run all the defined test suites by running the following:
|
|
762
|
+
|
|
763
|
+
```bash
|
|
764
|
+
# Run tests and watch file changes.
|
|
765
|
+
pnpm test:watch
|
|
766
|
+
|
|
767
|
+
# Run tests in a CI environment.
|
|
768
|
+
pnpm test:ci
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
- See [`package.json`](./package.json) file scripts for more info.
|
|
772
|
+
|
|
773
|
+
Run tests with coverage.
|
|
774
|
+
|
|
775
|
+
An HTTP server is then started to serve coverage files from `./coverage` folder.
|
|
776
|
+
|
|
777
|
+
⚠️ You may see a blank page the first time you run this command. Simply refresh the browser to see the updates.
|
|
778
|
+
|
|
779
|
+
```bash
|
|
780
|
+
test:coverage:serve
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
---
|
|
784
|
+
|
|
785
|
+
### Contributing
|
|
786
|
+
|
|
787
|
+
Contributions are truly welcome!
|
|
788
|
+
|
|
789
|
+
Please refer to the [Contributing Doc](./CONTRIBUTING.md) for more information on how to start contributing to this project.
|
|
790
|
+
|
|
791
|
+
Help keep this project up to date with [GitHub Sponsor][sponsor-url].
|
|
792
|
+
|
|
793
|
+
[![GitHub Sponsor][sponsor-badge]][sponsor-url]
|
|
794
|
+
|
|
795
|
+
---
|
|
796
|
+
|
|
797
|
+
### Security
|
|
798
|
+
|
|
799
|
+
If you believe you have found a security vulnerability, we encourage you to **_responsibly disclose this and NOT open a public issue_**. We will investigate all legitimate reports. Email `security@alessiofrittoli.it` to disclose any security vulnerabilities.
|
|
800
|
+
|
|
801
|
+
### Made with ☕
|
|
802
|
+
|
|
803
|
+
<table style='display:flex;gap:20px;'>
|
|
804
|
+
<tbody>
|
|
805
|
+
<tr>
|
|
806
|
+
<td>
|
|
807
|
+
<img alt="avatar" src='https://avatars.githubusercontent.com/u/35973186' style='width:60px;border-radius:50%;object-fit:contain;'>
|
|
808
|
+
</td>
|
|
809
|
+
<td>
|
|
810
|
+
<table style='display:flex;gap:2px;flex-direction:column;'>
|
|
811
|
+
<tbody>
|
|
812
|
+
<tr>
|
|
813
|
+
<td>
|
|
814
|
+
<a href='https://github.com/alessiofrittoli' target='_blank' rel='noopener'>Alessio Frittoli</a>
|
|
815
|
+
</td>
|
|
816
|
+
</tr>
|
|
817
|
+
<tr>
|
|
818
|
+
<td>
|
|
819
|
+
<small>
|
|
820
|
+
<a href='https://alessiofrittoli.it' target='_blank' rel='noopener'>https://alessiofrittoli.it</a> |
|
|
821
|
+
<a href='mailto:info@alessiofrittoli.it' target='_blank' rel='noopener'>info@alessiofrittoli.it</a>
|
|
822
|
+
</small>
|
|
823
|
+
</td>
|
|
824
|
+
</tr>
|
|
825
|
+
</tbody>
|
|
826
|
+
</table>
|
|
827
|
+
</td>
|
|
828
|
+
</tr>
|
|
829
|
+
</tbody>
|
|
830
|
+
</table>
|