diff --git a/packages/api-generator/src/locale/en/v-click-outside.json b/packages/api-generator/src/locale/en/v-click-outside.json index 4ad298e984b..c497b88056c 100644 --- a/packages/api-generator/src/locale/en/v-click-outside.json +++ b/packages/api-generator/src/locale/en/v-click-outside.json @@ -1,5 +1,3 @@ { - "argument": { - "value": "By default takes a function that is invoked when user clicks outside of the element the directive is attached to. It can also be an object, which allows you to provide `closeConditional` and `include` callbacks." - } + "value": "Takes either a function that is invoked when user clicks outside of the element the directive is attached to, or an object containing `handler`, `closeConditional` and `include` callbacks." } diff --git a/packages/api-generator/src/locale/en/v-intersect.json b/packages/api-generator/src/locale/en/v-intersect.json index 3f8323b59bc..cfcc797ad29 100644 --- a/packages/api-generator/src/locale/en/v-intersect.json +++ b/packages/api-generator/src/locale/en/v-intersect.json @@ -1,9 +1,7 @@ { - "argument": { - "value": "By default takes a handler function that is invoked when the element that the directive is attached to enters or leaves the visible browser area. It can also take an object, which allows you to pass along [IntersectionObserver options](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver)." - }, + "value": "A handler function that is invoked when the element that the directive is attached to enters or leaves the visible browser area, or an object of [IntersectionObserver options](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver).", "modifiers": { - "once": "The provided handler function is only invoked once, the first time the element is visible.", + "once": "The handler function is only invoked once, the first time the element is visible.", "quiet": "Will not invoke the handler function if the element is visible when the IntersectionObserver is created." } } diff --git a/packages/api-generator/src/locale/en/v-mutate.json b/packages/api-generator/src/locale/en/v-mutate.json index 14ccab50ff3..35cdb4c1d65 100644 --- a/packages/api-generator/src/locale/en/v-mutate.json +++ b/packages/api-generator/src/locale/en/v-mutate.json @@ -1,7 +1,5 @@ { - "argument": { - "value": "By default takes a handler function that is invoked when the element that the directive is attached to is mutated. It can also take an object, which allows you to pass along [MutationObserver options](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe)." - }, + "value": "A handler function that is invoked when the element that the directive is attached to is mutated, or an object of [MutationObserver options](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe).", "modifiers": { "attr": "Sets the value of [attributes](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit/attributes) to true.", "char": "Sets the value of [characterData](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit/characterData) to true.", diff --git a/packages/api-generator/src/locale/en/v-resize.json b/packages/api-generator/src/locale/en/v-resize.json index cf1e2cbfc47..74617b6ab0a 100644 --- a/packages/api-generator/src/locale/en/v-resize.json +++ b/packages/api-generator/src/locale/en/v-resize.json @@ -1,9 +1,7 @@ { - "argument": { - "value": "The provided handler function will be invoked each time the browser window is resized." - }, + "value": "A function that will be invoked each time the browser window is resized.", "modifiers": { "active": "By default the resize event listener is added to window with the `passive` option. This modifier sets `passive` to **false**.", - "quiet": "By default the provided handler function is invoked once when the directive is attached to the element. This modifier disabled that behavior." + "quiet": "By default the provided handler function is invoked once when the directive is attached to the element. This modifier disables that behavior." } } diff --git a/packages/api-generator/src/locale/en/v-ripple.json b/packages/api-generator/src/locale/en/v-ripple.json index a2bc46b706c..dc62d082184 100644 --- a/packages/api-generator/src/locale/en/v-ripple.json +++ b/packages/api-generator/src/locale/en/v-ripple.json @@ -1,7 +1,5 @@ { - "argument": { - "value": "An object containing options for the ripple effect. `class` applies a custom class to the ripple, and can be used for changing color. `center` forces the ripple to originate from the center of the target." - }, + "value": "An object containing options for the ripple effect. `class` applies a custom class to the ripple, and can be used for changing color. `center` forces the ripple to originate from the center of the target instead of the cursor position.", "modifiers": { "center": "Makes it so that the ripple originates from the center of the element, instead where the user clicked on it.", "circle": "Changes the ripple behavior to better match circular elements.", diff --git a/packages/api-generator/src/locale/en/v-scroll.json b/packages/api-generator/src/locale/en/v-scroll.json index 53c15a46610..408d0cff9a2 100644 --- a/packages/api-generator/src/locale/en/v-scroll.json +++ b/packages/api-generator/src/locale/en/v-scroll.json @@ -1,8 +1,6 @@ { - "argument": { - "arg": "The argument can be used to specify a query selector to attach the scroll event listener to. If no argument is provided then it is attached to the window object.", - "value": "By default takes a handler function that is invoked whenever the target of the directive is scrolled. It can also take an object, which allows you to pass along event listener options as described [here](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)." - }, + "argument": "Specify a query selector to attach the scroll event listener to. If no argument is provided then it is attached to the window object.", + "value": "A handler function that is invoked whenever the target element is scrolled, or an object of [event listener options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener).", "modifiers": { "self": "By default the scroll event listener is attached to the argument provided to the directive, interpreted as a query selector. If no argument is provided then it is attached to the window object. If this modifier is used then it is instead attached to the element the directive is used on." } diff --git a/packages/api-generator/src/locale/en/v-tooltip.json b/packages/api-generator/src/locale/en/v-tooltip.json new file mode 100644 index 00000000000..2f4b2229614 --- /dev/null +++ b/packages/api-generator/src/locale/en/v-tooltip.json @@ -0,0 +1,4 @@ +{ + "argument": "Applies the VTooltip location prop.", + "value": "**string**: Sets the tooltip content. \n**boolean**: Controls visibility, tooltip content will be the innerText of the bound element. \n**object**: Use any [VTooltip props](/api/v-tooltip), content can be set with `text`. Keys are camelCase." +} diff --git a/packages/api-generator/src/locale/en/v-touch.json b/packages/api-generator/src/locale/en/v-touch.json index fc54d336092..58316fdb587 100644 --- a/packages/api-generator/src/locale/en/v-touch.json +++ b/packages/api-generator/src/locale/en/v-touch.json @@ -1,5 +1,3 @@ { - "argument": { - "value": "The value is always an object. The `start`, `end`, `move`, `left`, `right`, `up` and `down` functions can be used to invoke a function when the corresponding touch action occurs. If the `parent` option attaches the touch listeners to the parent element instead of the element the directive is used on. The `options` object is described [here](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)." - } + "value": "The value is always an object. The `start`, `end`, `move`, `left`, `right`, `up` and `down` functions can be used to invoke a function when the corresponding touch action occurs. If the `parent` option attaches the touch listeners to the parent element instead of the element the directive is used on. The `options` object is described [here](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)." } diff --git a/packages/api-generator/src/shims.d.ts b/packages/api-generator/src/shims.d.ts index 4c34c60a035..da9265cb539 100644 --- a/packages/api-generator/src/shims.d.ts +++ b/packages/api-generator/src/shims.d.ts @@ -1,4 +1,4 @@ -import { ts } from '@ts-morph/common' +import '@ts-morph/common' declare module 'ts-morph' { export interface Type { @@ -18,3 +18,5 @@ declare module 'ts-morph' { } } } + +export {} diff --git a/packages/api-generator/src/types.ts b/packages/api-generator/src/types.ts index 998b7b83481..76c410b852a 100644 --- a/packages/api-generator/src/types.ts +++ b/packages/api-generator/src/types.ts @@ -81,7 +81,8 @@ export async function generateDirectiveDataFromTypes (): Promise { const sourceFile = project.addSourceFileAtPath(`./templates/tmp/${component}.d.ts`) - const props = await inspect(project, sourceFile.getTypeAlias('ComponentProps')) - const events = await inspect(project, sourceFile.getTypeAlias('ComponentEvents')) - const slots = await inspect(project, sourceFile.getTypeAlias('ComponentSlots')) - const exposed = await inspect(project, sourceFile.getTypeAlias('ComponentExposed')) + const [ + props, + events, + slots, + exposed, + ] = await Promise.all([ + inspect(project, sourceFile.getTypeAlias('ComponentProps')), + inspect(project, sourceFile.getTypeAlias('ComponentEvents')), + inspect(project, sourceFile.getTypeAlias('ComponentSlots')), + inspect(project, sourceFile.getTypeAlias('ComponentExposed')), + ]) const sections = [props, events, slots, exposed] @@ -114,6 +122,10 @@ export async function generateComponentDataFromTypes (component: string): Promis events: events.properties, slots: slots.properties, exposed: exposed.properties, + displayName: component, + fileName: component, + pathName: kebabCase(component), + sass: {}, } } @@ -217,6 +229,7 @@ export type ComponentData = BaseData & { slots: Record events: Record exposed: Record + value?: never argument?: never modifiers?: never } @@ -226,7 +239,8 @@ export type DirectiveData = BaseData & { slots?: never events?: never exposed?: never - argument: { value: Definition } + value: Definition + argument: Definition modifiers: Record } export type ComposableData = BaseData & { @@ -235,6 +249,7 @@ export type ComposableData = BaseData & { slots?: never events?: never exposed: Record + value?: never argument?: never modifiers?: never } @@ -268,21 +283,21 @@ function getSource (declaration?: Node) { return filePath && startLine ? `${filePath}#L${startLine}-L${endLine}` : undefined } -function listFlags (flags: object, value?: number) { - if (!value) return [] +// function listFlags (flags: object, value?: number) { +// if (!value) return [] - const entries = Object.entries(flags).filter(([_, flag]) => typeof flag === 'number') +// const entries = Object.entries(flags).filter(([_, flag]) => typeof flag === 'number') - return entries.reduce((arr, [name, flag]) => { - if (value & flag) { - arr.push(name) - } - return arr - }, []) -} +// return entries.reduce((arr, [name, flag]) => { +// if (value & flag) { +// arr.push(name) +// } +// return arr +// }, []) +// } function getCleanText (text: string) { - return text.replaceAll(/import\(.*?\)\./g, '') + return text.replace(/import\(.*?\)\./g, '') } function count (arr: string[], needle: string) { @@ -607,7 +622,7 @@ function getRecursiveTypes (recursiveTypes: string[], type: Type) { function findPotentialRecursiveTypes (type?: Type): string[] { if (type == null) return [] - const recursiveTypes = [] + const recursiveTypes: string[] = [] if (type.isUnion()) { recursiveTypes.push(...getUnionTypes(type).map(t => t.getText())) diff --git a/packages/api-generator/src/utils.ts b/packages/api-generator/src/utils.ts index 66ee2f0450d..b575f215039 100644 --- a/packages/api-generator/src/utils.ts +++ b/packages/api-generator/src/utils.ts @@ -2,7 +2,7 @@ import { execSync } from 'child_process' import stringifyObject from 'stringify-object' import prettier from 'prettier' import * as typescriptParser from 'prettier/plugins/typescript' -import type { Definition } from './types' +import type { Definition, DirectiveData } from './types' function parseFunctionParams (func: string) { const [, regular] = /function\s\((.*)\)\s\{.*/i.exec(func) || [] @@ -134,10 +134,12 @@ async function getSources (name: string, locale: string, sources: string[]) { const sourcesMap = [name, ...sources, 'generic'] return { - find: (section: string, key: string, ogSource = name) => { + find (section: string, key?: string, ogSource = name) { for (let i = 0; i < arr.length; i++) { const source = arr[i] as any - const found: string | undefined = source?.[section]?.[key] + const found: string | undefined = ['argument', 'value'].includes(section) + ? source?.[section] + : source?.[section]?.[key!] if (found) { return { text: found, source: sourcesMap[i] } } @@ -167,25 +169,26 @@ export async function addDescriptions (name: string, componentData: ComponentDat export async function addDirectiveDescriptions ( name: string, - componentData: { argument: { value: Definition }, modifiers: Record }, + componentData: DirectiveData, locales: string[], sources: string[] = [], ) { for (const locale of locales) { const descriptions = await getSources(name, locale, sources) - if (componentData.argument) { - for (const [name, arg] of Object.entries(componentData.argument)) { - arg.description = arg.description ?? {} + if (componentData.value) { + componentData.value.description = componentData.value.description ?? {} + componentData.value.description[locale] = descriptions.find('value')?.text + } - arg.description[locale] = descriptions.find('argument', name)?.text - } + if (componentData.argument) { + componentData.argument.description = componentData.argument.description ?? {} + componentData.argument.description[locale] = descriptions.find('argument')?.text } if (componentData.modifiers) { for (const [name, modifier] of Object.entries(componentData.modifiers)) { modifier.description = modifier.description ?? {} - modifier.description[locale] = descriptions.find('modifiers', name)?.text } } diff --git a/packages/api-generator/src/web-types.ts b/packages/api-generator/src/web-types.ts index 2303e7a2271..e2cc530afb7 100644 --- a/packages/api-generator/src/web-types.ts +++ b/packages/api-generator/src/web-types.ts @@ -95,7 +95,7 @@ export const createWebTypesApi = (componentData: ComponentData[], directiveData: const createAttributeValue = (argument: any) => { return { kind: 'expression', - type: argument.type?.trim(), + type: argument.text, } } @@ -106,14 +106,14 @@ export const createWebTypesApi = (componentData: ComponentData[], directiveData: 'doc-url': getDocUrl(directive.pathName), default: '', required: false, - value: createAttributeValue(directive.argument), + value: createAttributeValue(directive.value), source: { module: './src/directives/index.ts', symbol: capitalize(directive.displayName.slice(2)), }, - 'vue-argument': directive.argument?.value && createAttributeVueArgument(directive.argument?.value), // TODO: how to use this in comparison to value? + 'vue-argument': directive.argument && createAttributeVueArgument(directive.argument), 'vue-modifiers': directive.modifiers && - Object.entries(directive.modifiers ?? {}).map(createAttributeVueModifier), + Object.entries(directive.modifiers).map(createAttributeVueModifier), } } diff --git a/packages/api-generator/templates/directives.d.ts b/packages/api-generator/templates/directives.d.ts index 94eb580b2a5..06211c10b29 100644 --- a/packages/api-generator/templates/directives.d.ts +++ b/packages/api-generator/templates/directives.d.ts @@ -1,17 +1,30 @@ -import type { DirectiveBinding } from 'vue' -import type * as directives from '../../vuetify/lib/directives/index.d.mts' +import type { DirectiveBinding, ObjectDirective } from 'vue' +import type { CustomDirective } from '../../vuetify/src/composables/directiveComponent' +import type * as directives from '../../vuetify/src/directives/index.ts' type ExtractDirectiveBindings = T extends object ? { - [K in keyof T]: T[K] extends { mounted: infer M } - ? M extends (first: any, second: infer B) => any - ? B extends object - ? { - [KK in keyof B as KK extends keyof Omit ? never : KK]: B[KK] - } + [K in keyof T]: T[K] extends CustomDirective + ? { + [K in Exclude]: K extends 'modifiers' + ? Record extends M[K] ? never : M[K] + : K extends 'arg' + ? string extends M[K] ? never : M[K] + : M[K] + } & {} + : T[K] extends { mounted: infer M } + ? M extends (first: any, second: infer B) => any + ? B extends object + ? { + [KK in keyof B as KK extends keyof Omit ? never : KK]: B[KK] + } + : never + : never + : T[K] extends ObjectDirective + ? B extends object + ? { value: B, modifiers: {} } + : never : never - : never - : {} } : never diff --git a/packages/docs/build/api-plugin.ts b/packages/docs/build/api-plugin.ts index 8da512afb60..60433356134 100644 --- a/packages/docs/build/api-plugin.ts +++ b/packages/docs/build/api-plugin.ts @@ -14,7 +14,7 @@ const API_PAGES_ROOT = resolve('./node_modules/.cache/api-pages') const require = createRequire(import.meta.url) -const sections = ['props', 'events', 'slots', 'exposed', 'sass', 'argument', 'modifiers'] as const +const sections = ['props', 'events', 'slots', 'exposed', 'sass', 'argument', 'modifiers', 'value'] as const // This can't be imported from the api-generator because it mixes the type definitions up type Data = { displayName: string // user visible name used in page titles diff --git a/packages/docs/components.d.ts b/packages/docs/components.d.ts index eb5a98f672b..6b86cc81f43 100644 --- a/packages/docs/components.d.ts +++ b/packages/docs/components.d.ts @@ -11,6 +11,7 @@ declare module 'vue' { AboutTeamMembers: typeof import('./src/components/about/TeamMembers.vue')['default'] Alert: typeof import('./src/components/Alert.vue')['default'] ApiApiTable: typeof import('./src/components/api/ApiTable.vue')['default'] + ApiDirectiveTable: typeof import('./src/components/api/DirectiveTable.vue')['default'] ApiEventsTable: typeof import('./src/components/api/EventsTable.vue')['default'] ApiExposedTable: typeof import('./src/components/api/ExposedTable.vue')['default'] ApiInline: typeof import('./src/components/api/Inline.vue')['default'] @@ -23,8 +24,6 @@ declare module 'vue' { ApiSection: typeof import('./src/components/api/Section.vue')['default'] ApiSlotsTable: typeof import('./src/components/api/SlotsTable.vue')['default'] AppBackToTop: typeof import('./src/components/app/BackToTop.vue')['default'] - AppBanner: typeof import('./src/components/app/Banner.vue')['default'] - AppBarAuthDialog: typeof import('./src/components/app/bar/AuthDialog.vue')['default'] AppBarBar: typeof import('./src/components/app/bar/Bar.vue')['default'] AppBarEcosystemMenu: typeof import('./src/components/app/bar/EcosystemMenu.vue')['default'] AppBarEnterpriseLink: typeof import('./src/components/app/bar/EnterpriseLink.vue')['default'] @@ -78,7 +77,6 @@ declare module 'vue' { AppSettingsOptionsQuickbarOption: typeof import('./src/components/app/settings/options/QuickbarOption.vue')['default'] AppSettingsOptionsRailDrawerOption: typeof import('./src/components/app/settings/options/RailDrawerOption.vue')['default'] AppSettingsOptionsSlashSearchOption: typeof import('./src/components/app/settings/options/SlashSearchOption.vue')['default'] - AppSettingsOptionsSyncOption: typeof import('./src/components/app/settings/options/SyncOption.vue')['default'] AppSettingsOptionsThemeOption: typeof import('./src/components/app/settings/options/ThemeOption.vue')['default'] AppSettingsPerksOptions: typeof import('./src/components/app/settings/PerksOptions.vue')['default'] AppSettingsSettingsHeader: typeof import('./src/components/app/settings/SettingsHeader.vue')['default'] @@ -152,16 +150,10 @@ declare module 'vue' { SponsorCard: typeof import('./src/components/sponsor/Card.vue')['default'] SponsorLink: typeof import('./src/components/sponsor/Link.vue')['default'] SponsorSponsors: typeof import('./src/components/sponsor/Sponsors.vue')['default'] - UserAccountConnectedAccounts: typeof import('./src/components/user/account/ConnectedAccounts.vue')['default'] - UserAccountOneSubscription: typeof import('./src/components/user/account/OneSubscription.vue')['default'] UserBadgesUserAdminBadge: typeof import('./src/components/user/badges/UserAdminBadge.vue')['default'] UserBadgesUserOneBadge: typeof import('./src/components/user/badges/UserOneBadge.vue')['default'] UserBadgesUserSponsorBadge: typeof import('./src/components/user/badges/UserSponsorBadge.vue')['default'] - UserDiscordLogin: typeof import('./src/components/user/DiscordLogin.vue')['default'] - UserGithubLogin: typeof import('./src/components/user/GithubLogin.vue')['default'] UserOneSubCard: typeof import('./src/components/user/OneSubCard.vue')['default'] - UserUserBadges: typeof import('./src/components/user/UserBadges.vue')['default'] - UserUserProfile: typeof import('./src/components/user/UserProfile.vue')['default'] UserUserTabs: typeof import('./src/components/user/UserTabs.vue')['default'] } } diff --git a/packages/docs/src/components/api/DirectiveTable.vue b/packages/docs/src/components/api/DirectiveTable.vue new file mode 100644 index 00000000000..e749fbaf545 --- /dev/null +++ b/packages/docs/src/components/api/DirectiveTable.vue @@ -0,0 +1,32 @@ + + + diff --git a/packages/docs/src/components/api/Section.vue b/packages/docs/src/components/api/Section.vue index 558924c8668..f586345d7ac 100644 --- a/packages/docs/src/components/api/Section.vue +++ b/packages/docs/src/components/api/Section.vue @@ -15,6 +15,7 @@ + + diff --git a/packages/docs/src/examples/v-tooltip-directive/usage.vue b/packages/docs/src/examples/v-tooltip-directive/usage.vue new file mode 100644 index 00000000000..0177d215963 --- /dev/null +++ b/packages/docs/src/examples/v-tooltip-directive/usage.vue @@ -0,0 +1,27 @@ + + + diff --git a/packages/docs/src/i18n/messages/en.json b/packages/docs/src/i18n/messages/en.json index ce4cccbb38c..e818fbf05f2 100644 --- a/packages/docs/src/i18n/messages/en.json +++ b/packages/docs/src/i18n/messages/en.json @@ -19,7 +19,8 @@ "props": "Props", "sass": "SASS Variables", "slots": "Slots", - "exposed": "Exposed" + "exposed": "Exposed", + "value": "Value" }, "api-explorer": "API Explorer", "application": "Application", diff --git a/packages/docs/src/pages/en/directives/tooltip.md b/packages/docs/src/pages/en/directives/tooltip.md new file mode 100644 index 00000000000..312dd5ec6ad --- /dev/null +++ b/packages/docs/src/pages/en/directives/tooltip.md @@ -0,0 +1,56 @@ +--- +emphasized: true +meta: + nav: Tooltip + title: Tooltip directive + description: The Tooltip directive is an easy to use implementation of VTooltip. + keywords: Tooltip, vuetify Tooltip directive, vue Tooltip directive, mobile Tooltip directive +related: + - /components/navigation-drawers/ + - /components/slide-groups/ + - /components/windows/ +features: + report: true +--- + +# Tooltip directive + +The `v-tooltip` directive is a shorthand way of adding tooltips to elements in your application. + + + + + +## Usage + +The `v-tooltip` directive makes it easy to add a tooltip to any element in your application. It is a wrapper around the `v-tooltip` component. + + + +## API + +| Directive | Description | +|------------------------------------|---------------------| +| [v-tooltip](/api/v-tooltip-directive/) | The Tooltip directive | + +## Guide + +The `v-tooltip` directive is a simple way to add a tooltip to any element in your application. It is a wrapper around the `v-tooltip`. + +### Args + +The `v-tooltip` directive has a number of args that can be used to customize the behavior of the tooltip. + + + +### Modifiers + +Modifiers are values that are passed to the `v-tooltip` component. This is an easy way to make small modifications to boolean [v-tooltip](/api/v-tooltip/) props. + + + +### Object literals + +The `v-tooltip` directive can also accept an object literal as a value. This is useful when you need to pass multiple props to the `v-tooltip` component. + + diff --git a/packages/docs/tsconfig.json b/packages/docs/tsconfig.json index cafeeee0235..433e6b2fe8a 100644 --- a/packages/docs/tsconfig.json +++ b/packages/docs/tsconfig.json @@ -29,5 +29,6 @@ ], "outDir": "./dist" }, + "include": ["../api-generator/src/shims.d.ts"], "exclude": ["dist", "node_modules"], } diff --git a/packages/vuetify/src/composables/directiveComponent.ts b/packages/vuetify/src/composables/directiveComponent.ts index 11d1c8af9bc..b9479156b83 100644 --- a/packages/vuetify/src/composables/directiveComponent.ts +++ b/packages/vuetify/src/composables/directiveComponent.ts @@ -1,5 +1,6 @@ // Utilities import { h, mergeProps, render, resolveComponent } from 'vue' +import { isObject } from '@/util' // Types import type { @@ -11,43 +12,83 @@ import type { ObjectDirective, VNode, } from 'vue' +import type { ComponentInstance } from '@/util' -export const useDirectiveComponent = ( +type ExcludeProps = + | 'v-slots' + | `v-slot:${string}` + | `on${Uppercase}${string}` + | 'key' + | 'ref' + | 'ref_for' + | 'ref_key' + | '$children' + +declare const CustomDirectiveSymbol: unique symbol +type DirectiveHook = (el: any, binding: B, vnode: VNode, prevVNode: VNode) => void +export interface CustomDirective { + created?: DirectiveHook + beforeMount?: DirectiveHook + mounted?: DirectiveHook + beforeUpdate?: DirectiveHook + updated?: DirectiveHook + beforeUnmount?: DirectiveHook + unmounted?: DirectiveHook + [CustomDirectiveSymbol]: true +} + +export function useDirectiveComponent < + Binding extends DirectiveBinding, +> (component: string | Component, props?: (binding: Binding) => Record): CustomDirective +export function useDirectiveComponent < + C extends Component, + Props = Omit['$props'], ExcludeProps> +> (component: string | C, props?: Record): ObjectDirective +export function useDirectiveComponent ( component: string | Component, - props?: any -): ObjectDirective => { + props?: Record | ((binding: DirectiveBinding) => Record) +): ObjectDirective | CustomDirective { const concreteComponent = (typeof component === 'string' ? resolveComponent(component) : component) as ConcreteComponent + const hook = mountComponent(concreteComponent, props) + return { - mounted (el: HTMLElement, binding: DirectiveBinding, vnode: VNode) { - const { value } = binding - - // Get the children from the props or directive value, or the element's children - const children = props.text || value.text || el.innerHTML - - // If vnode.ctx is the same as the instance, then we're bound to a plain element - // and need to find the nearest parent component instance to inherit provides from - const provides = (vnode.ctx === binding.instance!.$ - ? findComponentParent(vnode, binding.instance!.$)?.provides - : vnode.ctx?.provides) ?? binding.instance!.$.provides - - const node = h(concreteComponent, mergeProps(props, value), children) - node.appContext = Object.assign( - Object.create(null), - (binding.instance as ComponentPublicInstance).$.appContext, - { provides } - ) - - render(node, el) - }, + mounted: hook, + updated: hook, unmounted (el: HTMLElement) { render(null, el) }, } } +function mountComponent (component: ConcreteComponent, props?: Record | ((binding: DirectiveBinding) => Record)) { + return function (el: HTMLElement, binding: DirectiveBinding, vnode: VNode) { + const _props = typeof props === 'function' ? props(binding) : props + const text = binding.value?.text ?? binding.value + const value = isObject(binding.value) ? binding.value : {} + + // Get the children from the props or directive value, or the element's children + const children = () => text ?? el.innerHTML + + // If vnode.ctx is the same as the instance, then we're bound to a plain element + // and need to find the nearest parent component instance to inherit provides from + const provides = (vnode.ctx === binding.instance!.$ + ? findComponentParent(vnode, binding.instance!.$)?.provides + : vnode.ctx?.provides) ?? binding.instance!.$.provides + + const node = h(component, mergeProps(_props, value), children) + node.appContext = Object.assign( + Object.create(null), + (binding.instance as ComponentPublicInstance).$.appContext, + { provides } + ) + + render(node, el) + } +} + function findComponentParent (vnode: VNode, root: ComponentInternalInstance): ComponentInternalInstance | null { // Walk the tree from root until we find the child vnode const stack = new Set() @@ -60,16 +101,16 @@ function findComponentParent (vnode: VNode, root: ComponentInternalInstance): Co } stack.add(child) - if (Array.isArray(child.children)) { - const result = walk(child.children as VNode[]) - if (result) { - return result - } + let result + if (child.suspense) { + result = walk([child.ssContent!]) + } else if (Array.isArray(child.children)) { + result = walk(child.children as VNode[]) } else if (child.component?.vnode) { - const result = walk([child.component?.subTree]) - if (result) { - return result - } + result = walk([child.component?.subTree]) + } + if (result) { + return result } stack.delete(child) } diff --git a/packages/vuetify/src/directives/index.ts b/packages/vuetify/src/directives/index.ts index 1003d5b3e1c..54dc6a234aa 100644 --- a/packages/vuetify/src/directives/index.ts +++ b/packages/vuetify/src/directives/index.ts @@ -6,3 +6,4 @@ export { Resize } from './resize' export { Ripple } from './ripple' export { Scroll } from './scroll' export { Touch } from './touch' +export { Tooltip } from './tooltip' diff --git a/packages/vuetify/src/directives/tooltip/index.ts b/packages/vuetify/src/directives/tooltip/index.ts new file mode 100644 index 00000000000..597955fdc3a --- /dev/null +++ b/packages/vuetify/src/directives/tooltip/index.ts @@ -0,0 +1,24 @@ +// Components +import { VTooltip } from '@/components/VTooltip' + +// Composables +import { useDirectiveComponent } from '@/composables/directiveComponent' + +// Types +import type { DirectiveBinding } from 'vue' +import type { Anchor } from '@/util' + +export interface TooltipDirectiveBinding extends Omit, 'arg' | 'value'> { + arg?: { [T in Anchor]: T extends `${infer A} ${infer B}` ? `${A}-${B}` : T }[Anchor] + value: boolean | string | Record +} + +export const Tooltip = useDirectiveComponent(VTooltip, binding => { + return { + activator: 'parent', + location: binding.arg?.replace('-', ' ') ?? 'top', + text: typeof binding.value === 'boolean' ? undefined : binding.value, + } +}) + +export default Tooltip diff --git a/packages/vuetify/src/globals.d.ts b/packages/vuetify/src/globals.d.ts index a4fab108ede..cb06c7aeac7 100644 --- a/packages/vuetify/src/globals.d.ts +++ b/packages/vuetify/src/globals.d.ts @@ -132,6 +132,7 @@ declare module '@vue/runtime-core' { export interface VNode { ctx: ComponentInternalInstance | null + ssContent: VNode | null } } diff --git a/packages/vuetify/src/util/defineComponent.tsx b/packages/vuetify/src/util/defineComponent.tsx index 7753afd4673..fee604b39f4 100644 --- a/packages/vuetify/src/util/defineComponent.tsx +++ b/packages/vuetify/src/util/defineComponent.tsx @@ -12,6 +12,7 @@ import { propsFactory } from '@/util/propsFactory' // Types import type { AllowedComponentProps, + Component, ComponentCustomProps, ComponentInjectOptions, ComponentObjectPropsOptions, @@ -20,6 +21,7 @@ import type { ComponentOptionsWithObjectProps, ComponentOptionsWithoutProps, ComponentPropsOptions, + ComponentPublicInstance, ComputedOptions, DefineComponent, EmitsOptions, @@ -299,3 +301,29 @@ export interface FilterPropsOptions> > (props: T): Partial> } + +// https://github.com/vuejs/core/pull/10557 +export type ComponentInstance = T extends { new (): ComponentPublicInstance } + ? InstanceType + : T extends FunctionalComponent + ? ComponentPublicInstance> + : T extends Component< + infer Props, + infer RawBindings, + infer D, + infer C, + infer M + > + ? // NOTE we override Props/RawBindings/D to make sure is not `unknown` + ComponentPublicInstance< + unknown extends Props ? {} : Props, + unknown extends RawBindings ? {} : RawBindings, + unknown extends D ? {} : D, + C, + M + > + : never // not a vue Component + +type ShortEmitsToObject = E extends Record ? { + [K in keyof E]: (...args: E[K]) => any; +} : E; diff --git a/packages/vuetify/src/util/helpers.ts b/packages/vuetify/src/util/helpers.ts index 73c8f1254f4..cb60134d49a 100644 --- a/packages/vuetify/src/util/helpers.ts +++ b/packages/vuetify/src/util/helpers.ts @@ -131,7 +131,7 @@ export function convertToUnit (str: string | number | null | undefined, unit = ' } } -export function isObject (obj: any): obj is object { +export function isObject (obj: any): obj is Record { return obj !== null && typeof obj === 'object' && !Array.isArray(obj) }