From c1dc0b7ee0f15134578d50a3d344ab4bdad1119f Mon Sep 17 00:00:00 2001
From: Hailey <153161762+haileyok@users.noreply.github.com>
Date: Tue, 2 Jan 2024 12:16:28 -0800
Subject: emoji picker improvements (#2392)

* rework emoji picker

* dynamic position

* always prefer the left if it will fit

* add accessibility label

* Update EmojiPicker.web.tsx

oops. remove accessibility from fake button
---
 .../composer/text-input/web/EmojiPicker.web.tsx    | 140 ++++++++++++---------
 1 file changed, 83 insertions(+), 57 deletions(-)

(limited to 'src/view/com/composer/text-input/web/EmojiPicker.web.tsx')

diff --git a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx
index f4b2d99b0..6d16403ff 100644
--- a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx
+++ b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx
@@ -1,11 +1,17 @@
 import React from 'react'
 import Picker from '@emoji-mart/react'
-import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
-import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
+import {
+  StyleSheet,
+  TouchableWithoutFeedback,
+  useWindowDimensions,
+  View,
+} from 'react-native'
 import {textInputWebEmitter} from '../TextInput.web'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useMediaQuery} from 'react-responsive'
+
+const HEIGHT_OFFSET = 40
+const WIDTH_OFFSET = 100
+const PICKER_HEIGHT = 435 + HEIGHT_OFFSET
+const PICKER_WIDTH = 350 + WIDTH_OFFSET
 
 export type Emoji = {
   aliases?: string[]
@@ -18,59 +24,87 @@ export type Emoji = {
   unified: string
 }
 
-export function EmojiPickerButton() {
-  const pal = usePalette('default')
-  const [open, setOpen] = React.useState(false)
-  const onOpenChange = (o: boolean) => {
-    setOpen(o)
-  }
-  const close = () => {
-    setOpen(false)
-  }
+export interface EmojiPickerState {
+  isOpen: boolean
+  pos: {top: number; left: number; right: number; bottom: number}
+}
 
-  return (
-    <DropdownMenu.Root open={open} onOpenChange={onOpenChange}>
-      <DropdownMenu.Trigger style={styles.trigger}>
-        <FontAwesomeIcon
-          icon={['far', 'face-smile']}
-          color={pal.colors.link}
-          size={22}
-        />
-      </DropdownMenu.Trigger>
-
-      <DropdownMenu.Portal>
-        <EmojiPicker close={close} />
-      </DropdownMenu.Portal>
-    </DropdownMenu.Root>
-  )
+interface IProps {
+  state: EmojiPickerState
+  close: () => void
 }
 
-export function EmojiPicker({close}: {close: () => void}) {
+export function EmojiPicker({state, close}: IProps) {
+  const {height, width} = useWindowDimensions()
+
+  const isShiftDown = React.useRef(false)
+
+  const position = React.useMemo(() => {
+    const fitsBelow = state.pos.top + PICKER_HEIGHT < height
+    const fitsAbove = PICKER_HEIGHT < state.pos.top
+    const placeOnLeft = PICKER_WIDTH < state.pos.left
+    const screenYMiddle = height / 2 - PICKER_HEIGHT / 2
+
+    if (fitsBelow) {
+      return {
+        top: state.pos.top + HEIGHT_OFFSET,
+      }
+    } else if (fitsAbove) {
+      return {
+        bottom: height - state.pos.bottom + HEIGHT_OFFSET,
+      }
+    } else {
+      return {
+        top: screenYMiddle,
+        left: placeOnLeft ? state.pos.left - PICKER_WIDTH : undefined,
+        right: !placeOnLeft
+          ? width - state.pos.right - PICKER_WIDTH
+          : undefined,
+      }
+    }
+  }, [state.pos, height, width])
+
+  React.useEffect(() => {
+    if (!state.isOpen) return
+
+    const onKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Shift') {
+        isShiftDown.current = true
+      }
+    }
+    const onKeyUp = (e: KeyboardEvent) => {
+      if (e.key === 'Shift') {
+        isShiftDown.current = false
+      }
+    }
+    window.addEventListener('keydown', onKeyDown, true)
+    window.addEventListener('keyup', onKeyUp, true)
+
+    return () => {
+      window.removeEventListener('keydown', onKeyDown, true)
+      window.removeEventListener('keyup', onKeyUp, true)
+    }
+  }, [state.isOpen])
+
   const onInsert = (emoji: Emoji) => {
     textInputWebEmitter.emit('emoji-inserted', emoji)
-    close()
+
+    if (!isShiftDown.current) {
+      close()
+    }
   }
-  const reducedPadding = useMediaQuery({query: '(max-height: 750px)'})
-  const noPadding = useMediaQuery({query: '(max-height: 550px)'})
-  const noPicker = useMediaQuery({query: '(max-height: 350px)'})
+
+  if (!state.isOpen) return null
 
   return (
-    // eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors
-    <TouchableWithoutFeedback onPress={close} accessibilityViewIsModal>
+    <TouchableWithoutFeedback
+      accessibilityRole="button"
+      onPress={close}
+      accessibilityViewIsModal>
       <View style={styles.mask}>
         {/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors */}
-        <TouchableWithoutFeedback
-          onPress={e => {
-            e.stopPropagation() // prevent event from bubbling up to the mask
-          }}>
-          <View
-            style={[
-              styles.picker,
-              {
-                paddingTop: noPadding ? 0 : reducedPadding ? 150 : 325,
-                display: noPicker ? 'none' : 'flex',
-              },
-            ]}>
+        <TouchableWithoutFeedback onPress={e => e.stopPropagation()}>
+          <View style={[{position: 'absolute'}, position]}>
             <Picker
               data={async () => {
                 return (await import('./EmojiPickerData.json')).default
@@ -93,15 +127,7 @@ const styles = StyleSheet.create({
     right: 0,
     width: '100%',
     height: '100%',
-  },
-  trigger: {
-    backgroundColor: 'transparent',
-    // @ts-ignore web only -prf
-    border: 'none',
-    paddingTop: 4,
-    paddingLeft: 12,
-    paddingRight: 12,
-    cursor: 'pointer',
+    alignItems: 'center',
   },
   picker: {
     marginHorizontal: 'auto',
-- 
cgit 1.4.1

