ycs77 / headlessui-float

Easily use Headless UI with Floating UI to position floating elements.
https://headlessui-float.vercel.app
MIT License
344 stars 11 forks source link

Using a custom Portal component #38

Closed ragulka closed 1 year ago

ragulka commented 1 year ago

Use Version Use version when question appear:

Describe the question I'm trying to render a HeadlessUI Combobox inside a Popover. For this, I need to teleport the combobox options to the body, so that the dropdown options can overflow the popover, and the popover won't show scrollbars.

However, there's an issue - when clicking any of the combobox options, the popover is immediately closed. I believe it is because of the click outside handler, which now sees the options as being rendered outside of the popover. I saw a mention in HeadlessUI repo that using the Portal from @headlessui/vue instead of the regular Teleport component should help with this. Unfortunately, I don't see that it's possible to customize the portal component for Float. Would you consider supporting something like that? Or perhaps there's another solution to the issue?

Screenshots If applicable, add screenshots to help explain your problem.

ycs77 commented 1 year ago

Hi @ragulka, can you provide a minimal reproducible question example (like github repo, codesandbox, stackblitz...) for me to debug 😅? I will refer to your suggestion and try to find a solution.

ycs77 commented 1 year ago

Hi @ragulka, I think this can be broken down into two parts.

First, regarding the change of Teleport to use the <Portal> component of Headless UI, I have already modified it and released it to v0.11, thank you for your reminder.

Second, if you want to use Popover and Combobox, you don’t need to use the portal, here is an example for you to test:

<template>
  <Popover v-slot="{ close }">
    <Float
      placement="bottom-start"
      :offset="15"
      :shift="6"
      :flip="10"
      arrow
      enter="transition duration-200 ease-out"
      enter-from="opacity-0 -translate-y-1"
      enter-to="opacity-100 translate-y-0"
      leave="transition duration-150 ease-in"
      leave-from="opacity-100 translate-y-0"
      leave-to="opacity-0 -translate-y-1"
    >
      <PopoverButton class="px-3 py-1.5 flex justify-center items-center bg-rose-50 hover:bg-rose-100 text-rose-500 rounded">
        Open
      </PopoverButton>

      <PopoverPanel class="w-[240px] bg-white border border-gray-200 rounded-md shadow-lg focus:outline-none">
        <FloatArrow class="absolute bg-white w-5 h-5 rotate-45 border border-gray-200" />

        <div class="relative p-3 bg-white rounded-md">
          <label class="inline-block mb-1">Combobox</label>
          <Combobox v-model="selected">
            <Float
              as="div"
              class="relative"
              placement="bottom-start"
              :offset="4"
              leave="transition ease-in duration-100"
              leave-from="opacity-100"
              leave-to="opacity-0"
              floating-as="template"
              @hide="query = ''"
            >
              <div class="relative w-full text-left bg-white border border-gray-200 rounded-lg shadow-md cursor-default focus:outline-none sm:text-sm overflow-hidden">
                <ComboboxInput
                  class="w-full border-none py-2 pl-3 pr-10 text-sm leading-5 text-gray-900 focus:outline-none focus:ring-0"
                  :display-value="person => person?.name"
                  @change="query = $event.target.value"
                />

                <ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
                  <HeroiconsChevronUpDown20Solid class="w-5 h-5 text-gray-400" aria-hidden="true" />
                </ComboboxButton>
              </div>

              <ComboboxOptions class="absolute w-full py-1 overflow-auto text-base bg-white rounded-md shadow-lg max-h-60 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
                <div
                  v-if="filteredPeople.length === 0 && query !== ''"
                  class="relative py-2 px-4 text-gray-700 cursor-default select-none"
                >
                  Nothing found.
                </div>

                <ComboboxOption
                  v-for="person in filteredPeople"
                  v-slot="{ selected, active }"
                  :key="person.id"
                  :value="person"
                  as="template"
                >
                  <li
                    class="relative py-2 pl-10 pr-4 cursor-default select-none"
                    :class="active ? 'text-white bg-rose-600' : 'text-gray-900'"
                  >
                    <span class="block truncate" :class="selected ? 'font-medium' : 'font-normal'">
                      {{ person.name }}
                    </span>
                    <span
                      v-if="selected"
                      class="absolute inset-y-0 left-0 flex items-center pl-3"
                      :class="active ? 'text-white' : 'text-rose-600'"
                    >
                      <HeroiconsCheck20Solid class="w-5 h-5" aria-hidden="true" />
                    </span>
                  </li>
                </ComboboxOption>
              </ComboboxOptions>
            </Float>
          </Combobox>

          <div class="mt-4 flex justify-end">
            <button type="button" class="px-4 py-1 bg-rose-500 text-white text-sm rounded" @click="close">
              Submit
            </button>
          </div>
        </div>
      </PopoverPanel>
    </Float>
  </Popover>
</template>

<script setup>
import { computed, ref } from 'vue'
import { Combobox, ComboboxButton, ComboboxInput, ComboboxOption, ComboboxOptions, Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
import { Float, FloatArrow } from '@headlessui-float/vue'
import HeroiconsCheck20Solid from '~icons/heroicons/check-20-solid'
import HeroiconsChevronUpDown20Solid from '~icons/heroicons/chevron-up-down-20-solid'

const people = [
  { id: 1, name: 'Wade Cooper' },
  { id: 2, name: 'Arlene Mccoy' },
  { id: 3, name: 'Devon Webb' },
  { id: 4, name: 'Tom Cook' },
  { id: 5, name: 'Tanya Fox' },
  { id: 6, name: 'Hellen Schmidt' },
]
const selected = ref(people[0])
const query = ref('')

const filteredPeople = computed(() =>
  query.value === ''
    ? people
    : people.filter(person =>
      person.name
        .toLowerCase()
        .replace(/\s+/g, '')
        .includes(query.value.toLowerCase().replace(/\s+/g, ''))
    )
)
</script>

If you have other questions, open a new issue and attach your sample code or minimally reproducible GitHub project. Online demo is provided here to test <Float> at any time.