ycs77 / headlessui-float

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

Empty dropdown on combobox #46

Closed nielseulink closed 1 year ago

nielseulink commented 1 year ago

Use Version Use version when question appear:

Describe the question I tried to add this package to my project, it all seems to work fine besides the combobox. It only shows the shadows:

image

I got it fixed by modifieing the HTML in the browser (by adding display: contents; to one of the div's):

image image

Any idea what i'm doing wrong and how to fix it ?

Kind regards, Niels

ycs77 commented 1 year ago

Hi @nielseulink, I think you can change floating-as to template to fix it:

<Float floating-as="template">
nielseulink commented 1 year ago

Hi @nielseulink, I think you can change floating-as to template to fix it:

<Float floating-as="template">

Hi @ycs77, thank you for your response. When adding that the dropdown is visible but stretched out and out of sight. Any idea ?

image

ycs77 commented 1 year ago

Can you privide the code of the filter dropdown for me?

ycs77 commented 1 year ago

I think you can try to add the as="div" and class="relative" to <Float>, to keep the <ComboboxOptions>'s with is same as root <Float>.

<Combobox v-model="selected">
  <Float
    as="div"
    class="relative"
    floating-as="template"
    ...
  >
    <div class="relative w-full ...">
      <ComboboxInput ... />
      <ComboboxButton ... />
    </div>

    <ComboboxOptions class="absolute w-full ...">
      ...
    </ComboboxOptions>
  </Float>
</Combobox>

Edit 2023/02/27: change to vue example

ycs77 commented 1 year ago

This is an example of me trying to reproduce your dropdown:

<template>
  <Popover>
    <Float
      placement="bottom-start"
      :offset="8"
      portal
      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="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none">
        Filters
      </PopoverButton>

      <PopoverPanel v-slot="{ close }" class="bg-white p-4 border border-gray-200 rounded-md shadow-lg focus:outline-none">
        <label class="text-sm text-gray-700 font-bold inline-block mb-2">Brand</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 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: any) => 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-indigo-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-indigo-600'"
                  >
                    <HeroiconsCheck20Solid class="w-5 h-5" aria-hidden="true" />
                  </span>
                </li>
              </ComboboxOption>
            </ComboboxOptions>
          </Float>
        </Combobox>

        <div class="flex justify-end mt-4">
          <button
            type="button"
            class="px-2.5 py-2 bg-indigo-100 text-indigo-700 text-xs font-bold rounded"
            @click="close"
          >
            Apply filters
          </button>
        </div>
      </PopoverPanel>
    </Float>
  </Popover>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'
import { Combobox, ComboboxButton, ComboboxInput, ComboboxOption, ComboboxOptions, Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
import { Float } 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>

Edit 2023/02/27: change to vue example

nielseulink commented 1 year ago

Yeah that seems to work, thank you for your time and help. Final code:


<template>
    <Combobox as="div" v-model="selectedOption" @update="console.log('test')" @update:modelValue="value => $emit('update:modelValue', value)">
        <ComboboxLabel v-if="label" :for="label" v-text="label" class="block text-sm font-medium text-gray-700 dark:text-gray-300" />
        <div class="relative mt-1">
            <Float
                as="div"
                floating-as="template"
                className="relative"
                placement="bottom-end"
                enter="transition duration-200 ease-out"
                enter-from="scale-95 opacity-0"
                enter-to="scale-100 opacity-100"
                leave="transition duration-150 ease-in"
                leave-from="scale-100 opacity-100"
                leave-to="scale-95 opacity-0">
                <div>
                    <ComboboxInput class="w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm" @change="query = $event.target.value" @focus="$event.target.value = ''" @focusout="query = ''" :display-value="(selected) => (selected.id) ? selected.name : (props.options.filter(option => option.id == selected)[0].name)" />
                        <ComboboxButton class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none">
                            <ChevronDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
                        </ComboboxButton>
                </div>
                <ComboboxOptions v-if="filteredOptions.length > 0"
                    class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
                    <ComboboxOption v-for="option in filteredOptions" :key="option.id" :value="option.id" as="template" v-slot="{ active, selected }">
                        <li :class="['relative cursor-default select-none py-2 pl-3 pr-9', active ? 'bg-indigo-600 text-white' : 'text-gray-900']">
                            <span :class="['block truncate', selected && 'font-semibold']">
                                {{ option.name }}
                            </span>
                            <span v-if="selected"
                                :class="['absolute inset-y-0 right-0 flex items-center pr-4', active ? 'text-white' : 'text-indigo-600']">
                                <CheckIcon class="h-5 w-5" aria-hidden="true" />
                            </span>
                        </li>
                    </ComboboxOption>
                </ComboboxOptions>
            </Float>
        </div>
        <p v-if="error" v-text="error" class="mt-2 text-sm text-red-600 dark:text-red-400" />
    </Combobox>
</template>

<script setup>
    import { computed, ref, watch } from 'vue'
    import { CheckIcon, ChevronDownIcon } from 'vue-tabler-icons'
    import { Combobox, ComboboxButton, ComboboxInput, ComboboxLabel, ComboboxOption, ComboboxOptions } from '@headlessui/vue'
    import { Float } from '@headlessui-float/vue'

    let props = defineProps({
        label: {
            type: [String, Boolean],
            default: false,
        },
        error: {
            type: [String, Boolean],
            default: false,
        },
        modelValue: {
            type: Number,
            default: null,
        },
        options: {
            type: Object,
            default: null,
        }
    });

    const query = ref('')
    const selectedOption = ref(props.options.filter(option => option.id == props.modelValue)[0])

    const filteredOptions = computed(() =>
        query.value === ''
            ? props.options
            : props.options.filter((option) => {
                return option.name.toLowerCase().includes(query.value.toLowerCase())
            })
    )
</script>
ycs77 commented 1 year ago

Sorry for the React example, I've changed it for Vue. For reference if necessary 😊.

cxammar commented 1 year ago

Hi, sorry to reopen this. I've been playing with as="div" and other options you mention on the issue but I can't seem to make the combobox options as wide as the combobox input.

image

This is my implementation


    <Combobox as="div" :disabled="disabled" v-model="selected" @update:modelValue="item => emit('selectedKeyChanged', item)"
        v-if="props.items">
        <Float floating-as="template" as="div" placement="bottom-start" :offset="2" leave="transition ease-in duration-100"
            leave-from="opacity-100" leave-to="opacity-0" @hide="query = ''">
            <div class="relative">
                <ComboboxInput :disabled="disabled"
                    class="w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 shadow-sm focus:border-[#fe428c] focus:outline-none focus:ring-1 focus:ring-[#fe428c]"
                    @change="query = $event.target.value" :displayValue="(key) => {
                        const matchingItem = props.items.find(item => item.key === key);
                        return matchingItem ? matchingItem.value : '';
                    }" />
                <ComboboxButton class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none">
                    <ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
                </ComboboxButton>
            </div>
            <ComboboxOptions :class="filteredItems.length === 0 ? 'hidden' : ''"
                class="relative max-h-44 overflow-auto w-full rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
                <div v-if="filteredItems.length === 0 && query !== ''"
                    class="relative cursor-default select-none py-2 px-4 text-gray-700">
                    Sin resultados
                </div>
                <ComboboxOption v-for="item   in   filteredItems" :key="item.key" :value="item.key" as="template"
                    v-slot="{ active, selected }">
                    <li
                        :class="['relative cursor-default select-none py-2 pl-8 pr-4', active ? 'bg-[#fe428c] text-white' : 'text-gray-900']">
                        <span :class="['block truncate', selected && 'font-semibold']">
                            {{ item.value }}
                        </span>

                        <span v-if="selected"
                            :class="['absolute inset-y-0 left-0 flex items-center pl-1.5', active ? 'text-white' : 'text-[#fe428c]']">
                            <CheckIcon class="h-5 w-5" aria-hidden="true" />
                        </span>
                    </li>
                </ComboboxOption>
            </ComboboxOptions>
        </Float>
    </Combobox>

If I add theclass "relative" it's the correct with but it doesn't break the parent container overflow. So it's cut off:

image

So I can't make it both the correct with and keep the break overflow cappabilities

ycs77 commented 1 year ago

Hi @cxammar, I can't reproduce this bug, please open a new issue and provide a minimally reproducible repo using GitHub or stackblitz, Online demo provided the Headless UI Float stackblitz examples.

cxammar commented 1 year ago

Owner

Hi, I filed a new Issue with a stacblitz as you request. https://github.com/ycs77/headlessui-float/issues/75