diff --git a/packages/vue-instantsearch/src/util/vue-compat/index-vue2.js b/packages/vue-instantsearch/src/util/vue-compat/index-vue2.js index 6177a8eca6a..1cf8dd6ce09 100644 --- a/packages/vue-instantsearch/src/util/vue-compat/index-vue2.js +++ b/packages/vue-instantsearch/src/util/vue-compat/index-vue2.js @@ -9,8 +9,8 @@ export { Vue, Vue2, isVue2, isVue3, version }; const augmentCreateElement = (createElement) => - (tag, propsWithClassName = {}, ...children) => { - const { className, ...props } = propsWithClassName; + (tag, propsWithClassName, ...children) => { + const { className, ...props } = propsWithClassName || {}; if (typeof tag === 'function') { return tag( @@ -23,12 +23,33 @@ const augmentCreateElement = if (typeof tag === 'string') { const { on, style, attrs, domProps, nativeOn, key, ...rest } = props; + // React-style `onClick` / `onAuxClick` props (e.g. from shared + // `instantsearch-ui-components` JSX) need to be remapped to Vue 2's + // `on: { click, auxclick }` event API so they don't fall through to + // `attrs` and end up rendered as literal HTML attributes. + const reactStyleHandlers = {}; + const remainingAttrs = {}; + Object.keys(rest).forEach((prop) => { + if ( + prop.length > 2 && + prop[0] === 'o' && + prop[1] === 'n' && + prop[2] === prop[2].toUpperCase() && + typeof rest[prop] === 'function' + ) { + reactStyleHandlers[prop.slice(2).toLowerCase()] = rest[prop]; + } else { + remainingAttrs[prop] = rest[prop]; + } + }); return createElement( tag, { class: className || props.class, - attrs: attrs || rest, - on, + attrs: attrs || remainingAttrs, + on: Object.keys(reactStyleHandlers).length + ? Object.assign({}, reactStyleHandlers, on) + : on, nativeOn, style, domProps, @@ -51,6 +72,17 @@ export function renderCompat(fn) { }; } +/** + * Fragment shim for the augmented JSX renderer used by `renderCompat`. + * Functional pragmas in `instantsearch-ui-components` use + * `{children}` to skip wrapping markup; Vue 2 has no + * native fragment, so we return the children array directly and let Vue 2 + * flatten it into the surrounding vnode. + */ +export const Fragment = function Fragment(props) { + return props && props.children !== undefined ? props.children : null; +}; + export function getDefaultSlot(component) { return component.$slots.default; } diff --git a/packages/vue-instantsearch/src/util/vue-compat/index-vue3.js b/packages/vue-instantsearch/src/util/vue-compat/index-vue3.js index 8d8ee1e7007..a2352f54856 100644 --- a/packages/vue-instantsearch/src/util/vue-compat/index-vue3.js +++ b/packages/vue-instantsearch/src/util/vue-compat/index-vue3.js @@ -4,14 +4,28 @@ const isVue2 = false; const isVue3 = true; const Vue2 = undefined; -export { createApp, createSSRApp, h, version, nextTick } from 'vue'; +export { createApp, createSSRApp, h, version, nextTick, Fragment } from 'vue'; export { Vue, Vue2, isVue2, isVue3 }; export function renderCompat(fn) { function h(tag, props, ...childrenArray) { const children = childrenArray.length > 0 ? childrenArray : undefined; + // Components from `instantsearch-ui-components` are React-style functional + // components that read `children` from props. Vue 3 instead puts children + // in `slots.default`, which would leave `children` undefined and break + // components like `Button` that render `{children}`. Mirror the Vue 2 + // augmented `h` and forward children as a prop for plain function tags + // (excluding Vue's own components/symbols like `Fragment`). + if (typeof tag === 'function') { + return Vue.h( + tag, + Object.assign({}, props || {}, { children }), + children + ); + } if ( typeof props === 'object' && + props !== null && (props.attrs || props.props || props.scopedSlots || props.on) ) { // In vue 3, we no longer wrap with `attrs` or `props` key.