@pushwoosh/dumb-components 4.3.3 → 4.3.4

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.
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { autoUpdate, flip, offset, shift, useFloating } from '@floating-ui/react-dom';
3
- import { useEffect, useState } from 'react';
3
+ import { useState } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
5
  import { Color } from '@pushwoosh/kit-constants';
6
6
  import { Horizontal, Vertical } from '@pushwoosh/kit-helpers';
@@ -45,12 +45,18 @@ export const CalendarDropdown = props => {
45
45
  const [value, setValue] = useState(props.value ?? (isRangePicker ? [] : undefined));
46
46
  const initialTimezone = calendarProps.timezone || 'UTC';
47
47
  const [timezone, setTimezone] = useState(initialTimezone);
48
- useEffect(() => {
48
+ const valueDeps = [props.value, isRangePicker];
49
+ const [prevValueDeps, setPrevValueDeps] = useState(null);
50
+ if (prevValueDeps === null || valueDeps.some((dep, index) => dep !== prevValueDeps[index])) {
51
+ setPrevValueDeps(valueDeps);
49
52
  setValue(props.value ?? (isRangePicker ? [] : undefined));
50
- }, [props.value, isRangePicker]);
51
- useEffect(() => {
53
+ }
54
+ const timezoneDeps = [calendarProps.timezone];
55
+ const [prevTimezoneDeps, setPrevTimezoneDeps] = useState(null);
56
+ if (prevTimezoneDeps === null || timezoneDeps.some((dep, index) => dep !== prevTimezoneDeps[index])) {
57
+ setPrevTimezoneDeps(timezoneDeps);
52
58
  setTimezone(calendarProps.timezone || 'UTC');
53
- }, [calendarProps.timezone]);
59
+ }
54
60
  const closePopover = () => {
55
61
  setIsOpen(false);
56
62
  setValue(props.value ?? (isRangePicker ? [] : undefined));
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect } from 'react';
2
+ import { useState } from 'react';
3
3
  import { Color } from '@pushwoosh/kit-constants';
4
4
  import { Spinner } from '../Spinner';
5
5
  import { RootBox, ImgPreview, PlaceholderWrapper } from './styles';
@@ -21,12 +21,15 @@ export function ExternalImagePreview(props) {
21
21
  } = props;
22
22
  const [isLoading, setIsLoading] = useState(false);
23
23
  const [isError, setIsError] = useState(false);
24
- useEffect(() => {
24
+ const srcDeps = [src];
25
+ const [prevSrcDeps, setPrevSrcDeps] = useState(null);
26
+ if (prevSrcDeps === null || srcDeps.some((dep, index) => dep !== prevSrcDeps[index])) {
27
+ setPrevSrcDeps(srcDeps);
25
28
  if (src) {
26
29
  setIsLoading(true);
27
30
  setIsError(false);
28
31
  }
29
- }, [src]);
32
+ }
30
33
  const handleOnError = error => {
31
34
  setIsLoading(false);
32
35
  setIsError(true);
@@ -36,9 +36,12 @@ export function NumberPicker({
36
36
  onChange(newValue);
37
37
  }
38
38
  });
39
- useEffect(() => {
39
+ const valueDeps = [value];
40
+ const [prevValueDeps, setPrevValueDeps] = useState(null);
41
+ if (prevValueDeps === null || valueDeps.some((dep, index) => dep !== prevValueDeps[index])) {
42
+ setPrevValueDeps(valueDeps);
40
43
  setLocalValue(value !== undefined ? value.toString() : '');
41
- }, [value]);
44
+ }
42
45
  useEffect(() => {
43
46
  if (!autoDiff) {
44
47
  return;
@@ -11,7 +11,6 @@ export function useRecaptcha() {
11
11
  const [widgetID, setWidgetID] = useState(null);
12
12
  const [isRecaptchaReady, setRecaptchaReady] = useState(!!(isRecaptchaLoaded && widgetID));
13
13
  const [recaptchaErrorCode, setRecaptchaErrorCode] = useState(''); // RIEXXX - ReCaptcha Internal Error
14
- const [intervalID, setIntervalID] = useState(0);
15
14
  // Load external script effect, fires once
16
15
  useEffect(() => {
17
16
  asyncScriptLoad(RECAPTCHA_SCRIPT_URL).catch(() => setRecaptchaErrorCode('RIE001'));
@@ -21,9 +20,9 @@ export function useRecaptcha() {
21
20
  const newInterval = window.setInterval(() => {
22
21
  if (window?.grecaptcha?.render) {
23
22
  setRecaptchaLoaded(true);
23
+ clearInterval(newInterval);
24
24
  }
25
25
  }, 500);
26
- setIntervalID(newInterval);
27
26
  return () => {
28
27
  clearInterval(newInterval);
29
28
  };
@@ -31,7 +30,6 @@ export function useRecaptcha() {
31
30
  // Render recaptcha widget when script loaded, stop interval
32
31
  useEffect(() => {
33
32
  if (isRecaptchaLoaded && widgetID === null) {
34
- clearInterval(intervalID);
35
33
  try {
36
34
  const widget = window.grecaptcha.render(RECAPTCHA_CONTAINER_ID, {
37
35
  sitekey: RECAPTCHA_KEY,
@@ -46,7 +44,7 @@ export function useRecaptcha() {
46
44
  setRecaptchaErrorCode('RIE002');
47
45
  }
48
46
  }
49
- }, [isRecaptchaLoaded, widgetID, intervalID, promiseRef]);
47
+ }, [isRecaptchaLoaded, widgetID, promiseRef]);
50
48
  // async function to get recaptcha token, resolves by resolveAction callback
51
49
  const getRecaptchaToken = useCallback(async () => new Promise((resolve, reject) => {
52
50
  try {
@@ -27,9 +27,12 @@ export function SandboxedHtmlFrame(props) {
27
27
  const onReady = mode === 'inspectable' ? props.onReady : undefined;
28
28
  const $ref = useRef(null);
29
29
  const [isReady, setIsReady] = useState(false);
30
- useEffect(() => {
30
+ const htmlDeps = [html];
31
+ const [prevHtmlDeps, setPrevHtmlDeps] = useState(null);
32
+ if (prevHtmlDeps === null || htmlDeps.some((dep, index) => dep !== prevHtmlDeps[index])) {
33
+ setPrevHtmlDeps(htmlDeps);
31
34
  setIsReady(false);
32
- }, [html]);
35
+ }
33
36
  const handleLoad = useCallback(() => {
34
37
  setIsReady(true);
35
38
  }, []);
@@ -52,11 +52,14 @@ export function Tooltip({
52
52
  assignRef(originalRef, node);
53
53
  }, [originalRef]);
54
54
  const externalReferenceEl = reference?.current ?? null;
55
- useEffect(() => {
55
+ const referenceDeps = [reference, externalReferenceEl];
56
+ const [prevReferenceDeps, setPrevReferenceDeps] = useState(null);
57
+ if (prevReferenceDeps === null || referenceDeps.some((dep, index) => dep !== prevReferenceDeps[index])) {
58
+ setPrevReferenceDeps(referenceDeps);
56
59
  if (reference) {
57
60
  setReferenceEl(externalReferenceEl);
58
61
  }
59
- }, [reference, externalReferenceEl]);
62
+ }
60
63
  useEffect(() => {
61
64
  setReference(referenceEl);
62
65
  }, [referenceEl, setReference]);
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useState } from 'react';
2
+ import { useCallback, useState } from 'react';
3
3
  import { Color } from '@pushwoosh/kit-constants';
4
4
  import { Columns, Vertical } from '@pushwoosh/kit-helpers';
5
5
  import { ImageIcon } from '@pushwoosh/kit-icons';
@@ -26,7 +26,12 @@ export function WhatsAppAttachment(props) {
26
26
  const [hasFailed, setHasFailed] = useState(false);
27
27
  const kind = resolveAttachmentKind(attachment);
28
28
  const extension = getAttachmentExtension(attachment);
29
- useEffect(() => setHasFailed(false), [source, thumbnailSource]);
29
+ const sourceDeps = [source, thumbnailSource];
30
+ const [prevSourceDeps, setPrevSourceDeps] = useState(null);
31
+ if (prevSourceDeps === null || sourceDeps.some((dep, index) => dep !== prevSourceDeps[index])) {
32
+ setPrevSourceDeps(sourceDeps);
33
+ setHasFailed(false);
34
+ }
30
35
  const handleError = useCallback(() => setHasFailed(true), []);
31
36
  const handleLoadedMetadata = useCallback(event => {
32
37
  const {
package/hooks/useLayer.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
1
+ import { useId, useLayoutEffect, useRef, useSyncExternalStore } from 'react';
2
2
  const BASE_Z_INDEX = 10000;
3
3
  const Z_INDEX_STEP = 100;
4
4
  const GLOBAL_KEY = '__pushwoosh_dumb_components_layer_state__';
@@ -80,6 +80,12 @@ function resolveZIndex(id, fallbackZIndex) {
80
80
  }
81
81
  return fallbackZIndex ?? BASE_Z_INDEX;
82
82
  }
83
+ function subscribeToLayers(onChange) {
84
+ getState().subscribers.add(onChange);
85
+ return () => {
86
+ getState().subscribers.delete(onChange);
87
+ };
88
+ }
83
89
  /**
84
90
  * Manages a stacked overlay layer (modals, drawers, popovers): registers while
85
91
  * `isOpen`, computes a stacking `zIndex` above lower layers, and calls `onClose` on
@@ -97,14 +103,6 @@ export function useLayer({
97
103
  onCloseRef.current = onClose;
98
104
  const zIndexRef = useRef(zIndex);
99
105
  zIndexRef.current = zIndex;
100
- const [, forceRender] = useState(0);
101
- useEffect(() => {
102
- const subscriber = () => forceRender(value => value + 1);
103
- getState().subscribers.add(subscriber);
104
- return () => {
105
- getState().subscribers.delete(subscriber);
106
- };
107
- }, []);
108
106
  useLayoutEffect(() => {
109
107
  if (!isOpen) {
110
108
  return undefined;
@@ -114,7 +112,6 @@ export function useLayer({
114
112
  close: () => onCloseRef.current?.(),
115
113
  baseZIndex: zIndexRef.current
116
114
  });
117
- forceRender(value => value + 1);
118
115
  return () => unregisterLayer(id);
119
116
  }, [isOpen, id]);
120
117
  useLayoutEffect(() => {
@@ -124,7 +121,7 @@ export function useLayer({
124
121
  notify();
125
122
  }
126
123
  }, [zIndex, id]);
127
- const resolvedZIndex = resolveZIndex(id, zIndex);
124
+ const resolvedZIndex = useSyncExternalStore(subscribeToLayers, () => resolveZIndex(id, zIndex));
128
125
  return {
129
126
  zIndex: resolvedZIndex
130
127
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/dumb-components",
3
- "version": "4.3.3",
3
+ "version": "4.3.4",
4
4
  "description": "React components to build Pushwoosh products",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
@@ -1,16 +1,19 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useState } from 'react';
2
2
  /**
3
3
  * Holds the active (highlighted) row index, clamping it to `total` and resetting to
4
4
  * `resetIndex` when the list changes.
5
5
  */
6
6
  export function useActiveIndex(total, resetIndex) {
7
7
  const [activeIndex, setActiveIndex] = useState(resetIndex);
8
- useEffect(() => {
9
- setActiveIndex(prev => {
10
- if (total === 0) return -1;
11
- if (prev < 0 || prev >= total) return resetIndex;
12
- return prev;
13
- });
14
- }, [total, resetIndex]);
8
+ const clampDeps = [total, resetIndex];
9
+ const [prevClampDeps, setPrevClampDeps] = useState(null);
10
+ if (prevClampDeps === null || clampDeps.some((dep, index) => dep !== prevClampDeps[index])) {
11
+ setPrevClampDeps(clampDeps);
12
+ if (total === 0) {
13
+ setActiveIndex(-1);
14
+ } else if (activeIndex < 0 || activeIndex >= total) {
15
+ setActiveIndex(resetIndex);
16
+ }
17
+ }
15
18
  return [activeIndex, setActiveIndex];
16
19
  }
@@ -2,9 +2,11 @@ import { useEffect, useState } from 'react';
2
2
  /** Returns `value` debounced by `delay` ms (updates immediately when `delay <= 0`). */
3
3
  export function useDebouncedValue(value, delay) {
4
4
  const [debounced, setDebounced] = useState(value);
5
+ if (delay <= 0 && debounced !== value) {
6
+ setDebounced(value);
7
+ }
5
8
  useEffect(() => {
6
9
  if (delay <= 0) {
7
- setDebounced(value);
8
10
  return undefined;
9
11
  }
10
12
  const id = setTimeout(() => setDebounced(value), delay);