Repository navigation
fix(vue-disabled-reason): unmount reason layers and add a disabled prop - #320
Merged
Merged
Conversation
…mounts or a container is no longer targeted The imperatively rendered reason layers were never torn down, leaking their component instances. Track the containers that hold a layer and unmount them on component unmount and whenever a container is no longer targeted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CbForHXxf1BzugdeQsHpjv
…over DOM detection Consumers that know their own state can pass it in instead of relying on detection from the DOM. Detection remains the fallback when it is not specified. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CbForHXxf1BzugdeQsHpjv
🦋 Changeset detectedLatest commit: ec70991 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
schwarz9791
commented
Oct 15, 2026
schwarz9791
left a comment
Collaborator
Author
There was a problem hiding this comment.
コードレビューで見つかった指摘をインラインでまとめました。
- [要修正] unmount 時の teardown が、親のパッチの途中で post-flush キューを実行してしまう
- [要確認] vui
VDisabledReasonで、既存のdisabledの意味が変わる - [要確認] 有効なコントロールに
disabled: trueを渡すと、操作できなくなる - nit / FYI が 3 件
…s patch Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CbForHXxf1BzugdeQsHpjv
… removed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CbForHXxf1BzugdeQsHpjv
… align API types Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CbForHXxf1BzugdeQsHpjv
…element Rendering the layer straight into the target container made it impossible to unmount once the container's content was replaced (e.g. a text-only child update), because a Fragment root lost its anchors and teardown threw. A dedicated display: contents span now holds the layer's nodes, so the layer can always be unmounted and its presence shows whether it is in place. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CbForHXxf1BzugdeQsHpjv
schwarz9791
marked this pull request as ready for review
October 15, 2026 09:13
dadajam4
approved these changes
Oct 15, 2026
Merged
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.
Summary
Changes to
defineDisabledReasonComponentin@fastkit/vue-disabled-reason. They apply to vui'sVDisabledReasonand to any app component built with the factory.Reason layers were never unmounted
The reason layer (e.g. a tooltip) is rendered into the wrapped element with
render(layer, container), and it was never torn down. Every time a disabled-reason component unmounted, the layer's component instances stayed alive. Layers also stayed alive when a container stopped being targeted on update, for example when the container element was replaced or the slot became empty.The component now tracks which container holds which layer. It unmounts a layer with
render(null, container)and removesdata-disabled-reason-activatedfrom the container in two cases:On unmount, the teardown is deferred to
nextTick.render()flushes the pre/post-flush queues when it is called outside a flush. If it were called fromonBeforeUnmount, it would run inside the parent's patch, and the siblings'mountedhooks, post watchers and template refs would run before the remaining siblings are patched.Each layer has its own host element
Each layer is now rendered into its own
<span>withdisplay: contents, placed inside the container, instead of into the container itself. The span hasborder-radius: inheritso that the overlay'sborder-radius: inheritstill resolves to the container's radius.This matters when a container's children are text only (e.g.
<button :disabled="d">{{ label }}</button>). A text update replaces the container's content, so the layer's nodes are detached too. When a layer's root is a Fragment, as in vui (VTooltip→VMenu→<>{$activator}{$contents}</>), its anchors are detached along with it.render(null, container)then threw inremoveFragment. Because the host keeps the layer's nodes together, the layer can always be unmounted.A container counts as already handled only while its host is inside it. If the host is gone, the stale layer is unmounted and a new one is mounted.
As a result, the layer's elements are no longer direct children of the container. Nothing in this repository or in the known consumer selects them that way.
disabledpropThe disabled state used to be inferred from the DOM only, from the first disableable element's
disabled/aria-disabled. A wrapped component that knows its own state had no way to pass it in, so detection could go wrong. For example:disabledattribute.<a>inside a label, that link decides the state.The new optional
disabledprop takes precedence over DOM detection. When it is omitted, DOM detection works as before.The prop reports the target's state; it does not disable the target.
trueis treated exactly like a DOM-detected disabled state, so renderers such as vui'sVDisabledReasonactivate their overlay, and the overlay catches pointer events. Passtrueonly when the target really is disabled. For a partially disabled group, pass the group's own state so that the group's overlay stays inactive. Per-option reasons need each option to be wrapped.Behavior change for existing callers
Base props are spread after the component's own props, so a
disabledprop defined in thepropspassed to the factory is replaced by this one. In vui'sVDisabledReason, this replaces thedisabledinherited from the menu props. That prop previously had no effect, because the wrapper always overrides the tooltip'sdisabled. Callers that pass it now get the new meaning:falsehides the reason even on a disabled target.trueactivates the tooltip and the overlay on an enabled target.No such caller exists in this repository. A separate changeset for
@fastkit/vuidescribes this.Tests
packages/vue-disabled-reason/src/__tests__/disabled-reason.client.spec.ts:v-ifunmounts the layer.DISABLED_REASON_CONTAINER_BINDcontainer unmounts the old layer and removes the activated attribute.mountedhook before a later sibling is patched.disabledprop takes precedence over the DOM in both directions.The test layer's root is a Fragment, the same structure as vui's layer. Each of 1–4 and 6 fails on the code without the corresponding fix. Without the host element, test 4 throws in
removeFragment.I also checked the docs
testpage by hand, using a temporary block:VDisabledReasonwithv-if. On every cycle,$vstack.controls.length(the number of live tooltips) went back to the same value instead of growing.:disabled="true"around an enabled button showed the tooltip.:disabled="false"around a disabled button did not show it.Release
Changesets:
@fastkit/vue-disabled-reason: minor@fastkit/vui: minor, for the change toVDisabledReason'sdisabled🤖 Generated with Claude Code
https://claude.ai/code/session_01CbForHXxf1BzugdeQsHpjv