@contember/react-utils 1.2.0 → 1.2.2
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/hooks/useAddClassNameDuringResize.js +0 -5
- package/dist/development/hooks/useAddClassNameDuringResize.js.map +1 -1
- package/dist/development/hooks/useAutoHeightTextArea.js +5 -5
- package/dist/development/hooks/useAutoHeightTextArea.js.map +1 -1
- package/dist/development/hooks/useChildrenAsLabel.js +53 -0
- package/dist/development/hooks/useChildrenAsLabel.js.map +1 -0
- package/dist/development/hooks/useElementSize.js +13 -23
- package/dist/development/hooks/useElementSize.js.map +1 -1
- package/dist/development/hooks/useOnElementResize.js +24 -28
- package/dist/development/hooks/useOnElementResize.js.map +1 -1
- package/dist/development/hooks/useScrollOffsets.js +6 -7
- package/dist/development/hooks/useScrollOffsets.js.map +1 -1
- package/dist/development/index.js +3 -0
- package/dist/development/index.js.map +1 -1
- package/dist/production/hooks/useAddClassNameDuringResize.js +0 -5
- package/dist/production/hooks/useAddClassNameDuringResize.js.map +1 -1
- package/dist/production/hooks/useAutoHeightTextArea.js +5 -5
- package/dist/production/hooks/useAutoHeightTextArea.js.map +1 -1
- package/dist/production/hooks/useChildrenAsLabel.js +52 -0
- package/dist/production/hooks/useChildrenAsLabel.js.map +1 -0
- package/dist/production/hooks/useElementSize.js +13 -23
- package/dist/production/hooks/useElementSize.js.map +1 -1
- package/dist/production/hooks/useOnElementResize.js +24 -28
- package/dist/production/hooks/useOnElementResize.js.map +1 -1
- package/dist/production/hooks/useScrollOffsets.js +6 -7
- package/dist/production/hooks/useScrollOffsets.js.map +1 -1
- package/dist/production/index.js +3 -0
- package/dist/production/index.js.map +1 -1
- package/dist/types/hooks/index.d.ts +1 -0
- package/dist/types/hooks/index.d.ts.map +1 -1
- package/dist/types/hooks/useAddClassNameDuringResize.d.ts.map +1 -1
- package/dist/types/hooks/useChildrenAsLabel.d.ts +18 -0
- package/dist/types/hooks/useChildrenAsLabel.d.ts.map +1 -0
- package/dist/types/hooks/useElementSize.d.ts +4 -0
- package/dist/types/hooks/useElementSize.d.ts.map +1 -1
- package/dist/types/hooks/useOnElementResize.d.ts.map +1 -1
- package/dist/types/hooks/useScrollOffsets.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useAddClassNameDuringResize.ts +0 -6
- package/src/hooks/useAutoHeightTextArea.ts +5 -5
- package/src/hooks/useChildrenAsLabel.ts +72 -0
- package/src/hooks/useElementSize.ts +17 -34
- package/src/hooks/useOnElementResize.ts +30 -33
- package/src/hooks/useScrollOffsets.ts +6 -7
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contember/react-utils",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"main": "./dist/production/index.js",
|
|
@@ -25,13 +25,14 @@
|
|
|
25
25
|
"build:js:dev": "NODE_ENV=development vite build --mode development",
|
|
26
26
|
"build:js:prod": "vite build --mode production",
|
|
27
27
|
"ae:build": "api-extractor run --local",
|
|
28
|
-
"ae:test": "api-extractor run"
|
|
28
|
+
"ae:test": "api-extractor run",
|
|
29
|
+
"test": "vitest --environment jsdom"
|
|
29
30
|
},
|
|
30
31
|
"peerDependencies": {
|
|
31
32
|
"react": "^17 || ^18"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
|
-
"@contember/utilities": "1.2.
|
|
35
|
+
"@contember/utilities": "1.2.2",
|
|
35
36
|
"fast-deep-equal": "^3.1.3"
|
|
36
37
|
}
|
|
37
38
|
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './useAbortController'
|
|
|
3
3
|
export * from './useAddClassNameDuringResize'
|
|
4
4
|
export * from './useArrayMapMemo'
|
|
5
5
|
export * from './useAutoHeightTextArea'
|
|
6
|
+
export * from './useChildrenAsLabel'
|
|
6
7
|
export * from './useComposeRef'
|
|
7
8
|
export * from './useConstantLengthInvariant'
|
|
8
9
|
export * from './useConstantValueInvariant'
|
|
@@ -2,17 +2,12 @@ import { useLayoutEffect, useRef } from 'react'
|
|
|
2
2
|
import { useReferentiallyStableCallback } from '../referentiallyStable'
|
|
3
3
|
import { useOnWindowResize } from './useOnWindowResize'
|
|
4
4
|
|
|
5
|
-
function intendedFlushOfCSSChangesToCauseImmediateReflow(element: HTMLElement) {
|
|
6
|
-
element.offsetHeight
|
|
7
|
-
}
|
|
8
|
-
|
|
9
5
|
function addClassName(
|
|
10
6
|
className: string,
|
|
11
7
|
element: HTMLElement,
|
|
12
8
|
) {
|
|
13
9
|
if (element.classList.contains(className)) {
|
|
14
10
|
element.classList.remove(className)
|
|
15
|
-
intendedFlushOfCSSChangesToCauseImmediateReflow(element)
|
|
16
11
|
}
|
|
17
12
|
}
|
|
18
13
|
|
|
@@ -22,7 +17,6 @@ function removeClassName(
|
|
|
22
17
|
) {
|
|
23
18
|
if (!element.classList.contains(className)) {
|
|
24
19
|
element.classList.add(className)
|
|
25
|
-
intendedFlushOfCSSChangesToCauseImmediateReflow(element)
|
|
26
20
|
}
|
|
27
21
|
}
|
|
28
22
|
|
|
@@ -9,7 +9,7 @@ export const useAutoHeightTextArea = (
|
|
|
9
9
|
minRows: number,
|
|
10
10
|
maxRows: number,
|
|
11
11
|
) => {
|
|
12
|
-
const measure = useCallback((ref: HTMLTextAreaElement | null, minRows: number, maxRows: number
|
|
12
|
+
const measure = useCallback(async (ref: HTMLTextAreaElement | null, minRows: number, maxRows: number) => {
|
|
13
13
|
if (ref) {
|
|
14
14
|
const rowsAttribute = ref.rows
|
|
15
15
|
|
|
@@ -23,13 +23,13 @@ export const useAutoHeightTextArea = (
|
|
|
23
23
|
let maxHeight: number
|
|
24
24
|
|
|
25
25
|
ref.rows = minRows
|
|
26
|
-
minHeight = ref.
|
|
26
|
+
minHeight = await ref.getBoundingClientRect().height
|
|
27
27
|
|
|
28
28
|
if (maxRows === Infinity) {
|
|
29
29
|
maxHeight = Infinity
|
|
30
30
|
} else {
|
|
31
31
|
ref.rows = maxRows
|
|
32
|
-
maxHeight = ref.
|
|
32
|
+
maxHeight = await ref.getBoundingClientRect().height
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
ref.style.height = px(height)
|
|
@@ -42,10 +42,10 @@ export const useAutoHeightTextArea = (
|
|
|
42
42
|
}, [])
|
|
43
43
|
|
|
44
44
|
useOnElementResize(textAreaRef, () => {
|
|
45
|
-
measure(unwrapRefValue(textAreaRef), minRows, maxRows
|
|
45
|
+
measure(unwrapRefValue(textAreaRef), minRows, maxRows)
|
|
46
46
|
})
|
|
47
47
|
|
|
48
48
|
useLayoutEffect(() => {
|
|
49
|
-
measure(unwrapRefValue(textAreaRef), minRows, maxRows
|
|
49
|
+
measure(unwrapRefValue(textAreaRef), minRows, maxRows)
|
|
50
50
|
}, [maxRows, measure, minRows, textAreaRef, value])
|
|
51
51
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Children, ReactNode, useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
const MULTIPLE_SPACES_REG_EXP = / +/g
|
|
4
|
+
|
|
5
|
+
function normalize(text: string): string {
|
|
6
|
+
return text.replace(MULTIPLE_SPACES_REG_EXP, ' ').trim()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Walks through the children and returns a string representation of the text content.
|
|
11
|
+
*
|
|
12
|
+
* @param node - The children to walk through.
|
|
13
|
+
* @returns String representation of the text content or undefined if there is no text content.
|
|
14
|
+
*
|
|
15
|
+
*/
|
|
16
|
+
export function getChildrenAsLabel(node: ReactNode): string | undefined {
|
|
17
|
+
if (typeof node === 'string' || typeof node === 'number') {
|
|
18
|
+
return String(node)
|
|
19
|
+
} else if (node === null || node === undefined || typeof node === 'boolean') {
|
|
20
|
+
return undefined
|
|
21
|
+
} else if (Array.isArray(node)) {
|
|
22
|
+
return normalize(node.map(getChildrenAsLabel).join(''))
|
|
23
|
+
} else {
|
|
24
|
+
let label: (string | number)[] = []
|
|
25
|
+
|
|
26
|
+
Children.map(node, child => {
|
|
27
|
+
if (child === null || child === undefined || typeof child === 'boolean') {
|
|
28
|
+
} else if (typeof child === 'string') {
|
|
29
|
+
label.push(child)
|
|
30
|
+
} else if (typeof child === 'number') {
|
|
31
|
+
label.push(child)
|
|
32
|
+
} else if (Array.isArray(child)) {
|
|
33
|
+
const childLabel = getChildrenAsLabel(child)
|
|
34
|
+
|
|
35
|
+
if (childLabel) {
|
|
36
|
+
label.push(childLabel)
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
// ReactElement | JSXElementConstructor | ReactPortal
|
|
40
|
+
if ('props' in child && 'children' in child.props) {
|
|
41
|
+
const childLabel = getChildrenAsLabel(child.props.children)
|
|
42
|
+
|
|
43
|
+
if (childLabel) {
|
|
44
|
+
label.push(childLabel)
|
|
45
|
+
}
|
|
46
|
+
} else if ('children' in child) {
|
|
47
|
+
// ReactPortal
|
|
48
|
+
const childLabel = getChildrenAsLabel(child.children)
|
|
49
|
+
|
|
50
|
+
if (childLabel) {
|
|
51
|
+
label.push(childLabel)
|
|
52
|
+
}
|
|
53
|
+
} else if (import.meta.env.DEV) {
|
|
54
|
+
console.warn('Unsupported child:', child)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return label.length > 0 ? label.join('') : undefined
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Walks through the children and returns a string representation of the text content.
|
|
65
|
+
*
|
|
66
|
+
* @param node - The children to walk through.
|
|
67
|
+
* @returns String representation of the text content or undefined if there is no text content.
|
|
68
|
+
*
|
|
69
|
+
*/
|
|
70
|
+
export function useChildrenAsLabel(node: ReactNode): string | undefined {
|
|
71
|
+
return useMemo(() => getChildrenAsLabel(node), [node])
|
|
72
|
+
}
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import { getSizeFromResizeObserverEntryFactory } from '@contember/utilities'
|
|
2
|
-
import { useLayoutEffect, useMemo,
|
|
1
|
+
import { getElementDimensionsCallback, getSizeFromResizeObserverEntryFactory } from '@contember/utilities'
|
|
2
|
+
import { useLayoutEffect, useMemo, useState } from 'react'
|
|
3
3
|
import { useScopedConsoleRef } from '../debug-context'
|
|
4
|
+
import { useReferentiallyStableCallback } from '../referentiallyStable'
|
|
4
5
|
import { RefObjectOrElement, unwrapRefValue } from './unwrapRefValue'
|
|
5
6
|
import { useOnElementResize } from './useOnElementResize'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function getElementHeight(element: HTMLElement) {
|
|
12
|
-
return element.offsetHeight
|
|
8
|
+
export type ElementSize = {
|
|
9
|
+
height: number
|
|
10
|
+
width: number
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
/**
|
|
@@ -28,45 +26,30 @@ export function useElementSize(
|
|
|
28
26
|
const { box = 'border-box' } = options
|
|
29
27
|
const getSizeFromResizeObserverEntry = useMemo(() => getSizeFromResizeObserverEntryFactory(box), [box])
|
|
30
28
|
|
|
31
|
-
const
|
|
32
|
-
const [
|
|
33
|
-
const [height, setHeight] = useState<number | undefined>(refValue?.offsetHeight)
|
|
29
|
+
const [width, setWidth] = useState<number | undefined>()
|
|
30
|
+
const [height, setHeight] = useState<number | undefined>()
|
|
34
31
|
|
|
35
32
|
logged('dimensions', { width, height })
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
const dimensions = logged('useOnElementResize => entry:dimensions', getSizeFromResizeObserverEntry(entry))
|
|
39
|
-
|
|
34
|
+
const maybeSetNewDimensions = useReferentiallyStableCallback((dimensions: ElementSize) => {
|
|
40
35
|
if (width !== dimensions.width) {
|
|
41
36
|
setWidth(dimensions.width)
|
|
42
37
|
}
|
|
43
38
|
if (height !== dimensions.height) {
|
|
44
39
|
setHeight(dimensions.height)
|
|
45
40
|
}
|
|
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
41
|
})
|
|
66
42
|
|
|
43
|
+
useOnElementResize(refOrElement, entry => {
|
|
44
|
+
const dimensions = logged('useOnElementResize => entry:dimensions', getSizeFromResizeObserverEntry(entry))
|
|
45
|
+
maybeSetNewDimensions(dimensions)
|
|
46
|
+
}, { box }, timeout)
|
|
47
|
+
|
|
67
48
|
useLayoutEffect(() => {
|
|
68
49
|
const element = unwrapRefValue(refOrElement)
|
|
69
|
-
|
|
50
|
+
if (element) {
|
|
51
|
+
getElementDimensionsCallback(element, dimensions => maybeSetNewDimensions(dimensions))
|
|
52
|
+
}
|
|
70
53
|
})
|
|
71
54
|
|
|
72
55
|
return { height, width }
|
|
@@ -16,40 +16,37 @@ export function useOnElementResize(
|
|
|
16
16
|
|
|
17
17
|
useLayoutEffect(() => {
|
|
18
18
|
const element = unwrapRefValue(refOrElement)
|
|
19
|
+
if (!element) {
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
if (!(element instanceof HTMLElement)) {
|
|
23
|
+
throw new Error('Exhaustive error: Expecting element to be instance of HTMLElement')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let timeoutID: number | undefined = undefined
|
|
27
|
+
|
|
28
|
+
function debouncedOnChange([entry]: ResizeObserverEntry[]) {
|
|
29
|
+
const timeStamp = Date.now()
|
|
30
|
+
const delta = timeStamp - lastTimeStamp.current
|
|
31
|
+
|
|
32
|
+
clearTimeout(timeoutID)
|
|
33
|
+
const timeoutFinal = delta > timeout ? 0 : timeout
|
|
34
|
+
|
|
35
|
+
timeoutID = setTimeout(() => {
|
|
36
|
+
const message = timeoutFinal > 0 ? 'element.resize:debounced' : 'element.resize:immediate'
|
|
37
|
+
scopedConsoleRef.current.warned(message, null)
|
|
38
|
+
callbackRef.current(entry)
|
|
39
|
+
lastTimeStamp.current = timeStamp
|
|
40
|
+
}, timeoutFinal)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const resizeObserver = new ResizeObserver(debouncedOnChange)
|
|
44
|
+
|
|
45
|
+
resizeObserver.observe(element, { box })
|
|
19
46
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
function debouncedOnChange([entry]: ResizeObserverEntry[]) {
|
|
25
|
-
const timeStamp = Date.now()
|
|
26
|
-
const delta = timeStamp - lastTimeStamp.current
|
|
27
|
-
|
|
28
|
-
if (delta > timeout) {
|
|
29
|
-
scopedConsoleRef.current.warned('element.resize:immediate', null)
|
|
30
|
-
callbackRef.current(entry)
|
|
31
|
-
lastTimeStamp.current = timeStamp
|
|
32
|
-
} else {
|
|
33
|
-
clearTimeout(timeoutID)
|
|
34
|
-
timeoutID = setTimeout(() => {
|
|
35
|
-
scopedConsoleRef.current.warned('element.resize:debounced', null)
|
|
36
|
-
callbackRef.current(entry)
|
|
37
|
-
lastTimeStamp.current = timeStamp
|
|
38
|
-
}, timeout)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const resizeObserver = new ResizeObserver(debouncedOnChange)
|
|
43
|
-
|
|
44
|
-
resizeObserver.observe(element, { box })
|
|
45
|
-
|
|
46
|
-
return () => {
|
|
47
|
-
clearTimeout(timeoutID)
|
|
48
|
-
resizeObserver.unobserve(element)
|
|
49
|
-
}
|
|
50
|
-
} else {
|
|
51
|
-
throw new Error('Exhaustive error: Expecting element to be instance of HTMLElement')
|
|
52
|
-
}
|
|
47
|
+
return () => {
|
|
48
|
+
clearTimeout(timeoutID)
|
|
49
|
+
resizeObserver.unobserve(element)
|
|
53
50
|
}
|
|
54
51
|
}, [box, refOrElement, scopedConsoleRef, timeout])
|
|
55
52
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assert } from '@contember/utilities'
|
|
1
|
+
import { assert, getElementDimensions } from '@contember/utilities'
|
|
2
2
|
import { useLayoutEffect, useMemo, useState } from 'react'
|
|
3
3
|
import { useScopedConsoleRef } from '../debug-context'
|
|
4
4
|
import { useReferentiallyStableCallback } from '../referentiallyStable'
|
|
@@ -37,9 +37,7 @@ export function useScrollOffsets(refOrElement: RefObjectOrElement<HTMLElement |
|
|
|
37
37
|
const maybeScrollContent = unwrapRefValue(refOrElement) ?? document
|
|
38
38
|
|
|
39
39
|
if (maybeScrollContent) {
|
|
40
|
-
updateScrollOffsetsState
|
|
41
|
-
getElementScrollOffsets(maybeScrollContent),
|
|
42
|
-
)
|
|
40
|
+
getElementScrollOffsets(maybeScrollContent).then(updateScrollOffsetsState)
|
|
43
41
|
}
|
|
44
42
|
})
|
|
45
43
|
|
|
@@ -69,14 +67,15 @@ function getIntrinsicScrollContainer(element: HTMLElement | Document): HTMLEleme
|
|
|
69
67
|
}
|
|
70
68
|
}
|
|
71
69
|
|
|
72
|
-
function getElementScrollOffsets(element: HTMLElement | Document): Offsets {
|
|
70
|
+
async function getElementScrollOffsets(element: HTMLElement | Document): Promise<Offsets> {
|
|
73
71
|
const isIntrinsicScrolling = isIntrinsicScrollElement(element)
|
|
74
72
|
const scrollContainer = getIntrinsicScrollContainer(element)
|
|
75
73
|
|
|
76
74
|
const { scrollLeft, scrollTop, scrollHeight, scrollWidth } = scrollContainer
|
|
75
|
+
const { height: scrollContainerHeight, width: scrollContainerWidth } = await getElementDimensions(scrollContainer)
|
|
77
76
|
|
|
78
|
-
const height = isIntrinsicScrolling ? window.innerHeight :
|
|
79
|
-
const width = isIntrinsicScrolling ? window.innerWidth :
|
|
77
|
+
const height = isIntrinsicScrolling ? window.innerHeight : scrollContainerHeight
|
|
78
|
+
const width = isIntrinsicScrolling ? window.innerWidth : scrollContainerWidth
|
|
80
79
|
|
|
81
80
|
const left = Math.round(scrollLeft)
|
|
82
81
|
const top = Math.round(scrollTop)
|