@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.
Files changed (46) hide show
  1. package/dist/development/hooks/useAddClassNameDuringResize.js +0 -5
  2. package/dist/development/hooks/useAddClassNameDuringResize.js.map +1 -1
  3. package/dist/development/hooks/useAutoHeightTextArea.js +5 -5
  4. package/dist/development/hooks/useAutoHeightTextArea.js.map +1 -1
  5. package/dist/development/hooks/useChildrenAsLabel.js +53 -0
  6. package/dist/development/hooks/useChildrenAsLabel.js.map +1 -0
  7. package/dist/development/hooks/useElementSize.js +13 -23
  8. package/dist/development/hooks/useElementSize.js.map +1 -1
  9. package/dist/development/hooks/useOnElementResize.js +24 -28
  10. package/dist/development/hooks/useOnElementResize.js.map +1 -1
  11. package/dist/development/hooks/useScrollOffsets.js +6 -7
  12. package/dist/development/hooks/useScrollOffsets.js.map +1 -1
  13. package/dist/development/index.js +3 -0
  14. package/dist/development/index.js.map +1 -1
  15. package/dist/production/hooks/useAddClassNameDuringResize.js +0 -5
  16. package/dist/production/hooks/useAddClassNameDuringResize.js.map +1 -1
  17. package/dist/production/hooks/useAutoHeightTextArea.js +5 -5
  18. package/dist/production/hooks/useAutoHeightTextArea.js.map +1 -1
  19. package/dist/production/hooks/useChildrenAsLabel.js +52 -0
  20. package/dist/production/hooks/useChildrenAsLabel.js.map +1 -0
  21. package/dist/production/hooks/useElementSize.js +13 -23
  22. package/dist/production/hooks/useElementSize.js.map +1 -1
  23. package/dist/production/hooks/useOnElementResize.js +24 -28
  24. package/dist/production/hooks/useOnElementResize.js.map +1 -1
  25. package/dist/production/hooks/useScrollOffsets.js +6 -7
  26. package/dist/production/hooks/useScrollOffsets.js.map +1 -1
  27. package/dist/production/index.js +3 -0
  28. package/dist/production/index.js.map +1 -1
  29. package/dist/types/hooks/index.d.ts +1 -0
  30. package/dist/types/hooks/index.d.ts.map +1 -1
  31. package/dist/types/hooks/useAddClassNameDuringResize.d.ts.map +1 -1
  32. package/dist/types/hooks/useChildrenAsLabel.d.ts +18 -0
  33. package/dist/types/hooks/useChildrenAsLabel.d.ts.map +1 -0
  34. package/dist/types/hooks/useElementSize.d.ts +4 -0
  35. package/dist/types/hooks/useElementSize.d.ts.map +1 -1
  36. package/dist/types/hooks/useOnElementResize.d.ts.map +1 -1
  37. package/dist/types/hooks/useScrollOffsets.d.ts.map +1 -1
  38. package/dist/types/tsconfig.tsbuildinfo +1 -1
  39. package/package.json +4 -3
  40. package/src/hooks/index.ts +1 -0
  41. package/src/hooks/useAddClassNameDuringResize.ts +0 -6
  42. package/src/hooks/useAutoHeightTextArea.ts +5 -5
  43. package/src/hooks/useChildrenAsLabel.ts +72 -0
  44. package/src/hooks/useElementSize.ts +17 -34
  45. package/src/hooks/useOnElementResize.ts +30 -33
  46. 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.0",
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.0",
35
+ "@contember/utilities": "1.2.2",
35
36
  "fast-deep-equal": "^3.1.3"
36
37
  }
37
38
  }
@@ -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, value: string) => {
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.offsetHeight
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.offsetHeight
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, value)
45
+ measure(unwrapRefValue(textAreaRef), minRows, maxRows)
46
46
  })
47
47
 
48
48
  useLayoutEffect(() => {
49
- measure(unwrapRefValue(textAreaRef), minRows, maxRows, value)
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, useRef, useState } from 'react'
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
- function getElementWidth(element: HTMLElement) {
8
- return element.offsetWidth
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 refValue = unwrapRefValue(refOrElement)
32
- const [width, setWidth] = useState<number | undefined>(refValue?.offsetWidth)
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
- useOnElementResize(refOrElement, entry => {
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
- logged('useLayoutEffect => measure', measure.current(element))
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
- if (element) {
21
- if (element instanceof HTMLElement) {
22
- let timeoutID: number | undefined = undefined
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 : scrollContainer.offsetHeight
79
- const width = isIntrinsicScrolling ? window.innerWidth : scrollContainer.offsetWidth
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)