feat(design-system): make DsPopover and DsTooltip controllable and composable [AR-75956] - #744
Merged
iromanchuk-dn merged 11 commits intoOct 1, 2026
Conversation
…mposable [AR-75956] - DsTooltip: controlled open / defaultOpen / onOpenChange; forward ref and asChild-injected props; share the trigger id with an outer trigger. - DsPopover.Trigger / .Anchor: forward ref and injected props so a tooltip and a popover can share one trigger in either nesting order. - Add DsPopover.CloseTrigger and a DsPopover.Header actions slot. - openOn="hover": a click pins the panel until Escape, outside click, close button or another trigger click; any close cancels a pending hover open. - Hoist DsTreeRowTriggerProps into a shared DsAsChildTriggerProps type (aliased, public API unchanged).
✅ Deploy Preview for drivenets-design-system ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
…ver-tooltip-composition # Conflicts: # packages/design-system/src/components/ds-popover/ds-popover.hover-intent.ts # packages/design-system/src/components/ds-popover/ds-popover.tsx
iromanchuk-dn
commented
Sep 29, 2026
…isabled [AR-75956] Zag's disabled prop also drops pointer leave, so a tooltip disabled under the pointer kept its opened-by-pointer flag and ignored the next hover. Treat disabled as a controlled close so the machine keeps tracking the pointer.
…ver-tooltip-composition
… zag [AR-75956] zag 1.41.2 (allowed by the ^1.42 / ark ^5.37.2 ranges) matches data-ownedby exactly and does not merge it, so the fallback lookup cannot find a trigger whose id an outer DsTooltip replaced. Resolve ids.trigger lazily from an id the trigger registers in a layout effect, which runs before the Root machine's effects.
…mposition' into feat/AR-75956-popover-tooltip-composition
…sed pointer leave [AR-75956]
Forcing a controlled open={false} while disabled could leave the machine stuck in closing after an instant open, holding the shared visible-tooltip id. Pass zag's disabled through again and, once re-enabled, replay a pointer leave that happened while disabled so the next hover still opens.
…mposition' into feat/AR-75956-popover-tooltip-composition
vpolessky-dn
approved these changes
Sep 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes AR-75956
Summary
AR-75956 was filed against 0.13.0. DsPopover's controlled
open, hover opening, delays and Header/Content/Footer slots have shipped since. This PR covers what was still missing, plus the hover → click-pin behavior from AR-39567.open/defaultOpen/onOpenChange(open).refand asChild-injected props to the trigger, including whencontentis undefined.open, so a controlledopennow also drives presence.DsPopover.Triggerand.Anchorforwardrefand injected props, so a tooltip and a popover can share one trigger element.DsPopover.Trigger > DsTooltip > elementis recommended.data-scope/data-part/data-state, so zag still finds it, and the Anchor is positioned from its registered ref.DsPopover.CloseTrigger, an icon-only tertiaryDsButtonV3withlocale={{ close }}defaulting to'Close'and noaria-pressed. It goes in the newDsPopover.Headeractionsslot, which sits outside the title and so isn't part of the dialog's accessible name.openOn="hover"pins on clickopenOn="hover"shipped in 0.20.0, where a click on a hover-opened panel closed it and a click-opened panel closed on mouse-out. Both now pin instead.DsTreeRowTriggerPropsis hoisted into a sharedDsAsChildTriggerPropsinsrc/utils. It stays exported as an alias, so the public API is unchanged.WithContentItemsAndCTA, a new popoverWithTooltiprecipe (preview on hover, full panel on click), and a new tooltipControlledstory. Both docs snapshots are updated.Why forward props and
refWhat they want: one element on screen, such as a workflow name, that shows a small tooltip on hover and opens a detailed popover on click.
How DS wrappers work:
DsTooltipandDsPopover.Triggerdon't render an element of their own. They pass their behavior onto their child: hover and click handlers,aria-*attributes, anid, and arefso they can find and position against the element.Why it breaks when you combine them:
<DsTooltip><DsPopover.Trigger><button/></DsPopover.Trigger></DsTooltip>. The tooltip hands its handlers to its child, but that child isDsPopover.Trigger, which only readchildrenandclassNameand dropped everything else. The tooltip never reaches the button, so hovering does nothing. Swap the order and the popover is the one that never opens. That's what "nesting drops the inner wrapper's handlers" means.<span>for the tooltip with the button inside it for the popover. Now there are two elements instead of one. That means two separate hover areas, the tooltip and panel positioned against different boxes, and extra markup. The bulk-actions menu does exactly this. That's "sibling composition creates two DOM anchors".The fix they asked for: make each wrapper pass through whatever it receives, including the
ref(in React 19,refis an ordinary prop, so "forwardRef" just means passing it through). Then both behaviors land on the same element.One snag beyond the ask: both wrappers also want to put their own
idon that element, and an element can only have one. Whichever wrapper loses can no longer find the element, so its panel isn't positioned and its outside-click check misfires. That's why this PR also makes DsTooltip adopt the id it's given, and makes the popover keep its own markers so it can still find the trigger.Test plan
pnpm eslinton the changed folderspnpm --filter @drivenets/design-system typechecktests/exports.unit.test.ts: 182/182 passingdefaultOpen, Escape returns focus.refforwarding on DsTooltip,DsPopover.Triggerand.Anchor.actions.pnpm test:storybook-docs): popover and tooltip goldens updated, diffs reviewed.figma:lintpasses.Notes
idcombined with open-on-mount can break Escape and outside-click dismissal in the first session.DsPopover.Triggerlose the tree node id.DsDropdownMenu.Itemwrapped in DsTooltip loses its menu item id.ds-bulk-actions(extra span) and DsSavedFilters (hand-built close button) can be removed.