Преглед на файлове

Merge branch 'emoji-popovers' into 'develop'

use Popover for Emoji picker + suggestor

See merge request pleroma/pleroma-fe!1648
HJ преди 2 години
родител
ревизия
72a5eaf40a

+ 1 - 0
index.html

@@ -10,5 +10,6 @@
     <noscript>To use Pleroma, please enable JavaScript.</noscript>
     <div id="app"></div>
     <!-- built files will be auto injected -->
+    <div id="popovers" />
   </body>
 </html>

+ 0 - 1
src/App.vue

@@ -73,7 +73,6 @@
     <UpdateNotification />
     <div id="modal" />
     <GlobalNoticeList />
-    <div id="popovers" />
   </div>
 </template>
 

+ 67 - 75
src/components/emoji_input/emoji_input.js

@@ -1,5 +1,6 @@
 import Completion from '../../services/completion/completion.js'
 import EmojiPicker from '../emoji_picker/emoji_picker.vue'
+import Popover from 'src/components/popover/popover.vue'
 import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
 import { take } from 'lodash'
 import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
@@ -109,18 +110,20 @@ const EmojiInput = {
   data () {
     return {
       input: undefined,
+      caretEl: undefined,
       highlighted: 0,
       caret: 0,
       focused: false,
       blurTimeout: null,
-      showPicker: false,
       temporarilyHideSuggestions: false,
-      keepOpen: false,
       disableClickOutside: false,
-      suggestions: []
+      suggestions: [],
+      overlayStyle: {},
+      pickerShown: false
     }
   },
   components: {
+    Popover,
     EmojiPicker,
     UnicodeDomainIndicator
   },
@@ -128,15 +131,21 @@ const EmojiInput = {
     padEmoji () {
       return this.$store.getters.mergedConfig.padEmoji
     },
+    preText () {
+      return this.modelValue.slice(0, this.caret)
+    },
+    postText () {
+      return this.modelValue.slice(this.caret)
+    },
     showSuggestions () {
       return this.focused &&
         this.suggestions &&
         this.suggestions.length > 0 &&
-        !this.showPicker &&
+        !this.pickerShown &&
         !this.temporarilyHideSuggestions
     },
     textAtCaret () {
-      return (this.wordAtCaret || {}).word || ''
+      return this.wordAtCaret?.word
     },
     wordAtCaret () {
       if (this.modelValue && this.caret) {
@@ -188,13 +197,35 @@ const EmojiInput = {
 
         return emoji.displayText
       }
+    },
+    onInputScroll () {
+      this.$refs.hiddenOverlay.scrollTo({
+        top: this.input.scrollTop,
+        left: this.input.scrollLeft
+      })
     }
   },
   mounted () {
-    const { root } = this.$refs
+    const { root, hiddenOverlayCaret, suggestorPopover } = this.$refs
     const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea')
     if (!input) return
     this.input = input
+    this.caretEl = hiddenOverlayCaret
+    if (suggestorPopover.setAnchorEl) {
+      suggestorPopover.setAnchorEl(this.caretEl) // unit test compat
+      this.$refs.picker.setAnchorEl(this.caretEl)
+    } else {
+      console.warn('setAnchorEl not found, are we in a unit test?')
+    }
+    const style = getComputedStyle(this.input)
+    this.overlayStyle.padding = style.padding
+    this.overlayStyle.border = style.border
+    this.overlayStyle.margin = style.margin
+    this.overlayStyle.lineHeight = style.lineHeight
+    this.overlayStyle.fontFamily = style.fontFamily
+    this.overlayStyle.fontSize = style.fontSize
+    this.overlayStyle.wordWrap = style.wordWrap
+    this.overlayStyle.whiteSpace = style.whiteSpace
     this.resize()
     input.addEventListener('blur', this.onBlur)
     input.addEventListener('focus', this.onFocus)
@@ -204,6 +235,7 @@ const EmojiInput = {
     input.addEventListener('click', this.onClickInput)
     input.addEventListener('transitionend', this.onTransition)
     input.addEventListener('input', this.onInput)
+    input.addEventListener('scroll', this.onInputScroll)
   },
   unmounted () {
     const { input } = this
@@ -216,45 +248,43 @@ const EmojiInput = {
       input.removeEventListener('click', this.onClickInput)
       input.removeEventListener('transitionend', this.onTransition)
       input.removeEventListener('input', this.onInput)
+      input.removeEventListener('scroll', this.onInputScroll)
     }
   },
   watch: {
-    showSuggestions: function (newValue) {
+    showSuggestions: function (newValue, oldValue) {
       this.$emit('shown', newValue)
+      if (newValue) {
+        this.$refs.suggestorPopover.showPopover()
+      } else {
+        this.$refs.suggestorPopover.hidePopover()
+      }
     },
     textAtCaret: async function (newWord) {
+      if (newWord === undefined) return
       const firstchar = newWord.charAt(0)
-      this.suggestions = []
-      if (newWord === firstchar) return
+      if (newWord === firstchar) {
+        this.suggestions = []
+        return
+      }
       const matchedSuggestions = await this.suggest(newWord, this.maybeLocalizedEmojiNamesAndKeywords)
       // Async: cancel if textAtCaret has changed during wait
-      if (this.textAtCaret !== newWord) return
-      if (matchedSuggestions.length <= 0) return
+      if (this.textAtCaret !== newWord || matchedSuggestions.length <= 0) {
+        this.suggestions = []
+        return
+      }
       this.suggestions = take(matchedSuggestions, 5)
         .map(({ imageUrl, ...rest }) => ({
           ...rest,
           img: imageUrl || ''
         }))
-    },
-    suggestions: {
-      handler (newValue) {
-        this.$nextTick(this.resize)
-      },
-      deep: true
     }
   },
   methods: {
-    focusPickerInput () {
-      const pickerEl = this.$refs.picker.$el
-      if (!pickerEl) return
-      const pickerInput = pickerEl.querySelector('input')
-      if (pickerInput) pickerInput.focus()
-    },
     triggerShowPicker () {
-      this.showPicker = true
       this.$nextTick(() => {
+        this.$refs.picker.showPicker()
         this.scrollIntoView()
-        this.focusPickerInput()
       })
       // This temporarily disables "click outside" handler
       // since external trigger also means click originates
@@ -266,11 +296,12 @@ const EmojiInput = {
     },
     togglePicker () {
       this.input.focus()
-      this.showPicker = !this.showPicker
-      if (this.showPicker) {
+      if (!this.pickerShown) {
         this.scrollIntoView()
+        this.$refs.picker.showPicker()
         this.$refs.picker.startEmojiLoad()
-        this.$nextTick(this.focusPickerInput)
+      } else {
+        this.$refs.picker.hidePicker()
       }
     },
     replace (replacement) {
@@ -307,7 +338,6 @@ const EmojiInput = {
         spaceAfter,
         after
       ].join('')
-      this.keepOpen = keepOpen
       this.$emit('update:modelValue', newValue)
       const position = this.caret + (insertion + spaceAfter + spaceBefore).length
       if (!keepOpen) {
@@ -407,8 +437,11 @@ const EmojiInput = {
         }
       })
     },
-    onTransition (e) {
-      this.resize()
+    onPickerShown () {
+      this.pickerShown = true
+    },
+    onPickerClosed () {
+      this.pickerShown = false
     },
     onBlur (e) {
       // Clicking on any suggestion removes focus from autocomplete,
@@ -416,7 +449,6 @@ const EmojiInput = {
       this.blurTimeout = setTimeout(() => {
         this.focused = false
         this.setCaret(e)
-        this.resize()
       }, 200)
     },
     onClick (e, suggestion) {
@@ -428,18 +460,13 @@ const EmojiInput = {
         this.blurTimeout = null
       }
 
-      if (!this.keepOpen) {
-        this.showPicker = false
-      }
       this.focused = true
       this.setCaret(e)
-      this.resize()
       this.temporarilyHideSuggestions = false
     },
     onKeyUp (e) {
       const { key } = e
       this.setCaret(e)
-      this.resize()
 
       // Setting hider in keyUp to prevent suggestions from blinking
       // when moving away from suggested spot
@@ -451,7 +478,6 @@ const EmojiInput = {
     },
     onPaste (e) {
       this.setCaret(e)
-      this.resize()
     },
     onKeyDown (e) {
       const { ctrlKey, shiftKey, key } = e
@@ -496,58 +522,24 @@ const EmojiInput = {
           this.input.focus()
         }
       }
-
-      this.showPicker = false
-      this.resize()
     },
     onInput (e) {
-      this.showPicker = false
       this.setCaret(e)
-      this.resize()
       this.$emit('update:modelValue', e.target.value)
     },
-    onClickInput (e) {
-      this.showPicker = false
-    },
-    onClickOutside (e) {
-      if (this.disableClickOutside) return
-      this.showPicker = false
-    },
     onStickerUploaded (e) {
-      this.showPicker = false
       this.$emit('sticker-uploaded', e)
     },
     onStickerUploadFailed (e) {
-      this.showPicker = false
       this.$emit('sticker-upload-Failed', e)
     },
     setCaret ({ target: { selectionStart } }) {
       this.caret = selectionStart
+      this.$nextTick(() => {
+        this.$refs.suggestorPopover.updateStyles()
+      })
     },
     resize () {
-      const panel = this.$refs.panel
-      if (!panel) return
-      const picker = this.$refs.picker.$el
-      const panelBody = this.$refs['panel-body']
-      const { offsetHeight, offsetTop } = this.input
-      const offsetBottom = offsetTop + offsetHeight
-
-      this.setPlacement(panelBody, panel, offsetBottom)
-      this.setPlacement(picker, picker, offsetBottom)
-    },
-    setPlacement (container, target, offsetBottom) {
-      if (!container || !target) return
-
-      target.style.top = offsetBottom + 'px'
-      target.style.bottom = 'auto'
-
-      if (this.placement === 'top' || (this.placement === 'auto' && this.overflowsBottom(container))) {
-        target.style.top = 'auto'
-        target.style.bottom = this.input.offsetHeight + 'px'
-      }
-    },
-    overflowsBottom (el) {
-      return el.getBoundingClientRect().bottom > window.innerHeight
     }
   }
 }

+ 110 - 108
src/components/emoji_input/emoji_input.vue

@@ -1,11 +1,16 @@
 <template>
   <div
     ref="root"
-    v-click-outside="onClickOutside"
     class="emoji-input"
     :class="{ 'with-picker': !hideEmojiButton }"
   >
     <slot />
+    <!-- TODO: make the 'x' disappear if at the end maybe? -->
+    <div class="hidden-overlay" :style="overlayStyle" ref="hiddenOverlay">
+      <span>{{ preText }}</span>
+      <span class="caret" ref="hiddenOverlayCaret">x</span>
+      <span>{{ postText }}</span>
+    </div>
     <template v-if="enableEmojiPicker">
       <button
         v-if="!hideEmojiButton"
@@ -18,59 +23,61 @@
       <EmojiPicker
         v-if="enableEmojiPicker"
         ref="picker"
-        :class="{ hide: !showPicker }"
-        :showing="showPicker"
         :enable-sticker-picker="enableStickerPicker"
         class="emoji-picker-panel"
         @emoji="insert"
         @sticker-uploaded="onStickerUploaded"
         @sticker-upload-failed="onStickerUploadFailed"
+        @show="onPickerShown"
+        @close="onPickerClosed"
       />
     </template>
-    <div
-      ref="panel"
+    <Popover
       class="autocomplete-panel"
-      :class="{ hide: !showSuggestions }"
+      placement="bottom"
+      ref="suggestorPopover"
     >
-      <div
-        ref="panel-body"
-        class="autocomplete-panel-body"
-      >
+      <template #content>
         <div
-          v-for="(suggestion, index) in suggestions"
-          :key="index"
-          class="autocomplete-item"
-          :class="{ highlighted: index === highlighted }"
-          @click.stop.prevent="onClick($event, suggestion)"
+          ref="panel-body"
+          class="autocomplete-panel-body"
         >
-          <span class="image">
-            <img
-              v-if="suggestion.img"
-              :src="suggestion.img"
-            >
-            <span v-else>{{ suggestion.replacement }}</span>
-          </span>
-          <div class="label">
-            <span
-              v-if="suggestion.user"
-              class="displayText"
-            >
-              {{ suggestion.displayText }}<UnicodeDomainIndicator
-                :user="suggestion.user"
-                :at="false"
-              />
+          <div
+            v-for="(suggestion, index) in suggestions"
+            :key="index"
+            class="autocomplete-item"
+            :class="{ highlighted: index === highlighted }"
+            @click.stop.prevent="onClick($event, suggestion)"
+          >
+            <span class="image">
+              <img
+                v-if="suggestion.img"
+                :src="suggestion.img"
+              >
+              <span v-else>{{ suggestion.replacement }}</span>
             </span>
-            <span
-              v-if="!suggestion.user"
-              class="displayText"
-            >
-              {{ maybeLocalizedEmojiName(suggestion) }}
-            </span>
-            <span class="detailText">{{ suggestion.detailText }}</span>
+            <div class="label">
+              <span
+                v-if="suggestion.user"
+                class="displayText"
+              >
+                {{ suggestion.displayText }}<UnicodeDomainIndicator
+                  :user="suggestion.user"
+                  :at="false"
+                />
+              </span>
+              <span
+                v-if="!suggestion.user"
+                class="displayText"
+              >
+                {{ maybeLocalizedEmojiName(suggestion) }}
+              </span>
+              <span class="detailText">{{ suggestion.detailText }}</span>
+            </div>
           </div>
         </div>
-      </div>
-    </div>
+      </template>
+    </Popover>
   </div>
 </template>
 
@@ -102,6 +109,7 @@
       color: var(--text, $fallback--text);
     }
   }
+
   .emoji-picker-panel {
     position: absolute;
     z-index: 20;
@@ -112,89 +120,83 @@
     }
   }
 
-  .autocomplete {
-    &-panel {
-      position: absolute;
-      z-index: 20;
-      margin-top: 2px;
-
-      &.hide {
-        display: none
-      }
+  input, textarea {
+    flex: 1 0 auto;
+  }
 
-      &-body {
-        margin: 0 0.5em 0 0.5em;
-        border-radius: $fallback--tooltipRadius;
-        border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
-        box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
-        box-shadow: var(--popupShadow);
-        min-width: 75%;
-        background-color: $fallback--bg;
-        background-color: var(--popover, $fallback--bg);
-        color: $fallback--link;
-        color: var(--popoverText, $fallback--link);
-        --faint: var(--popoverFaintText, $fallback--faint);
-        --faintLink: var(--popoverFaintLink, $fallback--faint);
-        --lightText: var(--popoverLightText, $fallback--lightText);
-        --postLink: var(--popoverPostLink, $fallback--link);
-        --postFaintLink: var(--popoverPostFaintLink, $fallback--link);
-        --icon: var(--popoverIcon, $fallback--icon);
-      }
+  .hidden-overlay {
+    opacity: 0;
+    pointer-events: none;
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    right: 0;
+    left: 0;
+    overflow: hidden;
+    /* DEBUG STUFF */
+    color: red;
+    /* set opacity to non-zero to see the overlay */
+
+    .caret {
+      width: 0;
+      margin-right: calc(-1ch - 1px);
+      border: 1px solid red;
     }
+  }
+}
+.autocomplete {
+  &-panel {
+    position: absolute;
+  }
 
-    &-item {
-      display: flex;
-      cursor: pointer;
-      padding: 0.2em 0.4em;
-      border-bottom: 1px solid rgba(0, 0, 0, 0.4);
+  &-item {
+    display: flex;
+    cursor: pointer;
+    padding: 0.2em 0.4em;
+    border-bottom: 1px solid rgba(0, 0, 0, 0.4);
+    height: 32px;
+
+    .image {
+      width: 32px;
       height: 32px;
+      line-height: 32px;
+      text-align: center;
+      font-size: 32px;
+
+      margin-right: 4px;
 
-      .image {
+      img {
         width: 32px;
         height: 32px;
-        line-height: 32px;
-        text-align: center;
-        font-size: 32px;
-
-        margin-right: 4px;
-
-        img {
-          width: 32px;
-          height: 32px;
-          object-fit: contain;
-        }
+        object-fit: contain;
       }
+    }
 
-      .label {
-        display: flex;
-        flex-direction: column;
-        justify-content: center;
-        margin: 0 0.1em 0 0.2em;
-
-        .displayText {
-          line-height: 1.5;
-        }
+    .label {
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      margin: 0 0.1em 0 0.2em;
 
-        .detailText {
-          font-size: 9px;
-          line-height: 9px;
-        }
+      .displayText {
+        line-height: 1.5;
       }
 
-      &.highlighted {
-        background-color: $fallback--fg;
-        background-color: var(--selectedMenuPopover, $fallback--fg);
-        color: var(--selectedMenuPopoverText, $fallback--text);
-        --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
-        --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
-        --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
-        --icon: var(--selectedMenuPopoverIcon, $fallback--icon);
+      .detailText {
+        font-size: 9px;
+        line-height: 9px;
       }
     }
-  }
 
-  input, textarea {
-    flex: 1 0 auto;
+    &.highlighted {
+      background-color: $fallback--fg;
+      background-color: var(--selectedMenuPopover, $fallback--fg);
+      color: var(--selectedMenuPopoverText, $fallback--text);
+      --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
+      --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
+      --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
+      --icon: var(--selectedMenuPopoverIcon, $fallback--icon);
+    }
   }
 }
 </style>

+ 25 - 15
src/components/emoji_picker/emoji_picker.js

@@ -1,5 +1,6 @@
 import { defineAsyncComponent } from 'vue'
 import Checkbox from '../checkbox/checkbox.vue'
+import Popover from 'src/components/popover/popover.vue'
 import StillImage from '../still-image/still-image.vue'
 import { ensureFinalFallback } from '../../i18n/languages.js'
 import lozad from 'lozad'
@@ -87,10 +88,6 @@ const EmojiPicker = {
       required: false,
       type: Boolean,
       default: false
-    },
-    showing: {
-      required: true,
-      type: Boolean
     }
   },
   data () {
@@ -111,15 +108,32 @@ const EmojiPicker = {
   components: {
     StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
     Checkbox,
-    StillImage
+    StillImage,
+    Popover
   },
   methods: {
+    showPicker () {
+      this.$refs.popover.showPopover()
+      this.onShowing()
+    },
+    hidePicker () {
+      this.$refs.popover.hidePopover()
+    },
+    setAnchorEl (el) {
+      this.$refs.popover.setAnchorEl(el)
+    },
     setGroupRef (name) {
       return el => { this.groupRefs[name] = el }
     },
     setEmojiRef (name) {
       return el => { this.emojiRefs[name] = el }
     },
+    onPopoverShown () {
+      this.$emit('show')
+    },
+    onPopoverClosed () {
+      this.$emit('close')
+    },
     onStickerUploaded (e) {
       this.$emit('sticker-uploaded', e)
     },
@@ -128,6 +142,9 @@ const EmojiPicker = {
     },
     onEmoji (emoji) {
       const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
+      if (!this.keepOpen) {
+        this.$refs.popover.hidePopover()
+      }
       this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
     },
     onScroll (e) {
@@ -223,6 +240,9 @@ const EmojiPicker = {
     },
     onShowing () {
       const oldContentLoaded = this.contentLoaded
+      this.$nextTick(() => {
+        this.$refs.search.focus()
+      })
       this.contentLoaded = true
       this.waitForDomAndInitializeLazyLoad()
       this.filteredEmojiGroups = this.getFilteredEmojiGroups()
@@ -251,16 +271,6 @@ const EmojiPicker = {
     allCustomGroups () {
       this.waitForDomAndInitializeLazyLoad()
       this.filteredEmojiGroups = this.getFilteredEmojiGroups()
-    },
-    showing (val) {
-      if (val) {
-        this.onShowing()
-      }
-    }
-  },
-  mounted () {
-    if (this.showing) {
-      this.onShowing()
     }
   },
   destroyed () {

+ 2 - 6
src/components/emoji_picker/emoji_picker.scss

@@ -6,14 +6,10 @@ $emoji-picker-header-picture-height: 32px;
 $emoji-picker-emoji-size: 32px;
 
 .emoji-picker {
+  width: 25em;
+  max-width: 100vw;
   display: flex;
   flex-direction: column;
-  position: absolute;
-  right: 0;
-  left: 0;
-  margin: 0 !important;
-  // TODO: actually use popover in emoji picker
-  z-index: var(--ZI_popovers);
   background-color: $fallback--bg;
   background-color: var(--popover, $fallback--bg);
   color: $fallback--link;

+ 109 - 102
src/components/emoji_picker/emoji_picker.vue

@@ -1,129 +1,136 @@
 <template>
-  <div
-    class="emoji-picker panel panel-default panel-body"
+  <Popover
+    trigger="click"
+    popover-class="emoji-picker popover-default"
+    ref="popover"
+    @show="onPopoverShown"
+    @close="onPopoverClosed"
   >
-    <div class="heading">
-      <span
-        ref="header"
-        class="emoji-tabs"
-      >
+    <template #content>
+      <div class="heading">
         <span
-          v-for="group in filteredEmojiGroups"
-          :ref="setGroupRef('group-header-' + group.id)"
-          :key="group.id"
-          class="emoji-tabs-item"
-          :class="{
-            active: activeGroupView === group.id
-          }"
-          :title="group.text"
-          @click.prevent="highlight(group.id)"
+          ref="header"
+          class="emoji-tabs"
         >
           <span
-            v-if="group.image"
-            class="emoji-picker-header-image"
+            v-for="group in filteredEmojiGroups"
+            :ref="setGroupRef('group-header-' + group.id)"
+            :key="group.id"
+            class="emoji-tabs-item"
+            :class="{
+              active: activeGroupView === group.id
+            }"
+            :title="group.text"
+            @click.prevent="highlight(group.id)"
           >
-            <still-image
-              :alt="group.text"
-              :src="group.image"
+            <span
+              v-if="group.image"
+              class="emoji-picker-header-image"
+            >
+              <still-image
+                :alt="group.text"
+                :src="group.image"
+              />
+            </span>
+            <FAIcon
+              v-else
+              :icon="group.icon"
+              fixed-width
             />
           </span>
-          <FAIcon
-            v-else
-            :icon="group.icon"
-            fixed-width
-          />
         </span>
-      </span>
-      <span
-        v-if="stickerPickerEnabled"
-        class="additional-tabs"
-      >
         <span
-          class="stickers-tab-icon additional-tabs-item"
-          :class="{active: showingStickers}"
-          :title="$t('emoji.stickers')"
-          @click.prevent="toggleStickers"
+          v-if="stickerPickerEnabled"
+          class="additional-tabs"
         >
-          <FAIcon
-            icon="sticky-note"
-            fixed-width
-          />
+          <span
+            class="stickers-tab-icon additional-tabs-item"
+            :class="{active: showingStickers}"
+            :title="$t('emoji.stickers')"
+            @click.prevent="toggleStickers"
+          >
+            <FAIcon
+              icon="sticky-note"
+              fixed-width
+            />
+          </span>
         </span>
-      </span>
-    </div>
-    <div
-      v-if="contentLoaded"
-      class="content"
-    >
+      </div>
       <div
-        class="emoji-content"
-        :class="{hidden: showingStickers}"
+        v-if="contentLoaded"
+        class="content"
       >
-        <div class="emoji-search">
-          <input
-            v-model="keyword"
-            type="text"
-            class="form-control"
-            :placeholder="$t('emoji.search_emoji')"
-            @input="$event.target.composing = false"
-          >
-        </div>
         <div
-          ref="emoji-groups"
-          class="emoji-groups"
-          :class="groupsScrolledClass"
-          @scroll="onScroll"
+          class="emoji-content"
+          :class="{hidden: showingStickers}"
         >
+          <div class="emoji-search">
+            <input
+              v-model="keyword"
+              type="text"
+              class="form-control"
+              :placeholder="$t('emoji.search_emoji')"
+              @input="$event.target.composing = false"
+              ref="search"
+            >
+          </div>
           <div
-            v-for="group in filteredEmojiGroups"
-            :key="group.id"
-            class="emoji-group"
+            ref="emoji-groups"
+            class="emoji-groups"
+            :class="groupsScrolledClass"
+            @scroll="onScroll"
           >
-            <h6
-              :ref="setGroupRef('group-' + group.id)"
-              class="emoji-group-title"
-            >
-              {{ group.text }}
-            </h6>
-            <span
-              v-for="emoji in group.emojis"
-              :key="group.id + emoji.displayText"
-              :title="maybeLocalizedEmojiName(emoji)"
-              class="emoji-item"
-              @click.stop.prevent="onEmoji(emoji)"
+            <div
+              v-for="group in filteredEmojiGroups"
+              :key="group.id"
+              class="emoji-group"
             >
+              <h6
+                :ref="setGroupRef('group-' + group.id)"
+                class="emoji-group-title"
+              >
+                {{ group.text }}
+              </h6>
               <span
-                v-if="!emoji.imageUrl"
-                class="emoji-picker-emoji -unicode"
-              >{{ emoji.replacement }}</span>
-              <still-image
-                v-else
-                :ref="setEmojiRef(group.id + emoji.displayText)"
-                class="emoji-picker-emoji -custom"
-                :data-src="emoji.imageUrl"
-                :data-emoji-name="group.id + emoji.displayText"
-              />
-            </span>
-            <span :ref="setGroupRef('group-end-' + group.id)" />
+                v-for="emoji in group.emojis"
+                :key="group.id + emoji.displayText"
+                :title="maybeLocalizedEmojiName(emoji)"
+                class="emoji-item"
+                @click.stop.prevent="onEmoji(emoji)"
+              >
+                <span
+                  v-if="!emoji.imageUrl"
+                  class="emoji-picker-emoji -unicode"
+                >{{ emoji.replacement }}</span>
+                <still-image
+                  v-else
+                  :ref="setEmojiRef(group.id + emoji.displayText)"
+                  class="emoji-picker-emoji -custom"
+                  :data-src="emoji.imageUrl"
+                  :data-emoji-name="group.id + emoji.displayText"
+                />
+              </span>
+              <span :ref="setGroupRef('group-end-' + group.id)" />
+            </div>
+          </div>
+          <div class="keep-open">
+            <Checkbox v-model="keepOpen">
+              {{ $t('emoji.keep_open') }}
+            </Checkbox>
           </div>
         </div>
-        <div class="keep-open">
-          <Checkbox v-model="keepOpen">
-            {{ $t('emoji.keep_open') }}
-          </Checkbox>
+        <div
+          v-if="showingStickers"
+          class="stickers-content"
+        >
+          <sticker-picker
+            @uploaded="onStickerUploaded"
+            @upload-failed="onStickerUploadFailed"
+          />
         </div>
       </div>
-      <div
-        v-if="showingStickers"
-        class="stickers-content"
-      >
-        <sticker-picker
-          @uploaded="onStickerUploaded"
-          @upload-failed="onStickerUploadFailed"
-        />
-      </div>
-    </div>
-  </div>
+    </template>
+  </Popover>
 </template>
 
 <script src="./emoji_picker.js"></script>

+ 16 - 1
src/components/popover/popover.js

@@ -56,6 +56,10 @@ const Popover = {
       // lockReEntry is a flag that is set when mouse cursor is leaving the popover's content
       // so that if mouse goes back into popover it won't be re-shown again to prevent annoyance
       // with popovers refusing to be hidden when user wants to interact with something in below popover
+      anchorEl: null,
+      // There's an issue where having teleport enabled by default causes things just...
+      // not render at all, i.e. main post status form and its emoji inputs
+      teleport: false,
       lockReEntry: false,
       hidden: true,
       styles: {},
@@ -64,10 +68,15 @@ const Popover = {
       // used to avoid blinking if hovered onto popover
       graceTimeout: null,
       parentPopover: null,
+      disableClickOutside: false,
       childrenShown: new Set()
     }
   },
   methods: {
+    setAnchorEl (el) {
+      this.anchorEl = el
+      this.updateStyles()
+    },
     containerBoundingClientRect () {
       const container = this.boundToSelector ? this.$el.closest(this.boundToSelector) : this.$el.offsetParent
       return container.getBoundingClientRect()
@@ -80,7 +89,7 @@ const Popover = {
 
       // Popover will be anchored around this element, trigger ref is the container, so
       // its children are what are inside the slot. Expect only one v-slot:trigger.
-      const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
+      const anchorEl = this.anchorEl || (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
       // SVGs don't have offsetWidth/Height, use fallback
       const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
       const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
@@ -231,6 +240,10 @@ const Popover = {
     },
     showPopover () {
       if (this.disabled) return
+      this.disableClickOutside = true
+      setTimeout(() => {
+        this.disableClickOutside = false
+      }, 0)
       const wasHidden = this.hidden
       this.hidden = false
       this.parentPopover && this.parentPopover.onChildPopoverState(this, true)
@@ -291,6 +304,7 @@ const Popover = {
       }
     },
     onClickOutside (e) {
+      if (this.disableClickOutside) return
       if (this.hidden) return
       if (this.$refs.content && this.$refs.content.contains(e.target)) return
       if (this.$el.contains(e.target)) return
@@ -324,6 +338,7 @@ const Popover = {
     }
   },
   mounted () {
+    this.teleport = true
     let scrollable = this.$refs.trigger.closest('.column.-scrollable') ||
         this.$refs.trigger.closest('.mobile-notifications')
     if (!scrollable) scrollable = window

+ 1 - 1
src/components/popover/popover.vue

@@ -12,7 +12,7 @@
     >
       <slot name="trigger" />
     </button>
-    <teleport to="#popovers">
+    <teleport :disabled="!teleport" to="#popovers">
       <transition name="fade">
         <div
           v-if="!hidden"

+ 0 - 3
src/components/post_status_form/post_status_form.js

@@ -501,7 +501,6 @@ const PostStatusForm = {
       if (target.value === '') {
         target.style.height = null
         this.$emit('resize')
-        this.$refs['emoji-input'].resize()
         return
       }
 
@@ -588,8 +587,6 @@ const PostStatusForm = {
       } else {
         scrollerRef.scrollTop = targetScroll
       }
-
-      this.$refs['emoji-input'].resize()
     },
     showEmojiPicker () {
       this.$refs.textarea.focus()