Repository: tailwindlabs/headlessui
Pull request #3061 (2024-03-27 14:22:16)

Expose missing `data-disabled` and `data-focus` attributes on the `TabsPanel`, `MenuButton`, `PopoverButton` and `DisclosureButton` components
This PR exposes more data attributes on some components, mainly:

- Expose `data-focus` on the `TabsPanel` component
- Expose `data-disabled` on the `MenuButton` component
- Expose `data-disabled` on the `PopoverButton` component
- Expose `data-disabled` on the `DisclosureButton` component


This PR also cleans up some repetition on related to this, because in some places we used `props.autoFocus ?? false` or `props.disabled ?? false` in multiple spots. Now, this is destructured from the props object with a fallback.


vercel[bot]: [vc]: #QJBT93JJXWGcCOaYYPykHKluwuhigtT18QQw7yuq/A0=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJoZWFkbGVzc3VpLXZ1ZSIsInJvb3REaXJlY3RvcnkiOiJwbGF5Z3JvdW5kcy92dWUiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vdGFpbHdpbmRsYWJzL2hlYWRsZXNzdWktdnVlL0ZWemMxbnpyNDlXYnNKUks4WVBXZ2FKQXVDa3EiLCJwcmV2aWV3VXJsIjoiaGVhZGxlc3N1aS12dWUtZ2l0LWZlYXQtZXhwb3NlLW1vcmUtZGF0YS1hdC0zZmMwYjEtdGFpbHdpbmRsYWJzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiUEVORElORyIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJoZWFkbGVzc3VpLXZ1ZS1naXQtZmVhdC1leHBvc2UtbW9yZS1kYXRhLWF0LTNmYzBiMS10YWlsd2luZGxhYnMudmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJoZWFkbGVzc3VpLXJlYWN0Iiwicm9vdERpcmVjdG9yeSI6InBsYXlncm91bmRzL3JlYWN0IiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3RhaWx3aW5kbGFicy9oZWFkbGVzc3VpLXJlYWN0LzNnUGhDUXRrUXY4bWJYS2ptc2RUekVmdENWc1kiLCJwcmV2aWV3VXJsIjoiaGVhZGxlc3N1aS1yZWFjdC1naXQtZmVhdC1leHBvc2UtbW9yZS1kYXRhLTRjYmE4Ny10YWlsd2luZGxhYnMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJQRU5ESU5HIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImhlYWRsZXNzdWktcmVhY3QtZ2l0LWZlYXQtZXhwb3NlLW1vcmUtZGF0YS00Y2JhODctdGFpbHdpbmRsYWJzLnZlcmNlbC5hcHAifX1dfQ==
**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-react** | 🔄 Building ([Inspect](https://vercel.com/tailwindlabs/headlessui-react/3gPhCQtkQv8mbXKjmsdTzEftCVsY)) | [Visit Preview](https://vercel.live/open-feedback/headlessui-react-git-feat-expose-more-data-4cba87-tailwindlabs.vercel.app?via=pr-comment-visit-preview-link&passThrough=1) | 💬 [**Add feedback**](https://vercel.live/open-feedback/headlessui-react-git-feat-expose-more-data-4cba87-tailwindlabs.vercel.app?via=pr-comment-feedback-link) | Mar 27, 2024 2:22pm |
| **headlessui-vue** | 🔄 Building ([Inspect](https://vercel.com/tailwindlabs/headlessui-vue/FVzc1nzr49WbsJRK8YPWgaJAuCkq)) | [Visit Preview](https://vercel.live/open-feedback/headlessui-vue-git-feat-expose-more-data-at-3fc0b1-tailwindlabs.vercel.app?via=pr-comment-visit-preview-link&passThrough=1) | 💬 [**Add feedback**](https://vercel.live/open-feedback/headlessui-vue-git-feat-expose-more-data-at-3fc0b1-tailwindlabs.vercel.app?via=pr-comment-feedback-link) | Mar 27, 2024 2:22pm |




Patch:
diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md
index e010ef6..c8291c4 100644
--- a/packages/@headlessui-react/CHANGELOG.md
+++ b/packages/@headlessui-react/CHANGELOG.md
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Prefer incoming `data-*` attributes, over the ones set by Headless UI ([#3035](https://github.com/tailwindlabs/headlessui/pull/3035))
 - Respect `selectedIndex` for controlled `<Tab/>` components ([#3037](https://github.com/tailwindlabs/headlessui/pull/3037))
 - Prevent unnecessary execution of the `displayValue` callback in the `ComboboxInput` component ([#3048](https://github.com/tailwindlabs/headlessui/pull/3048))
+- Expose missing `data-disabled` and `data-focus` attributes on the `TabsPanel`, `MenuButton`, `PopoverButton` and `DisclosureButton` components ([#3061](https://github.com/tailwindlabs/headlessui/pull/3061))
 
 ### Changed
 
diff --git a/packages/@headlessui-react/src/components/button/button.tsx b/packages/@headlessui-react/src/components/button/button.tsx
index 031fef5..daf3e9b 100644
--- a/packages/@headlessui-react/src/components/button/button.tsx
+++ b/packages/@headlessui-react/src/components/button/button.tsx
@@ -41,34 +41,27 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
   ref: Ref<HTMLElement>
 ) {
   let providedDisabled = useDisabled()
-  let { disabled = providedDisabled || false, ...theirProps } = props
+  let { disabled = providedDisabled || false, autoFocus = false, ...theirProps } = props
 
-  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false })
+  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus })
   let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
   let { pressed: active, pressProps } = useActivePress({ disabled })
 
   let ourProps = mergeProps(
     {
       ref,
-      disabled: disabled || undefined,
       type: theirProps.type ?? 'button',
+      disabled: disabled || undefined,
+      autoFocus,
     },
     focusProps,
     hoverProps,
     pressProps
   )
 
-  let slot = useMemo(
-    () =>
-      ({
-        disabled,
-        hover,
-        focus,
-        active,
-        autofocus: props.autoFocus ?? false,
-      }) satisfies ButtonRenderPropArg,
-    [disabled, hover, focus, active, props.autoFocus]
-  )
+  let slot = useMemo(() => {
+    return { disabled, hover, focus, active, autofocus: autoFocus } satisfies ButtonRenderPropArg
+  }, [disabled, hover, focus, active, autoFocus])
 
   return render({
     ourProps,
diff --git a/packages/@headlessui-react/src/components/checkbox/checkbox.tsx b/packages/@headlessui-react/src/components/checkbox/checkbox.tsx
index c67e965..8ee45ba 100644
--- a/packages/@headlessui-react/src/components/checkbox/checkbox.tsx
+++ b/packages/@headlessui-react/src/components/checkbox/checkbox.tsx
@@ -83,6 +83,7 @@ function CheckboxFn<TTag extends ElementType = typeof DEFAULT_CHECKBOX_TAG, TTyp
   let {
     id = providedId || `headlessui-checkbox-${internalId}`,
     disabled = providedDisabled || false,
+    autoFocus = false,
     checked: controlledChecked,
     defaultChecked = false,
     onChange: controlledOnChange,
@@ -127,9 +128,9 @@ function CheckboxFn<TTag extends ElementType = typeof DEFAULT_CHECKBOX_TAG, TTyp
   // This is needed so that we can "cancel" the click event when we use the `Enter` key on a button.
   let handleKeyPress = useEvent((event: ReactKeyboardEvent<HTMLElement>) => event.preventDefault())
 
-  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false })
-  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled ?? false })
-  let { pressed: active, pressProps } = useActivePress({ disabled: disabled ?? false })
+  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus })
+  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
+  let { pressed: active, pressProps } = useActivePress({ disabled })
 
   let ourProps = mergeProps(
     {
@@ -151,20 +152,18 @@ function CheckboxFn<TTag extends ElementType = typeof DEFAULT_CHECKBOX_TAG, TTyp
     pressProps
   )
 
-  let slot = useMemo(
-    () =>
-      ({
-        checked,
-        disabled,
-        hover,
-        focus,
-        active,
-        indeterminate,
-        changing,
-        autofocus: props.autoFocus ?? false,
-      }) satisfies CheckboxRenderPropArg,
-    [checked, indeterminate, disabled, hover, focus, active, changing, props.autoFocus]
-  )
+  let slot = useMemo(() => {
+    return {
+      checked,
+      disabled,
+      hover,
+      focus,
+      active,
+      indeterminate,
+      changing,
+      autofocus: autoFocus,
+    } satisfies CheckboxRenderPropArg
+  }, [checked, indeterminate, disabled, hover, focus, active, changing, autoFocus])
 
   let reset = useCallback(() => {
     return onChange?.(defaultChecked)
diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx
index f5752a0..42bd044 100644
--- a/packages/@headlessui-react/src/components/combobox/combobox.tsx
+++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx
@@ -770,22 +770,20 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
     data.comboboxState === ComboboxState.Open
   )
 
-  let slot = useMemo(
-    () =>
-      ({
-        open: data.comboboxState === ComboboxState.Open,
-        disabled,
-        activeIndex: data.activeOptionIndex,
-        activeOption:
-          data.activeOptionIndex === null
-            ? null
-            : data.virtual
-              ? data.virtual.options[data.activeOptionIndex ?? 0]
-              : (data.options[data.activeOptionIndex]?.dataRef.current.value as TValue) ?? null,
-        value,
-      }) satisfies ComboboxRenderPropArg<unknown>,
-    [data, disabled, value]
-  )
+  let slot = useMemo(() => {
+    return {
+      open: data.comboboxState === ComboboxState.Open,
+      disabled,
+      activeIndex: data.activeOptionIndex,
+      activeOption:
+        data.activeOptionIndex === null
+          ? null
+          : data.virtual
+            ? data.virtual.options[data.activeOptionIndex ?? 0]
+            : (data.options[data.activeOptionIndex]?.dataRef.current.value as TValue) ?? null,
+      value,
+    } satisfies ComboboxRenderPropArg<unknown>
+  }, [data, disabled, value])
 
   let selectActiveOption = useEvent(() => {
     if (data.activeOptionIndex === null) return
@@ -958,6 +956,7 @@ export type ComboboxInputProps<
   InputPropsWeControl,
   {
     defaultValue?: TType
+    disabled?: boolean
     displayValue?(item: TType): string
     onChange?(event: React.ChangeEvent<HTMLInputElement>): void
     autoFocus?: boolean
@@ -970,18 +969,21 @@ function InputFn<
   // But today is not that day..
   TType = Parameters<typeof ComboboxRoot>[0]['value'],
 >(props: ComboboxInputProps<TTag, TType>, ref: Ref<HTMLInputElement>) {
+  let data = useData('Combobox.Input')
+  let actions = useActions('Combobox.Input')
+
   let internalId = useId()
   let providedId = useProvidedId()
   let {
     id = providedId || `headlessui-combobox-input-${internalId}`,
     onChange,
     displayValue,
+    disabled = data.disabled || false,
+    autoFocus = false,
     // @ts-ignore: We know this MAY NOT exist for a given tag but we only care when it _does_ exist.
     type = 'text',
     ...theirProps
   } = props
-  let data = useData('Combobox.Input')
-  let actions = useActions('Combobox.Input')
 
   let inputRef = useSyncRefs(data.inputRef, ref, useFloatingReference())
   let ownerDocument = useOwnerDocument(data.inputRef)
@@ -1320,20 +1322,18 @@ function InputFn<
   let labelledBy = useLabelledBy()
   let describedBy = useDescribedBy()
 
-  let { isFocused: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false })
-  let { isHovered: hover, hoverProps } = useHover({ isDisabled: data.disabled ?? false })
+  let { isFocused: focus, focusProps } = useFocusRing({ autoFocus })
+  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
 
-  let slot = useMemo(
-    () =>
-      ({
-        open: data.comboboxState === ComboboxState.Open,
-        disabled: data.disabled,
-        hover,
-        focus,
-        autofocus: props.autoFocus ?? false,
-      }) satisfies InputRenderPropArg,
-    [data, hover, focus, props.autoFocus]
-  )
+  let slot = useMemo(() => {
+    return {
+      open: data.comboboxState === ComboboxState.Open,
+      disabled,
+      hover,
+      focus,
+      autofocus: autoFocus,
+    } satisfies InputRenderPropArg
+  }, [data, hover, focus, autoFocus, disabled])
 
   let ourProps = mergeProps(
     {
@@ -1365,7 +1365,8 @@ function InputFn<
           ? displayValue?.(data.defaultValue as unknown as TType)
           : null) ??
         data.defaultValue,
-      disabled: data.disabled,
+      disabled: disabled || undefined,
+      autoFocus,
       onCompositionStart: handleCompositionStart,
       onCompositionEnd: handleCompositionEnd,
       onKeyDown: handleKeyDown,
@@ -1411,6 +1412,7 @@ export type ComboboxButtonProps<TTag extends ElementType = typeof DEFAULT_BUTTON
   ButtonPropsWeControl,
   {
     autoFocus?: boolean
+    disabled?: boolean
   }
 >
 
@@ -1422,7 +1424,12 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
   let actions = useActions('Combobox.Button')
   let buttonRef = useSyncRefs(data.buttonRef, ref)
   let internalId = useId()
-  let { id = `headlessui-combobox-button-${internalId}`, ...theirProps } = props
+  let {
+    id = `headlessui-combobox-button-${internalId}`,
+    disabled = data.disabled || false,
+    autoFocus = false,
+    ...theirProps
+  } = props
   let d = useDisposables()
 
   let handleKeyDown = useEvent((event: ReactKeyboardEvent<HTMLUListElement>) => {
@@ -1479,22 +1486,20 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
 
   let labelledBy = useLabelledBy([id])
 
-  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false })
-  let { isHovered: hover, hoverProps } = useHover({ isDisabled: data.disabled ?? false })
-  let { pressed: active, pressProps } = useActivePress({ disabled: data.disabled ?? false })
+  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus })
+  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
+  let { pressed: active, pressProps } = useActivePress({ disabled })
 
-  let slot = useMemo(
-    () =>
-      ({
-        open: data.comboboxState === ComboboxState.Open,
-        active: active || data.comboboxState === ComboboxState.Open,
-        disabled: data.disabled,
-        value: data.value,
-        hover,
-        focus,
-      }) satisfies ButtonRenderPropArg,
-    [data, hover, focus, active]
-  )
+  let slot = useMemo(() => {
+    return {
+      open: data.comboboxState === ComboboxState.Open,
+      active: active || data.comboboxState === ComboboxState.Open,
+      disabled,
+      value: data.value,
+      hover,
+      focus,
+    } satisfies ButtonRenderPropArg
+  }, [data, hover, focus, active, disabled])
   let ourProps = mergeProps(
     {
       ref: buttonRef,
@@ -1505,7 +1510,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
       'aria-controls': data.optionsRef.current?.id,
       'aria-expanded': data.comboboxState === ComboboxState.Open,
       'aria-labelledby': labelledBy,
-      disabled: data.disabled,
+      disabled: disabled || undefined,
+      autoFocus,
       onClick: handleClick,
       onKeyDown: handleKeyDown,
     },
@@ -1592,14 +1598,12 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
 
   let labelledBy = useLabelledBy([data.buttonRef.current?.id])
 
-  let slot = useMemo(
-    () =>
-      ({
-        open: data.comboboxState === ComboboxState.Open,
-        option: undefined,
-      }) satisfies OptionsRenderPropArg,
-    [data]
-  )
+  let slot = useMemo(() => {
+    return {
+      open: data.comboboxState === ComboboxState.Open,
+      option: undefined,
+    } satisfies OptionsRenderPropArg
+  }, [data])
   let ourProps = mergeProps(anchor ? getFloatingPanelProps() : {}, {
     'aria-labelledby': labelledBy,
     role: 'listbox',
diff --git a/packages/@headlessui-react/src/components/disclosure/disclosure.tsx b/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
index 70c01b1..4c6d01d 100644
--- a/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
+++ b/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
@@ -220,14 +220,12 @@ function DisclosureFn<TTag extends ElementType = typeof DEFAULT_DISCLOSURE_TAG>(
 
   let api = useMemo<ContextType<typeof DisclosureAPIContext>>(() => ({ close }), [close])
 
-  let slot = useMemo(
-    () =>
-      ({
-        open: disclosureState === DisclosureStates.Open,
-        close,
-      }) satisfies DisclosureRenderPropArg,
-    [disclosureState, close]
-  )
+  let slot = useMemo(() => {
+    return {
+      open: disclosureState === DisclosureStates.Open,
+      close,
+    } satisfies DisclosureRenderPropArg
+  }, [disclosureState, close])
 
   let ourProps = {
     ref: disclosureRef,
@@ -262,6 +260,7 @@ type ButtonRenderPropArg = {
   open: boolean
   hover: boolean
   active: boolean
+  disabled: boolean
   focus: boolean
   autofocus: boolean
 }
@@ -282,7 +281,12 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
   ref: Ref<HTMLButtonElement>
 ) {
   let internalId = useId()
-  let { id = `headlessui-disclosure-button-${internalId}`, ...theirProps } = props
+  let {
+    id = `headlessui-disclosure-button-${internalId}`,
+    disabled = false,
+    autoFocus = false,
+    ...theirProps
+  } = props
   let [state, dispatch] = useDisclosureContext('Disclosure.Button')
   let panelContext = useDisclosurePanelContext()
   let isWithinPanel = panelContext === null ? false : panelContext === state.panelId
@@ -338,7 +342,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
 
   let handleClick = useEvent((event: ReactMouseEvent) => {
     if (isDisabledReactIssue7711(event.currentTarget)) return
-    if (props.disabled) return
+    if (disabled) return
 
     if (isWithinPanel) {
       dispatch({ type: ActionTypes.ToggleDisclosure })
@@ -348,21 +352,20 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
     }
   })
 
-  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false })
-  let { isHovered: hover, hoverProps } = useHover({ isDisabled: props.disabled ?? false })
-  let { pressed: active, pressProps } = useActivePress({ disabled: props.disabled ?? false })
-
-  let slot = useMemo(
-    () =>
-      ({
-        open: state.disclosureState === DisclosureStates.Open,
-        hover,
-        active,
-        focus,
-        autofocus: props.autoFocus ?? false,
-      }) satisfies ButtonRenderPropArg,
-    [state, hover, active, focus, props.autoFocus]
-  )
+  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus })
+  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
+  let { pressed: active, pressProps } = useActivePress({ disabled })
+
+  let slot = useMemo(() => {
+    return {
+      open: state.disclosureState === DisclosureStates.Open,
+      hover,
+      active,
+      disabled,
+      focus,
+      autofocus: autoFocus,
+    } satisfies ButtonRenderPropArg
+  }, [state, hover, active, focus, disabled, autoFocus])
 
   let type = useResolveButtonType(props, internalButtonRef)
   let ourProps = isWithinPanel
@@ -370,6 +373,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
         {
           ref: buttonRef,
           type,
+          disabled: disabled || undefined,
+          autoFocus,
           onKeyDown: handleKeyDown,
           onClick: handleClick,
         },
@@ -384,6 +389,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
           type,
           'aria-expanded': state.disclosureState === DisclosureStates.Open,
           'aria-controls': state.linkedPanel ? state.panelId : undefined,
+          disabled: disabled || undefined,
+          autoFocus,
           onKeyDown: handleKeyDown,
           onKeyUp: handleKeyUp,
           onClick: handleClick,
@@ -451,14 +458,12 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
     return state.disclosureState === DisclosureStates.Open
   })()
 
-  let slot = useMemo(
-    () =>
-      ({
-        open: state.disclosureState === DisclosureStates.Open,
-        close,
-      }) satisfies PanelRenderPropArg,
-    [state, close]
-  )
+  let slot = useMemo(() => {
+    return {
+      open: state.disclosureState === DisclosureStates.Open,
+      close,
+    } satisfies PanelRenderPropArg
+  }, [state, close])
 
   let ourProps = {
     ref: panelRef,
diff --git a/packages/@headlessui-react/src/components/field/field.tsx b/packages/@headlessui-react/src/components/field/field.tsx
index d770c16..5225433 100644
--- a/packages/@headlessui-react/src/components/field/field.tsx
+++ b/packages/@headlessui-react/src/components/field/field.tsx
@@ -40,7 +40,7 @@ function FieldFn<TTag extends ElementType = typeof DEFAULT_FIELD_TAG>(
 
   let ourProps = {
     ref,
-    disabled,
+    disabled: disabled || undefined,
     'aria-disabled': disabled || undefined,
   }
 
diff --git a/packages/@headlessui-react/src/components/input/input.tsx b/packages/@headlessui-react/src/components/input/input.tsx
index 27b0d68..2eac0d9 100644
--- a/packages/@headlessui-react/src/components/input/input.tsx
+++ b/packages/@headlessui-react/src/components/input/input.tsx
@@ -49,6 +49,7 @@ function InputFn<TTag extends ElementType = typeof DEFAULT_INPUT_TAG>(
   let {
     id = providedId || `headlessui-input-${internalId}`,
     disabled = providedDisabled || false,
+    autoFocus = false,
     invalid = false,
     ...theirProps
   } = props
@@ -56,10 +57,8 @@ function InputFn<TTag extends ElementType = typeof DEFAULT_INPUT_TAG>(
   let labelledBy = useLabelledBy()
   let describedBy = useDescribedBy()
 
-  let { isFocused: focus, focusProps } = useFocusRing({
-    autoFocus: props.autoFocus ?? false,
-  })
-  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled ?? false })
+  let { isFocused: focus, focusProps } = useFocusRing({ autoFocus })
+  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
 
   let ourProps = mergeProps(
     {
@@ -69,22 +68,15 @@ function InputFn<TTag extends ElementType = typeof DEFAULT_INPUT_TAG>(
       'aria-describedby': describedBy,
       'aria-invalid': invalid ? '' : undefined,
       disabled: disabled || undefined,
+      autoFocus,
     },
     focusProps,
     hoverProps
   )
 
-  let slot = useMemo(
-    () =>
-      ({
-        disabled,
-        invalid,
-        hover,
-        focus,
-        autofocus: props.autoFocus ?? false,
-      }) satisfies InputRenderPropArg,
-    [disabled, invalid, hover, focus, props.autoFocus]
-  )
+  let slot = useMemo(() => {
+    return { disabled, invalid, hover, focus, autofocus: autoFocus } satisfies InputRenderPropArg
+  }, [disabled, invalid, hover, focus, autoFocus])
 
   return render({
     ourProps,
diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx
index 129dd74..d9d5ed6 100644
--- a/packages/@headlessui-react/src/components/listbox/listbox.tsx
+++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx
@@ -554,16 +554,14 @@ function ListboxFn<
     data.listboxState === ListboxStates.Open
   )
 
-  let slot = useMemo(
-    () =>
-      ({
-        open: data.listboxState === ListboxStates.Open,
-        disabled,
-        invalid,
-        value,
-      }) satisfies ListboxRenderPropArg<TType>,
-    [data, disabled, value, invalid]
-  )
+  let slot = useMemo(() => {
+    return {
+      open: data.listboxState === ListboxStates.Open,
+      disabled,
+      invalid,
+      value,
+    } satisfies ListboxRenderPropArg<TType>
+  }, [data, disabled, value, invalid])
 
   let selectOption = useEvent((id: string) => {
     let option = data.options.find((item) => item.id === id)
@@ -718,6 +716,7 @@ export type ListboxButtonProps<TTag extends ElementType = typeof DEFAULT_BUTTON_
   ButtonPropsWeControl,
   {
     autoFocus?: boolean
+    disabled?: boolean
   }
 >
 
@@ -725,11 +724,17 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
   props: ListboxButtonProps<TTag>,
   ref: Ref<HTMLButtonElement>
 ) {
-  let internalId = useId()
-  let providedId = useProvidedId()
-  let { id = providedId || `headlessui-listbox-button-${internalId}`, ...theirProps } = props
   let data = useData('Listbox.Button')
   let actions = useActions('Listbox.Button')
+
+  let internalId = useId()
+  let providedId = useProvidedId()
+  let {
+    id = providedId || `headlessui-listbox-button-${internalId}`,
+    disabled = data.disabled || false,
+    autoFocus = false,
+    ...theirProps
+  } = props
   let buttonRef = useSyncRefs(data.buttonRef, ref, useFloatingReference())
   let getFloatingReferenceProps = useFloatingReferenceProps()
 
@@ -790,33 +795,22 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
   let labelledBy = useLabelledBy([id])
   let describedBy = useDescribedBy()
 
-  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false })
-  let { isHovered: hover, hoverProps } = useHover({ isDisabled: data.disabled ?? false })
-  let { pressed: active, pressProps } = useActivePress({ disabled: data.disabled ?? false })
+  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus })
+  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
+  let { pressed: active, pressProps } = useActivePress({ disabled })
 
-  let slot = useMemo(
-    () =>
-      ({
-        open: data.listboxState === ListboxStates.Open,
-        active: active || data.listboxState === ListboxStates.Open,
-        disabled: data.disabled,
-        invalid: data.invalid,
-        value: data.value,
-        hover,
-        focus,
-        autofocus: props.autoFocus ?? false,
-      }) satisfies ButtonRenderPropArg,
-    [
-      data.listboxState,
-      data.disabled,
-      data.value,
+  let slot = useMemo(() => {
+    return {
+      open: data.listboxState === ListboxStates.Open,
+      active: active || data.listboxState === ListboxStates.Open,
+      disabled,
+      invalid: data.invalid,
+      value: data.value,
       hover,
       focus,
-      active,
-      data.invalid,
-      props.autoFocus,
-    ]
-  )
+      autofocus: autoFocus,
+    } satisfies ButtonRenderPropArg
+  }, [data.listboxState, data.value, disabled, hover, focus, active, data.invalid, autoFocus])
 
   let ourProps = mergeProps(
     getFloatingReferenceProps(),
@@ -829,7 +823,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
       'aria-expanded': data.listboxState === ListboxStates.Open,
       'aria-labelledby': labelledBy,
       'aria-describedby': describedBy,
-      disabled: data.disabled,
+      disabled: disabled || undefined,
+      autoFocus,
       onKeyDown: handleKeyDown,
       onKeyUp: handleKeyUp,
       onKeyPress: handleKeyPress,
@@ -1225,17 +1220,15 @@ function OptionFn<
     actions.goToOption(Focus.Nothing)
   })
 
-  let slot = useMemo(
-    () =>
-      ({
-        active,
-        focus: active,
-        selected,
-        disabled,
-        selectedOption: selected && usedInSelectedOption,
-      }) satisfies OptionRenderPropArg,
-    [active, selected, disabled, usedInSelectedOption]
-  )
+  let slot = useMemo(() => {
+    return {
+      active,
+      focus: active,
+      selected,
+      disabled,
+      selectedOption: selected && usedInSelectedOption,
+    } satisfies OptionRenderPropArg
+  }, [active, selected, disabled, usedInSelectedOption])
   let ourProps = !usedInSelectedOption
     ? {
         id,
diff --git a/packages/@headlessui-react/src/components/menu/menu.tsx b/packages/@headlessui-react/src/components/menu/menu.tsx
index 359e674..9cf528e 100644
--- a/packages/@headlessui-react/src/components/menu/menu.tsx
+++ b/packages/@headlessui-react/src/components/menu/menu.tsx
@@ -436,6 +436,7 @@ type ButtonRenderPropArg = {
   active: boolean
   hover: boolean
   focus: boolean
+  disabled: boolean
   autofocus: boolean
 }
 type ButtonPropsWeControl = 'aria-controls' | 'aria-expanded' | 'aria-haspopup'
@@ -455,7 +456,12 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
   ref: Ref<HTMLButtonElement>
 ) {
   let internalId = useId()
-  let { id = `headlessui-menu-button-${internalId}`, ...theirProps } = props
+  let {
+    id = `headlessui-menu-button-${internalId}`,
+    disabled = false,
+    autoFocus = false,
+    ...theirProps
+  } = props
   let [state, dispatch] = useMenuContext('Menu.Button')
   let getFloatingReferenceProps = useFloatingReferenceProps()
   let buttonRef = useSyncRefs(state.buttonRef, ref, useFloatingReference())
@@ -497,7 +503,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
 
   let handleClick = useEvent((event: ReactMouseEvent) => {
     if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
-    if (props.disabled) return
+    if (disabled) return
     if (state.menuState === MenuStates.Open) {
       dispatch({ type: ActionTypes.CloseMenu })
       d.nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
@@ -507,21 +513,20 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
     }
   })
 
-  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false })
-  let { isHovered: hover, hoverProps } = useHover({ isDisabled: props.disabled ?? false })
-  let { pressed: active, pressProps } = useActivePress({ disabled: props.disabled ?? false })
+  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus })
+  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
+  let { pressed: active, pressProps } = useActivePress({ disabled })
 
-  let slot = useMemo(
-    () =>
-      ({
-        open: state.menuState === MenuStates.Open,
-        active: active || state.menuState === MenuStates.Open,
-        hover,
-        focus,
-        autofocus: props.autoFocus ?? false,
-      }) satisfies ButtonRenderPropArg,
-    [state, hover, focus, active, props.autoFocus]
-  )
+  let slot = useMemo(() => {
+    return {
+      open: state.menuState === MenuStates.Open,
+      active: active || state.menuState === MenuStates.Open,
+      disabled,
+      hover,
+      focus,
+      autofocus: autoFocus,
+    } satisfies ButtonRenderPropArg
+  }, [state, hover, focus, active, disabled, autoFocus])
 
   let ourProps = mergeProps(
     getFloatingReferenceProps(),
@@ -532,6 +537,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
       'aria-haspopup': 'menu',
       'aria-controls': state.itemsRef.current?.id,
       'aria-expanded': state.menuState === MenuStates.Open,
+      disabled: disabled || undefined,
+      autoFocus,
       onKeyDown: handleKeyDown,
       onKeyUp: handleKeyUp,
       onClick: handleClick,
diff --git a/packages/@headlessui-react/src/components/popover/popover.tsx b/packages/@headlessui-react/src/components/popover/popover.tsx
index 6a26b21..9ff7687 100644
--- a/packages/@headlessui-react/src/components/popover/popover.tsx
+++ b/packages/@headlessui-react/src/components/popover/popover.tsx
@@ -440,6 +440,7 @@ type ButtonRenderPropArg = {
   active: boolean
   hover: boolean
   focus: boolean
+  disabled: boolean
   autofocus: boolean
 }
 type ButtonPropsWeControl = 'aria-controls' | 'aria-expanded'
@@ -459,7 +460,12 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
   ref: Ref<HTMLButtonElement>
 ) {
   let internalId = useId()
-  let { id = `headlessui-popover-button-${internalId}`, ...theirProps } = props
+  let {
+    id = `headlessui-popover-button-${internalId}`,
+    disabled = false,
+    autoFocus = false,
+    ...theirProps
+  } = props
   let [state, dispatch] = usePopoverContext('Popover.Button')
   let { isPortalled } = usePopoverAPIContext('Popover.Button')
   let internalButtonRef = useRef<HTMLButtonElement | null>(null)
@@ -584,7 +590,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
 
   let handleClick = useEvent((event: ReactMouseEvent) => {
     if (isDisabledReactIssue7711(event.currentTarget)) return
-    if (props.disabled) return
+    if (disabled) return
     if (isWithinPanel) {
       dispatch({ type: ActionTypes.ClosePopover })
       state.button?.focus() // Re-focus the original opening Button
@@ -602,22 +608,21 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
     event.stopPropagation()
   })
 
-  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false })
-  let { isHovered: hover, hoverProps } = useHover({ isDisabled: props.disabled ?? false })
-  let { pressed: active, pressProps } = useActivePress({ disabled: props.disabled ?? false })
+  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus })
+  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
+  let { pressed: active, pressProps } = useActivePress({ disabled })
 
   let visible = state.popoverState === PopoverStates.Open
-  let slot = useMemo(
-    () =>
-      ({
-        open: visible,
-        active: active || visible,
-        hover,
-        focus,
-        autofocus: props.autoFocus ?? false,
-      }) satisfies ButtonRenderPropArg,
-    [visible, hover, focus, active, props.autoFocus]
-  )
+  let slot = useMemo(() => {
+    return {
+      open: visible,
+      active: active || visible,
+      disabled,
+      hover,
+      focus,
+      autofocus: autoFocus,
+    } satisfies ButtonRenderPropArg
+  }, [visible, hover, focus, active, disabled, autoFocus])
 
   let type = useResolveButtonType(props, internalButtonRef)
   let ourProps = isWithinPanel
@@ -627,6 +632,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
           type,
           onKeyDown: handleKeyDown,
           onClick: handleClick,
+          disabled: disabled || undefined,
+          autoFocus,
         },
         focusProps,
         hoverProps,
@@ -639,6 +646,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
           type,
           'aria-expanded': state.popoverState === PopoverStates.Open,
           'aria-controls': state.panel ? state.panelId : undefined,
+          disabled: disabled || undefined,
+          autoFocus,
           onKeyDown: handleKeyDown,
           onKeyUp: handleKeyUp,
           onClick: handleClick,
diff --git a/packages/@headlessui-react/src/components/radio-group/radio-group.tsx b/packages/@headlessui-react/src/components/radio-group/radio-group.tsx
index 2fabaa2..c3c46ce 100644
--- a/packages/@headlessui-react/src/components/radio-group/radio-group.tsx
+++ b/packages/@headlessui-react/src/components/radio-group/radio-group.tsx
@@ -371,13 +371,18 @@ function OptionFn<
   // But today is not that day..
   TType = Parameters<typeof RadioGroupRoot>[0]['value'],
 >(props: RadioOptionProps<TTag, TType>, ref: Ref<HTMLElement>) {
+  let data = useData('RadioGroup.Option')
+  let actions = useActions('RadioGroup.Option')
+
   let internalId = useId()
   let {
     id = `headlessui-radiogroup-option-${internalId}`,
     value,
-    disabled = false,
+    disabled = data.disabled || false,
+    autoFocus = false,
     ...theirProps
   } = props
+
   let internalOptionRef = useRef<HTMLElement | null>(null)
   let optionRef = useSyncRefs(internalOptionRef, ref)
 
@@ -386,9 +391,6 @@ function OptionFn<
 
   let propsRef = useLatestValue({ value, disabled })
 
-  let data = useData('RadioGroup.Option')
-  let actions = useActions('RadioGroup.Option')
-
   useIsoMorphicEffect(
     () => actions.registerOption({ id, element: internalOptionRef, propsRef }),
     [id, actions, internalOptionRef, propsRef]
@@ -401,10 +403,9 @@ function OptionFn<
   })
 
   let isFirstOption = data.firstOption?.id === id
-  let isDisabled = data.disabled || disabled
 
-  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false })
-  let { isHovered: hover, hoverProps } = useHover({ isDisabled: isDisabled ?? false })
+  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus })
+  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
 
   let checked = data.compare(data.value as TType, value)
   let ourProps = mergeProps(
@@ -415,31 +416,30 @@ function OptionFn<
       'aria-checked': checked ? 'true' : 'false',
       'aria-labelledby': labelledby,
       'aria-describedby': describedby,
-      'aria-disabled': isDisabled ? true : undefined,
+      'aria-disabled': disabled ? true : undefined,
       tabIndex: (() => {
-        if (isDisabled) return -1
+        if (disabled) return -1
         if (checked) return 0
         if (!data.containsCheckedOption && isFirstOption) return 0
         return -1
       })(),
-      onClick: isDisabled ? undefined : handleClick,
+      onClick: disabled ? undefined : handleClick,
+      autoFocus,
     },
     focusProps,
     hoverProps
   )
 
-  let slot = useMemo(
-    () =>
-      ({
-        checked,
-        disabled: isDisabled,
-        active: focus,
-        hover,
-        focus,
-        autofocus: props.autoFocus ?? false,
-      }) satisfies OptionRenderPropArg,
-    [checked, isDisabled, hover, focus, props.autoFocus]
-  )
+  let slot = useMemo(() => {
+    return {
+      checked,
+      disabled,
+      active: focus,
+      hover,
+      focus,
+      autofocus: autoFocus,
+    } satisfies OptionRenderPropArg
+  }, [checked, disabled, hover, focus, autoFocus])
 
   return (
     <DescriptionProvider name="RadioGroup.Description">
@@ -500,6 +500,7 @@ function RadioFn<
     id = providedId || `headlessui-radio-${internalId}`,
     value,
     disabled = data.disabled || providedDisabled || false,
+    autoFocus = false,
     ...theirProps
   } = props
   let internalRadioRef = useRef<HTMLElement | null>(null)
@@ -522,8 +523,8 @@ function RadioFn<
     internalRadioRef.current?.focus()
   })
 
-  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false })
-  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled ?? false })
+  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus })
+  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
 
   let isFirstOption = data.firstOption?.id === id
 
@@ -543,22 +544,15 @@ function RadioFn<
         if (!data.containsCheckedOption && isFirstOption) return 0
         return -1
       })(),
+      autoFocus,
       onClick: disabled ? undefined : handleClick,
     },
     focusProps,
     hoverProps
   )
-  let slot = useMemo(
-    () =>
-      ({
-        checked,
-        disabled,
-        hover,
-        focus,
-        autofocus: props.autoFocus ?? false,
-      }) satisfies RadioRenderPropArg,
-    [checked, disabled, hover, focus, props.autoFocus]
-  )
+  let slot = useMemo(() => {
+    return { checked, disabled, hover, focus, autofocus: autoFocus } satisfies RadioRenderPropArg
+  }, [checked, disabled, hover, focus, autoFocus])
 
   return render({
     ourProps,
diff --git a/packages/@headlessui-react/src/components/select/select.tsx b/packages/@headlessui-react/src/components/select/select.tsx
index 3b83496..c3823b3 100644
--- a/packages/@headlessui-react/src/components/select/select.tsx
+++ b/packages/@headlessui-react/src/components/select/select.tsx
@@ -52,15 +52,16 @@ function SelectFn<TTag extends ElementType = typeof DEFAULT_SELECT_TAG>(
     id = providedId || `headlessui-select-${internalId}`,
     disabled = providedDisabled || false,
     invalid = false,
+    autoFocus = false,
     ...theirProps
   } = props
 
   let labelledBy = useLabelledBy()
   let describedBy = useDescribedBy()
 
-  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false })
-  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled ?? false })
-  let { pressed: active, pressProps } = useActivePress({ disabled: disabled ?? false })
+  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus })
+  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
+  let { pressed: active, pressProps } = useActivePress({ disabled })
 
   let ourProps = mergeProps(
     {
@@ -70,24 +71,23 @@ function SelectFn<TTag extends ElementType = typeof DEFAULT_SELECT_TAG>(
       'aria-describedby': describedBy,
       'aria-invalid': invalid ? '' : undefined,
       disabled: disabled || undefined,
+      autoFocus,
     },
     focusProps,
     hoverProps,
     pressProps
   )
 
-  let slot = useMemo(
-    () =>
-      ({
-        disabled,
-        invalid,
-        hover,
-        focus,
-        active,
-        autofocus: props.autoFocus ?? false,
-      }) satisfies SelectRenderPropArg,
-    [disabled, invalid, hover, focus, active, props.autoFocus]
-  )
+  let slot = useMemo(() => {
+    return {
+      disabled,
+      invalid,
+      hover,
+      focus,
+      active,
+      autofocus: autoFocus,
+    } satisfies SelectRenderPropArg
+  }, [disabled, invalid, hover, focus, active, autoFocus])
 
   return render({
     ourProps,
diff --git a/packages/@headlessui-react/src/components/switch/switch.tsx b/packages/@headlessui-react/src/components/switch/switch.tsx
index eedf7a9..78bf9a3 100644
--- a/packages/@headlessui-react/src/components/switch/switch.tsx
+++ b/packages/@headlessui-react/src/components/switch/switch.tsx
@@ -151,6 +151,7 @@ function SwitchFn<TTag extends ElementType = typeof DEFAULT_SWITCH_TAG>(
     name,
     value,
     form,
+    autoFocus = false,
     ...theirProps
   } = props
   let groupContext = useContext(GroupContext)
@@ -192,23 +193,22 @@ function SwitchFn<TTag extends ElementType = typeof DEFAULT_SWITCH_TAG>(
 
   let labelledBy = useLabelledBy()
   let describedBy = useDescribedBy()
-  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false })
-  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled ?? false })
-  let { pressed: active, pressProps } = useActivePress({ disabled: disabled ?? false })
 
-  let slot = useMemo(
-    () =>
-      ({
-        checked,
-        disabled,
-        hover,
-        focus,
-        active,
-        autofocus: props.autoFocus ?? false,
-        changing,
-      }) satisfies SwitchRenderPropArg,
-    [checked, hover, focus, active, disabled, changing, props.autoFocus]
-  )
+  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus })
+  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
+  let { pressed: active, pressProps } = useActivePress({ disabled })
+
+  let slot = useMemo(() => {
+    return {
+      checked,
+      disabled,
+      hover,
+      focus,
+      active,
+      autofocus: autoFocus,
+      changing,
+    } satisfies SwitchRenderPropArg
+  }, [checked, hover, focus, active, disabled, changing, autoFocus])
 
   let ourProps = mergeProps(
     {
@@ -220,7 +220,8 @@ function SwitchFn<TTag extends ElementType = typeof DEFAULT_SWITCH_TAG>(
       'aria-checked': checked,
       'aria-labelledby': labelledBy,
       'aria-describedby': describedBy,
-      disabled,
+      disabled: disabled || undefined,
+      autoFocus,
       onClick: handleClick,
       onKeyUp: handleKeyUp,
       onKeyPress: handleKeyPress,
diff --git a/packages/@headlessui-react/src/components/tabs/tabs.tsx b/packages/@headlessui-react/src/components/tabs/tabs.tsx
index f259050..e98e813 100644
--- a/packages/@headlessui-react/src/components/tabs/tabs.tsx
+++ b/packages/@headlessui-react/src/components/tabs/tabs.tsx
@@ -421,7 +421,12 @@ function TabFn<TTag extends ElementType = typeof DEFAULT_TAB_TAG>(
   ref: Ref<HTMLElement>
 ) {
   let internalId = useId()
-  let { id = `headlessui-tabs-tab-${internalId}`, ...theirProps } = props
+  let {
+    id = `headlessui-tabs-tab-${internalId}`,
+    disabled = false,
+    autoFocus = false,
+    ...theirProps
+  } = props
 
   let { orientation, activation, selectedIndex, tabs, panels } = useData('Tab')
   let actions = useActions('Tab')
@@ -515,22 +520,20 @@ function TabFn<TTag extends ElementType = typeof DEFAULT_TAB_TAG>(
     event.preventDefault()
   })
 
-  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false })
-  let { isHovered: hover, hoverProps } = useHover({ isDisabled: props.disabled ?? false })
-  let { pressed: active, pressProps } = useActivePress({ disabled: props.disabled ?? false })
+  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus })
+  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
+  let { pressed: active, pressProps } = useActivePress({ disabled })
 
-  let slot = useMemo(
-    () =>
-      ({
-        selected,
-        hover,
-        active,
-        focus,
-        autofocus: props.autoFocus ?? false,
-        disabled: props.disabled ?? false,
-      }) satisfies TabRenderPropArg,
-    [selected, hover, focus, active, props.autoFocus, props.disabled]
-  )
+  let slot = useMemo(() => {
+    return {
+      selected,
+      hover,
+      active,
+      focus,
+      autofocus: autoFocus,
+      disabled,
+    } satisfies TabRenderPropArg
+  }, [selected, hover, focus, active, autoFocus, disabled])
 
   let ourProps = mergeProps(
     {
@@ -544,6 +547,8 @@ function TabFn<TTag extends ElementType = typeof DEFAULT_TAB_TAG>(
       'aria-controls': panels[myIndex]?.current?.id,
       'aria-selected': selected,
       tabIndex: selected ? 0 : -1,
+      disabled: disabled || undefined,
+      autoFocus,
     },
     focusProps,
     hoverProps,
@@ -578,7 +583,7 @@ function PanelsFn<TTag extends ElementType = typeof DEFAULT_PANELS_TAG>(
   let { selectedIndex } = useData('Tab.Panels')
   let panelsRef = useSyncRefs(ref)
 
-  let slot = useMemo(() => ({ selectedIndex }), [selectedIndex])
+  let slot = useMemo(() => ({ selectedIndex }) satisfies PanelsRenderPropArg, [selectedIndex])
 
   let theirProps = props
   let ourProps = { ref: panelsRef }
@@ -597,6 +602,7 @@ function PanelsFn<TTag extends ElementType = typeof DEFAULT_PANELS_TAG>(
 let DEFAULT_PANEL_TAG = 'div' as const
 type PanelRenderPropArg = {
   selected: boolean
+  focus: boolean
 }
 type PanelPropsWeControl = 'role' | 'aria-labelledby'
 let PanelRenderFeatures = RenderFeatures.RenderStrategy | RenderFeatures.Static
@@ -629,15 +635,19 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
 
   let selected = myIndex === selectedIndex
 
-  let slot = useMemo(() => ({ selected }), [selected])
+  let { isFocusVisible: focus, focusProps } = useFocusRing()
+  let slot = useMemo(() => ({ selected, focus }) satisfies PanelRenderPropArg, [selected, focus])
 
-  let ourProps = {
-    ref: panelRef,
-    id,
-    role: 'tabpanel',
-    'aria-labelledby': tabs[myIndex]?.current?.id,
-    tabIndex: selected ? tabIndex : -1,
-  }
+  let ourProps = mergeProps(
+    {
+      ref: panelRef,
+      id,
+      role: 'tabpanel',
+      'aria-labelledby': tabs[myIndex]?.current?.id,
+      tabIndex: selected ? tabIndex : -1,
+    },
+    focusProps
+  )
 
   if (!selected && (theirProps.unmount ?? true) && !(theirProps.static ?? false)) {
     return <Hidden as="span" aria-hidden="true" {...ourProps} />
diff --git a/packages/@headlessui-react/src/components/textarea/textarea.tsx b/packages/@headlessui-react/src/components/textarea/textarea.tsx
index 9091f8d..f1fcf9d 100644
--- a/packages/@headlessui-react/src/components/textarea/textarea.tsx
+++ b/packages/@headlessui-react/src/components/textarea/textarea.tsx
@@ -49,6 +49,7 @@ function TextareaFn<TTag extends ElementType = typeof DEFAULT_TEXTAREA_TAG>(
   let {
     id = providedId || `headlessui-textarea-${internalId}`,
     disabled = providedDisabled || false,
+    autoFocus = false,
     invalid = false,
     ...theirProps
   } = props
@@ -56,8 +57,8 @@ function TextareaFn<TTag extends ElementType = typeof DEFAULT_TEXTAREA_TAG>(
   let labelledBy = useLabelledBy()
   let describedBy = useDescribedBy()
 
-  let { isFocused: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false })
-  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled ?? false })
+  let { isFocused: focus, focusProps } = useFocusRing({ autoFocus })
+  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
 
   let ourProps = mergeProps(
     {
@@ -67,22 +68,15 @@ function TextareaFn<TTag extends ElementType = typeof DEFAULT_TEXTAREA_TAG>(
       'aria-describedby': describedBy,
       'aria-invalid': invalid ? '' : undefined,
       disabled: disabled || undefined,
+      autoFocus,
     },
     focusProps,
     hoverProps
   )
 
-  let slot = useMemo(
-    () =>
-      ({
-        disabled,
-        invalid,
-        hover,
-        focus,
-        autofocus: props.autoFocus ?? false,
-      }) satisfies TextareaRenderPropArg,
-    [disabled, invalid, hover, focus, props.autoFocus]
-  )
+  let slot = useMemo(() => {
+    return { disabled, invalid, hover, focus, autofocus: autoFocus } satisfies TextareaRenderPropArg
+  }, [disabled, invalid, hover, focus, autoFocus])
 
   return render({
     ourProps,
diff --git a/packages/@headlessui-react/src/components/tooltip/tooltip.tsx b/packages/@headlessui-react/src/components/tooltip/tooltip.tsx
index 577cf3c..99f57f7 100644
--- a/packages/@headlessui-react/src/components/tooltip/tooltip.tsx
+++ b/packages/@headlessui-react/src/components/tooltip/tooltip.tsx
@@ -326,15 +326,15 @@ function TriggerFn<TTag extends ElementType = typeof DEFAULT_TRIGGER_TAG>(
   props: TooltipTriggerProps<TTag>,
   ref: Ref<HTMLElement>
 ) {
-  let { ...theirProps } = props
+  let { disabled = false, autoFocus = false, ...theirProps } = props
   let data = useData('TooltipTrigger')
   let actions = useActions('TooltipTrigger')
   let describedBy = useDescribedBy()
   let internalButtonRef = useRef<HTMLElement | null>(null)
   let triggerRef = useSyncRefs(internalButtonRef, ref, useFloatingReference())
 
-  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false })
-  let { isHovered: hover, hoverProps } = useHover({ isDisabled: props.disabled ?? false })
+  let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus })
+  let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
 
   let handleKeyDown = useEvent((event: ReactKeyboardEvent<HTMLButtonElement>) => {
     switch (event.key) {
@@ -375,8 +375,8 @@ function TriggerFn<TTag extends ElementType = typeof DEFAULT_TRIGGER_TAG>(
   })
 
   let slot = useMemo(
-    () => ({ hover, focus, autofocus: props.autoFocus ?? false }) satisfies TriggerRenderPropArg,
-    [hover, focus, props.autoFocus]
+    () => ({ hover, focus, autofocus: autoFocus }) satisfies TriggerRenderPropArg,
+    [hover, focus, autoFocus]
   )
   let ourProps = mergeProps(
     {


Test patch:
diff --git a/packages/@headlessui-react/src/components/disclosure/disclosure.test.tsx b/packages/@headlessui-react/src/components/disclosure/disclosure.test.tsx
index 349c06e..2f99355 100644
--- a/packages/@headlessui-react/src/components/disclosure/disclosure.test.tsx
+++ b/packages/@headlessui-react/src/components/disclosure/disclosure.test.tsx
@@ -268,6 +268,7 @@ describe('Rendering', () => {
             open: false,
             hover: false,
             active: false,
+            disabled: false,
             focus: false,
             autofocus: false,
           }),
@@ -283,6 +284,7 @@ describe('Rendering', () => {
             open: true,
             hover: false,
             active: false,
+            disabled: false,
             focus: false,
             autofocus: false,
           }),
@@ -310,6 +312,7 @@ describe('Rendering', () => {
             open: false,
             hover: false,
             active: false,
+            disabled: false,
             focus: false,
             autofocus: false,
           }),
@@ -325,6 +328,7 @@ describe('Rendering', () => {
             open: true,
             hover: false,
             active: false,
+            disabled: false,
             focus: false,
             autofocus: false,
           }),
diff --git a/packages/@headlessui-react/src/components/menu/menu.test.tsx b/packages/@headlessui-react/src/components/menu/menu.test.tsx
index 2a26ef1..3646ee4 100644
--- a/packages/@headlessui-react/src/components/menu/menu.test.tsx
+++ b/packages/@headlessui-react/src/components/menu/menu.test.tsx
@@ -179,6 +179,7 @@ describe('Rendering', () => {
           textContent: JSON.stringify({
             open: false,
             active: false,
+            disabled: false,
             hover: false,
             focus: false,
             autofocus: false,
@@ -194,6 +195,7 @@ describe('Rendering', () => {
           textContent: JSON.stringify({
             open: true,
             active: true,
+            disabled: false,
             hover: false,
             focus: false,
             autofocus: false,
@@ -225,6 +227,7 @@ describe('Rendering', () => {
           textContent: JSON.stringify({
             open: false,
             active: false,
+            disabled: false,
             hover: false,
             focus: false,
             autofocus: false,
@@ -240,6 +243,7 @@ describe('Rendering', () => {
           textContent: JSON.stringify({
             open: true,
             active: true,
+            disabled: false,
             hover: false,
             focus: false,
             autofocus: false,
@@ -248,6 +252,7 @@ describe('Rendering', () => {
         assertMenu({ state: MenuState.Visible })
       })
     )
+
     describe('`type` attribute', () => {
       it('should set the `type` to "button" by default', async () => {
         render(
diff --git a/packages/@headlessui-react/src/components/popover/popover.test.tsx b/packages/@headlessui-react/src/components/popover/popover.test.tsx
index cdcc775..405778a 100644
--- a/packages/@headlessui-react/src/components/popover/popover.test.tsx
+++ b/packages/@headlessui-react/src/components/popover/popover.test.tsx
@@ -387,6 +387,7 @@ describe('Rendering', () => {
           textContent: JSON.stringify({
             open: false,
             active: false,
+            disabled: false,
             hover: false,
             focus: false,
             autofocus: false,
@@ -402,6 +403,7 @@ describe('Rendering', () => {
           textContent: JSON.stringify({
             open: true,
             active: true,
+            disabled: false,
             hover: false,
             focus: false,
             autofocus: false,
@@ -429,6 +431,7 @@ describe('Rendering', () => {
           textContent: JSON.stringify({
             open: false,
             active: false,
+            disabled: false,
             hover: false,
             focus: false,
             autofocus: false,
@@ -444,6 +447,7 @@ describe('Rendering', () => {
           textContent: JSON.stringify({
             open: true,
             active: true,
+            disabled: false,
             hover: false,
             focus: false,
             autofocus: false,
diff --git a/packages/@headlessui-react/src/components/tabs/tabs.test.tsx b/packages/@headlessui-react/src/components/tabs/tabs.test.tsx
index 659140e..e2da039 100644
--- a/packages/@headlessui-react/src/components/tabs/tabs.test.tsx
+++ b/packages/@headlessui-react/src/components/tabs/tabs.test.tsx
@@ -691,25 +691,25 @@ describe('Rendering', () => {
         )
 
         expect(document.querySelector('[data-panel="0"]')).toHaveTextContent(
-          JSON.stringify({ selected: true })
+          JSON.stringify({ selected: true, focus: false })
         )
         expect(document.querySelector('[data-panel="1"]')).toHaveTextContent(
-          JSON.stringify({ selected: false })
+          JSON.stringify({ selected: false, focus: false })
         )
         expect(document.querySelector('[data-panel="2"]')).toHaveTextContent(
-          JSON.stringify({ selected: false })
+          JSON.stringify({ selected: false, focus: false })
         )
 
         await click(getByText('Tab 2'))
 
         expect(document.querySelector('[data-panel="0"]')).toHaveTextContent(
-          JSON.stringify({ selected: false })
+          JSON.stringify({ selected: false, focus: false })
         )
         expect(document.querySelector('[data-panel="1"]')).toHaveTextContent(
-          JSON.stringify({ selected: true })
+          JSON.stringify({ selected: true, focus: false })
         )
         expect(document.querySelector('[data-panel="2"]')).toHaveTextContent(
-          JSON.stringify({ selected: false })
+          JSON.stringify({ selected: false, focus: false })
         )
       })
     )