@contember/react-utils 1.2.0-alpha.0 → 1.2.0-alpha.1
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/development/context/createNonNullableContextFactory.js +17 -0
- package/dist/development/context/createNonNullableContextFactory.js.map +1 -0
- package/dist/development/debug-context/Constants.js +25 -0
- package/dist/development/debug-context/Constants.js.map +1 -0
- package/dist/development/debug-context/Context.js +67 -0
- package/dist/development/debug-context/Context.js.map +1 -0
- package/dist/development/hooks/unwrapRefValue.js +11 -0
- package/dist/development/hooks/unwrapRefValue.js.map +1 -0
- package/dist/development/hooks/useAddClassNameDuringResize.js +34 -0
- package/dist/development/hooks/useAddClassNameDuringResize.js.map +1 -0
- package/dist/development/hooks/useComposeRef.js +31 -0
- package/dist/development/hooks/useComposeRef.js.map +1 -0
- package/dist/development/hooks/useCountLayoutSlotChildren.js +31 -0
- package/dist/development/hooks/useCountLayoutSlotChildren.js.map +1 -0
- package/dist/development/hooks/useElementSize.js +47 -0
- package/dist/development/hooks/useElementSize.js.map +1 -0
- package/dist/development/hooks/useEventHandler.js +19 -0
- package/dist/development/hooks/useEventHandler.js.map +1 -0
- package/dist/development/hooks/useExpectSameValueReference.js +15 -0
- package/dist/development/hooks/useExpectSameValueReference.js.map +1 -0
- package/dist/development/hooks/useOnElementMutation.js +35 -0
- package/dist/development/hooks/useOnElementMutation.js.map +1 -0
- package/dist/development/hooks/useOnElementResize.js +44 -0
- package/dist/development/hooks/useOnElementResize.js.map +1 -0
- package/dist/development/hooks/useOnScrollWithin.js +50 -0
- package/dist/development/hooks/useOnScrollWithin.js.map +1 -0
- package/dist/development/hooks/useOnWindowResize.js +36 -0
- package/dist/development/hooks/useOnWindowResize.js.map +1 -0
- package/dist/development/hooks/useScrollOffsets.js +68 -0
- package/dist/development/hooks/useScrollOffsets.js.map +1 -0
- package/dist/development/hooks/useStoredState.js.map +1 -1
- package/dist/development/hooks/useUpdatedRef.js +10 -0
- package/dist/development/hooks/useUpdatedRef.js.map +1 -0
- package/dist/development/hooks/useWindowSize.js +20 -0
- package/dist/development/hooks/useWindowSize.js.map +1 -0
- package/dist/development/index.js +39 -1
- package/dist/development/index.js.map +1 -1
- package/dist/production/context/createNonNullableContextFactory.js +17 -0
- package/dist/production/context/createNonNullableContextFactory.js.map +1 -0
- package/dist/production/debug-context/Constants.js +25 -0
- package/dist/production/debug-context/Constants.js.map +1 -0
- package/dist/production/debug-context/Context.js +67 -0
- package/dist/production/debug-context/Context.js.map +1 -0
- package/dist/production/hooks/unwrapRefValue.js +11 -0
- package/dist/production/hooks/unwrapRefValue.js.map +1 -0
- package/dist/production/hooks/useAddClassNameDuringResize.js +34 -0
- package/dist/production/hooks/useAddClassNameDuringResize.js.map +1 -0
- package/dist/production/hooks/useComposeRef.js +31 -0
- package/dist/production/hooks/useComposeRef.js.map +1 -0
- package/dist/production/hooks/useCountLayoutSlotChildren.js +31 -0
- package/dist/production/hooks/useCountLayoutSlotChildren.js.map +1 -0
- package/dist/production/hooks/useElementSize.js +47 -0
- package/dist/production/hooks/useElementSize.js.map +1 -0
- package/dist/production/hooks/useEventHandler.js +19 -0
- package/dist/production/hooks/useEventHandler.js.map +1 -0
- package/dist/production/hooks/useExpectSameValueReference.js +15 -0
- package/dist/production/hooks/useExpectSameValueReference.js.map +1 -0
- package/dist/production/hooks/useOnElementMutation.js +35 -0
- package/dist/production/hooks/useOnElementMutation.js.map +1 -0
- package/dist/production/hooks/useOnElementResize.js +44 -0
- package/dist/production/hooks/useOnElementResize.js.map +1 -0
- package/dist/production/hooks/useOnScrollWithin.js +50 -0
- package/dist/production/hooks/useOnScrollWithin.js.map +1 -0
- package/dist/production/hooks/useOnWindowResize.js +36 -0
- package/dist/production/hooks/useOnWindowResize.js.map +1 -0
- package/dist/production/hooks/useScrollOffsets.js +68 -0
- package/dist/production/hooks/useScrollOffsets.js.map +1 -0
- package/dist/production/hooks/useStoredState.js.map +1 -1
- package/dist/production/hooks/useUpdatedRef.js +10 -0
- package/dist/production/hooks/useUpdatedRef.js.map +1 -0
- package/dist/production/hooks/useWindowSize.js +20 -0
- package/dist/production/hooks/useWindowSize.js.map +1 -0
- package/dist/production/index.js +39 -1
- package/dist/production/index.js.map +1 -1
- package/dist/types/context/createNonNullableContextFactory.d.ts +3 -0
- package/dist/types/context/createNonNullableContextFactory.d.ts.map +1 -0
- package/dist/types/context/index.d.ts +2 -0
- package/dist/types/context/index.d.ts.map +1 -0
- package/dist/types/debug-context/Constants.d.ts +6 -0
- package/dist/types/debug-context/Constants.d.ts.map +1 -0
- package/dist/types/debug-context/Context.d.ts +15 -0
- package/dist/types/debug-context/Context.d.ts.map +1 -0
- package/dist/types/debug-context/Types.d.ts +13 -0
- package/dist/types/debug-context/Types.d.ts.map +1 -0
- package/dist/types/debug-context/index.d.ts +4 -0
- package/dist/types/debug-context/index.d.ts.map +1 -0
- package/dist/types/hooks/index.d.ts +13 -0
- package/dist/types/hooks/index.d.ts.map +1 -1
- package/dist/types/hooks/unwrapRefValue.d.ts +9 -0
- package/dist/types/hooks/unwrapRefValue.d.ts.map +1 -0
- package/dist/types/hooks/useAddClassNameDuringResize.d.ts +13 -0
- package/dist/types/hooks/useAddClassNameDuringResize.d.ts.map +1 -0
- package/dist/types/hooks/useComposeRef.d.ts +16 -0
- package/dist/types/hooks/useComposeRef.d.ts.map +1 -0
- package/dist/types/hooks/useCountLayoutSlotChildren.d.ts +5 -0
- package/dist/types/hooks/useCountLayoutSlotChildren.d.ts.map +1 -0
- package/dist/types/hooks/useElementSize.d.ts +12 -0
- package/dist/types/hooks/useElementSize.d.ts.map +1 -0
- package/dist/types/hooks/useEventHandler.d.ts +10 -0
- package/dist/types/hooks/useEventHandler.d.ts.map +1 -0
- package/dist/types/hooks/useExpectSameValueReference.d.ts +2 -0
- package/dist/types/hooks/useExpectSameValueReference.d.ts.map +1 -0
- package/dist/types/hooks/useOnElementMutation.d.ts +3 -0
- package/dist/types/hooks/useOnElementMutation.d.ts.map +1 -0
- package/dist/types/hooks/useOnElementResize.d.ts +3 -0
- package/dist/types/hooks/useOnElementResize.d.ts.map +1 -0
- package/dist/types/hooks/useOnScrollWithin.d.ts +10 -0
- package/dist/types/hooks/useOnScrollWithin.d.ts.map +1 -0
- package/dist/types/hooks/useOnWindowResize.d.ts +2 -0
- package/dist/types/hooks/useOnWindowResize.d.ts.map +1 -0
- package/dist/types/hooks/useScrollOffsets.d.ts +14 -0
- package/dist/types/hooks/useScrollOffsets.d.ts.map +1 -0
- package/dist/types/hooks/useStoredState.d.ts +2 -3
- package/dist/types/hooks/useStoredState.d.ts.map +1 -1
- package/dist/types/hooks/useUpdatedRef.d.ts +8 -0
- package/dist/types/hooks/useUpdatedRef.d.ts.map +1 -0
- package/dist/types/hooks/useWindowSize.d.ts +5 -0
- package/dist/types/hooks/useWindowSize.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dist/types/types/PropsWithRequiredChildren.d.ts +5 -0
- package/dist/types/types/PropsWithRequiredChildren.d.ts.map +1 -0
- package/dist/types/{types.d.ts → types/Serializable.d.ts} +1 -1
- package/dist/types/types/Serializable.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +3 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/package.json +5 -1
- package/src/context/createNonNullableContextFactory.ts +19 -0
- package/src/context/index.ts +1 -0
- package/src/debug-context/Constants.ts +22 -0
- package/src/debug-context/Context.tsx +82 -0
- package/src/debug-context/Types.ts +13 -0
- package/src/debug-context/index.ts +3 -0
- package/src/hooks/index.ts +13 -0
- package/src/hooks/unwrapRefValue.ts +16 -0
- package/src/hooks/useAddClassNameDuringResize.ts +58 -0
- package/src/hooks/useComposeRef.ts +46 -0
- package/src/hooks/useCountLayoutSlotChildren.ts +46 -0
- package/src/hooks/useElementSize.ts +73 -0
- package/src/hooks/useEventHandler.ts +24 -0
- package/src/hooks/useExpectSameValueReference.ts +26 -0
- package/src/hooks/useOnElementMutation.ts +42 -0
- package/src/hooks/useOnElementResize.ts +52 -0
- package/src/hooks/useOnScrollWithin.ts +67 -0
- package/src/hooks/useOnWindowResize.ts +42 -0
- package/src/hooks/useScrollOffsets.ts +88 -0
- package/src/hooks/useStoredState.ts +2 -2
- package/src/hooks/useUpdatedRef.ts +14 -0
- package/src/hooks/useWindowSize.ts +20 -0
- package/src/index.ts +2 -0
- package/src/tsconfig.json +4 -1
- package/src/types/PropsWithRequiredChildren.ts +3 -0
- package/src/types/index.ts +2 -0
- package/dist/types/types.d.ts.map +0 -1
- /package/src/{types.ts → types/Serializable.ts} +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from 'react'
|
|
2
|
+
import { RefObjectOrElement, unwrapRefValue } from './unwrapRefValue'
|
|
3
|
+
import { useOnElementMutation } from './useOnElementMutation'
|
|
4
|
+
|
|
5
|
+
// TODO: Remove when Firefox supports :has selector, e.g.
|
|
6
|
+
// when selectors like `:has(:not(.layout-slot:empty))` work
|
|
7
|
+
// or below issue can be solved more easily:
|
|
8
|
+
//
|
|
9
|
+
// ISSUE:
|
|
10
|
+
// Targeting containers that are visibly empty and hide them
|
|
11
|
+
// or remove paddings without support for :has() is tedious.
|
|
12
|
+
/** @deprecated Check support for :has() for Firefox before using. */
|
|
13
|
+
export function useCountLayoutSlotChildren(
|
|
14
|
+
refOrElement: RefObjectOrElement<HTMLElement | undefined>,
|
|
15
|
+
selector: string = '.layout-slot',
|
|
16
|
+
) {
|
|
17
|
+
const [count, setCount] = useState(Infinity)
|
|
18
|
+
|
|
19
|
+
const countChildren = useRef((element: HTMLElement | null | undefined) => {
|
|
20
|
+
if (element) {
|
|
21
|
+
setCount(element.querySelectorAll(`${selector} > *`).length)
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
useOnElementMutation(refOrElement, () => {
|
|
26
|
+
countChildren.current(unwrapRefValue(refOrElement))
|
|
27
|
+
}, { attributes: true, childList: true, subtree: true })
|
|
28
|
+
|
|
29
|
+
useLayoutEffect(() => {
|
|
30
|
+
countChildren.current(unwrapRefValue(refOrElement))
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return count
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useHasEmptySlotsClassName(
|
|
37
|
+
refOrElement: RefObjectOrElement<HTMLElement | undefined>,
|
|
38
|
+
) {
|
|
39
|
+
const slotChildrenCount = useCountLayoutSlotChildren(refOrElement)
|
|
40
|
+
|
|
41
|
+
if (slotChildrenCount === 0) {
|
|
42
|
+
return 'has-empty-slots'
|
|
43
|
+
} else {
|
|
44
|
+
return 'has-non-empty-slots'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { getSizeFromResizeObserverEntryFactory } from '@contember/utilities'
|
|
2
|
+
import { useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
3
|
+
import { useScopedConsoleRef } from '../debug-context'
|
|
4
|
+
import { RefObjectOrElement, unwrapRefValue } from './unwrapRefValue'
|
|
5
|
+
import { useOnElementResize } from './useOnElementResize'
|
|
6
|
+
|
|
7
|
+
function getElementWidth(element: HTMLElement) {
|
|
8
|
+
return element.offsetWidth
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getElementHeight(element: HTMLElement) {
|
|
12
|
+
return element.offsetHeight
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Measure HTMLElement
|
|
17
|
+
*
|
|
18
|
+
* @param refOrElement - HTMLElement passed directly or indirectly as RefObject
|
|
19
|
+
* @returns Size vector consisting of width and height properties
|
|
20
|
+
*/
|
|
21
|
+
export function useElementSize(
|
|
22
|
+
refOrElement: RefObjectOrElement<HTMLElement>,
|
|
23
|
+
options: ResizeObserverOptions = {},
|
|
24
|
+
timeout: number = 300,
|
|
25
|
+
): { height: number | undefined, width: number | undefined } {
|
|
26
|
+
const { logged } = useScopedConsoleRef('useElementSize').current
|
|
27
|
+
|
|
28
|
+
const { box = 'border-box' } = options
|
|
29
|
+
const getSizeFromResizeObserverEntry = useMemo(() => getSizeFromResizeObserverEntryFactory(box), [box])
|
|
30
|
+
|
|
31
|
+
const refValue = unwrapRefValue(refOrElement)
|
|
32
|
+
const [width, setWidth] = useState<number | undefined>(refValue?.offsetWidth)
|
|
33
|
+
const [height, setHeight] = useState<number | undefined>(refValue?.offsetHeight)
|
|
34
|
+
|
|
35
|
+
logged('dimensions', { width, height })
|
|
36
|
+
|
|
37
|
+
useOnElementResize(refOrElement, entry => {
|
|
38
|
+
const dimensions = logged('useOnElementResize => entry:dimensions', getSizeFromResizeObserverEntry(entry))
|
|
39
|
+
|
|
40
|
+
if (width !== dimensions.width) {
|
|
41
|
+
setWidth(dimensions.width)
|
|
42
|
+
}
|
|
43
|
+
if (height !== dimensions.height) {
|
|
44
|
+
setHeight(dimensions.height)
|
|
45
|
+
}
|
|
46
|
+
}, { box }, timeout)
|
|
47
|
+
|
|
48
|
+
// INTENTIONAL AVOID OF ESLint ERROR: We need measure on every Layout side-effect
|
|
49
|
+
// otherwise the browser will paint and cause layout shifts.
|
|
50
|
+
//
|
|
51
|
+
// React Hook useLayoutEffect contains a call to 'setWidth'. Without a list of dependencies,
|
|
52
|
+
// this can lead to an infinite chain of updates. To fix this, pass [element] as a second argument
|
|
53
|
+
// to the useLayoutEffect Hook.
|
|
54
|
+
//
|
|
55
|
+
// Seems like only pure function lets us measure before the browser has a chance to paint
|
|
56
|
+
const measure = useRef((element: HTMLElement | null) => {
|
|
57
|
+
logged('measure(element)', element)
|
|
58
|
+
|
|
59
|
+
if (element instanceof HTMLElement) {
|
|
60
|
+
setWidth(logged('getElementWidth(element):', getElementWidth(element)))
|
|
61
|
+
setHeight(logged('getElementHeight(element):', getElementHeight(element)))
|
|
62
|
+
} else if (element) {
|
|
63
|
+
throw new Error('Exhaustive error: Expecting element to be instance of HTMLElement or Window')
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
useLayoutEffect(() => {
|
|
68
|
+
const element = unwrapRefValue(refOrElement)
|
|
69
|
+
logged('useLayoutEffect => measure', measure.current(element))
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return { height, width }
|
|
73
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { EventHandler, SyntheticEvent, useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Similar to useEvent with inferred type for the event parameter according to event type
|
|
5
|
+
*
|
|
6
|
+
* @param type - String, keyof DocumentEventMap, e.g. 'click', 'keydown',...
|
|
7
|
+
* @param callback - EventHandler
|
|
8
|
+
* @returns Stable reference callback
|
|
9
|
+
*/
|
|
10
|
+
export function useEventHandler<E extends SyntheticEvent<any>>(type: E['type'], callback: EventHandler<E>): EventHandler<E> {
|
|
11
|
+
const callbackRef = useRef(callback); callbackRef.current = callback
|
|
12
|
+
|
|
13
|
+
const stableHandler = useRef(function (event: any) {
|
|
14
|
+
return callbackRef.current(event)
|
|
15
|
+
}).current
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
return () => {
|
|
19
|
+
callbackRef.current = () => { }
|
|
20
|
+
}
|
|
21
|
+
}, [])
|
|
22
|
+
|
|
23
|
+
return stableHandler
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import deepEqual from 'deep-equal'
|
|
2
|
+
import { useRef } from 'react'
|
|
3
|
+
|
|
4
|
+
// TODO: Unify error behavior with useConstantValueInvariant() and maybe merge into one hook.
|
|
5
|
+
export function useExpectSameValueReference<T>(next: T, shouldThrow: boolean = false) {
|
|
6
|
+
const previous = useRef(next)
|
|
7
|
+
|
|
8
|
+
if (next !== previous.current
|
|
9
|
+
&& typeof next === 'object'
|
|
10
|
+
&& typeof previous.current === 'object'
|
|
11
|
+
&& deepEqual(next, previous.current)
|
|
12
|
+
) {
|
|
13
|
+
if (import.meta.env.DEV) {
|
|
14
|
+
console.error('Previous and next next do not match in reference although match in value', {
|
|
15
|
+
previous: previous.current,
|
|
16
|
+
next,
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (shouldThrow) {
|
|
21
|
+
throw new Error('Expecting same reference for same values')
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
previous.current = next
|
|
26
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import deepEqual from 'deep-equal'
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
3
|
+
import { useScopedConsoleRef } from '../debug-context'
|
|
4
|
+
import { RefObjectOrElement, unwrapRefValue } from './unwrapRefValue'
|
|
5
|
+
|
|
6
|
+
export function useOnElementMutation(
|
|
7
|
+
refOrElement: RefObjectOrElement<HTMLElement | undefined>,
|
|
8
|
+
callback: MutationCallback,
|
|
9
|
+
options: MutationObserverInit = { attributes: true, childList: true, subtree: true },
|
|
10
|
+
) {
|
|
11
|
+
const scopedConsoleRef = useScopedConsoleRef('useOnElementMutation')
|
|
12
|
+
|
|
13
|
+
const callbackRef = useRef(callback); callbackRef.current = callback
|
|
14
|
+
const optionsRef = useRef<MutationObserverInit>(options); optionsRef.current = options
|
|
15
|
+
|
|
16
|
+
const [optionsState, setOptionsState] = useState<MutationObserverInit>(options)
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!deepEqual(optionsRef.current, options)) {
|
|
20
|
+
setOptionsState(options)
|
|
21
|
+
}
|
|
22
|
+
}, [options])
|
|
23
|
+
|
|
24
|
+
useLayoutEffect(() => {
|
|
25
|
+
const element = unwrapRefValue(refOrElement)
|
|
26
|
+
|
|
27
|
+
if (element) {
|
|
28
|
+
function handler(mutations: MutationRecord[], observer: MutationObserver) {
|
|
29
|
+
scopedConsoleRef.current.logged('element.mutations', mutations)
|
|
30
|
+
callbackRef.current(mutations, observer)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const observer = new MutationObserver(handler)
|
|
34
|
+
|
|
35
|
+
observer.observe(element, optionsState)
|
|
36
|
+
|
|
37
|
+
return () => {
|
|
38
|
+
observer.disconnect()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}, [optionsState, refOrElement, scopedConsoleRef])
|
|
42
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from 'react'
|
|
2
|
+
import { useScopedConsoleRef } from '../debug-context'
|
|
3
|
+
import { RefObjectOrElement, unwrapRefValue } from './unwrapRefValue'
|
|
4
|
+
|
|
5
|
+
export function useOnElementResize(
|
|
6
|
+
refOrElement: RefObjectOrElement<HTMLElement | null> | null,
|
|
7
|
+
callback: (entries: ResizeObserverEntry) => void,
|
|
8
|
+
options: ResizeObserverOptions = {},
|
|
9
|
+
timeout: number = 300,
|
|
10
|
+
): void {
|
|
11
|
+
const scopedConsoleRef = useScopedConsoleRef('useOnElementResize')
|
|
12
|
+
|
|
13
|
+
const { box = 'border-box' } = options
|
|
14
|
+
const element = unwrapRefValue(refOrElement)
|
|
15
|
+
const callbackRef = useRef(callback); callbackRef.current = callback
|
|
16
|
+
const lastTimeStamp = useRef<number>(0)
|
|
17
|
+
|
|
18
|
+
useLayoutEffect(() => {
|
|
19
|
+
let timeoutID: number | undefined = undefined
|
|
20
|
+
|
|
21
|
+
function debouncedOnChange([entry]: ResizeObserverEntry[]) {
|
|
22
|
+
const timeStamp = Date.now()
|
|
23
|
+
const delta = timeStamp - lastTimeStamp.current
|
|
24
|
+
|
|
25
|
+
if (delta > timeout) {
|
|
26
|
+
scopedConsoleRef.current.warned('element.resize:immediate', null)
|
|
27
|
+
callbackRef.current(entry)
|
|
28
|
+
lastTimeStamp.current = timeStamp
|
|
29
|
+
} else {
|
|
30
|
+
clearTimeout(timeoutID)
|
|
31
|
+
timeoutID = setTimeout(() => {
|
|
32
|
+
scopedConsoleRef.current.warned('element.resize:debounced', null)
|
|
33
|
+
callbackRef.current(entry)
|
|
34
|
+
lastTimeStamp.current = timeStamp
|
|
35
|
+
}, timeout)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (element instanceof HTMLElement) {
|
|
40
|
+
const resizeObserver = new ResizeObserver(debouncedOnChange)
|
|
41
|
+
|
|
42
|
+
resizeObserver.observe(element, { box })
|
|
43
|
+
|
|
44
|
+
return () => {
|
|
45
|
+
clearTimeout(timeoutID)
|
|
46
|
+
resizeObserver.unobserve(element)
|
|
47
|
+
}
|
|
48
|
+
} else if (element) {
|
|
49
|
+
throw new Error('Exhaustive error: Expecting element to be instance of HTMLElement')
|
|
50
|
+
}
|
|
51
|
+
}, [box, element, scopedConsoleRef, timeout])
|
|
52
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from 'react'
|
|
2
|
+
import { useScopedConsoleRef } from '../debug-context'
|
|
3
|
+
import { RefObjectOrElement, unwrapRefValue } from './unwrapRefValue'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Invokes callback on scroll event within the scroll container
|
|
7
|
+
*
|
|
8
|
+
* @param refOrElement - Scroll container
|
|
9
|
+
* @param callback - Callback that is invoked on scroll event
|
|
10
|
+
* @param interval - Specifies how often should scroll event invoke callback
|
|
11
|
+
*/
|
|
12
|
+
export function useOnScrollWithin(
|
|
13
|
+
refOrElement: RefObjectOrElement<HTMLElement | null> | null,
|
|
14
|
+
callback: (event: Event) => void,
|
|
15
|
+
interval: number = 1000 / 60,
|
|
16
|
+
) {
|
|
17
|
+
const scopedConsoleRef = useScopedConsoleRef('useOnScrollWithin')
|
|
18
|
+
|
|
19
|
+
const callbackRef = useRef(callback); callbackRef.current = callback
|
|
20
|
+
const lastTimeStamp = useRef<number>(0)
|
|
21
|
+
|
|
22
|
+
const element = unwrapRefValue(refOrElement)
|
|
23
|
+
|
|
24
|
+
useLayoutEffect(() => {
|
|
25
|
+
let timeoutID: number | undefined = undefined
|
|
26
|
+
|
|
27
|
+
function debouncedHandler(event: Event) {
|
|
28
|
+
clearTimeout(timeoutID)
|
|
29
|
+
|
|
30
|
+
const delta = event.timeStamp - lastTimeStamp.current
|
|
31
|
+
scopedConsoleRef.current.logged('event:element.scroll:delta', delta)
|
|
32
|
+
|
|
33
|
+
if (delta > interval) {
|
|
34
|
+
scopedConsoleRef.current.warned('event:element.scroll:immediately', event)
|
|
35
|
+
callbackRef.current(event)
|
|
36
|
+
lastTimeStamp.current = event.timeStamp
|
|
37
|
+
} else {
|
|
38
|
+
timeoutID = setTimeout(() => {
|
|
39
|
+
scopedConsoleRef.current.warned('event:element.scroll:debounced', event)
|
|
40
|
+
lastTimeStamp.current = event.timeStamp
|
|
41
|
+
callbackRef.current(event)
|
|
42
|
+
}, interval)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (element && element instanceof HTMLElement) {
|
|
47
|
+
if (element instanceof HTMLBodyElement || element instanceof HTMLHtmlElement) {
|
|
48
|
+
window.addEventListener('scroll', debouncedHandler, { capture: true, passive: true })
|
|
49
|
+
window.addEventListener('resize', debouncedHandler)
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
window.removeEventListener('scroll', debouncedHandler)
|
|
53
|
+
window.removeEventListener('resize', debouncedHandler)
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
element.addEventListener('scroll', debouncedHandler, { passive: true, capture: true })
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
element.removeEventListener('scroll', debouncedHandler)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} else if (element) {
|
|
63
|
+
throw new Error('Exhaustive error: Expecting element to be instance of HTMLElement')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
}, [element, interval, scopedConsoleRef])
|
|
67
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from 'react'
|
|
2
|
+
import { useScopedConsoleRef } from '../debug-context'
|
|
3
|
+
|
|
4
|
+
export function useOnWindowResize(
|
|
5
|
+
callback: (event: Event) => void,
|
|
6
|
+
interval: number = 300,
|
|
7
|
+
) {
|
|
8
|
+
const scopedConsoleRef = useScopedConsoleRef('useOnWindowResize')
|
|
9
|
+
|
|
10
|
+
const callbackRef = useRef(callback); callbackRef.current = callback
|
|
11
|
+
const lastTimeStamp = useRef<number>(0)
|
|
12
|
+
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
let timeoutID: number | undefined = undefined
|
|
15
|
+
|
|
16
|
+
function debouncedHandler(event: Event) {
|
|
17
|
+
clearTimeout(timeoutID)
|
|
18
|
+
|
|
19
|
+
const delta = event.timeStamp - lastTimeStamp.current
|
|
20
|
+
scopedConsoleRef.current.logged('window.resize:delta', delta)
|
|
21
|
+
|
|
22
|
+
if (delta > interval) {
|
|
23
|
+
scopedConsoleRef.current.warned('event:window.resize:immediately', null)
|
|
24
|
+
callbackRef.current(event)
|
|
25
|
+
lastTimeStamp.current = event.timeStamp
|
|
26
|
+
} else {
|
|
27
|
+
timeoutID = setTimeout(() => {
|
|
28
|
+
scopedConsoleRef.current.warned('event:window.resize:debounced', null)
|
|
29
|
+
lastTimeStamp.current = event.timeStamp
|
|
30
|
+
callbackRef.current(event)
|
|
31
|
+
}, interval)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
window.addEventListener('resize', debouncedHandler)
|
|
36
|
+
|
|
37
|
+
return () => {
|
|
38
|
+
clearTimeout(timeoutID)
|
|
39
|
+
window.removeEventListener('resize', debouncedHandler)
|
|
40
|
+
}
|
|
41
|
+
}, [callbackRef, interval, scopedConsoleRef])
|
|
42
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { assert } from '@contember/utilities'
|
|
2
|
+
import { useLayoutEffect, useMemo, useState } from 'react'
|
|
3
|
+
import { useScopedConsoleRef } from '../debug-context'
|
|
4
|
+
import { useReferentiallyStableCallback } from '../referentiallyStable'
|
|
5
|
+
import { RefObjectOrElement, unwrapRefValue } from './unwrapRefValue'
|
|
6
|
+
import { useOnScrollWithin } from './useOnScrollWithin'
|
|
7
|
+
import { useOnWindowResize } from './useOnWindowResize'
|
|
8
|
+
|
|
9
|
+
export type Offsets = {
|
|
10
|
+
bottom: number;
|
|
11
|
+
left: number;
|
|
12
|
+
right: number;
|
|
13
|
+
top: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useScrollOffsets(refOrElement: RefObjectOrElement<HTMLElement | null> | null, interval: number = 1000 / 60) {
|
|
17
|
+
const { logged } = useScopedConsoleRef('useScrollOffsets').current
|
|
18
|
+
|
|
19
|
+
const [top, setTop] = useState(0)
|
|
20
|
+
const [bottom, setBottom] = useState(0)
|
|
21
|
+
const [left, setLeft] = useState(0)
|
|
22
|
+
const [right, setRight] = useState(0)
|
|
23
|
+
|
|
24
|
+
const updateScrollOffsetsState = useReferentiallyStableCallback((offsets: Offsets) => {
|
|
25
|
+
logged('Update the offsets:', {
|
|
26
|
+
previous: { bottom, left, right, top },
|
|
27
|
+
next: offsets,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
if (offsets.bottom !== bottom) setBottom(offsets.bottom)
|
|
31
|
+
if (offsets.left !== left) setLeft(offsets.left)
|
|
32
|
+
if (offsets.right !== right) setRight(offsets.right)
|
|
33
|
+
if (offsets.top !== top) setTop(offsets.top)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const handler = useReferentiallyStableCallback(() => {
|
|
37
|
+
const maybeScrollContent = unwrapRefValue(refOrElement) ?? document
|
|
38
|
+
|
|
39
|
+
if (maybeScrollContent) {
|
|
40
|
+
updateScrollOffsetsState(
|
|
41
|
+
getElementScrollOffsets(maybeScrollContent),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
useOnScrollWithin(refOrElement, handler, interval)
|
|
47
|
+
useOnWindowResize(handler)
|
|
48
|
+
useLayoutEffect(handler, [handler])
|
|
49
|
+
|
|
50
|
+
return useMemo(() => ({ bottom, left, right, top }), [bottom, left, right, top])
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isIntrinsicScrollElement(element: unknown): element is Document {
|
|
54
|
+
return element instanceof HTMLBodyElement
|
|
55
|
+
|| element instanceof HTMLHtmlElement
|
|
56
|
+
|| element instanceof DocumentType
|
|
57
|
+
|| element instanceof Document
|
|
58
|
+
|| element instanceof HTMLDocument
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getIntrinsicScrollContainer(element: HTMLElement | Document): HTMLElement {
|
|
62
|
+
const html = document.body.parentElement
|
|
63
|
+
assert('HTML element exists', html, (value): value is HTMLElement => value instanceof HTMLElement)
|
|
64
|
+
|
|
65
|
+
if (isIntrinsicScrollElement(element)) {
|
|
66
|
+
return html
|
|
67
|
+
} else {
|
|
68
|
+
return element
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getElementScrollOffsets(element: HTMLElement | Document): Offsets {
|
|
73
|
+
const isIntrinsicScrolling = isIntrinsicScrollElement(element)
|
|
74
|
+
const scrollContainer = getIntrinsicScrollContainer(element)
|
|
75
|
+
|
|
76
|
+
const { scrollLeft, scrollTop, scrollHeight, scrollWidth } = scrollContainer
|
|
77
|
+
|
|
78
|
+
const height = isIntrinsicScrolling ? window.innerHeight : scrollContainer.offsetHeight
|
|
79
|
+
const width = isIntrinsicScrolling ? window.innerWidth : scrollContainer.offsetWidth
|
|
80
|
+
|
|
81
|
+
const left = Math.round(scrollLeft)
|
|
82
|
+
const top = Math.round(scrollTop)
|
|
83
|
+
|
|
84
|
+
const right = Math.round(scrollWidth - width - left)
|
|
85
|
+
const bottom = Math.round(scrollHeight - height - top)
|
|
86
|
+
|
|
87
|
+
return { bottom, left, right, top }
|
|
88
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useCallback, useState } from 'react'
|
|
2
2
|
import { Serializable } from '../types'
|
|
3
3
|
|
|
4
|
-
type SetState<V extends Serializable> = (value: V | ((current: V) => V)) => void;
|
|
5
|
-
type ValueInitializer<V extends Serializable> = (storedValue: V | undefined) => V;
|
|
4
|
+
export type SetState<V extends Serializable> = (value: V | ((current: V) => V)) => void;
|
|
5
|
+
export type ValueInitializer<V extends Serializable> = (storedValue: V | undefined) => V;
|
|
6
6
|
|
|
7
7
|
const createStoredStateHook = (getStorage: () => Storage) => {
|
|
8
8
|
return <V extends Serializable>(key: string, initializeValue: ValueInitializer<V>): [V, SetState<V>] => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns a stable mutable object with value that is updated upon each call
|
|
5
|
+
*
|
|
6
|
+
* @param value - Value that could update after initial call
|
|
7
|
+
* @returns Mutable ref object with updated value
|
|
8
|
+
*/
|
|
9
|
+
export function useUpdatedRef<T>(value: T) {
|
|
10
|
+
const ref = useRef(value)
|
|
11
|
+
ref.current = value
|
|
12
|
+
|
|
13
|
+
return ref
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react'
|
|
2
|
+
import { useOnWindowResize } from './useOnWindowResize'
|
|
3
|
+
|
|
4
|
+
export function useWindowSize() {
|
|
5
|
+
const [width, setWidth] = useState(window.innerWidth)
|
|
6
|
+
const [height, setHeight] = useState(window.innerHeight)
|
|
7
|
+
|
|
8
|
+
useOnWindowResize(() => {
|
|
9
|
+
const { innerHeight, innerWidth } = window
|
|
10
|
+
if (innerHeight !== height) {
|
|
11
|
+
setHeight(innerHeight)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (innerWidth !== width) {
|
|
15
|
+
setWidth(innerWidth)
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
return useMemo(() => ({ height, width }), [height, width])
|
|
20
|
+
}
|
package/src/index.ts
CHANGED
package/src/tsconfig.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GACrB,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,SAAS,YAAY,EAAE,GACvB;IAAE,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,CAAC,EAAE,YAAY;CAAE,CAAA"}
|
|
File without changes
|