diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md index 9736078..d818ab7 100644 --- a/packages/@headlessui-vue/CHANGELOG.md +++ b/packages/@headlessui-vue/CHANGELOG.md @@ -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 diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.ts b/packages/@headlessui-vue/src/components/combobox/combobox.ts index 596822c..5a39b16 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.ts @@ -202,6 +202,9 @@ export let Combobox = defineComponent({ computed(() => props.defaultValue) ) + let goToOptionRaf: ReturnType | null = null + let orderOptionsRaf: ReturnType | 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 }, + { + 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 }, - { - 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 diff --git a/packages/playground-react/data.ts b/packages/playground-react/data.ts new file mode 100644 index 0000000..7df0810 --- /dev/null +++ b/packages/playground-react/data.ts @@ -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', +] diff --git a/packages/playground-react/pages/combobox/combobox-countries.tsx b/packages/playground-react/pages/combobox/combobox-countries.tsx new file mode 100644 index 0000000..2c3c761 --- /dev/null +++ b/packages/playground-react/pages/combobox/combobox-countries.tsx @@ -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(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 ( +
+
+
Selected country: {activeCountry}
+
+ { + setActiveCountry(value) + setQuery('') + }} + as="div" + > + + Country + + +
+ + setQuery(e.target.value)} + className="border-none px-3 py-1 outline-none" + /> + + + + + + + + + +
+ + {countries.map((country) => ( + { + 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 }) => ( + <> + + {country} + + {selected && ( + + + + + + )} + + )} + + ))} + +
+
+
+
+
+
+ ) +} diff --git a/packages/playground-vue/src/components/combobox/combobox-countries.vue b/packages/playground-vue/src/components/combobox/combobox-countries.vue new file mode 100644 index 0000000..8c6508a --- /dev/null +++ b/packages/playground-vue/src/components/combobox/combobox-countries.vue @@ -0,0 +1,133 @@ + + + diff --git a/packages/playground-vue/src/data.ts b/packages/playground-vue/src/data.ts new file mode 100644 index 0000000..7df0810 --- /dev/null +++ b/packages/playground-vue/src/data.ts @@ -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', +]