diff --git a/src/runtime/components/Button.vue b/src/runtime/components/Button.vue index 4ca2824ffc..66e3517faf 100644 --- a/src/runtime/components/Button.vue +++ b/src/runtime/components/Button.vue @@ -60,6 +60,11 @@ import UAvatar from './Avatar.vue' import ULink from './Link.vue' import ULinkBase from './LinkBase.vue' +// Attrs are forwarded manually in both branches so fallthrough attributes keep +// the exact position (and DOM attribute order) they had when they traveled +// through `ULink`'s slot props. +defineOptions({ inheritAttrs: false }) + const _props = defineProps() const slots = defineSlots() @@ -74,6 +79,19 @@ const { orientation, size: buttonSize } = useFieldGroup(_props) const linkProps = useForwardProps(pickLinkProps(props)) const forwardedLinkProps = computed(() => omit(linkProps.value, ['type', 'disabled', 'onClick'])) +// A plain button doesn't need `ULink`'s resolution (route matching, `rel` +// computation, the reka `Slot` wrapper): render `ULinkBase` directly and skip +// two component instances. Any prop `ULink` would turn into markup or behavior +// (`to`/`href`, or `target`/`rel`/`noRel`/`external` which shape the `rel` and +// `target` attributes) forces the full path. +const isLink = computed(() => + props.to !== undefined || props.href !== undefined || props.target !== undefined + || props.rel !== undefined || props.noRel !== undefined || props.external !== undefined +) + +// Mirrors `ULink`'s slot `active` in the no-link branch (`active ?? false`). +const isActive = computed(() => props.active ?? false) + const loadingAutoState = ref(false) const formLoading = inject | undefined>(formLoadingInjectionKey, undefined) @@ -137,10 +155,11 @@ const ui = computed(() => tv({