Improve performance of Combobox component (#2574)
* add countries data and combobox example * directly push to the options array No need to spread here because this will slow things down tremendously. If you add 5 options, then this will happen: ```js let options = [] options = [...options, 1] options = [...options, 2] options = [...options, 3] options = [...options, 4] options = [...options, 5] ``` It's making a lot of copies and has to spread everything it just copied again. Instead, let's just push to the array: ```js let options = [] options.push(1) options.push(2) options.push(3) options.push(4) options.push(5) ``` * only sort options by DOM node once When registering 50 options, we will re-sort the options based on the DOM node position for every option that gets registered. This is overkill and unnecessary. The re-sorting is useful if an option is injected between 2 options. When we sort the full list for _every_ `registerOption` call then the result after the last `registerOption` will be the same if we only sorted after the last `registerOption` call only. This will ensure that we are skipping a ton of work and only do the work once at the end where it matters. * debounce the `goToOption` call until the next frame This will allow us to perform the task of `goToOption` once instead of `n` times if they happen really fast after eachother. This can happen when you are leaving option B and going to option A. Then the following steps happen: - Leaving Option B `goToOption(Nothing)` - Entering Option A `goToOption(A)` Both will re-render everything because the internal active option index would be changed. But only the last `goToOption(A)` is the interesting version here. We could also move the `goToOption(Nothing)` to the `ComboboxOptions` (wrapper) itself instead of adding these listeners on each individual `ComboboxOption`. However, if you add an additional visual piece of DOM above or between the options, hovering that would not leave the `ComboboxOptions` and therefore the last `ComboboxOption` would still be active which we don't want. * update changelog
This commit is contained in:
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Ensure the caret is in a consistent position when syncing the `Combobox.Input` value ([#2568](https://github.com/tailwindlabs/headlessui/pull/2568))
|
||||
- Improve "outside click" behaviour in combination with 3rd party libraries ([#2572](https://github.com/tailwindlabs/headlessui/pull/2572))
|
||||
- Improve performance of `Combobox` component ([#2574](https://github.com/tailwindlabs/headlessui/pull/2574))
|
||||
|
||||
## [1.7.14] - 2023-06-01
|
||||
|
||||
|
||||
@@ -202,6 +202,9 @@ export let Combobox = defineComponent({
|
||||
computed(() => props.defaultValue)
|
||||
)
|
||||
|
||||
let goToOptionRaf: ReturnType<typeof requestAnimationFrame> | null = null
|
||||
let orderOptionsRaf: ReturnType<typeof requestAnimationFrame> | null = null
|
||||
|
||||
let api = {
|
||||
comboboxState,
|
||||
value,
|
||||
@@ -275,47 +278,53 @@ export let Combobox = defineComponent({
|
||||
comboboxState.value = ComboboxStates.Open
|
||||
},
|
||||
goToOption(focus: Focus, id?: string, trigger?: ActivationTrigger) {
|
||||
defaultToFirstOption.value = false
|
||||
|
||||
if (props.disabled) return
|
||||
if (
|
||||
optionsRef.value &&
|
||||
!optionsPropsRef.value.static &&
|
||||
comboboxState.value === ComboboxStates.Closed
|
||||
) {
|
||||
return
|
||||
if (goToOptionRaf !== null) {
|
||||
cancelAnimationFrame(goToOptionRaf)
|
||||
}
|
||||
|
||||
let adjustedState = adjustOrderedState()
|
||||
goToOptionRaf = requestAnimationFrame(() => {
|
||||
defaultToFirstOption.value = false
|
||||
|
||||
// It's possible that the activeOptionIndex is set to `null` internally, but
|
||||
// this means that we will fallback to the first non-disabled option by default.
|
||||
// We have to take this into account.
|
||||
if (adjustedState.activeOptionIndex === null) {
|
||||
let localActiveOptionIndex = adjustedState.options.findIndex(
|
||||
(option) => !option.dataRef.disabled
|
||||
if (props.disabled) return
|
||||
if (
|
||||
optionsRef.value &&
|
||||
!optionsPropsRef.value.static &&
|
||||
comboboxState.value === ComboboxStates.Closed
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
let adjustedState = adjustOrderedState()
|
||||
|
||||
// It's possible that the activeOptionIndex is set to `null` internally, but
|
||||
// this means that we will fallback to the first non-disabled option by default.
|
||||
// We have to take this into account.
|
||||
if (adjustedState.activeOptionIndex === null) {
|
||||
let localActiveOptionIndex = adjustedState.options.findIndex(
|
||||
(option) => !option.dataRef.disabled
|
||||
)
|
||||
|
||||
if (localActiveOptionIndex !== -1) {
|
||||
adjustedState.activeOptionIndex = localActiveOptionIndex
|
||||
}
|
||||
}
|
||||
|
||||
let nextActiveOptionIndex = calculateActiveIndex(
|
||||
focus === Focus.Specific
|
||||
? { focus: Focus.Specific, id: id! }
|
||||
: { focus: focus as Exclude<Focus, Focus.Specific> },
|
||||
{
|
||||
resolveItems: () => adjustedState.options,
|
||||
resolveActiveIndex: () => adjustedState.activeOptionIndex,
|
||||
resolveId: (option) => option.id,
|
||||
resolveDisabled: (option) => option.dataRef.disabled,
|
||||
}
|
||||
)
|
||||
|
||||
if (localActiveOptionIndex !== -1) {
|
||||
adjustedState.activeOptionIndex = localActiveOptionIndex
|
||||
}
|
||||
}
|
||||
|
||||
let nextActiveOptionIndex = calculateActiveIndex(
|
||||
focus === Focus.Specific
|
||||
? { focus: Focus.Specific, id: id! }
|
||||
: { focus: focus as Exclude<Focus, Focus.Specific> },
|
||||
{
|
||||
resolveItems: () => adjustedState.options,
|
||||
resolveActiveIndex: () => adjustedState.activeOptionIndex,
|
||||
resolveId: (option) => option.id,
|
||||
resolveDisabled: (option) => option.dataRef.disabled,
|
||||
}
|
||||
)
|
||||
|
||||
activeOptionIndex.value = nextActiveOptionIndex
|
||||
activationTrigger.value = trigger ?? ActivationTrigger.Other
|
||||
options.value = adjustedState.options
|
||||
activeOptionIndex.value = nextActiveOptionIndex
|
||||
activationTrigger.value = trigger ?? ActivationTrigger.Other
|
||||
options.value = adjustedState.options
|
||||
})
|
||||
},
|
||||
selectOption(id: string) {
|
||||
let option = options.value.find((item) => item.id === id)
|
||||
@@ -369,8 +378,14 @@ export let Combobox = defineComponent({
|
||||
api.goToOption(Focus.Specific, id)
|
||||
},
|
||||
registerOption(id: string, dataRef: ComboboxOptionData) {
|
||||
if (orderOptionsRaf) cancelAnimationFrame(orderOptionsRaf)
|
||||
|
||||
let option = { id, dataRef }
|
||||
let adjustedState = adjustOrderedState((options) => [...options, option])
|
||||
|
||||
let adjustedState = adjustOrderedState((options) => {
|
||||
options.push(option)
|
||||
return options
|
||||
})
|
||||
|
||||
// Check if we have a selected value that we can make active.
|
||||
if (activeOptionIndex.value === null) {
|
||||
@@ -394,7 +409,7 @@ export let Combobox = defineComponent({
|
||||
|
||||
// If some of the DOM elements aren't ready yet, then we can retry in the next tick.
|
||||
if (adjustedState.options.some((option) => !dom(option.dataRef.domRef))) {
|
||||
requestAnimationFrame(() => {
|
||||
orderOptionsRaf = requestAnimationFrame(() => {
|
||||
let adjustedState = adjustOrderedState()
|
||||
options.value = adjustedState.options
|
||||
activeOptionIndex.value = adjustedState.activeOptionIndex
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
export let countries = [
|
||||
'Afghanistan',
|
||||
'Albania',
|
||||
'Algeria',
|
||||
'American Samoa',
|
||||
'Andorra',
|
||||
'Angola',
|
||||
'Anguilla',
|
||||
'Antarctica',
|
||||
'Antigua and Barbuda',
|
||||
'Argentina',
|
||||
'Armenia',
|
||||
'Aruba',
|
||||
'Australia',
|
||||
'Austria',
|
||||
'Azerbaijan',
|
||||
'Bahamas (the)',
|
||||
'Bahrain',
|
||||
'Bangladesh',
|
||||
'Barbados',
|
||||
'Belarus',
|
||||
'Belgium',
|
||||
'Belize',
|
||||
'Benin',
|
||||
'Bermuda',
|
||||
'Bhutan',
|
||||
'Bolivia (Plurinational State of)',
|
||||
'Bonaire, Sint Eustatius and Saba',
|
||||
'Bosnia and Herzegovina',
|
||||
'Botswana',
|
||||
'Bouvet Island',
|
||||
'Brazil',
|
||||
'British Indian Ocean Territory (the)',
|
||||
'Brunei Darussalam',
|
||||
'Bulgaria',
|
||||
'Burkina Faso',
|
||||
'Burundi',
|
||||
'Cabo Verde',
|
||||
'Cambodia',
|
||||
'Cameroon',
|
||||
'Canada',
|
||||
'Cayman Islands (the)',
|
||||
'Central African Republic (the)',
|
||||
'Chad',
|
||||
'Chile',
|
||||
'China',
|
||||
'Christmas Island',
|
||||
'Cocos (Keeling) Islands (the)',
|
||||
'Colombia',
|
||||
'Comoros (the)',
|
||||
'Congo (the Democratic Republic of the)',
|
||||
'Congo (the)',
|
||||
'Cook Islands (the)',
|
||||
'Costa Rica',
|
||||
'Croatia',
|
||||
'Cuba',
|
||||
'Curaçao',
|
||||
'Cyprus',
|
||||
'Czechia',
|
||||
"Côte d'Ivoire",
|
||||
'Denmark',
|
||||
'Djibouti',
|
||||
'Dominica',
|
||||
'Dominican Republic (the)',
|
||||
'Ecuador',
|
||||
'Egypt',
|
||||
'El Salvador',
|
||||
'Equatorial Guinea',
|
||||
'Eritrea',
|
||||
'Estonia',
|
||||
'Eswatini',
|
||||
'Ethiopia',
|
||||
'Falkland Islands (the) [Malvinas]',
|
||||
'Faroe Islands (the)',
|
||||
'Fiji',
|
||||
'Finland',
|
||||
'France',
|
||||
'French Guiana',
|
||||
'French Polynesia',
|
||||
'French Southern Territories (the)',
|
||||
'Gabon',
|
||||
'Gambia (the)',
|
||||
'Georgia',
|
||||
'Germany',
|
||||
'Ghana',
|
||||
'Gibraltar',
|
||||
'Greece',
|
||||
'Greenland',
|
||||
'Grenada',
|
||||
'Guadeloupe',
|
||||
'Guam',
|
||||
'Guatemala',
|
||||
'Guernsey',
|
||||
'Guinea',
|
||||
'Guinea-Bissau',
|
||||
'Guyana',
|
||||
'Haiti',
|
||||
'Heard Island and McDonald Islands',
|
||||
'Holy See (the)',
|
||||
'Honduras',
|
||||
'Hong Kong',
|
||||
'Hungary',
|
||||
'Iceland',
|
||||
'India',
|
||||
'Indonesia',
|
||||
'Iran (Islamic Republic of)',
|
||||
'Iraq',
|
||||
'Ireland',
|
||||
'Isle of Man',
|
||||
'Israel',
|
||||
'Italy',
|
||||
'Jamaica',
|
||||
'Japan',
|
||||
'Jersey',
|
||||
'Jordan',
|
||||
'Kazakhstan',
|
||||
'Kenya',
|
||||
'Kiribati',
|
||||
"Korea (the Democratic People's Republic of)",
|
||||
'Korea (the Republic of)',
|
||||
'Kuwait',
|
||||
'Kyrgyzstan',
|
||||
"Lao People's Democratic Republic (the)",
|
||||
'Latvia',
|
||||
'Lebanon',
|
||||
'Lesotho',
|
||||
'Liberia',
|
||||
'Libya',
|
||||
'Liechtenstein',
|
||||
'Lithuania',
|
||||
'Luxembourg',
|
||||
'Macao',
|
||||
'Madagascar',
|
||||
'Malawi',
|
||||
'Malaysia',
|
||||
'Maldives',
|
||||
'Mali',
|
||||
'Malta',
|
||||
'Marshall Islands (the)',
|
||||
'Martinique',
|
||||
'Mauritania',
|
||||
'Mauritius',
|
||||
'Mayotte',
|
||||
'Mexico',
|
||||
'Micronesia (Federated States of)',
|
||||
'Moldova (the Republic of)',
|
||||
'Monaco',
|
||||
'Mongolia',
|
||||
'Montenegro',
|
||||
'Montserrat',
|
||||
'Morocco',
|
||||
'Mozambique',
|
||||
'Myanmar',
|
||||
'Namibia',
|
||||
'Nauru',
|
||||
'Nepal',
|
||||
'Netherlands (the)',
|
||||
'New Caledonia',
|
||||
'New Zealand',
|
||||
'Nicaragua',
|
||||
'Niger (the)',
|
||||
'Nigeria',
|
||||
'Niue',
|
||||
'Norfolk Island',
|
||||
'Northern Mariana Islands (the)',
|
||||
'Norway',
|
||||
'Oman',
|
||||
'Pakistan',
|
||||
'Palau',
|
||||
'Palestine, State of',
|
||||
'Panama',
|
||||
'Papua New Guinea',
|
||||
'Paraguay',
|
||||
'Peru',
|
||||
'Philippines (the)',
|
||||
'Pitcairn',
|
||||
'Poland',
|
||||
'Portugal',
|
||||
'Puerto Rico',
|
||||
'Qatar',
|
||||
'Republic of North Macedonia',
|
||||
'Romania',
|
||||
'Russian Federation (the)',
|
||||
'Rwanda',
|
||||
'Réunion',
|
||||
'Saint Barthélemy',
|
||||
'Saint Helena, Ascension and Tristan da Cunha',
|
||||
'Saint Kitts and Nevis',
|
||||
'Saint Lucia',
|
||||
'Saint Martin (French part)',
|
||||
'Saint Pierre and Miquelon',
|
||||
'Saint Vincent and the Grenadines',
|
||||
'Samoa',
|
||||
'San Marino',
|
||||
'Sao Tome and Principe',
|
||||
'Saudi Arabia',
|
||||
'Senegal',
|
||||
'Serbia',
|
||||
'Seychelles',
|
||||
'Sierra Leone',
|
||||
'Singapore',
|
||||
'Sint Maarten (Dutch part)',
|
||||
'Slovakia',
|
||||
'Slovenia',
|
||||
'Solomon Islands',
|
||||
'Somalia',
|
||||
'South Africa',
|
||||
'South Georgia and the South Sandwich Islands',
|
||||
'South Sudan',
|
||||
'Spain',
|
||||
'Sri Lanka',
|
||||
'Sudan (the)',
|
||||
'Suriname',
|
||||
'Svalbard and Jan Mayen',
|
||||
'Sweden',
|
||||
'Switzerland',
|
||||
'Syrian Arab Republic',
|
||||
'Taiwan',
|
||||
'Tajikistan',
|
||||
'Tanzania, United Republic of',
|
||||
'Thailand',
|
||||
'Timor-Leste',
|
||||
'Togo',
|
||||
'Tokelau',
|
||||
'Tonga',
|
||||
'Trinidad and Tobago',
|
||||
'Tunisia',
|
||||
'Turkey',
|
||||
'Turkmenistan',
|
||||
'Turks and Caicos Islands (the)',
|
||||
'Tuvalu',
|
||||
'Uganda',
|
||||
'Ukraine',
|
||||
'United Arab Emirates (the)',
|
||||
'United Kingdom of Great Britain and Northern Ireland (the)',
|
||||
'United States Minor Outlying Islands (the)',
|
||||
'United States of America (the)',
|
||||
'Uruguay',
|
||||
'Uzbekistan',
|
||||
'Vanuatu',
|
||||
'Venezuela (Bolivarian Republic of)',
|
||||
'Viet Nam',
|
||||
'Virgin Islands (British)',
|
||||
'Virgin Islands (U.S.)',
|
||||
'Wallis and Futuna',
|
||||
'Western Sahara',
|
||||
'Yemen',
|
||||
'Zambia',
|
||||
'Zimbabwe',
|
||||
'Åland Islands',
|
||||
]
|
||||
@@ -0,0 +1,126 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Combobox } from '@headlessui/react'
|
||||
|
||||
import { classNames } from '../../utils/class-names'
|
||||
import { Button } from '../../components/button'
|
||||
import { countries as allCountries } from '../../data'
|
||||
|
||||
function useDebounce<T>(value: T, delay: number) {
|
||||
let [debouncedValue, setDebouncedValue] = useState(value)
|
||||
useEffect(() => {
|
||||
let timer = setTimeout(() => setDebouncedValue(value), delay)
|
||||
return () => clearTimeout(timer)
|
||||
}, [value, delay])
|
||||
return debouncedValue
|
||||
}
|
||||
export default function Home() {
|
||||
let [query, setQuery] = useState('')
|
||||
let [activeCountry, setActiveCountry] = useState(allCountries[2])
|
||||
|
||||
// Mimic delayed response from an API
|
||||
let actualQuery = useDebounce(query, 0 /* Change to higher value like 100 for testing purposes */)
|
||||
|
||||
// Choose a random person on mount
|
||||
useEffect(() => {
|
||||
setActiveCountry(allCountries[Math.floor(Math.random() * allCountries.length)])
|
||||
}, [])
|
||||
|
||||
let countries =
|
||||
actualQuery === ''
|
||||
? allCountries
|
||||
: allCountries.filter((person) => person.toLowerCase().includes(actualQuery.toLowerCase()))
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
|
||||
<div className="mx-auto w-full max-w-xs">
|
||||
<div className="py-8 font-mono text-xs">Selected country: {activeCountry}</div>
|
||||
<div className="space-y-1">
|
||||
<Combobox
|
||||
value={activeCountry}
|
||||
onChange={(value) => {
|
||||
setActiveCountry(value)
|
||||
setQuery('')
|
||||
}}
|
||||
as="div"
|
||||
>
|
||||
<Combobox.Label className="block text-sm font-medium leading-5 text-gray-700">
|
||||
Country
|
||||
</Combobox.Label>
|
||||
|
||||
<div className="relative">
|
||||
<span className="relative inline-flex flex-row overflow-hidden rounded-md border shadow-sm">
|
||||
<Combobox.Input
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="border-none px-3 py-1 outline-none"
|
||||
/>
|
||||
<Combobox.Button as={Button}>
|
||||
<span className="pointer-events-none flex items-center px-2">
|
||||
<svg
|
||||
className="h-5 w-5 text-gray-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</Combobox.Button>
|
||||
</span>
|
||||
|
||||
<div className="absolute mt-1 w-full rounded-md bg-white shadow-lg">
|
||||
<Combobox.Options className="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
|
||||
{countries.map((country) => (
|
||||
<Combobox.Option
|
||||
key={country}
|
||||
value={country}
|
||||
className={({ active }) => {
|
||||
return classNames(
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
|
||||
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
|
||||
)
|
||||
}}
|
||||
>
|
||||
{({ active, selected }) => (
|
||||
<>
|
||||
<span
|
||||
className={classNames(
|
||||
'block truncate',
|
||||
selected ? 'font-semibold' : 'font-normal'
|
||||
)}
|
||||
>
|
||||
{country}
|
||||
</span>
|
||||
{selected && (
|
||||
<span
|
||||
className={classNames(
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
active ? 'text-white' : 'text-indigo-600'
|
||||
)}
|
||||
>
|
||||
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<script>
|
||||
import { countries as allCountries } from '../../data'
|
||||
import { ref, defineComponent, computed, onMounted, watch } from 'vue'
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxButton,
|
||||
ComboboxInput,
|
||||
ComboboxLabel,
|
||||
ComboboxOption,
|
||||
ComboboxOptions,
|
||||
} from '@headlessui/vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Combobox,
|
||||
ComboboxButton,
|
||||
ComboboxInput,
|
||||
ComboboxLabel,
|
||||
ComboboxOption,
|
||||
ComboboxOptions,
|
||||
},
|
||||
setup() {
|
||||
let query = ref('')
|
||||
let activeCountry = ref(allCountries[2]) // allCountries[Math.floor(Math.random() * allCountries.length)]
|
||||
let filteredCountries = computed(() => {
|
||||
return query.value === ''
|
||||
? allCountries
|
||||
: allCountries.filter((country) => {
|
||||
return country.toLowerCase().includes(query.value.toLowerCase())
|
||||
})
|
||||
})
|
||||
|
||||
// Choose a random country on mount
|
||||
onMounted(() => {
|
||||
activeCountry.value = allCountries[Math.floor(Math.random() * allCountries.length)]
|
||||
})
|
||||
|
||||
watch(activeCountry, () => {
|
||||
query.value = ''
|
||||
})
|
||||
|
||||
return {
|
||||
query,
|
||||
activeCountry,
|
||||
filteredCountries,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
|
||||
<div class="mx-auto w-full max-w-xs">
|
||||
<div class="py-8 font-mono text-xs">
|
||||
Selected country: {{ activeCountry?.name ?? 'Nothing yet' }}
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Combobox v-model="activeCountry" as="div">
|
||||
<ComboboxLabel class="block text-sm font-medium leading-5 text-gray-700">
|
||||
Assigned to
|
||||
</ComboboxLabel>
|
||||
|
||||
<div class="relative">
|
||||
<span class="relative inline-flex flex-row overflow-hidden rounded-md border shadow-sm">
|
||||
<ComboboxInput
|
||||
@change="query = $event.target.value"
|
||||
class="border-none px-3 py-1 outline-none"
|
||||
/>
|
||||
<ComboboxButton
|
||||
class="cursor-default border-l bg-gray-100 px-1 text-indigo-600 focus:outline-none"
|
||||
>
|
||||
<span class="pointer-events-none flex items-center px-2">
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</ComboboxButton>
|
||||
</span>
|
||||
|
||||
<div class="absolute mt-1 w-full rounded-md bg-white shadow-lg">
|
||||
<ComboboxOptions
|
||||
class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5"
|
||||
>
|
||||
<ComboboxOption
|
||||
v-for="country in filteredCountries"
|
||||
:key="country"
|
||||
:value="country"
|
||||
v-slot="{ active, selected }"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
|
||||
active ? 'bg-indigo-600 text-white' : 'text-gray-900',
|
||||
]"
|
||||
>
|
||||
<span :class="['block truncate', selected ? 'font-semibold' : 'font-normal']">
|
||||
{{ country }}
|
||||
</span>
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
active ? 'text-white' : 'text-indigo-600',
|
||||
]"
|
||||
>
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</ComboboxOption>
|
||||
</ComboboxOptions>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,251 @@
|
||||
export let countries = [
|
||||
'Afghanistan',
|
||||
'Albania',
|
||||
'Algeria',
|
||||
'American Samoa',
|
||||
'Andorra',
|
||||
'Angola',
|
||||
'Anguilla',
|
||||
'Antarctica',
|
||||
'Antigua and Barbuda',
|
||||
'Argentina',
|
||||
'Armenia',
|
||||
'Aruba',
|
||||
'Australia',
|
||||
'Austria',
|
||||
'Azerbaijan',
|
||||
'Bahamas (the)',
|
||||
'Bahrain',
|
||||
'Bangladesh',
|
||||
'Barbados',
|
||||
'Belarus',
|
||||
'Belgium',
|
||||
'Belize',
|
||||
'Benin',
|
||||
'Bermuda',
|
||||
'Bhutan',
|
||||
'Bolivia (Plurinational State of)',
|
||||
'Bonaire, Sint Eustatius and Saba',
|
||||
'Bosnia and Herzegovina',
|
||||
'Botswana',
|
||||
'Bouvet Island',
|
||||
'Brazil',
|
||||
'British Indian Ocean Territory (the)',
|
||||
'Brunei Darussalam',
|
||||
'Bulgaria',
|
||||
'Burkina Faso',
|
||||
'Burundi',
|
||||
'Cabo Verde',
|
||||
'Cambodia',
|
||||
'Cameroon',
|
||||
'Canada',
|
||||
'Cayman Islands (the)',
|
||||
'Central African Republic (the)',
|
||||
'Chad',
|
||||
'Chile',
|
||||
'China',
|
||||
'Christmas Island',
|
||||
'Cocos (Keeling) Islands (the)',
|
||||
'Colombia',
|
||||
'Comoros (the)',
|
||||
'Congo (the Democratic Republic of the)',
|
||||
'Congo (the)',
|
||||
'Cook Islands (the)',
|
||||
'Costa Rica',
|
||||
'Croatia',
|
||||
'Cuba',
|
||||
'Curaçao',
|
||||
'Cyprus',
|
||||
'Czechia',
|
||||
"Côte d'Ivoire",
|
||||
'Denmark',
|
||||
'Djibouti',
|
||||
'Dominica',
|
||||
'Dominican Republic (the)',
|
||||
'Ecuador',
|
||||
'Egypt',
|
||||
'El Salvador',
|
||||
'Equatorial Guinea',
|
||||
'Eritrea',
|
||||
'Estonia',
|
||||
'Eswatini',
|
||||
'Ethiopia',
|
||||
'Falkland Islands (the) [Malvinas]',
|
||||
'Faroe Islands (the)',
|
||||
'Fiji',
|
||||
'Finland',
|
||||
'France',
|
||||
'French Guiana',
|
||||
'French Polynesia',
|
||||
'French Southern Territories (the)',
|
||||
'Gabon',
|
||||
'Gambia (the)',
|
||||
'Georgia',
|
||||
'Germany',
|
||||
'Ghana',
|
||||
'Gibraltar',
|
||||
'Greece',
|
||||
'Greenland',
|
||||
'Grenada',
|
||||
'Guadeloupe',
|
||||
'Guam',
|
||||
'Guatemala',
|
||||
'Guernsey',
|
||||
'Guinea',
|
||||
'Guinea-Bissau',
|
||||
'Guyana',
|
||||
'Haiti',
|
||||
'Heard Island and McDonald Islands',
|
||||
'Holy See (the)',
|
||||
'Honduras',
|
||||
'Hong Kong',
|
||||
'Hungary',
|
||||
'Iceland',
|
||||
'India',
|
||||
'Indonesia',
|
||||
'Iran (Islamic Republic of)',
|
||||
'Iraq',
|
||||
'Ireland',
|
||||
'Isle of Man',
|
||||
'Israel',
|
||||
'Italy',
|
||||
'Jamaica',
|
||||
'Japan',
|
||||
'Jersey',
|
||||
'Jordan',
|
||||
'Kazakhstan',
|
||||
'Kenya',
|
||||
'Kiribati',
|
||||
"Korea (the Democratic People's Republic of)",
|
||||
'Korea (the Republic of)',
|
||||
'Kuwait',
|
||||
'Kyrgyzstan',
|
||||
"Lao People's Democratic Republic (the)",
|
||||
'Latvia',
|
||||
'Lebanon',
|
||||
'Lesotho',
|
||||
'Liberia',
|
||||
'Libya',
|
||||
'Liechtenstein',
|
||||
'Lithuania',
|
||||
'Luxembourg',
|
||||
'Macao',
|
||||
'Madagascar',
|
||||
'Malawi',
|
||||
'Malaysia',
|
||||
'Maldives',
|
||||
'Mali',
|
||||
'Malta',
|
||||
'Marshall Islands (the)',
|
||||
'Martinique',
|
||||
'Mauritania',
|
||||
'Mauritius',
|
||||
'Mayotte',
|
||||
'Mexico',
|
||||
'Micronesia (Federated States of)',
|
||||
'Moldova (the Republic of)',
|
||||
'Monaco',
|
||||
'Mongolia',
|
||||
'Montenegro',
|
||||
'Montserrat',
|
||||
'Morocco',
|
||||
'Mozambique',
|
||||
'Myanmar',
|
||||
'Namibia',
|
||||
'Nauru',
|
||||
'Nepal',
|
||||
'Netherlands (the)',
|
||||
'New Caledonia',
|
||||
'New Zealand',
|
||||
'Nicaragua',
|
||||
'Niger (the)',
|
||||
'Nigeria',
|
||||
'Niue',
|
||||
'Norfolk Island',
|
||||
'Northern Mariana Islands (the)',
|
||||
'Norway',
|
||||
'Oman',
|
||||
'Pakistan',
|
||||
'Palau',
|
||||
'Palestine, State of',
|
||||
'Panama',
|
||||
'Papua New Guinea',
|
||||
'Paraguay',
|
||||
'Peru',
|
||||
'Philippines (the)',
|
||||
'Pitcairn',
|
||||
'Poland',
|
||||
'Portugal',
|
||||
'Puerto Rico',
|
||||
'Qatar',
|
||||
'Republic of North Macedonia',
|
||||
'Romania',
|
||||
'Russian Federation (the)',
|
||||
'Rwanda',
|
||||
'Réunion',
|
||||
'Saint Barthélemy',
|
||||
'Saint Helena, Ascension and Tristan da Cunha',
|
||||
'Saint Kitts and Nevis',
|
||||
'Saint Lucia',
|
||||
'Saint Martin (French part)',
|
||||
'Saint Pierre and Miquelon',
|
||||
'Saint Vincent and the Grenadines',
|
||||
'Samoa',
|
||||
'San Marino',
|
||||
'Sao Tome and Principe',
|
||||
'Saudi Arabia',
|
||||
'Senegal',
|
||||
'Serbia',
|
||||
'Seychelles',
|
||||
'Sierra Leone',
|
||||
'Singapore',
|
||||
'Sint Maarten (Dutch part)',
|
||||
'Slovakia',
|
||||
'Slovenia',
|
||||
'Solomon Islands',
|
||||
'Somalia',
|
||||
'South Africa',
|
||||
'South Georgia and the South Sandwich Islands',
|
||||
'South Sudan',
|
||||
'Spain',
|
||||
'Sri Lanka',
|
||||
'Sudan (the)',
|
||||
'Suriname',
|
||||
'Svalbard and Jan Mayen',
|
||||
'Sweden',
|
||||
'Switzerland',
|
||||
'Syrian Arab Republic',
|
||||
'Taiwan',
|
||||
'Tajikistan',
|
||||
'Tanzania, United Republic of',
|
||||
'Thailand',
|
||||
'Timor-Leste',
|
||||
'Togo',
|
||||
'Tokelau',
|
||||
'Tonga',
|
||||
'Trinidad and Tobago',
|
||||
'Tunisia',
|
||||
'Turkey',
|
||||
'Turkmenistan',
|
||||
'Turks and Caicos Islands (the)',
|
||||
'Tuvalu',
|
||||
'Uganda',
|
||||
'Ukraine',
|
||||
'United Arab Emirates (the)',
|
||||
'United Kingdom of Great Britain and Northern Ireland (the)',
|
||||
'United States Minor Outlying Islands (the)',
|
||||
'United States of America (the)',
|
||||
'Uruguay',
|
||||
'Uzbekistan',
|
||||
'Vanuatu',
|
||||
'Venezuela (Bolivarian Republic of)',
|
||||
'Viet Nam',
|
||||
'Virgin Islands (British)',
|
||||
'Virgin Islands (U.S.)',
|
||||
'Wallis and Futuna',
|
||||
'Western Sahara',
|
||||
'Yemen',
|
||||
'Zambia',
|
||||
'Zimbabwe',
|
||||
'Åland Islands',
|
||||
]
|
||||
Reference in New Issue
Block a user