@toptal/picasso-utils 1.0.1-alpha-fx-4861-find-missing-deep-imports-in-staff-portal-ca4ef823d.4082 → 1.0.1-alpha-fx-4861-find-missing-deep-imports-in-staff-portal-c6a9da7e9.4084
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/dist-package/tsconfig.tsbuildinfo +1 -0
- package/package.json +5 -10
- package/src/index.ts +1 -0
- package/src/utils/Breakpoints/story/Breakpoints.example.tsx +49 -0
- package/src/utils/Breakpoints/story/MediaQueries.example.tsx +18 -0
- package/src/utils/Breakpoints/story/index.jsx +60 -0
- package/src/utils/Breakpoints/story/useBreakpoint.example.tsx +17 -0
- package/src/utils/Breakpoints/story/useScreens.example.tsx +60 -0
- package/src/utils/Colors/index.ts +2 -0
- package/src/utils/Colors/story/Default.example.tsx +59 -0
- package/src/utils/Colors/story/HowToUse.example.tsx +14 -0
- package/src/utils/Colors/story/index.jsx +21 -0
- package/src/utils/Formatters/format-amount.ts +34 -0
- package/src/utils/Formatters/index.ts +3 -0
- package/src/utils/Formatters/story/amount.example.tsx +63 -0
- package/src/utils/Formatters/story/index.jsx +16 -0
- package/src/utils/Formatters/test.ts +102 -0
- package/src/utils/Gradients/index.ts +2 -0
- package/src/utils/Gradients/story/Default.example.tsx +30 -0
- package/src/utils/Gradients/story/HowToUse.example.tsx +26 -0
- package/src/utils/Gradients/story/index.jsx +26 -0
- package/src/utils/Modal/__snapshots__/test.tsx.snap +209 -0
- package/src/utils/Modal/index.ts +2 -0
- package/src/utils/Modal/modal-manager.ts +27 -0
- package/src/utils/Modal/test.tsx +86 -0
- package/src/utils/Modal/use-modal.tsx +17 -0
- package/src/utils/Shadows/story/Default.example.tsx +45 -0
- package/src/utils/Shadows/story/HowToUse.example.tsx +19 -0
- package/src/utils/Shadows/story/index.jsx +19 -0
- package/src/utils/Transitions/Rotate180/Rotate180.tsx +44 -0
- package/src/utils/Transitions/Rotate180/__snapshots__/test.tsx.snap +29 -0
- package/src/utils/Transitions/Rotate180/index.ts +4 -0
- package/src/utils/Transitions/Rotate180/story/Default.example.tsx +22 -0
- package/src/utils/Transitions/Rotate180/styles.ts +11 -0
- package/src/utils/Transitions/Rotate180/test.tsx +26 -0
- package/src/utils/Transitions/index.ts +2 -0
- package/src/utils/Transitions/story/index.jsx +13 -0
- package/src/utils/Utils/story/Browser.example.tsx +28 -0
- package/src/utils/Utils/story/Colors.example.tsx +29 -0
- package/src/utils/Utils/story/Generic.example.tsx +30 -0
- package/src/utils/Utils/story/React.example.tsx +32 -0
- package/src/utils/Utils/story/Strings.example.tsx +33 -0
- package/src/utils/__tests__/use-page-scroll-lock.test.tsx +117 -0
- package/src/utils/capitalize.ts +1 -0
- package/src/utils/constants.ts +1 -0
- package/src/utils/disable-unsupported-props.ts +45 -0
- package/src/utils/forward-ref.ts +38 -0
- package/src/utils/get-name-initials.ts +15 -0
- package/src/utils/get-react-node-text-content.ts +36 -0
- package/src/utils/index.ts +92 -0
- package/src/utils/is-boolean.ts +3 -0
- package/src/utils/is-number.ts +3 -0
- package/src/utils/is-overflown.ts +20 -0
- package/src/utils/is-pointer-device.ts +9 -0
- package/src/utils/is-string.ts +3 -0
- package/src/utils/is-substring.ts +4 -0
- package/src/utils/kebab-to-camel-case.ts +7 -0
- package/src/utils/loader-palette.ts +9 -0
- package/src/utils/monads.ts +1 -0
- package/src/utils/noop.ts +1 -0
- package/src/utils/sum.ts +4 -0
- package/src/utils/test.tsx +372 -0
- package/src/utils/to-title-case.ts +14 -0
- package/src/utils/unsafe-error-log.ts +5 -0
- package/src/utils/use-combined-refs.ts +26 -0
- package/src/utils/use-deprecation-warnings.ts +56 -0
- package/src/utils/use-multiple-forward-refs.ts +37 -0
- package/src/utils/use-page-scroll-lock.ts +64 -0
- package/src/utils/use-safe-state.ts +25 -0
- package/src/utils/use-width-of.ts +26 -0
- package/src/utils/useBoolean/index.ts +1 -0
- package/src/utils/useBoolean/test.tsx +25 -0
- package/src/utils/useBoolean/use-boolean.ts +30 -0
- package/src/utils/useInterval/index.ts +1 -0
- package/src/utils/useInterval/test.ts +61 -0
- package/src/utils/useInterval/use-interval.ts +46 -0
- package/src/utils/useMouseEnter/index.ts +1 -0
- package/src/utils/useMouseEnter/test.ts +51 -0
- package/src/utils/useMouseEnter/use-mouse-enter.ts +30 -0
- package/src/utils/useOnScreen/index.ts +1 -0
- package/src/utils/useOnScreen/use-on-screen.ts +50 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { render, act } from '@toptal/picasso-test-utils'
|
|
2
|
+
import type { Ref } from 'react'
|
|
3
|
+
import React, { createRef, useEffect } from 'react'
|
|
4
|
+
|
|
5
|
+
import type { ReferenceObject } from './index'
|
|
6
|
+
import {
|
|
7
|
+
capitalize,
|
|
8
|
+
getNameInitials,
|
|
9
|
+
isBoolean,
|
|
10
|
+
isNumber,
|
|
11
|
+
isString,
|
|
12
|
+
isSubstring,
|
|
13
|
+
toTitleCase,
|
|
14
|
+
kebabToCamelCase,
|
|
15
|
+
useCombinedRefs,
|
|
16
|
+
useWidthOf,
|
|
17
|
+
useSafeState,
|
|
18
|
+
forwardRef,
|
|
19
|
+
documentable,
|
|
20
|
+
disableUnsupportedProps,
|
|
21
|
+
sum,
|
|
22
|
+
getReactNodeTextContent,
|
|
23
|
+
isBrowser,
|
|
24
|
+
} from './index'
|
|
25
|
+
import unsafeErrorLog from './unsafe-error-log'
|
|
26
|
+
|
|
27
|
+
jest.mock('./unsafe-error-log')
|
|
28
|
+
|
|
29
|
+
describe('capitalize', () => {
|
|
30
|
+
it('should capitalize first letter', () => {
|
|
31
|
+
const string = capitalize('test string')
|
|
32
|
+
|
|
33
|
+
expect(string).toBe('Test string')
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('getNameInitials', () => {
|
|
38
|
+
it('should extract first letters', () => {
|
|
39
|
+
expect(getNameInitials('John Doe')).toBe('JD')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should ignore extra spaces', () => {
|
|
43
|
+
expect(getNameInitials(' John Doe ')).toBe('JD')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should ignore single letter middle names', () => {
|
|
47
|
+
expect(getNameInitials('John T Doe')).toBe('JD')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should extract up to 3 letters', () => {
|
|
51
|
+
expect(getNameInitials('John Doe John Doe')).toBe('JDJ')
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('isBoolean', () => {
|
|
56
|
+
it('should return true for booleans', () => {
|
|
57
|
+
expect(isBoolean(true)).toBe(true)
|
|
58
|
+
expect(isBoolean(false)).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should return false for other types', () => {
|
|
62
|
+
expect(isBoolean(1)).toBe(false)
|
|
63
|
+
expect(isBoolean('1')).toBe(false)
|
|
64
|
+
expect(isBoolean({})).toBe(false)
|
|
65
|
+
expect(isBoolean(null)).toBe(false)
|
|
66
|
+
expect(isBoolean(undefined)).toBe(false)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('isNumber', () => {
|
|
71
|
+
it('should return true for numbers', () => {
|
|
72
|
+
expect(isNumber(1)).toBe(true)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should return false for other types', () => {
|
|
76
|
+
expect(isNumber(true)).toBe(false)
|
|
77
|
+
expect(isNumber('1')).toBe(false)
|
|
78
|
+
expect(isNumber({})).toBe(false)
|
|
79
|
+
expect(isNumber(null)).toBe(false)
|
|
80
|
+
expect(isNumber(undefined)).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('isString', () => {
|
|
85
|
+
it('should return true for strings', () => {
|
|
86
|
+
expect(isString('')).toBe(true)
|
|
87
|
+
expect(isString('1')).toBe(true)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should return false for other types', () => {
|
|
91
|
+
expect(isString(true)).toBe(false)
|
|
92
|
+
expect(isString(1)).toBe(false)
|
|
93
|
+
expect(isString({})).toBe(false)
|
|
94
|
+
expect(isString(null)).toBe(false)
|
|
95
|
+
expect(isString(undefined)).toBe(false)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('toTitleCase', () => {
|
|
100
|
+
it('should convert strings', () => {
|
|
101
|
+
expect(toTitleCase('ab bc')).toBe('Ab Bc')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should ignore react nodes', () => {
|
|
105
|
+
const node = <div>ab bc</div>
|
|
106
|
+
|
|
107
|
+
expect(toTitleCase(node)).toBe(node)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('isSubstring', () => {
|
|
112
|
+
it('should check if a string contains another ignoring case', () => {
|
|
113
|
+
expect(isSubstring('TEST', 'a test string')).toBe(true)
|
|
114
|
+
expect(isSubstring('test', 'a test string')).toBe(true)
|
|
115
|
+
expect(isSubstring('test word', 'a test string')).toBe(false)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe('kebabToCamelCase', () => {
|
|
120
|
+
it('should convert kebab to camel case', () => {
|
|
121
|
+
expect(kebabToCamelCase('a-test-string')).toBe('aTestString')
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('sum', () => {
|
|
126
|
+
it('returns the total of all numbers in an array', () => {
|
|
127
|
+
expect(sum([0, 1, 2, 3])).toBe(6)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const TestUseCombinedRefs = ({ refs }: { refs: Ref<HTMLDivElement>[] }) => {
|
|
132
|
+
return <div ref={useCombinedRefs(...refs)} />
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
describe('useCombinedRefs', () => {
|
|
136
|
+
it('should combine object and function refs', async () => {
|
|
137
|
+
const refObject = createRef<HTMLDivElement>()
|
|
138
|
+
const refFunction = jest.fn()
|
|
139
|
+
|
|
140
|
+
render(<TestUseCombinedRefs refs={[refObject, refFunction]} />)
|
|
141
|
+
|
|
142
|
+
expect(refObject.current).toBeDefined()
|
|
143
|
+
expect(refFunction.mock.calls[0][0]).toBeDefined()
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const TestForwardRef = documentable(
|
|
148
|
+
forwardRef(
|
|
149
|
+
<T extends string>(
|
|
150
|
+
{ item }: { ref: Ref<HTMLDivElement>; item: T },
|
|
151
|
+
ref: Ref<HTMLDivElement>
|
|
152
|
+
) => {
|
|
153
|
+
return <div ref={ref}>{item}</div>
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
describe('forwardRef', () => {
|
|
159
|
+
it('should forward a ref with generic component', () => {
|
|
160
|
+
const ref = createRef<HTMLDivElement>()
|
|
161
|
+
|
|
162
|
+
render(<TestForwardRef ref={ref} item='item' />)
|
|
163
|
+
|
|
164
|
+
expect(ref.current).toBeDefined()
|
|
165
|
+
expect(ref.current?.tagName).toBe('DIV')
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const TestUseSafeState = () => {
|
|
170
|
+
const [state, setState] = useSafeState('initial')
|
|
171
|
+
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
setTimeout(() => setState('changed'), 100)
|
|
174
|
+
}, [setState])
|
|
175
|
+
|
|
176
|
+
return <div>{state}</div>
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
describe('useSafeState', () => {
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
jest.useFakeTimers()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
afterEach(() => {
|
|
185
|
+
jest.useRealTimers()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('should use initial state', () => {
|
|
189
|
+
const { queryByText } = render(<TestUseSafeState />)
|
|
190
|
+
|
|
191
|
+
expect(queryByText('initial')).toBeInTheDocument()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should set state in an async effect', () => {
|
|
195
|
+
const { queryByText } = render(<TestUseSafeState />)
|
|
196
|
+
|
|
197
|
+
act(() => {
|
|
198
|
+
jest.runAllTimers()
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
expect(queryByText('changed')).toBeInTheDocument()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should not throw when state is set after unmounting', () => {
|
|
205
|
+
const { unmount } = render(<TestUseSafeState />)
|
|
206
|
+
|
|
207
|
+
unmount()
|
|
208
|
+
|
|
209
|
+
expect(() =>
|
|
210
|
+
act(() => {
|
|
211
|
+
jest.runAllTimers()
|
|
212
|
+
})
|
|
213
|
+
).not.toThrow()
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
const TestUseWidthOf = ({ element }: { element: ReferenceObject }) => {
|
|
218
|
+
const width = useWidthOf(element)
|
|
219
|
+
|
|
220
|
+
return <div>{width}</div>
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
describe('useWidthOf', () => {
|
|
224
|
+
it('should measure width of passed element', () => {
|
|
225
|
+
const rect = {
|
|
226
|
+
top: 10,
|
|
227
|
+
left: 10,
|
|
228
|
+
right: 110,
|
|
229
|
+
bottom: 30,
|
|
230
|
+
width: 100,
|
|
231
|
+
height: 20,
|
|
232
|
+
}
|
|
233
|
+
const element = {
|
|
234
|
+
getBoundingClientRect: () => rect,
|
|
235
|
+
} as ReferenceObject
|
|
236
|
+
|
|
237
|
+
const { queryByText } = render(<TestUseWidthOf element={element} />)
|
|
238
|
+
const message = queryByText('100px')
|
|
239
|
+
|
|
240
|
+
expect(message).toBeInTheDocument()
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
const TestDisableUnsupportedProps = (props: {
|
|
245
|
+
type: string
|
|
246
|
+
max?: number | string
|
|
247
|
+
}) => {
|
|
248
|
+
const { type, max } = disableUnsupportedProps(
|
|
249
|
+
'TestDisableUnsupportedProps',
|
|
250
|
+
props,
|
|
251
|
+
{
|
|
252
|
+
featureProps: {
|
|
253
|
+
type: 'text',
|
|
254
|
+
},
|
|
255
|
+
unsupportedProps: {
|
|
256
|
+
max: '',
|
|
257
|
+
},
|
|
258
|
+
}
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return <input type={type} max={max} />
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
describe('disableUnsupportedProps', () => {
|
|
265
|
+
it('should render with supported props', () => {
|
|
266
|
+
const { getByRole } = render(
|
|
267
|
+
<TestDisableUnsupportedProps type='number' max={2} />
|
|
268
|
+
)
|
|
269
|
+
const input = getByRole('spinbutton')
|
|
270
|
+
|
|
271
|
+
expect(input).toHaveProperty('type', 'number')
|
|
272
|
+
expect(input).toHaveProperty('max', '2')
|
|
273
|
+
expect(unsafeErrorLog).not.toHaveBeenCalled()
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('should override unsupported props and warn the developer', () => {
|
|
277
|
+
const { getByRole } = render(
|
|
278
|
+
<TestDisableUnsupportedProps type='text' max={2} />
|
|
279
|
+
)
|
|
280
|
+
const input = getByRole('textbox')
|
|
281
|
+
|
|
282
|
+
expect(input).toHaveProperty('type', 'text')
|
|
283
|
+
expect(input).toHaveProperty('max', '')
|
|
284
|
+
expect(unsafeErrorLog).toHaveBeenCalledWith(
|
|
285
|
+
'TestDisableUnsupportedProps doesn\'t support: max props when used with {"type":"text"}'
|
|
286
|
+
)
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
describe('isBrowser', () => {
|
|
291
|
+
let windowSpy: any
|
|
292
|
+
|
|
293
|
+
beforeEach(() => {
|
|
294
|
+
windowSpy = jest.spyOn(window, 'window', 'get')
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
afterEach(() => {
|
|
298
|
+
windowSpy?.mockRestore()
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('should return true if window is undefined', () => {
|
|
302
|
+
windowSpy?.mockImplementation(() => undefined)
|
|
303
|
+
|
|
304
|
+
expect(isBrowser()).toBe(false)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('should return true for window is not undefined', () => {
|
|
308
|
+
windowSpy.mockImplementation(() => ({}))
|
|
309
|
+
|
|
310
|
+
expect(isBrowser()).toBe(true)
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
describe('getRectNodeTextContent', () => {
|
|
315
|
+
describe('when getting text from string node', () => {
|
|
316
|
+
it.each(['foo', ''])(
|
|
317
|
+
"returns its content original content, value: '%s'",
|
|
318
|
+
txt => {
|
|
319
|
+
expect(getReactNodeTextContent(txt)).toBe(txt)
|
|
320
|
+
}
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
it('strips spaces from start and end of the text', () => {
|
|
324
|
+
expect(getReactNodeTextContent(' foo ')).toBe('foo')
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
describe('when getting text from a number node', () => {
|
|
329
|
+
it.each([42, -12, Infinity, NaN, -0])(
|
|
330
|
+
'returns its content original content, value: %s',
|
|
331
|
+
num => {
|
|
332
|
+
expect(getReactNodeTextContent(num)).toBe(String(num))
|
|
333
|
+
}
|
|
334
|
+
)
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe('when getting text from a array node', () => {
|
|
338
|
+
it('returns the contents of its elements joined by space', () => {
|
|
339
|
+
expect(getReactNodeTextContent(['foo', <div>bar</div>, 45])).toBe(
|
|
340
|
+
'foo bar 45'
|
|
341
|
+
)
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
describe('when getting text from non printable nodes in react', () => {
|
|
346
|
+
it.each([null, undefined, true, false])(
|
|
347
|
+
'returns an empty string, value: "%s"',
|
|
348
|
+
nonOp => {
|
|
349
|
+
expect(getReactNodeTextContent(nonOp)).toBe('')
|
|
350
|
+
}
|
|
351
|
+
)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
describe('when getting text from a complex node', () => {
|
|
355
|
+
it("returns it's children content recursively", () => {
|
|
356
|
+
expect(
|
|
357
|
+
getReactNodeTextContent(
|
|
358
|
+
<div>
|
|
359
|
+
<h1>Title</h1>
|
|
360
|
+
<caption>
|
|
361
|
+
<span>Subtitle</span>
|
|
362
|
+
</caption>
|
|
363
|
+
<p>
|
|
364
|
+
<img />
|
|
365
|
+
Amet quidem quod doloribus dignissimos
|
|
366
|
+
</p>
|
|
367
|
+
</div>
|
|
368
|
+
)
|
|
369
|
+
).toBe('Title Subtitle Amet quidem quod doloribus dignissimos')
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import titleCase from 'ap-style-title-case'
|
|
3
|
+
|
|
4
|
+
import isString from './is-string'
|
|
5
|
+
|
|
6
|
+
const toTitleCase = (node: ReactNode) => {
|
|
7
|
+
if (!node || !isString(node)) {
|
|
8
|
+
return node
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return titleCase(node as string)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default toTitleCase
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RefObject, Ref } from 'react'
|
|
2
|
+
import { useRef, useEffect } from 'react'
|
|
3
|
+
|
|
4
|
+
const useCombinedRefs = <T>(...refs: (RefObject<T> | Ref<T>)[]) => {
|
|
5
|
+
const targetRef = useRef<T>(null)
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
refs.forEach(ref => {
|
|
9
|
+
if (!ref) {
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (typeof ref === 'function') {
|
|
14
|
+
ref(targetRef.current)
|
|
15
|
+
} else {
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
17
|
+
// @ts-ignore
|
|
18
|
+
ref.current = targetRef.current
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
}, [refs])
|
|
22
|
+
|
|
23
|
+
return targetRef
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default useCombinedRefs
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import unsafeErrorLog from './unsafe-error-log'
|
|
4
|
+
|
|
5
|
+
interface UseDeprecationWarningArgs {
|
|
6
|
+
description?: string
|
|
7
|
+
name: string
|
|
8
|
+
newName?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const useDeprecationWarning = ({
|
|
12
|
+
description,
|
|
13
|
+
name,
|
|
14
|
+
newName,
|
|
15
|
+
}: UseDeprecationWarningArgs) => {
|
|
16
|
+
const message =
|
|
17
|
+
`'${name}' component is deprecated and will be removed in the next major release of Picasso.'` +
|
|
18
|
+
`${newName ? ` Please use '${newName}' instead.` : ''}` +
|
|
19
|
+
`${description ? `\n${description}` : ''}`
|
|
20
|
+
|
|
21
|
+
React.useEffect(() => {
|
|
22
|
+
unsafeErrorLog(message)
|
|
23
|
+
}, [message])
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface UsePropDeprecationWarningArgs<P> {
|
|
27
|
+
props: P
|
|
28
|
+
componentName: string
|
|
29
|
+
description?: string
|
|
30
|
+
name: keyof P
|
|
31
|
+
newName?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const usePropDeprecationWarning = <P extends {}>({
|
|
35
|
+
props,
|
|
36
|
+
componentName,
|
|
37
|
+
description,
|
|
38
|
+
name,
|
|
39
|
+
newName,
|
|
40
|
+
}: UsePropDeprecationWarningArgs<P>) => {
|
|
41
|
+
const propName = String(name)
|
|
42
|
+
const message =
|
|
43
|
+
`${componentName}'s '${propName}' prop is deprecated and will be removed in the next major release of Picasso.` +
|
|
44
|
+
`${newName ? ` Please use '${newName}' instead.` : ''}` +
|
|
45
|
+
`${description ? `\n${description}` : ''}`
|
|
46
|
+
|
|
47
|
+
const isDeprecatedPropUsed = name in props
|
|
48
|
+
|
|
49
|
+
React.useEffect(() => {
|
|
50
|
+
if (isDeprecatedPropUsed) {
|
|
51
|
+
unsafeErrorLog(message)
|
|
52
|
+
}
|
|
53
|
+
}, [isDeprecatedPropUsed, message])
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { useDeprecationWarning, usePropDeprecationWarning }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ForwardedRef } from 'react'
|
|
2
|
+
import { useCallback } from 'react'
|
|
3
|
+
|
|
4
|
+
const forwardRef = <T>(ref: ForwardedRef<T>, value: T) => {
|
|
5
|
+
if (typeof ref === 'function') {
|
|
6
|
+
ref(value)
|
|
7
|
+
} else if (ref) {
|
|
8
|
+
ref.current = value
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* This hook allows to forward ref to multiple holders.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
*
|
|
17
|
+
* const ref1 = useRef(null)
|
|
18
|
+
* const ref2 = useRef(null)
|
|
19
|
+
*
|
|
20
|
+
* const ref = useMultipleForwardRefs([ref1, ref2])
|
|
21
|
+
*
|
|
22
|
+
* <div ref={ref} />
|
|
23
|
+
*
|
|
24
|
+
* console.log(ref1.current) // <div />
|
|
25
|
+
* console.log(ref2.current) // <div />
|
|
26
|
+
*/
|
|
27
|
+
const useMultipleForwardRefs = <T>(refs: ForwardedRef<T>[]) =>
|
|
28
|
+
useCallback(
|
|
29
|
+
(refValue: T) => {
|
|
30
|
+
for (const ref of refs) {
|
|
31
|
+
forwardRef(ref, refValue)
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
[...refs]
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
export default useMultipleForwardRefs
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isBrowser } from '@toptal/picasso-shared'
|
|
2
|
+
import { useEffect, useMemo } from 'react'
|
|
3
|
+
|
|
4
|
+
const layers = new Set<number>()
|
|
5
|
+
let scrollLock: { prevHtmlOverflow: string } | undefined = undefined
|
|
6
|
+
|
|
7
|
+
export const usePageScrollLock = (isLocked: boolean) => {
|
|
8
|
+
const layerId = useMemo(generateLayerId, [])
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (isLocked) {
|
|
12
|
+
layers.add(layerId)
|
|
13
|
+
syncPageScrollLock()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return () => {
|
|
17
|
+
layers.delete(layerId)
|
|
18
|
+
syncPageScrollLock()
|
|
19
|
+
}
|
|
20
|
+
}, [layerId, isLocked])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const generateLayerId = (() => {
|
|
24
|
+
let count = 0
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
count = count + 1
|
|
28
|
+
|
|
29
|
+
return count
|
|
30
|
+
}
|
|
31
|
+
})()
|
|
32
|
+
|
|
33
|
+
const syncPageScrollLock = () => {
|
|
34
|
+
if (layers.size > 0) {
|
|
35
|
+
addPageScrollLock()
|
|
36
|
+
} else {
|
|
37
|
+
removePageScrollLock()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const addPageScrollLock = () => {
|
|
42
|
+
if (!isBrowser()) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!scrollLock) {
|
|
47
|
+
scrollLock = {
|
|
48
|
+
prevHtmlOverflow: document.getElementsByTagName('html')[0].style.overflow,
|
|
49
|
+
}
|
|
50
|
+
document.getElementsByTagName('html')[0].style.overflow = 'hidden'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const removePageScrollLock = () => {
|
|
55
|
+
if (!isBrowser()) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (scrollLock) {
|
|
60
|
+
document.getElementsByTagName('html')[0].style.overflow =
|
|
61
|
+
scrollLock.prevHtmlOverflow
|
|
62
|
+
scrollLock = undefined
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useRef, useState, useCallback, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
const useSafeState = <S>(initState: S | (() => S)) => {
|
|
4
|
+
const [state, unsafeSetState] = useState<S>(initState)
|
|
5
|
+
|
|
6
|
+
const isMounted = useRef(false)
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
isMounted.current = true
|
|
10
|
+
|
|
11
|
+
return () => {
|
|
12
|
+
isMounted.current = false
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const setState: typeof unsafeSetState = useCallback(newState => {
|
|
17
|
+
if (isMounted.current) {
|
|
18
|
+
unsafeSetState(newState)
|
|
19
|
+
}
|
|
20
|
+
}, [])
|
|
21
|
+
|
|
22
|
+
return [state, setState]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default useSafeState as typeof useState
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useIsomorphicLayoutEffect } from '@toptal/picasso-shared'
|
|
3
|
+
|
|
4
|
+
export interface ReferenceObject {
|
|
5
|
+
offsetParent?: Element
|
|
6
|
+
getBoundingClientRect: () => ClientRect
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const useWidthOf = <T extends ReferenceObject>(element: T | null) => {
|
|
10
|
+
const [menuWidth, setMenuWidth] = useState<string | undefined>()
|
|
11
|
+
|
|
12
|
+
const offsetParent = element?.offsetParent
|
|
13
|
+
|
|
14
|
+
useIsomorphicLayoutEffect(() => {
|
|
15
|
+
if (!element) {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
const { width } = element.getBoundingClientRect()
|
|
19
|
+
|
|
20
|
+
setMenuWidth(`${width}px`)
|
|
21
|
+
}, [element, offsetParent])
|
|
22
|
+
|
|
23
|
+
return menuWidth
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default useWidthOf
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './use-boolean'
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react-hooks'
|
|
2
|
+
|
|
3
|
+
import useBoolean from './use-boolean'
|
|
4
|
+
|
|
5
|
+
describe('useBoolean', () => {
|
|
6
|
+
it('sets state correctly', () => {
|
|
7
|
+
const { result } = renderHook(() => useBoolean())
|
|
8
|
+
const [, open, close, toggle] = result.current
|
|
9
|
+
|
|
10
|
+
expect(result.current[0]).toBeFalsy()
|
|
11
|
+
|
|
12
|
+
act(() => open())
|
|
13
|
+
|
|
14
|
+
expect(result.current[0]).toBeTruthy()
|
|
15
|
+
|
|
16
|
+
act(() => close())
|
|
17
|
+
expect(result.current[0]).toBeFalsy()
|
|
18
|
+
|
|
19
|
+
act(() => toggle())
|
|
20
|
+
expect(result.current[0]).toBeTruthy()
|
|
21
|
+
|
|
22
|
+
act(() => toggle())
|
|
23
|
+
expect(result.current[0]).toBeFalsy()
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
/** Set value to true */
|
|
4
|
+
type SetTruthy = () => void
|
|
5
|
+
/** Set value to false */
|
|
6
|
+
type SetFalsy = () => void
|
|
7
|
+
/** Toggle the value */
|
|
8
|
+
type Toggle = () => void
|
|
9
|
+
type IsTruthy = boolean
|
|
10
|
+
|
|
11
|
+
type UseBooleanType = (
|
|
12
|
+
defaultValue?: boolean
|
|
13
|
+
) => [IsTruthy, SetTruthy, SetFalsy, Toggle]
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
*
|
|
17
|
+
* @param defaultValue?: Boolean
|
|
18
|
+
* @returns [isTruthy, setTruthy, setFalsy, toggle]
|
|
19
|
+
*/
|
|
20
|
+
const useBoolean: UseBooleanType = (defaultValue = false) => {
|
|
21
|
+
const [isTruthy, setBoolean] = useState(defaultValue)
|
|
22
|
+
|
|
23
|
+
const setTruthy: SetTruthy = () => setBoolean(true)
|
|
24
|
+
const setFalsy: SetFalsy = () => setBoolean(false)
|
|
25
|
+
const toggle: Toggle = () => setBoolean(value => !value)
|
|
26
|
+
|
|
27
|
+
return [isTruthy, setTruthy, setFalsy, toggle]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default useBoolean
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './use-interval'
|