@namiml/expo-sdk 3.4.0-dev.202605060437
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/index.cjs +4000 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.mjs +3966 -0
- package/dist/index.mjs.map +1 -0
- package/nami-expo-nami-iap.tgz +0 -0
- package/package.json +92 -0
- package/src/adapters/expo-device.adapter.ts +106 -0
- package/src/adapters/expo-purchase.adapter.ts +79 -0
- package/src/adapters/expo-storage.adapter.ts +92 -0
- package/src/adapters/expo-ui.adapter.ts +57 -0
- package/src/adapters/index.ts +33 -0
- package/src/amazon-kepler.d.ts +7 -0
- package/src/components/NamiView.tsx +1006 -0
- package/src/components/PaywallScreen.tsx +245 -0
- package/src/components/TemplateRenderer.tsx +243 -0
- package/src/components/containers/NamiBackgroundContainer.tsx +103 -0
- package/src/components/containers/NamiCarousel.tsx +217 -0
- package/src/components/containers/NamiCollapseContainer.tsx +116 -0
- package/src/components/containers/NamiContainer.tsx +315 -0
- package/src/components/containers/NamiContentContainer.tsx +140 -0
- package/src/components/containers/NamiFooter.tsx +35 -0
- package/src/components/containers/NamiHeader.tsx +45 -0
- package/src/components/containers/NamiProductContainer.tsx +248 -0
- package/src/components/containers/NamiRepeatingGrid.tsx +81 -0
- package/src/components/containers/NamiResponsiveGrid.tsx +75 -0
- package/src/components/containers/NamiStack.tsx +69 -0
- package/src/components/elements/NamiButton.tsx +285 -0
- package/src/components/elements/NamiCountdownTimer.tsx +123 -0
- package/src/components/elements/NamiImage.tsx +177 -0
- package/src/components/elements/NamiPlayPauseButton.tsx +93 -0
- package/src/components/elements/NamiProgressBar.tsx +90 -0
- package/src/components/elements/NamiProgressIndicator.tsx +41 -0
- package/src/components/elements/NamiQRCode.tsx +51 -0
- package/src/components/elements/NamiRadioButton.tsx +62 -0
- package/src/components/elements/NamiSegmentPicker.tsx +67 -0
- package/src/components/elements/NamiSegmentPickerItem.tsx +184 -0
- package/src/components/elements/NamiSpacer.tsx +23 -0
- package/src/components/elements/NamiSymbol.tsx +104 -0
- package/src/components/elements/NamiText.tsx +311 -0
- package/src/components/elements/NamiToggleButton.tsx +102 -0
- package/src/components/elements/NamiToggleSwitch.tsx +64 -0
- package/src/components/elements/NamiVideo.kepler.tsx +638 -0
- package/src/components/elements/NamiVideo.tsx +133 -0
- package/src/components/elements/NamiVolumeButton.tsx +93 -0
- package/src/context/FocusContext.tsx +169 -0
- package/src/context/PaywallContext.tsx +343 -0
- package/src/global.d.ts +5 -0
- package/src/index.ts +62 -0
- package/src/nami.ts +24 -0
- package/src/react-native-qrcode-svg.d.ts +4 -0
- package/src/utils/actionHandler.ts +281 -0
- package/src/utils/fonts.ts +359 -0
- package/src/utils/iconMap.ts +67 -0
- package/src/utils/impression.ts +39 -0
- package/src/utils/rendering.ts +197 -0
- package/src/utils/smartText.ts +148 -0
- package/src/utils/styles.ts +668 -0
- package/src/utils/tvFocus.ts +31 -0
- package/src/utils/videoControls.ts +49 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { View, Text, StyleSheet } from 'react-native';
|
|
3
|
+
import { applyStyles, parseSizeOrPercent } from '../../utils/styles';
|
|
4
|
+
import { usePaywallContext } from '../../context/PaywallContext';
|
|
5
|
+
import { buildSmartTextReplacements, interpolateSmartText } from '../../utils/smartText';
|
|
6
|
+
import { initializeVideoControls, subscribeVideoControls } from '../../utils/videoControls';
|
|
7
|
+
|
|
8
|
+
// Resolve expo-video at module scope so hooks are called unconditionally
|
|
9
|
+
let ExpoVideoView: React.ComponentType<any> | null = null;
|
|
10
|
+
let useExpoVideoPlayer: ((source: string, setup?: (player: any) => void) => any) | null = null;
|
|
11
|
+
try {
|
|
12
|
+
const expoVideo = require('expo-video');
|
|
13
|
+
ExpoVideoView = expoVideo.VideoView;
|
|
14
|
+
useExpoVideoPlayer = expoVideo.useVideoPlayer;
|
|
15
|
+
} catch {
|
|
16
|
+
// expo-video is optional
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
component: any;
|
|
21
|
+
scaleFactor: number;
|
|
22
|
+
parentDirection?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Inner component that uses expo-video hooks.
|
|
27
|
+
* Only rendered when expo-video is available, so the hook call is safe.
|
|
28
|
+
*/
|
|
29
|
+
const ExpoVideoPlayer: React.FC<{
|
|
30
|
+
url: string;
|
|
31
|
+
width: any;
|
|
32
|
+
height: any;
|
|
33
|
+
containerStyle: any;
|
|
34
|
+
component: any;
|
|
35
|
+
playing: boolean;
|
|
36
|
+
muted: boolean;
|
|
37
|
+
}> = ({ url, width, height, containerStyle, component, playing, muted }) => {
|
|
38
|
+
const VideoView = ExpoVideoView as React.ComponentType<any>;
|
|
39
|
+
const player = useExpoVideoPlayer!(url, (p) => {
|
|
40
|
+
p.loop = component.loopVideo ?? component.loop ?? false;
|
|
41
|
+
p.muted = muted;
|
|
42
|
+
if (playing) {
|
|
43
|
+
p.play();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!player) return;
|
|
49
|
+
if (playing) {
|
|
50
|
+
player.play();
|
|
51
|
+
} else {
|
|
52
|
+
player.pause();
|
|
53
|
+
}
|
|
54
|
+
}, [player, playing]);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!player) return;
|
|
58
|
+
player.muted = muted;
|
|
59
|
+
}, [player, muted]);
|
|
60
|
+
|
|
61
|
+
const contentFit = component.imageCropping === 'fit' ? 'contain' : 'cover';
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<View style={containerStyle}>
|
|
65
|
+
<VideoView
|
|
66
|
+
player={player}
|
|
67
|
+
style={{ width, height }}
|
|
68
|
+
contentFit={contentFit}
|
|
69
|
+
nativeControls={component.controlsType !== 'none'}
|
|
70
|
+
/>
|
|
71
|
+
</View>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const NamiVideo: React.FC<Props> = ({ component, scaleFactor, parentDirection }) => {
|
|
76
|
+
const ctx = usePaywallContext();
|
|
77
|
+
const [playing, setPlaying] = useState(!!component.autoplayVideo);
|
|
78
|
+
const [muted, setMuted] = useState(!!(component.mute ?? component.muted));
|
|
79
|
+
const smartTextSku = component?.smartTextSku ?? component?.sku;
|
|
80
|
+
|
|
81
|
+
const url = useMemo(() => {
|
|
82
|
+
const raw = component.url ?? component.videoUrl ?? (ctx.state as any).appSuppliedVideoUrl ?? '';
|
|
83
|
+
const replacements = buildSmartTextReplacements(ctx.state, ctx.flow, smartTextSku);
|
|
84
|
+
const resolved = interpolateSmartText(raw, replacements);
|
|
85
|
+
return resolved == null ? '' : String(resolved);
|
|
86
|
+
}, [component, ctx.state, ctx.flow, smartTextSku]);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const playingFromConfig = component.paused != null
|
|
90
|
+
? !component.paused
|
|
91
|
+
: !!component.autoplayVideo;
|
|
92
|
+
initializeVideoControls({
|
|
93
|
+
playing: playingFromConfig,
|
|
94
|
+
muted: !!(component.mute ?? component.muted),
|
|
95
|
+
});
|
|
96
|
+
}, [component.paused, component.autoplayVideo, component.mute, component.muted]);
|
|
97
|
+
|
|
98
|
+
useEffect(() => subscribeVideoControls((state) => {
|
|
99
|
+
setPlaying(state.playing);
|
|
100
|
+
setMuted(state.muted);
|
|
101
|
+
}), []);
|
|
102
|
+
|
|
103
|
+
const containerStyle = applyStyles(component, scaleFactor, false, parentDirection);
|
|
104
|
+
const rawW = component.width ?? component.fixedWidth;
|
|
105
|
+
const rawH = component.height ?? component.fixedHeight;
|
|
106
|
+
const w = parseSizeOrPercent(rawW, scaleFactor) ?? '100%';
|
|
107
|
+
const h = parseSizeOrPercent(rawH, scaleFactor) ?? 200;
|
|
108
|
+
|
|
109
|
+
if (ExpoVideoView && useExpoVideoPlayer && url) {
|
|
110
|
+
return (
|
|
111
|
+
<ExpoVideoPlayer
|
|
112
|
+
url={url}
|
|
113
|
+
width={w}
|
|
114
|
+
height={h}
|
|
115
|
+
containerStyle={containerStyle}
|
|
116
|
+
component={component}
|
|
117
|
+
playing={playing}
|
|
118
|
+
muted={muted}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<View style={[containerStyle, styles.placeholder, { width: w as any, height: h as any }]}>
|
|
125
|
+
<Text style={styles.placeholderText} allowFontScaling={false}>Video</Text>
|
|
126
|
+
</View>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const styles = StyleSheet.create({
|
|
131
|
+
placeholder: { backgroundColor: '#000', justifyContent: 'center', alignItems: 'center' },
|
|
132
|
+
placeholderText: { color: '#fff', fontSize: 14 },
|
|
133
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { Pressable, StyleSheet, View } from 'react-native';
|
|
3
|
+
import { applyStyles, childSpacingStyle, focusedStyleOverrides } from '../../utils/styles';
|
|
4
|
+
import { TemplateRenderer } from '../TemplateRenderer';
|
|
5
|
+
import { usePaywallContext } from '../../context/PaywallContext';
|
|
6
|
+
import { handleAction } from '../../utils/actionHandler';
|
|
7
|
+
import { getVideoControlState, subscribeVideoControls } from '../../utils/videoControls';
|
|
8
|
+
import type { TComponent } from '@namiml/sdk-core';
|
|
9
|
+
import { FocusedStyleProvider, useFocusableState, useFocusEnabled, useRegisterPreferredFocus } from '../../context/FocusContext';
|
|
10
|
+
import { useTVPreferredFocus } from '../../utils/tvFocus';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
component: any;
|
|
14
|
+
scaleFactor: number;
|
|
15
|
+
onClose?: () => void;
|
|
16
|
+
parentDirection?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const NamiVolumeButton: React.FC<Props> = ({
|
|
20
|
+
component,
|
|
21
|
+
scaleFactor,
|
|
22
|
+
onClose,
|
|
23
|
+
parentDirection,
|
|
24
|
+
}) => {
|
|
25
|
+
const ctx = usePaywallContext();
|
|
26
|
+
const focusEnabled = useFocusEnabled();
|
|
27
|
+
const pressableRef = useRef<any>(null);
|
|
28
|
+
const { isFocused, onFocus, onBlur } = useFocusableState(
|
|
29
|
+
Boolean(component.focused),
|
|
30
|
+
focusEnabled,
|
|
31
|
+
);
|
|
32
|
+
useTVPreferredFocus(
|
|
33
|
+
pressableRef,
|
|
34
|
+
focusEnabled && Boolean(component.focused),
|
|
35
|
+
);
|
|
36
|
+
useRegisterPreferredFocus(
|
|
37
|
+
pressableRef.current,
|
|
38
|
+
focusEnabled && Boolean(component.focused),
|
|
39
|
+
);
|
|
40
|
+
const [muted, setMuted] = useState(getVideoControlState().muted);
|
|
41
|
+
|
|
42
|
+
useEffect(() => subscribeVideoControls((next) => setMuted(next.muted)), []);
|
|
43
|
+
|
|
44
|
+
const containerStyle = useMemo(() => {
|
|
45
|
+
const base = applyStyles(component, scaleFactor, false, parentDirection);
|
|
46
|
+
if (isFocused) {
|
|
47
|
+
Object.assign(base, focusedStyleOverrides(component, scaleFactor));
|
|
48
|
+
}
|
|
49
|
+
return base;
|
|
50
|
+
}, [component, scaleFactor, parentDirection, isFocused]);
|
|
51
|
+
|
|
52
|
+
const onPress = () => {
|
|
53
|
+
handleAction({
|
|
54
|
+
onTap: muted ? component.mutedOnTap : component.volumeOnTap,
|
|
55
|
+
ctx,
|
|
56
|
+
onClose,
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const direction = component.direction ?? 'horizontal';
|
|
61
|
+
const children = (muted ? component.mutedComponents : component.volumeComponents) ?? [];
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Pressable
|
|
65
|
+
ref={pressableRef}
|
|
66
|
+
onPress={onPress}
|
|
67
|
+
onFocus={onFocus}
|
|
68
|
+
onBlur={onBlur}
|
|
69
|
+
hasTVPreferredFocus={focusEnabled && Boolean(component.focused)}
|
|
70
|
+
style={({ pressed }) => [containerStyle, pressed ? styles.pressed : null]}
|
|
71
|
+
>
|
|
72
|
+
<FocusedStyleProvider focused={isFocused}>
|
|
73
|
+
{children.map((child: TComponent, i: number) => (
|
|
74
|
+
<View
|
|
75
|
+
key={child.id ?? i}
|
|
76
|
+
style={i === 0 ? null : childSpacingStyle(i, { spacing: component.spacing, direction }, scaleFactor)}
|
|
77
|
+
>
|
|
78
|
+
<TemplateRenderer
|
|
79
|
+
component={child}
|
|
80
|
+
scaleFactor={scaleFactor}
|
|
81
|
+
onClose={onClose}
|
|
82
|
+
parentDirection={direction}
|
|
83
|
+
/>
|
|
84
|
+
</View>
|
|
85
|
+
))}
|
|
86
|
+
</FocusedStyleProvider>
|
|
87
|
+
</Pressable>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const styles = StyleSheet.create({
|
|
92
|
+
pressed: { opacity: 0.7 },
|
|
93
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import React, { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
type FocusScopeValue = {
|
|
4
|
+
reportDescendantFocusDelta(delta: number): void;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type PreferredFocusValue = {
|
|
8
|
+
registerPreferredTarget(target: unknown): () => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const FocusScopeContext = createContext<FocusScopeValue | null>(null);
|
|
12
|
+
const FocusedStyleContext = createContext(false);
|
|
13
|
+
const FocusContext = createContext(true);
|
|
14
|
+
const PreferredFocusContext = createContext<PreferredFocusValue | null>(null);
|
|
15
|
+
|
|
16
|
+
export const FocusScope: React.FC<{
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
onFocusWithinChange?: (focused: boolean) => void;
|
|
19
|
+
}> = ({ children, onFocusWithinChange }) => {
|
|
20
|
+
const parentScope = useContext(FocusScopeContext);
|
|
21
|
+
const focusedCountRef = useRef(0);
|
|
22
|
+
|
|
23
|
+
const reportDescendantFocusDelta = useCallback((delta: number) => {
|
|
24
|
+
const previousCount = focusedCountRef.current;
|
|
25
|
+
const nextCount = Math.max(0, previousCount + delta);
|
|
26
|
+
focusedCountRef.current = nextCount;
|
|
27
|
+
|
|
28
|
+
if ((previousCount === 0) === (nextCount === 0)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const isFocused = nextCount > 0;
|
|
33
|
+
onFocusWithinChange?.(isFocused);
|
|
34
|
+
parentScope?.reportDescendantFocusDelta(isFocused ? 1 : -1);
|
|
35
|
+
}, [onFocusWithinChange, parentScope]);
|
|
36
|
+
|
|
37
|
+
useEffect(() => () => {
|
|
38
|
+
if (focusedCountRef.current > 0) {
|
|
39
|
+
parentScope?.reportDescendantFocusDelta(-1);
|
|
40
|
+
}
|
|
41
|
+
}, [parentScope]);
|
|
42
|
+
|
|
43
|
+
const value = useMemo<FocusScopeValue>(
|
|
44
|
+
() => ({ reportDescendantFocusDelta }),
|
|
45
|
+
[reportDescendantFocusDelta],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<FocusScopeContext.Provider value={value}>
|
|
50
|
+
{children}
|
|
51
|
+
</FocusScopeContext.Provider>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const FocusedStyleProvider: React.FC<{
|
|
56
|
+
children: React.ReactNode;
|
|
57
|
+
focused: boolean;
|
|
58
|
+
}> = ({ children, focused }) => {
|
|
59
|
+
const parentFocused = useContext(FocusedStyleContext);
|
|
60
|
+
const value = parentFocused || focused;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<FocusedStyleContext.Provider value={value}>
|
|
64
|
+
{children}
|
|
65
|
+
</FocusedStyleContext.Provider>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const FocusProvider: React.FC<{
|
|
70
|
+
children: React.ReactNode;
|
|
71
|
+
enabled: boolean;
|
|
72
|
+
}> = ({ children, enabled }) => (
|
|
73
|
+
<FocusContext.Provider value={enabled}>
|
|
74
|
+
{children}
|
|
75
|
+
</FocusContext.Provider>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
export const PreferredFocusProvider: React.FC<{
|
|
79
|
+
children: React.ReactNode;
|
|
80
|
+
onPreferredTargetChange?: (target: unknown | null) => void;
|
|
81
|
+
}> = ({ children, onPreferredTargetChange }) => {
|
|
82
|
+
const registerPreferredTarget = useCallback((target: unknown) => {
|
|
83
|
+
onPreferredTargetChange?.(target);
|
|
84
|
+
|
|
85
|
+
return () => {
|
|
86
|
+
onPreferredTargetChange?.(null);
|
|
87
|
+
};
|
|
88
|
+
}, [onPreferredTargetChange]);
|
|
89
|
+
|
|
90
|
+
const value = useMemo<PreferredFocusValue>(
|
|
91
|
+
() => ({ registerPreferredTarget }),
|
|
92
|
+
[registerPreferredTarget],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<PreferredFocusContext.Provider value={value}>
|
|
97
|
+
{children}
|
|
98
|
+
</PreferredFocusContext.Provider>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export function useInheritedFocusedStyle(): boolean {
|
|
103
|
+
return useContext(FocusedStyleContext);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function useFocusEnabled(): boolean {
|
|
107
|
+
return useContext(FocusContext);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function useRegisterPreferredFocus(target: unknown, enabled: boolean): void {
|
|
111
|
+
const context = useContext(PreferredFocusContext);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!enabled || target == null) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return context?.registerPreferredTarget(target);
|
|
119
|
+
}, [context, enabled, target]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function useFocusableState(initialFocused = false, enabled = true) {
|
|
123
|
+
const scope = useContext(FocusScopeContext);
|
|
124
|
+
const [isFocused, setIsFocused] = useState(initialFocused);
|
|
125
|
+
const isFocusedRef = useRef(initialFocused);
|
|
126
|
+
const reportedToScopeRef = useRef(false);
|
|
127
|
+
|
|
128
|
+
const setFocused = useCallback((focused: boolean) => {
|
|
129
|
+
if (isFocusedRef.current === focused) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
isFocusedRef.current = focused;
|
|
134
|
+
setIsFocused(focused);
|
|
135
|
+
if (focused && !reportedToScopeRef.current) {
|
|
136
|
+
scope?.reportDescendantFocusDelta(1);
|
|
137
|
+
reportedToScopeRef.current = true;
|
|
138
|
+
} else if (!focused && reportedToScopeRef.current) {
|
|
139
|
+
scope?.reportDescendantFocusDelta(-1);
|
|
140
|
+
reportedToScopeRef.current = false;
|
|
141
|
+
}
|
|
142
|
+
}, [scope]);
|
|
143
|
+
|
|
144
|
+
useLayoutEffect(() => {
|
|
145
|
+
if (!enabled) {
|
|
146
|
+
setFocused(false);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (initialFocused && !reportedToScopeRef.current) {
|
|
151
|
+
scope?.reportDescendantFocusDelta(1);
|
|
152
|
+
reportedToScopeRef.current = true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return () => {
|
|
156
|
+
if (reportedToScopeRef.current) {
|
|
157
|
+
reportedToScopeRef.current = false;
|
|
158
|
+
scope?.reportDescendantFocusDelta(-1);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}, [enabled, initialFocused, scope, setFocused]);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
isFocused,
|
|
165
|
+
onFocus: useCallback(() => setFocused(true), [setFocused]),
|
|
166
|
+
onBlur: useCallback(() => setFocused(false), [setFocused]),
|
|
167
|
+
setFocused,
|
|
168
|
+
};
|
|
169
|
+
}
|