refactor: use better context
This commit is contained in:
parent
d9635bb135
commit
e165a829b7
|
@ -4,36 +4,37 @@ import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
View,
|
View,
|
||||||
FlatList,
|
FlatList,
|
||||||
ViewStyle,
|
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
Animated,
|
Animated,
|
||||||
NativeSyntheticEvent,
|
NativeSyntheticEvent,
|
||||||
NativeScrollEvent,
|
NativeScrollEvent,
|
||||||
|
ViewToken,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { CATEGORIES, CategoryTypes, EmojiType } from './types';
|
import { CATEGORIES, CategoryTypes } from './types';
|
||||||
import { EmojiCategory } from './components/EmojiCategory';
|
import { EmojiCategory } from './components/EmojiCategory';
|
||||||
import { KeyboardContext } from './KeyboardContext';
|
import { KeyboardContext } from './KeyboardContext';
|
||||||
import { Categories } from './components/Categories';
|
import { Categories } from './components/Categories';
|
||||||
|
|
||||||
type EmojiKeyboardProps = {
|
export const EmojiKeyboard = () => {
|
||||||
onEmojiSelected: (emoji: EmojiType) => void;
|
|
||||||
containerStyles?: ViewStyle;
|
|
||||||
numberOfColumns?: number;
|
|
||||||
emojiSize?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EmojiKeyboard = ({
|
|
||||||
onEmojiSelected,
|
|
||||||
containerStyles,
|
|
||||||
numberOfColumns = 7,
|
|
||||||
emojiSize = 28,
|
|
||||||
}: EmojiKeyboardProps) => {
|
|
||||||
const { width } = useWindowDimensions();
|
const { width } = useWindowDimensions();
|
||||||
|
const ctx = React.useContext(KeyboardContext);
|
||||||
|
|
||||||
const flatListRef = React.useRef<FlatList>(null);
|
const flatListRef = React.useRef<FlatList>(null);
|
||||||
|
|
||||||
const scrollX = React.useRef(new Animated.Value(0)).current;
|
const scrollX = React.useRef(new Animated.Value(0)).current;
|
||||||
const scrollNav = React.useRef(new Animated.Value(0)).current;
|
const scrollNav = React.useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
const onViewableItemsChanged = React.useRef(
|
||||||
|
({ viewableItems }: { viewableItems: Array<ViewToken> }) => {
|
||||||
|
if (viewableItems.length > 0 && viewableItems[0].index !== null) {
|
||||||
|
ctx?.setActiveCategoryIndex(viewableItems[0].index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const viewabilityConfig = React.useRef({
|
||||||
|
viewAreaCoveragePercentThreshold: 60,
|
||||||
|
});
|
||||||
|
|
||||||
const getItemLayout = (
|
const getItemLayout = (
|
||||||
_: CategoryTypes[] | null | undefined,
|
_: CategoryTypes[] | null | undefined,
|
||||||
index: number
|
index: number
|
||||||
|
@ -63,15 +64,9 @@ export const EmojiKeyboard = ({
|
||||||
useNativeDriver: false,
|
useNativeDriver: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardContext.Provider
|
<View style={[styles.container, ctx?.containerStyles]}>
|
||||||
value={{
|
|
||||||
onEmojiSelected,
|
|
||||||
numberOfColumns,
|
|
||||||
emojiSize,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View style={[styles.container, containerStyles]}>
|
|
||||||
<FlatList
|
<FlatList
|
||||||
data={CATEGORIES}
|
data={CATEGORIES}
|
||||||
keyExtractor={(item) => item}
|
keyExtractor={(item) => item}
|
||||||
|
@ -86,10 +81,11 @@ export const EmojiKeyboard = ({
|
||||||
decelerationRate="fast"
|
decelerationRate="fast"
|
||||||
getItemLayout={getItemLayout}
|
getItemLayout={getItemLayout}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
|
onViewableItemsChanged={onViewableItemsChanged.current}
|
||||||
|
viewabilityConfig={viewabilityConfig.current}
|
||||||
/>
|
/>
|
||||||
<Categories flatListRef={flatListRef} scrollNav={scrollNav} />
|
<Categories flatListRef={flatListRef} scrollNav={scrollNav} />
|
||||||
</View>
|
</View>
|
||||||
</KeyboardContext.Provider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,11 @@ import {
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import type { EmojiKeyboardProps } from './types';
|
|
||||||
import { EmojiKeyboard } from './EmojiKeyboard';
|
import { EmojiKeyboard } from './EmojiKeyboard';
|
||||||
import { Knob } from './components/Knob';
|
import { Knob } from './components/Knob';
|
||||||
|
import { KeyboardProvider } from './KeyboardProvider';
|
||||||
|
import type { KeyboardProps } from './KeyboardContext';
|
||||||
|
import type { EmojiType } from './types';
|
||||||
|
|
||||||
type EmojiPickerProps = {
|
type EmojiPickerProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
@ -21,12 +23,22 @@ export const EmojiPicker = ({
|
||||||
onEmojiSelected,
|
onEmojiSelected,
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
}: EmojiPickerProps & EmojiKeyboardProps) => {
|
...props
|
||||||
|
}: EmojiPickerProps & KeyboardProps) => {
|
||||||
const { height: screenHeight } = useWindowDimensions();
|
const { height: screenHeight } = useWindowDimensions();
|
||||||
const offsetY = React.useRef(new Animated.Value(0)).current;
|
const offsetY = React.useRef(new Animated.Value(0)).current;
|
||||||
const height = React.useRef(new Animated.Value(screenHeight * 0.4)).current;
|
const height = React.useRef(new Animated.Value(screenHeight * 0.4)).current;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<KeyboardProvider
|
||||||
|
onEmojiSelected={(emoji: EmojiType) => {
|
||||||
|
height.setValue(screenHeight * 0.4);
|
||||||
|
offsetY.setValue(0);
|
||||||
|
onEmojiSelected(emoji);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
<Modal visible={isOpen} animationType="slide" transparent={true}>
|
<Modal visible={isOpen} animationType="slide" transparent={true}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.modalContainer}
|
style={styles.modalContainer}
|
||||||
|
@ -39,17 +51,12 @@ export const EmojiPicker = ({
|
||||||
<>
|
<>
|
||||||
<Knob height={height} offsetY={offsetY} onClose={onClose} />
|
<Knob height={height} offsetY={offsetY} onClose={onClose} />
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={{ height: Animated.subtract(height, offsetY) }}
|
style={[
|
||||||
|
{ height: Animated.subtract(height, offsetY) },
|
||||||
|
styles.container,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<EmojiKeyboard
|
<EmojiKeyboard />
|
||||||
onEmojiSelected={(emoji) => {
|
|
||||||
height.setValue(screenHeight * 0.4);
|
|
||||||
offsetY.setValue(0);
|
|
||||||
onEmojiSelected(emoji);
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
containerStyles={styles.container}
|
|
||||||
/>
|
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</>
|
</>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
@ -57,6 +64,7 @@ export const EmojiPicker = ({
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
</KeyboardProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import type { ViewStyle } from 'react-native';
|
||||||
import type { EmojiType } from './types';
|
import type { EmojiType } from './types';
|
||||||
|
|
||||||
export const defaultKeyboardContext = {
|
export type KeyboardProps = {
|
||||||
onEmojiSelected: (_emoji: EmojiType) => {},
|
onEmojiSelected: (emoji: EmojiType) => void;
|
||||||
numberOfColumns: 7,
|
numberOfColumns?: number;
|
||||||
emojiSize: 24,
|
emojiSize?: number;
|
||||||
|
containerStyles?: ViewStyle;
|
||||||
|
};
|
||||||
|
export type ContextValues = {
|
||||||
|
activeCategoryIndex: number;
|
||||||
|
setActiveCategoryIndex: (index: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KeyboardContext = React.createContext(defaultKeyboardContext);
|
export const KeyboardContext = React.createContext<
|
||||||
|
(KeyboardProps & ContextValues) | null
|
||||||
|
>(null);
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
KeyboardProps,
|
||||||
|
ContextValues,
|
||||||
|
KeyboardContext,
|
||||||
|
} from './KeyboardContext';
|
||||||
|
import type { EmojiType } from './types';
|
||||||
|
|
||||||
|
type ProviderProps = KeyboardProps & {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultKeyboardContext: KeyboardProps = {
|
||||||
|
onEmojiSelected: (_emoji: EmojiType) => {},
|
||||||
|
numberOfColumns: 7,
|
||||||
|
emojiSize: 24,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultKeyboardValues: Partial<ContextValues> = {
|
||||||
|
// activeCategoryIndex: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KeyboardProvider: React.FC<ProviderProps> = React.memo((props) => {
|
||||||
|
const [activeCategoryIndex, setActiveCategoryIndex] = React.useState(0);
|
||||||
|
|
||||||
|
const value: KeyboardProps & ContextValues = {
|
||||||
|
...defaultKeyboardContext,
|
||||||
|
...defaultKeyboardValues,
|
||||||
|
...props,
|
||||||
|
activeCategoryIndex,
|
||||||
|
setActiveCategoryIndex,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<KeyboardContext.Provider value={value}>
|
||||||
|
{props.children}
|
||||||
|
</KeyboardContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
KeyboardProvider.displayName = 'KeyboardProvider';
|
|
@ -1,5 +1,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { View, Animated, StyleSheet, FlatList } from 'react-native';
|
import { View, Animated, StyleSheet, FlatList } from 'react-native';
|
||||||
|
import { KeyboardContext } from '../KeyboardContext';
|
||||||
import { CATEGORIES, CATEGORIES_NAVIGATION, CategoryTypes } from '../types';
|
import { CATEGORIES, CATEGORIES_NAVIGATION, CategoryTypes } from '../types';
|
||||||
import { CategoryItem } from './CategoryItem';
|
import { CategoryItem } from './CategoryItem';
|
||||||
|
|
||||||
|
@ -9,6 +10,7 @@ type CategoriesProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Categories = ({ flatListRef, scrollNav }: CategoriesProps) => {
|
export const Categories = ({ flatListRef, scrollNav }: CategoriesProps) => {
|
||||||
|
const ctx = React.useContext(KeyboardContext);
|
||||||
const handleScrollToCategory = (category: CategoryTypes) => {
|
const handleScrollToCategory = (category: CategoryTypes) => {
|
||||||
flatListRef?.current?.scrollToIndex({
|
flatListRef?.current?.scrollToIndex({
|
||||||
index: CATEGORIES.indexOf(category),
|
index: CATEGORIES.indexOf(category),
|
||||||
|
@ -20,9 +22,10 @@ export const Categories = ({ flatListRef, scrollNav }: CategoriesProps) => {
|
||||||
<FlatList
|
<FlatList
|
||||||
data={CATEGORIES_NAVIGATION}
|
data={CATEGORIES_NAVIGATION}
|
||||||
keyExtractor={(item) => item.category}
|
keyExtractor={(item) => item.category}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item, index }) => (
|
||||||
<CategoryItem
|
<CategoryItem
|
||||||
item={item}
|
item={item}
|
||||||
|
index={index}
|
||||||
handleScrollToCategory={handleScrollToCategory}
|
handleScrollToCategory={handleScrollToCategory}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -37,6 +40,7 @@ export const Categories = ({ flatListRef, scrollNav }: CategoriesProps) => {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
extraData={ctx?.activeCategoryIndex}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -1,22 +1,38 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
|
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
||||||
|
import { KeyboardContext } from '../KeyboardContext';
|
||||||
import type { CategoryTypes } from '../types';
|
import type { CategoryTypes } from '../types';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
|
||||||
type CategoryItemProps = {
|
type CategoryItemProps = {
|
||||||
item: any;
|
item: any;
|
||||||
|
index: number;
|
||||||
handleScrollToCategory: (category: CategoryTypes) => void;
|
handleScrollToCategory: (category: CategoryTypes) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CategoryItem = ({
|
export const CategoryItem = ({
|
||||||
item,
|
item,
|
||||||
|
index,
|
||||||
handleScrollToCategory,
|
handleScrollToCategory,
|
||||||
}: CategoryItemProps) => (
|
}: CategoryItemProps) => {
|
||||||
<TouchableOpacity onPress={() => handleScrollToCategory(item.category)}>
|
const ctx = React.useContext(KeyboardContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
handleScrollToCategory(item.category);
|
||||||
|
ctx?.setActiveCategoryIndex(index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.icon}>{item.icon}</Text>
|
<Icon
|
||||||
|
iconName={item.icon}
|
||||||
|
isActive={ctx?.activeCategoryIndex === index}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
|
|
@ -16,7 +16,7 @@ export const EmojiCategory = ({ item }: { item: CategoryTypes }) => {
|
||||||
const { width } = useWindowDimensions();
|
const { width } = useWindowDimensions();
|
||||||
const ctx = React.useContext(KeyboardContext);
|
const ctx = React.useContext(KeyboardContext);
|
||||||
const numberOfColumns = React.useRef<number>(
|
const numberOfColumns = React.useRef<number>(
|
||||||
Math.floor(width / (ctx.emojiSize + 16))
|
Math.floor(width / (ctx?.emojiSize ? ctx?.emojiSize : 0 + 16))
|
||||||
);
|
);
|
||||||
const [data, setData] = React.useState<EmojiType[]>([]);
|
const [data, setData] = React.useState<EmojiType[]>([]);
|
||||||
// console.log(width);
|
// console.log(width);
|
||||||
|
@ -31,8 +31,10 @@ export const EmojiCategory = ({ item }: { item: CategoryTypes }) => {
|
||||||
}, [item]);
|
}, [item]);
|
||||||
|
|
||||||
const getItemLayout = (_: EmojiType[] | null | undefined, index: number) => ({
|
const getItemLayout = (_: EmojiType[] | null | undefined, index: number) => ({
|
||||||
length: ctx.emojiSize,
|
length: ctx?.emojiSize ? ctx?.emojiSize : 0,
|
||||||
offset: ctx.emojiSize * Math.ceil(index / ctx.numberOfColumns),
|
offset:
|
||||||
|
(ctx?.emojiSize ? ctx?.emojiSize : 0) *
|
||||||
|
Math.ceil(index / (ctx?.numberOfColumns ? ctx?.numberOfColumns : 1)),
|
||||||
index,
|
index,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,11 @@ export const SingleEmoji = ({ item }: { item: EmojiType }) => {
|
||||||
const ctx = React.useContext(KeyboardContext);
|
const ctx = React.useContext(KeyboardContext);
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => ctx.onEmojiSelected(item)}
|
onPress={() => ctx?.onEmojiSelected(item)}
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<Text style={{ fontSize: ctx.emojiSize }}>{item.emoji}</Text>
|
<Text style={{ fontSize: ctx?.emojiSize }}>{item.emoji}</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { EmojiKeyboard } from './EmojiKeyboard';
|
import { EmojiKeyboard } from './EmojiKeyboard';
|
||||||
import { EmojiPicker } from './EmojiPicker';
|
import { EmojiPicker } from './EmojiPicker';
|
||||||
|
import { KeyboardProvider } from './KeyboardProvider';
|
||||||
|
|
||||||
export { EmojiKeyboard };
|
export { EmojiKeyboard, KeyboardProvider };
|
||||||
|
|
||||||
export default EmojiPicker;
|
export default EmojiPicker;
|
||||||
|
|
37
src/types.ts
37
src/types.ts
|
@ -1,5 +1,3 @@
|
||||||
import type { ViewStyle } from 'react-native';
|
|
||||||
|
|
||||||
export type EmojiType = {
|
export type EmojiType = {
|
||||||
emoji: string;
|
emoji: string;
|
||||||
emoji_version: string;
|
emoji_version: string;
|
||||||
|
@ -38,20 +36,25 @@ type CategoryNavigationItem = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CATEGORIES_NAVIGATION: CategoryNavigationItem[] = [
|
export const CATEGORIES_NAVIGATION: CategoryNavigationItem[] = [
|
||||||
{ icon: '😀', category: 'Smileys & Emotion' },
|
{ icon: 'Smile', category: 'Smileys & Emotion' },
|
||||||
{ icon: '👋', category: 'People & Body' },
|
{ icon: 'Users', category: 'People & Body' },
|
||||||
{ icon: '🐵', category: 'Animals & Nature' },
|
{ icon: 'Trees', category: 'Animals & Nature' },
|
||||||
{ icon: '🍇', category: 'Food & Drink' },
|
{ icon: 'Pizza', category: 'Food & Drink' },
|
||||||
{ icon: '🌍', category: 'Travel & Places' },
|
{ icon: 'Plane', category: 'Travel & Places' },
|
||||||
{ icon: '🎃', category: 'Activities' },
|
{ icon: 'Football', category: 'Activities' },
|
||||||
{ icon: '👓', category: 'Objects' },
|
{ icon: 'Lightbulb', category: 'Objects' },
|
||||||
{ icon: '🏧', category: 'Symbols' },
|
{ icon: 'Ban', category: 'Symbols' },
|
||||||
{ icon: '🏁', category: 'Flags' },
|
{ icon: 'Flag', category: 'Flags' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export type EmojiKeyboardProps = {
|
// export const CATEGORIES_NAVIGATION: CategoryNavigationItem[] = [
|
||||||
onEmojiSelected: (emoji: EmojiType) => void;
|
// { icon: '😀', category: 'Smileys & Emotion' },
|
||||||
containerStyles?: ViewStyle;
|
// { icon: '👋', category: 'People & Body' },
|
||||||
numberOfColumns?: number;
|
// { icon: '🐵', category: 'Animals & Nature' },
|
||||||
emojiSize?: number;
|
// { icon: '🍇', category: 'Food & Drink' },
|
||||||
};
|
// { icon: '🌍', category: 'Travel & Places' },
|
||||||
|
// { icon: '🎃', category: 'Activities' },
|
||||||
|
// { icon: '👓', category: 'Objects' },
|
||||||
|
// { icon: '🏧', category: 'Symbols' },
|
||||||
|
// { icon: '🏁', category: 'Flags' },
|
||||||
|
// ];
|
||||||
|
|
Loading…
Reference in New Issue