From dbb7782498f4adc680e0d4941089ab8db03ee111 Mon Sep 17 00:00:00 2001 From: Mateusz Rostkowski Date: Fri, 24 Jan 2025 13:34:26 +0100 Subject: [PATCH 1/6] chore: add rn-emoji-keyboard dependency to package.json and yarn.lock --- package.json | 1 + yarn.lock | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/package.json b/package.json index 46e95563..42475570 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,7 @@ "react-native-svg": "15.8.0", "react-native-web": "~0.19.6", "reactotron-react-native": "^5.1.7", + "rn-emoji-keyboard": "^1.7.0", "sass": "^1.77.2", "setimmediate": "^1.0.5", "use-debounce": "^9.0.4" diff --git a/yarn.lock b/yarn.lock index 480be601..d7640e99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1344,6 +1344,7 @@ __metadata: react-test-renderer: "npm:^18.3.1" reactotron-react-native: "npm:^5.1.7" readline: "npm:^1.3.0" + rn-emoji-keyboard: "npm:^1.7.0" sass: "npm:^1.77.2" setimmediate: "npm:^1.0.5" text-encoding-polyfill: "npm:^0.6.7" @@ -13996,6 +13997,16 @@ __metadata: languageName: node linkType: hard +"rn-emoji-keyboard@npm:^1.7.0": + version: 1.7.0 + resolution: "rn-emoji-keyboard@npm:1.7.0" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/88f4bb02f848ca3400df57ecc48c87fabe79704f3591c396b33f1969f22fd7addd96aa8f853f31294e33220ea61d3ecee406fa3bd7ef8754778ed2fc4e2260b1 + languageName: node + linkType: hard + "rtl-detect@npm:^1.0.2": version: 1.1.2 resolution: "rtl-detect@npm:1.1.2" From a084be873701f78e948ab619e9a74dcccdc69b32 Mon Sep 17 00:00:00 2001 From: Mateusz Rostkowski Date: Fri, 24 Jan 2025 16:38:00 +0100 Subject: [PATCH 2/6] chore: add emoji picker component --- src/components/molecules/Field/Field.tsx | 3 + .../molecules/Field/FieldEmojiPicker.tsx | 78 +++++++++++++++++ src/components/molecules/Field/types.ts | 10 +++ .../ControlledField/ControlledEmojiPicker.tsx | 44 ++++++++++ .../ControlledField/ControlledField.tsx | 3 + .../organisms/ControlledField/types.ts | 11 +++ src/design-system/components/EmojiPicker.tsx | 84 +++++++++++++++++++ src/design-system/components/index.ts | 1 + src/design-system/components/types.ts | 25 ++++++ 9 files changed, 259 insertions(+) create mode 100644 src/components/molecules/Field/FieldEmojiPicker.tsx create mode 100644 src/components/organisms/ControlledField/ControlledEmojiPicker.tsx create mode 100644 src/design-system/components/EmojiPicker.tsx diff --git a/src/components/molecules/Field/Field.tsx b/src/components/molecules/Field/Field.tsx index 580ef547..23c637e8 100644 --- a/src/components/molecules/Field/Field.tsx +++ b/src/components/molecules/Field/Field.tsx @@ -2,6 +2,7 @@ import React, { PropsWithChildren } from 'react' import { FieldCheckbox } from './FieldCheckbox' import { FieldCheckboxGroup } from './FieldCheckboxGroup' +import { FieldEmojiPicker } from './FieldEmojiPicker' import { FieldInput } from './FieldInput' import { FieldRadioGroup } from './FieldRadioGroup' import { FieldSelect } from './FieldSelect' @@ -12,6 +13,7 @@ type FieldComposition = React.FC & { Checkbox: typeof FieldCheckbox RadioGroup: typeof FieldRadioGroup Select: typeof FieldSelect + EmojiPicker: typeof FieldEmojiPicker } const Field: FieldComposition = ({ children }) => { @@ -23,6 +25,7 @@ Field.CheckboxGroup = FieldCheckboxGroup Field.Checkbox = FieldCheckbox Field.RadioGroup = FieldRadioGroup Field.Select = FieldSelect +Field.EmojiPicker = FieldEmojiPicker export { Field } export * from './types' diff --git a/src/components/molecules/Field/FieldEmojiPicker.tsx b/src/components/molecules/Field/FieldEmojiPicker.tsx new file mode 100644 index 00000000..d2a48561 --- /dev/null +++ b/src/components/molecules/Field/FieldEmojiPicker.tsx @@ -0,0 +1,78 @@ +import { forwardRef, useCallback, useImperativeHandle, useRef, useMemo } from 'react' +import { NativeSyntheticEvent, TextInputFocusEventData } from 'react-native' + +import type { FieldEmojiPickerProps } from './types' + +import { + FormErrorMessage, + FormLabel, + Box, + EmojiPicker, + EmojiPickerRef, +} from '@/design-system/components' +import { getLayoutProps } from '@/design-system/utils/getLayoutProps' + +export const FieldEmojiPicker = forwardRef( + ( + { + errorMessage, + isInvalid, + isRequired, + label, + labelStyle, + onBlur, + onFocus, + testID, + onChangeEmoji, + ...props + }, + ref + ) => { + const _emojiPickerRef = useRef(null) + + const { layoutProps, restProps: emojiPickerProps } = useMemo( + () => getLayoutProps(props), + [props] + ) + + const handleFocus = useCallback(() => { + _emojiPickerRef?.current?.focus() + }, [onFocus]) + + const handleBlur = useCallback( + (e?: NativeSyntheticEvent) => { + onBlur && e && onBlur(e) + _emojiPickerRef.current?.blur() + }, + [onBlur] + ) + + useImperativeHandle( + ref, + () => ({ + focus: handleFocus, + blur: handleBlur, + ..._emojiPickerRef.current, + }), + [handleBlur, handleFocus] + ) + + return ( + + + + + + ) + } +) diff --git a/src/components/molecules/Field/types.ts b/src/components/molecules/Field/types.ts index acde71ce..27eab121 100644 --- a/src/components/molecules/Field/types.ts +++ b/src/components/molecules/Field/types.ts @@ -7,6 +7,7 @@ import { SelectProps, StyledProps, TouchableRef, + EmojiPickerProps, } from '@/design-system' // ----------------------- @@ -92,3 +93,12 @@ export type FieldCheckboxProps = FormLabelProps & CheckboxProps & { errorMessage?: string } + +// ----------------------- +// ----- EMOJI PICKER ---- +// ----------------------- + +export type FieldEmojiPickerProps = FormLabelProps & + EmojiPickerProps & { + errorMessage?: string + } diff --git a/src/components/organisms/ControlledField/ControlledEmojiPicker.tsx b/src/components/organisms/ControlledField/ControlledEmojiPicker.tsx new file mode 100644 index 00000000..27d17b6d --- /dev/null +++ b/src/components/organisms/ControlledField/ControlledEmojiPicker.tsx @@ -0,0 +1,44 @@ +import { useCallback } from 'react' +import { Controller, ControllerProps, FieldValues, get } from 'react-hook-form' + +import type { ControlledEmojiPickerProps } from './types' +import { Field } from '../../molecules' + +export const ControlledEmojiPicker = ({ + control, + name, + errors, + rules, + children, + ...props +}: ControlledEmojiPickerProps) => { + const errorMessage = get(errors, name)?.message + + const renderEmojiPicker = useCallback( + ({ + field: { onChange, name, ref, value, ...fieldProps }, + }: Parameters[0]) => { + return ( + + ) + }, + [errorMessage, props] + ) + + return ( + + ) +} diff --git a/src/components/organisms/ControlledField/ControlledField.tsx b/src/components/organisms/ControlledField/ControlledField.tsx index 64a21940..6ce99f53 100644 --- a/src/components/organisms/ControlledField/ControlledField.tsx +++ b/src/components/organisms/ControlledField/ControlledField.tsx @@ -2,6 +2,7 @@ import React, { PropsWithChildren } from 'react' import { ControlledCheckbox } from './ControlledCheckbox' import { ControlledCheckboxGroup } from './ControlledCheckboxGroup' +import { ControlledEmojiPicker } from './ControlledEmojiPicker' import { ControlledInput } from './ControlledInput' import { ControlledRadioGroup } from './ControlledRadioGroup' import { ControlledSelect } from './ControlledSelect' @@ -12,6 +13,7 @@ type ControlledFieldComposition = React.FC & { RadioGroup: typeof ControlledRadioGroup Checkbox: typeof ControlledCheckbox Select: typeof ControlledSelect + EmojiPicker: typeof ControlledEmojiPicker } const ControlledField: ControlledFieldComposition = ({ children }) => { @@ -23,6 +25,7 @@ ControlledField.CheckboxGroup = ControlledCheckboxGroup ControlledField.Checkbox = ControlledCheckbox ControlledField.RadioGroup = ControlledRadioGroup ControlledField.Select = ControlledSelect +ControlledField.EmojiPicker = ControlledEmojiPicker export { ControlledField } export * from './types' diff --git a/src/components/organisms/ControlledField/types.ts b/src/components/organisms/ControlledField/types.ts index 1050d955..664d5881 100644 --- a/src/components/organisms/ControlledField/types.ts +++ b/src/components/organisms/ControlledField/types.ts @@ -6,6 +6,7 @@ import { FieldRadioGroupProps, FieldSelectProps, FieldCheckboxProps, + FieldEmojiPickerProps, } from '@/components/molecules' // ----------------------- @@ -68,3 +69,13 @@ export type ControlledCheckboxProps & ControlledFieldProps + +// ----------------------- +// ----- EMOJI PICKER ---- +// ----------------------- + +export type ControlledEmojiPickerProps = Omit< + FieldEmojiPickerProps, + 'ref' | 'onChangeEmoji' | 'emoji' +> & + ControlledFieldProps diff --git a/src/design-system/components/EmojiPicker.tsx b/src/design-system/components/EmojiPicker.tsx new file mode 100644 index 00000000..9d4e00f8 --- /dev/null +++ b/src/design-system/components/EmojiPicker.tsx @@ -0,0 +1,84 @@ +import { forwardRef, useImperativeHandle } from 'react' +import EmojiKeyboard from 'rn-emoji-keyboard' + +import { BoxWithShadow } from './BoxWithShadow' +import { Row } from './Row' +import { Text } from './Text' +import { Touchable } from './Touchables' +import { EmojiPickerProps, EmojiPickerRef } from './types' + +import { useBoolean, useTheme } from '@/hooks' +import { hex2rgba } from '@/utils' + +export const EmojiPicker = forwardRef( + ({ emoji, onChangeEmoji, placeholder, isDisabled, isInvalid, ...props }, ref) => { + const [isOpen, setIsOpen] = useBoolean(false) + const { colors } = useTheme() + + useImperativeHandle( + ref, + () => ({ + focus: setIsOpen.on, + blur: setIsOpen.off, + }), + [setIsOpen.on, setIsOpen.off] + ) + + return ( + <> + + + + + {emoji?.emoji || placeholder} + + + + + { + onChangeEmoji(selectedEmoji) + setIsOpen.off() + }} + theme={{ + backdrop: hex2rgba(colors.bg.primary, 0.2), + header: colors.text.primary, + container: colors.bg.primary, + knob: colors.border.primary, + category: { + icon: colors.text.secondary, + iconActive: colors.text.primary, + container: colors.bg.quaternary, + containerActive: colors.bg.active, + }, + search: { + text: colors.text.primary, + placeholder: colors.text.placeholder, + icon: colors.text.primary, + background: colors.bg.primary, + }, + }} + enableSearchBar + /> + + ) + } +) diff --git a/src/design-system/components/index.ts b/src/design-system/components/index.ts index 245103eb..02520d63 100644 --- a/src/design-system/components/index.ts +++ b/src/design-system/components/index.ts @@ -5,6 +5,7 @@ export * from './BoxWithShadow' export * from './Button' export * from './Center' export * from './Column' +export * from './EmojiPicker' export * from './GradientBox' export * from './Row' export * from './Spacer' diff --git a/src/design-system/components/types.ts b/src/design-system/components/types.ts index 4f0d0897..827892bd 100644 --- a/src/design-system/components/types.ts +++ b/src/design-system/components/types.ts @@ -269,3 +269,28 @@ export type CheckboxProps = TouchableProps & { size?: 'sm' | 'md' pb?: SizingValue } + +// ----------------------- +// ----- EMOJI PICKER ---- +// ----------------------- + +export type EmojiPickerRef = { focus: () => void; blur: () => void } + +export type EmojiPickerProps = TouchableProps & { + // Logic + onChangeEmoji: (newValue: EmojiType) => void + emoji?: EmojiType + + // UI + placeholder?: string + isDisabled?: boolean + isInvalid?: boolean + rightElement?: JSX.Element + leftElement?: JSX.Element +} + +type EmojiType = { + emoji: string + name: string + slug: string +} From 5d292a9c0cbe5955962bdfbd20514e20a0bf3e5c Mon Sep 17 00:00:00 2001 From: Mateusz Rostkowski Date: Fri, 24 Jan 2025 16:48:18 +0100 Subject: [PATCH 3/6] chore: add implementation example of emoji picker --- src/hooks/forms/useTestForm.ts | 2 ++ src/i18n/translations/en.json | 2 ++ src/i18n/translations/pl.json | 2 ++ src/screens/TestFormScreen.tsx | 8 ++++++++ src/types/testForm.ts | 1 + 5 files changed, 15 insertions(+) diff --git a/src/hooks/forms/useTestForm.ts b/src/hooks/forms/useTestForm.ts index a3f16ed1..609fd964 100644 --- a/src/hooks/forms/useTestForm.ts +++ b/src/hooks/forms/useTestForm.ts @@ -18,6 +18,7 @@ const defaultValues: TestFormValues = { age: '', education: '', interests: [], + icon: '', } export const useTestForm = () => { @@ -52,6 +53,7 @@ export const useTestForm = () => { music: { required: t('test_form.errors.music') }, interests: { required: t('test_form.errors.interests') }, sex: { required: t('test_form.errors.sex') }, + icon: { required: t('test_form.errors.icon') }, } const { diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index 00e55679..f322b0be 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -295,6 +295,7 @@ "education": "Education is required", "email": "Email is required", "interests": "At least 1 interest is needed", + "icon": "Icon is required", "music": "At least 1 music is needed", "name": "Name is required", "phone_format": "Phone number must be in form 000-000-000", @@ -308,6 +309,7 @@ "female": "Female", "games": "Games", "interests": "Interests", + "icon": "Icon", "male": "Male", "middle": "Middle", "name_placeholder": "Name", diff --git a/src/i18n/translations/pl.json b/src/i18n/translations/pl.json index 6cb1495a..0ebd0c57 100644 --- a/src/i18n/translations/pl.json +++ b/src/i18n/translations/pl.json @@ -294,6 +294,7 @@ "education": "Wykształcenie jest wymagane", "email": "Email jest wymagany", "interests": "Zaznacz przynajmniej 1 zainteresowanie", + "icon": "Ikona jest wymagana", "music": "Zaznacz przynajmniej 1 rodzaj muzyki", "name": "Imie jest wymagane", "phone_format": "Numer telefonu musi być w formacie 000-000-000", @@ -307,6 +308,7 @@ "female": "Kobieta", "games": "Gry", "interests": "Zainteresowania", + "icon": "Ikona", "male": "Mezczyzna", "middle": "Gimnazjalne", "name_placeholder": "Imie", diff --git a/src/screens/TestFormScreen.tsx b/src/screens/TestFormScreen.tsx index 69d61094..13eaa7a4 100644 --- a/src/screens/TestFormScreen.tsx +++ b/src/screens/TestFormScreen.tsx @@ -184,6 +184,14 @@ export const TestFormScreen = (): JSX.Element => { name="music" rules={VALIDATION.music} /> + Date: Fri, 24 Jan 2025 21:27:28 +0100 Subject: [PATCH 4/6] chore: add FIXME comment for emoji picker styling issues on web and larger screens --- src/design-system/components/EmojiPicker.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/design-system/components/EmojiPicker.tsx b/src/design-system/components/EmojiPicker.tsx index 9d4e00f8..95a53a9c 100644 --- a/src/design-system/components/EmojiPicker.tsx +++ b/src/design-system/components/EmojiPicker.tsx @@ -51,6 +51,7 @@ export const EmojiPicker = forwardRef( + {/* FIXME: Emoji picker is not looking nice on web and bigger screens, we need to do something with that */} Date: Wed, 29 Jan 2025 11:45:55 +0100 Subject: [PATCH 5/6] feat: add clear button to emoji picker --- src/design-system/components/EmojiPicker.tsx | 38 +++++++++++++++++--- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/design-system/components/EmojiPicker.tsx b/src/design-system/components/EmojiPicker.tsx index 95a53a9c..a0b79cc0 100644 --- a/src/design-system/components/EmojiPicker.tsx +++ b/src/design-system/components/EmojiPicker.tsx @@ -1,7 +1,9 @@ -import { forwardRef, useImperativeHandle } from 'react' +import { forwardRef, useCallback, useImperativeHandle } from 'react' +import { StyleSheet } from 'react-native' import EmojiKeyboard from 'rn-emoji-keyboard' import { BoxWithShadow } from './BoxWithShadow' +import { Icon } from './Icon' import { Row } from './Row' import { Text } from './Text' import { Touchable } from './Touchables' @@ -15,6 +17,14 @@ export const EmojiPicker = forwardRef( const [isOpen, setIsOpen] = useBoolean(false) const { colors } = useTheme() + const clearEmoji = useCallback(() => { + onChangeEmoji({ + emoji: '', + name: '', + slug: '', + }) + }, [onChangeEmoji]) + useImperativeHandle( ref, () => ({ @@ -31,7 +41,6 @@ export const EmojiPicker = forwardRef( onPress={setIsOpen.on} width="100%" minW={48} - pr={2} disabled={isDisabled} alignItems="center" borderColor={ @@ -44,10 +53,25 @@ export const EmojiPicker = forwardRef( overflow="hidden" {...props} > - - + + {emoji?.emoji || placeholder} + {emoji?.emoji ? ( + + + + ) : null} @@ -83,3 +107,9 @@ export const EmojiPicker = forwardRef( ) } ) + +const styles = StyleSheet.create({ + input: { + fontFamily: 'system', + }, +}) From 84abc158aa8735eb19be77c6bfc1ed5592172a3f Mon Sep 17 00:00:00 2001 From: Mateusz Rostkowski Date: Wed, 29 Jan 2025 11:55:16 +0100 Subject: [PATCH 6/6] fix: update emoji display logic in EmojiPicker component --- src/design-system/components/EmojiPicker.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/design-system/components/EmojiPicker.tsx b/src/design-system/components/EmojiPicker.tsx index a0b79cc0..51a5dfdb 100644 --- a/src/design-system/components/EmojiPicker.tsx +++ b/src/design-system/components/EmojiPicker.tsx @@ -62,8 +62,8 @@ export const EmojiPicker = forwardRef( width={'100%'} > {emoji?.emoji || placeholder}