[tooltip] Vue 3: ancestor component attributes leak onto tooltip directive's rendered element

Summary

When v-gl-tooltip is used as a directive on an element inside a component, the tooltip's rendered <div> unexpectedly receives attributes from ancestor components that have nothing to do with the tooltip. This happens because templateAttributes() in bv-tooltip-template.js spreads this.bvParent.bvParent.$attrs — and on Vue 3, attributes fall through component hierarchies when components do not declare those attributes as props.

Steps to reproduce / Evidence

In gitlab-org/gitlab (a gitlab-ui consumer):

  1. work_item_labels.vue passes data-testid="work-item-labels" to <work-item-sidebar-dropdown-widget>.
  2. That component renders only <work-item-sidebar-widget> as its root child, so the data-testid falls through into WorkItemSidebarWidget's $attrs.
  3. WorkItemSidebarWidget contains a GlButton with v-gl-tooltip="Change label...".
  4. The tooltip's rendered <div> receives data-testid="work-item-labels" from the fallthrough.
  5. A test using within('[data-testid="work-item-labels"]') unexpectedly matched the tooltip's <div> (which bootstrap-vue renders to <body>), causing test assertion failures.

This triggered a real CI failure: https://gitlab.com/gitlab-org/gitlab/-/jobs/16018296488 (linked from gitlab-org/gitlab!250548 (comment 3722820268)).

A workaround was applied in gitlab-org/gitlab at spec/features/work_items/issues/issue_sidebar_spec.rb (around line 160): tests now qualify the selector as within('section[data-testid="work-item-labels"]') to avoid matching the tooltip's <div>.

Why this happens

The templateAttributes() getter in packages/gitlab-ui/src/vendor/bootstrap-vue/src/components/tooltip/helpers/bv-tooltip-template.js (lines 69-83) spreads this.bvParent.bvParent.$attrs into the tooltip's rendered attributes.

When v-gl-tooltip is used as a directive, bvParent resolves (via getInstanceFromDirective in packages/gitlab-ui/src/vendor/bootstrap-vue/src/directives/tooltip/tooltip.js) to the Vue component instance that owns the directive's host element — not to a <gl-tooltip> component instance.

On Vue 3, when a component receives an attribute it does not declare as a prop, that attribute enters $attrs. If that component's template renders a single root child component, Vue 3 automatically forwards those attributes ("fallthrough") into the child's $attrs, cascading through single-root component chains. This is documented at https://vuejs.org/guide/components/attrs.html#nested-component-inheritance.

Result: the tooltip directive reads $attrs from an arbitrary ancestor's Vue instance (whichever component instance owns the element carrying the directive), inheriting attributes that were meant for completely different elements.

Impact and risk

Scope: This is not specific to work items. Any component using v-gl-tooltip as a directive, where that component receives undeclared attributes from further-up parents (extremely common — testids, aria labels, data attributes passed through wrapper components), is affected on Vue 3. Every gitlab-ui consumer migrating to Vue 3 will encounter this.

Confirmed impact today: In gitlab-org/gitlab, the only attribute observed leaking so far is data-testid. This only affects tests, not real users, and both known cases have workarounds (see above).

Unconfirmed but likely impact today: The leak is not limited to data-testid. Any data-* attribute could leak the same way, and could change application behavior if that data attribute is read by other code (for example, JavaScript that queries elements by a data-* attribute, or CSS that styles by attribute selector). Any aria-* attribute could also leak, causing accessibility bugs (assistive technology could announce the wrong label or role for a tooltip). Because these leaks only show up when a page is rendered and inspected closely, and are not test-only like data-testid, they could exist elsewhere in gitlab-org/gitlab or in other gitlab-ui consumers without anyone noticing yet.

Future impact: This will worsen when consumers drop Vue 2 compat mode. Currently, gitlab-org/gitlab sets INSTANCE_ATTRS_CLASS_STYLE: 'suppress-warning' in its Vue 3 compat config, which keeps class and style out of $attrs. Once compat mode is removed, class and style also become part of $attrs (see https://v3-migration.vuejs.org/breaking-changes/attrs-includes-class-style.html). At that point, ancestor class and style values will also leak onto tooltip elements, which will very likely produce visible, user-facing bugs (wrong styling or layout on tooltips), not just test or accessibility issues.

The bvParent.bvParent.$attrs spread logic was written for component usage, where bvParent.bvParent predictably refers to the wrapping tooltip component. When used as a directive, bvParent resolves to whatever component owns the directive's host element, making it an unpredictable and uncontrolled source of attributes to copy.

A fix likely needs to reconsider what bvParent.bvParent.$attrs should mean for the directive usage pattern, and whether those attributes should be copied to the tooltip's rendered element at all in that case.


Raised as a non-blocking review comment by @kivikakk on an approved merge request: gitlab-org/gitlab!250548 (comment 3722820268)

Observed with @gitlab/ui 136.3.1.