@contember/react-utils 1.1.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.
Files changed (184) hide show
  1. package/dist/development/context/createNonNullableContextFactory.js +17 -0
  2. package/dist/development/context/createNonNullableContextFactory.js.map +1 -0
  3. package/dist/development/debug-context/Constants.js +25 -0
  4. package/dist/development/debug-context/Constants.js.map +1 -0
  5. package/dist/development/debug-context/Context.js +67 -0
  6. package/dist/development/debug-context/Context.js.map +1 -0
  7. package/dist/development/hooks/unwrapRefValue.js +11 -0
  8. package/dist/development/hooks/unwrapRefValue.js.map +1 -0
  9. package/dist/development/hooks/useAddClassNameDuringResize.js +34 -0
  10. package/dist/development/hooks/useAddClassNameDuringResize.js.map +1 -0
  11. package/dist/development/hooks/useArrayMapMemo.js +0 -2
  12. package/dist/development/hooks/useArrayMapMemo.js.map +1 -1
  13. package/dist/development/hooks/useComposeRef.js +31 -0
  14. package/dist/development/hooks/useComposeRef.js.map +1 -0
  15. package/dist/development/hooks/useConstantLengthInvariant.js +0 -9
  16. package/dist/development/hooks/useConstantLengthInvariant.js.map +1 -1
  17. package/dist/development/hooks/useConstantValueInvariant.js +0 -9
  18. package/dist/development/hooks/useConstantValueInvariant.js.map +1 -1
  19. package/dist/development/hooks/useCountLayoutSlotChildren.js +31 -0
  20. package/dist/development/hooks/useCountLayoutSlotChildren.js.map +1 -0
  21. package/dist/development/hooks/useDebounceCallback.js +1 -1
  22. package/dist/development/hooks/useDebounceCallback.js.map +1 -1
  23. package/dist/development/hooks/useElementSize.js +47 -0
  24. package/dist/development/hooks/useElementSize.js.map +1 -0
  25. package/dist/development/hooks/useEventHandler.js +19 -0
  26. package/dist/development/hooks/useEventHandler.js.map +1 -0
  27. package/dist/development/hooks/useExpectSameValueReference.js +15 -0
  28. package/dist/development/hooks/useExpectSameValueReference.js.map +1 -0
  29. package/dist/development/hooks/useObjectMemo.js.map +1 -1
  30. package/dist/development/hooks/useOnElementMutation.js +35 -0
  31. package/dist/development/hooks/useOnElementMutation.js.map +1 -0
  32. package/dist/development/hooks/useOnElementResize.js +44 -0
  33. package/dist/development/hooks/useOnElementResize.js.map +1 -0
  34. package/dist/development/hooks/useOnScrollWithin.js +50 -0
  35. package/dist/development/hooks/useOnScrollWithin.js.map +1 -0
  36. package/dist/development/hooks/useOnWindowResize.js +36 -0
  37. package/dist/development/hooks/useOnWindowResize.js.map +1 -0
  38. package/dist/development/hooks/useScrollOffsets.js +68 -0
  39. package/dist/development/hooks/useScrollOffsets.js.map +1 -0
  40. package/dist/development/hooks/useStoredState.js.map +1 -1
  41. package/dist/development/hooks/useUpdatedRef.js +10 -0
  42. package/dist/development/hooks/useUpdatedRef.js.map +1 -0
  43. package/dist/development/hooks/useWindowSize.js +20 -0
  44. package/dist/development/hooks/useWindowSize.js.map +1 -0
  45. package/dist/development/index.js +41 -1
  46. package/dist/development/index.js.map +1 -1
  47. package/dist/development/referentiallyStable/useReferentiallyStableCallback.js +12 -0
  48. package/dist/development/referentiallyStable/useReferentiallyStableCallback.js.map +1 -0
  49. package/dist/production/context/createNonNullableContextFactory.js +17 -0
  50. package/dist/production/context/createNonNullableContextFactory.js.map +1 -0
  51. package/dist/production/debug-context/Constants.js +25 -0
  52. package/dist/production/debug-context/Constants.js.map +1 -0
  53. package/dist/production/debug-context/Context.js +67 -0
  54. package/dist/production/debug-context/Context.js.map +1 -0
  55. package/dist/production/hooks/unwrapRefValue.js +11 -0
  56. package/dist/production/hooks/unwrapRefValue.js.map +1 -0
  57. package/dist/production/hooks/useAddClassNameDuringResize.js +34 -0
  58. package/dist/production/hooks/useAddClassNameDuringResize.js.map +1 -0
  59. package/dist/production/hooks/useComposeRef.js +31 -0
  60. package/dist/production/hooks/useComposeRef.js.map +1 -0
  61. package/dist/production/hooks/useCountLayoutSlotChildren.js +31 -0
  62. package/dist/production/hooks/useCountLayoutSlotChildren.js.map +1 -0
  63. package/dist/production/hooks/useDebounceCallback.js +1 -1
  64. package/dist/production/hooks/useDebounceCallback.js.map +1 -1
  65. package/dist/production/hooks/useElementSize.js +47 -0
  66. package/dist/production/hooks/useElementSize.js.map +1 -0
  67. package/dist/production/hooks/useEventHandler.js +19 -0
  68. package/dist/production/hooks/useEventHandler.js.map +1 -0
  69. package/dist/production/hooks/useExpectSameValueReference.js +15 -0
  70. package/dist/production/hooks/useExpectSameValueReference.js.map +1 -0
  71. package/dist/production/hooks/useObjectMemo.js.map +1 -1
  72. package/dist/production/hooks/useOnElementMutation.js +35 -0
  73. package/dist/production/hooks/useOnElementMutation.js.map +1 -0
  74. package/dist/production/hooks/useOnElementResize.js +44 -0
  75. package/dist/production/hooks/useOnElementResize.js.map +1 -0
  76. package/dist/production/hooks/useOnScrollWithin.js +50 -0
  77. package/dist/production/hooks/useOnScrollWithin.js.map +1 -0
  78. package/dist/production/hooks/useOnWindowResize.js +36 -0
  79. package/dist/production/hooks/useOnWindowResize.js.map +1 -0
  80. package/dist/production/hooks/useScrollOffsets.js +68 -0
  81. package/dist/production/hooks/useScrollOffsets.js.map +1 -0
  82. package/dist/production/hooks/useStoredState.js.map +1 -1
  83. package/dist/production/hooks/useUpdatedRef.js +10 -0
  84. package/dist/production/hooks/useUpdatedRef.js.map +1 -0
  85. package/dist/production/hooks/useWindowSize.js +20 -0
  86. package/dist/production/hooks/useWindowSize.js.map +1 -0
  87. package/dist/production/index.js +41 -1
  88. package/dist/production/index.js.map +1 -1
  89. package/dist/production/referentiallyStable/useReferentiallyStableCallback.js +12 -0
  90. package/dist/production/referentiallyStable/useReferentiallyStableCallback.js.map +1 -0
  91. package/dist/types/context/createNonNullableContextFactory.d.ts +3 -0
  92. package/dist/types/context/createNonNullableContextFactory.d.ts.map +1 -0
  93. package/dist/types/context/index.d.ts +2 -0
  94. package/dist/types/context/index.d.ts.map +1 -0
  95. package/dist/types/debug-context/Constants.d.ts +6 -0
  96. package/dist/types/debug-context/Constants.d.ts.map +1 -0
  97. package/dist/types/debug-context/Context.d.ts +15 -0
  98. package/dist/types/debug-context/Context.d.ts.map +1 -0
  99. package/dist/types/debug-context/Types.d.ts +13 -0
  100. package/dist/types/debug-context/Types.d.ts.map +1 -0
  101. package/dist/types/debug-context/index.d.ts +4 -0
  102. package/dist/types/debug-context/index.d.ts.map +1 -0
  103. package/dist/types/hooks/index.d.ts +13 -0
  104. package/dist/types/hooks/index.d.ts.map +1 -1
  105. package/dist/types/hooks/unwrapRefValue.d.ts +9 -0
  106. package/dist/types/hooks/unwrapRefValue.d.ts.map +1 -0
  107. package/dist/types/hooks/useAddClassNameDuringResize.d.ts +13 -0
  108. package/dist/types/hooks/useAddClassNameDuringResize.d.ts.map +1 -0
  109. package/dist/types/hooks/useComposeRef.d.ts +16 -0
  110. package/dist/types/hooks/useComposeRef.d.ts.map +1 -0
  111. package/dist/types/hooks/useCountLayoutSlotChildren.d.ts +5 -0
  112. package/dist/types/hooks/useCountLayoutSlotChildren.d.ts.map +1 -0
  113. package/dist/types/hooks/useElementSize.d.ts +12 -0
  114. package/dist/types/hooks/useElementSize.d.ts.map +1 -0
  115. package/dist/types/hooks/useEventHandler.d.ts +10 -0
  116. package/dist/types/hooks/useEventHandler.d.ts.map +1 -0
  117. package/dist/types/hooks/useExpectSameValueReference.d.ts +2 -0
  118. package/dist/types/hooks/useExpectSameValueReference.d.ts.map +1 -0
  119. package/dist/types/hooks/useForceRender.d.ts +0 -1
  120. package/dist/types/hooks/useForceRender.d.ts.map +1 -1
  121. package/dist/types/hooks/useIsMounted.d.ts +0 -1
  122. package/dist/types/hooks/useIsMounted.d.ts.map +1 -1
  123. package/dist/types/hooks/useOnElementMutation.d.ts +3 -0
  124. package/dist/types/hooks/useOnElementMutation.d.ts.map +1 -0
  125. package/dist/types/hooks/useOnElementResize.d.ts +3 -0
  126. package/dist/types/hooks/useOnElementResize.d.ts.map +1 -0
  127. package/dist/types/hooks/useOnScrollWithin.d.ts +10 -0
  128. package/dist/types/hooks/useOnScrollWithin.d.ts.map +1 -0
  129. package/dist/types/hooks/useOnWindowResize.d.ts +2 -0
  130. package/dist/types/hooks/useOnWindowResize.d.ts.map +1 -0
  131. package/dist/types/hooks/useScrollOffsets.d.ts +14 -0
  132. package/dist/types/hooks/useScrollOffsets.d.ts.map +1 -0
  133. package/dist/types/hooks/useStoredState.d.ts +2 -3
  134. package/dist/types/hooks/useStoredState.d.ts.map +1 -1
  135. package/dist/types/hooks/useUpdatedRef.d.ts +8 -0
  136. package/dist/types/hooks/useUpdatedRef.d.ts.map +1 -0
  137. package/dist/types/hooks/useWindowSize.d.ts +5 -0
  138. package/dist/types/hooks/useWindowSize.d.ts.map +1 -0
  139. package/dist/types/index.d.ts +2 -0
  140. package/dist/types/index.d.ts.map +1 -1
  141. package/dist/types/referentiallyStable/index.d.ts +1 -0
  142. package/dist/types/referentiallyStable/index.d.ts.map +1 -1
  143. package/dist/types/referentiallyStable/useReferentiallyStableCallback.d.ts +2 -0
  144. package/dist/types/referentiallyStable/useReferentiallyStableCallback.d.ts.map +1 -0
  145. package/dist/types/tsconfig.tsbuildinfo +1 -1
  146. package/dist/types/types/PropsWithRequiredChildren.d.ts +5 -0
  147. package/dist/types/types/PropsWithRequiredChildren.d.ts.map +1 -0
  148. package/dist/types/{types.d.ts → types/Serializable.d.ts} +1 -1
  149. package/dist/types/types/Serializable.d.ts.map +1 -0
  150. package/dist/types/types/index.d.ts +3 -0
  151. package/dist/types/types/index.d.ts.map +1 -0
  152. package/package.json +7 -2
  153. package/src/context/createNonNullableContextFactory.ts +19 -0
  154. package/src/context/index.ts +1 -0
  155. package/src/debug-context/Constants.ts +22 -0
  156. package/src/debug-context/Context.tsx +82 -0
  157. package/src/debug-context/Types.ts +13 -0
  158. package/src/debug-context/index.ts +3 -0
  159. package/src/hooks/index.ts +13 -0
  160. package/src/hooks/unwrapRefValue.ts +16 -0
  161. package/src/hooks/useAddClassNameDuringResize.ts +58 -0
  162. package/src/hooks/useComposeRef.ts +46 -0
  163. package/src/hooks/useCountLayoutSlotChildren.ts +46 -0
  164. package/src/hooks/useDebounceCallback.ts +1 -1
  165. package/src/hooks/useElementSize.ts +73 -0
  166. package/src/hooks/useEventHandler.ts +24 -0
  167. package/src/hooks/useExpectSameValueReference.ts +26 -0
  168. package/src/hooks/useOnElementMutation.ts +42 -0
  169. package/src/hooks/useOnElementResize.ts +52 -0
  170. package/src/hooks/useOnScrollWithin.ts +67 -0
  171. package/src/hooks/useOnWindowResize.ts +42 -0
  172. package/src/hooks/useScrollOffsets.ts +88 -0
  173. package/src/hooks/useStoredState.ts +2 -2
  174. package/src/hooks/useUpdatedRef.ts +14 -0
  175. package/src/hooks/useWindowSize.ts +20 -0
  176. package/src/index.ts +2 -0
  177. package/src/referentiallyStable/index.ts +1 -0
  178. package/src/referentiallyStable/useReferentiallyStableCallback.ts +11 -0
  179. package/src/tsconfig.json +4 -1
  180. package/src/types/PropsWithRequiredChildren.ts +3 -0
  181. package/src/types/index.ts +2 -0
  182. package/dist/types/types.d.ts.map +0 -1
  183. package/src/ambient.d.ts +0 -1
  184. /package/src/{types.ts → types/Serializable.ts} +0 -0
@@ -0,0 +1,22 @@
1
+ import type { ScopedConsoleContextType } from './Types'
2
+
3
+ export function noopLog(...parameters: any[]) { }
4
+
5
+ export function noopLogged<T>(message: string, value: T): T {
6
+ return value
7
+ }
8
+
9
+ export const noopScopedConsole: ScopedConsoleContextType = Object.freeze({
10
+ log: noopLog,
11
+ logged: noopLogged,
12
+ warn: noopLog,
13
+ warned: noopLogged,
14
+ error: noopLog,
15
+ errored: noopLogged,
16
+ trace: noopLog,
17
+ traced: noopLogged,
18
+ })
19
+
20
+ export function isNoopScopedConsole(value: unknown): boolean {
21
+ return value === noopScopedConsole
22
+ }
@@ -0,0 +1,82 @@
1
+ import { assert, isNonEmptyString } from '@contember/utilities'
2
+ import { ReactNode, createContext, memo, useContext, useMemo, useRef } from 'react'
3
+ import { isNoopScopedConsole, noopScopedConsole } from './Constants'
4
+ import { ScopedConsoleContextType } from './Types'
5
+
6
+ function createPrefixedConsole(message: string, prefixedConsole?: ScopedConsoleContextType): ScopedConsoleContextType {
7
+ return {
8
+ error: function error(...parameters: any[]) {
9
+ (prefixedConsole ?? console).error(message, ...parameters)
10
+ },
11
+ errored: function errored<T>(message: string, value: T): T {
12
+ (prefixedConsole ?? console).error(message, value)
13
+ return value
14
+ },
15
+ log: function log(...parameters: any[]) {
16
+ (prefixedConsole ?? console).log(message, ...parameters)
17
+ },
18
+ logged: function logged<T>(message: string, value: T): T {
19
+ (prefixedConsole ?? console).log(message, value)
20
+ return value
21
+ },
22
+ trace: function trace(...parameters: any[]) {
23
+ (prefixedConsole ?? console).trace(message, ...parameters)
24
+ },
25
+ traced: function traced<T>(message: string, value: T): T {
26
+ (prefixedConsole ?? console).trace(message, value)
27
+ return value
28
+ },
29
+ warn: function warn(...parameters: any[]) {
30
+ (prefixedConsole ?? console).warn(message, ...parameters)
31
+ },
32
+ warned: function warned<T>(message: string, value: T): T {
33
+ (prefixedConsole ?? console).warn(message, value)
34
+ return value
35
+ },
36
+ }
37
+ }
38
+
39
+ export const ScopedConsoleContext = createContext<ScopedConsoleContextType>(null!)
40
+ ScopedConsoleContext.displayName = 'ScopedConsoleContext'
41
+
42
+ function parentScopedConsoleOrNone(parentConsole: ScopedConsoleContextType): ScopedConsoleContextType | undefined {
43
+ return isNoopScopedConsole(parentConsole) ? undefined : parentConsole
44
+ }
45
+
46
+ export const useScopedConsoleRef = (prefix: string) => {
47
+ assert('prefix is non-empty string', prefix, isNonEmptyString)
48
+
49
+ const parentConsole = parentScopedConsoleOrNone(useContext(ScopedConsoleContext))
50
+
51
+ const scopedConsole = useMemo(() => {
52
+ if (parentConsole) {
53
+ return createPrefixedConsole(prefix, parentConsole)
54
+ } else {
55
+ return noopScopedConsole
56
+ }
57
+ }, [parentConsole, prefix])
58
+
59
+ const ref = useRef(scopedConsole)
60
+ ref.current = scopedConsole
61
+
62
+ return ref
63
+ }
64
+
65
+ export type DebugChildrenProps =
66
+ | { active: true; children: ReactNode; id: string; }
67
+ | { active?: false; children: ReactNode; id?: string; }
68
+
69
+ export const DebugChildren = memo<DebugChildrenProps>(({ active = true, children, id }) => {
70
+ const parentScopedConsole = parentScopedConsoleOrNone(useContext(ScopedConsoleContext))
71
+
72
+ const console = active && id
73
+ ? createPrefixedConsole(id, parentScopedConsole)
74
+ : noopScopedConsole
75
+
76
+ return (
77
+ <ScopedConsoleContext.Provider value={active ? console : noopScopedConsole}>
78
+ {children}
79
+ </ScopedConsoleContext.Provider>
80
+ )
81
+ })
82
+ DebugChildren.displayName = 'DebugChildren'
@@ -0,0 +1,13 @@
1
+ export type DebugMethod = (...parameters: any[]) => void
2
+ export type ResultDebuggedMethod = <T>(message: string, value: T) => T
3
+
4
+ export type ScopedConsoleContextType = {
5
+ log: DebugMethod;
6
+ logged: ResultDebuggedMethod,
7
+ warn: DebugMethod;
8
+ warned: ResultDebuggedMethod,
9
+ error: DebugMethod;
10
+ errored: ResultDebuggedMethod,
11
+ trace: DebugMethod;
12
+ traced: ResultDebuggedMethod,
13
+ }
@@ -0,0 +1,3 @@
1
+ export * from './Constants'
2
+ export * from './Context'
3
+ export * from './Types'
@@ -1,11 +1,24 @@
1
+ export * from './unwrapRefValue'
1
2
  export * from './useAbortController'
3
+ export * from './useAddClassNameDuringResize'
2
4
  export * from './useArrayMapMemo'
5
+ export * from './useComposeRef'
3
6
  export * from './useConstantLengthInvariant'
4
7
  export * from './useConstantValueInvariant'
8
+ export * from './useCountLayoutSlotChildren'
5
9
  export * from './useDebounce'
6
10
  export * from './useDebounceCallback'
11
+ export * from './useElementSize'
12
+ export * from './useEventHandler'
13
+ export * from './useExpectSameValueReference'
7
14
  export * from './useForceRender'
8
15
  export * from './useIsMounted'
9
16
  export * from './useObjectMemo'
17
+ export * from './useOnElementMutation'
18
+ export * from './useOnElementResize'
19
+ export * from './useOnWindowResize'
10
20
  export * from './usePreviousValue'
21
+ export * from './useScrollOffsets'
11
22
  export * from './useStoredState'
23
+ export * from './useUpdatedRef'
24
+ export * from './useWindowSize'
@@ -0,0 +1,16 @@
1
+ import { RefObject } from 'react'
2
+
3
+ export type RefObjectOrElement<T> = T | RefObject<T>
4
+
5
+ /**
6
+ * Unwraps a ref object or returns the value if it is not a ref object.
7
+ * @param value - Value to unwrap
8
+ * @returns Unwrapped value
9
+ */
10
+ export function unwrapRefValue<T>(value: RefObjectOrElement<T>): T | null {
11
+ if (value && typeof value === 'object' && 'current' in value) {
12
+ return value.current
13
+ } else {
14
+ return value
15
+ }
16
+ }
@@ -0,0 +1,58 @@
1
+ import { useLayoutEffect, useRef } from 'react'
2
+ import { useReferentiallyStableCallback } from '../referentiallyStable'
3
+ import { useOnWindowResize } from './useOnWindowResize'
4
+
5
+ function intendedFlushOfCSSChangesToCauseImmediateReflow(element: HTMLElement) {
6
+ element.offsetHeight
7
+ }
8
+
9
+ function addClassName(
10
+ className: string,
11
+ element: HTMLElement,
12
+ ) {
13
+ if (element.classList.contains(className)) {
14
+ element.classList.remove(className)
15
+ intendedFlushOfCSSChangesToCauseImmediateReflow(element)
16
+ }
17
+ }
18
+
19
+ function removeClassName(
20
+ className: string,
21
+ element: HTMLElement,
22
+ ) {
23
+ if (!element.classList.contains(className)) {
24
+ element.classList.add(className)
25
+ intendedFlushOfCSSChangesToCauseImmediateReflow(element)
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Adds CSS class during resize event to element
31
+ *
32
+ * - Falls back to document.body when no element is specified
33
+ * - Adds CSS class to element also on the initial load
34
+ * - Uses element.classList directly
35
+ *
36
+ * @param className - CSS class to apply during the resize event
37
+ * @param timeoutToRestore - Timeout in milliseconds to wait before finally removing the CSS class
38
+ * @param element - Optional HTML element to apply CSS class to
39
+ */
40
+ export function useAddClassNameDuringResize(
41
+ className: string,
42
+ timeoutToRestore: number = 300,
43
+ element?: HTMLElement,
44
+ ) {
45
+ const timeoutID = useRef<number>()
46
+
47
+ const addTemporaryClassName = useReferentiallyStableCallback(() => {
48
+ clearTimeout(timeoutID.current)
49
+ removeClassName(className, element ?? document.body)
50
+
51
+ timeoutID.current = setTimeout(() => {
52
+ addClassName(className, element ?? document.body)
53
+ }, timeoutToRestore)
54
+ })
55
+
56
+ useLayoutEffect(addTemporaryClassName, [addTemporaryClassName])
57
+ useOnWindowResize(addTemporaryClassName)
58
+ }
@@ -0,0 +1,46 @@
1
+ import { MutableRefObject, RefCallback, useMemo, useRef } from 'react'
2
+
3
+ export type MaybeRef<T> = ((instance: T | null) => void) | MutableRefObject<T | null> | null | undefined
4
+ export type ComposedRefCallback<T> = RefCallback<T> & { current: T | null };
5
+
6
+ function setRef<T>(ref: MaybeRef<T>, instance: T | null): void {
7
+ if (ref) {
8
+ if (typeof ref === 'function') {
9
+ ref(instance)
10
+ } else if (instance) {
11
+ ref.current = instance
12
+ }
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Fills any number of refs with an instance
18
+ *
19
+ * Useful when you need to use outer ref and inner in the same time, e.g. passing element
20
+ * back to forwardedRef an also using the same value within the component locally.
21
+ *
22
+ * @param refs - Rest parameter of refs to fill with instance
23
+ * @returns Returns a function similar to a RefCallback
24
+ */
25
+ export function useComposeRef<T>(...refs: MaybeRef<T>[]): ComposedRefCallback<T> {
26
+ const initialRefs = useRef(refs)
27
+
28
+ const composeRef: ComposedRefCallback<T> = useMemo(() => {
29
+ function composedRefCallback(instance: T | null) {
30
+ initialRefs.current.forEach(ref => setRef(ref, instance))
31
+ setCurrent(instance)
32
+ }
33
+
34
+ function setCurrent(instance: T | null) {
35
+ if (instance) {
36
+ composedRefCallback.current = instance
37
+ }
38
+ }
39
+
40
+ composedRefCallback.current = null as T | null
41
+
42
+ return composedRefCallback
43
+ }, [])
44
+
45
+ return composeRef
46
+ }
@@ -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
+ }
@@ -6,7 +6,7 @@ export const useDebounceCallback = (cb: () => any, debounceMs: number) => {
6
6
  cbRef.current = cb
7
7
 
8
8
  useEffect(() => {
9
- return () => timerRef.current && clearTimeout(timerRef.current)
9
+ return () => clearTimeout(timerRef.current)
10
10
  }, [])
11
11
 
12
12
  return useCallback(() => {
@@ -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
+ }