Repository: tailwindlabs/headlessui
Pull request #3126 (2024-04-24 00:29:00)

Add frozen value to `ComboboxOptions` component
This PR introduces a frozen value, similar to the `Listbox` component. This allows us to keep the visual state of the "old" value when selecting a new value.

The reason for this is that when you select a value, the combobox will close and in a lot of cases a (fade out) transition is happening. If we change the selected state to the new value, then the UI will flicker (or at least changes visual state) when the transition is happening. This PR prevents that. This is also how the native `<select>` works. 

Note: the `Combobox` and `ComboboxInput` still know about the correct newly selected value and will update `aria-activedescendant` information correctly.

Also note that this only happens in "single value mode", if you use the `multiple` prop, then the selected value will be updated immediately.


vercel[bot]: [vc]: #Q6Spq9H7HihUHtoByLPiBnk/Ri8b9rqIeUFqAw/TM9s=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJoZWFkbGVzc3VpLXZ1ZSIsInJvb3REaXJlY3RvcnkiOiJwbGF5Z3JvdW5kcy92dWUiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vdGFpbHdpbmRsYWJzL2hlYWRsZXNzdWktdnVlLzRwNHU1S0M3MlJiajhBTGV2VTd0MUtRQVB5aVMiLCJwcmV2aWV3VXJsIjoiaGVhZGxlc3N1aS12dWUtZ2l0LWZlYXQtYWRkLWZyb3plbi12YWx1ZS10by0zZjFmNzEtdGFpbHdpbmRsYWJzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiUEVORElORyIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJoZWFkbGVzc3VpLXZ1ZS1naXQtZmVhdC1hZGQtZnJvemVuLXZhbHVlLXRvLTNmMWY3MS10YWlsd2luZGxhYnMudmVyY2VsLmFwcCJ9fV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **headlessui-vue** | 🔄 Building ([Inspect](https://vercel.com/tailwindlabs/headlessui-vue/4p4u5KC72Rbj8ALevU7t1KQAPyiS)) | [Visit Preview](https://vercel.live/open-feedback/headlessui-vue-git-feat-add-frozen-value-to-3f1f71-tailwindlabs.vercel.app?via=pr-comment-visit-preview-link&passThrough=1) | 💬 [**Add feedback**](https://vercel.live/open-feedback/headlessui-vue-git-feat-add-frozen-value-to-3f1f71-tailwindlabs.vercel.app?via=pr-comment-feedback-link) | Apr 24, 2024 0:29am |



Patch:
diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md
index 758a8dc..10eeead 100644
--- a/packages/@headlessui-react/CHANGELOG.md
+++ b/packages/@headlessui-react/CHANGELOG.md
@@ -45,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Add new `CloseButton` component and `useClose` hook ([#3096](https://github.com/tailwindlabs/headlessui/pull/3096))
 - Allow passing a boolean to the `anchor` prop ([#3121](https://github.com/tailwindlabs/headlessui/pull/3121))
 - Add `portal` prop to `Combobox`, `Listbox`, `Menu` and `Popover` components ([#3124](https://github.com/tailwindlabs/headlessui/pull/3124))
+- Add frozen value to `ComboboxOptions` component ([#3126](https://github.com/tailwindlabs/headlessui/pull/3126))
 
 ## [1.7.19] - 2024-04-15
 
diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx
index 204a2bc..118d8df 100644
--- a/packages/@headlessui-react/src/components/combobox/combobox.tsx
+++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx
@@ -1646,17 +1646,35 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
     })
   }
 
+  // Frozen state, the selected value will only update visually when the user re-opens the <Combobox />
+  let [frozenValue, setFrozenValue] = useState(data.value)
+  if (
+    data.value !== frozenValue &&
+    data.comboboxState === ComboboxState.Open &&
+    data.mode !== ValueMode.Multi
+  ) {
+    setFrozenValue(data.value)
+  }
+
+  let isSelected = useEvent((compareValue: unknown) => {
+    return data.compare(frozenValue, compareValue)
+  })
+
   return (
     <Portal enabled={visible && portal}>
-      {render({
-        ourProps,
-        theirProps,
-        slot,
-        defaultTag: DEFAULT_OPTIONS_TAG,
-        features: OptionsRenderFeatures,
-        visible,
-        name: 'Combobox.Options',
-      })}
+      <ComboboxDataContext.Provider
+        value={data.mode === ValueMode.Multi ? data : { ...data, isSelected }}
+      >
+        {render({
+          ourProps,
+          theirProps,
+          slot,
+          defaultTag: DEFAULT_OPTIONS_TAG,
+          features: OptionsRenderFeatures,
+          visible,
+          name: 'Combobox.Options',
+        })}
+      </ComboboxDataContext.Provider>
     </Portal>
   )
 }
@@ -1796,7 +1814,7 @@ function OptionFn<
     }
 
     if (data.mode === ValueMode.Single) {
-      requestAnimationFrame(() => actions.closeCombobox())
+      actions.closeCombobox()
     }
   })
 


Test patch:
diff --git a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx
index 912964d..4012bd6 100644
--- a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx
+++ b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx
@@ -4289,7 +4289,6 @@ describe.each([{ virtual: true }, { virtual: false }])(
 
             // Verify that we don't have an selected option anymore
             assertNotActiveComboboxOption(options[1])
-            assertNoSelectedComboboxOption()
 
             // Verify that we saw the `null` change coming in
             expect(handleChange).toHaveBeenCalledTimes(1)