Feature/static keyboard (#2)

* fix: use container styles fix

* refactor: rename static modal

* refactor: let use EmojiStaticKeyboard outside modal

* chore: add static keyboard to readme

* fix: improve readme

* refactor: easier use of static keyboard and refactor contexts

* refactor: update readme example
This commit is contained in:
Jakub Grzywacz 2021-07-26 15:30:04 +02:00 committed by GitHub
parent bdb8799fe1
commit cbe4243248
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 210 additions and 124 deletions

View File

@ -61,6 +61,16 @@ export default function App() {
| onCategoryChangeFailed | function | warn(info) | no | Callback on category change failed (info: {index, highestMeasuredFrameIndex, averageItemLength}) | | onCategoryChangeFailed | function | warn(info) | no | Callback on category change failed (info: {index, highestMeasuredFrameIndex, averageItemLength}) |
| translation | CategoryTranslation | en | no | Translation object *see translation section* | | translation | CategoryTranslation | en | no | Translation object *see translation section* |
| disabledCategory | CategoryTypes[] | [] | no | Hide categories by passing their slugs | | disabledCategory | CategoryTypes[] | [] | no | Hide categories by passing their slugs |
## 🖼 Usage as static
```js
import { EmojiKeyboard } from 'rn-emoji-keyboard';
// ...
<EmojiKeyboard onEmojiSelected={handlePick} />
```
Example about serving as static keyboard [you can find here](/example/src/Dark/Dark.tsx).
## 🇺🇸 Internationalization ## 🇺🇸 Internationalization
### Pre-defined ### Pre-defined
Due to the limited translation possibilities, we only provide a few pre-defined translations into the following languages: Due to the limited translation possibilities, we only provide a few pre-defined translations into the following languages:
@ -101,11 +111,13 @@ You can clone the repo and run `yarn example ios` or `yarn example android` to p
![Preview](/example/assets/translated-preview.jpg) ![Preview](/example/assets/translated-preview.jpg)
### [DisabledCategories](/example/src/DisabledCategories/DisabledCategories.tsx) ### [DisabledCategories](/example/src/DisabledCategories/DisabledCategories.tsx)
![Preview](/example/assets/categories-preview.jpg) ![Preview](/example/assets/categories-preview.jpg)
### [Static (without knob)](/example/src/Static/Static.tsx) ### [StaticModal (without knob)](/example/src/StaticModal/StaticModal.tsx)
![Preview](/example/assets/static-modal-preview.jpg)
### [Static](/example/src/Static/Static.tsx)
![Preview](/example/assets/static-preview.jpg) ![Preview](/example/assets/static-preview.jpg)
## 📈 Future plans ## 📈 Future plans
* Skin tone palette selector. * Skin tone palette selector.
* Search bar. * Search bar.
* Hide forbidden emojis. * Write native module to display forbidden emojis on android.
## ⚖️ License ## ⚖️ License
**[MIT](/LICENSE)** **[MIT](/LICENSE)**

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -7,6 +7,7 @@ import Basic from './Basic/Basic';
import Dark from './Dark/Dark'; import Dark from './Dark/Dark';
import Translated from './Translated/Translated'; import Translated from './Translated/Translated';
import DisabledCategories from './DisabledCategories/DisabledCategories'; import DisabledCategories from './DisabledCategories/DisabledCategories';
import StaticModal from './StaticModal/StaticModal';
import Static from './Static/Static'; import Static from './Static/Static';
const Stack = createStackNavigator(); const Stack = createStackNavigator();
@ -22,6 +23,7 @@ export default () => {
name="DisabledCategories" name="DisabledCategories"
component={DisabledCategories} component={DisabledCategories}
/> />
<Stack.Screen name="StaticModal" component={StaticModal} />
<Stack.Screen name="Static" component={Static} /> <Stack.Screen name="Static" component={Static} />
</Stack.Navigator> </Stack.Navigator>
</NavigationContainer> </NavigationContainer>

View File

@ -14,7 +14,7 @@ const Basic = () => {
setIsModalOpen((prev) => !prev); setIsModalOpen((prev) => !prev);
}; };
return ( return (
<SafeAreaView> <SafeAreaView style={styles.container}>
<Text style={styles.text}>Result: {result}</Text> <Text style={styles.text}>Result: {result}</Text>
<TouchableOpacity onPress={() => setIsModalOpen(true)}> <TouchableOpacity onPress={() => setIsModalOpen(true)}>
<Text style={styles.text}>Open</Text> <Text style={styles.text}>Open</Text>

View File

@ -14,7 +14,7 @@ const DisabledCategories = () => {
setIsModalOpen((prev) => !prev); setIsModalOpen((prev) => !prev);
}; };
return ( return (
<SafeAreaView> <SafeAreaView style={styles.container}>
<Text style={styles.text}>Result: {result}</Text> <Text style={styles.text}>Result: {result}</Text>
<TouchableOpacity onPress={() => setIsModalOpen(true)}> <TouchableOpacity onPress={() => setIsModalOpen(true)}>
<Text style={styles.text}>Open</Text> <Text style={styles.text}>Open</Text>

View File

@ -9,6 +9,7 @@ type RootStackParamList = {
Dark: undefined; Dark: undefined;
Translated: undefined; Translated: undefined;
DisabledCategories: undefined; DisabledCategories: undefined;
StaticModal: undefined;
Static: undefined; Static: undefined;
}; };
@ -28,6 +29,10 @@ const Examples = ({ navigation }: Props) => {
title="DisabledCategories" title="DisabledCategories"
onPress={() => navigation.navigate('DisabledCategories')} onPress={() => navigation.navigate('DisabledCategories')}
/> />
<Button
title="StaticModal"
onPress={() => navigation.navigate('StaticModal')}
/>
<Button title="Static" onPress={() => navigation.navigate('Static')} /> <Button title="Static" onPress={() => navigation.navigate('Static')} />
</View> </View>
</SafeAreaView> </SafeAreaView>

View File

@ -1,32 +1,26 @@
import * as React from 'react'; import * as React from 'react';
import { StyleSheet, Text, TouchableOpacity } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import EmojiPicker from 'rn-emoji-keyboard'; import { EmojiKeyboard } from 'rn-emoji-keyboard';
import type { EmojiType } from 'src/types'; import type { EmojiType } from 'src/types';
const Static = () => { const Static = () => {
const [result, setResult] = React.useState<string>(); const [result, setResult] = React.useState<string>();
const [isModalOpen, setIsModalOpen] = React.useState<boolean>(false);
const handlePick = (emoji: EmojiType) => { const handlePick = (emoji: EmojiType) => {
console.log(emoji);
setResult(emoji.emoji); setResult(emoji.emoji);
setIsModalOpen((prev) => !prev);
}; };
return ( return (
<SafeAreaView> <SafeAreaView style={styles.container}>
<Text style={styles.text}>Result: {result}</Text> <View style={styles.container}>
<TouchableOpacity onPress={() => setIsModalOpen(true)}> <Text style={styles.text}>Result: {result}</Text>
<Text style={styles.text}>Open</Text> </View>
</TouchableOpacity> <View style={styles.container}>
<EmojiKeyboard
<EmojiPicker onEmojiSelected={handlePick}
onEmojiSelected={handlePick} containerStyles={styles.keyboardContainer}
open={isModalOpen} />
onClose={() => setIsModalOpen(false)} </View>
expandable={false}
defaultHeight="65%"
/>
</SafeAreaView> </SafeAreaView>
); );
}; };
@ -40,6 +34,9 @@ const styles = StyleSheet.create({
margin: 64, margin: 64,
fontSize: 18, fontSize: 18,
}, },
keyboardContainer: {
borderRadius: 0,
},
}); });
export default Static; export default Static;

View File

@ -0,0 +1,45 @@
import * as React from 'react';
import { StyleSheet, Text, TouchableOpacity } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import EmojiPicker from 'rn-emoji-keyboard';
import type { EmojiType } from 'src/types';
const StaticModal = () => {
const [result, setResult] = React.useState<string>();
const [isModalOpen, setIsModalOpen] = React.useState<boolean>(false);
const handlePick = (emoji: EmojiType) => {
console.log(emoji);
setResult(emoji.emoji);
setIsModalOpen((prev) => !prev);
};
return (
<SafeAreaView style={styles.container}>
<Text style={styles.text}>Result: {result}</Text>
<TouchableOpacity onPress={() => setIsModalOpen(true)}>
<Text style={styles.text}>Open</Text>
</TouchableOpacity>
<EmojiPicker
onEmojiSelected={handlePick}
open={isModalOpen}
onClose={() => setIsModalOpen(false)}
expandable={false}
defaultHeight="65%"
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
text: {
textAlign: 'center',
margin: 64,
fontSize: 18,
},
});
export default StaticModal;

View File

@ -14,7 +14,7 @@ const Translated = () => {
setIsModalOpen((prev) => !prev); setIsModalOpen((prev) => !prev);
}; };
return ( return (
<SafeAreaView> <SafeAreaView style={styles.container}>
<Text style={styles.text}>Emotka: {result}</Text> <Text style={styles.text}>Emotka: {result}</Text>
<TouchableOpacity onPress={() => setIsModalOpen(true)}> <TouchableOpacity onPress={() => setIsModalOpen(true)}>
<Text style={styles.text}>Otwórz</Text> <Text style={styles.text}>Otwórz</Text>

View File

@ -1,90 +1,19 @@
import * as React from 'react'; import * as React from 'react';
import { EmojiStaticKeyboard } from './components/EmojiStaticKeyboard';
import { KeyboardProvider } from './contexts/KeyboardProvider';
import type {
KeyboardProps,
OnEmojiSelected,
} from './contexts/KeyboardContext';
import { type EmojiKeyboardProps = {
StyleSheet, onEmojiSelected: OnEmojiSelected;
View, } & Partial<KeyboardProps>;
FlatList,
useWindowDimensions,
Animated,
} from 'react-native';
import type { CategoryTypes, EmojisByCategory } from './types';
import { EmojiCategory } from './components/EmojiCategory';
import { KeyboardContext } from './KeyboardContext';
import { Categories } from './components/Categories';
import emojisByGroup from './assets/emojis.json';
export const EmojiKeyboard = () => {
const { width } = useWindowDimensions();
const {
activeCategoryIndex,
containerStyles,
onCategoryChangeFailed,
disabledCategory,
} = React.useContext(KeyboardContext);
const flatListRef = React.useRef<FlatList>(null);
const scrollNav = React.useRef(new Animated.Value(0)).current;
const getItemLayout = (
_: CategoryTypes[] | null | undefined,
index: number
) => ({
length: width,
offset: width * index,
index,
});
const renderItem = React.useCallback(
(props) => <EmojiCategory {...props} />,
[]
);
React.useEffect(() => {
Animated.spring(scrollNav, {
toValue: activeCategoryIndex * (28 + 9),
useNativeDriver: true,
}).start();
}, [activeCategoryIndex, scrollNav]);
export const EmojiKeyboard = (props: EmojiKeyboardProps) => {
return ( return (
<View style={[styles.container, styles.containerShadow, containerStyles]}> <KeyboardProvider {...props}>
<Animated.FlatList <EmojiStaticKeyboard />
data={emojisByGroup.filter((category) => { </KeyboardProvider>
const title = category.title as CategoryTypes;
return !disabledCategory.includes(title);
})}
keyExtractor={(item: EmojisByCategory) => item.title}
renderItem={renderItem}
removeClippedSubviews={true}
ref={flatListRef}
onScrollToIndexFailed={onCategoryChangeFailed}
horizontal
showsHorizontalScrollIndicator={false}
pagingEnabled
scrollEventThrottle={16}
getItemLayout={getItemLayout}
scrollEnabled={false}
initialNumToRender={1}
windowSize={2}
maxToRenderPerBatch={1}
/>
<Categories flatListRef={flatListRef} scrollNav={scrollNav} />
</View>
); );
}; };
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 16,
borderRadius: 16,
backgroundColor: '#fff',
},
containerShadow: {
shadowColor: 'black',
shadowOpacity: 0.15,
shadowOffset: { width: 0, height: 0 },
shadowRadius: 5,
elevation: 10,
},
});

View File

@ -1,9 +1,12 @@
import * as React from 'react'; import * as React from 'react';
import { Animated, useWindowDimensions } from 'react-native'; import { Animated, useWindowDimensions } from 'react-native';
import { EmojiKeyboard } from './EmojiKeyboard'; import { EmojiStaticKeyboard } from './components/EmojiStaticKeyboard';
import { Knob } from './components/Knob'; import { Knob } from './components/Knob';
import { defaultKeyboardContext, KeyboardProvider } from './KeyboardProvider'; import {
import type { KeyboardProps } from './KeyboardContext'; defaultKeyboardContext,
KeyboardProvider,
} from './contexts/KeyboardProvider';
import type { KeyboardProps } from './contexts/KeyboardContext';
import type { EmojiType } from './types'; import type { EmojiType } from './types';
import { ModalWithBackdrop } from './components/ModalWithBackdrop'; import { ModalWithBackdrop } from './components/ModalWithBackdrop';
import { getHeight } from './utils'; import { getHeight } from './utils';
@ -61,7 +64,7 @@ export const EmojiPicker = ({
}, },
]} ]}
> >
<EmojiKeyboard /> <EmojiStaticKeyboard />
</Animated.View> </Animated.View>
</> </>
</ModalWithBackdrop> </ModalWithBackdrop>

View File

@ -1,6 +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 { KeyboardContext } from '../contexts/KeyboardContext';
import { import {
CATEGORIES, CATEGORIES,
CATEGORIES_NAVIGATION, CATEGORIES_NAVIGATION,

View File

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { View, StyleSheet, TouchableOpacity } from 'react-native'; import { View, StyleSheet, TouchableOpacity } from 'react-native';
import { KeyboardContext } from '../KeyboardContext'; import { KeyboardContext } from '../contexts/KeyboardContext';
import type { CategoryNavigationItem, CategoryTypes } from '../types'; import type { CategoryNavigationItem, CategoryTypes } from '../types';
import { Icon } from './Icon'; import { Icon } from './Icon';

View File

@ -3,7 +3,7 @@ import * as React from 'react';
import { StyleSheet, View, Text, FlatList } from 'react-native'; import { StyleSheet, View, Text, FlatList } from 'react-native';
import type { EmojisByCategory, EmojiType, JsonEmoji } from '../types'; import type { EmojisByCategory, EmojiType, JsonEmoji } from '../types';
import { SingleEmoji } from './SingleEmoji'; import { SingleEmoji } from './SingleEmoji';
import { KeyboardContext } from '../KeyboardContext'; import { KeyboardContext } from '../contexts/KeyboardContext';
const emptyEmoji = { const emptyEmoji = {
emoji: '', emoji: '',

View File

@ -0,0 +1,90 @@
import * as React from 'react';
import {
StyleSheet,
View,
FlatList,
useWindowDimensions,
Animated,
} from 'react-native';
import type { CategoryTypes, EmojisByCategory } from '../types';
import { EmojiCategory } from './EmojiCategory';
import { KeyboardContext } from '../contexts/KeyboardContext';
import { Categories } from './Categories';
import emojisByGroup from '../assets/emojis.json';
export const EmojiStaticKeyboard = () => {
const { width } = useWindowDimensions();
const {
activeCategoryIndex,
containerStyles,
onCategoryChangeFailed,
disabledCategory,
} = React.useContext(KeyboardContext);
const flatListRef = React.useRef<FlatList>(null);
const scrollNav = React.useRef(new Animated.Value(0)).current;
const getItemLayout = (
_: CategoryTypes[] | null | undefined,
index: number
) => ({
length: width,
offset: width * index,
index,
});
const renderItem = React.useCallback(
(props) => <EmojiCategory {...props} />,
[]
);
React.useEffect(() => {
Animated.spring(scrollNav, {
toValue: activeCategoryIndex * (28 + 9),
useNativeDriver: true,
}).start();
}, [activeCategoryIndex, scrollNav]);
return (
<View style={[styles.container, styles.containerShadow, containerStyles]}>
<Animated.FlatList
data={emojisByGroup.filter((category) => {
const title = category.title as CategoryTypes;
return !disabledCategory.includes(title);
})}
keyExtractor={(item: EmojisByCategory) => item.title}
renderItem={renderItem}
removeClippedSubviews={true}
ref={flatListRef}
onScrollToIndexFailed={onCategoryChangeFailed}
horizontal
showsHorizontalScrollIndicator={false}
pagingEnabled
scrollEventThrottle={16}
getItemLayout={getItemLayout}
scrollEnabled={false}
initialNumToRender={1}
windowSize={2}
maxToRenderPerBatch={1}
/>
<Categories flatListRef={flatListRef} scrollNav={scrollNav} />
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 16,
borderRadius: 16,
backgroundColor: '#fff',
},
containerShadow: {
shadowColor: 'black',
shadowOpacity: 0.15,
shadowOffset: { width: 0, height: 0 },
shadowRadius: 5,
elevation: 10,
},
});

View File

@ -7,7 +7,7 @@ import {
PanResponder, PanResponder,
} from 'react-native'; } from 'react-native';
import { getHeight } from '../utils'; import { getHeight } from '../utils';
import { KeyboardContext } from '../KeyboardContext'; import { KeyboardContext } from '../contexts/KeyboardContext';
type KnobProps = { type KnobProps = {
offsetY: Animated.Value; offsetY: Animated.Value;

View File

@ -8,7 +8,7 @@ import {
TouchableOpacity, TouchableOpacity,
View, View,
} from 'react-native'; } from 'react-native';
import { KeyboardContext } from '../KeyboardContext'; import { KeyboardContext } from '../contexts/KeyboardContext';
type ModalWithBackdropProps = { type ModalWithBackdropProps = {
isOpen: boolean; isOpen: boolean;

View File

@ -4,12 +4,14 @@ import {
defaultKeyboardContext, defaultKeyboardContext,
defaultKeyboardValues, defaultKeyboardValues,
} from './KeyboardProvider'; } from './KeyboardProvider';
import type { CategoryTranslation, EmojiType, CategoryTypes } from './types'; import type { CategoryTranslation, EmojiType, CategoryTypes } from '../types';
export type OnEmojiSelected = (emoji: EmojiType) => void;
export type KeyboardProps = { export type KeyboardProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onEmojiSelected: (emoji: EmojiType) => void; onEmojiSelected: OnEmojiSelected;
emojiSize?: number; emojiSize?: number;
containerStyles?: ViewStyle; containerStyles?: ViewStyle;
knobStyles?: ViewStyle; knobStyles?: ViewStyle;

View File

@ -4,12 +4,14 @@ import {
KeyboardProps, KeyboardProps,
ContextValues, ContextValues,
KeyboardContext, KeyboardContext,
OnEmojiSelected,
} from './KeyboardContext'; } from './KeyboardContext';
import en from './translation/en'; import en from '../translation/en';
import type { EmojiType } from './types'; import type { EmojiType } from '../types';
type ProviderProps = KeyboardProps & { type ProviderProps = Partial<KeyboardProps> & {
children: React.ReactNode; children: React.ReactNode;
onEmojiSelected: OnEmojiSelected;
}; };
export const defaultKeyboardContext: Required<KeyboardProps> = { export const defaultKeyboardContext: Required<KeyboardProps> = {

View File

@ -1,10 +1,9 @@
import { EmojiKeyboard } from './EmojiKeyboard';
import { EmojiPicker } from './EmojiPicker'; import { EmojiPicker } from './EmojiPicker';
import { KeyboardProvider } from './KeyboardProvider'; import { EmojiKeyboard } from './EmojiKeyboard';
import en from './translation/en'; import en from './translation/en';
import pl from './translation/pl'; import pl from './translation/pl';
export { EmojiKeyboard, KeyboardProvider }; export { EmojiKeyboard };
export { en, pl }; export { en, pl };
export default EmojiPicker; export default EmojiPicker;