Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance of Combobox component #2574

Merged
merged 5 commits into from
Jul 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/@headlessui-vue/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
87 changes: 51 additions & 36 deletions packages/@headlessui-vue/src/components/combobox/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (localActiveOptionIndex !== -1) {
adjustedState.activeOptionIndex = localActiveOptionIndex
if (props.disabled) return
if (
optionsRef.value &&
!optionsPropsRef.value.static &&
comboboxState.value === ComboboxStates.Closed
) {
return
}
}

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,
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
}
}
)

activeOptionIndex.value = nextActiveOptionIndex
activationTrigger.value = trigger ?? ActivationTrigger.Other
options.value = adjustedState.options
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
})
},
selectOption(id: string) {
let option = options.value.find((item) => item.id === id)
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down
251 changes: 251 additions & 0 deletions packages/playground-react/data.ts
Original file line number Diff line number Diff line change
@@ -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',
]
Loading