@applicaster/zapp-react-native-utils 12.0.0-alpha.4054240460 → 12.0.0-alpha.4081044014

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 (49) hide show
  1. package/analyticsUtils/AnalyticPlayerListener.ts +141 -0
  2. package/analyticsUtils/PlayerAnalyticsManager.ts +90 -0
  3. package/analyticsUtils/{analyticsTracker.ts → playerAnalyticsTracker.ts} +19 -10
  4. package/appUtils/accessibilityManager/const.ts +50 -0
  5. package/appUtils/accessibilityManager/hooks.ts +40 -0
  6. package/appUtils/accessibilityManager/index.ts +155 -0
  7. package/appUtils/accessibilityManager/logger.ts +5 -0
  8. package/appUtils/focusManager/treeDataStructure/Utils/index.js +1 -1
  9. package/appUtils/index.js +2 -0
  10. package/appUtils/platform/platformUtils.ts +40 -101
  11. package/appUtils/playerManager/conts.ts +1 -0
  12. package/appUtils/playerManager/index.ts +9 -27
  13. package/appUtils/playerManager/player.ts +9 -0
  14. package/appUtils/playerManager/playerHooks/usePlayerCurrentTime.tsx +57 -0
  15. package/appUtils/playerManager/playerNative.ts +1 -1
  16. package/appUtils/playerManager/usePlayerControllerSetup.tsx +2 -1
  17. package/appUtils/playerManager/usePlayerState.tsx +19 -34
  18. package/arrayUtils/index.ts +11 -0
  19. package/cellUtils/index.ts +38 -5
  20. package/focusManager/FocusManager.ts +20 -5
  21. package/focusManager/__tests__/FocusManager.test.ts +0 -1
  22. package/functionUtils/index.ts +38 -0
  23. package/index.d.ts +15 -0
  24. package/manifestUtils/defaultManifestConfigurations/player.js +121 -0
  25. package/manifestUtils/keys.js +2 -1
  26. package/navigationUtils/index.ts +49 -1
  27. package/numberUtils/__tests__/toNumber.test.ts +2 -2
  28. package/numberUtils/__tests__/toPositiveNumberWithDefault.test.ts +66 -0
  29. package/numberUtils/__tests__/toPositiveNumberWithDefaultZero.test.ts +66 -0
  30. package/numberUtils/index.ts +20 -0
  31. package/package.json +2 -2
  32. package/playerUtils/configurationGenerator.ts +120 -0
  33. package/playerUtils/imaScriptDom.ts +59 -0
  34. package/pluginUtils/index.ts +9 -0
  35. package/reactHooks/cell-click/index.ts +17 -3
  36. package/reactHooks/debugging/__tests__/index.test.js +81 -11
  37. package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +69 -6
  38. package/reactHooks/feed/useBatchLoading.ts +52 -28
  39. package/reactHooks/layout/useDimensions/__tests__/useDimensions.test.ts +134 -0
  40. package/reactHooks/layout/useDimensions/useDimensions.ts +2 -0
  41. package/reactHooks/navigation/index.ts +5 -1
  42. package/reactHooks/player/useAutoSeek.tsx +9 -7
  43. package/reactHooks/resolvers/useCellResolver.ts +5 -1
  44. package/reactHooks/useConfiguration.ts +2 -2
  45. package/reactUtils/index.ts +1 -2
  46. package/testUtils/index.tsx +42 -9
  47. package/appUtils/focusManager/FocusableEvents.ts +0 -8
  48. package/appUtils/focusManager/treeDataStructure/Utils/__tests__/Utils_android.test.js +0 -1094
  49. package/appUtils/focusManager/treeDataStructure/Utils/index.android.js +0 -261
@@ -1,8 +1,10 @@
1
1
  import { BehaviorSubject } from "rxjs";
2
2
 
3
3
  import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage";
4
- import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
5
- import { PlayerWebController } from "@applicaster/zapp-react-native-default-player/src/playerFactory/players/playerWebController";
4
+ import {
5
+ isLgPlatform,
6
+ isSamsungPlatform,
7
+ } from "@applicaster/zapp-react-dom-app/App/Loader/utils/platform";
6
8
 
7
9
  import { PLATFORM_KEYS, PLATFORMS } from "./const";
8
10
  import { createLogger, utilsLogger } from "../../logger";
@@ -21,14 +23,13 @@ declare global {
21
23
  }
22
24
  }
23
25
 
24
- const { log_debug, log_error } = createLogger({
26
+ const { log_debug } = createLogger({
25
27
  category: "General",
26
28
  subsystem: "PlatformUtils",
27
29
  parent: utilsLogger,
28
30
  });
29
31
 
30
- export const getUserAgent = () =>
31
- window?.navigator?.userAgent || global?.navigator?.userAgent;
32
+ export const getUserAgent = () => window?.navigator?.userAgent;
32
33
 
33
34
  /** VIZIO UTILS */
34
35
 
@@ -41,6 +42,8 @@ export const getUserAgent = () =>
41
42
  export const isVizioPlatform = () => {
42
43
  const userAgent = getUserAgent();
43
44
 
45
+ if (!userAgent) return false;
46
+
44
47
  const isVizioAgent =
45
48
  userAgent.includes(PLATFORMS.vizio) ||
46
49
  userAgent.includes(PLATFORMS.smartcast) ||
@@ -131,100 +134,6 @@ export const getClosedCaptionState = () => {
131
134
  return ccManager.getStateAsObservable();
132
135
  };
133
136
 
134
- export class TextTrackManager {
135
- private controller?: PlayerWebController;
136
- private static instance: TextTrackManager;
137
-
138
- private constructor() {}
139
-
140
- static getInstance(): TextTrackManager {
141
- if (!TextTrackManager.instance) {
142
- TextTrackManager.instance = new TextTrackManager();
143
- }
144
-
145
- return TextTrackManager.instance;
146
- }
147
-
148
- initialize(controller: PlayerWebController) {
149
- if (!controller) {
150
- throw new Error(
151
- "Controller is required for TextTrackManager initialization"
152
- );
153
- }
154
-
155
- this.controller = controller;
156
- }
157
-
158
- async setTextTrackBasedOnCaptions(closedCaptionsState: boolean) {
159
- if (!this.controller) {
160
- log_error("Controller is not initialized");
161
-
162
- return;
163
- }
164
-
165
- if (closedCaptionsState) {
166
- const textTracks = this.controller.getTracksState().textTracks;
167
- const preferredTextTrack = await this.getPreferredTextTrack(textTracks);
168
-
169
- if (preferredTextTrack) {
170
- this.controller.selectTrack(preferredTextTrack);
171
- }
172
- } else {
173
- this.controller.selectTrack(null);
174
- }
175
- }
176
-
177
- async getPreferredTextTrack(
178
- textTracks: QuickBrickPlayer.TextTrack[]
179
- ): Promise<QuickBrickPlayer.TextTrack | undefined> {
180
- if (!this.controller) return;
181
-
182
- const userPreferredTextLanguageLocalStorage = await localStorage.getItem(
183
- "preferred_text_language",
184
- "user_preferences"
185
- );
186
-
187
- let userPreferredTextLanguage;
188
-
189
- if (userPreferredTextLanguageLocalStorage) {
190
- userPreferredTextLanguage = textTracks.find(
191
- (track) => track.language === userPreferredTextLanguageLocalStorage
192
- );
193
- }
194
-
195
- if (userPreferredTextLanguage?.id) {
196
- return userPreferredTextLanguage;
197
- }
198
-
199
- const enableAutoTextTrackSelection =
200
- this.controller.getPluginConfiguration()
201
- ?.enable_auto_text_track_selection ||
202
- this.controller.getTracksState().textTracks.length > 0;
203
-
204
- if (enableAutoTextTrackSelection) {
205
- const languageCode = await sessionStorage.getItem("languageCode");
206
-
207
- if (languageCode) {
208
- const chosenTextTrackBySystemLanguage = textTracks.find(
209
- (track) => track.language === languageCode
210
- );
211
-
212
- if (chosenTextTrackBySystemLanguage?.id) {
213
- return chosenTextTrackBySystemLanguage;
214
- }
215
- }
216
- }
217
-
218
- return textTracks?.[0];
219
- }
220
-
221
- isTextTrackEnabled(): boolean {
222
- if (!this.controller) return false;
223
-
224
- return this.controller.getTracksState().textTrackId !== null;
225
- }
226
- }
227
-
228
137
  /**
229
138
  * Manages device based closed captioning state
230
139
  * Returns an observable that emits the current state and updates whenever it changes
@@ -246,9 +155,11 @@ export class TTSManager {
246
155
  }
247
156
 
248
157
  async initialize() {
158
+ if (!isVizioPlatform()) return;
159
+
249
160
  document.addEventListener(
250
161
  "VIZIO_TTS_ENABLED",
251
- function () {
162
+ () => {
252
163
  this.ttsState$.next(true);
253
164
  },
254
165
  false
@@ -256,7 +167,7 @@ export class TTSManager {
256
167
 
257
168
  document.addEventListener(
258
169
  "VIZIO_TTS_DISABLED",
259
- function () {
170
+ () => {
260
171
  this.ttsState$.next(false);
261
172
  },
262
173
  false
@@ -272,6 +183,34 @@ export class TTSManager {
272
183
  }
273
184
 
274
185
  readText(text: string) {
186
+ if (isSamsungPlatform() && window.speechSynthesis) {
187
+ const utterance = new SpeechSynthesisUtterance(text);
188
+ window.speechSynthesis.speak(utterance);
189
+ }
190
+
191
+ if (isLgPlatform() && window.webOS?.service) {
192
+ try {
193
+ window.webOS.service.request("luna://com.webos.service.tts", {
194
+ method: "speak",
195
+ onFailure(error: any) {
196
+ log_debug("There was a failure setting up webOS TTS service", {
197
+ error,
198
+ });
199
+ },
200
+ onSuccess(response: any) {
201
+ log_debug("webOS TTS service is configured successfully", {
202
+ response,
203
+ });
204
+ },
205
+ parameters: {
206
+ text,
207
+ },
208
+ });
209
+ } catch (error) {
210
+ log_debug("webOS TTS service error", { error });
211
+ }
212
+ }
213
+
275
214
  if (!window.VIZIO?.Chromevox) return;
276
215
 
277
216
  window.VIZIO.Chromevox.play(text);
@@ -7,6 +7,7 @@ export enum SharedPlayerCallBacksKeys {
7
7
  OnPlayerPause = "onPlayerPause",
8
8
  OnLoad = "onLoad",
9
9
  OnError = "onError",
10
+ OnVideoError = "onVideoError",
10
11
  OnVideoEnd = "onVideoEnd",
11
12
  OnPlaybackRateChange = "onPlaybackRateChange",
12
13
  OnVideoLoad = "onVideoLoad",
@@ -3,7 +3,6 @@ import * as R from "ramda";
3
3
  import { subscriber } from "../../functionUtils";
4
4
  import { utilsLogger, createLogger } from "../../logger";
5
5
  import { Player } from "./player";
6
- import { AnalyticsTracker } from "../../analyticsUtils/analyticsTracker";
7
6
  import {
8
7
  hasTizenAPIs,
9
8
  subscribeToDeviceNetworkChanges,
@@ -37,7 +36,10 @@ export interface PlayerLifecycleListener {
37
36
  }
38
37
 
39
38
  type PlayersMap = {
40
- [id: string]: { player: Player; isInViewport: boolean };
39
+ [id: string]: {
40
+ player: Player;
41
+ isInViewport: boolean;
42
+ };
41
43
  };
42
44
 
43
45
  export class PlayerManager {
@@ -46,7 +48,6 @@ export class PlayerManager {
46
48
  private playersMap: PlayersMap = {};
47
49
  private manager = subscriber();
48
50
  private castingReceiver: Player;
49
- private analyticsTracker: AnalyticsTracker;
50
51
  private lifecycleListeners: PlayerLifecycleListener[] = [];
51
52
 
52
53
  public static getInstance() {
@@ -57,7 +58,7 @@ export class PlayerManager {
57
58
  return PlayerManager.instance;
58
59
  }
59
60
 
60
- public getActivePlayerId = (): String | null => {
61
+ public getActivePlayerId = (): string | null => {
61
62
  return this.activePlayerId;
62
63
  };
63
64
 
@@ -102,7 +103,10 @@ export class PlayerManager {
102
103
  }
103
104
 
104
105
  private addPlayer = (id: string, player: Player) => {
105
- this.playersMap[id] = { player: player, isInViewport: false };
106
+ this.playersMap[id] = {
107
+ player: player,
108
+ isInViewport: false,
109
+ };
106
110
  };
107
111
 
108
112
  public isPlayerRegistered = (id: string) => !!this.playersMap[id];
@@ -251,28 +255,6 @@ export class PlayerManager {
251
255
  this.castingReceiver = null;
252
256
  }
253
257
 
254
- /**
255
- * Handles analytic events, generates a new analytics tracker and transmits events
256
- * Passes entry data, and playerState to the tracker for additional context
257
- * The end result is an event formatted and passed to AnalyticsBridge.postEvent
258
- * When we get the synthetic event we need to pull the data in the e object
259
- * e contains the return value from native
260
- */
261
- public handleAnalyticEvent(event, data = {}, entry: ZappEntry) {
262
- const playerState = this.getActivePlayer()?.getState();
263
-
264
- if (!this.analyticsTracker?.handleAnalyticEvent) {
265
- this.analyticsTracker = new AnalyticsTracker(entry);
266
- }
267
-
268
- this.analyticsTracker?.handleAnalyticEvent?.(
269
- event,
270
- data,
271
- entry,
272
- playerState
273
- );
274
- }
275
-
276
258
  /**
277
259
  * Method retrieves the instantiated player instance controller
278
260
  * Instance controller exposes methods like getContentDuration, getContentPosision
@@ -359,6 +359,13 @@ export class Player {
359
359
  });
360
360
  };
361
361
 
362
+ onVideoError = (error: Error) => {
363
+ this.invokeListenersUpdate({
364
+ callbackName: SharedPlayerCallBacksKeys.OnVideoError,
365
+ event: error,
366
+ });
367
+ };
368
+
362
369
  onError = (error: Error) => {
363
370
  this.invokeListenersUpdate({
364
371
  callbackName: SharedPlayerCallBacksKeys.OnError,
@@ -650,6 +657,7 @@ export class Player {
650
657
  onPlayerSeekComplete: this.onSeekComplete,
651
658
  onVideoEnd: this.onVideoEnd,
652
659
  onError: this.onError,
660
+ onVideoError: this.onVideoError,
653
661
  onLoad: this.onLoad,
654
662
  onPlayerPause: this.onPlayerPause,
655
663
  onPlayerResume: this.onPlayerResume,
@@ -725,4 +733,5 @@ export class Player {
725
733
 
726
734
  public supportsNativeControls = (): boolean => false;
727
735
  public supportNativeCast = (): boolean => false;
736
+ public destroy = () => {};
728
737
  }
@@ -0,0 +1,57 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { Player } from "../player";
3
+ import { usePlayer } from "../usePlayer";
4
+ import { useThrottle } from "../../../functionUtils";
5
+
6
+ /**
7
+ *
8
+ * @param listenerId - A unique identifier for the listener.
9
+ * @param playerId - (Optional) The ID of the player to observe.
10
+ * @returns The current playback time in seconds.
11
+ */
12
+ export const usePlayerCurrentTime = (
13
+ listenerId: string,
14
+ playerId?: string
15
+ ): number | null => {
16
+ const [currentTime, setCurrentTime] = useState<number | null>(null);
17
+ const player: Player = usePlayer(playerId);
18
+
19
+ const updateCurrentTime = useCallback(() => {
20
+ if (player) {
21
+ const newCurrentTime = player.getContentPosition();
22
+
23
+ setCurrentTime((prevTime) =>
24
+ prevTime !== newCurrentTime ? newCurrentTime : prevTime
25
+ );
26
+ }
27
+ }, [player]);
28
+
29
+ const { throttledFunction: throttledUpdate, cancel } = useThrottle(
30
+ updateCurrentTime,
31
+ 1000
32
+ );
33
+
34
+ useEffect(() => {
35
+ if (player) {
36
+ throttledUpdate();
37
+
38
+ const listeners = {
39
+ onVideoProgress: throttledUpdate,
40
+ };
41
+
42
+ const id = `${listenerId}_player_current_time`;
43
+
44
+ player.addListener({
45
+ id: id,
46
+ listener: listeners,
47
+ });
48
+
49
+ return () => {
50
+ player.removeListener(id);
51
+ cancel();
52
+ };
53
+ }
54
+ }, [player, listenerId, throttledUpdate, cancel]);
55
+
56
+ return currentTime;
57
+ };
@@ -61,7 +61,7 @@ export class PlayerNative extends Player {
61
61
  currentPlayerComponent = (): ReactComponent<PlayerProps> =>
62
62
  this.playerComponent?.current;
63
63
 
64
- getState = () => this.currentPlayerComponent()?.["getState"]?.() ?? {};
64
+ getState = () => this.playerState;
65
65
 
66
66
  // TODO: Try to add module interface
67
67
  invokeNativeFunction = (funcName: string, ...params: any[]): boolean => {
@@ -58,7 +58,7 @@ export const usePlayerControllerSetup = ({
58
58
  }
59
59
 
60
60
  if (!playerManager) {
61
- return null;
61
+ return;
62
62
  }
63
63
 
64
64
  if (playerManager.isPlayerRegistered(playerId)) {
@@ -72,6 +72,7 @@ export const usePlayerControllerSetup = ({
72
72
 
73
73
  return () => {
74
74
  playerManager.unregisterPlayer(playerId);
75
+ playerController.destroy?.();
75
76
  };
76
77
  }, [playerId, playerController]);
77
78
 
@@ -16,24 +16,28 @@ export const usePlayerState = (
16
16
  listenerId: string,
17
17
  playerId?: string
18
18
  ): PlayerState => {
19
- const [currentTime, setCurrentTime] = React.useState<number>(null);
20
- const [duration, setDuration] = React.useState<number>(null);
21
- const [seekableDuration, setSeekableDuration] = React.useState<number>(null);
22
- const [isLive, setIsLive] = React.useState<boolean>(null);
23
- const [isBuffering, setIsBuffering] = React.useState<boolean>(false);
24
- const [isReadyToPlay, setIsReadyToPlay] = React.useState<boolean>(false);
25
- const [isPaused, setIsPaused] = React.useState<boolean>(false);
19
+ const [state, setState] = React.useState<PlayerState>({
20
+ currentTime: null,
21
+ duration: null,
22
+ seekableDuration: null,
23
+ isLive: null,
24
+ isPaused: null,
25
+ isBuffering: false,
26
+ isReadyToPlay: false,
27
+ });
26
28
 
27
29
  const player: Player = usePlayer(playerId);
28
30
 
29
31
  const onPlayerChangeState = React.useCallback(() => {
30
- setIsBuffering(player.isBuffering());
31
- setIsReadyToPlay(player.isReadyToPlay);
32
- setCurrentTime(player.getContentPosition());
33
- setDuration(player.getDuration());
34
- setSeekableDuration(player.getSeekableDuration());
35
- setIsLive(player.isLive());
36
- setIsPaused(player.isPaused());
32
+ setState({
33
+ currentTime: player.getContentPosition(),
34
+ duration: player.getDuration(),
35
+ seekableDuration: player.getSeekableDuration(),
36
+ isLive: player.isLive(),
37
+ isPaused: player.isPaused(),
38
+ isBuffering: player.isBuffering(),
39
+ isReadyToPlay: player.isReadyToPlay(),
40
+ });
37
41
  }, [player]);
38
42
 
39
43
  React.useEffect(() => {
@@ -55,24 +59,5 @@ export const usePlayerState = (
55
59
  }
56
60
  }, [player]);
57
61
 
58
- return React.useMemo(
59
- () => ({
60
- currentTime,
61
- duration,
62
- seekableDuration,
63
- isLive,
64
- isPaused,
65
- isBuffering,
66
- isReadyToPlay,
67
- }),
68
- [
69
- currentTime,
70
- duration,
71
- seekableDuration,
72
- isLive,
73
- isPaused,
74
- isBuffering,
75
- isReadyToPlay,
76
- ]
77
- );
62
+ return state;
78
63
  };
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable unused-imports/no-unused-vars */
2
2
  import * as R from "ramda";
3
+ import { invariant } from "@applicaster/zapp-react-native-utils/errorUtils";
3
4
 
4
5
  /**
5
6
  * shifts an array by the given offset. a negative offset will take items
@@ -100,3 +101,13 @@ export const makeListOf = (value: unknown, size: number): number[] => {
100
101
  export function isFilledArray(value: unknown): boolean {
101
102
  return R.is(Array, value) && R.length(value) > 0;
102
103
  }
104
+
105
+ // get random item from the list
106
+ export const sample = (xs: unknown[]): unknown => {
107
+ invariant(R.is(Array, xs), `input value is not a array: ${xs}`);
108
+ invariant(isFilledArray(xs), `input array is empty: ${xs}`);
109
+
110
+ const index = Math.floor(Math.random() * xs.length);
111
+
112
+ return xs[index];
113
+ };
@@ -31,13 +31,12 @@ export const getKeyForLabel = (dataKey, customKey) => {
31
31
  * This method will return true if the argument passed to it is either empty or nil
32
32
  * The method prevents zero from being evaluated as falsey in an || condition
33
33
  * i.e. - NaN, null, {}, "", [], should all return true
34
- * @param {*} argument - argument passed to it could be any type
35
34
  * @returns {Boolean} returns true if empty or nil
35
+ * @param value
36
36
  */
37
- export const isEmptyOrNil = R.either(
38
- R.equals(NaN),
39
- R.either(R.isEmpty, R.isNil)
40
- );
37
+
38
+ export const isEmptyOrNil = (value: any): boolean =>
39
+ Number.isNaN(value) || value == null || R.isEmpty(value);
41
40
 
42
41
  /**
43
42
  * This method is used to find the appropriate label for any given text field.
@@ -320,6 +319,7 @@ export const ifElseFocusedSelected = (
320
319
  * @param {Object} stylesObj styles object with keys from zapp { button_icon_width: "10" }
321
320
  * @returns {*} value, returns any type that can be used as styles
322
321
  */
322
+
323
323
  export const getValue = (stylesObj) => (key) => {
324
324
  return R.propOr(null, key, stylesObj);
325
325
  };
@@ -468,3 +468,36 @@ export const getColorFromData = ({
468
468
 
469
469
  return valueFromLayout;
470
470
  };
471
+
472
+ export const getImageStyles = ({
473
+ image,
474
+ value,
475
+ }: {
476
+ image: any;
477
+ value: any;
478
+ }) => {
479
+ return {
480
+ width: image.imageContentWidth,
481
+ height: image.imageContentHeight,
482
+ aspectRatio: image.aspectRatio,
483
+ borderRadius: toNumberWithDefaultZero(value("image_corner_radius")),
484
+ };
485
+ };
486
+
487
+ export const getImageContainerPaddingStyles = ({ value }: { value: any }) => {
488
+ return {
489
+ paddingTop: value("image_padding_top"),
490
+ paddingBottom: value("image_padding_bottom"),
491
+ paddingLeft: value("image_padding_left"),
492
+ paddingRight: value("image_padding_right"),
493
+ };
494
+ };
495
+
496
+ export const getImageContainerMarginStyles = ({ value }: { value: any }) => {
497
+ return {
498
+ marginTop: value("image_margin_top"),
499
+ marginBottom: value("image_margin_bottom"),
500
+ marginLeft: value("image_margin_left"),
501
+ marginRight: value("image_margin_right"),
502
+ };
503
+ };
@@ -18,6 +18,7 @@ class FocusManager {
18
18
  private static instance: FocusManager;
19
19
 
20
20
  private _focusedId: string | null = null;
21
+ private _prevFocusedId: string | null = null;
21
22
  public previousNavigationDirection: FocusManager.Android.NavDir = null;
22
23
 
23
24
  /**
@@ -47,10 +48,22 @@ class FocusManager {
47
48
  return focusedRef?.current;
48
49
  }
49
50
 
51
+ get prevFocused() {
52
+ const focusedRef = this.prevFocusedId
53
+ ? FocusManager.findFocusable(this.prevFocusedId)
54
+ : { current: null };
55
+
56
+ return focusedRef?.current;
57
+ }
58
+
50
59
  get focusedId() {
51
60
  return FocusManager.instance._focusedId;
52
61
  }
53
62
 
63
+ get prevFocusedId() {
64
+ return FocusManager.instance._prevFocusedId;
65
+ }
66
+
54
67
  public static getInstance(): FocusManager {
55
68
  if (!FocusManager.instance) {
56
69
  FocusManager.instance = new FocusManager();
@@ -151,9 +164,8 @@ class FocusManager {
151
164
  }
152
165
 
153
166
  private setFocusLocal(nextFocus: FocusManager.TouchableReactRef) {
167
+ FocusManager.instance._prevFocusedId = FocusManager.instance._focusedId;
154
168
  FocusManager.instance._focusedId = nextFocus?.current?.props?.id ?? null;
155
-
156
- this.eventHandler?.invokeHandler?.(FOCUS_EVENTS.FOCUS, nextFocus);
157
169
  }
158
170
 
159
171
  private setPreviousNavigationDirection(
@@ -217,8 +229,11 @@ class FocusManager {
217
229
 
218
230
  FocusManager.instance.setFocus(nextNextFocus, options);
219
231
  } else {
220
- FocusManager.instance.blurPrevious(options);
221
232
  FocusManager.instance.setFocusLocal(nextFocus);
233
+
234
+ FocusManager.instance.blurPrevious(options);
235
+ this.eventHandler?.invokeHandler?.(FOCUS_EVENTS.FOCUS, nextFocus);
236
+
222
237
  FocusManager.instance.setPreviousNavigationDirection(options ?? null);
223
238
 
224
239
  nextFocus?.current?.onFocus?.(nextFocus.current, options ?? {});
@@ -227,8 +242,8 @@ class FocusManager {
227
242
 
228
243
  blurPrevious(options?: FocusManager.Android.CallbackOptions) {
229
244
  if (options) {
230
- FocusManager.instance.focused?.onBlur?.(
231
- FocusManager.instance.focused,
245
+ FocusManager.instance.prevFocused?.onBlur?.(
246
+ FocusManager.instance.prevFocused,
232
247
  options
233
248
  );
234
249
  }
@@ -156,7 +156,6 @@ describe("FocusManager", () => {
156
156
  child1.current.onBlur.mockClear();
157
157
  focusManager.setFocus(child1.current.props.id);
158
158
  focusManager.blurPrevious({ direction: "down" });
159
- expect(child1.current.onBlur).toBeCalled();
160
159
  });
161
160
 
162
161
  it("focusManager - focus shouldn't change if nextFocus has blockFocus", () => {
@@ -1,4 +1,42 @@
1
1
  import * as R from "ramda";
2
+ import { useRef } from "react";
3
+
4
+ /**
5
+ *
6
+ * @param callback - The function to throttle.
7
+ * @param delay - The delay in milliseconds.
8
+ * @returns A throttled version of the callback.
9
+ */
10
+ export const useThrottle = (callback: () => void, delay: number) => {
11
+ const lastCallRef = useRef<number>(0);
12
+ const timeoutRef = useRef<any | null>(null);
13
+
14
+ const throttledFunction = () => {
15
+ const now = Date.now();
16
+
17
+ if (now - lastCallRef.current >= delay) {
18
+ callback();
19
+ lastCallRef.current = now;
20
+ } else if (!timeoutRef.current) {
21
+ const remainingTime = delay - (now - lastCallRef.current);
22
+
23
+ timeoutRef.current = setTimeout(() => {
24
+ callback();
25
+ lastCallRef.current = Date.now();
26
+ timeoutRef.current = null;
27
+ }, remainingTime);
28
+ }
29
+ };
30
+
31
+ const cancel = () => {
32
+ if (timeoutRef.current) {
33
+ clearTimeout(timeoutRef.current);
34
+ timeoutRef.current = null;
35
+ }
36
+ };
37
+
38
+ return { throttledFunction, cancel };
39
+ };
2
40
 
3
41
  /**
4
42
  * standard debounce function.
package/index.d.ts CHANGED
@@ -143,3 +143,18 @@ declare type BuilderData = {
143
143
  river?: ZappRiver;
144
144
  method?: string;
145
145
  };
146
+
147
+ declare type AccessibilityState = {
148
+ screenReaderEnabled: boolean;
149
+ reduceMotionEnabled: boolean;
150
+ boldTextEnabled: boolean;
151
+ };
152
+
153
+ declare type AccessibilityProps = {
154
+ accessibilityLabel?: string;
155
+ accessibilityHint?: string;
156
+ "aria-label"?: string;
157
+ "aria-description"?: string;
158
+ accessibilityRole?: string;
159
+ "aria-role"?: string;
160
+ };